Skip to content

Commit

Permalink
fix(demo): improved app initialization to have all config during demo…
Browse files Browse the repository at this point in the history
… generation (#1890)

* moved app initialisation steps to APP_INITIALIZER
* fix: demo config is generated before other demo entities
  • Loading branch information
TheSlimvReal authored Jun 8, 2023
1 parent cb7aea3 commit 7e58e13
Show file tree
Hide file tree
Showing 13 changed files with 126 additions and 151 deletions.
6 changes: 6 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@
"src/assets",
"src/favicon.ico",
"src/manifest.webmanifest"
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.spec.ts"
}
]
}
},
Expand Down
69 changes: 69 additions & 0 deletions src/app/app-initializers.ts
Original file line number Diff line number Diff line change
@@ -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,
};
72 changes: 11 additions & 61 deletions src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,89 +15,39 @@
* along with ndb-core. If not, see <http://www.gnu.org/licenses/>.
*/

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<AppComponent>;
let entityUpdates: Subject<UpdatedEntity<Config>>;
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();
}));
});
75 changes: 2 additions & 73 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,7 @@
* along with ndb-core. If not, see <http://www.gnu.org/licenses/>.
*/

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.
Expand All @@ -42,58 +25,4 @@ import { EntityRegistry } from "./core/entity/database-entity.decorator";
selector: "app-root",
template: "<app-ui></app-ui>",
})
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 {}
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -154,6 +155,7 @@ import { LoginState } from "./core/session/session-states/login-state.enum";
}),
deps: [SessionService],
},
appInitializers,
],
bootstrap: [AppComponent],
})
Expand Down
1 change: 1 addition & 0 deletions src/app/core/demo-data/demo-data.module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 5 additions & 4 deletions src/app/core/demo-data/demo-data.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -66,8 +67,6 @@ const demoDataGeneratorProviders = [
maxCountAttributes: 5,
}),
...DemoTodoGeneratorService.provider(),
// keep Demo service last to ensure all entities are already initialized
...DemoConfigGeneratorService.provider(),
];

/**
Expand Down Expand Up @@ -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();
}
}
9 changes: 1 addition & 8 deletions src/app/core/export/query.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ describe("NavigationComponent", () => {

beforeEach(waitForAsync(() => {
mockConfigUpdated = new BehaviorSubject<Config>(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);

Expand Down
5 changes: 4 additions & 1 deletion src/app/core/session/login/login.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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()],
Expand Down
Loading

0 comments on commit 7e58e13

Please sign in to comment.