diff --git a/tests/e2e/default/enketo/training-cards.wdio-spec.js b/tests/e2e/default/enketo/training-cards.wdio-spec.js index c7005b80e1f..fec01db6f5b 100644 --- a/tests/e2e/default/enketo/training-cards.wdio-spec.js +++ b/tests/e2e/default/enketo/training-cards.wdio-spec.js @@ -18,6 +18,12 @@ describe('Training Cards', () => { const formDocId = 'training:training-cards-text-only'; + const setLastViewedDateInThePast = () => { + return browser.execute(function() { + this.localStorage.setItem('training-cards-last-viewed-date-user1', new Date('2024-10-05 20:10:05').toISOString()); + }); + }; + before(async () => { const parent = placeFactory.place().build({ _id: 'dist1', type: 'district_hospital' }); const user = userFactory.build({ roles: [ 'nurse', 'chw' ] }); @@ -52,9 +58,8 @@ describe('Training Cards', () => { expect(await reportsPage.leftPanelSelectors.allReports()).to.be.empty; }); - it('should display training after it was canceled and the training doc was updated', async () => { - await commonPage.goToMessages(); - await commonElements.waitForPageLoaded(); + it('should not display training after it was canceled and the training doc was updated', async () => { + await setLastViewedDateInThePast(); // Unfinished trainings should appear again after reload. await browser.refresh(); await trainingCardsPage.waitForTrainingCards(); @@ -73,12 +78,7 @@ describe('Training Cards', () => { const updatedTrainingForm = await utils.getDoc(`form:${formDocId}`); expect(updatedTrainingForm.context.duration).to.equal(10); - await trainingCardsPage.waitForTrainingCards(); - const context = 'training_cards_text_only'; - const introCard = await trainingCardsPage.getCardContent(context, 'intro/intro_note_1:label"]'); - expect(introCard).to.equal( - 'There have been some changes to icons in your app. The next few screens will show you the difference.' - ); + await trainingCardsPage.checkTrainingCardIsNotDisplayed(); }); it('should display training after privacy policy', async () => { @@ -86,6 +86,7 @@ describe('Training Cards', () => { await utils.saveDocs([privacyPolicy]); await commonPage.goToReports(); await commonElements.sync(); + await setLastViewedDateInThePast(); await browser.refresh(); await trainingCardsPage.checkTrainingCardIsNotDisplayed(); @@ -97,6 +98,7 @@ describe('Training Cards', () => { it('should display training after reload and complete training', async () => { await commonPage.goToMessages(); await commonElements.waitForPageLoaded(); + await setLastViewedDateInThePast(); // Unfinished trainings should appear again after reload. await browser.refresh(); await trainingCardsPage.waitForTrainingCards(); diff --git a/webapp/src/ts/app.component.ts b/webapp/src/ts/app.component.ts index 2b665e40e14..6716a0e5b39 100644 --- a/webapp/src/ts/app.component.ts +++ b/webapp/src/ts/app.component.ts @@ -501,10 +501,7 @@ export class AppComponent implements OnInit, AfterViewInit { } private displayTrainingCards() { - if (this.showPrivacyPolicy && !this.privacyPolicyAccepted) { - return; - } - if (!this.trainingCardFormId) { + if (!this.trainingCardFormId || (this.showPrivacyPolicy && !this.privacyPolicyAccepted)) { return; } diff --git a/webapp/src/ts/services/training-cards.service.ts b/webapp/src/ts/services/training-cards.service.ts index 1a4358f7a89..dae1b27ff6c 100644 --- a/webapp/src/ts/services/training-cards.service.ts +++ b/webapp/src/ts/services/training-cards.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import * as moment from 'moment'; import { v4 as uuid } from 'uuid'; +import { first } from 'rxjs/operators'; import { XmlFormsService } from '@mm-services/xml-forms.service'; import { TrainingCardsComponent } from '@mm-modals/training-cards/training-cards.component'; @@ -17,7 +18,8 @@ export const TRAINING_PREFIX: string = 'training:'; providedIn: 'root' }) export class TrainingCardsService { - private globalActions; + private readonly globalActions: GlobalActions; + private readonly STORAGE_KEY_LAST_VIEWED_DATE = 'training-cards-last-viewed-date'; constructor( private store: Store, @@ -27,7 +29,7 @@ export class TrainingCardsService { private sessionService: SessionService, private routeSnapshotService: RouteSnapshotService, ) { - this.globalActions = new GlobalActions(store); + this.globalActions = new GlobalActions(this.store); } private getAvailableTrainingCards(xForms, userCtx) { @@ -98,11 +100,25 @@ export class TrainingCardsService { } displayTrainingCards() { + if (this.hasBeenDisplayed()) { + return; + } + const routeSnapshot = this.routeSnapshotService.get(); if (routeSnapshot?.data?.hideTraining) { return; } - this.modalService.show(TrainingCardsComponent); + + this.modalService + .show(TrainingCardsComponent) + ?.afterOpened() + .pipe(first()) + .subscribe(() => { + const key = this.getLocalStorageKey(); + if (key) { + window.localStorage.setItem(key, new Date().toISOString()); + } + }); } private async getFirstChronologicalForm(xForms) { @@ -138,4 +154,32 @@ export class TrainingCardsService { } return `${TRAINING_PREFIX}${userName}:${uuid()}`; } + + private hasBeenDisplayed() { + const key = this.getLocalStorageKey(); + if (!key) { + return false; + } + + const dateString = window.localStorage.getItem(key) ?? ''; + const lastViewedDate = new Date(dateString); + if (isNaN(lastViewedDate.getTime())) { + return false; + } + + lastViewedDate.setHours(0, 0, 0, 0); + const today = new Date(); + today.setHours(0, 0, 0, 0); + + return lastViewedDate >= today; + } + + private getLocalStorageKey() { + const username = this.sessionService.userCtx()?.name; + if (!username) { + return; + } + + return `${this.STORAGE_KEY_LAST_VIEWED_DATE}-${username}`; + } } diff --git a/webapp/tests/karma/ts/services/training-cards.service.spec.ts b/webapp/tests/karma/ts/services/training-cards.service.spec.ts index dd590478f9a..36d40557a32 100644 --- a/webapp/tests/karma/ts/services/training-cards.service.spec.ts +++ b/webapp/tests/karma/ts/services/training-cards.service.spec.ts @@ -771,4 +771,52 @@ describe('TrainingCardsService', () => { .to.equal('Training Cards :: Cannot create document ID, user context does not have the "name" property.'); } }); + + describe('Display training cards once', () => { + afterEach(() => window.localStorage.removeItem('training-cards-last-viewed-date')); + + it('should display training when it has not been displayed today', async () => { + routeSnapshotService.get.returns({ data: { hideTraining: false } }); + sessionService.userCtx.returns({ name: 'ronald' }); + window.localStorage.setItem('training-cards-last-viewed-date-ronald', '2024-05-23 20:29:25'); + clock = sinon.useFakeTimers(new Date('2025-05-25 20:29:25')); + + service.displayTrainingCards(); + + expect(modalService.show.calledOnce).to.be.true; + }); + + it('should display training when last viewed date is empty', async () => { + routeSnapshotService.get.returns({ data: { hideTraining: false } }); + window.localStorage.setItem('training-cards-last-viewed-date-ronald', ''); + clock = sinon.useFakeTimers(new Date('2025-05-25 20:29:25')); + + service.displayTrainingCards(); + + expect(modalService.show.calledOnce).to.be.true; + }); + + it('should not display training when it has been displayed today for the same user', async () => { + routeSnapshotService.get.returns({ data: { hideTraining: false } }); + sessionService.userCtx.returns({ name: 'ronald' }); + window.localStorage.setItem('training-cards-last-viewed-date-ronald', '2024-05-23 20:29:25'); + clock = sinon.useFakeTimers(new Date('2024-05-23 06:29:25')); + + service.displayTrainingCards(); + + expect(modalService.show.notCalled).to.be.true; + }); + + it('should display training when it has not been displayed for a different user', async () => { + routeSnapshotService.get.returns({ data: { hideTraining: false } }); + sessionService.userCtx.returns({ name: 'sarah' }); + window.localStorage.setItem('training-cards-last-viewed-date-ronald', '2024-05-23 20:29:25'); + clock = sinon.useFakeTimers(new Date('2024-05-23 06:29:25')); + + service.displayTrainingCards(); + + expect(modalService.show.calledOnce).to.be.true; + }); + }); + });