diff --git a/packages/core/src/debug/debug_node.ts b/packages/core/src/debug/debug_node.ts index c31e082490..d81c8fb621 100644 --- a/packages/core/src/debug/debug_node.ts +++ b/packages/core/src/debug/debug_node.ts @@ -10,9 +10,10 @@ import {Injector} from '../di'; import {getComponent, getContext, getInjectionTokens, getInjector, getListeners, getLocalRefs, isBrowserEvents, loadLContext, loadLContextFromNode} from '../render3/discovery_utils'; import {TNode} from '../render3/interfaces/node'; import {StylingIndex} from '../render3/interfaces/styling'; -import {TVIEW} from '../render3/interfaces/view'; +import {LView, TData, TVIEW} from '../render3/interfaces/view'; import {getProp, getValue, isClassBasedValue} from '../render3/styling/class_and_style_bindings'; import {getStylingContext} from '../render3/styling/util'; +import {INTERPOLATION_DELIMITER, isPropMetadataString, renderStringify} from '../render3/util'; import {assertDomNode} from '../util/assert'; import {DebugContext} from '../view/index'; @@ -240,14 +241,16 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme get name(): string { return this.nativeElement !.nodeName; } /** - * Returns a map of property names to property values for an element. + * Gets a map of property names to property values for an element. * * This map includes: - * - Regular property bindings (e.g. `[id]="id"`) - TODO + * - Regular property bindings (e.g. `[id]="id"`) * - Host property bindings (e.g. `host: { '[id]': "id" }`) - * - Interpolated property bindings (e.g. `id="{{ value }}") - TODO + * - Interpolated property bindings (e.g. `id="{{ value }}") * - * It should NOT include input property bindings or attribute bindings. + * It does not include: + * - input property bindings (e.g. `[myCustomInput]="value"`) + * - attribute bindings (e.g. `[attr.role]="menu"`) */ get properties(): {[key: string]: any;} { const context = loadLContext(this.nativeNode) !; @@ -255,20 +258,9 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme const tData = lView[TVIEW].data; const tNode = tData[context.nodeIndex] as TNode; - // TODO(kara): include regular property binding values (not just host properties) - const properties: {[key: string]: string} = {}; - - // Host binding values for a node are stored after directives on that node - let index = tNode.directiveEnd; - let propertyName = tData[index]; - - // When we reach a value in TView.data that is not a string, we know we've - // hit the next node's providers and directives and should stop copying data. - while (typeof propertyName === 'string') { - properties[propertyName] = lView[index]; - propertyName = tData[++index]; - } - return properties; + const properties = collectPropertyBindings(tNode, lView, tData); + const hostProperties = collectHostPropertyBindings(tNode, lView, tData); + return {...properties, ...hostProperties}; } get attributes(): {[key: string]: string | null;} { @@ -410,6 +402,89 @@ function _queryNodeChildrenR3( } } +/** + * Iterates through the property bindings for a given node and generates + * a map of property names to values. This map only contains property bindings + * defined in templates, not in host bindings. + */ +function collectPropertyBindings( + tNode: TNode, lView: LView, tData: TData): {[key: string]: string} { + const properties: {[key: string]: string} = {}; + let bindingIndex = getFirstBindingIndex(tNode.propertyMetadataStartIndex, tData); + + while (bindingIndex < tNode.propertyMetadataEndIndex) { + let value = ''; + let propMetadata = tData[bindingIndex] as string; + while (!isPropMetadataString(propMetadata)) { + // This is the first value for an interpolation. We need to build up + // the full interpolation by combining runtime values in LView with + // the static interstitial values stored in TData. + value += renderStringify(lView[bindingIndex]) + tData[bindingIndex]; + propMetadata = tData[++bindingIndex] as string; + } + value += lView[bindingIndex]; + // Property metadata string has 3 parts: property name, prefix, and suffix + const metadataParts = propMetadata.split(INTERPOLATION_DELIMITER); + const propertyName = metadataParts[0]; + // Attr bindings don't have property names and should be skipped + if (propertyName) { + // Wrap value with prefix and suffix (will be '' for normal bindings) + properties[propertyName] = metadataParts[1] + value + metadataParts[2]; + } + bindingIndex++; + } + return properties; +} + +/** + * Retrieves the first binding index that holds values for this property + * binding. + * + * For normal bindings (e.g. `[id]="id"`), the binding index is the + * same as the metadata index. For interpolations (e.g. `id="{{id}}-{{name}}"`), + * there can be multiple binding values, so we might have to loop backwards + * from the metadata index until we find the first one. + * + * @param metadataIndex The index of the first property metadata string for + * this node. + * @param tData The data array for the current TView + * @returns The first binding index for this binding + */ +function getFirstBindingIndex(metadataIndex: number, tData: TData): number { + let currentBindingIndex = metadataIndex - 1; + + // If the slot before the metadata holds a string, we know that this + // metadata applies to an interpolation with at least 2 bindings, and + // we need to search further to access the first binding value. + let currentValue = tData[currentBindingIndex]; + + // We need to iterate until we hit either a: + // - TNode (it is an element slot marking the end of `consts` section), OR a + // - metadata string (slot is attribute metadata or a previous node's property metadata) + while (typeof currentValue === 'string' && !isPropMetadataString(currentValue)) { + currentValue = tData[--currentBindingIndex]; + } + return currentBindingIndex + 1; +} + +function collectHostPropertyBindings( + tNode: TNode, lView: LView, tData: TData): {[key: string]: string} { + const properties: {[key: string]: string} = {}; + + // Host binding values for a node are stored after directives on that node + let hostPropIndex = tNode.directiveEnd; + let propMetadata = tData[hostPropIndex] as any; + + // When we reach a value in TView.data that is not a string, we know we've + // hit the next node's providers and directives and should stop copying data. + while (typeof propMetadata === 'string') { + const propertyName = propMetadata.split(INTERPOLATION_DELIMITER)[0]; + properties[propertyName] = lView[hostPropIndex]; + propMetadata = tData[++hostPropIndex]; + } + return properties; +} + // Need to keep the nodes in a global Map so that multiple angular apps are supported. const _nativeNodeToDebugNode = new Map(); diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 5dc30ac629..46de046b50 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -32,7 +32,7 @@ import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection' import {LQueries} from './interfaces/query'; import {GlobalTargetResolver, ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer'; import {SanitizerFn} from './interfaces/sanitization'; -import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TVIEW, TView} from './interfaces/view'; +import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; @@ -41,7 +41,7 @@ import {getInitialClassNameValue, initializeStaticContext as initializeStaticSty import {BoundPlayerFactory} from './styling/player_factory'; import {createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util'; import {NO_CHANGE} from './tokens'; -import {findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util'; +import {INTERPOLATION_DELIMITER, findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util'; @@ -719,8 +719,8 @@ export function createTView( template: templateFn, viewQuery: viewQuery, node: null !, - data: blueprint.slice(), // Fill in to match HEADER_OFFSET in LView - childIndex: -1, // Children set in addToViewTree(), if any + data: blueprint.slice().fill(null, bindingStartIndex), + childIndex: -1, // Children set in addToViewTree(), if any bindingStartIndex: bindingStartIndex, viewQueryStartIndex: initialViewLength, expandoStartIndex: initialViewLength, @@ -1154,14 +1154,9 @@ function elementPropertyInternal( validateProperty(propName); ngDevMode.rendererSetProperty++; } - const tView = lView[TVIEW]; - const lastBindingIndex = lView[BINDING_INDEX] - 1; - if (nativeOnly && tView.data[lastBindingIndex] == null) { - // We need to store the host property name so it can be accessed by DebugElement.properties. - // Host properties cannot have interpolations, so using the last binding index is - // sufficient. - tView.data[lastBindingIndex] = propName; - } + + savePropertyDebugData(tNode, lView, propName, lView[TVIEW].data, nativeOnly); + const renderer = loadRendererFn ? loadRendererFn(tNode, lView) : lView[RENDERER]; // It is assumed that the sanitizer is only added when the compiler determines that the property // is risky, so sanitization can be done without further checks. @@ -1175,6 +1170,34 @@ function elementPropertyInternal( } } +/** + * Stores debugging data for this property binding on first template pass. + * This enables features like DebugElement.properties. + */ +function savePropertyDebugData( + tNode: TNode, lView: LView, propName: string, tData: TData, + nativeOnly: boolean | undefined): void { + const lastBindingIndex = lView[BINDING_INDEX] - 1; + + // Bind/interpolation functions save binding metadata in the last binding index, + // but leave the property name blank. If the interpolation delimiter is at the 0 + // index, we know that this is our first pass and the property name still needs to + // be set. + const bindingMetadata = tData[lastBindingIndex] as string; + if (bindingMetadata[0] == INTERPOLATION_DELIMITER) { + tData[lastBindingIndex] = propName + bindingMetadata; + + // We don't want to store indices for host bindings because they are stored in a + // different part of LView (the expando section). + if (!nativeOnly) { + if (tNode.propertyMetadataStartIndex == -1) { + tNode.propertyMetadataStartIndex = lastBindingIndex; + } + tNode.propertyMetadataEndIndex = lastBindingIndex + 1; + } + } +} + /** * Constructs a TNode object from the arguments. * @@ -1204,6 +1227,8 @@ export function createTNode( injectorIndex: tParent ? tParent.injectorIndex : -1, directiveStart: -1, directiveEnd: -1, + propertyMetadataStartIndex: -1, + propertyMetadataEndIndex: -1, flags: 0, providerIndexes: 0, tagName: tagName, @@ -2756,7 +2781,9 @@ export function markDirty(component: T) { */ export function bind(value: T): T|NO_CHANGE { const lView = getLView(); - return bindingUpdated(lView, lView[BINDING_INDEX]++, value) ? value : NO_CHANGE; + const bindingIndex = lView[BINDING_INDEX]++; + storeBindingMetadata(lView); + return bindingUpdated(lView, bindingIndex, value) ? value : NO_CHANGE; } /** @@ -2789,13 +2816,23 @@ export function interpolationV(values: any[]): string|NO_CHANGE { ngDevMode && assertEqual(values.length % 2, 1, 'should have an odd number of values'); let different = false; const lView = getLView(); - + const tData = lView[TVIEW].data; let bindingIndex = lView[BINDING_INDEX]; + + if (tData[bindingIndex] == null) { + // 2 is the index of the first static interstitial value (ie. not prefix) + for (let i = 2; i < values.length; i += 2) { + tData[bindingIndex++] = values[i]; + } + bindingIndex = lView[BINDING_INDEX]; + } + for (let i = 1; i < values.length; i += 2) { // Check if bindings (odd indexes) have changed bindingUpdated(lView, bindingIndex++, values[i]) && (different = true); } lView[BINDING_INDEX] = bindingIndex; + storeBindingMetadata(lView, values[0], values[values.length - 1]); if (!different) { return NO_CHANGE; @@ -2819,8 +2856,8 @@ export function interpolationV(values: any[]): string|NO_CHANGE { */ export function interpolation1(prefix: string, v0: any, suffix: string): string|NO_CHANGE { const lView = getLView(); - const different = bindingUpdated(lView, lView[BINDING_INDEX], v0); - lView[BINDING_INDEX] += 1; + const different = bindingUpdated(lView, lView[BINDING_INDEX]++, v0); + storeBindingMetadata(lView, prefix, suffix); return different ? prefix + renderStringify(v0) + suffix : NO_CHANGE; } @@ -2828,9 +2865,16 @@ export function interpolation1(prefix: string, v0: any, suffix: string): string| export function interpolation2( prefix: string, v0: any, i0: string, v1: any, suffix: string): string|NO_CHANGE { const lView = getLView(); - const different = bindingUpdated2(lView, lView[BINDING_INDEX], v0, v1); + const bindingIndex = lView[BINDING_INDEX]; + const different = bindingUpdated2(lView, bindingIndex, v0, v1); lView[BINDING_INDEX] += 2; + // Only set static strings the first time (data will be null subsequent runs). + const data = storeBindingMetadata(lView, prefix, suffix); + if (data) { + lView[TVIEW].data[bindingIndex] = i0; + } + return different ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + suffix : NO_CHANGE; } @@ -2839,9 +2883,18 @@ export function interpolation3( prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string): string| NO_CHANGE { const lView = getLView(); - const different = bindingUpdated3(lView, lView[BINDING_INDEX], v0, v1, v2); + const bindingIndex = lView[BINDING_INDEX]; + const different = bindingUpdated3(lView, bindingIndex, v0, v1, v2); lView[BINDING_INDEX] += 3; + // Only set static strings the first time (data will be null subsequent runs). + const data = storeBindingMetadata(lView, prefix, suffix); + if (data) { + const tData = lView[TVIEW].data; + tData[bindingIndex] = i0; + tData[bindingIndex + 1] = i1; + } + return different ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + suffix : NO_CHANGE; @@ -2852,9 +2905,19 @@ export function interpolation4( prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, suffix: string): string|NO_CHANGE { const lView = getLView(); - const different = bindingUpdated4(lView, lView[BINDING_INDEX], v0, v1, v2, v3); + const bindingIndex = lView[BINDING_INDEX]; + const different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3); lView[BINDING_INDEX] += 4; + // Only set static strings the first time (data will be null subsequent runs). + const data = storeBindingMetadata(lView, prefix, suffix); + if (data) { + const tData = lView[TVIEW].data; + tData[bindingIndex] = i0; + tData[bindingIndex + 1] = i1; + tData[bindingIndex + 2] = i2; + } + return different ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + renderStringify(v3) + suffix : @@ -2871,6 +2934,16 @@ export function interpolation5( different = bindingUpdated(lView, bindingIndex + 4, v4) || different; lView[BINDING_INDEX] += 5; + // Only set static strings the first time (data will be null subsequent runs). + const data = storeBindingMetadata(lView, prefix, suffix); + if (data) { + const tData = lView[TVIEW].data; + tData[bindingIndex] = i0; + tData[bindingIndex + 1] = i1; + tData[bindingIndex + 2] = i2; + tData[bindingIndex + 3] = i3; + } + return different ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + renderStringify(v3) + i3 + renderStringify(v4) + suffix : @@ -2887,6 +2960,17 @@ export function interpolation6( different = bindingUpdated2(lView, bindingIndex + 4, v4, v5) || different; lView[BINDING_INDEX] += 6; + // Only set static strings the first time (data will be null subsequent runs). + const data = storeBindingMetadata(lView, prefix, suffix); + if (data) { + const tData = lView[TVIEW].data; + tData[bindingIndex] = i0; + tData[bindingIndex + 1] = i1; + tData[bindingIndex + 2] = i2; + tData[bindingIndex + 3] = i3; + tData[bindingIndex + 4] = i4; + } + return different ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + suffix : @@ -2904,6 +2988,18 @@ export function interpolation7( different = bindingUpdated3(lView, bindingIndex + 4, v4, v5, v6) || different; lView[BINDING_INDEX] += 7; + // Only set static strings the first time (data will be null subsequent runs). + const data = storeBindingMetadata(lView, prefix, suffix); + if (data) { + const tData = lView[TVIEW].data; + tData[bindingIndex] = i0; + tData[bindingIndex + 1] = i1; + tData[bindingIndex + 2] = i2; + tData[bindingIndex + 3] = i3; + tData[bindingIndex + 4] = i4; + tData[bindingIndex + 5] = i5; + } + return different ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + i5 + @@ -2922,6 +3018,19 @@ export function interpolation8( different = bindingUpdated4(lView, bindingIndex + 4, v4, v5, v6, v7) || different; lView[BINDING_INDEX] += 8; + // Only set static strings the first time (data will be null subsequent runs). + const data = storeBindingMetadata(lView, prefix, suffix); + if (data) { + const tData = lView[TVIEW].data; + tData[bindingIndex] = i0; + tData[bindingIndex + 1] = i1; + tData[bindingIndex + 2] = i2; + tData[bindingIndex + 3] = i3; + tData[bindingIndex + 4] = i4; + tData[bindingIndex + 5] = i5; + tData[bindingIndex + 6] = i6; + } + return different ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + i5 + @@ -2929,6 +3038,30 @@ export function interpolation8( NO_CHANGE; } +/** + * Creates binding metadata for a particular binding and stores it in + * TView.data. These are generated in order to support DebugElement.properties. + * + * Each binding / interpolation will have one (including attribute bindings) + * because at the time of binding, we don't know to which instruction the binding + * belongs. It is always stored in TView.data at the index of the last binding + * value in LView (e.g. for interpolation8, it would be stored at the index of + * the 8th value). + * + * @param lView The LView that contains the current binding index. + * @param prefix The static prefix string + * @param suffix The static suffix string + * + * @returns Newly created binding metadata string for this binding or null + */ +function storeBindingMetadata(lView: LView, prefix = '', suffix = ''): string|null { + const tData = lView[TVIEW].data; + const lastBindingIndex = lView[BINDING_INDEX] - 1; + const value = INTERPOLATION_DELIMITER + prefix + INTERPOLATION_DELIMITER + suffix; + + return tData[lastBindingIndex] == null ? (tData[lastBindingIndex] = value) : null; +} + /** Store a value in the `data` at a given `index`. */ export function store(index: number, value: T): void { const lView = getLView(); diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 2faf312e7e..1c8775668f 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -169,6 +169,18 @@ export interface TNode { */ directiveEnd: number; + /** + * Stores the first index where property binding metadata is stored for + * this node. + */ + propertyMetadataStartIndex: number; + + /** + * Stores the exclusive final index where property binding metadata is + * stored for this node. + */ + propertyMetadataEndIndex: number; + /** * Stores if Node isComponent, isProjected, hasContentQuery and hasClassInput */ @@ -476,7 +488,6 @@ export type PropertyAliases = { */ export type PropertyAliasValue = (number | string)[]; - /** * This array contains information about input properties that * need to be set once from attribute data. It's ordered by diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 6ac11c11ac..e7ed535133 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -559,6 +559,18 @@ export type HookData = (number | (() => void))[]; * Each host property's name is stored here at the same index as its value in the * data array. * + * Each property binding name is stored here at the same index as its value in + * the data array. If the binding is an interpolation, the static string values + * are stored parallel to the dynamic values. Example: + * + * id="prefix {{ v0 }} a {{ v1 }} b {{ v2 }} suffix" + * + * LView | TView.data + *------------------------ + * v0 value | 'a' + * v1 value | 'b' + * v2 value | id � prefix � suffix + * * Injector bloom filters are also stored here. */ export type TData = diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts index 8376c7881f..f05f3764ac 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -291,4 +291,28 @@ export function resolveDocument(element: RElement & {ownerDocument: Document}) { export function resolveBody(element: RElement & {ownerDocument: Document}) { return {name: 'body', target: element.ownerDocument.body}; -} \ No newline at end of file +} + +/** + * The special delimiter we use to separate property names, prefixes, and suffixes + * in property binding metadata. See storeBindingMetadata(). + * + * We intentionally use the Unicode "REPLACEMENT CHARACTER" (U+FFFD) as a delimiter + * because it is a very uncommon character that is unlikely to be part of a user's + * property names or interpolation strings. If it is in fact used in a property + * binding, DebugElement.properties will not return the correct value for that + * binding. However, there should be no runtime effect for real applications. + * + * This character is typically rendered as a question mark inside of a diamond. + * See https://en.wikipedia.org/wiki/Specials_(Unicode_block) + * + */ +export const INTERPOLATION_DELIMITER = `�`; + +/** + * Determines whether or not the given string is a property metadata string. + * See storeBindingMetadata(). + */ +export function isPropMetadataString(str: string): boolean { + return str.indexOf(INTERPOLATION_DELIMITER) >= 0; +} diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 29bd825d2c..f22de17b05 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -77,6 +77,9 @@ { "name": "INJECTOR_BLOOM_PARENT_SIZE" }, + { + "name": "INTERPOLATION_DELIMITER" + }, { "name": "InjectFlags" }, @@ -1100,6 +1103,9 @@ { "name": "saveNameToExportMap" }, + { + "name": "savePropertyDebugData" + }, { "name": "saveResolvedLocalsInData" }, @@ -1190,6 +1196,9 @@ { "name": "shouldSearchParent" }, + { + "name": "storeBindingMetadata" + }, { "name": "storeCleanupFn" }, diff --git a/packages/core/test/test_bed_spec.ts b/packages/core/test/test_bed_spec.ts index a8f1f82cde..c76adf23e8 100644 --- a/packages/core/test/test_bed_spec.ts +++ b/packages/core/test/test_bed_spec.ts @@ -52,13 +52,22 @@ export class WithRefsCmp { export class InheritedCmp extends SimpleCmp { } -@Directive({selector: '[dir]', host: {'[id]': 'id'}}) +@Directive({selector: '[hostBindingDir]', host: {'[id]': 'id'}}) export class HostBindingDir { id = 'one'; } -@Component({selector: 'host-binding-parent', template: '
'}) -export class HostBindingParent { +@Component({ + selector: 'component-with-prop-bindings', + template: ` +
+

+

+ ` +}) +export class ComponentWithPropBindings { + title = 'some title'; + label = 'some label'; } @Component({ @@ -72,7 +81,8 @@ export class SimpleApp { @NgModule({ declarations: [ - HelloWorld, SimpleCmp, WithRefsCmp, InheritedCmp, SimpleApp, HostBindingParent, HostBindingDir + HelloWorld, SimpleCmp, WithRefsCmp, InheritedCmp, SimpleApp, ComponentWithPropBindings, + HostBindingDir ], imports: [GreetingModule], providers: [ @@ -123,12 +133,21 @@ describe('TestBed', () => { expect(greetingByCss.nativeElement).toHaveText('Hello TestBed!'); }); - it('should give the ability to access host properties', () => { - const fixture = TestBed.createComponent(HostBindingParent); + it('should give the ability to access property bindings on a node', () => { + const fixture = TestBed.createComponent(ComponentWithPropBindings); fixture.detectChanges(); - const divElement = fixture.debugElement.children[0]; - expect(divElement.properties).toEqual({id: 'one'}); + const divElement = fixture.debugElement.query(By.css('div')); + expect(divElement.properties).toEqual({id: 'one', title: 'some title'}); + }); + + it('should give the ability to access interpolated properties on a node', () => { + const fixture = TestBed.createComponent(ComponentWithPropBindings); + fixture.detectChanges(); + + const paragraphEl = fixture.debugElement.query(By.css('p')); + expect(paragraphEl.properties) + .toEqual({title: '( some label - some title )', id: '[ some label ] [ some title ]'}); }); it('should give access to the node injector', () => {