Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,14 @@ export class CheckInComponents {

const eventData = this.prepareEventRequest(year, month, day);
this.postCheckIn(eventData, this.checkInForm.getFormAction())
.then((resp) => {
.then(async(resp) => {
if (!resp.ok) {
throw Error(`Check-in request failed. Status: ${resp.status}`);
}
const data = await resp.json();
if (data.id) {
this.checkInForm.setEventId(data.id);
}
this.updateDateAndShowDisplay(year, month, day);
})
.catch(() => {
Expand Down Expand Up @@ -147,10 +151,14 @@ export class CheckInComponents {

const eventData = this.prepareEventRequest(year, month, day);
this.postCheckIn(eventData, this.checkInForm.getFormAction())
.then((resp) => {
.then(async(resp) => {
if (!resp.ok) {
throw Error(`Check-in request failed. Status: ${resp.status}`);
}
const data = await resp.json();
if (data.id) {
this.checkInForm.setEventId(data.id);
}
this.updateDateAndShowDisplay(year, month, day);
})
.catch(() => {
Expand Down
136 changes: 134 additions & 2 deletions tests/unit/js/my-books.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,141 @@
import { listCreationForm } from './sample-html/lists-test-data';
import { checkInForm } from './sample-html/checkIns-test-data';
import { checkInForm, checkInContainer, checkInFormModal } from './sample-html/checkIns-test-data';
import { CreateListForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/CreateListForm';
import { CheckInForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents';
import { CheckInComponents, CheckInForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents';

jest.mock('jquery-ui/ui/widgets/dialog', () => {});
jest.mock('../../../openlibrary/plugins/openlibrary/js/dialog', () => ({
initDialogClosers: jest.fn(),
}));
jest.mock('../../../openlibrary/plugins/openlibrary/js/Toast', () => ({
PersistentToast: jest.fn().mockImplementation(() => ({ show: jest.fn() })),
}));

/**
* Creates and initializes a CheckInComponents instance for testing.
* @returns {CheckInComponents}
*/
function createAndInitialize() {
const containerElem = document.querySelector('.check-in-container');
const components = new CheckInComponents(containerElem);
components.initialize();
return components;
}

/** Shorthand to flush pending promises in async tests. */
function flushPromises() {
return new Promise(resolve => setTimeout(resolve, 0));
}

/**
* Dispatches a submit-check-in event on the check-in prompt element
* and waits for async resolution.
* @param {CheckInComponents} components
* @param {{year: number, month: number, day: number}} detail
*/
async function dispatchSubmitOnPrompt(components, {year, month, day}) {
const submitEvent = new CustomEvent('submit-check-in', {
detail: {year, month, day},
});
components.checkInPrompt.getRootElement().dispatchEvent(submitEvent);
await flushPromises();
}

/**
* Dispatches a submit-check-in event on the form element
* and waits for async resolution.
* @param {CheckInComponents} components
* @param {{year: number, month: number, day: number}} detail
*/
async function dispatchSubmitOnForm(components, {year, month, day}) {
const submitEvent = new CustomEvent('submit-check-in', {
detail: {year, month, day},
});
components.checkInForm.getRootElement().dispatchEvent(submitEvent);
await flushPromises();
}

describe('CheckInComponents class', () => {
beforeEach(() => {
document.body.innerHTML = checkInContainer + checkInFormModal;
// Mock $.colorbox used by closeModal()
global.$ = { colorbox: { close: jest.fn() } };
});

afterEach(() => {
jest.clearAllMocks();
});

it('stores the event id from the server response after a successful check-in via prompt', async() => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({ status: 'ok', id: 789 }),
});

const components = createAndInitialize();

await dispatchSubmitOnPrompt(components, {year: 2024, month: 6, day: 15});

// The event id returned by the server should now be stored in the form
// so that a subsequent DELETE request uses the correct URL
expect(components.checkInForm.getEventId()).toBe('789');
});

it('stores the event id from the server response after a successful check-in via form submit button', async() => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({ status: 'ok', id: 456 }),
});

const components = createAndInitialize();

await dispatchSubmitOnForm(components, {year: 2024, month: 1, day: 1});

expect(components.checkInForm.getEventId()).toBe('456');
});

it('does not set event id when server response has no id field', async() => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
// Server returns ok but no id (edge case)
json: jest.fn().mockResolvedValue({ status: 'ok' }),
});

const components = createAndInitialize();

await dispatchSubmitOnPrompt(components, {year: 2024, month: 3, day: 10});

// eventId should remain empty — no crash, no bad value stored
expect(components.checkInForm.getEventId()).toBe('');
});

it('performs a DELETE request with the stored event id after a check-in then delete', async() => {
const mockFetch = jest.fn()
.mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValue({ status: 'ok', id: 789 }),
})
.mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValue({ status: 'ok' }),
});
global.fetch = mockFetch;

const components = createAndInitialize();

// Step 1: Check in — stores the event id from the response
await dispatchSubmitOnPrompt(components, {year: 2024, month: 6, day: 15});
expect(components.checkInForm.getEventId()).toBe('789');

// Step 2: Dispatch delete event — triggers DELETE /check-ins/<id>
const deleteEvent = new CustomEvent('delete-check-in');
components.checkInForm.getRootElement().dispatchEvent(deleteEvent);
await flushPromises();

// Step 3: Verify the DELETE was sent to the correct URL
expect(mockFetch).toHaveBeenNthCalledWith(2, '/check-ins/789', { method: 'DELETE' });
});
});

