perf(core): avoid storing LView in __ngContext__ (#41358)

Currently we save a reference to an `LView` on most DOM nodes created by Angular either by saving
the `LView` directly in the `__ngContext__` or by saving the `LContext` which has a reference to
the `LView`. This can be a problem if the DOM node is retained in memory, because the `LView` has
references to all of the child nodes of the view, as well as other internal data structures.

Previously we tried to resolve the issue by clearing the `__ngContext__` when a node is removed
(see https://github.com/angular/angular/pull/36011), but we decided not to proceeed, because it can
slow down destruction due to a megamorphic write.

These changes aim to address the issue while reducing the performance impact by assigning a unique
ID when an `LView` is created and adding it to `__ngContext__`. All active views are tracked in
a map where their unique ID is used as the key. We don't need to worry about leaks within that map,
because `LView`s are an internal data structure and we have complete control over when they are
created and destroyed.

Fixes #41047.

PR Close #41358
This commit is contained in:
Kristiyan Kostadinov 2021-03-30 11:26:54 +02:00 committed by Jessica Janiuk
parent 6d9e3400e1
commit 18b33e79d3
37 changed files with 650 additions and 396 deletions

View File

@ -12,7 +12,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 3033, "runtime-es2015": 3033,
"main-es2015": 453111, "main-es2015": 453725,
"polyfills-es2015": 55230 "polyfills-es2015": 55230
} }
} }

View File

@ -3,7 +3,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 1170, "runtime-es2015": 1170,
"main-es2015": 138189, "main-es2015": 138718,
"polyfills-es2015": 36964 "polyfills-es2015": 36964
} }
} }
@ -30,7 +30,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 1190, "runtime-es2015": 1190,
"main-es2015": 136546, "main-es2015": 137087,
"polyfills-es2015": 37641 "polyfills-es2015": 37641
} }
} }
@ -39,7 +39,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 3354, "runtime-es2015": 3354,
"main-es2015": 295076, "main-es2015": 295755,
"polyfills-es2015": 36975, "polyfills-es2015": 36975,
"src_app_lazy_lazy_module_ts-es2015": 825 "src_app_lazy_lazy_module_ts-es2015": 825
} }

View File

