From 9ea091bbaad1d5ca06d4a2cb26ed0c633d8a5e51 Mon Sep 17 00:00:00 2001 From: Fanis Prodromou Date: Sun, 25 Feb 2024 15:34:40 +0200 Subject: [PATCH 1/4] feat(spectator): support defer block behavior --- .../spectator/jest/test/defer-block.spec.ts | 75 +++++++++++++++++++ .../spectator/src/lib/base/initial-module.ts | 4 +- projects/spectator/src/lib/base/options.ts | 4 +- projects/spectator/test/defer-block.spec.ts | 75 +++++++++++++++++++ 4 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 projects/spectator/jest/test/defer-block.spec.ts create mode 100644 projects/spectator/test/defer-block.spec.ts diff --git a/projects/spectator/jest/test/defer-block.spec.ts b/projects/spectator/jest/test/defer-block.spec.ts new file mode 100644 index 00000000..f085baa5 --- /dev/null +++ b/projects/spectator/jest/test/defer-block.spec.ts @@ -0,0 +1,75 @@ +import { Component } from '@angular/core'; +import { DeferBlockBehavior, DeferBlockState, fakeAsync } from '@angular/core/testing'; +import { createComponentFactory } from '@ngneat/spectator/jest'; + +describe('DeferBlock', () => { + describe('Playthrough Behavior', () => { + @Component({ + selector: 'app-root', + template: ` + + + @defer (when isVisible) { +
empty defer block
+ } , + `, + standalone: true, + }) + class DummyComponent { + isVisible = false; + } + + const createComponent = createComponentFactory({ + component: DummyComponent, + deferBlockBehavior: DeferBlockBehavior.Playthrough, + }); + + it('should render the defer block when isVisible is true', fakeAsync(() => { + // Arrange + const spectator = createComponent(); + + const button = spectator.query('[data-test="button--isVisible"]')!; + + // Act + spectator.click(button); + spectator.tick(); + spectator.detectChanges(); + + // Assert + expect(spectator.element.outerHTML).toContain('empty defer block'); + })); + }); + + describe('Manual Behavior', () => { + @Component({ + selector: 'app-root', + template: ` + @defer (on viewport) { +
empty defer block
+ } @placeholder { +
placeholder
+ } + `, + }) + class DummyComponent {} + + const createComponent = createComponentFactory({ + component: DummyComponent, + deferBlockBehavior: DeferBlockBehavior.Manual, + }); + + it('should render the complete state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const deferFixture = (await spectator.fixture.getDeferBlocks())[0]; + deferFixture.render(DeferBlockState.Complete); + spectator.detectChanges(); + await spectator.fixture.whenStable(); + + // Assert + expect(spectator.element.outerHTML).toContain('empty defer block'); + }); + }); +}); diff --git a/projects/spectator/src/lib/base/initial-module.ts b/projects/spectator/src/lib/base/initial-module.ts index 6d5f39c7..42a109ce 100644 --- a/projects/spectator/src/lib/base/initial-module.ts +++ b/projects/spectator/src/lib/base/initial-module.ts @@ -1,5 +1,5 @@ import { SchemaMetadata, Type } from '@angular/core'; -import { ModuleTeardownOptions } from '@angular/core/testing'; +import { DeferBlockBehavior, ModuleTeardownOptions } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { getGlobalsInjections } from '../globals-injections'; @@ -16,6 +16,7 @@ export interface ModuleMetadata { entryComponents: Type[]; schemas?: (SchemaMetadata | any[])[]; teardown?: ModuleTeardownOptions; + deferBlockBehavior: DeferBlockBehavior; errorOnUnknownElements: boolean; errorOnUnknownProperties: boolean; } @@ -36,6 +37,7 @@ export function initialModule(options: Required): ModuleMe // is always defined. If the user calls `defineGlobalsInjections({ teardown: { ... } })` and we merge it with // `options.teardown`, then `options.teardown` will always override global options. { ...(globals.teardown || options.teardown) }, + deferBlockBehavior: globals.deferBlockBehavior || options.deferBlockBehavior, errorOnUnknownElements: globals.errorOnUnknownElements || options.errorOnUnknownElements, errorOnUnknownProperties: globals.errorOnUnknownProperties || options.errorOnUnknownProperties, }; diff --git a/projects/spectator/src/lib/base/options.ts b/projects/spectator/src/lib/base/options.ts index c62d43b4..f3737a4c 100644 --- a/projects/spectator/src/lib/base/options.ts +++ b/projects/spectator/src/lib/base/options.ts @@ -1,5 +1,5 @@ import { Component, Directive, NgModule, Pipe, Provider, SchemaMetadata, Type } from '@angular/core'; -import { MetadataOverride, ModuleTeardownOptions } from '@angular/core/testing'; +import { DeferBlockBehavior, MetadataOverride, ModuleTeardownOptions } from '@angular/core/testing'; import { merge } from '../internals/merge'; import { mockProvider, MockProvider } from '../mock'; @@ -22,6 +22,7 @@ export interface BaseSpectatorOptions { overrideDirectives?: [Type, MetadataOverride][]; overridePipes?: [Type, MetadataOverride][]; teardown?: ModuleTeardownOptions; + deferBlockBehavior?: DeferBlockBehavior; errorOnUnknownElements?: boolean; errorOnUnknownProperties?: boolean; } @@ -49,6 +50,7 @@ const defaultOptions: OptionalsRequired = { teardown: { destroyAfterEach: false }, errorOnUnknownElements: false, errorOnUnknownProperties: false, + deferBlockBehavior: DeferBlockBehavior.Playthrough, }; /** diff --git a/projects/spectator/test/defer-block.spec.ts b/projects/spectator/test/defer-block.spec.ts new file mode 100644 index 00000000..2967e1b7 --- /dev/null +++ b/projects/spectator/test/defer-block.spec.ts @@ -0,0 +1,75 @@ +import { Component } from '@angular/core'; +import { DeferBlockBehavior, DeferBlockState, fakeAsync } from '@angular/core/testing'; +import { createComponentFactory } from '@ngneat/spectator'; + +describe('DeferBlock', () => { + describe('Playthrough Behavior', () => { + @Component({ + selector: 'app-root', + template: ` + + + @defer (when isVisible) { +
empty defer block
+ } , + `, + standalone: true, + }) + class DummyComponent { + isVisible = false; + } + + const createComponent = createComponentFactory({ + component: DummyComponent, + deferBlockBehavior: DeferBlockBehavior.Playthrough, + }); + + it('should render the defer block when isVisible is true', fakeAsync(() => { + // Arrange + const spectator = createComponent(); + + const button = spectator.query('[data-test="button--isVisible"]')!; + + // Act + spectator.click(button); + spectator.tick(); + spectator.detectChanges(); + + // Assert + expect(spectator.element.outerHTML).toContain('empty defer block'); + })); + }); + + describe('Manual Behavior', () => { + @Component({ + selector: 'app-root', + template: ` + @defer (on viewport) { +
empty defer block
+ } @placeholder { +
placeholder
+ } + `, + }) + class DummyComponent {} + + const createComponent = createComponentFactory({ + component: DummyComponent, + deferBlockBehavior: DeferBlockBehavior.Manual, + }); + + it('should render the complete state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const deferFixture = (await spectator.fixture.getDeferBlocks())[0]; + deferFixture.render(DeferBlockState.Complete); + spectator.detectChanges(); + await spectator.fixture.whenStable(); + + // Assert + expect(spectator.element.outerHTML).toContain('empty defer block'); + }); + }); +}); From eca951bbe0c482f21836fb41289008d60ea592b2 Mon Sep 17 00:00:00 2001 From: Fanis Prodromou Date: Sat, 2 Mar 2024 16:11:28 +0200 Subject: [PATCH 2/4] feat(spectator): improve the deferBlocks API --- .../spectator/jest/test/defer-block.spec.ts | 196 +++++++++++++++++- .../spectator/src/lib/spectator/spectator.ts | 79 ++++++- projects/spectator/src/lib/types.ts | 11 + projects/spectator/test/defer-block.spec.ts | 157 +++++++++++++- 4 files changed, 430 insertions(+), 13 deletions(-) diff --git a/projects/spectator/jest/test/defer-block.spec.ts b/projects/spectator/jest/test/defer-block.spec.ts index f085baa5..56e2184b 100644 --- a/projects/spectator/jest/test/defer-block.spec.ts +++ b/projects/spectator/jest/test/defer-block.spec.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { DeferBlockBehavior, DeferBlockState, fakeAsync } from '@angular/core/testing'; +import { DeferBlockBehavior, fakeAsync } from '@angular/core/testing'; import { createComponentFactory } from '@ngneat/spectator/jest'; describe('DeferBlock', () => { @@ -47,7 +47,11 @@ describe('DeferBlock', () => { @defer (on viewport) {
empty defer block
} @placeholder { -
placeholder
+
this is the placeholder text
+ } @loading { +
this is the loading text
+ } @error { +
this is the error text
} `, }) @@ -63,13 +67,193 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - const deferFixture = (await spectator.fixture.getDeferBlocks())[0]; - deferFixture.render(DeferBlockState.Complete); - spectator.detectChanges(); - await spectator.fixture.whenStable(); + await spectator.deferBlocks.renderComplete(); // Assert expect(spectator.element.outerHTML).toContain('empty defer block'); }); + + it('should render the placeholder state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlocks.renderPlaceholder(); + + // Assert + expect(spectator.element.outerHTML).toContain('this is the placeholder text'); + }); + + it('should render the loading state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlocks.renderLoading(); + + // Assert + expect(spectator.element.outerHTML).toContain('this is the loading text'); + }); + + it('should render the error state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlocks.renderError(); + + // Assert + expect(spectator.element.outerHTML).toContain('this is the error text'); + }); + }); + + describe('Manual Behavior with nested states', () => { + @Component({ + selector: 'app-root', + template: ` + @defer (on viewport) { +
complete state #1
+ + + @defer { +
complete state #1.1
+ + + @defer { +
complete state #1.1.1
+ } @placeholder { +
placeholder state #1.1.1
+ } + + + + @defer { +
complete state #1.1.2
+ } @placeholder { +
placeholder state #1.1.2
+ } + + + } @placeholder { +
nested placeholder text
+ } @loading { +
nested loading text
+ } @error { +
nested error text
+ } + + + } @placeholder { +
placeholder state #1
+ } @loading { +
loading state #1
+ } @error { +
error state #1
+ } + `, + }) + class DummyComponent {} + + const createComponent = createComponentFactory({ + component: DummyComponent, + deferBlockBehavior: DeferBlockBehavior.Manual, + }); + + it('should render the first nested complete state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlocks.renderComplete(); + await parentCompleteState.deferBlocks.renderComplete(); + + // Assert + expect(spectator.element.outerHTML).toContain('complete state #1.1'); + }); + + it('should render the first deep nested complete state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlocks.renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); + await childrenCompleteState.deferBlocks.renderComplete(); + + // Assert + expect(spectator.element.outerHTML).toContain('complete state #1.1.1'); + }); + + it('should render the first deep nested placeholder state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlocks.renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); + await childrenCompleteState.deferBlocks.renderPlaceholder(); + + // Assert + expect(spectator.element.outerHTML).toContain('placeholder state #1.1.1'); + }); + + it('should render the second nested complete state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlocks.renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); + await childrenCompleteState.deferBlocks.renderComplete(1); + + // Assert + expect(spectator.element.outerHTML).toContain('complete state #1.1.2'); + }); + + it('should render the second nested placeholder state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlocks.renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); + await childrenCompleteState.deferBlocks.renderPlaceholder(1); + + // Assert + expect(spectator.element.outerHTML).toContain('placeholder state #1.1.2'); + }); + + it('should render the placeholder state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlocks.renderPlaceholder(); + + // Assert + expect(spectator.element.outerHTML).toContain('placeholder state #1'); + }); + + it('should render the loading state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlocks.renderLoading(); + + // Assert + expect(spectator.element.outerHTML).toContain('loading state #1'); + }); + + it('should render the error state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlocks.renderError(); + + // Assert + expect(spectator.element.outerHTML).toContain('error state #1'); + }); }); }); diff --git a/projects/spectator/src/lib/spectator/spectator.ts b/projects/spectator/src/lib/spectator/spectator.ts index e49fbbbb..7784c61b 100644 --- a/projects/spectator/src/lib/spectator/spectator.ts +++ b/projects/spectator/src/lib/spectator/spectator.ts @@ -1,11 +1,11 @@ import { ChangeDetectorRef, DebugElement } from '@angular/core'; -import { ComponentFixture } from '@angular/core/testing'; +import { ComponentFixture, DeferBlockFixture, DeferBlockState } from '@angular/core/testing'; import { DomSpectator } from '../base/dom-spectator'; import { setProps } from '../internals/query'; import { SpyObject } from '../mock'; import { Token } from '../token'; -import { InferInputSignal, InferInputSignals } from '../types'; +import { DeferBlocks, InferInputSignal, InferInputSignals, NestedDeferBlocks } from '../types'; /** * @publicApi @@ -50,4 +50,79 @@ export class Spectator extends DomSpectator { // Force cd on the host component for cases such as: https://github.com/ngneat/spectator/issues/539 this.detectChanges(); } + + public get deferBlocks(): DeferBlocks { + return this._deferBlocksForGivenFixture(this.fixture.getDeferBlocks()); + } + + /** + * + * @param deferBlockFixture Defer block fixture + * @returns deferBlock object with methods to access the defer blocks + */ + private _deferBlocksForGivenFixture(deferBlockFixture: Promise): DeferBlocks { + return { + renderComplete: async (deferBlockIndex = 0) => { + const renderedDeferFixture = await this._renderDeferStateAndGetFixture( + DeferBlockState.Complete, + deferBlockIndex, + deferBlockFixture + ); + + return this._childrenDeferFixtures(renderedDeferFixture); + }, + renderPlaceholder: async (deferBlockIndex = 0) => { + const renderedDeferFixture = await this._renderDeferStateAndGetFixture( + DeferBlockState.Placeholder, + deferBlockIndex, + deferBlockFixture + ); + + return this._childrenDeferFixtures(renderedDeferFixture); + }, + renderLoading: async (deferBlockIndex = 0) => { + const renderedDeferFixture = await this._renderDeferStateAndGetFixture(DeferBlockState.Loading, deferBlockIndex, deferBlockFixture); + + return this._childrenDeferFixtures(renderedDeferFixture); + }, + renderError: async (deferBlockIndex = 0) => { + const renderedDeferFixture = await this._renderDeferStateAndGetFixture(DeferBlockState.Error, deferBlockIndex, deferBlockFixture); + + return this._childrenDeferFixtures(renderedDeferFixture); + }, + }; + } + + /** + * Renders the given defer block state and returns the defer block fixture + * + * @param deferBlockState complete, placeholder, loading or error + * @param deferBlockIndex index of the defer block to render + * @param deferBlockFixture Defer block fixture + * @returns Defer block fixture + */ + private async _renderDeferStateAndGetFixture( + deferBlockState: DeferBlockState, + deferBlockIndex = 0, + deferBlockFixture: Promise + ): Promise { + const deferFixture = (await deferBlockFixture)[deferBlockIndex]; + + await deferFixture.render(deferBlockState); + + return deferFixture; + } + + /** + * + * @param deferFixture Defer block fixture + * @returns deferBlock object with methods to access the nested defer blocks + */ + private _childrenDeferFixtures(deferFixture: DeferBlockFixture): NestedDeferBlocks { + return { + deferBlocks: { + ...this._deferBlocksForGivenFixture(deferFixture.getDeferBlocks()), + }, + }; + } } diff --git a/projects/spectator/src/lib/types.ts b/projects/spectator/src/lib/types.ts index f074a11f..2beab309 100644 --- a/projects/spectator/src/lib/types.ts +++ b/projects/spectator/src/lib/types.ts @@ -30,6 +30,17 @@ export type KeysMatching = { [K in keyof T]: T[K] extends V ? K : never }[ export type SelectOptions = string | string[] | HTMLOptionElement | HTMLOptionElement[]; +export type NestedDeferBlocks = { + deferBlocks: DeferBlocks; +}; + +export interface DeferBlocks { + renderComplete(deferBlockIndex?: number): Promise; + renderPlaceholder(deferBlockIndex?: number): Promise; + renderLoading(deferBlockIndex?: number): Promise; + renderError(deferBlockIndex?: number): Promise; +} + export interface KeyboardEventOptions { key: string; keyCode: number; diff --git a/projects/spectator/test/defer-block.spec.ts b/projects/spectator/test/defer-block.spec.ts index 2967e1b7..febd210f 100644 --- a/projects/spectator/test/defer-block.spec.ts +++ b/projects/spectator/test/defer-block.spec.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { DeferBlockBehavior, DeferBlockState, fakeAsync } from '@angular/core/testing'; +import { DeferBlockBehavior, fakeAsync } from '@angular/core/testing'; import { createComponentFactory } from '@ngneat/spectator'; describe('DeferBlock', () => { @@ -63,13 +63,160 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - const deferFixture = (await spectator.fixture.getDeferBlocks())[0]; - deferFixture.render(DeferBlockState.Complete); - spectator.detectChanges(); - await spectator.fixture.whenStable(); + spectator.deferBlocks.renderComplete(); // Assert expect(spectator.element.outerHTML).toContain('empty defer block'); }); }); + + describe('Manual Behavior with nested states', () => { + @Component({ + selector: 'app-root', + template: ` + @defer (on viewport) { +
complete state #1
+ + + @defer { +
complete state #1.1
+ + + @defer { +
complete state #1.1.1
+ } @placeholder { +
placeholder state #1.1.1
+ } + + + + @defer { +
complete state #1.1.2
+ } @placeholder { +
placeholder state #1.1.2
+ } + + + } @placeholder { +
nested placeholder text
+ } @loading { +
nested loading text
+ } @error { +
nested error text
+ } + + + } @placeholder { +
placeholder state #1
+ } @loading { +
loading state #1
+ } @error { +
error state #1
+ } + `, + }) + class DummyComponent {} + + const createComponent = createComponentFactory({ + component: DummyComponent, + deferBlockBehavior: DeferBlockBehavior.Manual, + }); + + it('should render the first nested complete state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlocks.renderComplete(); + await parentCompleteState.deferBlocks.renderComplete(); + + // Assert + expect(spectator.element.outerHTML).toContain('complete state #1.1'); + }); + + it('should render the first deep nested complete state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlocks.renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); + await childrenCompleteState.deferBlocks.renderComplete(); + + // Assert + expect(spectator.element.outerHTML).toContain('complete state #1.1.1'); + }); + + it('should render the first deep nested placeholder state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlocks.renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); + await childrenCompleteState.deferBlocks.renderPlaceholder(); + + // Assert + expect(spectator.element.outerHTML).toContain('placeholder state #1.1.1'); + }); + + it('should render the second nested complete state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlocks.renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); + await childrenCompleteState.deferBlocks.renderComplete(1); + + // Assert + expect(spectator.element.outerHTML).toContain('complete state #1.1.2'); + }); + + it('should render the second nested placeholder state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlocks.renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); + await childrenCompleteState.deferBlocks.renderPlaceholder(1); + + // Assert + expect(spectator.element.outerHTML).toContain('placeholder state #1.1.2'); + }); + + it('should render the placeholder state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlocks.renderPlaceholder(); + + // Assert + expect(spectator.element.outerHTML).toContain('placeholder state #1'); + }); + + it('should render the loading state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlocks.renderLoading(); + + // Assert + expect(spectator.element.outerHTML).toContain('loading state #1'); + }); + + it('should render the error state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlocks.renderError(); + + // Assert + expect(spectator.element.outerHTML).toContain('error state #1'); + }); + }); }); From cefc3a5dcc2b1192961cdf8a0735324151421020 Mon Sep 17 00:00:00 2001 From: Fanis Prodromou Date: Mon, 4 Mar 2024 12:48:05 +0200 Subject: [PATCH 3/4] feat(spectator): provide the index in the defer block method --- .../spectator/jest/test/defer-block.spec.ts | 42 +++++++++---------- .../spectator/src/lib/spectator/spectator.ts | 38 +++++++++-------- projects/spectator/src/lib/types.ts | 10 ++--- projects/spectator/test/defer-block.spec.ts | 36 ++++++++-------- 4 files changed, 64 insertions(+), 62 deletions(-) diff --git a/projects/spectator/jest/test/defer-block.spec.ts b/projects/spectator/jest/test/defer-block.spec.ts index 56e2184b..06d84823 100644 --- a/projects/spectator/jest/test/defer-block.spec.ts +++ b/projects/spectator/jest/test/defer-block.spec.ts @@ -67,7 +67,7 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - await spectator.deferBlocks.renderComplete(); + await spectator.deferBlock().renderComplete(); // Assert expect(spectator.element.outerHTML).toContain('empty defer block'); @@ -78,7 +78,7 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - await spectator.deferBlocks.renderPlaceholder(); + await spectator.deferBlock().renderPlaceholder(); // Assert expect(spectator.element.outerHTML).toContain('this is the placeholder text'); @@ -89,7 +89,7 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - await spectator.deferBlocks.renderLoading(); + await spectator.deferBlock().renderLoading(); // Assert expect(spectator.element.outerHTML).toContain('this is the loading text'); @@ -100,7 +100,7 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - await spectator.deferBlocks.renderError(); + await spectator.deferBlock().renderError(); // Assert expect(spectator.element.outerHTML).toContain('this is the error text'); @@ -164,8 +164,8 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - const parentCompleteState = await spectator.deferBlocks.renderComplete(); - await parentCompleteState.deferBlocks.renderComplete(); + const parentCompleteState = await spectator.deferBlock().renderComplete(); + await parentCompleteState.deferBlock().renderComplete(); // Assert expect(spectator.element.outerHTML).toContain('complete state #1.1'); @@ -176,9 +176,9 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - const parentCompleteState = await spectator.deferBlocks.renderComplete(); - const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); - await childrenCompleteState.deferBlocks.renderComplete(); + const parentCompleteState = await spectator.deferBlock().renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete(); + await childrenCompleteState.deferBlock().renderComplete(); // Assert expect(spectator.element.outerHTML).toContain('complete state #1.1.1'); @@ -189,9 +189,9 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - const parentCompleteState = await spectator.deferBlocks.renderComplete(); - const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); - await childrenCompleteState.deferBlocks.renderPlaceholder(); + const parentCompleteState = await spectator.deferBlock().renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete(); + await childrenCompleteState.deferBlock().renderPlaceholder(); // Assert expect(spectator.element.outerHTML).toContain('placeholder state #1.1.1'); @@ -202,9 +202,9 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - const parentCompleteState = await spectator.deferBlocks.renderComplete(); - const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); - await childrenCompleteState.deferBlocks.renderComplete(1); + const parentCompleteState = await spectator.deferBlock().renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete(); + await childrenCompleteState.deferBlock(1).renderComplete(); // Assert expect(spectator.element.outerHTML).toContain('complete state #1.1.2'); @@ -215,9 +215,9 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - const parentCompleteState = await spectator.deferBlocks.renderComplete(); - const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); - await childrenCompleteState.deferBlocks.renderPlaceholder(1); + const parentCompleteState = await spectator.deferBlock().renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete(); + await childrenCompleteState.deferBlock(1).renderPlaceholder(); // Assert expect(spectator.element.outerHTML).toContain('placeholder state #1.1.2'); @@ -228,7 +228,7 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - await spectator.deferBlocks.renderPlaceholder(); + await spectator.deferBlock().renderPlaceholder(); // Assert expect(spectator.element.outerHTML).toContain('placeholder state #1'); @@ -239,7 +239,7 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - await spectator.deferBlocks.renderLoading(); + await spectator.deferBlock().renderLoading(); // Assert expect(spectator.element.outerHTML).toContain('loading state #1'); @@ -250,7 +250,7 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - await spectator.deferBlocks.renderError(); + await spectator.deferBlock().renderError(); // Assert expect(spectator.element.outerHTML).toContain('error state #1'); diff --git a/projects/spectator/src/lib/spectator/spectator.ts b/projects/spectator/src/lib/spectator/spectator.ts index 7784c61b..db370871 100644 --- a/projects/spectator/src/lib/spectator/spectator.ts +++ b/projects/spectator/src/lib/spectator/spectator.ts @@ -51,42 +51,46 @@ export class Spectator extends DomSpectator { this.detectChanges(); } - public get deferBlocks(): DeferBlocks { - return this._deferBlocksForGivenFixture(this.fixture.getDeferBlocks()); + public deferBlock(deferBlockIndex = 0): DeferBlocks { + return this._deferBlocksForGivenFixture(deferBlockIndex, this.fixture.getDeferBlocks()); } /** * - * @param deferBlockFixture Defer block fixture + * @param deferBlockFixtures Defer block fixture * @returns deferBlock object with methods to access the defer blocks */ - private _deferBlocksForGivenFixture(deferBlockFixture: Promise): DeferBlocks { + private _deferBlocksForGivenFixture(deferBlockIndex = 0, deferBlockFixtures: Promise): DeferBlocks { return { - renderComplete: async (deferBlockIndex = 0) => { + renderComplete: async () => { const renderedDeferFixture = await this._renderDeferStateAndGetFixture( DeferBlockState.Complete, deferBlockIndex, - deferBlockFixture + deferBlockFixtures ); return this._childrenDeferFixtures(renderedDeferFixture); }, - renderPlaceholder: async (deferBlockIndex = 0) => { + renderPlaceholder: async () => { const renderedDeferFixture = await this._renderDeferStateAndGetFixture( DeferBlockState.Placeholder, deferBlockIndex, - deferBlockFixture + deferBlockFixtures ); return this._childrenDeferFixtures(renderedDeferFixture); }, - renderLoading: async (deferBlockIndex = 0) => { - const renderedDeferFixture = await this._renderDeferStateAndGetFixture(DeferBlockState.Loading, deferBlockIndex, deferBlockFixture); + renderLoading: async () => { + const renderedDeferFixture = await this._renderDeferStateAndGetFixture( + DeferBlockState.Loading, + deferBlockIndex, + deferBlockFixtures + ); return this._childrenDeferFixtures(renderedDeferFixture); }, - renderError: async (deferBlockIndex = 0) => { - const renderedDeferFixture = await this._renderDeferStateAndGetFixture(DeferBlockState.Error, deferBlockIndex, deferBlockFixture); + renderError: async () => { + const renderedDeferFixture = await this._renderDeferStateAndGetFixture(DeferBlockState.Error, deferBlockIndex, deferBlockFixtures); return this._childrenDeferFixtures(renderedDeferFixture); }, @@ -98,15 +102,15 @@ export class Spectator extends DomSpectator { * * @param deferBlockState complete, placeholder, loading or error * @param deferBlockIndex index of the defer block to render - * @param deferBlockFixture Defer block fixture + * @param deferBlockFixtures Defer block fixture * @returns Defer block fixture */ private async _renderDeferStateAndGetFixture( deferBlockState: DeferBlockState, deferBlockIndex = 0, - deferBlockFixture: Promise + deferBlockFixtures: Promise ): Promise { - const deferFixture = (await deferBlockFixture)[deferBlockIndex]; + const deferFixture = (await deferBlockFixtures)[deferBlockIndex]; await deferFixture.render(deferBlockState); @@ -120,9 +124,7 @@ export class Spectator extends DomSpectator { */ private _childrenDeferFixtures(deferFixture: DeferBlockFixture): NestedDeferBlocks { return { - deferBlocks: { - ...this._deferBlocksForGivenFixture(deferFixture.getDeferBlocks()), - }, + deferBlock: (deferBlockIndex = 0) => this._deferBlocksForGivenFixture(deferBlockIndex, deferFixture.getDeferBlocks()), }; } } diff --git a/projects/spectator/src/lib/types.ts b/projects/spectator/src/lib/types.ts index 2beab309..e83133d1 100644 --- a/projects/spectator/src/lib/types.ts +++ b/projects/spectator/src/lib/types.ts @@ -31,14 +31,14 @@ export type KeysMatching = { [K in keyof T]: T[K] extends V ? K : never }[ export type SelectOptions = string | string[] | HTMLOptionElement | HTMLOptionElement[]; export type NestedDeferBlocks = { - deferBlocks: DeferBlocks; + deferBlock: (deferBlockIndex?: number) => DeferBlocks; }; export interface DeferBlocks { - renderComplete(deferBlockIndex?: number): Promise; - renderPlaceholder(deferBlockIndex?: number): Promise; - renderLoading(deferBlockIndex?: number): Promise; - renderError(deferBlockIndex?: number): Promise; + renderComplete(): Promise; + renderPlaceholder(): Promise; + renderLoading(): Promise; + renderError(): Promise; } export interface KeyboardEventOptions { diff --git a/projects/spectator/test/defer-block.spec.ts b/projects/spectator/test/defer-block.spec.ts index febd210f..2bd8bfd9 100644 --- a/projects/spectator/test/defer-block.spec.ts +++ b/projects/spectator/test/defer-block.spec.ts @@ -63,7 +63,7 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - spectator.deferBlocks.renderComplete(); + await spectator.deferBlock().renderComplete(); // Assert expect(spectator.element.outerHTML).toContain('empty defer block'); @@ -127,8 +127,8 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - const parentCompleteState = await spectator.deferBlocks.renderComplete(); - await parentCompleteState.deferBlocks.renderComplete(); + const parentCompleteState = await spectator.deferBlock().renderComplete(); + await parentCompleteState.deferBlock().renderComplete(); // Assert expect(spectator.element.outerHTML).toContain('complete state #1.1'); @@ -139,9 +139,9 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - const parentCompleteState = await spectator.deferBlocks.renderComplete(); - const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); - await childrenCompleteState.deferBlocks.renderComplete(); + const parentCompleteState = await spectator.deferBlock().renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete(); + await childrenCompleteState.deferBlock().renderComplete(); // Assert expect(spectator.element.outerHTML).toContain('complete state #1.1.1'); @@ -152,9 +152,9 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - const parentCompleteState = await spectator.deferBlocks.renderComplete(); - const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); - await childrenCompleteState.deferBlocks.renderPlaceholder(); + const parentCompleteState = await spectator.deferBlock().renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete(); + await childrenCompleteState.deferBlock().renderPlaceholder(); // Assert expect(spectator.element.outerHTML).toContain('placeholder state #1.1.1'); @@ -165,9 +165,9 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - const parentCompleteState = await spectator.deferBlocks.renderComplete(); - const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); - await childrenCompleteState.deferBlocks.renderComplete(1); + const parentCompleteState = await spectator.deferBlock().renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete(); + await childrenCompleteState.deferBlock(1).renderComplete(); // Assert expect(spectator.element.outerHTML).toContain('complete state #1.1.2'); @@ -178,9 +178,9 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - const parentCompleteState = await spectator.deferBlocks.renderComplete(); - const childrenCompleteState = await parentCompleteState.deferBlocks.renderComplete(); - await childrenCompleteState.deferBlocks.renderPlaceholder(1); + const parentCompleteState = await spectator.deferBlock().renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete(); + await childrenCompleteState.deferBlock(1).renderPlaceholder(); // Assert expect(spectator.element.outerHTML).toContain('placeholder state #1.1.2'); @@ -191,7 +191,7 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - await spectator.deferBlocks.renderPlaceholder(); + await spectator.deferBlock().renderPlaceholder(); // Assert expect(spectator.element.outerHTML).toContain('placeholder state #1'); @@ -202,7 +202,7 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - await spectator.deferBlocks.renderLoading(); + await spectator.deferBlock().renderLoading(); // Assert expect(spectator.element.outerHTML).toContain('loading state #1'); @@ -213,7 +213,7 @@ describe('DeferBlock', () => { const spectator = createComponent(); // Act - await spectator.deferBlocks.renderError(); + await spectator.deferBlock().renderError(); // Assert expect(spectator.element.outerHTML).toContain('error state #1'); From bc19e4b649bea51ddc2637fda163dc8d13bb6d09 Mon Sep 17 00:00:00 2001 From: Fanis Prodromou Date: Mon, 4 Mar 2024 12:49:58 +0200 Subject: [PATCH 4/4] docs(spectator): deferrable views in the readme file --- README.md | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/README.md b/README.md index d5f67795..619870eb 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,8 @@ Become a bronze sponsor and get your logo on our README on GitHub. - [Testing Select Elements](#testing-select-elements) - [Mocking Components](#mocking-components) - [Testing Single Component/Directive Angular Modules](#testing-single-componentdirective-angular-modules) + - [Deferrable Views](#deferrable-views) + - [Nested Deferrable Views](#nested-deferrable-views) - [Testing with Host](#testing-with-host) - [Custom Host Component](#custom-host-component) - [Testing with Routing](#testing-with-routing) @@ -154,6 +156,7 @@ const createComponent = createComponentFactory({ declareComponent: false, // Defaults to true disableAnimations: false, // Defaults to true shallow: true, // Defaults to false + deferBlockBehavior: DeferBlockBehavior // Defaults to DeferBlockBehavior.Playthrough }); ``` @@ -551,7 +554,107 @@ const createDirective = createDirectiveFactory({ }); ``` +### Deferrable Views +The Spectator provides a convenient API to access the deferrable views (`@defer {}`). +Access the desired defer block using the `spectator.deferBlock(optionalIndex)` method. The `optionalIndex` parameter is optional and allows you to specify the index of the defer block you want to access. + +- **Accessing the first defer block**: Simply call `spectator.deferBlock()`. +- **Accessing subsequent defer blocks**: Use the corresponding index as an argument. For example, `spectator.deferBlock(1)` accesses the second block (zero-based indexing). + +The `spectator.deferBlock(optionalIndex)` returns four methods for rendering different states of the specified defer block: + +- `renderComplete()` - Renders the **complete** state of the defer block. +- `renderPlaceholder()` - Renders the **placeholder** state of the defer block. +- `renderLoading()` - Renders the **loading** state of the defer block. +- `renderError()` - Renders the **error** state of the defer block. + +**Example:** + +```ts + @Component({ + selector: 'app-cmp', + template: ` + @defer (on viewport) { +
Complete state of the first defer block
+ } @placeholder { +
Placeholder
+ } + `, + }) + class DummyComponent {} + + const createComponent = createComponentFactory({ + component: DummyComponent, + deferBlockBehavior: DeferBlockBehavior.Manual, + }); + + it('should render the complete state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlock().renderComplete(); + + // Assert + expect(spectator.element.outerHTML).toContain('first defer block'); + }); +``` + + +#### Nested Deferrable Views + +To access states within nested defer blocks, call the `deferBlock` method **chaining** from the returned block state method. + +**Example:** Accessing the nested complete state: + +```ts +// Assuming `spectator.deferBlock(0).renderComplete()` renders the complete state of the parent defer block +const parentCompleteState = await spectator.deferBlock().renderComplete(); + +// Access the nested complete state of the parent defer block +const nestedCompleteState = await parentCompleteState.renderComplete().deferBlock(); +``` + +**Complete Example**: + +```ts + @Component({ + selector: 'app-cmp', + template: ` + @defer (on viewport) { +
Complete state of the first defer block
+ + @defer { +
Complete state of the nested defer block
+ } + } @placeholder { +
Placeholder
+ } + `, + }) + class DummyComponent {} + + const createComponent = createComponentFactory({ + component: DummyComponent, + deferBlockBehavior: DeferBlockBehavior.Manual, + }); + + it('should render the first nested complete state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + // Renders the parent complete state + const parentCompleteState = await spectator.deferBlock().renderComplete(); + + // Renders the nested complete state + await parentCompleteState.deferBlock().renderComplete(); + + // Assert + expect(spectator.element.outerHTML).toContain('nested defer block'); + }); +``` ## Testing with Host Testing a component with a host component is a more elegant and powerful technique to test your component.