[SG-487] Web component unit tests (#3189)

* Configure jest for web

* attempt to fix linter fule preventing angular core imports

* trial initiation tests

* small trial initiation component fix

* Small naming fix in tests
This commit is contained in:
Robyn MacCallum 2022-08-04 10:19:31 -04:00 committed by GitHub
parent 1e1a0b1481
commit 34f33ebb54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 332 additions and 3 deletions

View File

@ -6,7 +6,7 @@
"no-restricted-imports": [ "no-restricted-imports": [
"error", "error",
{ {
"patterns": ["**/core/*", "**/reports/*"] "patterns": ["**/app/core/*", "**/reports/*"]
} }
] ]
} }

15
apps/web/jest.config.js Normal file
View File

@ -0,0 +1,15 @@
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
module.exports = {
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",
preset: "jest-preset-angular",
setupFilesAfterEnv: ["<rootDir>/test.config.ts"],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/",
}),
modulePathIgnorePatterns: ["jslib"],
};

View File

@ -18,6 +18,9 @@
"clean:l10n": "git push origin --delete l10n_master", "clean:l10n": "git push origin --delete l10n_master",
"dist:bit:cloud": "npm run build:bit:cloud", "dist:bit:cloud": "npm run build:bit:cloud",
"dist:oss:selfhost": "npm run build:oss:selfhost:prod", "dist:oss:selfhost": "npm run build:oss:selfhost:prod",
"dist:bit:selfhost": "npm run build:bit:selfhost:prod" "dist:bit:selfhost": "npm run build:bit:selfhost:prod",
"test": "jest",
"test:watch": "jest --watch",
"test:watch:all": "jest --watchAll"
} }
} }

View File

