diff --git a/tests/javascripts/errorBanner.test.js b/tests/javascripts/errorBanner.test.js index 55affdc7d..932826043 100644 --- a/tests/javascripts/errorBanner.test.js +++ b/tests/javascripts/errorBanner.test.js @@ -1,6 +1,11 @@ beforeAll(() => { - // ErrorBanner module sets window.NotifyModules.ErrorBanner for backward compatibility + const originalNotifyModules = window.NotifyModules; + delete window.NotifyModules; + require('../../app/assets/javascripts/errorBanner.js'); + + expect(window.NotifyModules).toBeDefined(); + expect(window.NotifyModules.ErrorBanner).toBeDefined(); }); afterAll(() => { @@ -20,6 +25,13 @@ describe("Error Banner", () => { window.NotifyModules.ErrorBanner.hideBanner(); expect(document.querySelector('.banner-dangerous').classList).toContain('display-none') }); + + test("Will not error when no banner elements exist", () => { + document.body.innerHTML = `
No banners here
`; + expect(() => { + window.NotifyModules.ErrorBanner.hideBanner(); + }).not.toThrow(); + }); }); describe("The `showBanner` method", () => { @@ -34,6 +46,13 @@ describe("Error Banner", () => { test("Will show the element", () => { expect(document.querySelector('.banner-dangerous').classList).not.toContain('display-none') }); + + test("Will not error when no banner elements exist", () => { + document.body.innerHTML = `
No banners here
`; + expect(() => { + window.NotifyModules.ErrorBanner.showBanner(); + }).not.toThrow(); + }); }); test("Module exports ErrorBanner to window.NotifyModules", () => { diff --git a/tests/javascripts/fileUpload.test.js b/tests/javascripts/fileUpload.test.js index 2d8bf06f9..f02c48880 100644 --- a/tests/javascripts/fileUpload.test.js +++ b/tests/javascripts/fileUpload.test.js @@ -184,4 +184,15 @@ describe('announceUploadStatusFromElement', () => { // Still unchanged expect(srRegion.textContent).toBe('Old message'); }); + + test('does nothing if upload-status-live element does not exist', () => { + document.body.innerHTML = ` + File upload failed + `; + + expect(() => { + announceUploadStatusFromElement(); + jest.advanceTimersByTime(300); + }).not.toThrow(); + }); }); diff --git a/tests/javascripts/listEntry.test.js b/tests/javascripts/listEntry.test.js index 176949eea..e4fac6859 100644 --- a/tests/javascripts/listEntry.test.js +++ b/tests/javascripts/listEntry.test.js @@ -420,4 +420,23 @@ describe("List entry", () => { expect(inputList.innerHTML).toContain('id="test-2"'); }); }); + + describe("when the container element has no id attribute", () => { + test("the module should not initialize", () => { + document.body.innerHTML = ` +
+
+ +
+
`; + + const inputList = document.querySelector('[data-module="list-entry"]'); + + expect(() => { + window.NotifyModules.start(); + }).not.toThrow(); + + expect(inputList.querySelector('.input-list__button--add')).toBeNull(); + }); + }); }); diff --git a/tests/javascripts/liveSearch.test.js b/tests/javascripts/liveSearch.test.js index c0918fbd2..570b6170c 100644 --- a/tests/javascripts/liveSearch.test.js +++ b/tests/javascripts/liveSearch.test.js @@ -276,4 +276,133 @@ describe('Live search', () => { }); + describe("When items have checked checkboxes", () => { + + beforeEach(() => { + + const users = [ + { + "label": "Template editor", + "email": "template-editor@nhs.uk", + "permissions" : ["Add and edit templates"], + "checked": true + }, + { + "label": "Administrator", + "email": "admin@nhs.uk", + "permissions" : ["Send messages", "Add and edit templates", "Manage settings, team and usage", "API integration"], + "checked": false + } + ]; + + document.body.innerHTML = ` + +
+
+ + ${users[0].label} (${users[0].email}) +
+
+ + ${users[1].label} (${users[1].email}) +
+
`; + + searchTextbox = document.getElementById('search'); + liveRegion = document.querySelector('.live-search__status'); + list = document.querySelector('form'); + + }); + + test("Items with checked checkboxes should always be visible", () => { + + searchTextbox.value = 'Administrator'; + + window.NotifyModules.start(); + + const listItems = list.querySelectorAll('.user-list-item'); + const listItemsShowing = Array.from(listItems).filter(item => !item.classList.contains('js-hidden')); + + expect(listItemsShowing.length).toEqual(2); + + }); + + }); + + describe("When no results message element exists", () => { + + beforeEach(() => { + + document.body.innerHTML = ` + +
+
+ Test User (test@example.com) +
+
`; + + searchTextbox = document.getElementById('search'); + liveRegion = document.querySelector('.live-search__status'); + list = document.querySelector('form'); + + }); + + test("No results message should show when search yields no results", () => { + + searchTextbox.value = 'nomatch'; + + window.NotifyModules.start(); + + const noResultsMessage = document.querySelector('.js-live-search-no-results'); + expect(noResultsMessage.classList.contains('js-hidden')).toBe(false); + + }); + + test("No results message should hide when search yields results", () => { + + searchTextbox.value = 'nomatch'; + window.NotifyModules.start(); + + let noResultsMessage = document.querySelector('.js-live-search-no-results'); + expect(noResultsMessage.classList.contains('js-hidden')).toBe(false); + + searchTextbox.value = 'Test'; + helpers.triggerEvent(searchTextbox, 'input'); + + expect(noResultsMessage.classList.contains('js-hidden')).toBe(true); + + }); + + test("No results message should hide when search is empty", () => { + + searchTextbox.value = 'nomatch'; + window.NotifyModules.start(); + + searchTextbox.value = ''; + helpers.triggerEvent(searchTextbox, 'input'); + + const noResultsMessage = document.querySelector('.js-live-search-no-results'); + expect(noResultsMessage.classList.contains('js-hidden')).toBe(true); + + }); + + }); + }); diff --git a/tests/javascripts/notifyModal.test.js b/tests/javascripts/notifyModal.test.js index be703fd97..5e8156194 100644 --- a/tests/javascripts/notifyModal.test.js +++ b/tests/javascripts/notifyModal.test.js @@ -159,3 +159,70 @@ describe("Modal trigger buttons", () => { expect(modalWrapper.classList.contains("is-hidden")).toBe(true); }); }); + +describe("Modal edge cases", () => { + test("openModal does nothing if wrapper doesn't exist", () => { + expect(() => { + openModal("nonExistentModal"); + }).not.toThrow(); + }); + + test("openModal does nothing if modal element doesn't exist inside wrapper", () => { + document.body.innerHTML = `
`; + expect(() => { + openModal("emptyWrapper"); + }).not.toThrow(); + }); + + test("openModal works when there is no focusable element", () => { + document.body.innerHTML = ` +
+
+

No focusable elements here

+
+
+ `; + expect(() => { + openModal("noFocusModal"); + }).not.toThrow(); + expect(document.getElementById("noFocusModal").classList.contains("is-hidden")).toBe(false); + }); + + test("closeModal does nothing if no modal is active", () => { + expect(() => { + closeModal(); + }).not.toThrow(); + }); + + test("closeModal handles case where modal element is missing", () => { + document.body.innerHTML = ` + +
+ `; + openModal("testModal"); + expect(() => { + closeModal(); + }).not.toThrow(); + }); + + test("Modal keydown handler ignores non-Tab keys", () => { + document.body.innerHTML = ` +
+
+ +
+
+ `; + openModal("keyModal"); + const modal = document.querySelector(".usa-modal"); + + const enterEvent = new KeyboardEvent("keydown", { + key: "Enter", + bubbles: true + }); + + expect(() => { + modal.dispatchEvent(enterEvent); + }).not.toThrow(); + }); +}); diff --git a/tests/javascripts/preventDuplicateFormSubmissions.test.js b/tests/javascripts/preventDuplicateFormSubmissions.test.js index af043b3e6..450ef483f 100644 --- a/tests/javascripts/preventDuplicateFormSubmissions.test.js +++ b/tests/javascripts/preventDuplicateFormSubmissions.test.js @@ -65,5 +65,140 @@ describe('Prevent duplicate form submissions', () => { }); + test("It should not error when form has no submit button", () => { + + document.body.innerHTML = ` +
+ +
`; + + form = document.querySelector('form'); + formEventSpy = helpers.spyOnFormSubmitEventPrevention(jest, form); + + jest.resetModules(); + require('../../app/assets/javascripts/preventDuplicateFormSubmissions.js'); + + expect(() => { + helpers.triggerEvent(form, 'submit'); + }).not.toThrow(); + + }); + + test("It should disable and add spinner to 'send' button", () => { + + document.body.innerHTML = ` +
+ +
`; + + form = document.querySelector('form'); + button = document.querySelector('button'); + formEventSpy = helpers.spyOnFormSubmitEventPrevention(jest, form); + + jest.resetModules(); + require('../../app/assets/javascripts/preventDuplicateFormSubmissions.js'); + + helpers.triggerEvent(button, 'click'); + + jest.advanceTimersByTime(100); + + expect(button.disabled).toBe(true); + expect(button.getAttribute('aria-busy')).toBe('true'); + expect(button.querySelector('.loading-spinner')).not.toBeNull(); + + }); + + test("It should disable and add spinner to 'schedule' button", () => { + + document.body.innerHTML = ` +
+ +
`; + + form = document.querySelector('form'); + button = document.querySelector('button'); + formEventSpy = helpers.spyOnFormSubmitEventPrevention(jest, form); + + jest.resetModules(); + require('../../app/assets/javascripts/preventDuplicateFormSubmissions.js'); + + helpers.triggerEvent(button, 'click'); + + jest.advanceTimersByTime(100); + + expect(button.disabled).toBe(true); + expect(button.getAttribute('aria-busy')).toBe('true'); + expect(button.querySelector('.loading-spinner')).not.toBeNull(); + + }); + + test("It should disable both send and cancel buttons when send is clicked", () => { + + document.body.innerHTML = ` +
+ + +
`; + + form = document.querySelector('form'); + const sendButton = document.querySelector('button[name="send"]'); + const cancelButton = document.querySelector('button[name="cancel"]'); + formEventSpy = helpers.spyOnFormSubmitEventPrevention(jest, form); + + jest.resetModules(); + require('../../app/assets/javascripts/preventDuplicateFormSubmissions.js'); + + helpers.triggerEvent(sendButton, 'click'); + + jest.advanceTimersByTime(100); + + expect(sendButton.disabled).toBe(true); + expect(cancelButton.disabled).toBe(true); + + }); + + test("It should not add duplicate spinner if one already exists", () => { + + document.body.innerHTML = ` +
+ +
`; + + form = document.querySelector('form'); + button = document.querySelector('button'); + formEventSpy = helpers.spyOnFormSubmitEventPrevention(jest, form); + + jest.resetModules(); + require('../../app/assets/javascripts/preventDuplicateFormSubmissions.js'); + + helpers.triggerEvent(button, 'click'); + + jest.advanceTimersByTime(100); + + expect(button.querySelectorAll('.loading-spinner').length).toBe(1); + + }); + + test("It should handle button with no name attribute", () => { + + document.body.innerHTML = ` +
+ +
`; + + form = document.querySelector('form'); + button = document.querySelector('button'); + formEventSpy = helpers.spyOnFormSubmitEventPrevention(jest, form); + + jest.resetModules(); + require('../../app/assets/javascripts/preventDuplicateFormSubmissions.js'); + + expect(() => { + helpers.triggerEvent(button, 'click'); + jest.advanceTimersByTime(1600); + }).not.toThrow(); + + }); + }); diff --git a/tests/javascripts/radioSelect.test.js b/tests/javascripts/radioSelect.test.js index 9e67288a0..387efe5ae 100644 --- a/tests/javascripts/radioSelect.test.js +++ b/tests/javascripts/radioSelect.test.js @@ -434,6 +434,17 @@ describe('RadioSelect', () => { }); + test("clicking the 'Done' button without selecting an option should reset to default state", () => { + + const doneButton = document.querySelector('.radio-select__column:nth-child(2) input[type=button]'); + + helpers.triggerEvent(doneButton, 'click'); + + const categoryButtons = document.querySelectorAll('.radio-select__column:nth-child(2) .radio-select__button--category'); + expect(categoryButtons.length).toBeGreaterThan(0); + + }); + describe("after selecting an option clicking the 'Choose a different time' button should", () => { beforeEach(() => { diff --git a/tests/javascripts/timeoutPopup.test.js b/tests/javascripts/timeoutPopup.test.js index 1afea4b86..26d35a1f1 100644 --- a/tests/javascripts/timeoutPopup.test.js +++ b/tests/javascripts/timeoutPopup.test.js @@ -135,4 +135,37 @@ describe('The session timer ', () => { window.NotifyModules.TimeoutPopup.checkTimer(); expect(checkTimerMock).toHaveBeenCalled(); }); + + test('checkTimer expires and redirects when time runs out', () => { + const restore = mockWindowLocation(); + const sessionTimer = document.getElementById('sessionTimer'); + sessionTimer.showModal = jest.fn(); + sessionTimer.close = jest.fn(); + + const closeTimerMock = jest.spyOn(sessionTimer, 'close'); + + const pastTime = new Date().getTime() - 1000; + + window.NotifyModules.TimeoutPopup.checkTimer(pastTime); + + expect(closeTimerMock).toHaveBeenCalled(); + + restore(); + }); + + test('checkTimer updates time display and shows modal when time is remaining', () => { + const sessionTimer = document.getElementById('sessionTimer'); + const timeLeftElement = document.getElementById('timeLeft'); + sessionTimer.showModal = jest.fn(); + sessionTimer.close = jest.fn(); + + const showModalMock = jest.spyOn(sessionTimer, 'showModal'); + + const futureTime = new Date().getTime() + (2 * 60 * 1000); + + window.NotifyModules.TimeoutPopup.checkTimer(futureTime); + + expect(showModalMock).toHaveBeenCalled(); + expect(timeLeftElement.textContent).toMatch(/\d+m \d+s/); + }); }); diff --git a/tests/javascripts/updateStatus.test.js b/tests/javascripts/updateStatus.test.js index 30c53384e..0504db2e8 100644 --- a/tests/javascripts/updateStatus.test.js +++ b/tests/javascripts/updateStatus.test.js @@ -179,4 +179,25 @@ describe('Update content', () => { }); + test("It should handle fetch errors gracefully", async () => { + + global.fetch.mockImplementationOnce(() => + Promise.resolve({ + ok: false, + status: 500, + json: () => Promise.resolve({}) + }) + ); + + window.NotifyModules.start(); + + const textbox = document.getElementById('template_content'); + helpers.triggerEvent(textbox, 'input'); + + jest.runAllTimers(); + + expect(document.querySelector('[data-module="update-status"]')).not.toBeNull(); + + }); + });