refactor(ivy): run the compiler compliance tests against ngtsc (#24862)

This commit moves the compiler compliance tests into compiler-cli,
and uses ngtsc to run them instead of the custom compilation
pipeline used before. Testing against ngtsc allows for validation
of the real compiler output.

This commit also fixes a few small issues that prevented the tests
from passing.

PR Close #24862
This commit is contained in:
Alex Rickabaugh
2018-07-12 15:10:55 -07:00
committed by Victor Berchet
parent b7bbc82e3e
commit 9fd70c9715
17 changed files with 529 additions and 652 deletions

View File

@ -0,0 +1,35 @@
load("//tools:defaults.bzl", "ts_library")
load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")
ts_library(
name = "test_lib",
testonly = 1,
srcs = glob(
["**/*.ts"],
),
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli",
"//packages/compiler/test:test_utils",
"//packages/compiler/testing",
"//packages/core",
],
)
jasmine_node_test(
name = "compliance",
bootstrap = ["angular/tools/testing/init_node_spec.js"],
data = [
"//packages/common:npm_package",
"//packages/core:npm_package",
],
tags = [
"ivy-local",
"ivy-only",
],
deps = [
":test_lib",
"//tools/testing:node",
],
)

View File

@ -0,0 +1 @@
Tests in this directory are excluded from running in the browser and only run in node.

View File

