From 96b323774aa6481006e29669bc25757d6e4c3ebd Mon Sep 17 00:00:00 2001 From: Roberto Meza <67155761+Roberto-Meza@users.noreply.github.com> Date: Wed, 1 Sep 2021 16:12:14 -0600 Subject: [PATCH] DMND-674 Update open source repo with osmt-ui tests (#31) --- ui/package-lock.json | 35 +- ui/package.json | 6 +- ui/src/app/abstract.service.spec.ts | 302 +++++++++ ui/src/app/abstract.service.ts | 5 +- ui/src/app/app.metadata.ts | 5 + ui/src/app/app.module.ts | 12 +- ui/src/app/collection/ApiCollection.spec.ts | 36 ++ .../add-skills-collection.component.spec.ts | 237 +++++++ .../collection-skill-search.component.spec.ts | 234 +++++++ .../collections-list.component.spec.ts | 420 +++++++++++++ .../collection-form.component.spec.ts | 160 +++++ .../collection-public.component.spec.ts | 167 +++++ .../manage-collection.component.spec.ts | 497 +++++++++++++++ .../publish-collection.component.spec.ts | 183 ++++++ ui/src/app/core/environment.service.ts | 27 + ui/src/app/core/status-bar.component.spec.ts | 131 ++++ ui/src/app/job-codes/Jobcode.spec.ts | 19 + ui/src/app/loading/loading.component.spec.ts | 64 ++ ui/src/app/models/app-config.model.ts | 1 + .../abstract-search.component.spec.ts | 112 ++++ ui/src/app/richskill/ApiBatchResult.spec.ts | 19 + ui/src/app/richskill/ApiSkill.spec.ts | 190 ++++++ ui/src/app/richskill/ApiSkillSummary.spec.ts | 32 + ui/src/app/richskill/ApiSkillUpdate.spec.ts | 55 ++ .../AbstractRichSkillDetailComponent.spec.ts | 225 +++++++ .../detail/audit-log.component.spec.ts | 147 +++++ .../occupations-card-section.component.ts | 4 +- .../form/rich-skill-form.component.spec.ts | 583 ++++++++++++++++++ .../form/rich-skill-form.component.ts | 15 +- ...kill-collections-display.component.spec.ts | 112 ++++ .../import/batch-import.component.spec.ts | 387 ++++++++++++ .../list/skills-list.component.spec.ts | 473 ++++++++++++++ .../service/rich-skill.service.spec.ts | 142 +++++ .../richskill/service/rich-skill.service.ts | 2 +- ...vanced-search-action-bar.component.spec.ts | 96 +++ .../advanced-search.component.spec.ts | 75 +++ ui/src/app/search/search.service.spec.ts | 131 ++++ .../table/abstract-table.component.spec.ts | 227 +++++++ .../collections-library.component.spec.ts | 81 +++ .../pagination.component.spec.ts | 152 +++++ ui/src/app/task/ApiTaskResult.spec.ts | 18 + ui/src/app/toast/toast.component.spec.ts | 77 +++ ui/src/app/toast/toast.service.spec.ts | 87 +++ ui/src/test.ts | 1 + ui/src/whitelabel/whitelabel.json | 13 + ui/test/resource/mock-data.ts | 267 ++++++++ ui/test/resource/mock-stubs.ts | 249 ++++++++ ui/test/util/activated-route-stub.spec.ts | 51 ++ ui/test/util/deep-equals.ts | 20 + .../util/router-link-directive-stub.spec.ts | 17 + ui/test/util/test-page.spec.ts | 44 ++ ui/test/util/test-util.spec.ts | 26 + 52 files changed, 6653 insertions(+), 18 deletions(-) create mode 100644 ui/src/app/abstract.service.spec.ts create mode 100644 ui/src/app/app.metadata.ts create mode 100644 ui/src/app/collection/ApiCollection.spec.ts create mode 100644 ui/src/app/collection/add-skills-collection.component.spec.ts create mode 100644 ui/src/app/collection/collection-skill-search.component.spec.ts create mode 100644 ui/src/app/collection/collections-list.component.spec.ts create mode 100644 ui/src/app/collection/create-collection/collection-form.component.spec.ts create mode 100644 ui/src/app/collection/detail/collection-public/collection-public.component.spec.ts create mode 100644 ui/src/app/collection/detail/manage-collection.component.spec.ts create mode 100644 ui/src/app/collection/detail/publish-collection.component.spec.ts create mode 100644 ui/src/app/core/environment.service.ts create mode 100644 ui/src/app/core/status-bar.component.spec.ts create mode 100644 ui/src/app/job-codes/Jobcode.spec.ts create mode 100644 ui/src/app/loading/loading.component.spec.ts create mode 100644 ui/src/app/navigation/abstract-search.component.spec.ts create mode 100644 ui/src/app/richskill/ApiBatchResult.spec.ts create mode 100644 ui/src/app/richskill/ApiSkill.spec.ts create mode 100644 ui/src/app/richskill/ApiSkillSummary.spec.ts create mode 100644 ui/src/app/richskill/ApiSkillUpdate.spec.ts create mode 100644 ui/src/app/richskill/detail/AbstractRichSkillDetailComponent.spec.ts create mode 100644 ui/src/app/richskill/detail/audit-log.component.spec.ts create mode 100644 ui/src/app/richskill/form/rich-skill-form.component.spec.ts create mode 100644 ui/src/app/richskill/form/skill-collections-display.component.spec.ts create mode 100644 ui/src/app/richskill/import/batch-import.component.spec.ts create mode 100644 ui/src/app/richskill/list/skills-list.component.spec.ts create mode 100644 ui/src/app/richskill/service/rich-skill.service.spec.ts create mode 100644 ui/src/app/search/advanced-search/action-bar/abstract-advanced-search-action-bar.component.spec.ts create mode 100644 ui/src/app/search/advanced-search/advanced-search.component.spec.ts create mode 100644 ui/src/app/search/search.service.spec.ts create mode 100644 ui/src/app/table/abstract-table.component.spec.ts create mode 100644 ui/src/app/table/collections-library.component.spec.ts create mode 100644 ui/src/app/table/skills-library-table/pagination.component.spec.ts create mode 100644 ui/src/app/task/ApiTaskResult.spec.ts create mode 100644 ui/src/app/toast/toast.component.spec.ts create mode 100644 ui/src/app/toast/toast.service.spec.ts create mode 100644 ui/src/whitelabel/whitelabel.json create mode 100644 ui/test/resource/mock-data.ts create mode 100644 ui/test/resource/mock-stubs.ts create mode 100644 ui/test/util/activated-route-stub.spec.ts create mode 100644 ui/test/util/deep-equals.ts create mode 100644 ui/test/util/router-link-directive-stub.spec.ts create mode 100644 ui/test/util/test-page.spec.ts create mode 100644 ui/test/util/test-util.spec.ts diff --git a/ui/package-lock.json b/ui/package-lock.json index be3a6343d..eafb6477d 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1974,6 +1974,12 @@ "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", "dev": true }, + "@types/lodash": { + "version": "4.14.172", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz", + "integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==", + "dev": true + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -7405,6 +7411,23 @@ } } }, + "karma-sonarqube-unit-reporter": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/karma-sonarqube-unit-reporter/-/karma-sonarqube-unit-reporter-0.0.23.tgz", + "integrity": "sha512-Mp1b8pkZzFxRWS2eeEDzkrPXIlotRckLJ0C2fXUiUoilAVYJ5AiAyYN14cJzxGbRx65DdXBpu5LI+lt+MHigWA==", + "dev": true, + "requires": { + "xmlbuilder": "^13.0.2" + }, + "dependencies": { + "xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", + "dev": true + } + } + }, "karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -7570,9 +7593,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "lodash.memoize": { @@ -11919,9 +11942,9 @@ } }, "ssri": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", - "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", "dev": true, "requires": { "minipass": "^3.1.1" diff --git a/ui/package.json b/ui/package.json index 8f1cb1e4b..7ea577253 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,7 +10,8 @@ "test": "./node_modules/@angular/cli/bin/ng test", "lint": "./node_modules/@angular/cli/bin/ng lint", "e2e": "./node_modules/@angular/cli/bin/ng e2e", - "ci-test": "./node_modules/@angular/cli/bin/ng test --no-watch --no-progress --karma-config=karma.ci.conf.js" + "ci-test": "./node_modules/@angular/cli/bin/ng test --no-watch --no-progress --karma-config=karma.ci.conf.js", + "clean": "rm -rf dist; rm -rf coverage; rm -rf reports; rm -rf test-results; rm -rf node_modules; rm -rf src/env.js" }, "private": true, "dependencies": { @@ -42,6 +43,7 @@ "@angular/compiler-cli": "~10.0.9", "@types/jasmine": "~3.5.0", "@types/jasminewd2": "~2.0.3", + "@types/lodash": "^4.14.168", "@types/node": "^12.11.1", "codelyzer": "^6.0.0", "jasmine-core": "~3.5.0", @@ -52,6 +54,8 @@ "karma-jasmine": "~3.3.0", "karma-jasmine-html-reporter": "^1.5.0", "karma-junit-reporter": "^2.0.1", + "karma-sonarqube-unit-reporter": "^0.0.23", + "lodash": "^4.17.21", "protractor": "~7.0.0", "ts-node": "~8.3.0", "tslint": "~6.1.0", diff --git a/ui/src/app/abstract.service.spec.ts b/ui/src/app/abstract.service.spec.ts new file mode 100644 index 000000000..f361061b1 --- /dev/null +++ b/ui/src/app/abstract.service.spec.ts @@ -0,0 +1,302 @@ +import { Location } from "@angular/common" +import { HttpClient, HttpClientModule } from "@angular/common/http" +import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing" +import { Injectable } from "@angular/core" +import { async, TestBed } from "@angular/core/testing" +import { Data, Router } from "@angular/router" +import { Observable, of } from "rxjs" +import { map } from "rxjs/operators" +import { createMockTaskResult } from "../../test/resource/mock-data" +import { AuthServiceData, AuthServiceStub, RouterData, RouterStub } from "../../test/resource/mock-stubs" +import { AbstractService } from "./abstract.service" +import { AppConfig } from "./app.config" +import { AuthService } from "./auth/auth-service" +import { EnvironmentService } from "./core/environment.service" +import { PublishStatus } from "./PublishStatus" +import { ApiSortOrder } from "./richskill/ApiSkill" +import { ApiSearch } from "./richskill/service/rich-skill-search.service" +import { ApiTaskResult } from "./task/ApiTaskResult" + + +@Injectable({ + providedIn: "root" +}) +export class ConcreteService extends AbstractService { + constructor(httpClient: HttpClient, authService: AuthService, router: Router, location: Location) { + super(httpClient, authService, router, location) + } + + public buildUrl(path: string): string { + return super.buildUrl(path) + } + + public safeUnwrapBody(body: T | null, failureMessage: string): T { + return super.safeUnwrapBody(body, failureMessage) + } +} + +interface IWork { + foo: string // Data def that is not likely to collide with real code +} +class Work implements IWork { // This just follows the pattern used throughout the source code + foo: string + constructor(work: IWork) { + this.foo = work.foo + } + + doWork(id: string): Observable { + return of(new ApiTaskResult({ + status: PublishStatus.Draft, + contentType: "my content type", + id, + uuid: "my collection summary uuid" + })) + } +} + + +describe("AbstractService (no HTTP needed)", () => { + let router: RouterStub + let authService: AuthServiceStub + let testService: ConcreteService + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [], + imports: [ + HttpClientModule + ], + providers: [ + ConcreteService, + Location, + { provide: AuthService, useClass: AuthServiceStub }, + { provide: Router, useClass: RouterStub } + ] + }) + + router = TestBed.inject(Router) + authService = TestBed.inject(AuthService) + testService = TestBed.inject(ConcreteService) + })) + + it("should be created", () => { + expect(testService).toBeTruthy() + }) + + it("redirectToLogin with no status should ignore it", () => { + [ + { input: undefined, + output: { commands: [], isDown: false }}, + { input: { }, + output: { commands: [], isDown: false }}, + { input: { status: 401 }, + output: { commands: ["/login"], isDown: false }}, + { input: { status: 0 }, + output: { commands: [], isDown: true }} + ].forEach((params) => { + // Arrange + RouterData.commands = [] + AuthServiceData.isDown = false + + // Act + testService.redirectToLogin(params.input) + + // Assert + expect(RouterData.commands).toEqual(params.output.commands) + expect(AuthServiceData.isDown).toEqual(params.output.isDown) + }) + }) + + it("buildTableParams should be correct", () => { + // Arrange + const size = 5 + const from = 1 + const filter = new Set([PublishStatus.Published, PublishStatus.Draft]) + const sort = ApiSortOrder.NameAsc + + // Act + const params = testService.buildTableParams( + size, + from, + filter, + sort + ) + + // Assert + expect(params).toEqual({ + size, + from, + status: Array.from(filter).map(s => s.toString()), + sort: sort.toString() + }) + }) +}) + +describe("AbstractService (HTTP needed)", () => { + let httpClient: HttpClient + let httpTestingController: HttpTestingController + let router: RouterStub + let authService: AuthServiceStub + let testService: ConcreteService + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [], + imports: [ + HttpClientTestingModule + ], + providers: [ + EnvironmentService, + AppConfig, + ConcreteService, + Location, + { provide: AuthService, useClass: AuthServiceStub }, + { provide: Router, useClass: RouterStub } + ] + }) + .compileComponents() + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() + + httpClient = TestBed.inject(HttpClient) + httpTestingController = TestBed.inject(HttpTestingController) + router = TestBed.inject(Router) + authService = TestBed.inject(AuthService) + testService = TestBed.inject(ConcreteService) + })) + + afterEach(() => { + httpTestingController.verify() + }) + + it("should be created", () => { + expect(testService).toBeTruthy() + }) + + it("buildUrl should return", () => { + const path = "data" + expect(testService.buildUrl(path)).toEqual(AppConfig.settings.baseApiUrl + "/" + path) + }) + + /* See https://angular.io/guide/http#testing-http-requests */ + it("get should return", () => { + // Arrange + RouterData.commands = [] + AuthServiceData.isDown = false + const path = "any/path" + const testData: Data = { foo: "bar" } // Data that is unlikely to exist in any AbstractService derivative + + // Act + const result$ = testService.get({ path }) + + // Assert + result$ + .pipe(map(({body}) => body)) // Convert from HttpResponse to just Data + .subscribe(data => { + expect(data).toEqual(testData) + expect(RouterData.commands).toEqual([ ]) // No errors + expect(AuthServiceData.isDown).toEqual(false) + }) + + const req = httpTestingController.expectOne(AppConfig.settings.baseApiUrl + "/" + path) + expect(req.request.method).toEqual("GET") + req.flush(testData) + }) + + /* See https://angular.io/guide/http#testing-http-requests */ + it("post should return", () => { + // Arrange + RouterData.commands = [] + AuthServiceData.isDown = false + const path = "any/path" + const fullUrl = `${AppConfig.settings.baseApiUrl}/${path}` + const testData: Data = { foo: "bar" } // Data that is unlikely to exist in any AbstractService derivative + const errorMsg = `Could not unwrap Data exception...` + + // Act + const result$ = testService.post({ path }) + + // Assert + result$ + .pipe(map(({body}) => body)) // Convert from HttpResponse to just Data + .subscribe(unsafe => { + const data = testService.safeUnwrapBody(unsafe, errorMsg) + expect(data).toEqual(testData) + expect(RouterData.commands).toEqual([ ]) // No errors + expect(AuthServiceData.isDown).toEqual(false) + }) + + const req = httpTestingController.expectOne(fullUrl) + expect(req.request.method).toEqual("POST") + req.flush(testData) + }) + + it("bulkStatusChange should return", () => { + // Arrange + const newStatus = PublishStatus.Draft + const path = "any/path" + const fullUrl = `${AppConfig.settings.baseApiUrl}/${path}?newStatus=${newStatus.toString()}` + const query = "my query string" + const apiSearch = new ApiSearch({ query }) + const expected = new ApiTaskResult(createMockTaskResult()) + + // Act + const result$ = testService.bulkStatusChange( + path, + apiSearch, + newStatus + ) + + // Assert + result$ + .subscribe(data => + expect(data).toEqual(expected) + ) + + const req = httpTestingController.expectOne(fullUrl) + expect(req.request.method).toEqual("POST") + req.flush(expected) + }) + + it("pollForTaskResult should return", (done) => { + // Arrange + const testData: IWork = { foo: "bar" } // Data that is unlikely to exist in any AbstractService derivative + const path = "tasks/42" + const fullUrl = AppConfig.settings.baseApiUrl + "/" + path + const worker = new Work(testData) + + // Act + testService.pollForTaskResult(worker.doWork(path), 1000) + .subscribe((data) => { + expect(data).toEqual(testData) + done() + }) + const req = httpTestingController.expectOne(fullUrl) + req.flush(testData) + + // Assert + expect(req.request.method).toEqual("GET") + }) + + it("pollForTaskResult should error", (done) => { + // Arrange + const testData: IWork = { foo: "bar" } // Data that is unlikely to exist in any AbstractService derivative + const path = "tasks/42" + const fullUrl = AppConfig.settings.baseApiUrl + "/" + path + const worker = new Work(testData) + + // Act + testService.pollForTaskResult(worker.doWork(path), 1000) + .subscribe( + (data) => { + expect(data).toBeFalsy() + done() + }) + const req = httpTestingController.expectOne(fullUrl) + req.flush("Missing", { status: 404, statusText: "Not found" }) + + // Assert + expect(req.request.method).toEqual("GET") + }) +}) diff --git a/ui/src/app/abstract.service.ts b/ui/src/app/abstract.service.ts index a73e2e403..53541dbac 100644 --- a/ui/src/app/abstract.service.ts +++ b/ui/src/app/abstract.service.ts @@ -83,7 +83,7 @@ export abstract class AbstractService { const baseUrl = AppConfig.settings.baseApiUrl // if user defined, make sure it delineates between the host and path - if (baseUrl && !baseUrl.endsWith("/")) { + if (baseUrl && !baseUrl.endsWith("/") && !path.startsWith("/")) { return baseUrl + "/" + path } else { return baseUrl + path @@ -118,7 +118,8 @@ export abstract class AbstractService { return new Observable((observer) => { const tick = () => { - this.httpClient.get(task.id, { + // TODO: For DMND-635, we temporarily added the following buildUrl call, but the permanent fix should be in the back-end, not here. Fix the 2 tests for pollForTaskResults also, by passing fullUrl into doWork() instead of path. + this.httpClient.get(this.buildUrl(task.id), { headers: this.wrapHeaders(), observe: "response" }).subscribe(({body, status}) => { diff --git a/ui/src/app/app.metadata.ts b/ui/src/app/app.metadata.ts new file mode 100644 index 000000000..4ecdb4a0d --- /dev/null +++ b/ui/src/app/app.metadata.ts @@ -0,0 +1,5 @@ +declare function require(moduleName: string): any; + +export const appMetadata = { + package: require("../../package.json") +} diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 05243e4d2..1acce1fe9 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -3,6 +3,7 @@ import {APP_INITIALIZER, NgModule} from "@angular/core" import {AppRoutingModule} from "./app-routing.module" import {AppComponent} from "./app.component" import {HttpClientModule} from "@angular/common/http" +import {EnvironmentService} from "./core/environment.service" import {RichSkillsLibraryComponent} from "./richskill/library/rich-skills-library.component" import {RichSkillPublicComponent} from "./richskill/detail/rich-skill-public/rich-skill-public.component" import {AppConfig} from "./app.config" @@ -88,11 +89,11 @@ import {FormFieldSearchMultiSelectComponent} from "./form/form-field-search-sele import {FormFieldSearchSelectJobcodeComponent} from "./form/form-field-search-select/jobcode-select/form-field-search-select-jobcode.component" import {AuditLogComponent} from "./richskill/detail/audit-log.component" import {OccupationsCardSectionComponent} from "./richskill/detail/occupations-card-section/occupations-card-section.component" -import {CheckerComponent} from "./richskill/form/checker.component"; -import {SystemMessageComponent} from "./core/system-message.component"; -import {LogoutComponent} from "./auth/logout.component"; -import {NgIdleKeepaliveModule} from "@ng-idle/keepalive"; -import {LabelWithSelectComponent} from "./table/skills-library-table/label-with-select.component"; +import {CheckerComponent} from "./richskill/form/checker.component" +import {SystemMessageComponent} from "./core/system-message.component" +import {LogoutComponent} from "./auth/logout.component" +import {NgIdleKeepaliveModule} from "@ng-idle/keepalive" +import {LabelWithSelectComponent} from "./table/skills-library-table/label-with-select.component" export function initializeApp(appConfig: AppConfig): () => void { return () => appConfig.load() @@ -205,6 +206,7 @@ export function initializeApp(appConfig: AppConfig): () => void { CommonModule ], providers: [ + EnvironmentService, Title, AppConfig, FormDirtyGuard, diff --git a/ui/src/app/collection/ApiCollection.spec.ts b/ui/src/app/collection/ApiCollection.spec.ts new file mode 100644 index 000000000..1a254846c --- /dev/null +++ b/ui/src/app/collection/ApiCollection.spec.ts @@ -0,0 +1,36 @@ +import { createMockCollection, createMockCollectionUpdate } from "test/resource/mock-data" +import { deepEqualSkipOuterType, mismatched } from "../../../test/util/deep-equals" +import { PublishStatus } from "../PublishStatus" +import { ApiCollection, ApiCollectionUpdate, ICollection, ICollectionUpdate } from "./ApiCollection" + + +describe("ApiCollection", () => { + it("ApiCollection should be created", () => { + // Arrange + const date = new Date("2020-06-25T14:58:46.313Z") + const iCollection: ICollection = createMockCollection(date, date, date, date, PublishStatus.Draft) + + // Act + const apiCollection = new ApiCollection(iCollection) + + // Assert + expect(apiCollection).toBeTruthy() + expect(deepEqualSkipOuterType(apiCollection, iCollection)).toBeTruthy(mismatched(apiCollection, iCollection)) + }) + + + it("ApiCollectionUpdate should be created", () => { + // Arrange + const date = new Date("2020-06-25T14:58:46.313Z") + const iCollectionUpdate: ICollectionUpdate = createMockCollectionUpdate(date, date, date, date, PublishStatus.Draft) + + // Act + const apiCollectionUpdate = new ApiCollectionUpdate(iCollectionUpdate) + + // Assert + expect(apiCollectionUpdate).toBeTruthy() + /* Shallow equal check is good enough here because only 2 fields is non-primitive and is checked separately. */ + /* Cannot do deep check on Interface. i.e., expect(apiCollectionUpdate).toEqual(iCollectionUpdate) */ + expect(deepEqualSkipOuterType(apiCollectionUpdate, iCollectionUpdate)).toBeTruthy(mismatched(apiCollectionUpdate, iCollectionUpdate)) + }) +}) diff --git a/ui/src/app/collection/add-skills-collection.component.spec.ts b/ui/src/app/collection/add-skills-collection.component.spec.ts new file mode 100644 index 000000000..89dc4454c --- /dev/null +++ b/ui/src/app/collection/add-skills-collection.component.spec.ts @@ -0,0 +1,237 @@ +import { Location } from "@angular/common" +import { HttpClientTestingModule } from "@angular/common/http/testing" +import { Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { Title } from "@angular/platform-browser" +import { ActivatedRoute, Router } from "@angular/router" +import { RouterTestingModule } from "@angular/router/testing" +import { ActivatedRouteStubSpec } from "test/util/activated-route-stub.spec" +import { TestPage } from "test/util/test-page.spec" +import { createMockPaginatedCollections } from "../../../test/resource/mock-data" +import { CollectionServiceStub, EnvironmentServiceStub, RouterStub } from "../../../test/resource/mock-stubs" +import { AppConfig } from "../app.config" +import { initializeApp } from "../app.module" +import { EnvironmentService } from "../core/environment.service" +import { PublishStatus } from "../PublishStatus" +import { ApiSortOrder } from "../richskill/ApiSkill" +import { PaginatedCollections } from "../richskill/service/rich-skill-search.service" +import { ToastService } from "../toast/toast.service" +import { AddSkillsCollectionComponent } from "./add-skills-collection.component" +import { CollectionService } from "./service/collection.service" + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let activatedRoute: ActivatedRouteStubSpec +let component: AddSkillsCollectionComponent +let fixture: ComponentFixture + + +describe("AddSkillsCollectionComponent", () => { + const search = "testSearchQuery" + + beforeEach(() => { + activatedRoute = new ActivatedRouteStubSpec() + }) + + beforeEach(async(() => { + const routerSpy = ActivatedRouteStubSpec.createRouterSpy() + + TestBed.configureTestingModule({ + declarations: [ + AddSkillsCollectionComponent + ], + imports: [ + RouterTestingModule, + HttpClientTestingModule + ], + providers: [ + AppConfig, + Title, + Location, + ToastService, + { provide: EnvironmentService, useClass: EnvironmentServiceStub }, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useClass: RouterStub }, + { provide: CollectionService, useClass: CollectionServiceStub }, + ] + }) + .compileComponents() + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() + + activatedRoute.setParamMap({ userId: 126 }) + createComponent(AddSkillsCollectionComponent) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + it("ngOnInit should be correct", () => { + expect(component.totalCount).toBeFalsy() + }) + + it("handleDefaultSubmit should load", () => { + // Arrange + component.from = 10 + + // Act + const result = component.handleDefaultSubmit() + + // Assert + expect(result).toBeFalse() + expect(component.from).toEqual(0) + // Note that we're not bothering to check the results of loadNextPage()! + }) + + it("loadNextPage should load nothing", () => { + // Arrange + const totalCount = createMockPaginatedCollections().totalCount + + // Act + component.loadNextPage() + + // Assert + expect(component.results).toBeTruthy() + if (component.results) { + expect((component.results as PaginatedCollections).totalCount).toEqual(0) + } + }) + + it("loadNextPage should load something", () => { + // Arrange + component.results = undefined + component.searchForm.setValue({search}) + const totalCount = createMockPaginatedCollections().totalCount + + // Act + component.loadNextPage() + while (!component.results) {} + + // Assert + expect(component.results).toBeTruthy() + if (component.results) { + expect((component.results as PaginatedCollections).totalCount).toEqual(totalCount) + } + }) + + it("isPlural should be correct", () => { + // Arrange + component.state = { + selectedSkills: [], + totalCount: 0, + } + // Act/Assert + expect(component.isPlural).toBeFalse() + + // Arrange + component.state = { + selectedSkills: [], + totalCount: 2, + } + // Act/Assert + expect(component.isPlural).toBeTruthy() + }) + + it("clearSearch should clear", () => { + component.searchForm.setValue({search}) + expect(component.searchQuery).toEqual(search) + expect(component.clearSearch()).toBeFalse() + expect(component.searchQuery).toEqual("") + }) + + it("handleFiltersChanged should be correct", () => { + // Arrange + component.results = undefined + component.searchForm.setValue({search}) + const filters = new Set([ PublishStatus.Unarchived, PublishStatus.Deleted ]) + const totalCount = createMockPaginatedCollections().totalCount + + // Act + component.handleFiltersChanged(filters) + while (!component.results) {} + + // Assert + expect(component.results).toBeTruthy() + if (component.results) { + expect((component.results as PaginatedCollections).totalCount).toEqual(totalCount) + } + }) + + it("handlePageClicked should be correct", () => { + // Arrange + component.from = 23 + component.size = 17 + const newPage = 42 + const expected = (newPage - 1) * 17 + component.results = undefined + component.searchForm.setValue({search}) + const totalCount = createMockPaginatedCollections().totalCount + + // Act + component.handlePageClicked(newPage) + while (!component.results) {} + + // Assert + expect(component.from).toEqual(expected) + expect(component.results).toBeTruthy() + if (component.results) { + expect((component.results as PaginatedCollections).totalCount).toEqual(totalCount) + } + }) + + it("handleHeaderColumnSort should be correct", () => { + // Arrange + component.columnSort = ApiSortOrder.SkillAsc + component.from = 10 + component.results = undefined + component.searchForm.setValue({search}) + const totalCount = createMockPaginatedCollections().totalCount + const expected = ApiSortOrder.NameDesc + + // Act + component.handleHeaderColumnSort(expected) + while (!component.results) {} + + // Assert + expect(component.columnSort).toEqual(expected) + expect(component.from).toEqual(0) + expect(component.results).toBeTruthy() + if (component.results) { + expect((component.results as PaginatedCollections).totalCount).toEqual(totalCount) + } + }) + + it("firstRecordNo should be correct", () => { + // Arrange + component.from = 11 + const expected = 12 + + // Act/Assert + expect(component.firstRecordNo).toEqual(expected) + }) + + it("lastRecordNo should be correct", () => { + // Arrange + component.from = 17 + component.results = createMockPaginatedCollections(3, 51) + const expected = Math.min(17 + 3, 51) + + // Act/Assert + expect(component.lastRecordNo).toEqual(expected) + }) +}) diff --git a/ui/src/app/collection/collection-skill-search.component.spec.ts b/ui/src/app/collection/collection-skill-search.component.spec.ts new file mode 100644 index 000000000..1b8be529b --- /dev/null +++ b/ui/src/app/collection/collection-skill-search.component.spec.ts @@ -0,0 +1,234 @@ +import { Location } from "@angular/common" +import { HttpClientTestingModule } from "@angular/common/http/testing" +import { Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { Title } from "@angular/platform-browser" +import { RouterTestingModule } from "@angular/router/testing" +import { createMockPaginatedSkills, createMockSkillSummary } from "../../../test/resource/mock-data" +import { CollectionServiceStub, RichSkillServiceStub } from "../../../test/resource/mock-stubs" +import { AppConfig } from "../app.config" +import { EnvironmentService } from "../core/environment.service" +import { ApiSearch, PaginatedSkills } from "../richskill/service/rich-skill-search.service" +import { RichSkillService } from "../richskill/service/rich-skill.service" +import { TableActionDefinition } from "../table/skills-library-table/has-action-definitions" +import { ToastService } from "../toast/toast.service" +import { CollectionSkillSearchComponent } from "./collection-skill-search.component" +import { CollectionService } from "./service/collection.service" + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let component: CollectionSkillSearchComponent +let fixture: ComponentFixture + + +describe("CollectionSkillSearchComponent", () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + CollectionSkillSearchComponent + ], + imports: [ + HttpClientTestingModule, + RouterTestingModule, + ], + providers: [ + EnvironmentService, + AppConfig, + Title, + Location, + ToastService, + { provide: CollectionService, useClass: CollectionServiceStub }, + { provide: RichSkillService, useClass: RichSkillServiceStub }, + ] + }) + .compileComponents() + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() // This avoids the race condition on reading the config's whitelabel.toolName + + createComponent(CollectionSkillSearchComponent) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + it("handleDefaultSubmit should be correct", () => { + // Arrange + spyOn(component, "loadNextPage") + component.from = 1 + + // Act + component.handleDefaultSubmit() + + // Assert + expect(component.from).toEqual(0) + expect(component.loadNextPage).toHaveBeenCalled() + }) + + it("loadNextPage should load none", () => { + // Arrange + const search = "" + component.searchForm.setValue({search}) + + // Act + component.loadNextPage() + + // Assert + expect(component.results).toEqual(new PaginatedSkills([], 0)) + }) + + it("loadNextPage should load some", () => { + // Arrange + const search = "testSearchValue" + component.searchForm.setValue({search}) + + // Act + component.loadNextPage() + + // Assert + expect(component.results).toEqual(createMockPaginatedSkills()) + }) + + it("clearSearch should reset form", () => { + // Arrange + spyOn(component.searchForm, "reset") + + // Act + const result = component.clearSearch() + + // Assert + expect(result).toBeFalse() + expect(component.searchForm.reset).toHaveBeenCalled() + }) + + it("rowActions should return 1", () => { + // Arrange + const expected = new TableActionDefinition({ + label: "Add to Collection", + callback: undefined + }) + + // Act + const result = component.rowActions() + + // Assert + expect(result[0].label).toEqual(expected.label) + }) + + it("actionsVisible should be correct", () => { + // Arrange + component.results = undefined + // Act/Assert + expect(component.actionsVisible()).toBeFalse() + + // Arrange + component.results = new PaginatedSkills([], 0) + // Act/Assert + expect(component.actionsVisible()).toBeTrue() + }) + + it("tableActions should return 2", () => { + // Arrange + const expected = [ + new TableActionDefinition({ + label: "Back to Top", + icon: "up", + offset: true, + callback: undefined + }), + new TableActionDefinition({ + label: "Add to Collection", + icon: "collection", + primary: true, + callback: undefined, + visible: undefined + }) + ] + + // Act + const result = component.tableActions() + + // Assert + expect(result.length).toEqual(expected.length) + for (let j = 0; j < expected.length; ++j) { + expect(result[j].label).toEqual(expected[j].label) + expect(result[j].icon).toEqual(expected[j].icon) + expect(result[j].offset).toEqual(expected[j].offset) + expect(result[j].primary).toEqual(expected[j].primary) + } + }) + + it("handleClickAddCollection should return", () => { + // Arrange + const row = component.rowActions()[0] + const skill = createMockSkillSummary() + + // Act + let result + if (row.callback) { + result = row.callback(row, skill) + } + + // Assert + expect(result).toBeFalse() + }) + + it("getApiSearch should return canned search", () => { + // Arrange + component.multiplePagesSelected = true + const search = "testQueryString" + component.searchForm.setValue({search}) + + // Act + const result = component.getApiSearch() + + // Assert + expect(result).toEqual(new ApiSearch({ query: search })) + }) + + it("getApiSearch should return specified search", () => { + // Arrange + component.multiplePagesSelected = false + const skill = createMockSkillSummary() + + // Act + const result = component.getApiSearch(skill) + + // Assert + expect(result).toEqual(new ApiSearch({ uuids: [ skill.uuid ] } )) + }) + + it("handleSelectAll should be correct", () => { + // Arrange + component.results = new PaginatedSkills([], 10) + component.size = 5 + + // Act + component.handleSelectAll(true) + // Assert + expect(component.multiplePagesSelected).toBeTrue() + + // Arrange + component.results = new PaginatedSkills([], 5) + component.size = 5 + + // Act + component.handleSelectAll(true) + // Assert + expect(component.multiplePagesSelected).toBeFalse() + }) +}) diff --git a/ui/src/app/collection/collections-list.component.spec.ts b/ui/src/app/collection/collections-list.component.spec.ts new file mode 100644 index 000000000..ff0b200d4 --- /dev/null +++ b/ui/src/app/collection/collections-list.component.spec.ts @@ -0,0 +1,420 @@ +// noinspection MagicNumberJS,LocalVariableNamingConventionJS + +import { Component, Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { RouterTestingModule } from "@angular/router/testing" +import { createMockCollectionSummary, createMockPaginatedCollections } from "../../../test/resource/mock-data" +import { CollectionServiceStub } from "../../../test/resource/mock-stubs" +import { PublishStatus } from "../PublishStatus" +import { ApiSortOrder } from "../richskill/ApiSkill" +import { ApiSearch, PaginatedCollections } from "../richskill/service/rich-skill-search.service" +import { ToastService } from "../toast/toast.service" +import { CollectionsListComponent } from "./collections-list.component" +import { CollectionService } from "./service/collection.service" + + +@Component({ + selector: "app-concrete-component", + template: `` +}) +class ConcreteComponent extends CollectionsListComponent { + matchingQuery?: string[] + title = "Concrete Collections" + + loadNextPage(): void {} + + handleSelectAll(selectAllChecked: boolean): void {} + + public setResults(results: PaginatedCollections): void { + super.setResults(results) + } +} + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let component: ConcreteComponent +let fixture: ComponentFixture + + +describe("CollectionsListComponent", () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + ConcreteComponent + ], + imports: [ + RouterTestingModule // CollectionsListComponent depends on the Router + ], + providers: [ + ToastService, + { provide: CollectionService, useClass: CollectionServiceStub }, + ] + }) + + createComponent(ConcreteComponent) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + it("title should be correct", () => { + expect(component.title).toEqual("Concrete Collections") + }) + + it("setResults should set results", () => { + // Arrange + const paginatedCollections = createMockPaginatedCollections() + + // Act + component.setResults(paginatedCollections) + + // Assert + expect(component.results).toEqual(paginatedCollections) + expect(component.selectedCollections).toBeFalsy() + expect(component.totalCount).toEqual(paginatedCollections.totalCount) + expect(component.curPageCount).toEqual(paginatedCollections.collections.length) + expect(component.getSelectAllCount()).toEqual(component.curPageCount) + }) + + it("collectionCountLabel should be correct", () => { + component.setResults(createMockPaginatedCollections(0, 0)) + expect(component.collectionCountLabel).toEqual("0 collections") + + component.setResults(createMockPaginatedCollections(0, 1)) + expect(component.collectionCountLabel).toEqual("1 collection") + + component.setResults(createMockPaginatedCollections(1, 1)) + expect(component.collectionCountLabel).toEqual("1 collection") + + component.setResults(createMockPaginatedCollections(1, 10)) + expect(component.collectionCountLabel).toEqual("10 collections") + }) + + it("totalCount should be correct", () => { + component.setResults(createMockPaginatedCollections(11, 23)) + expect(component.totalCount).toEqual(23) + }) + + it("curPageCount should be correct", () => { + component.setResults(createMockPaginatedCollections(11, 23)) + expect(component.curPageCount).toEqual(11) + }) + + it("getMobileSortOptions should be correct", () => { + const result = component.getMobileSortOptions() + expect(result).toEqual({ + "name.asc": "Collection name (ascending)", + "name.desc": "Collection name (descending)", + "skill.asc": "Skill count (ascending)", + "skill.desc": "Skill count (descending)", + }) + }) + + it("emptyResults should be correct", () => { + component.setResults(createMockPaginatedCollections(0, 0)) + expect(component.emptyResults).toBeTruthy() + + component.setResults(createMockPaginatedCollections(1, 1)) + expect(component.emptyResults).toBeFalsy() + }) + + it("firstRecordNo should return", () => { + // Note that we're choosing primes for inputs to avoid math irregularities + component.from = 11 + expect(component.firstRecordNo).toEqual(12) + }) + + it("lastRecordNo should return", () => { + component.from = 11 + component.size = 47 + component.setResults(createMockPaginatedCollections(29, 123)) + expect(component.lastRecordNo).toEqual(11 + 29) // Expecting from+curPageCount + + component.from = 11 + component.size = 47 + component.setResults(createMockPaginatedCollections(12, 13)) + expect(component.lastRecordNo).toEqual(13) // Expecting totalCount + }) + + it("totalPageCount should return", () => { + component.size = 47 + component.setResults(createMockPaginatedCollections(29, 123)) + expect(component.totalPageCount).toEqual(Math.trunc((123 + 46) / 47)) + }) + + it("currentPageNo should return", () => { + component.from = 11 + component.size = 47 + expect(component.currentPageNo).toEqual(Math.trunc(11 / 47) + 1) + }) + + it("navigateToPage should load next page", () => { + spyOn(component, "loadNextPage").and.callThrough() + + component.from = 11 + component.size = 47 + component.navigateToPage(31) + expect(component.from).toEqual((31 - 1) * 47) + expect(component.loadNextPage).toHaveBeenCalledTimes(1) + }) + + it("actionsVisible should be true", () => { + expect(component.actionsVisible()).toBeTruthy() + }) + + it("publishVisible should be correct", () => { + /* Assumption: status doesn't matter! */ + // id, date undefined + const result0 = component.publishVisible( + createMockCollectionSummary("id1", PublishStatus.Draft, "") + ) + expect(result0).toBeTruthy() + + // No collections + component.selectedCollections = [] + const result1 = component.publishVisible(undefined) + expect(result1).toBeFalsy() + + // one with date, one without + component.selectedCollections = [ + createMockCollectionSummary("id1", PublishStatus.Draft, ""), // No publishDate + createMockCollectionSummary("id1", PublishStatus.Draft) // Has publishDate + ] + const result2 = component.publishVisible(undefined) + expect(result2).toBeTruthy() + }) + + it("archiveVisible should be correct", () => { + /* Assumption: status *does* matter. */ + // id, different status + const result0 = component.archiveVisible( + createMockCollectionSummary("id1", PublishStatus.Draft) + ) + expect(result0).toBeTruthy() + + // No collections + component.selectedCollections = [] + const result1 = component.archiveVisible(undefined) + expect(result1).toBeFalsy() + + // one with date, one without + component.selectedCollections = [ + createMockCollectionSummary("id1", PublishStatus.Draft), + createMockCollectionSummary("id1", PublishStatus.Archived) + ] + const result2 = component.archiveVisible(undefined) + expect(result2).toBeTruthy() + }) + + it("unarchiveVisible should be correct", () => { + /* Assumption: status *does* matter. */ + // id, different status + const result0 = component.unarchiveVisible( + createMockCollectionSummary("id1", PublishStatus.Archived) + ) + expect(result0).toBeTruthy() + + // No collections + component.selectedCollections = [] + const result1 = component.unarchiveVisible(undefined) + expect(result1).toBeFalsy() + + // one with date, one without + component.selectedCollections = [ + createMockCollectionSummary("id1", PublishStatus.Draft), + createMockCollectionSummary("id1", PublishStatus.Archived) + ] + const result2 = component.unarchiveVisible(undefined) + expect(result2).toBeTruthy() + }) + + it("handleFiltersChanged should be correct", () => { + // Arrange + const newFilters = new Set([PublishStatus.Unarchived]) + spyOn(component, "loadNextPage").and.callThrough() + let called = false + component.clearSelectedItemsFromTable.subscribe(() => { + called = true + }) + + // Act + component.handleFiltersChanged(newFilters) + while (!called) {} + + // Assert + expect(component.selectedFilters).toEqual(newFilters) + expect(component.loadNextPage).toHaveBeenCalledTimes(1) + }) + + it("handlePageClicked should be correct", () => { + spyOn(component, "loadNextPage").and.callThrough() + + component.from = 11 + component.size = 47 + component.handlePageClicked(31) + expect(component.from).toEqual((31 - 1) * 47) + expect(component.loadNextPage).toHaveBeenCalledTimes(1) + }) + + it("handleNewSelection should be correct", () => { + const selected = createMockPaginatedCollections(2, 3).collections + component.handleNewSelection(selected) + expect(component.selectedCollections).toEqual(selected) + }) + + it("handleHeaderColumnSort should load next page", () => { + // Arrange + spyOn(component, "loadNextPage").and.callThrough() + const sort = ApiSortOrder.SkillDesc + component.columnSort = ApiSortOrder.NameAsc + component.from = 47 + + // Act + component.handleHeaderColumnSort(sort) + + // Assert + expect(component.columnSort).toEqual(sort) + expect(component.from).toEqual(0) + expect(component.loadNextPage).toHaveBeenCalledTimes(1) + }) + + it("rowActions should be correct", () => { + const rowActions = component.rowActions() + expect(rowActions).toBeTruthy() + + const collection0 = createMockCollectionSummary("id1", PublishStatus.Draft) + const action0 = rowActions[0] + expect(action0.label).toEqual("Archive collection") + expect(action0 && action0.callback).toBeTruthy() + expect(action0.callback?.(action0, collection0)).toBeFalsy() // Always false + expect(action0.visible?.(collection0)).toBeTruthy() // != Archived + + const collection1 = createMockCollectionSummary("id2", PublishStatus.Archived) + const action1 = rowActions[1] + expect(action1.label).toEqual("Unarchive collection") + expect(action1 && action1.callback).toBeTruthy() + expect(action1.callback?.(action1, collection1)).toBeFalsy() // Always false + expect(action1.visible?.(collection1)).toBeTruthy() // == Archived + + spyOn(window, "confirm").and.returnValue(true) + const collection2 = createMockCollectionSummary("id3", PublishStatus.Draft, "") + const action2 = rowActions[2] + expect(action2.label).toEqual("Publish collection") + expect(action2 && action2.callback).toBeTruthy() + expect(action2.callback?.(action2, collection2)).toBeFalsy() // Always false + expect(action2.visible?.(collection2)).toBeTruthy() // !has publish date + }) + + it("tableActions should be correct", () => { + const tableActions = component.tableActions() + expect(tableActions).toBeTruthy() + + const collection0 = createMockCollectionSummary("id3", PublishStatus.Draft, "") + const action0 = tableActions[0] + expect(action0.label).toEqual("Back to Top") + expect(action0 && action0.callback).toBeTruthy() + expect(action0.callback?.(action0, collection0)).toBeFalsy() // Always false + expect(action0.visible?.(collection0)).toBeTruthy() // Always true + + spyOn(window, "confirm").and.returnValue(true) + const collection1 = createMockCollectionSummary("id3", PublishStatus.Draft, "") + const action1 = tableActions[1] + expect(action1.label).toEqual("Publish") + expect(action1 && action1.callback).toBeTruthy() + expect(action1.callback?.(action1, collection1)).toBeFalsy() // Always false + expect(action1.visible?.(collection1)).toBeTruthy() // !has publish date + + const collection2 = createMockCollectionSummary("id1", PublishStatus.Draft) + const action2 = tableActions[2] + expect(action2.label).toEqual("Archive") + expect(action2 && action2.callback).toBeTruthy() + expect(action2.callback?.(action2, collection2)).toBeFalsy() // Always false + expect(action2.visible?.(collection2)).toBeTruthy() // == Archived + + const collection3 = createMockCollectionSummary("id2", PublishStatus.Archived) + const action3 = tableActions[3] + expect(action3.label).toEqual("Unarchive") + expect(action3 && action3.callback).toBeTruthy() + expect(action3.callback?.(action3, collection3)).toBeFalsy() // Always false + expect(action3.visible?.(collection3)).toBeTruthy() // != Archived + }) + + it("getSelectedSkills should be correct", () => { + component.selectedCollections = [ + createMockCollectionSummary("id1", PublishStatus.Draft), + ] + + expect(component.getSelectedSkills( + undefined + )).toEqual(component.selectedCollections) + + const collection = createMockCollectionSummary() + expect(component.getSelectedSkills( + )).toEqual([collection]) + }) + + it("selectedUuids should be correct", () => { + component.selectedCollections = [ + createMockCollectionSummary("id1", PublishStatus.Draft), + ] + + expect(component.selectedUuids( + undefined + )).toEqual([component.selectedCollections[0].uuid]) + + const collection = createMockCollectionSummary() + expect(component.selectedUuids( + )).toEqual([collection.uuid]) + }) + + it("getApiSearch should be correct", () => { + const collection = createMockCollectionSummary("id2", PublishStatus.Draft) + const uuids = [collection.uuid] + + expect(component.getApiSearch(collection)).toEqual( + new ApiSearch({ uuids }) + ) + + component.selectedCollections = undefined + expect(component.getApiSearch(undefined)).toEqual( + undefined + ) + + component.selectedCollections = [ + createMockCollectionSummary("id1", PublishStatus.Draft) + ] + expect(component.getApiSearch(undefined)).toEqual( + new ApiSearch({ uuids: [component.selectedCollections[0].uuid] }) + ) + }) + + it("submitStatusChange should be correct", () => { + component.selectedCollections = undefined + expect(component.submitStatusChange(PublishStatus.Published, "published", undefined)).toEqual( + false + ) + + const collection = createMockCollectionSummary("id2", PublishStatus.Draft) + expect(component.submitStatusChange(PublishStatus.Published, "published", collection)).toEqual( + false + ) + }) + + it("getSelectAllEnabled should be true", () => { + expect(component.getSelectAllEnabled()).toBeTruthy() + }) +}) diff --git a/ui/src/app/collection/create-collection/collection-form.component.spec.ts b/ui/src/app/collection/create-collection/collection-form.component.spec.ts new file mode 100644 index 000000000..4fc70e76e --- /dev/null +++ b/ui/src/app/collection/create-collection/collection-form.component.spec.ts @@ -0,0 +1,160 @@ +import { Location } from "@angular/common" +import { HttpClientTestingModule } from "@angular/common/http/testing" +import { Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { FormsModule } from "@angular/forms" +import { Title } from "@angular/platform-browser" +import { ActivatedRoute, Router } from "@angular/router" +import { RouterTestingModule } from "@angular/router/testing" +import { ActivatedRouteStubSpec } from "test/util/activated-route-stub.spec" +import { TestPage } from "test/util/test-page.spec" +import { CollectionServiceStub, EnvironmentServiceStub } from "../../../../test/resource/mock-stubs" +import { AppConfig } from "../../app.config" +import { initializeApp } from "../../app.module" +import { EnvironmentService } from "../../core/environment.service" +import { ApiNamedReference } from "../../richskill/ApiSkill" +import { ToastService } from "../../toast/toast.service" +import { CollectionService } from "../service/collection.service" +import { CollectionFormComponent } from "./collection-form.component" + + +class Page extends TestPage { + get myElement(): HTMLInputElement { return this.query("#mycomponent-mytype-myelement") } + + // myMethodSpy: jasmine.Spy + + constructor(aFixture: ComponentFixture) { + super(aFixture) + + const aComponent = aFixture.componentInstance + // this.myMethodSpy = spyOn(aComponent, 'myMethodSpy').and.callThrough(); + } +} + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + page = new Page(fixture) + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let activatedRoute: ActivatedRouteStubSpec +let component: CollectionFormComponent +let fixture: ComponentFixture +let page: Page + + +describe("CollectionFormComponent", () => { + beforeEach(() => { + activatedRoute = new ActivatedRouteStubSpec() + }) + + beforeEach(async(() => { + const routerSpy = ActivatedRouteStubSpec.createRouterSpy() + + TestBed.configureTestingModule({ + declarations: [ + CollectionFormComponent + ], + imports: [ + RouterTestingModule, + HttpClientTestingModule + ], + providers: [ + AppConfig, + Location, + Title, + ToastService, + { provide: EnvironmentService, useClass: EnvironmentServiceStub }, + { provide: CollectionService, useClass: CollectionServiceStub }, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: routerSpy }, + ] + }) + .compileComponents() + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() + + const environmentService = TestBed.inject(EnvironmentService) + environmentService.environment.editableAuthor = true + AppConfig.settings.editableAuthor = true // Doubly sure + + activatedRoute.setParamMap({ uuid: "uuid1" }) + createComponent(CollectionFormComponent) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + it("should initialize", () => { + expect(component.collectionUuid).toEqual("uuid1") + expect(component.collectionForm.get("collectionName")?.value).toEqual("my collection name") + }) + + it("nameLabel should return", () => { + component.collectionUuid = "" + expect(component.nameLabel).toEqual("New Collection Name") + + component.collectionUuid = "uuid" + expect(component.nameLabel).toEqual("Collection Name") + }) + + it("formGroup should return", () => { + expect(component.formGroup()).toBeTruthy() + }) + + it("namedReferenceForString should be correct", () => { + expect(component.namedReferenceForString("")).toEqual(undefined) + expect(component.namedReferenceForString("a://b")).toEqual(new ApiNamedReference({ id: "a://b" })) + expect(component.namedReferenceForString("abc")).toEqual(new ApiNamedReference({ name: "abc" })) + }) + + it("updateObject should return", () => { + // Arrange + const value = { + collectionName: "collection1", + author: "author1" + } + component.collectionForm.setValue(value) + + // Act + const result = component.updateObject() + + // Assert + expect(result.name).toEqual(value.collectionName) + expect(result.author?.name).toEqual(value.author) + }) + + it("onSubmit should be correct", (done) => { + // Arrange + const uuid = "uuid1" + component.collectionUuid = uuid + const value = { + collectionName: "collection1", + author: "author1" + } + component.collectionForm.setValue(value) + const router = TestBed.inject(Router) + + // Act + component.onSubmit() + + // Assert + component.collectionSaved?.subscribe((collection) => { + expect(collection.uuid).toEqual(uuid) + expect(router.navigate).toHaveBeenCalledWith([ "/collections/uuid1/manage" ]) + done() + }) + }) +}) diff --git a/ui/src/app/collection/detail/collection-public/collection-public.component.spec.ts b/ui/src/app/collection/detail/collection-public/collection-public.component.spec.ts new file mode 100644 index 000000000..6c236479e --- /dev/null +++ b/ui/src/app/collection/detail/collection-public/collection-public.component.spec.ts @@ -0,0 +1,167 @@ +import { HttpClientTestingModule } from "@angular/common/http/testing" +import { Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { Title } from "@angular/platform-browser" +import { ActivatedRoute, Router } from "@angular/router" +import { RouterTestingModule } from "@angular/router/testing" +import { ActivatedRouteStubSpec } from "test/util/activated-route-stub.spec" +import { createMockPaginatedSkills } from "../../../../../test/resource/mock-data" +import { CollectionServiceStub, RichSkillServiceStub } from "../../../../../test/resource/mock-stubs" +import { AppConfig } from "../../../app.config" +import { EnvironmentService } from "../../../core/environment.service" +import { ApiSortOrder } from "../../../richskill/ApiSkill" +import { RichSkillService } from "../../../richskill/service/rich-skill.service" +import { ToastService } from "../../../toast/toast.service" +import { CollectionService } from "../../service/collection.service" +import { CollectionPublicComponent } from "./collection-public.component" + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let activatedRoute: ActivatedRouteStubSpec +let component: CollectionPublicComponent +let fixture: ComponentFixture + + +describe("CollectionPublicComponent", () => { + beforeEach(() => { + activatedRoute = new ActivatedRouteStubSpec() + }) + + beforeEach(async(() => { + const routerSpy = ActivatedRouteStubSpec.createRouterSpy() + + TestBed.configureTestingModule({ + declarations: [ + CollectionPublicComponent + ], + imports: [ + RouterTestingModule, + HttpClientTestingModule + ], + providers: [ + AppConfig, + EnvironmentService, + Title, + ToastService, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: routerSpy }, + { provide: RichSkillService, useClass: RichSkillServiceStub }, + { provide: CollectionService, useClass: CollectionServiceStub }, + ] + }).compileComponents() + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() + + activatedRoute.setParamMap({ uuid: "uuid1" }) + createComponent(CollectionPublicComponent) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + expect(component.collection).toBeTruthy() + expect(component.resultsLoaded).toBeTruthy() + }) + + it("title should be correct", () => { + const titleService = TestBed.inject(Title) + expect(titleService.getTitle()).toEqual("my collection name | Collection | OSMT") + }) + + it("totalCount should be correct", () => { + const results = createMockPaginatedSkills() // Loaded by ngOnInit + expect(component.totalCount).toEqual(results.totalCount) + }) + + it("emptyResults should be correct", () => { + expect(component.emptyResults).toBeFalsy() + }) + + it("curPageCount should be correct", () => { + const results = createMockPaginatedSkills() // Loaded by ngOnInit + expect(component.curPageCount).toEqual(results.skills.length) + }) + + it("totalPageCount should be correct", () => { + const totalPageCount = 1 // Loaded by ngOnInit + expect(component.totalPageCount).toEqual(totalPageCount) + }) + + it("currentPageNo should be correct", () => { + const curPageCount = 1 // Loaded by ngOnInit + expect(component.currentPageNo).toEqual(curPageCount) + }) + + it("collectionUrl should be correct", () => { + expect(component.collectionUrl).toEqual("id1") + }) + + it("collectionUuid should be correct", () => { + const curPageCount = 1 // Loaded by ngOnInit + expect(component.collectionUuid).toEqual("uuid1") + }) + + it("collectionName should be correct", () => { + const curPageCount = 1 // Loaded by ngOnInit + expect(component.collectionName).toEqual("my collection name") + }) + + it("loadSkillsInCollection should be correct", () => { + // Arrange + const expected = createMockPaginatedSkills() + component.results = undefined + + // Act + component.loadSkillsInCollection() + while (!component.results) {} + + // Assert + expect(component.results).toEqual(expected) + }) + + it("handleHeaderColumnSort should be correct", () => { + // Arrange + const expected = createMockPaginatedSkills() + component.columnSort = ApiSortOrder.SkillAsc + component.from = 1 + component.results = undefined + + // Act + component.handleHeaderColumnSort(ApiSortOrder.NameDesc) + while (!component.results) {} + + // Assert + expect(component.columnSort).toEqual(ApiSortOrder.NameDesc) + expect(component.from).toEqual(0) + expect(component.results).toEqual(expected) + }) + + it("handlePageClicked should navigate", () => { + // Arrange + const expected = createMockPaginatedSkills() + component.from = 111 + component.results = undefined + + // Act + component.handlePageClicked(13) + while (!component.results) {} + + // Assert + expect(component.size).toEqual(50) + expect(component.from).toEqual((13 - 1) * 50) + expect(component.results).toEqual(expected) + }) +}) diff --git a/ui/src/app/collection/detail/manage-collection.component.spec.ts b/ui/src/app/collection/detail/manage-collection.component.spec.ts new file mode 100644 index 000000000..1b332b643 --- /dev/null +++ b/ui/src/app/collection/detail/manage-collection.component.spec.ts @@ -0,0 +1,497 @@ +// noinspection MagicNumberJS,LocalVariableNamingConventionJS + +import { HttpClientTestingModule } from "@angular/common/http/testing" +import { Component, ElementRef, Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { Title } from "@angular/platform-browser" +import { Router } from "@angular/router" +import { RouterTestingModule } from "@angular/router/testing" +import { of } from "rxjs" +import { + createMockCollection, + createMockPaginatedSkills, + createMockSkillSummary +} from "../../../../test/resource/mock-data" +import { + CollectionServiceStub, + EnvironmentServiceStub, + RichSkillServiceStub +} from "../../../../test/resource/mock-stubs" +import { AppConfig } from "../../app.config" +import { initializeApp } from "../../app.module" +import { EnvironmentService } from "../../core/environment.service" +import { PublishStatus } from "../../PublishStatus" +import { ApiSkillSummary } from "../../richskill/ApiSkillSummary" +import { ApiSearch, PaginatedSkills } from "../../richskill/service/rich-skill-search.service" +import { RichSkillService } from "../../richskill/service/rich-skill.service" +import { ToastService } from "../../toast/toast.service" +import { ApiCollection } from "../ApiCollection" +import { CollectionService } from "../service/collection.service" +import { ManageCollectionComponent } from "./manage-collection.component" + + +@Component({ + selector: "app-concrete-component", + template: `` +}) +class ConcreteComponent extends ManageCollectionComponent { + setSize(size: number): void { super.size = size } + setFrom(from: number): void { super.from = from } +} + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let component: ConcreteComponent +let fixture: ComponentFixture + + +describe("ManageCollectionComponent", () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + ConcreteComponent + ], + imports: [ + RouterTestingModule.withRoutes([ + { path: "collections/uuid1/add-skills", component: ManageCollectionComponent }, + { path: "collections/UUID1/edit", component: ManageCollectionComponent }, + { path: "collections/uuid1/publish", component: ManageCollectionComponent } + ]), + HttpClientTestingModule + ], + providers: [ + AppConfig, + Title, + ToastService, + { provide: EnvironmentService, useClass: EnvironmentServiceStub }, // Example of using a service stub + { provide: RichSkillService, useClass: RichSkillServiceStub }, + { provide: CollectionService, useClass: CollectionServiceStub }, + ] + }) + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() + + createComponent(ConcreteComponent) + component.titleElement = new ElementRef(document.getElementById("titleHeading")) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + it("collectionHasSkills should be correct", () => { + component.collection = undefined + expect(component.collectionHasSkills).toBeFalsy() + + component.collection = new ApiCollection(createMockCollection( + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + PublishStatus.Draft + // The default is to have some skills + )) + expect(component.collectionHasSkills).toBeTruthy() + }) + + it("reloadCollection should be correct", () => { + // Arrange + component.uuidParam = "uuid1" + const collection = new ApiCollection(createMockCollection( + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + PublishStatus.Draft + )) + + // Act + component.reloadCollection() + + // Assert + expect(component.collection).toEqual(collection) + }) + + it("loadNextPage should be correct", () => { + expect(component.collection).toBeTruthy() // collection should be set via ngOnInit() > reloadCollection() + + // Arrange + const totalCount = createMockPaginatedSkills().totalCount + component.results = undefined + + // Act + component.loadNextPage() + while (!component.results) {} + + // Assert + expect((component.results as PaginatedSkills).totalCount).toEqual(totalCount) + }) + + it("loadNextPage should handle undefined", () => { + // Arrange + component.collection = undefined + const collectionService = TestBed.inject(CollectionService) + spyOn(collectionService, "getCollectionSkills") + + // Act + component.loadNextPage() + + // Assert + expect(collectionService.getCollectionSkills).not.toHaveBeenCalled() + }) + + it("getSelectAllCount should be correct", () => { + expect(component.collection).toBeTruthy() // collection should be set via ngOnInit() > reloadCollection() + + // Arrange + const totalCount = createMockPaginatedSkills().totalCount + component.results = undefined + + // Act + component.loadNextPage() + while (!component.results) {} + + // Assert + expect(component.getSelectAllCount()).toEqual(totalCount) + }) + + it("handleSelectAll should be correct", () => { + // Arrange + component.selectAllChecked = false + + // Act + const result = component.handleSelectAll(true) + + // Assert + expect(result).toBeFalsy() // Always false + expect(component.selectAllChecked).toBeTruthy() + }) + + it("selectedCount (1/2) should count results", () => { + // Arrange + component.selectAllChecked = true + const skills = createMockPaginatedSkills(5, 10) + component.selectedSkills = [ + createMockSkillSummary("id2") // just one item + ] + component.results = skills + + // Act + const result = component.selectedCount + + // Assert + expect(result).toEqual(skills.totalCount) + }) + + it("selectedCount (2/2) should count selected", () => { + // Arrange + component.selectAllChecked = false + const skills = createMockPaginatedSkills(5, 10) + component.selectedSkills = [ + createMockSkillSummary("id2") // just one item + ] + component.results = skills + + // Act + const result = component.selectedCount + + // Assert + expect(result).toEqual(component.selectedSkills.length) + }) + + it("clearSearch should be correct", () => { + // Arrange + component.apiSearch = new ApiSearch({}) + component.from = 10 + component.results = undefined + + // Act + const result = component.clearSearch() + while (!component.results) {} + + // Assert + expect(component.apiSearch).toBeFalsy() + expect(component.from).toBeFalsy() + expect(component.results).toBeTruthy() + expect(result).toBeFalsy() + }) + + it("handleDefaultSubmit should be correct", () => { + // Arrange + component.searchForm.setValue({search: "my search string"}) + component.apiSearch = undefined + component.matchingQuery = undefined + component.from = 1 + spyOn(component, "loadNextPage").and.stub() + + // Act + const result = component.handleDefaultSubmit() + + // Assert + expect(result).toBeFalsy() + expect(component.apiSearch).toBeTruthy() + expect(component.matchingQuery).toBeTruthy() + expect(component.from).toEqual(0) + }) + + it("actionDefinitions should be correct", () => { + const router = TestBed.inject(Router) + const spyNavigate = spyOn(router, "navigate").and.callThrough() + spyOn(component, "submitCollectionStatusChange").and.stub(); + + [ + { publishDate: new Date("2020-06-25T14:58:46.313Z"), status: PublishStatus.Published, action2Label: "View Published Collection" }, + { publishDate: undefined, status: PublishStatus.Draft, action2Label: "Publish Collection" } + ].forEach((params) => { + // Arrange + component.uuidParam = "UUID1" + component.collection = new ApiCollection(createMockCollection( + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + params.publishDate, + params.status + // The default is to have some skills + )) + + // Act + const actions = component.actionDefinitions() + + // Assert + expect(actions).toBeTruthy() + expect(actions.length).toEqual(5) + + let action = actions[0] + expect(action.label).toEqual("Add RSDs to This Collection") + expect(action.primary).toBeFalsy() + expect(action && action.callback).toBeTruthy() + spyNavigate.calls.reset() + action.callback?.(action) + expect(router.navigate).toHaveBeenCalledWith(["/collections/uuid1/add-skills"]) + + action = actions[1] + expect(action.label).toEqual("Edit Collection Name") + expect(action.primary).toBeFalsy() + expect(action && action.callback).toBeTruthy() + spyNavigate.calls.reset() + action.callback?.(action) + expect(router.navigate).toHaveBeenCalledWith(["/collections/UUID1/edit"]) + + action = actions[2] + expect(action.label).toEqual(params.action2Label) + expect(action && action.callback).toBeTruthy() + + action = actions[3] + expect(action.label).toEqual("Archive Collection ") + expect(action.primary).toBeFalsy() + expect(action && action.callback).toBeTruthy() + action.callback?.(action) + expect(action.visible?.()).toBeTruthy() // !== PublishStatus.Archived && !== PublishStatus.Deleted + + action = actions[4] + expect(action.label).toEqual("Unarchive Collection ") + expect(action.primary).toBeFalsy() + expect(action && action.callback).toBeTruthy() + action.callback?.(action) + expect(action.visible?.()).toBeFalsy() // === PublishStatus.Archived || === PublishStatus.Deleted + }) + }) + + it("publishAction should be correct", () => { + // Arrange for all + const router = TestBed.inject(Router) + const collectionService = TestBed.inject(CollectionService) + const spyNavigate = spyOn(router, "navigate").and.callThrough() + spyOn(window, "confirm").and.returnValue(true) + const spySubmitCollectionStatusChange = spyOn(component, "submitCollectionStatusChange").and.stub() + let readyUuid + spyOn(collectionService, "collectionReadyToPublish") + .and.callFake((uuid) => { + readyUuid = uuid + return of(uuid !== "uuid1") + }) + + // Arrange + const skill1 = createMockSkillSummary("id1") + component.uuidParam = skill1.uuid + // Act + component.publishAction() + while (!readyUuid) {} + // Assert + expect(component.submitCollectionStatusChange).not.toHaveBeenCalled() + expect(router.navigate).toHaveBeenCalledWith(["/collections/uuid1/publish"]) + + // Arrange + spyNavigate.calls.reset() + spySubmitCollectionStatusChange.calls.reset() + readyUuid = undefined + const skill2 = createMockSkillSummary("id2") + component.uuidParam = skill2.uuid + // Act + component.publishAction() + while (!readyUuid) {} + // Assert + expect(component.submitCollectionStatusChange).toHaveBeenCalled() + expect(router.navigate).not.toHaveBeenCalled() + + // Arrange + spyNavigate.calls.reset() + spySubmitCollectionStatusChange.calls.reset() + component.uuidParam = undefined + // Act + component.publishAction() + // Assert + expect(component.submitCollectionStatusChange).not.toHaveBeenCalled() + expect(router.navigate).not.toHaveBeenCalled() + }) + + it("submitCollectionStatusChange should be correct", () => { + // Arrange + const uuid = "uuid1" + const status = PublishStatus.Draft + const collection = new ApiCollection(createMockCollection( + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + PublishStatus.Draft + // The default is to have some skills + )) + const collectionService = TestBed.inject(CollectionService) + component.uuidParam = uuid + component.results = undefined + // An example of a one-off service stub. If reused, then put it in mock-stubs.ts + spyOn(collectionService, "updateCollection").and.callFake( + (theUuid, updateObject) => { + expect(theUuid).toEqual(uuid) + expect(updateObject.status).toEqual(status) + return of(collection) + } + ) + + // Act + component.submitCollectionStatusChange(PublishStatus.Draft, "whatever") + + // Assert + while (!component.results) {} + expect(component.results).toEqual(createMockPaginatedSkills()) + expect(component.collection).toEqual(collection) + }) + + it("submitCollectionStatusChange should handle undefined", () => { + // Arrange + const collectionService = TestBed.inject(CollectionService) + component.uuidParam = undefined + spyOn(collectionService, "updateCollection") + + // Act + component.submitCollectionStatusChange(PublishStatus.Draft, "whatever") + + // Assert + expect(collectionService.updateCollection).not.toHaveBeenCalled() + }) + + it("removeFromCollection should be correct", () => { + // Arrange for allo + const spySubmitSkillRemoval = spyOn(component, "submitSkillRemoval").and.returnValue() // Test just this method + const spyGetApiSearch = spyOn(component, "getApiSearch").and.callThrough() + spyOn(window, "confirm").and.returnValue(true) + const skill = new ApiSkillSummary(createMockSkillSummary("id1")) + const skills = createMockPaginatedSkills(5, 10) + component.selectedSkills = [ + createMockSkillSummary("id2"), + createMockSkillSummary("id3") + ] + component.results = skills + component.showingMultipleConfirm = false + component.selectAllChecked = false + + // Act + component.removeFromCollection(skill) + // Assert + expect(component.showingMultipleConfirm).toBeFalsy() + expect(component.submitSkillRemoval).toHaveBeenCalled() + + // Arrange + spySubmitSkillRemoval.calls.reset() + // Act + component.removeFromCollection() + // Assert + expect(component.showingMultipleConfirm).toBeTruthy() + expect(component.submitSkillRemoval).not.toHaveBeenCalled() + + // Arrange + spySubmitSkillRemoval.calls.reset() + component.selectAllChecked = true + // Act + component.removeFromCollection(skill) + // Assert + expect(component.showingMultipleConfirm).toBeTruthy() + expect(component.submitSkillRemoval).not.toHaveBeenCalled() + + // Arrange + spyGetApiSearch.calls.reset() + component.uuidParam = undefined + // Act + component.removeFromCollection(skill) + // Assert + expect(component.getApiSearch).not.toHaveBeenCalled() + }) + + it("submitSkillRemoval should be correct", () => { + // Arrange + const spyReloadCollection = spyOn(component, "reloadCollection").and.returnValue() + const spyLoadNextPage = spyOn(component, "loadNextPage").and.returnValue() + const apiSearch = new ApiSearch({}) + + // Act + component.submitSkillRemoval(apiSearch) + + // Assert + expect(component.reloadCollection).toHaveBeenCalled() + expect(component.loadNextPage).toHaveBeenCalled() + }) + + it("handleClickConfirmMulti should be correct", () => { + // Arrange + const spySubmitSkillRemoval = spyOn(component, "submitSkillRemoval").and.returnValue() // Test just this method + component.showingMultipleConfirm = true + component.apiSearch = new ApiSearch({}) + + // Act + const result = component.handleClickConfirmMulti() + + // Assert + expect(result).toBeFalsy() + expect(component.showingMultipleConfirm).toBeFalsy() + expect(component.apiSearch).toBeFalsy() + expect(component.submitSkillRemoval).toHaveBeenCalled() + }) + + it("handleClickCancel should be correct", () => { + // Arrange + component.showingMultipleConfirm = true + component.apiSearch = new ApiSearch({}) + + // Act + component.handleClickCancel() + + // Assert + expect(component.showingMultipleConfirm).toBeFalsy() + expect(component.apiSearch).toBeFalsy() + }) +}) diff --git a/ui/src/app/collection/detail/publish-collection.component.spec.ts b/ui/src/app/collection/detail/publish-collection.component.spec.ts new file mode 100644 index 000000000..7c1c12883 --- /dev/null +++ b/ui/src/app/collection/detail/publish-collection.component.spec.ts @@ -0,0 +1,183 @@ +import { Location } from "@angular/common" +import { HttpClientTestingModule } from "@angular/common/http/testing" +import { Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { Title } from "@angular/platform-browser" +import { ActivatedRoute, Router } from "@angular/router" +import { ActivatedRouteStubSpec } from "test/util/activated-route-stub.spec" +import { createMockCollection, createMockPaginatedSkills } from "../../../../test/resource/mock-data" +import { CollectionServiceStub, RichSkillServiceStub } from "../../../../test/resource/mock-stubs" +import { AppConfig } from "../../app.config" +import { EnvironmentService } from "../../core/environment.service" +import { PublishStatus } from "../../PublishStatus" +import { RichSkillService } from "../../richskill/service/rich-skill.service" +import { ToastService } from "../../toast/toast.service" +import { ApiCollection } from "../ApiCollection" +import { CollectionService } from "../service/collection.service" +import { PublishCollectionComponent } from "./publish-collection.component" + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let activatedRoute: ActivatedRouteStubSpec +let component: PublishCollectionComponent +let fixture: ComponentFixture + + +describe("PublishCollectionComponent", () => { + beforeEach(() => { + activatedRoute = new ActivatedRouteStubSpec() + }) + + beforeEach(async(() => { + const routerSpy = ActivatedRouteStubSpec.createRouterSpy() + + TestBed.configureTestingModule({ + declarations: [ + PublishCollectionComponent + ], + imports: [ + HttpClientTestingModule + ], + providers: [ + AppConfig, + EnvironmentService, + Title, + Location, + ToastService, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: routerSpy }, + { provide: RichSkillService, useClass: RichSkillServiceStub }, + { provide: CollectionService, useClass: CollectionServiceStub }, + ] + }) + .compileComponents() + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() + + activatedRoute.setParamMap({ uuid: "uuid1" }) + createComponent(PublishCollectionComponent) + })) + + it("should be created", () => { + const collection = new ApiCollection(createMockCollection( + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + PublishStatus.Draft + )) + + expect(component).toBeTruthy() + expect(component.uuidParam).toEqual("uuid1") + expect(component.collectionLoaded).toBeTruthy() + expect(component.collection).toEqual(collection) + expect(component.activeState).toEqual(1) // Got advanced by nextState() + // Will test nextState() below + }) + + it("checkingDraft should be correct", () => { + [ + { checkingDraft: false }, + { checkingDraft: false }, + { checkingDraft: true }, + { checkingDraft: false }, + { checkingDraft: false }, + ].forEach((param, index) => { + component.activeState = index + expect(component.checkingDraft).toEqual(param.checkingDraft) + }) + }) + + it("checkingArchived should be correct", () => { + [ + { checkingArchived: false }, + { checkingArchived: true }, + { checkingArchived: false }, + { checkingArchived: false }, + { checkingArchived: false }, + ].forEach((param, index) => { + component.activeState = index + expect(component.checkingArchived).toEqual(param.checkingArchived) + }) + }) + + it("nextState should be correct", () => { + expect(component.activeState).toEqual(1) + component.nextState() + expect(component.activeState).toEqual(2) + component.nextState() + expect(component.activeState).toEqual(3) + component.nextState() + expect(component.activeState).toEqual(4) + }) + + it("verb should be correct", () => { + expect(component.activeState).toEqual(1) + expect(component.verb).toEqual("archived") + component.nextState() + expect(component.verb).toEqual("draft") + }) + + it("checkForStatus should be correct", () => { + // Arrange + const expected = createMockPaginatedSkills() + component.blockingSkills = undefined + + // Act + component.checkForStatus(new Set([PublishStatus.Draft])) + while (!component.blockingSkills) { } // wait + + // Assert + expect(component.blockingSkills).toEqual(expected) + }) + + it("handleClickCancel should cancel", () => { + expect(component.handleClickCancel()).toBeFalse() + }) + + it("handleClickConfirmRemove should be correct", () => { + // Arrange + component.activeState = 1 + component.skillsSaved = undefined + + // Act + expect(component.handleClickConfirmRemove()).toBeFalse() + while (component.activeState === 1) { } // wait + + // Assert + expect(component.skillsSaved).toBeTruthy() + expect(component.activeState).toEqual(2) + }) + + it("handleClickConfirmUnarchive should be correct", () => { + expect(component.handleClickConfirmUnarchive()).toBeFalse() + }) + + it("handleClickConfirmPublish should be correct", () => { + // Arrange + component.activeState = 1 + component.skillsSaved = undefined + + // Act + expect(component.handleClickConfirmPublish()).toBeFalse() + while (component.activeState === 1) { } // wait + + // Assert + expect(component.skillsSaved).toBeTruthy() + expect(component.activeState).toEqual(2) + }) +}) diff --git a/ui/src/app/core/environment.service.ts b/ui/src/app/core/environment.service.ts new file mode 100644 index 000000000..85a17f4ef --- /dev/null +++ b/ui/src/app/core/environment.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from "@angular/core" +import { environment } from "../../environments/environment" +import { appMetadata } from "../app.metadata" +import { IAppConfig } from "../models/app-config.model" + + +@Injectable() +/* tslint:disable:no-any */ +export class EnvironmentService { + + public environment: IAppConfig; + + constructor() { + console.log("OSMT-UI Version: ", appMetadata.package.version) + + if // there is an externally provided env use it + ((window as any).__env) { + this.environment = ((window as any).__env) as IAppConfig + console.log("External environment config used") + } else if (!environment.production) { // This should only be pulled in during 'ng test'. + this.environment = environment as any + console.warn(">>>>> Testing environment config used <<<<<") + } else { // use local default environment + throw new Error("Missing env script!") + } + } +} diff --git a/ui/src/app/core/status-bar.component.spec.ts b/ui/src/app/core/status-bar.component.spec.ts new file mode 100644 index 000000000..c249f0f91 --- /dev/null +++ b/ui/src/app/core/status-bar.component.spec.ts @@ -0,0 +1,131 @@ +import { Component, Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { By } from "@angular/platform-browser" +import { PublishStatus } from "../PublishStatus" +import { StatusBarComponent } from "./status-bar.component" + + +const EXPECTED_STATUS = PublishStatus.Deleted +const EXPECTED_PUBLISH_DATE = "2020-05-25T14:58:46.313Z" +const EXPECTED_ARCHIVE_DATE = "2020-06-25T14:58:46.313Z" +const EXPECTED_SHOW_DATES = false + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + myStatus = EXPECTED_STATUS + myPublishDate = EXPECTED_PUBLISH_DATE + myArchiveDate = EXPECTED_ARCHIVE_DATE + myShowDates = EXPECTED_SHOW_DATES +} + + +export function createComponent(T: Type): Promise { + hostFixture = TestBed.createComponent(T) + hostComponent = hostFixture.componentInstance + + const debugEl = hostFixture.debugElement.query(By.directive(StatusBarComponent)) + childComponent = debugEl.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + hostFixture.detectChanges() + + return hostFixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + hostFixture.detectChanges() + }) +} + + +let hostFixture: ComponentFixture +let hostComponent: TestHostComponent +let childComponent: StatusBarComponent + + +describe("StatusBarComponent", () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + StatusBarComponent, + TestHostComponent + ] + }) + .compileComponents() + + createComponent(TestHostComponent) + })) + + it("should be created", () => { + expect(hostComponent).toBeTruthy() + expect(childComponent).toBeTruthy() + }) + + it("should assign inputs", () => { + expect(childComponent.status).toEqual(EXPECTED_STATUS) + expect(childComponent.publishDate).toEqual(EXPECTED_PUBLISH_DATE) + expect(childComponent.archiveDate).toEqual(EXPECTED_ARCHIVE_DATE) + expect(childComponent.showDates).toEqual(EXPECTED_SHOW_DATES) + }) + + it("should be archived", () => { + childComponent.archiveDate = "2020-02-25T14:58:46.313Z" + childComponent.status = PublishStatus.Published + expect(childComponent.isArchived()).toBeTruthy() + + childComponent.archiveDate = "2020-03-25T14:58:46.313Z" + childComponent.status = PublishStatus.Deleted + expect(childComponent.isArchived()).toBeTruthy() + + childComponent.archiveDate = "" + childComponent.status = PublishStatus.Archived + expect(childComponent.isArchived()).toBeTruthy() + + childComponent.archiveDate = "" + childComponent.status = PublishStatus.Archived + expect(childComponent.isArchived()).toBeTruthy() + }) + + it("should be published", () => { + childComponent.publishDate = "2020-06-25T14:58:46.313Z" + childComponent.status = PublishStatus.Published + expect(childComponent.isPublished()).toBeTruthy() + + childComponent.publishDate = "2020-05-25T14:58:46.313Z" + childComponent.status = PublishStatus.Unarchived + expect(childComponent.isPublished()).toBeTruthy() + + childComponent.publishDate = "" + childComponent.status = PublishStatus.Published + expect(childComponent.isPublished()).toBeTruthy() + + childComponent.publishDate = "" + childComponent.status = PublishStatus.Unarchived + expect(childComponent.isPublished()).toBeFalsy() + }) + + it("should be draft", () => { + childComponent.publishDate = "2020-06-25T14:58:46.313Z" + childComponent.status = PublishStatus.Published + expect(childComponent.isDraft()).toBeFalsy() + + childComponent.publishDate = "2020-05-25T14:58:46.313Z" + childComponent.status = PublishStatus.Unarchived + expect(childComponent.isDraft()).toBeFalsy() + + childComponent.publishDate = "" + childComponent.status = PublishStatus.Published + expect(childComponent.isDraft()).toBeFalsy() + + childComponent.publishDate = "" + childComponent.status = PublishStatus.Unarchived + expect(childComponent.isDraft()).toBeTruthy() + }) +}) diff --git a/ui/src/app/job-codes/Jobcode.spec.ts b/ui/src/app/job-codes/Jobcode.spec.ts new file mode 100644 index 000000000..eec42f096 --- /dev/null +++ b/ui/src/app/job-codes/Jobcode.spec.ts @@ -0,0 +1,19 @@ +import { createMockJobcode } from "test/resource/mock-data" +import { deepEqualSkipOuterType, mismatched } from "../../../test/util/deep-equals" +import { ApiJobCode, IJobCode } from "./Jobcode" + + +describe("Jobcode", () => { + it("ApiJobcode should be created", () => { + // Arrange + const iJobcode: IJobCode = createMockJobcode() + + // Act + const apiJobcode = new ApiJobCode(iJobcode) + + // Assert + expect(apiJobcode).toBeTruthy() + expect(deepEqualSkipOuterType(apiJobcode, iJobcode)).toBeTruthy(mismatched(apiJobcode, iJobcode)) + }) +}) + diff --git a/ui/src/app/loading/loading.component.spec.ts b/ui/src/app/loading/loading.component.spec.ts new file mode 100644 index 000000000..982dc7398 --- /dev/null +++ b/ui/src/app/loading/loading.component.spec.ts @@ -0,0 +1,64 @@ +import { Component, Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { By } from "@angular/platform-browser" +import { LoadingComponent } from "./loading.component" + +// An example of how to test an @Input + + +const EXPECTED_CLASS_NAME = "foo" + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + myClass = EXPECTED_CLASS_NAME +} + + +export function createComponent(T: Type): Promise { + hostFixture = TestBed.createComponent(T) + hostComponent = hostFixture.componentInstance + + const debugEl = hostFixture.debugElement.query(By.directive(LoadingComponent)) + childComponent = debugEl.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + hostFixture.detectChanges() + + return hostFixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + hostFixture.detectChanges() + }) +} + + +let hostFixture: ComponentFixture +let hostComponent: TestHostComponent +let childComponent: LoadingComponent + + +describe("LoadingComponent", () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + LoadingComponent, + TestHostComponent + ] + }) + .compileComponents() + + createComponent(TestHostComponent) + })) + + it("should be created", () => { + expect(hostComponent).toBeTruthy() + expect(childComponent).toBeTruthy() + }) + + it("should assign className correctly", () => { + expect(childComponent.className).toEqual(EXPECTED_CLASS_NAME) + }) +}) diff --git a/ui/src/app/models/app-config.model.ts b/ui/src/app/models/app-config.model.ts index e5b312bdf..e2d43b57d 100644 --- a/ui/src/app/models/app-config.model.ts +++ b/ui/src/app/models/app-config.model.ts @@ -33,4 +33,5 @@ export class DefaultAppConfig implements IAppConfig { poweredByLabel = "Open Skills Network" idleTimeoutInSeconds = 15 * 60 colorBrandAccent1 = undefined + dynamicWhitelabel = false } diff --git a/ui/src/app/navigation/abstract-search.component.spec.ts b/ui/src/app/navigation/abstract-search.component.spec.ts new file mode 100644 index 000000000..31f151a5e --- /dev/null +++ b/ui/src/app/navigation/abstract-search.component.spec.ts @@ -0,0 +1,112 @@ +import { HttpClientModule } from "@angular/common/http" +import { Component, Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { ActivatedRoute, Router } from "@angular/router" +import { SearchServiceStub } from "../../../test/resource/mock-stubs" +import { ActivatedRouteStubSpec } from "../../../test/util/activated-route-stub.spec" +import { SearchService } from "../search/search.service" +import { AbstractSearchComponent } from "./abstract-search.component" + + +@Component({ + selector: "app-advanced-search", + template: `` +}) +export class ConcreteSearchComponent extends AbstractSearchComponent { + constructor(searchService: SearchService, route: ActivatedRoute) { + super(searchService, route) + } +} + + +let component: ConcreteSearchComponent +let activatedRoute: ActivatedRouteStubSpec +let fixture: ComponentFixture + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + +describe("AbstractSearchComponent", () => { + const search = "testSearchQuery" + + beforeEach(() => { + activatedRoute = new ActivatedRouteStubSpec() + }) + beforeEach(async(() => { + const routerSpy = ActivatedRouteStubSpec.createRouterSpy() + + TestBed.configureTestingModule({ + declarations: [ + ConcreteSearchComponent + ], + imports: [ + HttpClientModule + ], + providers: [ + { provide: SearchService, useClass: SearchServiceStub }, + { provide: Router, useValue: routerSpy }, + { provide: ActivatedRoute, useValue: activatedRoute } + + ] + }) + .compileComponents() + + activatedRoute.setParamMap({ userId: 126 }) + createComponent(ConcreteSearchComponent) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + it("constructor searchQuery updates correctly", () => { + // Arrange + component.searchForm.setValue({search}) + expect(component.searchQuery).toEqual(search) + const searchService = TestBed.inject(SearchService); + + // Act + (searchService as unknown as SearchServiceStub).setLatestSearch(undefined) + + // Assert + while (component.searchQuery !== ""){} + expect(component.searchQuery).toEqual("") + }) + + it("clearSearch should be correct", () => { + component.searchForm.setValue({search}) + expect(component.searchQuery).toEqual(search) + expect(component.clearSearch()).toBeFalse() + expect(component.searchQuery).toEqual("") + }) + + it("handleDefaultSubmit should be false", () => { + component.searchForm.setValue({search: ""}) + expect(component.handleDefaultSubmit()).toBeFalse() + expect(component.searchQuery).toEqual("") + component.searchForm.setValue({search}) + expect(component.handleDefaultSubmit()).toBeFalse() + expect(component.searchQuery).toEqual(search) + }) + + it("submitCollections should be false", () => { + component.clearSearch() + expect(component.submitCollectionSearch()).toBeFalse() + expect(component.searchQuery).toEqual("") + component.searchForm.setValue({search}) + expect(component.submitCollectionSearch()).toBeFalse() + expect(component.searchQuery).toEqual(search) + }) + +}) diff --git a/ui/src/app/richskill/ApiBatchResult.spec.ts b/ui/src/app/richskill/ApiBatchResult.spec.ts new file mode 100644 index 000000000..c4ab37229 --- /dev/null +++ b/ui/src/app/richskill/ApiBatchResult.spec.ts @@ -0,0 +1,19 @@ +import { createMockBatchResult } from "test/resource/mock-data" +import { deepEqualSkipOuterType, mismatched } from "../../../test/util/deep-equals" +import { ApiBatchResult, IBatchResult } from "./ApiBatchResult" + + +describe("BatchResult", () => { + it("BatchResult should be created", () => { + // Arrange + const iBatchResult: IBatchResult = createMockBatchResult() + + // Act + const apiBatchResult = new ApiBatchResult(iBatchResult) + + // Assert + expect(apiBatchResult).toBeTruthy() + expect(deepEqualSkipOuterType(apiBatchResult, iBatchResult)).toBeTruthy(mismatched(apiBatchResult, iBatchResult)) + }) +}) + diff --git a/ui/src/app/richskill/ApiSkill.spec.ts b/ui/src/app/richskill/ApiSkill.spec.ts new file mode 100644 index 000000000..101a6edd2 --- /dev/null +++ b/ui/src/app/richskill/ApiSkill.spec.ts @@ -0,0 +1,190 @@ +import * as _ from "lodash" +import { + createMockApiNamedReference, + createMockAuditLog, + createMockNamedReference, + createMockSkill, + createMockUuidReference +} from "../../../test/resource/mock-data" +import { deepEqualSkipOuterType, mismatched } from "../../../test/util/deep-equals" +import { PublishStatus } from "../PublishStatus" +import { + ApiAuditLog, + ApiNamedReference, + ApiSkill, + ApiUuidReference, + AuditOperationType, + IAuditLog, + INamedReference, + ISkill, + IUuidReference +} from "./ApiSkill" + +// An example of a class-level test + + +describe("ApiSkill", () => { + it("ApiUuidReference should be created", () => { + // Arrange + const iUuidReference: IUuidReference = createMockUuidReference() + + // Act + const apiUuidReference = new ApiUuidReference(iUuidReference) + + // Assert + expect(apiUuidReference).toBeTruthy() + expect(deepEqualSkipOuterType(apiUuidReference, iUuidReference)).toBeTruthy(mismatched(apiUuidReference, iUuidReference)) + }) + + + it("ApiNamedReference should be created", () => { + // Arrange + const iNamedReference: INamedReference = createMockNamedReference() + + // Act + const apiNamedReference = new ApiNamedReference(iNamedReference) + + // Assert + expect(apiNamedReference).toBeTruthy() + expect(deepEqualSkipOuterType(apiNamedReference, iNamedReference)).toBeTruthy(mismatched(apiNamedReference, iNamedReference)) + }) + + it("ApiNamedReference should be equal", () => { + // Arrange + const a = new ApiNamedReference(createMockNamedReference("A", "name")) + const b = new ApiNamedReference(createMockNamedReference("A", "name")) + + const c = new ApiNamedReference(createMockNamedReference("A")) + const d = new ApiNamedReference(createMockNamedReference("A", undefined)) + + const e = new ApiNamedReference(createMockNamedReference(undefined, undefined)) + const f = new ApiNamedReference(createMockNamedReference()) + + // Act + const result1 = a.equals(b) + const result2 = c.equals(d) + const result3 = e.equals(f) + + // Assert + expect(result1).toBeTruthy() + expect(result2).toBeTruthy() + expect(result3).toBeTruthy() + }) + + it("ApiNamedReference should be unequal", () => { + // None of the following value pairs should match! + const parameters = [ + ["A", "name1"], + ["B", "name1"], + ["B", "name2"], + ["A", "name2"], + ["A", undefined], + [undefined, "name1"], + [undefined, undefined], + ] + + for (let i = 0; i < parameters.length; ++i) { + for (let j = 0; j < parameters.length; ++j) { + if (i !== j) { + // Arrange + const iParam = parameters[i] + const jParam = parameters[j] + // WARNING: the createMock... functions use default parameters!! + const iValue = createMockApiNamedReference(iParam[0], iParam[1]) + const jValue = createMockApiNamedReference(jParam[0], jParam[1]) + + // Act + const result = iValue.equals(jValue) + + // Assert + expect(result).toBeFalsy(mismatched(iValue, jValue)) + } + } + } + }) + + it("ApiNamedReference should be id from text", () => { + expect(ApiNamedReference.fromString("")).toEqual(undefined) + expect(ApiNamedReference.fromString("a://b")).toEqual(new ApiNamedReference({ id: "a://b" })) + expect(ApiNamedReference.fromString("abc")).toEqual(new ApiNamedReference({ name: "abc" })) + }) + + it("ApiNamedReference should be id from text", () => { + // Arrange + const a = ApiNamedReference.fromString("my_name") + const b = new ApiNamedReference({name: "my_name"}) // id field is ignored + + const c = ApiNamedReference.fromString("https://my_id") + const d = new ApiNamedReference({ id: "https://my_id"}) // name field is ignored + + // Act + const result1 = a?.equals(b) + const result2 = c?.equals(d) + + // Assert + expect(result1).toBeTruthy(mismatched(a, b)) + expect(result2).toBeTruthy(mismatched(c, d)) + }) + + + it("ApiAuditLog should be created", () => { + // Arrange + const iAuditLog: IAuditLog = createMockAuditLog() + + // Act + const apiAuditLog = new ApiAuditLog(iAuditLog) + + // Assert + expect(apiAuditLog).toBeTruthy() + /* cannot do deep equals because the date formats are different (i.e., string != Date) */ + expect(apiAuditLog.creationDate?.toISOString()).toEqual(iAuditLog.creationDate) + expect(apiAuditLog.operationType).toEqual(iAuditLog.operationType) + expect(apiAuditLog.user).toEqual(iAuditLog.user) + expect(_.isEqual(apiAuditLog.changedFields, iAuditLog.changedFields)).toBeTruthy() + }) + + it("ApiAuditLog should match status", () => { + // Arrange + const i1 = new ApiAuditLog(createMockAuditLog(AuditOperationType.Insert)) + const i2 = new ApiAuditLog(createMockAuditLog(AuditOperationType.Update)) + const i3 = new ApiAuditLog(createMockAuditLog(AuditOperationType.PublishStatusChange)) + + // Act + + // Assert + expect(i1.isPublishStatusChange()).toBeFalsy() + expect(i2.isPublishStatusChange()).toBeFalsy() + expect(i3.isPublishStatusChange()).toBeTruthy() + }) + + + it("ApiSkill should be created", () => { + // Arrange + const date = new Date("2020-06-25T14:58:46.313Z") + const iSkill: ISkill = createMockSkill(date, date, PublishStatus.Draft) + + // Act + const apiSkill = new ApiSkill(iSkill) + + // Assert + expect(apiSkill).toBeTruthy() + /* cannot do deep equals because the date formats are different (i.e., string != Date) */ + expect(apiSkill.status).toEqual(iSkill.status) + expect(apiSkill.id).toEqual(iSkill.id) + expect(apiSkill.uuid).toEqual(iSkill.uuid) + expect(apiSkill.creationDate?.toISOString()).toEqual(iSkill.creationDate) // <-- + expect(apiSkill.updateDate?.toISOString()).toEqual(iSkill.updateDate) // <-- + expect(apiSkill.type).toEqual(iSkill.type) + expect(apiSkill.skillName).toEqual(iSkill.skillName) + expect(apiSkill.skillStatement).toEqual(iSkill.skillStatement) + expect(apiSkill.category).toEqual(iSkill.category) + expect(apiSkill.collections).toEqual(iSkill.collections) + expect(apiSkill.keywords).toEqual(iSkill.keywords) + expect(apiSkill.alignments).toEqual(iSkill.alignments) + expect(apiSkill.standards).toEqual(iSkill.standards) + expect(apiSkill.certifications).toEqual(iSkill.certifications) + expect(apiSkill.occupations).toEqual(iSkill.occupations) + expect(apiSkill.employers).toEqual(iSkill.employers) + expect(apiSkill.author).toEqual(iSkill.author) + }) +}) diff --git a/ui/src/app/richskill/ApiSkillSummary.spec.ts b/ui/src/app/richskill/ApiSkillSummary.spec.ts new file mode 100644 index 000000000..d4e4fa931 --- /dev/null +++ b/ui/src/app/richskill/ApiSkillSummary.spec.ts @@ -0,0 +1,32 @@ +import { createMockCollectionSummary, createMockSkillSummary } from "../../../test/resource/mock-data" +import { deepEqualSkipOuterType, mismatched } from "../../../test/util/deep-equals" +import { ApiCollectionSummary, ApiSkillSummary, ISkillSummary } from "./ApiSkillSummary" + + +describe("ApiSkillSummary", () => { + it("ApiSkillSummary should be created", () => { + // Arrange + const iSkillSummary: ISkillSummary = createMockSkillSummary() + + // Act + const apiSkillSummary = new ApiSkillSummary(iSkillSummary) + + // Assert + expect(apiSkillSummary).toBeTruthy() + expect(deepEqualSkipOuterType(apiSkillSummary, iSkillSummary)).toBeTruthy(mismatched(apiSkillSummary, iSkillSummary)) + }) + + + it("ApiCollectionSummary should be created", () => { + // Arrange + const iCollectionSummary: ApiCollectionSummary = createMockCollectionSummary() + + // Act + const apiCollectionSummary = new ApiCollectionSummary(iCollectionSummary) + + // Assert + expect(apiCollectionSummary).toBeTruthy() + expect(deepEqualSkipOuterType(apiCollectionSummary, iCollectionSummary)) + .toBeTruthy(mismatched(apiCollectionSummary, iCollectionSummary)) + }) +}) diff --git a/ui/src/app/richskill/ApiSkillUpdate.spec.ts b/ui/src/app/richskill/ApiSkillUpdate.spec.ts new file mode 100644 index 000000000..f6df3a42f --- /dev/null +++ b/ui/src/app/richskill/ApiSkillUpdate.spec.ts @@ -0,0 +1,55 @@ +import { + createMockSkillUpdate, + createMockApiReferenceListUpdate, + createMockStringListUpdate +} from "test/resource/mock-data" +import { deepEqualSkipOuterType, mismatched } from "../../../test/util/deep-equals" +import { ApiNamedReference } from "./ApiSkill" +import { + ApiSkillUpdate, + ApiReferenceListUpdate, + IRichSkillUpdate, + IReferenceListUpdate, + IStringListUpdate, ApiStringListUpdate +} from "./ApiSkillUpdate" + + +describe("ApiSkillUpdate", () => { + it("ApiSkillUpdate should be created", () => { + // Arrange + const iSkillUpdate: IRichSkillUpdate = createMockSkillUpdate() + + // Act + const apiSkillUpdate = new ApiSkillUpdate(iSkillUpdate) + + // Assert + expect(apiSkillUpdate).toBeTruthy() + expect(deepEqualSkipOuterType(apiSkillUpdate, iSkillUpdate)).toBeTruthy(mismatched(apiSkillUpdate, iSkillUpdate)) + }) + + + it("ApiStringListUpdate should be created", () => { + // Arrange + const iStringListUpdate: IStringListUpdate = createMockStringListUpdate() + + // Act + const apiStringListUpdate = new ApiStringListUpdate(iStringListUpdate.add, iStringListUpdate.remove) + + // Assert + expect(apiStringListUpdate).toBeTruthy() + expect(deepEqualSkipOuterType(apiStringListUpdate, iStringListUpdate)).toBeTruthy(mismatched(apiStringListUpdate, iStringListUpdate)) + }) + + + it("ApiReferenceListUpdate should be created", () => { + // Arrange + const ref: ApiReferenceListUpdate = createMockApiReferenceListUpdate() + + // Act + const apiReferenceListUpdate = new ApiReferenceListUpdate(ref.add, ref.remove) + + // Assert + expect(apiReferenceListUpdate).toBeTruthy() + expect(deepEqualSkipOuterType(apiReferenceListUpdate, ref)).toBeTruthy(mismatched(apiReferenceListUpdate, ref)) + }) +}) diff --git a/ui/src/app/richskill/detail/AbstractRichSkillDetailComponent.spec.ts b/ui/src/app/richskill/detail/AbstractRichSkillDetailComponent.spec.ts new file mode 100644 index 000000000..57d236a85 --- /dev/null +++ b/ui/src/app/richskill/detail/AbstractRichSkillDetailComponent.spec.ts @@ -0,0 +1,225 @@ +import { HttpClientTestingModule } from "@angular/common/http/testing" +import { Component, Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { Title } from "@angular/platform-browser" +import { ActivatedRoute, Router } from "@angular/router" +import { RouterTestingModule } from "@angular/router/testing" +import { ActivatedRouteStubSpec } from "test/util/activated-route-stub.spec" +import { createMockSkill } from "../../../../test/resource/mock-data" +import { RichSkillServiceStub } from "../../../../test/resource/mock-stubs" +import { AppConfig } from "../../app.config" +import { dateformat } from "../../core/DateHelper" +import { EnvironmentService } from "../../core/environment.service" +import { IDetailCardSectionData } from "../../detail-card/section/section.component" +import { PublishStatus } from "../../PublishStatus" +import { ApiSkill } from "../ApiSkill" +import { RichSkillService } from "../service/rich-skill.service" +import { AbstractRichSkillDetailComponent } from "./AbstractRichSkillDetailComponent" + + +@Component({ + selector: "app-concrete-component", + template: `` +}) +class ConcreteComponent extends AbstractRichSkillDetailComponent { + getCardFormat(): IDetailCardSectionData[] { + return [] + } + + get _locale(): string { + return this.locale + } + + public formatAssociatedCollections(isAuthorized: boolean): string { + return super.formatAssociatedCollections(isAuthorized) + } +} + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let activatedRoute: ActivatedRouteStubSpec +let component: ConcreteComponent +let fixture: ComponentFixture + + +describe("ConcreteComponent", () => { + const date = new Date("2020-06-25T14:58:46.313Z") + const skill = new ApiSkill(createMockSkill(date, date, PublishStatus.Draft)) + + beforeEach(() => { + activatedRoute = new ActivatedRouteStubSpec() + }) + + beforeEach(async(() => { + const routerSpy = ActivatedRouteStubSpec.createRouterSpy() + + TestBed.configureTestingModule({ + declarations: [ + ConcreteComponent + ], + imports: [ + RouterTestingModule, // Required for routerLink + HttpClientTestingModule, // Needed to avoid the toolName race condition below + ], + providers: [ + EnvironmentService, // Needed to avoid the toolName race condition below + AppConfig, // Needed to avoid the toolName race condition below + Title, + { provide: RichSkillService, useClass: RichSkillServiceStub }, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: routerSpy }, + ] + }) + .compileComponents() + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() // This avoids the race condition on reading the config's whitelabel.toolName + + activatedRoute.setParamMap({ uuid: "126" }) + createComponent(ConcreteComponent) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + + expect(component.uuidParam).toEqual("126") + }) + + it("getAuthor should return", () => { + // Arrange + component.richSkill = null + // Act/Assert + expect(component.getAuthor()).toEqual("") + + // Arrange + component.richSkill = skill + // Act/Assert + expect(component.getAuthor()).toEqual(skill.author.name as string) + }) + + it("getUuid should return", () => { + // Arrange + component.richSkill = null + // Act/Assert + expect(component.getSkillUuid()).toEqual("") + + // Arrange + component.richSkill = skill + // Act/Assert + expect(component.getSkillUuid()).toEqual(skill.uuid) + }) + + it("getSkillName should return", () => { + // Arrange + component.richSkill = null + // Act/Assert + expect(component.getSkillName()).toEqual("") + + // Arrange + component.richSkill = skill + // Act/Assert + expect(component.getSkillName()).toEqual(skill.skillName) + }) + + it("getPublishStatus should return", () => { + // Arrange + component.richSkill = null + // Act/Assert + expect(component.getPublishStatus()).toEqual(PublishStatus.Draft) + + // Arrange + component.richSkill = skill + // Act/Assert + expect(component.getPublishStatus()).toEqual(skill.status) + }) + + it("getSkillUrl should return", () => { + // Arrange + component.richSkill = null + // Act/Assert + expect(component.getSkillUrl()).toEqual("") + + // Arrange + component.richSkill = skill + // Act/Assert + expect(component.getSkillUrl()).toEqual(skill.id) + }) + + it("getPublishedDate should return", () => { + // Arrange + component.richSkill = null + // Act/Assert + expect(component.getPublishedDate()).toEqual("") + + // Arrange + skill.publishDate = date + component.richSkill = skill + // Act/Assert + expect(component.getPublishedDate()).toEqual(dateformat(skill.publishDate, component._locale)) + }) + + it("getArchivedDate should return", () => { + // Arrange + component.richSkill = null + // Act/Assert + expect(component.getArchivedDate()).toEqual("") + + // Arrange + skill.archiveDate = date + component.richSkill = skill + // Act/Assert + expect(component.getArchivedDate()).toEqual(dateformat(skill.archiveDate, component._locale)) + }) + + it("joinKeywords should return", () => { + // Arrange + component.richSkill = null + // Act/Assert + expect(component.joinKeywords()).toEqual("") + + // Arrange + skill.archiveDate = date + component.richSkill = skill + // Act/Assert + expect(component.joinKeywords()).toEqual(skill.keywords.join("; ")) + }) + + it("joinEmployers should return", () => { + // Arrange + component.richSkill = null + // Act/Assert + expect(component.joinEmployers()).toEqual("") + + // Arrange + const expected = "Acme; Joe's Pizza" + skill.employers = [ { name: "Acme" }, { name: "Joe's Pizza"}] + component.richSkill = skill + // Act/Assert + expect(component.joinEmployers()).toEqual(expected) + }) + + /* tslint:disable:quotemark */ + it("formatAssociatedCollections should return", () => { + // Arrange + const expected = + '
' + + '' + skill.collections = [ { uuid: "1", name: "Intro" }, { uuid: "2", name: "Advanced" } ] + component.richSkill = skill + // Act/Assert + expect(component.formatAssociatedCollections(true)).toEqual(expected) + }) +}) diff --git a/ui/src/app/richskill/detail/audit-log.component.spec.ts b/ui/src/app/richskill/detail/audit-log.component.spec.ts new file mode 100644 index 000000000..adc4bf960 --- /dev/null +++ b/ui/src/app/richskill/detail/audit-log.component.spec.ts @@ -0,0 +1,147 @@ +import { HttpClient } from "@angular/common/http" +import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing" +import { Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { CollectionServiceStub, RichSkillServiceStub } from "../../../../test/resource/mock-stubs" +import { AppConfig } from "../../app.config" +import { CollectionService } from "../../collection/service/collection.service" +import { EnvironmentService } from "../../core/environment.service" +import { PublishStatus } from "../../PublishStatus" +import { ApiAuditLog, AuditOperationType } from "../ApiSkill" +import { RichSkillService } from "../service/rich-skill.service" +import { AuditLogComponent } from "./audit-log.component" + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let component: AuditLogComponent +let fixture: ComponentFixture + + +describe("AuditLogComponent", () => { + let httpClient: HttpClient + let httpTestingController: HttpTestingController + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + AuditLogComponent + ], + imports: [ + HttpClientTestingModule, + ], + providers: [ + EnvironmentService, + AppConfig, + { provide: RichSkillService, useClass: RichSkillServiceStub }, + { provide: CollectionService, useClass: CollectionServiceStub }, + ] + }) + .compileComponents() + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() // This avoids the race condition on reading the config's whitelabel.toolName + + httpClient = TestBed.inject(HttpClient) + httpTestingController = TestBed.inject(HttpTestingController) + + createComponent(AuditLogComponent) + })) + + afterEach(() => { + httpTestingController.verify() + }) + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + it("toggle should return", () => { + // Arrange + spyOn(component, "fetch") + + // Act + component.toggle() + + // Assert + expect(component.fetch).toHaveBeenCalled() + }) + + it("fetchLog should return nothing", () => { + // Arrange + const expected = undefined + component.skillUuid = undefined + component.collectionUuid = undefined + + // Act + const result = component.fetchLog() + + // Assert + expect(result).toEqual(expected) + }) + + it("iconForEntry should return", () => { + [ + { op: AuditOperationType.Insert, new: "", expected: component.editIcon }, + { op: AuditOperationType.Update, new: "", expected: component.editIcon }, + { op: AuditOperationType.PublishStatusChange, new: PublishStatus.Published, expected: component.publishIcon }, + { op: AuditOperationType.PublishStatusChange, new: PublishStatus.Archived, expected: component.archiveIcon }, + { op: AuditOperationType.PublishStatusChange, new: PublishStatus.Unarchived, expected: component.unarchiveIcon }, + { op: AuditOperationType.PublishStatusChange, new: PublishStatus.Deleted, expected: component.archiveIcon }, + { op: AuditOperationType.PublishStatusChange, new: PublishStatus.Draft, expected: component.unarchiveIcon }, + { op: AuditOperationType.PublishStatusChange, new: "", expected: component.publishIcon }, + ].forEach((params) => { + const entry = new ApiAuditLog({ + creationDate: "", + operationType: params.op, + user: "", + changedFields: [ { fieldName: "foo", new: params.new, old: "" } ] }) + expect(component.iconForEntry(entry)).toEqual(params.expected) + }) + }) + + it("labelForEntry should return", () => { + [ + { op: AuditOperationType.Insert, new: "", old: "-", expected: "Created" }, + { op: AuditOperationType.Update, new: "", old: "-", expected: "Edited" }, + { op: AuditOperationType.PublishStatusChange, new: PublishStatus.Deleted, old: "-", expected: "Archived" }, + { op: AuditOperationType.PublishStatusChange, new: PublishStatus.Draft, old: "-", expected: "Unarchived" }, + { op: AuditOperationType.PublishStatusChange, new: PublishStatus.Published, old: "Archived", expected: "Unarchived" }, + { op: AuditOperationType.PublishStatusChange, new: PublishStatus.Published, old: "Draft", expected: "Published" }, + { op: AuditOperationType.PublishStatusChange, new: "foo", old: "", expected: "foo"} + ].forEach((params) => { + const entry = new ApiAuditLog({ + creationDate: "", + operationType: params.op, + user: "", + changedFields: [ { fieldName: "foo", new: params.new, old: params.old } ] }) + expect(component.labelForEntry(entry)).toEqual(params.expected) + }) + }) + + it("visibleFieldName should return", () => { + [ + { fieldname: "sTATEMENT", expected: "Skill Statement" }, + { fieldname: "statement", expected: "Skill Statement" }, + { fieldname: "publishstatus", expected: "Publish Status" }, + { fieldname: "searchingkeywords", expected: "Keywords" }, + { fieldname: "alignments", expected: "Alignment" }, + { fieldname: "jobcodes", expected: "Occupations" }, + { fieldname: "foo", expected: "foo" }, + ].forEach((params) => { + expect(component.visibleFieldName(params.fieldname)).toEqual(params.expected) + }) + }) +}) diff --git a/ui/src/app/richskill/detail/occupations-card-section/occupations-card-section.component.ts b/ui/src/app/richskill/detail/occupations-card-section/occupations-card-section.component.ts index dec043e1d..dd3229bfe 100644 --- a/ui/src/app/richskill/detail/occupations-card-section/occupations-card-section.component.ts +++ b/ui/src/app/richskill/detail/occupations-card-section/occupations-card-section.component.ts @@ -61,7 +61,9 @@ export class OccupationsCardSectionComponent implements OnInit { constructor() { } distinctJobcodes(input: Array): Array { - return input.filter((item, idx, arr) => arr.findIndex(it => it.code === item.code) === idx) + return input.sort((a,b) => b.name?.localeCompare(a.name!) ? 1 : -1) + .filter((item, idx, arr) => arr.findIndex(it => it.code === item.code) === idx) + .sort((a,b) => a.code > b.code ? 1 : -1) } ngOnInit(): void { diff --git a/ui/src/app/richskill/form/rich-skill-form.component.spec.ts b/ui/src/app/richskill/form/rich-skill-form.component.spec.ts new file mode 100644 index 000000000..aef59cab6 --- /dev/null +++ b/ui/src/app/richskill/form/rich-skill-form.component.spec.ts @@ -0,0 +1,583 @@ +// noinspection LocalVariableNamingConventionJS + +import { Location } from "@angular/common" +import { HttpClientTestingModule } from "@angular/common/http/testing" +import { Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { FormBuilder } from "@angular/forms" +import { Title } from "@angular/platform-browser" +import { ActivatedRoute, Router } from "@angular/router" +import { RouterTestingModule } from "@angular/router/testing" +import { of } from "rxjs" +import { + createMockNamedReference, + createMockSkill, + createMockSkillSummary, + createMockUuidReference +} from "../../../../test/resource/mock-data" +import { EnvironmentServiceStub, RichSkillServiceStub } from "../../../../test/resource/mock-stubs" +import { ActivatedRouteStubSpec } from "../../../../test/util/activated-route-stub.spec" +import { AppConfig } from "../../app.config" +import { initializeApp } from "../../app.module" +import { EnvironmentService } from "../../core/environment.service" +import { IJobCode } from "../../job-codes/Jobcode" +import { PublishStatus } from "../../PublishStatus" +import { ToastService } from "../../toast/toast.service" +import { ApiNamedReference, ApiSkill, INamedReference } from "../ApiSkill" +import { ApiSkillSummary } from "../ApiSkillSummary" +import { + ApiReferenceListUpdate, + ApiSkillUpdate, + ApiStringListUpdate, + IReferenceListUpdate, + IStringListUpdate +} from "../ApiSkillUpdate" +import { RichSkillService } from "../service/rich-skill.service" +import { RichSkillFormComponent } from "./rich-skill-form.component" + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let activatedRoute: ActivatedRouteStubSpec +let component: RichSkillFormComponent +let fixture: ComponentFixture + + +describe("RichSkillFormComponent", () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + RichSkillFormComponent + ], + imports: [ + RouterTestingModule, + HttpClientTestingModule + ], + providers: [ + AppConfig, + EnvironmentService, + Title, + FormBuilder, + Location, + ToastService, + { provide: EnvironmentService, useClass: EnvironmentServiceStub }, + { provide: RichSkillService, useClass: RichSkillServiceStub }, + ] + }) + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() + + createComponent(RichSkillFormComponent) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + it("pageTitle should return title", () => { + const date = new Date("2020-06-25T14:58:46.313Z") + + // Arrange + component.isDuplicating = true + // Act + const result1 = component.pageTitle() + // Assert + expect(result1).toEqual("Edit Copy of RSD") + + // Arrange + component.isDuplicating = false + component.existingSkill = new ApiSkill(createMockSkill(date, date, PublishStatus.Draft)) + // Act + const result2 = component.pageTitle() + // Assert + expect(result2).toEqual("Edit Rich Skill Descriptor") + + // Arrange + component.isDuplicating = false + component.existingSkill = null + // Act + const result3 = component.pageTitle() + // Assert + expect(result3).toEqual("Create Rich Skill Descriptor") + }) + + it("nameErrorMessage should return error ", () => { + // Arrange + component.skillForm.get("skillName")?.setValue("Copy of unique name") + // Act + const result1 = component.nameErrorMessage() + // Assert + expect(result1).toEqual("Name is still a copy") + + + // Arrange + component.skillForm.get("skillName")?.setValue("") + // Act + const result2 = component.nameErrorMessage() + // Assert + expect(result2).toEqual("Name is required") + }) + + it("diffUuidList should be correct", () => { + // Arrange + const collections = [ + createMockUuidReference("uuid1", "zebra"), + createMockUuidReference("uuid2", "hippo"), + createMockUuidReference("uuid3", "fox"), + createMockUuidReference("uuid4", "aardvark") + ] + + // Act - adding 1, removing 2 + const updates1 = component.diffUuidList([ "aardvark", "giraffe", "zebra" ], collections) + // Assert + expect(updates1).toEqual(new ApiStringListUpdate( + [ "giraffe" ], + [ "hippo", "fox" ] + )) + + // Act - adding 0, removing 2 + const updates2 = component.diffUuidList([ "aardvark", "zebra" ], collections) + // Assert + expect(updates2).toEqual(new ApiStringListUpdate( + [ ], + [ "hippo", "fox" ] + )) + + // Act - adding 1, removing 0 + const updates3 = component.diffUuidList([ "aardvark", "giraffe", "hippo", "zebra", "fox" ], collections) + // Assert + expect(updates3).toEqual(new ApiStringListUpdate( + [ "giraffe" ], + [ ] + )) + + // Act + const updates4 = component.diffUuidList([ "fox", "aardvark", "hippo", "zebra" ], collections) + // Assert + expect(updates4).toEqual(undefined) + }) + + it("diffStringList should be correct", () => { + // Arrange + const keywords = [ + "zebra", + "hippo", + "fox", + "aardvark" + ] + + // Act - adding 1, removing 2 + const updates1 = component.diffStringList([ "aardvark", "giraffe", "zebra" ], keywords) + // Assert + expect(updates1).toEqual(new ApiStringListUpdate( + [ "giraffe" ], + [ "hippo", "fox" ] + )) + + // Act - adding 0, removing 2 + const updates2 = component.diffStringList([ "aardvark", "zebra" ], keywords) + // Assert + expect(updates2).toEqual(new ApiStringListUpdate( + [ ], + [ "hippo", "fox" ] + )) + + // Act - adding 1, removing 0 + const updates3 = component.diffStringList([ "aardvark", "giraffe", "hippo", "zebra", "fox" ], keywords) + // Assert + expect(updates3).toEqual(new ApiStringListUpdate( + [ "giraffe" ], + [ ] + )) + + // Act + const updates4 = component.diffStringList([ "fox", "aardvark", "hippo", "zebra" ], keywords) + // Assert + expect(updates4).toEqual(undefined) + }) + + it("diffReferenceList should be correct", () => { + // Arrange + const references = [ + createMockNamedReference("id1", "zebra"), + createMockNamedReference("id2", "hippo"), + createMockNamedReference("id3", "fox"), + createMockNamedReference("id4", "aardvark") + ] + + // Act - adding 1, removing 2 + const updates1 = component.diffReferenceList([ "aardvark", "giraffe", "zebra" ], references) + // Assert + expect(updates1).toEqual(new ApiReferenceListUpdate( + [ new ApiNamedReference({ name: "giraffe" }) ], + [ new ApiNamedReference({ name: "hippo" }), new ApiNamedReference({ name: "fox" }) ] + )) + + // Act - adding 0, removing 2 + const updates2 = component.diffReferenceList([ "aardvark", "zebra" ], references) + // Assert + expect(updates2).toEqual(new ApiReferenceListUpdate( + [ ], + [ new ApiNamedReference({ name: "hippo" }), new ApiNamedReference({ name: "fox" }) ] + )) + + // Act - adding 1, removing 0 + const updates3 = component.diffReferenceList([ "aardvark", "giraffe", "hippo", "zebra", "fox" ], references) + // Assert + expect(updates3).toEqual(new ApiReferenceListUpdate( + [ new ApiNamedReference({ name: "giraffe" }) ], + [ ] + )) + + // Act + const updates4 = component.diffReferenceList([ "fox", "aardvark", "hippo", "zebra" ], references) + // Assert + expect(updates4).toEqual(undefined) + }) + + it("splitTextArea should split into words", () => { + // Arrange + const words = "apple; banana;chocolate" // 'banana' has a space that will be trimmed out + // Act + const result = component.splitTextarea(words) + // Assert + expect(result).toEqual([ "apple", "banana", "chocolate" ]) + }) + + it("nonEmptyOrNull should trim and return null for empty strings", () => { + expect(component.nonEmptyOrNull(undefined)).toEqual(undefined) + expect(component.nonEmptyOrNull("")).toEqual(undefined) + expect(component.nonEmptyOrNull("hello")).toEqual("hello") + expect(component.nonEmptyOrNull(" padded ")).toEqual("padded") + }) + + it("updateObject should return updates", () => { + // Arrange + const { // These should not be modified + category, + collections, + skillName, + skillStatement, + // tslint:disable-next-line:no-any + } = setupForm(false) as any + const { // These will be overwritten by the component's selectedXYZ fields + certifications, + employers, + keywords, + occupations, + standards + // tslint:disable-next-line:no-any + } = setupSelectedFields(false) as any + component.isDuplicating = false + component.existingSkill = null // For this test, assume the best + + // Act + const update = component.updateObject() + + // Assert + expect(update.skillName).toEqual(skillName) + expect(update.skillStatement).toEqual(skillStatement) + expect(update.category).toEqual(category) + expect((update.keywords as IStringListUpdate).add).toEqual(keywords) + expect((update.standards as IReferenceListUpdate).add).toEqual(standards) + expect((update.collections as IStringListUpdate).add).toEqual(collections) + expect((update.certifications as IReferenceListUpdate).add).toEqual(certifications) + expect((update.occupations as IStringListUpdate).add).toEqual(occupations) + expect((update.employers as IReferenceListUpdate).add).toEqual(employers) + }) + + it("onSubmit should be correct", () => { + // Arrange + const router = TestBed.inject(Router) + const spyNavigate = spyOn(router, "navigate").and.stub() + component.isDuplicating = false + const date = new Date("2020-06-25T14:58:46.313Z") + const iSkill = createMockSkill(date, date, PublishStatus.Draft) + const skill = new ApiSkill(iSkill) + component.existingSkill = skill + component.skillUuid = iSkill.uuid + setupForm(false) + setupSelectedFields(false) + const richSkillService = TestBed.inject(RichSkillService) + spyOn(richSkillService, "createSkill").and.callThrough() + spyOn(richSkillService, "updateSkill").and.callFake( + (uuid: string, updateObject: ApiSkillUpdate) => of(skill)) + + // Act + component.onSubmit() + + // Assert + component.skillSaved?.subscribe((skil) => { + expect(skil).toEqual(new ApiSkill(iSkill)) + }) + expect(richSkillService.createSkill).not.toHaveBeenCalled() + expect(richSkillService.updateSkill).toHaveBeenCalled() + expect(router.navigate).toHaveBeenCalledWith(["/skills/my skill uuid/manage"]) + }) + + it("namedReferenceString should return NamedReference or undefined", () => { + expect(component.namedReferenceForString("")).toEqual(undefined) + expect(component.namedReferenceForString("a://b")).toEqual(new ApiNamedReference({ id: "a://b" })) + expect(component.namedReferenceForString("abc")).toEqual(new ApiNamedReference({ name: "abc" })) + }) + + it("stringFromJobCode should return string", () => { + expect(component.stringFromJobCode(undefined)).toEqual("") + expect(component.stringFromJobCode({ code: "abcd" })).toEqual("abcd") + expect(component.stringFromJobCode({ name: "id1" } as IJobCode)).toEqual("") + }) + + it("setSkill should be correct", () => { + // Arrange + component.isDuplicating = false + const date = new Date("2020-06-25T14:58:46.313Z") + const iSkill = createMockSkill(date, date, PublishStatus.Draft) + const skill = new ApiSkill(iSkill) + const titleService = TestBed.inject(Title) + + // Act + component.setSkill(skill) + + // Assert + expect(component.existingSkill).toEqual(skill) + expect(titleService.getTitle()).toEqual(`${skill.skillName} | Edit Rich Skill Descriptor | OSMT`) + }) + + it("handleFormErrors should ignore them", () => { + expect(component.handleFormErrors(undefined)).toBeFalsy() + }) + + it("handleClickCancel should go back", () => { + // Arrange + const location = TestBed.inject(Location) + spyOn(location, "back").and.stub() + + // Act + const result = component.handleClickCancel() + + // Assert + expect(result).toBeFalsy() + expect(location.back).toHaveBeenCalled() + }) + + it("showAuthor should return", () => { + expect(component.showAuthor()).toBeTruthy() + }) + + it("populateTypeAheadFieldsWithResults should fill form with defaults", () => { + // Arrange + const form = component.skillForm.value + setupSelectedFields(false) + + // Act + component.populateTypeAheadFieldsWithResults() + + // Assert + expect(form.standards).toEqual("standard1; standard2") + expect(form.occupations).toEqual("occupation1; occupation2") + expect(form.keywords).toEqual("keyword1; keyword2") + expect(form.certifications).toEqual("certification1; certification2") + expect(form.employers).toEqual("employer1; employer2") + }) + + it("handleStandardsTypeAheadResults should be correct", () => { + // Arrange + component.selectedStandards = [] + const strings = [ "string1", "string2" ] + + // Act + component.handleStandardsTypeAheadResults(strings) + + // Assert + expect(component.selectedStandards).toEqual(strings) + }) + + it("handleJobCodesTypeAheadResults should be correct", () => { + // Arrange + component.selectedJobCodes = [] + const strings = [ "string1", "string2" ] + + // Act + component.handleJobCodesTypeAheadResults(strings) + + // Assert + expect(component.selectedJobCodes).toEqual(strings) + }) + + it("handleKeywordTypeAheadResults should be correct", () => { + // Arrange + component.selectedKeywords = [] + const strings = [ "string1", "string2" ] + + // Act + component.handleKeywordTypeAheadResults(strings) + + // Assert + expect(component.selectedKeywords).toEqual(strings) + }) + + it("handleCertificationTypeAheadResults should be correct", () => { + // Arrange + component.selectedCertifications = [] + const strings = [ "string1", "string2" ] + + // Act + component.handleCertificationTypeAheadResults(strings) + + // Assert + expect(component.selectedCertifications).toEqual(strings) + }) + + it("handleEmployersTypeAheadResults should be correct", () => { + // Arrange + component.selectedEmployers = [] + const strings = [ "string1", "string2" ] + + // Act + component.handleEmployersTypeAheadResults(strings) + + // Assert + expect(component.selectedEmployers).toEqual(strings) + }) + + it("handleStatementBlur should be correct", () => { + // Arrange + const value = "test" + component.skillForm.controls.skillStatement.setValue(value) + spyOn(component, "checkForStatementSimilarity").and.callFake((statement) => { + expect(statement).toEqual(value) + }) + + // Act + component.handleStatementBlur(new FocusEvent("some type")) + }) + + it("hasStatementWarning should detect similar skills", () => { + component.similarSkills = [ new ApiSkillSummary(createMockSkillSummary())] + expect(component.hasStatementWarning).toBeTruthy() + + component.similarSkills = [] + expect(component.hasStatementWarning).toBeFalsy() + }) +}) + +describe("RichSkillFormComponent (with parameter)", () => { + const EXPECTED_UUID = "126" + + beforeEach(() => { + activatedRoute = new ActivatedRouteStubSpec() + }) + + beforeEach(async(() => { + const routerSpy = ActivatedRouteStubSpec.createRouterSpy() + + TestBed.configureTestingModule({ + declarations: [ + RichSkillFormComponent + ], + imports: [ + RouterTestingModule, + HttpClientTestingModule + ], + providers: [ + AppConfig, + EnvironmentService, + Title, + FormBuilder, + Location, + ToastService, + { provide: EnvironmentService, useClass: EnvironmentServiceStub }, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: RichSkillService, useClass: RichSkillServiceStub }, + ] + }) + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() + + activatedRoute.setParamMap({ uuid: EXPECTED_UUID }) + createComponent(RichSkillFormComponent) + })) + + it("should be created with uuid parameter", () => { + // Arrange + const richSkillService = TestBed.inject(RichSkillService) + richSkillService.getSkillByUUID("any").subscribe((skill) => { + // Assert + expect(component).toBeTruthy() + expect(component.skillUuid).toEqual(EXPECTED_UUID) + expect(component.existingSkill).toEqual(skill) + }) + }) +}) + + +function setupForm(isBlank: boolean): object { + const form = component.skillForm + const fields = isBlank + ? { + skillName: "", + skillStatement: "", + author: "", + category: "", + keywords: [], + collections: [], + occupations: [], + standards: [], + certifications: [], + employers: [] + } + : { + skillName: "my skill", + skillStatement: "my statement", + author: "my author", + category: "my category", + keywords: ["keyword1", "keyword2", "keyword3"], + collections: ["collection1", "collection2"], + occupations: ["occupation1", "occupation2", "occupation3"], + standards: ["standard1", "standard2", "standard3"], + certifications: ["certification1", "certification2", "certification3"], + employers: ["employer1", "employer2", "employer3"] + } + form.setValue(fields) + return fields +} + +function setupSelectedFields(isBlank: boolean): object { + if (isBlank) { + component.selectedKeywords = [""] + component.selectedJobCodes = [] + component.selectedStandards = [] + component.selectedCertifications = [] + component.selectedEmployers = [] + } + else { + component.selectedKeywords = ["keyword1", "keyword2"] + component.selectedJobCodes = ["occupation1", "occupation2"] + component.selectedStandards = ["standard1", "standard2"] + component.selectedCertifications = ["certification1", "certification2"] + component.selectedEmployers = ["employer1", "employer2"] + } + + // Return the new values for easy deconstruction + return { + keywords: component.selectedKeywords, + occupations: component.selectedJobCodes, + standards: component.selectedStandards.map(x => new ApiNamedReference({ id: undefined, name: x }) as INamedReference), + certifications: component.selectedCertifications.map(x => new ApiNamedReference({ id: undefined, name: x })), + employers: component.selectedEmployers.map(x => new ApiNamedReference({ id: undefined, name: x })) + } +} diff --git a/ui/src/app/richskill/form/rich-skill-form.component.ts b/ui/src/app/richskill/form/rich-skill-form.component.ts index d027a9ce3..6ae273573 100644 --- a/ui/src/app/richskill/form/rich-skill-form.component.ts +++ b/ui/src/app/richskill/form/rich-skill-form.component.ts @@ -28,7 +28,7 @@ import {Title} from "@angular/platform-browser" import {HasFormGroup} from "../../core/abstract-form.component" import {notACopyValidator} from "../../validators/not-a-copy.validator" import {ApiSkillSummary} from "../ApiSkillSummary" -import {Whitelabelled} from "../../../whitelabel"; +import {Whitelabelled} from "../../../whitelabel" @Component({ @@ -94,7 +94,6 @@ export class RichSkillFormComponent extends Whitelabelled implements OnInit, Has this.searchingSimilarity = undefined } }) - } pageTitle(): string { @@ -288,6 +287,18 @@ export class RichSkillFormComponent extends Whitelabelled implements OnInit, Has stringFromAlignment(ref?: IAlignment): string { return ref?.skillName ?? ref?.id ?? "" } + + namedReferenceForString(value: string): ApiNamedReference | undefined { + const str = value.trim() + if (str.length < 1) { + return undefined + } else if (str.indexOf("://") !== -1) { + return new ApiNamedReference({id: str}) + } else { + return new ApiNamedReference({name: str}) + } + } + stringFromNamedReference(ref?: INamedReference): string { return ref?.name ?? ref?.id ?? "" } diff --git a/ui/src/app/richskill/form/skill-collections-display.component.spec.ts b/ui/src/app/richskill/form/skill-collections-display.component.spec.ts new file mode 100644 index 000000000..b7bcd4446 --- /dev/null +++ b/ui/src/app/richskill/form/skill-collections-display.component.spec.ts @@ -0,0 +1,112 @@ +import { Component, Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { FormControl } from "@angular/forms" +import { By } from "@angular/platform-browser" +import { SkillCollectionsDisplayComponent } from "./skill-collections-display.component" + + +const EXPECTED_CONTROL_VALUES = ["value1", "value2"] +const EXPECTED_LABEL = "mylabel" +const EXPECTED_PLACEHOLDER = "myplaceholder" +const EXPECTED_ERRORMESSAGE = "myerror" +const EXPECTED_HELPMESSAGE = "myhelp" +const EXPECTED_REQUIRED = true +const EXPECTED_NAME = "myname" + + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + myControl = new FormControl(EXPECTED_CONTROL_VALUES) + myLabel = EXPECTED_LABEL + myPlaceholder = EXPECTED_PLACEHOLDER + myErrorMessage = EXPECTED_ERRORMESSAGE + myHelpMessage = EXPECTED_HELPMESSAGE + myRequired = EXPECTED_REQUIRED + myName = EXPECTED_NAME +} + + +export function createComponent(T: Type): Promise { + hostFixture = TestBed.createComponent(T) + hostComponent = hostFixture.componentInstance + + const debugEl = hostFixture.debugElement.query(By.directive(SkillCollectionsDisplayComponent)) + childComponent = debugEl.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + hostFixture.detectChanges() + + return hostFixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + hostFixture.detectChanges() + }) +} + + +let hostFixture: ComponentFixture +let hostComponent: TestHostComponent +let childComponent: SkillCollectionsDisplayComponent + + +describe("TestHostComponent", () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + SkillCollectionsDisplayComponent, + TestHostComponent + ], + }) + .compileComponents() + + createComponent(TestHostComponent) + })) + + it("should be created", () => { + expect(hostComponent).toBeTruthy() + + expect(childComponent.control.value).toEqual(EXPECTED_CONTROL_VALUES) + expect(childComponent.label).toEqual(EXPECTED_LABEL) + expect(childComponent.placeholder).toEqual(EXPECTED_PLACEHOLDER) + expect(childComponent.errorMessage).toEqual(EXPECTED_ERRORMESSAGE) + expect(childComponent.helpMessage).toEqual(EXPECTED_HELPMESSAGE) + expect(childComponent.required).toEqual(EXPECTED_REQUIRED) + expect(childComponent.name).toEqual(EXPECTED_NAME) + }) + + it("handleClickIcon should remove", () => { + // Arrange + childComponent.toRemove = [] + + // Act + const result = childComponent.handleClickIcon("value2") + + // Assert + expect(result).toBeFalse() + expect(childComponent.control.value).toEqual(["value1"]) + }) + + it("handleClickIcon should replace (undo)", () => { + // Arrange + childComponent.toRemove = [ "value3" ] + + // Act + const result = childComponent.handleClickIcon("value3") + + // Assert + expect(result).toBeFalse() + expect(childComponent.toRemove.length).toEqual(0) + // expect(childComponent.control.value).toEqual(["value1", "value3"]) // value is not consistent. + }) +}) diff --git a/ui/src/app/richskill/import/batch-import.component.spec.ts b/ui/src/app/richskill/import/batch-import.component.spec.ts new file mode 100644 index 000000000..b5f8fa3c3 --- /dev/null +++ b/ui/src/app/richskill/import/batch-import.component.spec.ts @@ -0,0 +1,387 @@ +import { Location } from "@angular/common" +import { HttpClientTestingModule } from "@angular/common/http/testing" +import { Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { Title } from "@angular/platform-browser" +import { ActivatedRoute, Router } from "@angular/router" +import { Papa, ParseResult } from "ngx-papaparse" +import { ActivatedRouteStubSpec } from "test/util/activated-route-stub.spec" +import { TestPage } from "test/util/test-page.spec" +import { EnvironmentServiceStub, RichSkillServiceStub } from "../../../../test/resource/mock-stubs" +import { AppConfig } from "../../app.config" +import { EnvironmentService } from "../../core/environment.service" +import { ToastService } from "../../toast/toast.service" +import { RichSkillService } from "../service/rich-skill.service" +import { BatchImportComponent, ImportStep } from "./batch-import.component" + + +class Page extends TestPage { + get myElement(): HTMLInputElement { return this.query("#mycomponent-mytype-myelement") } + +// myMethodSpy: jasmine.Spy + + constructor(aFixture: ComponentFixture) { + super(aFixture) + + const aComponent = aFixture.componentInstance + // this.myMethodSpy = spyOn(aComponent, 'myMethodSpy').and.callThrough(); + } +} + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + page = new Page(fixture) + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let activatedRoute: ActivatedRouteStubSpec +let component: BatchImportComponent +let fixture: ComponentFixture +let page: Page + + +describe("BatchImportComponent", () => { + beforeEach(() => { + activatedRoute = new ActivatedRouteStubSpec() + }) + + beforeEach(async(() => { + const routerSpy = ActivatedRouteStubSpec.createRouterSpy() + + TestBed.configureTestingModule({ + declarations: [ + BatchImportComponent + ], + imports: [ + HttpClientTestingModule + ], + providers: [ + AppConfig, + Location, + ToastService, + Papa, + Title, + { provide: EnvironmentService, useClass: EnvironmentServiceStub }, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: routerSpy }, + { provide: RichSkillService, useClass: RichSkillServiceStub }, + ] + }) + .compileComponents() + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() + + // const environmentService = TestBed.inject(EnvironmentService) + + activatedRoute.setParamMap({ userId: 126 }) + createComponent(BatchImportComponent) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + it("stepName should be correct", () => { + expect(Object.keys(ImportStep).length / 2).toEqual(4) // /2 because enums have a reverse mapping too: { "Foo": 1, "1": "Foo" } + expect(component.stepName(ImportStep.UploadFile)).toEqual("Select File") + expect(component.stepName(ImportStep.FieldMapping)).toEqual("Map Fields") + expect(component.stepName(ImportStep.ReviewRecords)).toEqual("Review and Import") + expect(component.stepName(ImportStep.Success)).toEqual("Success!") + }) + + it("nextButtonLabel should be correct", () => { + // Arrange + component.currentStep = ImportStep.ReviewRecords + + // Act + const result = component.nextButtonLabel + + // Assert + expect(result).toEqual("Import") + }) + + it("cancelButtonLabel should be correct", () => { + // Arrange + component.currentStep = ImportStep.FieldMapping + + // Act + const result = component.cancelButtonLabel + + // Assert + expect(result).toEqual("Cancel Import") + }) + + it("recordCount should be correct", () => { + // Arrange + component.parseResults = makeResults() + + // Act + const count = component.recordCount + + // Assert + expect(count).toEqual(component.parseResults.data.length) + }) + + it("hideStepLoader should be correct", () => { + component.hideStepLoader() + expect(component.stepLoaded).toBeFalsy() + }) + + it("showStepLoader should be correct", () => { + component.showStepLoader() + expect(component.stepLoaded).toBeTruthy() + }) + + it("handleClickNext should handle", () => { + [ + { input: ImportStep.UploadFile, expected: ImportStep.FieldMapping }, + { input: ImportStep.FieldMapping, expected: ImportStep.ReviewRecords }, + { input: ImportStep.ReviewRecords, expected: ImportStep.Success }, + ].forEach((params) => { + // Arrange + component.parseResults = makeResults() + component.currentStep = params.input + + // Act + component.handleClickNext() + + // Assert + expect(component.currentStep.valueOf()).toEqual(params.expected) + }) + }) + + it("handleClickCancel should handle", () => { + [ + { input: ImportStep.ReviewRecords, expected: ImportStep.FieldMapping }, + { input: ImportStep.FieldMapping, expected: ImportStep.UploadFile }, + { input: ImportStep.UploadFile, expected: ImportStep.UploadFile }, + { input: ImportStep.Success, expected: ImportStep.Success }, + ].forEach((params) => { + // Arrange + component.parseResults = makeResults() + component.currentStep = params.input + + // Act + component.handleClickCancel() + + // Assert + expect(component.currentStep.valueOf()).toEqual(params.expected) + }) + }) + + it("isMappingValid should return true", () => { + // Arrange + component.fieldMappings = { + key1: "skillName", + key2: "skillStatement" + } + + // Act + const result = component.isMappingValid() + + // Assert + expect(result).toBeTruthy() + }) + + it("isMappingValid should detect empty map", () => { + // Arrange + component.fieldMappings = undefined + + // Act + const result = component.isMappingValid() + + // Assert + expect(result).toBeFalse() + }) + + it("isMappingValid should detect missing key", () => { + // Arrange + component.fieldMappings = { + // key1: "skillName", + key2: "skillStatement" + } + + // Act + const result = component.isMappingValid() + + // Assert + expect(result).toBeFalse() + }) + + it("isMappingValid should detect duplicate", () => { + // Arrange + component.fieldMappings = { + key1: "skillName", + key2: "skillStatement", + key3: "skillStatement" + } + + // Act + const result = component.isMappingValid() + + // Assert + expect(result).toBeFalse() + }) + + it("handleFileDrop should be correct", () => { + // Arrange + + // Act + const result = component.handleFileDrop(new DragEvent("x")) + + // Assert + expect(result).toBeFalse() + }) + + it("handleFileDrag should be correct", () => { + // Arrange + + // Act + const result = component.handleFileDrag(new DragEvent("x")) + + // Assert + expect(result).toBeFalse() + }) + + it("handleFileLeave should be correct", () => { + // Arrange + + // Act + const result = component.handleFileLeave(new DragEvent("x")) + + // Assert + expect(result).toBeTruthy() + }) + + it("handleFileChange should be correct", () => { + // Arrange + component.currentStep = ImportStep.Success + const f = new File([""], "filename", { type: "text/html" }) + const evt = { target: { files: [f] }} + + // Act + const result = component.handleFileChange(evt as unknown as Event) + + // Assert + expect(component.currentStep.valueOf()).toEqual(ImportStep.UploadFile) + }) + + it("duplicateFieldNames should detect single duplicate", () => { + // Arrange + component.fieldMappings = { + key1: "skillName", + key2: "skillStatement", + key3: "skillStatement" + } + + // Act + const result = component.duplicateFieldNames() + console.log("batch-import.spec: result", result) + // Assert + expect(result).toEqual("\"Skill Statement\"") + }) + + it("duplicateFieldNames should detect duplicates", () => { + // Arrange + component.fieldMappings = { + key1: "skillName", + key2: "skillName", + key3: "skillStatement", + key4: "skillStatement" + } + + // Act + const result = component.duplicateFieldNames() + console.log("batch-import.spec: result", result) + // Assert + expect(result).toEqual("\"RSD Name\", and \"Skill Statement\"") + }) + + it("handleSimilarityOk should set correctly", () => { + // Arrange/Act + component.handleSimilarityOk(true) + // Assert + expect(component.importSimilarSkills).toBeTrue() + + // Arrange/Act + component.handleSimilarityOk(false) + // Assert + expect(component.importSimilarSkills).toBeFalse() + }) +}) + + +function makeResults(): ParseResult { + return { + data: [ + { + "RSD Name": "", + Author: "Data and Data Store Access", + "Skill Statement": ".NET Framework", + Category: "Access data and data stores using the .NET Framework.", + Keywords: ".NET Framework; ADO.NET; Language Integrated Query (LINQ); WCF Data Services; XML", + Standards: "15-0000", + Certifications: "15-1200", + "Occupation Major Groups": "15-1220; 15-1250; 15-1290", + "Occupation Minor Groups": "15-1251; 15-1252; 15-1253; 15-1254; 15-1255; 15-1221; 15-1299", + "Broad Occupations": "", + "Detailed Occupations": "", + "O*NET Job Codes": "", + Employers: ".NET Framework", + "Alignment Name": "https://skills.emsidata.com/skills/KS1200B62W5ZF38RJ7TD" + }, + { + "RSD Name": "" + } + ], + errors: [ + { + type: "FieldMismatch", + code: "TooFewFields", + message: "Too few fields: expected 15 fields but parsed 14", + row: 0 + }, + { + type: "FieldMismatch", + code: "TooFewFields", + message: "Too few fields: expected 15 fields but parsed 1", + row: 1 + } + ], + meta: { + delimiter: ",", + linebreak: "\n", + aborted: false, + truncated: false, + // cursor: 604, + fields: [ + "RSD Name", + "Author", + "Skill Statement", + "Category", + "Keywords", + "Standards", + "Certifications", + "Occupation Major Groups", + "Occupation Minor Groups", + "Broad Occupations", + "Detailed Occupations", + "O*NET Job Codes", + "Employers", + "Alignment Name", + "Alignment URL" + ] + } + } + } diff --git a/ui/src/app/richskill/list/skills-list.component.spec.ts b/ui/src/app/richskill/list/skills-list.component.spec.ts new file mode 100644 index 000000000..76bcd4d92 --- /dev/null +++ b/ui/src/app/richskill/list/skills-list.component.spec.ts @@ -0,0 +1,473 @@ +// noinspection MagicNumberJS,LocalVariableNamingConventionJS + +import { Component, ElementRef, Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { RouterTestingModule } from "@angular/router/testing" +import { createMockPaginatedSkills, createMockSkillSummary } from "../../../../test/resource/mock-data" +import { RichSkillServiceStub } from "../../../../test/resource/mock-stubs" +import { PublishStatus } from "../../PublishStatus" +import { ToastService } from "../../toast/toast.service" +import { ApiSortOrder } from "../ApiSkill" +import { ApiSearch, PaginatedSkills } from "../service/rich-skill-search.service" +import { RichSkillService } from "../service/rich-skill.service" +import { SkillsListComponent } from "./skills-list.component" + + +@Component({ + selector: "app-concrete-component", + template: `` +}) +class ConcreteComponent extends SkillsListComponent { + matchingQuery?: string[] + title = "Concrete Skills" + + loadNextPage(): void {} + + handleSelectAll(selectAllChecked: boolean): void {} + + public setResults(results: PaginatedSkills): void { + super.setResults(results) + } +} + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let component: ConcreteComponent +let fixture: ComponentFixture + + +describe("SkillsListComponent", () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + ConcreteComponent + ], + imports: [ + RouterTestingModule.withRoutes([ + { path: "collections/add-skills", component: SkillsListComponent } + ]) + ], + providers: [ + ToastService, + { provide: RichSkillService, useClass: RichSkillServiceStub }, + ] + }) + + createComponent(ConcreteComponent) + component.titleElement = new ElementRef(document.getElementById("titleHeading")) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + it("title should be correct", () => { + expect(component.title).toEqual("Concrete Skills") + }) + + it("setResults should set results", () => { + // Arrange + const paginatedSkills = createMockPaginatedSkills() + + // Act + component.setResults(paginatedSkills) + + // Assert + expect(component.results).toEqual(paginatedSkills) + expect(component.selectedSkills).toBeFalsy() + expect(component.totalCount).toEqual(paginatedSkills.totalCount) + expect(component.curPageCount).toEqual(paginatedSkills.skills.length) + expect(component.getSelectAllCount()).toEqual(component.curPageCount) + }) + + it("skillCountLabel should be correct", () => { + component.setResults(createMockPaginatedSkills(0, 0)) + expect(component.skillCountLabel).toEqual("0 RSDs") + + component.setResults(createMockPaginatedSkills(0, 1)) + expect(component.skillCountLabel).toEqual("1 RSD") + + component.setResults(createMockPaginatedSkills(1, 1)) + expect(component.skillCountLabel).toEqual("1 RSD") + + component.setResults(createMockPaginatedSkills(1, 10)) + expect(component.skillCountLabel).toEqual("10 RSDs") + }) + + it("totalCount should be correct", () => { + component.setResults(createMockPaginatedSkills(11, 23)) + expect(component.totalCount).toEqual(23) + }) + + it("curPageCount should be correct", () => { + component.setResults(createMockPaginatedSkills(11, 23)) + expect(component.curPageCount).toEqual(11) + }) + + it("getMobileSortOptions should be correct", () => { + const result = component.getMobileSortOptions() + expect(result).toEqual({ + "name.asc": "Category (ascending)", + "name.desc": "Category (descending)", + "skill.asc": "RSD Name (ascending)", + "skill.desc": "RSD Name (descending)", + }) + }) + + it("emptyResults should be correct", () => { + component.setResults(createMockPaginatedSkills(0, 0)) + expect(component.emptyResults).toBeTruthy() + + component.setResults(createMockPaginatedSkills(1, 1)) + expect(component.emptyResults).toBeFalsy() + }) + + it("firstRecordNo should return", () => { + // Note that we're choosing primes for inputs to avoid math irregularities + component.from = 11 + expect(component.firstRecordNo).toEqual(12) + }) + + it("lastRecordNo should return", () => { + component.from = 11 + component.size = 47 + component.setResults(createMockPaginatedSkills(29, 123)) + expect(component.lastRecordNo).toEqual(11 + 29) // Expecting from+curPageCount + + component.from = 11 + component.size = 47 + component.setResults(createMockPaginatedSkills(12, 13)) + expect(component.lastRecordNo).toEqual(13) // Expecting totalCount + }) + + it("totalPageCount should return", () => { + component.size = 47 + component.setResults(createMockPaginatedSkills(29, 123)) + expect(component.totalPageCount).toEqual(Math.trunc((123 + 46) / 47)) + }) + + it("currentPageNo should return", () => { + component.from = 11 + component.size = 47 + expect(component.currentPageNo).toEqual(Math.trunc(11 / 47) + 1) + }) + + it("navigateToPage should load next page", () => { + spyOn(component, "loadNextPage").and.callThrough() + + component.from = 11 + component.size = 47 + component.navigateToPage(31) + expect(component.from).toEqual((31 - 1) * 47) + expect(component.loadNextPage).toHaveBeenCalledTimes(1) + }) + + it("actionsVisible should be true", () => { + expect(component.actionsVisible()).toBeTruthy() + }) + + it("publishVisible should be correct", () => { + /* Assumption: status doesn't matter! */ + // id, date undefined + const result0 = component.publishVisible( + createMockSkillSummary("id1", PublishStatus.Draft, "") + ) + expect(result0).toBeTruthy() + + // No skills + component.selectedSkills = [] + const result1 = component.publishVisible(undefined) + expect(result1).toBeFalsy() + + // one with date, one without + component.selectedSkills = [ + createMockSkillSummary("id1", PublishStatus.Draft, ""), // No publishDate + createMockSkillSummary("id1", PublishStatus.Draft) // Has publishDate + ] + const result2 = component.publishVisible(undefined) + expect(result2).toBeTruthy() + }) + + it("archiveVisible should be correct", () => { + /* Assumption: status *does* matter. */ + // id, different status + const result0 = component.archiveVisible( + createMockSkillSummary("id1", PublishStatus.Draft) + ) + expect(result0).toBeTruthy() + + // No skills + component.selectedSkills = [] + const result1 = component.archiveVisible(undefined) + expect(result1).toBeFalsy() + + // one with date, one without + component.selectedSkills = [ + createMockSkillSummary("id1", PublishStatus.Draft), + createMockSkillSummary("id1", PublishStatus.Archived) + ] + const result2 = component.archiveVisible(undefined) + expect(result2).toBeTruthy() + }) + + it("unarchiveVisible should be correct", () => { + /* Assumption: status *does* matter. */ + // id, different status + const result0 = component.unarchiveVisible( + createMockSkillSummary("id1", PublishStatus.Archived) + ) + expect(result0).toBeTruthy() + + // No skills + component.selectedSkills = [] + const result1 = component.unarchiveVisible(undefined) + expect(result1).toBeFalsy() + + // one with date, one without + component.selectedSkills = [ + createMockSkillSummary("id1", PublishStatus.Draft), + createMockSkillSummary("id1", PublishStatus.Archived) + ] + const result2 = component.unarchiveVisible(undefined) + expect(result2).toBeTruthy() + }) + + it("addToCollectionVisible should be correct", () => { // No skills + /* Assumption: skill parameter does not matter. */ + component.selectedSkills = [] + const result1 = component.addToCollectionVisible(undefined) + expect(result1).toBeFalsy() + + // one with date, one without + component.selectedSkills = [ + createMockSkillSummary("id1", PublishStatus.Draft), + createMockSkillSummary("id1", PublishStatus.Archived) + ] + const result2 = component.addToCollectionVisible(undefined) + expect(result2).toBeTruthy() + }) + + it("handleFiltersChanged should be correct", () => { + // Arrange + const newFilters = new Set([PublishStatus.Unarchived]) + spyOn(component, "loadNextPage").and.callThrough() + + // Act + component.handleFiltersChanged(newFilters) + + // Assert + expect(component.selectedFilters).toEqual(newFilters) + expect(component.loadNextPage).toHaveBeenCalledTimes(1) + }) + + it("handlePageClicked should be correct", () => { + spyOn(component, "loadNextPage").and.callThrough() + + component.from = 11 + component.size = 47 + component.handlePageClicked(31) + expect(component.from).toEqual((31 - 1) * 47) + expect(component.loadNextPage).toHaveBeenCalledTimes(1) + }) + + it("handleNewSelection should be correct", () => { + const selected = createMockPaginatedSkills(2, 3).skills + component.handleNewSelection(selected) + expect(component.selectedSkills).toEqual(selected) + }) + + it("handleHeaderColumnSort should load next page", () => { + // Arrange + spyOn(component, "loadNextPage").and.callThrough() + const sort = ApiSortOrder.SkillDesc + component.columnSort = ApiSortOrder.NameAsc + component.from = 47 + + // Act + component.handleHeaderColumnSort(sort) + + // Assert + expect(component.columnSort).toEqual(sort) + expect(component.from).toEqual(0) + expect(component.loadNextPage).toHaveBeenCalledTimes(1) + }) + + it("rowActions should be correct", () => { + let rowActions = component.rowActions() + expect(rowActions).toBeTruthy() + + const skill0 = createMockSkillSummary("id0", PublishStatus.Draft) + const action0 = rowActions[0] + expect(action0.label).toEqual("Archive RSD") + expect(action0 && action0.callback).toBeTruthy() + expect(action0.callback?.(action0, skill0)).toBeFalsy() // Always false + expect(action0.visible?.(skill0)).toBeTruthy() // != Archived + + const skill1 = createMockSkillSummary("id1", PublishStatus.Archived) + const action1 = rowActions[1] + expect(action1.label).toEqual("Unarchive RSD") + expect(action1 && action1.callback).toBeTruthy() + expect(action1.callback?.(action1, skill1)).toBeFalsy() // Always false + expect(action1.visible?.(skill1)).toBeTruthy() // == Archived + + spyOn(window, "confirm").and.returnValue(true) + const skill2 = createMockSkillSummary("id2", PublishStatus.Draft, "") + const action2 = rowActions[2] + expect(action2.label).toEqual("Publish RSD") + expect(action2 && action2.callback).toBeTruthy() + expect(action2.callback?.(action2, skill2)).toBeFalsy() // Always false + expect(action2.visible?.(skill2)).toBeTruthy() // !has publish date + + component.showAddToCollection = true + rowActions = component.rowActions() + let skill3 = createMockSkillSummary("id3", PublishStatus.Draft, "") + let action3 = rowActions[3] + expect(action3.label).toEqual("Add to Collection") + expect(action3 && action3.callback).toBeTruthy() + expect(action3.callback?.(action3, skill3)).toBeFalsy() // Always false + + component.showAddToCollection = false + rowActions = component.rowActions() + skill3 = createMockSkillSummary("id3", PublishStatus.Draft, "") + action3 = rowActions[3] + expect(action3.label).toEqual("Remove from Collection") + expect(action3 && action3.callback).toBeTruthy() + expect(action3.callback?.(action3, skill3)).toBeFalsy() // Always false + }) + + it("tableActions should be correct", () => { + let tableActions = component.tableActions() + expect(tableActions).toBeTruthy() + + const skill0 = createMockSkillSummary("id0", PublishStatus.Draft, "") + const action0 = tableActions[0] + expect(action0.label).toEqual("Back to Top") + expect(action0 && action0.callback).toBeTruthy() + expect(action0.callback?.(action0, skill0)).toBeFalsy() // Always false + expect(action0.visible?.(skill0)).toBeTruthy() // Always true + + spyOn(window, "confirm").and.returnValue(true) + const skill1 = createMockSkillSummary("id1", PublishStatus.Draft, "") + const action1 = tableActions[1] + expect(action1.label).toEqual("Publish") + expect(action1 && action1.callback).toBeTruthy() + expect(action1.callback?.(action1, skill1)).toBeFalsy() // Always false + expect(action1.visible?.(skill1)).toBeTruthy() // !has publish date + + const skill2 = createMockSkillSummary("id2", PublishStatus.Draft) + const action2 = tableActions[2] + expect(action2.label).toEqual("Archive") + expect(action2 && action2.callback).toBeTruthy() + expect(action2.callback?.(action2, skill2)).toBeFalsy() // Always false + expect(action2.visible?.(skill2)).toBeTruthy() // == Archived + + const skill3 = createMockSkillSummary("id3", PublishStatus.Draft) + const action3 = tableActions[3] + expect(action3.label).toEqual("Unarchive") + expect(action3 && action3.callback).toBeTruthy() + expect(action3.callback?.(action3, skill3)).toBeFalsy() // Always false + expect(action3.visible?.(skill3)).toBeFalsy() // != Archived + + component.selectedSkills = [ + createMockSkillSummary("id1", PublishStatus.Draft), + ] + component.showAddToCollection = true + tableActions = component.tableActions() + let skill4 = createMockSkillSummary("id4", PublishStatus.Archived) + let action4 = tableActions[4] + expect(action4.label).toEqual("Add to Collection") + expect(action4 && action4.callback).toBeTruthy() + expect(action4.callback?.(action4, skill4)).toBeFalsy() // Always false + expect(action4.visible?.(skill4)).toBeTruthy() // There are selected skills + + component.selectedSkills = [ + createMockSkillSummary("id1", PublishStatus.Draft), + ] + component.showAddToCollection = false + tableActions = component.tableActions() + skill4 = createMockSkillSummary("id4", PublishStatus.Archived) + action4 = tableActions[4] + expect(action4.label).toEqual("Remove from Collection") + expect(action4 && action4.callback).toBeTruthy() + expect(action4.callback?.(action4, skill4)).toBeFalsy() // Always false + expect(action4.visible?.(skill4)).toBeTruthy() // There are selected skills + }) + + it("getSelectedSkills should be correct", () => { + component.selectedSkills = [ + createMockSkillSummary("id1", PublishStatus.Draft), + ] + + expect(component.getSelectedSkills( + undefined + )).toEqual(component.selectedSkills) + + const skill = createMockSkillSummary() + expect(component.getSelectedSkills( + )).toEqual([skill]) + }) + + it("selectedUuids should be correct", () => { + component.selectedSkills = [ + createMockSkillSummary("id1", PublishStatus.Draft), + ] + + expect(component.selectedUuids( + undefined + )).toEqual([component.selectedSkills[0].uuid]) + + const skill = createMockSkillSummary() + expect(component.selectedUuids( + )).toEqual([skill.uuid]) + }) + + it("getApiSearch should be correct", () => { + const skill = createMockSkillSummary("id2", PublishStatus.Draft) + const uuids = [skill.uuid] + + expect(component.getApiSearch(skill)).toEqual( + new ApiSearch({ uuids }) + ) + + component.selectedSkills = undefined + expect(component.getApiSearch(undefined)).toEqual( + undefined + ) + + component.selectedSkills = [ + createMockSkillSummary("id1", PublishStatus.Draft) + ] + expect(component.getApiSearch(undefined)).toEqual( + new ApiSearch({ uuids: [component.selectedSkills[0].uuid] }) + ) + }) + + it("submitStatusChange should be correct", () => { + component.selectedSkills = undefined + expect(component.submitStatusChange(PublishStatus.Published, "published", undefined)).toEqual( + false + ) + + const skill = createMockSkillSummary("id2", PublishStatus.Draft) + expect(component.submitStatusChange(PublishStatus.Published, "published", skill)).toEqual( + false + ) + }) + + it("getSelectAllEnabled should be true", () => { + expect(component.getSelectAllEnabled()).toBeTruthy() + }) +}) diff --git a/ui/src/app/richskill/service/rich-skill.service.spec.ts b/ui/src/app/richskill/service/rich-skill.service.spec.ts new file mode 100644 index 000000000..c94086ab6 --- /dev/null +++ b/ui/src/app/richskill/service/rich-skill.service.spec.ts @@ -0,0 +1,142 @@ +import { Location } from "@angular/common" +import { HttpClient } from "@angular/common/http" +import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing" +import { TestBed } from "@angular/core/testing" +import { Router } from "@angular/router" +import { createMockPaginatedSkills, createMockSkill } from "../../../../test/resource/mock-data" +import { AuthServiceData, AuthServiceStub, RouterData, RouterStub } from "../../../../test/resource/mock-stubs" +import { AppConfig } from "../../app.config" +import { AuthService } from "../../auth/auth-service" +import { EnvironmentService } from "../../core/environment.service" +import { PublishStatus } from "../../PublishStatus" +import { ApiSkill, ApiSortOrder } from "../ApiSkill" +import { PaginatedSkills } from "./rich-skill-search.service" +import { RichSkillService } from "./rich-skill.service" + + +// An example of how to test a service + + +describe("RichSkillService", () => { + let httpClient: HttpClient + let httpTestingController: HttpTestingController + let router: RouterStub + let authService: AuthServiceStub + let testService: RichSkillService + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [], + imports: [ + HttpClientTestingModule + ], + providers: [ + EnvironmentService, + AppConfig, + RichSkillService, + Location, + { provide: AuthService, useClass: AuthServiceStub }, + { provide: Router, useClass: RouterStub } + ] + }) + .compileComponents() + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() + + httpClient = TestBed.inject(HttpClient) + httpTestingController = TestBed.inject(HttpTestingController) + router = TestBed.inject(Router) + authService = TestBed.inject(AuthService) + testService = TestBed.inject(RichSkillService) + }) + + afterEach(() => { + httpTestingController.verify() + }) + + it("should be created", () => { + expect(testService).toBeTruthy() + }) + + it("getSkills should return", () => { + // Arrange + RouterData.commands = [] + AuthServiceData.isDown = false + const path = "api/skills?sort=name.asc&status=Draft&size=3&from=0" + const testData: PaginatedSkills = createMockPaginatedSkills(3, 10) + const statuses = new Set([ PublishStatus.Draft ]) + + // Act + // noinspection LocalVariableNamingConventionJS + const result$ = testService.getSkills(testData.skills.length, 0, statuses, ApiSortOrder.NameAsc) + + // Assert + result$ + .subscribe((data: PaginatedSkills) => { + expect(data).toEqual(testData) + expect(RouterData.commands).toEqual([ ]) // No errors + expect(AuthServiceData.isDown).toEqual(false) + }) + + const req = httpTestingController.expectOne(AppConfig.settings.baseApiUrl + "/" + path) + expect(req.request.method).toEqual("GET") + req.flush(testData.skills, { + headers: { "x-total-count": "" + testData.totalCount} + }) + }) + + it("getSkillsByUUID should return", () => { + // Arrange + RouterData.commands = [] + AuthServiceData.isDown = false + const uuid = "uuid1" + const path = "api/skills/" + uuid + const date = new Date("2020-06-25T14:58:46.313Z") + const testData: ApiSkill = new ApiSkill(createMockSkill(date, date, PublishStatus.Draft)) + + // Act + // noinspection LocalVariableNamingConventionJS + const result$ = testService.getSkillByUUID(uuid) + + // Assert + result$ + .subscribe((data: ApiSkill) => { + expect(data).toEqual(testData) + expect(RouterData.commands).toEqual([ ]) // No errors + expect(AuthServiceData.isDown).toEqual(false) + }) + + const req = httpTestingController.expectOne(AppConfig.settings.baseApiUrl + "/" + path) + expect(req.request.method).toEqual("GET") + req.flush(testData) + }) + + it("getSkillCsvByUUID should return", () => { + // Arrange + RouterData.commands = [] + AuthServiceData.isDown = false + const uuid = "f6aacc9e-bfc6-4cc9-924d-c7ef83afef07" + const path = "api/skills/" + uuid + const testData = + "\"Canonical URL\",\"RSD Name\",\"Author\",\"Skill Statement\",\"Category\",\"Keywords\",\"Standards\",\"Certifications\",\"Occupation Major Groups\",\"Occupation Minor Groups\",\"Broad Occupations\",\"Detailed Occupations\",\"O*Net Job Codes\",\"Employers\",\"Alignment Name\",\"Alignment URL\"\n" + + "\"https://localhost:8080/api/skills/f6aacc9e-bfc6-4cc9-924d-c7ef83afef07\",\"Situational Parameters\",\"Western Governors University\",\"Identify appropriate modes of written communication based on situational parameters.\",\"Written Communication\",\"SEL: Interpersonal Communication; Written Communication; Writing; Academic Writing\",\"\",\"\",\"29-0000\",\"29-1000\",\"29-1140\",\"29-1141\",\"29-1141.00; 29-1141.01; 29-1141.02; 29-1141.03; 29-1141.04\",\"Health Open Skills\",\"Written Communication\",\"skills.emsidata.com/skills/KS4425C63RPH46FJ9BX7\"" + + // Act + // noinspection LocalVariableNamingConventionJS + const result$ = testService.getSkillCsvByUuid(uuid) + + // Assert + result$ + .subscribe((data: string) => { + expect(data).toEqual(testData) + expect(RouterData.commands).toEqual([ ]) // No errors + expect(AuthServiceData.isDown).toEqual(false) + }) + + const req = httpTestingController.expectOne(AppConfig.settings.baseApiUrl + "/" + path) + expect(req.request.method).toEqual("GET") + expect(req.request.headers.get("Accept")).toEqual("text/csv") + req.flush(testData) + }) +}) diff --git a/ui/src/app/richskill/service/rich-skill.service.ts b/ui/src/app/richskill/service/rich-skill.service.ts index c99c9e755..10996bd2e 100644 --- a/ui/src/app/richskill/service/rich-skill.service.ts +++ b/ui/src/app/richskill/service/rich-skill.service.ts @@ -65,7 +65,7 @@ export class RichSkillService extends AbstractService { const errorMsg = `Could not find skill by uuid [${uuid}]` return this.httpClient - .get(`${this.serviceUrl}/${uuid}`, { + .get(this.buildUrl(`${this.serviceUrl}/${uuid}`), { headers: this.wrapHeaders(new HttpHeaders({ Accept: "text/csv" } diff --git a/ui/src/app/search/advanced-search/action-bar/abstract-advanced-search-action-bar.component.spec.ts b/ui/src/app/search/advanced-search/action-bar/abstract-advanced-search-action-bar.component.spec.ts new file mode 100644 index 000000000..a46ffd92c --- /dev/null +++ b/ui/src/app/search/advanced-search/action-bar/abstract-advanced-search-action-bar.component.spec.ts @@ -0,0 +1,96 @@ +import { Component, Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { RouterTestingModule } from "@angular/router/testing" +import { first } from "rxjs/operators" +import { TestPage } from "../../../../../test/util/test-page.spec" +import { AbstractAdvancedSearchActionBarComponent } from "./abstract-advanced-search-action-bar.component" + + +// An example of how to test an @Output field + + +@Component({ + template: ` + + ` +}) +class ConcreteComponent extends AbstractAdvancedSearchActionBarComponent { + constructor() { + super() + } +} + + +class Page extends TestPage { + get skillButton(): HTMLButtonElement { return this.query("#skillButton") } + get collectionButton(): HTMLButtonElement { return this.query("#collectionButton") } +} + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + page = new Page(fixture) + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let component: ConcreteComponent +let fixture: ComponentFixture +let page: Page + + +describe("AbstractAdvancedSearchActionBarComponent", () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + ConcreteComponent + ], + imports: [ + RouterTestingModule + ] + }) + .compileComponents() + + createComponent(ConcreteComponent) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + it("should emit search skills clicked", () => { + // Arrange + let clicked = false + component.searchSkillsClicked.pipe(first()).subscribe( + () => { clicked = true; return } + ) + + // Act + page.skillButton.click() + + // Assert + expect(clicked).toBeTruthy() + }) + + it("should emit search skills clicked", () => { + // Arrange + let clicked = false + component.searchCollectionsClicked.pipe(first()).subscribe( + () => { clicked = true; return } + ) + + // Act + page.collectionButton.click() + + // Assert + expect(clicked).toBeTruthy() + }) +}) diff --git a/ui/src/app/search/advanced-search/advanced-search.component.spec.ts b/ui/src/app/search/advanced-search/advanced-search.component.spec.ts new file mode 100644 index 000000000..47ded4207 --- /dev/null +++ b/ui/src/app/search/advanced-search/advanced-search.component.spec.ts @@ -0,0 +1,75 @@ +import { HttpClientModule } from "@angular/common/http" +import { Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms" +import { Router } from "@angular/router" +import { EnvironmentServiceStub } from "test/resource/mock-stubs" +import { ActivatedRouteStubSpec } from "test/util/activated-route-stub.spec" +import { AppConfig } from "../../app.config" +import { initializeApp } from "../../app.module" +import { EnvironmentService } from "../../core/environment.service" +import { FormFieldText } from "../../form/form-field-text.component" +import { FormField } from "../../form/form-field.component" +import { AdvancedSearchHorizontalActionBarComponent } from "./action-bar/advanced-search-horizontal-action-bar.component" +import { AdvancedSearchVerticalActionBarComponent } from "./action-bar/advanced-search-vertical-action-bar.component" +import { AdvancedSearchComponent } from "./advanced-search.component" + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let activatedRoute: ActivatedRouteStubSpec +let component: AdvancedSearchComponent +let fixture: ComponentFixture + + +describe("AdvancedSearchComponent", () => { + beforeEach(() => { + activatedRoute = new ActivatedRouteStubSpec() + }) + + beforeEach(async(() => { + const routerSpy = ActivatedRouteStubSpec.createRouterSpy() + + TestBed.configureTestingModule({ + declarations: [ + AdvancedSearchComponent, + AdvancedSearchHorizontalActionBarComponent, + AdvancedSearchVerticalActionBarComponent, + FormField, + FormFieldText + ], + imports: [ + ReactiveFormsModule, + HttpClientModule + ], + providers: [ + AppConfig, + { provide: EnvironmentService, useClass: EnvironmentServiceStub }, // Example of using a service stub + { provide: Router, useValue: routerSpy }, + ] + }) + .compileComponents() + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() + + activatedRoute.setParamMap({ userId: 126 }) + createComponent(AdvancedSearchComponent) + })) + + it("AdvancedSearchComponent should be created", () => { + expect(component).toBeTruthy() + }) +}) diff --git a/ui/src/app/search/search.service.spec.ts b/ui/src/app/search/search.service.spec.ts new file mode 100644 index 000000000..44f6fc0b7 --- /dev/null +++ b/ui/src/app/search/search.service.spec.ts @@ -0,0 +1,131 @@ +import {async, TestBed} from "@angular/core/testing" +import { ApiAdvancedSearch, ApiSearch } from "../richskill/service/rich-skill-search.service" +import {SearchService} from "./search.service" +import {Router} from "@angular/router" +import { RouterData, RouterStub } from "../../../test/resource/mock-stubs" + + +describe("SearchService", () => { + let service: SearchService + beforeEach(async(() => { + + TestBed.configureTestingModule({ + providers: [ + SearchService, + { provide: Router, useClass: RouterStub}, + ] + }) + service = TestBed.inject(SearchService) + // .compileComponents() + + })) + + it("should be created", () => { + expect(service).toBeTruthy() + }) + + it("should perform simple skill search", () => { + // Arrange + let result: ApiSearch | undefined + const query = "testQuery" + RouterData.commands = [] + RouterData.extras = {} + + service.searchQuery$.subscribe((msg) => { + result = msg + const expected = new ApiSearch({query}) + expect(msg).toEqual(expected) + }) + + // Act + service.simpleSkillSearch(query) + + // Assert + expect(RouterData.commands).toEqual(["/skills/search"]) + expect(RouterData.extras).toEqual({queryParams: {q: query}}) + }) + + it("should perform advanced skill search", () => { + // Arrange + let result: ApiSearch | undefined + // const query = "testQuery" + const advanced = new ApiAdvancedSearch() + RouterData.commands = [] + RouterData.extras = {} + + service.searchQuery$.subscribe((msg) => { + result = msg + const expected = new ApiSearch({advanced}) + expect(msg).toEqual(expected) + }) + + // Act + service.advancedSkillSearch(advanced) + + // Assert + expect(RouterData.commands).toEqual(["/skills/search"]) + }) + + it("should perform simple collection search", () => { + // Arrange + let result: ApiSearch | undefined + const query = "testQuery" + RouterData.commands = [] + RouterData.extras = {} + + service.searchQuery$.subscribe((msg) => { + result = msg + const expected = new ApiSearch({query}) + expect(msg).toEqual(expected) + }) + + // Act + service.simpleCollectionSearch(query) + + // Assert + expect(RouterData.commands).toEqual(["/collections/search"]) + expect(RouterData.extras).toEqual({queryParams: {q: query}}) + // expect(service.simpleCollectionSearch("foo")).toHaveBeenCalledWith(["foo"], {queryParams: {extras: "foo"}}) + }) + + it("should perform advanced collection search", () => { + // Arrange + let result: ApiSearch | undefined + // const query = "testQuery" + const advanced = new ApiAdvancedSearch() + RouterData.commands = [] + RouterData.extras = {} + + service.searchQuery$.subscribe((msg) => { + result = msg + const expected = new ApiSearch({advanced}) + expect(msg).toEqual(expected) + }) + + // Act + service.advancedCollectionSearch(advanced) + + // Assert + expect(RouterData.commands).toEqual(["/collections/search"]) + }) + + it("should set/clear search", () => { + let result: ApiSearch | undefined + const query = "testQuery" + const expected = new ApiSearch({query}) + + service.searchQuery$.subscribe((msg) => { + result = msg + }) + + // Set searchQuery$ + service.simpleSkillSearch(query) + while (!result) { } // wait + expect(result).toEqual(expected) + + // Clear searchQuery$ + service.clearSearch() + while (result) { } // wait + expect(service.latestSearch).toBeFalsy() + }) +}) diff --git a/ui/src/app/table/abstract-table.component.spec.ts b/ui/src/app/table/abstract-table.component.spec.ts new file mode 100644 index 000000000..fdba9ff23 --- /dev/null +++ b/ui/src/app/table/abstract-table.component.spec.ts @@ -0,0 +1,227 @@ +import { Injectable, Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { createMockSkillSummary } from "../../../test/resource/mock-data" +import { ApiSortOrder } from "../richskill/ApiSkill" +import { ApiSkillSummary, ISkillSummary } from "../richskill/ApiSkillSummary" +import { AbstractTableComponent } from "./abstract-table.component" + + +@Injectable({ + providedIn: "root" +}) +export class ConcreteComponent extends AbstractTableComponent { + constructor() { + super() + } +} + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let component: ConcreteComponent +let fixture: ComponentFixture + + +describe("AbstractTableComponent", () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + ConcreteComponent + ], + }) + .compileComponents() + + createComponent(ConcreteComponent) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + it("getNameSort should detect sort order", () => { + [ + { input: ApiSortOrder.NameAsc, expected: true }, + { input: ApiSortOrder.NameDesc, expected: false }, + { input: ApiSortOrder.SkillAsc, expected: undefined }, + { input: ApiSortOrder.SkillDesc, expected: undefined } + ].forEach((param) => { + // Arrange + component.currentSort = param.input + + // Act/Assert + expect(component.getNameSort()).toEqual(param.expected) + }) + }) + + it("getSkillSort should detect sort order", () => { + [ + { input: ApiSortOrder.NameAsc, expected: undefined }, + { input: ApiSortOrder.NameDesc, expected: undefined }, + { input: ApiSortOrder.SkillAsc, expected: true }, + { input: ApiSortOrder.SkillDesc, expected: false } + ].forEach((param) => { + // Arrange + component.currentSort = param.input + + // Act/Assert + expect(component.getSkillSort()).toEqual(param.expected) + }) + }) + + it("sortColumn should set sort order", () => { + [ + { col: "name", ascending: true, expected: ApiSortOrder.NameAsc }, + { col: "name", ascending: false, expected: ApiSortOrder.NameDesc }, + { col: "skill", ascending: true, expected: ApiSortOrder.SkillAsc }, + { col: "skill", ascending: false, expected: ApiSortOrder.SkillDesc } + ].forEach((param) => { + // Arrange/Act + component.sortColumn(param.col, param.ascending) + + // Assert + expect(component.currentSort).toEqual(param.expected) + }) + }) + + it("mobileSortColumn should set sort order", () => { + [ + { expected: ApiSortOrder.NameAsc }, + { expected: ApiSortOrder.NameDesc }, + { expected: ApiSortOrder.SkillAsc }, + { expected: ApiSortOrder.SkillDesc } + ].forEach((param) => { + // Arrange + component.mobileSortColumn(param.expected) + + // Act/Assert + expect(component.currentSort).toEqual(param.expected) + }) + }) + + it("numberOfSelected should be correct", () => { + // Arrange + component.selectedItems.clear() + component.selectedItems.add(createMockSkillSummary()) + + // Act + const result = component.numberOfSelected() + + // Assert + expect(result).toEqual(1) + }) + + it("isSelected should detect item", () => { + // Arrange + const item = createMockSkillSummary() + component.selectedItems.clear() + component.selectedItems.add(item) + + // Act + const result = component.isSelected(item) + + // Assert + expect(result).toBeTrue() + }) + + it("getSelectAllCount should be correct", () => { + // Arrange + const item = createMockSkillSummary() + component.items = [ item ] + component.selectAllCount = undefined + + // Act + const result = component.getSelectAllCount() + + // Assert + expect(result).toEqual(1) + }) + + it("getSelectAllCount should be overridden", () => { + // Arrange + const item = createMockSkillSummary() + component.items = [ item ] + component.selectAllCount = 2 + + // Act + const result = component.getSelectAllCount() + + // Assert + expect(result).toEqual(2) + }) + + it("onRowToggle should add", () => { + // Arrange + const item = createMockSkillSummary() + component.selectedItems.clear() + spyOn(component.rowSelected, "emit") + + // Act + component.onRowToggle(item) + + // Assert + expect(component.selectedItems.size).toEqual(1) + expect(component.rowSelected.emit).toHaveBeenCalledWith([item]) + }) + it("onRowToggle should remove", () => { + // Arrange + const item = createMockSkillSummary() + component.selectedItems.clear() + component.selectedItems.add(item) + spyOn(component.rowSelected, "emit") + + // Act + component.onRowToggle(item) + + // Assert + expect(component.selectedItems.size).toEqual(0) + expect(component.rowSelected.emit).toHaveBeenCalledWith([]) + }) + + it("handleSelectAll should add items", () => { + // Arrange + const evt = { target: { checked: true }} + const item = createMockSkillSummary() + component.selectedItems.clear() + component.items = [item] + const expected: ISkillSummary[] = [item] + spyOn(component.selectAllSelected, "emit") + spyOn(component.rowSelected, "emit") + + // Act + component.handleSelectAll(evt as unknown as Event) + + // Assert + expect(component.selectedItems).toEqual(new Set(expected)) + expect(component.selectAllSelected.emit).toHaveBeenCalledWith(evt.target.checked) + expect(component.rowSelected.emit).toHaveBeenCalledWith(expected) + }) + it("handleSelectAll should remove items", () => { + // Arrange + const evt = { target: { checked: false }} + const item = createMockSkillSummary() + component.selectedItems.clear() + component.selectedItems.add(item) + const expected: ISkillSummary[] = [] + spyOn(component.selectAllSelected, "emit") + spyOn(component.rowSelected, "emit") + + // Act + component.handleSelectAll(evt as unknown as Event) + + // Assert + expect(component.selectedItems).toEqual(new Set(expected)) + expect(component.selectAllSelected.emit).toHaveBeenCalledWith(evt.target.checked) + expect(component.rowSelected.emit).toHaveBeenCalledWith(expected) + }) +}) diff --git a/ui/src/app/table/collections-library.component.spec.ts b/ui/src/app/table/collections-library.component.spec.ts new file mode 100644 index 000000000..6c8f91627 --- /dev/null +++ b/ui/src/app/table/collections-library.component.spec.ts @@ -0,0 +1,81 @@ +import { HttpClientTestingModule } from "@angular/common/http/testing" +import { Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { Title } from "@angular/platform-browser" +import { RouterTestingModule } from "@angular/router/testing" +import { createMockCollectionSummary } from "../../../test/resource/mock-data" +import { CollectionServiceStub } from "../../../test/resource/mock-stubs" +import { AppConfig } from "../app.config" +import { CollectionService } from "../collection/service/collection.service" +import { EnvironmentService } from "../core/environment.service" +import { PaginatedCollections } from "../richskill/service/rich-skill-search.service" +import { ToastService } from "../toast/toast.service" +import { CollectionsLibraryComponent } from "./collections-library.component" + + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let component: CollectionsLibraryComponent +let fixture: ComponentFixture + + +describe("CollectionsLibraryComponent", () => { + let collectionService: CollectionService + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + CollectionsLibraryComponent + ], + imports: [ + HttpClientTestingModule, + RouterTestingModule, // Required for routerLink + ], + providers: [ + EnvironmentService, + AppConfig, + Title, + ToastService, + { provide: CollectionService, useClass: CollectionServiceStub }, + ] + }) + .compileComponents() + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() // This avoids the race condition on reading the config's whitelabel.toolName + + collectionService = TestBed.inject(CollectionService) + createComponent(CollectionsLibraryComponent) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + // Other cases were already covered by other tests + it("nextLoadPage should setResults", () => { + // Arrange + component.selectedFilters = new Set([]) + component.results = undefined as unknown as PaginatedCollections + component.selectedCollections = [ createMockCollectionSummary()] + const emptyPage = new PaginatedCollections([], 0) + + // Act + component.loadNextPage() + + // Assert + expect(component.results).toEqual(emptyPage) + }) +}) diff --git a/ui/src/app/table/skills-library-table/pagination.component.spec.ts b/ui/src/app/table/skills-library-table/pagination.component.spec.ts new file mode 100644 index 000000000..3e71867f9 --- /dev/null +++ b/ui/src/app/table/skills-library-table/pagination.component.spec.ts @@ -0,0 +1,152 @@ +import {Component, Type} from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { By } from "@angular/platform-browser" +import { ActivatedRouteStubSpec } from "test/util/activated-route-stub.spec" +import {PaginationComponent} from "./pagination.component" +import {FormsModule} from "@angular/forms" +import {RouterTestingModule} from "@angular/router/testing" +import {ActivatedRoute, Router} from "@angular/router" +import {EnvironmentService} from "../../core/environment.service" + + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + currentPage = 1 + totalPages = 1 +} + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + hostComponent = fixture.componentInstance + + const debugEl = fixture.debugElement.query(By.directive(PaginationComponent)) + childComponent = debugEl.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let activatedRoute: ActivatedRouteStubSpec +let hostComponent: TestHostComponent +let childComponent: PaginationComponent +let fixture: ComponentFixture + + +describe("PaginationComponent", () => { + beforeEach(() => { + activatedRoute = new ActivatedRouteStubSpec() + }) + + beforeEach(async(() => { + const routerSpy = ActivatedRouteStubSpec.createRouterSpy() + + TestBed.configureTestingModule({ + declarations: [ + PaginationComponent, + TestHostComponent + ], + imports: [ + FormsModule, // Required for ([ngModel]) + RouterTestingModule // Required for routerLink + ], + providers: [ + EnvironmentService, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: routerSpy }, + ] + }) + .compileComponents() + createComponent(TestHostComponent) + })) + + it("should be created", () => { + expect(childComponent).toBeTruthy() + expect(hostComponent).toBeTruthy() + }) + + it("currentPage should stay updated with correct values", () => { + expect(childComponent.currentPage).toEqual(1) + + hostComponent.currentPage = 5 + fixture.detectChanges() + expect(childComponent.currentPage).toEqual(5) + }) + + it("pageNumbers should return an array with all page numbers (middle)", () => { + expect(childComponent.pageNumbers()).toEqual([1, 1]) + hostComponent.currentPage = 8 + hostComponent.totalPages = 20 + fixture.detectChanges() + expect(childComponent.pageNumbers(2, 2)).toEqual([1, NaN, 6, 7, 8, 9, 10, NaN, 20]) + }) + it("pageNumbers should return an array with all page numbers (lead)", () => { + expect(childComponent.pageNumbers()).toEqual([1, 1]) + hostComponent.currentPage = 1 + hostComponent.totalPages = 3 + fixture.detectChanges() + expect(childComponent.pageNumbers(2, 2)).toEqual([1, 2, 3]) + }) + it("pageNumbers should return an array with all page numbers (trail)", () => { + expect(childComponent.pageNumbers()).toEqual([1, 1]) + hostComponent.currentPage = 5 + hostComponent.totalPages = 5 + fixture.detectChanges() + expect(childComponent.pageNumbers(2, 2)).toEqual([1, NaN, 3, 4, 5]) + }) + + it("isEllipsis should return correctly", () => { + expect(childComponent.isEllipsis(5)).toBeFalse() + expect(childComponent.isEllipsis(Number("..."))).toBeTrue() + }) + + it("isCurrentPage should return the proper page", () => { + expect(childComponent.isCurrentPage(5)).toBeFalse() + hostComponent.currentPage = 5 + fixture.detectChanges() + expect(childComponent.isCurrentPage(5)).toBeTrue() + }) + + it("isPrevDisabled should properly return either an empty string or a null value", () => { + expect(childComponent.isPrevDisabled()).toEqual("") + hostComponent.currentPage = 2 + hostComponent.totalPages = 5 + fixture.detectChanges() + expect(childComponent.isPrevDisabled()).toEqual(null) + }) + + it("isNextDisabled should properly return either an empty string or a null value", () => { + expect(childComponent.isNextDisabled()).toEqual("") + hostComponent.currentPage = 1 + hostComponent.totalPages = 5 + fixture.detectChanges() + expect(childComponent.isNextDisabled()).toEqual(null) + }) + + it("handleClickPrev should return previous page number", () => { + hostComponent.currentPage = 5 + fixture.detectChanges() + spyOn(childComponent.pageClicked, "emit") + childComponent.handleClickPrev() + fixture.detectChanges() + expect(childComponent.pageClicked.emit).toHaveBeenCalledWith(4) + }) + + it("handleClickNext should return next page number", () => { + spyOn(childComponent.pageClicked, "emit") + childComponent.handleClickNext() + // expect(childComponent.handleClickPrev()).toBeFalse() + fixture.detectChanges() + expect(childComponent.pageClicked.emit).toHaveBeenCalledWith(2) + expect(childComponent.handleClickNext()).toBeFalse() + }) +}) diff --git a/ui/src/app/task/ApiTaskResult.spec.ts b/ui/src/app/task/ApiTaskResult.spec.ts new file mode 100644 index 000000000..78740b86e --- /dev/null +++ b/ui/src/app/task/ApiTaskResult.spec.ts @@ -0,0 +1,18 @@ +import { createMockTaskResult } from "../../../test/resource/mock-data" +import { deepEqualSkipOuterType, mismatched } from "../../../test/util/deep-equals" +import { ApiTaskResult, ITaskResult } from "./ApiTaskResult" + + +describe("ApiTaskResult", () => { + it("ApiTaskResult should be created", () => { + // Arrange + const iTaskResult: ITaskResult = createMockTaskResult() + + // Act + const apiTaskResult = new ApiTaskResult(iTaskResult) + + // Assert + expect(apiTaskResult).toBeTruthy() + expect(deepEqualSkipOuterType(apiTaskResult, iTaskResult)).toBeTruthy(mismatched(apiTaskResult, iTaskResult)) + }) +}) diff --git a/ui/src/app/toast/toast.component.spec.ts b/ui/src/app/toast/toast.component.spec.ts new file mode 100644 index 000000000..d79db6b2c --- /dev/null +++ b/ui/src/app/toast/toast.component.spec.ts @@ -0,0 +1,77 @@ +import { Type } from "@angular/core" +import { async, ComponentFixture, TestBed } from "@angular/core/testing" +import { ToastComponent } from "./toast.component" +import { ToastMessage, ToastService } from "./toast.service" + +// An example of a component-level test + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + + +let component: ToastComponent +let fixture: ComponentFixture + + +describe("ToastComponent", () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + ToastComponent + ], + providers: [ + ToastService + // { provide: ToastService, useClass: ToastServiceStub }, + ] + }) + .compileComponents() + + createComponent(ToastComponent) + })) + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + it("should set message", () => { + const service: ToastService = TestBed.get(ToastService) + const message: ToastMessage = { + isAttention: true, + message: "my first message", + title: "message one" + } + service.showToast(message.title, message.message, message.isAttention) + fixture.detectChanges() + expect(component.message).toEqual(message) + + component.dismiss() + fixture.detectChanges() + expect(component.message).toBeFalsy() + }) + + it("should be visible/invisible", () => { + const service: ToastService = TestBed.get(ToastService) + const message: ToastMessage = { + isAttention: true, + message: "my first message", + title: "message one" + } + service.showToast(message.title, message.message, message.isAttention) + fixture.detectChanges() + expect(component.isToastVisible()).toBeTruthy() + + component.dismiss() + fixture.detectChanges() + expect(component.isToastVisible()).toBeFalsy() + }) +}) diff --git a/ui/src/app/toast/toast.service.spec.ts b/ui/src/app/toast/toast.service.spec.ts new file mode 100644 index 000000000..cd51fdaef --- /dev/null +++ b/ui/src/app/toast/toast.service.spec.ts @@ -0,0 +1,87 @@ +import { TestBed } from "@angular/core/testing" +import { ToastMessage, ToastService } from "./toast.service" + + +// An example of how to test a service + + +describe("ToastService", () => { + let service: ToastService + + beforeEach(() => { + TestBed.configureTestingModule({}) + service = TestBed.inject(ToastService) + }) + + it("should be created", () => { + expect(service).toBeTruthy() + }) + + it("should set/clear message", () => { + const m = { + title: "my title", + message: "my message", + isAttention: true + } + let result: ToastMessage | undefined + service.subject.subscribe((msg) => { + result = msg + }) + + // Set message + service.showToast(m.title, m.message, m.isAttention) + while (!result) { } // wait + expect(result).toEqual(m) + + // Clear message + service.dismiss() + while (result) { } // wait + expect(result).toBeFalsy() + }) + + // TODO: Can't get this test to pass. Inexplicably, the this.dismiss() doesn't work inside of setTimeout() + xit("should auto/clear message", () => { + const m = { + title: "my title", + message: "my message", + isAttention: true + } + let result: ToastMessage | undefined + service.subject.subscribe((msg) => { + result = msg + console.log("toast.service.spec: got toast! msg=" + JSON.stringify(result)) + }) + + // Set message + service.showToast(m.title, m.message, m.isAttention, 2) + while (!result) { } // wait + expect(result).toEqual(m) + + // Auto-clear message +// service.dismiss() // <-- Without this, the test hangs + while (result) { } // wait + expect(result).toBeFalsy() + }) + + it("should set/clear blocking loader", () => { + let result: boolean | undefined + service.loaderSubject.subscribe((val) => { + result = val + }) + + // Set, being unsure of original value + service.showBlockingLoader() + while (!result) { } // wait + expect(result).toBeTruthy() + + // Clear + service.hideBlockingLoader() + while (result) { } // wait + expect(result).toBeFalsy() + + // And set again + service.showBlockingLoader() + while (!result) { } // wait + expect(result).toBeTruthy() + }) +}) diff --git a/ui/src/test.ts b/ui/src/test.ts index 50193eb0f..809e8a7c6 100644 --- a/ui/src/test.ts +++ b/ui/src/test.ts @@ -1,5 +1,6 @@ // This file is required by karma.conf.js and loads recursively all the .spec and framework files +import './app/app.module'; // Force Karma to load all source files, not just those that have a corresponding spec file import 'zone.js/dist/zone-testing'; import { getTestBed } from '@angular/core/testing'; import { diff --git a/ui/src/whitelabel/whitelabel.json b/ui/src/whitelabel/whitelabel.json new file mode 100644 index 000000000..5b8dcb004 --- /dev/null +++ b/ui/src/whitelabel/whitelabel.json @@ -0,0 +1,13 @@ +{ + "editableAuthor": false, + "defaultAuthorValue": "Western Governors University", + "toolName": "OSMT", + "toolNameLong": "Open Skills Management Tool", + "publicSkillTitle": "Rich Skill Descriptor", + "publicCollectionTitle": "Rich Skill Descriptor Collection", + "licensePrimary": "© 2021 Western Governors University - WGU.", + "licenseSecondary": "All Rights Reserved.", + "poweredBy": "Powered by the", + "poweredByUrl": "https://openskillsnetwork.org", + "poweredByLabel": "Open Skills Network" +} diff --git a/ui/test/resource/mock-data.ts b/ui/test/resource/mock-data.ts new file mode 100644 index 000000000..bdec7d505 --- /dev/null +++ b/ui/test/resource/mock-data.ts @@ -0,0 +1,267 @@ +import { ICollection, ICollectionUpdate } from "../../src/app/collection/ApiCollection" +import { IJobCode } from "../../src/app/job-codes/Jobcode" +import { PublishStatus } from "../../src/app/PublishStatus" +import { IBatchResult } from "../../src/app/richskill/ApiBatchResult" +import { + ApiNamedReference, + AuditOperationType, + IAuditLog, + INamedReference, + ISkill, + IUuidReference +} from "../../src/app/richskill/ApiSkill" +import { ICollectionSummary, ISkillSummary } from "../../src/app/richskill/ApiSkillSummary" +import { ApiReferenceListUpdate, IRichSkillUpdate, IStringListUpdate } from "../../src/app/richskill/ApiSkillUpdate" +import { PaginatedCollections, PaginatedSkills } from "../../src/app/richskill/service/rich-skill-search.service" +import { ITaskResult } from "../../src/app/task/ApiTaskResult" + +// Add mock data here. +// For more examples, see https://github.com/WGU-edu/ema-eval-ui/blob/develop/src/app/admin/pages/edit-user/edit-user.component.spec.ts + + +export function createMockBatchResult(): IBatchResult { + return { + message: "my batchResults message", + modifiedCount: 42, + totalCount: 103, + success: true + } +} + +export function createMockJobcode(): IJobCode { + return { + id: 42, + name: "my jobcode name", + code: "my jobcode", + broad: "my jobcode broad", + broadCode: "my jobcode broadCode", + detailed: "my jobcode detailed", + level: "Broad", + major: "my jobcode major", + majorCode: "my jobcode majorCode", + framework: "my jobcode framework", + minor: "my jobcode minor", + minorCode: "my jobcode minorCode", + url: "my jobcode url", + parents: undefined + } +} + +export function createMockUuidReference(uuid = "my uuidReference id", name = "my uuidReference name"): IUuidReference { + return { + uuid, + name + } +} + +export function createMockNamedReference(id = "id", name = "name"): INamedReference { + return { + id, + name + } +} + +export function createMockApiNamedReference(id?: string, name?: string): ApiNamedReference { + if (id && name) { + return new ApiNamedReference({ id, name }) + } + if (id) { + return new ApiNamedReference({ id }) + } + if (name) { + return new ApiNamedReference({ name }) + } + return new ApiNamedReference({ }) +} + +export function createMockAuditLog(operationType = AuditOperationType.Insert): IAuditLog { + return { + creationDate: "2020-06-25T14:58:46.313Z", + operationType, + user: "my user", + changedFields: [ + { + fieldName: "my field name", + old: "old value", + new: "new value" + }, + ] + } +} + +export function createMockStringListUpdate(): IStringListUpdate { + return { + add: [ "string 1", "string 2" ], + remove: [ "string 3" ] + } +} + +export function createMockApiReferenceListUpdate(): ApiReferenceListUpdate { + return { + add: [ + new ApiNamedReference(createMockNamedReference( "1", "one")), + new ApiNamedReference(createMockNamedReference( "2", "two")), + ], + remove: [ + new ApiNamedReference(createMockNamedReference( "3", "three")), + ] + } +} + +export function createMockSkillUpdate(): IRichSkillUpdate { + return { + skillName: "my skill name", + skillStatement: "my skill statement", + status: PublishStatus.Draft, + category: "my skill category", + keywords: createMockStringListUpdate(), + collections: createMockStringListUpdate(), + alignments: createMockApiReferenceListUpdate(), + certifications: createMockApiReferenceListUpdate(), + standards: createMockApiReferenceListUpdate(), + occupations: createMockStringListUpdate(), + employers: createMockApiReferenceListUpdate(), + author: new ApiNamedReference(createMockNamedReference("a", "author")) + } +} + +export function createMockSkillSummary( + id = "id1", + status = PublishStatus.Draft, + publishDate = "2020-06-25T14:58:46.313Z" +): ISkillSummary { + return { + id, + uuid: "uu" + id, + status, + archiveDate: "2020-06-25T14:58:46.313Z", + publishDate: publishDate ? publishDate : undefined, // i.e., if "" is passed in, then treat as undefined here. + skillName: "my skill summary name", + skillStatement: "my skill summary statement", + category: "my skill category", + keywords: [ "keyword 1", "keyword 2" ], + occupations: [ createMockJobcode(), createMockJobcode() ] + } +} +export function createMockPaginatedSkills(skillCount = 1, total = 10): PaginatedSkills { + if (skillCount > total) { + throw new RangeError(`'pageCount' must be <= 'total'`) + } + + const skills = [] + for (let c = 1; c <= skillCount; ++c) { + skills.push( + createMockSkillSummary( + "id" + c, + PublishStatus.Draft, + "2020-06-25T14:58:46.313Z" + ) + ) + } + + return new PaginatedSkills( + skills, + total + ) +} + +export function createMockCollectionSummary( + id = "id1", + status = PublishStatus.Draft, + publishDate = "2020-06-25T14:58:46.313Z" +): ICollectionSummary { + return { + id, + uuid: "my collection summary uuid", + name: "my collection summary name", + skillCount: 42, + status, + archiveDate: "2020-06-25T14:58:46.313Z", + publishDate: publishDate ? publishDate : undefined // i.e., if "" is passed in, then treat as undefined here. + } +} +export function createMockPaginatedCollections(collectionCount = 1, total = 10): PaginatedCollections { + if (collectionCount > total) { + throw new RangeError(`'pageCount' must be <= 'total'`) + } + + const collections = [] + for (let c = 1; c <= collectionCount; ++c) { + collections.push( + createMockCollectionSummary( + "id" + c, + PublishStatus.Draft, + "2020-06-25T14:58:46.313Z" + ) + ) + } + + return new PaginatedCollections( + collections, + total + ) +} + +export function createMockTaskResult(): ITaskResult { + return { + status: PublishStatus.Draft, + contentType: "my content type", + id: "my collection summary id", + uuid: "my collection summary uuid" + } +} + +export function createMockSkill(creationDate: Date, updateDate: Date, status: PublishStatus): ISkill { + return { + creationDate: creationDate.toISOString(), + updateDate: updateDate.toISOString(), + id: "my skill id", + uuid: "my skill uuid", + type: "some type", + skillName: "my skill name", + skillStatement: "my skill statement", + status, + category: "my skill category", + collections: [createMockUuidReference()], + keywords: ["keyword 1", "keyword 2"], + alignments: [createMockNamedReference()], + standards: [createMockNamedReference()], + certifications: [createMockNamedReference()], + occupations: [createMockJobcode()], + employers: [createMockNamedReference()], + author: createMockNamedReference() + } +} + +export function createMockCollection( + creationDate: Date | undefined, + updateDate: Date | undefined, + archiveDate: Date | undefined, + publishDate: Date | undefined, + status: PublishStatus, + skills: string[] = ["skill 1", "skill 2"] +): ICollection { + return { + creationDate, + updateDate, + archiveDate, + publishDate, + status, + id: "id1", + uuid: "uuid1", + name: "my collection name", + author: createMockNamedReference(), + skills, + creator: "creator" + } +} + +export function createMockCollectionUpdate(creationDate: Date, updateDate: Date, archiveDate: Date, publishDate: Date, + status: PublishStatus): ICollectionUpdate { + return { + status, + name: "my collection name", + author: createMockNamedReference(), + skills: createMockStringListUpdate() + } +} diff --git a/ui/test/resource/mock-stubs.ts b/ui/test/resource/mock-stubs.ts new file mode 100644 index 000000000..888de1f68 --- /dev/null +++ b/ui/test/resource/mock-stubs.ts @@ -0,0 +1,249 @@ +import { Navigation } from "@angular/router" +import { Observable, of, Subject } from "rxjs" +import { ApiCollection, ICollectionUpdate } from "../../src/app/collection/ApiCollection" +import { PublishStatus } from "../../src/app/PublishStatus" +import { ApiBatchResult } from "../../src/app/richskill/ApiBatchResult" +import { ApiSkill, ApiSortOrder } from "../../src/app/richskill/ApiSkill" +import { ApiSkillSummary } from "../../src/app/richskill/ApiSkillSummary" +import { ApiSkillUpdate } from "../../src/app/richskill/ApiSkillUpdate" +import { + ApiAdvancedSearch, + ApiSearch, + ApiSkillListUpdate, + PaginatedCollections, + PaginatedSkills +} from "../../src/app/richskill/service/rich-skill-search.service" +import { ApiTaskResult } from "../../src/app/task/ApiTaskResult" +import { + createMockBatchResult, + createMockCollection, + createMockPaginatedCollections, + createMockPaginatedSkills, + createMockSkill, + createMockSkillSummary, + createMockTaskResult +} from "./mock-data" + + +// Add service stubs here. +// For more examples, see https://github.com/WGU-edu/ema-eval-ui/blob/develop/src/app/admin/pages/edit-user/edit-user.component.spec.ts + + +export class EnvironmentServiceStub { + environment = { + env: "local" + } +} + +export let RouterData = { commands: [] as string[], extras: {} } +export class RouterStub { + // this.router.navigate(["/skills/search"], {queryParams: {q: query}}) + public navigate(commands: string[], extras: object): Promise { + RouterData.commands = commands + RouterData.extras = extras + return new Promise((resolve, reject) => { }) + } + + getCurrentNavigation(): Navigation | null { + return null + } +} + +export let AuthServiceData = { isDown: false } +export class AuthServiceStub { // TODO consider using real class + public logout(): void { + } + public setServerIsDown(isDown: boolean): void { + AuthServiceData.isDown = isDown + } + public currentAuthToken(): string | null { + return "fake-token" + } +} + +export let SearchServiceData = { + latestSearch: new ApiSearch({}) as ApiSearch | undefined, + searchQuerySource: new Subject(), + _latestQuery: "" +} +export class SearchServiceStub { + public searchQuery$ = SearchServiceData.searchQuerySource.asObservable() + + simpleSkillSearch(query: string): void { + SearchServiceData._latestQuery = query + } + advancedSkillSearch(advanced: ApiAdvancedSearch): void { + } + + simpleCollectionSearch(query: string): void { + SearchServiceData._latestQuery = query + } + advancedCollectionSearch(advanced: ApiAdvancedSearch): void { + } + + setLatestSearch(apiSearch?: ApiSearch): void { + SearchServiceData.latestSearch = apiSearch + SearchServiceData.searchQuerySource.next(SearchServiceData.latestSearch) + } + + public clearSearch(): void { + SearchServiceData.latestSearch = undefined + SearchServiceData.searchQuerySource.next(SearchServiceData.latestSearch) + } +} + +export let CollectionServiceData = { + uuid: "uuid", + apiSearch: new ApiSearch({}) as ApiSearch | undefined, +} +export class CollectionServiceStub { + public publishCollectionsWithResult( + apiSearch: ApiSearch, + newStatus: PublishStatus = PublishStatus.Published, + filterByStatuses?: Set, + pollIntervalMs: number = 1000, + ): Observable { + return of(new ApiBatchResult({ + success: true, + message: "yup!", + totalCount: 42, + modifiedCount: 7 + })) + } + + getCollectionByUUID(uuid: string): Observable { + return of(new ApiCollection(createMockCollection( + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + PublishStatus.Draft + ))) + } + + getCollections( + size: number = 50, + from: number = 0, + filterByStatuses: Set | undefined, + sort: ApiSortOrder | undefined, + ): Observable { + return of(createMockPaginatedCollections()) + } + + getCollectionSkills( + collectionUuid: string, + size?: number, + from?: number, + filterByStatuses?: Set, + sort?: ApiSortOrder, + apiSearch?: ApiSearch + ): Observable { + return of(createMockPaginatedSkills()) + } + + /** @param collectionUuid "uuid1" means not ready to publish. All others are ready */ + collectionReadyToPublish(collectionUuid: string): Observable { + return of(collectionUuid !== "uuid1") + } + + updateSkills(collectionUuid: string, + skillListUpdate: ApiSkillListUpdate, + filterByStatuses?: Set + ): Observable { + return of(new ApiTaskResult(createMockTaskResult())) + } + + updateCollection(uuid: string, updateObject: ICollectionUpdate): Observable { + return of( + new ApiCollection(createMockCollection( + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + new Date("2020-06-25T14:58:46.313Z"), + PublishStatus.Draft + // The default is to have some skills + )) + ) + } + + updateSkillsWithResult(collectionUuid: string, + skillListUpdate: ApiSkillListUpdate, + filterByStatus?: Set, + pollIntervalMs: number = 1000 + ): Observable { + CollectionServiceData.uuid = collectionUuid + CollectionServiceData.apiSearch = skillListUpdate.remove + return of(new ApiBatchResult({ + success: true, + message: "yup!", + totalCount: 42, + modifiedCount: 7 + })) + } + + searchCollections( + apiSearch: ApiSearch, + size: number | undefined, + from: number | undefined, + filterByStatuses?: Set, + sort?: ApiSortOrder, + ): Observable { + return of(createMockPaginatedCollections()) + } +} + +export let RichSkillServiceData = { +} +export class RichSkillServiceStub { + publishSkillsWithResult( + apiSearch: ApiSearch, + newStatus: PublishStatus = PublishStatus.Published, + filterByStatuses?: Set, + collectionUuid?: string, + pollIntervalMs: number = 1000, + ): Observable { + return of(createMockBatchResult()) + } + + getSkillByUUID(uuid: string): Observable { + const date = new Date("2020-06-25T14:58:46.313Z") + return of(new ApiSkill(createMockSkill(date, date, PublishStatus.Draft))) + } + + createSkill(updateObject: ApiSkillUpdate, pollIntervalMs: number = 1000): Observable { + const date = new Date("2020-06-25T14:58:46.313Z") + return of(new ApiSkill(createMockSkill(date, date, PublishStatus.Draft))) + } + + createSkills(updateObjects: ApiSkillUpdate[]): Observable { + return of(new ApiTaskResult(createMockTaskResult())) + } + + updateSkill(uuid: string, updateObject: ApiSkillUpdate): Observable { + const now = new Date() + return of(new ApiSkill(createMockSkill(now, now, PublishStatus.Draft))) + } + + similarityCheck(statement: string): Observable { + const isoDate = new Date().toISOString() + return of([ new ApiSkillSummary(createMockSkillSummary("id1", PublishStatus.Draft, isoDate)) ]) + } + similaritiesCheck(statement: string): Observable { + const isoDate = new Date().toISOString() + return of([ true ]) + } + + pollForTaskResult(obs: Observable, pollIntervalMs: number = 1000): Observable { + return new Observable() + } + + searchSkills( + apiSearch: ApiSearch, + size?: number, + from?: number, + filterByStatuses?: Set, + sort?: ApiSortOrder, + ): Observable { + return of(createMockPaginatedSkills()) + } +} diff --git a/ui/test/util/activated-route-stub.spec.ts b/ui/test/util/activated-route-stub.spec.ts new file mode 100644 index 000000000..4589daf84 --- /dev/null +++ b/ui/test/util/activated-route-stub.spec.ts @@ -0,0 +1,51 @@ +import { convertToParamMap, ParamMap, Params } from "@angular/router" +import { Observable, ReplaySubject } from "rxjs" +import SpyObj = jasmine.SpyObj + + +/** + * An ActivateRoute test double with a `paramMap` observable. + * Use the `setParamMap()` method to add the next `paramMap` value. + */ +export class ActivatedRouteStubSpec { + // Use a ReplaySubject to share previous values with subscribers + // and pump new values into the `paramMap` observable + private paramMap$ = new ReplaySubject() + private params$ = new ReplaySubject() + private queryParams$ = new ReplaySubject() + + private testParams: Params = {} + + // tslint:disable-next-line:no-any + static createRouterSpy(): any { + return jasmine.createSpyObj("Router", ["navigate"]) + } + + + constructor(initialParams?: Params) { + this.setParamMap(initialParams) + } + + /** Set the paramMap observables's next value */ + setParamMap(params?: Params): void { + if (params) { + this.params$.next(this.testParams = params) + this.paramMap$.next(convertToParamMap(params)) + } + } + + get params(): Observable { + return this.params$.asObservable() + } + + get queryParams(): Observable { + return this.queryParams$.asObservable() + } + + get snapshot(): object { + return { + params: this.testParams, + paramMap: convertToParamMap(this.testParams) + } + } +} diff --git a/ui/test/util/deep-equals.ts b/ui/test/util/deep-equals.ts new file mode 100644 index 000000000..e27b8468a --- /dev/null +++ b/ui/test/util/deep-equals.ts @@ -0,0 +1,20 @@ +import * as _ from "lodash" + +// This is a deep equality check, but it does *not* check type equality of the outermost layer. +// This allows for object equality when comparing an interface instantiation with a concrete instantiation. +// tslint:disable-next-line:no-any +export function deepEqualSkipOuterType(thiz: any, that: any): boolean { + for (const key of Object.getOwnPropertyNames(thiz)) { + const value = that[key] + + if (!_.isEqual(thiz[key], value)) { + return false + } + } + return true +} + +// tslint:disable-next-line:no-any +export function mismatched(o1: any, o2: any): string { + return `Mismatch: expected != actual (${JSON.stringify(o1)} != ${JSON.stringify(o2)})` +} diff --git a/ui/test/util/router-link-directive-stub.spec.ts b/ui/test/util/router-link-directive-stub.spec.ts new file mode 100644 index 000000000..f01cc62bf --- /dev/null +++ b/ui/test/util/router-link-directive-stub.spec.ts @@ -0,0 +1,17 @@ +/* tslint:disable:no-any */ +import { Directive, HostListener, Input } from "@angular/core" + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: "[routerLink]" +}) +// tslint:disable-next-line:directive-class-suffix +export class RouterLinkDirectiveStub { + @Input("routerLink") linkParams: any + navigatedTo: any = null + + @HostListener("click") + onClick(): void { + this.navigatedTo = this.linkParams + } +} diff --git a/ui/test/util/test-page.spec.ts b/ui/test/util/test-page.spec.ts new file mode 100644 index 000000000..5bf6b0bcc --- /dev/null +++ b/ui/test/util/test-page.spec.ts @@ -0,0 +1,44 @@ +import { ComponentFixture } from "@angular/core/testing" +import { Router } from "@angular/router" +// import * as jasmine from 'node_modules/jasmine'; + + +/** + * Reusable class for interacting with the DOM. + * + * The naming convention for element IDs is: + * componentName-elementType-elementName + * + * For example, on the EditUserComponent, there is an