Skip to content

Commit

Permalink
refactor: Reworked Startup Process (#1259)
Browse files Browse the repository at this point in the history
solves #595 

Co-authored-by: Sebastian Leidig <sebastian.leidig@gmail.com>
  • Loading branch information
TheSlimvReal and sleidig authored May 24, 2022
1 parent ec324bf commit 9c154b7
Show file tree
Hide file tree
Showing 62 changed files with 717 additions and 1,013 deletions.
89 changes: 24 additions & 65 deletions src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,63 +25,47 @@ import {
waitForAsync,
} from "@angular/core/testing";
import { AppComponent } from "./app.component";
import { ApplicationInitStatus } from "@angular/core";
import { AppModule } from "./app.module";
import { AppConfig } from "./core/app-config/app-config";
import { IAppConfig } from "./core/app-config/app-config.model";
import { Angulartics2Matomo } from "angulartics2/matomo";
import { Config } from "./core/config/config";
import { USAGE_ANALYTICS_CONFIG_ID } from "./core/analytics/usage-analytics-config";
import { environment } from "../environments/environment";
import { HttpClientTestingModule } from "@angular/common/http/testing";
import { EntityRegistry } from "./core/entity/database-entity.decorator";
import { BehaviorSubject } from "rxjs";
import { SyncState } from "./core/session/session-states/sync-state.enum";
import { LoginState } from "./core/session/session-states/login-state.enum";
import { SessionService } from "./core/session/session-service/session.service";
import { Router } from "@angular/router";
import { ConfigService } from "./core/config/config.service";
import { PouchDatabase } from "./core/database/pouch-database";
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 { DemoDataService } from "./core/demo-data/demo-data.service";
import { SessionType } from "./core/session/session-type";
import { HttpClientTestingModule } from "@angular/common/http/testing";

describe("AppComponent", () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
const syncState = new BehaviorSubject(SyncState.UNSYNCED);
const loginState = new BehaviorSubject(LoginState.LOGGED_OUT);
let mockSessionService: jasmine.SpyObj<SessionService>;
let entityUpdates: Subject<UpdatedEntity<Config>>;

const mockAppSettings: IAppConfig = {
database: { name: "", remote_url: "" },
session_type: undefined,
session_type: SessionType.local,
demo_mode: false,
site_name: "",
};

beforeEach(
waitForAsync(() => {
mockSessionService = jasmine.createSpyObj(
["getCurrentUser", "isLoggedIn"],
{
syncState: syncState,
loginState: loginState,
}
);
mockSessionService.getCurrentUser.and.returnValue({
name: "test user",
roles: [],
});
AppConfig.settings = mockAppSettings;
const entityMapper = mockEntityMapper();
entityUpdates = new Subject();
spyOn(entityMapper, "receiveUpdates").and.returnValue(entityUpdates);

TestBed.configureTestingModule({
imports: [AppModule, HttpClientTestingModule],
providers: [
{ provide: AppConfig, useValue: jasmine.createSpyObj(["load"]) },
{ provide: SessionService, useValue: mockSessionService },
{ provide: Database, useValue: PouchDatabase.create() },
],
providers: [{ provide: EntityMapperService, useValue: entityMapper }],
}).compileComponents();

TestBed.inject(ApplicationInitStatus); // This ensures that the AppConfig is loaded before test execution
spyOn(TestBed.inject(EntityRegistry), "add"); // Prevent error with duplicate registration
})
);
Expand All @@ -106,18 +90,18 @@ describe("AppComponent", () => {
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, {
"appConfig:usage-analytics": {
[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();
tick();
TestBed.inject(ConfigService).configUpdates.next(testConfig);

expect(startTrackingSpy).toHaveBeenCalledTimes(1);
expect(window["_paq"]).toContain([
Expand All @@ -128,41 +112,16 @@ describe("AppComponent", () => {
discardPeriodicTasks();
}));

it("should navigate on same page only when the config changes", fakeAsync(() => {
const routeSpy = spyOn(TestBed.inject(Router), "navigate");
mockSessionService.isLoggedIn.and.returnValue(true);
createComponent();
tick();
expect(routeSpy).toHaveBeenCalledTimes(1);

const configService = TestBed.inject(ConfigService);
const config = configService.configUpdates.value;
configService.configUpdates.next(config);
tick();
expect(routeSpy).toHaveBeenCalledTimes(1);

config.data["someProp"] = "some change";
configService.configUpdates.next(config);
tick();
expect(routeSpy).toHaveBeenCalledTimes(2);
it("published the demo data", fakeAsync(() => {
const demoDataService = TestBed.inject(DemoDataService);
spyOn(demoDataService, "publishDemoData").and.callThrough();
AppConfig.settings.demo_mode = true;

createComponent();
flush();
discardPeriodicTasks();
}));

it("should reload the config whenever the sync completes", () => {
const configSpy = spyOn(TestBed.inject(ConfigService), "loadConfig");
createComponent();

expect(configSpy).not.toHaveBeenCalled();

syncState.next(SyncState.STARTED);
expect(configSpy).not.toHaveBeenCalled();

syncState.next(SyncState.COMPLETED);
expect(configSpy).toHaveBeenCalledTimes(1);

syncState.next(SyncState.COMPLETED);
expect(configSpy).toHaveBeenCalledTimes(2);
});
expect(demoDataService.publishDemoData).toHaveBeenCalled();
AppConfig.settings.demo_mode = false;
}));
});
17 changes: 2 additions & 15 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ 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 { SyncState } from "./core/session/session-states/sync-state.enum";
import { ActivatedRoute, Router } from "@angular/router";
import { environment } from "../environments/environment";
import { Child } from "./child-dev-project/children/model/child";
Expand All @@ -31,7 +30,6 @@ import { AppConfig } from "./core/app-config/app-config";
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 { filter } from "rxjs/operators";

/**
* Component as the main entry point for the app.
Expand Down Expand Up @@ -66,21 +64,11 @@ export class AppComponent {

// first register to events

// Reload config once the database is synced after someone logged in
this.sessionService.syncState
.pipe(filter((state) => state === SyncState.COMPLETED))
.subscribe(() => this.configService.loadConfig());

// Re-trigger services that depend on the config when something changes
let lastConfig: string;
this.configService.configUpdates.subscribe((config) => {
this.configService.configUpdates.subscribe(() => {
this.routerService.initRouting();
this.entityConfigService.setupEntitiesFromConfig();
const configString = JSON.stringify(config);
if (this.sessionService.isLoggedIn() && configString !== lastConfig) {
this.router.navigate([], { relativeTo: this.activatedRoute });
lastConfig = configString;
}
this.router.navigate([], { relativeTo: this.activatedRoute });
});

// update the user context for remote error logging and tracking and load config initially
Expand All @@ -89,7 +77,6 @@ export class AppComponent {
const username = this.sessionService.getCurrentUser().name;
LoggingService.setLoggingContextUser(username);
this.analyticsService.setUser(username);
this.configService.loadConfig();
} else {
LoggingService.setLoggingContextUser(undefined);
this.analyticsService.setUser(undefined);
Expand Down
6 changes: 4 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { HttpClientModule } from "@angular/common/http";

import { AppComponent } from "./app.component";
import { UiModule } from "./core/ui/ui.module";
import { AppConfigModule } from "./core/app-config/app-config.module";
import { RouteRegistry, routesRegistry, routing } from "./app.routing";
import { AlertsModule } from "./core/alerts/alerts.module";
import { SessionModule } from "./core/session/session.module";
Expand Down Expand Up @@ -80,6 +79,8 @@ import { fas } from "@fortawesome/free-solid-svg-icons";
import { far } from "@fortawesome/free-regular-svg-icons";
import { DemoPermissionGeneratorService } from "./core/permissions/demo-permission-generator.service";
import { SupportModule } from "./core/support/support.module";
import { DemoConfigGeneratorService } from "./core/config/demo-config-generator.service";
import { DatabaseModule } from "./core/database/database.module";

/**
* Main entry point of the application.
Expand All @@ -106,7 +107,6 @@ import { SupportModule } from "./core/support/support.module";
FormDialogModule,
AlertsModule,
EntityModule,
AppConfigModule,
SessionModule,
ConfigModule,
UiModule,
Expand Down Expand Up @@ -150,11 +150,13 @@ import { SupportModule } from "./core/support/support.module";
maxCountAttributes: 5,
}),
...DemoPermissionGeneratorService.provider(),
...DemoConfigGeneratorService.provider(),
]),
AttendanceModule,
DashboardShortcutWidgetModule,
HistoricalDataModule,
SupportModule,
DatabaseModule,
],
providers: [
{ provide: ErrorHandler, useClass: LoggingErrorHandler },
Expand Down
7 changes: 4 additions & 3 deletions src/app/app.routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@

import { RouterModule, Routes } from "@angular/router";
import { ModuleWithProviders } from "@angular/core";
import { UserRoleGuard } from "./core/permissions/permission-guard/user-role.guard";
import { ComponentType } from "@angular/cdk/overlay";
import { Registry } from "./core/registry/dynamic-registry";
import { ApplicationLoadingComponent } from "./core/view/dynamic-routing/empty/application-loading.component";
import { NotFoundComponent } from "./core/view/dynamic-routing/not-found/not-found.component";

export class RouteRegistry extends Registry<ComponentType<any>> {}
export const routesRegistry = new RouteRegistry();
Expand All @@ -46,7 +47,6 @@ export const allRoutes: Routes = [
// routes are added dynamically by the RouterService
{
path: "admin/conflicts",
canActivate: [UserRoleGuard],
loadChildren: () =>
import("./conflict-resolution/conflict-resolution.module").then(
(m) => m["ConflictResolutionModule"]
Expand All @@ -59,7 +59,8 @@ export const allRoutes: Routes = [
(m) => m["ComingSoonModule"]
),
},
{ path: "**", redirectTo: "/" },
{ path: "404", component: NotFoundComponent },
{ path: "**", pathMatch: "full", component: ApplicationLoadingComponent },
];

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";

import { ChildrenCountDashboardComponent } from "./children-count-dashboard.component";
import { RouterTestingModule } from "@angular/router/testing";
import { Center, Child } from "../../model/child";
import { ConfigurableEnumValue } from "../../../../core/configurable-enum/configurable-enum.interface";
import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing";
Expand All @@ -11,6 +10,7 @@ import {
MockEntityMapperService,
} from "../../../../core/entity/mock-entity-mapper-service";
import { ChildrenModule } from "../../children.module";
import { MockedTestingModule } from "../../../../utils/mocked-testing.module";

describe("ChildrenCountDashboardComponent", () => {
let component: ChildrenCountDashboardComponent;
Expand All @@ -29,7 +29,7 @@ describe("ChildrenCountDashboardComponent", () => {
TestBed.configureTestingModule({
imports: [
ChildrenModule,
RouterTestingModule,
MockedTestingModule.withState(),
FontAwesomeTestingModule,
],
providers: [{ provide: EntityMapperService, useValue: entityMapper }],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,9 @@ import { noteGroupStories } from "./notes_group-stories";
import { centersUnique } from "../../children/demo-data-generators/fixtures/centers";
import { absenceRemarks } from "./remarks";
import moment from "moment";
import { EntitySchemaService } from "../../../core/entity/schema/entity-schema.service";
import {
ATTENDANCE_STATUS_CONFIG_ID,
AttendanceLogicalStatus,
AttendanceStatusType,
} from "../../attendance/model/attendance-status";
import { ConfigService } from "../../../core/config/config.service";
import {
CONFIGURABLE_ENUM_CONFIG_PREFIX,
ConfigurableEnumConfig,
} from "../../../core/configurable-enum/configurable-enum.interface";
import { AttendanceLogicalStatus } from "../../attendance/model/attendance-status";
import { DemoUserGeneratorService } from "../../../core/user/demo-user-generator.service";
import { defaultAttendanceStatusTypes } from "../../../core/config/default-config/default-attendance-status-types";

export class DemoNoteConfig {
minNotesPerChild: number;
Expand Down Expand Up @@ -52,20 +43,12 @@ export class DemoNoteGeneratorService extends DemoDataGenerator<Note> {
];
}

private availableStatusTypes: AttendanceStatusType[];

constructor(
private config: DemoNoteConfig,
private demoChildren: DemoChildGenerator,
private demoUsers: DemoUserGeneratorService,
private schemaService: EntitySchemaService,
private configService: ConfigService
private demoUsers: DemoUserGeneratorService
) {
super();

this.availableStatusTypes = this.configService.getConfig<
ConfigurableEnumConfig<AttendanceStatusType>
>(CONFIGURABLE_ENUM_CONFIG_PREFIX + ATTENDANCE_STATUS_CONFIG_ID);
}

public generateEntities(): Note[] {
Expand Down Expand Up @@ -113,15 +96,10 @@ export class DemoNoteGeneratorService extends DemoDataGenerator<Note> {
}

private generateNoteForChild(child: Child, date?: Date): Note {
let note = new Note();
const note = new Note();

const selectedStory = faker.random.arrayElement(noteIndividualStories);
Object.assign(note, selectedStory);
// transform to ensure the category object is loaded from the generic config
note = this.schemaService.transformDatabaseToEntityFormat(
note,
Note.schema
);

note.addChild(child.getId());
note.authors = [faker.random.arrayElement(this.demoUsers.entities).getId()];
Expand Down Expand Up @@ -151,27 +129,22 @@ export class DemoNoteGeneratorService extends DemoDataGenerator<Note> {
}

private generateGroupNote(children: Child[]) {
let note = new Note();
const note = new Note();

const selectedStory = faker.random.arrayElement(noteGroupStories);
Object.assign(note, selectedStory);
// transform to ensure the category object is loaded from the generic config
note = this.schemaService.transformDatabaseToEntityFormat(
note,
Note.schema
);

note.children = children.map((c) => c.getId());
children.forEach((child) => {
const attendance = note.getAttendance(child.getId());
// get an approximate presence of 85%
if (faker.datatype.number(100) <= 15) {
attendance.status = this.availableStatusTypes.find(
attendance.status = defaultAttendanceStatusTypes.find(
(t) => t.countAs === AttendanceLogicalStatus.ABSENT
);
attendance.remarks = faker.random.arrayElement(absenceRemarks);
} else {
attendance.status = this.availableStatusTypes.find(
attendance.status = defaultAttendanceStatusTypes.find(
(t) => t.countAs === AttendanceLogicalStatus.PRESENT
);
}
Expand Down
Loading

0 comments on commit 9c154b7

Please sign in to comment.