diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js index 64f62ff4e0a..e53e89c3258 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js @@ -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(() => { @@ -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(() => { diff --git a/tests/unit/js/my-books.test.js b/tests/unit/js/my-books.test.js index 4130f33c511..5051911b831 100644 --- a/tests/unit/js/my-books.test.js +++ b/tests/unit/js/my-books.test.js @@ -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/ + 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; diff --git a/tests/unit/js/sample-html/checkIns-test-data.js b/tests/unit/js/sample-html/checkIns-test-data.js index a653c1b4db1..f8eedc91fdd 100644 --- a/tests/unit/js/sample-html/checkIns-test-data.js +++ b/tests/unit/js/sample-html/checkIns-test-data.js @@ -1,3 +1,79 @@ +/** + * HTML fixture for the CheckInComponents container element. + * Mirrors the structure expected by the CheckInComponents constructor. + */ +export const checkInContainer = ` +
+ + +
+`; + +/** + * 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 = ` +
+
+ + + +
+ + + + + + + Today +
+ + + + +
+
+`; + export const checkInForm = `