Skip to content

Commit

Permalink
test(compiler): add expression absolute span tests for TemplateAsts (
Browse files Browse the repository at this point in the history
…#33253)

Previously, we had tested that expressions parsed in a Render3 AST
had correctly-defined absolute spans (spans relative to the entire
template, not the local expression). Sometimes we use Template ASTs
rather than Render3 ASTs, and it's desirable to test for correct
expression spans in the template parser as well.

Adding these tests resolved one bug, similar to the one fixed in
fd4fed1, where expressions in the value
of a template attribute were not given an absolute span corresponding to
the start of the attribute name rather than the start of the attribute
value.

The diff on this commit is large, partially because it involves some
structural changes of the template parser testing layout. In particular,
the following is done:

1. Move `createMeta*`-like functions from `template_parser_spec.ts` to
   be exported from a new test utility file.
2. Create an `ExpressionSourceHumanizer`, similar to the one created in
   b04488d, to allow convenient testing
   of expressions' locations.
3. Create `template_parser_absolute_span_spec.ts`, testing the spans of
   expressions parsed by the template parser. This is very similar to
   the `r3_ast_absolute_span_spec`.

PR Close #33253
  • Loading branch information
ayazhafiz authored and AndrewKushnir committed Oct 24, 2019
1 parent 2805af9 commit 3d11355
Show file tree
Hide file tree
Showing 5 changed files with 589 additions and 92 deletions.
5 changes: 3 additions & 2 deletions packages/compiler/src/template_parser/template_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,10 @@ class TemplateParseVisitor implements html.Visitor {
}
hasInlineTemplates = true;
const parsedVariables: ParsedVariable[] = [];
const absoluteOffset = (attr.valueSpan || attr.sourceSpan).start.offset;
this._bindingParser.parseInlineTemplateBinding(
templateKey !, templateValue !, attr.sourceSpan, attr.sourceSpan.start.offset,
templateMatchableAttrs, templateElementOrDirectiveProps, parsedVariables);
templateKey !, templateValue !, attr.sourceSpan, absoluteOffset, templateMatchableAttrs,
templateElementOrDirectiveProps, parsedVariables);
templateElementVars.push(...parsedVariables.map(v => t.VariableAst.fromParsedVariable(v)));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
/**
* @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 {AbsoluteSourceSpan, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, SchemaMetadata} from '@angular/compiler';
import {TemplateAst} from '@angular/compiler/src/template_parser/template_ast';
import {TemplateParser} from '@angular/compiler/src/template_parser/template_parser';
import {inject} from '@angular/core/testing';

import {humanizeExpressionSource} from './util/expression';
import {compileDirectiveMetadataCreate, compileTemplateMetadata, createTypeMeta} from './util/metadata';

describe('expression AST absolute source spans', () => {
const fakeTemplate = compileTemplateMetadata({animations: []});
const fakeComponent = compileDirectiveMetadataCreate({
isHost: false,
selector: 'app-fake',
template: fakeTemplate,
type: createTypeMeta({reference: {filePath: 'fake-path', name: 'FakeComponent'}}),
isComponent: true
});
const ngIf = compileDirectiveMetadataCreate({
selector: '[ngIf]',
template: fakeTemplate,
type: createTypeMeta({reference: {filePath: 'fake-path', name: 'NgIf'}}),
inputs: ['ngIf']
}).toSummary();
let parse: (
template: string, directives?: CompileDirectiveSummary[], pipes?: CompilePipeSummary[],
schemas?: SchemaMetadata[], preserveWhitespaces?: boolean) => TemplateAst[];

beforeEach(inject([TemplateParser], (parser: TemplateParser) => {
parse =
(template: string, directives: CompileDirectiveSummary[] = [],
pipes: CompilePipeSummary[] | null = null, schemas: SchemaMetadata[] = [],
preserveWhitespaces = true): TemplateAst[] => {
if (pipes === null) {
pipes = [];
}
return parser
.parse(
fakeComponent, template, directives, pipes, schemas, 'TestComponent',
preserveWhitespaces)
.template;
};
}));

it('should provide absolute offsets of an expression in a bound text', () => {
expect(humanizeExpressionSource(parse('<div>{{foo}}</div>'))).toContain([
'{{ foo }}', new AbsoluteSourceSpan(5, 12)
]);
});

it('should provide absolute offsets of an expression in a bound event', () => {
expect(humanizeExpressionSource(parse('<div (click)="foo();bar();"></div>'))).toContain([
'foo(); bar();', new AbsoluteSourceSpan(14, 26)
]);

expect(humanizeExpressionSource(parse('<div on-click="foo();bar();"></div>'))).toContain([
'foo(); bar();', new AbsoluteSourceSpan(15, 27)
]);
});

it('should provide absolute offsets of an expression in a bound attribute', () => {
expect(humanizeExpressionSource(parse('<input [disabled]="condition ? true : false" />')))
.toContain(['condition ? true : false', new AbsoluteSourceSpan(19, 43)]);

expect(humanizeExpressionSource(parse('<input bind-disabled="condition ? true : false" />')))
.toContain(['condition ? true : false', new AbsoluteSourceSpan(22, 46)]);
});

it('should provide absolute offsets of an expression in a template attribute', () => {
const ngTemplate =
compileDirectiveMetadataCreate({
selector: 'ng-template',
type: createTypeMeta({reference: {filePath: 'fake-path', name: 'OnTemplate'}})
}).toSummary();

expect(humanizeExpressionSource(parse('<div *ngIf="value"></div>', [ngIf, ngTemplate])))
.toContain(['value', new AbsoluteSourceSpan(12, 17)]);
});

describe('binary expression', () => {
it('should provide absolute offsets of a binary expression', () => {
expect(humanizeExpressionSource(parse('<div>{{1 + 2}}<div>'))).toContain([
'1 + 2', new AbsoluteSourceSpan(7, 12)
]);
});

it('should provide absolute offsets of expressions in a binary expression', () => {
expect(humanizeExpressionSource(parse('<div>{{1 + 2}}<div>')))
.toEqual(jasmine.arrayContaining([
// TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions
// with trailing whitespace in a binary expression. Look into fixing this.
['1', new AbsoluteSourceSpan(7, 9)],
['2', new AbsoluteSourceSpan(11, 12)],
]));
});
});

describe('conditional', () => {
it('should provide absolute offsets of a conditional', () => {
expect(humanizeExpressionSource(parse('<div>{{bool ? 1 : 0}}<div>'))).toContain([
'bool ? 1 : 0', new AbsoluteSourceSpan(7, 19)
]);
});

it('should provide absolute offsets of expressions in a conditional', () => {
expect(humanizeExpressionSource(parse('<div>{{bool ? 1 : 0}}<div>')))
.toEqual(jasmine.arrayContaining([
// TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions
// with trailing whitespace in a conditional expression. Look into fixing this.
['bool', new AbsoluteSourceSpan(7, 12)],
['1', new AbsoluteSourceSpan(14, 16)],
['0', new AbsoluteSourceSpan(18, 19)],
]));
});
});

describe('chain', () => {
it('should provide absolute offsets of a chain', () => {
expect(humanizeExpressionSource(parse('<div (click)="a(); b();"><div>'))).toContain([
'a(); b();', new AbsoluteSourceSpan(14, 23)
]);
});

it('should provide absolute offsets of expressions in a chain', () => {
expect(humanizeExpressionSource(parse('<div (click)="a(); b();"><div>')))
.toEqual(jasmine.arrayContaining([
['a()', new AbsoluteSourceSpan(14, 17)],
['b()', new AbsoluteSourceSpan(19, 22)],
]));
});
});

describe('function call', () => {
it('should provide absolute offsets of a function call', () => {
expect(humanizeExpressionSource(parse('<div>{{fn()()}}<div>'))).toContain([
'fn()()', new AbsoluteSourceSpan(7, 13)
]);
});

it('should provide absolute offsets of expressions in a function call', () => {
expect(humanizeExpressionSource(parse('<div>{{fn()(param)}}<div>'))).toContain([
'param', new AbsoluteSourceSpan(12, 17)
]);
});
});

it('should provide absolute offsets of an implicit receiver', () => {
expect(humanizeExpressionSource(parse('<div>{{a.b}}<div>'))).toContain([
'', new AbsoluteSourceSpan(7, 7)
]);
});

describe('interpolation', () => {
it('should provide absolute offsets of an interpolation', () => {
expect(humanizeExpressionSource(parse('<div>{{1 + foo.length}}<div>'))).toContain([
'{{ 1 + foo.length }}', new AbsoluteSourceSpan(5, 23)
]);
});

it('should provide absolute offsets of expressions in an interpolation', () => {
expect(humanizeExpressionSource(parse('<div>{{1 + 2}}<div>')))
.toEqual(jasmine.arrayContaining([
// TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions
// with trailing whitespace in a conditional expression. Look into fixing this.
['1', new AbsoluteSourceSpan(7, 9)],
['2', new AbsoluteSourceSpan(11, 12)],
]));
});
});

describe('keyed read', () => {
it('should provide absolute offsets of a keyed read', () => {
expect(humanizeExpressionSource(parse('<div>{{obj[key]}}<div>'))).toContain([
'obj[key]', new AbsoluteSourceSpan(7, 15)
]);
});

it('should provide absolute offsets of expressions in a keyed read', () => {
expect(humanizeExpressionSource(parse('<div>{{obj[key]}}<div>'))).toContain([
'key', new AbsoluteSourceSpan(11, 14)
]);
});
});

describe('keyed write', () => {
it('should provide absolute offsets of a keyed write', () => {
expect(humanizeExpressionSource(parse('<div>{{obj[key] = 0}}<div>'))).toContain([
'obj[key] = 0', new AbsoluteSourceSpan(7, 19)
]);
});

it('should provide absolute offsets of expressions in a keyed write', () => {
expect(humanizeExpressionSource(parse('<div>{{obj[key] = 0}}<div>')))
.toEqual(jasmine.arrayContaining([
['key', new AbsoluteSourceSpan(11, 14)],
['0', new AbsoluteSourceSpan(18, 19)],
]));
});
});

it('should provide absolute offsets of a literal primitive', () => {
expect(humanizeExpressionSource(parse('<div>{{100}}<div>'))).toContain([
'100', new AbsoluteSourceSpan(7, 10)
]);
});

describe('literal array', () => {
it('should provide absolute offsets of a literal array', () => {
expect(humanizeExpressionSource(parse('<div>{{[0, 1, 2]}}<div>'))).toContain([
'[0, 1, 2]', new AbsoluteSourceSpan(7, 16)
]);
});

it('should provide absolute offsets of expressions in a literal array', () => {
expect(humanizeExpressionSource(parse('<div>{{[0, 1, 2]}}<div>')))
.toEqual(jasmine.arrayContaining([
['0', new AbsoluteSourceSpan(8, 9)],
['1', new AbsoluteSourceSpan(11, 12)],
['2', new AbsoluteSourceSpan(14, 15)],
]));
});
});

describe('literal map', () => {
it('should provide absolute offsets of a literal map', () => {
expect(humanizeExpressionSource(parse('<div>{{ {a: 0} }}<div>'))).toContain([
// TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions
// with trailing whitespace in a literal map. Look into fixing this.
'{a: 0}', new AbsoluteSourceSpan(8, 15)
]);
});

it('should provide absolute offsets of expressions in a literal map', () => {
expect(humanizeExpressionSource(parse('<div>{{ {a: 0} }}<div>')))
.toEqual(jasmine.arrayContaining([
['0', new AbsoluteSourceSpan(12, 13)],
]));
});
});

describe('method call', () => {
it('should provide absolute offsets of a method call', () => {
expect(humanizeExpressionSource(parse('<div>{{method()}}</div>'))).toContain([
'method()', new AbsoluteSourceSpan(7, 15)
]);
});

it('should provide absolute offsets of expressions in a method call', () => {
expect(humanizeExpressionSource(parse('<div>{{method(param)}}<div>'))).toContain([
'param', new AbsoluteSourceSpan(14, 19)
]);
});
});

describe('non-null assert', () => {
it('should provide absolute offsets of a non-null assert', () => {
expect(humanizeExpressionSource(parse('<div>{{prop!}}</div>'))).toContain([
'prop!', new AbsoluteSourceSpan(7, 12)
]);
});

it('should provide absolute offsets of expressions in a non-null assert', () => {
expect(humanizeExpressionSource(parse('<div>{{prop!}}<div>'))).toContain([
'prop', new AbsoluteSourceSpan(7, 11)
]);
});
});

describe('pipe', () => {
const testPipe = new CompilePipeMetadata({
name: 'test',
type: createTypeMeta({reference: {filePath: 'fake-path', name: 'TestPipe'}}),
pure: false
}).toSummary();

it('should provide absolute offsets of a pipe', () => {
expect(humanizeExpressionSource(parse('<div>{{prop | test}}<div>', [], [testPipe])))
.toContain(['(prop | test)', new AbsoluteSourceSpan(7, 18)]);
});

it('should provide absolute offsets expressions in a pipe', () => {
expect(humanizeExpressionSource(parse('<div>{{prop | test}}<div>', [], [testPipe])))
.toContain([
// TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions
// with trailing whitespace in a pipe. Look into fixing this.
'prop', new AbsoluteSourceSpan(7, 12)
]);
});
});

it('should provide absolute offsets of a property read', () => {
expect(humanizeExpressionSource(parse('<div>{{prop}}</div>'))).toContain([
'prop', new AbsoluteSourceSpan(7, 11)
]);
});

describe('property write', () => {
it('should provide absolute offsets of a property write', () => {
expect(humanizeExpressionSource(parse('<div (click)="prop = 0"></div>'))).toContain([
'prop = 0', new AbsoluteSourceSpan(14, 22)
]);
});

it('should provide absolute offsets of expressions in a property write', () => {
expect(humanizeExpressionSource(parse('<div (click)="prop = 0"></div>'))).toContain([
'0', new AbsoluteSourceSpan(21, 22)
]);
});
});

describe('"not" prefix', () => {
it('should provide absolute offsets of a "not" prefix', () => {
expect(humanizeExpressionSource(parse('<div>{{!prop}}</div>'))).toContain([
'!prop', new AbsoluteSourceSpan(7, 12)
]);
});

it('should provide absolute offsets of expressions in a "not" prefix', () => {
expect(humanizeExpressionSource(parse('<div>{{!prop}}<div>'))).toContain([
'prop', new AbsoluteSourceSpan(8, 12)
]);
});
});

describe('safe method call', () => {
it('should provide absolute offsets of a safe method call', () => {
expect(humanizeExpressionSource(parse('<div>{{prop?.safe()}}<div>'))).toContain([
'prop?.safe()', new AbsoluteSourceSpan(7, 19)
]);
});

it('should provide absolute offsets of expressions in safe method call', () => {
expect(humanizeExpressionSource(parse('<div>{{prop?.safe()}}<div>'))).toContain([
'prop', new AbsoluteSourceSpan(7, 11)
]);
});
});

describe('safe property read', () => {
it('should provide absolute offsets of a safe property read', () => {
expect(humanizeExpressionSource(parse('<div>{{prop?.safe}}<div>'))).toContain([
'prop?.safe', new AbsoluteSourceSpan(7, 17)
]);
});

it('should provide absolute offsets of expressions in safe property read', () => {
expect(humanizeExpressionSource(parse('<div>{{prop?.safe}}<div>'))).toContain([
'prop', new AbsoluteSourceSpan(7, 11)
]);
});
});

it('should provide absolute offsets of a quote', () => {
expect(humanizeExpressionSource(parse('<div [class.some-class]="a:b"></div>'))).toContain([
'a:b', new AbsoluteSourceSpan(25, 28)
]);
});
});

0 comments on commit 3d11355

Please sign in to comment.