@ -0,0 +1,178 @@
/**
* @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 {AotCompilerOptions} from '@angular/compiler';
import {escapeRegExp} from '@angular/compiler/src/util';
import {MockCompilerHost, MockData, MockDirectory, arrayToMockDir, settings, toMockFileArray} from '@angular/compiler/test/aot/test_util';
import * as ts from 'typescript';
import {NgtscProgram} from '../../src/ngtsc/program';
const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/;
const OPERATOR =
/!|%|\*|\/|\^|&&?|\|\|?|\(|\)|\{|\}|\[|\]|:|;|<=?|>=?|={1,3}|!==?|=>|\+\+?|--?|@|,|\.|\.\.\./;
const STRING = /'[^']*'|"[^"]*"|`[\s\S]*?`/;
const NUMBER = /\d+/;
const ELLIPSIS = '…';
const TOKEN = new RegExp(
`\\s*((${IDENTIFIER.source})|(${OPERATOR.source})|(${STRING.source})|${NUMBER.source}|${ELLIPSIS})`,
'y');
type Piece = string | RegExp;
const SKIP = /(?:.|\n|\r)*/;
const ERROR_CONTEXT_WIDTH = 30;
// Transform the expected output to set of tokens
function tokenize(text: string): Piece[] {
TOKEN.lastIndex = 0;
let match: RegExpMatchArray|null;
const pieces: Piece[] = [];
while ((match = TOKEN.exec(text)) !== null) {
const token = match[1];
if (token === 'IDENT') {
pieces.push(IDENTIFIER);
} else if (token === ELLIPSIS) {
pieces.push(SKIP);
} else {
pieces.push(token);
}
}
if (pieces.length === 0 || TOKEN.lastIndex !== 0) {
const from = TOKEN.lastIndex;
const to = from + ERROR_CONTEXT_WIDTH;
throw Error(`Invalid test, no token found for '${text.substr(from, to)}...'`);
}
return pieces;
}
export function expectEmit(
source: string, expected: string, description: string,
assertIdentifiers?: {[name: string]: RegExp}) {
// turns `// ...` into `…`
// remove `// TODO` comment lines
expected = expected.replace(/\/\/\s*\.\.\./g, ELLIPSIS).replace(/\/\/\s*TODO.*?\n/g, '');
const pieces = tokenize(expected);
const {regexp, groups} = buildMatcher(pieces);
const matches = source.match(regexp);
if (matches === null) {
let last: number = 0;
for (let i = 1; i < pieces.length; i++) {
const {regexp} = buildMatcher(pieces.slice(0, i));
const m = source.match(regexp);
const expectedPiece = pieces[i - 1] == IDENTIFIER ? '<IDENT>' : pieces[i - 1];
if (!m) {
fail(
`${description}: Expected to find ${expectedPiece} '${source.substr(0,last)}[<---HERE expected "${expectedPiece}"]${source.substr(last)}'`);
return;
} else {
last = (m.index || 0) + m[0].length;
}
}
fail(
`Test helper failure: Expected expression failed but the reporting logic could not find where it failed in: ${source}`);
} else {
if (assertIdentifiers) {
// It might be possible to add the constraints in the original regexp (see `buildMatcher`)
// by transforming the assertion regexps when using anchoring, grouping, back references,
// flags, ...
//
// Checking identifiers after they have matched allows for a simple and flexible
// implementation.
// The overall performance are not impacted when `assertIdentifiers` is empty.
const ids = Object.keys(assertIdentifiers);
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
if (groups.has(id)) {
const name = matches[groups.get(id) as number];
const regexp = assertIdentifiers[id];
if (!regexp.test(name)) {
throw Error(
`${description}: The matching identifier "${id}" is "${name}" which doesn't match ${regexp}`);
}
}
}
}
}
}
const IDENT_LIKE = /^[a-z][A-Z]/;
const MATCHING_IDENT = /^\$.*\$$/;
/*
* Builds a regexp that matches the given `pieces`
*
* It returns:
* - the `regexp` to be used to match the generated code,
* - the `groups` which maps `$...$` identifier to their position in the regexp matches.
*/
function buildMatcher(pieces: (string | RegExp)[]): {regexp: RegExp, groups: Map<string, number>} {
const results: string[] = [];
let first = true;
let group = 0;
const groups = new Map<string, number>();
for (const piece of pieces) {
if (!first)
results.push(`\\s${typeof piece === 'string' && IDENT_LIKE.test(piece) ? '+' : '*'}`);
first = false;
if (typeof piece === 'string') {
if (MATCHING_IDENT.test(piece)) {
const matchGroup = groups.get(piece);
if (!matchGroup) {
results.push('(' + IDENTIFIER.source + ')');
const newGroup = ++group;
groups.set(piece, newGroup);
} else {
results.push(`\\${matchGroup}`);
}
} else {
results.push(escapeRegExp(piece));
}
} else {
results.push('(?:' + piece.source + ')');
}
}
return {
regexp: new RegExp(results.join('')),
groups,
};
}
export function compile(
data: MockDirectory, angularFiles: MockData, options: AotCompilerOptions = {},
errorCollector: (error: any, fileName?: string) => void = error => { throw error;}): {
source: string,
} {
const testFiles = toMockFileArray(data);
const scripts = testFiles.map(entry => entry.fileName);
const angularFilesArray = toMockFileArray(angularFiles);
const files = arrayToMockDir([...testFiles, ...angularFilesArray]);
const mockCompilerHost = new MockCompilerHost(scripts, files);
const program = new NgtscProgram(
scripts, {
target: ts.ScriptTarget.ES2015,
module: ts.ModuleKind.ES2015,
moduleResolution: ts.ModuleResolutionKind.NodeJs, ...options,
},
mockCompilerHost);
program.emit();
const source =
scripts.map(script => mockCompilerHost.readFile(script.replace(/\.ts$/, '.js'))).join('\n');
return {source};
}

View File

