angular-docs-cn/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts
Paul Gschwendtner aca339e864 fix(ivy): unable to project into multiple slots with default selector (#30561)
With View engine it was possible to declare multiple projection
definitions and to programmatically project nodes into the slots.

e.g.

```html
<ng-content></ng-content>
<ng-content></ng-content>
```

Using `ViewContainerRef#createComponent` allowed projecting
nodes into one of the projection defs (through index)

This no longer works with Ivy as the `projectionDef` instruction only
retrieves a list of selectors instead of also retrieving entries for
reserved projection slots which appear when using the default
selector multiple times (as seen above).

In order to fix this issue, the Ivy compiler now passes all
projection slots to the `projectionDef` instruction. Meaning that
there can be multiple projection slots with the same wildcard
selector. This allows multi-slot projection as seen in the
example above, and it also allows us to match the multi-slot node
projection order from View Engine (to avoid breaking changes).

It basically ensures that Ivy fully matches the View Engine behavior
except of a very small edge case that has already been discussed
in FW-886 (with the conclusion of working as intended).

Read more here: https://hackmd.io/s/Sy2kQlgTE

PR Close #30561
2019-05-31 09:52:32 -07:00

545 lines
21 KiB
TypeScript

/**
* @license
* Copyright Google Inc. 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 {inspect} from 'util';
import {tsSourceMapBug29300Fixed} from '../../src/ngtsc/util/src/ts_source_map_bug_29300';
import {NgtscTestEnvironment} from './env';
import {SegmentMapping, getMappedSegments} from './sourcemap_utils';
describe('template source-mapping', () => {
let env !: NgtscTestEnvironment;
beforeEach(() => { env = NgtscTestEnvironment.setup(); });
describe('Inline templates', () => {
describe('(element creation)', () => {
it('should map simple element with content', () => {
const mappings = compileAndMap('<h1>Heading 1</h1>');
expect(mappings).toContain(
{source: '<h1>', generated: 'i0.ɵɵelementStart(0, "h1")', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: 'Heading 1', generated: 'i0.ɵɵtext(1, "Heading 1")', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: '</h1>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
it('should map void element', () => {
const mappings = compileAndMap('<hr>');
expect(mappings).toContain(
{source: '<hr>', generated: 'i0.ɵɵelement(0, "hr")', sourceUrl: '../test.ts'});
});
});
describe('(interpolations)', () => {
it('should map a mix of interpolated and static content', () => {
const mappings = compileAndMap('<h3>Hello {{ name }}</h3>');
expect(mappings).toContain(
{source: '<h3>', generated: 'i0.ɵɵelementStart(0, "h3")', sourceUrl: '../test.ts'});
expect(mappings).toContain({
source: 'Hello {{ name }}',
generated: 'i0.ɵɵtextInterpolate1("Hello ", ctx.name, "")',
sourceUrl: '../test.ts'
});
expect(mappings).toContain(
{source: '</h3>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
it('should map a complex interpolated expression', () => {
const mappings = compileAndMap('<h2>{{ greeting + " " + name }}</h2>');
expect(mappings).toContain(
{source: '<h2>', generated: 'i0.ɵɵelementStart(0, "h2")', sourceUrl: '../test.ts'});
expect(mappings).toContain({
source: '{{ greeting + " " + name }}',
generated: 'i0.ɵɵtextInterpolate(ctx.greeting + " " + ctx.name)',
sourceUrl: '../test.ts'
});
expect(mappings).toContain(
{source: '</h2>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
it('should map interpolated properties', () => {
const mappings = compileAndMap('<div id="{{name}}"></div>');
expect(mappings).toContain({
source: '<div id="{{name}}"></div>',
generated: 'i0.ɵɵelement(0, "div", _c0)',
sourceUrl: '../test.ts'
});
expect(mappings).toContain({
source: 'id="{{name}}"',
generated: 'i0.ɵɵpropertyInterpolate("id", ctx.name)',
sourceUrl: '../test.ts'
});
});
it('should map interpolation with pipe', () => {
const mappings = compileAndMap('<div>{{200.3 | percent : 2 }}</div>');
expect(mappings).toContain(
{source: '<div>', generated: 'i0.ɵɵelementStart(0, "div")', sourceUrl: '../test.ts'});
expect(mappings).toContain({
source: '{{200.3 | percent : 2 }}',
generated: 'i0.ɵɵtextInterpolate(i0.ɵɵpipeBind2(2, 1, 200.3, 2))',
sourceUrl: '../test.ts'
});
expect(mappings).toContain(
{source: '</div>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
});
describe('(property bindings)', () => {
it('should map a simple input binding expression', () => {
const mappings = compileAndMap('<div [attr]="name"></div>');
expect(mappings).toContain({
source: '<div [attr]="name"></div>',
generated: 'i0.ɵɵelement(0, "div", _c0)',
sourceUrl: '../test.ts'
});
expect(mappings).toContain({
source: '[attr]="name"',
generated: 'i0.ɵɵproperty("attr", ctx.name)',
sourceUrl: '../test.ts'
});
});
it('should map a complex input binding expression', () => {
const mappings = compileAndMap('<div [attr]="greeting + name"></div>');
expect(mappings).toContain({
source: '<div [attr]="greeting + name"></div>',
generated: 'i0.ɵɵelement(0, "div", _c0)',
sourceUrl: '../test.ts'
});
expect(mappings).toContain({
source: '[attr]="greeting + name"',
generated: 'i0.ɵɵproperty("attr", ctx.greeting + ctx.name)',
sourceUrl: '../test.ts'
});
});
it('should map a longhand input binding expression', () => {
const mappings = compileAndMap('<div bind-attr="name"></div>');
expect(mappings).toContain({
source: '<div bind-attr="name"></div>',
generated: 'i0.ɵɵelement(0, "div", _c0)',
sourceUrl: '../test.ts'
});
expect(mappings).toContain({
source: 'bind-attr="name"',
generated: 'i0.ɵɵproperty("attr", ctx.name)',
sourceUrl: '../test.ts'
});
});
it('should map a simple output binding expression', () => {
const mappings = compileAndMap('<button (click)="doSomething()">Do it</button>');
expect(mappings).toContain({
source: '<button (click)="doSomething()">',
generated: 'i0.ɵɵelementStart(0, "button", _c0)',
sourceUrl: '../test.ts'
});
expect(mappings).toContain(
{source: 'Do it', generated: 'i0.ɵɵtext(1, "Do it")', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: 'doSomething()', generated: 'ctx.doSomething()', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: '</button>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
it('should map a complex output binding expression', () => {
const mappings =
compileAndMap(`<button (click)="items.push('item' + items.length)">Add Item</button>`);
expect(mappings).toContain({
source: `<button (click)="items.push('item' + items.length)">`,
generated: 'i0.ɵɵelementStart(0, "button", _c0)',
sourceUrl: '../test.ts'
});
expect(mappings).toContain(
{source: 'Add Item', generated: 'i0.ɵɵtext(1, "Add Item")', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: 'items.push(', generated: 'ctx.items.push(', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: `'item' `, generated: `"item"`, sourceUrl: '../test.ts'});
expect(mappings).toContain({
source: '+ items.length)',
generated: ' + ctx.items.length)',
sourceUrl: '../test.ts'
});
expect(mappings).toContain(
{source: '</button>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
it('should map a longhand output binding expression', () => {
const mappings = compileAndMap('<button on-click="doSomething()">Do it</button>');
expect(mappings).toContain({
source: '<button on-click="doSomething()">',
generated: 'i0.ɵɵelementStart(0, "button", _c0)',
sourceUrl: '../test.ts'
});
expect(mappings).toContain(
{source: 'Do it', generated: 'i0.ɵɵtext(1, "Do it")', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: 'doSomething()', generated: 'ctx.doSomething()', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: '</button>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
it('should map a two-way binding expression', () => {
const mappings = compileAndMap('Name: <input [(ngModel)]="name">');
expect(mappings).toContain({
source: '<input [(ngModel)]="name">',
generated: 'i0.ɵɵelementStart(1, "input", _c0)',
sourceUrl: '../test.ts'
});
// TODO: improve mappings here
expect(mappings).toContain({
source: '[(ngModel)]="name"',
generated:
'i0.ɵɵlistener("ngModelChange", function TestCmp_Template_input_ngModelChange_1_listener($event) { return ctx.name = $event; })',
sourceUrl: '../test.ts'
});
expect(mappings).toContain({
source: '<input [(ngModel)]="name">',
generated: 'i0.ɵɵelementEnd()',
sourceUrl: '../test.ts'
});
});
it('should map a longhand two-way binding expression', () => {
const mappings = compileAndMap('Name: <input bindon-ngModel="name">');
expect(mappings).toContain({
source: '<input bindon-ngModel="name">',
generated: 'i0.ɵɵelementStart(1, "input", _c0)',
sourceUrl: '../test.ts'
});
// TODO: improve mappings here
expect(mappings).toContain({
source: 'bindon-ngModel="name"',
generated:
'i0.ɵɵlistener("ngModelChange", function TestCmp_Template_input_ngModelChange_1_listener($event) { return ctx.name = $event; })',
sourceUrl: '../test.ts'
});
expect(mappings).toContain({
source: '<input bindon-ngModel="name">',
generated: 'i0.ɵɵelementEnd()',
sourceUrl: '../test.ts'
});
});
it('should map a class input binding', () => {
const mappings = compileAndMap('<div [class.initial]="isInitial">Message</div>');
expect(mappings).toContain({
source: '<div [class.initial]="isInitial">',
generated: 'i0.ɵɵelementStart(0, "div")',
sourceUrl: '../test.ts'
});
// TODO: Add better mappings for binding
expect(mappings).toContain(
{source: 'Message', generated: 'i0.ɵɵtext(1, "Message")', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: '</div>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
});
describe('(structural directives)', () => {
it('should map *ngIf scenario', () => {
const mappings = compileAndMap('<div *ngIf="showMessage()">{{ name }}</div>');
expect(mappings).toContain({
source: '<div *ngIf="showMessage()">',
generated: 'i0.ɵɵelementStart(0, "div")',
sourceUrl: '../test.ts'
});
// TODO - map the bindings better
expect(mappings).toContain(
{source: '</div>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
// TODO: the `ctx_r...` appears to be dependent upon previous tests!!!
// expect(mappings).toContain({
// source: '{{ name }}',
// generated: 'i0.ɵɵtextInterpolate(ctx_r0.name)',
// sourceUrl: '../test.ts'
// });
});
it('should map ng-template [ngIf] scenario', () => {
const mappings = compileAndMap(
`<ng-template [ngIf]="showMessage()">\n` +
` <div>{{ name }}</div>\n` +
` <hr>\n` +
`</ng-template>`);
expect(mappings).toContain(
{source: '<div>', generated: 'i0.ɵɵelementStart(0, "div")', sourceUrl: '../test.ts'});
// TODO - map the bindings better
expect(mappings).toContain(
{source: '</div>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
// TODO: the `ctx_r...` appears to be dependent upon previous tests!!!
// expect(mappings).toContain({
// source: '{{ name }}',
// generated: 'i0.ɵɵtextInterpolate(ctx_r0.name)',
// sourceUrl: '../test.ts'
// });
});
it('should map *ngFor scenario', () => {
const mappings = compileAndMap(
'<div *ngFor="let item of items; index as i; trackBy: trackByFn">{{ item }}</div>');
expect(mappings).toContain({
source: '<div *ngFor="let item of items; index as i; trackBy: trackByFn">',
generated: 'i0.ɵɵelementStart(0, "div")',
sourceUrl: '../test.ts'
});
// TODO - map the bindings better
expect(mappings).toContain(
{source: '</div>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
it('should map ng-template [ngFor] scenario', () => {
const mappings =
compileAndMap(`<ng-template ngFor [ngForOf]="items" let-item>{{ item }}</ng-template>`);
// TODO - map the bindings better
});
});
describe('(content projection)', () => {
it('should map default and selected projection', () => {
const mappings = compileAndMap(
`<h3><ng-content select="title"></ng-content></h3>\n` +
`<div><ng-content></ng-content></div>`);
expect(mappings).toContain(
{source: '<h3>', generated: 'i0.ɵɵelementStart(0, "h3")', sourceUrl: '../test.ts'});
expect(mappings).toContain({
source: '<ng-content select="title">',
generated: 'i0.ɵɵprojection(1)',
sourceUrl: '../test.ts'
});
expect(mappings).toContain(
{source: '</h3>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: '<div>', generated: 'i0.ɵɵelementStart(2, "div")', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: '<ng-content>', generated: 'i0.ɵɵprojection(3, 1)', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: '</div>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
});
it('should create (simple string) inline template source-mapping', () => {
const mappings = compileAndMap('<div>this is a test</div><div>{{ 1 + 2 }}</div>');
// Creation mode
expect(mappings).toContain(
{generated: 'i0.ɵɵelementStart(0, "div")', source: '<div>', sourceUrl: '../test.ts'});
expect(mappings).toContain({
generated: 'i0.ɵɵtext(1, "this is a test")',
source: 'this is a test',
sourceUrl: '../test.ts'
});
expect(mappings).toContain(
{generated: 'i0.ɵɵelementEnd()', source: '</div>', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{generated: 'i0.ɵɵelementStart(2, "div")', source: '<div>', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{generated: 'i0.ɵɵtext(3)', source: '{{ 1 + 2 }}', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{generated: 'i0.ɵɵelementEnd()', source: '</div>', sourceUrl: '../test.ts'});
// Update mode
expect(mappings).toContain({
generated: 'i0.ɵɵtextInterpolate(1 + 2)',
source: '{{ 1 + 2 }}',
sourceUrl: '../test.ts'
});
});
it('should create (simple backtick string) inline template source-mapping', () => {
const mappings = compileAndMap('<div>this is a test</div><div>{{ 1 + 2 }}</div>');
// Creation mode
expect(mappings).toContain(
{generated: 'i0.ɵɵelementStart(0, "div")', source: '<div>', sourceUrl: '../test.ts'});
expect(mappings).toContain({
generated: 'i0.ɵɵtext(1, "this is a test")',
source: 'this is a test',
sourceUrl: '../test.ts'
});
expect(mappings).toContain(
{generated: 'i0.ɵɵelementEnd()', source: '</div>', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{generated: 'i0.ɵɵelementStart(2, "div")', source: '<div>', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{generated: 'i0.ɵɵtext(3)', source: '{{ 1 + 2 }}', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{generated: 'i0.ɵɵelementEnd()', source: '</div>', sourceUrl: '../test.ts'});
// TODO(benlesh): We need to circle back and prevent the extra parens from being generated.
// Update mode
expect(mappings).toContain({
generated: 'i0.ɵɵtextInterpolate(1 + 2)',
source: '{{ 1 + 2 }}',
sourceUrl: '../test.ts'
});
});
it('should create correct inline template source-mapping when the source contains escape sequences',
() => {
// Note that the escaped double quotes, which need un-escaping to be parsed correctly.
const mappings = compileAndMap('<div class=\\"some-class\\">this is a test</div>');
expect(mappings).toContain({
generated: 'i0.ɵɵelementStart(0, "div", _c0)',
source: '<div class=\\"some-class\\">',
sourceUrl: '../test.ts'
});
const c2Mapping =
mappings.find(mapping => /var _c0 = \[1, "some-class"\];/.test(mapping.generated));
expect(c2Mapping).toBeDefined();
});
});
if (tsSourceMapBug29300Fixed()) {
describe('External templates (where TS supports source-mapping)', () => {
it('should create external template source-mapping', () => {
const mappings =
compileAndMap('<div>this is a test</div><div>{{ 1 + 2 }}</div>', './dir/test.html');
// Creation mode
expect(mappings).toContain({
generated: 'i0.ɵɵelementStart(0, "div")',
source: '<div>',
sourceUrl: '../dir/test.html'
});
expect(mappings).toContain({
generated: 'i0.ɵɵtext(1, "this is a test")',
source: 'this is a test',
sourceUrl: '../dir/test.html'
});
expect(mappings).toContain(
{generated: 'i0.ɵɵelementEnd()', source: '</div>', sourceUrl: '../dir/test.html'});
expect(mappings).toContain({
generated: 'i0.ɵɵelementStart(2, "div")',
source: '<div>',
sourceUrl: '../dir/test.html'
});
expect(mappings).toContain(
{generated: 'i0.ɵɵtext(3)', source: '{{ 1 + 2 }}', sourceUrl: '../dir/test.html'});
expect(mappings).toContain(
{generated: 'i0.ɵɵelementEnd()', source: '</div>', sourceUrl: '../dir/test.html'});
// Update mode
expect(mappings).toContain({
generated: 'i0.ɵɵtextInterpolate(1 + 2)',
source: '{{ 1 + 2 }}',
sourceUrl: '../dir/test.html'
});
});
it('should create correct mappings when templateUrl is in a different rootDir', () => {
const mappings = compileAndMap(
'<div>this is a test</div><div>{{ 1 + 2 }}</div>', 'extraRootDir/test.html');
// Creation mode
expect(mappings).toContain({
generated: 'i0.ɵɵelementStart(0, "div")',
source: '<div>',
sourceUrl: '../extraRootDir/test.html'
});
expect(mappings).toContain({
generated: 'i0.ɵɵtext(1, "this is a test")',
source: 'this is a test',
sourceUrl: '../extraRootDir/test.html'
});
expect(mappings).toContain({
generated: 'i0.ɵɵelementEnd()',
source: '</div>',
sourceUrl: '../extraRootDir/test.html'
});
expect(mappings).toContain({
generated: 'i0.ɵɵelementStart(2, "div")',
source: '<div>',
sourceUrl: '../extraRootDir/test.html'
});
expect(mappings).toContain({
generated: 'i0.ɵɵtext(3)',
source: '{{ 1 + 2 }}',
sourceUrl: '../extraRootDir/test.html'
});
expect(mappings).toContain({
generated: 'i0.ɵɵelementEnd()',
source: '</div>',
sourceUrl: '../extraRootDir/test.html'
});
// Update mode
expect(mappings).toContain({
generated: 'i0.ɵɵtextInterpolate(1 + 2)',
source: '{{ 1 + 2 }}',
sourceUrl: '../extraRootDir/test.html'
});
});
});
}
function compileAndMap(template: string, templateUrl: string | null = null) {
const templateConfig = templateUrl ? `templateUrl: '${templateUrl}'` :
('template: `' + template.replace(/`/g, '\\`') + '`');
env.tsconfig({sourceMap: true});
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test-cmp',
${templateConfig}
})
export class TestCmp {}
`);
if (templateUrl) {
env.write(templateUrl, template);
}
env.driveMain();
return getMappedSegments(env, 'test.js');
}
/**
* Helper function for debugging failed mappings.
* This lays out the segment mappings in the console to make it easier to compare.
*/
function dumpMappings(mappings: SegmentMapping[]) {
mappings.forEach(map => {
// tslint:disable-next-line:no-console
console.log(
padValue(map.sourceUrl, 20, 0) + ' : ' + padValue(inspect(map.source), 100, 23) + ' : ' +
inspect(map.generated));
});
function padValue(value: string, max: number, start: number) {
const padding = value.length > max ? ('\n' +
' '.repeat(max + start)) :
' '.repeat(max - value.length);
return value + padding;
}
}
});