diff --git a/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.html b/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.html index 2f5f2f99d3..66fe5b1cdb 100644 --- a/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.html +++ b/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.html @@ -16,8 +16,17 @@

New member form

- - + + @@ -69,18 +78,14 @@

New member form

- + - -
- - {{ colorError.message }} - -
+ + diff --git a/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.spec.ts index a348799fb8..ba0cf2f438 100644 --- a/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.spec.ts @@ -1,6 +1,7 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyAppTestUtility } from '@skyux-sdk/testing'; import { SkyInputBoxHarness } from '@skyux/forms/testing'; import { DemoComponent } from './demo.component'; @@ -40,6 +41,22 @@ describe('Basic input box demo', () => { }); }); + describe('last name field', () => { + it('should have last name required', async () => { + const harness = await setupTest({ + dataSkyId: 'input-box-last-name', + }); + const inputEl = document.querySelector( + 'input.last-name-input-box', + ) as HTMLInputElement; + inputEl.value = ''; + SkyAppTestUtility.fireDomEvent(inputEl, 'input'); + SkyAppTestUtility.fireDomEvent(inputEl, 'blur'); + + await expectAsync(harness.hasRequiredError()).toBeResolvedTo(true); + }); + }); + describe('bio field', () => { it('should have a character limit of 250', async () => { const harness = await setupTest({ @@ -71,7 +88,7 @@ describe('Basic input box demo', () => { }); describe('favorite color field', () => { - it('should not allow bird to be selected', async () => { + it('should not allow invalid color to be selected', async () => { const harness = await setupTest({ dataSkyId: 'input-box-favorite-color', }); @@ -80,19 +97,11 @@ describe('Basic input box demo', () => { '.input-box-favorite-color-select', ) as HTMLSelectElement; - selectEl.value = 'bird'; + selectEl.value = 'invalid'; selectEl.dispatchEvent(new Event('change')); - const customErrors = await harness.getCustomErrors(); - - expect(customErrors.length).toBe(1); - - const birdError = customErrors[0]; - - await expectAsync(birdError.getDescriptionType()).toBeResolvedTo('error'); - await expectAsync(birdError.getIndicatorType()).toBeResolvedTo('danger'); - await expectAsync(birdError.getText()).toBeResolvedTo( - 'Bird is not a color.', + await expectAsync(harness.hasCustomFormError('invalid')).toBeResolvedTo( + true, ); }); }); diff --git a/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.ts index c6f2f3abc0..3529ee4acc 100644 --- a/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.ts @@ -44,13 +44,8 @@ export class DemoComponent { constructor() { this.favoriteColor = new FormControl('none', [ (control): ValidationErrors | null => { - if (control.value === 'bird') { - return { - color: { - invalid: true, - message: 'Bird is not a color.', - }, - }; + if (control.value === 'invalid') { + return { invalid: true }; } return null; diff --git a/apps/playground/src/app/components/forms/input-box/input-box.component.html b/apps/playground/src/app/components/forms/input-box/input-box.component.html index 7e113fea6c..413f6eb57d 100644 --- a/apps/playground/src/app/components/forms/input-box/input-box.component.html +++ b/apps/playground/src/app/components/forms/input-box/input-box.component.html @@ -245,6 +245,18 @@
+ + + + { + console.log(control.value); + if (control.value !== 'blue') { + return { blue: true }; + } + return null; + }, + Validators.required, + Validators.maxLength(1), + ]); + this.errorField.markAsTouched(); this.errorForm = new UntypedFormGroup({ errorFormField: new UntypedFormControl('', [Validators.required]), + customError: this.customError, }); this.errorAutofillForm = new UntypedFormGroup({ errorAutofillFormField: new UntypedFormControl('', [ diff --git a/libs/components/forms/src/index.ts b/libs/components/forms/src/index.ts index 5f047eecc3..ce2a456365 100644 --- a/libs/components/forms/src/index.ts +++ b/libs/components/forms/src/index.ts @@ -4,6 +4,7 @@ export { SkyCheckboxChange } from './lib/modules/checkbox/checkbox-change'; export { SkyCheckboxModule } from './lib/modules/checkbox/checkbox.module'; export { SkyFormErrorsModule } from './lib/modules/form-error/form-errors.module'; +export { SkyFormErrorModule } from './lib/modules/form-error/form-error.module'; export { SkyFileAttachmentsModule } from './lib/modules/file-attachment/file-attachments.module'; export { SkyFileItem } from './lib/modules/file-attachment/file-item'; @@ -29,6 +30,8 @@ export { SkySelectionBoxGridAlignItemsType } from './lib/modules/selection-box/t export { SkyToggleSwitchModule } from './lib/modules/toggle-switch/toggle-switch.module'; export { SkyToggleSwitchChange } from './lib/modules/toggle-switch/types/toggle-switch-change'; +export { SKY_FORM_ERRORS_ENABLED } from './lib/modules/form-error/form-errors-enabled-token'; + // Components and directives must be exported to support Angular's "partial" Ivy compiler. // Obscure names are used to indicate types are not part of the public API. @@ -41,6 +44,7 @@ export { SkyFileAttachmentComponent as λ7 } from './lib/modules/file-attachment export { SkyFileDropComponent as λ8 } from './lib/modules/file-attachment/file-drop.component'; export { SkyFileItemComponent as λ9 } from './lib/modules/file-attachment/file-item.component'; export { SkyFormErrorsComponent as λ21 } from './lib/modules/form-error/form-errors.component'; +export { SkyFormErrorComponent as λ22 } from './lib/modules/form-error/form-error.component'; export { SkyInputBoxControlDirective as λ20 } from './lib/modules/input-box/input-box-control.directive'; export { SkyInputBoxComponent as λ10 } from './lib/modules/input-box/input-box.component'; export { SkyRadioGroupComponent as λ11 } from './lib/modules/radio/radio-group.component'; diff --git a/libs/components/forms/src/lib/modules/form-error/form-error.component.spec.ts b/libs/components/forms/src/lib/modules/form-error/form-error.component.spec.ts new file mode 100644 index 0000000000..534140d2e8 --- /dev/null +++ b/libs/components/forms/src/lib/modules/form-error/form-error.component.spec.ts @@ -0,0 +1,46 @@ +import { Component } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { expect } from '@skyux-sdk/testing'; + +import { SkyFormErrorModule } from './form-error.module'; +import { SKY_FORM_ERRORS_ENABLED } from './form-errors-enabled-token'; +import { SkyFormErrorsModule } from './form-errors.module'; + +@Component({ + standalone: true, + imports: [SkyFormErrorsModule, SkyFormErrorModule], + providers: [{ provide: SKY_FORM_ERRORS_ENABLED, useValue: true }], + template: ` + + `, +}) +class FormErrorWithTokenComponent {} + +@Component({ + standalone: true, + imports: [SkyFormErrorsModule, SkyFormErrorModule], + template: ` + + `, +}) +class FormErrorWithoutTokenComponent {} + +describe('Form error component', () => { + it('renders an error message when form errors enabled token is provided', () => { + const fixture = TestBed.createComponent(FormErrorWithTokenComponent); + + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelector('.sky-form-error'), + ).toBeVisible(); + }); + + it('throws an error when form errors enabled token is not provided', () => { + expect(() => + TestBed.createComponent(FormErrorWithoutTokenComponent), + ).toThrowError( + 'The `sky-form-error` component is not supported in the provided context.', + ); + }); +}); diff --git a/libs/components/forms/src/lib/modules/form-error/form-error.component.ts b/libs/components/forms/src/lib/modules/form-error/form-error.component.ts index 1aae6fd575..6773084805 100644 --- a/libs/components/forms/src/lib/modules/form-error/form-error.component.ts +++ b/libs/components/forms/src/lib/modules/form-error/form-error.component.ts @@ -1,20 +1,29 @@ -import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + Input, + inject, +} from '@angular/core'; import { SkyStatusIndicatorModule } from '@skyux/indicators'; +import { SKY_FORM_ERRORS_ENABLED } from './form-errors-enabled-token'; + /** - * @internal + * Displays default and custom input error messages for SKY UX form components. */ @Component({ selector: 'sky-form-error', standalone: true, - imports: [SkyStatusIndicatorModule], + imports: [SkyStatusIndicatorModule, CommonModule], template: ` - + {{ errorText }} `, styles: [ @@ -28,6 +37,27 @@ import { SkyStatusIndicatorModule } from '@skyux/indicators'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class SkyFormErrorComponent { - @HostBinding('class') - protected readonly cssClass = 'sky-form-error-indicator'; + /** + * The name of the error. + */ + @Input({ required: true }) + public errorName!: string; + + /** + * The error message to display. + */ + @Input({ required: true }) + public errorText!: string; + + protected readonly formErrors = inject(SKY_FORM_ERRORS_ENABLED, { + optional: true, + }); + + constructor() { + if (!this.formErrors) { + throw new Error( + 'The `sky-form-error` component is not supported in the provided context.', + ); + } + } } diff --git a/libs/components/forms/src/lib/modules/form-error/form-error.module.ts b/libs/components/forms/src/lib/modules/form-error/form-error.module.ts new file mode 100644 index 0000000000..0224e3456f --- /dev/null +++ b/libs/components/forms/src/lib/modules/form-error/form-error.module.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core'; + +import { SkyFormErrorComponent } from './form-error.component'; + +@NgModule({ + imports: [SkyFormErrorComponent], + exports: [SkyFormErrorComponent], +}) +export class SkyFormErrorModule {} diff --git a/libs/components/forms/src/lib/modules/form-error/form-errors-enabled-token.ts b/libs/components/forms/src/lib/modules/form-error/form-errors-enabled-token.ts new file mode 100644 index 0000000000..33ab025185 --- /dev/null +++ b/libs/components/forms/src/lib/modules/form-error/form-errors-enabled-token.ts @@ -0,0 +1,8 @@ +import { InjectionToken } from '@angular/core'; + +/** + * @internal + */ +export const SKY_FORM_ERRORS_ENABLED = new InjectionToken( + 'SKY_FORM_ERRORS_ENABLED', +); diff --git a/libs/components/forms/src/lib/modules/form-error/form-errors.component.html b/libs/components/forms/src/lib/modules/form-error/form-errors.component.html index 43003248ee..365cb38250 100644 --- a/libs/components/forms/src/lib/modules/form-error/form-errors.component.html +++ b/libs/components/forms/src/lib/modules/form-error/form-errors.component.html @@ -1,56 +1,56 @@ - - {{ 'skyux_form_error_required' | skyLibResources : labelText }} - + - {{ + errorName="maxlength" + [errorText]=" 'skyux_form_error_maxlength' | skyLibResources : labelText : maxLengthError.requiredLength - }} - + " + /> - {{ + errorName="minlength" + [errorText]=" 'skyux_form_error_minlength' | skyLibResources : labelText : minLengthError.requiredLength - }} - + " + /> - {{ - 'skyux_form_error_character_count' - | skyLibResources : labelText : characterCounterError.limit - }} - - - - {{ 'skyux_form_error_date' | skyLibResources }} - + *ngIf="errors['skyDate']" + errorName="date" + [errorText]="'skyux_form_error_date' | skyLibResources" + /> - - {{ 'skyux_form_error_email' | skyLibResources }} - + - - {{ 'skyux_form_error_phone' | skyLibResources }} - + - - {{ 'skyux_form_error_time' | skyLibResources }} - + - - {{ 'skyux_form_error_url' | skyLibResources }} - + diff --git a/libs/components/forms/src/lib/modules/form-error/form-errors.component.scss b/libs/components/forms/src/lib/modules/form-error/form-errors.component.scss index de61745539..5f35216baf 100644 --- a/libs/components/forms/src/lib/modules/form-error/form-errors.component.scss +++ b/libs/components/forms/src/lib/modules/form-error/form-errors.component.scss @@ -1,4 +1,5 @@ :host, sky-status-indicator { display: block; + line-height: normal; } diff --git a/libs/components/forms/src/lib/modules/form-error/form-errors.module.ts b/libs/components/forms/src/lib/modules/form-error/form-errors.module.ts index 1f4ec9af83..8dcdd053c6 100644 --- a/libs/components/forms/src/lib/modules/form-error/form-errors.module.ts +++ b/libs/components/forms/src/lib/modules/form-error/form-errors.module.ts @@ -1,12 +1,13 @@ import { NgModule } from '@angular/core'; +import { SkyFormErrorComponent } from './form-error.component'; import { SkyFormErrorsComponent } from './form-errors.component'; /** * @internal */ @NgModule({ - imports: [SkyFormErrorsComponent], + imports: [SkyFormErrorsComponent, SkyFormErrorComponent], exports: [SkyFormErrorsComponent], }) export class SkyFormErrorsModule {} diff --git a/libs/components/forms/src/lib/modules/input-box/input-box.component.html b/libs/components/forms/src/lib/modules/input-box/input-box.component.html index 477d94e91c..8f0f00991e 100644 --- a/libs/components/forms/src/lib/modules/input-box/input-box.component.html +++ b/libs/components/forms/src/lib/modules/input-box/input-box.component.html @@ -175,7 +175,7 @@ [labelText]="labelText" [showErrors]="controlDir?.touched || controlDir?.dirty" > - + diff --git a/libs/components/forms/src/lib/modules/input-box/input-box.component.ts b/libs/components/forms/src/lib/modules/input-box/input-box.component.ts index ac67a3b24d..b0752d4391 100644 --- a/libs/components/forms/src/lib/modules/input-box/input-box.component.ts +++ b/libs/components/forms/src/lib/modules/input-box/input-box.component.ts @@ -31,6 +31,8 @@ import { SkyContentInfoProvider, SkyIdService } from '@skyux/core'; import { ReplaySubject } from 'rxjs'; +import { SKY_FORM_ERRORS_ENABLED } from '../form-error/form-errors-enabled-token'; + import { SkyInputBoxAdapterService } from './input-box-adapter.service'; import { SkyInputBoxControlDirective } from './input-box-control.directive'; import { SkyInputBoxHostService } from './input-box-host.service'; @@ -47,6 +49,10 @@ import { SkyInputBoxPopulateArgs } from './input-box-populate-args'; SkyContentInfoProvider, SkyInputBoxAdapterService, SkyInputBoxHostService, + { + provide: SKY_FORM_ERRORS_ENABLED, + useValue: true, + }, ], // Note that change detection is not set to OnPush; default change detection allows the // invalid CSS class to be added when the content control's invalid/dirty state changes. diff --git a/libs/components/forms/src/lib/modules/input-box/input-box.module.ts b/libs/components/forms/src/lib/modules/input-box/input-box.module.ts index a07ab8ddca..60735b390d 100644 --- a/libs/components/forms/src/lib/modules/input-box/input-box.module.ts +++ b/libs/components/forms/src/lib/modules/input-box/input-box.module.ts @@ -3,7 +3,8 @@ import { NgModule } from '@angular/core'; import { SkyThemeModule } from '@skyux/theme'; import { SkyCharacterCounterModule } from '../character-counter/character-counter.module'; -import { SkyFormErrorsComponent } from '../form-error/form-errors.component'; +import { SkyFormErrorModule } from '../form-error/form-error.module'; +import { SkyFormErrorsModule } from '../form-error/form-errors.module'; import { SkyInputBoxControlDirective } from './input-box-control.directive'; import { SkyInputBoxHelpInlineComponent } from './input-box-help-inline.component'; @@ -14,11 +15,16 @@ import { SkyInputBoxComponent } from './input-box.component'; imports: [ CommonModule, SkyCharacterCounterModule, - SkyFormErrorsComponent, + SkyFormErrorsModule, + SkyFormErrorModule, SkyInputBoxControlDirective, SkyInputBoxHelpInlineComponent, SkyThemeModule, ], - exports: [SkyInputBoxComponent, SkyInputBoxControlDirective], + exports: [ + SkyInputBoxComponent, + SkyInputBoxControlDirective, + SkyFormErrorModule, + ], }) export class SkyInputBoxModule {} diff --git a/libs/components/forms/testing/src/form-error/form-error-harness.ts b/libs/components/forms/testing/src/form-error/form-error-harness.ts index cc6c5ada58..bcbd4d1a8a 100644 --- a/libs/components/forms/testing/src/form-error/form-error-harness.ts +++ b/libs/components/forms/testing/src/form-error/form-error-harness.ts @@ -19,15 +19,10 @@ export class SkyFormErrorHarness extends SkyComponentHarness { return SkyFormErrorHarness.getDataSkyIdPredicate(filters); } - async #getFormErrorClasses(): Promise { - const formErrorClasses = await (await this.host()).getProperty('classList'); - return Array.from(formErrorClasses); - } - - /* - * Gets the error class that signifies which error has fired. + /** + * Gets the error name. */ - public async getFirstClassError(): Promise { - return (await this.#getFormErrorClasses())[0]; + public async getErrorName(): Promise { + return (await this.host()).getAttribute('errorName'); } } diff --git a/libs/components/forms/testing/src/form-error/form-errors-harness.spec.ts b/libs/components/forms/testing/src/form-error/form-errors-harness.spec.ts index e7d9e626c6..48353abe3f 100644 --- a/libs/components/forms/testing/src/form-error/form-errors-harness.spec.ts +++ b/libs/components/forms/testing/src/form-error/form-errors-harness.spec.ts @@ -4,13 +4,23 @@ import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ValidationErrors } from '@angular/forms'; // eslint-disable-next-line @nx/enforce-module-boundaries -import { SkyFormErrorsModule } from '@skyux/forms'; +import { + SKY_FORM_ERRORS_ENABLED, + SkyFormErrorModule, + SkyFormErrorsModule, +} from '@skyux/forms'; import { SkyFormErrorsHarness } from './form-errors-harness'; //#region Test component @Component({ selector: 'sky-form-errors-test', + providers: [ + { + provide: SKY_FORM_ERRORS_ENABLED, + useValue: true, + }, + ], template: ` { }> { await TestBed.configureTestingModule({ declarations: [TestComponent], - imports: [SkyFormErrorsModule], + imports: [SkyFormErrorsModule, SkyFormErrorModule], }).compileComponents(); const fixture = TestBed.createComponent(TestComponent); @@ -53,60 +63,28 @@ describe('Form errors harness', () => { dataSkyId: 'other-error', }); + fixture.detectChanges(); fixture.detectChanges(); - await expectAsync(formErrorsHarness.getNumberOfErrors()).toBeResolvedTo(1); - await expectAsync(formErrorsHarness.hasRequiredError()).toBeResolvedTo( + await expectAsync(formErrorsHarness.hasError('required')).toBeResolvedTo( true, ); + await expectAsync(formErrorsHarness.getFormErrors()).toBeResolvedTo([ + { errorName: 'required' }, + ]); }); - it('should get number of errors', async () => { - const { formErrorsHarness, fixture } = await setupTest(); - - fixture.detectChanges(); - - await expectAsync(formErrorsHarness.getNumberOfErrors()).toBeResolvedTo(0); - - fixture.componentInstance.errors = { - required: true, - minlength: true, - }; - fixture.detectChanges(); - - await expectAsync(formErrorsHarness.getNumberOfErrors()).toBeResolvedTo(2); - }); - - it('should return whether first class errors are fired', async () => { + it('should return an array of current errors', async () => { const { formErrorsHarness, fixture } = await setupTest(); fixture.detectChanges(); - await expectAsync(formErrorsHarness.hasRequiredError()).toBeResolvedTo( - false, - ); - await expectAsync(formErrorsHarness.hasMaxLengthError()).toBeResolvedTo( - false, - ); - await expectAsync(formErrorsHarness.hasMinLengthError()).toBeResolvedTo( - false, - ); - await expectAsync( - formErrorsHarness.hasCharacterCountError(), - ).toBeResolvedTo(false); - await expectAsync(formErrorsHarness.hasDateError()).toBeResolvedTo(false); - await expectAsync(formErrorsHarness.hasEmailError()).toBeResolvedTo(false); - await expectAsync(formErrorsHarness.hasPhoneFieldError()).toBeResolvedTo( - false, - ); - await expectAsync(formErrorsHarness.hasTimeError()).toBeResolvedTo(false); - await expectAsync(formErrorsHarness.hasUrlError()).toBeResolvedTo(false); + await expectAsync(formErrorsHarness.getFormErrors()).toBeResolvedTo([]); fixture.componentInstance.errors = { required: true, minlength: true, maxlength: true, - skyCharacterCounter: true, skyDate: true, skyEmail: true, skyPhoneField: true, @@ -115,24 +93,15 @@ describe('Form errors harness', () => { }; fixture.detectChanges(); - await expectAsync(formErrorsHarness.hasRequiredError()).toBeResolvedTo( - true, - ); - await expectAsync(formErrorsHarness.hasMaxLengthError()).toBeResolvedTo( - true, - ); - await expectAsync(formErrorsHarness.hasMinLengthError()).toBeResolvedTo( - true, - ); - await expectAsync( - formErrorsHarness.hasCharacterCountError(), - ).toBeResolvedTo(true); - await expectAsync(formErrorsHarness.hasDateError()).toBeResolvedTo(true); - await expectAsync(formErrorsHarness.hasEmailError()).toBeResolvedTo(true); - await expectAsync(formErrorsHarness.hasPhoneFieldError()).toBeResolvedTo( - true, - ); - await expectAsync(formErrorsHarness.hasTimeError()).toBeResolvedTo(true); - await expectAsync(formErrorsHarness.hasUrlError()).toBeResolvedTo(true); + await expectAsync(formErrorsHarness.getFormErrors()).toBeResolvedTo([ + { errorName: 'required' }, + { errorName: 'maxlength' }, + { errorName: 'minlength' }, + { errorName: 'date' }, + { errorName: 'email' }, + { errorName: 'phone' }, + { errorName: 'time' }, + { errorName: 'url' }, + ]); }); }); diff --git a/libs/components/forms/testing/src/form-error/form-errors-harness.ts b/libs/components/forms/testing/src/form-error/form-errors-harness.ts index 462e5d3575..6411541317 100644 --- a/libs/components/forms/testing/src/form-error/form-errors-harness.ts +++ b/libs/components/forms/testing/src/form-error/form-errors-harness.ts @@ -10,20 +10,6 @@ export class SkyFormErrorsHarness extends SkyComponentHarness { */ public static hostSelector = 'sky-form-errors'; - #getFormError = this.locatorForAll('sky-form-error'); - - async #getFormErrorsClasses(): Promise { - const formErrorHarnesses = await this.locatorForAll( - SkyFormErrorHarness.with({}), - )(); - - return Promise.all( - formErrorHarnesses.map((formError) => { - return formError.getFirstClassError(); - }), - ); - } - /** * Gets a `HarnessPredicate` that can be used to search for a * `SkyFormErrorsHarness` that meets certain criteria @@ -34,85 +20,28 @@ export class SkyFormErrorsHarness extends SkyComponentHarness { return SkyFormErrorsHarness.getDataSkyIdPredicate(filters); } - /* - * Gets total number of errors. - */ - public async getNumberOfErrors(): Promise { - return (await this.#getFormError()).length; - } - - /* - * Gets if the required error has triggered. - */ - public async hasRequiredError(): Promise { - return (await this.#getFormErrorsClasses()).includes( - 'sky-form-error-required', - ); - } - - /* - * Gets if the maximum length error has triggered. - */ - public async hasMaxLengthError(): Promise { - return (await this.#getFormErrorsClasses()).includes( - 'sky-form-error-maxlength', - ); - } - - /* - * Gets if the minimum length has triggered. - */ - public async hasMinLengthError(): Promise { - return (await this.#getFormErrorsClasses()).includes( - 'sky-form-error-minlength', - ); - } - - /* - * Gets if the character count error has triggered. - */ - public async hasCharacterCountError(): Promise { - return (await this.#getFormErrorsClasses()).includes( - 'sky-form-error-character-counter', - ); - } - - /* - * Gets if the date error has triggered. - */ - public async hasDateError(): Promise { - return (await this.#getFormErrorsClasses()).includes('sky-form-error-date'); - } - - /* - * Gets if the email error has triggered. + /** + * Gets a list of all errors fired. */ - public async hasEmailError(): Promise { - return (await this.#getFormErrorsClasses()).includes( - 'sky-form-error-email', - ); - } + public async getFormErrors(): Promise<{ errorName: string | null }[]> { + const formErrorHarnesses = await this.locatorForAll( + SkyFormErrorHarness.with({}), + )(); - /* - * Gets if the phone field error has triggered. - */ - public async hasPhoneFieldError(): Promise { - return (await this.#getFormErrorsClasses()).includes( - 'sky-form-error-phone', + return Promise.all( + formErrorHarnesses.map(async (formError) => { + return { errorName: await formError.getErrorName() }; + }), ); } - /* - * Gets if the time field error has triggered. - */ - public async hasTimeError(): Promise { - return (await this.#getFormErrorsClasses()).includes('sky-form-error-time'); - } - - /* - * Gets if the URL error has triggered. + /** + * Whether an error with the given name has fired. */ - public async hasUrlError(): Promise { - return (await this.#getFormErrorsClasses()).includes('sky-form-error-url'); + public async hasError(errorName: string): Promise { + const formErrors = await this.getFormErrors(); + return formErrors.some((error) => { + return error.errorName === errorName; + }); } } diff --git a/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.component.html b/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.component.html index 9419510535..a1fff97fec 100644 --- a/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.component.html +++ b/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.component.html @@ -40,4 +40,53 @@
+
+ + + + + + + + + + + + + + + + + + + + + + +
+ Help content from template diff --git a/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.component.ts b/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.component.ts index baa02a6c51..3f9b9beaa7 100644 --- a/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.component.ts +++ b/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.component.ts @@ -11,6 +11,7 @@ import { }) export class InputBoxHarnessTestComponent { public myForm: UntypedFormGroup; + public directiveErrorForm: UntypedFormGroup; @ViewChild('helpContentTemplate', { read: TemplateRef, @@ -32,5 +33,10 @@ export class InputBoxHarnessTestComponent { firstName: new UntypedFormControl('John'), lastName: new UntypedFormControl('Doe'), }); + this.directiveErrorForm = formBuilder.group({ + easyModeDatepicker: new UntypedFormControl('123'), + easyModeTimepicker: new UntypedFormControl('abc'), + easyModePhoneField: new UntypedFormControl('abc'), + }); } } diff --git a/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.module.ts b/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.module.ts index 26b4933f3d..08ca311bf2 100644 --- a/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.module.ts +++ b/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.module.ts @@ -1,8 +1,10 @@ import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { SkyIdModule } from '@skyux/core'; +import { SkyDatepickerModule, SkyTimepickerModule } from '@skyux/datetime'; import { SkyInputBoxModule } from '@skyux/forms'; import { SkyStatusIndicatorModule } from '@skyux/indicators'; +import { SkyPhoneFieldModule } from '@skyux/phone-field'; import { InputBoxHarnessTestComponent } from './input-box-harness-test.component'; @@ -13,6 +15,9 @@ import { InputBoxHarnessTestComponent } from './input-box-harness-test.component SkyIdModule, SkyInputBoxModule, SkyStatusIndicatorModule, + SkyDatepickerModule, + SkyTimepickerModule, + SkyPhoneFieldModule, ], declarations: [InputBoxHarnessTestComponent], }) diff --git a/libs/components/forms/testing/src/input-box/input-box-harness.spec.ts b/libs/components/forms/testing/src/input-box/input-box-harness.spec.ts index b349563143..8539749f63 100644 --- a/libs/components/forms/testing/src/input-box/input-box-harness.spec.ts +++ b/libs/components/forms/testing/src/input-box/input-box-harness.spec.ts @@ -2,6 +2,7 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Validators } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyValidators } from '@skyux/validation'; import { InputBoxHarnessTestComponent } from './fixtures/input-box-harness-test.component'; import { InputBoxHarnessTestModule } from './fixtures/input-box-harness-test.module'; @@ -138,6 +139,127 @@ describe('Input box harness', () => { await expectAsync(customError.getText()).toBeResolvedTo('Test error'); }); + it('should return whether custom form error has fired', async () => { + const { inputBoxHarness } = await setupTest({ + dataSkyId: 'custom-error-easy-mode', + }); + + await expectAsync( + inputBoxHarness.hasCustomFormError('custom'), + ).toBeResolvedTo(true); + }); + + it('should return whether required error has fired', async () => { + const { component, fixture, inputBoxHarness } = await setupTest({ + dataSkyId: 'my-input-box-last-name-easy-mode', + }); + + const control = component.myForm.controls['lastName']; + control.addValidators(Validators.required); + control.setValue(''); + control.markAsDirty(); + + fixture.detectChanges(); + + await expectAsync(inputBoxHarness.hasRequiredError()).toBeResolvedTo(true); + }); + + it('should return whether minimum length error has fired', async () => { + const { component, fixture, inputBoxHarness } = await setupTest({ + dataSkyId: 'my-input-box-last-name-easy-mode', + }); + + const control = component.myForm.controls['lastName']; + control.addValidators(Validators.minLength(2)); + control.setValue('a'); + control.markAsDirty(); + + fixture.detectChanges(); + + await expectAsync(inputBoxHarness.hasMinLengthError()).toBeResolvedTo(true); + }); + + it('should return whether maximum length error has fired', async () => { + const { component, fixture, inputBoxHarness } = await setupTest({ + dataSkyId: 'my-input-box-last-name-easy-mode', + }); + + const control = component.myForm.controls['lastName']; + control.addValidators(Validators.maxLength(1)); + control.setValue('abc'); + control.markAsDirty(); + + fixture.detectChanges(); + + await expectAsync(inputBoxHarness.hasMaxLengthError()).toBeResolvedTo(true); + }); + + it('should return whether email validator error has fired', async () => { + const { component, fixture, inputBoxHarness } = await setupTest({ + dataSkyId: 'my-input-box-last-name-easy-mode', + }); + + const control = component.myForm.controls['lastName']; + control.addValidators(SkyValidators.email); + control.setValue('abc'); + control.markAsDirty(); + + fixture.detectChanges(); + + await expectAsync(inputBoxHarness.hasEmailError()).toBeResolvedTo(true); + }); + + it('should return whether url validator error has fired', async () => { + const { component, fixture, inputBoxHarness } = await setupTest({ + dataSkyId: 'my-input-box-last-name-easy-mode', + }); + + const control = component.myForm.controls['lastName']; + control.addValidators(SkyValidators.url); + control.setValue('abc'); + control.markAsDirty(); + + fixture.detectChanges(); + + await expectAsync(inputBoxHarness.hasUrlError()).toBeResolvedTo(true); + }); + + it('should return whether date picker validator error has fired', async () => { + const { fixture, inputBoxHarness } = await setupTest({ + dataSkyId: 'datepicker-easy-mode', + }); + + fixture.detectChanges(); + + await expectAsync(inputBoxHarness.hasDateError()).toBeResolvedTo(true); + }); + + it('should return whether time picker validator error has fired', async () => { + const { component, fixture, inputBoxHarness } = await setupTest({ + dataSkyId: 'timepicker-easy-mode', + }); + + const control = component.directiveErrorForm.controls['easyModeTimepicker']; + control.markAsDirty(); + fixture.detectChanges(); + + await expectAsync(inputBoxHarness.hasTimeError()).toBeResolvedTo(true); + }); + + it('should return whether phone field validator error has fired', async () => { + const { component, fixture, inputBoxHarness } = await setupTest({ + dataSkyId: 'phone-field-easy-mode', + }); + + const control = component.directiveErrorForm.controls['easyModePhoneField']; + control.markAsDirty(); + fixture.detectChanges(); + + await expectAsync(inputBoxHarness.hasPhoneFieldError()).toBeResolvedTo( + true, + ); + }); + it('should return character counter indicator', async () => { const { component, fixture, inputBoxHarness } = await setupTest({ dataSkyId: DATA_SKY_ID_EASY_MODE, diff --git a/libs/components/forms/testing/src/input-box/input-box-harness.ts b/libs/components/forms/testing/src/input-box/input-box-harness.ts index 01aefb6fcc..9288756e5f 100644 --- a/libs/components/forms/testing/src/input-box/input-box-harness.ts +++ b/libs/components/forms/testing/src/input-box/input-box-harness.ts @@ -9,6 +9,7 @@ import { SkyStatusIndicatorHarness } from '@skyux/indicators/testing'; import { SkyPopoverHarness } from '@skyux/popovers/testing'; import { SkyCharacterCounterIndicatorHarness } from '../character-counter/character-counter-indicator-harness'; +import { SkyFormErrorsHarness } from '../form-error/form-errors-harness'; import { SkyInputBoxHarnessFilters } from './input-box-harness-filters'; @@ -25,6 +26,10 @@ export class SkyInputBoxHarness extends SkyComponentHarness { #getLabel = this.locatorForOptional('.sky-control-label'); #getWrapper = this.locatorFor('.sky-input-box'); + async #getFormError(): Promise { + return this.locatorFor(SkyFormErrorsHarness)(); + } + /** * Gets a `HarnessPredicate` that can be used to search for a * `SkyInputBoxHarness` that meets certain criteria. @@ -79,6 +84,69 @@ export class SkyInputBoxHarness extends SkyComponentHarness { return errors; } + /** + * Whether the custom error is triggered. + */ + public async hasCustomFormError(errorName: string): Promise { + return (await this.#getFormError()).hasError(errorName); + } + + /** + * Whether the required field is empty. + */ + public async hasRequiredError(): Promise { + return (await this.#getFormError()).hasError('required'); + } + + /** + * Whether the field has more characters than allowed. + */ + public async hasMaxLengthError(): Promise { + return (await this.#getFormError()).hasError('maxlength'); + } + + /** + * Whether the field has fewer characters than allowed. + */ + public async hasMinLengthError(): Promise { + return (await this.#getFormError()).hasError('minlength'); + } + + /* + * Whether the field is set to an invalid email address. + */ + public async hasEmailError(): Promise { + return (await this.#getFormError()).hasError('email'); + } + + /* + * Whether the field is set to an invalid URL. + */ + public async hasUrlError(): Promise { + return (await this.#getFormError()).hasError('url'); + } + + /* + * Whether the field is set to an invalid date. + */ + public async hasDateError(): Promise { + return (await this.#getFormError()).hasError('date'); + } + + /* + * Whether the field is set to an invalid phone number. + */ + public async hasPhoneFieldError(): Promise { + return (await this.#getFormError()).hasError('phone'); + } + + /* + * Whether the field is set to an invalid time. + */ + public async hasTimeError(): Promise { + return (await this.#getFormError()).hasError('time'); + } + /** * Indicates whether the input box has disabled styles applied. */