diff --git a/app/assets/javascripts/liveSearch.js b/app/assets/javascripts/liveSearch.js index 99243504e..469b34575 100644 --- a/app/assets/javascripts/liveSearch.js +++ b/app/assets/javascripts/liveSearch.js @@ -12,24 +12,25 @@ } }; - let filter = ($searchBox, $searchLabel, $liveRegion, $targets) => () => { + let filter = (searchBox, searchLabel, liveRegion, targets) => () => { - let query = normalize($searchBox.val()); + let query = normalize(searchBox.value); let results = 0; - let $noResultsMessage = $('.js-live-search-no-results'); + let noResultsMessage = document.querySelector('.js-live-search-no-results'); - $targets.each(function() { + targets.forEach(function(target) { - let content = $('.live-search-relevant', this).text() || $(this).text(); + let relevantElement = target.querySelector('.live-search-relevant'); + let content = relevantElement ? relevantElement.textContent : target.textContent; - if ($(this).has(':checked').length) { - $(this).show(); + if (target.querySelector(':checked')) { + target.classList.remove('js-hidden'); results++; return; } if (query == '') { - $(this).removeClass('js-hidden'); + target.classList.remove('js-hidden'); results++; return; } @@ -37,27 +38,29 @@ let isMatch = normalize(content).includes(normalize(query)); if (isMatch) { - $(this).removeClass('js-hidden'); + target.classList.remove('js-hidden'); results++; } else { - $(this).addClass('js-hidden'); + target.classList.add('js-hidden'); } }); - if (query !== '' && results === 0) { - $noResultsMessage.removeClass('js-hidden'); - } else { - $noResultsMessage.addClass('js-hidden'); + if (noResultsMessage) { + if (query !== '' && results === 0) { + noResultsMessage.classList.remove('js-hidden'); + } else { + noResultsMessage.classList.add('js-hidden'); + } } if (state === 'loaded') { if (query !== '') { - $searchBox.attr('aria-label', $searchLabel.text().trim() + ', ' + resultsSummary(results)); + searchBox.setAttribute('aria-label', searchLabel.textContent.trim() + ', ' + resultsSummary(results)); } state = 'active'; } else { - $searchBox.removeAttr('aria-label'); - $liveRegion.text(resultsSummary(results)); + searchBox.removeAttribute('aria-label'); + liveRegion.textContent = resultsSummary(results); } // make sticky JS recalculate its cache of the element's position @@ -73,22 +76,24 @@ this.start = function(component) { - let $component = $(component); + let searchBox = component.querySelector('input'); + let searchLabel = component.querySelector('label'); + let liveRegion = component.querySelector('.live-search__status'); - let $searchBox = $('input', $component); - let $searchLabel = $('label', $component); - let $liveRegion = $('.live-search__status', $component); + let targetsSelector = component.dataset.targets; + let targets = Array.from(document.querySelectorAll(targetsSelector)); let filterFunc = filter( - $searchBox, - $searchLabel, - $liveRegion, - $($component.data('targets')) + searchBox, + searchLabel, + liveRegion, + targets ); state = 'loaded'; - $searchBox.on('keyup input', filterFunc); + searchBox.addEventListener('keyup', filterFunc); + searchBox.addEventListener('input', filterFunc); filterFunc(); diff --git a/app/assets/javascripts/updateStatus.js b/app/assets/javascripts/updateStatus.js index ce6aff6ce..fc690be97 100644 --- a/app/assets/javascripts/updateStatus.js +++ b/app/assets/javascripts/updateStatus.js @@ -3,9 +3,9 @@ window.NotifyModules['update-status'] = function() { - const getRenderer = $component => response => $component.html( - response.html - ); + const getRenderer = component => response => { + component.innerHTML = response.html; + }; const throttle = (func, limit) => { @@ -41,39 +41,38 @@ let id = 'update-status'; - this.$component = $(component); - this.$textbox = $('#' + this.$component.data('target')); + this.component = component; + this.textbox = document.getElementById(this.component.dataset.target); - this.$component - .attr('id', id); + this.component.setAttribute('id', id); - this.$textbox - .attr( - 'aria-describedby', - ( - this.$textbox.attr('aria-describedby') || '' - ) + ( - this.$textbox.attr('aria-describedby') ? ' ' : '' - ) + id - ) - .on('input', throttle(this.update, 150)) - .trigger('input'); + const currentAriaDescribedBy = this.textbox.getAttribute('aria-describedby') || ''; + const newAriaDescribedBy = currentAriaDescribedBy + (currentAriaDescribedBy ? ' ' : '') + id; + this.textbox.setAttribute('aria-describedby', newAriaDescribedBy); + + this.textbox.addEventListener('input', throttle(this.update, 150)); + this.textbox.dispatchEvent(new Event('input')); }; this.update = () => { - $.ajax( - this.$component.data('updates-url'), - { - 'method': 'post', - 'data': this.$textbox.parents('form').serialize() - } - ).done( - getRenderer(this.$component) - ).fail( - () => {} - ); + const form = this.textbox.closest('form'); + const formData = new FormData(form); + + fetch(this.component.dataset.updatesUrl, { + method: 'POST', + body: formData, + credentials: 'same-origin' + }) + .then(response => { + if (!response.ok) { + throw new Error('HTTP error'); + } + return response.json(); + }) + .then(getRenderer(this.component)) + .catch(() => {}); }; diff --git a/tests/javascripts/updateStatus.test.js b/tests/javascripts/updateStatus.test.js index ef01ca10e..30c53384e 100644 --- a/tests/javascripts/updateStatus.test.js +++ b/tests/javascripts/updateStatus.test.js @@ -6,27 +6,19 @@ const serviceNumber = '6658542f-0cad-491f-bec8-ab8457700ead'; const updatesURL = `/services/${serviceNumber}/templates/count-sms-length`; let responseObj = {}; -let jqueryAJAXReturnObj; beforeAll(() => { // ensure all timers go through Jest jest.useFakeTimers(); - // mock the bits of jQuery used - jest.spyOn(window.$, 'ajax'); - - // set up the object returned from $.ajax so it responds with whatever responseObj is set to - jqueryAJAXReturnObj = { - done: callback => { - // For these tests the server responds immediately - callback(responseObj); - return jqueryAJAXReturnObj; - }, - fail: () => {} - }; - - $.ajax.mockImplementation(() => jqueryAJAXReturnObj); + // mock fetch + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(responseObj) + }) + ); require('../../app/assets/javascripts/updateStatus.js'); @@ -58,8 +50,8 @@ describe('Update content', () => { document.body.innerHTML = ''; - // tidy up record of mocked AJAX calls - $.ajax.mockClear(); + // tidy up record of mocked fetch calls + fetch.mockClear(); // ensure any timers set by continually starting the module are cleared jest.clearAllTimers(); @@ -102,18 +94,21 @@ describe('Update content', () => { window.NotifyModules.start(); - expect($.ajax.mock.calls[0][0]).toEqual(updatesURL); - expect($.ajax.mock.calls[0]).toEqual([ - updatesURL, - { - "data": "csrf_token=abc123&template_content=Content%20of%20message", - "method": "post" - } - ]); + expect(fetch.mock.calls[0][0]).toEqual(updatesURL); + expect(fetch.mock.calls[0][1]).toMatchObject({ + method: 'POST', + credentials: 'same-origin', + body: expect.any(FormData) + }); + + // Verify FormData contents + const formData = fetch.mock.calls[0][1].body; + expect(formData.get('csrf_token')).toBe('abc123'); + expect(formData.get('template_content')).toBe('Content of message'); }); - test("It should replace the content of the div with the returned HTML", () => { + test("It should replace the content of the div with the returned HTML", async () => { responseObj = {'html': 'Updated content'} @@ -123,14 +118,23 @@ describe('Update content', () => { "Initial content" ); + // Use real timers for async operations + jest.useRealTimers(); + window.NotifyModules.start(); + // Wait for the promise chain to complete + await new Promise(resolve => setTimeout(resolve, 0)); + expect( document.querySelectorAll('[data-module=update-status]')[0].textContent.trim() ).toEqual( "Updated content" ); + // Restore fake timers + jest.useFakeTimers(); + }); test("It should fire when the content of the textarea changes", () => { @@ -139,13 +143,13 @@ describe('Update content', () => { // Initial update triggered window.NotifyModules.start(); - expect($.ajax.mock.calls.length).toEqual(1); + expect(fetch.mock.calls.length).toEqual(1); // 150ms of inactivity jest.advanceTimersByTime(150); helpers.triggerEvent(textarea, 'input'); - expect($.ajax.mock.calls.length).toEqual(2); + expect(fetch.mock.calls.length).toEqual(2); }); @@ -155,23 +159,23 @@ describe('Update content', () => { // Initial update triggered window.NotifyModules.start(); - expect($.ajax.mock.calls.length).toEqual(1); + expect(fetch.mock.calls.length).toEqual(1); helpers.triggerEvent(textarea, 'input'); jest.advanceTimersByTime(149); - expect($.ajax.mock.calls.length).toEqual(1); + expect(fetch.mock.calls.length).toEqual(1); helpers.triggerEvent(textarea, 'input'); jest.advanceTimersByTime(149); - expect($.ajax.mock.calls.length).toEqual(1); + expect(fetch.mock.calls.length).toEqual(1); helpers.triggerEvent(textarea, 'input'); jest.advanceTimersByTime(149); - expect($.ajax.mock.calls.length).toEqual(1); + expect(fetch.mock.calls.length).toEqual(1); // > 150ms of inactivity jest.advanceTimersByTime(1); - expect($.ajax.mock.calls.length).toEqual(2); + expect(fetch.mock.calls.length).toEqual(2); });