diff --git a/angular.json b/angular.json index dada2fbe23..792424196c 100644 --- a/angular.json +++ b/angular.json @@ -116,6 +116,12 @@ "src/assets", "src/favicon.ico", "src/manifest.webmanifest" + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.spec.ts" + } ] } }, diff --git a/src/app/app-initializers.ts b/src/app/app-initializers.ts new file mode 100644 index 0000000000..f5b6cb8ede --- /dev/null +++ b/src/app/app-initializers.ts @@ -0,0 +1,69 @@ +import { + APP_INITIALIZER, + Injector, + ɵcreateInjector as createInjector, +} from "@angular/core"; +import { ConfigService } from "./core/config/config.service"; +import { RouterService } from "./core/view/dynamic-routing/router.service"; +import { EntityConfigService } from "./core/entity/entity-config.service"; +import { Router } from "@angular/router"; +import { SessionService } from "./core/session/session-service/session.service"; +import { AnalyticsService } from "./core/analytics/analytics.service"; +import { LoginState } from "./core/session/session-states/login-state.enum"; +import { LoggingService } from "./core/logging/logging.service"; +import { environment } from "../environments/environment"; + +export const appInitializers = { + provide: APP_INITIALIZER, + useFactory: + ( + injector: Injector, + configService: ConfigService, + routerService: RouterService, + entityConfigService: EntityConfigService, + router: Router, + sessionService: SessionService, + analyticsService: AnalyticsService + ) => + async () => { + // Re-trigger services that depend on the config when something changes + configService.configUpdates.subscribe(() => { + routerService.initRouting(); + entityConfigService.setupEntitiesFromConfig(); + const url = location.href.replace(location.origin, ""); + router.navigateByUrl(url, { skipLocationChange: true }); + }); + + // update the user context for remote error logging and tracking and load config initially + sessionService.loginState.subscribe((newState) => { + if (newState === LoginState.LOGGED_IN) { + const username = sessionService.getCurrentUser().name; + LoggingService.setLoggingContextUser(username); + analyticsService.setUser(username); + } else { + LoggingService.setLoggingContextUser(undefined); + analyticsService.setUser(undefined); + } + }); + + if (environment.production) { + analyticsService.init(); + } + if (environment.demo_mode) { + const m = await import("./core/demo-data/demo-data.module"); + await createInjector(m.DemoDataModule, injector) + .get(m.DemoDataModule) + .publishDemoData(); + } + }, + deps: [ + Injector, + ConfigService, + RouterService, + EntityConfigService, + Router, + SessionService, + AnalyticsService, + ], + multi: true, +}; diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 0bda7e47a3..950c3255ea 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -15,89 +15,39 @@ * along with ndb-core. If not, see . */ -import { - ComponentFixture, - discardPeriodicTasks, - fakeAsync, - flush, - TestBed, - waitForAsync, -} from "@angular/core/testing"; +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { AppComponent } from "./app.component"; import { AppModule } from "./app.module"; -import { Config } from "./core/config/config"; -import { USAGE_ANALYTICS_CONFIG_ID } from "./core/analytics/usage-analytics-config"; import { environment } from "../environments/environment"; -import { EntityRegistry } from "./core/entity/database-entity.decorator"; -import { Subject } from "rxjs"; import { Database } from "./core/database/database"; -import { UpdatedEntity } from "./core/entity/model/entity-update"; -import { EntityMapperService } from "./core/entity/entity-mapper.service"; -import { mockEntityMapper } from "./core/entity/mock-entity-mapper-service"; -import { SessionType } from "./core/session/session-type"; import { HttpClientTestingModule } from "@angular/common/http/testing"; -import { Angulartics2Matomo } from "angulartics2"; -import { componentRegistry } from "./dynamic-components"; describe("AppComponent", () => { let component: AppComponent; let fixture: ComponentFixture; - let entityUpdates: Subject>; + const intervalBefore = jasmine.DEFAULT_TIMEOUT_INTERVAL; - beforeAll(() => { - componentRegistry.allowDuplicates(); - }); beforeEach(waitForAsync(() => { - environment.session_type = SessionType.mock; - environment.production = false; - environment.demo_mode = false; - const entityMapper = mockEntityMapper(); - entityUpdates = new Subject(); - spyOn(entityMapper, "receiveUpdates").and.returnValue(entityUpdates); - + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + environment.demo_mode = true; TestBed.configureTestingModule({ imports: [AppModule, HttpClientTestingModule], - providers: [{ provide: EntityMapperService, useValue: entityMapper }], }).compileComponents(); - - spyOn(TestBed.inject(EntityRegistry), "add"); // Prevent error with duplicate registration })); - function createComponent() { + beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(AppComponent); component = fixture.componentInstance; fixture.detectChanges(); - } + })); - afterEach(() => TestBed.inject(Database).destroy()); + afterEach(() => { + environment.demo_mode = false; + jasmine.DEFAULT_TIMEOUT_INTERVAL = intervalBefore; + return TestBed.inject(Database).destroy(); + }); it("should be created", () => { - createComponent(); expect(component).toBeTruthy(); }); - - it("should start tracking with config from db", fakeAsync(() => { - environment.production = true; // tracking is only active in production mode - const testConfig = new Config(Config.CONFIG_KEY, { - [USAGE_ANALYTICS_CONFIG_ID]: { - url: "matomo-test-endpoint", - site_id: "101", - }, - }); - entityUpdates.next({ entity: testConfig, type: "new" }); - const angulartics = TestBed.inject(Angulartics2Matomo); - const startTrackingSpy = spyOn(angulartics, "startTracking"); - window["_paq"] = []; - - createComponent(); - flush(); - - expect(startTrackingSpy).toHaveBeenCalledTimes(1); - expect(window["_paq"]).toContain([ - "setSiteId", - testConfig.data[USAGE_ANALYTICS_CONFIG_ID].site_id, - ]); - - discardPeriodicTasks(); - })); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 340d76407c..f78b371f37 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -15,24 +15,7 @@ * along with ndb-core. If not, see . */ -import { - Component, - Injector, - ViewContainerRef, - ɵcreateInjector as createInjector, -} from "@angular/core"; -import { AnalyticsService } from "./core/analytics/analytics.service"; -import { ConfigService } from "./core/config/config.service"; -import { RouterService } from "./core/view/dynamic-routing/router.service"; -import { EntityConfigService } from "./core/entity/entity-config.service"; -import { SessionService } from "./core/session/session-service/session.service"; -import { ActivatedRoute, Router } from "@angular/router"; -import { environment } from "../environments/environment"; -import { Child } from "./child-dev-project/children/model/child"; -import { School } from "./child-dev-project/schools/model/school"; -import { LoginState } from "./core/session/session-states/login-state.enum"; -import { LoggingService } from "./core/logging/logging.service"; -import { EntityRegistry } from "./core/entity/database-entity.decorator"; +import { Component } from "@angular/core"; /** * Component as the main entry point for the app. @@ -42,58 +25,4 @@ import { EntityRegistry } from "./core/entity/database-entity.decorator"; selector: "app-root", template: "", }) -export class AppComponent { - constructor( - private viewContainerRef: ViewContainerRef, // need this small hack in order to catch application root view container ref - private analyticsService: AnalyticsService, - private configService: ConfigService, - private routerService: RouterService, - private entityConfigService: EntityConfigService, - private sessionService: SessionService, - private activatedRoute: ActivatedRoute, - private router: Router, - private entities: EntityRegistry, - private injector: Injector - ) { - this.initBasicServices(); - } - - private async initBasicServices() { - // TODO: remove this after issue #886 now in next release (keep as fallback for one version) - this.entities.add("Participant", Child); - this.entities.add("Team", School); - - // first register to events - - // Re-trigger services that depend on the config when something changes - this.configService.configUpdates.subscribe(() => { - this.routerService.initRouting(); - this.entityConfigService.setupEntitiesFromConfig(); - this.router.navigate([], { - relativeTo: this.activatedRoute, - queryParamsHandling: "preserve", - }); - }); - - // update the user context for remote error logging and tracking and load config initially - this.sessionService.loginState.subscribe((newState) => { - if (newState === LoginState.LOGGED_IN) { - const username = this.sessionService.getCurrentUser().name; - LoggingService.setLoggingContextUser(username); - this.analyticsService.setUser(username); - } else { - LoggingService.setLoggingContextUser(undefined); - this.analyticsService.setUser(undefined); - } - }); - - if (environment.production) { - this.analyticsService.init(); - } - - if (environment.demo_mode) { - const m = await import("./core/demo-data/demo-data.module"); - createInjector(m.DemoDataModule, this.injector); - } - } -} +export class AppComponent {} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3e871d9c96..f06a805547 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -78,6 +78,7 @@ import { TodosModule } from "./features/todos/todos.module"; import { SessionService } from "./core/session/session-service/session.service"; import { waitForChangeTo } from "./core/session/session-states/session-utils"; import { LoginState } from "./core/session/session-states/login-state.enum"; +import { appInitializers } from "./app-initializers"; /** * Main entry point of the application. @@ -154,6 +155,7 @@ import { LoginState } from "./core/session/session-states/login-state.enum"; }), deps: [SessionService], }, + appInitializers, ], bootstrap: [AppComponent], }) diff --git a/src/app/core/demo-data/demo-data.module.spec.ts b/src/app/core/demo-data/demo-data.module.spec.ts index b7e936785c..dcf043027e 100644 --- a/src/app/core/demo-data/demo-data.module.spec.ts +++ b/src/app/core/demo-data/demo-data.module.spec.ts @@ -22,6 +22,7 @@ describe("DemoDataModule", () => { }); it("should generate the demo data once the module is loaded", fakeAsync(() => { + TestBed.inject(DemoDataModule).publishDemoData(); expect(mockEntityMapper.saveAll).not.toHaveBeenCalled(); TestBed.inject(DemoDataModule); diff --git a/src/app/core/demo-data/demo-data.module.ts b/src/app/core/demo-data/demo-data.module.ts index 6a27664c86..4c0f9d083a 100644 --- a/src/app/core/demo-data/demo-data.module.ts +++ b/src/app/core/demo-data/demo-data.module.ts @@ -40,6 +40,7 @@ import { DemoConfigurableEnumGeneratorService } from "../configurable-enum/demo- import { DemoPublicFormGeneratorService } from "../../features/public-form/demo-public-form-generator.service"; const demoDataGeneratorProviders = [ + ...DemoConfigGeneratorService.provider(), ...DemoPermissionGeneratorService.provider(), ...DemoPublicFormGeneratorService.provider(), ...DemoUserGeneratorService.provider(), @@ -66,8 +67,6 @@ const demoDataGeneratorProviders = [ maxCountAttributes: 5, }), ...DemoTodoGeneratorService.provider(), - // keep Demo service last to ensure all entities are already initialized - ...DemoConfigGeneratorService.provider(), ]; /** @@ -106,7 +105,9 @@ const demoDataGeneratorProviders = [ exports: [DemoDataGeneratingProgressDialogComponent], }) export class DemoDataModule { - constructor(demoDataInitializer: DemoDataInitializerService) { - demoDataInitializer.run(); + constructor(private demoDataInitializer: DemoDataInitializerService) {} + + publishDemoData() { + return this.demoDataInitializer.run(); } } diff --git a/src/app/core/export/query.service.spec.ts b/src/app/core/export/query.service.spec.ts index a22859ba75..489ac0965a 100644 --- a/src/app/core/export/query.service.spec.ts +++ b/src/app/core/export/query.service.spec.ts @@ -18,8 +18,6 @@ import { expectEntitiesToMatch } from "../../utils/expect-entity-data.spec"; import { Database } from "../database/database"; import { Note } from "../../child-dev-project/notes/model/note"; import { genders } from "../../child-dev-project/children/model/genders"; -import { EntityConfigService } from "app/core/entity/entity-config.service"; -import { ConfigService } from "app/core/config/config.service"; import { EventAttendance } from "../../child-dev-project/attendance/model/event-attendance"; import { AttendanceStatusType } from "../../child-dev-project/attendance/model/attendance-status"; import { DatabaseTestingModule } from "../../utils/database-testing.module"; @@ -44,17 +42,12 @@ describe("QueryService", () => { (i) => i.id === "COACHING_CLASS" ); - beforeEach(waitForAsync(async () => { + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [DatabaseTestingModule], }); service = TestBed.inject(QueryService); - const configService = TestBed.inject(ConfigService); - const entityConfigService = TestBed.inject(EntityConfigService); entityMapper = TestBed.inject(EntityMapperService); - await configService.loadConfig(); - entityConfigService.addConfigAttributes(School); - entityConfigService.addConfigAttributes(Child); })); afterEach(() => TestBed.inject(Database).destroy()); diff --git a/src/app/core/navigation/navigation/navigation.component.spec.ts b/src/app/core/navigation/navigation/navigation.component.spec.ts index 5ff35ece67..e8d9edd4b0 100644 --- a/src/app/core/navigation/navigation/navigation.component.spec.ts +++ b/src/app/core/navigation/navigation/navigation.component.spec.ts @@ -36,10 +36,11 @@ describe("NavigationComponent", () => { beforeEach(waitForAsync(() => { mockConfigUpdated = new BehaviorSubject(null); - mockConfigService = jasmine.createSpyObj(["getConfig"], { + mockConfigService = jasmine.createSpyObj(["getConfig", "getAllConfigs"], { configUpdates: mockConfigUpdated, }); mockConfigService.getConfig.and.returnValue({ items: [] }); + mockConfigService.getAllConfigs.and.returnValue([]); mockUserRoleGuard = jasmine.createSpyObj(["checkRoutePermissions"]); mockUserRoleGuard.checkRoutePermissions.and.returnValue(true); diff --git a/src/app/core/session/login/login.component.spec.ts b/src/app/core/session/login/login.component.spec.ts index 2af9ccf772..42a964424a 100644 --- a/src/app/core/session/login/login.component.spec.ts +++ b/src/app/core/session/login/login.component.spec.ts @@ -43,7 +43,10 @@ describe("LoginComponent", () => { let loader: HarnessLoader; beforeEach(waitForAsync(() => { - mockSessionService = jasmine.createSpyObj(["login"], { loginState }); + mockSessionService = jasmine.createSpyObj(["login", "getCurrentUser"], { + loginState, + }); + mockSessionService.getCurrentUser.and.returnValue({ name: "", roles: [] }); TestBed.configureTestingModule({ imports: [LoginComponent, MockedTestingModule], providers: [ diff --git a/src/app/features/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.spec.ts b/src/app/features/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.spec.ts index 10ee3e8173..da754978cd 100644 --- a/src/app/features/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.spec.ts +++ b/src/app/features/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.spec.ts @@ -11,7 +11,7 @@ import { EntityMapperService } from "../../../core/entity/entity-mapper.service" import { AlertService } from "../../../core/alerts/alert.service"; import { ProgressDashboardConfig } from "./progress-dashboard-config"; import { MatDialog } from "@angular/material/dialog"; -import { BehaviorSubject, Subject } from "rxjs"; +import { BehaviorSubject, NEVER, Subject } from "rxjs"; import { take } from "rxjs/operators"; import { SessionService } from "../../../core/session/session-service/session.service"; import { SyncState } from "../../../core/session/session-states/sync-state.enum"; @@ -27,7 +27,10 @@ describe("ProgressDashboardComponent", () => { beforeEach(waitForAsync(() => { mockSync = new BehaviorSubject(SyncState.UNSYNCED); - mockSession = jasmine.createSpyObj([], { syncState: mockSync }); + mockSession = jasmine.createSpyObj([], { + syncState: mockSync, + loginState: NEVER, + }); TestBed.configureTestingModule({ imports: [ProgressDashboardComponent, MockedTestingModule.withState()], diff --git a/src/app/utils/mocked-testing.module.ts b/src/app/utils/mocked-testing.module.ts index 399bbca04a..bfa0f44cc0 100644 --- a/src/app/utils/mocked-testing.module.ts +++ b/src/app/utils/mocked-testing.module.ts @@ -52,7 +52,11 @@ export const TEST_PASSWORD = "pass"; { provide: SwRegistrationOptions, useValue: { enabled: false } }, { provide: AnalyticsService, - useValue: { eventTrack: () => undefined }, + useValue: { + eventTrack: () => undefined, + setUser: () => undefined, + init: () => undefined, + }, }, { provide: DatabaseIndexingService, diff --git a/src/environments/environment.spec.ts b/src/environments/environment.spec.ts new file mode 100644 index 0000000000..272a2d5770 --- /dev/null +++ b/src/environments/environment.spec.ts @@ -0,0 +1,13 @@ +import { SessionType } from "../app/core/session/session-type"; +import { AuthProvider } from "../app/core/session/auth/auth-provider"; + +export const environment = { + production: false, + appVersion: "test", + repositoryId: "Aam-Digital/ndb-core", + demo_mode: false, + session_type: SessionType.mock, + authenticator: AuthProvider.CouchDB, + account_url: "https://accounts.aam-digital.net", + email: undefined, +};