@ -0,0 +1,217 @@
/**
* @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 {MockDirectory, setup} from '@angular/compiler/test/aot/test_util';
import {compile, expectEmit} from './mock_compile';
describe('mock_compiler', () => {
// This produces a MockDirectory of the file needed to compile an Angular application.
// This setup is performed in a beforeAll which populates the map returned.
const angularFiles = setup({
compileAngular: true,
compileAnimations: false,
compileCommon: true,
});
describe('compiling', () => {
// To use compile you need to supply the files in a MockDirectory that can be merged
// with a set of "environment" files such as the angular files.
it('should be able to compile a simple application', () => {
const files = {
app: {
'hello.component.ts': `
import {Component, Input} from '@angular/core';
@Component({template: 'Hello {{name}}!'})
export class HelloComponent {
@Input() name: string = 'world';
}
`,
'hello.module.ts': `
import {NgModule} from '@angular/core';
import {HelloComponent} from './hello.component';
@NgModule({declarations: [HelloComponent]})
export class HelloModule {}
`
}
};
const result = compile(files, angularFiles);
// result.source contains just the emitted factory declarations regardless of the original
// module.
expect(result.source).toContain('Hello');
});
});
describe('expecting emitted output', () => {
it('should be able to find a simple expression in the output', () => {
const files = {
app: {
'hello.component.ts': `
import {Component, Input} from '@angular/core';
@Component({template: 'Hello {{name}}! Your name as {{name.length}} characters'})
export class HelloComponent {
@Input() name: string = 'world';
}
`,
'hello.module.ts': `
import {NgModule} from '@angular/core';
import {HelloComponent} from './hello.component';
@NgModule({declarations: [HelloComponent]})
export class HelloModule {}
`
}
};
const result = compile(files, angularFiles);
// The expression can expected directly.
expectEmit(result.source, 'name.length', 'name length expression not found');
// Whitespace is not significant
expectEmit(
result.source, 'name \n\n . \n length',
'name length expression not found (whitespace)');
});
});
it('should be able to skip untested regions (… and // ...)', () => {
const files = {
app: {
'hello.component.ts': `
import {Component, Input} from '@angular/core';
@Component({template: 'Hello {{name}}! Your name as {{name.length}} characters'})
export class HelloComponent {
@Input() name: string = 'world';
}
`,
'hello.module.ts': `
import {NgModule} from '@angular/core';
import {HelloComponent} from './hello.component';
@NgModule({declarations: [HelloComponent]})
export class HelloModule {}
`
}
};
const result = compile(files, angularFiles);
// The special character … means anything can be generated between the two sections allowing
// skipping sections of the output that are not under test. The ellipsis unicode char (…) is
// used instead of '...' because '...' is legal JavaScript (the spread operator) and might
// need to be tested. `// ...` could also be used in place of `…`.
expectEmit(result.source, 'ctx.name … ctx.name.length', 'could not find correct length access');
expectEmit(
result.source, 'ctx.name // ... ctx.name.length', 'could not find correct length access');
});
it('should be able to skip TODO comments (// TODO)', () => {
const files = {
app: {
'hello.component.ts': `
import {Component, Input} from '@angular/core';
@Component({template: 'Hello!'})
export class HelloComponent { }
`,
'hello.module.ts': `
import {NgModule} from '@angular/core';
import {HelloComponent} from './hello.component';
@NgModule({declarations: [HelloComponent]})
export class HelloModule {}
`
}
};
const result = compile(files, angularFiles);
expectEmit(
result.source, `
// TODO: this comment should not be taken into account
$r3$.ɵT(0, "Hello!");
// TODO: this comment should not be taken into account
`,
'todo comments should be ignored');
});
it('should be able to enforce consistent identifiers', () => {
const files = {
app: {
'hello.component.ts': `
import {Component, Input} from '@angular/core';
@Component({template: 'Hello {{name}}! Your name as {{name.length}} characters'})
export class HelloComponent {
@Input() name: string = 'world';
}
`,
'hello.module.ts': `
import {NgModule} from '@angular/core';
import {HelloComponent} from './hello.component';
@NgModule({declarations: [HelloComponent]})
export class HelloModule {}
`
}
};
const result = compile(files, angularFiles);
// IDENT can be used a wild card for any identifier
expectEmit(result.source, 'IDENT.name', 'could not find context access');
// $<ident>$ can be used as a wild-card but all the content matched by the identifiers must
// match each other.
// This is useful if the code generator is free to invent a name but should use the name
// consistently.
expectEmit(
result.source, '$ctx$.$name$ … $ctx$.$name$.length',
'could not find correct length access');
});
it('should be able to enforce that identifiers match a regexp', () => {
const files = {
app: {
'hello.component.ts': `
import {Component, Input} from '@angular/core';
@Component({template: 'Hello {{name}}! Your name as {{name.length}} characters'})
export class HelloComponent {
@Input() name: string = 'world';
}
`,
'hello.module.ts': `
import {NgModule} from '@angular/core';
import {HelloComponent} from './hello.component';
@NgModule({declarations: [HelloComponent]})
export class HelloModule {}
`
}
};
const result = compile(files, angularFiles);
// Pass: `$n$` ends with `ME` in the generated code
expectEmit(result.source, '$ctx$.$n$ … $ctx$.$n$.length', 'Match names', {'$n$': /ME$/i});
// Fail: `$n$` does not match `/(not)_(\1)/` in the generated code
expect(() => {
expectEmit(
result.source, '$ctx$.$n$ … $ctx$.$n$.length', 'Match names', {'$n$': /(not)_(\1)/});
}).toThrowError(/"\$n\$" is "name" which doesn't match \/\(not\)_\(\\1\)\//);
});
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,121 @@
/**
* @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 {MockDirectory, setup} from '@angular/compiler/test/aot/test_util';
import {compile, expectEmit} from './mock_compile';
describe('compiler compliance: bindings', () => {
const angularFiles = setup({
compileAngular: true,
compileAnimations: false,
compileCommon: false,
});
describe('text bindings', () => {
it('should generate interpolation instruction', () => {
const files: MockDirectory = {
app: {
'example.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`
<div>Hello {{ name }}</div>\`
})
export class MyComponent {
name = 'World';
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
template:function MyComponent_Template(rf, $ctx$){
if (rf & 1) {
$i0$.ɵE(0, "div");
$i0$.ɵT(1);
$i0$.ɵe();
}
if (rf & 2) {
$i0$.ɵt(1, $i0$.ɵi1("Hello ", $ctx$.name, ""));
}
}`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect interpolated text binding');
});
});
describe('property bindings', () => {
it('should generate bind instruction', () => {
const files: MockDirectory = {
app: {
'example.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-app',
template: '<a [title]="title"></a>'
})
export class MyComponent {
title = 'Hello World';
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}`
}
};
const template = `
template:function MyComponent_Template(rf, $ctx$){
if (rf & 1) {
$i0$.ɵEe(0, "a");
}
if (rf & 2) {
$i0$.ɵp(0, "title", $i0$.ɵb($ctx$.title));
}
}`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect property binding');
});
it('should generate interpolation instruction for {{...}} bindings', () => {
const files: MockDirectory = {
app: {
'example.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`
<a title="Hello {{name}}"></a>\`
})
export class MyComponent {
name = 'World';
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
template:function MyComponent_Template(rf, $ctx$){
if (rf & 1) {
$i0$.ɵEe(0, "a");
}
if (rf & 2) {
$i0$.ɵp(0, "title", $i0$.ɵi1("Hello ", $ctx$.name, ""));
}
}`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect interpolated property binding');
});
});
});

View File

@ -0,0 +1,70 @@
/**
* @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 {MockDirectory, setup} from '@angular/compiler/test/aot/test_util';
import {compile, expectEmit} from './mock_compile';
describe('compiler compliance: dependency injection', () => {
const angularFiles = setup({
compileAngular: true,
compileAnimations: false,
compileCommon: true,
});
it('should create factory methods', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule, Injectable, Attribute, Host, SkipSelf, Self, Optional} from '@angular/core';
import {CommonModule} from '@angular/common';
@Injectable()
export class MyService {}
@Component({
selector: 'my-component',
template: \`\`
})
export class MyComponent {
constructor(
@Attribute('name') name:string,
s1: MyService,
@Host() s2: MyService,
@Self() s4: MyService,
@SkipSelf() s3: MyService,
@Optional() s5: MyService,
@Self() @Optional() s6: MyService,
) {}
}
@NgModule({declarations: [MyComponent], imports: [CommonModule], providers: [MyService]})
export class MyModule {}
`
}
};
const factory = `
factory: function MyComponent_Factory() {
return new MyComponent(
$r3$.ɵinjectAttribute('name'),
$r3$.ɵdirectiveInject(MyService),
$r3$.ɵdirectiveInject(MyService, 1),
$r3$.ɵdirectiveInject(MyService, 2),
$r3$.ɵdirectiveInject(MyService, 4),
$r3$.ɵdirectiveInject(MyService, 8),
$r3$.ɵdirectiveInject(MyService, 10)
);
}`;
const result = compile(files, angularFiles);
expectEmit(result.source, factory, 'Incorrect factory');
});
});

View File

@ -0,0 +1,225 @@
/**
* @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 {MockDirectory, setup} from '@angular/compiler/test/aot/test_util';
import {compile, expectEmit} from './mock_compile';
const TRANSLATION_NAME_REGEXP = /^MSG_[A-Z0-9]+/;
describe('i18n support in the view compiler', () => {
const angularFiles = setup({
compileAngular: true,
compileAnimations: false,
compileCommon: true,
});
describe('single text nodes', () => {
it('should translate single text nodes with the i18n attribute', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`
<div i18n>Hello world</div>
<div>&</div>
<div i18n>farewell</div>
<div i18n>farewell</div>
\`
})
export class MyComponent {}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
const $msg_1$ = goog.getMsg("Hello world");
const $msg_2$ = goog.getMsg("farewell");
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵT(1, $msg_1$);
$r3$.ɵT(3,"&");
$r3$.ɵT(5, $msg_2$);
$r3$.ɵT(7, $msg_2$);
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template', {
'$msg_1$': TRANSLATION_NAME_REGEXP,
'$msg_2$': TRANSLATION_NAME_REGEXP,
});
});
it('should add the meaning and description as JsDoc comments', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`
<div i18n="meaning|desc@@id" i18n-title="desc" title="introduction">Hello world</div>
\`
})
export class MyComponent {}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
/**
* @desc desc
*/
const $msg_1$ = goog.getMsg("introduction");
const $c1$ = ["title", $msg_1$];
/**
* @desc desc
* @meaning meaning
*/
const $msg_2$ = goog.getMsg("Hello world");
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵE(0, "div", $c1$);
$r3$.ɵT(1, $msg_2$);
$r3$.ɵe();
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template', {
'$msg_1$': TRANSLATION_NAME_REGEXP,
});
});
});
describe('static attributes', () => {
it('should translate static attributes', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`
<div i18n id="static" i18n-title="m|d" title="introduction"></div>
\`
})
export class MyComponent {}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
/**
* @desc d
* @meaning m
*/
const $msg_1$ = goog.getMsg("introduction");
const $c1$ = ["id", "static", "title", $msg_1$];
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵEe(0, "div", $c1$);
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template', {
'$msg_1$': TRANSLATION_NAME_REGEXP,
});
});
});
// TODO(vicb): this feature is not supported yet
xdescribe('nested nodes', () => {
it('should generate the placeholders maps', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`
<div i18n>Hello <b>{{name}}<i>!</i><i>!</i></b></div>
<div>Other</div>
<div i18n>2nd</div>
<div i18n><i>3rd</i></div>
\`
})
export class MyComponent {}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
const $r1$ = {"b":[2], "i":[4, 6]};
const $r2$ = {"i":[13]};
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
});
describe('errors', () => {
it('should throw on nested i18n sections', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`
<div i18n><div i18n></div></div>
\`
})
export class MyComponent {}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
expect(() => compile(files, angularFiles))
.toThrowError(
'Could not mark an element as translatable inside of a translatable section');
});
});
});

View File

@ -0,0 +1,89 @@
/**
* @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 {MockDirectory, setup} from '@angular/compiler/test/aot/test_util';
import {compile, expectEmit} from './mock_compile';
describe('compiler compliance: listen()', () => {
const angularFiles = setup({
compileAngular: true,
compileAnimations: false,
compileCommon: true,
});
it('should create declare inputs/outputs', () => {
const files = {
app: {
'spec.ts': `
import {Component, Directive, NgModule, Input, Output} from '@angular/core';
@Component({
selector: 'my-component',
template: \`\`
})
export class MyComponent {
@Input() componentInput;
@Input('renamedComponentInput') originalComponentInput;
@Output() componentOutput;
@Output('renamedComponentOutput') originalComponentOutput;
}
@Directive({
selector: '[my-directive]',
})
export class MyDirective {
@Input() directiveInput;
@Input('renamedDirectiveInput') originalDirectiveInput;
@Output() directiveOutput;
@Output('renamedDirectiveOutput') originalDirectiveOutput;
}
@NgModule({declarations: [MyComponent, MyDirective]})
export class MyModule {}
`
}
};
const componentDef = `
MyComponent.ngComponentDef = IDENT.ɵdefineComponent({
inputs:{
componentInput: "componentInput",
originalComponentInput: "renamedComponentInput"
},
outputs: {
componentOutput: "componentOutput",
originalComponentOutput: "renamedComponentOutput"
}
});`;
const directiveDef = `
MyDirective.ngDirectiveDef = IDENT.ɵdefineDirective({
inputs:{
directiveInput: "directiveInput",
originalDirectiveInput: "renamedDirectiveInput"
},
outputs: {
directiveOutput: "directiveOutput",
originalDirectiveOutput: "renamedDirectiveOutput"
}
});`;
const result = compile(files, angularFiles);
expectEmit(result.source, componentDef, 'Incorrect component definition');
expectEmit(result.source, directiveDef, 'Incorrect directive definition');
});
});

