refactor(core): Add defer block testing fixture (#51698)

This adds a fixture for being able to access and test defer blocks.

PR Close #51698
This commit is contained in:
Jessica Janiuk 2023-09-01 13:29:14 -04:00 committed by Andrew Kushnir
parent 5a0d6aac74
commit 06bbc2fc4e
16 changed files with 784 additions and 56 deletions

View file

@ -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<T> {
destroy(): void;
detectChanges(checkNoChanges?: boolean): void;
elementRef: ElementRef;
getDeferBlocks(): Promise<DeferBlockFixture[]>;
isStable(): boolean;
nativeElement: any;
// (undocumented)
@ -55,6 +59,17 @@ export const ComponentFixtureAutoDetect: InjectionToken<boolean>;
// @public (undocumented)
export const ComponentFixtureNoNgZone: InjectionToken<boolean>;
export { DeferBlockBehavior }
// @public
export class DeferBlockFixture {
constructor(block: ɵDeferBlockDetails, componentFixture: ComponentFixture<unknown>);
getDeferBlocks(): Promise<DeferBlockFixture[]>;
render(state: DeferBlockState): Promise<void>;
}
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)

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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<DeferBlockDependencyInterceptor>(
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<DeferBlockConfig>(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);
}
}
}

View file

@ -17,7 +17,7 @@ export type DependencyResolverFn = () => Array<Promise<DependencyType>>;
/**
* 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<unknown> {
[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,
}

View file

@ -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);

View file

@ -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: `<div>Deferred Component</div>`,
})
class DeferredComp {
}
@Component({
selector: 'second-deferred-comp',
standalone: true,
template: `<div>More Deferred Component</div>`,
})
class SecondDeferredComp {
}
@Component({
selector: 'my-if-comp',
template: `MyIf(<span *ngIf="showMore">More</span>)`,
@ -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: `<div>
{#defer on immediate}
<DeferredComp />
{/defer}
{#defer on idle}
<SecondDeferredComp />
{/defer}
</div>`
})
class DeferComp {
}
const componentFixture = TestBed.createComponent(DeferComp);
const deferBlocks = await componentFixture.getDeferBlocks();
expect(deferBlocks.length).toBe(2);
});
});
describe('No NgZone', () => {
beforeEach(() => {
TestBed.configureTestingModule(

View file

@ -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: `<div class="more">More Deferred Component</div>`,
})
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: `<div>
{#defer on immediate}
<second-deferred-comp />
{/defer}
</div>`
})
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: `<div>
{#defer on immediate}
<second-deferred-comp />
{/defer}
</div>`
})
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: `<div>
{#defer when shouldLoad}
<second-deferred-comp />
{/defer}
</div>`
})
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: `<div>
{#defer when shouldLoad}
<second-deferred-comp />
{/defer}
</div>`
})
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: `<div>
{#defer on immediate}
<second-deferred-comp />
{/defer}
</div>`
})
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: `<div>
{#defer on immediate}
<second-deferred-comp />
{:placeholder}
<span class="ph">This is placeholder content</span>
{/defer}
</div>`
})
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: `<div>
{#defer on immediate}
<second-deferred-comp />
{:loading}
<span class="loading">Loading...</span>
{/defer}
</div>`
})
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: `<div>
{#defer on immediate}
<second-deferred-comp />
{:error}
<span class="error">Flagrant Error!</span>
{/defer}
</div>`
})
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: `<div>
{#defer on immediate}
<second-deferred-comp />
{/defer}
</div>`
})
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: `<div>
{#defer on immediate}
<second-deferred-comp />
{/defer}
</div>`,
})
class DeferredComp {
}
@Component({
selector: 'defer-comp',
standalone: true,
imports: [DeferredComp],
template: `<div>
{#defer on immediate}
<deferred-comp />
{/defer}
</div>`
})
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);
});
});

View file

@ -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(() => {

View file

@ -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<T> {
private _onMicrotaskEmptySubscription: Subscription|null = null;
private _onErrorSubscription: Subscription|null = null;
/** @nodoc */
constructor(
public componentRef: ComponentRef<T>, public ngZone: NgZone|null,
private effectRunner: FlushableEffectRunner|null, private _autoDetect: boolean) {
@ -181,6 +184,22 @@ export class ComponentFixture<T> {
}
}
/**
* Retrieves all defer block fixtures in the component fixture
*/
getDeferBlocks(): Promise<DeferBlockFixture[]> {
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) {

View file

@ -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<unknown>) {}
/**
* Renders the specified state of the defer fixture.
* @param state the defer state to render
*/
async render(state: DeferBlockState): Promise<void> {
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<DeferBlockFixture[]> {
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';
}
}

View file

@ -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) {

View file

@ -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;
}
/**

View file

@ -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<any>;
private testModuleRef: NgModuleRef<any>|null = null;
private deferBlockBehavior = DeferBlockBehavior.Manual;
constructor(private platform: PlatformRef, private additionalModuleTypes: Type<any>|Type<any>[]) {
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<any>, override: MetadataOverride<NgModule>): 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,
];

View file

@ -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';