diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index 08bbaafdd76b4..ad2750482a80f 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -3,7 +3,7 @@ "master": { "uncompressed": { "runtime-es2015": 1170, - "main-es2015": 138189, + "main-es2015": 138718, "polyfills-es2015": 36964 } } @@ -30,7 +30,7 @@ "master": { "uncompressed": { "runtime-es2015": 1190, - "main-es2015": 136546, + "main-es2015": 137087, "polyfills-es2015": 37641 } } diff --git a/packages/core/src/debug/debug_node.ts b/packages/core/src/debug/debug_node.ts index f5db218fac7a3..c630f82a3cd6f 100644 --- a/packages/core/src/debug/debug_node.ts +++ b/packages/core/src/debug/debug_node.ts @@ -262,9 +262,10 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme } get name(): string { - const context = getLContext(this.nativeNode); - if (context !== null) { - const lView = context.lView; + const context = getLContext(this.nativeNode)!; + const lView = context ? context.lView : null; + + if (lView !== null) { const tData = lView[TVIEW].data; const tNode = tData[context.nodeIndex] as TNode; return tNode.value!; @@ -286,12 +287,13 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme * - attribute bindings (e.g. `[attr.role]="menu"`) */ get properties(): {[key: string]: any;} { - const context = getLContext(this.nativeNode); - if (context === null) { + const context = getLContext(this.nativeNode)!; + const lView = context ? context.lView : null; + + if (lView === null) { return {}; } - const lView = context.lView; const tData = lView[TVIEW].data; const tNode = tData[context.nodeIndex] as TNode; @@ -312,12 +314,13 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme return attributes; } - const context = getLContext(element); - if (context === null) { + const context = getLContext(element)!; + const lView = context ? context.lView : null; + + if (lView === null) { return {}; } - const lView = context.lView; const tNodeAttrs = (lView[TVIEW].data[context.nodeIndex] as TNode).attrs; const lowercaseTNodeAttrs: string[] = []; @@ -502,11 +505,12 @@ function _queryAllR3( function _queryAllR3( parentElement: DebugElement, predicate: Predicate|Predicate, matches: DebugElement[]|DebugNode[], elementsOnly: boolean) { - const context = getLContext(parentElement.nativeNode); - if (context !== null) { - const parentTNode = context.lView[TVIEW].data[context.nodeIndex] as TNode; + const context = getLContext(parentElement.nativeNode)!; + const lView = context ? context.lView : null; + if (lView !== null) { + const parentTNode = lView[TVIEW].data[context.nodeIndex] as TNode; _queryNodeChildrenR3( - parentTNode, context.lView, predicate, matches, elementsOnly, parentElement.nativeNode); + parentTNode, lView, predicate, matches, elementsOnly, parentElement.nativeNode); } else { // If the context is null, then `parentElement` was either created with Renderer2 or native DOM // APIs. diff --git a/packages/core/src/render3/context_discovery.ts b/packages/core/src/render3/context_discovery.ts index 43f4207eb7ad2..e57b980f0c6a3 100644 --- a/packages/core/src/render3/context_discovery.ts +++ b/packages/core/src/render3/context_discovery.ts @@ -8,12 +8,15 @@ import '../util/ng_dev_mode'; import {assertDefined, assertDomNode} from '../util/assert'; - import {EMPTY_ARRAY} from '../util/empty'; + +import {assertLView} from './assert'; import {LContext} from './interfaces/context'; +import {getLViewById} from './interfaces/lview_tracking'; import {TNode, TNodeFlags} from './interfaces/node'; 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'; @@ -43,7 +46,7 @@ export function getLContext(target: any): LContext|null { if (mpValue) { // only when it's an array is it considered an LView instance // ... otherwise it's an already constructed LContext instance - if (Array.isArray(mpValue)) { + if (isLView(mpValue)) { const lView: LView = mpValue!; let nodeIndex: number; let component: any = undefined; @@ -105,12 +108,7 @@ export function getLContext(target: any): LContext|null { while (parent = parent.parentNode) { const parentContext = readPatchedData(parent); if (parentContext) { - let lView: LView|null; - if (Array.isArray(parentContext)) { - lView = parentContext as LView; - } else { - lView = parentContext.lView; - } + const lView = Array.isArray(parentContext) ? parentContext as LView : parentContext.lView; // the edge of the app was also reached here through another means // (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 */ function createLContext(lView: LView, nodeIndex: number, native: RNode): LContext { - return { - lView, - nodeIndex, - native, - component: undefined, - directives: undefined, - localRefs: undefined, - }; + return new LContext(lView[ID], nodeIndex, native); } /** @@ -153,21 +144,24 @@ function createLContext(lView: LView, nodeIndex: number, native: RNode): LContex * @returns The component's view */ export function getComponentViewByInstance(componentInstance: {}): LView { - let lView = readPatchedData(componentInstance); - let view: LView; + let patchedData = readPatchedData(componentInstance); + let lView: LView; - if (Array.isArray(lView)) { - const nodeIndex = findViaComponent(lView, componentInstance); - view = getComponentLViewByIndex(nodeIndex, lView); - const context = createLContext(lView, nodeIndex, view[HOST] as RElement); + if (isLView(patchedData)) { + const contextLView: LView = patchedData; + const nodeIndex = findViaComponent(contextLView, componentInstance); + lView = getComponentLViewByIndex(nodeIndex, contextLView); + const context = createLContext(contextLView, nodeIndex, lView[HOST] as RElement); context.component = componentInstance; attachPatchData(componentInstance, context); attachPatchData(context.native, context); } else { - const context = lView as any as LContext; - view = getComponentLViewByIndex(context.nodeIndex, context.lView); + const context = patchedData as unknown as LContext; + 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) { 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 { 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 { const value = readPatchedData(target); if (value) { - return Array.isArray(value) ? value : (value as LContext).lView; + return isLView(value) ? value : value.lView; } return null; } diff --git a/packages/core/src/render3/instructions/lview_debug.ts b/packages/core/src/render3/instructions/lview_debug.ts index 1fd21dfbfdbef..7a7551f27df69 100644 --- a/packages/core/src/render3/instructions/lview_debug.ts +++ b/packages/core/src/render3/instructions/lview_debug.ts @@ -25,7 +25,7 @@ import {LQueries, TQueries} from '../interfaces/query'; import {Renderer3, RendererFactory3} from '../interfaces/renderer'; import {RComment, RElement, RNode} from '../interfaces/renderer_dom'; 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 {getParentInjectorIndex, getParentInjectorView} from '../util/injector_utils'; import {unwrapRNode} from '../util/view_utils'; @@ -513,6 +513,9 @@ export class LViewDebug implements ILViewDebug { get tHost(): ITNode|null { return this._raw_lView[T_HOST]; } + get id(): number { + return this._raw_lView[ID]; + } get decls(): LViewDebugRange { return toLViewRange(this.tView, this._raw_lView, HEADER_OFFSET, this.tView.bindingStartIndex); diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 34d6ef6daf871..47590176da46a 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -28,12 +28,13 @@ import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} fr 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 {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 {isProceduralRenderer, Renderer3, RendererFactory3} from '../interfaces/renderer'; import {RComment, RElement, RNode, RText} from '../interfaces/renderer_dom'; import {SanitizerFn} from '../interfaces/sanitization'; 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 {updateTextNode} from '../node_manipulation'; import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher'; @@ -143,6 +144,7 @@ export function createLView( lView[SANITIZER] = sanitizer || parentLView && parentLView[SANITIZER] || null!; lView[INJECTOR as any] = injector || parentLView && parentLView[INJECTOR] || null; lView[T_HOST] = tHostNode; + lView[ID] = registerLView(lView); ngDevMode && assertEqual( tView.type == TViewType.Embedded ? parentLView !== null : true, true, @@ -1906,9 +1908,12 @@ export function scheduleTick(rootContext: RootContext, flags: RootContextFlags) export function tickRootContext(rootContext: RootContext) { for (let i = 0; i < rootContext.components.length; i++) { const rootComponent = rootContext.components[i]; - const lView = readPatchedLView(rootComponent)!; - const tView = lView[TVIEW]; - renderComponentOrTemplate(tView, lView, tView.template, rootComponent); + const lView = readPatchedLView(rootComponent); + // We might not have an `LView` if the component was destroyed. + if (lView !== null) { + const tView = lView[TVIEW]; + renderComponentOrTemplate(tView, lView, tView.template, rootComponent); + } } } diff --git a/packages/core/src/render3/interfaces/context.ts b/packages/core/src/render3/interfaces/context.ts index 6f78e268918d5..e097719190d11 100644 --- a/packages/core/src/render3/interfaces/context.ts +++ b/packages/core/src/render3/interfaces/context.ts @@ -7,6 +7,7 @@ */ +import {getLViewById} from './lview_tracking'; import {RNode} from './renderer_dom'; 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 * of the context. */ -export interface 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; - +export class LContext { /** * The instance of the Component node. */ - component: {}|null|undefined; + public component: {}|null|undefined; /** * 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 - * on this element. + * The map of local references (local reference name => element or directive instance) that + * 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) {} } diff --git a/packages/core/src/render3/interfaces/lview_tracking.ts b/packages/core/src/render3/interfaces/lview_tracking.ts new file mode 100644 index 0000000000000..7c965ff98a622 --- /dev/null +++ b/packages/core/src/render3/interfaces/lview_tracking.ts @@ -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(); + +// 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]); +} diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index af0202bcf64f3..34d12482f4db6 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -47,6 +47,7 @@ export const DECLARATION_COMPONENT_VIEW = 16; export const DECLARATION_LCONTAINER = 17; export const PREORDER_HOOK_FLAGS = 18; export const QUERIES = 19; +export const ID = 20; /** * 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 * 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 @@ -326,6 +327,9 @@ export interface LView extends Array { * are not `Dirty`/`CheckAlways`. */ [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]) */ diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 801957fe21dc5..10f17061f367b 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -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 {ComponentDef} from './interfaces/definition'; 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 {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection'; import {isProceduralRenderer, ProceduralRenderer3, Renderer3, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; @@ -441,6 +442,9 @@ function cleanUpView(tView: TView, lView: LView): void { lQueries.detachView(tView); } } + + // Unregister the view once everything else has been cleaned up. + unregisterLView(lView); } } diff --git a/packages/core/src/render3/util/discovery_utils.ts b/packages/core/src/render3/util/discovery_utils.ts index 430df1bf2d8f8..37d60efe8fd35 100644 --- a/packages/core/src/render3/util/discovery_utils.ts +++ b/packages/core/src/render3/util/discovery_utils.ts @@ -11,7 +11,7 @@ import {Injector} from '../../di/injector'; import {ViewEncapsulation} from '../../metadata/view'; import {assertEqual} from '../../util/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 {NodeInjector} from '../di'; import {buildDebugNode} from '../instructions/lview_debug'; @@ -20,6 +20,7 @@ import {DirectiveDef} from '../interfaces/definition'; import {TElementNode, TNode, TNodeProviderIndexes} from '../interfaces/node'; import {isLView} from '../interfaces/type_checks'; import {CLEANUP, CONTEXT, DebugNode, FLAGS, LView, LViewFlags, T_HOST, TVIEW, TViewType} from '../interfaces/view'; + import {stringifyForError} from './stringify_utils'; import {getLViewParent, getRootContext} from './view_traversal_utils'; import {getTNode, unwrapRNode} from './view_utils'; @@ -52,12 +53,16 @@ import {getTNode, unwrapRNode} from './view_utils'; * @globalApi ng */ export function getComponent(element: Element): T|null { - assertDomElement(element); + ngDevMode && assertDomElement(element); const context = getLContext(element); if (context === null) return null; 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; @@ -78,8 +83,9 @@ export function getComponent(element: Element): T|null { */ export function getContext(element: Element): T|null { assertDomElement(element); - const context = getLContext(element); - return context === null ? null : context.lView[CONTEXT] as T; + const context = getLContext(element)!; + const lView = context ? context.lView : null; + return lView === null ? null : lView[CONTEXT] as T; } /** @@ -98,12 +104,11 @@ export function getContext(element: Element): T|null { * @globalApi ng */ export function getOwningComponent(elementOrDir: Element|{}): T|null { - const context = getLContext(elementOrDir); - if (context === null) return null; + const context = getLContext(elementOrDir)!; + let lView = context ? context.lView : null; + if (lView === null) return null; - let lView = context.lView; let parent: LView|null; - ngDevMode && assertLView(lView); while (lView[TVIEW].type === TViewType.Embedded && (parent = getLViewParent(lView)!)) { lView = parent; } @@ -122,7 +127,8 @@ export function getOwningComponent(elementOrDir: Element|{}): T|null { * @globalApi ng */ 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 */ export function getInjector(elementOrDir: Element|{}): Injector { - const context = getLContext(elementOrDir); - if (context === null) return Injector.NULL; + const context = getLContext(elementOrDir)!; + const lView = context ? context.lView : null; + if (lView === null) return Injector.NULL; - const tNode = context.lView[TVIEW].data[context.nodeIndex] as TElementNode; - return new NodeInjector(tNode, context.lView); + const tNode = lView[TVIEW].data[context.nodeIndex] as TElementNode; + 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. */ export function getInjectionTokens(element: Element): any[] { - const context = getLContext(element); - if (context === null) return []; - const lView = context.lView; + const context = getLContext(element)!; + const lView = context ? context.lView : null; + if (lView === null) return []; const tView = lView[TVIEW]; const tNode = tView.data[context.nodeIndex] as TNode; const providerTokens: any[] = []; @@ -200,12 +207,12 @@ export function getDirectives(node: Node): {}[] { return []; } - const context = getLContext(node); - if (context === null) { + const context = getLContext(node)!; + const lView = context ? context.lView : null; + if (lView === null) { return []; } - const lView = context.lView; const tView = lView[TVIEW]; const nodeIndex = context.nodeIndex; if (!tView?.data[nodeIndex]) { @@ -297,7 +304,11 @@ export function getLocalRefs(target: {}): {[key: string]: any} { if (context === null) return {}; 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 || {}; @@ -383,11 +394,11 @@ export interface Listener { * @globalApi ng */ export function getListeners(element: Element): Listener[] { - assertDomElement(element); + ngDevMode && assertDomElement(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 lCleanup = lView[CLEANUP]; const tCleanup = tView.cleanup; @@ -441,12 +452,13 @@ export function getDebugNode(element: Element): DebugNode|null { throw new Error('Expecting instance of DOM Element'); } - const lContext = getLContext(element); - if (lContext === null) { + const lContext = getLContext(element)!; + const lView = lContext ? lContext.lView : null; + + if (lView === null) { return null; } - const lView = lContext.lView; const nodeIndex = lContext.nodeIndex; if (nodeIndex !== -1) { const valueInLView = lView[nodeIndex]; @@ -473,7 +485,8 @@ export function getDebugNode(element: Element): DebugNode|null { export function getComponentLView(target: any): LView { const lContext = getLContext(target)!; const nodeIndx = lContext.nodeIndex; - const lView = lContext.lView; + const lView = lContext.lView!; + ngDevMode && assertLView(lView); const componentLView = lView[nodeIndx]; ngDevMode && assertLView(componentLView); return componentLView; diff --git a/packages/core/test/acceptance/component_spec.ts b/packages/core/test/acceptance/component_spec.ts index 31994aadac738..5b4c2b23aeeb7 100644 --- a/packages/core/test/acceptance/component_spec.ts +++ b/packages/core/test/acceptance/component_spec.ts @@ -141,6 +141,47 @@ describe('component', () => { 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: `
`}) + 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) describe('encapsulation', () => { @Component({ diff --git a/packages/core/test/acceptance/debug_spec.ts b/packages/core/test/acceptance/debug_spec.ts index 436d134d8bac8..c12985b8abe96 100644 --- a/packages/core/test/acceptance/debug_spec.ts +++ b/packages/core/test/acceptance/debug_spec.ts @@ -27,7 +27,7 @@ onlyInIvy('Ivy specific').describe('Debug Representation', () => { const fixture = TestBed.createComponent(MyComponent); fixture.detectChanges(); - const hostView = getLContext(fixture.componentInstance)!.lView.debug!; + const hostView = getLContext(fixture.componentInstance)!.lView!.debug!; expect(hostView.hostHTML).toEqual(null); const myCompView = hostView.childViews[0] as LViewDebug; expect(myCompView.hostHTML).toContain('
Hello World
'); @@ -47,7 +47,7 @@ onlyInIvy('Ivy specific').describe('Debug Representation', () => { const fixture = TestBed.createComponent(MyComponent); fixture.detectChanges(); - const hostView = getLContext(fixture.componentInstance)!.lView.debug!; + const hostView = getLContext(fixture.componentInstance)!.lView!.debug!; const myComponentView = hostView.childViews[0] as LViewDebug; expect(myComponentView.decls).toEqual({ start: HEADER_OFFSET, diff --git a/packages/core/test/acceptance/discover_utils_spec.ts b/packages/core/test/acceptance/discover_utils_spec.ts index a1d589eabe755..05a583b3a4b90 100644 --- a/packages/core/test/acceptance/discover_utils_spec.ts +++ b/packages/core/test/acceptance/discover_utils_spec.ts @@ -69,16 +69,19 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { @Component({ selector: 'my-app', template: ` - {{text}} + {{text}}
- +

+ Bold ` }) class MyApp { text: string = 'INIT'; + spanVisible = true; + conditionalChildVisible = true; @Input('a') b = 2; @Output('c') d = new EventEmitter(); constructor() { @@ -105,6 +108,15 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { expect(getComponent(child[0])).toEqual(childComponent[0]); expect(getComponent(child[1])).toEqual(childComponent[1]); }); + it('should not throw when called on a destroyed node', () => { + expect(getComponent(span[0])).toEqual(null); + expect(getComponent(child[2])).toEqual(childComponent[2]); + fixture.componentInstance.spanVisible = false; + fixture.componentInstance.conditionalChildVisible = false; + fixture.detectChanges(); + expect(getComponent(span[0])).toEqual(null); + expect(getComponent(child[2])).toEqual(childComponent[2]); + }); }); describe('getComponentLView', () => { @@ -131,6 +143,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { expect(getContext<{$implicit: boolean}>(child[2])!.$implicit).toEqual(true); expect(getContext(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', () => { @@ -146,6 +164,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { it('should throw on unknown target', () => { 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', () => { @@ -163,6 +187,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { expect(getInjector(dirA[0]).get(String)).toEqual('Module'); 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', () => { @@ -175,6 +205,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { expect(getDirectives(div[0])).toEqual([dirA[0]]); 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', () => { @@ -202,6 +238,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { expect(getOwningComponent(dirA[0])).toEqual(myApp); expect(getOwningComponent(dirA[1])).toEqual(myApp); }); + it('should return null for destroyed node', () => { + expect(getOwningComponent(span[0])).toEqual(myApp); + fixture.componentInstance.spanVisible = false; + fixture.detectChanges(); + expect(getOwningComponent(span[0])).toEqual(null); + }); }); describe('getLocalRefs', () => { @@ -219,6 +261,13 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { expect(getLocalRefs(child[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', () => { @@ -234,6 +283,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { expect(getRootComponents(div[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', () => { @@ -251,6 +306,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { listeners[0].callback('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', () => { @@ -259,6 +320,12 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { expect(getInjectionTokens(child[0])).toEqual([String, Child]); 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', () => { diff --git a/packages/core/test/acceptance/integration_spec.ts b/packages/core/test/acceptance/integration_spec.ts index e01aa0ab36859..07e7fd8cd4e00 100644 --- a/packages/core/test/acceptance/integration_spec.ts +++ b/packages/core/test/acceptance/integration_spec.ts @@ -11,7 +11,11 @@ import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/brow import {CommonModule} from '@angular/common'; import {Component, ContentChild, Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, NgModule, OnInit, Output, Pipe, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; import {Inject} from '@angular/core/src/di'; -import {TVIEW} from '@angular/core/src/render3/interfaces/view'; +import {readPatchedLView} from '@angular/core/src/render3/context_discovery'; +import {LContainer} from '@angular/core/src/render3/interfaces/container'; +import {getLViewById} from '@angular/core/src/render3/interfaces/lview_tracking'; +import {isLView} from '@angular/core/src/render3/interfaces/type_checks'; +import {ID, LView, PARENT, TVIEW} from '@angular/core/src/render3/interfaces/view'; import {getLView} from '@angular/core/src/render3/state'; import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode'; import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; @@ -2011,6 +2015,43 @@ describe('acceptance integration tests', () => { expect(fixture.nativeElement.innerHTML).toContain('Hello'); }); + onlyInIvy('The test is checking Ivy-specific logic') + .it('should remove child LView from the registry when the root view is destroyed', () => { + @Component({template: ''}) + class App { + } + + @Component({selector: 'child', template: ''}) + class Child { + } + + @Component({selector: 'grand-child', template: ''}) + class GrandChild { + } + + TestBed.configureTestingModule({declarations: [App, Child, GrandChild]}); + const fixture = TestBed.createComponent(App); + const grandChild = fixture.debugElement.query(By.directive(GrandChild)).componentInstance; + fixture.detectChanges(); + const leafLView = readPatchedLView(grandChild)!; + const lViewIds: number[] = []; + let current: LView|LContainer|null = leafLView; + + while (current) { + isLView(current) && lViewIds.push(current[ID]); + current = current[PARENT]; + } + + // We expect 3 views: `GrandChild`, `Child` and `App`. + expect(lViewIds).toEqual([leafLView[ID], leafLView[ID] - 1, leafLView[ID] - 2]); + expect(lViewIds.every(id => getLViewById(id) !== null)).toBe(true); + + fixture.destroy(); + + // Expect all 3 views to be removed from the registry once the root is destroyed. + expect(lViewIds.map(getLViewById)).toEqual([null, null, null]); + }); + describe('tView.firstUpdatePass', () => { function isFirstUpdatePass() { const lView = getLView(); diff --git a/packages/core/test/acceptance/ngdevmode_debug_spec.ts b/packages/core/test/acceptance/ngdevmode_debug_spec.ts index 64763f7650906..f3db593b68d75 100644 --- a/packages/core/test/acceptance/ngdevmode_debug_spec.ts +++ b/packages/core/test/acceptance/ngdevmode_debug_spec.ts @@ -32,7 +32,7 @@ onlyInIvy('Debug information exist in ivy only').describe('ngDevMode debug', () TestBed.configureTestingModule({declarations: [MyApp], imports: [CommonModule]}); const fixture = TestBed.createComponent(MyApp); - const rootLView = getLContext(fixture.nativeElement)!.lView; + const rootLView = getLContext(fixture.nativeElement)!.lView!; expect(rootLView.constructor.name).toEqual('LRootView'); const componentLView = getComponentLView(fixture.componentInstance); @@ -41,7 +41,7 @@ onlyInIvy('Debug information exist in ivy only').describe('ngDevMode debug', () const element: HTMLElement = fixture.nativeElement; fixture.detectChanges(); const li = element.querySelector('li')!; - const embeddedLView = getLContext(li)!.lView; + const embeddedLView = getLContext(li)!.lView!; expect(embeddedLView.constructor.name).toEqual('LEmbeddedView_MyApp_li_1'); }); }); diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index dd29afd07a730..5b040b0822dc9 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -47,6 +47,9 @@ { "name": "SimpleChange" }, + { + "name": "TRACKED_LVIEWS" + }, { "name": "TriggerComponent" }, @@ -254,6 +257,9 @@ { "name": "isInlineTemplate" }, + { + "name": "isLView" + }, { "name": "isNodeMatchingSelector" }, @@ -353,6 +359,9 @@ { "name": "setUpAttributes" }, + { + "name": "uniqueIdCounter" + }, { "name": "updateTransplantedViewCount" }, diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json index b89832be26bc1..3bd0484344a12 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -548,6 +548,9 @@ { "name": "THROW_IF_NOT_FOUND" }, + { + "name": "TRACKED_LVIEWS" + }, { "name": "TRANSITION_ID" }, @@ -1568,6 +1571,9 @@ { "name": "u" }, + { + "name": "uniqueIdCounter" + }, { "name": "unwrapRNode" }, diff --git a/packages/core/test/bundling/forms_reactive/forms_e2e_spec.ts b/packages/core/test/bundling/forms_reactive/forms_e2e_spec.ts index 65ee1ee746dc3..5efe851e897e1 100644 --- a/packages/core/test/bundling/forms_reactive/forms_e2e_spec.ts +++ b/packages/core/test/bundling/forms_reactive/forms_e2e_spec.ts @@ -7,7 +7,6 @@ */ import '@angular/compiler'; -import {ɵwhenRendered as whenRendered} from '@angular/core'; import {withBody} from '@angular/private/testing'; import * as path from 'path'; @@ -18,8 +17,8 @@ describe('functional test for reactive forms', () => { BUNDLES.forEach((bundle) => { describe(`using ${bundle} bundle`, () => { it('should render template form', withBody('', async () => { - require(path.join(PACKAGE, bundle)); - await (window as any).waitForApp; + const {whenRendered, bootstrapApp} = require(path.join(PACKAGE, bundle)); + await bootstrapApp(); // Reactive forms const reactiveFormsComponent = (window as any).reactiveFormsComponent; diff --git a/packages/core/test/bundling/forms_reactive/index.ts b/packages/core/test/bundling/forms_reactive/index.ts index b44c4f410be69..441b97afb824c 100644 --- a/packages/core/test/bundling/forms_reactive/index.ts +++ b/packages/core/test/bundling/forms_reactive/index.ts @@ -5,7 +5,7 @@ * 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 {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 {BrowserModule, platformBrowser} from '@angular/platform-browser'; @@ -93,5 +93,15 @@ class FormsExampleModule { } } -(window as any).waitForApp = platformBrowser().bootstrapModuleFactory( - new NgModuleFactory(FormsExampleModule), {ngZone: 'noop'}); +function bootstrapApp() { + 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 +}; diff --git a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json index df9a8e4ae4a7f..76743abf90da2 100644 --- a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json @@ -536,6 +536,9 @@ { "name": "THROW_IF_NOT_FOUND" }, + { + "name": "TRACKED_LVIEWS" + }, { "name": "TRANSITION_ID" }, @@ -1541,6 +1544,9 @@ { "name": "u" }, + { + "name": "uniqueIdCounter" + }, { "name": "unwrapRNode" }, diff --git a/packages/core/test/bundling/forms_template_driven/forms_e2e_spec.ts b/packages/core/test/bundling/forms_template_driven/forms_e2e_spec.ts index fb67ed4908cf8..3cbb8b2a97910 100644 --- a/packages/core/test/bundling/forms_template_driven/forms_e2e_spec.ts +++ b/packages/core/test/bundling/forms_template_driven/forms_e2e_spec.ts @@ -7,7 +7,6 @@ */ import '@angular/compiler'; -import {ɵwhenRendered as whenRendered} from '@angular/core'; import {withBody} from '@angular/private/testing'; import * as path from 'path'; @@ -18,8 +17,8 @@ describe('functional test for forms', () => { BUNDLES.forEach((bundle) => { describe(`using ${bundle} bundle`, () => { it('should render template form', withBody('', async () => { - require(path.join(PACKAGE, bundle)); - await (window as any).waitForApp; + const {bootstrapApp, whenRendered} = require(path.join(PACKAGE, bundle)); + await bootstrapApp(); // Template forms const templateFormsComponent = (window as any).templateFormsComponent; diff --git a/packages/core/test/bundling/forms_template_driven/index.ts b/packages/core/test/bundling/forms_template_driven/index.ts index fb03c5c0c50c8..ac81bc9e2342f 100644 --- a/packages/core/test/bundling/forms_template_driven/index.ts +++ b/packages/core/test/bundling/forms_template_driven/index.ts @@ -5,7 +5,7 @@ * 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 {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 {BrowserModule, platformBrowser} from '@angular/platform-browser'; @@ -70,5 +70,15 @@ class FormsExampleModule { } } -(window as any).waitForApp = platformBrowser().bootstrapModuleFactory( - new NgModuleFactory(FormsExampleModule), {ngZone: 'noop'}); +function bootstrapApp() { + 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 +}; diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 9f68ceb496c71..99bd5d32ecba1 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -41,6 +41,9 @@ { "name": "SimpleChange" }, + { + "name": "TRACKED_LVIEWS" + }, { "name": "ViewEncapsulation" }, @@ -173,6 +176,9 @@ { "name": "isInCheckNoChangesMode" }, + { + "name": "isLView" + }, { "name": "isProceduralRenderer" }, @@ -236,6 +242,9 @@ { "name": "setSelectedIndex" }, + { + "name": "uniqueIdCounter" + }, { "name": "updateTransplantedViewCount" }, diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index 2f236a71e8139..d84e3b0b762d0 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -785,6 +785,9 @@ { "name": "TQuery_" }, + { + "name": "TRACKED_LVIEWS" + }, { "name": "TRANSITION_ID" }, @@ -2015,6 +2018,9 @@ { "name": "u" }, + { + "name": "uniqueIdCounter" + }, { "name": "unwrapElementRef" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 057cd33ab4948..80bf8cb9911c3 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -32,6 +32,9 @@ { "name": "IterableDiffers" }, + { + "name": "LContext" + }, { "name": "NG_COMP_DEF" }, @@ -110,6 +113,9 @@ { "name": "SkipSelf" }, + { + "name": "TRACKED_LVIEWS" + }, { "name": "TemplateRef" }, @@ -380,6 +386,9 @@ { "name": "getLView" }, + { + "name": "getLViewById" + }, { "name": "getLViewParent" }, @@ -740,6 +749,9 @@ { "name": "trackByIdentity" }, + { + "name": "uniqueIdCounter" + }, { "name": "unwrapRNode" }, diff --git a/packages/core/test/bundling/todo/index.ts b/packages/core/test/bundling/todo/index.ts index c9efb5b83b079..de6dc37dbe138 100644 --- a/packages/core/test/bundling/todo/index.ts +++ b/packages/core/test/bundling/todo/index.ts @@ -9,7 +9,7 @@ import '@angular/core/test/bundling/util/src/reflect_metadata'; 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 { editing: boolean; @@ -133,7 +133,9 @@ class TodoStore { class ToDoAppComponent { newTodoText = ''; - constructor(public todoStore: TodoStore) {} + constructor(public todoStore: TodoStore) { + (window as any).todoAppComponent = this; + } cancelEditingTodo(todo: Todo) { todo.editing = false; @@ -200,3 +202,8 @@ class ToDoAppModule { } 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}; diff --git a/packages/core/test/bundling/todo/todo_e2e_spec.ts b/packages/core/test/bundling/todo/todo_e2e_spec.ts index b3506d457bcf4..be682aa0f2ab2 100644 --- a/packages/core/test/bundling/todo/todo_e2e_spec.ts +++ b/packages/core/test/bundling/todo/todo_e2e_spec.ts @@ -7,14 +7,9 @@ */ import '@angular/compiler'; -import {ɵwhenRendered as whenRendered} from '@angular/core'; -import {getComponent} from '@angular/core/src/render3'; import {withBody} from '@angular/private/testing'; import * as path from 'path'; -const UTF8 = { - encoding: 'utf-8' -}; const PACKAGE = 'angular/packages/core/test/bundling/todo'; const BUNDLES = ['bundle.js', 'bundle.min_debug.js', 'bundle.min.js']; @@ -22,13 +17,12 @@ describe('functional test for todo', () => { BUNDLES.forEach(bundle => { describe(bundle, () => { it('should render todo', withBody('', async () => { - require(path.join(PACKAGE, bundle)); - const toDoAppComponent = getComponent(document.querySelector('todo-app')!); + const {whenRendered} = require(path.join(PACKAGE, bundle)); expect(document.body.textContent).toContain('todos'); expect(document.body.textContent).toContain('Demonstrate Components'); expect(document.body.textContent).toContain('4 items left'); document.querySelector('button')!.click(); - await whenRendered(toDoAppComponent); + await whenRendered((window as any).todoAppComponent); expect(document.body.textContent).toContain('3 items left'); })); }); diff --git a/packages/core/test/bundling/todo_i18n/index.ts b/packages/core/test/bundling/todo_i18n/index.ts index 0025af70e594b..b20a3532b59aa 100644 --- a/packages/core/test/bundling/todo_i18n/index.ts +++ b/packages/core/test/bundling/todo_i18n/index.ts @@ -8,7 +8,7 @@ import '@angular/core/test/bundling/util/src/reflect_metadata'; import './translations'; 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 { editing: boolean; @@ -127,7 +127,9 @@ class TodoStore { class ToDoAppComponent { newTodoText = ''; - constructor(public todoStore: TodoStore) {} + constructor(public todoStore: TodoStore) { + (window as any).todoAppComponent = this; + } cancelEditingTodo(todo: Todo) { todo.editing = false; @@ -194,3 +196,8 @@ class ToDoAppModule { } 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}; diff --git a/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts b/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts index 9e55a6df77a92..5979f08c69d66 100644 --- a/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts +++ b/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts @@ -8,8 +8,6 @@ import '@angular/localize/init'; import '@angular/compiler'; -import {ɵwhenRendered as whenRendered} from '@angular/core'; -import {getComponent} from '@angular/core/src/render3'; import {clearTranslations} from '@angular/localize'; import {withBody} from '@angular/private/testing'; import * as path from 'path'; @@ -22,8 +20,7 @@ describe('functional test for todo i18n', () => { describe(bundle, () => { it('should render todo i18n', withBody('', async () => { clearTranslations(); - require(path.join(PACKAGE, bundle)); - const toDoAppComponent = getComponent(document.querySelector('todo-app')!); + const {whenRendered} = require(path.join(PACKAGE, bundle)); 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 NgModules'); @@ -31,7 +28,7 @@ describe('functional test for todo i18n', () => { expect(document.querySelector('.new-todo')!.getAttribute('placeholder')) .toEqual(`Qu'y a-t-il à faire ?`); document.querySelector('button')!.click(); - await whenRendered(toDoAppComponent); + await whenRendered((window as any).todoAppComponent); expect(document.body.textContent).toContain('3 tâches restantes'); })); }); diff --git a/packages/core/test/bundling/todo_r2/index.ts b/packages/core/test/bundling/todo_r2/index.ts index bd8b360f868f8..2c1860b4a7278 100644 --- a/packages/core/test/bundling/todo_r2/index.ts +++ b/packages/core/test/bundling/todo_r2/index.ts @@ -9,7 +9,7 @@ import '@angular/core/test/bundling/util/src/reflect_metadata'; 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'; class Todo { @@ -195,5 +195,15 @@ class ToDoAppModule { } } -(window as any).waitForApp = - platformBrowser().bootstrapModuleFactory(new NgModuleFactory(ToDoAppModule), {ngZone: 'noop'}); +function bootstrapApp() { + 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 +}; diff --git a/packages/core/test/bundling/todo_r2/todo_e2e_spec.ts b/packages/core/test/bundling/todo_r2/todo_e2e_spec.ts index 7fb449cd96699..98781a6407567 100644 --- a/packages/core/test/bundling/todo_r2/todo_e2e_spec.ts +++ b/packages/core/test/bundling/todo_r2/todo_e2e_spec.ts @@ -7,13 +7,9 @@ */ import '@angular/compiler'; -import {ɵwhenRendered as whenRendered} from '@angular/core'; import {withBody} from '@angular/private/testing'; import * as path from 'path'; -const UTF8 = { - encoding: 'utf-8' -}; const PACKAGE = 'angular/packages/core/test/bundling/todo_r2'; const BUNDLES = ['bundle.js', 'bundle.min_debug.js', 'bundle.min.js']; @@ -22,8 +18,8 @@ describe('functional test for todo', () => { describe(bundle, () => { it('should place styles on the elements within the component', withBody('', async () => { - require(path.join(PACKAGE, bundle)); - await (window as any).waitForApp; + const {bootstrapApp, whenRendered} = require(path.join(PACKAGE, bundle)); + await bootstrapApp(); const toDoAppComponent = (window as any).toDoAppComponent; await whenRendered(toDoAppComponent); diff --git a/packages/core/test/render3/i18n/i18n_parse_spec.ts b/packages/core/test/render3/i18n/i18n_parse_spec.ts index 7608ac69aa729..6408a30495660 100644 --- a/packages/core/test/render3/i18n/i18n_parse_spec.ts +++ b/packages/core/test/render3/i18n/i18n_parse_spec.ts @@ -40,16 +40,16 @@ describe('i18n_parse', () => { // TData | LView // ---------------------------+------------------------------- // ----- DECL ----- - // 20: TI18n | + // 21: TI18n | // ----- VARS ----- - // 21: Binding for ICU | + // 22: Binding for ICU | // ----- EXPANDO ----- - // 22: null | #text(before|) - // 23: TIcu | - // 24: null | currently selected ICU case - // 25: null | #text(caseA) - // 26: null | #text(otherCase) - // 27: null | #text(|after) + // 23: null | #text(before|) + // 24: TIcu | + // 25: null | currently selected ICU case + // 26: null | #text(caseA) + // 27: null | #text(otherCase) + // 28: null | #text(|after) const tI18n = toT18n(`before|{ �0�, select, A {caseA} @@ -153,21 +153,21 @@ describe('i18n_parse', () => { // TData | LView // ---------------------------+------------------------------- // ----- DECL ----- - // 20: TI18n | + // 21: TI18n | // ----- VARS ----- - // 21: Binding for parent ICU | - // 22: Binding for child ICU | + // 22: Binding for parent ICU | // 23: Binding for child ICU | + // 24: Binding for child ICU | // ----- EXPANDO ----- - // 24: TIcu (parent) | - // 25: null | currently selected ICU case - // 26: null | #text( parentA ) - // 27: TIcu (child) | - // 28: null | currently selected ICU case - // 29: null | #text(nested0) - // 30: null | #text({{�2�}}) - // 31: null | #text( ) - // 32: null | #text( parentOther ) + // 25: TIcu (parent) | + // 26: null | currently selected ICU case + // 27: null | #text( parentA ) + // 28: TIcu (child) | + // 29: null | currently selected ICU case + // 30: null | #text(nested0) + // 31: null | #text({{�2�}}) + // 32: null | #text( ) + // 33: null | #text( parentOther ) const tI18n = toT18n(`{ �0�, select, A {parentA {�1�, select, 0 {nested0} other {�2�}}!} diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index fdd830d44b842..56784af80870a 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -13,7 +13,7 @@ import {AttributeMarker, ɵɵadvance, ɵɵattribute, ɵɵdefineComponent, ɵɵde import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵprojection, ɵɵprojectionDef, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all'; import {RenderFlags} from '../../src/render3/interfaces/definition'; 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 {Sanitizer} from '../../src/sanitization/sanitizer'; import {SecurityContext} from '../../src/sanitization/security'; @@ -399,19 +399,17 @@ describe('element discovery', () => { const section = fixture.hostElement.querySelector('section')!; const sectionContext = getLContext(section)!; - const sectionLView = sectionContext.lView!; expect(sectionContext.nodeIndex).toEqual(HEADER_OFFSET); - expect(sectionLView.length).toBeGreaterThan(HEADER_OFFSET); + expect(sectionContext.lView!.length).toBeGreaterThan(HEADER_OFFSET); expect(sectionContext.native).toBe(section); const div = fixture.hostElement.querySelector('div')!; const divContext = getLContext(div)!; - const divLView = divContext.lView!; 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(divLView).toBe(sectionLView); + expect(divContext.lView).toBe(sectionContext.lView); }); 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 div2 = hostElm.querySelector('div:last-child')! as any; const context = getLContext(hostElm)!; - const componentView = context.lView[context.nodeIndex]; + const componentView = context.lView![context.nodeIndex]; expect(componentView).toContain(myDir1Instance); expect(componentView).toContain(myDir2Instance); @@ -917,7 +915,7 @@ describe('element discovery', () => { const context = getLContext(child)!; expect(readPatchedData(child)).toBeTruthy(); - const componentData = context.lView[context.nodeIndex]; + const componentData = context.lView![context.nodeIndex]; const component = componentData[CONTEXT]; expect(component instanceof ChildComp).toBeTruthy(); expect(readPatchedData(component)).toBe(context.lView);