Skip to content

Commit

Permalink
feat(components/phone-field): add phone field test harness (#2849)
Browse files Browse the repository at this point in the history
  • Loading branch information
Blackbaud-CoreyArcher authored Oct 31, 2024
1 parent 1bfa483 commit bb7e4a6
Show file tree
Hide file tree
Showing 9 changed files with 466 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<div class="sky-padding-even-lg">
<form class="phone-field-demo" [formGroup]="phoneForm">
<sky-input-box
data-sky-id="my-phone-field"
helpPopoverTitle="How we use your phone number"
hintText="Enter the phone number where you can be reached before 5 p.m., Monday through Friday."
labelText="Phone number"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { SkyInputBoxHarness } from '@skyux/forms/testing';
import { SkyPhoneFieldCountry } from '@skyux/phone-field';
import { SkyPhoneFieldHarness } from '@skyux/phone-field/testing';

import { DemoComponent } from './demo.component';

const COUNTRY_AU: SkyPhoneFieldCountry = {
name: 'Australia',
iso2: 'au',
dialCode: '+61',
};

const DATA_SKY_ID = 'my-phone-field';
const VALID_AU_NUMBER = '0212345678';
const VALID_US_NUMBER = '8675555309';

describe('Basic phone field demo', () => {
async function setupTest(options: { dataSkyId: string }): Promise<{
harness: SkyPhoneFieldHarness;
fixture: ComponentFixture<DemoComponent>;
}> {
const fixture = TestBed.createComponent(DemoComponent);
const loader = TestbedHarnessEnvironment.loader(fixture);

const harness = await (
await loader.getHarness(
SkyInputBoxHarness.with({ dataSkyId: options.dataSkyId }),
)
).queryHarness(SkyPhoneFieldHarness);

fixture.detectChanges();
await fixture.whenStable();

return { harness, fixture };
}

beforeEach(() => {
TestBed.configureTestingModule({
imports: [DemoComponent, NoopAnimationsModule],
});
});

it('should set up phone field input and clear value', async () => {
const { harness } = await setupTest({
dataSkyId: DATA_SKY_ID,
});

// First, set a value on the phoneField.
const inputHarness = await harness.getControl();
await inputHarness.focus();
await inputHarness.setValue(VALID_US_NUMBER);

await expectAsync(inputHarness.getValue()).toBeResolvedTo(VALID_US_NUMBER);

// Now, clear the value.
await inputHarness.clear();
await expectAsync(inputHarness.getValue()).toBeResolvedTo('');
});

it('should use selected country', async () => {
const { harness, fixture } = await setupTest({
dataSkyId: DATA_SKY_ID,
});

const inputHarness = await harness.getControl();
await inputHarness.focus();
// enter a valid phone number for the default country
await inputHarness.setValue(VALID_US_NUMBER);

// expect the model to use the proper dial code and format
await expectAsync(inputHarness.getValue()).toBeResolvedTo(VALID_US_NUMBER);
expect(fixture.componentInstance.phoneControl.value).toEqual(
'(867) 555-5309',
);

if (COUNTRY_AU.name) {
// change the country
await harness.selectCountry(COUNTRY_AU.name);
}

const countryName: string | null = await harness.getSelectedCountryName();

const countryIos2: string | null = await harness.getSelectedCountryIso2();

fixture.detectChanges();
await fixture.whenStable();

if (COUNTRY_AU.name && countryName) {
expect(countryName).toBe(COUNTRY_AU.name);
}
if (COUNTRY_AU.iso2 && countryIos2) {
expect(countryIos2).toBe(COUNTRY_AU.iso2);
}

// enter a valid phone number for the new country
await inputHarness.setValue(VALID_AU_NUMBER);

// expect the model to use the proper dial code and format
await expectAsync(inputHarness.getValue()).toBeResolvedTo(VALID_AU_NUMBER);
expect(fixture.componentInstance.phoneControl.value).toEqual(
'+61 2 1234 5678',
);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, inject } from '@angular/core';
import {
FormBuilder,
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
Expand All @@ -21,15 +22,17 @@ import { SkyPhoneFieldModule } from '@skyux/phone-field';
],
})
export class DemoComponent {
protected phoneForm: FormGroup;
public phoneForm: FormGroup;
public phoneControl: FormControl;

#formBuilder = inject(FormBuilder);

constructor() {
this.phoneControl = this.#formBuilder.control(undefined, {
validators: Validators.required,
});
this.phoneForm = this.#formBuilder.group({
phoneControl: this.#formBuilder.control(undefined, {
validators: Validators.required,
}),
phoneControl: this.phoneControl,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Component, inject } from '@angular/core';
import {
FormBuilder,
FormGroup,
FormsModule,
ReactiveFormsModule,
UntypedFormControl,
} from '@angular/forms';
import { SkyStatusIndicatorModule } from '@skyux/indicators';
import {
SkyPhoneFieldCountry,
SkyPhoneFieldModule,
SkyPhoneFieldNumberReturnFormat,
} from '@skyux/phone-field';

