Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved app initialisation #1890

Merged
merged 10 commits into from
Jun 8, 2023
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