From dcd9dac6d97ff0a8860f0e53e6e218119843d881 Mon Sep 17 00:00:00 2001 From: Corey Archer Date: Thu, 27 Feb 2025 10:28:40 -0500 Subject: [PATCH 1/2] feat(components/lists): add paging test harnesses --- .../paging/with-content/demo-data.service.ts | 2 +- .../paging/with-content/demo.component.html | 1 + .../with-content/demo.component.spec.ts | 78 +++++++++++ .../paging-harness-test.component.html | 33 +++++ .../fixtures/paging-harness-test.component.ts | 126 ++++++++++++++++++ .../modules/paging/page-control-harness.ts | 51 +++++++ .../modules/paging/paging-content-harness.ts | 11 ++ .../modules/paging/paging-harness-filters.ts | 8 ++ .../src/modules/paging/paging-harness.spec.ts | 126 ++++++++++++++++++ .../src/modules/paging/paging-harness.ts | 106 +++++++++++++++ .../lists/testing/src/public-api.ts | 5 + 11 files changed, 546 insertions(+), 1 deletion(-) create mode 100644 apps/code-examples/src/app/code-examples/lists/paging/with-content/demo.component.spec.ts create mode 100644 libs/components/lists/testing/src/modules/paging/fixtures/paging-harness-test.component.html create mode 100644 libs/components/lists/testing/src/modules/paging/fixtures/paging-harness-test.component.ts create mode 100644 libs/components/lists/testing/src/modules/paging/page-control-harness.ts create mode 100644 libs/components/lists/testing/src/modules/paging/paging-content-harness.ts create mode 100644 libs/components/lists/testing/src/modules/paging/paging-harness-filters.ts create mode 100644 libs/components/lists/testing/src/modules/paging/paging-harness.spec.ts create mode 100644 libs/components/lists/testing/src/modules/paging/paging-harness.ts diff --git a/apps/code-examples/src/app/code-examples/lists/paging/with-content/demo-data.service.ts b/apps/code-examples/src/app/code-examples/lists/paging/with-content/demo-data.service.ts index 11b589a918..5f5603210e 100644 --- a/apps/code-examples/src/app/code-examples/lists/paging/with-content/demo-data.service.ts +++ b/apps/code-examples/src/app/code-examples/lists/paging/with-content/demo-data.service.ts @@ -102,7 +102,7 @@ export class DemoDataService { totalCount: people.length, }).pipe( // Simulate network latency. - delay(1000), + delay(100), ); } } diff --git a/apps/code-examples/src/app/code-examples/lists/paging/with-content/demo.component.html b/apps/code-examples/src/app/code-examples/lists/paging/with-content/demo.component.html index bd1ee3c576..4dc0b0ff73 100644 --- a/apps/code-examples/src/app/code-examples/lists/paging/with-content/demo.component.html +++ b/apps/code-examples/src/app/code-examples/lists/paging/with-content/demo.component.html @@ -1,4 +1,5 @@ { + async function setupTest( + options: { + dataSkyId?: string; + } = {}, + ): Promise<{ + pagingHarness: SkyPagingHarness; + fixture: ComponentFixture; + }> { + await TestBed.configureTestingModule({ + imports: [DemoComponent, NoopAnimationsModule], + }).compileComponents(); + + const fixture = TestBed.createComponent(DemoComponent); + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + + const pagingHarness: SkyPagingHarness = options.dataSkyId + ? await loader.getHarness( + SkyPagingHarness.with({ + dataSkyId: options.dataSkyId, + }), + ) + : await loader.getHarness(SkyPagingHarness); + + return { pagingHarness, fixture }; + } + + it('should set up the paging content', async () => { + const { pagingHarness, fixture } = await setupTest({ + dataSkyId: 'my-paging-content', + }); + + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1); + + const contentHarness = await ( + await pagingHarness.getPagingContent() + ).queryHarness(SkyRepeaterHarness); + + let items = await contentHarness.getRepeaterItems(); + + await expectAsync(items[0].getTitleText()).toBeResolvedTo('Abed'); + + const controls = await pagingHarness.getPageControls(); + + await controls[2].clickButton(); + + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(3); + + items = await contentHarness.getRepeaterItems(); + + await expectAsync(items[0].getTitleText()).toBeResolvedTo('Leonard'); + + await pagingHarness.clickNextButton(); + + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(4); + + items = await contentHarness.getRepeaterItems(); + + await expectAsync(items[0].getTitleText()).toBeResolvedTo('Shirley'); + + await expectAsync(pagingHarness.clickNextButton()).toBeRejectedWithError( + 'Could not click the next button because it is disabled.', + ); + }); +}); diff --git a/libs/components/lists/testing/src/modules/paging/fixtures/paging-harness-test.component.html b/libs/components/lists/testing/src/modules/paging/fixtures/paging-harness-test.component.html new file mode 100644 index 0000000000..54e152bcc2 --- /dev/null +++ b/libs/components/lists/testing/src/modules/paging/fixtures/paging-harness-test.component.html @@ -0,0 +1,33 @@ + + + + @for (person of (pagedData | async)?.people; track person) { + + + {{ person.name }} + + + + + + Formal name + + + {{ person.formal }} + + + + + + } + + + + + diff --git a/libs/components/lists/testing/src/modules/paging/fixtures/paging-harness-test.component.ts b/libs/components/lists/testing/src/modules/paging/fixtures/paging-harness-test.component.ts new file mode 100644 index 0000000000..3de62e3009 --- /dev/null +++ b/libs/components/lists/testing/src/modules/paging/fixtures/paging-harness-test.component.ts @@ -0,0 +1,126 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyDescriptionListModule } from '@skyux/layout'; +import { + SkyPagingContentChangeArgs, + SkyPagingModule, + SkyRepeaterModule, +} from '@skyux/lists'; + +import { Subject, of, shareReplay, switchMap } from 'rxjs'; + +const people = [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + { + name: 'Alex', + formal: 'Mr. Osbourne', + }, + { + name: 'Ben', + formal: 'Mr. Chang', + }, + { + name: 'Britta', + formal: 'Ms. Perry', + }, + { + name: 'Buzz', + formal: 'Mr. Hickey', + }, + { + name: 'Craig', + formal: 'Mr. Pelton', + }, + { + name: 'Elroy', + formal: 'Mr. Patashnik', + }, + { + name: 'Garrett', + formal: 'Mr. Lambert', + }, + { + name: 'Ian', + formal: 'Mr. Duncan', + }, + { + name: 'Jeff', + formal: 'Mr. Winger', + }, + { + name: 'Leonard', + formal: 'Mr. Rodriguez', + }, + { + name: 'Neil', + formal: 'Mr. Neil', + }, + { + name: 'Pierce', + formal: 'Mr. Hawthorne', + }, + { + name: 'Preston', + formal: 'Mr. Koogler', + }, + { + name: 'Rachel', + formal: 'Ms. Rachel', + }, + { + name: 'Shirley', + formal: 'Ms. Bennett', + }, + { + name: 'Todd', + formal: 'Mr. Jacobson', + }, + { + name: 'Troy', + formal: 'Mr. Barnes', + }, + { + name: 'Vaughn', + formal: 'Mr. Miller', + }, + { + name: 'Vicki', + formal: 'Ms. Jenkins', + }, +]; + +@Component({ + standalone: true, + selector: 'test-paging-harness', + templateUrl: './paging-harness-test.component.html', + imports: [ + CommonModule, + SkyDescriptionListModule, + SkyPagingModule, + SkyRepeaterModule, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PagingHarnessTestComponent { + public pageSize = 5; + public maxPages = 3; + protected currentPage = 1; + protected contentChange = new Subject(); + + protected pagedData = this.contentChange.pipe( + switchMap((args) => { + const startIndex = (args.currentPage - 1) * this.pageSize; + + args.loadingComplete(); + + return of({ + people: people.slice(startIndex, startIndex + this.pageSize), + totalCount: people.length, + }); + }), + shareReplay(1), + ); +} diff --git a/libs/components/lists/testing/src/modules/paging/page-control-harness.ts b/libs/components/lists/testing/src/modules/paging/page-control-harness.ts new file mode 100644 index 0000000000..08a4636033 --- /dev/null +++ b/libs/components/lists/testing/src/modules/paging/page-control-harness.ts @@ -0,0 +1,51 @@ +import { ComponentHarness, TestElement } from '@angular/cdk/testing'; + +/** + * Harness to interact with a page control element in tests. + */ +export class SkyPageControlHarness extends ComponentHarness { + /** + * @internal + */ + public static hostSelector = 'li.sky-list-paging-link'; + + #getButton = this.locatorFor('button.sky-paging-btn'); + + /** + * Clicks the page button. + */ + public async clickButton(): Promise { + const button = await this.#getButton(); + + if (await this.#buttonIsDisabled(button)) { + const label = await button.text(); + throw new Error( + `Could not click page button ${label} because it is currently the active page.`, + ); + } + + await button.click(); + } + + /** + * Gets the page button text. + */ + public async getText(): Promise { + const button = await this.#getButton(); + + return await button.text(); + } + + /** + * Whether the page button is disabled. + */ + public async isDisabled(): Promise { + const button = await this.#getButton(); + return await this.#buttonIsDisabled(button); + } + + async #buttonIsDisabled(button: TestElement): Promise { + const disabled = await button.getAttribute('disabled'); + return disabled !== null; + } +} diff --git a/libs/components/lists/testing/src/modules/paging/paging-content-harness.ts b/libs/components/lists/testing/src/modules/paging/paging-content-harness.ts new file mode 100644 index 0000000000..6dda1072bf --- /dev/null +++ b/libs/components/lists/testing/src/modules/paging/paging-content-harness.ts @@ -0,0 +1,11 @@ +import { SkyQueryableComponentHarness } from '@skyux/core/testing'; + +/** + * Harness to interact with a paging content component in tests. + */ +export class SkyPagingContentHarness extends SkyQueryableComponentHarness { + /** + * @internal + */ + public static hostSelector = 'sky-paging-content'; +} diff --git a/libs/components/lists/testing/src/modules/paging/paging-harness-filters.ts b/libs/components/lists/testing/src/modules/paging/paging-harness-filters.ts new file mode 100644 index 0000000000..b1df1b1843 --- /dev/null +++ b/libs/components/lists/testing/src/modules/paging/paging-harness-filters.ts @@ -0,0 +1,8 @@ +import { SkyHarnessFilters } from '@skyux/core/testing'; + +/** + * A set of criteria that can be used to filter a list of `SkyPagingHarness` instances. + * @internal + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type +export interface SkyPagingHarnessFilters extends SkyHarnessFilters {} diff --git a/libs/components/lists/testing/src/modules/paging/paging-harness.spec.ts b/libs/components/lists/testing/src/modules/paging/paging-harness.spec.ts new file mode 100644 index 0000000000..3613c4fd43 --- /dev/null +++ b/libs/components/lists/testing/src/modules/paging/paging-harness.spec.ts @@ -0,0 +1,126 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { PagingHarnessTestComponent } from './fixtures/paging-harness-test.component'; +import { SkyPagingHarness } from './paging-harness'; + +describe('Paging test harness', () => { + async function setupTest( + options: { + dataSkyId?: string; + pageSize?: number; + maxPages?: number; + } = {}, + ): Promise<{ + pagingHarness: SkyPagingHarness; + fixture: ComponentFixture; + }> { + await TestBed.configureTestingModule({ + imports: [PagingHarnessTestComponent, NoopAnimationsModule], + }).compileComponents(); + + const fixture = TestBed.createComponent(PagingHarnessTestComponent); + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + + if (options.pageSize) { + fixture.componentInstance.pageSize = options.pageSize; + fixture.detectChanges(); + } + + if (options.maxPages) { + fixture.componentInstance.maxPages = options.maxPages; + fixture.detectChanges(); + } + + const pagingHarness: SkyPagingHarness = options.dataSkyId + ? await loader.getHarness( + SkyPagingHarness.with({ + dataSkyId: options.dataSkyId, + }), + ) + : await loader.getHarness(SkyPagingHarness); + + return { pagingHarness, fixture }; + } + + it('should get the paging component by data-sky-id', async () => { + const { pagingHarness } = await setupTest({ dataSkyId: 'other-paging' }); + + await expectAsync(pagingHarness.getPagingContent()).toBeRejected(); + }); + + it('should get the current page', async () => { + const { pagingHarness } = await setupTest(); + + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1); + }); + + it('should click the next and previous buttons', async () => { + const { pagingHarness } = await setupTest({ pageSize: 10 }); + + await pagingHarness.clickNextButton(); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(2); + await expectAsync(pagingHarness.clickNextButton()).toBeRejectedWithError( + 'Could not click the next button because it is disabled.', + ); + + await pagingHarness.clickPreviousButton(); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1); + await expectAsync( + pagingHarness.clickPreviousButton(), + ).toBeRejectedWithError( + 'Could not click the previous button because it is disabled.', + ); + }); + + it('should get the page controls', async () => { + const { pagingHarness } = await setupTest(); + + let pageControls = await pagingHarness.getPageControls(); + + expect(pageControls.length).toBe(3); + + await expectAsync(pageControls[0].getText()).toBeResolvedTo('1'); + await expectAsync(pageControls[0].isDisabled()).toBeResolvedTo(true); + await expectAsync(pageControls[1].getText()).toBeResolvedTo('2'); + await expectAsync(pageControls[1].isDisabled()).toBeResolvedTo(false); + await expectAsync(pageControls[2].getText()).toBeResolvedTo('3'); + await expectAsync(pageControls[2].isDisabled()).toBeResolvedTo(false); + + await pageControls[1].clickButton(); + + await expectAsync(pageControls[1].isDisabled()).toBeResolvedTo(true); + await expectAsync(pageControls[1].clickButton()).toBeRejectedWithError( + 'Could not click page button 2 because it is currently the active page.', + ); + + await pageControls[2].clickButton(); + + pageControls = await pagingHarness.getPageControls(); + + await expectAsync(pageControls[0].getText()).toBeResolvedTo('2'); + await expectAsync(pageControls[0].isDisabled()).toBeResolvedTo(false); + await expectAsync(pageControls[1].getText()).toBeResolvedTo('3'); + await expectAsync(pageControls[1].isDisabled()).toBeResolvedTo(true); + await expectAsync(pageControls[2].getText()).toBeResolvedTo('4'); + await expectAsync(pageControls[2].isDisabled()).toBeResolvedTo(false); + }); + + it('should throw an error if the paging controls are not present', async () => { + const { pagingHarness } = await setupTest({ dataSkyId: 'other-paging' }); + + await expectAsync(pagingHarness.getCurrentPage()).toBeRejectedWithError( + 'Could not find current page.', + ); + await expectAsync(pagingHarness.getPageControls()).toBeRejectedWithError( + 'Could not find any page controls.', + ); + await expectAsync(pagingHarness.clickNextButton()).toBeRejectedWithError( + 'Could not find the next button.', + ); + await expectAsync( + pagingHarness.clickPreviousButton(), + ).toBeRejectedWithError('Could not find the previous button.'); + }); +}); diff --git a/libs/components/lists/testing/src/modules/paging/paging-harness.ts b/libs/components/lists/testing/src/modules/paging/paging-harness.ts new file mode 100644 index 0000000000..880eff92b5 --- /dev/null +++ b/libs/components/lists/testing/src/modules/paging/paging-harness.ts @@ -0,0 +1,106 @@ +import { HarnessPredicate, TestElement } from '@angular/cdk/testing'; +import { SkyComponentHarness } from '@skyux/core/testing'; + +import { SkyPageControlHarness } from './page-control-harness'; +import { SkyPagingContentHarness } from './paging-content-harness'; +import { SkyPagingHarnessFilters } from './paging-harness-filters'; + +/** + * Harness for interacting with a paging component in tests. + */ +export class SkyPagingHarness extends SkyComponentHarness { + /** + * @internal + */ + public static hostSelector = 'sky-paging'; + + #getNextButton = this.locatorForOptional('li button.sky-paging-btn-next'); + #getPreviousButton = this.locatorForOptional( + 'li button.sky-paging-btn-previous', + ); + + /** + * Gets a `HarnessPredicate` that can be used to search for a + * `SkyPagingHarness` that meets certain criteria. + */ + public static with( + filters: SkyPagingHarnessFilters, + ): HarnessPredicate { + return SkyPagingHarness.getDataSkyIdPredicate(filters); + } + + /** + * Clicks the next button. + */ + public async clickNextButton(): Promise { + const button = await this.#getNextButton(); + + if (button === null) { + throw new Error('Could not find the next button.'); + } + + if (await this.#buttonIsDisabled(button)) { + throw new Error( + 'Could not click the next button because it is disabled.', + ); + } + + await button.click(); + } + + /** + * Clicks the previous button. + */ + public async clickPreviousButton(): Promise { + const button = await this.#getPreviousButton(); + + if (button === null) { + throw new Error('Could not find the previous button.'); + } + + if (await this.#buttonIsDisabled(button)) { + throw new Error( + 'Could not click the previous button because it is disabled.', + ); + } + + await button.click(); + } + + public async getCurrentPage(): Promise { + const currentPage = await this.locatorForOptional( + 'button.sky-paging-current', + )(); + + if (currentPage === null) { + throw new Error('Could not find current page.'); + } + + return parseInt(await currentPage.text()); + } + + /** + * Gets the page control buttons. + */ + public async getPageControls(): Promise { + const controls = await this.locatorForAll(SkyPageControlHarness)(); + + if (controls.length === 0) { + throw new Error('Could not find any page controls.'); + } + + return controls; + } + + /** + * Gets the paging content. + */ + public async getPagingContent(): Promise { + return await this.locatorFor(SkyPagingContentHarness)(); + } + + async #buttonIsDisabled(button: TestElement): Promise { + const disabled = await button.getAttribute('disabled'); + return disabled !== null; + } +} diff --git a/libs/components/lists/testing/src/public-api.ts b/libs/components/lists/testing/src/public-api.ts index 878af8addb..9e6109ee64 100644 --- a/libs/components/lists/testing/src/public-api.ts +++ b/libs/components/lists/testing/src/public-api.ts @@ -6,6 +6,11 @@ export { SkyPagingFixture } from './legacy/paging/paging-fixture'; export { SkyPagingFixtureButton } from './legacy/paging/paging-fixture-button'; export { SkyPagingTestingModule } from './legacy/paging/paging-testing.module'; +export { SkyPageControlHarness } from './modules/paging/page-control-harness'; +export { SkyPagingContentHarness } from './modules/paging/paging-content-harness'; +export { SkyPagingHarness } from './modules/paging/paging-harness'; +export { SkyPagingHarnessFilters } from './modules/paging/paging-harness-filters'; + export { SkyRepeaterHarness } from './modules/repeater/repeater-harness'; export { SkyRepeaterHarnessFilters } from './modules/repeater/repeater-harness-filters'; export { SkyRepeaterItemHarness } from './modules/repeater/repeater-item-harness'; From 92a29574227287bb1c1df9a091c23e776adec719 Mon Sep 17 00:00:00 2001 From: Corey Archer Date: Mon, 3 Mar 2025 17:03:41 -0500 Subject: [PATCH 2/2] feedback --- .../fixtures/paging-harness-test.component.html | 1 + .../src/modules/paging/paging-harness-filters.ts | 1 - .../src/modules/paging/paging-harness.spec.ts | 11 +++++++++++ .../testing/src/modules/paging/paging-harness.ts | 13 +++++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/libs/components/lists/testing/src/modules/paging/fixtures/paging-harness-test.component.html b/libs/components/lists/testing/src/modules/paging/fixtures/paging-harness-test.component.html index 54e152bcc2..e06ffbaea0 100644 --- a/libs/components/lists/testing/src/modules/paging/fixtures/paging-harness-test.component.html +++ b/libs/components/lists/testing/src/modules/paging/fixtures/paging-harness-test.component.html @@ -2,6 +2,7 @@ [itemCount]="(pagedData | async)?.totalCount ?? 0" [maxPages]="maxPages" [pageSize]="pageSize" + pagingLabel="Paging label" [(currentPage)]="currentPage" (contentChange)="contentChange.next($event)" > diff --git a/libs/components/lists/testing/src/modules/paging/paging-harness-filters.ts b/libs/components/lists/testing/src/modules/paging/paging-harness-filters.ts index b1df1b1843..9935d0bda7 100644 --- a/libs/components/lists/testing/src/modules/paging/paging-harness-filters.ts +++ b/libs/components/lists/testing/src/modules/paging/paging-harness-filters.ts @@ -2,7 +2,6 @@ import { SkyHarnessFilters } from '@skyux/core/testing'; /** * A set of criteria that can be used to filter a list of `SkyPagingHarness` instances. - * @internal */ // eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type export interface SkyPagingHarnessFilters extends SkyHarnessFilters {} diff --git a/libs/components/lists/testing/src/modules/paging/paging-harness.spec.ts b/libs/components/lists/testing/src/modules/paging/paging-harness.spec.ts index 3613c4fd43..165fb3b34a 100644 --- a/libs/components/lists/testing/src/modules/paging/paging-harness.spec.ts +++ b/libs/components/lists/testing/src/modules/paging/paging-harness.spec.ts @@ -107,9 +107,20 @@ describe('Paging test harness', () => { await expectAsync(pageControls[2].isDisabled()).toBeResolvedTo(false); }); + it('should get the paging label', async () => { + const { pagingHarness } = await setupTest(); + + await expectAsync(pagingHarness.getPagingLabel()).toBeResolvedTo( + 'Paging label', + ); + }); + it('should throw an error if the paging controls are not present', async () => { const { pagingHarness } = await setupTest({ dataSkyId: 'other-paging' }); + await expectAsync(pagingHarness.getPagingLabel()).toBeRejectedWithError( + 'Could not find paging label.', + ); await expectAsync(pagingHarness.getCurrentPage()).toBeRejectedWithError( 'Could not find current page.', ); diff --git a/libs/components/lists/testing/src/modules/paging/paging-harness.ts b/libs/components/lists/testing/src/modules/paging/paging-harness.ts index 880eff92b5..b0f0cc5d7a 100644 --- a/libs/components/lists/testing/src/modules/paging/paging-harness.ts +++ b/libs/components/lists/testing/src/modules/paging/paging-harness.ts @@ -67,6 +67,9 @@ export class SkyPagingHarness extends SkyComponentHarness { await button.click(); } + /** + * Gets the current page number. + */ public async getCurrentPage(): Promise { const currentPage = await this.locatorForOptional( 'button.sky-paging-current', @@ -99,6 +102,16 @@ export class SkyPagingHarness extends SkyComponentHarness { return await this.locatorFor(SkyPagingContentHarness)(); } + public async getPagingLabel(): Promise { + const pageNav = await this.locatorForOptional('nav.sky-paging')(); + + if (pageNav === null) { + throw new Error('Could not find paging label.'); + } + + return (await pageNav.getAttribute('aria-label')) as string; + } + async #buttonIsDisabled(button: TestElement): Promise { const disabled = await button.getAttribute('disabled'); return disabled !== null;