From b1fa54be186178a6a32fb04c23ac60dc8f489cbc Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 22 Jan 2024 16:25:14 +0000 Subject: [PATCH 01/14] spike: task service spec tests configuration --- angular.json | 1 + .../services/template-field.service.spec.ts | 33 +++++++++++++++ .../app-config/app-config.service.spec.ts | 12 ++++++ .../shared/services/task/task.service.spec.ts | 42 ++++++++++++++++++- src/test.ts | 12 ++++++ tsconfig.spec.json | 3 +- 6 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 src/app/shared/components/template/services/template-field.service.spec.ts create mode 100644 src/test.ts diff --git a/angular.json b/angular.json index da21949633..c0b238df7c 100644 --- a/angular.json +++ b/angular.json @@ -163,6 +163,7 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { + "main": "src/test.ts", "karmaConfig": "karma.conf.js", "polyfills": ["zone.js", "zone.js/testing"], "tsConfig": "tsconfig.spec.json", diff --git a/src/app/shared/components/template/services/template-field.service.spec.ts b/src/app/shared/components/template/services/template-field.service.spec.ts new file mode 100644 index 0000000000..e9e05ab474 --- /dev/null +++ b/src/app/shared/components/template/services/template-field.service.spec.ts @@ -0,0 +1,33 @@ +import { TestBed } from "@angular/core/testing"; +import { TemplateFieldService } from "./template-field.service"; +import { PromiseExtended } from "dexie"; + +/** Mock calls for field values from the template field service to return test data */ +export class MockTemplateFieldService implements Partial { + mockFields: any; + + // allow additional specs implementing service to provide their own fields + constructor(mockFields: any = {}) { + this.mockFields = mockFields; + } + public getField(key: string) { + return this.mockFields[key]; + } + public setField(key: string, value: string) { + this.mockFields[key] = value; + return new Promise(() => {}) as PromiseExtended; + } +} + +describe("TaskService", () => { + let service: TemplateFieldService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(TemplateFieldService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/shared/services/app-config/app-config.service.spec.ts b/src/app/shared/services/app-config/app-config.service.spec.ts index 65647e583e..fe0d1a4757 100644 --- a/src/app/shared/services/app-config/app-config.service.spec.ts +++ b/src/app/shared/services/app-config/app-config.service.spec.ts @@ -1,6 +1,18 @@ import { TestBed } from "@angular/core/testing"; import { AppConfigService } from "./app-config.service"; +import { BehaviorSubject } from "rxjs/internal/BehaviorSubject"; +import { IAppConfig } from "../../model"; + +/** Mock calls for field values from the template field service to return test data */ +export class MockAppConfigService implements Partial { + appConfig$ = new BehaviorSubject(undefined as any); + + // allow additional specs implementing service to provide their own partial appConfig + constructor(mockAppConfig: Partial = {}) { + this.appConfig$.next(mockAppConfig as any); + } +} describe("AppConfigService", () => { let service: AppConfigService; diff --git a/src/app/shared/services/task/task.service.spec.ts b/src/app/shared/services/task/task.service.spec.ts index 9ebba70395..de9cdfc3d0 100644 --- a/src/app/shared/services/task/task.service.spec.ts +++ b/src/app/shared/services/task/task.service.spec.ts @@ -1,16 +1,56 @@ import { TestBed } from "@angular/core/testing"; import { TaskService } from "./task.service"; +import { TemplateFieldService } from "../../components/template/services/template-field.service"; +import { MockTemplateFieldService } from "../../components/template/services/template-field.service.spec"; +import { AppConfigService } from "../app-config/app-config.service"; +import { MockAppConfigService } from "../app-config/app-config.service.spec"; +import { IAppConfig } from "../../model"; +// This must match the corresponding vallue in the deployment config, if the default value is overridden +const highlightedTaskField = "_task_highlighted_group_id" as Partial< + IAppConfig["TASKS"]["highlightedTaskField"] +>; + +const MOCK_FIELDS = { + [highlightedTaskField]: "task_a", +}; + +const MOCK_CONFIG = { + TASKS: { + highlightedTaskField: "_task_highlighted_group_id", + }, +} as Partial; + +/** + * Call standalone tests via: + * yarn ng test --include src/app/shared/services/task/task.service.spec.ts + */ describe("TaskService", () => { let service: TaskService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + providers: [ + TaskService, + { + provide: TemplateFieldService, + useValue: new MockTemplateFieldService(MOCK_FIELDS), + }, + { + provide: AppConfigService, + useValue: new MockAppConfigService(MOCK_CONFIG), + }, + ], + }); service = TestBed.inject(TaskService); }); it("should be created", () => { expect(service).toBeTruthy(); }); + + it("should return the name of the current highlighted task", () => { + expect(service.getHighlightedTaskGroup()).toBe("task_a"); + }); }); diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 0000000000..9c7b99c448 --- /dev/null +++ b/src/test.ts @@ -0,0 +1,12 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import "zone.js/testing"; + +import { getTestBed } from "@angular/core/testing"; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from "@angular/platform-browser-dynamic/testing"; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 47e3dd7551..a04061ad70 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -5,5 +5,6 @@ "outDir": "./out-tsc/spec", "types": ["jasmine"] }, - "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"], + "files": ["src/test.ts"] } From 9352815144c4e1b556e54ec66873cbe98b0ec90b Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 29 Jan 2024 11:31:44 +0000 Subject: [PATCH 02/14] test: fix unit testing infrastructure to support imports from data-models; WIP test configuration for task service --- angular.json | 1 - .../services/template-field.service.ts | 7 ++- .../template-translate.service.spec.ts | 22 ++++++++ .../services/data/app-data.service.spec.ts | 8 +++ .../shared/services/data/app-data.service.ts | 7 ++- .../shared/services/task/task.service.spec.ts | 54 +++++++++++++++++-- src/app/shared/services/task/task.service.ts | 2 +- src/test.ts | 12 ----- tsconfig.spec.json | 12 ++++- 9 files changed, 104 insertions(+), 21 deletions(-) create mode 100644 src/app/shared/components/template/services/template-translate.service.spec.ts delete mode 100644 src/test.ts diff --git a/angular.json b/angular.json index c0b238df7c..da21949633 100644 --- a/angular.json +++ b/angular.json @@ -163,7 +163,6 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "main": "src/test.ts", "karmaConfig": "karma.conf.js", "polyfills": ["zone.js", "zone.js/testing"], "tsConfig": "tsconfig.spec.json", diff --git a/src/app/shared/components/template/services/template-field.service.ts b/src/app/shared/components/template/services/template-field.service.ts index 9750e2c76b..0b9a08dd07 100644 --- a/src/app/shared/components/template/services/template-field.service.ts +++ b/src/app/shared/components/template/services/template-field.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@angular/core"; +import { Injectable, InjectionToken } from "@angular/core"; import { IFlowEvent } from "data-models"; import { FlowTypes } from "data-models"; import { DbService } from "src/app/shared/services/db/db.service"; @@ -110,3 +110,8 @@ export class TemplateFieldService extends AsyncServiceBase { this.globals[row.name] = row; } } + +// For testing, provide an injection token +export const TEMPLATE_FIELD_SERVICE_TOKEN = new InjectionToken( + "TemplateFieldService" +); diff --git a/src/app/shared/components/template/services/template-translate.service.spec.ts b/src/app/shared/components/template/services/template-translate.service.spec.ts new file mode 100644 index 0000000000..227a7abd89 --- /dev/null +++ b/src/app/shared/components/template/services/template-translate.service.spec.ts @@ -0,0 +1,22 @@ +import { TestBed } from "@angular/core/testing"; +import { TemplateTranslateService } from "./template-translate.service"; + +/** Mock calls for field values from the template field service to return test data */ +export class MockTemplateTranslateService implements Partial { + public translateValue(value: string) { + return value; + } +} + +describe("TaskService", () => { + let service: TemplateTranslateService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(TemplateTranslateService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/shared/services/data/app-data.service.spec.ts b/src/app/shared/services/data/app-data.service.spec.ts index a4986484c2..8394190d5d 100644 --- a/src/app/shared/services/data/app-data.service.spec.ts +++ b/src/app/shared/services/data/app-data.service.spec.ts @@ -104,6 +104,11 @@ export class MockAppDataService implements Partial { constructor(mockData: Partial = {}) { this.appDataCache = { ...DATA_CACHE_CLEAN, ...mockData }; } + + public ready() { + return true; + } + public async getSheet( flow_type: FlowTypes.FlowType, flow_name: string @@ -111,6 +116,9 @@ export class MockAppDataService implements Partial { await _wait(50); return this.appDataCache[flow_type]?.[flow_name] as T; } + public async getTranslationStrings(language_code: string) { + return {}; + } } /** Use an extended service for testing to allow override of protected variables */ diff --git a/src/app/shared/services/data/app-data.service.ts b/src/app/shared/services/data/app-data.service.ts index dd6a3dc595..09f4b31af2 100644 --- a/src/app/shared/services/data/app-data.service.ts +++ b/src/app/shared/services/data/app-data.service.ts @@ -1,5 +1,5 @@ import { HttpClient } from "@angular/common/http"; -import { Injectable } from "@angular/core"; +import { Injectable, InjectionToken } from "@angular/core"; import { SHEETS_CONTENT_LIST, TRANSLATIONS_CONTENT_LIST } from "src/app/data/app-data"; import { lastValueFrom } from "rxjs"; import { FlowTypes } from "../../model"; @@ -109,6 +109,8 @@ export class AppDataService extends SyncServiceBase { } // Populate flow from cache if exists, or load json if it does not let flow = this.appDataCache[flow_type][flow_name]; + console.log("***flow_name", flow_name); + console.log("***flow", flow); if (!flow) { flow = await this.loadSheetFromJson(flowContents); this.addFlowToCache(flow); @@ -196,3 +198,6 @@ export class AppDataService extends SyncServiceBase { export type IAppDataCache = { [flow_type in FlowTypes.FlowType]: { [flow_name: string]: FlowTypes.FlowTypeWithData }; }; + +// For testing, provide an injection token +export const APP_DATA_SERVICE_TOKEN = new InjectionToken("AppDataService"); diff --git a/src/app/shared/services/task/task.service.spec.ts b/src/app/shared/services/task/task.service.spec.ts index de9cdfc3d0..e5e339e236 100644 --- a/src/app/shared/services/task/task.service.spec.ts +++ b/src/app/shared/services/task/task.service.spec.ts @@ -1,11 +1,19 @@ import { TestBed } from "@angular/core/testing"; import { TaskService } from "./task.service"; -import { TemplateFieldService } from "../../components/template/services/template-field.service"; +import { + TemplateFieldService, + TEMPLATE_FIELD_SERVICE_TOKEN, +} from "../../components/template/services/template-field.service"; +import { HttpClientTestingModule } from "@angular/common/http/testing"; import { MockTemplateFieldService } from "../../components/template/services/template-field.service.spec"; import { AppConfigService } from "../app-config/app-config.service"; import { MockAppConfigService } from "../app-config/app-config.service.spec"; import { IAppConfig } from "../../model"; +import { AppDataService, IAppDataCache, APP_DATA_SERVICE_TOKEN } from "../data/app-data.service"; +import { MockAppDataService } from "../data/app-data.service.spec"; +import { TemplateTranslateService } from "../../components/template/services/template-translate.service"; +import { MockTemplateTranslateService } from "../../components/template/services/template-translate.service.spec"; // This must match the corresponding vallue in the deployment config, if the default value is overridden const highlightedTaskField = "_task_highlighted_group_id" as Partial< @@ -19,9 +27,40 @@ const MOCK_FIELDS = { const MOCK_CONFIG = { TASKS: { highlightedTaskField: "_task_highlighted_group_id", + taskGroupsListName: "mock_task_groups_data", }, } as Partial; +const MOCK_DATA: Partial = { + data_list: { + mock_task_groups_data: { + flow_type: "data_list", + flow_name: "mock_task_groups_data", + data_list_name: "feat_task_groups", + rows: [ + { + id: "task_group_1", + number: 1, + title: "Task Group 1", + completed_field: "task_group_1_completed", + }, + { + id: "task_group_2", + number: 3, + title: "Task Group 2", + completed_field: "task_group_2_completed", + }, + { + id: "task_group_3", + number: 2, + title: "Task Group 3", + completed_field: "task_group_3_completed", + }, + ], + }, + }, +}; + /** * Call standalone tests via: * yarn ng test --include src/app/shared/services/task/task.service.spec.ts @@ -31,15 +70,24 @@ describe("TaskService", () => { beforeEach(() => { TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], providers: [ TaskService, + { + provide: AppConfigService, + useValue: new MockAppConfigService(MOCK_CONFIG), + }, { provide: TemplateFieldService, useValue: new MockTemplateFieldService(MOCK_FIELDS), }, { - provide: AppConfigService, - useValue: new MockAppConfigService(MOCK_CONFIG), + provide: TemplateTranslateService, + useValue: new MockTemplateTranslateService(), + }, + { + provide: AppDataService, + useValue: new MockAppDataService(MOCK_DATA), }, ], }); diff --git a/src/app/shared/services/task/task.service.ts b/src/app/shared/services/task/task.service.ts index 6bdfbcd1f3..7a44e02818 100644 --- a/src/app/shared/services/task/task.service.ts +++ b/src/app/shared/services/task/task.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@angular/core"; +import { Inject, Injectable } from "@angular/core"; import { TemplateFieldService } from "../../components/template/services/template-field.service"; import { AppDataService } from "../data/app-data.service"; import { arrayToHashmap } from "../../utils"; diff --git a/src/test.ts b/src/test.ts deleted file mode 100644 index 9c7b99c448..0000000000 --- a/src/test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import "zone.js/testing"; - -import { getTestBed } from "@angular/core/testing"; -import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting, -} from "@angular/platform-browser-dynamic/testing"; - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); diff --git a/tsconfig.spec.json b/tsconfig.spec.json index a04061ad70..f8459c6a55 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -5,6 +5,14 @@ "outDir": "./out-tsc/spec", "types": ["jasmine"] }, - "include": ["src/**/*.spec.ts", "src/**/*.d.ts"], - "files": ["src/test.ts"] + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts", + // Ensure odk-form components incluided to fix console error + "src/app/shared/components/template/components/odk-form/libs/**/*.ts", + // Ensure any ts-based packages used by frontend app are listed here + // to allow import/live-reload within angular compiler + "packages/data-models/**/*.ts", + "packages/shared/**/*.ts" + ] } From 53956508a815aa62c5a7b0b0e6ff888929e86b95 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 29 Jan 2024 11:33:32 +0000 Subject: [PATCH 03/14] code tidy --- src/app/shared/services/data/app-data.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/shared/services/data/app-data.service.ts b/src/app/shared/services/data/app-data.service.ts index 09f4b31af2..dc5984c2e7 100644 --- a/src/app/shared/services/data/app-data.service.ts +++ b/src/app/shared/services/data/app-data.service.ts @@ -109,8 +109,6 @@ export class AppDataService extends SyncServiceBase { } // Populate flow from cache if exists, or load json if it does not let flow = this.appDataCache[flow_type][flow_name]; - console.log("***flow_name", flow_name); - console.log("***flow", flow); if (!flow) { flow = await this.loadSheetFromJson(flowContents); this.addFlowToCache(flow); From 3cf628fe020eaf59475cd8f0259d6b58000e4dca Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 29 Jan 2024 10:52:49 -0800 Subject: [PATCH 04/14] feat: add test polyfills --- angular.json | 2 +- src/test/polyfills.ts | 10 ++++++++++ tsconfig.spec.json | 4 +++- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 src/test/polyfills.ts diff --git a/angular.json b/angular.json index da21949633..ca278890c6 100644 --- a/angular.json +++ b/angular.json @@ -164,7 +164,7 @@ "builder": "@angular-devkit/build-angular:karma", "options": { "karmaConfig": "karma.conf.js", - "polyfills": ["zone.js", "zone.js/testing"], + "polyfills": ["zone.js", "zone.js/testing", "src/test/polyfills.ts"], "tsConfig": "tsconfig.spec.json", "inlineStyleLanguage": "scss", "assets": ["src/favicon.ico", "src/assets"], diff --git a/src/test/polyfills.ts b/src/test/polyfills.ts new file mode 100644 index 0000000000..63a3b80ce6 --- /dev/null +++ b/src/test/polyfills.ts @@ -0,0 +1,10 @@ +// Additional polyfills used only during testing + +// This file must be imported into angular.json test polyfills and tsconfig.spec.ts +// https://stackoverflow.com/questions/51781014/angular-6-ng-test-library-with-ie-polyfills + +// Include core-js polyfill to fix httpTestingModule injection within nested services +// https://github.com/angular/angular/issues/21440 +// https://stackoverflow.com/questions/55398923/error-cant-resolve-core-js-es7-reflect-in-node-modules-angular-devkit-bui + +import "core-js/proposals/reflect-metadata"; diff --git a/tsconfig.spec.json b/tsconfig.spec.json index f8459c6a55..de6b654a75 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -5,10 +5,12 @@ "outDir": "./out-tsc/spec", "types": ["jasmine"] }, + "files": [], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts", - // Ensure odk-form components incluided to fix console error + "src/test/**/*.ts", + // Ensure odk-form components included to fix console error "src/app/shared/components/template/components/odk-form/libs/**/*.ts", // Ensure any ts-based packages used by frontend app are listed here // to allow import/live-reload within angular compiler From befc47a7e203668e374ca29f195934b909de78a6 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 29 Jan 2024 10:53:04 -0800 Subject: [PATCH 05/14] chore: deprecate test polyfills --- src/test/polyfills.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/polyfills.ts b/src/test/polyfills.ts index 63a3b80ce6..db6f3ec061 100644 --- a/src/test/polyfills.ts +++ b/src/test/polyfills.ts @@ -7,4 +7,6 @@ // https://github.com/angular/angular/issues/21440 // https://stackoverflow.com/questions/55398923/error-cant-resolve-core-js-es7-reflect-in-node-modules-angular-devkit-bui -import "core-js/proposals/reflect-metadata"; +// CC NOTE - No longer required but keeping file in case needed in future + +// import "core-js/proposals/reflect-metadata"; From d8f87f82764636a007bc165f8b5066a4e2130134 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 29 Jan 2024 10:53:35 -0800 Subject: [PATCH 06/14] chore: add mock campaign service --- src/app/feature/campaign/campaign.service.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/app/feature/campaign/campaign.service.spec.ts diff --git a/src/app/feature/campaign/campaign.service.spec.ts b/src/app/feature/campaign/campaign.service.spec.ts new file mode 100644 index 0000000000..d76fdcbdee --- /dev/null +++ b/src/app/feature/campaign/campaign.service.spec.ts @@ -0,0 +1,8 @@ +import { CampaignService } from "./campaign.service"; + +export class MockCampaignService implements Partial { + public async ready(timeoutValue?: number): Promise { + return true; + } + // TODO - implement further methods +} From 27065140afab1491281e06dce08fe20c20b53a18 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 29 Jan 2024 10:54:53 -0800 Subject: [PATCH 07/14] fix: template field service spec --- .../template/services/template-field.service.spec.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/app/shared/components/template/services/template-field.service.spec.ts b/src/app/shared/components/template/services/template-field.service.spec.ts index e9e05ab474..d2284c8bfc 100644 --- a/src/app/shared/components/template/services/template-field.service.spec.ts +++ b/src/app/shared/components/template/services/template-field.service.spec.ts @@ -1,6 +1,7 @@ +import { HttpClientTestingModule } from "@angular/common/http/testing"; import { TestBed } from "@angular/core/testing"; import { TemplateFieldService } from "./template-field.service"; -import { PromiseExtended } from "dexie"; +import type { PromiseExtended } from "dexie"; /** Mock calls for field values from the template field service to return test data */ export class MockTemplateFieldService implements Partial { @@ -17,13 +18,18 @@ export class MockTemplateFieldService implements Partial { this.mockFields[key] = value; return new Promise(() => {}) as PromiseExtended; } + public async ready(timeoutValue?: number): Promise { + return true; + } } -describe("TaskService", () => { +describe("TemplateFieldService", () => { let service: TemplateFieldService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); service = TestBed.inject(TemplateFieldService); }); From d992b86e0420a63b31471c7c2e1eaa114df8384b Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 29 Jan 2024 11:35:05 -0800 Subject: [PATCH 08/14] fix: mock service ready --- .../template/services/template-translate.service.spec.ts | 4 ++++ src/app/shared/services/app-config/app-config.service.spec.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/app/shared/components/template/services/template-translate.service.spec.ts b/src/app/shared/components/template/services/template-translate.service.spec.ts index 227a7abd89..e293081bfa 100644 --- a/src/app/shared/components/template/services/template-translate.service.spec.ts +++ b/src/app/shared/components/template/services/template-translate.service.spec.ts @@ -6,6 +6,10 @@ export class MockTemplateTranslateService implements Partial { + return true; + } } describe("TaskService", () => { diff --git a/src/app/shared/services/app-config/app-config.service.spec.ts b/src/app/shared/services/app-config/app-config.service.spec.ts index fe0d1a4757..928c7d276b 100644 --- a/src/app/shared/services/app-config/app-config.service.spec.ts +++ b/src/app/shared/services/app-config/app-config.service.spec.ts @@ -12,6 +12,10 @@ export class MockAppConfigService implements Partial { constructor(mockAppConfig: Partial = {}) { this.appConfig$.next(mockAppConfig as any); } + + public ready(timeoutValue?: number) { + return true; + } } describe("AppConfigService", () => { From b570b6c9c71dfe7c4c4f6e9693f9569994c6821f Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 29 Jan 2024 11:35:20 -0800 Subject: [PATCH 09/14] chore: code tidying --- src/app/shared/services/task/task.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/services/task/task.service.ts b/src/app/shared/services/task/task.service.ts index 7a44e02818..1ab0ec35bc 100644 --- a/src/app/shared/services/task/task.service.ts +++ b/src/app/shared/services/task/task.service.ts @@ -1,11 +1,11 @@ -import { Inject, Injectable } from "@angular/core"; +import { Injectable } from "@angular/core"; import { TemplateFieldService } from "../../components/template/services/template-field.service"; import { AppDataService } from "../data/app-data.service"; import { arrayToHashmap } from "../../utils"; import { AsyncServiceBase } from "../asyncService.base"; import { AppConfigService } from "../app-config/app-config.service"; import { IAppConfig } from "../../model"; -import { CampaignService } from "src/app/feature/campaign/campaign.service"; +import { CampaignService } from "../../../feature/campaign/campaign.service"; export type IProgressStatus = "notStarted" | "inProgress" | "completed"; From 65c098aa65940e8699bc86f0e4d3714b538fb7b9 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 29 Jan 2024 11:35:45 -0800 Subject: [PATCH 10/14] fix: task service spec tests --- .../shared/services/task/task.service.spec.ts | 69 ++++++++++++------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/src/app/shared/services/task/task.service.spec.ts b/src/app/shared/services/task/task.service.spec.ts index e5e339e236..3bc2a4fee0 100644 --- a/src/app/shared/services/task/task.service.spec.ts +++ b/src/app/shared/services/task/task.service.spec.ts @@ -1,21 +1,20 @@ import { TestBed } from "@angular/core/testing"; +import { HttpClientTestingModule } from "@angular/common/http/testing"; +import { IAppConfig } from "../../model"; import { TaskService } from "./task.service"; -import { - TemplateFieldService, - TEMPLATE_FIELD_SERVICE_TOKEN, -} from "../../components/template/services/template-field.service"; -import { HttpClientTestingModule } from "@angular/common/http/testing"; + +// Mock Services import { MockTemplateFieldService } from "../../components/template/services/template-field.service.spec"; -import { AppConfigService } from "../app-config/app-config.service"; import { MockAppConfigService } from "../app-config/app-config.service.spec"; -import { IAppConfig } from "../../model"; -import { AppDataService, IAppDataCache, APP_DATA_SERVICE_TOKEN } from "../data/app-data.service"; import { MockAppDataService } from "../data/app-data.service.spec"; -import { TemplateTranslateService } from "../../components/template/services/template-translate.service"; -import { MockTemplateTranslateService } from "../../components/template/services/template-translate.service.spec"; +// Mocked Services +import { AppDataService, IAppDataCache } from "../data/app-data.service"; +import { AppConfigService } from "../app-config/app-config.service"; +import { CampaignService } from "../../../feature/campaign/campaign.service"; +import { TemplateFieldService } from "../../components/template/services/template-field.service"; -// This must match the corresponding vallue in the deployment config, if the default value is overridden +// This must match the corresponding value in the deployment config, if the default value is overridden const highlightedTaskField = "_task_highlighted_group_id" as Partial< IAppConfig["TASKS"]["highlightedTaskField"] >; @@ -28,6 +27,7 @@ const MOCK_CONFIG = { TASKS: { highlightedTaskField: "_task_highlighted_group_id", taskGroupsListName: "mock_task_groups_data", + enabled: true, }, } as Partial; @@ -67,27 +67,31 @@ const MOCK_DATA: Partial = { */ describe("TaskService", () => { let service: TaskService; + let scheduleCampaignNotificationsSpy: jasmine.Spy; - beforeEach(() => { + beforeEach(async () => { + scheduleCampaignNotificationsSpy = jasmine.createSpy(); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ - TaskService, { - provide: AppConfigService, - useValue: new MockAppConfigService(MOCK_CONFIG), + provide: TemplateFieldService, + useValue: new MockTemplateFieldService({ highlightedTaskField }), }, { - provide: TemplateFieldService, - useValue: new MockTemplateFieldService(MOCK_FIELDS), + provide: AppDataService, + useValue: new MockAppDataService(MOCK_DATA), }, { - provide: TemplateTranslateService, - useValue: new MockTemplateTranslateService(), + provide: AppConfigService, + useValue: new MockAppConfigService(MOCK_CONFIG), }, + // Mock single method from campaign service called { - provide: AppDataService, - useValue: new MockAppDataService(MOCK_DATA), + provide: CampaignService, + useValue: { + scheduleCampaignNotifications: scheduleCampaignNotificationsSpy, + }, }, ], }); @@ -97,8 +101,27 @@ describe("TaskService", () => { it("should be created", () => { expect(service).toBeTruthy(); }); + it("enables service via app config", async () => { + await service.ready(); + expect(service.tasksFeatureEnabled).toEqual(true); + }); - it("should return the name of the current highlighted task", () => { - expect(service.getHighlightedTaskGroup()).toBe("task_a"); + it("should return the name of the current highlighted task", async () => { + await service.ready(); + expect(service.getHighlightedTaskGroup()).toBe("task_group_1"); }); + + // TODO - test if campaign service mock functions as intended + + // fit("schedules campaign notifications", async () => { + // await service.ready(); + // await service.evaluateTaskGroupData(MOCK_DATA.data_list.mock_task_groups_data.rows, { + // completedColumnName: "", + // completedField: "", + // completedFieldColumnName: "", + // dataListName: "", + // useDynamicData: false, + // }); + // expect(scheduleCampaignNotificationsSpy).toHaveBeenCalledTimes(1); + // }); }); From 40649730ab2cb8cb32b67d9abbc46321030a3ab0 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 2 Feb 2024 17:11:57 +0000 Subject: [PATCH 11/14] wip: task service spec tests --- .../shared/services/task/task.service.spec.ts | 84 ++++++++++++++----- 1 file changed, 63 insertions(+), 21 deletions(-) diff --git a/src/app/shared/services/task/task.service.spec.ts b/src/app/shared/services/task/task.service.spec.ts index 3bc2a4fee0..444b98280d 100644 --- a/src/app/shared/services/task/task.service.spec.ts +++ b/src/app/shared/services/task/task.service.spec.ts @@ -1,5 +1,6 @@ import { TestBed } from "@angular/core/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; +import clone from "clone"; import { IAppConfig } from "../../model"; import { TaskService } from "./task.service"; @@ -15,18 +16,18 @@ import { CampaignService } from "../../../feature/campaign/campaign.service"; import { TemplateFieldService } from "../../components/template/services/template-field.service"; // This must match the corresponding value in the deployment config, if the default value is overridden -const highlightedTaskField = "_task_highlighted_group_id" as Partial< - IAppConfig["TASKS"]["highlightedTaskField"] ->; +const highlightedTaskFieldName = "_task_highlighted_group_id"; + +const taskGroupsListName = "mock_task_groups_data"; const MOCK_FIELDS = { - [highlightedTaskField]: "task_a", + [highlightedTaskFieldName]: "_placeholder", }; const MOCK_CONFIG = { TASKS: { - highlightedTaskField: "_task_highlighted_group_id", - taskGroupsListName: "mock_task_groups_data", + highlightedTaskField: highlightedTaskFieldName, + taskGroupsListName, enabled: true, }, } as Partial; @@ -35,7 +36,7 @@ const MOCK_DATA: Partial = { data_list: { mock_task_groups_data: { flow_type: "data_list", - flow_name: "mock_task_groups_data", + flow_name: taskGroupsListName, data_list_name: "feat_task_groups", rows: [ { @@ -61,6 +62,9 @@ const MOCK_DATA: Partial = { }, }; +// Define at this namespace to allow tests to call methods to update fields. +let mockTemplateFieldService: MockTemplateFieldService; + /** * Call standalone tests via: * yarn ng test --include src/app/shared/services/task/task.service.spec.ts @@ -71,12 +75,15 @@ describe("TaskService", () => { beforeEach(async () => { scheduleCampaignNotificationsSpy = jasmine.createSpy(); + // Clone MOCK_FIELDS data so that updates do not persist between tests + mockTemplateFieldService = new MockTemplateFieldService(clone(MOCK_FIELDS)); + TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ { provide: TemplateFieldService, - useValue: new MockTemplateFieldService({ highlightedTaskField }), + useValue: mockTemplateFieldService, }, { provide: AppDataService, @@ -105,23 +112,58 @@ describe("TaskService", () => { await service.ready(); expect(service.tasksFeatureEnabled).toEqual(true); }); - - it("should return the name of the current highlighted task", async () => { + it("Initial highlighted task is set to first in list", async () => { await service.ready(); - expect(service.getHighlightedTaskGroup()).toBe("task_group_1"); + expect(service.evaluateHighlightedTaskGroup().newHighlightedTaskGroup).toBe( + MOCK_DATA.data_list[taskGroupsListName].rows[0].id + ); + }); + it("Highlighted task field is updated correctly", async () => { + await service.ready(); + expect(service.getHighlightedTaskGroup()).toBe( + MOCK_DATA.data_list[taskGroupsListName].rows[0].id + ); + }); + it("checking whether a task group is highlighted returns the correct value", async () => { + await service.ready(); + expect( + service.checkHighlightedTaskGroup(MOCK_DATA.data_list[taskGroupsListName].rows[1].id) + ).toBe(false); + expect( + service.checkHighlightedTaskGroup(MOCK_DATA.data_list[taskGroupsListName].rows[0].id) + ).toBe(true); + }); + it("completing a task causes the next highest priority task to be made the highlighted one upon re-evaluation", async () => { + await service.ready(); + mockTemplateFieldService.setField( + MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, + "true" + ); + expect(service.evaluateHighlightedTaskGroup().previousHighlightedTaskGroup).toBe( + MOCK_DATA.data_list[taskGroupsListName].rows[0].id + ); + expect(service.evaluateHighlightedTaskGroup().newHighlightedTaskGroup).toBe( + MOCK_DATA.data_list[taskGroupsListName].rows[2].id + ); }); // TODO - test if campaign service mock functions as intended - - // fit("schedules campaign notifications", async () => { + // it("schedules campaign notifications on change of highlighted task", async () => { + // await service.ready(); + // // Update task group + // // service.setHighlightedTaskGroup(MOCK_DATA.data_list[taskGroupsListName].rows[1].id) + // // await service.evaluateTaskGroupData(MOCK_DATA.data_list.mock_task_groups_data.rows, { + // // completedColumnName: "", + // // completedField: "", + // // completedFieldColumnName: "", + // // dataListName: "", + // // useDynamicData: false, + // // }); + // // expect(scheduleCampaignNotificationsSpy).toHaveBeenCalledTimes(1); + // }); + // TODO: test setHighlightedTaskGroup + // it("setting...", async () => { // await service.ready(); - // await service.evaluateTaskGroupData(MOCK_DATA.data_list.mock_task_groups_data.rows, { - // completedColumnName: "", - // completedField: "", - // completedFieldColumnName: "", - // dataListName: "", - // useDynamicData: false, - // }); - // expect(scheduleCampaignNotificationsSpy).toHaveBeenCalledTimes(1); + // service.setHighlightedTaskGroup(MOCK_DATA.data_list[taskGroupsListName].rows[1].id); // }); }); From d88c6d20cdebc05dd4b571b823cda8e8ec0b287e Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 6 Feb 2024 08:59:51 +0000 Subject: [PATCH 12/14] test: unit tests for task service --- .../shared/services/task/task.service.spec.ts | 53 ++++++++++--------- src/app/shared/services/task/task.service.ts | 14 ++--- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/app/shared/services/task/task.service.spec.ts b/src/app/shared/services/task/task.service.spec.ts index 444b98280d..ddc6c6c358 100644 --- a/src/app/shared/services/task/task.service.spec.ts +++ b/src/app/shared/services/task/task.service.spec.ts @@ -14,6 +14,7 @@ import { AppDataService, IAppDataCache } from "../data/app-data.service"; import { AppConfigService } from "../app-config/app-config.service"; import { CampaignService } from "../../../feature/campaign/campaign.service"; import { TemplateFieldService } from "../../components/template/services/template-field.service"; +import { _wait } from "packages/shared/src/utils/async-utils"; // This must match the corresponding value in the deployment config, if the default value is overridden const highlightedTaskFieldName = "_task_highlighted_group_id"; @@ -97,6 +98,9 @@ describe("TaskService", () => { { provide: CampaignService, useValue: { + ready: async () => { + return true; + }, scheduleCampaignNotifications: scheduleCampaignNotificationsSpy, }, }, @@ -112,15 +116,18 @@ describe("TaskService", () => { await service.ready(); expect(service.tasksFeatureEnabled).toEqual(true); }); - it("Initial highlighted task is set to first in list", async () => { + it("can get highlighted task group (set to highest priority task group on init)", async () => { await service.ready(); - expect(service.evaluateHighlightedTaskGroup().newHighlightedTaskGroup).toBe( + expect(service.getHighlightedTaskGroup()).toBe( MOCK_DATA.data_list[taskGroupsListName].rows[0].id ); }); - it("Highlighted task field is updated correctly", async () => { + it("evaluates highlighted task group correctly after init", async () => { await service.ready(); - expect(service.getHighlightedTaskGroup()).toBe( + expect(service.evaluateHighlightedTaskGroup().previousHighlightedTaskGroup).toBe( + MOCK_DATA.data_list[taskGroupsListName].rows[0].id + ); + expect(service.evaluateHighlightedTaskGroup().newHighlightedTaskGroup).toBe( MOCK_DATA.data_list[taskGroupsListName].rows[0].id ); }); @@ -133,34 +140,32 @@ describe("TaskService", () => { service.checkHighlightedTaskGroup(MOCK_DATA.data_list[taskGroupsListName].rows[0].id) ).toBe(true); }); - it("completing a task causes the next highest priority task to be made the highlighted one upon re-evaluation", async () => { + it("completing the highlighted task causes the next highest priority task to be highlighted upon re-evaluation", async () => { await service.ready(); + // Complete highlighted task by setting its associated completed field to true mockTemplateFieldService.setField( MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, "true" ); - expect(service.evaluateHighlightedTaskGroup().previousHighlightedTaskGroup).toBe( - MOCK_DATA.data_list[taskGroupsListName].rows[0].id - ); - expect(service.evaluateHighlightedTaskGroup().newHighlightedTaskGroup).toBe( - MOCK_DATA.data_list[taskGroupsListName].rows[2].id + const { previousHighlightedTaskGroup, newHighlightedTaskGroup } = + service.evaluateHighlightedTaskGroup(); + expect(previousHighlightedTaskGroup).toBe(MOCK_DATA.data_list[taskGroupsListName].rows[0].id); + expect(newHighlightedTaskGroup).toBe(MOCK_DATA.data_list[taskGroupsListName].rows[2].id); + }); + it("schedules campaign notifications on change of highlighted task", async () => { + await service.ready(); + // Complete highlighted task by setting its associated completed field to true + mockTemplateFieldService.setField( + MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, + "true" ); + service.evaluateHighlightedTaskGroup(); + await _wait(50); + // scheduleCampaignNotifications() should be called once on init (since the highlighted task group changes), + // and again on the evaluation called above + expect(scheduleCampaignNotificationsSpy).toHaveBeenCalledTimes(2); }); - // TODO - test if campaign service mock functions as intended - // it("schedules campaign notifications on change of highlighted task", async () => { - // await service.ready(); - // // Update task group - // // service.setHighlightedTaskGroup(MOCK_DATA.data_list[taskGroupsListName].rows[1].id) - // // await service.evaluateTaskGroupData(MOCK_DATA.data_list.mock_task_groups_data.rows, { - // // completedColumnName: "", - // // completedField: "", - // // completedFieldColumnName: "", - // // dataListName: "", - // // useDynamicData: false, - // // }); - // // expect(scheduleCampaignNotificationsSpy).toHaveBeenCalledTimes(1); - // }); // TODO: test setHighlightedTaskGroup // it("setting...", async () => { // await service.ready(); diff --git a/src/app/shared/services/task/task.service.ts b/src/app/shared/services/task/task.service.ts index 1ab0ec35bc..80c70cf4d6 100644 --- a/src/app/shared/services/task/task.service.ts +++ b/src/app/shared/services/task/task.service.ts @@ -66,6 +66,12 @@ export class TaskService extends AsyncServiceBase { } console.log("[HIGHLIGHTED TASK GROUP] - ", newHighlightedTaskGroup); } + // HACK - reschedule campaign notifications when the highlighted task group has changed, + // in order to handle any that are conditional on the highlighted task group + if (previousHighlightedTaskGroup !== newHighlightedTaskGroup) { + // Doesn't need to be awaited – use .then() to avoid making parent function async + this.campaignService.ready().then(() => this.campaignService.scheduleCampaignNotifications()); + } return { previousHighlightedTaskGroup, newHighlightedTaskGroup }; } @@ -183,13 +189,7 @@ export class TaskService extends AsyncServiceBase { progressStatus = "notStarted"; } } - const { previousHighlightedTaskGroup, newHighlightedTaskGroup } = - this.evaluateHighlightedTaskGroup(); - // HACK - reschedule campaign notifications when the highlighted task group has changed, - // in order to handle any that are conditional on the highlighted task group - if (previousHighlightedTaskGroup !== newHighlightedTaskGroup) { - this.campaignService.scheduleCampaignNotifications(); - } + this.evaluateHighlightedTaskGroup(); return { subtasksTotal, subtasksCompleted, progressStatus, newlyCompleted }; } From 8b9fc5737f1adf5bc29a99e210b7d2a7bbc09bdd Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 7 Feb 2024 14:22:35 +0000 Subject: [PATCH 13/14] test: further tests for task service and minor refactors in response --- .../components/carousel/carousel.component.ts | 28 ++++--- .../instance/template-action.service.ts | 9 -- .../services/template-field.service.spec.ts | 5 +- .../shared/services/task/task.service.spec.ts | 57 ++++++++++--- src/app/shared/services/task/task.service.ts | 84 +++++++++---------- 5 files changed, 106 insertions(+), 77 deletions(-) diff --git a/src/app/shared/components/template/components/carousel/carousel.component.ts b/src/app/shared/components/template/components/carousel/carousel.component.ts index 9746696a2d..40617fed8d 100644 --- a/src/app/shared/components/template/components/carousel/carousel.component.ts +++ b/src/app/shared/components/template/components/carousel/carousel.component.ts @@ -18,6 +18,7 @@ export class TmplCarouselComponent extends TemplateBaseComponent implements OnIn config: SwiperOptions = {}; swiper: Swiper; initialSlide: number; + taskGroupsListName: string; constructor(private taskService: TaskService) { super(); @@ -25,7 +26,10 @@ export class TmplCarouselComponent extends TemplateBaseComponent implements OnIn async ngOnInit() { await this.getParams(); - await this.hackSetHighlightedTask(); + // When using carousel within task group context, set initial slide based on highlighted task + if (this.taskGroupsListName) { + this.hackSetInitialSlide(); + } } async getParams() { @@ -39,6 +43,7 @@ export class TmplCarouselComponent extends TemplateBaseComponent implements OnIn } this.config.centeredSlides = getBooleanParamFromTemplateRow(this._row, "centred_slides", true); this.initialSlide = getNumberParamFromTemplateRow(this._row, "initial_slide_index", 0); + this.taskGroupsListName = getStringParamFromTemplateRow(this._row, "task_group_data", null); } /** Event emitter called when swiper initialised */ @@ -47,17 +52,16 @@ export class TmplCarouselComponent extends TemplateBaseComponent implements OnIn this.swiper.slideTo(this.initialSlide, 0, false); } - /** When using carousel within task_group context set additional highlighted slide from task data */ - private async hackSetHighlightedTask() { - const taskGroupsList = getStringParamFromTemplateRow(this._row, "task_group_data", null); - if (taskGroupsList) { - const highlightedTaskGroup = this.taskService.getHighlightedTaskGroup(); - if (highlightedTaskGroup) { - this.initialSlide = await this.taskService.getHighlightedTaskGroupIndex( - highlightedTaskGroup, - taskGroupsList - ); - } + /** Set initial slide based on highlighted task */ + private async hackSetInitialSlide() { + const indexOfHighlightedTask = await this.taskService.getHighlightedTaskGroupIndex( + this.taskGroupsListName + ); + // if highlightes task is not in list, default to 0 for initial slide + if (indexOfHighlightedTask === -1) { + this.initialSlide = 0; + } else { + this.initialSlide = indexOfHighlightedTask; } } } diff --git a/src/app/shared/components/template/services/instance/template-action.service.ts b/src/app/shared/components/template/services/instance/template-action.service.ts index 53ebeea142..6d187ab282 100644 --- a/src/app/shared/components/template/services/instance/template-action.service.ts +++ b/src/app/shared/components/template/services/instance/template-action.service.ts @@ -228,15 +228,6 @@ export class TemplateActionService extends SyncServiceBase { return processor.processTemplateWithoutRender(templateToProcess); case "google_auth": return await this.authService.signInWithGoogle(); - case "task_group_set_highlighted": - const { previousHighlightedTaskGroup, newHighlightedTaskGroup } = - this.taskService.setHighlightedTaskGroup(args[0]); - // HACK - reschedule campaign notifications when the highlighted task group has changed, - // in order to handle any that are conditional on the highlighted task group - if (previousHighlightedTaskGroup !== newHighlightedTaskGroup) { - this.campaignService.scheduleCampaignNotifications(); - } - return; case "emit": const [emit_value, emit_data] = args; const container: TemplateContainerComponent = this.container; diff --git a/src/app/shared/components/template/services/template-field.service.spec.ts b/src/app/shared/components/template/services/template-field.service.spec.ts index d2284c8bfc..f2ae3e1291 100644 --- a/src/app/shared/components/template/services/template-field.service.spec.ts +++ b/src/app/shared/components/template/services/template-field.service.spec.ts @@ -2,6 +2,7 @@ import { HttpClientTestingModule } from "@angular/common/http/testing"; import { TestBed } from "@angular/core/testing"; import { TemplateFieldService } from "./template-field.service"; import type { PromiseExtended } from "dexie"; +import { booleanStringToBoolean } from "src/app/shared/utils"; /** Mock calls for field values from the template field service to return test data */ export class MockTemplateFieldService implements Partial { @@ -12,11 +13,11 @@ export class MockTemplateFieldService implements Partial { this.mockFields = mockFields; } public getField(key: string) { - return this.mockFields[key]; + return booleanStringToBoolean(this.mockFields[key]); } public setField(key: string, value: string) { this.mockFields[key] = value; - return new Promise(() => {}) as PromiseExtended; + return Promise.resolve("_") as PromiseExtended; } public async ready(timeoutValue?: number): Promise { return true; diff --git a/src/app/shared/services/task/task.service.spec.ts b/src/app/shared/services/task/task.service.spec.ts index ddc6c6c358..8d51cc23a8 100644 --- a/src/app/shared/services/task/task.service.spec.ts +++ b/src/app/shared/services/task/task.service.spec.ts @@ -116,12 +116,22 @@ describe("TaskService", () => { await service.ready(); expect(service.tasksFeatureEnabled).toEqual(true); }); + it("can get task group data rows", async () => { + await service.ready(); + expect(await service.getTaskGroupDataRows(taskGroupsListName)).toEqual( + MOCK_DATA.data_list[taskGroupsListName].rows + ); + }); it("can get highlighted task group (set to highest priority task group on init)", async () => { await service.ready(); expect(service.getHighlightedTaskGroup()).toBe( MOCK_DATA.data_list[taskGroupsListName].rows[0].id ); }); + it("can get highlighted task group index", async () => { + await service.ready(); + expect(await service.getHighlightedTaskGroupIndex(taskGroupsListName)).toBe(0); + }); it("evaluates highlighted task group correctly after init", async () => { await service.ready(); expect(service.evaluateHighlightedTaskGroup().previousHighlightedTaskGroup).toBe( @@ -140,24 +150,53 @@ describe("TaskService", () => { service.checkHighlightedTaskGroup(MOCK_DATA.data_list[taskGroupsListName].rows[0].id) ).toBe(true); }); + it("can set a task group's completed status", async () => { + await service.ready(); + await service.setTaskGroupCompletedStatus( + MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, + true + ); + expect( + await mockTemplateFieldService.getField( + MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field + ) + ).toBe(true); + }); it("completing the highlighted task causes the next highest priority task to be highlighted upon re-evaluation", async () => { await service.ready(); - // Complete highlighted task by setting its associated completed field to true - mockTemplateFieldService.setField( + // Complete highlighted task + await service.setTaskGroupCompletedStatus( MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, - "true" + true ); const { previousHighlightedTaskGroup, newHighlightedTaskGroup } = service.evaluateHighlightedTaskGroup(); expect(previousHighlightedTaskGroup).toBe(MOCK_DATA.data_list[taskGroupsListName].rows[0].id); expect(newHighlightedTaskGroup).toBe(MOCK_DATA.data_list[taskGroupsListName].rows[2].id); }); + it("when all tasks are completed, the highlighted task group is set to ''", async () => { + await service.ready(); + // Complete all tasks + await service.setTaskGroupCompletedStatus( + MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, + true + ); + await service.setTaskGroupCompletedStatus( + MOCK_DATA.data_list[taskGroupsListName].rows[1].completed_field, + true + ); + await service.setTaskGroupCompletedStatus( + MOCK_DATA.data_list[taskGroupsListName].rows[2].completed_field, + true + ); + expect(service.evaluateHighlightedTaskGroup().newHighlightedTaskGroup).toBe(""); + }); it("schedules campaign notifications on change of highlighted task", async () => { await service.ready(); - // Complete highlighted task by setting its associated completed field to true - mockTemplateFieldService.setField( + // Complete highlighted task + await service.setTaskGroupCompletedStatus( MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, - "true" + true ); service.evaluateHighlightedTaskGroup(); await _wait(50); @@ -165,10 +204,4 @@ describe("TaskService", () => { // and again on the evaluation called above expect(scheduleCampaignNotificationsSpy).toHaveBeenCalledTimes(2); }); - - // TODO: test setHighlightedTaskGroup - // it("setting...", async () => { - // await service.ready(); - // service.setHighlightedTaskGroup(MOCK_DATA.data_list[taskGroupsListName].rows[1].id); - // }); }); diff --git a/src/app/shared/services/task/task.service.ts b/src/app/shared/services/task/task.service.ts index 80c70cf4d6..a5707e2a2d 100644 --- a/src/app/shared/services/task/task.service.ts +++ b/src/app/shared/services/task/task.service.ts @@ -52,6 +52,7 @@ export class TaskService extends AsyncServiceBase { if (taskGroupsNotCompletedAndNotSkipped.length === 0) { this.templateFieldService.setField(this.highlightedTaskField, ""); console.log("[HIGHLIGHTED TASK GROUP] - No highlighted task group is set"); + newHighlightedTaskGroup = ""; } // Else set the highlighted task group to the task group with the highest priority of those // not completed or skipped @@ -83,39 +84,19 @@ export class TaskService extends AsyncServiceBase { /** * For a given task groups list, lookup the current highlighted task group and return the index * of the highlighted task within it - * @return the index of the highlighted task group within that list, or 0 if not found - * */ - public async getHighlightedTaskGroupIndex(highlightedTaskGroup: string, taskGroupsList: string) { - const taskGroupsDataList = await this.appDataService.getSheet("data_list", taskGroupsList); - const arrayOfIds = taskGroupsDataList.rows.map((taskGroup) => taskGroup.id); - const indexOfHighlightedTask = arrayOfIds.indexOf(highlightedTaskGroup); - return indexOfHighlightedTask === -1 ? 0 : indexOfHighlightedTask; - } - - /** - * Set the value of the skipped field to true for all uncompleted tasks groups with - * a priority lower than the target task group. Then re-evaluate the highlighted task group - * NB "highest priority" is defined as having the lowest numerical value for the "number" column + * @param taskGroupsListName The name of the data list of hilightable task groups + * (currently only the list matching this.taskGroupsListName will return a positive match) + * @return the index of the highlighted task group within that list (-1 if not found) **/ - public setHighlightedTaskGroup(targetTaskGroupId: string) { - const taskGroupsNotCompleted = this.taskGroups.filter((taskGroup) => { - return !this.templateFieldService.getField(taskGroup.completed_field); - }); - const targetTaskGroupPriority = this.taskGroupsHashmap[targetTaskGroupId].number; - taskGroupsNotCompleted.forEach((taskGroup) => { - // Case: "skipping forward" – target task group is lower in priority than current highlighted task, - // so "skip" all tasks with lower priority than target task - if (taskGroup.number < targetTaskGroupPriority) { - this.templateFieldService.setField(taskGroup.skipped_field, "true"); - } - // Case: "skipping backward" – target task group is higher in priority than current highlighted task, - // so "un-skip" all tasks with equal or higher priority than target task (including target task) - if (taskGroup.number >= targetTaskGroupPriority) { - this.templateFieldService.setField(taskGroup.skipped_field, "false"); - } - }); - // Re-evaluate highlighted task group - return this.evaluateHighlightedTaskGroup(); + public async getHighlightedTaskGroupIndex(taskGroupsListName: string) { + let indexOfHighlightedTask = -1; + const highlightedTaskGroup = this.getHighlightedTaskGroup(); + if (highlightedTaskGroup) { + const taskGroupDataRows = await this.getTaskGroupDataRows(taskGroupsListName); + const arrayOfIds = taskGroupDataRows.map((taskGroup) => taskGroup.id); + indexOfHighlightedTask = arrayOfIds.indexOf(highlightedTaskGroup); + } + return indexOfHighlightedTask; } /** @@ -203,7 +184,8 @@ export class TaskService extends AsyncServiceBase { this.ensureSyncServicesReady([this.appDataService, this.appConfigService]); this.subscribeToAppConfigChanges(); if (this.tasksFeatureEnabled) { - await this.getListOfTaskGroups(); + this.taskGroups = await this.getTaskGroupDataRows(this.taskGroupsListName); + this.taskGroupsHashmap = arrayToHashmap(this.taskGroups, "id"); if (this.taskGroups.length > 0) { this.evaluateHighlightedTaskGroup(); } @@ -218,13 +200,31 @@ export class TaskService extends AsyncServiceBase { }); } - /** Get the list of highlight-able task groups, from the relevant data_list */ - private async getListOfTaskGroups() { - const taskGroupsDataList = await this.appDataService.getSheet( - "data_list", - this.taskGroupsListName - ); - this.taskGroups = taskGroupsDataList?.rows || []; - this.taskGroupsHashmap = arrayToHashmap(this.taskGroups, "id"); - } + /** + * TODO: this is not currently implemented, and should likely be reworked as part of a broader overhaul of the task system + * + * Set the value of the skipped field to true for all uncompleted tasks groups with + * a priority lower than the target task group. Then re-evaluate the highlighted task group + * NB "highest priority" is defined as having the lowest numerical value for the "number" column + **/ + // public setHighlightedTaskGroup(targetTaskGroupId: string) { + // const taskGroupsNotCompleted = this.taskGroups.filter((taskGroup) => { + // return !this.templateFieldService.getField(taskGroup.completed_field); + // }); + // const targetTaskGroupPriority = this.taskGroupsHashmap[targetTaskGroupId].number; + // taskGroupsNotCompleted.forEach((taskGroup) => { + // // Case: "skipping forward" – target task group is lower in priority than current highlighted task, + // // so "skip" all tasks with lower priority than target task + // if (taskGroup.number < targetTaskGroupPriority) { + // this.templateFieldService.setField(taskGroup.skipped_field, "true"); + // } + // // Case: "skipping backward" – target task group is higher in priority than current highlighted task, + // // so "un-skip" all tasks with equal or higher priority than target task (including target task) + // if (taskGroup.number >= targetTaskGroupPriority) { + // this.templateFieldService.setField(taskGroup.skipped_field, "false"); + // } + // }); + // // Re-evaluate highlighted task group + // return this.evaluateHighlightedTaskGroup(); + // } } From 742cbacca82ebc64834e752553e9169775fa3114 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 7 Feb 2024 16:00:57 +0000 Subject: [PATCH 14/14] chore: code tidy --- .../components/template/services/template-field.service.ts | 7 +------ src/app/shared/services/data/app-data.service.ts | 5 +---- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/app/shared/components/template/services/template-field.service.ts b/src/app/shared/components/template/services/template-field.service.ts index 0b9a08dd07..9750e2c76b 100644 --- a/src/app/shared/components/template/services/template-field.service.ts +++ b/src/app/shared/components/template/services/template-field.service.ts @@ -1,4 +1,4 @@ -import { Injectable, InjectionToken } from "@angular/core"; +import { Injectable } from "@angular/core"; import { IFlowEvent } from "data-models"; import { FlowTypes } from "data-models"; import { DbService } from "src/app/shared/services/db/db.service"; @@ -110,8 +110,3 @@ export class TemplateFieldService extends AsyncServiceBase { this.globals[row.name] = row; } } - -// For testing, provide an injection token -export const TEMPLATE_FIELD_SERVICE_TOKEN = new InjectionToken( - "TemplateFieldService" -); diff --git a/src/app/shared/services/data/app-data.service.ts b/src/app/shared/services/data/app-data.service.ts index dc5984c2e7..dd6a3dc595 100644 --- a/src/app/shared/services/data/app-data.service.ts +++ b/src/app/shared/services/data/app-data.service.ts @@ -1,5 +1,5 @@ import { HttpClient } from "@angular/common/http"; -import { Injectable, InjectionToken } from "@angular/core"; +import { Injectable } from "@angular/core"; import { SHEETS_CONTENT_LIST, TRANSLATIONS_CONTENT_LIST } from "src/app/data/app-data"; import { lastValueFrom } from "rxjs"; import { FlowTypes } from "../../model"; @@ -196,6 +196,3 @@ export class AppDataService extends SyncServiceBase { export type IAppDataCache = { [flow_type in FlowTypes.FlowType]: { [flow_name: string]: FlowTypes.FlowTypeWithData }; }; - -// For testing, provide an injection token -export const APP_DATA_SERVICE_TOKEN = new InjectionToken("AppDataService");