From f2cf5c3386d41f411ff5eb07bc7d3ae0ad709ad4 Mon Sep 17 00:00:00 2001 From: eyelidlessness Date: Thu, 6 Oct 2022 16:06:09 -0700 Subject: [PATCH] Test applicaiton-cache.js --- public/js/src/module/application-cache.js | 32 ++++- test/client/application-cache.spec.js | 168 ++++++++++++++++++++++ 2 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 test/client/application-cache.spec.js diff --git a/public/js/src/module/application-cache.js b/public/js/src/module/application-cache.js index 0a6345b26..c1157e213 100644 --- a/public/js/src/module/application-cache.js +++ b/public/js/src/module/application-cache.js @@ -5,6 +5,28 @@ import events from './event'; import settings from './settings'; +/** + * @private + * + * Used only for mocking `window.reload` in tests. + */ +const location = { + get protocol() { + return window.location.protocol; + }, + + reload() { + window.location.reload(); + }, +}; + +/** + * @private + * + * Exported only for testing. + */ +const UPDATE_REGISTRATION_INTERVAL = 60 * 60 * 1000; + /** * @typedef {import('../../../../app/models/survey-model').SurveyObject} Survey */ @@ -14,7 +36,7 @@ import settings from './settings'; */ const init = async (survey) => { try { - if ('serviceWorker' in navigator) { + if (navigator.serviceWorker != null) { const registration = await navigator.serviceWorker.register( `${settings.basePath}/x/offline-app-worker.js` ); @@ -29,7 +51,7 @@ const init = async (survey) => { 'Checking for offline application cache service worker update' ); registration.update(); - }, 60 * 60 * 1000); + }, UPDATE_REGISTRATION_INTERVAL); const currentActive = registration.active; @@ -41,7 +63,7 @@ const init = async (survey) => { navigator.serviceWorker.addEventListener( 'controllerchange', () => { - window.location.reload(); + location.reload(); } ); } @@ -49,7 +71,7 @@ const init = async (survey) => { await registration.update(); if (currentActive == null) { - window.location.reload(); + location.reload(); } else { _reportOfflineLaunchCapable(true); } @@ -88,6 +110,8 @@ function _reportOfflineLaunchCapable(capable = true) { export default { init, + location, + UPDATE_REGISTRATION_INTERVAL, get serviceWorkerScriptUrl() { if ( 'serviceWorker' in navigator && diff --git a/test/client/application-cache.spec.js b/test/client/application-cache.spec.js new file mode 100644 index 000000000..b01ef7051 --- /dev/null +++ b/test/client/application-cache.spec.js @@ -0,0 +1,168 @@ +import applicationCache from '../../public/js/src/module/application-cache'; +import events from '../../public/js/src/module/event'; +import settings from '../../public/js/src/module/settings'; + +describe('Application Cache', () => { + const basePath = '-'; + const offlineLaunchCapableType = events.OfflineLaunchCapable().type; + + /** @type {ServiceWorker | null} */ + let activeServiceWorker; + + /** @type {sinon.SinonSandbox} */ + let sandbox; + + /** @type {sinon.SinonFakeTimers} */ + let timers; + + /** @type {sinon.SinonFake} */ + let offlineLaunchCapableListener; + + /** @type {sinon.SinonStub} */ + let reloadStub; + + /** @type {sinon.SinonStub} */ + let registrationStub; + + /** @type {sinon.SinonFake} */ + let registrationUpdateFake; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + timers = sandbox.useFakeTimers(Date.now()); + + offlineLaunchCapableListener = sinon.fake(); + + document.addEventListener( + offlineLaunchCapableType, + offlineLaunchCapableListener + ); + + activeServiceWorker = null; + + registrationUpdateFake = sandbox.fake(() => Promise.resolve()); + + registrationStub = sandbox + .stub(navigator.serviceWorker, 'register') + .callsFake(() => + Promise.resolve({ + addEventListener() {}, + active: activeServiceWorker, + update: registrationUpdateFake, + }) + ); + reloadStub = sandbox + .stub(applicationCache.location, 'reload') + .callsFake(() => {}); + + if (!('basePath' in settings)) { + settings.basePath = undefined; + } + + sandbox.stub(settings, 'basePath').value(basePath); + }); + + afterEach(() => { + document.removeEventListener( + offlineLaunchCapableType, + offlineLaunchCapableListener + ); + timers.restore(); + sandbox.restore(); + }); + + it('registers the service worker script', async () => { + await applicationCache.init(); + + expect(registrationStub).to.have.been.calledWith( + `${basePath}/x/offline-app-worker.js` + ); + }); + + it('reloads immediately after registering the service worker for the first time', async () => { + await applicationCache.init(); + + expect(reloadStub).to.have.been.called; + }); + + it('does not reload immediately after registering the service worker for subsequent times', async () => { + activeServiceWorker = {}; + + await applicationCache.init(); + + expect(reloadStub).not.to.have.been.called; + }); + + it('reports offline capability after registering the service worker for subsequent times', async () => { + activeServiceWorker = {}; + + await applicationCache.init(); + + expect(offlineLaunchCapableListener).to.have.been.calledWith( + events.OfflineLaunchCapable({ capable: true }) + ); + }); + + it('reports offline capability is not available when service workers are not available', async () => { + activeServiceWorker = {}; + + sandbox.stub(navigator, 'serviceWorker').value(null); + + await applicationCache.init(); + + expect(offlineLaunchCapableListener).to.have.been.calledWith( + events.OfflineLaunchCapable({ capable: false }) + ); + }); + + it('reports offline capability is not available when registration throws an error', async () => { + activeServiceWorker = {}; + + const error = new Error('Something bad'); + + registrationStub.callsFake(() => Promise.reject(error)); + + /** @type {Error} */ + let caught; + + try { + await applicationCache.init(); + } catch (error) { + caught = error; + } + + expect(offlineLaunchCapableListener).to.have.been.calledWith( + events.OfflineLaunchCapable({ capable: false }) + ); + expect(caught instanceof Error).to.equal(true); + expect(caught.message).to.include(error.message); + expect(caught.stack).to.equal(error.stack); + }); + + it('reloads when an updated service worker becomes active', async () => { + activeServiceWorker = {}; + await applicationCache.init(); + + expect(applicationCache.location.reload).not.to.have.been.called; + + navigator.serviceWorker.dispatchEvent(new Event('controllerchange')); + + expect(applicationCache.location.reload).to.have.been.called; + }); + + it('checks for updates immediately after registration', async () => { + await applicationCache.init(); + + expect(registrationUpdateFake).to.have.been.calledOnce; + }); + + it('checks for updates immediately after registration', async () => { + await applicationCache.init(); + + expect(registrationUpdateFake).to.have.been.calledOnce; + + timers.tick(applicationCache.UPDATE_REGISTRATION_INTERVAL); + + expect(registrationUpdateFake).to.have.been.calledTwice; + }); +});