From 687623969ec5c22250880e246dc4fed29df0e5ca Mon Sep 17 00:00:00 2001 From: Diana Barsan <35681649+dianabarsan@users.noreply.github.com> Date: Wed, 6 Nov 2024 22:51:07 +0300 Subject: [PATCH] feat(#7375): service worker auto update (#9417) Show the reload modal even when the service worker updates automatically in the background due to browser internals. #7375 --- .../db/initial-replication.wdio-spec.js | 1 + .../default/common/common.wdio.page.js | 45 +++++++++++-------- webapp/src/ts/app.component.ts | 2 + webapp/src/ts/services/form.service.ts | 2 - webapp/tests/karma/ts/app.component.spec.ts | 24 +++++++++- 5 files changed, 51 insertions(+), 23 deletions(-) diff --git a/tests/e2e/default/db/initial-replication.wdio-spec.js b/tests/e2e/default/db/initial-replication.wdio-spec.js index 4acb8bfd82b..92a907973d7 100644 --- a/tests/e2e/default/db/initial-replication.wdio-spec.js +++ b/tests/e2e/default/db/initial-replication.wdio-spec.js @@ -155,6 +155,7 @@ describe('initial-replication', () => { setTimeout(() => browser.refresh(), 3000); setTimeout(() => browser.refresh(), 5000); + await utils.delayPromise(5000); // wait for above timers to expire await commonPage.waitForPageLoaded(); await validateReplication(); }); diff --git a/tests/page-objects/default/common/common.wdio.page.js b/tests/page-objects/default/common/common.wdio.page.js index 7ff038c8a2b..3d2e371d606 100644 --- a/tests/page-objects/default/common/common.wdio.page.js +++ b/tests/page-objects/default/common/common.wdio.page.js @@ -51,6 +51,8 @@ const FEEDBACK = '#feedback'; //About menu const ABOUT_MENU = 'aria/About'; //Configuration App +const ELEMENT_DISPLAY_PAUSE = 500; // 500ms + const configurationAppMenuOption = () => $('aria/App Management'); const errorLog = () => $(`error-log`); const sideBarMenuTitle = () => $('aria/Menu'); @@ -72,7 +74,7 @@ const waitForSnackbarToClose = async () => { const clickFastActionById = async (id) => { // Wait for the Angular Material's animation to complete. - await browser.pause(500); + await browser.pause(ELEMENT_DISPLAY_PAUSE); await (await fastActionListContainer()).waitForDisplayed(); await (await fastActionById(id)).waitForClickable(); await (await fastActionById(id)).click(); @@ -107,7 +109,7 @@ const getFastActionItemsLabels = async () => { await fab.waitForClickable(); await fab.click(); - await browser.pause(500); + await browser.pause(ELEMENT_DISPLAY_PAUSE); await (await fastActionListContainer()).waitForDisplayed(); const items = await fastActionItems(); @@ -202,10 +204,9 @@ const openHamburgerMenu = async () => { if (!(await isHamburgerMenuOpen())) { await (await hamburgerMenu()).waitForClickable(); await (await hamburgerMenu()).click(); + // Adding pause here as we have to wait for sidebar nav menu animation to load + await browser.pause(ELEMENT_DISPLAY_PAUSE); } - - // Adding pause here as we have to wait for sidebar nav menu animation to load - await browser.pause(500); await (await sideBarMenuTitle()).waitForDisplayed(); }; @@ -345,16 +346,25 @@ const syncAndNotWaitForSuccess = async () => { await (await syncButton()).click(); }; -const syncAndWaitForSuccess = async (timeout = 20000) => { - await openHamburgerMenu(); - await (await syncButton()).waitForClickable(); - await (await syncButton()).click(); - await closeReloadModal(false); - await openHamburgerMenu(); - if (await (await syncInProgress()).isExisting()) { - await (await syncInProgress()).waitForDisplayed({ reverse: true, timeout }); +const syncAndWaitForSuccess = async (timeout = 20000, retry = 10) => { + if (retry < 0) { + throw new Error('Failed to sync after 10 retries'); + } + await closeReloadModal(false, 0); + + try { + await openHamburgerMenu(); + if (!await (await syncInProgress()).isExisting()) { + await (await syncButton()).click(); + await openHamburgerMenu(); + } + + await (await syncInProgress()).waitForDisplayed({ timeout, reverse: true }); + await (await syncSuccess()).waitForDisplayed({ timeout }); + } catch (err) { + console.error(err); + await syncAndWaitForSuccess(timeout, retry - 1); } - await (await syncSuccess()).waitForDisplayed({ timeout }); }; const hideModalOverlay = () => { @@ -372,15 +382,13 @@ const sync = async (expectReload, timeout) => { let closedModal = false; if (expectReload) { // it's possible that sync already happened organically, and we already have the reload modal - closedModal = await closeReloadModal(); + closedModal = await closeReloadModal(false, 0); } await syncAndWaitForSuccess(timeout); if (expectReload && !closedModal) { await closeReloadModal(); } - // sync status sometimes lies when multiple changes are fired in quick succession - await syncAndWaitForSuccess(timeout); await closeHamburgerMenu(); }; @@ -393,12 +401,11 @@ const syncAndWaitForFailure = async () => { const closeReloadModal = async (shouldUpdate = false, timeout = 5000) => { try { - await browser.waitUntil( async () => await modalPage.modal().isDisplayed(), { timeout: 10000, interval: 500 } ); shouldUpdate ? await modalPage.submit(timeout) : await modalPage.cancel(timeout); shouldUpdate && await waitForAngularLoaded(); return true; } catch (err) { - console.error('Reload modal not showed up'); + timeout && console.error('Reload modal has not showed up'); return false; } }; diff --git a/webapp/src/ts/app.component.ts b/webapp/src/ts/app.component.ts index 6716a0e5b39..6ee6738633a 100644 --- a/webapp/src/ts/app.component.ts +++ b/webapp/src/ts/app.component.ts @@ -377,6 +377,8 @@ export class AppComponent implements OnInit, AfterViewInit { } private watchDDocChanges() { + this.updateServiceWorker.update(() => this.ngZone.run(() => this.showUpdateReady())); + this.changesService.subscribe({ key: 'ddoc', filter: (change) => { diff --git a/webapp/src/ts/services/form.service.ts b/webapp/src/ts/services/form.service.ts index 8e4f99deb37..2329c820407 100644 --- a/webapp/src/ts/services/form.service.ts +++ b/webapp/src/ts/services/form.service.ts @@ -8,7 +8,6 @@ import * as medicXpathExtensions from '../../js/enketo/medic-xpath-extensions'; import { DbService } from '@mm-services/db.service'; import { FileReaderService } from '@mm-services/file-reader.service'; import { LineageModelGeneratorService } from '@mm-services/lineage-model-generator.service'; -import { SearchService } from '@mm-services/search.service'; import { SubmitFormBySmsService } from '@mm-services/submit-form-by-sms.service'; import { UserContactService } from '@mm-services/user-contact.service'; import { XmlFormsService } from '@mm-services/xml-forms.service'; @@ -46,7 +45,6 @@ export class FormService { private dbService: DbService, private fileReaderService: FileReaderService, private lineageModelGeneratorService: LineageModelGeneratorService, - private searchService: SearchService, private submitFormBySmsService: SubmitFormBySmsService, private userContactService: UserContactService, private userSettingsService:UserSettingsService, diff --git a/webapp/tests/karma/ts/app.component.spec.ts b/webapp/tests/karma/ts/app.component.spec.ts index 1744063b60c..f19119185ef 100644 --- a/webapp/tests/karma/ts/app.component.spec.ts +++ b/webapp/tests/karma/ts/app.component.spec.ts @@ -49,6 +49,7 @@ import { UserSettingsService } from '@mm-services/user-settings.service'; import { FormService } from '@mm-services/form.service'; import { OLD_NAV_PERMISSION } from '@mm-components/header/header.component'; import { SidebarMenuComponent } from '@mm-components/sidebar-menu/sidebar-menu.component'; +import { ReloadingComponent } from '@mm-modals/reloading/reloading.component'; describe('AppComponent', () => { let component: AppComponent; @@ -89,6 +90,7 @@ describe('AppComponent', () => { let trainingCardsService; let userSettingsService; let formService; + let updateServiceWorkerService; // End Services let globalActions; @@ -123,7 +125,11 @@ describe('AppComponent', () => { unreadRecordsService = { init: sinon.stub() }; setLanguageService = { set: sinon.stub() }; translateService = { instant: sinon.stub().returnsArg(0) }; - modalService = { show: sinon.stub().resolves() }; + modalService = { + show: sinon.stub().returns({ + afterClosed: sinon.stub().returns(of()) + }) + }; browserDetectorService = { isUsingOutdatedBrowser: sinon.stub().returns(false) }; chtDatasourceService = { isInitialized: sinon.stub() }; analyticsModulesService = { get: sinon.stub() }; @@ -175,6 +181,7 @@ describe('AppComponent', () => { trainingCardsService = { initTrainingCards: sinon.stub() }; userSettingsService = { get: sinon.stub().resolves({ facility_id: ['facility'], contact_id: 'contact' }) }; formService = { setUserContext: sinon.stub() }; + updateServiceWorkerService = { update: sinon.stub() }; consoleErrorStub = sinon.stub(console, 'error'); const mockedSelectors = [ @@ -203,7 +210,7 @@ describe('AppComponent', () => { { provide: AuthService, useValue: authService }, { provide: ResourceIconsService, useValue: resourceIconsService }, { provide: ChangesService, useValue: changesService }, - { provide: UpdateServiceWorkerService, useValue: {} }, + { provide: UpdateServiceWorkerService, useValue: updateServiceWorkerService }, { provide: LocationService, useValue: locationService }, { provide: ModalService, useValue: modalService }, { provide: BrowserDetectorService, useValue: browserDetectorService}, @@ -282,6 +289,19 @@ describe('AppComponent', () => { expect(userSettingsService.get.calledOnce).to.equal(true); expect(globalActions.setUserFacilityIds.calledOnceWith(['facility'])).to.equal(true); expect(globalActions.setUserContactId.calledOnceWith('contact')).to.equal(true); + expect(updateServiceWorkerService.update.callCount).to.equal(1); + }); + + it('should show reload popup when service worker is updated', async () => { + await getComponent(); + await component.setupPromise; + + expect(updateServiceWorkerService.update.callCount).to.equal(1); + const callback = updateServiceWorkerService.update.args[0][0]; + callback(); + expect(modalService.show.calledOnce).to.be.true; + expect(modalService.show.args[0]).to.have.deep.members([ReloadingComponent]); + }); it('should display browser compatibility modal if using outdated chrome browser', async () => {