@ -262,9 +262,10 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
} }
get name(): string { get name(): string {
const context = getLContext(this.nativeNode); const context = getLContext(this.nativeNode)!;
if (context !== null) { const lView = context ? context.lView : null;
const lView = context.lView;
if (lView !== null) {
const tData = lView[TVIEW].data; const tData = lView[TVIEW].data;
const tNode = tData[context.nodeIndex] as TNode; const tNode = tData[context.nodeIndex] as TNode;
return tNode.value!; return tNode.value!;
@ -286,12 +287,13 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
* - attribute bindings (e.g. `[attr.role]="menu"`) * - attribute bindings (e.g. `[attr.role]="menu"`)
*/ */
get properties(): {[key: string]: any;} { get properties(): {[key: string]: any;} {
const context = getLContext(this.nativeNode); const context = getLContext(this.nativeNode)!;
if (context === null) { const lView = context ? context.lView : null;
if (lView === null) {
return {}; return {};
} }
const lView = context.lView;
const tData = lView[TVIEW].data; const tData = lView[TVIEW].data;
const tNode = tData[context.nodeIndex] as TNode; const tNode = tData[context.nodeIndex] as TNode;
@ -312,12 +314,13 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
return attributes; return attributes;
} }
const context = getLContext(element); const context = getLContext(element)!;
if (context === null) { const lView = context ? context.lView : null;
if (lView === null) {
return {}; return {};
} }
const lView = context.lView;
const tNodeAttrs = (lView[TVIEW].data[context.nodeIndex] as TNode).attrs; const tNodeAttrs = (lView[TVIEW].data[context.nodeIndex] as TNode).attrs;
const lowercaseTNodeAttrs: string[] = []; const lowercaseTNodeAttrs: string[] = [];
@ -502,11 +505,12 @@ function _queryAllR3(
function _queryAllR3( function _queryAllR3(
parentElement: DebugElement, predicate: Predicate<DebugElement>|Predicate<DebugNode>, parentElement: DebugElement, predicate: Predicate<DebugElement>|Predicate<DebugNode>,
matches: DebugElement[]|DebugNode[], elementsOnly: boolean) { matches: DebugElement[]|DebugNode[], elementsOnly: boolean) {
const context = getLContext(parentElement.nativeNode); const context = getLContext(parentElement.nativeNode)!;
if (context !== null) { const lView = context ? context.lView : null;
const parentTNode = context.lView[TVIEW].data[context.nodeIndex] as TNode; if (lView !== null) {
const parentTNode = lView[TVIEW].data[context.nodeIndex] as TNode;
_queryNodeChildrenR3( _queryNodeChildrenR3(
parentTNode, context.lView, predicate, matches, elementsOnly, parentElement.nativeNode); parentTNode, lView, predicate, matches, elementsOnly, parentElement.nativeNode);
} else { } else {
// If the context is null, then `parentElement` was either created with Renderer2 or native DOM // If the context is null, then `parentElement` was either created with Renderer2 or native DOM
// APIs. // APIs.

View File

@ -8,12 +8,15 @@
import '../util/ng_dev_mode'; import '../util/ng_dev_mode';
import {assertDefined, assertDomNode} from '../util/assert'; import {assertDefined, assertDomNode} from '../util/assert';
import {EMPTY_ARRAY} from '../util/empty'; import {EMPTY_ARRAY} from '../util/empty';
import {assertLView} from './assert';
import {LContext} from './interfaces/context'; import {LContext} from './interfaces/context';
import {getLViewById} from './interfaces/lview_tracking';
import {TNode, TNodeFlags} from './interfaces/node'; import {TNode, TNodeFlags} from './interfaces/node';
import {RElement, RNode} from './interfaces/renderer_dom'; import {RElement, RNode} from './interfaces/renderer_dom';
import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view'; import {isLView} from './interfaces/type_checks';
import {CONTEXT, HEADER_OFFSET, HOST, ID, LView, TVIEW} from './interfaces/view';
import {getComponentLViewByIndex, unwrapRNode} from './util/view_utils'; import {getComponentLViewByIndex, unwrapRNode} from './util/view_utils';
@ -43,7 +46,7 @@ export function getLContext(target: any): LContext|null {
if (mpValue) { if (mpValue) {
// only when it's an array is it considered an LView instance // only when it's an array is it considered an LView instance
// ... otherwise it's an already constructed LContext instance // ... otherwise it's an already constructed LContext instance
if (Array.isArray(mpValue)) { if (isLView(mpValue)) {
const lView: LView = mpValue!; const lView: LView = mpValue!;
let nodeIndex: number; let nodeIndex: number;
let component: any = undefined; let component: any = undefined;
@ -105,12 +108,7 @@ export function getLContext(target: any): LContext|null {
while (parent = parent.parentNode) { while (parent = parent.parentNode) {
const parentContext = readPatchedData(parent); const parentContext = readPatchedData(parent);
if (parentContext) { if (parentContext) {
let lView: LView|null; const lView = Array.isArray(parentContext) ? parentContext as LView : parentContext.lView;
if (Array.isArray(parentContext)) {
lView = parentContext as LView;
} else {
lView = parentContext.lView;
}
// the edge of the app was also reached here through another means // the edge of the app was also reached here through another means
// (maybe because the DOM was changed manually). // (maybe because the DOM was changed manually).
@ -136,14 +134,7 @@ export function getLContext(target: any): LContext|null {
* Creates an empty instance of a `LContext` context * Creates an empty instance of a `LContext` context
*/ */
function createLContext(lView: LView, nodeIndex: number, native: RNode): LContext { function createLContext(lView: LView, nodeIndex: number, native: RNode): LContext {
return { return new LContext(lView[ID], nodeIndex, native);
lView,
nodeIndex,
native,
component: undefined,
directives: undefined,
localRefs: undefined,
};
} }
/** /**
@ -153,21 +144,24 @@ function createLContext(lView: LView, nodeIndex: number, native: RNode): LContex
* @returns The component's view * @returns The component's view
*/ */
export function getComponentViewByInstance(componentInstance: {}): LView { export function getComponentViewByInstance(componentInstance: {}): LView {
let lView = readPatchedData(componentInstance); let patchedData = readPatchedData(componentInstance);
let view: LView; let lView: LView;
if (Array.isArray(lView)) { if (isLView(patchedData)) {
const nodeIndex = findViaComponent(lView, componentInstance); const contextLView: LView = patchedData;
view = getComponentLViewByIndex(nodeIndex, lView); const nodeIndex = findViaComponent(contextLView, componentInstance);
const context = createLContext(lView, nodeIndex, view[HOST] as RElement); lView = getComponentLViewByIndex(nodeIndex, contextLView);
const context = createLContext(contextLView, nodeIndex, lView[HOST] as RElement);
context.component = componentInstance; context.component = componentInstance;
attachPatchData(componentInstance, context); attachPatchData(componentInstance, context);
attachPatchData(context.native, context); attachPatchData(context.native, context);
} else { } else {
const context = lView as any as LContext; const context = patchedData as unknown as LContext;
view = getComponentLViewByIndex(context.nodeIndex, context.lView); const contextLView = context.lView!;
ngDevMode && assertLView(contextLView);
lView = getComponentLViewByIndex(context.nodeIndex, contextLView);
} }
return view; return lView;
} }
/** /**
@ -181,7 +175,10 @@ const MONKEY_PATCH_KEY_NAME = '__ngContext__';
*/ */
export function attachPatchData(target: any, data: LView|LContext) { export function attachPatchData(target: any, data: LView|LContext) {
ngDevMode && assertDefined(target, 'Target expected'); ngDevMode && assertDefined(target, 'Target expected');
target[MONKEY_PATCH_KEY_NAME] = data; // Only attach the ID of the view in order to avoid memory leaks (see #41047). We only do this
// for `LView`, because we have control over when an `LView` is created and destroyed, whereas
// we can't know when to remove an `LContext`.
target[MONKEY_PATCH_KEY_NAME] = isLView(data) ? data[ID] : data;
} }
/** /**
@ -190,13 +187,14 @@ export function attachPatchData(target: any, data: LView|LContext) {
*/ */
export function readPatchedData(target: any): LView|LContext|null { export function readPatchedData(target: any): LView|LContext|null {
ngDevMode && assertDefined(target, 'Target expected'); ngDevMode && assertDefined(target, 'Target expected');
return target[MONKEY_PATCH_KEY_NAME] || null; const data = target[MONKEY_PATCH_KEY_NAME];
return (typeof data === 'number') ? getLViewById(data) : data || null;
} }
export function readPatchedLView(target: any): LView|null { export function readPatchedLView(target: any): LView|null {
const value = readPatchedData(target); const value = readPatchedData(target);
if (value) { if (value) {
return Array.isArray(value) ? value : (value as LContext).lView; return isLView(value) ? value : value.lView;
} }
return null; return null;
} }

View File

@ -25,7 +25,7 @@ import {LQueries, TQueries} from '../interfaces/query';
import {Renderer3, RendererFactory3} from '../interfaces/renderer'; import {Renderer3, RendererFactory3} from '../interfaces/renderer';
import {RComment, RElement, RNode} from '../interfaces/renderer_dom'; import {RComment, RElement, RNode} from '../interfaces/renderer_dom';
import {getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate, TStylingKey, TStylingRange} from '../interfaces/styling'; import {getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate, TStylingKey, TStylingRange} from '../interfaces/styling';
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DebugNode, DECLARATION_VIEW, DestroyHookData, FLAGS, HEADER_OFFSET, HookData, HOST, HostBindingOpCodes, INJECTOR, LContainerDebug as ILContainerDebug, LView, LViewDebug as ILViewDebug, LViewDebugRange, LViewDebugRangeContent, LViewFlags, NEXT, NodeInjectorDebug, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TView as ITView, TVIEW, TView, TViewType, TViewTypeAsString} from '../interfaces/view'; import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DebugNode, DECLARATION_VIEW, DestroyHookData, FLAGS, HEADER_OFFSET, HookData, HOST, HostBindingOpCodes, ID, INJECTOR, LContainerDebug as ILContainerDebug, LView, LViewDebug as ILViewDebug, LViewDebugRange, LViewDebugRangeContent, LViewFlags, NEXT, NodeInjectorDebug, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TView as ITView, TVIEW, TView, TViewType, TViewTypeAsString} from '../interfaces/view';
import {attachDebugObject} from '../util/debug_utils'; import {attachDebugObject} from '../util/debug_utils';
import {getParentInjectorIndex, getParentInjectorView} from '../util/injector_utils'; import {getParentInjectorIndex, getParentInjectorView} from '../util/injector_utils';
import {unwrapRNode} from '../util/view_utils'; import {unwrapRNode} from '../util/view_utils';
@ -513,6 +513,9 @@ export class LViewDebug implements ILViewDebug {
get tHost(): ITNode|null { get tHost(): ITNode|null {
return this._raw_lView[T_HOST]; return this._raw_lView[T_HOST];
} }
get id(): number {
return this._raw_lView[ID];
}
get decls(): LViewDebugRange { get decls(): LViewDebugRange {
return toLViewRange(this.tView, this._raw_lView, HEADER_OFFSET, this.tView.bindingStartIndex); return toLViewRange(this.tView, this._raw_lView, HEADER_OFFSET, this.tView.bindingStartIndex);

View File

@ -28,12 +28,13 @@ import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} fr
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS} from '../interfaces/container'; import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS} from '../interfaces/container';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, HostBindingsFunction, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition'; import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, HostBindingsFunction, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
import {NodeInjectorFactory} from '../interfaces/injector'; import {NodeInjectorFactory} from '../interfaces/injector';
import {registerLView} from '../interfaces/lview_tracking';
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode} from '../interfaces/node'; import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode} from '../interfaces/node';
import {isProceduralRenderer, Renderer3, RendererFactory3} from '../interfaces/renderer'; import {isProceduralRenderer, Renderer3, RendererFactory3} from '../interfaces/renderer';
import {RComment, RElement, RNode, RText} from '../interfaces/renderer_dom'; import {RComment, RElement, RNode, RText} from '../interfaces/renderer_dom';
import {SanitizerFn} from '../interfaces/sanitization'; import {SanitizerFn} from '../interfaces/sanitization';
import {isComponentDef, isComponentHost, isContentQueryHost, isRootView} from '../interfaces/type_checks'; import {isComponentDef, isComponentHost, isContentQueryHost, isRootView} from '../interfaces/type_checks';
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HostBindingOpCodes, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, T_HOST, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType} from '../interfaces/view'; import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HostBindingOpCodes, ID, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, T_HOST, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType} from '../interfaces/view';
import {assertPureTNodeType, assertTNodeType} from '../node_assert'; import {assertPureTNodeType, assertTNodeType} from '../node_assert';
import {updateTextNode} from '../node_manipulation'; import {updateTextNode} from '../node_manipulation';
import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher'; import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher';
@ -143,6 +144,7 @@ export function createLView<T>(
lView[SANITIZER] = sanitizer || parentLView && parentLView[SANITIZER] || null!; lView[SANITIZER] = sanitizer || parentLView && parentLView[SANITIZER] || null!;
lView[INJECTOR as any] = injector || parentLView && parentLView[INJECTOR] || null; lView[INJECTOR as any] = injector || parentLView && parentLView[INJECTOR] || null;
lView[T_HOST] = tHostNode; lView[T_HOST] = tHostNode;
lView[ID] = registerLView(lView);
ngDevMode && ngDevMode &&
assertEqual( assertEqual(
tView.type == TViewType.Embedded ? parentLView !== null : true, true, tView.type == TViewType.Embedded ? parentLView !== null : true, true,
@ -1906,9 +1908,12 @@ export function scheduleTick(rootContext: RootContext, flags: RootContextFlags)
export function tickRootContext(rootContext: RootContext) { export function tickRootContext(rootContext: RootContext) {
for (let i = 0; i < rootContext.components.length; i++) { for (let i = 0; i < rootContext.components.length; i++) {
const rootComponent = rootContext.components[i]; const rootComponent = rootContext.components[i];
const lView = readPatchedLView(rootComponent)!; const lView = readPatchedLView(rootComponent);
const tView = lView[TVIEW]; // We might not have an `LView` if the component was destroyed.
renderComponentOrTemplate(tView, lView, tView.template, rootComponent); if (lView !== null) {
const tView = lView[TVIEW];
renderComponentOrTemplate(tView, lView, tView.template, rootComponent);
}
} }
} }

View File

@ -7,6 +7,7 @@
*/ */
import {getLViewById} from './lview_tracking';
import {RNode} from './renderer_dom'; import {RNode} from './renderer_dom';
import {LView} from './view'; import {LView} from './view';
@ -21,35 +22,41 @@ import {LView} from './view';
* function. The component, element and each directive instance will share the same instance * function. The component, element and each directive instance will share the same instance
* of the context. * of the context.
*/ */
export interface LContext { export class LContext {
/**
* The component's parent view data.
*/
lView: LView;
/**
* The index instance of the node.
*/
nodeIndex: number;
/**
* The instance of the DOM node that is attached to the lNode.
*/
native: RNode;
/** /**
* The instance of the Component node. * The instance of the Component node.
*/ */
component: {}|null|undefined; public component: {}|null|undefined;
/** /**
* The list of active directives that exist on this element. * The list of active directives that exist on this element.
*/ */
directives: any[]|null|undefined; public directives: any[]|null|undefined;
/** /**
* The map of local references (local reference name => element or directive instance) that exist * The map of local references (local reference name => element or directive instance) that
* on this element. * exist on this element.
*/ */
localRefs: {[key: string]: any}|null|undefined; public localRefs: {[key: string]: any}|null|undefined;
/** Component's parent view data. */
get lView(): LView|null {
return getLViewById(this.lViewId);
}
constructor(
/**
* ID of the component's parent view data.
*/
private lViewId: number,
/**
* The index instance of the node.
*/
public nodeIndex: number,
/**
* The instance of the DOM node that is attached to the lNode.
*/
public native: RNode) {}
} }

View File

@ -0,0 +1,35 @@
/**
* @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 {assertNumber} from '../../util/assert';
import {ID, LView} from './view';
// Keeps track of the currently-active LViews.
const TRACKED_LVIEWS = new Map<number, LView>();
// Used for generating unique IDs for LViews.
let uniqueIdCounter = 0;
/** Starts tracking an LView and returns a unique ID that can be used for future lookups. */
export function registerLView(lView: LView): number {
const id = uniqueIdCounter++;
TRACKED_LVIEWS.set(id, lView);
return id;
}
/** Gets an LView by its unique ID. */
export function getLViewById(id: number): LView|null {
ngDevMode && assertNumber(id, 'ID used for LView lookup must be a number');
return TRACKED_LVIEWS.get(id) || null;
}
/** Stops tracking an LView. */
export function unregisterLView(lView: LView): void {
ngDevMode && assertNumber(lView[ID], 'Cannot stop tracking an LView that does not have an ID');
TRACKED_LVIEWS.delete(lView[ID]);
}

View File

@ -47,6 +47,7 @@ export const DECLARATION_COMPONENT_VIEW = 16;
export const DECLARATION_LCONTAINER = 17; export const DECLARATION_LCONTAINER = 17;
export const PREORDER_HOOK_FLAGS = 18; export const PREORDER_HOOK_FLAGS = 18;
export const QUERIES = 19; export const QUERIES = 19;
export const ID = 20;
/** /**
* Size of LView's header. Necessary to adjust for it when setting slots. * Size of LView's header. Necessary to adjust for it when setting slots.
* *
@ -54,7 +55,7 @@ export const QUERIES = 19;
* instruction index into `LView` index. All other indexes should be in the `LView` index space and * instruction index into `LView` index. All other indexes should be in the `LView` index space and
* there should be no need to refer to `HEADER_OFFSET` anywhere else. * there should be no need to refer to `HEADER_OFFSET` anywhere else.
*/ */
export const HEADER_OFFSET = 20; export const HEADER_OFFSET = 21;
// This interface replaces the real LView interface if it is an arg or a // This interface replaces the real LView interface if it is an arg or a
@ -326,6 +327,9 @@ export interface LView extends Array<any> {
* are not `Dirty`/`CheckAlways`. * are not `Dirty`/`CheckAlways`.
*/ */
[TRANSPLANTED_VIEWS_TO_REFRESH]: number; [TRANSPLANTED_VIEWS_TO_REFRESH]: number;
/** Unique ID of the view. Used for `__ngContext__` lookups in the `LView` registry. */
[ID]: number;
} }
/** Flags associated with an LView (saved in LView[FLAGS]) */ /** Flags associated with an LView (saved in LView[FLAGS]) */

View File

@ -19,6 +19,7 @@ import {icuContainerIterate} from './i18n/i18n_tree_shaking';
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS, NATIVE, unusedValueExportToPlacateAjd as unused1} from './interfaces/container'; import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS, NATIVE, unusedValueExportToPlacateAjd as unused1} from './interfaces/container';
import {ComponentDef} from './interfaces/definition'; import {ComponentDef} from './interfaces/definition';
import {NodeInjectorFactory} from './interfaces/injector'; import {NodeInjectorFactory} from './interfaces/injector';
import {unregisterLView} from './interfaces/lview_tracking';
import {TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode, unusedValueExportToPlacateAjd as unused2} from './interfaces/node'; import {TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode, unusedValueExportToPlacateAjd as unused2} from './interfaces/node';
import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection'; import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection';
import {isProceduralRenderer, ProceduralRenderer3, Renderer3, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; import {isProceduralRenderer, ProceduralRenderer3, Renderer3, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer';
@ -395,6 +396,7 @@ export function destroyLView(tView: TView, lView: LView) {
} }
destroyViewTree(lView); destroyViewTree(lView);
unregisterLView(lView);
} }
} }

View File

@ -11,7 +11,7 @@ import {Injector} from '../../di/injector';
import {ViewEncapsulation} from '../../metadata/view'; import {ViewEncapsulation} from '../../metadata/view';
import {assertEqual} from '../../util/assert'; import {assertEqual} from '../../util/assert';
import {assertLView} from '../assert'; import {assertLView} from '../assert';
import {discoverLocalRefs, getComponentAtNodeIndex, getDirectivesAtNodeIndex, getLContext} from '../context_discovery'; import {discoverLocalRefs, getComponentAtNodeIndex, getDirectivesAtNodeIndex, getLContext, readPatchedLView} from '../context_discovery';
import {getComponentDef, getDirectiveDef} from '../definition'; import {getComponentDef, getDirectiveDef} from '../definition';
import {NodeInjector} from '../di'; import {NodeInjector} from '../di';
import {buildDebugNode} from '../instructions/lview_debug'; import {buildDebugNode} from '../instructions/lview_debug';
@ -20,6 +20,7 @@ import {DirectiveDef} from '../interfaces/definition';
import {TElementNode, TNode, TNodeProviderIndexes} from '../interfaces/node'; import {TElementNode, TNode, TNodeProviderIndexes} from '../interfaces/node';
import {isLView} from '../interfaces/type_checks'; import {isLView} from '../interfaces/type_checks';
import {CLEANUP, CONTEXT, DebugNode, FLAGS, LView, LViewFlags, T_HOST, TVIEW, TViewType} from '../interfaces/view'; import {CLEANUP, CONTEXT, DebugNode, FLAGS, LView, LViewFlags, T_HOST, TVIEW, TViewType} from '../interfaces/view';
import {stringifyForError} from './stringify_utils'; import {stringifyForError} from './stringify_utils';
import {getLViewParent, getRootContext} from './view_traversal_utils'; import {getLViewParent, getRootContext} from './view_traversal_utils';
import {getTNode, unwrapRNode} from './view_utils'; import {getTNode, unwrapRNode} from './view_utils';
@ -52,12 +53,16 @@ import {getTNode, unwrapRNode} from './view_utils';
* @globalApi ng * @globalApi ng
*/ */
export function getComponent<T>(element: Element): T|null { export function getComponent<T>(element: Element): T|null {
assertDomElement(element); ngDevMode && assertDomElement(element);
const context = getLContext(element); const context = getLContext(element);
if (context === null) return null; if (context === null) return null;
if (context.component === undefined) { if (context.component === undefined) {
context.component = getComponentAtNodeIndex(context.nodeIndex, context.lView); const lView = context.lView;
if (lView === null) {
return null;
}
context.component = getComponentAtNodeIndex(context.nodeIndex, lView);
} }
return context.component as T; return context.component as T;
@ -78,8 +83,9 @@ export function getComponent<T>(element: Element): T|null {
*/ */
export function getContext<T>(element: Element): T|null { export function getContext<T>(element: Element): T|null {
assertDomElement(element); assertDomElement(element);
const context = getLContext(element); const context = getLContext(element)!;
return context === null ? null : context.lView[CONTEXT] as T; const lView = context ? context.lView : null;
return lView === null ? null : lView[CONTEXT] as T;
} }
/** /**
@ -98,12 +104,11 @@ export function getContext<T>(element: Element): T|null {
* @globalApi ng * @globalApi ng
*/ */
export function getOwningComponent<T>(elementOrDir: Element|{}): T|null { export function getOwningComponent<T>(elementOrDir: Element|{}): T|null {
const context = getLContext(elementOrDir); const context = getLContext(elementOrDir)!;
if (context === null) return null; let lView = context ? context.lView : null;
if (lView === null) return null;
let lView = context.lView;
let parent: LView|null; let parent: LView|null;
ngDevMode && assertLView(lView);
while (lView[TVIEW].type === TViewType.Embedded && (parent = getLViewParent(lView)!)) { while (lView[TVIEW].type === TViewType.Embedded && (parent = getLViewParent(lView)!)) {
lView = parent; lView = parent;
} }
@ -122,7 +127,8 @@ export function getOwningComponent<T>(elementOrDir: Element|{}): T|null {
* @globalApi ng * @globalApi ng
*/ */
export function getRootComponents(elementOrDir: Element|{}): {}[] { export function getRootComponents(elementOrDir: Element|{}): {}[] {
return [...getRootContext(elementOrDir).components]; const lView = readPatchedLView(elementOrDir);
return lView !== null ? [...getRootContext(lView).components] : [];
} }
/** /**
@ -136,11 +142,12 @@ export function getRootComponents(elementOrDir: Element|{}): {}[] {
* @globalApi ng * @globalApi ng
*/ */
export function getInjector(elementOrDir: Element|{}): Injector { export function getInjector(elementOrDir: Element|{}): Injector {
const context = getLContext(elementOrDir); const context = getLContext(elementOrDir)!;
if (context === null) return Injector.NULL; const lView = context ? context.lView : null;
if (lView === null) return Injector.NULL;
const tNode = context.lView[TVIEW].data[context.nodeIndex] as TElementNode; const tNode = lView[TVIEW].data[context.nodeIndex] as TElementNode;
return new NodeInjector(tNode, context.lView); return new NodeInjector(tNode, lView);
} }
/** /**
@ -149,9 +156,9 @@ export function getInjector(elementOrDir: Element|{}): Injector {
* @param element Element for which the injection tokens should be retrieved. * @param element Element for which the injection tokens should be retrieved.
*/ */
export function getInjectionTokens(element: Element): any[] { export function getInjectionTokens(element: Element): any[] {
const context = getLContext(element); const context = getLContext(element)!;
if (context === null) return []; const lView = context ? context.lView : null;
const lView = context.lView; if (lView === null) return [];
const tView = lView[TVIEW]; const tView = lView[TVIEW];
const tNode = tView.data[context.nodeIndex] as TNode; const tNode = tView.data[context.nodeIndex] as TNode;
const providerTokens: any[] = []; const providerTokens: any[] = [];
@ -200,12 +207,12 @@ export function getDirectives(node: Node): {}[] {
return []; return [];
} }
const context = getLContext(node); const context = getLContext(node)!;
if (context === null) { const lView = context ? context.lView : null;
if (lView === null) {
return []; return [];
} }
const lView = context.lView;
const tView = lView[TVIEW]; const tView = lView[TVIEW];
const nodeIndex = context.nodeIndex; const nodeIndex = context.nodeIndex;
if (!tView?.data[nodeIndex]) { if (!tView?.data[nodeIndex]) {
@ -297,7 +304,11 @@ export function getLocalRefs(target: {}): {[key: string]: any} {
if (context === null) return {}; if (context === null) return {};
if (context.localRefs === undefined) { if (context.localRefs === undefined) {
context.localRefs = discoverLocalRefs(context.lView, context.nodeIndex); const lView = context.lView;
if (lView === null) {
return {};
}
context.localRefs = discoverLocalRefs(lView, context.nodeIndex);
} }
return context.localRefs || {}; return context.localRefs || {};
@ -383,11 +394,11 @@ export interface Listener {
* @globalApi ng * @globalApi ng
*/ */
export function getListeners(element: Element): Listener[] { export function getListeners(element: Element): Listener[] {
assertDomElement(element); ngDevMode && assertDomElement(element);
const lContext = getLContext(element); const lContext = getLContext(element);
if (lContext === null) return []; const lView = lContext === null ? null : lContext.lView;
if (lView === null) return [];
const lView = lContext.lView;
const tView = lView[TVIEW]; const tView = lView[TVIEW];
const lCleanup = lView[CLEANUP]; const lCleanup = lView[CLEANUP];
const tCleanup = tView.cleanup; const tCleanup = tView.cleanup;
@ -441,12 +452,13 @@ export function getDebugNode(element: Element): DebugNode|null {
throw new Error('Expecting instance of DOM Element'); throw new Error('Expecting instance of DOM Element');
} }
const lContext = getLContext(element); const lContext = getLContext(element)!;
if (lContext === null) { const lView = lContext ? lContext.lView : null;
if (lView === null) {
return null; return null;
} }
const lView = lContext.lView;
const nodeIndex = lContext.nodeIndex; const nodeIndex = lContext.nodeIndex;
if (nodeIndex !== -1) { if (nodeIndex !== -1) {
const valueInLView = lView[nodeIndex]; const valueInLView = lView[nodeIndex];
@ -473,7 +485,8 @@ export function getDebugNode(element: Element): DebugNode|null {
export function getComponentLView(target: any): LView { export function getComponentLView(target: any): LView {
const lContext = getLContext(target)!; const lContext = getLContext(target)!;
const nodeIndx = lContext.nodeIndex; const nodeIndx = lContext.nodeIndex;
const lView = lContext.lView; const lView = lContext.lView!;
ngDevMode && assertLView(lView);
const componentLView = lView[nodeIndx]; const componentLView = lView[nodeIndx];
ngDevMode && assertLView(componentLView); ngDevMode && assertLView(componentLView);
return componentLView; return componentLView;

View File

@ -141,6 +141,47 @@ describe('component', () => {
expect(fixture.nativeElement.textContent.trim()).toBe('hello'); expect(fixture.nativeElement.textContent.trim()).toBe('hello');
}); });
onlyInIvy('ViewEngine has a specific error for this while Ivy does not')
.it('should not throw when calling `detectChanges` on the ChangeDetectorRef of a destroyed view',
() => {
@Component({template: 'hello'})
class HelloComponent {
}
// TODO: This module is only used to declare the `entryComponets` since
// `configureTestingModule` doesn't support it. The module can be removed
// once ViewEngine is removed.
@NgModule({
declarations: [HelloComponent],
exports: [HelloComponent],
entryComponents: [HelloComponent]
})
class HelloModule {
}
@Component({template: `<div #insertionPoint></div>`})
class App {
@ViewChild('insertionPoint', {read: ViewContainerRef})
viewContainerRef!: ViewContainerRef;
constructor(public componentFactoryResolver: ComponentFactoryResolver) {}
}
TestBed.configureTestingModule({declarations: [App], imports: [HelloModule]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const instance = fixture.componentInstance;
const factory =
instance.componentFactoryResolver.resolveComponentFactory(HelloComponent);
const componentRef = instance.viewContainerRef.createComponent(factory);
fixture.detectChanges();
expect(() => {
componentRef.destroy();
componentRef.changeDetectorRef.detectChanges();
}).not.toThrow();
});
// TODO: add tests with Native once tests run in real browser (domino doesn't support shadow root) // TODO: add tests with Native once tests run in real browser (domino doesn't support shadow root)
describe('encapsulation', () => { describe('encapsulation', () => {
@Component({ @Component({

View File

@ -27,7 +27,7 @@ onlyInIvy('Ivy specific').describe('Debug Representation', () => {
const fixture = TestBed.createComponent(MyComponent); const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges(); fixture.detectChanges();
const hostView = getLContext(fixture.componentInstance)!.lView.debug!; const hostView = getLContext(fixture.componentInstance)!.lView!.debug!;
expect(hostView.hostHTML).toEqual(null); expect(hostView.hostHTML).toEqual(null);
const myCompView = hostView.childViews[0] as LViewDebug; const myCompView = hostView.childViews[0] as LViewDebug;
expect(myCompView.hostHTML).toContain('<div id="123">Hello World</div>'); expect(myCompView.hostHTML).toContain('<div id="123">Hello World</div>');
@ -47,7 +47,7 @@ onlyInIvy('Ivy specific').describe('Debug Representation', () => {
const fixture = TestBed.createComponent(MyComponent); const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges(); fixture.detectChanges();
const hostView = getLContext(fixture.componentInstance)!.lView.debug!; const hostView = getLContext(fixture.componentInstance)!.lView!.debug!;
const myComponentView = hostView.childViews[0] as LViewDebug; const myComponentView = hostView.childViews[0] as LViewDebug;
expect(myComponentView.decls).toEqual({ expect(myComponentView.decls).toEqual({
start: HEADER_OFFSET, start: HEADER_OFFSET,

View File

@ -69,16 +69,19 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
@Component({ @Component({
selector: 'my-app', selector: 'my-app',
template: ` template: `
<span (click)="log($event)">{{text}}</span> <span (click)="log($event)" *ngIf="spanVisible">{{text}}</span>
<div dirA #div #foo="dirA"></div> <div dirA #div #foo="dirA"></div>
<child></child> <child></child>
<child dirA #child></child> <child dirA #child></child>
<child dirA *ngIf="true"></child> <child dirA *ngIf="conditionalChildVisible"></child>
<ng-container><p></p></ng-container> <ng-container><p></p></ng-container>
<b *ngIf="visible">Bold</b>
` `
}) })
class MyApp { class MyApp {
text: string = 'INIT'; text: string = 'INIT';
spanVisible = true;
conditionalChildVisible = true;
@Input('a') b = 2; @Input('a') b = 2;
@Output('c') d = new EventEmitter(); @Output('c') d = new EventEmitter();
constructor() { constructor() {
@ -105,6 +108,15 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
expect(getComponent<Child>(child[0])).toEqual(childComponent[0]); expect(getComponent<Child>(child[0])).toEqual(childComponent[0]);
expect(getComponent<Child>(child[1])).toEqual(childComponent[1]); expect(getComponent<Child>(child[1])).toEqual(childComponent[1]);
}); });
it('should not throw when called on a destroyed node', () => {
expect(getComponent(span[0])).toEqual(null);
expect(getComponent<Child>(child[2])).toEqual(childComponent[2]);
fixture.componentInstance.spanVisible = false;
fixture.componentInstance.conditionalChildVisible = false;
fixture.detectChanges();
expect(getComponent(span[0])).toEqual(null);
expect(getComponent<Child>(child[2])).toEqual(childComponent[2]);
});
}); });
describe('getComponentLView', () => { describe('getComponentLView', () => {
@ -131,6 +143,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
expect(getContext<{$implicit: boolean}>(child[2])!.$implicit).toEqual(true); expect(getContext<{$implicit: boolean}>(child[2])!.$implicit).toEqual(true);
expect(getContext<Child>(p[0])).toEqual(childComponent[0]); expect(getContext<Child>(p[0])).toEqual(childComponent[0]);
}); });
it('should return null for destroyed node', () => {
expect(getContext(span[0])).toBeTruthy();
fixture.componentInstance.spanVisible = false;
fixture.detectChanges();
expect(getContext(span[0])).toBeNull();
});
}); });
describe('getHostElement', () => { describe('getHostElement', () => {
@ -146,6 +164,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
it('should throw on unknown target', () => { it('should throw on unknown target', () => {
expect(() => getHostElement({})).toThrowError(); // expect(() => getHostElement({})).toThrowError(); //
}); });
it('should return element for destroyed node', () => {
expect(getHostElement(span[0])).toEqual(span[0]);
fixture.componentInstance.spanVisible = false;
fixture.detectChanges();
expect(getHostElement(span[0])).toEqual(span[0]);
});
}); });
describe('getInjector', () => { describe('getInjector', () => {
@ -163,6 +187,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
expect(getInjector(dirA[0]).get(String)).toEqual('Module'); expect(getInjector(dirA[0]).get(String)).toEqual('Module');
expect(getInjector(dirA[1]).get(String)).toEqual('Child'); expect(getInjector(dirA[1]).get(String)).toEqual('Child');
}); });
it('should retrieve injector from destroyed node', () => {
expect(getInjector(span[0])).toBeTruthy();
fixture.componentInstance.spanVisible = false;
fixture.detectChanges();
expect(getInjector(span[0])).toBeTruthy();
});
}); });
describe('getDirectives', () => { describe('getDirectives', () => {
@ -175,6 +205,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
expect(getDirectives(div[0])).toEqual([dirA[0]]); expect(getDirectives(div[0])).toEqual([dirA[0]]);
expect(getDirectives(child[1])).toEqual([dirA[1]]); expect(getDirectives(child[1])).toEqual([dirA[1]]);
}); });
it('should return empty array for destroyed node', () => {
expect(getDirectives(span[0])).toEqual([]);
fixture.componentInstance.spanVisible = false;
fixture.detectChanges();
expect(getDirectives(span[0])).toEqual([]);
});
}); });
describe('getOwningComponent', () => { describe('getOwningComponent', () => {
@ -202,6 +238,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
expect(getOwningComponent<MyApp>(dirA[0])).toEqual(myApp); expect(getOwningComponent<MyApp>(dirA[0])).toEqual(myApp);
expect(getOwningComponent<MyApp>(dirA[1])).toEqual(myApp); expect(getOwningComponent<MyApp>(dirA[1])).toEqual(myApp);
}); });
it('should return null for destroyed node', () => {
expect(getOwningComponent<MyApp>(span[0])).toEqual(myApp);
fixture.componentInstance.spanVisible = false;
fixture.detectChanges();
expect(getOwningComponent<MyApp>(span[0])).toEqual(null);
});
}); });
describe('getLocalRefs', () => { describe('getLocalRefs', () => {
@ -219,6 +261,13 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
expect(getLocalRefs(child[1])).toEqual({child: childComponent[1]}); expect(getLocalRefs(child[1])).toEqual({child: childComponent[1]});
expect(getLocalRefs(dirA[1])).toEqual({child: childComponent[1]}); expect(getLocalRefs(dirA[1])).toEqual({child: childComponent[1]});
}); });
it('should retrieve from a destroyed node', () => {
expect(getLocalRefs(span[0])).toEqual({});
fixture.componentInstance.spanVisible = false;
fixture.detectChanges();
expect(getLocalRefs(span[0])).toEqual({});
});
}); });
describe('getRootComponents', () => { describe('getRootComponents', () => {
@ -234,6 +283,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
expect(getRootComponents(div[0])).toEqual(rootComponents); expect(getRootComponents(div[0])).toEqual(rootComponents);
expect(getRootComponents(p[0])).toEqual(rootComponents); expect(getRootComponents(p[0])).toEqual(rootComponents);
}); });
it('should return an empty array for a destroyed node', () => {
expect(getRootComponents(span[0])).toEqual([myApp]);
fixture.componentInstance.spanVisible = false;
fixture.detectChanges();
expect(getRootComponents(span[0])).toEqual([]);
});
}); });
describe('getListeners', () => { describe('getListeners', () => {
@ -251,6 +306,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
listeners[0].callback('CLICKED'); listeners[0].callback('CLICKED');
expect(log).toEqual(['CLICKED']); expect(log).toEqual(['CLICKED']);
}); });
it('should return no listeners for destroyed node', () => {
expect(getListeners(span[0]).length).toEqual(1);
fixture.componentInstance.spanVisible = false;
fixture.detectChanges();
expect(getListeners(span[0]).length).toEqual(0);
});
}); });
describe('getInjectionTokens', () => { describe('getInjectionTokens', () => {
@ -259,6 +320,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
expect(getInjectionTokens(child[0])).toEqual([String, Child]); expect(getInjectionTokens(child[0])).toEqual([String, Child]);
expect(getInjectionTokens(child[1])).toEqual([String, Child, DirectiveA]); expect(getInjectionTokens(child[1])).toEqual([String, Child, DirectiveA]);
}); });
it('should retrieve tokens from destroyed node', () => {
expect(getInjectionTokens(span[0])).toEqual([]);
fixture.componentInstance.spanVisible = false;
fixture.detectChanges();
expect(getInjectionTokens(span[0])).toEqual([]);
});
}); });
describe('markDirty', () => { describe('markDirty', () => {

View File

@ -652,12 +652,12 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
const lViewDebug = lView.debug!; const lViewDebug = lView.debug!;
fixture.detectChanges(); fixture.detectChanges();
expect((fixture.nativeElement as Element).textContent).toEqual('just now'); expect((fixture.nativeElement as Element).textContent).toEqual('just now');
expect(lViewDebug.nodes.map(toTypeContent)).toEqual(['IcuContainer(<!--ICU 20:0-->)']); expect(lViewDebug.nodes.map(toTypeContent)).toEqual(['IcuContainer(<!--ICU 21:0-->)']);
// We want to ensure that the ICU container does not have any content! // We want to ensure that the ICU container does not have any content!
// This is because the content is instance dependent and therefore can't be shared // This is because the content is instance dependent and therefore can't be shared
// across `TNode`s. // across `TNode`s.
expect(lViewDebug.nodes[0].children.map(toTypeContent)).toEqual([]); expect(lViewDebug.nodes[0].children.map(toTypeContent)).toEqual([]);
expect(fixture.nativeElement.innerHTML).toEqual('just now<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('just now<!--ICU 21:0-->');
}); });
it('should support multiple ICUs', () => { it('should support multiple ICUs', () => {
@ -674,15 +674,15 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
`); `);
const lView = getComponentLView(fixture.componentInstance); const lView = getComponentLView(fixture.componentInstance);
expect(lView.debug!.nodes.map(toTypeContent)).toEqual([ expect(lView.debug!.nodes.map(toTypeContent)).toEqual([
'IcuContainer(<!--ICU 20:0-->)',
'IcuContainer(<!--ICU 21:0-->)', 'IcuContainer(<!--ICU 21:0-->)',
'IcuContainer(<!--ICU 22:0-->)',
]); ]);
// We want to ensure that the ICU container does not have any content! // We want to ensure that the ICU container does not have any content!
// This is because the content is instance dependent and therefore can't be shared // This is because the content is instance dependent and therefore can't be shared
// across `TNode`s. // across `TNode`s.
expect(lView.debug!.nodes[0].children.map(toTypeContent)).toEqual([]); expect(lView.debug!.nodes[0].children.map(toTypeContent)).toEqual([]);
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual('just now<!--ICU 20:0-->Mr. Angular<!--ICU 21:0-->'); .toEqual('just now<!--ICU 21:0-->Mr. Angular<!--ICU 22:0-->');
}); });
}); });
}); });
@ -778,19 +778,19 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
other {({{name}})} other {({{name}})}
}</div>`); }</div>`);
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual(`<div>aucun <b>email</b>!<!--ICU 21:0--> - (Angular)<!--ICU 21:3--></div>`); .toEqual(`<div>aucun <b>email</b>!<!--ICU 22:0--> - (Angular)<!--ICU 22:3--></div>`);
fixture.componentRef.instance.count = 4; fixture.componentRef.instance.count = 4;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual( .toEqual(
`<div>4 <span title="Angular">emails</span><!--ICU 21:0--> - (Angular)<!--ICU 21:3--></div>`); `<div>4 <span title="Angular">emails</span><!--ICU 22:0--> - (Angular)<!--ICU 22:3--></div>`);
fixture.componentRef.instance.count = 0; fixture.componentRef.instance.count = 0;
fixture.componentRef.instance.name = 'John'; fixture.componentRef.instance.name = 'John';
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual(`<div>aucun <b>email</b>!<!--ICU 21:0--> - (John)<!--ICU 21:3--></div>`); .toEqual(`<div>aucun <b>email</b>!<!--ICU 22:0--> - (John)<!--ICU 22:3--></div>`);
}); });
it('with custom interpolation config', () => { it('with custom interpolation config', () => {
@ -829,9 +829,9 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual( .toEqual(
`<div>` + `<div>` +
`<span>aucun <b>email</b>!<!--ICU 21:0--></span>` + `<span>aucun <b>email</b>!<!--ICU 22:0--></span>` +
` - ` + ` - ` +
`<span>(Angular)<!--ICU 21:3--></span>` + `<span>(Angular)<!--ICU 22:3--></span>` +
`</div>`); `</div>`);
fixture.componentRef.instance.count = 4; fixture.componentRef.instance.count = 4;
@ -839,9 +839,9 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual( .toEqual(
`<div>` + `<div>` +
`<span>4 <span title="Angular">emails</span><!--ICU 21:0--></span>` + `<span>4 <span title="Angular">emails</span><!--ICU 22:0--></span>` +
` - ` + ` - ` +
`<span>(Angular)<!--ICU 21:3--></span>` + `<span>(Angular)<!--ICU 22:3--></span>` +
`</div>`); `</div>`);
fixture.componentRef.instance.count = 0; fixture.componentRef.instance.count = 0;
@ -850,9 +850,9 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual( .toEqual(
`<div>` + `<div>` +
`<span>aucun <b>email</b>!<!--ICU 21:0--></span>` + `<span>aucun <b>email</b>!<!--ICU 22:0--></span>` +
` - ` + ` - ` +
`<span>(John)<!--ICU 21:3--></span>` + `<span>(John)<!--ICU 22:3--></span>` +
`</div>`); `</div>`);
}); });
@ -867,7 +867,7 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
other {({{name}})} other {({{name}})}
}</span></div>`); }</span></div>`);
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual(`<div><span>(Angular)<!--ICU 20:0--></span><!--bindings={ .toEqual(`<div><span>(Angular)<!--ICU 21:0--></span><!--bindings={
"ng-reflect-ng-if": "true" "ng-reflect-ng-if": "true"
}--></div>`); }--></div>`);
@ -887,7 +887,7 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
other {({{name}})} other {({{name}})}
}</ng-container>`); }</ng-container>`);
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual(`(Angular)<!--ICU 21:0--><!--ng-container-->`); .toEqual(`(Angular)<!--ICU 22:0--><!--ng-container-->`);
}); });
it('inside <ng-template>', () => { it('inside <ng-template>', () => {
@ -922,12 +922,12 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
other {animals} other {animals}
}!} }!}
}</div>`); }</div>`);
expect(fixture.nativeElement.innerHTML).toEqual(`<div>zero<!--ICU 21:1--></div>`); expect(fixture.nativeElement.innerHTML).toEqual(`<div>zero<!--ICU 22:1--></div>`);
fixture.componentRef.instance.count = 4; fixture.componentRef.instance.count = 4;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual(`<div>4 animaux<!--nested ICU 0-->!<!--ICU 21:1--></div>`); .toEqual(`<div>4 animaux<!--nested ICU 0-->!<!--ICU 22:1--></div>`);
}); });
it('nested with interpolations in "other" blocks', () => { it('nested with interpolations in "other" blocks', () => {
@ -947,16 +947,16 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
}!} }!}
other {other - {{count}}} other {other - {{count}}}
}</div>`); }</div>`);
expect(fixture.nativeElement.innerHTML).toEqual(`<div>zero<!--ICU 21:1--></div>`); expect(fixture.nativeElement.innerHTML).toEqual(`<div>zero<!--ICU 22:1--></div>`);
fixture.componentRef.instance.count = 2; fixture.componentRef.instance.count = 2;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual(`<div>2 animaux<!--nested ICU 0-->!<!--ICU 21:1--></div>`); .toEqual(`<div>2 animaux<!--nested ICU 0-->!<!--ICU 22:1--></div>`);
fixture.componentRef.instance.count = 4; fixture.componentRef.instance.count = 4;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual(`<div>autre - 4<!--ICU 21:1--></div>`); expect(fixture.nativeElement.innerHTML).toEqual(`<div>autre - 4<!--ICU 22:1--></div>`);
}); });
it('should return the correct plural form for ICU expressions when using "ro" locale', () => { it('should return the correct plural form for ICU expressions when using "ro" locale', () => {
@ -989,31 +989,31 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
=other {lots of emails} =other {lots of emails}
}`); }`);
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 21:0-->');
// Change detection cycle, no model changes // Change detection cycle, no model changes
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 21:0-->');
fixture.componentInstance.count = 3; fixture.componentInstance.count = 3;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('a few emails<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('a few emails<!--ICU 21:0-->');
fixture.componentInstance.count = 1; fixture.componentInstance.count = 1;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('one email<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('one email<!--ICU 21:0-->');
fixture.componentInstance.count = 10; fixture.componentInstance.count = 10;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('a few emails<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('a few emails<!--ICU 21:0-->');
fixture.componentInstance.count = 20; fixture.componentInstance.count = 20;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 21:0-->');
fixture.componentInstance.count = 0; fixture.componentInstance.count = 0;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 21:0-->');
}); });
it(`should return the correct plural form for ICU expressions when using "es" locale`, () => { it(`should return the correct plural form for ICU expressions when using "es" locale`, () => {
@ -1040,31 +1040,31 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
=other {lots of emails} =other {lots of emails}
}`); }`);
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 21:0-->');
// Change detection cycle, no model changes // Change detection cycle, no model changes
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 21:0-->');
fixture.componentInstance.count = 3; fixture.componentInstance.count = 3;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 21:0-->');
fixture.componentInstance.count = 1; fixture.componentInstance.count = 1;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('one email<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('one email<!--ICU 21:0-->');
fixture.componentInstance.count = 10; fixture.componentInstance.count = 10;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 21:0-->');
fixture.componentInstance.count = 20; fixture.componentInstance.count = 20;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 21:0-->');
fixture.componentInstance.count = 0; fixture.componentInstance.count = 0;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 20:0-->'); expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 21:0-->');
}); });
it('projection', () => { it('projection', () => {
@ -1159,12 +1159,12 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
const fixture = TestBed.createComponent(App); const fixture = TestBed.createComponent(App);
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML) expect(fixture.debugElement.nativeElement.innerHTML)
.toContain('<my-cmp><div>ONE<!--ICU 21:0--></div><!--container--></my-cmp>'); .toContain('<my-cmp><div>ONE<!--ICU 22:0--></div><!--container--></my-cmp>');
fixture.componentRef.instance.count = 2; fixture.componentRef.instance.count = 2;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML) expect(fixture.debugElement.nativeElement.innerHTML)
.toContain('<my-cmp><div>OTHER<!--ICU 21:0--></div><!--container--></my-cmp>'); .toContain('<my-cmp><div>OTHER<!--ICU 22:0--></div><!--container--></my-cmp>');
// destroy component // destroy component
fixture.componentInstance.condition = false; fixture.componentInstance.condition = false;
@ -1176,7 +1176,7 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
fixture.componentInstance.count = 1; fixture.componentInstance.count = 1;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML) expect(fixture.debugElement.nativeElement.innerHTML)
.toContain('<my-cmp><div>ONE<!--ICU 21:0--></div><!--container--></my-cmp>'); .toContain('<my-cmp><div>ONE<!--ICU 22:0--></div><!--container--></my-cmp>');
}); });
it('with nested ICU expression and inside a container when creating a view via vcr.createEmbeddedView', it('with nested ICU expression and inside a container when creating a view via vcr.createEmbeddedView',
@ -1248,12 +1248,12 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML) expect(fixture.debugElement.nativeElement.innerHTML)
.toBe( .toBe(
'<my-cmp><div>2 animals<!--nested ICU 0-->!<!--ICU 21:1--></div><!--container--></my-cmp>'); '<my-cmp><div>2 animals<!--nested ICU 0-->!<!--ICU 22:1--></div><!--container--></my-cmp>');
fixture.componentRef.instance.count = 1; fixture.componentRef.instance.count = 1;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML) expect(fixture.debugElement.nativeElement.innerHTML)
.toBe('<my-cmp><div>ONE<!--ICU 21:1--></div><!--container--></my-cmp>'); .toBe('<my-cmp><div>ONE<!--ICU 22:1--></div><!--container--></my-cmp>');
}); });
it('with nested containers', () => { it('with nested containers', () => {
@ -2357,13 +2357,13 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual( .toEqual(
`<child><div>Contenu enfant et projection depuis Parent<!--ICU 21:0--></div></child>`); `<child><div>Contenu enfant et projection depuis Parent<!--ICU 22:0--></div></child>`);
fixture.componentRef.instance.name = 'angular'; fixture.componentRef.instance.name = 'angular';
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual( .toEqual(
`<child><div>Contenu enfant et projection depuis Angular<!--ICU 21:0--></div></child>`); `<child><div>Contenu enfant et projection depuis Angular<!--ICU 22:0--></div></child>`);
}); });
it(`shouldn't project deleted projections in i18n blocks`, () => { it(`shouldn't project deleted projections in i18n blocks`, () => {

View File

@ -32,7 +32,7 @@ onlyInIvy('Debug information exist in ivy only').describe('ngDevMode debug', ()
TestBed.configureTestingModule({declarations: [MyApp], imports: [CommonModule]}); TestBed.configureTestingModule({declarations: [MyApp], imports: [CommonModule]});
const fixture = TestBed.createComponent(MyApp); const fixture = TestBed.createComponent(MyApp);
const rootLView = getLContext(fixture.nativeElement)!.lView; const rootLView = getLContext(fixture.nativeElement)!.lView!;
expect(rootLView.constructor.name).toEqual('LRootView'); expect(rootLView.constructor.name).toEqual('LRootView');
const componentLView = getComponentLView(fixture.componentInstance); const componentLView = getComponentLView(fixture.componentInstance);
@ -41,7 +41,7 @@ onlyInIvy('Debug information exist in ivy only').describe('ngDevMode debug', ()
const element: HTMLElement = fixture.nativeElement; const element: HTMLElement = fixture.nativeElement;
fixture.detectChanges(); fixture.detectChanges();
const li = element.querySelector('li')!; const li = element.querySelector('li')!;
const embeddedLView = getLContext(li)!.lView; const embeddedLView = getLContext(li)!.lView!;
expect(embeddedLView.constructor.name).toEqual('LEmbeddedView_MyApp_li_1'); expect(embeddedLView.constructor.name).toEqual('LEmbeddedView_MyApp_li_1');
}); });
}); });

