mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
5a0d6aac74
commit
06bbc2fc4e
16 changed files with 784 additions and 56 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
369
packages/core/test/defer_fixture_spec.ts
Normal file
369
packages/core/test/defer_fixture_spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
90
packages/core/testing/src/defer.ts
Normal file
90
packages/core/testing/src/defer.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue