refactor(core): add debug ranges to `LViewDebug` with matchers (#38359)
This change provides better typing for the `LView.debug` property which is intended to be used by humans while debugging the application with `ngDevMode` turned on. In addition this chang also adds jasmine matchers for better asserting that `LView` is in the correct state. PR Close #38359
This commit is contained in:
parent
df7f3b04b5
commit
702958e968
|
@ -223,8 +223,23 @@
|
|||
"packages/core/src/render3/assert.ts",
|
||||
"packages/core/src/render3/interfaces/container.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/core.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts",
|
||||
"packages/core/src/di/injector.ts",
|
||||
"packages/core/src/di/r3_injector.ts",
|
||||
"packages/core/src/render3/definition.ts",
|
||||
"packages/core/src/metadata/ng_module.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/application_ref.ts",
|
||||
"packages/core/src/application_tokens.ts",
|
||||
"packages/core/src/linker/component_factory.ts",
|
||||
"packages/core/src/change_detection/change_detection.ts",
|
||||
"packages/core/src/change_detection/change_detector_ref.ts",
|
||||
"packages/core/src/render3/view_engine_compatibility.ts",
|
||||
"packages/core/src/render3/assert.ts",
|
||||
"packages/core/src/render3/interfaces/container.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts",
|
||||
"packages/core/src/metadata.ts",
|
||||
"packages/core/src/di.ts",
|
||||
"packages/core/src/di/index.ts",
|
||||
|
@ -247,25 +262,9 @@
|
|||
"packages/core/src/render3/assert.ts",
|
||||
"packages/core/src/render3/interfaces/container.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts",
|
||||
"packages/core/src/di/injector.ts",
|
||||
"packages/core/src/di/r3_injector.ts",
|
||||
"packages/core/src/render3/definition.ts",
|
||||
"packages/core/src/metadata/ng_module.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/application_ref.ts",
|
||||
"packages/core/src/application_tokens.ts",
|
||||
"packages/core/src/linker/component_factory.ts",
|
||||
"packages/core/src/change_detection/change_detection.ts",
|
||||
"packages/core/src/change_detection/change_detector_ref.ts",
|
||||
"packages/core/src/render3/view_engine_compatibility.ts",
|
||||
"packages/core/src/render3/assert.ts",
|
||||
"packages/core/src/render3/interfaces/container.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts",
|
||||
"packages/core/src/core.ts",
|
||||
"packages/core/src/metadata.ts",
|
||||
"packages/core/src/di.ts",
|
||||
"packages/core/src/di/index.ts",
|
||||
|
@ -1766,27 +1765,25 @@
|
|||
[
|
||||
"packages/core/src/render3/interfaces/container.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts"
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts"
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts",
|
||||
"packages/core/src/render3/interfaces/query.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts"
|
||||
"packages/core/src/render3/interfaces/query.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/render3/interfaces/query.ts",
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
|
||||
"bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338",
|
||||
"bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info",
|
||||
"bundle": 1212027
|
||||
"bundle": 1213130
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,15 +15,14 @@ import {createNamedArrayType} from '../../util/named_array_type';
|
|||
import {initNgDevMode} from '../../util/ng_dev_mode';
|
||||
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS, NATIVE} from '../interfaces/container';
|
||||
import {DirectiveDefList, PipeDefList, ViewQueriesFunction} from '../interfaces/definition';
|
||||
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, TIcu} from '../interfaces/i18n';
|
||||
import {PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TViewNode} from '../interfaces/node';
|
||||
import {PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TNodeTypeAsString, TViewNode} from '../interfaces/node';
|
||||
import {SelectorFlags} from '../interfaces/projection';
|
||||
import {LQueries, TQueries} from '../interfaces/query';
|
||||
import {RComment, RElement, Renderer3, RendererFactory3, RNode} from '../interfaces/renderer';
|
||||
import {getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate, TStylingKey, TStylingRange} from '../interfaces/styling';
|
||||
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, DestroyHookData, ExpandoInstructions, FLAGS, HEADER_OFFSET, HookData, HOST, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TVIEW, TView as ITView, TView, TViewType} from '../interfaces/view';
|
||||
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DebugNode, DECLARATION_VIEW, DestroyHookData, ExpandoInstructions, FLAGS, HEADER_OFFSET, HookData, HOST, INJECTOR, LContainerDebug as ILContainerDebug, LView, LViewDebug as ILViewDebug, LViewDebugRange, LViewDebugRangeContent, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TView as ITView, TVIEW, TView, TViewType} from '../interfaces/view';
|
||||
import {attachDebugObject} from '../util/debug_utils';
|
||||
import {getTNode, unwrapRNode} from '../util/view_utils';
|
||||
import {unwrapRNode} from '../util/view_utils';
|
||||
|
||||
const NG_DEV_MODE = ((typeof ngDevMode === 'undefined' || !!ngDevMode) && initNgDevMode());
|
||||
|
||||
|
@ -143,7 +142,10 @@ export const TViewConstructor = class TView implements ITView {
|
|||
public firstChild: ITNode|null, //
|
||||
public schemas: SchemaMetadata[]|null, //
|
||||
public consts: TConstants|null, //
|
||||
public incompleteFirstPass: boolean //
|
||||
public incompleteFirstPass: boolean, //
|
||||
public _decls: number, //
|
||||
public _vars: number, //
|
||||
|
||||
) {}
|
||||
|
||||
get template_(): string {
|
||||
|
@ -335,9 +337,9 @@ export function attachLContainerDebug(lContainer: LContainer) {
|
|||
attachDebugObject(lContainer, new LContainerDebug(lContainer));
|
||||
}
|
||||
|
||||
export function toDebug(obj: LView): LViewDebug;
|
||||
export function toDebug(obj: LView|null): LViewDebug|null;
|
||||
export function toDebug(obj: LView|LContainer|null): LViewDebug|LContainerDebug|null;
|
||||
export function toDebug(obj: LView): ILViewDebug;
|
||||
export function toDebug(obj: LView|null): ILViewDebug|null;
|
||||
export function toDebug(obj: LView|LContainer|null): ILViewDebug|ILContainerDebug|null;
|
||||
export function toDebug(obj: any): any {
|
||||
if (obj) {
|
||||
const debug = (obj as any).debug;
|
||||
|
@ -375,7 +377,7 @@ function toHtml(value: any, includeChildren: boolean = false): string|null {
|
|||
}
|
||||
}
|
||||
|
||||
export class LViewDebug {
|
||||
export class LViewDebug implements ILViewDebug {
|
||||
constructor(private readonly _raw_lView: LView) {}
|
||||
|
||||
/**
|
||||
|
@ -396,10 +398,10 @@ export class LViewDebug {
|
|||
indexWithinInitPhase: flags >> LViewFlags.IndexWithinInitPhaseShift,
|
||||
};
|
||||
}
|
||||
get parent(): LViewDebug|LContainerDebug|null {
|
||||
get parent(): ILViewDebug|ILContainerDebug|null {
|
||||
return toDebug(this._raw_lView[PARENT]);
|
||||
}
|
||||
get host(): string|null {
|
||||
get hostHTML(): string|null {
|
||||
return toHtml(this._raw_lView[HOST], true);
|
||||
}
|
||||
get html(): string {
|
||||
|
@ -410,10 +412,9 @@ export class LViewDebug {
|
|||
}
|
||||
/**
|
||||
* The tree of nodes associated with the current `LView`. The nodes have been normalized into
|
||||
* a
|
||||
* tree structure with relevant details pulled out for readability.
|
||||
* a tree structure with relevant details pulled out for readability.
|
||||
*/
|
||||
get nodes(): DebugNode[]|null {
|
||||
get nodes(): DebugNode[] {
|
||||
const lView = this._raw_lView;
|
||||
const tNode = lView[TVIEW].firstChild;
|
||||
return toDebugNodes(tNode, lView);
|
||||
|
@ -437,16 +438,16 @@ export class LViewDebug {
|
|||
get sanitizer(): Sanitizer|null {
|
||||
return this._raw_lView[SANITIZER];
|
||||
}
|
||||
get childHead(): LViewDebug|LContainerDebug|null {
|
||||
get childHead(): ILViewDebug|ILContainerDebug|null {
|
||||
return toDebug(this._raw_lView[CHILD_HEAD]);
|
||||
}
|
||||
get next(): LViewDebug|LContainerDebug|null {
|
||||
get next(): ILViewDebug|ILContainerDebug|null {
|
||||
return toDebug(this._raw_lView[NEXT]);
|
||||
}
|
||||
get childTail(): LViewDebug|LContainerDebug|null {
|
||||
get childTail(): ILViewDebug|ILContainerDebug|null {
|
||||
return toDebug(this._raw_lView[CHILD_TAIL]);
|
||||
}
|
||||
get declarationView(): LViewDebug|null {
|
||||
get declarationView(): ILViewDebug|null {
|
||||
return toDebug(this._raw_lView[DECLARATION_VIEW]);
|
||||
}
|
||||
get queries(): LQueries|null {
|
||||
|
@ -456,11 +457,35 @@ export class LViewDebug {
|
|||
return this._raw_lView[T_HOST];
|
||||
}
|
||||
|
||||
get decls(): LViewDebugRange {
|
||||
const tView = this.tView as any as {_decls: number, _vars: number};
|
||||
const start = HEADER_OFFSET;
|
||||
return toLViewRange(this.tView, this._raw_lView, start, start + tView._decls);
|
||||
}
|
||||
|
||||
get vars(): LViewDebugRange {
|
||||
const tView = this.tView as any as {_decls: number, _vars: number};
|
||||
const start = HEADER_OFFSET + tView._decls;
|
||||
return toLViewRange(this.tView, this._raw_lView, start, start + tView._vars);
|
||||
}
|
||||
|
||||
get i18n(): LViewDebugRange {
|
||||
const tView = this.tView as any as {_decls: number, _vars: number};
|
||||
const start = HEADER_OFFSET + tView._decls + tView._vars;
|
||||
return toLViewRange(this.tView, this._raw_lView, start, this.tView.expandoStartIndex);
|
||||
}
|
||||
|
||||
get expando(): LViewDebugRange {
|
||||
const tView = this.tView as any as {_decls: number, _vars: number};
|
||||
return toLViewRange(
|
||||
this.tView, this._raw_lView, this.tView.expandoStartIndex, this._raw_lView.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized view of child views (and containers) attached at this location.
|
||||
*/
|
||||
get childViews(): Array<LViewDebug|LContainerDebug> {
|
||||
const childViews: Array<LViewDebug|LContainerDebug> = [];
|
||||
get childViews(): Array<ILViewDebug|ILContainerDebug> {
|
||||
const childViews: Array<ILViewDebug|ILContainerDebug> = [];
|
||||
let child = this.childHead;
|
||||
while (child) {
|
||||
childViews.push(child);
|
||||
|
@ -470,11 +495,12 @@ export class LViewDebug {
|
|||
}
|
||||
}
|
||||
|
||||
export interface DebugNode {
|
||||
html: string|null;
|
||||
native: Node;
|
||||
nodes: DebugNode[]|null;
|
||||
component: LViewDebug|null;
|
||||
function toLViewRange(tView: TView, lView: LView, start: number, end: number): LViewDebugRange {
|
||||
let content: LViewDebugRangeContent[] = [];
|
||||
for (let index = start; index < end; index++) {
|
||||
content.push({index: index, t: tView.data[index], l: lView[index]});
|
||||
}
|
||||
return {start: start, end: end, length: end - start, content: content};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -483,7 +509,7 @@ export interface DebugNode {
|
|||
* @param tNode
|
||||
* @param lView
|
||||
*/
|
||||
export function toDebugNodes(tNode: ITNode|null, lView: LView): DebugNode[]|null {
|
||||
export function toDebugNodes(tNode: ITNode|null, lView: LView): DebugNode[] {
|
||||
if (tNode) {
|
||||
const debugNodes: DebugNode[] = [];
|
||||
let tNodeCursor: ITNode|null = tNode;
|
||||
|
@ -493,33 +519,32 @@ export function toDebugNodes(tNode: ITNode|null, lView: LView): DebugNode[]|null
|
|||
}
|
||||
return debugNodes;
|
||||
} else {
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDebugNode(tNode: ITNode, lView: LView, nodeIndex: number): DebugNode {
|
||||
const rawValue = lView[nodeIndex];
|
||||
const native = unwrapRNode(rawValue);
|
||||
const componentLViewDebug = toDebug(readLViewValue(rawValue));
|
||||
return {
|
||||
html: toHtml(native),
|
||||
type: TNodeTypeAsString[tNode.type],
|
||||
native: native as any,
|
||||
nodes: toDebugNodes(tNode.child, lView),
|
||||
component: componentLViewDebug,
|
||||
children: toDebugNodes(tNode.child, lView),
|
||||
};
|
||||
}
|
||||
|
||||
export class LContainerDebug {
|
||||
export class LContainerDebug implements ILContainerDebug {
|
||||
constructor(private readonly _raw_lContainer: LContainer) {}
|
||||
|
||||
get hasTransplantedViews(): boolean {
|
||||
return this._raw_lContainer[HAS_TRANSPLANTED_VIEWS];
|
||||
}
|
||||
get views(): LViewDebug[] {
|
||||
get views(): ILViewDebug[] {
|
||||
return this._raw_lContainer.slice(CONTAINER_HEADER_OFFSET)
|
||||
.map(toDebug as (l: LView) => LViewDebug);
|
||||
.map(toDebug as (l: LView) => ILViewDebug);
|
||||
}
|
||||
get parent(): LViewDebug|LContainerDebug|null {
|
||||
get parent(): ILViewDebug|null {
|
||||
return toDebug(this._raw_lContainer[PARENT]);
|
||||
}
|
||||
get movedViews(): LView[]|null {
|
||||
|
|
|
@ -699,7 +699,9 @@ export function createTView(
|
|||
null, // firstChild: TNode|null,
|
||||
schemas, // schemas: SchemaMetadata[]|null,
|
||||
consts, // consts: TConstants|null
|
||||
false // incompleteFirstPass: boolean
|
||||
false, // incompleteFirstPass: boolean
|
||||
decls, // ngDevMode only: decls
|
||||
vars, // ngDevMode only: vars
|
||||
) :
|
||||
{
|
||||
type: type,
|
||||
|
|
|
@ -7,14 +7,11 @@
|
|||
*/
|
||||
import {KeyValueArray} from '../../util/array_utils';
|
||||
import {TStylingRange} from '../interfaces/styling';
|
||||
|
||||
import {DirectiveDef} from './definition';
|
||||
import {CssSelector} from './projection';
|
||||
import {RNode} from './renderer';
|
||||
import {LView, TView} from './view';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* TNodeType corresponds to the {@link TNode} `type` property.
|
||||
*/
|
||||
|
@ -45,6 +42,20 @@ export const enum TNodeType {
|
|||
IcuContainer = 5,
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts `TNodeType` into human readable text.
|
||||
* Make sure this matches with `TNodeType`
|
||||
*/
|
||||
export const TNodeTypeAsString = [
|
||||
'Container', // 0
|
||||
'Projection', // 1
|
||||
'View', // 2
|
||||
'Element', // 3
|
||||
'ElementContainer', // 4
|
||||
'IcuContainer' // 5
|
||||
] as const;
|
||||
|
||||
|
||||
/**
|
||||
* Corresponds to the TNode.flags property.
|
||||
*/
|
||||
|
|
|
@ -15,10 +15,10 @@ import {Sanitizer} from '../../sanitization/sanitizer';
|
|||
import {LContainer} from './container';
|
||||
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefList, HostBindingsFunction, PipeDef, PipeDefList, ViewQueriesFunction} from './definition';
|
||||
import {I18nUpdateOpCodes, TI18n} from './i18n';
|
||||
import {TConstants, TElementNode, TNode, TViewNode} from './node';
|
||||
import {TConstants, TElementNode, TNode, TNodeTypeAsString, TViewNode} from './node';
|
||||
import {PlayerHandler} from './player';
|
||||
import {LQueries, TQueries} from './query';
|
||||
import {RElement, Renderer3, RendererFactory3} from './renderer';
|
||||
import {RComment, RElement, Renderer3, RendererFactory3} from './renderer';
|
||||
import {TStylingKey, TStylingRange} from './styling';
|
||||
|
||||
|
||||
|
@ -69,6 +69,15 @@ export interface OpaqueViewState {
|
|||
* don't have to edit the data array based on which views are present.
|
||||
*/
|
||||
export interface LView extends Array<any> {
|
||||
/**
|
||||
* Human readable representation of the `LView`.
|
||||
*
|
||||
* NOTE: This property only exists if `ngDevMode` is set to `true` and it is not present in
|
||||
* production. Its presence is purely to help debug issue in development, and should not be relied
|
||||
* on in production application.
|
||||
*/
|
||||
debug?: LViewDebug;
|
||||
|
||||
/**
|
||||
* The host node for this LView instance, if this is a component view.
|
||||
* If this is an embedded view, HOST will be null.
|
||||
|
@ -826,3 +835,190 @@ export type TData =
|
|||
// Note: This hack is necessary so we don't erroneously get a circular dependency
|
||||
// failure based on types.
|
||||
export const unusedValueExportToPlacateAjd = 1;
|
||||
|
||||
/**
|
||||
* Human readable version of the `LView`.
|
||||
*
|
||||
* `LView` is a data structure used internally to keep track of views. The `LView` is designed for
|
||||
* efficiency and so at times it is difficult to read or write tests which assert on its values. For
|
||||
* this reason when `ngDevMode` is true we patch a `LView.debug` property which points to
|
||||
* `LViewDebug` for easier debugging and test writing. It is the intent of `LViewDebug` to be used
|
||||
* in tests.
|
||||
*/
|
||||
export interface LViewDebug {
|
||||
/**
|
||||
* Flags associated with the `LView` unpacked into a more readable state.
|
||||
*
|
||||
* See `LViewFlags` for the flag meanings.
|
||||
*/
|
||||
readonly flags: {
|
||||
initPhaseState: number,
|
||||
creationMode: boolean,
|
||||
firstViewPass: boolean,
|
||||
checkAlways: boolean,
|
||||
dirty: boolean,
|
||||
attached: boolean,
|
||||
destroyed: boolean,
|
||||
isRoot: boolean,
|
||||
indexWithinInitPhase: number,
|
||||
};
|
||||
|
||||
/**
|
||||
* Parent view (or container)
|
||||
*/
|
||||
readonly parent: LViewDebug|LContainerDebug|null;
|
||||
|
||||
/**
|
||||
* Next sibling to the `LView`.
|
||||
*/
|
||||
readonly next: LViewDebug|LContainerDebug|null;
|
||||
|
||||
/**
|
||||
* The context used for evaluation of the `LView`
|
||||
*
|
||||
* (Usually the component)
|
||||
*/
|
||||
readonly context: {}|null;
|
||||
|
||||
/**
|
||||
* Hierarchical tree of nodes.
|
||||
*/
|
||||
readonly nodes: DebugNode[];
|
||||
|
||||
/**
|
||||
* HTML representation of the `LView`.
|
||||
*
|
||||
* This is only approximate to actual HTML as child `LView`s are removed.
|
||||
*/
|
||||
readonly html: string;
|
||||
|
||||
/**
|
||||
* The host element to which this `LView` is attached.
|
||||
*/
|
||||
readonly hostHTML: string|null;
|
||||
|
||||
/**
|
||||
* Child `LView`s
|
||||
*/
|
||||
readonly childViews: Array<LViewDebug|LContainerDebug>;
|
||||
|
||||
/**
|
||||
* Sub range of `LView` containing decls (DOM elements).
|
||||
*/
|
||||
readonly decls: LViewDebugRange;
|
||||
|
||||
/**
|
||||
* Sub range of `LView` containing vars (bindings).
|
||||
*/
|
||||
readonly vars: LViewDebugRange;
|
||||
|
||||
/**
|
||||
* Sub range of `LView` containing i18n (translated DOM elements).
|
||||
*/
|
||||
readonly i18n: LViewDebugRange;
|
||||
|
||||
/**
|
||||
* Sub range of `LView` containing expando (used by DI).
|
||||
*/
|
||||
readonly expando: LViewDebugRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Human readable version of the `LContainer`
|
||||
*
|
||||
* `LContainer` is a data structure used internally to keep track of child views. The `LContainer`
|
||||
* is designed for efficiency and so at times it is difficult to read or write tests which assert on
|
||||
* its values. For this reason when `ngDevMode` is true we patch a `LContainer.debug` property which
|
||||
* points to `LContainerDebug` for easier debugging and test writing. It is the intent of
|
||||
* `LContainerDebug` to be used in tests.
|
||||
*/
|
||||
export interface LContainerDebug {
|
||||
readonly native: RComment;
|
||||
/**
|
||||
* Child `LView`s.
|
||||
*/
|
||||
readonly views: LViewDebug[];
|
||||
readonly parent: LViewDebug|null;
|
||||
readonly movedViews: LView[]|null;
|
||||
readonly host: RElement|RComment|LView;
|
||||
readonly next: LViewDebug|LContainerDebug|null;
|
||||
readonly hasTransplantedViews: boolean;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* `LView` is subdivided to ranges where the actual data is stored. Some of these ranges such as
|
||||
* `decls` and `vars` are known at compile time. Other such as `i18n` and `expando` are runtime only
|
||||
* concepts.
|
||||
*/
|
||||
export interface LViewDebugRange {
|
||||
/**
|
||||
* The starting index in `LView` where the range begins. (Inclusive)
|
||||
*/
|
||||
start: number;
|
||||
|
||||
/**
|
||||
* The ending index in `LView` where the range ends. (Exclusive)
|
||||
*/
|
||||
end: number;
|
||||
|
||||
/**
|
||||
* The length of the range
|
||||
*/
|
||||
length: number;
|
||||
|
||||
/**
|
||||
* The merged content of the range. `t` contains data from `TView.data` and `l` contains `LView`
|
||||
* data at an index.
|
||||
*/
|
||||
content: LViewDebugRangeContent[];
|
||||
}
|
||||
|
||||
/**
|
||||
* For convenience the static and instance portions of `TView` and `LView` are merged into a single
|
||||
* object in `LViewRange`.
|
||||
*/
|
||||
export interface LViewDebugRangeContent {
|
||||
/**
|
||||
* Index into original `LView` or `TView.data`.
|
||||
*/
|
||||
index: number;
|
||||
|
||||
/**
|
||||
* Value from the `TView.data[index]` location.
|
||||
*/
|
||||
t: any;
|
||||
|
||||
/**
|
||||
* Value from the `LView[index]` location.
|
||||
*/
|
||||
l: any;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A logical node which comprise into `LView`s.
|
||||
*
|
||||
*/
|
||||
export interface DebugNode {
|
||||
/**
|
||||
* HTML representation of the node.
|
||||
*/
|
||||
html: string|null;
|
||||
|
||||
/**
|
||||
* Human readable node type.
|
||||
*/
|
||||
type: typeof TNodeTypeAsString[number];
|
||||
|
||||
/**
|
||||
* DOM native node.
|
||||
*/
|
||||
native: Node;
|
||||
|
||||
/**
|
||||
* Child nodes
|
||||
*/
|
||||
children: DebugNode[];
|
||||
}
|
|
@ -10,12 +10,12 @@ import {Injector} from '../../di/injector';
|
|||
import {assertLView} from '../assert';
|
||||
import {discoverLocalRefs, getComponentAtNodeIndex, getDirectivesAtNodeIndex, getLContext} from '../context_discovery';
|
||||
import {NodeInjector} from '../di';
|
||||
import {buildDebugNode, DebugNode} from '../instructions/lview_debug';
|
||||
import {buildDebugNode} from '../instructions/lview_debug';
|
||||
import {LContext} from '../interfaces/context';
|
||||
import {DirectiveDef} from '../interfaces/definition';
|
||||
import {TElementNode, TNode, TNodeProviderIndexes} from '../interfaces/node';
|
||||
import {isLView} from '../interfaces/type_checks';
|
||||
import {CLEANUP, CONTEXT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, T_HOST, TVIEW} from '../interfaces/view';
|
||||
import {CLEANUP, CONTEXT, DebugNode, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, T_HOST, TVIEW} from '../interfaces/view';
|
||||
|
||||
import {stringifyForError} from './misc_utils';
|
||||
import {getLViewParent, getRootContext} from './view_traversal_utils';
|
||||
|
|
|
@ -20,6 +20,7 @@ ts_library(
|
|||
"//packages/compiler/testing",
|
||||
"//packages/core",
|
||||
"//packages/core/src/util",
|
||||
"//packages/core/test/render3:matchers",
|
||||
"//packages/core/testing",
|
||||
"//packages/localize",
|
||||
"//packages/localize/init",
|
||||
|
|
|
@ -8,13 +8,17 @@
|
|||
|
||||
import {Component} from '@angular/core';
|
||||
import {getLContext} from '@angular/core/src/render3/context_discovery';
|
||||
import {LViewDebug, toDebug} from '@angular/core/src/render3/instructions/lview_debug';
|
||||
import {LViewDebug} from '@angular/core/src/render3/instructions/lview_debug';
|
||||
import {TNodeType} from '@angular/core/src/render3/interfaces/node';
|
||||
import {HEADER_OFFSET} from '@angular/core/src/render3/interfaces/view';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
import {onlyInIvy} from '@angular/private/testing';
|
||||
|
||||
describe('Debug Representation', () => {
|
||||
onlyInIvy('Ivy specific').it('should generate a human readable version', () => {
|
||||
import {matchDomElement, matchDomText, matchTI18n, matchTNode} from '../render3/matchers';
|
||||
|
||||
onlyInIvy('Ivy specific').describe('Debug Representation', () => {
|
||||
it('should generate a human readable version', () => {
|
||||
@Component({selector: 'my-comp', template: '<div id="123">Hello World</div>'})
|
||||
class MyComponent {
|
||||
}
|
||||
|
@ -23,11 +27,56 @@ describe('Debug Representation', () => {
|
|||
const fixture = TestBed.createComponent(MyComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const hostView = toDebug(getLContext(fixture.componentInstance)!.lView);
|
||||
expect(hostView.host).toEqual(null);
|
||||
const hostView = getLContext(fixture.componentInstance)!.lView.debug!;
|
||||
expect(hostView.hostHTML).toEqual(null);
|
||||
const myCompView = hostView.childViews[0] as LViewDebug;
|
||||
expect(myCompView.host).toContain('<div id="123">Hello World</div>');
|
||||
expect(myCompView.hostHTML).toContain('<div id="123">Hello World</div>');
|
||||
expect(myCompView.nodes![0].html).toEqual('<div id="123">');
|
||||
expect(myCompView.nodes![0].nodes![0].html).toEqual('Hello World');
|
||||
expect(myCompView.nodes![0].children![0].html).toEqual('Hello World');
|
||||
});
|
||||
|
||||
describe('LViewDebug', () => {
|
||||
describe('range', () => {
|
||||
it('should show ranges', () => {
|
||||
@Component({selector: 'my-comp', template: '<div i18n>Hello {{name}}</div>'})
|
||||
class MyComponent {
|
||||
name = 'World';
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [MyComponent]});
|
||||
const fixture = TestBed.createComponent(MyComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const hostView = getLContext(fixture.componentInstance)!.lView.debug!;
|
||||
const myComponentView = hostView.childViews[0] as LViewDebug;
|
||||
expect(myComponentView.decls).toEqual({
|
||||
start: HEADER_OFFSET,
|
||||
end: HEADER_OFFSET + 2,
|
||||
length: 2,
|
||||
content: [
|
||||
{index: HEADER_OFFSET + 0, t: matchTNode({tagName: 'div'}), l: matchDomElement('div')},
|
||||
{index: HEADER_OFFSET + 1, t: matchTI18n(), l: null},
|
||||
]
|
||||
});
|
||||
expect(myComponentView.vars).toEqual({
|
||||
start: HEADER_OFFSET + 2,
|
||||
end: HEADER_OFFSET + 3,
|
||||
length: 1,
|
||||
content: [{index: HEADER_OFFSET + 2, t: null, l: 'World'}]
|
||||
});
|
||||
expect(myComponentView.i18n).toEqual({
|
||||
start: HEADER_OFFSET + 3,
|
||||
end: HEADER_OFFSET + 4,
|
||||
length: 1,
|
||||
content: [{
|
||||
index: HEADER_OFFSET + 3,
|
||||
t: matchTNode({type: TNodeType.Element, tagName: null}),
|
||||
l: matchDomText('Hello World')
|
||||
}]
|
||||
});
|
||||
expect(myComponentView.expando)
|
||||
.toEqual({start: HEADER_OFFSET + 4, end: HEADER_OFFSET + 4, length: 0, content: []});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,10 +11,13 @@ ts_library(
|
|||
"**/*_perf.ts",
|
||||
"domino.d.ts",
|
||||
"load_domino.ts",
|
||||
"is_shape_of.ts",
|
||||
"jit_spec.ts",
|
||||
"matchers.ts",
|
||||
],
|
||||
),
|
||||
deps = [
|
||||
":matchers",
|
||||
"//packages:types",
|
||||
"//packages/animations",
|
||||
"//packages/animations/browser",
|
||||
|
@ -34,6 +37,18 @@ ts_library(
|
|||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "matchers",
|
||||
testonly = True,
|
||||
srcs = [
|
||||
"is_shape_of.ts",
|
||||
"matchers.ts",
|
||||
],
|
||||
deps = [
|
||||
"//packages/core",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "domino",
|
||||
testonly = True,
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* @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 {TNodeType, TNodeTypeAsString} from '@angular/core/src/render3/interfaces/node';
|
||||
|
||||
describe('node interfaces', () => {
|
||||
describe('TNodeType', () => {
|
||||
it('should agree with TNodeTypeAsString', () => {
|
||||
expect(TNodeTypeAsString[TNodeType.Container]).toEqual('Container');
|
||||
expect(TNodeTypeAsString[TNodeType.Projection]).toEqual('Projection');
|
||||
expect(TNodeTypeAsString[TNodeType.View]).toEqual('View');
|
||||
expect(TNodeTypeAsString[TNodeType.Element]).toEqual('Element');
|
||||
expect(TNodeTypeAsString[TNodeType.ElementContainer]).toEqual('ElementContainer');
|
||||
expect(TNodeTypeAsString[TNodeType.IcuContainer]).toEqual('IcuContainer');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,186 @@
|
|||
/**
|
||||
* @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 {TI18n} from '@angular/core/src/render3/interfaces/i18n';
|
||||
import {TNode} from '@angular/core/src/render3/interfaces/node';
|
||||
import {TView} from '@angular/core/src/render3/interfaces/view';
|
||||
|
||||
/**
|
||||
* A type used to create a runtime representation of a shape of object which matches the declared
|
||||
* interface at compile time.
|
||||
*
|
||||
* The purpose of this type is to ensure that the object must match all of the properties of a type.
|
||||
* This is later used by `isShapeOf` method to ensure that a particular object has a particular
|
||||
* shape.
|
||||
*
|
||||
* ```
|
||||
* interface MyShape {
|
||||
* foo: string,
|
||||
* bar: number
|
||||
* }
|
||||
*
|
||||
* const myShapeObj: {foo: '', bar: 0};
|
||||
* const ExpectedPropertiesOfShape = {foo: true, bar: true};
|
||||
*
|
||||
* isShapeOf(myShapeObj, ExpectedPropertiesOfShape);
|
||||
* ```
|
||||
*
|
||||
* The above code would verify that `myShapeObj` has `foo` and `bar` properties. However if later
|
||||
* `MyShape` is refactored to change a set of properties we would like to have a compile time error
|
||||
* that the `ExpectedPropertiesOfShape` also needs to be changed.
|
||||
*
|
||||
* ```
|
||||
* const ExpectedPropertiesOfShape = <ShapeOf<MyShape>>{foo: true, bar: true};
|
||||
* ```
|
||||
* The above code will force through compile time checks that the `ExpectedPropertiesOfShape` match
|
||||
* that of `MyShape`.
|
||||
*
|
||||
* See: `isShapeOf`
|
||||
*
|
||||
*/
|
||||
export type ShapeOf<T> = {
|
||||
[P in keyof T]: true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a particular object is of a given shape (duck-type version of `instanceof`.)
|
||||
*
|
||||
* ```
|
||||
* isShapeOf(someObj, {foo: true, bar: true});
|
||||
* ```
|
||||
*
|
||||
* The above code will be true if the `someObj` has both `foo` and `bar` property
|
||||
*
|
||||
* @param obj Object to test for.
|
||||
* @param shapeOf Desired shape.
|
||||
*/
|
||||
export function isShapeOf<T>(obj: any, shapeOf: ShapeOf<T>): obj is T {
|
||||
if (typeof obj === 'object' && obj) {
|
||||
return Object.keys(shapeOf).reduce(
|
||||
(prev, key) => prev && obj.hasOwnProperty(key), true as boolean);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if `obj` matches the shape `TI18n`.
|
||||
* @param obj
|
||||
*/
|
||||
export function isTI18n(obj: any): obj is TI18n {
|
||||
return isShapeOf<TI18n>(obj, ShapeOfTI18n);
|
||||
}
|
||||
const ShapeOfTI18n: ShapeOf<TI18n> = {
|
||||
vars: true,
|
||||
create: true,
|
||||
update: true,
|
||||
icus: true,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determines if `obj` matches the shape `TView`.
|
||||
* @param obj
|
||||
*/
|
||||
export function isTView(obj: any): obj is TView {
|
||||
return isShapeOf<TView>(obj, ShapeOfTView);
|
||||
}
|
||||
const ShapeOfTView: ShapeOf<TView> = {
|
||||
type: true,
|
||||
id: true,
|
||||
blueprint: true,
|
||||
template: true,
|
||||
viewQuery: true,
|
||||
node: true,
|
||||
firstCreatePass: true,
|
||||
firstUpdatePass: true,
|
||||
data: true,
|
||||
bindingStartIndex: true,
|
||||
expandoStartIndex: true,
|
||||
staticViewQueries: true,
|
||||
staticContentQueries: true,
|
||||
firstChild: true,
|
||||
expandoInstructions: true,
|
||||
directiveRegistry: true,
|
||||
pipeRegistry: true,
|
||||
preOrderHooks: true,
|
||||
preOrderCheckHooks: true,
|
||||
contentHooks: true,
|
||||
contentCheckHooks: true,
|
||||
viewHooks: true,
|
||||
viewCheckHooks: true,
|
||||
destroyHooks: true,
|
||||
cleanup: true,
|
||||
components: true,
|
||||
queries: true,
|
||||
contentQueries: true,
|
||||
schemas: true,
|
||||
consts: true,
|
||||
incompleteFirstPass: true,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determines if `obj` matches the shape `TI18n`.
|
||||
* @param obj
|
||||
*/
|
||||
export function isTNode(obj: any): obj is TNode {
|
||||
return isShapeOf<TNode>(obj, ShapeOfTNode);
|
||||
}
|
||||
const ShapeOfTNode: ShapeOf<TNode> = {
|
||||
type: true,
|
||||
index: true,
|
||||
injectorIndex: true,
|
||||
directiveStart: true,
|
||||
directiveEnd: true,
|
||||
directiveStylingLast: true,
|
||||
propertyBindings: true,
|
||||
flags: true,
|
||||
providerIndexes: true,
|
||||
tagName: true,
|
||||
attrs: true,
|
||||
mergedAttrs: true,
|
||||
localNames: true,
|
||||
initialInputs: true,
|
||||
inputs: true,
|
||||
outputs: true,
|
||||
tViews: true,
|
||||
next: true,
|
||||
projectionNext: true,
|
||||
child: true,
|
||||
parent: true,
|
||||
projection: true,
|
||||
styles: true,
|
||||
stylesWithoutHost: true,
|
||||
residualStyles: true,
|
||||
classes: true,
|
||||
classesWithoutHost: true,
|
||||
residualClasses: true,
|
||||
classBindings: true,
|
||||
styleBindings: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if `obj` is DOM `Node`.
|
||||
*/
|
||||
export function isDOMNode(obj: any): obj is Node {
|
||||
return obj instanceof Node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if `obj` is DOM `Text`.
|
||||
*/
|
||||
export function isDOMElement(obj: any): obj is Element {
|
||||
return obj instanceof Element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if `obj` is DOM `Text`.
|
||||
*/
|
||||
export function isDOMText(obj: any): obj is Text {
|
||||
return obj instanceof Text;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* @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 {isShapeOf, ShapeOf} from './is_shape_of';
|
||||
|
||||
describe('isShapeOf', () => {
|
||||
const ShapeOfEmptyObject: ShapeOf<{}> = {};
|
||||
it('should not match for non objects', () => {
|
||||
expect(isShapeOf(null, ShapeOfEmptyObject)).toBeFalse();
|
||||
expect(isShapeOf(0, ShapeOfEmptyObject)).toBeFalse();
|
||||
expect(isShapeOf(1, ShapeOfEmptyObject)).toBeFalse();
|
||||
expect(isShapeOf(true, ShapeOfEmptyObject)).toBeFalse();
|
||||
expect(isShapeOf(false, ShapeOfEmptyObject)).toBeFalse();
|
||||
expect(isShapeOf(undefined, ShapeOfEmptyObject)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should match on empty object', () => {
|
||||
expect(isShapeOf({}, ShapeOfEmptyObject)).toBeTrue();
|
||||
expect(isShapeOf({extra: 'is ok'}, ShapeOfEmptyObject)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should match on shape', () => {
|
||||
expect(isShapeOf({required: 1}, {required: true})).toBeTrue();
|
||||
expect(isShapeOf({required: true, extra: 'is ok'}, {required: true})).toBeTrue();
|
||||
});
|
||||
|
||||
it('should not match if missing property', () => {
|
||||
expect(isShapeOf({required: 1}, {required: true, missing: true})).toBeFalse();
|
||||
expect(isShapeOf({required: true, extra: 'is ok'}, {required: true, missing: true}))
|
||||
.toBeFalse();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,218 @@
|
|||
/**
|
||||
* @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 {TI18n} from '@angular/core/src/render3/interfaces/i18n';
|
||||
import {TNode} from '@angular/core/src/render3/interfaces/node';
|
||||
import {TView} from '@angular/core/src/render3/interfaces/view';
|
||||
|
||||
import {isDOMElement, isDOMText, isTI18n, isTNode, isTView} from './is_shape_of';
|
||||
|
||||
|
||||
/**
|
||||
* Generic matcher which asserts that an object is of a given shape (`shapePredicate`) and that it
|
||||
* contains a subset of properties.
|
||||
*
|
||||
* @param name Name of `shapePredicate` to display when assertion fails.
|
||||
* @param shapePredicate Predicate which verifies that the object is of correct shape.
|
||||
* @param expected Expected set of properties to be found on the object.
|
||||
*/
|
||||
export function matchObjectShape<T>(
|
||||
name: string, shapePredicate: (obj: any) => obj is T,
|
||||
expected: Partial<T> = {}): jasmine.AsymmetricMatcher<T> {
|
||||
const matcher = function() {};
|
||||
let _actual: any = null;
|
||||
|
||||
matcher.asymmetricMatch = function(actual: any) {
|
||||
_actual = actual;
|
||||
if (!shapePredicate(actual)) return false;
|
||||
for (const key in expected) {
|
||||
if (expected.hasOwnProperty(key) && !jasmine.matchersUtil.equals(actual[key], expected[key]))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
matcher.jasmineToString = function() {
|
||||
return `${toString(_actual, false)} != ${toString(expected, true)})`;
|
||||
};
|
||||
|
||||
function toString(obj: any, isExpected: boolean) {
|
||||
if (isExpected || shapePredicate(obj)) {
|
||||
const props =
|
||||
Object.keys(expected).map(key => `${key}: ${JSON.stringify((obj as any)[key])}`);
|
||||
if (isExpected === false) {
|
||||
// Push something to let the user know that there may be other ignored properties in actual
|
||||
props.push('...');
|
||||
}
|
||||
return `${name}({${props.length === 0 ? '' : '\n ' + props.join(',\n ') + '\n'}})`;
|
||||
} else {
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
}
|
||||
return matcher;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Asymmetric matcher which matches a `TView` of a given shape.
|
||||
*
|
||||
* Expected usage:
|
||||
* ```
|
||||
* expect(tNode).toEqual(matchTView({type: TViewType.Root}));
|
||||
* expect({
|
||||
* node: tNode
|
||||
* }).toEqual({
|
||||
* node: matchTNode({type: TViewType.Root})
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param expected optional properties which the `TView` must contain.
|
||||
*/
|
||||
export function matchTView(expected?: Partial<TView>): jasmine.AsymmetricMatcher<TView> {
|
||||
return matchObjectShape('TView', isTView, expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asymmetric matcher which matches a `TNode` of a given shape.
|
||||
*
|
||||
* Expected usage:
|
||||
* ```
|
||||
* expect(tNode).toEqual(matchTNode({type: TNodeType.Element}));
|
||||
* expect({
|
||||
* node: tNode
|
||||
* }).toEqual({
|
||||
* node: matchTNode({type: TNodeType.Element})
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param expected optional properties which the `TNode` must contain.
|
||||
*/
|
||||
export function matchTNode(expected?: Partial<TNode>): jasmine.AsymmetricMatcher<TNode> {
|
||||
return matchObjectShape('TNode', isTNode, expected);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Asymmetric matcher which matches a `T18n` of a given shape.
|
||||
*
|
||||
* Expected usage:
|
||||
* ```
|
||||
* expect(tNode).toEqual(matchT18n({vars: 0}));
|
||||
* expect({
|
||||
* node: tNode
|
||||
* }).toEqual({
|
||||
* node: matchT18n({vars: 0})
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param expected optional properties which the `TI18n` must contain.
|
||||
*/
|
||||
export function matchTI18n(expected?: Partial<TI18n>): jasmine.AsymmetricMatcher<TI18n> {
|
||||
return matchObjectShape('TI18n', isTI18n, expected);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Asymmetric matcher which matches a DOM Element.
|
||||
*
|
||||
* Expected usage:
|
||||
* ```
|
||||
* expect(div).toEqual(matchT18n('div', {id: '123'}));
|
||||
* expect({
|
||||
* node: div
|
||||
* }).toEqual({
|
||||
* node: matchT18n('div', {id: '123'})
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param expectedTagName optional DOM tag name.
|
||||
* @param expectedAttributes optional DOM element properties.
|
||||
*/
|
||||
export function matchDomElement(
|
||||
expectedTagName: string|undefined = undefined,
|
||||
expectedAttrs: {[key: string]: string|null} = {}): jasmine.AsymmetricMatcher<Element> {
|
||||
const matcher = function() {};
|
||||
let _actual: any = null;
|
||||
|
||||
matcher.asymmetricMatch = function(actual: any) {
|
||||
_actual = actual;
|
||||
if (!isDOMElement(actual)) return false;
|
||||
if (expectedTagName && (expectedTagName.toUpperCase() !== actual.tagName.toUpperCase())) {
|
||||
return false;
|
||||
}
|
||||
if (expectedAttrs) {
|
||||
for (const attrName in expectedAttrs) {
|
||||
if (expectedAttrs.hasOwnProperty(attrName)) {
|
||||
const expectedAttrValue = expectedAttrs[attrName];
|
||||
const actualAttrValue = actual.getAttribute(attrName);
|
||||
if (expectedAttrValue !== actualAttrValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
matcher.jasmineToString = function() {
|
||||
let actualStr = isDOMElement(_actual) ? `<${_actual.tagName}${toString(_actual.attributes)}>` :
|
||||
JSON.stringify(_actual);
|
||||
let expectedStr = `<${expectedTagName || '*'}${
|
||||
Object.keys(expectedAttrs).map(key => ` ${key}=${JSON.stringify(expectedAttrs[key])}`)}>`;
|
||||
return `[${actualStr} != ${expectedStr}]`;
|
||||
};
|
||||
|
||||
function toString(attrs: NamedNodeMap) {
|
||||
let text = '';
|
||||
for (let i = 0; i < attrs.length; i++) {
|
||||
const attr = attrs[i];
|
||||
text += ` ${attr.name}=${JSON.stringify(attr.value)}`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
return matcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asymmetric matcher which matches DOM text node.
|
||||
*
|
||||
* Expected usage:
|
||||
* ```
|
||||
* expect(div).toEqual(matchDomText('text'));
|
||||
* expect({
|
||||
* node: div
|
||||
* }).toEqual({
|
||||
* node: matchDomText('text')
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param expectedText optional DOM text.
|
||||
*/
|
||||
export function matchDomText(expectedText: string|undefined = undefined):
|
||||
jasmine.AsymmetricMatcher<Text> {
|
||||
const matcher = function() {};
|
||||
let _actual: any = null;
|
||||
|
||||
matcher.asymmetricMatch = function(actual: any) {
|
||||
_actual = actual;
|
||||
if (!isDOMText(actual)) return false;
|
||||
if (expectedText && (expectedText !== actual.textContent)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
matcher.jasmineToString = function() {
|
||||
let actualStr = isDOMText(_actual) ? `#TEXT: ${JSON.stringify(_actual.textContent)}` :
|
||||
JSON.stringify(_actual);
|
||||
let expectedStr = `#TEXT: ${JSON.stringify(expectedText)}`;
|
||||
return `[${actualStr} != ${expectedStr}]`;
|
||||
};
|
||||
|
||||
return matcher;
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* @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 {createTNode, createTView} from '@angular/core/src/render3/instructions/shared';
|
||||
import {TNodeType} from '@angular/core/src/render3/interfaces/node';
|
||||
import {TViewType} from '@angular/core/src/render3/interfaces/view';
|
||||
import {onlyInIvy} from '@angular/private/testing';
|
||||
|
||||
import {isShapeOf, ShapeOf} from './is_shape_of';
|
||||
import {matchDomElement, matchDomText, matchObjectShape, matchTNode, matchTView} from './matchers';
|
||||
import {dedent} from './utils';
|
||||
|
||||
describe('render3 matchers', () => {
|
||||
describe('matchObjectShape', () => {
|
||||
interface MyShape {
|
||||
propA: any;
|
||||
propB: any;
|
||||
}
|
||||
|
||||
const myShape: MyShape = {propA: 'value', propB: 3};
|
||||
function isMyShape(obj: any): obj is MyShape {
|
||||
return isShapeOf<MyShape>(obj, ShapeOfMyShape);
|
||||
}
|
||||
const ShapeOfMyShape: ShapeOf<MyShape> = {propA: true, propB: true};
|
||||
function matchMyShape(expected?: Partial<MyShape>): jasmine.AsymmetricMatcher<MyShape> {
|
||||
return matchObjectShape('MyShape', isMyShape, expected);
|
||||
}
|
||||
|
||||
it('should match', () => {
|
||||
expect(isMyShape(myShape)).toBeTrue();
|
||||
expect(myShape).toEqual(matchMyShape());
|
||||
expect(myShape).toEqual(matchMyShape({propA: 'value'}));
|
||||
expect({node: myShape}).toEqual({node: matchMyShape({propA: 'value'})});
|
||||
});
|
||||
|
||||
it('should produce human readable errors', () => {
|
||||
const matcher = matchMyShape({propA: 'different'});
|
||||
expect(matcher.asymmetricMatch(myShape, [])).toEqual(false);
|
||||
expect(matcher.jasmineToString!()).toEqual(dedent`
|
||||
MyShape({
|
||||
propA: "value",
|
||||
...
|
||||
}) != MyShape({
|
||||
propA: "different"
|
||||
}))`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchTView', () => {
|
||||
const tView = createTView(TViewType.Root, 1, null, 2, 3, null, null, null, null, null);
|
||||
it('should match', () => {
|
||||
expect(tView).toEqual(matchTView());
|
||||
expect(tView).toEqual(matchTView({type: TViewType.Root}));
|
||||
expect({node: tView}).toEqual({node: matchTView({type: TViewType.Root})});
|
||||
});
|
||||
});
|
||||
describe('matchTNode', () => {
|
||||
const tView = createTView(TViewType.Root, 1, null, 2, 3, null, null, null, null, null);
|
||||
const tNode = createTNode(tView, null, TNodeType.Element, 1, 'tagName', []);
|
||||
|
||||
it('should match', () => {
|
||||
expect(tNode).toEqual(matchTNode());
|
||||
expect(tNode).toEqual(matchTNode({type: TNodeType.Element, tagName: 'tagName'}));
|
||||
expect({node: tNode}).toEqual({node: matchTNode({type: TNodeType.Element})});
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchDomElement', () => {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('name', 'Name');
|
||||
it('should match', () => {
|
||||
expect(div).toEqual(matchDomElement());
|
||||
expect(div).toEqual(matchDomElement('div', {name: 'Name'}));
|
||||
});
|
||||
|
||||
it('should produce human readable error', () => {
|
||||
const matcher = matchDomElement('div', {name: 'other'});
|
||||
expect(matcher.asymmetricMatch(div, [])).toEqual(false);
|
||||
expect(matcher.jasmineToString!()).toEqual(`[<DIV name="Name"> != <div name="other">]`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchDomText', () => {
|
||||
const text = document.createTextNode('myText');
|
||||
it('should match', () => {
|
||||
expect(text).toEqual(matchDomText());
|
||||
expect(text).toEqual(matchDomText('myText'));
|
||||
});
|
||||
|
||||
it('should produce human readable error', () => {
|
||||
const matcher = matchDomText('other text');
|
||||
expect(matcher.asymmetricMatch(text, [])).toEqual(false);
|
||||
expect(matcher.jasmineToString!()).toEqual(`[#TEXT: "myText" != #TEXT: "other text"]`);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue