From 3821dc5f6c2e63cd08eac5cf4f898d3f8081af94 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Mon, 20 Jul 2020 22:06:09 -0700 Subject: [PATCH] refactor(core): add human readable `debug` for i18n (#38154) I18n code breaks up internationalization into opCodes which are then stored in arrays. To make it easier to debug the codebase this PR adds `debug` property to the arrays which presents the data in human readable format. PR Close #38154 --- .../size-tracking/integration-payloads.json | 2 +- packages/core/src/render3/i18n.ts | 43 +- packages/core/src/render3/i18n_debug.ts | 187 ++++++ .../src/render3/instructions/lview_debug.ts | 203 ------- .../core/src/render3/instructions/shared.ts | 7 + packages/core/src/render3/interfaces/i18n.ts | 147 +++-- packages/core/src/render3/interfaces/node.ts | 2 + packages/core/src/render3/util/debug_utils.ts | 35 +- packages/core/test/render3/i18n_debug_spec.ts | 140 +++++ packages/core/test/render3/i18n_spec.ts | 534 ++++++------------ packages/core/test/render3/utils.ts | 73 +++ 11 files changed, 741 insertions(+), 632 deletions(-) create mode 100644 packages/core/src/render3/i18n_debug.ts create mode 100644 packages/core/test/render3/i18n_debug_spec.ts create mode 100644 packages/core/test/render3/utils.ts diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index 6535e3d4b6..52acd55885 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -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": 1209659 + "bundle": 1212027 } } } diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index 1377482c80..77da44afd2 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import '../util/ng_i18n_closure_mode'; +import '../util/ng_dev_mode'; import {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization'; import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../sanitization/html_sanitizer'; @@ -16,8 +17,8 @@ import {assertDataInRange, assertDefined, assertEqual} from '../util/assert'; import {bindingUpdated} from './bindings'; import {attachPatchData} from './context_discovery'; +import {i18nMutateOpCodesToString, i18nUpdateOpCodesToString} from './i18n_debug'; import {setDelayProjection} from './instructions/all'; -import {attachI18nOpCodesDebug} from './instructions/lview_debug'; import {allocExpando, elementAttributeInternal, elementPropertyInternal, getOrCreateTNode, setInputsForProperty, setNgReflectProperties, textBindingInternal} from './instructions/shared'; import {LContainer, NATIVE} from './interfaces/container'; import {getDocument} from './interfaces/document'; @@ -29,6 +30,7 @@ import {isLContainer} from './interfaces/type_checks'; import {HEADER_OFFSET, LView, RENDERER, T_HOST, TVIEW, TView} from './interfaces/view'; import {appendChild, applyProjection, createTextNode, nativeRemoveNode} from './node_manipulation'; import {getBindingIndex, getIsParent, getLView, getPreviousOrParentTNode, getTView, nextBindingIndex, setIsNotParent, setPreviousOrParentTNode} from './state'; +import {attachDebugGetter} from './util/debug_utils'; import {renderStringify} from './util/misc_utils'; import {getNativeByIndex, getNativeByTNode, getTNode, load} from './util/view_utils'; @@ -267,6 +269,9 @@ function generateBindingUpdateOpCodes( str: string, destinationNode: number, attrName?: string, sanitizeFn: SanitizerFn|null = null): I18nUpdateOpCodes { const updateOpCodes: I18nUpdateOpCodes = [null, null]; // Alloc space for mask and size + if (ngDevMode) { + attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString); + } const textParts = str.split(BINDING_REGEXP); let mask = 0; @@ -395,6 +400,9 @@ function i18nStartFirstPass( let parentIndexPointer = 0; parentIndexStack[parentIndexPointer] = parentIndex; const createOpCodes: I18nMutateOpCodes = []; + if (ngDevMode) { + attachDebugGetter(createOpCodes, i18nMutateOpCodesToString); + } // If the previous node wasn't the direct parent then we have a translation without top level // element and we need to keep a reference of the previous element if there is one. We should also // keep track whether an element was a parent node or not, so that the logic that consumes @@ -411,6 +419,9 @@ function i18nStartFirstPass( createOpCodes.push(previousTNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select); } const updateOpCodes: I18nUpdateOpCodes = []; + if (ngDevMode) { + attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString); + } const icuExpressions: TIcu[] = []; if (message === '' && isRootTemplateMessage(subTemplateIndex)) { @@ -507,10 +518,6 @@ function i18nStartFirstPass( allocExpando(tView, lView, i18nVarsCount); } - ngDevMode && - attachI18nOpCodesDebug( - createOpCodes, updateOpCodes, icuExpressions.length ? icuExpressions : null, lView); - // NOTE: local var needed to properly assert the type of `TI18n`. const tI18n: TI18n = { vars: i18nVarsCount, @@ -780,7 +787,7 @@ function readCreateOpCodes( visitedNodes.push(textNodeIndex); setIsNotParent(); } else if (typeof opCode == 'number') { - switch (opCode & I18nMutateOpCode.MASK_OPCODE) { + switch (opCode & I18nMutateOpCode.MASK_INSTRUCTION) { case I18nMutateOpCode.AppendChild: const destinationNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_PARENT; let destinationTNode: TNode; @@ -799,9 +806,10 @@ function readCreateOpCodes( appendI18nNode(tView, currentTNode!, destinationTNode, previousTNode, lView); break; case I18nMutateOpCode.Select: - // Negative indicies indicate that a given TNode is a sibling node, not a parent node + // Negative indices indicate that a given TNode is a sibling node, not a parent node // (see `i18nStartFirstPass` for additional information). const isParent = opCode >= 0; + // FIXME(misko): This SHIFT_REF looks suspect as it does not have mask. const nodeIndex = (isParent ? opCode : ~opCode) >>> I18nMutateOpCode.SHIFT_REF; visitedNodes.push(nodeIndex); previousTNode = currentTNode; @@ -890,7 +898,6 @@ function readUpdateOpCodes( value += opCode; } else if (typeof opCode == 'number') { if (opCode < 0) { - // It's a binding index whose value is negative value += renderStringify(lView[bindingsStartIndex - opCode]); } else { const nodeIndex = opCode >>> I18nUpdateOpCode.SHIFT_REF; @@ -909,6 +916,7 @@ function readUpdateOpCodes( textBindingInternal(lView, nodeIndex, value); break; case I18nUpdateOpCode.IcuSwitch: + // FIXME(misko): Pull to a new function `icuSwitchCase` tIcuIndex = updateOpCodes[++j] as number; tIcu = icus![tIcuIndex]; icuTNode = getTNode(tView, nodeIndex) as TIcuContainerNode; @@ -917,7 +925,7 @@ function readUpdateOpCodes( const removeCodes = tIcu.remove[icuTNode.activeCaseIndex]; for (let k = 0; k < removeCodes.length; k++) { const removeOpCode = removeCodes[k] as number; - switch (removeOpCode & I18nMutateOpCode.MASK_OPCODE) { + switch (removeOpCode & I18nMutateOpCode.MASK_INSTRUCTION) { case I18nMutateOpCode.Remove: const nodeIndex = removeOpCode >>> I18nMutateOpCode.SHIFT_REF; // Remove DOM element, but do *not* mark TNode as detached, since we are @@ -951,6 +959,7 @@ function readUpdateOpCodes( } break; case I18nUpdateOpCode.IcuUpdate: + // FIXME(misko): Pull to a new function `icuUpdateCase` tIcuIndex = updateOpCodes[++j] as number; tIcu = icus![tIcuIndex]; icuTNode = getTNode(tView, nodeIndex) as TIcuContainerNode; @@ -1044,6 +1053,9 @@ function i18nAttributesFirstPass(lView: LView, tView: TView, index: number, valu const previousElement = getPreviousOrParentTNode(); const previousElementIndex = previousElement.index - HEADER_OFFSET; const updateOpCodes: I18nUpdateOpCodes = []; + if (ngDevMode) { + attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString); + } for (let i = 0; i < values.length; i += 2) { const attrName = values[i]; const message = values[i + 1]; @@ -1180,9 +1192,9 @@ function getCaseIndex(icuExpression: TIcu, bindingValue: string): number { function icuStart( tIcus: TIcu[], icuExpression: IcuExpression, startIndex: number, expandoStartIndex: number): void { - const createCodes = []; - const removeCodes = []; - const updateCodes = []; + const createCodes: I18nMutateOpCodes[] = []; + const removeCodes: I18nMutateOpCodes[] = []; + const updateCodes: I18nUpdateOpCodes[] = []; const vars = []; const childIcus: number[][] = []; for (let i = 0; i < icuExpression.values.length; i++) { @@ -1240,6 +1252,11 @@ function parseIcuCase( } const wrapper = getTemplateContent(inertBodyElement!) as Element || inertBodyElement; const opCodes: IcuCase = {vars: 0, childIcus: [], create: [], remove: [], update: []}; + if (ngDevMode) { + attachDebugGetter(opCodes.create, i18nMutateOpCodesToString); + attachDebugGetter(opCodes.remove, i18nMutateOpCodesToString); + attachDebugGetter(opCodes.update, i18nUpdateOpCodesToString); + } parseNodes(wrapper.firstChild, opCodes, parentIndex, nestedIcus, tIcus, expandoStartIndex); return opCodes; } @@ -1364,6 +1381,7 @@ function parseNodes( 3, // skip 3 opCodes if not changed -1 - nestedIcu.mainBinding, nestedIcuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch, + // FIXME(misko): Index should be part of the opcode nestTIcuIndex, mask, // mask of all the bindings of this ICU expression 2, // skip 2 opCodes if not changed @@ -1371,6 +1389,7 @@ function parseNodes( nestTIcuIndex); icuCase.remove.push( nestTIcuIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.RemoveNestedIcu, + // FIXME(misko): Index should be part of the opcode nestedIcuNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove); } } diff --git a/packages/core/src/render3/i18n_debug.ts b/packages/core/src/render3/i18n_debug.ts new file mode 100644 index 0000000000..da0cbe7fcb --- /dev/null +++ b/packages/core/src/render3/i18n_debug.ts @@ -0,0 +1,187 @@ +/** + * @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, assertString} from '../util/assert'; + +import {COMMENT_MARKER, ELEMENT_MARKER, getInstructionFromI18nMutateOpCode, getParentFromI18nMutateOpCode, getRefFromI18nMutateOpCode, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes} from './interfaces/i18n'; + +/** + * Converts `I18nUpdateOpCodes` array into a human readable format. + * + * This function is attached to the `I18nUpdateOpCodes.debug` property if `ngDevMode` is enabled. + * This function provides a human readable view of the opcodes. This is useful when debugging the + * application as well as writing more readable tests. + * + * @param this `I18nUpdateOpCodes` if attached as a method. + * @param opcodes `I18nUpdateOpCodes` if invoked as a function. + */ +export function i18nUpdateOpCodesToString( + this: I18nUpdateOpCodes|void, opcodes?: I18nUpdateOpCodes): string[] { + const parser = new OpCodeParser(opcodes || (Array.isArray(this) ? this : [])); + let lines: string[] = []; + + function consumeOpCode(value: number): string { + const ref = value >>> I18nUpdateOpCode.SHIFT_REF; + const opCode = value & I18nUpdateOpCode.MASK_OPCODE; + switch (opCode) { + case I18nUpdateOpCode.Text: + return `(lView[${ref}] as Text).textContent = $$$`; + case I18nUpdateOpCode.Attr: + const attrName = parser.consumeString(); + const sanitizationFn = parser.consumeFunction(); + const value = sanitizationFn ? `(${sanitizationFn})($$$)` : '$$$'; + return `(lView[${ref}] as Element).setAttribute('${attrName}', ${value})`; + case I18nUpdateOpCode.IcuSwitch: + return `icuSwitchCase(lView[${ref}] as Comment, ${parser.consumeNumber()}, $$$)`; + case I18nUpdateOpCode.IcuUpdate: + return `icuUpdateCase(lView[${ref}] as Comment, ${parser.consumeNumber()})`; + } + throw new Error('unexpected OpCode'); + } + + + while (parser.hasMore()) { + let mask = parser.consumeNumber(); + let size = parser.consumeNumber(); + const end = parser.i + size; + const statements: string[] = []; + let statement = ''; + while (parser.i < end) { + let value = parser.consumeNumberOrString(); + if (typeof value === 'string') { + statement += value; + } else if (value < 0) { + // Negative numbers are ref indexes + statement += '${lView[' + (0 - value) + ']}'; + } else { + // Positive numbers are operations. + const opCodeText = consumeOpCode(value); + statements.push(opCodeText.replace('$$$', '`' + statement + '`') + ';'); + statement = ''; + } + } + lines.push(`if (mask & 0b${mask.toString(2)}) { ${statements.join(' ')} }`); + } + return lines; +} + +/** + * Converts `I18nMutableOpCodes` array into a human readable format. + * + * This function is attached to the `I18nMutableOpCodes.debug` if `ngDevMode` is enabled. This + * function provides a human readable view of the opcodes. This is useful when debugging the + * application as well as writing more readable tests. + * + * @param this `I18nMutableOpCodes` if attached as a method. + * @param opcodes `I18nMutableOpCodes` if invoked as a function. + */ +export function i18nMutateOpCodesToString( + this: I18nMutateOpCodes|void, opcodes?: I18nMutateOpCodes): string[] { + const parser = new OpCodeParser(opcodes || (Array.isArray(this) ? this : [])); + let lines: string[] = []; + + function consumeOpCode(opCode: number): string { + const parent = getParentFromI18nMutateOpCode(opCode); + const ref = getRefFromI18nMutateOpCode(opCode); + switch (getInstructionFromI18nMutateOpCode(opCode)) { + case I18nMutateOpCode.Select: + lastRef = ref; + return ''; + case I18nMutateOpCode.AppendChild: + return `(lView[${parent}] as Element).appendChild(lView[${lastRef}])`; + case I18nMutateOpCode.Remove: + return `(lView[${parent}] as Element).remove(lView[${ref}])`; + case I18nMutateOpCode.Attr: + return `(lView[${ref}] as Element).setAttribute("${parser.consumeString()}", "${ + parser.consumeString()}")`; + case I18nMutateOpCode.ElementEnd: + return `setPreviousOrParentTNode(tView.data[${ref}] as TNode)`; + case I18nMutateOpCode.RemoveNestedIcu: + // FIXME(misko): refactor to have a real method in i18n.ts. + return `removeNestedICU(${ref})`; + } + throw new Error('Unexpected OpCode'); + } + + let lastRef = -1; + while (parser.hasMore()) { + let value = parser.consumeNumberStringOrMarker(); + if (value === COMMENT_MARKER) { + const text = parser.consumeString(); + lastRef = parser.consumeNumber(); + lines.push(`lView[${lastRef}] = document.createComment("${text}")`); + } else if (value === ELEMENT_MARKER) { + const text = parser.consumeString(); + lastRef = parser.consumeNumber(); + lines.push(`lView[${lastRef}] = document.createElement("${text}")`); + } else if (typeof value === 'string') { + lastRef = parser.consumeNumber(); + lines.push(`lView[${lastRef}] = document.createTextNode("${value}")`); + } else if (typeof value === 'number') { + const line = consumeOpCode(value); + line && lines.push(line); + } else { + throw new Error('Unexpected value'); + } + } + + return lines; +} + + +class OpCodeParser { + i: number = 0; + codes: any[]; + + constructor(codes: any[]) { + this.codes = codes; + } + + hasMore() { + return this.i < this.codes.length; + } + + consumeNumber(): number { + let value = this.codes[this.i++]; + assertNumber(value, 'expecting number in OpCode'); + return value; + } + + consumeString(): string { + let value = this.codes[this.i++]; + assertString(value, 'expecting string in OpCode'); + return value; + } + + consumeFunction(): Function|null { + let value = this.codes[this.i++]; + if (value === null || typeof value === 'function') { + return value; + } + throw new Error('expecting function in OpCode'); + } + + consumeNumberOrString(): number|string { + let value = this.codes[this.i++]; + if (typeof value === 'string') { + return value; + } + assertNumber(value, 'expecting number or string in OpCode'); + return value; + } + + consumeNumberStringOrMarker(): number|string|COMMENT_MARKER|ELEMENT_MARKER { + let value = this.codes[this.i++]; + if (typeof value === 'string' || typeof value === 'number' || value == COMMENT_MARKER || + value == ELEMENT_MARKER) { + return value; + } + assertNumber(value, 'expecting number, string, COMMENT_MARKER or ELEMENT_MARKER in OpCode'); + return value; + } +} diff --git a/packages/core/src/render3/instructions/lview_debug.ts b/packages/core/src/render3/instructions/lview_debug.ts index 1bd45018d0..50c592d28c 100644 --- a/packages/core/src/render3/instructions/lview_debug.ts +++ b/packages/core/src/render3/instructions/lview_debug.ts @@ -550,206 +550,3 @@ export function readLViewValue(value: any): LView|null { } return null; } - -export class I18NDebugItem { - [key: string]: any; - - get tNode() { - return getTNode(this._lView[TVIEW], this.nodeIndex); - } - - constructor( - public __raw_opCode: any, private _lView: LView, public nodeIndex: number, - public type: string) {} -} - -/** - * Turns a list of "Create" & "Update" OpCodes into a human-readable list of operations for - * debugging purposes. - * @param mutateOpCodes mutation opCodes to read - * @param updateOpCodes update opCodes to read - * @param icus list of ICU expressions - * @param lView The view the opCodes are acting on - */ -export function attachI18nOpCodesDebug( - mutateOpCodes: I18nMutateOpCodes, updateOpCodes: I18nUpdateOpCodes, icus: TIcu[]|null, - lView: LView) { - attachDebugObject(mutateOpCodes, new I18nMutateOpCodesDebug(mutateOpCodes, lView)); - attachDebugObject(updateOpCodes, new I18nUpdateOpCodesDebug(updateOpCodes, icus, lView)); - - if (icus) { - icus.forEach(icu => { - icu.create.forEach(icuCase => { - attachDebugObject(icuCase, new I18nMutateOpCodesDebug(icuCase, lView)); - }); - icu.update.forEach(icuCase => { - attachDebugObject(icuCase, new I18nUpdateOpCodesDebug(icuCase, icus, lView)); - }); - }); - } -} - -export class I18nMutateOpCodesDebug implements I18nOpCodesDebug { - constructor(private readonly __raw_opCodes: I18nMutateOpCodes, private readonly __lView: LView) {} - - /** - * A list of operation information about how the OpCodes will act on the view. - */ - get operations() { - const {__lView, __raw_opCodes} = this; - const results: any[] = []; - - for (let i = 0; i < __raw_opCodes.length; i++) { - const opCode = __raw_opCodes[i]; - let result: any; - if (typeof opCode === 'string') { - result = { - __raw_opCode: opCode, - type: 'Create Text Node', - nodeIndex: __raw_opCodes[++i], - text: opCode, - }; - } - - if (typeof opCode === 'number') { - switch (opCode & I18nMutateOpCode.MASK_OPCODE) { - case I18nMutateOpCode.AppendChild: - const destinationNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_PARENT; - result = new I18NDebugItem(opCode, __lView, destinationNodeIndex, 'AppendChild'); - break; - case I18nMutateOpCode.Select: - const nodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; - result = new I18NDebugItem(opCode, __lView, nodeIndex, 'Select'); - break; - case I18nMutateOpCode.ElementEnd: - let elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; - result = new I18NDebugItem(opCode, __lView, elementIndex, 'ElementEnd'); - break; - case I18nMutateOpCode.Attr: - elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; - result = new I18NDebugItem(opCode, __lView, elementIndex, 'Attr'); - result['attrName'] = __raw_opCodes[++i]; - result['attrValue'] = __raw_opCodes[++i]; - break; - } - } - - if (!result) { - switch (opCode) { - case COMMENT_MARKER: - result = { - __raw_opCode: opCode, - type: 'COMMENT_MARKER', - commentValue: __raw_opCodes[++i], - nodeIndex: __raw_opCodes[++i], - }; - break; - case ELEMENT_MARKER: - result = { - __raw_opCode: opCode, - type: 'ELEMENT_MARKER', - }; - break; - } - } - - if (!result) { - result = { - __raw_opCode: opCode, - type: 'Unknown Op Code', - code: opCode, - }; - } - - results.push(result); - } - - return results; - } -} - -export class I18nUpdateOpCodesDebug implements I18nOpCodesDebug { - constructor( - private readonly __raw_opCodes: I18nUpdateOpCodes, private readonly icus: TIcu[]|null, - private readonly __lView: LView) {} - - /** - * A list of operation information about how the OpCodes will act on the view. - */ - get operations() { - const {__lView, __raw_opCodes, icus} = this; - const results: any[] = []; - - for (let i = 0; i < __raw_opCodes.length; i++) { - // bit code to check if we should apply the next update - const checkBit = __raw_opCodes[i] as number; - // Number of opCodes to skip until next set of update codes - const skipCodes = __raw_opCodes[++i] as number; - let value = ''; - for (let j = i + 1; j <= (i + skipCodes); j++) { - const opCode = __raw_opCodes[j]; - if (typeof opCode === 'string') { - value += opCode; - } else if (typeof opCode == 'number') { - if (opCode < 0) { - // It's a binding index whose value is negative - // We cannot know the value of the binding so we only show the index - value += `�${- opCode - 1}�`; - } else { - const nodeIndex = opCode >>> I18nUpdateOpCode.SHIFT_REF; - let tIcuIndex: number; - let tIcu: TIcu; - switch (opCode & I18nUpdateOpCode.MASK_OPCODE) { - case I18nUpdateOpCode.Attr: - const attrName = __raw_opCodes[++j] as string; - const sanitizeFn = __raw_opCodes[++j]; - results.push({ - __raw_opCode: opCode, - checkBit, - type: 'Attr', - attrValue: value, - attrName, - sanitizeFn, - }); - break; - case I18nUpdateOpCode.Text: - results.push({ - __raw_opCode: opCode, - checkBit, - type: 'Text', - nodeIndex, - text: value, - }); - break; - case I18nUpdateOpCode.IcuSwitch: - tIcuIndex = __raw_opCodes[++j] as number; - tIcu = icus![tIcuIndex]; - let result = new I18NDebugItem(opCode, __lView, nodeIndex, 'IcuSwitch'); - result['tIcuIndex'] = tIcuIndex; - result['checkBit'] = checkBit; - result['mainBinding'] = value; - result['tIcu'] = tIcu; - results.push(result); - break; - case I18nUpdateOpCode.IcuUpdate: - tIcuIndex = __raw_opCodes[++j] as number; - tIcu = icus![tIcuIndex]; - result = new I18NDebugItem(opCode, __lView, nodeIndex, 'IcuUpdate'); - result['tIcuIndex'] = tIcuIndex; - result['checkBit'] = checkBit; - result['tIcu'] = tIcu; - results.push(result); - break; - } - } - } - } - i += skipCodes; - } - return results; - } -} - -export interface I18nOpCodesDebug { - operations: any[]; -} diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index ff8b4f0488..e648853793 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -236,6 +236,13 @@ export function getOrCreateTNode( const tNode = tView.data[adjustedIndex] as TNode || createTNodeAtIndex(tView, tHostNode, adjustedIndex, type, name, attrs); setPreviousOrParentTNode(tNode, true); + if (ngDevMode) { + // For performance reasons it is important that the tNode retains the same shape during runtime. + // (To make sure that all of the code is monomorphic.) For this reason we seal the object to + // prevent class transitions. + // FIXME(misko): re-enable this once i18n code is compliant with this. + // Object.seal(tNode); + } return tNode as TElementNode & TViewNode & TContainerNode & TElementContainerNode & TProjectionNode & TIcuContainerNode; } diff --git a/packages/core/src/render3/interfaces/i18n.ts b/packages/core/src/render3/interfaces/i18n.ts index abb5327c86..f379ed9125 100644 --- a/packages/core/src/render3/interfaces/i18n.ts +++ b/packages/core/src/render3/interfaces/i18n.ts @@ -6,18 +6,31 @@ * found in the LICENSE file at https://angular.io/license */ +import {SanitizerFn} from './sanitization'; + /** * `I18nMutateOpCode` defines OpCodes for `I18nMutateOpCodes` array. * + * OpCodes are efficient operations which can be applied to the DOM to update it. (For example to + * update to a new ICU case requires that we clean up previous elements and create new ones.) + * * OpCodes contain three parts: - * 1) Parent node index offset. - * 2) Reference node index offset. - * 3) The OpCode to execute. + * 1) Parent node index offset. (p) + * 2) Reference node index offset. (r) + * 3) The instruction to execute. (i) + * + * pppp pppp pppp pppp rrrr rrrr rrrr riii + * 3322 2222 2222 1111 1111 1110 0000 0000 + * 1098 7654 3210 9876 5432 1098 7654 3210 + * + * ``` + * var parent = lView[opCode >>> SHIFT_PARENT]; + * var refNode = lView[((opCode & MASK_REF) >>> SHIFT_REF)]; + * var instruction = opCode & MASK_OPCODE; + * ``` * * See: `I18nCreateOpCodes` for example of usage. */ -import {SanitizerFn} from './sanitization'; - export const enum I18nMutateOpCode { /** * Stores shift amount for bits 17-3 that contain reference index. @@ -30,36 +43,61 @@ export const enum I18nMutateOpCode { /** * Mask for OpCode */ - MASK_OPCODE = 0b111, + MASK_INSTRUCTION = 0b111, /** - * OpCode to select a node. (next OpCode will contain the operation.) + * Mask for the Reference node (bits 16-3) + */ + // FIXME(misko): Why is this not used? + MASK_REF = 0b11111111111111000, + // 11111110000000000 + // 65432109876543210 + + /** + * Instruction to select a node. (next OpCode will contain the operation.) */ Select = 0b000, + /** - * OpCode to append the current node to `PARENT`. + * Instruction to append the current node to `PARENT`. */ AppendChild = 0b001, + /** - * OpCode to remove the `REF` node from `PARENT`. + * Instruction to remove the `REF` node from `PARENT`. */ Remove = 0b011, + /** - * OpCode to set the attribute of a node. + * Instruction to set the attribute of a node. */ Attr = 0b100, + /** - * OpCode to simulate elementEnd() + * Instruction to simulate elementEnd() */ ElementEnd = 0b101, + /** - * OpCode to read the remove OpCodes for the nested ICU + * Instruction to removed the nested ICU. */ RemoveNestedIcu = 0b110, } +export function getParentFromI18nMutateOpCode(mergedCode: number): number { + return mergedCode >>> I18nMutateOpCode.SHIFT_PARENT; +} + +export function getRefFromI18nMutateOpCode(mergedCode: number): number { + return (mergedCode & I18nMutateOpCode.MASK_REF) >>> I18nMutateOpCode.SHIFT_REF; +} + +export function getInstructionFromI18nMutateOpCode(mergedCode: number): number { + return mergedCode & I18nMutateOpCode.MASK_INSTRUCTION; +} + /** - * Marks that the next string is for element. + * Marks that the next string is an element name. * * See `I18nMutateOpCodes` documentation. */ @@ -71,7 +109,7 @@ export interface ELEMENT_MARKER { } /** - * Marks that the next string is for comment. + * Marks that the next string is comment text. * * See `I18nMutateOpCodes` documentation. */ @@ -83,6 +121,18 @@ export interface COMMENT_MARKER { marker: 'comment'; } +export interface I18nDebug { + /** + * Human readable representation of the OpCode arrays. + * + * 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?: string[]; +} + + /** * Array storing OpCode for dynamically creating `i18n` blocks. * @@ -92,50 +142,27 @@ export interface COMMENT_MARKER { * // For adding text nodes * // --------------------- * // Equivalent to: - * // const node = lView[index++] = document.createTextNode('abc'); - * // lView[1].insertBefore(node, lView[2]); - * 'abc', 1 << SHIFT_PARENT | 2 << SHIFT_REF | InsertBefore, - * - * // Equivalent to: - * // const node = lView[index++] = document.createTextNode('xyz'); - * // lView[1].appendChild(node); - * 'xyz', 1 << SHIFT_PARENT | AppendChild, + * // lView[1].appendChild(lView[0] = document.createTextNode('xyz')); + * 'xyz', 0, 1 << SHIFT_PARENT | 0 << SHIFT_REF | AppendChild, * * // For adding element nodes * // --------------------- * // Equivalent to: - * // const node = lView[index++] = document.createElement('div'); - * // lView[1].insertBefore(node, lView[2]); - * ELEMENT_MARKER, 'div', 1 << SHIFT_PARENT | 2 << SHIFT_REF | InsertBefore, - * - * // Equivalent to: - * // const node = lView[index++] = document.createElement('div'); - * // lView[1].appendChild(node); - * ELEMENT_MARKER, 'div', 1 << SHIFT_PARENT | AppendChild, + * // lView[1].appendChild(lView[0] = document.createElement('div')); + * ELEMENT_MARKER, 'div', 0, 1 << SHIFT_PARENT | 0 << SHIFT_REF | AppendChild, * * // For adding comment nodes * // --------------------- * // Equivalent to: - * // const node = lView[index++] = document.createComment(''); - * // lView[1].insertBefore(node, lView[2]); - * COMMENT_MARKER, '', 1 << SHIFT_PARENT | 2 << SHIFT_REF | InsertBefore, - * - * // Equivalent to: - * // const node = lView[index++] = document.createComment(''); - * // lView[1].appendChild(node); - * COMMENT_MARKER, '', 1 << SHIFT_PARENT | AppendChild, + * // lView[1].appendChild(lView[0] = document.createComment('')); + * COMMENT_MARKER, '', 0, 1 << SHIFT_PARENT | 0 << SHIFT_REF | AppendChild, * * // For moving existing nodes to a different location * // -------------------------------------------------- * // Equivalent to: * // const node = lView[1]; - * // lView[2].insertBefore(node, lView[3]); - * 1 << SHIFT_REF | Select, 2 << SHIFT_PARENT | 3 << SHIFT_REF | InsertBefore, - * - * // Equivalent to: - * // const node = lView[1]; * // lView[2].appendChild(node); - * 1 << SHIFT_REF | Select, 2 << SHIFT_PARENT | AppendChild, + * 1 << SHIFT_REF | Select, 2 << SHIFT_PARENT | 0 << SHIFT_REF | AppendChild, * * // For removing existing nodes * // -------------------------------------------------- @@ -147,18 +174,14 @@ export interface COMMENT_MARKER { * // -------------------------------------------------- * // const node = lView[1]; * // node.setAttribute('attr', 'value'); - * 1 << SHIFT_REF | Select, 'attr', 'value' - * // NOTE: Select followed by two string (vs select followed by OpCode) + * 1 << SHIFT_REF | Attr, 'attr', 'value' * ]; * ``` - * NOTE: - * - `index` is initial location where the extra nodes should be stored in the EXPANDO section of - * `LVIewData`. * * See: `applyI18nCreateOpCodes`; */ -export interface I18nMutateOpCodes extends Array { -} +export interface I18nMutateOpCodes extends Array, + I18nDebug {} export const enum I18nUpdateOpCode { /** @@ -171,19 +194,19 @@ export const enum I18nUpdateOpCode { MASK_OPCODE = 0b11, /** - * OpCode to update a text node. + * Instruction to update a text node. */ Text = 0b00, /** - * OpCode to update a attribute of a node. + * Instruction to update a attribute of a node. */ Attr = 0b01, /** - * OpCode to switch the current ICU case. + * Instruction to switch the current ICU case. */ IcuSwitch = 0b10, /** - * OpCode to update the current ICU case. + * Instruction to update the current ICU case. */ IcuUpdate = 0b11, } @@ -197,6 +220,10 @@ export const enum I18nUpdateOpCode { * higher.) The OpCodes then compare its own change mask against the expression change mask to * determine if the OpCodes should execute. * + * NOTE: 32nd bit is special as it says 32nd or higher. This way if we have more than 32 bindings + * the code still works, but with lower efficiency. (it is unlikely that a translation would have + * more than 32 bindings.) + * * These OpCodes can be used by both the i18n block as well as ICU sub-block. * * ## Example @@ -220,8 +247,8 @@ export const enum I18nUpdateOpCode { * // The following OpCodes represent: `
` * // If `changeMask & 0b11` * // has changed then execute update OpCodes. - * // has NOT changed then skip `7` values and start processing next OpCodes. - * 0b11, 7, + * // has NOT changed then skip `8` values and start processing next OpCodes. + * 0b11, 8, * // Concatenate `newValue = 'pre'+lView[bindIndex-4]+'in'+lView[bindIndex-3]+'post';`. * 'pre', -4, 'in', -3, 'post', * // Update attribute: `elementAttribute(1, 'title', sanitizerFn(newValue));` @@ -240,8 +267,8 @@ export const enum I18nUpdateOpCode { * // The following OpCodes represent: `
{exp4, plural, ... }">` * // If `changeMask & 0b1000` * // has changed then execute update OpCodes. - * // has NOT changed then skip `4` values and start processing next OpCodes. - * 0b1000, 4, + * // has NOT changed then skip `2` values and start processing next OpCodes. + * 0b1000, 2, * // Concatenate `newValue = lView[bindIndex -1];`. * -1, * // Switch ICU: `icuSwitchCase(lView[1], 0, newValue);` @@ -256,7 +283,7 @@ export const enum I18nUpdateOpCode { * ``` * */ -export interface I18nUpdateOpCodes extends Array {} +export interface I18nUpdateOpCodes extends Array, I18nDebug {} /** * Store information for the i18n translation block. diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 58284f4c11..e78ca97129 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -701,7 +701,9 @@ export interface TIcuContainerNode extends TNode { /** * Indicates the current active case for an ICU expression. * It is null when there is no active case. + * */ + // FIXME(misko): This is at a wrong location as activeCase is `LView` (not `TView`) concern activeCaseIndex: number|null; } diff --git a/packages/core/src/render3/util/debug_utils.ts b/packages/core/src/render3/util/debug_utils.ts index eda7e99db0..518b0738f6 100644 --- a/packages/core/src/render3/util/debug_utils.ts +++ b/packages/core/src/render3/util/debug_utils.ts @@ -5,6 +5,37 @@ * 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 */ -export function attachDebugObject(obj: any, debug: any) { - Object.defineProperty(obj, 'debug', {value: debug, enumerable: false}); + +/** + * Patch a `debug` property on top of the existing object. + * + * NOTE: always call this method with `ngDevMode && attachDebugObject(...)` + * + * @param obj Object to patch + * @param debug Value to patch + */ +export function attachDebugObject(obj: any, debug: any): void { + if (ngDevMode) { + Object.defineProperty(obj, 'debug', {value: debug, enumerable: false}); + } else { + throw new Error( + 'This method should be guarded with `ngDevMode` so that it can be tree shaken in production!'); + } +} + +/** + * Patch a `debug` property getter on top of the existing object. + * + * NOTE: always call this method with `ngDevMode && attachDebugObject(...)` + * + * @param obj Object to patch + * @param debugGetter Getter returning a value to patch + */ +export function attachDebugGetter(obj: any, debugGetter: () => any): void { + if (ngDevMode) { + Object.defineProperty(obj, 'debug', {get: debugGetter, enumerable: false}); + } else { + throw new Error( + 'This method should be guarded with `ngDevMode` so that it can be tree shaken in production!'); + } } diff --git a/packages/core/test/render3/i18n_debug_spec.ts b/packages/core/test/render3/i18n_debug_spec.ts new file mode 100644 index 0000000000..50c1abd43d --- /dev/null +++ b/packages/core/test/render3/i18n_debug_spec.ts @@ -0,0 +1,140 @@ +/** + * @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 {i18nMutateOpCodesToString, i18nUpdateOpCodesToString} from '@angular/core/src/render3/i18n_debug'; +import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode} from '@angular/core/src/render3/interfaces/i18n'; + +describe('i18n debug', () => { + describe('i18nUpdateOpCodesToString', () => { + it('should print nothing', () => { + expect(i18nUpdateOpCodesToString([])).toEqual([]); + }); + + it('should print text opCode', () => { + expect(i18nUpdateOpCodesToString([ + 0b11, + 4, + 'pre ', + -4, + ' post', + 1 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text, + ])) + .toEqual( + ['if (mask & 0b11) { (lView[1] as Text).textContent = `pre ${lView[4]} post`; }']); + }); + + it('should print Attribute opCode', () => { + expect(i18nUpdateOpCodesToString([ + 0b01, 8, + 'pre ', -4, + ' in ', -3, + ' post', 1 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, + 'title', null, + 0b10, 8, + 'pre ', -4, + ' in ', -3, + ' post', 1 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, + 'title', (v) => v, + ])) + .toEqual([ + 'if (mask & 0b1) { (lView[1] as Element).setAttribute(\'title\', `pre ${lView[4]} in ${lView[3]} post`); }', + 'if (mask & 0b10) { (lView[1] as Element).setAttribute(\'title\', (function (v) { return v; })(`pre ${lView[4]} in ${lView[3]} post`)); }' + ]); + }); + + it('should print icuSwitch opCode', () => { + expect(i18nUpdateOpCodesToString([ + 0b100, 2, -5, 12 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch, + 2 // FIXME(misko): Should be part of IcuSwitch + ])).toEqual(['if (mask & 0b100) { icuSwitchCase(lView[12] as Comment, 2, `${lView[5]}`); }']); + }); + + it('should print icuUpdate opCode', () => { + expect(i18nUpdateOpCodesToString([ + 0b1000, 2, 13 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate, + 3 // FIXME(misko): should be part of IcuUpdate + ])).toEqual(['if (mask & 0b1000) { icuUpdateCase(lView[13] as Comment, 3); }']); + }); + }); + + describe('i18nMutateOpCodesToString', () => { + it('should print nothing', () => { + expect(i18nMutateOpCodesToString([])).toEqual([]); + }); + + it('should print Move', () => { + expect(i18nMutateOpCodesToString([ + 1 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, + 2 << I18nMutateOpCode.SHIFT_PARENT | 0 << I18nMutateOpCode.SHIFT_REF | + I18nMutateOpCode.AppendChild, + ])).toEqual(['(lView[2] as Element).appendChild(lView[1])']); + }); + + it('should print text AppendChild', () => { + expect(i18nMutateOpCodesToString([ + 'xyz', 0, + 1 << I18nMutateOpCode.SHIFT_PARENT | 0 << I18nMutateOpCode.SHIFT_REF | + I18nMutateOpCode.AppendChild + ])) + .toEqual([ + 'lView[0] = document.createTextNode("xyz")', + '(lView[1] as Element).appendChild(lView[0])' + ]); + }); + + + it('should print element AppendChild', () => { + expect(i18nMutateOpCodesToString([ + ELEMENT_MARKER, 'xyz', 0, + 1 << I18nMutateOpCode.SHIFT_PARENT | 0 << I18nMutateOpCode.SHIFT_REF | + I18nMutateOpCode.AppendChild + ])) + .toEqual([ + 'lView[0] = document.createElement("xyz")', + '(lView[1] as Element).appendChild(lView[0])' + ]); + }); + + it('should print comment AppendChild', () => { + expect(i18nMutateOpCodesToString([ + COMMENT_MARKER, 'xyz', 0, + 1 << I18nMutateOpCode.SHIFT_PARENT | 0 << I18nMutateOpCode.SHIFT_REF | + I18nMutateOpCode.AppendChild + ])) + .toEqual([ + 'lView[0] = document.createComment("xyz")', + '(lView[1] as Element).appendChild(lView[0])' + ]); + }); + + it('should print Remove', () => { + expect(i18nMutateOpCodesToString([ + 2 << I18nMutateOpCode.SHIFT_PARENT | 0 << I18nMutateOpCode.SHIFT_REF | + I18nMutateOpCode.Remove + ])).toEqual(['(lView[2] as Element).remove(lView[0])']); + }); + + it('should print Attr', () => { + expect(i18nMutateOpCodesToString([ + 1 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Attr, 'attr', 'value' + ])).toEqual(['(lView[1] as Element).setAttribute("attr", "value")']); + }); + + it('should print ElementEnd', () => { + expect(i18nMutateOpCodesToString([ + 1 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd, + ])).toEqual(['setPreviousOrParentTNode(tView.data[1] as TNode)']); + }); + + it('should print RemoveNestedIcu', () => { + expect(i18nMutateOpCodesToString([ + 1 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.RemoveNestedIcu, + ])).toEqual(['removeNestedICU(1)']); + }); + }); +}); diff --git a/packages/core/test/render3/i18n_spec.ts b/packages/core/test/render3/i18n_spec.ts index 977946f7bb..6d46484845 100644 --- a/packages/core/test/render3/i18n_spec.ts +++ b/packages/core/test/render3/i18n_spec.ts @@ -9,10 +9,11 @@ import {noop} from '../../../compiler/src/render3/view/util'; import {getTranslationForTemplate, ɵɵi18nAttributes, ɵɵi18nPostprocess, ɵɵi18nStart} from '../../src/render3/i18n'; import {setDelayProjection, ɵɵelementEnd, ɵɵelementStart} from '../../src/render3/instructions/all'; -import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, TI18n} from '../../src/render3/interfaces/i18n'; +import {I18nUpdateOpCodes, TI18n, TIcu} from '../../src/render3/interfaces/i18n'; import {HEADER_OFFSET, LView, TVIEW} from '../../src/render3/interfaces/view'; import {getNativeByIndex} from '../../src/render3/util/view_utils'; import {TemplateFixture} from './render_util'; +import {debugMatch} from './utils'; describe('Runtime i18n', () => { afterEach(() => { @@ -72,25 +73,15 @@ describe('Runtime i18n', () => { const nbConsts = 1; const index = 0; const opCodes = getOpCodes(() => { - ɵɵi18nStart(index, MSG_DIV); - }, null, nbConsts, index); - - // Check debug - const debugOps = (opCodes as any).create.debug!.operations; - expect(debugOps[0].__raw_opCode).toBe('simple text'); - expect(debugOps[0].type).toBe('Create Text Node'); - expect(debugOps[0].nodeIndex).toBe(1); - expect(debugOps[0].text).toBe('simple text'); - expect(debugOps[1].__raw_opCode).toBe(1); - expect(debugOps[1].type).toBe('AppendChild'); - expect(debugOps[1].nodeIndex).toBe(0); + ɵɵi18nStart(index, MSG_DIV); + }, null, nbConsts, index) as TI18n; expect(opCodes).toEqual({ vars: 1, - create: [ - 'simple text', nbConsts, - index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild - ], + create: debugMatch([ + 'lView[1] = document.createTextNode("simple text")', + '(lView[0] as Element).appendChild(lView[1])' + ]), update: [], icus: null }); @@ -102,37 +93,28 @@ describe('Runtime i18n', () => { // 3 consts for the 2 divs and 1 span + 1 const for `i18nStart` = 4 consts const nbConsts = 4; const index = 1; - const elementIndex = 2; - const elementIndex2 = 3; const opCodes = getOpCodes(() => { ɵɵi18nStart(index, MSG_DIV); }, null, nbConsts, index); expect(opCodes).toEqual({ vars: 5, - create: [ - 'Hello ', - nbConsts, - index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - elementIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, - index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - 'world', - nbConsts + 1, - elementIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - elementIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd, - ' and ', - nbConsts + 2, - index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - elementIndex2 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, - index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - 'universe', - nbConsts + 3, - elementIndex2 << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - elementIndex2 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd, - '!', - nbConsts + 4, - index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - ], + create: debugMatch([ + 'lView[4] = document.createTextNode("Hello ")', + '(lView[1] as Element).appendChild(lView[4])', + '(lView[1] as Element).appendChild(lView[2])', + 'lView[5] = document.createTextNode("world")', + '(lView[2] as Element).appendChild(lView[5])', + 'setPreviousOrParentTNode(tView.data[2] as TNode)', + 'lView[6] = document.createTextNode(" and ")', + '(lView[1] as Element).appendChild(lView[6])', + '(lView[1] as Element).appendChild(lView[3])', + 'lView[7] = document.createTextNode("universe")', + '(lView[3] as Element).appendChild(lView[7])', + 'setPreviousOrParentTNode(tView.data[3] as TNode)', + 'lView[8] = document.createTextNode("!")', + '(lView[1] as Element).appendChild(lView[8])', + ]), update: [], icus: null }); @@ -146,21 +128,18 @@ describe('Runtime i18n', () => { ɵɵi18nStart(index, MSG_DIV); }, null, nbConsts, index); - expect((opCodes as any).update.debug.operations).toEqual([ - {__raw_opCode: 8, checkBit: 1, type: 'Text', nodeIndex: 2, text: 'Hello �0�!'} + expect((opCodes as any).update.debug).toEqual([ + 'if (mask & 0b1) { (lView[2] as Text).textContent = `Hello ${lView[1]}!`; }' ]); expect(opCodes).toEqual({ vars: 1, - create: - ['', nbConsts, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild], - update: [ - 0b1, // bindings mask - 4, // if no update, skip 4 - 'Hello ', - -1, // binding index - '!', (index + 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text - ], + create: debugMatch([ + 'lView[2] = document.createTextNode("")', + '(lView[1] as Element).appendChild(lView[2])', + ]), + update: debugMatch( + ['if (mask & 0b1) { (lView[2] as Text).textContent = `Hello ${lView[1]}!`; }']), icus: null }); }); @@ -175,14 +154,12 @@ describe('Runtime i18n', () => { expect(opCodes).toEqual({ vars: 1, - create: - ['', nbConsts, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild], - update: [ - 0b11, // bindings mask - 8, // if no update, skip 8 - 'Hello ', -1, ' and ', -2, ', again ', -1, '!', - (index + 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text - ], + create: debugMatch([ + 'lView[2] = document.createTextNode("")', '(lView[1] as Element).appendChild(lView[2])' + ]), + update: debugMatch([ + 'if (mask & 0b11) { (lView[2] as Text).textContent = `Hello ${lView[1]} and ${lView[2]}, again ${lView[1]}!`; }' + ]), icus: null }); }); @@ -211,22 +188,14 @@ describe('Runtime i18n', () => { expect(opCodes).toEqual({ vars: 2, - create: [ - '', - nbConsts, - index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - ~rootTemplate << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, - index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - '!', - nbConsts + 1, - index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - ], - update: [ - 0b1, // bindings mask - 3, // if no update, skip 3 - -1, // binding index - ' is rendered as: ', firstTextNode << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text - ], + create: debugMatch([ + 'lView[3] = document.createTextNode("")', '(lView[1] as Element).appendChild(lView[3])', + '(lView[1] as Element).appendChild(lView[16381])', + 'lView[4] = document.createTextNode("!")', '(lView[1] as Element).appendChild(lView[4])' + ]), + update: debugMatch([ + 'if (mask & 0b1) { (lView[3] as Text).textContent = `${lView[1]} is rendered as: `; }' + ]), icus: null }); @@ -243,19 +212,15 @@ describe('Runtime i18n', () => { expect(opCodes).toEqual({ vars: 2, - create: [ - spanElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, - index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - 'before', - nbConsts, - spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - ~bElementSubTemplate << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, - spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - 'after', - nbConsts + 1, - spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - spanElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd, - ], + create: debugMatch([ + '(lView[0] as Element).appendChild(lView[1])', + 'lView[3] = document.createTextNode("before")', + '(lView[1] as Element).appendChild(lView[3])', + '(lView[1] as Element).appendChild(lView[16381])', + 'lView[4] = document.createTextNode("after")', + '(lView[1] as Element).appendChild(lView[4])', + 'setPreviousOrParentTNode(tView.data[1] as TNode)' + ]), update: [], icus: null }); @@ -272,14 +237,12 @@ describe('Runtime i18n', () => { expect(opCodes).toEqual({ vars: 1, - create: [ - bElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, - index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - 'middle', - nbConsts, - bElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - bElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd, - ], + create: debugMatch([ + '(lView[0] as Element).appendChild(lView[1])', + 'lView[2] = document.createTextNode("middle")', + '(lView[1] as Element).appendChild(lView[2])', + 'setPreviousOrParentTNode(tView.data[1] as TNode)' + ]), update: [], icus: null }); @@ -294,179 +257,76 @@ describe('Runtime i18n', () => { const nbConsts = 1; const index = 0; const opCodes = getOpCodes(() => { - ɵɵi18nStart(index, MSG_DIV); - }, null, nbConsts, index); - const tIcuIndex = 0; - const icuCommentNodeIndex = index + 1; - const firstTextNodeIndex = index + 2; - const bElementNodeIndex = index + 3; - const iElementNodeIndex = index + 3; - const spanElementNodeIndex = index + 3; - const innerTextNode = index + 4; - const lastTextNode = index + 5; - - const debugOps = (opCodes as any).update.debug.operations; - expect(debugOps[0].__raw_opCode).toBe(6); - expect(debugOps[0].checkBit).toBe(1); - expect(debugOps[0].type).toBe('IcuSwitch'); - expect(debugOps[0].nodeIndex).toBe(1); - expect(debugOps[0].tIcuIndex).toBe(0); - expect(debugOps[0].mainBinding).toBe('�0�'); - - expect(debugOps[1].__raw_opCode).toBe(7); - expect(debugOps[1].checkBit).toBe(3); - expect(debugOps[1].type).toBe('IcuUpdate'); - expect(debugOps[1].nodeIndex).toBe(1); - expect(debugOps[1].tIcuIndex).toBe(0); - - const icuDebugOps = (opCodes as any).icus[0].create[0].debug.operations; - let op: any; - let i = 0; - - op = icuDebugOps[i++]; - expect(op.__raw_opCode).toBe('no '); - expect(op.type).toBe('Create Text Node'); - expect(op.nodeIndex).toBe(2); - expect(op.text).toBe('no '); - - op = icuDebugOps[i++]; - expect(op.__raw_opCode).toBe(131073); - expect(op.type).toBe('AppendChild'); - expect(op.nodeIndex).toBe(1); - - op = icuDebugOps[i++]; - expect(op.__raw_opCode).toEqual({marker: 'element'}); - expect(op.type).toBe('ELEMENT_MARKER'); - - op = icuDebugOps[i++]; - expect(op.__raw_opCode).toBe('b'); - expect(op.type).toBe('Create Text Node'); - expect(op.nodeIndex).toBe(3); - expect(op.text).toBe('b'); - - op = icuDebugOps[i++]; - expect(op.__raw_opCode).toBe(131073); - expect(op.type).toBe('AppendChild'); - expect(op.nodeIndex).toBe(1); - - op = icuDebugOps[i++]; - expect(op.__raw_opCode).toBe(28); - expect(op.type).toBe('Attr'); - expect(op.nodeIndex).toBe(3); - expect(op.attrName).toBe('title'); - expect(op.attrValue).toBe('none'); - - op = icuDebugOps[i++]; - expect(op.__raw_opCode).toBe('emails'); - expect(op.type).toBe('Create Text Node'); - expect(op.nodeIndex).toBe(4); - expect(op.text).toBe('emails'); - - op = icuDebugOps[i++]; - expect(op.__raw_opCode).toBe(393217); - expect(op.type).toBe('AppendChild'); - expect(op.nodeIndex).toBe(3); - - op = icuDebugOps[i++]; - expect(op.__raw_opCode).toBe('!'); - expect(op.type).toBe('Create Text Node'); - expect(op.nodeIndex).toBe(5); - expect(op.text).toBe('!'); - - op = icuDebugOps[i++]; - expect(op.__raw_opCode).toBe(131073); - expect(op.type).toBe('AppendChild'); - expect(op.nodeIndex).toBe(1); + ɵɵi18nStart(index, MSG_DIV); + }, null, nbConsts, index) as TI18n; expect(opCodes).toEqual({ vars: 5, - create: [ - COMMENT_MARKER, 'ICU 1', icuCommentNodeIndex, - index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild - ], - update: [ - 0b1, // mask for ICU main binding - 3, // skip 3 if not changed - -1, // icu main binding - icuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch, tIcuIndex, - 0b11, // mask for all ICU bindings - 2, // skip 2 if not changed - icuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate, tIcuIndex - ], - icus: [{ + update: debugMatch([ + 'if (mask & 0b1) { icuSwitchCase(lView[1] as Comment, 0, `${lView[1]}`); }', + 'if (mask & 0b11) { icuUpdateCase(lView[1] as Comment, 0); }', + ]), + create: debugMatch([ + 'lView[1] = document.createComment("ICU 1")', + '(lView[0] as Element).appendChild(lView[1])', + ]), + icus: [{ type: 1, vars: [4, 3, 3], childIcus: [[], [], []], cases: ['0', '1', 'other'], create: [ - [ - 'no ', - firstTextNodeIndex, - icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - ELEMENT_MARKER, - 'b', - bElementNodeIndex, - icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - bElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Attr, - 'title', - 'none', - 'emails', - innerTextNode, - bElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - '!', - lastTextNode, - icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - ], - [ - 'one ', firstTextNodeIndex, - icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - ELEMENT_MARKER, 'i', iElementNodeIndex, - icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - 'email', innerTextNode, - iElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild - ], - [ - '', firstTextNodeIndex, - icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - ELEMENT_MARKER, 'span', spanElementNodeIndex, - icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - 'emails', innerTextNode, - spanElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild - ] + debugMatch([ + 'lView[2] = document.createTextNode("no ")', + '(lView[1] as Element).appendChild(lView[2])', + 'lView[3] = document.createElement("b")', + '(lView[1] as Element).appendChild(lView[3])', + '(lView[3] as Element).setAttribute("title", "none")', + 'lView[4] = document.createTextNode("emails")', + '(lView[3] as Element).appendChild(lView[4])', + 'lView[5] = document.createTextNode("!")', + '(lView[1] as Element).appendChild(lView[5])' + ]), + debugMatch([ + 'lView[2] = document.createTextNode("one ")', + '(lView[1] as Element).appendChild(lView[2])', + 'lView[3] = document.createElement("i")', + '(lView[1] as Element).appendChild(lView[3])', + 'lView[4] = document.createTextNode("email")', + '(lView[3] as Element).appendChild(lView[4])' + ]), + debugMatch([ + 'lView[2] = document.createTextNode("")', + '(lView[1] as Element).appendChild(lView[2])', + 'lView[3] = document.createElement("span")', + '(lView[1] as Element).appendChild(lView[3])', + 'lView[4] = document.createTextNode("emails")', + '(lView[3] as Element).appendChild(lView[4])' + ]) ], remove: [ - [ - firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, - innerTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, - bElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, - lastTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, - ], - [ - firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, - innerTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, - iElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, - ], - [ - firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, - innerTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, - spanElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, - ] + debugMatch([ + '(lView[0] as Element).remove(lView[2])', + '(lView[0] as Element).remove(lView[4])', + '(lView[0] as Element).remove(lView[3])', + '(lView[0] as Element).remove(lView[5])', + ]), + debugMatch([ + '(lView[0] as Element).remove(lView[2])', + '(lView[0] as Element).remove(lView[4])', + '(lView[0] as Element).remove(lView[3])', + ]), + debugMatch([ + '(lView[0] as Element).remove(lView[2])', + '(lView[0] as Element).remove(lView[4])', + '(lView[0] as Element).remove(lView[3])', + ]) ], update: [ - [], [], - [ - 0b1, // mask for the first binding - 3, // skip 3 if not changed - -1, // binding index - ' ', // text string to concatenate to the binding value - firstTextNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text, - 0b10, // mask for the title attribute binding - 4, // skip 4 if not changed - -2, // binding index - bElementNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, - 'title', // attribute name - null // sanitize function - ] + debugMatch([]), debugMatch([]), debugMatch([ + 'if (mask & 0b1) { (lView[2] as Text).textContent = `${lView[1]} `; }', + 'if (mask & 0b10) { (lView[3] as Element).setAttribute(\'title\', `${lView[2]}`); }' + ]) ] }] }); @@ -496,19 +356,14 @@ describe('Runtime i18n', () => { expect(opCodes).toEqual({ vars: 6, - create: [ - COMMENT_MARKER, 'ICU 1', icuCommentNodeIndex, - index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild - ], - update: [ - 0b1, // mask for ICU main binding - 3, // skip 3 if not changed - -1, // icu main binding - icuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch, tIcuIndex, - 0b11, // mask for all ICU bindings - 2, // skip 2 if not changed - icuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate, tIcuIndex - ], + create: debugMatch([ + 'lView[1] = document.createComment("ICU 1")', + '(lView[0] as Element).appendChild(lView[1])' + ]), + update: debugMatch([ + 'if (mask & 0b1) { icuSwitchCase(lView[1] as Comment, 1, `${lView[1]}`); }', + 'if (mask & 0b11) { icuUpdateCase(lView[1] as Comment, 1); }' + ]), icus: [ { type: 0, @@ -516,28 +371,29 @@ describe('Runtime i18n', () => { childIcus: [[], [], []], cases: ['cat', 'dog', 'other'], create: [ - [ - 'cats', nestedTextNodeIndex, - nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | - I18nMutateOpCode.AppendChild - ], - [ - 'dogs', nestedTextNodeIndex, - nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | - I18nMutateOpCode.AppendChild - ], - [ - 'animals', nestedTextNodeIndex, - nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | - I18nMutateOpCode.AppendChild - ] + debugMatch([ + 'lView[5] = document.createTextNode("cats")', + '(lView[3] as Element).appendChild(lView[5])' + ]), + debugMatch([ + 'lView[5] = document.createTextNode("dogs")', + '(lView[3] as Element).appendChild(lView[5])' + ]), + debugMatch([ + 'lView[5] = document.createTextNode("animals")', + '(lView[3] as Element).appendChild(lView[5])' + ]), ], remove: [ - [nestedTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove], - [nestedTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove], - [nestedTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove] + debugMatch(['(lView[0] as Element).remove(lView[5])']), + debugMatch(['(lView[0] as Element).remove(lView[5])']), + debugMatch(['(lView[0] as Element).remove(lView[5])']) ], - update: [[], [], []] + update: [ + debugMatch([]), + debugMatch([]), + debugMatch([]), + ] }, { type: 1, @@ -545,48 +401,33 @@ describe('Runtime i18n', () => { childIcus: [[], [0]], cases: ['0', 'other'], create: [ - [ - 'zero', firstTextNodeIndex, - icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild - ], - [ - '', firstTextNodeIndex, - icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - COMMENT_MARKER, 'nested ICU 0', nestedIcuCommentNodeIndex, - icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - '!', lastTextNodeIndex, - icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild - ] + debugMatch([ + 'lView[2] = document.createTextNode("zero")', + '(lView[1] as Element).appendChild(lView[2])' + ]), + debugMatch([ + 'lView[2] = document.createTextNode("")', + '(lView[1] as Element).appendChild(lView[2])', + 'lView[3] = document.createComment("nested ICU 0")', + '(lView[1] as Element).appendChild(lView[3])', + 'lView[4] = document.createTextNode("!")', + '(lView[1] as Element).appendChild(lView[4])' + ]), ], remove: [ - [firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove], - [ - firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, - lastTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, - 0 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.RemoveNestedIcu, - nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, - ] + debugMatch(['(lView[0] as Element).remove(lView[2])']), + debugMatch([ + '(lView[0] as Element).remove(lView[2])', '(lView[0] as Element).remove(lView[4])', + 'removeNestedICU(0)', '(lView[0] as Element).remove(lView[3])' + ]), ], update: [ - [], - [ - 0b1, // mask for ICU main binding - 3, // skip 3 if not changed - -1, // binding index - ' ', // text string to concatenate to the binding value - firstTextNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text, - 0b10, // mask for inner ICU main binding - 3, // skip 3 if not changed - -2, // inner ICU main binding - nestedIcuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF | - I18nUpdateOpCode.IcuSwitch, - nestedTIcuIndex, - 0b10, // mask for all inner ICU bindings - 2, // skip 2 if not changed - nestedIcuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF | - I18nUpdateOpCode.IcuUpdate, - nestedTIcuIndex - ] + debugMatch([]), + debugMatch([ + 'if (mask & 0b1) { (lView[2] as Text).textContent = `${lView[1]} `; }', + 'if (mask & 0b10) { icuSwitchCase(lView[3] as Comment, 0, `${lView[2]}`); }', + 'if (mask & 0b10) { icuUpdateCase(lView[3] as Comment, 0); }' + ]), ] } ] @@ -623,13 +464,9 @@ describe('Runtime i18n', () => { ɵɵi18nAttributes(index, MSG_div_attr); }, null, nbConsts, index); - expect(opCodes).toEqual([ - 0b1, // bindings mask - 6, // if no update, skip 4 - 'Hello ', - -1, // binding index - '!', (index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'title', null - ]); + expect(opCodes).toEqual(debugMatch([ + 'if (mask & 0b1) { (lView[0] as Element).setAttribute(\'title\', `Hello ${lView[1]}!`); }' + ])); }); it('for multiple bindings', () => { @@ -641,12 +478,9 @@ describe('Runtime i18n', () => { ɵɵi18nAttributes(index, MSG_div_attr); }, null, nbConsts, index); - expect(opCodes).toEqual([ - 0b11, // bindings mask - 10, // size - 'Hello ', -1, ' and ', -2, ', again ', -1, '!', - (index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'title', null - ]); + expect(opCodes).toEqual(debugMatch([ + 'if (mask & 0b11) { (lView[0] as Element).setAttribute(\'title\', `Hello ${lView[1]} and ${lView[2]}, again ${lView[1]}!`); }' + ])); }); it('for multiple attributes', () => { @@ -658,18 +492,10 @@ describe('Runtime i18n', () => { ɵɵi18nAttributes(index, MSG_div_attr); }, null, nbConsts, index); - expect(opCodes).toEqual([ - 0b1, // bindings mask - 6, // if no update, skip 4 - 'Hello ', - -1, // binding index - '!', (index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'title', null, - 0b1, // bindings mask - 6, // if no update, skip 4 - 'Hello ', - -1, // binding index - '!', (index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'aria-label', null - ]); + expect(opCodes).toEqual(debugMatch([ + 'if (mask & 0b1) { (lView[0] as Element).setAttribute(\'title\', `Hello ${lView[1]}!`); }', + 'if (mask & 0b1) { (lView[0] as Element).setAttribute(\'aria-label\', `Hello ${lView[1]}!`); }' + ])); }); }); diff --git a/packages/core/test/render3/utils.ts b/packages/core/test/render3/utils.ts new file mode 100644 index 0000000000..988b54b196 --- /dev/null +++ b/packages/core/test/render3/utils.ts @@ -0,0 +1,73 @@ + +/** + * @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 + */ + +/** Template string function that can be used to strip indentation from a given string literal. */ +export function dedent(strings: TemplateStringsArray, ...values: any[]) { + let joinedString = ''; + for (let i = 0; i < values.length; i++) { + joinedString += `${strings[i]}${values[i]}`; + } + joinedString += strings[strings.length - 1]; + const lines = joinedString.split('\n'); + while (isBlank(lines[0])) { + lines.shift(); + } + while (isBlank(lines[lines.length - 1])) { + lines.pop(); + } + let minWhitespacePrefix = lines.reduce( + (min, line) => Math.min(min, numOfWhiteSpaceLeadingChars(line)), Number.MAX_SAFE_INTEGER); + return lines.map((line) => line.substring(minWhitespacePrefix)).join('\n'); +} + +/** + * Tests to see if the line is blank. + * + * A blank line is such which contains only whitespace. + * @param text string to test for blank-ness. + */ +function isBlank(text: string): boolean { + return /^\s*$/.test(text); +} + +/** + * Returns number of whitespace leading characters. + * + * @param text + */ +function numOfWhiteSpaceLeadingChars(text: string): number { + return text.match(/^\s*/)![0].length; +} + + +/** + * Jasmine AsymmetricMatcher which can be used to assert `.debug` properties. + * + * ``` + * expect(obj).toEqual({ + * create: debugMatch('someValue') + * }) + * ``` + * + * In the above example it will assert that `obj.create.debug === 'someValue'`. + * + * @param expected Expected value. + */ +export function debugMatch(expected: T): any { + const matcher = function() {}; + let actual: any = null; + + matcher.asymmetricMatch = function(objectWithDebug: any) { + return jasmine.matchersUtil.equals(actual = objectWithDebug.debug, expected); + }; + matcher.jasmineToString = function() { + return `<${JSON.stringify(actual)} != ${JSON.stringify(expected)}>`; + }; + return matcher; +}