mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
Billing/pm 22625/tax id should not be saved when hidden (#15905)
* fix: add logic for ngOnUpdate * fix: use markAsTouched function * tests: add component tests
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { SimpleChange } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ReactiveFormsModule } from "@angular/forms";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SelectModule, FormFieldModule, BitSubmitDirective } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { ManageTaxInformationComponent } from "./manage-tax-information.component";
|
||||
|
||||
describe("ManageTaxInformationComponent", () => {
|
||||
let sut: ManageTaxInformationComponent;
|
||||
let fixture: ComponentFixture<ManageTaxInformationComponent>;
|
||||
let mockTaxService: MockProxy<TaxServiceAbstraction>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockTaxService = mock();
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ManageTaxInformationComponent],
|
||||
providers: [
|
||||
{ provide: TaxServiceAbstraction, useValue: mockTaxService },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
SelectModule,
|
||||
FormFieldModule,
|
||||
BitSubmitDirective,
|
||||
I18nPipe,
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ManageTaxInformationComponent);
|
||||
sut = fixture.componentInstance;
|
||||
fixture.autoDetectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("creates successfully", () => {
|
||||
expect(sut).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should initialize with all values empty in startWith", async () => {
|
||||
// Arrange
|
||||
sut.startWith = {
|
||||
country: "",
|
||||
postalCode: "",
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const startWithValue = sut.startWith;
|
||||
expect(startWithValue.line1).toHaveLength(0);
|
||||
expect(startWithValue.line2).toHaveLength(0);
|
||||
expect(startWithValue.city).toHaveLength(0);
|
||||
expect(startWithValue.state).toHaveLength(0);
|
||||
expect(startWithValue.postalCode).toHaveLength(0);
|
||||
expect(startWithValue.country).toHaveLength(0);
|
||||
expect(startWithValue.taxId).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should update the tax information protected state when form is updated", async () => {
|
||||
// Arrange
|
||||
const line1Value = "123 Street";
|
||||
const line2Value = "Apt. 5";
|
||||
const cityValue = "New York";
|
||||
const stateValue = "NY";
|
||||
const countryValue = "USA";
|
||||
const postalCodeValue = "123 Street";
|
||||
|
||||
sut.startWith = {
|
||||
country: countryValue,
|
||||
postalCode: "",
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
sut.showTaxIdField = false;
|
||||
mockTaxService.isCountrySupported.mockResolvedValue(true);
|
||||
|
||||
// Act
|
||||
await sut.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const line1: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='line1']",
|
||||
);
|
||||
const line2: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='line2']",
|
||||
);
|
||||
const city: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='city']",
|
||||
);
|
||||
const state: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='state']",
|
||||
);
|
||||
const postalCode: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='postalCode']",
|
||||
);
|
||||
|
||||
line1.value = line1Value;
|
||||
line2.value = line2Value;
|
||||
city.value = cityValue;
|
||||
state.value = stateValue;
|
||||
postalCode.value = postalCodeValue;
|
||||
|
||||
line1.dispatchEvent(new Event("input"));
|
||||
line2.dispatchEvent(new Event("input"));
|
||||
city.dispatchEvent(new Event("input"));
|
||||
state.dispatchEvent(new Event("input"));
|
||||
postalCode.dispatchEvent(new Event("input"));
|
||||
await fixture.whenStable();
|
||||
|
||||
// Assert
|
||||
|
||||
// Assert that the internal tax information reflects the form
|
||||
const taxInformation = sut.getTaxInformation();
|
||||
expect(taxInformation.line1).toBe(line1Value);
|
||||
expect(taxInformation.line2).toBe(line2Value);
|
||||
expect(taxInformation.city).toBe(cityValue);
|
||||
expect(taxInformation.state).toBe(stateValue);
|
||||
expect(taxInformation.postalCode).toBe(postalCodeValue);
|
||||
expect(taxInformation.country).toBe(countryValue);
|
||||
expect(taxInformation.taxId).toHaveLength(0);
|
||||
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue);
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should not show address fields except postal code if country is not supported for taxes", async () => {
|
||||
// Arrange
|
||||
const countryValue = "UNKNOWN";
|
||||
sut.startWith = {
|
||||
country: countryValue,
|
||||
postalCode: "",
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
sut.showTaxIdField = false;
|
||||
mockTaxService.isCountrySupported.mockResolvedValue(false);
|
||||
|
||||
// Act
|
||||
await sut.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const line1: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='line1']",
|
||||
);
|
||||
const line2: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='line2']",
|
||||
);
|
||||
const city: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='city']",
|
||||
);
|
||||
const state: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='state']",
|
||||
);
|
||||
const postalCode: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='postalCode']",
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(line1).toBeNull();
|
||||
expect(line2).toBeNull();
|
||||
expect(city).toBeNull();
|
||||
expect(state).toBeNull();
|
||||
//Should be visible
|
||||
expect(postalCode).toBeTruthy();
|
||||
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue);
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should not show the tax id field if showTaxIdField is set to false", async () => {
|
||||
// Arrange
|
||||
const countryValue = "USA";
|
||||
sut.startWith = {
|
||||
country: countryValue,
|
||||
postalCode: "",
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
|
||||
sut.showTaxIdField = false;
|
||||
mockTaxService.isCountrySupported.mockResolvedValue(true);
|
||||
|
||||
// Act
|
||||
await sut.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const taxId: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='taxId']",
|
||||
);
|
||||
expect(taxId).toBeNull();
|
||||
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue);
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should clear the tax id field if showTaxIdField is set to false after being true", async () => {
|
||||
// Arrange
|
||||
const countryValue = "USA";
|
||||
const taxIdValue = "A12345678";
|
||||
|
||||
sut.startWith = {
|
||||
country: countryValue,
|
||||
postalCode: "",
|
||||
taxId: taxIdValue,
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
sut.showTaxIdField = true;
|
||||
|
||||
mockTaxService.isCountrySupported.mockResolvedValue(true);
|
||||
await sut.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
const initialTaxIdValue = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='taxId']",
|
||||
).value;
|
||||
|
||||
// Act
|
||||
sut.showTaxIdField = false;
|
||||
sut.ngOnChanges({ showTaxIdField: new SimpleChange(true, false, false) });
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const taxId = fixture.nativeElement.querySelector("input[formControlName='taxId']");
|
||||
expect(taxId).toBeNull();
|
||||
|
||||
const taxInformation = sut.getTaxInformation();
|
||||
expect(taxInformation.taxId).toBeNull();
|
||||
expect(initialTaxIdValue).toEqual(taxIdValue);
|
||||
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue);
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,15 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { debounceTime } from "rxjs/operators";
|
||||
@@ -13,7 +22,7 @@ import { CountryListItem, TaxInformation } from "@bitwarden/common/billing/model
|
||||
templateUrl: "./manage-tax-information.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class ManageTaxInformationComponent implements OnInit, OnDestroy {
|
||||
export class ManageTaxInformationComponent implements OnInit, OnDestroy, OnChanges {
|
||||
@Input() startWith: TaxInformation;
|
||||
@Input() onSubmit?: (taxInformation: TaxInformation) => Promise<void>;
|
||||
@Input() showTaxIdField: boolean = true;
|
||||
@@ -56,7 +65,7 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
this.markAllAsTouched();
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
@@ -65,7 +74,7 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
validate(): boolean {
|
||||
this.formGroup.markAllAsTouched();
|
||||
this.markAllAsTouched();
|
||||
return this.formGroup.valid;
|
||||
}
|
||||
|
||||
@@ -142,6 +151,13 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Clear the value of the tax-id if states have been changed in the parent component
|
||||
if (!changes.showTaxIdField.currentValue) {
|
||||
this.formGroup.controls.taxId.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
||||
Reference in New Issue
Block a user