refactor(core): Change `TemplateFixture` to named parameters (#39233)

`TemplateFixture` used to have positional parameters and many tests got
hard to read as number of parameters reach 10+ with many of them `null`.
This refactoring changes `TemplateFixture` to take named parameters
which improves usability and readability in tests.

PR Close #39233
This commit is contained in:
Misko Hevery 2020-09-28 17:39:56 -07:00 committed by Alex Rickabaugh
parent eb4c05d97a
commit 61d56be83e
11 changed files with 194 additions and 109 deletions

View File

@ -5,7 +5,8 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {assertGreaterThan, assertIndexInRange} from '../../util/assert'; import {assertGreaterThan} from '../../util/assert';
import {assertIndexInDeclRange} from '../assert';
import {executeCheckHooks, executeInitAndCheckHooks} from '../hooks'; import {executeCheckHooks, executeInitAndCheckHooks} from '../hooks';
import {FLAGS, HEADER_OFFSET, InitPhaseState, LView, LViewFlags, TView} from '../interfaces/view'; import {FLAGS, HEADER_OFFSET, InitPhaseState, LView, LViewFlags, TView} from '../interfaces/view';
import {getLView, getSelectedIndex, getTView, isInCheckNoChangesMode, setSelectedIndex} from '../state'; import {getLView, getSelectedIndex, getTView, isInCheckNoChangesMode, setSelectedIndex} from '../state';
@ -41,8 +42,7 @@ export function ɵɵadvance(delta: number): void {
export function selectIndexInternal( export function selectIndexInternal(
tView: TView, lView: LView, index: number, checkNoChangesMode: boolean) { tView: TView, lView: LView, index: number, checkNoChangesMode: boolean) {
ngDevMode && assertGreaterThan(index, -1, 'Invalid index'); ngDevMode && assertIndexInDeclRange(lView, index + HEADER_OFFSET);
ngDevMode && assertIndexInRange(lView, index + HEADER_OFFSET);
// Flush the initial hooks for elements in the view that have been added up to this point. // Flush the initial hooks for elements in the view that have been added up to this point.
// PERF WARNING: do NOT extract this to a separate function without running benchmarks // PERF WARNING: do NOT extract this to a separate function without running benchmarks

View File

@ -29,7 +29,7 @@ import {NodeInjectorFactory, NodeInjectorOffset} from '../interfaces/injector';
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode} from '../interfaces/node'; import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode} from '../interfaces/node';
import {isProceduralRenderer, RComment, RElement, Renderer3, RendererFactory3, RNode, RText} from '../interfaces/renderer'; import {isProceduralRenderer, RComment, RElement, Renderer3, RendererFactory3, RNode, RText} from '../interfaces/renderer';
import {SanitizerFn} from '../interfaces/sanitization'; import {SanitizerFn} from '../interfaces/sanitization';
import {isComponentDef, isComponentHost, isContentQueryHost, isLContainer, isRootView} from '../interfaces/type_checks'; import {isComponentDef, isComponentHost, isContentQueryHost, isRootView} from '../interfaces/type_checks';
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, T_HOST, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType} from '../interfaces/view'; import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, T_HOST, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType} from '../interfaces/view';
import {assertNodeNotOfTypes, assertNodeOfPossibleTypes} from '../node_assert'; import {assertNodeNotOfTypes, assertNodeOfPossibleTypes} from '../node_assert';
import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher'; import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher';
@ -822,6 +822,7 @@ export function createTNode(
export function createTNode( export function createTNode(
tView: TView, tParent: TElementNode|TContainerNode|null, type: TNodeType, adjustedIndex: number, tView: TView, tParent: TElementNode|TContainerNode|null, type: TNodeType, adjustedIndex: number,
tagName: string|null, attrs: TAttributes|null): TNode { tagName: string|null, attrs: TAttributes|null): TNode {
ngDevMode && assertNotSame(attrs, undefined, '\'undefined\' is not valid value for \'attrs\'');
ngDevMode && ngDevMode.tNode++; ngDevMode && ngDevMode.tNode++;
let injectorIndex = tParent ? tParent.injectorIndex : -1; let injectorIndex = tParent ? tParent.injectorIndex : -1;
const tNode = ngDevMode ? const tNode = ngDevMode ?

View File

@ -176,8 +176,13 @@ export function viewAttachedToContainer(view: LView): boolean {
} }
/** Returns a constant from `TConstants` instance. */ /** Returns a constant from `TConstants` instance. */
export function getConstant<T>(consts: TConstants|null, index: null|undefined): null;
export function getConstant<T>(consts: TConstants, index: number): T|null;
export function getConstant<T>(consts: TConstants|null, index: number|null|undefined): T|null;
export function getConstant<T>(consts: TConstants|null, index: number|null|undefined): T|null { export function getConstant<T>(consts: TConstants|null, index: number|null|undefined): T|null {
return consts === null || index == null ? null : consts[index] as unknown as T; if (index === null || index === undefined) return null;
ngDevMode && assertIndexInRange(consts!, index);
return consts![index] as unknown as T;
} }
/** /**

View File

@ -111,6 +111,9 @@ export function assertDomNode(node: any): asserts node is Node {
export function assertIndexInRange(arr: any[], index: number) { export function assertIndexInRange(arr: any[], index: number) {
const maxLen = arr ? arr.length : 0; assertDefined(arr, 'Array must be defined.');
assertLessThan(index, maxLen, `Index expected to be less than ${maxLen} but got ${index}`); const maxLen = arr.length;
if (index < 0 || index > maxLen) {
throwError(`Index expected to be less than ${maxLen} but got ${index}`);
}
} }

View File

@ -57,19 +57,15 @@ describe('Runtime i18n', () => {
}); });
}); });
function prepareFixture(
createTemplate: () => void, updateTemplate: (() => void)|null, nbConsts = 0, nbVars = 0,
consts: TConstants = []): TemplateFixture {
return new TemplateFixture(
createTemplate, updateTemplate || noop, nbConsts, nbVars, null, null, null, undefined,
consts);
}
function getOpCodes( function getOpCodes(
messageOrAtrs: string|string[], createTemplate: () => void, updateTemplate: (() => void)|null, messageOrAtrs: string|string[], createTemplate: () => void, updateTemplate: (() => void)|null,
nbConsts: number, index: number): TI18n|I18nUpdateOpCodes { nbDecls: number, index: number): TI18n|I18nUpdateOpCodes {
const fixture = const fixture = new TemplateFixture({
prepareFixture(createTemplate, updateTemplate, nbConsts, undefined, [messageOrAtrs]); create: createTemplate,
update: updateTemplate || undefined,
decls: nbDecls,
consts: [messageOrAtrs]
});
const tView = fixture.hostView[TVIEW]; const tView = fixture.hostView[TVIEW];
return tView.data[index + HEADER_OFFSET] as TI18n; return tView.data[index + HEADER_OFFSET] as TI18n;
} }
@ -435,13 +431,18 @@ describe('Runtime i18n', () => {
it('for text', () => { it('for text', () => {
const message = `Hello world!`; const message = `Hello world!`;
const attrs = ['title', message]; const attrs = ['title', message];
const nbConsts = 2; const nbDecls = 2;
const index = 1; const index = 1;
const fixture = prepareFixture(() => { const fixture = new TemplateFixture({
ɵɵelementStart(0, 'div'); create: () => {
ɵɵi18nAttributes(index, 0); ɵɵelementStart(0, 'div');
ɵɵelementEnd(); ɵɵi18nAttributes(index, 0);
}, null, nbConsts, index, [attrs]); ɵɵelementEnd();
},
decls: nbDecls,
vars: index,
consts: [attrs],
});
const tView = fixture.hostView[TVIEW]; const tView = fixture.hostView[TVIEW];
const opCodes = tView.data[index + HEADER_OFFSET] as I18nUpdateOpCodes; const opCodes = tView.data[index + HEADER_OFFSET] as I18nUpdateOpCodes;

View File

@ -205,14 +205,16 @@ describe('lView_debug', () => {
}); });
} }
const fixture = new TemplateFixture( const fixture = new TemplateFixture({
() => { create: () => {
ɵɵelementStart(0, 'my-comp', 0); ɵɵelementStart(0, 'my-comp', 0);
ɵɵelement(1, 'my-child'); ɵɵelement(1, 'my-child');
ɵɵelementEnd(); ɵɵelementEnd();
}, },
() => null, 2, 0, [MyComponent, MyDirective, MyChild], null, null, undefined, decls: 2,
[['my-dir', '']]); directives: [MyComponent, MyDirective, MyChild],
consts: [['my-dir', '']]
});
const lView = fixture.hostView; const lView = fixture.hostView;
const lViewDebug = lView.debug!; const lViewDebug = lView.debug!;
const myCompNode = lViewDebug.nodes[0]; const myCompNode = lViewDebug.nodes[0];

View File

@ -26,7 +26,7 @@ describe('instructions', () => {
} }
function createDiv() { function createDiv() {
ɵɵelement(0, 'div', 0); ɵɵelement(0, 'div');
} }
function createScript() { function createScript() {
@ -36,7 +36,7 @@ describe('instructions', () => {
describe('ɵɵadvance', () => { describe('ɵɵadvance', () => {
it('should error in DevMode if index is out of range', () => { it('should error in DevMode if index is out of range', () => {
// Only one constant added, meaning only index `0` is valid. // Only one constant added, meaning only index `0` is valid.
const t = new TemplateFixture(createDiv, () => {}, 1, 0); const t = new TemplateFixture({create: createDiv, decls: 1});
expect(() => { expect(() => {
t.update(() => { t.update(() => {
ɵɵadvance(-1); ɵɵadvance(-1);
@ -52,7 +52,7 @@ describe('instructions', () => {
describe('bind', () => { describe('bind', () => {
it('should update bindings when value changes with the correct perf counters', () => { it('should update bindings when value changes with the correct perf counters', () => {
const t = new TemplateFixture(createAnchor, () => {}, 1, 1); const t = new TemplateFixture({create: createAnchor, update: () => {}, decls: 1, vars: 1});
t.update(() => { t.update(() => {
ɵɵproperty('title', 'Hello'); ɵɵproperty('title', 'Hello');
@ -77,7 +77,8 @@ describe('instructions', () => {
const idempotentUpdate = () => { const idempotentUpdate = () => {
ɵɵproperty('title', 'Hello'); ɵɵproperty('title', 'Hello');
}; };
const t = new TemplateFixture(createAnchor, idempotentUpdate, 1, 1); const t = new TemplateFixture(
{create: createAnchor, update: idempotentUpdate, decls: 1, vars: 1});
t.update(); t.update();
expect(t.html).toEqual('<a title="Hello"></a>'); expect(t.html).toEqual('<a title="Hello"></a>');
@ -96,9 +97,14 @@ describe('instructions', () => {
describe('element', () => { describe('element', () => {
it('should create an element with the correct perf counters', () => { it('should create an element with the correct perf counters', () => {
const t = new TemplateFixture(() => { const t = new TemplateFixture({
ɵɵelement(0, 'div', 0); create: () => {
}, () => {}, 1, 0, null, null, null, undefined, [['id', 'test', 'title', 'Hello']]); ɵɵelement(0, 'div', 0);
},
decls: 1,
vars: 0,
consts: [['id', 'test', 'title', 'Hello']]
});
const div = (t.hostElement as HTMLElement).querySelector('div')!; const div = (t.hostElement as HTMLElement).querySelector('div')!;
expect(div.id).toEqual('test'); expect(div.id).toEqual('test');
@ -114,7 +120,7 @@ describe('instructions', () => {
describe('attribute', () => { describe('attribute', () => {
it('should use sanitizer function', () => { it('should use sanitizer function', () => {
const t = new TemplateFixture(createDiv, () => {}, 1, 1); const t = new TemplateFixture({create: createDiv, decls: 1, vars: 1});
t.update(() => { t.update(() => {
ɵɵattribute('title', 'javascript:true', ɵɵsanitizeUrl); ɵɵattribute('title', 'javascript:true', ɵɵsanitizeUrl);
@ -143,7 +149,7 @@ describe('instructions', () => {
*/ */
it('should chain', () => { it('should chain', () => {
// <div [title]="title" [accesskey]="key"></div> // <div [title]="title" [accesskey]="key"></div>
const t = new TemplateFixture(createDiv, () => {}, 1, 2); const t = new TemplateFixture({create: createDiv, update: () => {}, decls: 1, vars: 2});
t.update(() => { t.update(() => {
ɵɵproperty('title', 'one')('accessKey', 'A'); ɵɵproperty('title', 'one')('accessKey', 'A');
}); });
@ -165,14 +171,16 @@ describe('instructions', () => {
describe('styleProp', () => { describe('styleProp', () => {
it('should allow values even if a bypass operation is applied', () => { it('should allow values even if a bypass operation is applied', () => {
let backgroundImage: string|SafeValue = 'url("http://server")'; let backgroundImage: string|SafeValue = 'url("http://server")';
const t = new TemplateFixture( const t = new TemplateFixture({
() => { create: () => {
return createDiv(); return createDiv();
}, },
() => { update: () => {
ɵɵstyleProp('background-image', backgroundImage); ɵɵstyleProp('background-image', backgroundImage);
}, },
2, 2); decls: 2,
vars: 2
});
// nothing is set because sanitizer suppresses it. // nothing is set because sanitizer suppresses it.
expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image')) expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image'))
.toEqual('url("http://server")'); .toEqual('url("http://server")');
@ -192,9 +200,15 @@ describe('instructions', () => {
} }
it('should add style', () => { it('should add style', () => {
const fixture = new TemplateFixture(createDivWithStyle, () => { const fixture = new TemplateFixture({
ɵɵstyleMap({'background-color': 'red'}); create: createDivWithStyle,
}, 1, 2, null, null, null, undefined, attrs); update: () => {
ɵɵstyleMap({'background-color': 'red'});
},
decls: 1,
vars: 2,
consts: attrs
});
fixture.update(); fixture.update();
expect(fixture.html).toEqual('<div style="background-color: red; height: 10px;"></div>'); expect(fixture.html).toEqual('<div style="background-color: red; height: 10px;"></div>');
}); });
@ -206,9 +220,14 @@ describe('instructions', () => {
} }
it('should add class', () => { it('should add class', () => {
const fixture = new TemplateFixture(createDivWithStyling, () => { const fixture = new TemplateFixture({
ɵɵclassMap('multiple classes'); create: createDivWithStyling,
}, 1, 2); update: () => {
ɵɵclassMap('multiple classes');
},
decls: 1,
vars: 2
});
const div = fixture.containerElement.querySelector('div.multiple')!; const div = fixture.containerElement.querySelector('div.multiple')!;
expect(getSortedClassName(div)).toEqual('classes multiple'); expect(getSortedClassName(div)).toEqual('classes multiple');
}); });
@ -284,7 +303,7 @@ describe('instructions', () => {
describe('sanitization injection compatibility', () => { describe('sanitization injection compatibility', () => {
it('should work for url sanitization', () => { it('should work for url sanitization', () => {
const s = new LocalMockSanitizer(value => `${value}-sanitized`); const s = new LocalMockSanitizer(value => `${value}-sanitized`);
const t = new TemplateFixture(createAnchor, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createAnchor, decls: 1, vars: 1, sanitizer: s});
const inputValue = 'http://foo'; const inputValue = 'http://foo';
const outputValue = 'http://foo-sanitized'; const outputValue = 'http://foo-sanitized';
@ -297,7 +316,7 @@ describe('instructions', () => {
it('should bypass url sanitization if marked by the service', () => { it('should bypass url sanitization if marked by the service', () => {
const s = new LocalMockSanitizer(value => ''); const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createAnchor, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createAnchor, decls: 1, vars: 1, sanitizer: s});
const inputValue = s.bypassSecurityTrustUrl('http://foo'); const inputValue = s.bypassSecurityTrustUrl('http://foo');
const outputValue = 'http://foo'; const outputValue = 'http://foo';
@ -310,7 +329,7 @@ describe('instructions', () => {
it('should bypass ivy-level url sanitization if a custom sanitizer is used', () => { it('should bypass ivy-level url sanitization if a custom sanitizer is used', () => {
const s = new LocalMockSanitizer(value => ''); const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createAnchor, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createAnchor, decls: 1, vars: 1, sanitizer: s});
const inputValue = bypassSanitizationTrustUrl('http://foo'); const inputValue = bypassSanitizationTrustUrl('http://foo');
const outputValue = 'http://foo-ivy'; const outputValue = 'http://foo-ivy';
@ -323,7 +342,7 @@ describe('instructions', () => {
it('should work for style sanitization', () => { it('should work for style sanitization', () => {
const s = new LocalMockSanitizer(value => `color:blue`); const s = new LocalMockSanitizer(value => `color:blue`);
const t = new TemplateFixture(createDiv, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createDiv, decls: 1, vars: 1, sanitizer: s});
const inputValue = 'color:red'; const inputValue = 'color:red';
const outputValue = 'color:blue'; const outputValue = 'color:blue';
@ -336,7 +355,7 @@ describe('instructions', () => {
it('should bypass style sanitization if marked by the service', () => { it('should bypass style sanitization if marked by the service', () => {
const s = new LocalMockSanitizer(value => ''); const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createDiv, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createDiv, decls: 1, vars: 1, sanitizer: s});
const inputValue = s.bypassSecurityTrustStyle('color:maroon'); const inputValue = s.bypassSecurityTrustStyle('color:maroon');
const outputValue = 'color:maroon'; const outputValue = 'color:maroon';
@ -349,7 +368,7 @@ describe('instructions', () => {
it('should bypass ivy-level style sanitization if a custom sanitizer is used', () => { it('should bypass ivy-level style sanitization if a custom sanitizer is used', () => {
const s = new LocalMockSanitizer(value => ''); const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createDiv, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createDiv, decls: 1, vars: 1, sanitizer: s});
const inputValue = bypassSanitizationTrustStyle('font-family:foo'); const inputValue = bypassSanitizationTrustStyle('font-family:foo');
const outputValue = 'font-family:foo-ivy'; const outputValue = 'font-family:foo-ivy';
@ -362,7 +381,7 @@ describe('instructions', () => {
it('should work for resourceUrl sanitization', () => { it('should work for resourceUrl sanitization', () => {
const s = new LocalMockSanitizer(value => `${value}-sanitized`); const s = new LocalMockSanitizer(value => `${value}-sanitized`);
const t = new TemplateFixture(createScript, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createScript, decls: 1, vars: 1, sanitizer: s});
const inputValue = 'http://resource'; const inputValue = 'http://resource';
const outputValue = 'http://resource-sanitized'; const outputValue = 'http://resource-sanitized';
@ -375,7 +394,7 @@ describe('instructions', () => {
it('should bypass resourceUrl sanitization if marked by the service', () => { it('should bypass resourceUrl sanitization if marked by the service', () => {
const s = new LocalMockSanitizer(value => ''); const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createScript, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createScript, decls: 1, vars: 1, sanitizer: s});
const inputValue = s.bypassSecurityTrustResourceUrl('file://all-my-secrets.pdf'); const inputValue = s.bypassSecurityTrustResourceUrl('file://all-my-secrets.pdf');
const outputValue = 'file://all-my-secrets.pdf'; const outputValue = 'file://all-my-secrets.pdf';
@ -388,7 +407,7 @@ describe('instructions', () => {
it('should bypass ivy-level resourceUrl sanitization if a custom sanitizer is used', () => { it('should bypass ivy-level resourceUrl sanitization if a custom sanitizer is used', () => {
const s = new LocalMockSanitizer(value => ''); const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createScript, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createScript, decls: 1, vars: 1, sanitizer: s});
const inputValue = bypassSanitizationTrustResourceUrl('file://all-my-secrets.pdf'); const inputValue = bypassSanitizationTrustResourceUrl('file://all-my-secrets.pdf');
const outputValue = 'file://all-my-secrets.pdf-ivy'; const outputValue = 'file://all-my-secrets.pdf-ivy';
@ -401,7 +420,7 @@ describe('instructions', () => {
it('should work for script sanitization', () => { it('should work for script sanitization', () => {
const s = new LocalMockSanitizer(value => `${value} //sanitized`); const s = new LocalMockSanitizer(value => `${value} //sanitized`);
const t = new TemplateFixture(createScript, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createScript, decls: 1, vars: 1, sanitizer: s});
const inputValue = 'fn();'; const inputValue = 'fn();';
const outputValue = 'fn(); //sanitized'; const outputValue = 'fn(); //sanitized';
@ -414,7 +433,7 @@ describe('instructions', () => {
it('should bypass script sanitization if marked by the service', () => { it('should bypass script sanitization if marked by the service', () => {
const s = new LocalMockSanitizer(value => ''); const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createScript, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createScript, decls: 1, vars: 1, sanitizer: s});
const inputValue = s.bypassSecurityTrustScript('alert("bar")'); const inputValue = s.bypassSecurityTrustScript('alert("bar")');
const outputValue = 'alert("bar")'; const outputValue = 'alert("bar")';
@ -427,7 +446,7 @@ describe('instructions', () => {
it('should bypass ivy-level script sanitization if a custom sanitizer is used', () => { it('should bypass ivy-level script sanitization if a custom sanitizer is used', () => {
const s = new LocalMockSanitizer(value => ''); const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createScript, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createScript, decls: 1, vars: 1, sanitizer: s});
const inputValue = bypassSanitizationTrustScript('alert("bar")'); const inputValue = bypassSanitizationTrustScript('alert("bar")');
const outputValue = 'alert("bar")-ivy'; const outputValue = 'alert("bar")-ivy';
@ -440,7 +459,7 @@ describe('instructions', () => {
it('should work for html sanitization', () => { it('should work for html sanitization', () => {
const s = new LocalMockSanitizer(value => `${value} <!--sanitized-->`); const s = new LocalMockSanitizer(value => `${value} <!--sanitized-->`);
const t = new TemplateFixture(createDiv, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createDiv, decls: 1, vars: 1, sanitizer: s});
const inputValue = '<header></header>'; const inputValue = '<header></header>';
const outputValue = '<header></header> <!--sanitized-->'; const outputValue = '<header></header> <!--sanitized-->';
@ -453,7 +472,7 @@ describe('instructions', () => {
it('should bypass html sanitization if marked by the service', () => { it('should bypass html sanitization if marked by the service', () => {
const s = new LocalMockSanitizer(value => ''); const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createDiv, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createDiv, decls: 1, vars: 1, sanitizer: s});
const inputValue = s.bypassSecurityTrustHtml('<div onclick="alert(123)"></div>'); const inputValue = s.bypassSecurityTrustHtml('<div onclick="alert(123)"></div>');
const outputValue = '<div onclick="alert(123)"></div>'; const outputValue = '<div onclick="alert(123)"></div>';
@ -466,7 +485,7 @@ describe('instructions', () => {
it('should bypass ivy-level script sanitization if a custom sanitizer is used', () => { it('should bypass ivy-level script sanitization if a custom sanitizer is used', () => {
const s = new LocalMockSanitizer(value => ''); const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createDiv, undefined, 1, 1, null, null, s); const t = new TemplateFixture({create: createDiv, decls: 1, vars: 1, sanitizer: s});
const inputValue = bypassSanitizationTrustHtml('<div onclick="alert(123)"></div>'); const inputValue = bypassSanitizationTrustHtml('<div onclick="alert(123)"></div>');
const outputValue = '<div onclick="alert(123)"></div>-ivy'; const outputValue = '<div onclick="alert(123)"></div>-ivy';

View File

@ -372,11 +372,16 @@ describe('event listeners', () => {
}); });
} }
const fixture = new TemplateFixture(() => { const fixture = new TemplateFixture({
ɵɵelementStart(0, 'button', 0); create: () => {
ɵɵtext(1, 'Click'); ɵɵelementStart(0, 'button', 0);
ɵɵelementEnd(); ɵɵtext(1, 'Click');
}, () => {}, 2, 0, [HostListenerDir], null, null, undefined, [['hostListenerDir', '']]); ɵɵelementEnd();
},
decls: 2,
directives: [HostListenerDir],
consts: [['hostListenerDir', '']]
});
const button = fixture.hostElement.querySelector('button')!; const button = fixture.hostElement.querySelector('button')!;
@ -388,9 +393,14 @@ describe('event listeners', () => {
}); });
it('should support global host listeners on directives', () => { it('should support global host listeners on directives', () => {
const fixture = new TemplateFixture(() => { const fixture = new TemplateFixture({
ɵɵelement(0, 'div', 0); create: () => {
}, () => {}, 1, 0, [GlobalHostListenerDir], null, null, undefined, [['hostListenerDir', '']]); ɵɵelement(0, 'div', 0);
},
decls: 1,
directives: [GlobalHostListenerDir],
consts: [['hostListenerDir', '']]
});
const doc = fixture.hostElement.ownerDocument!; const doc = fixture.hostElement.ownerDocument!;

View File

@ -52,14 +52,24 @@ describe('pipe', () => {
} }
it('should unwrap', () => { it('should unwrap', () => {
const fixture = const fixture = new TemplateFixture({
new TemplateFixture(createTemplate, updateTemplate, 2, 3, undefined, [WrappingPipe]); create: createTemplate,
update: updateTemplate,
decls: 2,
vars: 3,
pipes: [WrappingPipe]
});
expect(fixture.html).toEqual('Bar'); expect(fixture.html).toEqual('Bar');
}); });
it('should force change detection', () => { it('should force change detection', () => {
const fixture = const fixture = new TemplateFixture({
new TemplateFixture(createTemplate, updateTemplate, 2, 3, undefined, [WrappingPipe]); create: createTemplate,
update: updateTemplate,
decls: 2,
vars: 3,
pipes: [WrappingPipe]
});
expect(fixture.html).toEqual('Bar'); expect(fixture.html).toEqual('Bar');
fixture.hostElement.childNodes[0]!.textContent = 'Foo'; fixture.hostElement.childNodes[0]!.textContent = 'Foo';
@ -101,15 +111,18 @@ describe('pipe', () => {
static ɵpipe = ɵɵdefinePipe({name: 'sayHello', type: SayHelloPipe, pure: true}); static ɵpipe = ɵɵdefinePipe({name: 'sayHello', type: SayHelloPipe, pure: true});
} }
const fixture = new TemplateFixture( const fixture = new TemplateFixture({
() => { create: () => {
ɵɵtext(0); ɵɵtext(0);
ɵɵpipe(1, 'sayHello'); ɵɵpipe(1, 'sayHello');
}, },
() => { update: () => {
ɵɵtextInterpolate1('', ɵɵpipeBind1(1, 1, null), ''); ɵɵtextInterpolate1('', ɵɵpipeBind1(1, 1, null), '');
}, },
2, 3, undefined, [SayHelloPipe]); decls: 2,
vars: 3,
pipes: [SayHelloPipe]
});
expect(fixture.html).toBe('Hello there'); expect(fixture.html).toBe('Hello there');
}); });

View File

@ -15,6 +15,7 @@ import {Renderer2} from '@angular/core/src/render/api';
import {createLView, createTView, getOrCreateTComponentView, getOrCreateTNode, renderComponentOrTemplate} from '@angular/core/src/render3/instructions/shared'; import {createLView, createTView, getOrCreateTComponentView, getOrCreateTNode, renderComponentOrTemplate} from '@angular/core/src/render3/instructions/shared';
import {TConstants, TNodeType} from '@angular/core/src/render3/interfaces/node'; import {TConstants, TNodeType} from '@angular/core/src/render3/interfaces/node';
import {enterView, getLView} from '@angular/core/src/render3/state'; import {enterView, getLView} from '@angular/core/src/render3/state';
import {EMPTY_ARRAY} from '@angular/core/src/util/empty';
import {stringifyElement} from '@angular/platform-browser/testing/src/browser_util'; import {stringifyElement} from '@angular/platform-browser/testing/src/browser_util';
import {SWITCH_CHANGE_DETECTOR_REF_FACTORY__POST_R3__ as R3_CHANGE_DETECTOR_REF_FACTORY} from '../../src/change_detection/change_detector_ref'; import {SWITCH_CHANGE_DETECTOR_REF_FACTORY__POST_R3__ as R3_CHANGE_DETECTOR_REF_FACTORY} from '../../src/change_detection/change_detector_ref';
@ -97,6 +98,10 @@ export class TemplateFixture extends BaseFixture {
private _pipeDefs: PipeDefList|null; private _pipeDefs: PipeDefList|null;
private _sanitizer: Sanitizer|null; private _sanitizer: Sanitizer|null;
private _rendererFactory: RendererFactory3; private _rendererFactory: RendererFactory3;
private _consts: TConstants;
private _vars: number;
private createBlock: () => void;
private updateBlock: () => void;
/** /**
* *
@ -105,16 +110,36 @@ export class TemplateFixture extends BaseFixture {
* @param updateBlock Optional instructions which go into the update block: * @param updateBlock Optional instructions which go into the update block:
* `if (rf & RenderFlags.Update) { __here__ }`. * `if (rf & RenderFlags.Update) { __here__ }`.
*/ */
constructor( constructor({
private createBlock: () => void, private updateBlock: () => void = noop, decls: number = 0, create = noop,
private vars: number = 0, directives?: DirectiveTypesOrFactory|null, update = noop,
pipes?: PipeTypesOrFactory|null, sanitizer?: Sanitizer|null, decls = 0,
rendererFactory?: RendererFactory3, private _consts?: TConstants) { vars = 0,
directives,
pipes,
sanitizer = null,
rendererFactory = domRendererFactory3,
consts = EMPTY_ARRAY
}: {
create?: (() => void),
update?: (() => void),
decls?: number,
vars?: number,
directives?: DirectiveTypesOrFactory,
pipes?: PipeTypesOrFactory,
sanitizer?: Sanitizer|null,
rendererFactory?: RendererFactory3,
consts?: TConstants
}) {
super(); super();
this._consts = consts;
this._vars = vars;
this.createBlock = create;
this.updateBlock = update;
this._directiveDefs = toDefs(directives, extractDirectiveDef); this._directiveDefs = toDefs(directives, extractDirectiveDef);
this._pipeDefs = toDefs(pipes, extractPipeDef); this._pipeDefs = toDefs(pipes, extractPipeDef);
this._sanitizer = sanitizer || null; this._sanitizer = sanitizer;
this._rendererFactory = rendererFactory || domRendererFactory3; this._rendererFactory = rendererFactory;
this.hostView = renderTemplate( this.hostView = renderTemplate(
this.hostElement, this.hostElement,
(rf: RenderFlags, ctx: any) => { (rf: RenderFlags, ctx: any) => {
@ -136,7 +161,7 @@ export class TemplateFixture extends BaseFixture {
*/ */
update(updateBlock?: () => void): void { update(updateBlock?: () => void): void {
renderTemplate( renderTemplate(
this.hostElement, updateBlock || this.updateBlock, 0, this.vars, null!, this.hostElement, updateBlock || this.updateBlock, 0, this._vars, null!,
this._rendererFactory, this.hostView, this._directiveDefs, this._pipeDefs, this._sanitizer, this._rendererFactory, this.hostView, this._directiveDefs, this._pipeDefs, this._sanitizer,
this._consts); this._consts);
} }

View File

@ -232,9 +232,12 @@ describe('ViewContainerRef', () => {
ɵɵelement(1, 'footer'); ɵɵelement(1, 'footer');
} }
new TemplateFixture( new TemplateFixture({
createTemplate, undefined, 2, 0, [DirectiveWithVCRef], null, null, undefined, create: createTemplate,
[['vcref', '']]); decls: 2,
directives: [DirectiveWithVCRef],
consts: [['vcref', '']]
});
expect(directiveInstance!.vcref.element.nativeElement.tagName.toLowerCase()) expect(directiveInstance!.vcref.element.nativeElement.tagName.toLowerCase())
.toEqual('header'); .toEqual('header');
@ -253,9 +256,12 @@ describe('ViewContainerRef', () => {
ɵɵelement(1, 'footer'); ɵɵelement(1, 'footer');
} }
new TemplateFixture( new TemplateFixture({
createTemplate, undefined, 2, 0, [HeaderComponent, DirectiveWithVCRef], null, null, create: createTemplate,
undefined, [['vcref', '']]); decls: 2,
directives: [HeaderComponent, DirectiveWithVCRef],
consts: [['vcref', '']]
});
expect(directiveInstance!.vcref.element.nativeElement.tagName.toLowerCase()) expect(directiveInstance!.vcref.element.nativeElement.tagName.toLowerCase())
.toEqual('header-cmp'); .toEqual('header-cmp');
@ -430,10 +436,10 @@ describe('ViewContainerRef', () => {
fixture.update(); fixture.update();
// Destroying the parent view will also destroy all of its children views and call their // Destroying the parent view will also destroy all of its children views and call their
// onDestroy hooks. Here, our child view attempts to destroy itself *again* in its onDestroy. // onDestroy hooks. Here, our child view attempts to destroy itself *again* in its
// This test exists to verify that no errors are thrown when doing this. We want the test // onDestroy. This test exists to verify that no errors are thrown when doing this. We want
// component to destroy its own view in onDestroy because the destroy hooks happen as a // the test component to destroy its own view in onDestroy because the destroy hooks happen
// *part of* view destruction. We also ensure that the test component has at least one // as a *part of* view destruction. We also ensure that the test component has at least one
// listener so that it runs the event listener cleanup code path. // listener so that it runs the event listener cleanup code path.
fixture.destroy(); fixture.destroy();
}); });