From 06bbc2fc4e08fab8a5a645e2b6b8cdb067f9ac2c Mon Sep 17 00:00:00 2001 From: Jessica Janiuk Date: Fri, 1 Sep 2023 13:29:14 -0400 Subject: [PATCH] refactor(core): Add defer block testing fixture (#51698) This adds a fixture for being able to access and test defer blocks. PR Close #51698 --- goldens/public-api/core/testing/index.md | 16 + packages/core/src/core_private_export.ts | 2 + .../core/src/core_render3_private_export.ts | 4 + packages/core/src/render3/index.ts | 1 + .../core/src/render3/instructions/defer.ts | 154 ++++++-- packages/core/src/render3/interfaces/defer.ts | 60 ++- packages/core/test/acceptance/defer_spec.ts | 26 +- packages/core/test/component_fixture_spec.ts | 44 +++ packages/core/test/defer_fixture_spec.ts | 369 ++++++++++++++++++ packages/core/test/test_bed_spec.ts | 22 ++ .../core/testing/src/component_fixture.ts | 21 +- packages/core/testing/src/defer.ts | 90 +++++ packages/core/testing/src/test_bed.ts | 14 +- packages/core/testing/src/test_bed_common.ts | 8 +- .../core/testing/src/test_bed_compiler.ts | 7 +- packages/core/testing/src/testing.ts | 2 + 16 files changed, 784 insertions(+), 56 deletions(-) create mode 100644 packages/core/test/defer_fixture_spec.ts create mode 100644 packages/core/testing/src/defer.ts diff --git a/goldens/public-api/core/testing/index.md b/goldens/public-api/core/testing/index.md index 93c81a99fb2..72209b68502 100644 --- a/goldens/public-api/core/testing/index.md +++ b/goldens/public-api/core/testing/index.md @@ -8,6 +8,8 @@ import { ChangeDetectorRef } from '@angular/core'; import { Component } from '@angular/core'; import { ComponentRef } from '@angular/core'; import { DebugElement } from '@angular/core'; +import { ɵDeferBlockBehavior as DeferBlockBehavior } from '@angular/core'; +import { ɵDeferBlockState as DeferBlockState } from '@angular/core'; import { Directive } from '@angular/core'; import { ElementRef } from '@angular/core'; import { InjectFlags } from '@angular/core'; @@ -20,6 +22,7 @@ import { PlatformRef } from '@angular/core'; import { ProviderToken } from '@angular/core'; import { SchemaMetadata } from '@angular/core'; import { Type } from '@angular/core'; +import { ɵDeferBlockDetails } from '@angular/core'; import { ɵFlushableEffectRunner } from '@angular/core'; // @public @@ -41,6 +44,7 @@ export class ComponentFixture { destroy(): void; detectChanges(checkNoChanges?: boolean): void; elementRef: ElementRef; + getDeferBlocks(): Promise; isStable(): boolean; nativeElement: any; // (undocumented) @@ -55,6 +59,17 @@ export const ComponentFixtureAutoDetect: InjectionToken; // @public (undocumented) export const ComponentFixtureNoNgZone: InjectionToken; +export { DeferBlockBehavior } + +// @public +export class DeferBlockFixture { + constructor(block: ɵDeferBlockDetails, componentFixture: ComponentFixture); + getDeferBlocks(): Promise; + render(state: DeferBlockState): Promise; +} + +export { DeferBlockState } + // @public export function discardPeriodicTasks(): void; @@ -196,6 +211,7 @@ export interface TestEnvironmentOptions { export interface TestModuleMetadata { // (undocumented) declarations?: any[]; + deferBlockBehavior?: DeferBlockBehavior; errorOnUnknownElements?: boolean; errorOnUnknownProperties?: boolean; // (undocumented) diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 593dcdac9e3..9ae217048a0 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -27,6 +27,8 @@ export {ComponentFactory as ɵComponentFactory} from './linker/component_factory export {clearResolutionOfComponentResourcesQueue as ɵclearResolutionOfComponentResourcesQueue, isComponentDefPendingResolution as ɵisComponentDefPendingResolution, resolveComponentResources as ɵresolveComponentResources, restoreComponentResolutionQueue as ɵrestoreComponentResolutionQueue} from './metadata/resource_loading'; export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities'; export {InjectorProfilerContext as ɵInjectorProfilerContext, setInjectorProfilerContext as ɵsetInjectorProfilerContext} from './render3/debug/injector_profiler'; +export {DeferBlockDetails as ɵDeferBlockDetails, getDeferBlocks as ɵgetDeferBlocks, renderDeferBlockState as ɵrenderDeferBlockState, triggerResourceLoading as ɵtriggerResourceLoading} from './render3/instructions/defer'; +export {DeferBlockBehavior as ɵDeferBlockBehavior, DeferBlockConfig as ɵDeferBlockConfig, DeferBlockState as ɵDeferBlockState} from './render3/interfaces/defer'; export {allowSanitizationBypassAndThrow as ɵallowSanitizationBypassAndThrow, BypassType as ɵBypassType, getSanitizationBypassType as ɵgetSanitizationBypassType, SafeHtml as ɵSafeHtml, SafeResourceUrl as ɵSafeResourceUrl, SafeScript as ɵSafeScript, SafeStyle as ɵSafeStyle, SafeUrl as ɵSafeUrl, SafeValue as ɵSafeValue, unwrapSafeValue as ɵunwrapSafeValue} from './sanitization/bypass'; export {_sanitizeHtml as ɵ_sanitizeHtml} from './sanitization/html_sanitizer'; export {_sanitizeUrl as ɵ_sanitizeUrl} from './sanitization/url_sanitizer'; diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 3357af897bd..52628059bc0 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -70,6 +70,7 @@ export { store as ɵstore, ɵDeferBlockDependencyInterceptor, ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, + ɵDEFER_BLOCK_CONFIG, ɵɵadvance, ɵɵattribute, ɵɵattributeInterpolate1, @@ -240,6 +241,9 @@ export { ɵgetUnknownPropertyStrictMode, ɵsetUnknownPropertyStrictMode } from './render3/index'; +export { + CONTAINER_HEADER_OFFSET as ɵCONTAINER_HEADER_OFFSET, +} from './render3/interfaces/container'; export { LContext as ɵLContext, } from './render3/interfaces/context'; diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 451a1c8d34c..198d28bba9f 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -162,6 +162,7 @@ export { DeferBlockDependencyInterceptor as ɵDeferBlockDependencyInterceptor, DEFER_BLOCK_DEPENDENCY_INTERCEPTOR as ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, + DEFER_BLOCK_CONFIG as ɵDEFER_BLOCK_CONFIG, } from './instructions/all'; export {ɵɵi18n, ɵɵi18nApply, ɵɵi18nAttributes, ɵɵi18nEnd, ɵɵi18nExp,ɵɵi18nPostprocess, ɵɵi18nStart} from './instructions/i18n'; export {RenderFlags} from './interfaces/definition'; diff --git a/packages/core/src/render3/instructions/defer.ts b/packages/core/src/render3/instructions/defer.ts index a97b6b033f7..24bf6acdd5e 100644 --- a/packages/core/src/render3/instructions/defer.ts +++ b/packages/core/src/render3/instructions/defer.ts @@ -13,11 +13,11 @@ import {assertDefined, assertEqual, throwError} from '../../util/assert'; import {assertIndexInDeclRange, assertLContainer, assertTNodeForLView} from '../assert'; import {bindingUpdated} from '../bindings'; import {getComponentDef, getDirectiveDef, getPipeDef} from '../definition'; -import {LContainer} from '../interfaces/container'; -import {DEFER_BLOCK_STATE, DeferBlockInstanceState, DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, DependencyResolverFn, LDeferBlockDetails, TDeferBlockDetails} from '../interfaces/defer'; +import {CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container'; +import {DEFER_BLOCK_STATE, DeferBlockBehavior, DeferBlockConfig, DeferBlockInternalState, DeferBlockState, DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, DependencyResolverFn, LDeferBlockDetails, TDeferBlockDetails} from '../interfaces/defer'; import {DirectiveDefList, PipeDefList} from '../interfaces/definition'; import {TContainerNode, TNode} from '../interfaces/node'; -import {isDestroyed} from '../interfaces/type_checks'; +import {isDestroyed, isLContainer, isLView} from '../interfaces/type_checks'; import {HEADER_OFFSET, INJECTOR, LView, PARENT, TVIEW, TView} from '../interfaces/view'; import {getCurrentTNode, getLView, getSelectedTNode, getTView, nextBindingIndex} from '../state'; import {isPlatformBrowser} from '../util/misc_utils'; @@ -33,6 +33,10 @@ import {ɵɵtemplate} from './template'; * only placeholder content is rendered (if provided). */ function shouldTriggerDeferBlock(injector: Injector): boolean { + const config = injector.get(DEFER_BLOCK_CONFIG, {optional: true}); + if (config?.behavior === DeferBlockBehavior.Manual) { + return false; + } return isPlatformBrowser(injector); } @@ -100,7 +104,7 @@ export function ɵɵdefer( // Init instance-specific defer details and store it. const lDetails = []; - lDetails[DEFER_BLOCK_STATE] = DeferBlockInstanceState.INITIAL; + lDetails[DEFER_BLOCK_STATE] = DeferBlockInternalState.Initial; setLDeferBlockDetails(lView, adjustedIndex, lDetails as LDeferBlockDetails); } @@ -111,19 +115,18 @@ export function ɵɵdefer( export function ɵɵdeferWhen(rawValue: unknown) { const lView = getLView(); const bindingIndex = nextBindingIndex(); - if (bindingUpdated(lView, bindingIndex, rawValue)) { const value = Boolean(rawValue); // handle truthy or falsy values const tNode = getSelectedTNode(); const lDetails = getLDeferBlockDetails(lView, tNode); const renderedState = lDetails[DEFER_BLOCK_STATE]; - if (value === false && renderedState === DeferBlockInstanceState.INITIAL) { + if (value === false && renderedState === DeferBlockInternalState.Initial) { // If nothing is rendered yet, render a placeholder (if defined). renderPlaceholder(lView, tNode); } else if ( value === true && - (renderedState === DeferBlockInstanceState.INITIAL || - renderedState === DeferBlockInstanceState.PLACEHOLDER)) { + (renderedState === DeferBlockInternalState.Initial || + renderedState === DeferBlockState.Placeholder)) { // The `when` condition has changed to `true`, trigger defer block loading // if the block is either in initial (nothing is rendered) or a placeholder // state. @@ -147,7 +150,7 @@ export function ɵɵdeferPrefetchWhen(rawValue: unknown) { const tDetails = getTDeferBlockDetails(tView, tNode); if (value === true && tDetails.loadingState === DeferDependenciesLoadingState.NOT_STARTED) { // If loading has not been started yet, trigger it now. - triggerResourceLoading(tDetails, tView, lView); + triggerPrefetching(tDetails, lView); } } } @@ -186,7 +189,7 @@ export function ɵɵdeferPrefetchOnIdle() { // an underlying LView get destroyed (thus passing `null` as a second argument), // because there might be other LViews (that represent embedded views) that // depend on resource loading. - onIdle(() => triggerResourceLoading(tDetails, tView, lView), null /* LView */); + onIdle(() => triggerPrefetching(tDetails, lView), null /* LView */); } } @@ -332,6 +335,26 @@ function setTDeferBlockDetails( tView.data[slotIndex] = deferBlockConfig; } +function getTemplateIndexForState( + newState: DeferBlockState, hostLView: LView, tNode: TNode): number|null { + const tView = hostLView[TVIEW]; + const tDetails = getTDeferBlockDetails(tView, tNode); + + switch (newState) { + case DeferBlockState.Complete: + return tDetails.primaryTmplIndex; + case DeferBlockState.Loading: + return tDetails.loadingTmplIndex; + case DeferBlockState.Error: + return tDetails.errorTmplIndex; + case DeferBlockState.Placeholder: + return tDetails.placeholderTmplIndex; + default: + ngDevMode && throwError(`Unexpected defer block state: ${newState}`); + return null; + } +} + /** * Transitions a defer block to the new state. Updates the necessary * data structures and renders corresponding block. @@ -339,11 +362,9 @@ function setTDeferBlockDetails( * @param newState New state that should be applied to the defer block. * @param tNode TNode that represents a defer block. * @param lContainer Represents an instance of a defer block. - * @param stateTmplIndex Index of a template that should be rendered. */ -function renderDeferBlockState( - newState: DeferBlockInstanceState, tNode: TNode, lContainer: LContainer, - stateTmplIndex: number|null): void { +export function renderDeferBlockState( + newState: DeferBlockState, tNode: TNode, lContainer: LContainer): void { const hostLView = lContainer[PARENT]; // Check if this view is not destroyed. Since the loading process was async, @@ -357,6 +378,7 @@ function renderDeferBlockState( ngDevMode && assertDefined(lDetails, 'Expected a defer block state defined'); + const stateTmplIndex = getTemplateIndexForState(newState, hostLView, tNode); // Note: we transition to the next state if the previous state was represented // with a number that is less than the next state. For example, if the current // state is "loading" (represented as `2`), we should not show a placeholder @@ -372,7 +394,6 @@ function renderDeferBlockState( const viewIndex = 0; removeLViewFromLContainer(lContainer, viewIndex); - const dehydratedView = findMatchingDehydratedView(lContainer, tNode.tView!.ssrId); const embeddedLView = createAndRenderEmbeddedLView(hostLView, tNode, null, {dehydratedView}); addLViewToLContainer( @@ -380,19 +401,30 @@ function renderDeferBlockState( } } +/** + * Trigger prefetching of dependencies for a defer block. + * + * @param tDetails Static information about this defer block. + * @param lView LView of a host view. + */ +export function triggerPrefetching(tDetails: TDeferBlockDetails, lView: LView) { + if (lView[INJECTOR] && shouldTriggerDeferBlock(lView[INJECTOR]!)) { + triggerResourceLoading(tDetails, lView); + } +} + /** * Trigger loading of defer block dependencies if the process hasn't started yet. * * @param tDetails Static information about this defer block. - * @param tView TView of a host view. * @param lView LView of a host view. */ -function triggerResourceLoading(tDetails: TDeferBlockDetails, tView: TView, lView: LView) { +export function triggerResourceLoading(tDetails: TDeferBlockDetails, lView: LView) { const injector = lView[INJECTOR]!; + const tView = lView[TVIEW]; - if (!shouldTriggerDeferBlock(injector) || - (tDetails.loadingState !== DeferDependenciesLoadingState.NOT_STARTED && - tDetails.loadingState !== DeferDependenciesLoadingState.SCHEDULED)) { + if (tDetails.loadingState !== DeferDependenciesLoadingState.NOT_STARTED && + tDetails.loadingState !== DeferDependenciesLoadingState.SCHEDULED) { // If the loading status is different from initial one, it means that // the loading of dependencies is in progress and there is nothing to do // in this function. All details can be obtained from the `tDetails` object. @@ -477,8 +509,7 @@ function renderPlaceholder(lView: LView, tNode: TNode) { ngDevMode && assertLContainer(lContainer); const tDetails = getTDeferBlockDetails(tView, tNode); - renderDeferBlockState( - DeferBlockInstanceState.PLACEHOLDER, tNode, lContainer, tDetails.placeholderTmplIndex); + renderDeferBlockState(DeferBlockState.Placeholder, tNode, lContainer); } /** @@ -499,12 +530,10 @@ function renderDeferStateAfterResourceLoading( ngDevMode && assertDeferredDependenciesLoaded(tDetails); // Everything is loaded, show the primary block content - renderDeferBlockState( - DeferBlockInstanceState.COMPLETE, tNode, lContainer, tDetails.primaryTmplIndex); + renderDeferBlockState(DeferBlockState.Complete, tNode, lContainer); } else if (tDetails.loadingState === DeferDependenciesLoadingState.FAILED) { - renderDeferBlockState( - DeferBlockInstanceState.ERROR, tNode, lContainer, tDetails.errorTmplIndex); + renderDeferBlockState(DeferBlockState.Error, tNode, lContainer); } }); } @@ -532,13 +561,12 @@ function triggerDeferBlock(lView: LView, tNode: TNode) { // Condition is triggered, try to render loading state and start downloading. // Note: if a block is in a loading, completed or an error state, this call would be a noop. - renderDeferBlockState( - DeferBlockInstanceState.LOADING, tNode, lContainer, tDetails.loadingTmplIndex); + renderDeferBlockState(DeferBlockState.Loading, tNode, lContainer); switch (tDetails.loadingState) { case DeferDependenciesLoadingState.NOT_STARTED: case DeferDependenciesLoadingState.SCHEDULED: - triggerResourceLoading(tDetails, lView[TVIEW], lView); + triggerResourceLoading(tDetails, lView); // The `loadingState` might have changed to "loading". if ((tDetails.loadingState as DeferDependenciesLoadingState) === @@ -551,12 +579,10 @@ function triggerDeferBlock(lView: LView, tNode: TNode) { break; case DeferDependenciesLoadingState.COMPLETE: ngDevMode && assertDeferredDependenciesLoaded(tDetails); - renderDeferBlockState( - DeferBlockInstanceState.COMPLETE, tNode, lContainer, tDetails.primaryTmplIndex); + renderDeferBlockState(DeferBlockState.Complete, tNode, lContainer); break; case DeferDependenciesLoadingState.FAILED: - renderDeferBlockState( - DeferBlockInstanceState.ERROR, tNode, lContainer, tDetails.errorTmplIndex); + renderDeferBlockState(DeferBlockState.Error, tNode, lContainer); break; default: if (ngDevMode) { @@ -605,3 +631,65 @@ export interface DeferBlockDependencyInterceptor { export const DEFER_BLOCK_DEPENDENCY_INTERCEPTOR = new InjectionToken( ngDevMode ? 'DEFER_BLOCK_DEPENDENCY_INTERCEPTOR' : ''); + +/** + * Determines if a given value matches the expected structure of a defer block + * + * We can safely rely on the primaryTmplIndex because every defer block requires + * that a primary template exists. All the other template options are optional. + */ +function isTDeferBlockDetails(value: unknown): value is TDeferBlockDetails { + return (typeof value === 'object') && + (typeof (value as TDeferBlockDetails).primaryTmplIndex === 'number'); +} + +/** + * Internal token used for configuring defer block behavior. + */ +export const DEFER_BLOCK_CONFIG = + new InjectionToken(ngDevMode ? 'DEFER_BLOCK_CONFIG' : ''); + +/** + * Defer block instance for testing. + */ +export interface DeferBlockDetails { + lContainer: LContainer; + lView: LView; + tNode: TNode; + tDetails: TDeferBlockDetails; +} + +/** + * Retrieves all defer blocks in a given LView. + * + * @param lView lView with defer blocks + * @param deferBlocks defer block aggregator array + */ +export function getDeferBlocks(lView: LView, deferBlocks: DeferBlockDetails[]) { + const tView = lView[TVIEW]; + for (let i = HEADER_OFFSET; i < tView.bindingStartIndex; i++) { + if (isLContainer(lView[i])) { + const lContainer = lView[i]; + // An LContainer may represent an instance of a defer block, in which case + // we store it as a result. Otherwise, keep iterating over LContainer views and + // look for defer blocks. + const isLast = i === tView.bindingStartIndex - 1; + if (!isLast) { + const tNode = tView.data[i] as TNode; + const tDetails = getTDeferBlockDetails(tView, tNode); + if (isTDeferBlockDetails(tDetails)) { + deferBlocks.push({lContainer, lView, tNode, tDetails}); + // This LContainer represents a defer block, so we exit + // this iteration and don't inspect views in this LContainer. + continue; + } + } + for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) { + getDeferBlocks(lContainer[i] as LView, deferBlocks); + } + } else if (isLView(lView[i])) { + // This is a component, enter the `getDeferBlocks` recursively. + getDeferBlocks(lView[i], deferBlocks); + } + } +} diff --git a/packages/core/src/render3/interfaces/defer.ts b/packages/core/src/render3/interfaces/defer.ts index 1548d7a19dd..aca68a18256 100644 --- a/packages/core/src/render3/interfaces/defer.ts +++ b/packages/core/src/render3/interfaces/defer.ts @@ -17,7 +17,7 @@ export type DependencyResolverFn = () => Array>; /** * Describes the state of defer block dependency loading. */ -export const enum DeferDependenciesLoadingState { +export enum DeferDependenciesLoadingState { /** Initial state, dependency loading is not yet triggered */ NOT_STARTED, @@ -100,24 +100,39 @@ export interface TDeferBlockDetails { /** * Describes the current state of this {#defer} block instance. + * + * @publicApi + * @developerPreview */ -export const enum DeferBlockInstanceState { - /** Initial state, nothing is rendered yet */ - INITIAL, - +export enum DeferBlockState { /** The {:placeholder} block content is rendered */ - PLACEHOLDER, + Placeholder = 0, /** The {:loading} block content is rendered */ - LOADING, + Loading = 1, /** The main content block content is rendered */ - COMPLETE, + Complete = 2, /** The {:error} block content is rendered */ - ERROR + Error = 3, } +/** + * Describes the initial state of this {#defer} block instance. + * + * Note: this state is internal only and *must* be represented + * with a number lower than any value in the `DeferBlockState` enum. + */ +export enum DeferBlockInternalState { + /** Initial state. Nothing is rendered yet. */ + Initial = -1, +} + +/** + * A slot in the `LDeferBlockDetails` array that contains a number + * that represent a current block state that is being rendered. + */ export const DEFER_BLOCK_STATE = 0; /** @@ -128,5 +143,30 @@ export const DEFER_BLOCK_STATE = 0; * (which would require per-instance state). */ export interface LDeferBlockDetails extends Array { - [DEFER_BLOCK_STATE]: DeferBlockInstanceState; + [DEFER_BLOCK_STATE]: DeferBlockState|DeferBlockInternalState; +} + +/** + * Internal structure used for configuration of defer block behavior. + * */ +export interface DeferBlockConfig { + behavior: DeferBlockBehavior; +} + +/** + * Options for configuring defer blocks behavior. + * @publicApi + * @developerPreview + */ +export enum DeferBlockBehavior { + /** + * Manual triggering mode for defer blocks. Provides control over when defer blocks render + * and which state they render. This is the default behavior in test environments. + */ + Manual, + + /** + * Playthrough mode for defer blocks. This mode behaves like defer blocks would in a browser. + */ + Playthrough, } diff --git a/packages/core/test/acceptance/defer_spec.ts b/packages/core/test/acceptance/defer_spec.ts index fa7b3b673b5..797e3b3fcad 100644 --- a/packages/core/test/acceptance/defer_spec.ts +++ b/packages/core/test/acceptance/defer_spec.ts @@ -10,7 +10,7 @@ import {ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common'; import {ɵsetEnabledBlockTypes as setEnabledBlockTypes} from '@angular/compiler/src/jit_compiler_facade'; import {Component, Input, PLATFORM_ID, QueryList, Type, ViewChildren, ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR} from '@angular/core'; import {getComponentDef} from '@angular/core/src/render3/definition'; -import {TestBed} from '@angular/core/testing'; +import {DeferBlockBehavior, TestBed} from '@angular/core/testing'; /** * Clears all associated directive defs from a given component class. @@ -53,7 +53,8 @@ describe('#defer', () => { afterEach(() => setEnabledBlockTypes([])); beforeEach(() => { - TestBed.configureTestingModule({providers: COMMON_PROVIDERS}); + TestBed.configureTestingModule( + {providers: COMMON_PROVIDERS, deferBlockBehavior: DeferBlockBehavior.Playthrough}); }); it('should transition between placeholder, loading and loaded states', async () => { @@ -243,7 +244,8 @@ describe('#defer', () => { TestBed.configureTestingModule({ providers: [ {provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor}, - ] + ], + deferBlockBehavior: DeferBlockBehavior.Playthrough, }); const fixture = TestBed.createComponent(MyCmp); @@ -567,7 +569,8 @@ describe('#defer', () => { TestBed.configureTestingModule({ providers: [ {provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor}, - ] + ], + deferBlockBehavior: DeferBlockBehavior.Playthrough, }); clearDirectiveDefs(RootCmp); @@ -651,7 +654,8 @@ describe('#defer', () => { TestBed.configureTestingModule({ providers: [ {provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor}, - ] + ], + deferBlockBehavior: DeferBlockBehavior.Playthrough, }); clearDirectiveDefs(RootCmp); @@ -731,7 +735,8 @@ describe('#defer', () => { TestBed.configureTestingModule({ providers: [ {provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor}, - ] + ], + deferBlockBehavior: DeferBlockBehavior.Playthrough, }); clearDirectiveDefs(RootCmp); @@ -799,7 +804,8 @@ describe('#defer', () => { TestBed.configureTestingModule({ providers: [ {provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor}, - ] + ], + deferBlockBehavior: DeferBlockBehavior.Playthrough, }); clearDirectiveDefs(RootCmp); @@ -883,7 +889,8 @@ describe('#defer', () => { TestBed.configureTestingModule({ providers: [ {provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor}, - ] + ], + deferBlockBehavior: DeferBlockBehavior.Playthrough, }); clearDirectiveDefs(RootCmp); @@ -967,7 +974,8 @@ describe('#defer', () => { TestBed.configureTestingModule({ providers: [ {provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor}, - ] + ], + deferBlockBehavior: DeferBlockBehavior.Playthrough, }); clearDirectiveDefs(RootCmp); diff --git a/packages/core/test/component_fixture_spec.ts b/packages/core/test/component_fixture_spec.ts index 41b398b6d12..32e22622055 100644 --- a/packages/core/test/component_fixture_spec.ts +++ b/packages/core/test/component_fixture_spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {ɵsetEnabledBlockTypes as setEnabledBlockTypes} from '@angular/compiler/src/jit_compiler_facade'; import {Component, Injectable, Input} from '@angular/core'; import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone, TestBed, waitForAsync, withModule} from '@angular/core/testing'; import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; @@ -20,6 +21,22 @@ class SimpleComp { } } +@Component({ + selector: 'deferred-comp', + standalone: true, + template: `
Deferred Component
`, +}) +class DeferredComp { +} + +@Component({ + selector: 'second-deferred-comp', + standalone: true, + template: `
More Deferred Component
`, +}) +class SecondDeferredComp { +} + @Component({ selector: 'my-if-comp', template: `MyIf(More)`, @@ -100,6 +117,9 @@ class NestedAsyncTimeoutComp { { describe('ComponentFixture', () => { + beforeEach(() => setEnabledBlockTypes(['defer'])); + afterEach(() => setEnabledBlockTypes([])); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ @@ -295,6 +315,30 @@ class NestedAsyncTimeoutComp { }); })); + describe('defer', () => { + it('should return all defer blocks in the component', async () => { + @Component({ + selector: 'defer-comp', + standalone: true, + imports: [DeferredComp, SecondDeferredComp], + template: `
+ {#defer on immediate} + + {/defer} + {#defer on idle} + + {/defer} +
` + }) + class DeferComp { + } + + const componentFixture = TestBed.createComponent(DeferComp); + const deferBlocks = await componentFixture.getDeferBlocks(); + expect(deferBlocks.length).toBe(2); + }); + }); + describe('No NgZone', () => { beforeEach(() => { TestBed.configureTestingModule( diff --git a/packages/core/test/defer_fixture_spec.ts b/packages/core/test/defer_fixture_spec.ts new file mode 100644 index 00000000000..f63abea6b7c --- /dev/null +++ b/packages/core/test/defer_fixture_spec.ts @@ -0,0 +1,369 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common'; +import {ɵsetEnabledBlockTypes as setEnabledBlockTypes} from '@angular/compiler/src/jit_compiler_facade'; +import {Component, PLATFORM_ID} from '@angular/core'; +import {DeferBlockBehavior, DeferBlockState, TestBed} from '@angular/core/testing'; +import {expect} from '@angular/platform-browser/testing/src/matchers'; + +@Component({ + selector: 'second-deferred-comp', + standalone: true, + template: `
More Deferred Component
`, +}) +class SecondDeferredComp { +} + +const COMMON_PROVIDERS = [{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID}]; + + +describe('DeferFixture', () => { + beforeEach(() => setEnabledBlockTypes(['defer'])); + afterEach(() => setEnabledBlockTypes([])); + + it('should start in manual behavior mode', async () => { + @Component({ + selector: 'defer-comp', + standalone: true, + imports: [SecondDeferredComp], + template: `
+ {#defer on immediate} + + {/defer} +
` + }) + class DeferComp { + } + + TestBed.configureTestingModule({ + imports: [ + DeferComp, + SecondDeferredComp, + ], + providers: COMMON_PROVIDERS, + deferBlockBehavior: DeferBlockBehavior.Manual, + teardown: {destroyAfterEach: true}, + }); + + const componentFixture = TestBed.createComponent(DeferComp); + const el = componentFixture.nativeElement as HTMLElement; + expect(el.querySelectorAll('.more').length).toBe(0); + }); + + it('should start in manual trigger mode by default', async () => { + @Component({ + selector: 'defer-comp', + standalone: true, + imports: [SecondDeferredComp], + template: `
+ {#defer on immediate} + + {/defer} +
` + }) + class DeferComp { + } + + TestBed.configureTestingModule({ + imports: [ + DeferComp, + SecondDeferredComp, + ], + providers: COMMON_PROVIDERS, + }); + + const componentFixture = TestBed.createComponent(DeferComp); + const el = componentFixture.nativeElement as HTMLElement; + expect(el.querySelectorAll('.more').length).toBe(0); + }); + + it('should defer load immediately on playthrough', async () => { + @Component({ + selector: 'defer-comp', + standalone: true, + imports: [SecondDeferredComp], + template: `
+ {#defer when shouldLoad} + + {/defer} +
` + }) + class DeferComp { + shouldLoad = false; + } + + TestBed.configureTestingModule({ + imports: [ + DeferComp, + SecondDeferredComp, + ], + providers: COMMON_PROVIDERS, + deferBlockBehavior: DeferBlockBehavior.Playthrough, + }); + + const componentFixture = TestBed.createComponent(DeferComp); + const el = componentFixture.nativeElement as HTMLElement; + expect(el.querySelectorAll('.more').length).toBe(0); + + componentFixture.componentInstance.shouldLoad = true; + componentFixture.detectChanges(); + + await componentFixture.whenStable(); // await loading of deps + + expect(el.querySelector('.more')).toBeDefined(); + }); + + it('should not defer load immediately when set to manual', async () => { + @Component({ + selector: 'defer-comp', + standalone: true, + imports: [SecondDeferredComp], + template: `
+ {#defer when shouldLoad} + + {/defer} +
` + }) + class DeferComp { + shouldLoad = false; + } + + TestBed.configureTestingModule({ + imports: [ + DeferComp, + SecondDeferredComp, + ], + providers: COMMON_PROVIDERS, + }); + + const componentFixture = TestBed.createComponent(DeferComp); + const el = componentFixture.nativeElement as HTMLElement; + expect(el.querySelectorAll('.more').length).toBe(0); + + componentFixture.componentInstance.shouldLoad = true; + componentFixture.detectChanges(); + + await componentFixture.whenStable(); // await loading of deps + + expect(el.querySelectorAll('.more').length).toBe(0); + }); + + it('should render a completed defer state', async () => { + @Component({ + selector: 'defer-comp', + standalone: true, + imports: [SecondDeferredComp], + template: `
+ {#defer on immediate} + + {/defer} +
` + }) + class DeferComp { + } + + TestBed.configureTestingModule({ + imports: [ + DeferComp, + SecondDeferredComp, + ], + providers: COMMON_PROVIDERS, + deferBlockBehavior: DeferBlockBehavior.Manual, + }); + + const componentFixture = TestBed.createComponent(DeferComp); + const deferBlock = (await componentFixture.getDeferBlocks())[0]; + const el = componentFixture.nativeElement as HTMLElement; + await deferBlock.render(DeferBlockState.Complete); + expect(el.querySelector('.more')).toBeDefined(); + }); + + it('should render a placeholder defer state', async () => { + @Component({ + selector: 'defer-comp', + standalone: true, + imports: [SecondDeferredComp], + template: `
+ {#defer on immediate} + + {:placeholder} + This is placeholder content + {/defer} +
` + }) + class DeferComp { + } + + TestBed.configureTestingModule({ + imports: [ + DeferComp, + SecondDeferredComp, + ], + providers: COMMON_PROVIDERS, + deferBlockBehavior: DeferBlockBehavior.Manual, + }); + + const componentFixture = TestBed.createComponent(DeferComp); + const deferBlock = (await componentFixture.getDeferBlocks())[0]; + const el = componentFixture.nativeElement as HTMLElement; + await deferBlock.render(DeferBlockState.Placeholder); + expect(el.querySelectorAll('.more').length).toBe(0); + const phContent = el.querySelector('.ph'); + expect(phContent).toBeDefined(); + expect(phContent?.innerHTML).toBe('This is placeholder content'); + }); + + it('should render a loading defer state', async () => { + @Component({ + selector: 'defer-comp', + standalone: true, + imports: [SecondDeferredComp], + template: `
+ {#defer on immediate} + + {:loading} + Loading... + {/defer} +
` + }) + class DeferComp { + } + + TestBed.configureTestingModule({ + imports: [ + DeferComp, + SecondDeferredComp, + ], + providers: COMMON_PROVIDERS, + deferBlockBehavior: DeferBlockBehavior.Manual, + }); + + const componentFixture = TestBed.createComponent(DeferComp); + const deferBlock = (await componentFixture.getDeferBlocks())[0]; + const el = componentFixture.nativeElement as HTMLElement; + await deferBlock.render(DeferBlockState.Loading); + expect(el.querySelectorAll('.more').length).toBe(0); + const loadingContent = el.querySelector('.loading'); + expect(loadingContent).toBeDefined(); + expect(loadingContent?.innerHTML).toBe('Loading...'); + }); + + it('should render an error defer state', async () => { + @Component({ + selector: 'defer-comp', + standalone: true, + imports: [SecondDeferredComp], + template: `
+ {#defer on immediate} + + {:error} + Flagrant Error! + {/defer} +
` + }) + class DeferComp { + } + + TestBed.configureTestingModule({ + imports: [ + DeferComp, + SecondDeferredComp, + ], + providers: COMMON_PROVIDERS, + deferBlockBehavior: DeferBlockBehavior.Manual, + }); + + const componentFixture = TestBed.createComponent(DeferComp); + const deferBlock = (await componentFixture.getDeferBlocks())[0]; + const el = componentFixture.nativeElement as HTMLElement; + await deferBlock.render(DeferBlockState.Error); + expect(el.querySelectorAll('.more').length).toBe(0); + const errContent = el.querySelector('.error'); + expect(errContent).toBeDefined(); + expect(errContent?.innerHTML).toBe('Flagrant Error!'); + }); + + it('should throw when rendering a template that does not exist', async () => { + @Component({ + selector: 'defer-comp', + standalone: true, + imports: [SecondDeferredComp], + template: `
+ {#defer on immediate} + + {/defer} +
` + }) + class DeferComp { + } + + TestBed.configureTestingModule({ + imports: [ + DeferComp, + SecondDeferredComp, + ], + providers: COMMON_PROVIDERS, + deferBlockBehavior: DeferBlockBehavior.Manual, + }); + + const componentFixture = TestBed.createComponent(DeferComp); + const deferBlock = (await componentFixture.getDeferBlocks())[0]; + try { + await deferBlock.render(DeferBlockState.Placeholder); + } catch (er: any) { + expect(er.message) + .toBe( + 'Tried to render this defer block in the `Placeholder` state, but' + + ' there was no `{:placeholder}` section defined in a template.'); + } + }); + + it('should get child defer blocks', async () => { + @Component({ + selector: 'deferred-comp', + standalone: true, + imports: [SecondDeferredComp], + template: `
+ {#defer on immediate} + + {/defer} +
`, + }) + class DeferredComp { + } + + @Component({ + selector: 'defer-comp', + standalone: true, + imports: [DeferredComp], + template: `
+ {#defer on immediate} + + {/defer} +
` + }) + class DeferComp { + } + + TestBed.configureTestingModule({ + imports: [ + DeferComp, + DeferredComp, + SecondDeferredComp, + ], + providers: COMMON_PROVIDERS + }); + + const componentFixture = TestBed.createComponent(DeferComp); + const deferBlock = (await componentFixture.getDeferBlocks())[0]; + await deferBlock.render(DeferBlockState.Complete); + const fixtures = await deferBlock.getDeferBlocks(); + expect(fixtures.length).toBe(1); + }); +}); diff --git a/packages/core/test/test_bed_spec.ts b/packages/core/test/test_bed_spec.ts index 3a46e580785..c5a1bf53821 100644 --- a/packages/core/test/test_bed_spec.ts +++ b/packages/core/test/test_bed_spec.ts @@ -7,6 +7,7 @@ */ import {APP_INITIALIZER, ChangeDetectorRef, Compiler, Component, Directive, ElementRef, ErrorHandler, getNgModuleById, inject, Inject, Injectable, InjectFlags, InjectionToken, InjectOptions, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, Optional, Pipe, Type, ViewChild, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineInjector as defineInjector, ɵɵdefineNgModule as defineNgModule, ɵɵelementEnd as elementEnd, ɵɵelementStart as elementStart, ɵɵsetNgModuleScope as setNgModuleScope, ɵɵtext as text} from '@angular/core'; +import {DeferBlockBehavior} from '@angular/core/testing'; import {TestBed, TestBedImpl} from '@angular/core/testing/src/test_bed'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -2031,6 +2032,27 @@ describe('TestBed', () => { }); }); +describe('TestBed defer block behavior', () => { + beforeEach(() => { + TestBed.resetTestingModule(); + }); + + it('should default defer block behavior to manual', () => { + expect(TestBedImpl.INSTANCE.getDeferBlockBehavior()).toBe(DeferBlockBehavior.Manual); + }); + + it('should be able to configure defer block behavior', () => { + TestBed.configureTestingModule({deferBlockBehavior: DeferBlockBehavior.Playthrough}); + expect(TestBedImpl.INSTANCE.getDeferBlockBehavior()).toBe(DeferBlockBehavior.Playthrough); + }); + + it('should reset the defer block behavior back to the default when TestBed is reset', () => { + TestBed.configureTestingModule({deferBlockBehavior: DeferBlockBehavior.Playthrough}); + expect(TestBedImpl.INSTANCE.getDeferBlockBehavior()).toBe(DeferBlockBehavior.Playthrough); + TestBed.resetTestingModule(); + expect(TestBedImpl.INSTANCE.getDeferBlockBehavior()).toBe(DeferBlockBehavior.Manual); + }); +}); describe('TestBed module teardown', () => { beforeEach(() => { diff --git a/packages/core/testing/src/component_fixture.ts b/packages/core/testing/src/component_fixture.ts index c58a59bd911..25d41920582 100644 --- a/packages/core/testing/src/component_fixture.ts +++ b/packages/core/testing/src/component_fixture.ts @@ -6,9 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {ChangeDetectorRef, ComponentRef, DebugElement, ElementRef, getDebugNode, NgZone, RendererFactory2, ɵFlushableEffectRunner as FlushableEffectRunner} from '@angular/core'; +import {ChangeDetectorRef, ComponentRef, DebugElement, ElementRef, getDebugNode, NgZone, RendererFactory2, ɵDeferBlockDetails as DeferBlockDetails, ɵFlushableEffectRunner as FlushableEffectRunner, ɵgetDeferBlocks as getDeferBlocks} from '@angular/core'; import {Subscription} from 'rxjs'; +import {DeferBlockFixture} from './defer'; + /** * Fixture for debugging and testing a component. @@ -51,6 +53,7 @@ export class ComponentFixture { private _onMicrotaskEmptySubscription: Subscription|null = null; private _onErrorSubscription: Subscription|null = null; + /** @nodoc */ constructor( public componentRef: ComponentRef, public ngZone: NgZone|null, private effectRunner: FlushableEffectRunner|null, private _autoDetect: boolean) { @@ -181,6 +184,22 @@ export class ComponentFixture { } } + /** + * Retrieves all defer block fixtures in the component fixture + */ + getDeferBlocks(): Promise { + const deferBlocks: DeferBlockDetails[] = []; + const lView = (this.componentRef.hostView as any)['_lView']; + getDeferBlocks(lView, deferBlocks); + + const deferBlockFixtures = []; + for (const block of deferBlocks) { + deferBlockFixtures.push(new DeferBlockFixture(block, this)); + } + + return Promise.resolve(deferBlockFixtures); + } + private _getRenderer() { if (this._renderer === undefined) { diff --git a/packages/core/testing/src/defer.ts b/packages/core/testing/src/defer.ts new file mode 100644 index 00000000000..88f16b01cee --- /dev/null +++ b/packages/core/testing/src/defer.ts @@ -0,0 +1,90 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ɵCONTAINER_HEADER_OFFSET as CONTAINER_HEADER_OFFSET, ɵDeferBlockDetails as DeferBlockDetails, ɵDeferBlockState as DeferBlockState, ɵgetDeferBlocks as getDeferBlocks, ɵrenderDeferBlockState as renderDeferBlockState, ɵtriggerResourceLoading as triggerResourceLoading} from '@angular/core'; + +import type {ComponentFixture} from './component_fixture'; + +/** + * Represents an individual `{#defer}` block for testing purposes. + * + * @publicApi + * @developerPreview + */ +export class DeferBlockFixture { + /** @nodoc */ + constructor( + private block: DeferBlockDetails, private componentFixture: ComponentFixture) {} + + /** + * Renders the specified state of the defer fixture. + * @param state the defer state to render + */ + async render(state: DeferBlockState): Promise { + if (!hasStateTemplate(state, this.block)) { + const stateAsString = getDeferBlockStateNameFromEnum(state); + throw new Error( + `Tried to render this defer block in the \`${stateAsString}\` state, ` + + `but there was no \`{:${stateAsString.toLowerCase()}}\` section defined in a template.`); + } + if (state === DeferBlockState.Complete) { + await triggerResourceLoading(this.block.tDetails, this.block.lView); + } + renderDeferBlockState(state, this.block.tNode, this.block.lContainer); + this.componentFixture.detectChanges(); + return this.componentFixture.whenStable(); + } + + /** + * Retrieves all nested child defer block fixtures + * in a given defer block. + */ + getDeferBlocks(): Promise { + const deferBlocks: DeferBlockDetails[] = []; + // An LContainer that represents a defer block has at most 1 view, which is + // located right after an LContainer header. Get a hold of that view and inspect + // it for nested defer blocks. + const deferBlockFixtures = []; + if (this.block.lContainer.length >= CONTAINER_HEADER_OFFSET) { + const lView = this.block.lContainer[CONTAINER_HEADER_OFFSET]; + getDeferBlocks(lView, deferBlocks); + for (const block of deferBlocks) { + deferBlockFixtures.push(new DeferBlockFixture(block, this.componentFixture)); + } + } + return Promise.resolve(deferBlockFixtures); + } +} + +function hasStateTemplate(state: DeferBlockState, block: DeferBlockDetails) { + switch (state) { + case DeferBlockState.Placeholder: + return block.tDetails.placeholderTmplIndex !== null; + case DeferBlockState.Loading: + return block.tDetails.loadingTmplIndex !== null; + case DeferBlockState.Error: + return block.tDetails.errorTmplIndex !== null; + case DeferBlockState.Complete: + return true; + default: + return false; + } +} + +function getDeferBlockStateNameFromEnum(state: DeferBlockState) { + switch (state) { + case DeferBlockState.Placeholder: + return 'Placeholder'; + case DeferBlockState.Loading: + return 'Loading'; + case DeferBlockState.Error: + return 'Error'; + default: + return 'Main'; + } +} diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts index b05bcc2e075..932fde35663 100644 --- a/packages/core/testing/src/test_bed.ts +++ b/packages/core/testing/src/test_bed.ts @@ -16,7 +16,6 @@ import { Directive, EnvironmentInjector, InjectFlags, - InjectionToken, InjectOptions, Injector, NgModule, @@ -26,6 +25,7 @@ import { ProviderToken, Type, ɵconvertToBitFlags as convertToBitFlags, + ɵDeferBlockBehavior as DeferBlockBehavior, ɵflushModuleScopingQueueAsMuchAsPossible as flushModuleScopingQueueAsMuchAsPossible, ɵgetAsyncClassMetadata as getAsyncClassMetadata, ɵgetUnknownElementStrictMode as getUnknownElementStrictMode, @@ -200,6 +200,12 @@ export class TestBedImpl implements TestBed { */ private _instanceTeardownOptions: ModuleTeardownOptions|undefined; + /** + * Defer block behavior option that specifies whether defer blocks will be triggered manually + * or set to play through. + */ + private _instanceDeferBlockBehavior = DeferBlockBehavior.Manual; + /** * "Error on unknown elements" option that has been configured at the `TestBed` instance level. * This option takes precedence over the environment-level one. @@ -474,6 +480,7 @@ export class TestBedImpl implements TestBed { this._instanceTeardownOptions = undefined; this._instanceErrorOnUnknownElementsOption = undefined; this._instanceErrorOnUnknownPropertiesOption = undefined; + this._instanceDeferBlockBehavior = DeferBlockBehavior.Manual; } } return this; @@ -504,6 +511,7 @@ export class TestBedImpl implements TestBed { this._instanceTeardownOptions = moduleDef.teardown; this._instanceErrorOnUnknownElementsOption = moduleDef.errorOnUnknownElements; this._instanceErrorOnUnknownPropertiesOption = moduleDef.errorOnUnknownProperties; + this._instanceDeferBlockBehavior = moduleDef.deferBlockBehavior ?? DeferBlockBehavior.Manual; // Store the current value of the strict mode option, // so we can restore it later this._previousErrorOnUnknownElementsOption = getUnknownElementStrictMode(); @@ -741,6 +749,10 @@ export class TestBedImpl implements TestBed { TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT; } + getDeferBlockBehavior(): DeferBlockBehavior { + return this._instanceDeferBlockBehavior; + } + tearDownTestingModule() { // If the module ref has already been destroyed, we won't be able to get a test renderer. if (this._testModuleRef === null) { diff --git a/packages/core/testing/src/test_bed_common.ts b/packages/core/testing/src/test_bed_common.ts index 9202d883a83..27852534de4 100644 --- a/packages/core/testing/src/test_bed_common.ts +++ b/packages/core/testing/src/test_bed_common.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {InjectionToken, SchemaMetadata} from '@angular/core'; +import {InjectionToken, SchemaMetadata, ɵDeferBlockBehavior as DeferBlockBehavior} from '@angular/core'; /** Whether test modules should be torn down by default. */ @@ -61,6 +61,12 @@ export interface TestModuleMetadata { * @see [NG8002](/errors/NG8002) for the description of the error and how to fix it */ errorOnUnknownProperties?: boolean; + + /** + * Whether defer blocks should behave with manual triggering or play through normally. + * Defaults to `manual`. + */ + deferBlockBehavior?: DeferBlockBehavior; } /** diff --git a/packages/core/testing/src/test_bed_compiler.ts b/packages/core/testing/src/test_bed_compiler.ts index 339c36ec10b..2822bec89e2 100644 --- a/packages/core/testing/src/test_bed_compiler.ts +++ b/packages/core/testing/src/test_bed_compiler.ts @@ -7,7 +7,7 @@ */ import {ResourceLoader} from '@angular/compiler'; -import {ApplicationInitStatus, Compiler, COMPILER_OPTIONS, Component, Directive, Injector, InjectorType, LOCALE_ID, ModuleWithComponentFactories, ModuleWithProviders, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, provideZoneChangeDetection, resolveForwardRef, StaticProvider, Type, ɵclearResolutionOfComponentResourcesQueue, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵdepsTracker as depsTracker, ɵDirectiveDef as DirectiveDef, ɵgenerateStandaloneInDeclarationsError, ɵgetAsyncClassMetadata as getAsyncClassMetadata, ɵgetInjectableDef as getInjectableDef, ɵInternalEnvironmentProviders as InternalEnvironmentProviders, ɵisComponentDefPendingResolution, ɵisEnvironmentProviders as isEnvironmentProviders, ɵNG_COMP_DEF as NG_COMP_DEF, ɵNG_DIR_DEF as NG_DIR_DEF, ɵNG_INJ_DEF as NG_INJ_DEF, ɵNG_MOD_DEF as NG_MOD_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵresolveComponentResources, ɵrestoreComponentResolutionQueue, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵUSE_RUNTIME_DEPS_TRACKER_FOR_JIT as USE_RUNTIME_DEPS_TRACKER_FOR_JIT, ɵɵInjectableDeclaration as InjectableDeclaration} from '@angular/core'; +import {ApplicationInitStatus, Compiler, COMPILER_OPTIONS, Component, Directive, Injector, InjectorType, LOCALE_ID, ModuleWithComponentFactories, ModuleWithProviders, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, provideZoneChangeDetection, resolveForwardRef, StaticProvider, Type, ɵclearResolutionOfComponentResourcesQueue, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵDEFER_BLOCK_CONFIG as DEFER_BLOCK_CONFIG, ɵDeferBlockBehavior as DeferBlockBehavior, ɵdepsTracker as depsTracker, ɵDirectiveDef as DirectiveDef, ɵgenerateStandaloneInDeclarationsError, ɵgetAsyncClassMetadata as getAsyncClassMetadata, ɵgetInjectableDef as getInjectableDef, ɵInternalEnvironmentProviders as InternalEnvironmentProviders, ɵisComponentDefPendingResolution, ɵisEnvironmentProviders as isEnvironmentProviders, ɵNG_COMP_DEF as NG_COMP_DEF, ɵNG_DIR_DEF as NG_DIR_DEF, ɵNG_INJ_DEF as NG_INJ_DEF, ɵNG_MOD_DEF as NG_MOD_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵresolveComponentResources, ɵrestoreComponentResolutionQueue, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵUSE_RUNTIME_DEPS_TRACKER_FOR_JIT as USE_RUNTIME_DEPS_TRACKER_FOR_JIT, ɵɵInjectableDeclaration as InjectableDeclaration} from '@angular/core'; import {ComponentDef, ComponentType} from '../../src/render3'; @@ -105,6 +105,8 @@ export class TestBedCompiler { private testModuleType: NgModuleType; private testModuleRef: NgModuleRef|null = null; + private deferBlockBehavior = DeferBlockBehavior.Manual; + constructor(private platform: PlatformRef, private additionalModuleTypes: Type|Type[]) { class DynamicTestModule {} this.testModuleType = DynamicTestModule as any; @@ -139,6 +141,8 @@ export class TestBedCompiler { if (moduleDef.schemas !== undefined) { this.schemas.push(...moduleDef.schemas); } + + this.deferBlockBehavior = moduleDef.deferBlockBehavior ?? DeferBlockBehavior.Manual; } overrideModule(ngModule: Type, override: MetadataOverride): void { @@ -803,6 +807,7 @@ export class TestBedCompiler { const providers = [ provideZoneChangeDetection(), {provide: Compiler, useFactory: () => new R3TestCompiler(this)}, + {provide: DEFER_BLOCK_CONFIG, useValue: {behavior: this.deferBlockBehavior}}, ...this.providers, ...this.providerOverrides, ]; diff --git a/packages/core/testing/src/testing.ts b/packages/core/testing/src/testing.ts index 2c4c655d762..6de700dc073 100644 --- a/packages/core/testing/src/testing.ts +++ b/packages/core/testing/src/testing.ts @@ -20,3 +20,5 @@ export {TestComponentRenderer, ComponentFixtureAutoDetect, ComponentFixtureNoNgZ export * from './test_hooks'; export * from './metadata_override'; export {MetadataOverrider as ɵMetadataOverrider} from './metadata_overrider'; +export {ɵDeferBlockBehavior as DeferBlockBehavior, ɵDeferBlockState as DeferBlockState} from '@angular/core'; +export {DeferBlockFixture} from './defer';