View File

@ -47,6 +47,9 @@
{ {
"name": "SimpleChange" "name": "SimpleChange"
}, },
{
"name": "TRACKED_LVIEWS"
},
{ {
"name": "TriggerComponent" "name": "TriggerComponent"
}, },
@ -254,6 +257,9 @@
{ {
"name": "isInlineTemplate" "name": "isInlineTemplate"
}, },
{
"name": "isLView"
},
{ {
"name": "isNodeMatchingSelector" "name": "isNodeMatchingSelector"
}, },
@ -353,6 +359,9 @@
{ {
"name": "setUpAttributes" "name": "setUpAttributes"
}, },
{
"name": "uniqueIdCounter"
},
{ {
"name": "updateTransplantedViewCount" "name": "updateTransplantedViewCount"
}, },

View File

@ -548,6 +548,9 @@
{ {
"name": "THROW_IF_NOT_FOUND" "name": "THROW_IF_NOT_FOUND"
}, },
{
"name": "TRACKED_LVIEWS"
},
{ {
"name": "TRANSITION_ID" "name": "TRANSITION_ID"
}, },
@ -1568,6 +1571,9 @@
{ {
"name": "u" "name": "u"
}, },
{
"name": "uniqueIdCounter"
},
{ {
"name": "unwrapRNode" "name": "unwrapRNode"
}, },

