1
0
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:
Stephon Brown
2025-08-08 11:33:34 -04:00
committed by GitHub
parent 7835d01456
commit a75f9dc9f0
2 changed files with 282 additions and 4 deletions

View File

@@ -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);
});
});

View File

@@ -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();