describe('CreateListForm.js class', () => {
let form;
Expand Down
76 changes: 76 additions & 0 deletions tests/unit/js/sample-html/checkIns-test-data.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,79 @@
/**
* HTML fixture for the CheckInComponents container element.
* Mirrors the structure expected by the CheckInComponents constructor.
*/
export const checkInContainer = `
<div class="check-in-container" data-config='{"workOlid":"OL123W","editionKey":"/books/OL456M","lastReadDate":"","eventId":null,"i18n":{"failedSubmitCheckIn":"Failed to submit","failedDeleteCheckIn":"Failed to delete"}}'>
<div class="check-in-prompt">
<a class="prompt-current-year" href="javascript:;">This year</a>
<a class="prompt-today" href="javascript:;">Today</a>
</div>
<div class="last-read-date hidden">
<span class="check-in-date"></span>
</div>
</div>
`;

/**
* HTML fixture for the check-in form modal template.
* Must be present in the DOM with id="check-in-form-modal" so that
* CheckInComponents.createModalContentFromTemplate() can clone it.
*/
export const checkInFormModal = `
<div id="check-in-form-modal">
<form class="check-in__form" action="">
<input type="hidden" name="event_type" value="3">
<input type="hidden" name="event_id" value="">
<input type="hidden" name="edition_key" value="">
<div class="check-in__inputs">
<label class="check-in__year-label">Year:</label>
<select class="check-in__select" name="year">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WCAG 4.1.2: Form element has no accessible label.

Form elements must have labels. Use <label>, aria-label, or aria-labelledby.

Details

name: "year"

Every form input needs an accessible label so users understand what information to enter. Use a <label> element with a for attribute matching the input's id, wrap the input in a <label>, or use aria-label/aria-labelledby for custom components. Placeholders are not sufficient as labels since they disappear when typing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WCAG 4.1.2: Form element has no accessible label.

Form elements must have labels. Use <label>, aria-label, or aria-labelledby.

Details

name: "year"

Every form input needs an accessible label so users understand what information to enter. Use a <label> element with a for attribute matching the input's id, wrap the input in a <label>, or use aria-label/aria-labelledby for custom components. Placeholders are not sufficient as labels since they disappear when typing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WCAG 4.1.2: Form element has no accessible label.

Form elements must have labels. Use <label>, aria-label, or aria-labelledby.

Details

name: "year"

Every form input needs an accessible label so users understand what information to enter. Use a <label> element with a for attribute matching the input's id, wrap the input in a <label>, or use aria-label/aria-labelledby for custom components. Placeholders are not sufficient as labels since they disappear when typing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WCAG 4.1.2: Form element has no accessible label.

Form elements must have labels. Use <label>, aria-label, or aria-labelledby.

Details

name: "year"

Every form input needs an accessible label so users understand what information to enter. Use a <label> element with a for attribute matching the input's id, wrap the input in a <label>, or use aria-label/aria-labelledby for custom components. Placeholders are not sufficient as labels since they disappear when typing.

<option value="">Year</option>
<option class="hidden show-if-local-year" value="2025">2025</option>
<option value="2024">2024</option>
<option value="2023">2023</option>
</select>
<label class="check-in__month-label">Month:</label>
<select class="check-in__select" name="month" disabled>
<option value="">Month</option>
<option value="1">January</option>
<option value="2">February</option>
<option value="3">March</option>
<option value="4">April</option>
<option value="5">May</option>
<option value="6">June</option>
<option value="7">July</option>
<option value="8">August</option>
<option value="9">September</option>
<option value="10">October</option>
<option value="11">November</option>
<option value="12">December</option>
</select>
<label class="check-in__day-label">Day:</label>
<select class="check-in__select" name="day" disabled>
<option value="">Day</option>
<option value="1">1</option><option value="2">2</option><option value="3">3</option>
<option value="4">4</option><option value="5">5</option><option value="6">6</option>
<option value="7">7</option><option value="8">8</option><option value="9">9</option>
<option value="10">10</option><option value="11">11</option><option value="12">12</option>
<option value="13">13</option><option value="14">14</option><option value="15">15</option>
<option value="16">16</option><option value="17">17</option><option value="18">18</option>
<option value="19">19</option><option value="20">20</option><option value="21">21</option>
<option value="22">22</option><option value="23">23</option><option value="24">24</option>
<option value="25">25</option><option value="26">26</option><option value="27">27</option>
<option value="28">28</option><option value="29">29</option><option value="30">30</option>
<option value="31">31</option>
</select>
<a class="check-in__today" href="javascript:;">Today</a>
</div>
<span class="check-in__actions">
<button class="check-in__delete-btn cta-btn cta-btn--delete invisible">Delete Event</button>
<button type="submit" class="check-in__submit-btn cta-btn cta-btn--shell" disabled>Submit</button>
</span>
</form>
</div>
`;

export const checkInForm = `
<div class="check-in">
<form class="check-in__form" action="">
Expand Down