View File

@ -7,7 +7,6 @@
*/ */
import '@angular/compiler'; import '@angular/compiler';
import {ɵwhenRendered as whenRendered} from '@angular/core';
import {withBody} from '@angular/private/testing'; import {withBody} from '@angular/private/testing';
import * as path from 'path'; import * as path from 'path';
@ -18,8 +17,8 @@ describe('functional test for reactive forms', () => {
BUNDLES.forEach((bundle) => { BUNDLES.forEach((bundle) => {
describe(`using ${bundle} bundle`, () => { describe(`using ${bundle} bundle`, () => {
it('should render template form', withBody('<app-root></app-root>', async () => { it('should render template form', withBody('<app-root></app-root>', async () => {
require(path.join(PACKAGE, bundle)); const {whenRendered, bootstrapApp} = require(path.join(PACKAGE, bundle));
await (window as any).waitForApp; await bootstrapApp();
// Reactive forms // Reactive forms
const reactiveFormsComponent = (window as any).reactiveFormsComponent; const reactiveFormsComponent = (window as any).reactiveFormsComponent;

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {Component, NgModule, ɵNgModuleFactory as NgModuleFactory} from '@angular/core'; import {Component, NgModule, ɵNgModuleFactory as NgModuleFactory, ɵwhenRendered as whenRendered} from '@angular/core';
import {FormArray, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {FormArray, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {BrowserModule, platformBrowser} from '@angular/platform-browser'; import {BrowserModule, platformBrowser} from '@angular/platform-browser';
@ -93,5 +93,15 @@ class FormsExampleModule {
} }
} }
(window as any).waitForApp = platformBrowser().bootstrapModuleFactory( function bootstrapApp() {
new NgModuleFactory(FormsExampleModule), {ngZone: 'noop'}); return platformBrowser().bootstrapModuleFactory(
new NgModuleFactory(FormsExampleModule), {ngZone: 'noop'});
}
// This bundle includes `@angular/core` within it which means that the test asserting
// against it will load a different core bundle. These symbols are exposed so that they
// can interact with the correct `@angular/core` instance.
module.exports = {
whenRendered,
bootstrapApp
};

View File

@ -536,6 +536,9 @@
{ {
"name": "THROW_IF_NOT_FOUND" "name": "THROW_IF_NOT_FOUND"
}, },
{
"name": "TRACKED_LVIEWS"
},
{ {
"name": "TRANSITION_ID" "name": "TRANSITION_ID"
}, },
@ -1541,6 +1544,9 @@
{ {
"name": "u" "name": "u"
}, },
{
"name": "uniqueIdCounter"
},
{ {
"name": "unwrapRNode" "name": "unwrapRNode"
}, },

View File

@ -7,7 +7,6 @@
*/ */
import '@angular/compiler'; import '@angular/compiler';
import {ɵwhenRendered as whenRendered} from '@angular/core';
import {withBody} from '@angular/private/testing'; import {withBody} from '@angular/private/testing';
import * as path from 'path'; import * as path from 'path';
@ -18,8 +17,8 @@ describe('functional test for forms', () => {
BUNDLES.forEach((bundle) => { BUNDLES.forEach((bundle) => {
describe(`using ${bundle} bundle`, () => { describe(`using ${bundle} bundle`, () => {
it('should render template form', withBody('<app-root></app-root>', async () => { it('should render template form', withBody('<app-root></app-root>', async () => {
require(path.join(PACKAGE, bundle)); const {bootstrapApp, whenRendered} = require(path.join(PACKAGE, bundle));
await (window as any).waitForApp; await bootstrapApp();
// Template forms // Template forms
const templateFormsComponent = (window as any).templateFormsComponent; const templateFormsComponent = (window as any).templateFormsComponent;

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {Component, NgModule, ɵNgModuleFactory as NgModuleFactory} from '@angular/core'; import {Component, NgModule, ɵNgModuleFactory as NgModuleFactory, ɵwhenRendered as whenRendered} from '@angular/core';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
import {BrowserModule, platformBrowser} from '@angular/platform-browser'; import {BrowserModule, platformBrowser} from '@angular/platform-browser';
@ -70,5 +70,15 @@ class FormsExampleModule {
} }
} }
(window as any).waitForApp = platformBrowser().bootstrapModuleFactory( function bootstrapApp() {
new NgModuleFactory(FormsExampleModule), {ngZone: 'noop'}); return platformBrowser().bootstrapModuleFactory(
new NgModuleFactory(FormsExampleModule), {ngZone: 'noop'});
}
// This bundle includes `@angular/core` within it which means that the test asserting
// against it will load a different core bundle. These symbols are exposed so that they
// can interact with the correct `@angular/core` instance.
module.exports = {
whenRendered,
bootstrapApp
};

View File

@ -41,6 +41,9 @@
{ {
"name": "SimpleChange" "name": "SimpleChange"
}, },
{
"name": "TRACKED_LVIEWS"
},
{ {
"name": "ViewEncapsulation" "name": "ViewEncapsulation"
}, },
@ -173,6 +176,9 @@
{ {
"name": "isInCheckNoChangesMode" "name": "isInCheckNoChangesMode"
}, },
{
"name": "isLView"
},
{ {
"name": "isProceduralRenderer" "name": "isProceduralRenderer"
}, },
@ -236,6 +242,9 @@
{ {
"name": "setSelectedIndex" "name": "setSelectedIndex"
}, },
{
"name": "uniqueIdCounter"
},
{ {
"name": "updateTransplantedViewCount" "name": "updateTransplantedViewCount"
}, },

View File

@ -785,6 +785,9 @@
{ {
"name": "TQuery_" "name": "TQuery_"
}, },
{
"name": "TRACKED_LVIEWS"
},
{ {
"name": "TRANSITION_ID" "name": "TRANSITION_ID"
}, },
@ -2015,6 +2018,9 @@
{ {
"name": "u" "name": "u"
}, },
{
"name": "uniqueIdCounter"
},
{ {
"name": "unwrapElementRef" "name": "unwrapElementRef"
}, },

View File

@ -32,6 +32,9 @@
{ {
"name": "IterableDiffers" "name": "IterableDiffers"
}, },
{
"name": "LContext"
},
{ {
"name": "NG_COMP_DEF" "name": "NG_COMP_DEF"
}, },
@ -110,6 +113,9 @@
{ {
"name": "SkipSelf" "name": "SkipSelf"
}, },
{
"name": "TRACKED_LVIEWS"
},
{ {
"name": "TemplateRef" "name": "TemplateRef"
}, },
@ -380,6 +386,9 @@
{ {
"name": "getLView" "name": "getLView"
}, },
{
"name": "getLViewById"
},
{ {
"name": "getLViewParent" "name": "getLViewParent"
}, },
@ -740,6 +749,9 @@
{ {
"name": "trackByIdentity" "name": "trackByIdentity"
}, },
{
"name": "uniqueIdCounter"
},
{ {
"name": "unwrapRNode" "name": "unwrapRNode"
}, },

View File

@ -9,7 +9,7 @@
import '@angular/core/test/bundling/util/src/reflect_metadata'; import '@angular/core/test/bundling/util/src/reflect_metadata';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {Component, Injectable, NgModule, ViewEncapsulation, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core'; import {Component, Injectable, NgModule, ViewEncapsulation, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent, ɵwhenRendered as whenRendered} from '@angular/core';
class Todo { class Todo {
editing: boolean; editing: boolean;
@ -133,7 +133,9 @@ class TodoStore {
class ToDoAppComponent { class ToDoAppComponent {
newTodoText = ''; newTodoText = '';
constructor(public todoStore: TodoStore) {} constructor(public todoStore: TodoStore) {
(window as any).todoAppComponent = this;
}
cancelEditingTodo(todo: Todo) { cancelEditingTodo(todo: Todo) {
todo.editing = false; todo.editing = false;
@ -200,3 +202,8 @@ class ToDoAppModule {
} }
renderComponent(ToDoAppComponent); renderComponent(ToDoAppComponent);
// This bundle includes `@angular/core` within it which means that the test asserting
// against it will load a different core bundle. These symbols are exposed so that they
// can interact with the correct `@angular/core` instance.
module.exports = {whenRendered};

View File

@ -7,14 +7,9 @@
*/ */
import '@angular/compiler'; import '@angular/compiler';
import {ɵwhenRendered as whenRendered} from '@angular/core';
import {getComponent} from '@angular/core/src/render3';
import {withBody} from '@angular/private/testing'; import {withBody} from '@angular/private/testing';
import * as path from 'path'; import * as path from 'path';
const UTF8 = {
encoding: 'utf-8'
};
const PACKAGE = 'angular/packages/core/test/bundling/todo'; const PACKAGE = 'angular/packages/core/test/bundling/todo';
const BUNDLES = ['bundle.js', 'bundle.min_debug.js', 'bundle.min.js']; const BUNDLES = ['bundle.js', 'bundle.min_debug.js', 'bundle.min.js'];
@ -22,13 +17,12 @@ describe('functional test for todo', () => {
BUNDLES.forEach(bundle => { BUNDLES.forEach(bundle => {
describe(bundle, () => { describe(bundle, () => {
it('should render todo', withBody('<todo-app></todo-app>', async () => { it('should render todo', withBody('<todo-app></todo-app>', async () => {
require(path.join(PACKAGE, bundle)); const {whenRendered} = require(path.join(PACKAGE, bundle));
const toDoAppComponent = getComponent(document.querySelector('todo-app')!);
expect(document.body.textContent).toContain('todos'); expect(document.body.textContent).toContain('todos');
expect(document.body.textContent).toContain('Demonstrate Components'); expect(document.body.textContent).toContain('Demonstrate Components');
expect(document.body.textContent).toContain('4 items left'); expect(document.body.textContent).toContain('4 items left');
document.querySelector('button')!.click(); document.querySelector('button')!.click();
await whenRendered(toDoAppComponent); await whenRendered((window as any).todoAppComponent);
expect(document.body.textContent).toContain('3 items left'); expect(document.body.textContent).toContain('3 items left');
})); }));
}); });

View File

@ -8,7 +8,7 @@
import '@angular/core/test/bundling/util/src/reflect_metadata'; import '@angular/core/test/bundling/util/src/reflect_metadata';
import './translations'; import './translations';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {Component, Injectable, NgModule, ViewEncapsulation, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core'; import {Component, Injectable, NgModule, ViewEncapsulation, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent, ɵwhenRendered as whenRendered} from '@angular/core';
class Todo { class Todo {
editing: boolean; editing: boolean;
@ -127,7 +127,9 @@ class TodoStore {
class ToDoAppComponent { class ToDoAppComponent {
newTodoText = ''; newTodoText = '';
constructor(public todoStore: TodoStore) {} constructor(public todoStore: TodoStore) {
(window as any).todoAppComponent = this;
}
cancelEditingTodo(todo: Todo) { cancelEditingTodo(todo: Todo) {
todo.editing = false; todo.editing = false;
@ -194,3 +196,8 @@ class ToDoAppModule {
} }
renderComponent(ToDoAppComponent); renderComponent(ToDoAppComponent);
// This bundle includes `@angular/core` within it which means that the test asserting
// against it will load a different core bundle. These symbols are exposed so that they
// can interact with the correct `@angular/core` instance.
module.exports = {whenRendered};

View File

@ -8,8 +8,6 @@
import '@angular/localize/init'; import '@angular/localize/init';
import '@angular/compiler'; import '@angular/compiler';
import {ɵwhenRendered as whenRendered} from '@angular/core';
import {getComponent} from '@angular/core/src/render3';
import {clearTranslations} from '@angular/localize'; import {clearTranslations} from '@angular/localize';
import {withBody} from '@angular/private/testing'; import {withBody} from '@angular/private/testing';
import * as path from 'path'; import * as path from 'path';
@ -22,8 +20,7 @@ describe('functional test for todo i18n', () => {
describe(bundle, () => { describe(bundle, () => {
it('should render todo i18n', withBody('<todo-app></todo-app>', async () => { it('should render todo i18n', withBody('<todo-app></todo-app>', async () => {
clearTranslations(); clearTranslations();
require(path.join(PACKAGE, bundle)); const {whenRendered} = require(path.join(PACKAGE, bundle));
const toDoAppComponent = getComponent(document.querySelector('todo-app')!);
expect(document.body.textContent).toContain('liste de tâches'); expect(document.body.textContent).toContain('liste de tâches');
expect(document.body.textContent).toContain('Démontrer les components'); expect(document.body.textContent).toContain('Démontrer les components');
expect(document.body.textContent).toContain('Démontrer NgModules'); expect(document.body.textContent).toContain('Démontrer NgModules');
@ -31,7 +28,7 @@ describe('functional test for todo i18n', () => {
expect(document.querySelector('.new-todo')!.getAttribute('placeholder')) expect(document.querySelector('.new-todo')!.getAttribute('placeholder'))
.toEqual(`Qu'y a-t-il à faire ?`); .toEqual(`Qu'y a-t-il à faire ?`);
document.querySelector('button')!.click(); document.querySelector('button')!.click();
await whenRendered(toDoAppComponent); await whenRendered((window as any).todoAppComponent);
expect(document.body.textContent).toContain('3 tâches restantes'); expect(document.body.textContent).toContain('3 tâches restantes');
})); }));
}); });

View File

@ -9,7 +9,7 @@
import '@angular/core/test/bundling/util/src/reflect_metadata'; import '@angular/core/test/bundling/util/src/reflect_metadata';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {Component, Injectable, NgModule, ɵNgModuleFactory as NgModuleFactory} from '@angular/core'; import {Component, Injectable, NgModule, ɵNgModuleFactory as NgModuleFactory, ɵwhenRendered as whenRendered} from '@angular/core';
import {BrowserModule, platformBrowser} from '@angular/platform-browser'; import {BrowserModule, platformBrowser} from '@angular/platform-browser';
class Todo { class Todo {
@ -195,5 +195,15 @@ class ToDoAppModule {
} }
} }
(window as any).waitForApp = function bootstrapApp() {
platformBrowser().bootstrapModuleFactory(new NgModuleFactory(ToDoAppModule), {ngZone: 'noop'}); return platformBrowser().bootstrapModuleFactory(
new NgModuleFactory(ToDoAppModule), {ngZone: 'noop'});
}
// This bundle includes `@angular/core` within it which means that the test asserting
// against it will load a different core bundle. These symbols are exposed so that they
// can interact with the correct `@angular/core` instance.
module.exports = {
whenRendered,
bootstrapApp
};

View File

@ -7,13 +7,9 @@
*/ */
import '@angular/compiler'; import '@angular/compiler';
import {ɵwhenRendered as whenRendered} from '@angular/core';
import {withBody} from '@angular/private/testing'; import {withBody} from '@angular/private/testing';
import * as path from 'path'; import * as path from 'path';
const UTF8 = {
encoding: 'utf-8'
};
const PACKAGE = 'angular/packages/core/test/bundling/todo_r2'; const PACKAGE = 'angular/packages/core/test/bundling/todo_r2';
const BUNDLES = ['bundle.js', 'bundle.min_debug.js', 'bundle.min.js']; const BUNDLES = ['bundle.js', 'bundle.min_debug.js', 'bundle.min.js'];
@ -22,8 +18,8 @@ describe('functional test for todo', () => {
describe(bundle, () => { describe(bundle, () => {
it('should place styles on the elements within the component', it('should place styles on the elements within the component',
withBody('<todo-app></todo-app>', async () => { withBody('<todo-app></todo-app>', async () => {
require(path.join(PACKAGE, bundle)); const {bootstrapApp, whenRendered} = require(path.join(PACKAGE, bundle));
await (window as any).waitForApp; await bootstrapApp();
const toDoAppComponent = (window as any).toDoAppComponent; const toDoAppComponent = (window as any).toDoAppComponent;
await whenRendered(toDoAppComponent); await whenRendered(toDoAppComponent);

View File

@ -29,9 +29,9 @@ describe('addTNodeAndUpdateInsertBeforeIndex', () => {
it('should add first node', () => { it('should add first node', () => {
const previousTNodes: TNode[] = []; const previousTNodes: TNode[] = [];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(20)); addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(21));
expect(previousTNodes).toEqual([ expect(previousTNodes).toEqual([
matchTNode({index: 20, insertBeforeIndex: null}), matchTNode({index: 21, insertBeforeIndex: null}),
]); ]);
}); });
@ -39,14 +39,14 @@ describe('addTNodeAndUpdateInsertBeforeIndex', () => {
describe('whose index is greater than those already there', () => { describe('whose index is greater than those already there', () => {
it('should not update the `insertBeforeIndex` values', () => { it('should not update the `insertBeforeIndex` values', () => {
const previousTNodes: TNode[] = [ const previousTNodes: TNode[] = [
tPlaceholderElementNode(20),
tPlaceholderElementNode(21), tPlaceholderElementNode(21),
tPlaceholderElementNode(22),
]; ];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(22)); addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(23));
expect(previousTNodes).toEqual([ expect(previousTNodes).toEqual([
matchTNode({index: 20, insertBeforeIndex: null}),
matchTNode({index: 21, insertBeforeIndex: null}), matchTNode({index: 21, insertBeforeIndex: null}),
matchTNode({index: 22, insertBeforeIndex: null}), matchTNode({index: 22, insertBeforeIndex: null}),
matchTNode({index: 23, insertBeforeIndex: null}),
]); ]);
}); });
}); });
@ -54,44 +54,44 @@ describe('addTNodeAndUpdateInsertBeforeIndex', () => {
describe('whose index is smaller than current nodes', () => { describe('whose index is smaller than current nodes', () => {
it('should update the previous insertBeforeIndex', () => { it('should update the previous insertBeforeIndex', () => {
const previousTNodes: TNode[] = [ const previousTNodes: TNode[] = [
tPlaceholderElementNode(21),
tPlaceholderElementNode(22), tPlaceholderElementNode(22),
tPlaceholderElementNode(23),
]; ];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(20)); addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(21));
expect(previousTNodes).toEqual([ expect(previousTNodes).toEqual([
matchTNode({index: 21, insertBeforeIndex: 20}), matchTNode({index: 22, insertBeforeIndex: 21}),
matchTNode({index: 22, insertBeforeIndex: 20}), matchTNode({index: 23, insertBeforeIndex: 21}),
matchTNode({index: 20, insertBeforeIndex: null}), matchTNode({index: 21, insertBeforeIndex: null}),
]); ]);
}); });
it('should not update the previous insertBeforeIndex if it is already set', () => { it('should not update the previous insertBeforeIndex if it is already set', () => {
const previousTNodes: TNode[] = [ const previousTNodes: TNode[] = [
tPlaceholderElementNode(22, 21), tPlaceholderElementNode(23, 22),
tPlaceholderElementNode(23, 21), tPlaceholderElementNode(24, 22),
tPlaceholderElementNode(21), tPlaceholderElementNode(22),
]; ];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(20)); addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(21));
expect(previousTNodes).toEqual([ expect(previousTNodes).toEqual([
matchTNode({index: 23, insertBeforeIndex: 22}),
matchTNode({index: 24, insertBeforeIndex: 22}),
matchTNode({index: 22, insertBeforeIndex: 21}), matchTNode({index: 22, insertBeforeIndex: 21}),
matchTNode({index: 23, insertBeforeIndex: 21}), matchTNode({index: 21, insertBeforeIndex: null}),
matchTNode({index: 21, insertBeforeIndex: 20}),
matchTNode({index: 20, insertBeforeIndex: null}),
]); ]);
}); });
it('should not update the previous insertBeforeIndex if it is created after', () => { it('should not update the previous insertBeforeIndex if it is created after', () => {
const previousTNodes: TNode[] = [ const previousTNodes: TNode[] = [
tPlaceholderElementNode(25, 20), tPlaceholderElementNode(26, 21),
tPlaceholderElementNode(26, 20), tPlaceholderElementNode(27, 21),
tPlaceholderElementNode(20), tPlaceholderElementNode(21),
]; ];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(23)); addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(24));
expect(previousTNodes).toEqual([ expect(previousTNodes).toEqual([
matchTNode({index: 25, insertBeforeIndex: 20}), matchTNode({index: 26, insertBeforeIndex: 21}),
matchTNode({index: 26, insertBeforeIndex: 20}), matchTNode({index: 27, insertBeforeIndex: 21}),
matchTNode({index: 20, insertBeforeIndex: null}), matchTNode({index: 21, insertBeforeIndex: null}),
matchTNode({index: 23, insertBeforeIndex: null}), matchTNode({index: 24, insertBeforeIndex: null}),
]); ]);
}); });
}); });
@ -101,14 +101,14 @@ describe('addTNodeAndUpdateInsertBeforeIndex', () => {
describe('whose index is greater than those already there', () => { describe('whose index is greater than those already there', () => {
it('should not update the `insertBeforeIndex` values', () => { it('should not update the `insertBeforeIndex` values', () => {
const previousTNodes: TNode[] = [ const previousTNodes: TNode[] = [
tPlaceholderElementNode(20),
tPlaceholderElementNode(21), tPlaceholderElementNode(21),
tPlaceholderElementNode(22),
]; ];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(22)); addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(23));
expect(previousTNodes).toEqual([ expect(previousTNodes).toEqual([
matchTNode({index: 20, insertBeforeIndex: 22}), matchTNode({index: 21, insertBeforeIndex: 23}),
matchTNode({index: 21, insertBeforeIndex: 22}), matchTNode({index: 22, insertBeforeIndex: 23}),
matchTNode({index: 22, insertBeforeIndex: null}), matchTNode({index: 23, insertBeforeIndex: null}),
]); ]);
}); });
}); });
@ -116,44 +116,44 @@ describe('addTNodeAndUpdateInsertBeforeIndex', () => {
describe('whose index is smaller than current nodes', () => { describe('whose index is smaller than current nodes', () => {
it('should update the previous insertBeforeIndex', () => { it('should update the previous insertBeforeIndex', () => {
const previousTNodes: TNode[] = [ const previousTNodes: TNode[] = [
tPlaceholderElementNode(21),
tPlaceholderElementNode(22), tPlaceholderElementNode(22),
tPlaceholderElementNode(23),
]; ];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(20)); addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(21));
expect(previousTNodes).toEqual([ expect(previousTNodes).toEqual([
matchTNode({index: 21, insertBeforeIndex: 20}), matchTNode({index: 22, insertBeforeIndex: 21}),
matchTNode({index: 22, insertBeforeIndex: 20}), matchTNode({index: 23, insertBeforeIndex: 21}),
matchTNode({index: 20, insertBeforeIndex: null}), matchTNode({index: 21, insertBeforeIndex: null}),
]); ]);
}); });
it('should not update the previous insertBeforeIndex if it is already set', () => { it('should not update the previous insertBeforeIndex if it is already set', () => {
const previousTNodes: TNode[] = [ const previousTNodes: TNode[] = [
tPlaceholderElementNode(22, 21), tPlaceholderElementNode(23, 22),
tPlaceholderElementNode(23, 21), tPlaceholderElementNode(24, 22),
tPlaceholderElementNode(21), tPlaceholderElementNode(22),
]; ];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(20)); addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(21));
expect(previousTNodes).toEqual([ expect(previousTNodes).toEqual([
matchTNode({index: 23, insertBeforeIndex: 22}),
matchTNode({index: 24, insertBeforeIndex: 22}),
matchTNode({index: 22, insertBeforeIndex: 21}), matchTNode({index: 22, insertBeforeIndex: 21}),
matchTNode({index: 23, insertBeforeIndex: 21}), matchTNode({index: 21, insertBeforeIndex: null}),
matchTNode({index: 21, insertBeforeIndex: 20}),
matchTNode({index: 20, insertBeforeIndex: null}),
]); ]);
}); });
it('should not update the previous insertBeforeIndex if it is created after', () => { it('should not update the previous insertBeforeIndex if it is created after', () => {
const previousTNodes: TNode[] = [ const previousTNodes: TNode[] = [
tPlaceholderElementNode(25, 20), tPlaceholderElementNode(26, 21),
tPlaceholderElementNode(26, 20), tPlaceholderElementNode(27, 21),
tPlaceholderElementNode(20), tPlaceholderElementNode(21),
]; ];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(23)); addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(24));
expect(previousTNodes).toEqual([ expect(previousTNodes).toEqual([
matchTNode({index: 25, insertBeforeIndex: 20}), matchTNode({index: 26, insertBeforeIndex: 21}),
matchTNode({index: 26, insertBeforeIndex: 20}), matchTNode({index: 27, insertBeforeIndex: 21}),
matchTNode({index: 20, insertBeforeIndex: 23}), matchTNode({index: 21, insertBeforeIndex: 24}),
matchTNode({index: 23, insertBeforeIndex: null}), matchTNode({index: 24, insertBeforeIndex: null}),
]); ]);
}); });
}); });
@ -162,22 +162,22 @@ describe('addTNodeAndUpdateInsertBeforeIndex', () => {
describe('scenario', () => { describe('scenario', () => {
it('should rearrange the nodes', () => { it('should rearrange the nodes', () => {
const previousTNodes: TNode[] = []; const previousTNodes: TNode[] = [];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(22));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(28));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(24));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(25));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(29));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(23)); addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(23));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(27)); addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(29));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(25));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(26));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(30));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(24));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(28));
expect(previousTNodes).toEqual([ expect(previousTNodes).toEqual([
matchTNode({index: 22, insertBeforeIndex: 29}), matchTNode({index: 23, insertBeforeIndex: 30}),
matchTNode({index: 28, insertBeforeIndex: 24}), matchTNode({index: 29, insertBeforeIndex: 25}),
matchTNode({index: 24, insertBeforeIndex: 29}), matchTNode({index: 25, insertBeforeIndex: 30}),
matchTNode({index: 25, insertBeforeIndex: 29}), matchTNode({index: 26, insertBeforeIndex: 30}),
matchTNode({index: 29, insertBeforeIndex: null}), matchTNode({index: 30, insertBeforeIndex: null}),
matchTNode({index: 23, insertBeforeIndex: null}), matchTNode({index: 24, insertBeforeIndex: null}),
matchTNode({index: 27, insertBeforeIndex: null}), matchTNode({index: 28, insertBeforeIndex: null}),
]); ]);
}); });
}); });
}); });

View File

@ -26,8 +26,8 @@ describe('i18n_parse', () => {
const tI18n = toT18n('some text'); const tI18n = toT18n('some text');
expect(tI18n).toEqual(matchTI18n({ expect(tI18n).toEqual(matchTI18n({
create: matchDebug([ create: matchDebug([
'lView[22] = document.createText("some text");', 'lView[23] = document.createText("some text");',
'parent.appendChild(lView[22]);', 'parent.appendChild(lView[23]);',
]), ]),
update: [] as unknown as I18nUpdateOpCodes, update: [] as unknown as I18nUpdateOpCodes,
})); }));
@ -40,16 +40,16 @@ describe('i18n_parse', () => {
// TData | LView // TData | LView
// ---------------------------+------------------------------- // ---------------------------+-------------------------------
// ----- DECL ----- // ----- DECL -----
// 20: TI18n | // 21: TI18n |
// ----- VARS ----- // ----- VARS -----
// 21: Binding for ICU | // 22: Binding for ICU |
// ----- EXPANDO ----- // ----- EXPANDO -----
// 22: null | #text(before|) // 23: null | #text(before|)
// 23: TIcu | <!-- ICU 20:0 --> // 24: TIcu | <!-- ICU 20:0 -->
// 24: null | currently selected ICU case // 25: null | currently selected ICU case
// 25: null | #text(caseA) // 26: null | #text(caseA)
// 26: null | #text(otherCase) // 27: null | #text(otherCase)
// 27: null | #text(|after) // 28: null | #text(|after)
const tI18n = toT18n(`before|{ const tI18n = toT18n(`before|{
<EFBFBD>0<EFBFBD>, select, <EFBFBD>0<EFBFBD>, select,
A {caseA} A {caseA}
@ -57,30 +57,30 @@ describe('i18n_parse', () => {
}|after`); }|after`);
expect(tI18n).toEqual(matchTI18n({ expect(tI18n).toEqual(matchTI18n({
create: matchDebug([ create: matchDebug([
'lView[22] = document.createText("before|");', 'lView[23] = document.createText("before|");',
'parent.appendChild(lView[22]);',
'lView[23] = document.createComment("ICU 20:0");',
'parent.appendChild(lView[23]);', 'parent.appendChild(lView[23]);',
'lView[27] = document.createText("|after");', 'lView[24] = document.createComment("ICU 21:0");',
'parent.appendChild(lView[27]);', 'parent.appendChild(lView[24]);',
'lView[28] = document.createText("|after");',
'parent.appendChild(lView[28]);',
]), ]),
update: matchDebug([ update: matchDebug([
'if (mask & 0b1) { icuSwitchCase(23, `${lView[i-1]}`); }', 'if (mask & 0b1) { icuSwitchCase(24, `${lView[i-1]}`); }',
]) ])
})); }));
expect(getTIcu(fixture.tView, 23)).toEqual(matchTIcu({ expect(getTIcu(fixture.tView, 24)).toEqual(matchTIcu({
type: IcuType.select, type: IcuType.select,
anchorIdx: 23, anchorIdx: 24,
currentCaseLViewIndex: 24, currentCaseLViewIndex: 25,
cases: ['A', 'other'], cases: ['A', 'other'],
create: [ create: [
matchDebug([ matchDebug([
'lView[25] = document.createTextNode("caseA")', 'lView[26] = document.createTextNode("caseA")',
'(lView[0] as Element).appendChild(lView[25])' '(lView[0] as Element).appendChild(lView[26])'
]), ]),
matchDebug([ matchDebug([
'lView[26] = document.createTextNode("otherCase")', 'lView[27] = document.createTextNode("otherCase")',
'(lView[0] as Element).appendChild(lView[26])', '(lView[0] as Element).appendChild(lView[27])',
]) ])
], ],
update: [ update: [
@ -88,29 +88,29 @@ describe('i18n_parse', () => {
matchDebug([]), matchDebug([]),
], ],
remove: [ remove: [
matchDebug(['remove(lView[25])']),
matchDebug(['remove(lView[26])']), matchDebug(['remove(lView[26])']),
matchDebug(['remove(lView[27])']),
], ],
})); }));
fixture.apply(() => { fixture.apply(() => {
applyCreateOpCodes(fixture.lView, tI18n.create, fixture.host, null); applyCreateOpCodes(fixture.lView, tI18n.create, fixture.host, null);
expect(fixture.host.innerHTML).toEqual('before|<!--ICU 20:0-->|after'); expect(fixture.host.innerHTML).toEqual('before|<!--ICU 21:0-->|after');
}); });
fixture.apply(() => { fixture.apply(() => {
ɵɵi18nExp('A'); ɵɵi18nExp('A');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20; ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('before|caseA<!--ICU 20:0-->|after'); expect(fixture.host.innerHTML).toEqual('before|caseA<!--ICU 21:0-->|after');
}); });
fixture.apply(() => { fixture.apply(() => {
ɵɵi18nExp('x'); ɵɵi18nExp('x');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20; ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('before|otherCase<!--ICU 20:0-->|after'); expect(fixture.host.innerHTML).toEqual('before|otherCase<!--ICU 21:0-->|after');
}); });
fixture.apply(() => { fixture.apply(() => {
ɵɵi18nExp('A'); ɵɵi18nExp('A');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20; ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('before|caseA<!--ICU 20:0-->|after'); expect(fixture.host.innerHTML).toEqual('before|caseA<!--ICU 21:0-->|after');
}); });
}); });
@ -122,23 +122,23 @@ describe('i18n_parse', () => {
}`); }`);
fixture.apply(() => { fixture.apply(() => {
applyCreateOpCodes(fixture.lView, tI18n.create, fixture.host, null); applyCreateOpCodes(fixture.lView, tI18n.create, fixture.host, null);
expect(fixture.host.innerHTML).toEqual('<!--ICU 20:0-->'); expect(fixture.host.innerHTML).toEqual('<!--ICU 21:0-->');
}); });
fixture.apply(() => { fixture.apply(() => {
ɵɵi18nExp('A'); ɵɵi18nExp('A');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20; ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('Hello <b>world<i>!</i></b><!--ICU 20:0-->'); expect(fixture.host.innerHTML).toEqual('Hello <b>world<i>!</i></b><!--ICU 21:0-->');
}); });
fixture.apply(() => { fixture.apply(() => {
ɵɵi18nExp('x'); ɵɵi18nExp('x');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20; ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML) expect(fixture.host.innerHTML)
.toEqual('<div>nestedOther<!--nested ICU 0--></div><!--ICU 20:0-->'); .toEqual('<div>nestedOther<!--nested ICU 0--></div><!--ICU 21:0-->');
}); });
fixture.apply(() => { fixture.apply(() => {
ɵɵi18nExp('A'); ɵɵi18nExp('A');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20; ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('Hello <b>world<i>!</i></b><!--ICU 20:0-->'); expect(fixture.host.innerHTML).toEqual('Hello <b>world<i>!</i></b><!--ICU 21:0-->');
}); });
}); });
@ -148,21 +148,21 @@ describe('i18n_parse', () => {
// TData | LView // TData | LView
// ---------------------------+------------------------------- // ---------------------------+-------------------------------
// ----- DECL ----- // ----- DECL -----
// 20: TI18n | // 21: TI18n |
// ----- VARS ----- // ----- VARS -----
// 21: Binding for parent ICU | // 22: Binding for parent ICU |
// 22: Binding for child ICU |
// 23: Binding for child ICU | // 23: Binding for child ICU |
// 24: Binding for child ICU |
// ----- EXPANDO ----- // ----- EXPANDO -----
// 24: TIcu (parent) | <!-- ICU 20:0 --> // 25: TIcu (parent) | <!-- ICU 20:0 -->
// 25: null | currently selected ICU case // 26: null | currently selected ICU case
// 26: null | #text( parentA ) // 27: null | #text( parentA )
// 27: TIcu (child) | <!-- nested ICU 0 --> // 28: TIcu (child) | <!-- nested ICU 0 -->
// 28: null | currently selected ICU case // 29: null | currently selected ICU case
// 29: null | #text(nested0) // 30: null | #text(nested0)
// 30: null | #text({{<7B>2<EFBFBD>}}) // 31: null | #text({{<7B>2<EFBFBD>}})
// 31: null | #text( ) // 32: null | #text( )
// 32: null | #text( parentOther ) // 33: null | #text( parentOther )
const tI18n = toT18n(`{ const tI18n = toT18n(`{
<EFBFBD>0<EFBFBD>, select, <EFBFBD>0<EFBFBD>, select,
A {parentA {<EFBFBD>1<EFBFBD>, select, 0 {nested0} other {<EFBFBD>2<EFBFBD>}}!} A {parentA {<EFBFBD>1<EFBFBD>, select, 0 {nested0} other {<EFBFBD>2<EFBFBD>}}!}
@ -170,32 +170,32 @@ describe('i18n_parse', () => {
}`); }`);
expect(tI18n).toEqual(matchTI18n({ expect(tI18n).toEqual(matchTI18n({
create: matchDebug([ create: matchDebug([
'lView[24] = document.createComment("ICU 20:0");', 'lView[25] = document.createComment("ICU 21:0");',
'parent.appendChild(lView[24]);', 'parent.appendChild(lView[25]);',
]), ]),
update: matchDebug([ update: matchDebug([
'if (mask & 0b1) { icuSwitchCase(24, `${lView[i-1]}`); }', 'if (mask & 0b1) { icuSwitchCase(25, `${lView[i-1]}`); }',
'if (mask & 0b10) { icuSwitchCase(27, `${lView[i-2]}`); }', 'if (mask & 0b10) { icuSwitchCase(28, `${lView[i-2]}`); }',
'if (mask & 0b100) { icuUpdateCase(27); }', 'if (mask & 0b100) { icuUpdateCase(28); }',
]), ]),
})); }));
expect(getTIcu(fixture.tView, 24)).toEqual(matchTIcu({ expect(getTIcu(fixture.tView, 25)).toEqual(matchTIcu({
type: IcuType.select, type: IcuType.select,
anchorIdx: 24, anchorIdx: 25,
currentCaseLViewIndex: 25, currentCaseLViewIndex: 26,
cases: ['A', 'other'], cases: ['A', 'other'],
create: [ create: [
matchDebug([ matchDebug([
'lView[26] = document.createTextNode("parentA ")', 'lView[27] = document.createTextNode("parentA ")',
'(lView[0] as Element).appendChild(lView[26])',
'lView[27] = document.createComment("nested ICU 0")',
'(lView[0] as Element).appendChild(lView[27])', '(lView[0] as Element).appendChild(lView[27])',
'lView[31] = document.createTextNode("!")', 'lView[28] = document.createComment("nested ICU 0")',
'(lView[0] as Element).appendChild(lView[31])', '(lView[0] as Element).appendChild(lView[28])',
'lView[32] = document.createTextNode("!")',
'(lView[0] as Element).appendChild(lView[32])',
]), ]),
matchDebug([ matchDebug([
'lView[32] = document.createTextNode("parentOther")', 'lView[33] = document.createTextNode("parentOther")',
'(lView[0] as Element).appendChild(lView[32])', '(lView[0] as Element).appendChild(lView[33])',
]) ])
], ],
update: [ update: [
@ -204,47 +204,47 @@ describe('i18n_parse', () => {
], ],
remove: [ remove: [
matchDebug([ matchDebug([
'remove(lView[26])',
'removeNestedICU(27)',
'remove(lView[27])', 'remove(lView[27])',
'remove(lView[31])', 'removeNestedICU(28)',
'remove(lView[28])',
'remove(lView[32])',
]), ]),
matchDebug([ matchDebug([
'remove(lView[32])', 'remove(lView[33])',
]) ])
], ],
})); }));
expect(getTIcu(fixture.tView, 27)).toEqual(matchTIcu({ expect(getTIcu(fixture.tView, 28)).toEqual(matchTIcu({
type: IcuType.select, type: IcuType.select,
anchorIdx: 27, anchorIdx: 28,
currentCaseLViewIndex: 28, currentCaseLViewIndex: 29,
cases: ['0', 'other'], cases: ['0', 'other'],
create: [ create: [
matchDebug([ matchDebug([
'lView[29] = document.createTextNode("nested0")', 'lView[30] = document.createTextNode("nested0")',
'(lView[0] as Element).appendChild(lView[29])' '(lView[0] as Element).appendChild(lView[30])'
]), ]),
matchDebug([ matchDebug([
'lView[30] = document.createTextNode("")', 'lView[31] = document.createTextNode("")',
'(lView[0] as Element).appendChild(lView[30])', '(lView[0] as Element).appendChild(lView[31])',
]) ])
], ],
update: [ update: [
matchDebug([]), matchDebug([]),
matchDebug([ matchDebug([
'if (mask & 0b100) { (lView[30] as Text).textContent = `${lView[i-3]}`; }', 'if (mask & 0b100) { (lView[31] as Text).textContent = `${lView[i-3]}`; }',
]), ]),
], ],
remove: [ remove: [
matchDebug(['remove(lView[29])']),
matchDebug(['remove(lView[30])']), matchDebug(['remove(lView[30])']),
matchDebug(['remove(lView[31])']),
], ],
})); }));
fixture.apply(() => { fixture.apply(() => {
applyCreateOpCodes(fixture.lView, tI18n.create, fixture.host, null); applyCreateOpCodes(fixture.lView, tI18n.create, fixture.host, null);
expect(fixture.host.innerHTML).toEqual('<!--ICU 20:0-->'); expect(fixture.host.innerHTML).toEqual('<!--ICU 21:0-->');
}); });
fixture.apply(() => { fixture.apply(() => {
ɵɵi18nExp('A'); ɵɵi18nExp('A');
@ -252,28 +252,28 @@ describe('i18n_parse', () => {
ɵɵi18nExp('value1'); ɵɵi18nExp('value1');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20; ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML) expect(fixture.host.innerHTML)
.toEqual('parentA nested0<!--nested ICU 0-->!<!--ICU 20:0-->'); .toEqual('parentA nested0<!--nested ICU 0-->!<!--ICU 21:0-->');
}); });
fixture.apply(() => { fixture.apply(() => {
ɵɵi18nExp('A'); ɵɵi18nExp('A');
ɵɵi18nExp('x'); ɵɵi18nExp('x');
ɵɵi18nExp('value1'); ɵɵi18nExp('value1');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20; ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('parentA value1<!--nested ICU 0-->!<!--ICU 20:0-->'); expect(fixture.host.innerHTML).toEqual('parentA value1<!--nested ICU 0-->!<!--ICU 21:0-->');
}); });
fixture.apply(() => { fixture.apply(() => {
ɵɵi18nExp('x'); ɵɵi18nExp('x');
ɵɵi18nExp('x'); ɵɵi18nExp('x');
ɵɵi18nExp('value2'); ɵɵi18nExp('value2');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20; ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('parentOther<!--ICU 20:0-->'); expect(fixture.host.innerHTML).toEqual('parentOther<!--ICU 21:0-->');
}); });
fixture.apply(() => { fixture.apply(() => {
ɵɵi18nExp('A'); ɵɵi18nExp('A');
ɵɵi18nExp('A'); ɵɵi18nExp('A');
ɵɵi18nExp('value2'); ɵɵi18nExp('value2');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20; ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('parentA value2<!--nested ICU 0-->!<!--ICU 20:0-->'); expect(fixture.host.innerHTML).toEqual('parentA value2<!--nested ICU 0-->!<!--ICU 21:0-->');
}); });
}); });
}); });

View File

@ -128,7 +128,7 @@ describe('Runtime i18n', () => {
}, undefined, nbConsts, HEADER_OFFSET + index); }, undefined, nbConsts, HEADER_OFFSET + index);
expect((opCodes as any).update.debug).toEqual([ expect((opCodes as any).update.debug).toEqual([
'if (mask & 0b1) { (lView[22] as Text).textContent = `Hello ${lView[i-1]}!`; }' 'if (mask & 0b1) { (lView[23] as Text).textContent = `Hello ${lView[i-1]}!`; }'
]); ]);
expect(opCodes).toEqual({ expect(opCodes).toEqual({
@ -137,7 +137,7 @@ describe('Runtime i18n', () => {
`parent.appendChild(lView[${HEADER_OFFSET + 2}]);`, `parent.appendChild(lView[${HEADER_OFFSET + 2}]);`,
]), ]),
update: matchDebug([ update: matchDebug([
'if (mask & 0b1) { (lView[22] as Text).textContent = `Hello ${lView[i-1]}!`; }', 'if (mask & 0b1) { (lView[23] as Text).textContent = `Hello ${lView[i-1]}!`; }',
]), ]),
}); });
}); });
@ -158,7 +158,7 @@ describe('Runtime i18n', () => {
`parent.appendChild(lView[${HEADER_OFFSET + 2}]);`, `parent.appendChild(lView[${HEADER_OFFSET + 2}]);`,
]), ]),
update: matchDebug([ update: matchDebug([
'if (mask & 0b11) { (lView[22] as Text).textContent = `Hello ${lView[i-1]} and ${lView[i-2]}, again ${lView[i-1]}!`; }', 'if (mask & 0b11) { (lView[23] as Text).textContent = `Hello ${lView[i-1]} and ${lView[i-2]}, again ${lView[i-1]}!`; }',
]), ]),
}); });
}); });
@ -193,7 +193,7 @@ describe('Runtime i18n', () => {
`parent.appendChild(lView[${HEADER_OFFSET + 4}]);`, `parent.appendChild(lView[${HEADER_OFFSET + 4}]);`,
]), ]),
update: matchDebug([ update: matchDebug([
'if (mask & 0b1) { (lView[23] as Text).textContent = `${lView[i-1]} is rendered as: `; }', 'if (mask & 0b1) { (lView[24] as Text).textContent = `${lView[i-1]} is rendered as: `; }',
]), ]),
}); });
@ -249,26 +249,26 @@ describe('Runtime i18n', () => {
expect(opCodes).toEqual({ expect(opCodes).toEqual({
create: matchDebug([ create: matchDebug([
`lView[${HEADER_OFFSET + 2}] = document.createComment("ICU 21:0");`, `lView[${HEADER_OFFSET + 2}] = document.createComment("ICU 22:0");`,
`parent.appendChild(lView[${HEADER_OFFSET + 2}]);`, `parent.appendChild(lView[${HEADER_OFFSET + 2}]);`,
]), ]),
update: matchDebug([ update: matchDebug([
'if (mask & 0b1) { icuSwitchCase(22, `${lView[i-1]}`); }', 'if (mask & 0b1) { icuSwitchCase(23, `${lView[i-1]}`); }',
'if (mask & 0b1) { icuUpdateCase(22); }', 'if (mask & 0b1) { icuUpdateCase(23); }',
]), ]),
}); });
expect(getTIcu(tView, 22)).toEqual(<TIcu>{ expect(getTIcu(tView, 23)).toEqual(<TIcu>{
type: 1, type: 1,
currentCaseLViewIndex: 23, currentCaseLViewIndex: 24,
anchorIdx: 22, anchorIdx: 23,
cases: ['0', '1', 'other'], cases: ['0', '1', 'other'],
create: [ create: [
matchDebug([ matchDebug([
`lView[${HEADER_OFFSET + 4}] = document.createTextNode("no ")`, `lView[${HEADER_OFFSET + 4}] = document.createTextNode("no ")`,
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 4}])`, `(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 4}])`,
'lView[25] = document.createElement("b")', 'lView[26] = document.createElement("b")',
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 5}])`, `(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 5}])`,
'(lView[25] as Element).setAttribute("title", "none")', '(lView[26] as Element).setAttribute("title", "none")',
`lView[${HEADER_OFFSET + 6}] = document.createTextNode("emails")`, `lView[${HEADER_OFFSET + 6}] = document.createTextNode("emails")`,
`(lView[${HEADER_OFFSET + 5}] as Element).appendChild(lView[${HEADER_OFFSET + 6}])`, `(lView[${HEADER_OFFSET + 5}] as Element).appendChild(lView[${HEADER_OFFSET + 6}])`,
`lView[${HEADER_OFFSET + 7}] = document.createTextNode("!")`, `lView[${HEADER_OFFSET + 7}] = document.createTextNode("!")`,
@ -277,41 +277,41 @@ describe('Runtime i18n', () => {
matchDebug([ matchDebug([
`lView[${HEADER_OFFSET + 8}] = document.createTextNode("one ")`, `lView[${HEADER_OFFSET + 8}] = document.createTextNode("one ")`,
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 8}])`, `(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 8}])`,
'lView[29] = document.createElement("i")', 'lView[30] = document.createElement("i")',
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 9}])`, `(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 9}])`,
'lView[30] = document.createTextNode("email")', 'lView[31] = document.createTextNode("email")',
'(lView[29] as Element).appendChild(lView[30])', '(lView[30] as Element).appendChild(lView[31])',
]), ]),
matchDebug([ matchDebug([
'lView[31] = document.createTextNode("")', 'lView[32] = document.createTextNode("")',
'(lView[20] as Element).appendChild(lView[31])', '(lView[21] as Element).appendChild(lView[32])',
'lView[32] = document.createElement("span")', 'lView[33] = document.createElement("span")',
'(lView[20] as Element).appendChild(lView[32])', '(lView[21] as Element).appendChild(lView[33])',
'lView[33] = document.createTextNode("emails")', 'lView[34] = document.createTextNode("emails")',
'(lView[32] as Element).appendChild(lView[33])', '(lView[33] as Element).appendChild(lView[34])',
]), ]),
], ],
remove: [ remove: [
matchDebug([ matchDebug([
'remove(lView[24])',
'remove(lView[25])', 'remove(lView[25])',
'remove(lView[27])', 'remove(lView[26])',
]),
matchDebug([
'remove(lView[28])', 'remove(lView[28])',
'remove(lView[29])',
]), ]),
matchDebug([ matchDebug([
'remove(lView[31])', 'remove(lView[29])',
'remove(lView[30])',
]),
matchDebug([
'remove(lView[32])', 'remove(lView[32])',
'remove(lView[33])',
]), ]),
], ],
update: [ update: [
matchDebug([]), matchDebug([]),
matchDebug([]), matchDebug([]),
matchDebug([ matchDebug([
'if (mask & 0b1) { (lView[31] as Text).textContent = `${lView[i-1]} `; }', 'if (mask & 0b1) { (lView[32] as Text).textContent = `${lView[i-1]} `; }',
'if (mask & 0b10) { (lView[32] as Element).setAttribute(\'title\', `${lView[i-2]}`); }', 'if (mask & 0b10) { (lView[33] as Element).setAttribute(\'title\', `${lView[i-2]}`); }',
]), ]),
] ]
}); });
@ -336,19 +336,19 @@ describe('Runtime i18n', () => {
expect(opCodes).toEqual({ expect(opCodes).toEqual({
create: matchDebug([ create: matchDebug([
`lView[${HEADER_OFFSET + 2}] = document.createComment("ICU 21:0");`, `lView[${HEADER_OFFSET + 2}] = document.createComment("ICU 22:0");`,
`parent.appendChild(lView[${HEADER_OFFSET + 2}]);`, `parent.appendChild(lView[${HEADER_OFFSET + 2}]);`,
]), ]),
update: matchDebug([ update: matchDebug([
'if (mask & 0b1) { icuSwitchCase(22, `${lView[i-1]}`); }', 'if (mask & 0b1) { icuSwitchCase(23, `${lView[i-1]}`); }',
'if (mask & 0b10) { icuSwitchCase(26, `${lView[i-2]}`); }', 'if (mask & 0b10) { icuSwitchCase(27, `${lView[i-2]}`); }',
'if (mask & 0b1) { icuUpdateCase(22); }', 'if (mask & 0b1) { icuUpdateCase(23); }',
]), ]),
}); });
expect(getTIcu(tView, 22)).toEqual({ expect(getTIcu(tView, 23)).toEqual({
type: 1, type: 1,
anchorIdx: 22, anchorIdx: 23,
currentCaseLViewIndex: 23, currentCaseLViewIndex: 24,
cases: ['0', 'other'], cases: ['0', 'other'],
create: [ create: [
matchDebug([ matchDebug([
@ -358,34 +358,34 @@ describe('Runtime i18n', () => {
matchDebug([ matchDebug([
`lView[${HEADER_OFFSET + 5}] = document.createTextNode("")`, `lView[${HEADER_OFFSET + 5}] = document.createTextNode("")`,
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 5}])`, `(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 5}])`,
'lView[26] = document.createComment("nested ICU 0")', 'lView[27] = document.createComment("nested ICU 0")',
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 6}])`, `(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 6}])`,
'lView[31] = document.createTextNode("!")', 'lView[32] = document.createTextNode("!")',
'(lView[20] as Element).appendChild(lView[31])', '(lView[21] as Element).appendChild(lView[32])',
]), ]),
], ],
update: [ update: [
matchDebug([]), matchDebug([]),
matchDebug([ matchDebug([
'if (mask & 0b1) { (lView[25] as Text).textContent = `${lView[i-1]} `; }', 'if (mask & 0b1) { (lView[26] as Text).textContent = `${lView[i-1]} `; }',
]), ]),
], ],
remove: [ remove: [
matchDebug([ matchDebug([
'remove(lView[24])', 'remove(lView[25])',
]), ]),
matchDebug([ matchDebug([
'remove(lView[25])',
'removeNestedICU(26)',
'remove(lView[26])', 'remove(lView[26])',
'remove(lView[31])', 'removeNestedICU(27)',
'remove(lView[27])',
'remove(lView[32])',
]), ]),
], ],
}); });
expect(tView.data[26]).toEqual({ expect(tView.data[27]).toEqual({
type: 0, type: 0,
anchorIdx: 26, anchorIdx: 27,
currentCaseLViewIndex: 27, currentCaseLViewIndex: 28,
cases: ['cat', 'dog', 'other'], cases: ['cat', 'dog', 'other'],
create: [ create: [
matchDebug([ matchDebug([
@ -397,8 +397,8 @@ describe('Runtime i18n', () => {
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 9}])`, `(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 9}])`,
]), ]),
matchDebug([ matchDebug([
'lView[30] = document.createTextNode("animals")', 'lView[31] = document.createTextNode("animals")',
'(lView[20] as Element).appendChild(lView[30])', '(lView[21] as Element).appendChild(lView[31])',
]), ]),
], ],
update: [ update: [
@ -407,9 +407,9 @@ describe('Runtime i18n', () => {
matchDebug([]), matchDebug([]),
], ],
remove: [ remove: [
matchDebug(['remove(lView[28])']),
matchDebug(['remove(lView[29])']), matchDebug(['remove(lView[29])']),
matchDebug(['remove(lView[30])']), matchDebug(['remove(lView[30])']),
matchDebug(['remove(lView[31])']),
], ],
}); });
}); });
@ -428,7 +428,7 @@ describe('Runtime i18n', () => {
}, undefined, nbConsts, HEADER_OFFSET + index); }, undefined, nbConsts, HEADER_OFFSET + index);
expect(opCodes).toEqual(matchDebug([ expect(opCodes).toEqual(matchDebug([
'if (mask & 0b1) { (lView[20] as Element).setAttribute(\'title\', `Hello ${lView[i-1]}!`); }', 'if (mask & 0b1) { (lView[21] as Element).setAttribute(\'title\', `Hello ${lView[i-1]}!`); }',
])); ]));
}); });
@ -444,7 +444,7 @@ describe('Runtime i18n', () => {
}, undefined, nbConsts, HEADER_OFFSET + index); }, undefined, nbConsts, HEADER_OFFSET + index);
expect(opCodes).toEqual(matchDebug([ expect(opCodes).toEqual(matchDebug([
'if (mask & 0b11) { (lView[20] as Element).setAttribute(\'title\', `Hello ${lView[i-1]} and ${lView[i-2]}, again ${lView[i-1]}!`); }', 'if (mask & 0b11) { (lView[21] as Element).setAttribute(\'title\', `Hello ${lView[i-1]} and ${lView[i-2]}, again ${lView[i-1]}!`); }',
])); ]));
}); });
@ -460,8 +460,8 @@ describe('Runtime i18n', () => {
}, undefined, nbConsts, HEADER_OFFSET + index); }, undefined, nbConsts, HEADER_OFFSET + index);
expect(opCodes).toEqual(matchDebug([ expect(opCodes).toEqual(matchDebug([
'if (mask & 0b1) { (lView[20] as Element).setAttribute(\'title\', `Hello ${lView[i-1]}!`); }', 'if (mask & 0b1) { (lView[21] as Element).setAttribute(\'title\', `Hello ${lView[i-1]}!`); }',
'if (mask & 0b1) { (lView[20] as Element).setAttribute(\'aria-label\', `Hello ${lView[i-1]}!`); }', 'if (mask & 0b1) { (lView[21] as Element).setAttribute(\'aria-label\', `Hello ${lView[i-1]}!`); }',
])); ]));
}); });
}); });
@ -702,8 +702,8 @@ describe('Runtime i18n', () => {
], ],
})); }));
expect(ti18n.update).toEqual(matchDebug([ expect(ti18n.update).toEqual(matchDebug([
'if (mask & 0b1) { (lView[50] as Text).textContent = `${lView[i-1]} `; }', 'if (mask & 0b1) { (lView[51] as Text).textContent = `${lView[i-1]} `; }',
'if (mask & 0b10) { (lView[51] as Text).textContent = `${lView[i-2]}`; }' 'if (mask & 0b10) { (lView[52] as Text).textContent = `${lView[i-2]}`; }'
])); ]));
const lViewDebug = fixture.lView.debug!; const lViewDebug = fixture.lView.debug!;
expect(lViewDebug.template).toEqual('<div>{{?}}<Placeholder>{{?}}</Placeholder>!</div>'); expect(lViewDebug.template).toEqual('<div>{{?}}<Placeholder>{{?}}</Placeholder>!</div>');
@ -714,16 +714,16 @@ describe('Runtime i18n', () => {
fixture.tView, 0, fixture.lView, HEADER_OFFSET + 1, 'Hello <20>*2:1<>World<6C>/*2:1<>!', -1); fixture.tView, 0, fixture.lView, HEADER_OFFSET + 1, 'Hello <20>*2:1<>World<6C>/*2:1<>!', -1);
const ti18n = fixture.tView.data[HEADER_OFFSET + 1] as TI18n; const ti18n = fixture.tView.data[HEADER_OFFSET + 1] as TI18n;
expect(ti18n.create.debug).toEqual([ expect(ti18n.create.debug).toEqual([
'lView[50] = document.createText("Hello ");', 'lView[51] = document.createText("Hello ");',
'parent.appendChild(lView[50]);',
'lView[51] = document.createText("!");',
'parent.appendChild(lView[51]);', 'parent.appendChild(lView[51]);',
'lView[52] = document.createText("!");',
'parent.appendChild(lView[52]);',
]); ]);
// Leave behind `Placeholder` to be picked up by `TNode` creation. // Leave behind `Placeholder` to be picked up by `TNode` creation.
// It should insert itself in front of "!" // It should insert itself in front of "!"
expect(fixture.tView.data[HEADER_OFFSET + 2]).toEqual(matchTNode({ expect(fixture.tView.data[HEADER_OFFSET + 2]).toEqual(matchTNode({
type: TNodeType.Placeholder, type: TNodeType.Placeholder,
insertBeforeIndex: 51, insertBeforeIndex: 52,
})); }));
}); });
}); });

View File

@ -242,7 +242,7 @@ describe('lView_debug', () => {
cumulativeBloom: jasmine.anything(), cumulativeBloom: jasmine.anything(),
providers: [MyChild.ɵdir], providers: [MyChild.ɵdir],
viewProviders: [], viewProviders: [],
parentInjectorIndex: 22, parentInjectorIndex: 23,
}); });
}); });
}); });

View File

@ -13,7 +13,7 @@ import {AttributeMarker, ɵɵadvance, ɵɵattribute, ɵɵdefineComponent, ɵɵde
import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵprojection, ɵɵprojectionDef, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all'; import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵprojection, ɵɵprojectionDef, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all';
import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RenderFlags} from '../../src/render3/interfaces/definition';
import {domRendererFactory3, Renderer3, RendererFactory3} from '../../src/render3/interfaces/renderer'; import {domRendererFactory3, Renderer3, RendererFactory3} from '../../src/render3/interfaces/renderer';
import {CONTEXT, HEADER_OFFSET} from '../../src/render3/interfaces/view'; import {CONTEXT, HEADER_OFFSET, ID, LView} from '../../src/render3/interfaces/view';
import {ɵɵsanitizeUrl} from '../../src/sanitization/sanitization'; import {ɵɵsanitizeUrl} from '../../src/sanitization/sanitization';
import {Sanitizer} from '../../src/sanitization/sanitizer'; import {Sanitizer} from '../../src/sanitization/sanitizer';
import {SecurityContext} from '../../src/sanitization/security'; import {SecurityContext} from '../../src/sanitization/security';
@ -399,19 +399,17 @@ describe('element discovery', () => {
const section = fixture.hostElement.querySelector('section')!; const section = fixture.hostElement.querySelector('section')!;
const sectionContext = getLContext(section)!; const sectionContext = getLContext(section)!;
const sectionLView = sectionContext.lView!;
expect(sectionContext.nodeIndex).toEqual(HEADER_OFFSET); expect(sectionContext.nodeIndex).toEqual(HEADER_OFFSET);
expect(sectionLView.length).toBeGreaterThan(HEADER_OFFSET); expect(sectionContext.lView!.length).toBeGreaterThan(HEADER_OFFSET);
expect(sectionContext.native).toBe(section); expect(sectionContext.native).toBe(section);
const div = fixture.hostElement.querySelector('div')!; const div = fixture.hostElement.querySelector('div')!;
const divContext = getLContext(div)!; const divContext = getLContext(div)!;
const divLView = divContext.lView!;
expect(divContext.nodeIndex).toEqual(HEADER_OFFSET + 1); expect(divContext.nodeIndex).toEqual(HEADER_OFFSET + 1);
expect(divLView.length).toBeGreaterThan(HEADER_OFFSET); expect(divContext.lView!.length).toBeGreaterThan(HEADER_OFFSET);
expect(divContext.native).toBe(div); expect(divContext.native).toBe(div);
expect(divLView).toBe(sectionLView); expect(divContext.lView).toBe(sectionContext.lView);
}); });
it('should cache the element context on a element was pre-emptively monkey-patched', () => { it('should cache the element context on a element was pre-emptively monkey-patched', () => {
@ -738,7 +736,7 @@ describe('element discovery', () => {
const div1 = hostElm.querySelector('div:first-child')! as any; const div1 = hostElm.querySelector('div:first-child')! as any;
const div2 = hostElm.querySelector('div:last-child')! as any; const div2 = hostElm.querySelector('div:last-child')! as any;
const context = getLContext(hostElm)!; const context = getLContext(hostElm)!;
const componentView = context.lView[context.nodeIndex]; const componentView = context.lView![context.nodeIndex];
expect(componentView).toContain(myDir1Instance); expect(componentView).toContain(myDir1Instance);
expect(componentView).toContain(myDir2Instance); expect(componentView).toContain(myDir2Instance);
@ -917,7 +915,7 @@ describe('element discovery', () => {
const context = getLContext(child)!; const context = getLContext(child)!;
expect(readPatchedData(child)).toBeTruthy(); expect(readPatchedData(child)).toBeTruthy();
const componentData = context.lView[context.nodeIndex]; const componentData = context.lView![context.nodeIndex];
const component = componentData[CONTEXT]; const component = componentData[CONTEXT];
expect(component instanceof ChildComp).toBeTruthy(); expect(component instanceof ChildComp).toBeTruthy();
expect(readPatchedData(component)).toBe(context.lView); expect(readPatchedData(component)).toBe(context.lView);