@Component({
standalone: true,
selector: 'sky-phone-field-test',
imports: [
FormsModule,
ReactiveFormsModule,
SkyPhoneFieldModule,
SkyStatusIndicatorModule,
],
template: `
<form class="phone-field-demo" [formGroup]="phoneForm">
<sky-phone-field
data-sky-id="test-phone-field"
[allowExtensions]="allowExtensions"
[defaultCountry]="defaultCountry"
[returnFormat]="returnFormat"
[supportedCountryISOs]="supportedCountryISOs"
[(selectedCountry)]="selectedCountry"
(selectedCountryChange)="selectedCountryChange($event)"
>
<input
formControlName="phoneControl"
skyPhoneFieldInput
[attr.disabled]="disabled"
[skyPhoneFieldNoValidate]="noValidate"
/>
</sky-phone-field>
@if (!phoneControl?.valid) {
<sky-status-indicator descriptionType="none" indicatorType="danger">
Enter a phone number matching the format for the selected country.
</sky-status-indicator>
}
</form>
`,
})
export class PhoneFieldHarnessTestComponent {
public allowExtensions = true;
public defaultCountry: string | undefined;
public disabled: boolean | undefined;
public noValidate = false;
public returnFormat: SkyPhoneFieldNumberReturnFormat | undefined;
public selectedCountry: SkyPhoneFieldCountry | undefined;
public showInvalidDirective = false;
public supportedCountryISOs: string[] | undefined;

public phoneControl: UntypedFormControl | undefined;
public phoneForm: FormGroup;

public selectedCountryChange = jasmine.createSpy();

#formBuilder = inject(FormBuilder);

constructor() {
this.phoneControl = new UntypedFormControl();
this.phoneForm = this.#formBuilder.group({
phoneControl: this.phoneControl,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SkyHarnessFilters } from '@skyux/core/testing';

/**
* A set of criteria that can be used to filter a list of SkyPhoneFieldHarness instances.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type
export interface SkyPhoneFieldHarnessFilters extends SkyHarnessFilters {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { SkyPhoneFieldCountry } from '@skyux/phone-field';

import { PhoneFieldHarnessTestComponent } from './fixtures/phone-field-harness-test.component';
import { SkyPhoneFieldHarness } from './phone-field-harness';

const COUNTRY_AU: SkyPhoneFieldCountry = {
name: 'Australia',
iso2: 'au',
dialCode: '+61',
};
const COUNTRY_US: SkyPhoneFieldCountry = {
name: 'United States',
iso2: 'us',
dialCode: '+1',
};
const DATA_SKY_ID = 'test-phone-field';
const VALID_AU_NUMBER = '0212345678';
const VALID_US_NUMBER = '8675555309';

describe('Phone field harness', () => {
async function setupTest(options: { dataSkyId?: string } = {}): Promise<{
phoneFieldHarness: SkyPhoneFieldHarness;
fixture: ComponentFixture<PhoneFieldHarnessTestComponent>;
loader: HarnessLoader;
}> {
await TestBed.configureTestingModule({
imports: [PhoneFieldHarnessTestComponent, NoopAnimationsModule],
}).compileComponents();

const fixture = TestBed.createComponent(PhoneFieldHarnessTestComponent);
const loader = TestbedHarnessEnvironment.loader(fixture);

const phoneFieldHarness: SkyPhoneFieldHarness = options.dataSkyId
? await loader.getHarness(
SkyPhoneFieldHarness.with({ dataSkyId: options.dataSkyId }),
)
: await loader.getHarness(SkyPhoneFieldHarness);

return { phoneFieldHarness, fixture, loader };
}

it('should use selected country', async () => {
const { phoneFieldHarness, fixture } = await setupTest({
dataSkyId: DATA_SKY_ID,
});

await (await phoneFieldHarness.getControl()).focus();
// enter a valid phone number for the default country
await (await phoneFieldHarness.getControl()).setValue(VALID_US_NUMBER);

// expect the model to use the proper dial code and format
await expectAsync(
(await phoneFieldHarness.getControl()).getValue(),
).toBeResolvedTo(VALID_US_NUMBER);
expect(fixture.componentInstance.phoneControl?.value).toEqual(
'(867) 555-5309',
);
});

it('should use newly selected country', async () => {
const { phoneFieldHarness, fixture } = await setupTest({
dataSkyId: DATA_SKY_ID,
});
fixture.componentInstance.selectedCountryChange.calls.reset();

if (COUNTRY_AU.name) {
// change the country
await phoneFieldHarness.selectCountry(COUNTRY_AU.name);
}

const countryName: string | null =
await phoneFieldHarness.getSelectedCountryName();

const countryIos2: string | null =
await phoneFieldHarness.getSelectedCountryIso2();

fixture.detectChanges();
await fixture.whenStable();

if (COUNTRY_AU?.name && countryName) {
expect(countryName).toBe(COUNTRY_AU.name);
}
if (COUNTRY_AU?.iso2 && countryIos2) {
expect(countryIos2).toBe(COUNTRY_AU.iso2);
}
expect(
fixture.componentInstance.selectedCountryChange,
).toHaveBeenCalledWith(jasmine.objectContaining(COUNTRY_AU));

// enter a valid phone number for the new country
await (await phoneFieldHarness.getControl()).setValue(VALID_AU_NUMBER);

// expect the model to use the proper dial code and format
await expectAsync(
(await phoneFieldHarness.getControl()).getValue(),
).toBeResolvedTo(VALID_AU_NUMBER);
expect(fixture.componentInstance.phoneControl?.value).toEqual(
'+61 2 1234 5678',
);
});

it('should return expected country search results', async () => {
const { phoneFieldHarness, fixture } = await setupTest({
dataSkyId: DATA_SKY_ID,
});
fixture.detectChanges();
await fixture.whenStable();
fixture.componentInstance.selectedCountryChange.calls.reset();

if (COUNTRY_AU.name) {
// search for a country by name
const results = await phoneFieldHarness.searchCountry(COUNTRY_AU.name);

fixture.detectChanges();
await fixture.whenStable();

// ensure no country selection has taken place yet
expect(
fixture.componentInstance.selectedCountryChange,
).toHaveBeenCalledTimes(0);

// verify the country search results match the country
// the dial code that exists as part of the result label is missing the leading '+'
expect(results.length).toBe(1);
expect(results[0]).toEqual(
COUNTRY_AU.name + ' ' + COUNTRY_AU.dialCode?.substring(1),
);
}
});

it('should not have constructor race condition', async () => {
// There is some concern that the delayed instantiation of the country field in the fixture's
// constructor will cause a race condition. We attempt to access the country field as early as
// possible here to try and trigger any race condition.

const { phoneFieldHarness, fixture } = await setupTest({
dataSkyId: DATA_SKY_ID,
});

if (COUNTRY_US.name) {
await phoneFieldHarness.selectCountry(COUNTRY_US.name);
}

fixture.detectChanges();
await fixture.whenStable();

expect(fixture.componentInstance.selectedCountry?.name).toBe(
COUNTRY_US.name,
);
});
});
Loading

0 comments on commit bb7e4a6

Please sign in to comment.