View File

@ -0,0 +1,61 @@
/**
* @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 {MockDirectory, setup} from '@angular/compiler/test/aot/test_util';
import {compile, expectEmit} from './mock_compile';
/* These tests are codified version of the tests in compiler_canonical_spec.ts. Every
* test in compiler_canonical_spec.ts should have a corresponding test here.
*/
describe('compiler compliance: listen()', () => {
const angularFiles = setup({
compileAngular: true,
compileAnimations: false,
compileCommon: true,
});
it('should create listener instruction on element', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div (click)="onClick($event); 1 == 2"></div>\`
})
export class MyComponent {
onClick(event: any) {}
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
// The template should look like this (where IDENT is a wild card for an identifier):
const template = `
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵE(0, "div");
$r3$.ɵL("click", function MyComponent_Template_div_click_listener($event) {
ctx.onClick($event);
return (1 == 2);
});
$r3$.ɵe();
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
});

View File

@ -0,0 +1,124 @@
/**
* @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 {MockDirectory, setup} from '@angular/compiler/test/aot/test_util';
import {compile, expectEmit} from './mock_compile';
describe('r3_view_compiler', () => {
const angularFiles = setup({
compileAngular: true,
compileAnimations: false,
compileCommon: true,
});
describe('hello world', () => {
it('should be able to generate the hello world component', () => {
const files: MockDirectory = {
app: {
'hello.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'hello-world',
template: 'Hello, world!'
})
export class HelloWorldComponent {
}
@NgModule({
declarations: [HelloWorldComponent]
})
export class HelloWorldModule {}
`
}
};
compile(files, angularFiles);
});
});
it('should be able to generate the example', () => {
const files: MockDirectory = {
app: {
'example.ts': `
import {Component, OnInit, OnDestroy, ElementRef, Input, NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
@Component({
selector: 'my-app',
template: '<todo [data]="list"></todo>'
})
export class MyApp implements OnInit {
list: any[] = [];
constructor(public elementRef: ElementRef) {}
ngOnInit(): void {
}
}
@Component({
selector: 'todo',
template: '<ul class="list" [title]="myTitle"><li *ngFor="let item of data">{{data}}</li></ul>'
})
export class TodoComponent implements OnInit, OnDestroy {
@Input()
data: any[] = [];
myTitle: string;
constructor(public elementRef: ElementRef) {}
ngOnInit(): void {}
ngOnDestroy(): void {}
}
@NgModule({
declarations: [TodoComponent, MyApp],
imports: [CommonModule]
})
export class TodoModule{}
`
}
};
const result = compile(files, angularFiles);
expect(result.source).toContain('@angular/core');
});
describe('interpolations', () => {
// Regression #21927
it('should generate a correct call to bV with more than 8 interpolations', () => {
const files: MockDirectory = {
app: {
'example.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-app',
template: ' {{list[0]}} {{list[1]}} {{list[2]}} {{list[3]}} {{list[4]}} {{list[5]}} {{list[6]}} {{list[7]}} {{list[8]}} '
})
export class MyApp {
list: any[] = [];
}
@NgModule({declarations: [MyApp]})
export class MyModule {}`
}
};
const bV_call = `$r3$.ɵiV([" ",ctx.list[0]," ",ctx.list[1]," ",ctx.list[2]," ",ctx.list[3],
" ",ctx.list[4]," ",ctx.list[5]," ",ctx.list[6]," ",ctx.list[7]," ",ctx.list[8],
" "])`;
const result = compile(files, angularFiles);
expectEmit(result.source, bV_call, 'Incorrect bV call');
});
});
});

View File

@ -0,0 +1,264 @@
/**
* @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 {InitialStylingFlags} from '@angular/compiler/src/core';
import {MockDirectory, setup} from '@angular/compiler/test/aot/test_util';
import {compile, expectEmit} from './mock_compile';
describe('compiler compliance: styling', () => {
const angularFiles = setup({
compileAngular: true,
compileAnimations: false,
compileCommon: true,
});
describe('[style] and [style.prop]', () => {
it('should create style instructions on the element', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div [style]="myStyleExp"></div>\`
})
export class MyComponent {
myStyleExp = [{color:'red'}, {color:'blue', duration:1000}]
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵE(0, "div");
$r3$.ɵs();
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵsm(0, $ctx$.myStyleExp);
$r3$.ɵsa(0);
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should place initial, multi, singular and application followed by attribute style instructions in the template code in that order',
() => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div style="opacity:1"
[attr.style]="'border-width: 10px'"
[style.width]="myWidth"
[style]="myStyleExp"
[style.height]="myHeight"></div>\`
})
export class MyComponent {
myStyleExp = [{color:'red'}, {color:'blue', duration:1000}]
myWidth = '100px';
myHeight = '100px';
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
const _c0 = ["opacity","width","height",${InitialStylingFlags.VALUES_MODE},"opacity","1"];
MyComponent.ngComponentDef = i0.ɵdefineComponent({
type: MyComponent,
selectors:[["my-component"]],
factory:function MyComponent_Factory(){
return new MyComponent();
},
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵE(0, "div");
$r3$.ɵs(_c0);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵsm(0, $ctx$.myStyleExp);
$r3$.ɵsp(0, 1, $ctx$.myWidth);
$r3$.ɵsp(0, 2, $ctx$.myHeight);
$r3$.ɵsa(0);
$r3$.ɵa(0, "style", $r3$.ɵb("border-width: 10px"));
}
}
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
});
describe('[class]', () => {
it('should create class styling instructions on the element', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div [class]="myClassExp"></div>\`
})
export class MyComponent {
myClassExp = {'foo':true}
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵE(0, "div");
$r3$.ɵs();
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵsm(0,null,$ctx$.myClassExp);
$r3$.ɵsa(0);
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should place initial, multi, singular and application followed by attribute class instructions in the template code in that order',
() => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div class="grape"
[attr.class]="'banana'"
[class.apple]="yesToApple"
[class]="myClassExp"
[class.orange]="yesToOrange"></div>\`
})
export class MyComponent {
myClassExp = {a:true, b:true};
yesToApple = true;
yesToOrange = true;
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
const _c0 = ["grape","apple","orange",${InitialStylingFlags.VALUES_MODE},"grape",true];
MyComponent.ngComponentDef = i0.ɵdefineComponent({
type: MyComponent,
selectors:[["my-component"]],
factory:function MyComponent_Factory(){
return new MyComponent();
},
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵE(0, "div");
$r3$.ɵs(null, _c0);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵsm(0, null, $ctx$.myClassExp);
$r3$.ɵcp(0, 1, $ctx$.yesToApple);
$r3$.ɵcp(0, 2, $ctx$.yesToOrange);
$r3$.ɵsa(0);
$r3$.ɵa(0, "class", $r3$.ɵb("banana"));
}
}
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should not generate the styling apply instruction if there are only static style/class attributes',
() => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div class="foo"
style="width:100px"
[attr.class]="'round'"
[attr.style]="'height:100px'"></div>\`
})
export class MyComponent {}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
const _c0 = ["width",${InitialStylingFlags.VALUES_MODE},"width","100px"];
const _c1 = ["foo",${InitialStylingFlags.VALUES_MODE},"foo",true];
MyComponent.ngComponentDef = i0.ɵdefineComponent({
type: MyComponent,
selectors:[["my-component"]],
factory:function MyComponent_Factory(){
return new MyComponent();
},
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵE(0, "div");
$r3$.ɵs(_c0, _c1);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵa(0, "class", $r3$.ɵb("round"));
$r3$.ɵa(0, "style", $r3$.ɵb("height:100px"));
}
}
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
});
});

View File

@ -0,0 +1,112 @@
/**
* @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 {MockDirectory, setup} from '@angular/compiler/test/aot/test_util';
import {compile, expectEmit} from './mock_compile';
describe('compiler compliance: template', () => {
const angularFiles = setup({
compileAngular: true,
compileAnimations: false,
compileCommon: true,
});
it('should correctly bind to context in nested template', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
@Component({
selector: 'my-component',
template: \`
<ul *ngFor="let outer of items">
<li *ngFor="let middle of outer.items">
<div *ngFor="let inner of items"
(click)="onClick(outer, middle, inner)"
[title]="format(outer, middle, inner, component)"
>
{{format(outer, middle, inner, component)}}
</div>
</li>
</ul>\`
})
export class MyComponent {
component = this;
format(outer: any, middle: any, inner: any) { }
onClick(outer: any, middle: any, inner: any) { }
}
@NgModule({declarations: [MyComponent], imports: [CommonModule]})
export class MyModule {}
`
}
};
// The template should look like this (where IDENT is a wild card for an identifier):
const template = `
const $c0$ = ["ngFor","","ngForOf",""];
// ...
template:function MyComponent_Template(rf, $ctx$){
if (rf & 1) {
$i0$.ɵC(0, MyComponent_ul_Template_0, null, _c0);
}
if (rf & 2) {
$i0$.ɵp(0, "ngForOf", $i0$.ɵb($ctx$.items));
}
function MyComponent_ul_Template_0(rf, $ctx0$) {
if (rf & 1) {
$i0$.ɵE(0, "ul");
$i0$.ɵC(1, MyComponent_ul_li_Template_1, null, _c0);
$i0$.ɵe();
}
if (rf & 2) {
const $outer$ = $ctx0$.$implicit;
$i0$.ɵp(1, "ngForOf", $i0$.ɵb($outer$.items));
}
function MyComponent_ul_li_Template_1(rf, $ctx1$) {
if (rf & 1) {
$i0$.ɵE(0, "li");
$i0$.ɵC(1, MyComponent_ul_li_div_Template_1, null, _c0);
$i0$.ɵe();
}
if (rf & 2) {
$i0$.ɵp(1, "ngForOf", $i0$.ɵb($ctx$.items));
}
function MyComponent_ul_li_div_Template_1(rf, $ctx2$) {
if (rf & 1) {
$i0$.ɵE(0, "div");
$i0$.ɵL("click", function MyComponent_ul_li_div_Template_1_div_click_listener($event){
const $outer$ = $ctx0$.$implicit;
const $middle$ = $ctx1$.$implicit;
const $inner$ = $ctx2$.$implicit;
return ctx.onClick($outer$, $middle$, $inner$);
});
$i0$.ɵT(1);
$i0$.ɵe();
}
if (rf & 2) {
const $outer$ = $ctx0$.$implicit;
const $middle$ = $ctx1$.$implicit;
const $inner$ = $ctx2$.$implicit;
$i0$.ɵp(0, "title", $i0$.ɵb(ctx.format($outer$, $middle$, $inner$, $ctx$.component)));
$i0$.ɵt(1, $i0$.ɵi1(" ", ctx.format($outer$, $middle$, $inner$, $ctx$.component), " "));
}
}
}
}
}`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
});