diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/data-entry-grid-docs-demo.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/data-entry-grid-docs-demo.component.ts index 189b3a32e3..a4a62e43a0 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/data-entry-grid-docs-demo.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/data-entry-grid-docs-demo.component.ts @@ -118,6 +118,7 @@ export class SkyDataEntryGridDemoComponent { columnDefs: this.columnDefs, onGridReady: (gridReadyEvent) => this.onGridReady(gridReadyEvent), }; + this.gridOptions = this.agGridService.getGridOptions({ gridOptions: this.gridOptions, }); diff --git a/apps/code-examples/src/app/code-examples/modals/confirm/confirm-demo.component.html b/apps/code-examples/src/app/code-examples/modals/confirm/confirm-demo.component.html index 818a31bce0..961f046b2c 100644 --- a/apps/code-examples/src/app/code-examples/modals/confirm/confirm-demo.component.html +++ b/apps/code-examples/src/app/code-examples/modals/confirm/confirm-demo.component.html @@ -1,6 +1,6 @@ -

+

You selected the "{{ selectedText }}" button, which has an action of "{{ selectedAction }}."

-

+

You selected the "{{ selectedAction }}" action.

diff --git a/apps/code-examples/src/app/code-examples/modals/confirm/confirm-demo.component.spec.ts b/apps/code-examples/src/app/code-examples/modals/confirm/confirm-demo.component.spec.ts new file mode 100644 index 0000000000..3c96056f66 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/modals/confirm/confirm-demo.component.spec.ts @@ -0,0 +1,62 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { SkyConfirmHarness } from '@skyux/modals/testing'; + +import { ConfirmDemoComponent } from './confirm-demo.component'; +import { ConfirmDemoModule } from './confirm-demo.module'; + +describe('Confirm demo', () => { + async function setupTest(confirmBtnClass: string) { + const fixture = TestBed.createComponent(ConfirmDemoComponent); + const openBtn = fixture.nativeElement.querySelector(confirmBtnClass); + + openBtn.click(); + + const rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture); + + const confirmHarness = await rootLoader.getHarness(SkyConfirmHarness); + return { confirmHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ConfirmDemoModule, RouterTestingModule], + }); + }); + + it('should show the correct text when OK is clicked on an OK confirm', async () => { + const { confirmHarness, fixture } = await setupTest('.ok-confirm-btn'); + + await confirmHarness.clickOkButton(); + + const displayedTextEl: HTMLElement = + fixture.nativeElement.querySelector('.displayed-text'); + + expect(displayedTextEl.innerText).toEqual('You selected the "ok" action.'); + await expectAsync(confirmHarness.getMessageText()).toBeResolvedTo( + 'Cannot delete invoice because it has vendor, credit memo, or purchase order activity.' + ); + }); + + it('should show the correct text when "Finalize" is clicked on a custom confirm', async () => { + const { confirmHarness, fixture } = await setupTest( + '.two-action-confirm-btn' + ); + + await confirmHarness.clickCustomButton({ text: 'Finalize' }); + + const displayedTextEl: HTMLElement = + fixture.nativeElement.querySelector('.displayed-text'); + + expect(displayedTextEl.innerText).toEqual( + 'You selected the "Finalize" button, which has an action of "save."' + ); + await expectAsync(confirmHarness.getMessageText()).toBeResolvedTo( + 'Finalize report cards?' + ); + await expectAsync(confirmHarness.getBodyText()).toBeResolvedTo( + 'Grades cannot be changed once the report cards are finalized.' + ); + }); +}); diff --git a/libs/components/modals/package.json b/libs/components/modals/package.json index dcd56b2e50..f9c620f782 100644 --- a/libs/components/modals/package.json +++ b/libs/components/modals/package.json @@ -16,6 +16,7 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { + "@angular/cdk": "^13.3.2", "@angular/common": "^13.3.2", "@angular/core": "^13.3.2", "@angular/router": "^13.3.2", diff --git a/libs/components/modals/src/index.ts b/libs/components/modals/src/index.ts index 702ebf073b..87d4a6fce9 100644 --- a/libs/components/modals/src/index.ts +++ b/libs/components/modals/src/index.ts @@ -2,6 +2,7 @@ export * from './lib/modules/confirm/confirm-button'; export * from './lib/modules/confirm/confirm-button-action'; export * from './lib/modules/confirm/confirm-button-config'; +export * from './lib/modules/confirm/confirm-button-style-type'; export * from './lib/modules/confirm/confirm-closed-event-args'; export * from './lib/modules/confirm/confirm-config'; export * from './lib/modules/confirm/confirm-instance'; diff --git a/libs/components/modals/src/lib/modules/confirm/confirm-button-style-type.ts b/libs/components/modals/src/lib/modules/confirm/confirm-button-style-type.ts new file mode 100644 index 0000000000..2d3bdf8580 --- /dev/null +++ b/libs/components/modals/src/lib/modules/confirm/confirm-button-style-type.ts @@ -0,0 +1,5 @@ +export type SkyConfirmButtonStyleType = + | 'primary' + | 'default' + | 'link' + | 'danger'; diff --git a/libs/components/modals/src/lib/modules/confirm/confirm-button.ts b/libs/components/modals/src/lib/modules/confirm/confirm-button.ts index 310082ed6d..a4907374a1 100644 --- a/libs/components/modals/src/lib/modules/confirm/confirm-button.ts +++ b/libs/components/modals/src/lib/modules/confirm/confirm-button.ts @@ -1,4 +1,5 @@ import { SkyConfirmButtonAction } from './confirm-button-action'; +import { SkyConfirmButtonStyleType } from './confirm-button-style-type'; /** * The view model for button configuration that the confirm component uses. @@ -6,7 +7,8 @@ import { SkyConfirmButtonAction } from './confirm-button-action'; */ export interface SkyConfirmButton { action: SkyConfirmButtonAction; - styleType: string; + // TODO: Remove 'string' in a breaking change. + styleType: SkyConfirmButtonStyleType | string; text: string; autofocus?: boolean; } diff --git a/libs/components/modals/src/lib/modules/confirm/confirm.component.html b/libs/components/modals/src/lib/modules/confirm/confirm.component.html index 7b52e12735..38e61ee420 100644 --- a/libs/components/modals/src/lib/modules/confirm/confirm.component.html +++ b/libs/components/modals/src/lib/modules/confirm/confirm.component.html @@ -1,4 +1,4 @@ -
+
{ + /** + * Only find instances whose content matches the given value. + */ + text?: string | RegExp; + + /** + * Only find instances whose style matches the given value. + */ + styleType?: string; +} diff --git a/libs/components/modals/testing/src/confirm/confirm-button-harness.ts b/libs/components/modals/testing/src/confirm/confirm-button-harness.ts new file mode 100644 index 0000000000..740747fd00 --- /dev/null +++ b/libs/components/modals/testing/src/confirm/confirm-button-harness.ts @@ -0,0 +1,58 @@ +import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing'; +import { SkyConfirmButtonStyleType } from '@skyux/modals'; + +import { SkyConfirmButtonHarnessFilters } from './confirm-button-harness-filters'; + +/** + * Harness for interacting with a confirm component in tests. + * @internal + */ +export class SkyConfirmButtonHarness extends ComponentHarness { + public static hostSelector = '.sky-confirm-buttons .sky-btn'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a + * `SkyConfirmButtonHarness` that meets certain criteria. + */ + public static with( + filters: SkyConfirmButtonHarnessFilters + ): HarnessPredicate { + return new HarnessPredicate(SkyConfirmButtonHarness, filters) + .addOption('text', filters.text, async (harness, text) => + HarnessPredicate.stringMatches(await harness.getText(), text) + ) + .addOption('styleType', filters.styleType, async (harness, styleType) => + HarnessPredicate.stringMatches(await harness.getStyleType(), styleType) + ); + } + + /** + * Clicks the confirm button. + */ + public async click(): Promise { + return (await this.host()).click(); + } + + /** + * Gets the button style of the confirm button. + */ + public async getStyleType(): Promise { + const hostEl = await this.host(); + + if (await hostEl.hasClass('sky-btn-primary')) { + return 'primary'; + } else if (await hostEl.hasClass('sky-btn-link')) { + return 'link'; + } else if (await hostEl.hasClass('sky-btn-danger')) { + return 'danger'; + } + return 'default'; + } + + /** + * Gets the text content of the confirm button. + */ + public async getText(): Promise { + return (await this.host()).text(); + } +} diff --git a/libs/components/modals/testing/src/confirm/confirm-harness.spec.ts b/libs/components/modals/testing/src/confirm/confirm-harness.spec.ts new file mode 100644 index 0000000000..96093f6cdc --- /dev/null +++ b/libs/components/modals/testing/src/confirm/confirm-harness.spec.ts @@ -0,0 +1,339 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { Component } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { expect } from '@skyux-sdk/testing'; +import { + SkyConfirmCloseEventArgs, + SkyConfirmConfig, + SkyConfirmModule, + SkyConfirmService, + SkyConfirmType, +} from '@skyux/modals'; + +import { SkyConfirmHarness } from './confirm-harness'; + +const DEFAULT_CONFIRM_CONFIG = { + message: 'Confirm header', + type: SkyConfirmType.OK, +}; + +//#region Test component +@Component({ + selector: 'sky-confirm-test', + template: ` + + `, +}) +class TestComponent { + constructor(confirmService: SkyConfirmService) { + this.#confirmSvc = confirmService; + } + #confirmSvc: SkyConfirmService; + + public config: SkyConfirmConfig = DEFAULT_CONFIRM_CONFIG; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public closedChange(args: SkyConfirmCloseEventArgs) { + // Only exists for the spy. + } + + public openConfirm(): void { + const dialog = this.#confirmSvc.open(this.config); + + dialog.closed.subscribe((result) => { + this.closedChange(result); + }); + } +} +//#endregion Test component + +describe('Confirm harness', () => { + async function setupTest(config?: SkyConfirmConfig) { + TestBed.configureTestingModule({ + declarations: [TestComponent], + imports: [RouterTestingModule, SkyConfirmModule], + }); + + const fixture = TestBed.createComponent(TestComponent); + + if (config) { + fixture.componentInstance.config = config; + fixture.detectChanges(); + } + + const openBtn = fixture.nativeElement.querySelector('.open-btn'); + openBtn.click(); + fixture.detectChanges(); + + const rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const confirmHarness = await rootLoader.getHarness(SkyConfirmHarness); + + return { confirmHarness, fixture }; + } + + describe('getMessage', () => { + it('should return the message in the confirm header', async () => { + const message = 'Confirm message test'; + const { confirmHarness } = await setupTest({ + message, + type: SkyConfirmType.OK, + }); + + const messageText = await confirmHarness.getMessageText(); + expect(messageText).toEqual(message); + }); + }); + + describe('getBody', () => { + it('should return undefined when no body was set in the config', async () => { + const { confirmHarness } = await setupTest(); + const bodyText = await confirmHarness.getBodyText(); + + expect(bodyText).toBeUndefined(); + }); + + it('should return the body text set in the config', async () => { + const body = 'body text'; + const { confirmHarness } = await setupTest({ + ...DEFAULT_CONFIRM_CONFIG, + body, + }); + const bodyText = await confirmHarness.getBodyText(); + + expect(bodyText).toEqual(body); + }); + }); + + describe('getType', () => { + it('should return `OK` when the confirm type is set to `OK`', async () => { + const { confirmHarness } = await setupTest(); + const type = await confirmHarness.getType(); + + expect(type).toEqual(SkyConfirmType.OK); + }); + + it('should return `Custom` when the confirm type is custom', async () => { + const { confirmHarness } = await setupTest({ + ...DEFAULT_CONFIRM_CONFIG, + type: SkyConfirmType.Custom, + buttons: [{ text: 'Proceed', action: 'proceed' }], + }); + const type = await confirmHarness.getType(); + + expect(type).toEqual(SkyConfirmType.Custom); + }); + }); + + describe('getWhiteSpaceIsPreserved', () => { + it('should return false when whitespace is not preserved', async () => { + const { confirmHarness } = await setupTest(); + + const isPreserved = await confirmHarness.isWhiteSpacePreserved(); + + expect(isPreserved).toBeFalse(); + }); + it('should return true when whitespace is preserved', async () => { + const { confirmHarness } = await setupTest({ + ...DEFAULT_CONFIRM_CONFIG, + preserveWhiteSpace: true, + }); + + const isPreserved = await confirmHarness.isWhiteSpacePreserved(); + + expect(isPreserved).toBeTrue(); + }); + }); + + describe('getCustomButtons', () => { + it('should query and filter child button harnesses', async () => { + const { confirmHarness } = await setupTest({ + ...DEFAULT_CONFIRM_CONFIG, + type: SkyConfirmType.Custom, + buttons: [ + { + text: 'Proceed', + action: 'proceed', + styleType: 'primary', + }, + { + text: 'Cancel', + action: 'cancel', + styleType: 'link', + }, + ], + }); + + const results = await confirmHarness.getCustomButtons({ + text: 'Proceed', + styleType: 'primary', + }); + + expect(results.length).toEqual(1); + await expectAsync(results[0].getText()).toBeResolvedTo('Proceed'); + await expectAsync(results[0].getStyleType()).toBeResolvedTo('primary'); + }); + + it('should return harnesses for all matching buttons', async () => { + const { confirmHarness } = await setupTest({ + ...DEFAULT_CONFIRM_CONFIG, + type: SkyConfirmType.Custom, + buttons: [ + { + text: 'Delete', + action: 'delete', + styleType: 'danger', + }, + { + text: 'Learn more', + action: 'learn', + styleType: 'default', + }, + { + text: 'Cancel', + action: 'cancel', + styleType: 'link', + }, + ], + }); + + const results = await confirmHarness.getCustomButtons(); + + expect(results.length).toEqual(3); + await expectAsync(results[0].getText()).toBeResolvedTo('Delete'); + await expectAsync(results[0].getStyleType()).toBeResolvedTo('danger'); + await expectAsync(results[1].getText()).toBeResolvedTo('Learn more'); + await expectAsync(results[1].getStyleType()).toBeResolvedTo('default'); + await expectAsync(results[2].getText()).toBeResolvedTo('Cancel'); + await expectAsync(results[2].getStyleType()).toBeResolvedTo('link'); + }); + + it('should throw an error when no child button harnesses are found', async () => { + const { confirmHarness } = await setupTest({ + ...DEFAULT_CONFIRM_CONFIG, + type: SkyConfirmType.Custom, + buttons: [ + { + text: 'Proceed', + action: 'proceed', + styleType: 'default', + }, + ], + }); + + await expectAsync( + confirmHarness.getCustomButtons({ text: /invalidbuttonname/ }) + ).toBeRejectedWithError( + `Could not find buttons matching filter(s): {"text":"/invalidbuttonname/"}.` + ); + }); + + it('should throw an error when called on confirm of type OK', async () => { + const { confirmHarness } = await setupTest(); + + await expectAsync( + confirmHarness.getCustomButtons({}) + ).toBeRejectedWithError( + 'Cannot get custom buttons for confirm of type OK.' + ); + }); + }); + + describe('clickOkButton', () => { + it('should throw an error when called on a custom confirm', async () => { + const { confirmHarness } = await setupTest({ + ...DEFAULT_CONFIRM_CONFIG, + type: SkyConfirmType.Custom, + buttons: [ + { + text: 'Proceed', + action: 'proceed', + styleType: 'default', + }, + ], + }); + + await expectAsync(confirmHarness.clickOkButton()).toBeRejectedWithError( + 'Cannot click OK button on a confirm of type custom.' + ); + }); + + it('should ouput a closed event when OK button is clicked', async () => { + const { confirmHarness, fixture } = await setupTest(); + const closedSpy = spyOn(fixture.componentInstance, 'closedChange'); + fixture.detectChanges(); + + await confirmHarness.clickOkButton(); + + expect(closedSpy).toHaveBeenCalledWith({ + action: 'ok', + }); + }); + }); + + describe('clickCustomButton', () => { + it('should throw an error when called on an OK confirm', async () => { + const { confirmHarness } = await setupTest(); + + await expectAsync( + confirmHarness.clickCustomButton({}) + ).toBeRejectedWithError( + 'Cannot get custom buttons for confirm of type OK.' + ); + }); + + it('should throw an error if more than one button matches the filters', async () => { + const { confirmHarness } = await setupTest({ + ...DEFAULT_CONFIRM_CONFIG, + type: SkyConfirmType.Custom, + buttons: [ + { + text: 'Proceed', + action: 'proceed', + styleType: 'default', + }, + { + text: 'Cancel', + action: 'cancel', + styleType: 'link', + }, + ], + }); + + await expectAsync( + confirmHarness.clickCustomButton({ text: /c/ }) + ).toBeRejectedWithError( + 'More than one button matches the filter(s): {"text":"/c/"}.' + ); + }); + + it('should ouput a closed event when a custom button is clicked', async () => { + const { confirmHarness, fixture } = await setupTest({ + ...DEFAULT_CONFIRM_CONFIG, + type: SkyConfirmType.Custom, + buttons: [ + { + text: 'Proceed', + action: 'proceed', + styleType: 'default', + }, + ], + }); + const closedSpy = spyOn(fixture.componentInstance, 'closedChange'); + fixture.detectChanges(); + + await confirmHarness.clickCustomButton({ text: 'Proceed' }); + + expect(closedSpy).toHaveBeenCalledWith({ + action: 'proceed', + }); + }); + }); +}); diff --git a/libs/components/modals/testing/src/confirm/confirm-harness.ts b/libs/components/modals/testing/src/confirm/confirm-harness.ts new file mode 100644 index 0000000000..354d701ab8 --- /dev/null +++ b/libs/components/modals/testing/src/confirm/confirm-harness.ts @@ -0,0 +1,126 @@ +import { ComponentHarness, HarnessQuery } from '@angular/cdk/testing'; +import { SkyComponentHarness } from '@skyux/core/testing'; +import { SkyConfirmType } from '@skyux/modals'; + +import { SkyConfirmButtonHarness } from './confirm-button-harness'; +import { SkyConfirmButtonHarnessFilters } from './confirm-button-harness-filters'; + +/** + * Harness for interacting with a confirm component in tests. + */ +export class SkyConfirmHarness extends SkyComponentHarness { + public static hostSelector = 'sky-confirm'; + + #getBodyEl = this.locatorForOptional('.sky-confirm-body'); + #getButtons = this.locatorForAll(SkyConfirmButtonHarness); + #getConfirmEl = this.locatorFor('.sky-confirm'); + #getMessageEl = this.locatorFor('.sky-confirm-message'); + + /** + * Clicks a confirm button. + */ + public async clickCustomButton( + filters: SkyConfirmButtonHarnessFilters + ): Promise { + const buttons = await this.getCustomButtons(filters); + + if (buttons.length > 1) { + if (filters.text instanceof RegExp) { + filters.text = filters.text.toString(); + } + throw new Error( + `More than one button matches the filter(s): ${JSON.stringify( + filters + )}.` + ); + } + await buttons[0].click(); + } + + /** + * Clicks a confirm button. + */ + public async clickOkButton(): Promise { + const type = await this.getType(); + + if (type === SkyConfirmType.Custom) { + throw new Error('Cannot click OK button on a confirm of type custom.'); + } + const buttons = await this.#getButtons(); + await buttons[0].click(); + } + + /** + * Gets the body of the confirm component. + */ + public async getBodyText(): Promise { + return (await this.#getBodyEl())?.text(); + } + + /** + * Gets the confirm component's custom buttons. + */ + public async getCustomButtons( + filters?: SkyConfirmButtonHarnessFilters + ): Promise { + const confirmType = await this.getType(); + + if (confirmType === SkyConfirmType.OK) { + throw new Error('Cannot get custom buttons for confirm of type OK.'); + } + + const harnesses = await this.#queryHarnesses( + SkyConfirmButtonHarness.with(filters || {}) + ); + + if (filters && harnesses.length === 0) { + // Stringify the regular expression so that it's readable in the console log. + if (filters.text instanceof RegExp) { + filters.text = filters.text.toString(); + } + + throw new Error( + `Could not find buttons matching filter(s): ${JSON.stringify(filters)}.` + ); + } + + return harnesses; + } + + /** + * Gets the message of the confirm component. + */ + public async getMessageText(): Promise { + return (await this.#getMessageEl()).text(); + } + + /** + * Gets the type of the confirm component. + */ + public async getType(): Promise { + const confirmEl = await this.#getConfirmEl(); + if (await confirmEl.hasClass('sky-confirm-type-ok')) { + return SkyConfirmType.OK; + } + + return SkyConfirmType.Custom; + } + + /** + * Indicates if the whitespace is preserved on the confirom component. + */ + public async isWhiteSpacePreserved(): Promise { + return (await this.#getMessageEl()).hasClass( + 'sky-confirm-preserve-white-space' + ); + } + + /** + * Returns child harnesses. + */ + async #queryHarnesses( + harness: HarnessQuery + ): Promise { + return this.locatorForAll(harness)(); + } +} diff --git a/libs/components/modals/testing/src/public-api.ts b/libs/components/modals/testing/src/public-api.ts index 124c17ae28..33d8395622 100644 --- a/libs/components/modals/testing/src/public-api.ts +++ b/libs/components/modals/testing/src/public-api.ts @@ -1 +1,4 @@ export * from './modal-fixture'; +export * from './confirm/confirm-harness'; +export * from './confirm/confirm-button-harness'; +export * from './confirm/confirm-button-harness-filters';