@ -0,0 +1,285 @@
import { StepperSelectionEvent } from "@angular/cdk/stepper";
import { TitleCasePipe } from "@angular/common";
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
import { FormBuilder, UntypedFormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { RouterTestingModule } from "@angular/router/testing";
import { Substitute } from "@fluffy-spoon/substitute";
import { BehaviorSubject } from "rxjs";
import { I18nPipe } from "@bitwarden/angular/pipes/i18n.pipe";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy.service";
import { StateService as BaseStateService } from "@bitwarden/common/abstractions/state.service";
import { PlanType } from "@bitwarden/common/enums/planType";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/models/domain/masterPasswordPolicyOptions";
import { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.component";
import { TrialInitiationComponent } from "./trial-initiation.component";
describe("TrialInitiationComponent", () => {
let component: TrialInitiationComponent;
let fixture: ComponentFixture<TrialInitiationComponent>;
const mockQueryParams = new BehaviorSubject<any>({ org: "enterprise" });
const testOrgId = "91329456-5b9f-44b3-9279-6bb9ee6a0974";
const formBuilder: FormBuilder = new FormBuilder();
let routerSpy: jest.SpyInstance;
let stateServiceMock: any;
let apiServiceMock: any;
let policyServiceMock: any;
beforeEach(() => {
// only mock functions we use in this component
stateServiceMock = {
getOrganizationInvitation: jest.fn(),
};
apiServiceMock = {
getPoliciesByToken: jest.fn(),
};
policyServiceMock = {
getMasterPasswordPolicyOptions: jest.fn(),
};
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{ path: "trial", component: TrialInitiationComponent },
{
path: `organizations/${testOrgId}/vault`,
component: BlankComponent,
},
{
path: `organizations/${testOrgId}/manage/people`,
component: BlankComponent,
},
]),
],
declarations: [TrialInitiationComponent, I18nPipe],
providers: [
UntypedFormBuilder,
{
provide: ActivatedRoute,
useValue: {
queryParams: mockQueryParams.asObservable(),
},
},
{ provide: BaseStateService, useValue: stateServiceMock },
{ provide: PolicyService, useValue: policyServiceMock },
{ provide: ApiService, useValue: apiServiceMock },
{ provide: LogService, useClass: Substitute.for<LogService>() },
{ provide: I18nService, useClass: Substitute.for<I18nService>() },
{ provide: TitleCasePipe, useClass: Substitute.for<TitleCasePipe>() },
{
provide: VerticalStepperComponent,
useClass: VerticalStepperStubComponent,
},
],
schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component)
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TrialInitiationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
// These tests demonstrate mocking service calls
describe("onInit() enforcedPolicyOptions", () => {
it("should not set enforcedPolicyOptions if state service returns no invite", async () => {
stateServiceMock.getOrganizationInvitation.mockReturnValueOnce(null);
// Need to recreate component with new service mock
fixture = TestBed.createComponent(TrialInitiationComponent);
component = fixture.componentInstance;
await component.ngOnInit();
expect(component.enforcedPolicyOptions).toBe(undefined);
});
it("should set enforcedPolicyOptions if state service returns an invite", async () => {
// Set up service method mocks
stateServiceMock.getOrganizationInvitation.mockReturnValueOnce({
organizationId: testOrgId,
token: "token",
email: "testEmail",
organizationUserId: "123",
});
apiServiceMock.getPoliciesByToken.mockReturnValueOnce({
data: [
{
id: "345",
organizationId: testOrgId,
type: 1,
data: [
{
minComplexity: 4,
minLength: 10,
requireLower: null,
requireNumbers: null,
requireSpecial: null,
requireUpper: null,
},
],
enabled: true,
},
],
});
policyServiceMock.getMasterPasswordPolicyOptions.mockReturnValueOnce({
minComplexity: 4,
minLength: 10,
requireLower: null,
requireNumbers: null,
requireSpecial: null,
requireUpper: null,
} as MasterPasswordPolicyOptions);
// Need to recreate component with new service mocks
fixture = TestBed.createComponent(TrialInitiationComponent);
component = fixture.componentInstance;
await component.ngOnInit();
expect(component.enforcedPolicyOptions).toMatchObject({
minComplexity: 4,
minLength: 10,
requireLower: null,
requireNumbers: null,
requireSpecial: null,
requireUpper: null,
});
});
});
// These tests demonstrate route params
describe("Route params", () => {
it("should set org variable to be enterprise and plan to EnterpriseAnnually if org param is enterprise", fakeAsync(() => {
mockQueryParams.next({ org: "enterprise" });
tick(); // wait for resolution
fixture.detectChanges();
component.ngOnInit();
expect(component.org).toBe("enterprise");
expect(component.plan).toBe(PlanType.EnterpriseAnnually);
}));
it("should not set org variable if no org param is provided", fakeAsync(() => {
mockQueryParams.next({});
tick(); // wait for resolution
fixture = TestBed.createComponent(TrialInitiationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
expect(component.org).toBe("");
expect(component.accountCreateOnly).toBe(true);
}));
it("should set the org to be families and plan to FamiliesAnnually if org param is invalid ", fakeAsync(async () => {
mockQueryParams.next({ org: "hahahaha" });
tick(); // wait for resolution
fixture.detectChanges();
component.ngOnInit();
expect(component.org).toBe("families");
expect(component.plan).toBe(PlanType.FamiliesAnnually);
}));
});
// These tests demonstrate the use of a stub component
describe("createAccount()", () => {
beforeEach(() => {
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
.componentInstance as VerticalStepperComponent;
});
it("should set email and call verticalStepper.next()", fakeAsync(() => {
const verticalStepperNext = jest.spyOn(component.verticalStepper, "next");
component.createdAccount("test@email.com");
expect(verticalStepperNext).toHaveBeenCalled();
expect(component.email).toBe("test@email.com");
}));
});
describe("billingSuccess()", () => {
beforeEach(() => {
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
.componentInstance as VerticalStepperComponent;
});
it("should set orgId and call verticalStepper.next()", () => {
const verticalStepperNext = jest.spyOn(component.verticalStepper, "next");
component.billingSuccess({ orgId: testOrgId });
expect(verticalStepperNext).toHaveBeenCalled();
expect(component.orgId).toBe(testOrgId);
});
});
describe("stepSelectionChange()", () => {
beforeEach(() => {
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
.componentInstance as VerticalStepperComponent;
});
it("on step 2 should show organization copy text", () => {
component.stepSelectionChange({
selectedIndex: 1,
previouslySelectedIndex: 0,
} as StepperSelectionEvent);
expect(component.orgInfoSubLabel).toContain("Enter your");
expect(component.orgInfoSubLabel).toContain(" organization information");
});
it("going from step 2 to 3 should set the orgInforSubLabel to be the Org name from orgInfoFormGroup", () => {
component.orgInfoFormGroup = formBuilder.group({
name: ["Hooli"],
email: [""],
});
component.stepSelectionChange({
selectedIndex: 2,
previouslySelectedIndex: 1,
} as StepperSelectionEvent);
expect(component.orgInfoSubLabel).toContain("Hooli");
});
});
describe("previousStep()", () => {
beforeEach(() => {
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
.componentInstance as VerticalStepperComponent;
});
it("should call verticalStepper.previous()", fakeAsync(() => {
const verticalStepperPrevious = jest.spyOn(component.verticalStepper, "previous");
component.previousStep();
expect(verticalStepperPrevious).toHaveBeenCalled();
}));
});
// These tests demonstrate router navigation
describe("navigation methods", () => {
beforeEach(() => {
component.orgId = testOrgId;
const router = TestBed.inject(Router);
fixture.detectChanges();
routerSpy = jest.spyOn(router, "navigate");
});
describe("navigateToOrgVault", () => {
it("should call verticalStepper.previous()", fakeAsync(() => {
component.navigateToOrgVault();
expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "vault"]);
}));
});
describe("navigateToOrgVault", () => {
it("should call verticalStepper.previous()", fakeAsync(() => {
component.navigateToOrgInvite();
expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "manage", "people"]);
}));
});
});
});
export class VerticalStepperStubComponent extends VerticalStepperComponent {}
export class BlankComponent {} // For router tests

View File

@ -24,7 +24,7 @@ import { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.c
}) })
export class TrialInitiationComponent implements OnInit { export class TrialInitiationComponent implements OnInit {
email = ""; email = "";
org = "teams"; org = "";
orgInfoSubLabel = ""; orgInfoSubLabel = "";
orgId = ""; orgId = "";
orgLabel = ""; orgLabel = "";

23
apps/web/test.config.ts Normal file
View File

@ -0,0 +1,23 @@
import "jest-preset-angular/setup-jest";
Object.defineProperty(window, "CSS", { value: null });
Object.defineProperty(window, "getComputedStyle", {
value: () => {
return {
display: "none",
appearance: ["-webkit-appearance"],
};
},
});
Object.defineProperty(document, "doctype", {
value: "<!DOCTYPE html>",
});
Object.defineProperty(document.body.style, "transform", {
value: () => {
return {
enumerable: true,
configurable: true,
};
},
});

View File

@ -0,0 +1,3 @@
{
"extends": "./tsconfig.json"
}