refactor: move angular source to /packages rather than modules/@angular

This commit is contained in:
Jason Aden
2017-03-02 10:48:42 -08:00
parent 5ad5301a3e
commit 3e51a19983
1051 changed files with 18 additions and 18 deletions

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,248 @@
/**
* @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 {AotCompiler, AotCompilerHost, AotCompilerOptions, createAotCompiler} from '@angular/compiler';
import {RenderComponentType, ɵReflectionCapabilities as ReflectionCapabilities, ɵreflector as reflector} from '@angular/core';
import {async} from '@angular/core/testing';
import {MetadataBundler, MetadataCollector, ModuleMetadata, privateEntriesToIndex} from '@angular/tsc-wrapped';
import * as path from 'path';
import * as ts from 'typescript';
import {EmittingCompilerHost, MockAotCompilerHost, MockCompilerHost, MockData, MockMetadataBundlerHost, settings} from './test_util';
const DTS = /\.d\.ts$/;
const minCoreIndex = `
export * from './src/application_module';
export * from './src/change_detection';
export * from './src/metadata';
export * from './src/di/metadata';
export * from './src/di/injector';
export * from './src/di/injection_token';
export * from './src/linker';
export * from './src/render';
export * from './src/codegen_private_exports';
`;
describe('compiler (unbundled Angular)', () => {
let angularFiles: Map<string, string>;
beforeAll(() => {
const emittingHost = new EmittingCompilerHost([], {emitMetadata: true});
emittingHost.addScript('@angular/core/index.ts', minCoreIndex);
const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
emittingProgram.emit();
angularFiles = emittingHost.written;
});
describe('Quickstart', () => {
let host: MockCompilerHost;
let aotHost: MockAotCompilerHost;
beforeEach(() => {
host = new MockCompilerHost(QUICKSTART, FILES, angularFiles);
aotHost = new MockAotCompilerHost(host);
});
// Restore reflector since AoT compiler will update it with a new static reflector
afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); });
it('should compile',
async(() => compile(host, aotHost, expectNoDiagnostics).then(generatedFiles => {
expect(generatedFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
.toBeDefined();
expect(generatedFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl)))
.toBeDefined();
})));
it('should compile using summaries',
async(() => summaryCompile(host, aotHost).then(generatedFiles => {
expect(generatedFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
.toBeDefined();
expect(generatedFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl)))
.toBeDefined();
})));
});
});
describe('compiler (bundled Angular)', () => {
let angularFiles: Map<string, string>;
beforeAll(() => {
const emittingHost = new EmittingCompilerHost(['@angular/core/index'], {emitMetadata: false});
// Create the metadata bundled
const indexModule = emittingHost.effectiveName('@angular/core/index');
const bundler = new MetadataBundler(
indexModule, '@angular/core', new MockMetadataBundlerHost(emittingHost));
const bundle = bundler.getMetadataBundle();
const metadata = JSON.stringify(bundle.metadata, null, ' ');
const bundleIndexSource = privateEntriesToIndex('./index', bundle.privates);
emittingHost.override('@angular/core/bundle_index.ts', bundleIndexSource);
emittingHost.addWrittenFile(
'@angular/core/package.json', JSON.stringify({typings: 'bundle_index.d.ts'}));
emittingHost.addWrittenFile('@angular/core/bundle_index.metadata.json', metadata);
// Emit the sources
const bundleIndexName = emittingHost.effectiveName('@angular/core/bundle_index.ts');
const emittingProgram = ts.createProgram([bundleIndexName], settings, emittingHost);
emittingProgram.emit();
angularFiles = emittingHost.written;
});
describe('Quickstart', () => {
let host: MockCompilerHost;
let aotHost: MockAotCompilerHost;
beforeEach(() => {
host = new MockCompilerHost(QUICKSTART, FILES, angularFiles);
aotHost = new MockAotCompilerHost(host);
});
// Restore reflector since AoT compiler will update it with a new static reflector
afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); });
it('should compile',
async(() => compile(host, aotHost, expectNoDiagnostics).then(generatedFiles => {
expect(generatedFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
.toBeDefined();
expect(generatedFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl)))
.toBeDefined();
})));
});
});
function expectNoDiagnostics(program: ts.Program) {
function fileInfo(diagnostic: ts.Diagnostic): string {
if (diagnostic.file) {
return `${diagnostic.file.fileName}(${diagnostic.start}): `;
}
return '';
}
function chars(len: number, ch: string): string { return new Array(len).fill(ch).join(''); }
function lineNoOf(offset: number, text: string): number {
let result = 1;
for (let i = 0; i < offset; i++) {
if (text[i] == '\n') result++;
}
return result;
}
function lineInfo(diagnostic: ts.Diagnostic): string {
if (diagnostic.file) {
const start = diagnostic.start;
let end = diagnostic.start + diagnostic.length;
const source = diagnostic.file.text;
let lineStart = start;
let lineEnd = end;
while (lineStart > 0 && source[lineStart] != '\n') lineStart--;
if (lineStart < start) lineStart++;
while (lineEnd < source.length && source[lineEnd] != '\n') lineEnd++;
let line = source.substring(lineStart, lineEnd);
const lineIndex = line.indexOf('/n');
if (lineIndex > 0) {
line = line.substr(0, lineIndex);
end = start + lineIndex;
}
const lineNo = lineNoOf(start, source) + ': ';
return '\n' + lineNo + line + '\n' + chars(start - lineStart + lineNo.length, ' ') +
chars(end - start, '^');
}
return '';
}
function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
if (diagnostics && diagnostics.length) {
throw new Error(
'Errors from TypeScript:\n' +
diagnostics.map(d => `${fileInfo(d)}${d.messageText}${lineInfo(d)}`).join(' \n'));
}
}
expectNoDiagnostics(program.getOptionsDiagnostics());
expectNoDiagnostics(program.getSyntacticDiagnostics());
expectNoDiagnostics(program.getSemanticDiagnostics());
}
function isDTS(fileName: string): boolean {
return /\.d\.ts$/.test(fileName);
}
function isSource(fileName: string): boolean {
return /\.ts$/.test(fileName);
}
function isFactory(fileName: string): boolean {
return /\.ngfactory\./.test(fileName);
}
function summaryCompile(
host: MockCompilerHost, aotHost: MockAotCompilerHost,
preCompile?: (program: ts.Program) => void) {
// First compile the program to generate the summary files.
return compile(host, aotHost).then(generatedFiles => {
// Remove generated files that were not generated from a DTS file
host.remove(generatedFiles.filter(f => !isDTS(f.srcFileUrl)).map(f => f.genFileUrl));
// Next compile the program shrowding metadata and only treating .ts files as source.
aotHost.hideMetadata();
aotHost.tsFilesOnly();
return compile(host, aotHost);
});
}
function compile(
host: MockCompilerHost, aotHost: AotCompilerHost, preCompile?: (program: ts.Program) => void,
postCompile: (program: ts.Program) => void = expectNoDiagnostics,
options: AotCompilerOptions = {}) {
const scripts = host.scriptNames.slice(0);
const program = ts.createProgram(scripts, settings, host);
if (preCompile) preCompile(program);
const {compiler, reflector} = createAotCompiler(aotHost, options);
return compiler.compileAll(program.getSourceFiles().map(sf => sf.fileName))
.then(generatedFiles => {
generatedFiles.forEach(
file => isSource(file.genFileUrl) ? host.addScript(file.genFileUrl, file.source) :
host.override(file.genFileUrl, file.source));
const scripts = host.scriptNames.slice(0);
const newProgram = ts.createProgram(scripts, settings, host);
if (postCompile) postCompile(newProgram);
return generatedFiles;
});
}
const QUICKSTART = ['/quickstart/app/app.module.ts'];
const FILES: MockData = {
quickstart: {
app: {
'app.component.ts': `
import {Component} from '@angular/core';
@Component({
template: '<h1>Hello {{name}}</h1>'
})
export class AppComponent {
name = 'Angular';
}
`,
'app.module.ts': `
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
`
}
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,487 @@
/**
* @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 {StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost, Summary, SummaryResolver} from '@angular/compiler';
import {CollectorOptions, MetadataCollector} from '@angular/tsc-wrapped';
import * as ts from 'typescript';
// This matches .ts files but not .d.ts files.
const TS_EXT = /(^.|(?!\.d)..)\.ts$/;
describe('StaticSymbolResolver', () => {
const noContext = new StaticSymbol('', '', []);
let host: StaticSymbolResolverHost;
let symbolResolver: StaticSymbolResolver;
let symbolCache: StaticSymbolCache;
beforeEach(() => { symbolCache = new StaticSymbolCache(); });
function init(
testData: {[key: string]: any} = DEFAULT_TEST_DATA, summaries: Summary<StaticSymbol>[] = [],
summaryImportAs: {symbol: StaticSymbol, importAs: StaticSymbol}[] = []) {
host = new MockStaticSymbolResolverHost(testData);
symbolResolver = new StaticSymbolResolver(
host, symbolCache, new MockSummaryResolver(summaries, summaryImportAs));
}
beforeEach(() => init());
it('should throw an exception for unsupported metadata versions', () => {
expect(
() => symbolResolver.resolveSymbol(
symbolResolver.getSymbolByModule('src/version-error', 'e')))
.toThrow(new Error(
'Metadata version mismatch for module /tmp/src/version-error.d.ts, found version 100, expected 3'));
});
it('should throw an exception for version 2 metadata', () => {
expect(
() => symbolResolver.resolveSymbol(
symbolResolver.getSymbolByModule('src/version-2-error', 'e')))
.toThrowError(
'Unsupported metadata version 2 for module /tmp/src/version-2-error.d.ts. This module should be compiled with a newer version of ngc');
});
it('should be produce the same symbol if asked twice', () => {
const foo1 = symbolResolver.getStaticSymbol('main.ts', 'foo');
const foo2 = symbolResolver.getStaticSymbol('main.ts', 'foo');
expect(foo1).toBe(foo2);
});
it('should be able to produce a symbol for a module with no file', () => {
expect(symbolResolver.getStaticSymbol('angularjs', 'SomeAngularSymbol')).toBeDefined();
});
it('should be able to split the metadata per symbol', () => {
init({
'/tmp/src/test.ts': `
export var a = 1;
export var b = 2;
`
});
expect(symbolResolver.resolveSymbol(symbolResolver.getStaticSymbol('/tmp/src/test.ts', 'a'))
.metadata)
.toBe(1);
expect(symbolResolver.resolveSymbol(symbolResolver.getStaticSymbol('/tmp/src/test.ts', 'b'))
.metadata)
.toBe(2);
});
it('should be able to resolve static symbols with members', () => {
init({
'/tmp/src/test.ts': `
export {exportedObj} from './export';
export var obj = {a: 1};
export class SomeClass {
static someField = 2;
}
`,
'/tmp/src/export.ts': `
export var exportedObj = {};
`,
});
expect(symbolResolver
.resolveSymbol(symbolResolver.getStaticSymbol('/tmp/src/test.ts', 'obj', ['a']))
.metadata)
.toBe(1);
expect(symbolResolver
.resolveSymbol(
symbolResolver.getStaticSymbol('/tmp/src/test.ts', 'SomeClass', ['someField']))
.metadata)
.toBe(2);
expect(symbolResolver
.resolveSymbol(symbolResolver.getStaticSymbol(
'/tmp/src/test.ts', 'exportedObj', ['someMember']))
.metadata)
.toBe(symbolResolver.getStaticSymbol('/tmp/src/export.ts', 'exportedObj', ['someMember']));
});
it('should use summaries in resolveSymbol and prefer them over regular metadata', () => {
const someSymbol = symbolCache.get('/test.ts', 'a');
init({'/test.ts': 'export var a = 2'}, [{symbol: someSymbol, metadata: 1}]);
expect(symbolResolver.resolveSymbol(someSymbol).metadata).toBe(1);
});
it('should be able to get all exported symbols of a file', () => {
expect(symbolResolver.getSymbolsOf('/tmp/src/reexport/src/origin1.d.ts')).toEqual([
symbolResolver.getStaticSymbol('/tmp/src/reexport/src/origin1.d.ts', 'One'),
symbolResolver.getStaticSymbol('/tmp/src/reexport/src/origin1.d.ts', 'Two'),
symbolResolver.getStaticSymbol('/tmp/src/reexport/src/origin1.d.ts', 'Three'),
]);
});
it('should be able to get all reexported symbols of a file', () => {
expect(symbolResolver.getSymbolsOf('/tmp/src/reexport/reexport.d.ts')).toEqual([
symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'One'),
symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'Two'),
symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'Four'),
symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'Five'),
symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'Thirty')
]);
});
it('should merge the exported symbols of a file with the exported symbols of its summary', () => {
const someSymbol = symbolCache.get('/test.ts', 'a');
init(
{'/test.ts': 'export var b = 2'},
[{symbol: symbolCache.get('/test.ts', 'a'), metadata: 1}]);
expect(symbolResolver.getSymbolsOf('/test.ts')).toEqual([
symbolCache.get('/test.ts', 'a'), symbolCache.get('/test.ts', 'b')
]);
});
describe('importAs', () => {
it('should calculate importAs relationship for non source files without summaries', () => {
init(
{
'/test.d.ts': [{
'__symbolic': 'module',
'version': 3,
'metadata': {
'a': {'__symbolic': 'reference', 'name': 'b', 'module': './test2'},
}
}],
'/test2.d.ts': [{
'__symbolic': 'module',
'version': 3,
'metadata': {
'b': {'__symbolic': 'reference', 'name': 'c', 'module': './test3'},
}
}]
},
[]);
symbolResolver.getSymbolsOf('/test.d.ts');
symbolResolver.getSymbolsOf('/test2.d.ts');
expect(symbolResolver.getImportAs(symbolCache.get('/test2.d.ts', 'b')))
.toBe(symbolCache.get('/test.d.ts', 'a'));
expect(symbolResolver.getImportAs(symbolCache.get('/test3.d.ts', 'c')))
.toBe(symbolCache.get('/test.d.ts', 'a'));
});
it('should calculate importAs relationship for non source files with summaries', () => {
init(
{
'/test.ts': `
export {a} from './test2';
`
},
[], [{
symbol: symbolCache.get('/test2.d.ts', 'a'),
importAs: symbolCache.get('/test3.d.ts', 'b')
}]);
symbolResolver.getSymbolsOf('/test.ts');
expect(symbolResolver.getImportAs(symbolCache.get('/test2.d.ts', 'a')))
.toBe(symbolCache.get('/test3.d.ts', 'b'));
});
it('should calculate importAs for symbols with members based on importAs for symbols without',
() => {
init(
{
'/test.ts': `
export {a} from './test2';
`
},
[], [{
symbol: symbolCache.get('/test2.d.ts', 'a'),
importAs: symbolCache.get('/test3.d.ts', 'b')
}]);
symbolResolver.getSymbolsOf('/test.ts');
expect(symbolResolver.getImportAs(symbolCache.get('/test2.d.ts', 'a', ['someMember'])))
.toBe(symbolCache.get('/test3.d.ts', 'b', ['someMember']));
});
});
it('should replace references by StaticSymbols', () => {
init({
'/test.ts': `
import {b, y} from './test2';
export var a = b;
export var x = [y];
export function simpleFn(fnArg) {
return [a, y, fnArg];
}
`,
'/test2.ts': `
export var b;
export var y;
`
});
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'a')).metadata)
.toEqual(symbolCache.get('/test2.ts', 'b'));
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'x')).metadata).toEqual([
symbolCache.get('/test2.ts', 'y')
]);
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'simpleFn')).metadata).toEqual({
__symbolic: 'function',
parameters: ['fnArg'],
value: [
symbolCache.get('/test.ts', 'a'), symbolCache.get('/test2.ts', 'y'),
Object({__symbolic: 'reference', name: 'fnArg'})
]
});
});
it('should ignore module references without a name', () => {
init({
'/test.ts': `
import Default from './test2';
export {Default};
`
});
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'Default')).metadata)
.toBeFalsy();
});
it('should fill references to ambient symbols with undefined', () => {
init({
'/test.ts': `
export var y = 1;
export var z = [window, z];
`
});
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'z')).metadata).toEqual([
undefined, symbolCache.get('/test.ts', 'z')
]);
});
it('should allow to use symbols with __', () => {
init({
'/test.ts': `
export {__a__ as __b__} from './test2';
import {__c__} from './test2';
export var __x__ = 1;
export var __y__ = __c__;
`
});
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', '__x__')).metadata).toBe(1);
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', '__y__')).metadata)
.toBe(symbolCache.get('/test2.d.ts', '__c__'));
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', '__b__')).metadata)
.toBe(symbolCache.get('/test2.d.ts', '__a__'));
expect(symbolResolver.getSymbolsOf('/test.ts')).toEqual([
symbolCache.get('/test.ts', '__x__'), symbolCache.get('/test.ts', '__y__'),
symbolCache.get('/test.ts', '__b__')
]);
});
it('should be able to trace a named export', () => {
const symbol = symbolResolver
.resolveSymbol(symbolResolver.getSymbolByModule(
'./reexport/reexport', 'One', '/tmp/src/main.ts'))
.metadata;
expect(symbol.name).toEqual('One');
expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin1.d.ts');
});
it('should be able to trace a renamed export', () => {
const symbol = symbolResolver
.resolveSymbol(symbolResolver.getSymbolByModule(
'./reexport/reexport', 'Four', '/tmp/src/main.ts'))
.metadata;
expect(symbol.name).toEqual('Three');
expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin1.d.ts');
});
it('should be able to trace an export * export', () => {
const symbol = symbolResolver
.resolveSymbol(symbolResolver.getSymbolByModule(
'./reexport/reexport', 'Five', '/tmp/src/main.ts'))
.metadata;
expect(symbol.name).toEqual('Five');
expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin5.d.ts');
});
it('should be able to trace a multi-level re-export', () => {
const symbol1 = symbolResolver
.resolveSymbol(symbolResolver.getSymbolByModule(
'./reexport/reexport', 'Thirty', '/tmp/src/main.ts'))
.metadata;
expect(symbol1.name).toEqual('Thirty');
expect(symbol1.filePath).toEqual('/tmp/src/reexport/src/reexport2.d.ts');
const symbol2 = symbolResolver.resolveSymbol(symbol1).metadata;
expect(symbol2.name).toEqual('Thirty');
expect(symbol2.filePath).toEqual('/tmp/src/reexport/src/origin30.d.ts');
});
it('should cache tracing a named export', () => {
const moduleNameToFileNameSpy = spyOn(host, 'moduleNameToFileName').and.callThrough();
const getMetadataForSpy = spyOn(host, 'getMetadataFor').and.callThrough();
symbolResolver.resolveSymbol(
symbolResolver.getSymbolByModule('./reexport/reexport', 'One', '/tmp/src/main.ts'));
moduleNameToFileNameSpy.calls.reset();
getMetadataForSpy.calls.reset();
const symbol = symbolResolver
.resolveSymbol(symbolResolver.getSymbolByModule(
'./reexport/reexport', 'One', '/tmp/src/main.ts'))
.metadata;
expect(moduleNameToFileNameSpy.calls.count()).toBe(1);
expect(getMetadataForSpy.calls.count()).toBe(0);
expect(symbol.name).toEqual('One');
expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin1.d.ts');
});
});
export class MockSummaryResolver implements SummaryResolver<StaticSymbol> {
constructor(private summaries: Summary<StaticSymbol>[] = [], private importAs: {
symbol: StaticSymbol,
importAs: StaticSymbol
}[] = []) {}
resolveSummary(reference: StaticSymbol): Summary<StaticSymbol> {
return this.summaries.find(summary => summary.symbol === reference);
};
getSymbolsOf(filePath: string): StaticSymbol[] {
return this.summaries.filter(summary => summary.symbol.filePath === filePath)
.map(summary => summary.symbol);
}
getImportAs(symbol: StaticSymbol): StaticSymbol {
const entry = this.importAs.find(entry => entry.symbol === symbol);
return entry ? entry.importAs : undefined;
}
isLibraryFile(filePath: string): boolean { return filePath.endsWith('.d.ts'); }
getLibraryFileName(filePath: string): string { return filePath.replace(/(\.d)?\.ts$/, '.d.ts'); }
}
export class MockStaticSymbolResolverHost implements StaticSymbolResolverHost {
private collector: MetadataCollector;
constructor(private data: {[key: string]: any}, collectorOptions?: CollectorOptions) {
this.collector = new MetadataCollector(collectorOptions);
}
// In tests, assume that symbols are not re-exported
moduleNameToFileName(modulePath: string, containingFile?: string): string {
function splitPath(path: string): string[] { return path.split(/\/|\\/g); }
function resolvePath(pathParts: string[]): string {
const result: string[] = [];
pathParts.forEach((part, index) => {
switch (part) {
case '':
case '.':
if (index > 0) return;
break;
case '..':
if (index > 0 && result.length != 0) result.pop();
return;
}
result.push(part);
});
return result.join('/');
}
function pathTo(from: string, to: string): string {
let result = to;
if (to.startsWith('.')) {
const fromParts = splitPath(from);
fromParts.pop(); // remove the file name.
const toParts = splitPath(to);
result = resolvePath(fromParts.concat(toParts));
}
return result;
}
if (modulePath.indexOf('.') === 0) {
const baseName = pathTo(containingFile, modulePath);
const tsName = baseName + '.ts';
if (this._getMetadataFor(tsName)) {
return tsName;
}
return baseName + '.d.ts';
}
return '/tmp/' + modulePath + '.d.ts';
}
getMetadataFor(moduleId: string): any { return this._getMetadataFor(moduleId); }
private _getMetadataFor(filePath: string): any {
if (this.data[filePath] && filePath.match(TS_EXT)) {
const text = this.data[filePath];
if (typeof text === 'string') {
const sf = ts.createSourceFile(
filePath, this.data[filePath], ts.ScriptTarget.ES5, /* setParentNodes */ true);
const diagnostics: ts.Diagnostic[] = (<any>sf).parseDiagnostics;
if (diagnostics && diagnostics.length) {
throw Error(`Error encountered during parse of file ${filePath}`);
}
return [this.collector.getMetadata(sf)];
}
}
const result = this.data[filePath];
if (result) {
return Array.isArray(result) ? result : [result];
} else {
return null;
}
}
}
const DEFAULT_TEST_DATA: {[key: string]: any} = {
'/tmp/src/version-error.d.ts': {'__symbolic': 'module', 'version': 100, metadata: {e: 's'}},
'/tmp/src/version-2-error.d.ts': {'__symbolic': 'module', 'version': 2, metadata: {e: 's'}},
'/tmp/src/reexport/reexport.d.ts': {
__symbolic: 'module',
version: 3,
metadata: {},
exports: [
{from: './src/origin1', export: ['One', 'Two', {name: 'Three', as: 'Four'}]},
{from: './src/origin5'}, {from: './src/reexport2'}
]
},
'/tmp/src/reexport/src/origin1.d.ts': {
__symbolic: 'module',
version: 3,
metadata: {
One: {__symbolic: 'class'},
Two: {__symbolic: 'class'},
Three: {__symbolic: 'class'},
},
},
'/tmp/src/reexport/src/origin5.d.ts': {
__symbolic: 'module',
version: 3,
metadata: {
Five: {__symbolic: 'class'},
},
},
'/tmp/src/reexport/src/origin30.d.ts': {
__symbolic: 'module',
version: 3,
metadata: {
Thirty: {__symbolic: 'class'},
},
},
'/tmp/src/reexport/src/originNone.d.ts': {
__symbolic: 'module',
version: 3,
metadata: {},
},
'/tmp/src/reexport/src/reexport2.d.ts': {
__symbolic: 'module',
version: 3,
metadata: {},
exports: [{from: './originNone'}, {from: './origin30'}]
}
};

View File

@ -0,0 +1,111 @@
/**
* @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 {AotSummaryResolver, AotSummaryResolverHost, CompileSummaryKind, CompileTypeSummary, ResolvedStaticSymbol, StaticSymbol, StaticSymbolCache, StaticSymbolResolver} from '@angular/compiler';
import {deserializeSummaries, serializeSummaries} from '@angular/compiler/src/aot/summary_serializer';
import * as path from 'path';
import {MockStaticSymbolResolverHost, MockSummaryResolver} from './static_symbol_resolver_spec';
const EXT = /(\.d)?\.ts$/;
export function main() {
describe('AotSummaryResolver', () => {
let summaryResolver: AotSummaryResolver;
let symbolCache: StaticSymbolCache;
let host: MockAotSummaryResolverHost;
beforeEach(() => { symbolCache = new StaticSymbolCache(); });
function init(summaries: {[filePath: string]: string} = {}) {
host = new MockAotSummaryResolverHost(summaries);
summaryResolver = new AotSummaryResolver(host, symbolCache);
}
function serialize(symbols: ResolvedStaticSymbol[], types: CompileTypeSummary[]): string {
// Note: Don't use the top level host / summaryResolver as they might not be created yet
const mockSummaryResolver = new MockSummaryResolver([]);
const symbolResolver = new StaticSymbolResolver(
new MockStaticSymbolResolverHost({}), symbolCache, mockSummaryResolver);
return serializeSummaries(mockSummaryResolver, symbolResolver, symbols, types).json;
}
it('should load serialized summary files', () => {
const asymbol = symbolCache.get('/a.d.ts', 'a');
init({'/a.ngsummary.json': serialize([{symbol: asymbol, metadata: 1}], [])});
expect(summaryResolver.resolveSummary(asymbol)).toEqual({symbol: asymbol, metadata: 1});
});
it('should not load summaries for source files', () => {
init({});
spyOn(host, 'loadSummary').and.callThrough();
expect(summaryResolver.resolveSummary(symbolCache.get('/a.ts', 'a'))).toBeFalsy();
expect(host.loadSummary).not.toHaveBeenCalled();
});
it('should cache summaries', () => {
const asymbol = symbolCache.get('/a.d.ts', 'a');
init({'/a.ngsummary.json': serialize([{symbol: asymbol, metadata: 1}], [])});
expect(summaryResolver.resolveSummary(asymbol)).toBe(summaryResolver.resolveSummary(asymbol));
});
it('should return all symbols in a summary', () => {
const asymbol = symbolCache.get('/a.d.ts', 'a');
init({'/a.ngsummary.json': serialize([{symbol: asymbol, metadata: 1}], [])});
expect(summaryResolver.getSymbolsOf('/a.d.ts')).toEqual([asymbol]);
});
it('should fill importAs for deep symbols', () => {
const libSymbol = symbolCache.get('/lib.d.ts', 'Lib');
const srcSymbol = symbolCache.get('/src.ts', 'Src');
init({
'/src.ngsummary.json':
serialize([{symbol: srcSymbol, metadata: 1}, {symbol: libSymbol, metadata: 2}], [])
});
summaryResolver.getSymbolsOf('/src.d.ts');
expect(summaryResolver.getImportAs(symbolCache.get('/src.d.ts', 'Src'))).toBeFalsy();
expect(summaryResolver.getImportAs(libSymbol))
.toBe(symbolCache.get('/src.ngfactory.ts', 'Lib_1'));
});
describe('isLibraryFile', () => {
it('should use host.isSourceFile to calculate the result', () => {
init();
expect(summaryResolver.isLibraryFile('someFile.ts')).toBe(false);
expect(summaryResolver.isLibraryFile('someFile.d.ts')).toBe(true);
});
it('should calculate the result for generated files based on the result for non generated files',
() => {
init();
spyOn(host, 'isSourceFile').and.callThrough();
expect(summaryResolver.isLibraryFile('someFile.ngfactory.ts')).toBe(false);
expect(host.isSourceFile).toHaveBeenCalledWith('someFile.ts');
});
});
});
}
export class MockAotSummaryResolverHost implements AotSummaryResolverHost {
constructor(private summaries: {[fileName: string]: string}) {}
fileNameToModuleName(fileName: string): string {
return './' + path.basename(fileName).replace(EXT, '');
}
getOutputFileName(sourceFileName: string): string {
return sourceFileName.replace(EXT, '') + '.d.ts';
}
isSourceFile(filePath: string) { return !filePath.endsWith('.d.ts'); }
loadSummary(filePath: string): string { return this.summaries[filePath]; }
}

View File

@ -0,0 +1,228 @@
/**
* @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 {AotSummaryResolver, AotSummaryResolverHost, CompileSummaryKind, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost} from '@angular/compiler';
import {deserializeSummaries, serializeSummaries} from '@angular/compiler/src/aot/summary_serializer';
import {summaryFileName} from '@angular/compiler/src/aot/util';
import {MockStaticSymbolResolverHost} from './static_symbol_resolver_spec';
import {MockAotSummaryResolverHost} from './summary_resolver_spec';
export function main() {
describe('summary serializer', () => {
let summaryResolver: AotSummaryResolver;
let symbolResolver: StaticSymbolResolver;
let symbolCache: StaticSymbolCache;
let host: MockAotSummaryResolverHost;
beforeEach(() => { symbolCache = new StaticSymbolCache(); });
function init(
summaries: {[filePath: string]: string} = {}, metadata: {[key: string]: any} = {}) {
host = new MockAotSummaryResolverHost(summaries);
summaryResolver = new AotSummaryResolver(host, symbolCache);
symbolResolver = new StaticSymbolResolver(
new MockStaticSymbolResolverHost(metadata), symbolCache, summaryResolver);
}
describe('summaryFileName', () => {
it('should add .ngsummary.json to the filename', () => {
init();
expect(summaryFileName('a.ts')).toBe('a.ngsummary.json');
expect(summaryFileName('a.d.ts')).toBe('a.ngsummary.json');
expect(summaryFileName('a.js')).toBe('a.ngsummary.json');
});
});
it('should serialize various data correctly', () => {
init();
const serializedData = serializeSummaries(
summaryResolver, symbolResolver,
[
{
symbol: symbolCache.get('/tmp/some_values.ts', 'Values'),
metadata: {
aNumber: 1,
aString: 'hello',
anArray: [1, 2],
aStaticSymbol: symbolCache.get('/tmp/some_symbol.ts', 'someName'),
aStaticSymbolWithMembers:
symbolCache.get('/tmp/some_symbol.ts', 'someName', ['someMember']),
}
},
{
symbol: symbolCache.get('/tmp/some_service.ts', 'SomeService'),
metadata: {
__symbolic: 'class',
members: {'aMethod': {__symbolic: 'function'}},
statics: {aStatic: true}
}
}
],
[<any>{
summaryKind: CompileSummaryKind.Injectable,
type: {
reference: symbolCache.get('/tmp/some_service.ts', 'SomeService'),
}
}]);
const summaries = deserializeSummaries(symbolCache, serializedData.json).summaries;
expect(summaries.length).toBe(2);
// Note: change from .ts to .d.ts is expected
expect(summaries[0].symbol).toBe(symbolCache.get('/tmp/some_values.d.ts', 'Values'));
expect(summaries[0].metadata).toEqual({
aNumber: 1,
aString: 'hello',
anArray: [1, 2],
aStaticSymbol: symbolCache.get('/tmp/some_symbol.d.ts', 'someName'),
aStaticSymbolWithMembers:
symbolCache.get('/tmp/some_symbol.d.ts', 'someName', ['someMember'])
});
expect(summaries[1].symbol).toBe(symbolCache.get('/tmp/some_service.d.ts', 'SomeService'));
// serialization should only keep the statics...
expect(summaries[1].metadata).toEqual({__symbolic: 'class', statics: {aStatic: true}});
expect(summaries[1].type.type.reference)
.toBe(symbolCache.get('/tmp/some_service.d.ts', 'SomeService'));
});
it('should automatically add exported directives / pipes of NgModules that are not source files',
() => {
init();
const externalSerialized = serializeSummaries(summaryResolver, symbolResolver, [], [
<any>{
summaryKind: CompileSummaryKind.Pipe,
type: {
reference: symbolCache.get('/tmp/external.ts', 'SomeExternalPipe'),
}
},
<any>{
summaryKind: CompileSummaryKind.Directive,
type: {
reference: symbolCache.get('/tmp/external.ts', 'SomeExternalDir'),
}
}
]);
init({
'/tmp/external.ngsummary.json': externalSerialized.json,
});
const serialized = serializeSummaries(
summaryResolver, symbolResolver, [], [<any>{
summaryKind: CompileSummaryKind.NgModule,
type: {reference: symbolCache.get('/tmp/some_module.ts', 'SomeModule')},
exportedPipes: [
{reference: symbolCache.get('/tmp/some_pipe.ts', 'SomePipe')},
{reference: symbolCache.get('/tmp/external.d.ts', 'SomeExternalPipe')}
],
exportedDirectives: [
{reference: symbolCache.get('/tmp/some_dir.ts', 'SomeDir')},
{reference: symbolCache.get('/tmp/external.d.ts', 'SomeExternalDir')}
]
}]);
const summaries = deserializeSummaries(symbolCache, serialized.json).summaries;
expect(summaries.length).toBe(3);
expect(summaries[0].symbol).toBe(symbolCache.get('/tmp/some_module.d.ts', 'SomeModule'));
expect(summaries[1].symbol).toBe(symbolCache.get('/tmp/external.d.ts', 'SomeExternalDir'));
expect(summaries[2].symbol)
.toBe(symbolCache.get('/tmp/external.d.ts', 'SomeExternalPipe'));
});
it('should automatically add the metadata of referenced symbols that are not in the soure files',
() => {
init();
const externalSerialized = serializeSummaries(
summaryResolver, symbolResolver,
[
{
symbol: symbolCache.get('/tmp/external.ts', 'PROVIDERS'),
metadata: [symbolCache.get('/tmp/external_svc.ts', 'SomeService')]
},
{
symbol: symbolCache.get('/tmp/external_svc.ts', 'SomeService'),
metadata: {__symbolic: 'class'}
}
],
[<any>{
summaryKind: CompileSummaryKind.Injectable,
type: {
reference: symbolCache.get('/tmp/external_svc.ts', 'SomeService'),
}
}]);
init(
{
'/tmp/external.ngsummary.json': externalSerialized.json,
},
{
'/tmp/local.ts': `
export var local = 'a';
`,
'/tmp/non_summary.d.ts':
{__symbolic: 'module', version: 3, metadata: {'external': 'b'}}
});
const serialized = serializeSummaries(
summaryResolver, symbolResolver, [{
symbol: symbolCache.get('/tmp/test.ts', 'main'),
metadata: {
local: symbolCache.get('/tmp/local.ts', 'local'),
external: symbolCache.get('/tmp/external.d.ts', 'PROVIDERS'),
externalNonSummary: symbolCache.get('/tmp/non_summary.d.ts', 'external')
}
}],
[]);
const summaries = deserializeSummaries(symbolCache, serialized.json).summaries;
// Note: local should not show up!
expect(summaries.length).toBe(4);
expect(summaries[0].symbol).toBe(symbolCache.get('/tmp/test.d.ts', 'main'));
expect(summaries[0].metadata).toEqual({
local: symbolCache.get('/tmp/local.d.ts', 'local'),
external: symbolCache.get('/tmp/external.d.ts', 'PROVIDERS'),
externalNonSummary: symbolCache.get('/tmp/non_summary.d.ts', 'external')
});
expect(summaries[1].symbol).toBe(symbolCache.get('/tmp/external.d.ts', 'PROVIDERS'));
expect(summaries[1].metadata).toEqual([symbolCache.get(
'/tmp/external_svc.d.ts', 'SomeService')]);
// there was no summary for non_summary, but it should have
// been serialized as well.
expect(summaries[2].symbol).toBe(symbolCache.get('/tmp/non_summary.d.ts', 'external'));
expect(summaries[2].metadata).toEqual('b');
// SomService is a transitive dep, but sould have been serialized as well.
expect(summaries[3].symbol).toBe(symbolCache.get('/tmp/external_svc.d.ts', 'SomeService'));
expect(summaries[3].type.type.reference)
.toBe(symbolCache.get('/tmp/external_svc.d.ts', 'SomeService'));
});
it('should create "importAs" names for non source symbols', () => {
init();
const serialized = serializeSummaries(
summaryResolver, symbolResolver, [{
symbol: symbolCache.get('/tmp/test.ts', 'main'),
metadata: [
symbolCache.get('/tmp/external.d.ts', 'lib'),
symbolCache.get('/tmp/external.d.ts', 'lib', ['someMember']),
]
}],
[]);
// Note: no entry for the symbol with members!
expect(serialized.exportAs).toEqual([
{symbol: symbolCache.get('/tmp/external.d.ts', 'lib'), exportAs: 'lib_1'}
]);
const deserialized = deserializeSummaries(symbolCache, serialized.json);
// Note: no entry for the symbol with members!
expect(deserialized.importAs).toEqual([
{symbol: symbolCache.get('/tmp/external.d.ts', 'lib'), importAs: 'lib_1'}
]);
});
});
}

View File

@ -0,0 +1,431 @@
/**
* @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 {AotCompilerHost} from '@angular/compiler';
import {MetadataBundlerHost, MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
export type MockData = string | MockDirectory;
export type MockDirectory = {
[name: string]: MockData | undefined;
};
export function isDirectory(data: MockData): data is MockDirectory {
return typeof data !== 'string';
}
const NODE_MODULES = '/node_modules/';
const IS_GENERATED = /\.(ngfactory|ngstyle)$/;
const angularts = /@angular\/(\w|\/|-)+\.tsx?$/;
const rxjs = /\/rxjs\//;
const tsxfile = /\.tsx$/;
export const settings: ts.CompilerOptions = {
target: ts.ScriptTarget.ES5,
declaration: true,
module: ts.ModuleKind.CommonJS,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
emitDecoratorMetadata: true,
experimentalDecorators: true,
removeComments: false,
noImplicitAny: false,
skipLibCheck: true,
lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'],
types: []
};
export interface EmitterOptions { emitMetadata: boolean; }
export class EmittingCompilerHost implements ts.CompilerHost {
private angularSourcePath: string|undefined;
private nodeModulesPath: string|undefined;
private addedFiles = new Map<string, string>();
private writtenFiles = new Map<string, string>();
private scriptNames: string[];
private root = '/';
private collector = new MetadataCollector();
constructor(scriptNames: string[], private options: EmitterOptions) {
const moduleFilename = module.filename.replace(/\\/g, '/');
const distIndex = moduleFilename.indexOf('/dist/all');
if (distIndex >= 0) {
const root = moduleFilename.substr(0, distIndex);
this.nodeModulesPath = path.join(root, 'node_modules');
this.angularSourcePath = path.join(root, 'modules');
// Rewrite references to scripts with '@angular' to its corresponding location in
// the source tree.
this.scriptNames = scriptNames.map(f => this.effectiveName(f));
this.root = root;
}
}
public addScript(fileName: string, content: string) {
const scriptName = this.effectiveName(fileName);
this.addedFiles.set(scriptName, content);
this.scriptNames.push(scriptName);
}
public override(fileName: string, content: string) {
const scriptName = this.effectiveName(fileName);
this.addedFiles.set(scriptName, content);
}
public addWrittenFile(fileName: string, content: string) {
this.writtenFiles.set(this.effectiveName(fileName), content);
}
public getWrittenFiles(): {name: string, content: string}[] {
return Array.from(this.writtenFiles).map(f => ({name: f[0], content: f[1]}));
}
public get scripts(): string[] { return this.scriptNames; }
public get written(): Map<string, string> { return this.writtenFiles; }
public effectiveName(fileName: string): string {
return fileName.startsWith('@angular/') ? path.join(this.angularSourcePath, fileName) :
fileName;
}
// ts.ModuleResolutionHost
fileExists(fileName: string): boolean {
return this.addedFiles.has(fileName) || fs.existsSync(fileName);
}
readFile(fileName: string): string {
const result = this.addedFiles.get(fileName);
if (result) return result;
let basename = path.basename(fileName);
if (/^lib.*\.d\.ts$/.test(basename)) {
let libPath = ts.getDefaultLibFilePath(settings);
return fs.readFileSync(path.join(path.dirname(libPath), basename), 'utf8');
}
return fs.readFileSync(fileName, 'utf8');
}
directoryExists(directoryName: string): boolean {
return fs.existsSync(directoryName) && fs.statSync(directoryName).isDirectory();
}
getCurrentDirectory(): string { return this.root; }
getDirectories(dir: string): string[] {
return fs.readdirSync(dir).filter(p => {
const name = path.join(dir, p);
const stat = fs.statSync(name);
return stat && stat.isDirectory();
});
}
// ts.CompilerHost
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: (message: string) => void): ts.SourceFile {
const content = this.readFile(fileName);
if (content) {
return ts.createSourceFile(fileName, content, languageVersion, /* setParentNodes */ true);
}
}
getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; }
writeFile: ts.WriteFileCallback =
(fileName: string, data: string, writeByteOrderMark: boolean,
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
this.addWrittenFile(fileName, data);
if (this.options.emitMetadata && sourceFiles && sourceFiles.length && DTS.test(fileName)) {
const metadataFilePath = fileName.replace(DTS, '.metadata.json');
const metadata = this.collector.getMetadata(sourceFiles[0]);
if (metadata) {
this.addWrittenFile(metadataFilePath, JSON.stringify(metadata));
}
}
}
getCanonicalFileName(fileName: string): string {
return fileName;
}
useCaseSensitiveFileNames(): boolean { return false; }
getNewLine(): string { return '\n'; }
}
export class MockCompilerHost implements ts.CompilerHost {
scriptNames: string[];
private angularSourcePath: string|undefined;
private nodeModulesPath: string|undefined;
private overrides = new Map<string, string>();
private writtenFiles = new Map<string, string>();
private sourceFiles = new Map<string, ts.SourceFile>();
private assumeExists = new Set<string>();
private traces: string[] = [];
constructor(scriptNames: string[], private data: MockData, private angular: Map<string, string>) {
this.scriptNames = scriptNames.slice(0);
const moduleFilename = module.filename.replace(/\\/g, '/');
let angularIndex = moduleFilename.indexOf('@angular');
let distIndex = moduleFilename.indexOf('/dist/all');
if (distIndex >= 0) {
const root = moduleFilename.substr(0, distIndex);
this.nodeModulesPath = path.join(root, 'node_modules');
this.angularSourcePath = path.join(root, 'modules');
}
}
// Test API
override(fileName: string, content: string) {
if (content) {
this.overrides.set(fileName, content);
} else {
this.overrides.delete(fileName);
}
this.sourceFiles.delete(fileName);
}
addScript(fileName: string, content: string) {
this.overrides.set(fileName, content);
this.scriptNames.push(fileName);
this.sourceFiles.delete(fileName);
}
assumeFileExists(fileName: string) { this.assumeExists.add(fileName); }
remove(files: string[]) {
// Remove the files from the list of scripts.
const fileSet = new Set(files);
this.scriptNames = this.scriptNames.filter(f => fileSet.has(f));
// Remove files from written files
files.forEach(f => this.writtenFiles.delete(f));
}
// ts.ModuleResolutionHost
fileExists(fileName: string): boolean {
if (this.overrides.has(fileName) || this.writtenFiles.has(fileName) ||
this.assumeExists.has(fileName)) {
return true;
}
const effectiveName = this.getEffectiveName(fileName);
if (effectiveName == fileName) {
let result = open(fileName, this.data) != null;
return result;
} else {
if (fileName.match(rxjs)) {
let result = fs.existsSync(effectiveName);
return result;
}
let result = this.angular.has(effectiveName);
return result;
}
}
readFile(fileName: string): string { return this.getFileContent(fileName); }
trace(s: string): void { this.traces.push(s); }
getCurrentDirectory(): string { return '/'; }
getDirectories(dir: string): string[] {
const effectiveName = this.getEffectiveName(dir);
if (effectiveName === dir) {
const data = find(dir, this.data);
if (isDirectory(data)) {
return Object.keys(data).filter(k => isDirectory(data[k]));
}
return [];
} else {
return undefined;
}
}
// ts.CompilerHost
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: (message: string) => void): ts.SourceFile {
let result = this.sourceFiles.get(fileName);
if (!result) {
const content = this.getFileContent(fileName);
if (content) {
result = ts.createSourceFile(fileName, content, languageVersion);
this.sourceFiles.set(fileName, result);
}
}
return result;
}
getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; }
writeFile: ts.WriteFileCallback =
(fileName: string, data: string, writeByteOrderMark: boolean) => {
this.writtenFiles.set(fileName, data);
this.sourceFiles.delete(fileName);
}
getCanonicalFileName(fileName: string): string {
return fileName;
}
useCaseSensitiveFileNames(): boolean { return false; }
getNewLine(): string { return '\n'; }
// Private methods
private getFileContent(fileName: string): string|undefined {
if (this.overrides.has(fileName)) {
return this.overrides.get(fileName);
}
if (this.writtenFiles.has(fileName)) {
return this.writtenFiles.get(fileName);
}
let basename = path.basename(fileName);
if (/^lib.*\.d\.ts$/.test(basename)) {
let libPath = ts.getDefaultLibFilePath(settings);
return fs.readFileSync(path.join(path.dirname(libPath), basename), 'utf8');
} else {
let effectiveName = this.getEffectiveName(fileName);
if (effectiveName === fileName)
return open(fileName, this.data);
else {
if (fileName.match(rxjs)) {
if (fs.existsSync(fileName)) {
return fs.readFileSync(fileName, 'utf8');
}
}
return this.angular.get(effectiveName);
}
}
}
private getEffectiveName(name: string): string {
const node_modules = 'node_modules';
const at_angular = '/@angular';
const rxjs = '/rxjs';
if (name.startsWith('/' + node_modules)) {
if (this.angularSourcePath && name.startsWith('/' + node_modules + at_angular)) {
return path.join(this.angularSourcePath, name.substr(node_modules.length + 1));
}
if (this.nodeModulesPath && name.startsWith('/' + node_modules + rxjs)) {
return path.join(this.nodeModulesPath, name.substr(node_modules.length + 1));
}
}
return name;
}
}
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const DTS = /\.d\.ts$/;
const GENERATED_FILES = /\.ngfactory\.ts$|\.ngstyle\.ts$/;
export class MockAotCompilerHost implements AotCompilerHost {
private metadataCollector = new MetadataCollector();
private metadataVisible: boolean = true;
private dtsAreSource: boolean = true;
constructor(private tsHost: MockCompilerHost) {}
hideMetadata() { this.metadataVisible = false; }
tsFilesOnly() { this.dtsAreSource = false; }
// StaticSymbolResolverHost
getMetadataFor(modulePath: string): {[key: string]: any}[] {
if (!this.tsHost.fileExists(modulePath)) {
return undefined;
}
if (DTS.test(modulePath)) {
if (this.metadataVisible) {
const metadataPath = modulePath.replace(DTS, '.metadata.json');
if (this.tsHost.fileExists(metadataPath)) {
let result = JSON.parse(this.tsHost.readFile(metadataPath));
return Array.isArray(result) ? result : [result];
}
}
} else {
const sf = this.tsHost.getSourceFile(modulePath, ts.ScriptTarget.Latest);
const metadata = this.metadataCollector.getMetadata(sf);
return metadata ? [metadata] : [];
}
}
moduleNameToFileName(moduleName: string, containingFile: string): string|null {
if (!containingFile || !containingFile.length) {
if (moduleName.indexOf('.') === 0) {
throw new Error('Resolution of relative paths requires a containing file.');
}
// Any containing file gives the same result for absolute imports
containingFile = path.join('/', 'index.ts');
}
moduleName = moduleName.replace(EXT, '');
const resolved = ts.resolveModuleName(
moduleName, containingFile.replace(/\\/g, '/'),
{baseDir: '/', genDir: '/'}, this.tsHost)
.resolvedModule;
return resolved ? resolved.resolvedFileName : null;
}
// AotSummaryResolverHost
loadSummary(filePath: string): string|null { return this.tsHost.readFile(filePath); }
isSourceFile(sourceFilePath: string): boolean {
return !GENERATED_FILES.test(sourceFilePath) &&
(this.dtsAreSource || !DTS.test(sourceFilePath));
}
getOutputFileName(sourceFilePath: string): string {
return sourceFilePath.replace(EXT, '') + '.d.ts';
}
// AotCompilerHost
fileNameToModuleName(importedFile: string, containingFile: string): string|null {
return importedFile.replace(EXT, '');
}
loadResource(path: string): Promise<string> {
return Promise.resolve(this.tsHost.readFile(path));
}
}
export class MockMetadataBundlerHost implements MetadataBundlerHost {
private collector = new MetadataCollector();
constructor(private host: ts.CompilerHost) {}
getMetadataFor(moduleName: string): ModuleMetadata {
const source = this.host.getSourceFile(moduleName + '.ts', ts.ScriptTarget.Latest);
return this.collector.getMetadata(source);
}
}
function find(fileName: string, data: MockData): MockData|undefined {
let names = fileName.split('/');
if (names.length && !names[0].length) names.shift();
let current = data;
for (let name of names) {
if (typeof current === 'string')
return undefined;
else
current = (<MockDirectory>current)[name];
if (!current) return undefined;
}
return current;
}
function open(fileName: string, data: MockData): string|undefined {
let result = find(fileName, data);
if (typeof result === 'string') {
return result;
}
return undefined;
}
function directoryExists(dirname: string, data: MockData): boolean {
let result = find(dirname, data);
return result && typeof result !== 'string';
}

View File

@ -0,0 +1,376 @@
/**
* @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 {describe, expect, it} from '../../../core/testing/testing_internal';
import {CssLexer, CssLexerMode, CssToken, CssTokenType, cssScannerError, getRawMessage, getToken} from '../../src/css_parser/css_lexer';
export function main() {
function tokenize(
code: string, trackComments: boolean = false,
mode: CssLexerMode = CssLexerMode.ALL): CssToken[] {
const scanner = new CssLexer().scan(code, trackComments);
scanner.setMode(mode);
const tokens: CssToken[] = [];
let output = scanner.scan();
while (output != null) {
const error = output.error;
if (error != null) {
throw cssScannerError(getToken(error), getRawMessage(error));
}
tokens.push(output.token);
output = scanner.scan();
}
return tokens;
}
describe('CssLexer', () => {
it('should lex newline characters as whitespace when whitespace mode is on', () => {
const newlines = ['\n', '\r\n', '\r', '\f'];
newlines.forEach((line) => {
const token = tokenize(line, false, CssLexerMode.ALL_TRACK_WS)[0];
expect(token.type).toEqual(CssTokenType.Whitespace);
});
});
it('should combined newline characters as one newline token when whitespace mode is on', () => {
const newlines = ['\n', '\r\n', '\r', '\f'].join('');
const tokens = tokenize(newlines, false, CssLexerMode.ALL_TRACK_WS);
expect(tokens.length).toEqual(1);
expect(tokens[0].type).toEqual(CssTokenType.Whitespace);
});
it('should not consider whitespace or newline values at all when whitespace mode is off',
() => {
const newlines = ['\n', '\r\n', '\r', '\f'].join('');
const tokens = tokenize(newlines);
expect(tokens.length).toEqual(0);
});
it('should lex simple selectors and their inner properties', () => {
const cssCode = '\n' +
' .selector { my-prop: my-value; }\n';
const tokens = tokenize(cssCode);
expect(tokens[0].type).toEqual(CssTokenType.Character);
expect(tokens[0].strValue).toEqual('.');
expect(tokens[1].type).toEqual(CssTokenType.Identifier);
expect(tokens[1].strValue).toEqual('selector');
expect(tokens[2].type).toEqual(CssTokenType.Character);
expect(tokens[2].strValue).toEqual('{');
expect(tokens[3].type).toEqual(CssTokenType.Identifier);
expect(tokens[3].strValue).toEqual('my-prop');
expect(tokens[4].type).toEqual(CssTokenType.Character);
expect(tokens[4].strValue).toEqual(':');
expect(tokens[5].type).toEqual(CssTokenType.Identifier);
expect(tokens[5].strValue).toEqual('my-value');
expect(tokens[6].type).toEqual(CssTokenType.Character);
expect(tokens[6].strValue).toEqual(';');
expect(tokens[7].type).toEqual(CssTokenType.Character);
expect(tokens[7].strValue).toEqual('}');
});
it('should capture the column and line values for each token', () => {
const cssCode = '#id {\n' +
' prop:value;\n' +
'}';
const tokens = tokenize(cssCode);
// #
expect(tokens[0].type).toEqual(CssTokenType.Character);
expect(tokens[0].column).toEqual(0);
expect(tokens[0].line).toEqual(0);
// id
expect(tokens[1].type).toEqual(CssTokenType.Identifier);
expect(tokens[1].column).toEqual(1);
expect(tokens[1].line).toEqual(0);
// {
expect(tokens[2].type).toEqual(CssTokenType.Character);
expect(tokens[2].column).toEqual(4);
expect(tokens[2].line).toEqual(0);
// prop
expect(tokens[3].type).toEqual(CssTokenType.Identifier);
expect(tokens[3].column).toEqual(2);
expect(tokens[3].line).toEqual(1);
// :
expect(tokens[4].type).toEqual(CssTokenType.Character);
expect(tokens[4].column).toEqual(6);
expect(tokens[4].line).toEqual(1);
// value
expect(tokens[5].type).toEqual(CssTokenType.Identifier);
expect(tokens[5].column).toEqual(7);
expect(tokens[5].line).toEqual(1);
// ;
expect(tokens[6].type).toEqual(CssTokenType.Character);
expect(tokens[6].column).toEqual(12);
expect(tokens[6].line).toEqual(1);
// }
expect(tokens[7].type).toEqual(CssTokenType.Character);
expect(tokens[7].column).toEqual(0);
expect(tokens[7].line).toEqual(2);
});
it('should lex quoted strings and escape accordingly', () => {
const cssCode = 'prop: \'some { value } \\\' that is quoted\'';
const tokens = tokenize(cssCode);
expect(tokens[0].type).toEqual(CssTokenType.Identifier);
expect(tokens[1].type).toEqual(CssTokenType.Character);
expect(tokens[2].type).toEqual(CssTokenType.String);
expect(tokens[2].strValue).toEqual('\'some { value } \\\' that is quoted\'');
});
it('should treat attribute operators as regular characters', () => {
tokenize('^|~+*').forEach((token) => { expect(token.type).toEqual(CssTokenType.Character); });
});
it('should lex numbers properly and set them as numbers', () => {
const cssCode = '0 1 -2 3.0 -4.001';
const tokens = tokenize(cssCode);
expect(tokens[0].type).toEqual(CssTokenType.Number);
expect(tokens[0].strValue).toEqual('0');
expect(tokens[1].type).toEqual(CssTokenType.Number);
expect(tokens[1].strValue).toEqual('1');
expect(tokens[2].type).toEqual(CssTokenType.Number);
expect(tokens[2].strValue).toEqual('-2');
expect(tokens[3].type).toEqual(CssTokenType.Number);
expect(tokens[3].strValue).toEqual('3.0');
expect(tokens[4].type).toEqual(CssTokenType.Number);
expect(tokens[4].strValue).toEqual('-4.001');
});
it('should lex @keywords', () => {
const cssCode = '@import()@something';
const tokens = tokenize(cssCode);
expect(tokens[0].type).toEqual(CssTokenType.AtKeyword);
expect(tokens[0].strValue).toEqual('@import');
expect(tokens[1].type).toEqual(CssTokenType.Character);
expect(tokens[1].strValue).toEqual('(');
expect(tokens[2].type).toEqual(CssTokenType.Character);
expect(tokens[2].strValue).toEqual(')');
expect(tokens[3].type).toEqual(CssTokenType.AtKeyword);
expect(tokens[3].strValue).toEqual('@something');
});
it('should still lex a number even if it has a dimension suffix', () => {
const cssCode = '40% is 40 percent';
const tokens = tokenize(cssCode);
expect(tokens[0].type).toEqual(CssTokenType.Number);
expect(tokens[0].strValue).toEqual('40');
expect(tokens[1].type).toEqual(CssTokenType.Character);
expect(tokens[1].strValue).toEqual('%');
expect(tokens[2].type).toEqual(CssTokenType.Identifier);
expect(tokens[2].strValue).toEqual('is');
expect(tokens[3].type).toEqual(CssTokenType.Number);
expect(tokens[3].strValue).toEqual('40');
});
it('should allow escaped character and unicode character-strings in CSS selectors', () => {
const cssCode = '\\123456 .some\\thing \{\}';
const tokens = tokenize(cssCode);
expect(tokens[0].type).toEqual(CssTokenType.Identifier);
expect(tokens[0].strValue).toEqual('\\123456');
expect(tokens[1].type).toEqual(CssTokenType.Character);
expect(tokens[2].type).toEqual(CssTokenType.Identifier);
expect(tokens[2].strValue).toEqual('some\\thing');
});
it('should distinguish identifiers and numbers from special characters', () => {
const cssCode = 'one*two=-4+three-4-equals_value$';
const tokens = tokenize(cssCode);
expect(tokens[0].type).toEqual(CssTokenType.Identifier);
expect(tokens[0].strValue).toEqual('one');
expect(tokens[1].type).toEqual(CssTokenType.Character);
expect(tokens[1].strValue).toEqual('*');
expect(tokens[2].type).toEqual(CssTokenType.Identifier);
expect(tokens[2].strValue).toEqual('two');
expect(tokens[3].type).toEqual(CssTokenType.Character);
expect(tokens[3].strValue).toEqual('=');
expect(tokens[4].type).toEqual(CssTokenType.Number);
expect(tokens[4].strValue).toEqual('-4');
expect(tokens[5].type).toEqual(CssTokenType.Character);
expect(tokens[5].strValue).toEqual('+');
expect(tokens[6].type).toEqual(CssTokenType.Identifier);
expect(tokens[6].strValue).toEqual('three-4-equals_value');
expect(tokens[7].type).toEqual(CssTokenType.Character);
expect(tokens[7].strValue).toEqual('$');
});
it('should filter out comments and whitespace by default', () => {
const cssCode = '.selector /* comment */ { /* value */ }';
const tokens = tokenize(cssCode);
expect(tokens[0].strValue).toEqual('.');
expect(tokens[1].strValue).toEqual('selector');
expect(tokens[2].strValue).toEqual('{');
expect(tokens[3].strValue).toEqual('}');
});
it('should track comments when the flag is set to true', () => {
const cssCode = '.selector /* comment */ { /* value */ }';
const trackComments = true;
const tokens = tokenize(cssCode, trackComments, CssLexerMode.ALL_TRACK_WS);
expect(tokens[0].strValue).toEqual('.');
expect(tokens[1].strValue).toEqual('selector');
expect(tokens[2].strValue).toEqual(' ');
expect(tokens[3].type).toEqual(CssTokenType.Comment);
expect(tokens[3].strValue).toEqual('/* comment */');
expect(tokens[4].strValue).toEqual(' ');
expect(tokens[5].strValue).toEqual('{');
expect(tokens[6].strValue).toEqual(' ');
expect(tokens[7].type).toEqual(CssTokenType.Comment);
expect(tokens[7].strValue).toEqual('/* value */');
});
describe('Selector Mode', () => {
it('should throw an error if a selector is being parsed while in the wrong mode', () => {
const cssCode = '.class > tag';
let capturedMessage: string;
try {
tokenize(cssCode, false, CssLexerMode.STYLE_BLOCK);
} catch (e) {
capturedMessage = getRawMessage(e);
}
expect(capturedMessage).toMatch(/Unexpected character \[\>\] at column 0:7 in expression/g);
capturedMessage = null;
try {
tokenize(cssCode, false, CssLexerMode.SELECTOR);
} catch (e) {
capturedMessage = getRawMessage(e);
}
expect(capturedMessage).toEqual(null);
});
});
describe('Attribute Mode', () => {
it('should consider attribute selectors as valid input and throw when an invalid modifier is used',
() => {
function tokenizeAttr(modifier: string) {
const cssCode = 'value' + modifier + '=\'something\'';
return tokenize(cssCode, false, CssLexerMode.ATTRIBUTE_SELECTOR);
}
expect(tokenizeAttr('*').length).toEqual(4);
expect(tokenizeAttr('|').length).toEqual(4);
expect(tokenizeAttr('^').length).toEqual(4);
expect(tokenizeAttr('$').length).toEqual(4);
expect(tokenizeAttr('~').length).toEqual(4);
expect(tokenizeAttr('').length).toEqual(3);
expect(() => { tokenizeAttr('+'); }).toThrow();
});
});
describe('Media Query Mode', () => {
it('should validate media queries with a reduced subset of valid characters', () => {
function tokenizeQuery(code: string) {
return tokenize(code, false, CssLexerMode.MEDIA_QUERY);
}
// the reason why the numbers are so high is because MediaQueries keep
// track of the whitespace values
expect(tokenizeQuery('(prop: value)').length).toEqual(5);
expect(tokenizeQuery('(prop: value) and (prop2: value2)').length).toEqual(11);
expect(tokenizeQuery('tv and (prop: value)').length).toEqual(7);
expect(tokenizeQuery('print and ((prop: value) or (prop2: value2))').length).toEqual(15);
expect(tokenizeQuery('(content: \'something $ crazy inside &\')').length).toEqual(5);
expect(() => { tokenizeQuery('(max-height: 10 + 20)'); }).toThrow();
expect(() => { tokenizeQuery('(max-height: fifty < 100)'); }).toThrow();
});
});
describe('Pseudo Selector Mode', () => {
it('should validate pseudo selector identifiers with a reduced subset of valid characters',
() => {
function tokenizePseudo(code: string, withArgs = false): CssToken[] {
const mode = withArgs ? CssLexerMode.PSEUDO_SELECTOR_WITH_ARGUMENTS :
CssLexerMode.PSEUDO_SELECTOR;
return tokenize(code, false, mode);
}
expect(tokenizePseudo('hover').length).toEqual(1);
expect(tokenizePseudo('focus').length).toEqual(1);
expect(tokenizePseudo('lang(en-us)', true).length).toEqual(4);
expect(() => { tokenizePseudo('lang(something:broken)', true); }).toThrow();
expect(() => { tokenizePseudo('not(.selector)', true); }).toThrow();
});
});
describe(
'Style Block Mode', () => {
it('should style blocks with a reduced subset of valid characters',
() => {
function tokenizeStyles(code: string) {
return tokenize(code, false, CssLexerMode.STYLE_BLOCK);
}
expect(tokenizeStyles(`
key: value;
prop: 100;
style: value3!important;
`).length).toEqual(14);
expect(() => tokenizeStyles(` key$: value; `)).toThrow();
expect(() => tokenizeStyles(` key: value$; `)).toThrow();
expect(() => tokenizeStyles(` key: value + 10; `)).toThrow();
expect(() => tokenizeStyles(` key: &value; `)).toThrow();
});
});
});
}

View File

@ -0,0 +1,802 @@
/**
* @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 {describe, expect, it} from '../../../core/testing/testing_internal';
import {CssBlockAst, CssBlockDefinitionRuleAst, CssBlockRuleAst, CssDefinitionAst, CssInlineRuleAst, CssKeyframeDefinitionAst, CssKeyframeRuleAst, CssMediaQueryRuleAst, CssSelectorRuleAst, CssStyleSheetAst, CssStyleValueAst} from '../../src/css_parser/css_ast';
import {BlockType, CssParseError, CssParser, CssToken, ParsedCssResult} from '../../src/css_parser/css_parser';
import {ParseLocation} from '../../src/parse_util';
export function assertTokens(tokens: CssToken[], valuesArr: string[]) {
for (let i = 0; i < tokens.length; i++) {
expect(tokens[i].strValue == valuesArr[i]);
}
}
export function main() {
describe('CssParser', () => {
function parse(css: string): ParsedCssResult {
return new CssParser().parse(css, 'some-fake-css-file.css');
}
function makeAst(css: string): CssStyleSheetAst {
const output = parse(css);
const errors = output.errors;
if (errors.length > 0) {
throw new Error(errors.map((error: CssParseError) => error.msg).join(', '));
}
return output.ast;
}
it('should parse CSS into a stylesheet Ast', () => {
const styles = '.selector { prop: value123; }';
const ast = makeAst(styles);
expect(ast.rules.length).toEqual(1);
const rule = <CssSelectorRuleAst>ast.rules[0];
const selector = rule.selectors[0];
expect(selector.strValue).toEqual('.selector');
const block: CssBlockAst = rule.block;
expect(block.entries.length).toEqual(1);
const definition = <CssDefinitionAst>block.entries[0];
expect(definition.property.strValue).toEqual('prop');
const value = <CssStyleValueAst>definition.value;
expect(value.tokens[0].strValue).toEqual('value123');
});
it('should parse multiple CSS selectors sharing the same set of styles', () => {
const styles = `
.class, #id, tag, [attr], key + value, * value, :-moz-any-link {
prop: value123;
}
`;
const ast = makeAst(styles);
expect(ast.rules.length).toEqual(1);
const rule = <CssSelectorRuleAst>ast.rules[0];
expect(rule.selectors.length).toBe(7);
const classRule = rule.selectors[0];
const idRule = rule.selectors[1];
const tagRule = rule.selectors[2];
const attrRule = rule.selectors[3];
const plusOpRule = rule.selectors[4];
const starOpRule = rule.selectors[5];
const mozRule = rule.selectors[6];
assertTokens(classRule.selectorParts[0].tokens, ['.', 'class']);
assertTokens(idRule.selectorParts[0].tokens, ['.', 'class']);
assertTokens(attrRule.selectorParts[0].tokens, ['[', 'attr', ']']);
assertTokens(plusOpRule.selectorParts[0].tokens, ['key']);
expect(plusOpRule.selectorParts[0].operator.strValue).toEqual('+');
assertTokens(plusOpRule.selectorParts[1].tokens, ['value']);
assertTokens(starOpRule.selectorParts[0].tokens, ['*']);
assertTokens(starOpRule.selectorParts[1].tokens, ['value']);
assertTokens(mozRule.selectorParts[0].pseudoSelectors[0].tokens, [':', '-moz-any-link']);
const style1 = <CssDefinitionAst>rule.block.entries[0];
expect(style1.property.strValue).toEqual('prop');
assertTokens(style1.value.tokens, ['value123']);
});
it('should parse keyframe rules', () => {
const styles = `
@keyframes rotateMe {
from {
transform: rotate(-360deg);
}
50% {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
`;
const ast = makeAst(styles);
expect(ast.rules.length).toEqual(1);
const rule = <CssKeyframeRuleAst>ast.rules[0];
expect(rule.name.strValue).toEqual('rotateMe');
const block = <CssBlockAst>rule.block;
const fromRule = <CssKeyframeDefinitionAst>block.entries[0];
expect(fromRule.name.strValue).toEqual('from');
const fromStyle = <CssDefinitionAst>(<CssBlockAst>fromRule.block).entries[0];
expect(fromStyle.property.strValue).toEqual('transform');
assertTokens(fromStyle.value.tokens, ['rotate', '(', '-360', 'deg', ')']);
const midRule = <CssKeyframeDefinitionAst>block.entries[1];
expect(midRule.name.strValue).toEqual('50%');
const midStyle = <CssDefinitionAst>(<CssBlockAst>midRule.block).entries[0];
expect(midStyle.property.strValue).toEqual('transform');
assertTokens(midStyle.value.tokens, ['rotate', '(', '0', 'deg', ')']);
const toRule = <CssKeyframeDefinitionAst>block.entries[2];
expect(toRule.name.strValue).toEqual('to');
const toStyle = <CssDefinitionAst>(<CssBlockAst>toRule.block).entries[0];
expect(toStyle.property.strValue).toEqual('transform');
assertTokens(toStyle.value.tokens, ['rotate', '(', '360', 'deg', ')']);
});
it('should parse media queries into a stylesheet Ast', () => {
const styles = `
@media all and (max-width:100px) {
.selector {
prop: value123;
}
}
`;
const ast = makeAst(styles);
expect(ast.rules.length).toEqual(1);
const rule = <CssMediaQueryRuleAst>ast.rules[0];
assertTokens(rule.query.tokens, ['all', 'and', '(', 'max-width', ':', '100', 'px', ')']);
const block = <CssBlockAst>rule.block;
expect(block.entries.length).toEqual(1);
const rule2 = <CssSelectorRuleAst>block.entries[0];
expect(rule2.selectors[0].strValue).toEqual('.selector');
const block2 = <CssBlockAst>rule2.block;
expect(block2.entries.length).toEqual(1);
});
it('should parse inline CSS values', () => {
const styles = `
@import url('remote.css');
@charset "UTF-8";
@namespace ng url(http://angular.io/namespace/ng);
`;
const ast = makeAst(styles);
const importRule = <CssInlineRuleAst>ast.rules[0];
expect(importRule.type).toEqual(BlockType.Import);
assertTokens(importRule.value.tokens, ['url', '(', 'remote', '.', 'css', ')']);
const charsetRule = <CssInlineRuleAst>ast.rules[1];
expect(charsetRule.type).toEqual(BlockType.Charset);
assertTokens(charsetRule.value.tokens, ['UTF-8']);
const namespaceRule = <CssInlineRuleAst>ast.rules[2];
expect(namespaceRule.type).toEqual(BlockType.Namespace);
assertTokens(
namespaceRule.value.tokens, ['ng', 'url', '(', 'http://angular.io/namespace/ng', ')']);
});
it('should parse CSS values that contain functions and leave the inner function data untokenized',
() => {
const styles = `
.class {
background: url(matias.css);
animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
height: calc(100% - 50px);
background-image: linear-gradient( 45deg, rgba(100, 0, 0, 0.5), black );
}
`;
const ast = makeAst(styles);
expect(ast.rules.length).toEqual(1);
const defs = (<CssSelectorRuleAst>ast.rules[0]).block.entries;
expect(defs.length).toEqual(4);
assertTokens((<CssDefinitionAst>defs[0]).value.tokens, ['url', '(', 'matias.css', ')']);
assertTokens(
(<CssDefinitionAst>defs[1]).value.tokens,
['cubic-bezier', '(', '0.755, 0.050, 0.855, 0.060', ')']);
assertTokens((<CssDefinitionAst>defs[2]).value.tokens, ['calc', '(', '100% - 50px', ')']);
assertTokens(
(<CssDefinitionAst>defs[3]).value.tokens,
['linear-gradient', '(', '45deg, rgba(100, 0, 0, 0.5), black', ')']);
});
it('should parse un-named block-level CSS values', () => {
const styles = `
@font-face {
font-family: "Matias";
font-weight: bold;
src: url(font-face.ttf);
}
@viewport {
max-width: 100px;
min-height: 1000px;
}
`;
const ast = makeAst(styles);
const fontFaceRule = <CssBlockRuleAst>ast.rules[0];
expect(fontFaceRule.type).toEqual(BlockType.FontFace);
expect(fontFaceRule.block.entries.length).toEqual(3);
const viewportRule = <CssBlockRuleAst>ast.rules[1];
expect(viewportRule.type).toEqual(BlockType.Viewport);
expect(viewportRule.block.entries.length).toEqual(2);
});
it('should parse multiple levels of semicolons', () => {
const styles = `
;;;
@import url('something something')
;;;;;;;;
;;;;;;;;
;@font-face {
;src : url(font-face.ttf);;;;;;;;
;;;-webkit-animation:my-animation
};;;
@media all and (max-width:100px)
{;
.selector {prop: value123;};
;.selector2{prop:1}}
`;
const ast = makeAst(styles);
const importRule = <CssInlineRuleAst>ast.rules[0];
expect(importRule.type).toEqual(BlockType.Import);
assertTokens(importRule.value.tokens, ['url', '(', 'something something', ')']);
const fontFaceRule = <CssBlockRuleAst>ast.rules[1];
expect(fontFaceRule.type).toEqual(BlockType.FontFace);
expect(fontFaceRule.block.entries.length).toEqual(2);
const mediaQueryRule = <CssMediaQueryRuleAst>ast.rules[2];
assertTokens(
mediaQueryRule.query.tokens, ['all', 'and', '(', 'max-width', ':', '100', 'px', ')']);
expect(mediaQueryRule.block.entries.length).toEqual(2);
});
it('should throw an error if an unknown @value block rule is parsed', () => {
const styles = `
@matias { hello: there; }
`;
expect(() => {
makeAst(styles);
}).toThrowError(/^CSS Parse Error: The CSS "at" rule "@matias" is not allowed to used here/g);
});
it('should parse empty rules', () => {
const styles = `
.empty-rule { }
.somewhat-empty-rule { /* property: value; */ }
.non-empty-rule { property: value; }
`;
const ast = makeAst(styles);
const rules = ast.rules;
expect((<CssSelectorRuleAst>rules[0]).block.entries.length).toEqual(0);
expect((<CssSelectorRuleAst>rules[1]).block.entries.length).toEqual(0);
expect((<CssSelectorRuleAst>rules[2]).block.entries.length).toEqual(1);
});
it('should parse the @document rule', () => {
const styles = `
@document url(http://www.w3.org/),
url-prefix(http://www.w3.org/Style/),
domain(mozilla.org),
regexp("https:.*")
{
/* CSS rules here apply to:
- The page "http://www.w3.org/".
- Any page whose URL begins with "http://www.w3.org/Style/"
- Any page whose URL's host is "mozilla.org" or ends with
".mozilla.org"
- Any page whose URL starts with "https:" */
/* make the above-mentioned pages really ugly */
body {
color: purple;
background: yellow;
}
}
`;
const ast = makeAst(styles);
const rules = ast.rules;
const documentRule = <CssBlockDefinitionRuleAst>rules[0];
expect(documentRule.type).toEqual(BlockType.Document);
const rule = <CssSelectorRuleAst>documentRule.block.entries[0];
expect(rule.strValue).toEqual('body');
});
it('should parse the @page rule', () => {
const styles = `
@page one {
.selector { prop: value; }
}
@page two {
.selector2 { prop: value2; }
}
`;
const ast = makeAst(styles);
const rules = ast.rules;
const pageRule1 = <CssBlockDefinitionRuleAst>rules[0];
expect(pageRule1.query.strValue).toEqual('@page one');
expect(pageRule1.query.tokens[0].strValue).toEqual('one');
expect(pageRule1.type).toEqual(BlockType.Page);
const pageRule2 = <CssBlockDefinitionRuleAst>rules[1];
expect(pageRule2.query.strValue).toEqual('@page two');
expect(pageRule2.query.tokens[0].strValue).toEqual('two');
expect(pageRule2.type).toEqual(BlockType.Page);
const selectorOne = <CssSelectorRuleAst>pageRule1.block.entries[0];
expect(selectorOne.strValue).toEqual('.selector');
const selectorTwo = <CssSelectorRuleAst>pageRule2.block.entries[0];
expect(selectorTwo.strValue).toEqual('.selector2');
});
it('should parse the @supports rule', () => {
const styles = `
@supports (animation-name: "rotate") {
a:hover { animation: rotate 1s; }
}
`;
const ast = makeAst(styles);
const rules = ast.rules;
const supportsRule = <CssBlockDefinitionRuleAst>rules[0];
assertTokens(supportsRule.query.tokens, ['(', 'animation-name', ':', 'rotate', ')']);
expect(supportsRule.type).toEqual(BlockType.Supports);
const selectorOne = <CssSelectorRuleAst>supportsRule.block.entries[0];
expect(selectorOne.strValue).toEqual('a:hover');
});
it('should collect multiple errors during parsing', () => {
const styles = `
.class$value { something: something }
@custom { something: something }
#id { cool^: value }
`;
const output = parse(styles);
expect(output.errors.length).toEqual(3);
});
it('should recover from selector errors and continue parsing', () => {
const styles = `
tag& { key: value; }
.%tag { key: value; }
#tag$ { key: value; }
`;
const output = parse(styles);
const errors = output.errors;
const ast = output.ast;
expect(errors.length).toEqual(3);
expect(ast.rules.length).toEqual(3);
const rule1 = <CssSelectorRuleAst>ast.rules[0];
expect(rule1.selectors[0].strValue).toEqual('tag&');
expect(rule1.block.entries.length).toEqual(1);
const rule2 = <CssSelectorRuleAst>ast.rules[1];
expect(rule2.selectors[0].strValue).toEqual('.%tag');
expect(rule2.block.entries.length).toEqual(1);
const rule3 = <CssSelectorRuleAst>ast.rules[2];
expect(rule3.selectors[0].strValue).toEqual('#tag$');
expect(rule3.block.entries.length).toEqual(1);
});
it('should throw an error when parsing invalid CSS Selectors', () => {
const styles = '.class[[prop%=value}] { style: val; }';
const output = parse(styles);
const errors = output.errors;
expect(errors.length).toEqual(3);
expect(errors[0].msg).toMatch(/Unexpected character \[\[\] at column 0:7/g);
expect(errors[1].msg).toMatch(/Unexpected character \[%\] at column 0:12/g);
expect(errors[2].msg).toMatch(/Unexpected character \[}\] at column 0:19/g);
});
it('should throw an error if an attribute selector is not closed properly', () => {
const styles = '.class[prop=value { style: val; }';
const output = parse(styles);
const errors = output.errors;
expect(errors[0].msg).toMatch(/Unbalanced CSS attribute selector at column 0:12/g);
});
it('should throw an error if a pseudo function selector is not closed properly', () => {
const styles = 'body:lang(en { key:value; }';
const output = parse(styles);
const errors = output.errors;
expect(errors[0].msg)
.toMatch(/Character does not match expected Character value \("{" should match "\)"\)/);
});
it('should raise an error when a semi colon is missing from a CSS style/pair that isn\'t the last entry',
() => {
const styles = `.class {
color: red
background: blue
}`;
const output = parse(styles);
const errors = output.errors;
expect(errors.length).toEqual(1);
expect(errors[0].msg)
.toMatch(/The CSS key\/value definition did not end with a semicolon at column 1:15/g);
});
it('should parse the inner value of a :not() pseudo-selector as a CSS selector', () => {
const styles = `div:not(.ignore-this-div) {
prop: value;
}`;
const output = parse(styles);
const errors = output.errors;
const ast = output.ast;
expect(errors.length).toEqual(0);
const rule1 = <CssSelectorRuleAst>ast.rules[0];
expect(rule1.selectors.length).toEqual(1);
const simpleSelector = rule1.selectors[0].selectorParts[0];
assertTokens(simpleSelector.tokens, ['div']);
const pseudoSelector = simpleSelector.pseudoSelectors[0];
expect(pseudoSelector.name).toEqual('not');
assertTokens(pseudoSelector.tokens, ['.', 'ignore-this-div']);
});
it('should parse the inner selectors of a :host-context selector', () => {
const styles = `body > :host-context(.a, .b, .c:hover) {
prop: value;
}`;
const output = parse(styles);
const errors = output.errors;
const ast = output.ast;
expect(errors.length).toEqual(0);
const rule1 = <CssSelectorRuleAst>ast.rules[0];
expect(rule1.selectors.length).toEqual(1);
const simpleSelector = rule1.selectors[0].selectorParts[1];
const innerSelectors = simpleSelector.pseudoSelectors[0].innerSelectors;
assertTokens(innerSelectors[0].selectorParts[0].tokens, ['.', 'a']);
assertTokens(innerSelectors[1].selectorParts[0].tokens, ['.', 'b']);
const finalSelector = innerSelectors[2].selectorParts[0];
assertTokens(finalSelector.tokens, ['.', 'c', ':', 'hover']);
assertTokens(finalSelector.pseudoSelectors[0].tokens, [':', 'hover']);
});
it('should raise parse errors when CSS key/value pairs are invalid', () => {
const styles = `.class {
background color: value;
color: value
font-size;
font-weight
}`;
const output = parse(styles);
const errors = output.errors;
expect(errors.length).toEqual(4);
expect(errors[0].msg)
.toMatch(
/Identifier does not match expected Character value \("color" should match ":"\) at column 1:19/g);
expect(errors[1].msg)
.toMatch(/The CSS key\/value definition did not end with a semicolon at column 2:15/g);
expect(errors[2].msg)
.toMatch(/The CSS property was not paired with a style value at column 3:8/g);
expect(errors[3].msg)
.toMatch(/The CSS property was not paired with a style value at column 4:8/g);
});
it('should recover from CSS key/value parse errors', () => {
const styles = `
.problem-class { background color: red; color: white; }
.good-boy-class { background-color: red; color: white; }
`;
const output = parse(styles);
const ast = output.ast;
expect(ast.rules.length).toEqual(2);
const rule1 = <CssSelectorRuleAst>ast.rules[0];
expect(rule1.block.entries.length).toEqual(2);
const style1 = <CssDefinitionAst>rule1.block.entries[0];
expect(style1.property.strValue).toEqual('background color');
assertTokens(style1.value.tokens, ['red']);
const style2 = <CssDefinitionAst>rule1.block.entries[1];
expect(style2.property.strValue).toEqual('color');
assertTokens(style2.value.tokens, ['white']);
});
describe('location offsets', () => {
let styles: string;
function assertMatchesOffsetAndChar(
location: ParseLocation, expectedOffset: number, expectedChar: string): void {
expect(location.offset).toEqual(expectedOffset);
expect(styles[expectedOffset]).toEqual(expectedChar);
}
it('should collect the source span location of each AST node with regular selectors', () => {
styles = '.problem-class { border-top-right: 1px; color: white; }\n';
styles += '#good-boy-rule_ { background-color: #fe4; color: teal; }';
const output = parse(styles);
const ast = output.ast;
assertMatchesOffsetAndChar(ast.location.start, 0, '.');
assertMatchesOffsetAndChar(ast.location.end, 111, '}');
const rule1 = <CssSelectorRuleAst>ast.rules[0];
assertMatchesOffsetAndChar(rule1.location.start, 0, '.');
assertMatchesOffsetAndChar(rule1.location.end, 54, '}');
const rule2 = <CssSelectorRuleAst>ast.rules[1];
assertMatchesOffsetAndChar(rule2.location.start, 56, '#');
assertMatchesOffsetAndChar(rule2.location.end, 111, '}');
const selector1 = rule1.selectors[0];
assertMatchesOffsetAndChar(selector1.location.start, 0, '.');
assertMatchesOffsetAndChar(selector1.location.end, 1, 'p'); // problem-class
const selector2 = rule2.selectors[0];
assertMatchesOffsetAndChar(selector2.location.start, 56, '#');
assertMatchesOffsetAndChar(selector2.location.end, 57, 'g'); // good-boy-rule_
const block1 = rule1.block;
assertMatchesOffsetAndChar(block1.location.start, 15, '{');
assertMatchesOffsetAndChar(block1.location.end, 54, '}');
const block2 = rule2.block;
assertMatchesOffsetAndChar(block2.location.start, 72, '{');
assertMatchesOffsetAndChar(block2.location.end, 111, '}');
const block1def1 = <CssDefinitionAst>block1.entries[0];
assertMatchesOffsetAndChar(block1def1.location.start, 17, 'b'); // border-top-right
assertMatchesOffsetAndChar(block1def1.location.end, 36, 'p'); // px
const block1def2 = <CssDefinitionAst>block1.entries[1];
assertMatchesOffsetAndChar(block1def2.location.start, 40, 'c'); // color
assertMatchesOffsetAndChar(block1def2.location.end, 47, 'w'); // white
const block2def1 = <CssDefinitionAst>block2.entries[0];
assertMatchesOffsetAndChar(block2def1.location.start, 74, 'b'); // background-color
assertMatchesOffsetAndChar(block2def1.location.end, 93, 'f'); // fe4
const block2def2 = <CssDefinitionAst>block2.entries[1];
assertMatchesOffsetAndChar(block2def2.location.start, 98, 'c'); // color
assertMatchesOffsetAndChar(block2def2.location.end, 105, 't'); // teal
const block1value1 = block1def1.value;
assertMatchesOffsetAndChar(block1value1.location.start, 35, '1');
assertMatchesOffsetAndChar(block1value1.location.end, 36, 'p');
const block1value2 = block1def2.value;
assertMatchesOffsetAndChar(block1value2.location.start, 47, 'w');
assertMatchesOffsetAndChar(block1value2.location.end, 47, 'w');
const block2value1 = block2def1.value;
assertMatchesOffsetAndChar(block2value1.location.start, 92, '#');
assertMatchesOffsetAndChar(block2value1.location.end, 93, 'f');
const block2value2 = block2def2.value;
assertMatchesOffsetAndChar(block2value2.location.start, 105, 't');
assertMatchesOffsetAndChar(block2value2.location.end, 105, 't');
});
it('should collect the source span location of each AST node with media query data', () => {
styles = '@media (all and max-width: 100px) { a { display:none; } }';
const output = parse(styles);
const ast = output.ast;
const mediaQuery = <CssMediaQueryRuleAst>ast.rules[0];
assertMatchesOffsetAndChar(mediaQuery.location.start, 0, '@');
assertMatchesOffsetAndChar(mediaQuery.location.end, 56, '}');
const predicate = mediaQuery.query;
assertMatchesOffsetAndChar(predicate.location.start, 0, '@');
assertMatchesOffsetAndChar(predicate.location.end, 32, ')');
const rule = <CssSelectorRuleAst>mediaQuery.block.entries[0];
assertMatchesOffsetAndChar(rule.location.start, 36, 'a');
assertMatchesOffsetAndChar(rule.location.end, 54, '}');
});
it('should collect the source span location of each AST node with keyframe data', () => {
styles = '@keyframes rotateAndZoomOut { ';
styles += 'from { transform: rotate(0deg); } ';
styles += '100% { transform: rotate(360deg) scale(2); }';
styles += '}';
const output = parse(styles);
const ast = output.ast;
const keyframes = <CssKeyframeRuleAst>ast.rules[0];
assertMatchesOffsetAndChar(keyframes.location.start, 0, '@');
assertMatchesOffsetAndChar(keyframes.location.end, 108, '}');
const step1 = <CssKeyframeDefinitionAst>keyframes.block.entries[0];
assertMatchesOffsetAndChar(step1.location.start, 30, 'f');
assertMatchesOffsetAndChar(step1.location.end, 62, '}');
const step2 = <CssKeyframeDefinitionAst>keyframes.block.entries[1];
assertMatchesOffsetAndChar(step2.location.start, 64, '1');
assertMatchesOffsetAndChar(step2.location.end, 107, '}');
});
it('should collect the source span location of each AST node with an inline rule', () => {
styles = '@import url(something.css)';
const output = parse(styles);
const ast = output.ast;
const rule = <CssInlineRuleAst>ast.rules[0];
assertMatchesOffsetAndChar(rule.location.start, 0, '@');
assertMatchesOffsetAndChar(rule.location.end, 25, ')');
const value = rule.value;
assertMatchesOffsetAndChar(value.location.start, 8, 'u');
assertMatchesOffsetAndChar(value.location.end, 25, ')');
});
it('should property collect the start/end locations with an invalid stylesheet', () => {
styles = '#id { something: value';
const output = parse(styles);
const ast = output.ast;
assertMatchesOffsetAndChar(ast.location.start, 0, '#');
assertMatchesOffsetAndChar(ast.location.end, 22, undefined);
});
});
it('should parse minified CSS content properly', () => {
// this code was taken from the angular.io webpage's CSS code
const styles = `
.is-hidden{display:none!important}
.is-visible{display:block!important}
.is-visually-hidden{height:1px;width:1px;overflow:hidden;opacity:0.01;position:absolute;bottom:0;right:0;z-index:1}
.grid-fluid,.grid-fixed{margin:0 auto}
.grid-fluid .c1,.grid-fixed .c1,.grid-fluid .c2,.grid-fixed .c2,.grid-fluid .c3,.grid-fixed .c3,.grid-fluid .c4,.grid-fixed .c4,.grid-fluid .c5,.grid-fixed .c5,.grid-fluid .c6,.grid-fixed .c6,.grid-fluid .c7,.grid-fixed .c7,.grid-fluid .c8,.grid-fixed .c8,.grid-fluid .c9,.grid-fixed .c9,.grid-fluid .c10,.grid-fixed .c10,.grid-fluid .c11,.grid-fixed .c11,.grid-fluid .c12,.grid-fixed .c12{display:inline;float:left}
.grid-fluid .c1.grid-right,.grid-fixed .c1.grid-right,.grid-fluid .c2.grid-right,.grid-fixed .c2.grid-right,.grid-fluid .c3.grid-right,.grid-fixed .c3.grid-right,.grid-fluid .c4.grid-right,.grid-fixed .c4.grid-right,.grid-fluid .c5.grid-right,.grid-fixed .c5.grid-right,.grid-fluid .c6.grid-right,.grid-fixed .c6.grid-right,.grid-fluid .c7.grid-right,.grid-fixed .c7.grid-right,.grid-fluid .c8.grid-right,.grid-fixed .c8.grid-right,.grid-fluid .c9.grid-right,.grid-fixed .c9.grid-right,.grid-fluid .c10.grid-right,.grid-fixed .c10.grid-right,.grid-fluid .c11.grid-right,.grid-fixed .c11.grid-right,.grid-fluid .c12.grid-right,.grid-fixed .c12.grid-right{float:right}
.grid-fluid .c1.nb,.grid-fixed .c1.nb,.grid-fluid .c2.nb,.grid-fixed .c2.nb,.grid-fluid .c3.nb,.grid-fixed .c3.nb,.grid-fluid .c4.nb,.grid-fixed .c4.nb,.grid-fluid .c5.nb,.grid-fixed .c5.nb,.grid-fluid .c6.nb,.grid-fixed .c6.nb,.grid-fluid .c7.nb,.grid-fixed .c7.nb,.grid-fluid .c8.nb,.grid-fixed .c8.nb,.grid-fluid .c9.nb,.grid-fixed .c9.nb,.grid-fluid .c10.nb,.grid-fixed .c10.nb,.grid-fluid .c11.nb,.grid-fixed .c11.nb,.grid-fluid .c12.nb,.grid-fixed .c12.nb{margin-left:0}
.grid-fluid .c1.na,.grid-fixed .c1.na,.grid-fluid .c2.na,.grid-fixed .c2.na,.grid-fluid .c3.na,.grid-fixed .c3.na,.grid-fluid .c4.na,.grid-fixed .c4.na,.grid-fluid .c5.na,.grid-fixed .c5.na,.grid-fluid .c6.na,.grid-fixed .c6.na,.grid-fluid .c7.na,.grid-fixed .c7.na,.grid-fluid .c8.na,.grid-fixed .c8.na,.grid-fluid .c9.na,.grid-fixed .c9.na,.grid-fluid .c10.na,.grid-fixed .c10.na,.grid-fluid .c11.na,.grid-fixed .c11.na,.grid-fluid .c12.na,.grid-fixed .c12.na{margin-right:0}
`;
const output = parse(styles);
const errors = output.errors;
expect(errors.length).toEqual(0);
const ast = output.ast;
expect(ast.rules.length).toEqual(8);
});
it('should parse a snippet of keyframe code from animate.css properly', () => {
// this code was taken from the angular.io webpage's CSS code
const styles = `
@charset "UTF-8";
/*!
* animate.css -http://daneden.me/animate
* Version - 3.5.1
* Licensed under the MIT license - http://opensource.org/licenses/MIT
*
* Copyright (c) 2016 Daniel Eden
*/
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.animated.infinite {
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.animated.hinge {
-webkit-animation-duration: 2s;
animation-duration: 2s;
}
.animated.flipOutX,
.animated.flipOutY,
.animated.bounceIn,
.animated.bounceOut {
-webkit-animation-duration: .75s;
animation-duration: .75s;
}
@-webkit-keyframes bounce {
from, 20%, 53%, 80%, to {
-webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
-webkit-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
}
40%, 43% {
-webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -30px, 0);
transform: translate3d(0, -30px, 0);
}
70% {
-webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -15px, 0);
transform: translate3d(0, -15px, 0);
}
90% {
-webkit-transform: translate3d(0,-4px,0);
transform: translate3d(0,-4px,0);
}
}
`;
const output = parse(styles);
const errors = output.errors;
expect(errors.length).toEqual(0);
const ast = output.ast;
expect(ast.rules.length).toEqual(6);
const finalRule = <CssBlockRuleAst>ast.rules[ast.rules.length - 1];
expect(finalRule.type).toEqual(BlockType.Keyframes);
expect(finalRule.block.entries.length).toEqual(4);
});
});
}

View File

@ -0,0 +1,321 @@
/**
* @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 {beforeEach, describe, expect, it} from '../../../core/testing/testing_internal';
import {CssAst, CssAstVisitor, CssAtRulePredicateAst, CssBlockAst, CssDefinitionAst, CssInlineRuleAst, CssKeyframeDefinitionAst, CssKeyframeRuleAst, CssMediaQueryRuleAst, CssPseudoSelectorAst, CssRuleAst, CssSelectorAst, CssSelectorRuleAst, CssSimpleSelectorAst, CssStyleSheetAst, CssStyleValueAst, CssStylesBlockAst, CssUnknownRuleAst, CssUnknownTokenListAst} from '../../src/css_parser/css_ast';
import {BlockType, CssParseError, CssParser, CssToken} from '../../src/css_parser/css_parser';
function _assertTokens(tokens: CssToken[], valuesArr: string[]): void {
expect(tokens.length).toEqual(valuesArr.length);
for (let i = 0; i < tokens.length; i++) {
expect(tokens[i].strValue == valuesArr[i]);
}
}
class MyVisitor implements CssAstVisitor {
captures: {[key: string]: any[]} = {};
/**
* @internal
*/
_capture(method: string, ast: CssAst, context: any) {
this.captures[method] = this.captures[method] || [];
this.captures[method].push([ast, context]);
}
constructor(ast: CssStyleSheetAst, context: any) { ast.visit(this, context); }
visitCssValue(ast: CssStyleValueAst, context: any): void {
this._capture('visitCssValue', ast, context);
}
visitCssInlineRule(ast: CssInlineRuleAst, context: any): void {
this._capture('visitCssInlineRule', ast, context);
}
visitCssAtRulePredicate(ast: CssAtRulePredicateAst, context: any): void {
this._capture('visitCssAtRulePredicate', ast, context);
}
visitCssKeyframeRule(ast: CssKeyframeRuleAst, context: any): void {
this._capture('visitCssKeyframeRule', ast, context);
ast.block.visit(this, context);
}
visitCssKeyframeDefinition(ast: CssKeyframeDefinitionAst, context: any): void {
this._capture('visitCssKeyframeDefinition', ast, context);
ast.block.visit(this, context);
}
visitCssMediaQueryRule(ast: CssMediaQueryRuleAst, context: any): void {
this._capture('visitCssMediaQueryRule', ast, context);
ast.query.visit(this, context);
ast.block.visit(this, context);
}
visitCssSelectorRule(ast: CssSelectorRuleAst, context: any): void {
this._capture('visitCssSelectorRule', ast, context);
ast.selectors.forEach((selAst: CssSelectorAst) => { selAst.visit(this, context); });
ast.block.visit(this, context);
}
visitCssSelector(ast: CssSelectorAst, context: any): void {
this._capture('visitCssSelector', ast, context);
ast.selectorParts.forEach(
(simpleAst: CssSimpleSelectorAst) => { simpleAst.visit(this, context); });
}
visitCssSimpleSelector(ast: CssSimpleSelectorAst, context: any): void {
this._capture('visitCssSimpleSelector', ast, context);
ast.pseudoSelectors.forEach(
(pseudoAst: CssPseudoSelectorAst) => { pseudoAst.visit(this, context); });
}
visitCssDefinition(ast: CssDefinitionAst, context: any): void {
this._capture('visitCssDefinition', ast, context);
ast.value.visit(this, context);
}
visitCssBlock(ast: CssBlockAst, context: any): void {
this._capture('visitCssBlock', ast, context);
ast.entries.forEach((entryAst: CssAst) => { entryAst.visit(this, context); });
}
visitCssStylesBlock(ast: CssStylesBlockAst, context: any): void {
this._capture('visitCssStylesBlock', ast, context);
ast.definitions.forEach(
(definitionAst: CssDefinitionAst) => { definitionAst.visit(this, context); });
}
visitCssStyleSheet(ast: CssStyleSheetAst, context: any): void {
this._capture('visitCssStyleSheet', ast, context);
ast.rules.forEach((ruleAst: CssRuleAst) => { ruleAst.visit(this, context); });
}
visitCssUnknownRule(ast: CssUnknownRuleAst, context: any): void {
this._capture('visitCssUnknownRule', ast, context);
}
visitCssUnknownTokenList(ast: CssUnknownTokenListAst, context: any): void {
this._capture('visitCssUnknownTokenList', ast, context);
}
visitCssPseudoSelector(ast: CssPseudoSelectorAst, context: any): void {
this._capture('visitCssPseudoSelector', ast, context);
}
}
function _getCaptureAst(capture: any[], index = 0): CssAst {
return <CssAst>capture[index][0];
}
export function main() {
function parse(cssCode: string, ignoreErrors: boolean = false) {
const output = new CssParser().parse(cssCode, 'some-fake-css-file.css');
const errors = output.errors;
if (errors.length > 0 && !ignoreErrors) {
throw new Error(errors.map((error: CssParseError) => error.msg).join(', '));
}
return output.ast;
}
describe('CSS parsing and visiting', () => {
let ast: CssStyleSheetAst;
const context = {};
beforeEach(() => {
const cssCode = `
.rule1 { prop1: value1 }
.rule2 { prop2: value2 }
@media all (max-width: 100px) {
#id { prop3 :value3; }
}
@import url(file.css);
@keyframes rotate {
from {
prop4: value4;
}
50%, 100% {
prop5: value5;
}
}
`;
ast = parse(cssCode);
});
it('should parse and visit a stylesheet', () => {
const visitor = new MyVisitor(ast, context);
const captures = visitor.captures['visitCssStyleSheet'];
expect(captures.length).toEqual(1);
const capture = captures[0];
expect(capture[0]).toEqual(ast);
expect(capture[1]).toEqual(context);
});
it('should parse and visit each of the stylesheet selectors', () => {
const visitor = new MyVisitor(ast, context);
const captures = visitor.captures['visitCssSelectorRule'];
expect(captures.length).toEqual(3);
const rule1 = <CssSelectorRuleAst>_getCaptureAst(captures, 0);
expect(rule1).toEqual(ast.rules[0]);
const firstSelector = rule1.selectors[0];
const firstSimpleSelector = firstSelector.selectorParts[0];
_assertTokens(firstSimpleSelector.tokens, ['.', 'rule1']);
const rule2 = <CssSelectorRuleAst>_getCaptureAst(captures, 1);
expect(rule2).toEqual(ast.rules[1]);
const secondSelector = rule2.selectors[0];
const secondSimpleSelector = secondSelector.selectorParts[0];
_assertTokens(secondSimpleSelector.tokens, ['.', 'rule2']);
const rule3 = <CssSelectorRuleAst>_getCaptureAst(captures, 2);
expect(rule3).toEqual((<CssMediaQueryRuleAst>ast.rules[2]).block.entries[0]);
const thirdSelector = rule3.selectors[0];
const thirdSimpleSelector = thirdSelector.selectorParts[0];
_assertTokens(thirdSimpleSelector.tokens, ['#', 'rule3']);
});
it('should parse and visit each of the stylesheet style key/value definitions', () => {
const visitor = new MyVisitor(ast, context);
const captures = visitor.captures['visitCssDefinition'];
expect(captures.length).toEqual(5);
const def1 = <CssDefinitionAst>_getCaptureAst(captures, 0);
expect(def1.property.strValue).toEqual('prop1');
expect(def1.value.tokens[0].strValue).toEqual('value1');
const def2 = <CssDefinitionAst>_getCaptureAst(captures, 1);
expect(def2.property.strValue).toEqual('prop2');
expect(def2.value.tokens[0].strValue).toEqual('value2');
const def3 = <CssDefinitionAst>_getCaptureAst(captures, 2);
expect(def3.property.strValue).toEqual('prop3');
expect(def3.value.tokens[0].strValue).toEqual('value3');
const def4 = <CssDefinitionAst>_getCaptureAst(captures, 3);
expect(def4.property.strValue).toEqual('prop4');
expect(def4.value.tokens[0].strValue).toEqual('value4');
const def5 = <CssDefinitionAst>_getCaptureAst(captures, 4);
expect(def5.property.strValue).toEqual('prop5');
expect(def5.value.tokens[0].strValue).toEqual('value5');
});
it('should parse and visit the associated media query values', () => {
const visitor = new MyVisitor(ast, context);
const captures = visitor.captures['visitCssMediaQueryRule'];
expect(captures.length).toEqual(1);
const query1 = <CssMediaQueryRuleAst>_getCaptureAst(captures, 0);
_assertTokens(query1.query.tokens, ['all', 'and', '(', 'max-width', '100', 'px', ')']);
expect(query1.block.entries.length).toEqual(1);
});
it('should capture the media query predicate', () => {
const visitor = new MyVisitor(ast, context);
const captures = visitor.captures['visitCssAtRulePredicate'];
expect(captures.length).toEqual(1);
const predicate = <CssAtRulePredicateAst>_getCaptureAst(captures, 0);
expect(predicate.strValue).toEqual('@media all (max-width: 100px)');
});
it('should parse and visit the associated "@inline" rule values', () => {
const visitor = new MyVisitor(ast, context);
const captures = visitor.captures['visitCssInlineRule'];
expect(captures.length).toEqual(1);
const inline1 = <CssInlineRuleAst>_getCaptureAst(captures, 0);
expect(inline1.type).toEqual(BlockType.Import);
_assertTokens(inline1.value.tokens, ['url', '(', 'file.css', ')']);
});
it('should parse and visit the keyframe blocks', () => {
const visitor = new MyVisitor(ast, context);
const captures = visitor.captures['visitCssKeyframeRule'];
expect(captures.length).toEqual(1);
const keyframe1 = <CssKeyframeRuleAst>_getCaptureAst(captures, 0);
expect(keyframe1.name.strValue).toEqual('rotate');
expect(keyframe1.block.entries.length).toEqual(2);
});
it('should parse and visit the associated keyframe rules', () => {
const visitor = new MyVisitor(ast, context);
const captures = visitor.captures['visitCssKeyframeDefinition'];
expect(captures.length).toEqual(2);
const def1 = <CssKeyframeDefinitionAst>_getCaptureAst(captures, 0);
_assertTokens(def1.steps, ['from']);
expect(def1.block.entries.length).toEqual(1);
const def2 = <CssKeyframeDefinitionAst>_getCaptureAst(captures, 1);
_assertTokens(def2.steps, ['50%', '100%']);
expect(def2.block.entries.length).toEqual(1);
});
it('should visit an unknown `@` rule', () => {
const cssCode = `
@someUnknownRule param {
one two three
}
`;
ast = parse(cssCode, true);
const visitor = new MyVisitor(ast, context);
const captures = visitor.captures['visitCssUnknownRule'];
expect(captures.length).toEqual(1);
const rule = <CssUnknownRuleAst>_getCaptureAst(captures, 0);
expect(rule.ruleName).toEqual('@someUnknownRule');
_assertTokens(rule.tokens, ['param', '{', 'one', 'two', 'three', '}']);
});
it('should collect an invalid list of tokens before a valid selector', () => {
const cssCode = 'one two three four five; selector { }';
ast = parse(cssCode, true);
const visitor = new MyVisitor(ast, context);
const captures = visitor.captures['visitCssUnknownTokenList'];
expect(captures.length).toEqual(1);
const rule = <CssUnknownTokenListAst>_getCaptureAst(captures, 0);
_assertTokens(rule.tokens, ['one', 'two', 'three', 'four', 'five']);
});
it('should collect an invalid list of tokens after a valid selector', () => {
const cssCode = 'selector { } six seven eight';
ast = parse(cssCode, true);
const visitor = new MyVisitor(ast, context);
const captures = visitor.captures['visitCssUnknownTokenList'];
expect(captures.length).toEqual(1);
const rule = <CssUnknownTokenListAst>_getCaptureAst(captures, 0);
_assertTokens(rule.tokens, ['six', 'seven', 'eight']);
});
});
}

View File

@ -0,0 +1,132 @@
/**
* @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 {hasLifecycleHook} from '@angular/compiler/src/lifecycle_reflector';
import {SimpleChanges} from '@angular/core';
import {LifecycleHooks as Hooks} from '@angular/core/src/metadata/lifecycle_hooks';
export function main() {
describe('Create Directive', () => {
describe('lifecycle', () => {
describe('ngOnChanges', () => {
it('should be true when the directive has the ngOnChanges method', () => {
expect(hasLifecycleHook(Hooks.OnChanges, DirectiveWithOnChangesMethod)).toBe(true);
});
it('should be false otherwise',
() => { expect(hasLifecycleHook(Hooks.OnChanges, DirectiveNoHooks)).toBe(false); });
});
describe('ngOnDestroy', () => {
it('should be true when the directive has the ngOnDestroy method', () => {
expect(hasLifecycleHook(Hooks.OnDestroy, DirectiveWithOnDestroyMethod)).toBe(true);
});
it('should be false otherwise',
() => { expect(hasLifecycleHook(Hooks.OnDestroy, DirectiveNoHooks)).toBe(false); });
});
describe('ngOnInit', () => {
it('should be true when the directive has the ngOnInit method',
() => { expect(hasLifecycleHook(Hooks.OnInit, DirectiveWithOnInitMethod)).toBe(true); });
it('should be false otherwise',
() => { expect(hasLifecycleHook(Hooks.OnInit, DirectiveNoHooks)).toBe(false); });
});
describe('ngDoCheck', () => {
it('should be true when the directive has the ngDoCheck method', () => {
expect(hasLifecycleHook(Hooks.DoCheck, DirectiveWithOnCheckMethod)).toBe(true);
});
it('should be false otherwise',
() => { expect(hasLifecycleHook(Hooks.DoCheck, DirectiveNoHooks)).toBe(false); });
});
describe('ngAfterContentInit', () => {
it('should be true when the directive has the ngAfterContentInit method', () => {
expect(hasLifecycleHook(Hooks.AfterContentInit, DirectiveWithAfterContentInitMethod))
.toBe(true);
});
it('should be false otherwise', () => {
expect(hasLifecycleHook(Hooks.AfterContentInit, DirectiveNoHooks)).toBe(false);
});
});
describe('ngAfterContentChecked', () => {
it('should be true when the directive has the ngAfterContentChecked method', () => {
expect(
hasLifecycleHook(Hooks.AfterContentChecked, DirectiveWithAfterContentCheckedMethod))
.toBe(true);
});
it('should be false otherwise', () => {
expect(hasLifecycleHook(Hooks.AfterContentChecked, DirectiveNoHooks)).toBe(false);
});
});
describe('ngAfterViewInit', () => {
it('should be true when the directive has the ngAfterViewInit method', () => {
expect(hasLifecycleHook(Hooks.AfterViewInit, DirectiveWithAfterViewInitMethod))
.toBe(true);
});
it('should be false otherwise',
() => { expect(hasLifecycleHook(Hooks.AfterViewInit, DirectiveNoHooks)).toBe(false); });
});
describe('ngAfterViewChecked', () => {
it('should be true when the directive has the ngAfterViewChecked method', () => {
expect(hasLifecycleHook(Hooks.AfterViewChecked, DirectiveWithAfterViewCheckedMethod))
.toBe(true);
});
it('should be false otherwise', () => {
expect(hasLifecycleHook(Hooks.AfterViewChecked, DirectiveNoHooks)).toBe(false);
});
});
});
});
}
class DirectiveNoHooks {}
class DirectiveWithOnChangesMethod {
ngOnChanges(_: SimpleChanges) {}
}
class DirectiveWithOnInitMethod {
ngOnInit() {}
}
class DirectiveWithOnCheckMethod {
ngDoCheck() {}
}
class DirectiveWithOnDestroyMethod {
ngOnDestroy() {}
}
class DirectiveWithAfterContentInitMethod {
ngAfterContentInit() {}
}
class DirectiveWithAfterContentCheckedMethod {
ngAfterContentChecked() {}
}
class DirectiveWithAfterViewInitMethod {
ngAfterViewInit() {}
}
class DirectiveWithAfterViewCheckedMethod {
ngAfterViewChecked() {}
}

View File

@ -0,0 +1,553 @@
/**
* @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 {CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, CompileTypeMetadata} from '@angular/compiler/src/compile_metadata';
import {CompilerConfig} from '@angular/compiler/src/config';
import {DirectiveNormalizer} from '@angular/compiler/src/directive_normalizer';
import {ResourceLoader} from '@angular/compiler/src/resource_loader';
import {MockResourceLoader} from '@angular/compiler/testing/resource_loader_mock';
import {TEST_COMPILER_PROVIDERS} from '@angular/compiler/testing/test_bindings';
import {ViewEncapsulation} from '@angular/core/src/metadata/view';
import {TestBed} from '@angular/core/testing';
import {AsyncTestCompleter, beforeEach, describe, expect, inject, it} from '@angular/core/testing/testing_internal';
import {SpyResourceLoader} from './spies';
const SOME_MODULE_URL = 'package:some/module/a.js';
const SOME_HTTP_MODULE_URL = 'http://some/module/a.js';
export function main() {
describe('DirectiveNormalizer', () => {
beforeEach(() => { TestBed.configureCompiler({providers: TEST_COMPILER_PROVIDERS}); });
describe('normalizeDirective', () => {
it('should throw if no template was specified',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
expect(() => normalizer.normalizeTemplate({
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
})).toThrowError('No template specified for component SomeComp');
}));
it('should throw if template is not a string',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
expect(() => normalizer.normalizeTemplate({
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
template: <any>{}
})).toThrowError('The template specified for component SomeComp is not a string');
}));
it('should throw if templateUrl is not a string',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
expect(() => normalizer.normalizeTemplate({
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
templateUrl: <any>{}
})).toThrowError('The templateUrl specified for component SomeComp is not a string');
}));
});
describe('normalizeTemplateSync', () => {
it('should store the template',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeTemplateSync({
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
template: 'a',
templateUrl: null,
styles: [],
styleUrls: []
});
expect(template.template).toEqual('a');
expect(template.templateUrl).toEqual('package:some/module/a.js');
}));
it('should resolve styles on the annotation against the moduleUrl',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeTemplateSync({
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
template: '',
templateUrl: null,
styles: [],
styleUrls: ['test.css']
});
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
}));
it('should resolve styles in the template against the moduleUrl',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeTemplateSync({
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
template: '<style>@import test.css</style>',
templateUrl: null,
styles: [],
styleUrls: []
});
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
}));
it('should use ViewEncapsulation.Emulated by default',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeTemplateSync({
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
template: '',
templateUrl: null,
styles: [],
styleUrls: ['test.css']
});
expect(template.encapsulation).toEqual(ViewEncapsulation.Emulated);
}));
it('should use default encapsulation provided by CompilerConfig',
inject(
[CompilerConfig, DirectiveNormalizer],
(config: CompilerConfig, normalizer: DirectiveNormalizer) => {
config.defaultEncapsulation = ViewEncapsulation.None;
const template = normalizer.normalizeTemplateSync({
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
template: '',
templateUrl: null,
styles: [],
styleUrls: ['test.css']
});
expect(template.encapsulation).toEqual(ViewEncapsulation.None);
}));
});
describe('templateUrl', () => {
it('should load a template from a url that is resolved against moduleUrl',
inject(
[AsyncTestCompleter, DirectiveNormalizer, ResourceLoader],
(async: AsyncTestCompleter, normalizer: DirectiveNormalizer,
resourceLoader: MockResourceLoader) => {
resourceLoader.expect('package:some/module/sometplurl.html', 'a');
normalizer
.normalizeTemplateAsync({
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
template: null,
templateUrl: 'sometplurl.html',
styles: [],
styleUrls: ['test.css']
})
.then((template: CompileTemplateMetadata) => {
expect(template.template).toEqual('a');
expect(template.templateUrl).toEqual('package:some/module/sometplurl.html');
async.done();
});
resourceLoader.flush();
}));
it('should resolve styles on the annotation against the moduleUrl',
inject(
[AsyncTestCompleter, DirectiveNormalizer, ResourceLoader],
(async: AsyncTestCompleter, normalizer: DirectiveNormalizer,
resourceLoader: MockResourceLoader) => {
resourceLoader.expect('package:some/module/tpl/sometplurl.html', '');
normalizer
.normalizeTemplateAsync({
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
template: null,
templateUrl: 'tpl/sometplurl.html',
styles: [],
styleUrls: ['test.css']
})
.then((template: CompileTemplateMetadata) => {
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
async.done();
});
resourceLoader.flush();
}));
it('should resolve styles in the template against the templateUrl',
inject(
[AsyncTestCompleter, DirectiveNormalizer, ResourceLoader],
(async: AsyncTestCompleter, normalizer: DirectiveNormalizer,
resourceLoader: MockResourceLoader) => {
resourceLoader.expect(
'package:some/module/tpl/sometplurl.html', '<style>@import test.css</style>');
normalizer
.normalizeTemplateAsync({
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
template: null,
templateUrl: 'tpl/sometplurl.html',
styles: [],
styleUrls: []
})
.then((template: CompileTemplateMetadata) => {
expect(template.styleUrls).toEqual(['package:some/module/tpl/test.css']);
async.done();
});
resourceLoader.flush();
}));
});
describe('normalizeExternalStylesheets', () => {
beforeEach(() => {
TestBed.configureCompiler(
{providers: [{provide: ResourceLoader, useClass: SpyResourceLoader}]});
});
it('should load an external stylesheet',
inject(
[AsyncTestCompleter, DirectiveNormalizer, ResourceLoader],
(async: AsyncTestCompleter, normalizer: DirectiveNormalizer,
resourceLoader: SpyResourceLoader) => {
programResourceLoaderSpy(resourceLoader, {'package:some/module/test.css': 'a'});
normalizer
.normalizeExternalStylesheets(new CompileTemplateMetadata({
template: '',
templateUrl: '',
styleUrls: ['package:some/module/test.css']
}))
.then((template: CompileTemplateMetadata) => {
expect(template.externalStylesheets.length).toBe(1);
expect(template.externalStylesheets[0]).toEqual(new CompileStylesheetMetadata({
moduleUrl: 'package:some/module/test.css',
styles: ['a'],
styleUrls: []
}));
async.done();
});
}));
it('should load stylesheets referenced by external stylesheets',
inject(
[AsyncTestCompleter, DirectiveNormalizer, ResourceLoader],
(async: AsyncTestCompleter, normalizer: DirectiveNormalizer,
resourceLoader: SpyResourceLoader) => {
programResourceLoaderSpy(resourceLoader, {
'package:some/module/test.css': 'a@import "test2.css"',
'package:some/module/test2.css': 'b'
});
normalizer
.normalizeExternalStylesheets(new CompileTemplateMetadata({
template: '',
templateUrl: '',
styleUrls: ['package:some/module/test.css']
}))
.then((template: CompileTemplateMetadata) => {
expect(template.externalStylesheets.length).toBe(2);
expect(template.externalStylesheets[0]).toEqual(new CompileStylesheetMetadata({
moduleUrl: 'package:some/module/test.css',
styles: ['a'],
styleUrls: ['package:some/module/test2.css']
}));
expect(template.externalStylesheets[1]).toEqual(new CompileStylesheetMetadata({
moduleUrl: 'package:some/module/test2.css',
styles: ['b'],
styleUrls: []
}));
async.done();
});
}));
});
describe('caching', () => {
it('should work for templateUrl',
inject(
[AsyncTestCompleter, DirectiveNormalizer, ResourceLoader],
(async: AsyncTestCompleter, normalizer: DirectiveNormalizer,
resourceLoader: MockResourceLoader) => {
resourceLoader.expect('package:some/module/cmp.html', 'a');
const prenormMeta = {
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
templateUrl: 'cmp.html',
};
Promise
.all([
normalizer.normalizeTemplateAsync(prenormMeta),
normalizer.normalizeTemplateAsync(prenormMeta)
])
.then((templates: CompileTemplateMetadata[]) => {
expect(templates[0].template).toEqual('a');
expect(templates[1].template).toEqual('a');
async.done();
});
resourceLoader.flush();
}));
});
describe('normalizeLoadedTemplate', () => {
it('should store the viewEncapsulationin the result',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const viewEncapsulation = ViewEncapsulation.Native;
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: viewEncapsulation,
styles: [],
styleUrls: []
},
'', 'package:some/module/');
expect(template.encapsulation).toBe(viewEncapsulation);
}));
it('should keep the template as html',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'a', 'package:some/module/');
expect(template.template).toEqual('a');
}));
it('should collect ngContent',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<ng-content select="a"></ng-content>', 'package:some/module/');
expect(template.ngContentSelectors).toEqual(['a']);
}));
it('should normalize ngContent wildcard selector',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<ng-content></ng-content><ng-content select></ng-content><ng-content select="*"></ng-content>',
'package:some/module/');
expect(template.ngContentSelectors).toEqual(['*', '*', '*']);
}));
it('should collect top level styles in the template',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<style>a</style>', 'package:some/module/');
expect(template.styles).toEqual(['a']);
}));
it('should collect styles inside in elements',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<div><style>a</style></div>', 'package:some/module/');
expect(template.styles).toEqual(['a']);
}));
it('should collect styleUrls in the template',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<link rel="stylesheet" href="aUrl">', 'package:some/module/');
expect(template.styleUrls).toEqual(['package:some/module/aUrl']);
}));
it('should collect styleUrls in elements',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<div><link rel="stylesheet" href="aUrl"></div>', 'package:some/module/');
expect(template.styleUrls).toEqual(['package:some/module/aUrl']);
}));
it('should ignore link elements with non stylesheet rel attribute',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<link href="b" rel="a">', 'package:some/module/');
expect(template.styleUrls).toEqual([]);
}));
it('should ignore link elements with absolute urls but non package: scheme',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<link href="http://some/external.css" rel="stylesheet">', 'package:some/module/');
expect(template.styleUrls).toEqual([]);
}));
it('should extract @import style urls into styleAbsUrl',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: ['@import "test.css";'],
styleUrls: []
},
'', 'package:some/module/id');
expect(template.styles).toEqual(['']);
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
}));
it('should not resolve relative urls in inline styles',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: ['.foo{background-image: url(\'double.jpg\');'],
styleUrls: []
},
'', 'package:some/module/id');
expect(template.styles).toEqual(['.foo{background-image: url(\'double.jpg\');']);
}));
it('should resolve relative style urls in styleUrls',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: ['test.css']
},
'', 'package:some/module/id');
expect(template.styles).toEqual([]);
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
}));
it('should resolve relative style urls in styleUrls with http directive url',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_HTTP_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: ['test.css']
},
'', 'http://some/module/id');
expect(template.styles).toEqual([]);
expect(template.styleUrls).toEqual(['http://some/module/test.css']);
}));
it('should normalize ViewEncapsulation.Emulated to ViewEncapsulation.None if there are no styles nor stylesheets',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: ViewEncapsulation.Emulated,
styles: [],
styleUrls: []
},
'', 'package:some/module/id');
expect(template.encapsulation).toEqual(ViewEncapsulation.None);
}));
it('should ignore ng-content in elements with ngNonBindable',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<div ngNonBindable><ng-content select="a"></ng-content></div>',
'package:some/module/');
expect(template.ngContentSelectors).toEqual([]);
}));
it('should still collect <style> in elements with ngNonBindable',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizer.normalizeLoadedTemplate(
{
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<div ngNonBindable><style>div {color:red}</style></div>', 'package:some/module/');
expect(template.styles).toEqual(['div {color:red}']);
}));
});
});
}
function programResourceLoaderSpy(spy: SpyResourceLoader, results: {[key: string]: string}) {
spy.spy('get').and.callFake((url: string): Promise<any> => {
const result = results[url];
if (result) {
return Promise.resolve(result);
} else {
return Promise.reject(`Unknown mock url ${url}`);
}
});
}
class SomeComp {}

View File

@ -0,0 +1,94 @@
/**
* @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 {Component, Directive, Injector, ɵViewMetadata as ViewMetadata} from '@angular/core';
import {TestBed, inject} from '@angular/core/testing';
import {MockDirectiveResolver} from '../testing/index';
export function main() {
describe('MockDirectiveResolver', () => {
let dirResolver: MockDirectiveResolver;
beforeEach(() => {
TestBed.configureTestingModule(
{declarations: [SomeDirective, SomeOtherDirective, SomeComponent]});
});
beforeEach(inject([Injector], (injector: Injector) => {
dirResolver = new MockDirectiveResolver(injector);
}));
describe('Directive overriding', () => {
it('should fallback to the default DirectiveResolver when templates are not overridden',
() => {
const ngModule = dirResolver.resolve(SomeComponent);
expect(ngModule.selector).toEqual('cmp');
});
it('should allow overriding the @Directive', () => {
dirResolver.setDirective(SomeComponent, new Component({selector: 'someOtherSelector'}));
const metadata = dirResolver.resolve(SomeComponent);
expect(metadata.selector).toEqual('someOtherSelector');
});
});
describe('View overriding', () => {
it('should fallback to the default ViewResolver when templates are not overridden', () => {
const view = <Component>dirResolver.resolve(SomeComponent);
expect(view.template).toEqual('template');
});
it('should allow overriding the @View', () => {
dirResolver.setView(SomeComponent, new ViewMetadata({template: 'overridden template'}));
const view = <Component>dirResolver.resolve(SomeComponent);
expect(view.template).toEqual('overridden template');
});
it('should allow overriding a view after it has been resolved', () => {
dirResolver.resolve(SomeComponent);
dirResolver.setView(SomeComponent, new ViewMetadata({template: 'overridden template'}));
const view = <Component>dirResolver.resolve(SomeComponent);
expect(view.template).toEqual('overridden template');
});
});
describe('inline template definition overriding', () => {
it('should allow overriding the default template', () => {
dirResolver.setInlineTemplate(SomeComponent, 'overridden template');
const view = <Component>dirResolver.resolve(SomeComponent);
expect(view.template).toEqual('overridden template');
});
it('should allow overriding an overridden @View', () => {
dirResolver.setView(SomeComponent, new ViewMetadata({template: 'overridden template'}));
dirResolver.setInlineTemplate(SomeComponent, 'overridden template x 2');
const view = <Component>dirResolver.resolve(SomeComponent);
expect(view.template).toEqual('overridden template x 2');
});
it('should allow overriding a view after it has been resolved', () => {
dirResolver.resolve(SomeComponent);
dirResolver.setInlineTemplate(SomeComponent, 'overridden template');
const view = <Component>dirResolver.resolve(SomeComponent);
expect(view.template).toEqual('overridden template');
});
});
});
}
@Directive({selector: 'some-directive'})
class SomeDirective {
}
@Component({selector: 'cmp', template: 'template'})
class SomeComponent {
}
@Directive({selector: 'some-other-directive'})
class SomeOtherDirective {
}

View File

@ -0,0 +1,425 @@
/**
* @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 {DirectiveResolver} from '@angular/compiler/src/directive_resolver';
import {Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Input, Output, ViewChild, ViewChildren} from '@angular/core/src/metadata';
import {reflector} from '@angular/core/src/reflection/reflection';
@Directive({selector: 'someDirective'})
class SomeDirective {
}
@Directive({selector: 'someDirective', inputs: ['c']})
class SomeDirectiveWithInputs {
@Input() a: any;
@Input('renamed') b: any;
c: any;
}
@Directive({selector: 'someDirective', outputs: ['c']})
class SomeDirectiveWithOutputs {
@Output() a: any;
@Output('renamed') b: any;
c: any;
}
@Directive({selector: 'someDirective'})
class SomeDirectiveWithSetterProps {
@Input('renamed')
set a(value: any) {}
}
@Directive({selector: 'someDirective'})
class SomeDirectiveWithGetterOutputs {
@Output('renamed')
get a(): any { return null; }
}
@Directive({selector: 'someDirective', host: {'[c]': 'c'}})
class SomeDirectiveWithHostBindings {
@HostBinding() a: any;
@HostBinding('renamed') b: any;
c: any;
}
@Directive({selector: 'someDirective', host: {'(c)': 'onC()'}})
class SomeDirectiveWithHostListeners {
@HostListener('a')
onA() {}
@HostListener('b', ['$event.value'])
onB(value: any) {}
}
@Directive({selector: 'someDirective', queries: {'cs': new ContentChildren('c')}})
class SomeDirectiveWithContentChildren {
@ContentChildren('a') as: any;
c: any;
}
@Directive({selector: 'someDirective', queries: {'cs': new ViewChildren('c')}})
class SomeDirectiveWithViewChildren {
@ViewChildren('a') as: any;
c: any;
}
@Directive({selector: 'someDirective', queries: {'c': new ContentChild('c')}})
class SomeDirectiveWithContentChild {
@ContentChild('a') a: any;
c: any;
}
@Directive({selector: 'someDirective', queries: {'c': new ViewChild('c')}})
class SomeDirectiveWithViewChild {
@ViewChild('a') a: any;
c: any;
}
@Component({selector: 'sample', template: 'some template', styles: ['some styles']})
class ComponentWithTemplate {
}
@Directive({
selector: 'someDirective',
host: {'[decorator]': 'decorator'},
inputs: ['decorator'],
})
class SomeDirectiveWithSameHostBindingAndInput {
@Input() @HostBinding() prop: any;
}
@Directive({selector: 'someDirective'})
class SomeDirectiveWithMalformedHostBinding1 {
@HostBinding('(a)')
onA() {}
}
@Directive({selector: 'someDirective'})
class SomeDirectiveWithMalformedHostBinding2 {
@HostBinding('[a]')
onA() {}
}
class SomeDirectiveWithoutMetadata {}
export function main() {
describe('DirectiveResolver', () => {
let resolver: DirectiveResolver;
beforeEach(() => { resolver = new DirectiveResolver(); });
it('should read out the Directive metadata', () => {
const directiveMetadata = resolver.resolve(SomeDirective);
expect(directiveMetadata)
.toEqual(new Directive(
{selector: 'someDirective', inputs: [], outputs: [], host: {}, queries: {}}));
});
it('should throw if not matching metadata is found', () => {
expect(() => {
resolver.resolve(SomeDirectiveWithoutMetadata);
}).toThrowError('No Directive annotation found on SomeDirectiveWithoutMetadata');
});
it('should support inheriting the Directive metadata', function() {
@Directive({selector: 'p'})
class Parent {
}
class ChildNoDecorator extends Parent {}
@Directive({selector: 'c'})
class ChildWithDecorator extends Parent {
}
expect(resolver.resolve(ChildNoDecorator))
.toEqual(new Directive({selector: 'p', inputs: [], outputs: [], host: {}, queries: {}}));
expect(resolver.resolve(ChildWithDecorator))
.toEqual(new Directive({selector: 'c', inputs: [], outputs: [], host: {}, queries: {}}));
});
describe('inputs', () => {
it('should append directive inputs', () => {
const directiveMetadata = resolver.resolve(SomeDirectiveWithInputs);
expect(directiveMetadata.inputs).toEqual(['c', 'a', 'b: renamed']);
});
it('should work with getters and setters', () => {
const directiveMetadata = resolver.resolve(SomeDirectiveWithSetterProps);
expect(directiveMetadata.inputs).toEqual(['a: renamed']);
});
it('should remove duplicate inputs', () => {
@Directive({selector: 'someDirective', inputs: ['a', 'a']})
class SomeDirectiveWithDuplicateInputs {
}
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateInputs);
expect(directiveMetadata.inputs).toEqual(['a']);
});
it('should use the last input if duplicate inputs (with rename)', () => {
@Directive({selector: 'someDirective', inputs: ['a', 'localA: a']})
class SomeDirectiveWithDuplicateInputs {
}
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateInputs);
expect(directiveMetadata.inputs).toEqual(['localA: a']);
});
it('should prefer @Input over @Directive.inputs', () => {
@Directive({selector: 'someDirective', inputs: ['a']})
class SomeDirectiveWithDuplicateInputs {
@Input('a')
propA: any;
}
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateInputs);
expect(directiveMetadata.inputs).toEqual(['propA: a']);
});
it('should support inheriting inputs', () => {
@Directive({selector: 'p'})
class Parent {
@Input()
p1: any;
@Input('p21')
p2: any;
}
class Child extends Parent {
@Input('p22')
p2: any;
@Input()
p3: any;
}
const directiveMetadata = resolver.resolve(Child);
expect(directiveMetadata.inputs).toEqual(['p1', 'p2: p22', 'p3']);
});
});
describe('outputs', () => {
it('should append directive outputs', () => {
const directiveMetadata = resolver.resolve(SomeDirectiveWithOutputs);
expect(directiveMetadata.outputs).toEqual(['c', 'a', 'b: renamed']);
});
it('should work with getters and setters', () => {
const directiveMetadata = resolver.resolve(SomeDirectiveWithGetterOutputs);
expect(directiveMetadata.outputs).toEqual(['a: renamed']);
});
it('should remove duplicate outputs', () => {
@Directive({selector: 'someDirective', outputs: ['a', 'a']})
class SomeDirectiveWithDuplicateOutputs {
}
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateOutputs);
expect(directiveMetadata.outputs).toEqual(['a']);
});
it('should use the last output if duplicate outputs (with rename)', () => {
@Directive({selector: 'someDirective', outputs: ['a', 'localA: a']})
class SomeDirectiveWithDuplicateOutputs {
}
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateOutputs);
expect(directiveMetadata.outputs).toEqual(['localA: a']);
});
it('should prefer @Output over @Directive.outputs', () => {
@Directive({selector: 'someDirective', outputs: ['a']})
class SomeDirectiveWithDuplicateOutputs {
@Output('a')
propA: any;
}
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateOutputs);
expect(directiveMetadata.outputs).toEqual(['propA: a']);
});
it('should support inheriting outputs', () => {
@Directive({selector: 'p'})
class Parent {
@Output()
p1: any;
@Output('p21')
p2: any;
}
class Child extends Parent {
@Output('p22')
p2: any;
@Output()
p3: any;
}
const directiveMetadata = resolver.resolve(Child);
expect(directiveMetadata.outputs).toEqual(['p1', 'p2: p22', 'p3']);
});
});
describe('host', () => {
it('should append host bindings', () => {
const directiveMetadata = resolver.resolve(SomeDirectiveWithHostBindings);
expect(directiveMetadata.host).toEqual({'[c]': 'c', '[a]': 'a', '[renamed]': 'b'});
});
it('should append host binding and input on the same property', () => {
const directiveMetadata = resolver.resolve(SomeDirectiveWithSameHostBindingAndInput);
expect(directiveMetadata.host).toEqual({'[decorator]': 'decorator', '[prop]': 'prop'});
expect(directiveMetadata.inputs).toEqual(['decorator', 'prop']);
});
it('should append host listeners', () => {
const directiveMetadata = resolver.resolve(SomeDirectiveWithHostListeners);
expect(directiveMetadata.host)
.toEqual({'(c)': 'onC()', '(a)': 'onA()', '(b)': 'onB($event.value)'});
});
it('should throw when @HostBinding name starts with "("', () => {
expect(() => resolver.resolve(SomeDirectiveWithMalformedHostBinding1))
.toThrowError('@HostBinding can not bind to events. Use @HostListener instead.');
});
it('should throw when @HostBinding name starts with "["', () => {
expect(() => resolver.resolve(SomeDirectiveWithMalformedHostBinding2))
.toThrowError(
`@HostBinding parameter should be a property name, 'class.<name>', or 'attr.<name>'.`);
});
it('should support inheriting host bindings', () => {
@Directive({selector: 'p'})
class Parent {
@HostBinding()
p1: any;
@HostBinding('p21')
p2: any;
}
class Child extends Parent {
@HostBinding('p22')
p2: any;
@HostBinding()
p3: any;
}
const directiveMetadata = resolver.resolve(Child);
expect(directiveMetadata.host)
.toEqual({'[p1]': 'p1', '[p21]': 'p2', '[p22]': 'p2', '[p3]': 'p3'});
});
it('should support inheriting host listeners', () => {
@Directive({selector: 'p'})
class Parent {
@HostListener('p1')
p1() {}
@HostListener('p21')
p2() {}
}
class Child extends Parent {
@HostListener('p22')
p2() {}
@HostListener('p3')
p3() {}
}
const directiveMetadata = resolver.resolve(Child);
expect(directiveMetadata.host)
.toEqual({'(p1)': 'p1()', '(p21)': 'p2()', '(p22)': 'p2()', '(p3)': 'p3()'});
});
it('should combine host bindings and listeners during inheritance', () => {
@Directive({selector: 'p'})
class Parent {
@HostListener('p11') @HostListener('p12')
p1() {}
@HostBinding('p21') @HostBinding('p22')
p2: any;
}
class Child extends Parent {
@HostListener('c1')
p1() {}
@HostBinding('c2')
p2: any;
}
const directiveMetadata = resolver.resolve(Child);
expect(directiveMetadata.host).toEqual({
'(p11)': 'p1()',
'(p12)': 'p1()',
'(c1)': 'p1()',
'[p21]': 'p2',
'[p22]': 'p2',
'[c2]': 'p2'
});
});
});
describe('queries', () => {
it('should append ContentChildren', () => {
const directiveMetadata = resolver.resolve(SomeDirectiveWithContentChildren);
expect(directiveMetadata.queries)
.toEqual({'cs': new ContentChildren('c'), 'as': new ContentChildren('a')});
});
it('should append ViewChildren', () => {
const directiveMetadata = resolver.resolve(SomeDirectiveWithViewChildren);
expect(directiveMetadata.queries)
.toEqual({'cs': new ViewChildren('c'), 'as': new ViewChildren('a')});
});
it('should append ContentChild', () => {
const directiveMetadata = resolver.resolve(SomeDirectiveWithContentChild);
expect(directiveMetadata.queries)
.toEqual({'c': new ContentChild('c'), 'a': new ContentChild('a')});
});
it('should append ViewChild', () => {
const directiveMetadata = resolver.resolve(SomeDirectiveWithViewChild);
expect(directiveMetadata.queries)
.toEqual({'c': new ViewChild('c'), 'a': new ViewChild('a')});
});
it('should support inheriting queries', () => {
@Directive({selector: 'p'})
class Parent {
@ContentChild('p1')
p1: any;
@ContentChild('p21')
p2: any;
}
class Child extends Parent {
@ContentChild('p22')
p2: any;
@ContentChild('p3')
p3: any;
}
const directiveMetadata = resolver.resolve(Child);
expect(directiveMetadata.queries).toEqual({
'p1': new ContentChild('p1'),
'p2': new ContentChild('p22'),
'p3': new ContentChild('p3')
});
});
});
describe('Component', () => {
it('should read out the template related metadata from the Component metadata', () => {
const compMetadata: Component = resolver.resolve(ComponentWithTemplate);
expect(compMetadata.template).toEqual('some template');
expect(compMetadata.styles).toEqual(['some styles']);
});
});
});
}

View File

@ -0,0 +1,248 @@
/**
* @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 {Lexer, Token} from '@angular/compiler/src/expression_parser/lexer';
function lex(text: string): any[] {
return new Lexer().tokenize(text);
}
function expectToken(token: any, index: number) {
expect(token instanceof Token).toBe(true);
expect(token.index).toEqual(index);
}
function expectCharacterToken(token: any, index: number, character: string) {
expect(character.length).toBe(1);
expectToken(token, index);
expect(token.isCharacter(character.charCodeAt(0))).toBe(true);
}
function expectOperatorToken(token: any, index: number, operator: string) {
expectToken(token, index);
expect(token.isOperator(operator)).toBe(true);
}
function expectNumberToken(token: any, index: number, n: number) {
expectToken(token, index);
expect(token.isNumber()).toBe(true);
expect(token.toNumber()).toEqual(n);
}
function expectStringToken(token: any, index: number, str: string) {
expectToken(token, index);
expect(token.isString()).toBe(true);
expect(token.toString()).toEqual(str);
}
function expectIdentifierToken(token: any, index: number, identifier: string) {
expectToken(token, index);
expect(token.isIdentifier()).toBe(true);
expect(token.toString()).toEqual(identifier);
}
function expectKeywordToken(token: any, index: number, keyword: string) {
expectToken(token, index);
expect(token.isKeyword()).toBe(true);
expect(token.toString()).toEqual(keyword);
}
function expectErrorToken(token: Token, index: any, message: string) {
expectToken(token, index);
expect(token.isError()).toBe(true);
expect(token.toString()).toEqual(message);
}
export function main() {
describe('lexer', () => {
describe('token', () => {
it('should tokenize a simple identifier', () => {
const tokens: number[] = lex('j');
expect(tokens.length).toEqual(1);
expectIdentifierToken(tokens[0], 0, 'j');
});
it('should tokenize "this"', () => {
const tokens: number[] = lex('this');
expect(tokens.length).toEqual(1);
expectKeywordToken(tokens[0], 0, 'this');
});
it('should tokenize a dotted identifier', () => {
const tokens: number[] = lex('j.k');
expect(tokens.length).toEqual(3);
expectIdentifierToken(tokens[0], 0, 'j');
expectCharacterToken(tokens[1], 1, '.');
expectIdentifierToken(tokens[2], 2, 'k');
});
it('should tokenize an operator', () => {
const tokens: number[] = lex('j-k');
expect(tokens.length).toEqual(3);
expectOperatorToken(tokens[1], 1, '-');
});
it('should tokenize an indexed operator', () => {
const tokens: number[] = lex('j[k]');
expect(tokens.length).toEqual(4);
expectCharacterToken(tokens[1], 1, '[');
expectCharacterToken(tokens[3], 3, ']');
});
it('should tokenize numbers', () => {
const tokens: number[] = lex('88');
expect(tokens.length).toEqual(1);
expectNumberToken(tokens[0], 0, 88);
});
it('should tokenize numbers within index ops',
() => { expectNumberToken(lex('a[22]')[2], 2, 22); });
it('should tokenize simple quoted strings',
() => { expectStringToken(lex('"a"')[0], 0, 'a'); });
it('should tokenize quoted strings with escaped quotes',
() => { expectStringToken(lex('"a\\""')[0], 0, 'a"'); });
it('should tokenize a string', () => {
const tokens: Token[] = lex('j-a.bc[22]+1.3|f:\'a\\\'c\':"d\\"e"');
expectIdentifierToken(tokens[0], 0, 'j');
expectOperatorToken(tokens[1], 1, '-');
expectIdentifierToken(tokens[2], 2, 'a');
expectCharacterToken(tokens[3], 3, '.');
expectIdentifierToken(tokens[4], 4, 'bc');
expectCharacterToken(tokens[5], 6, '[');
expectNumberToken(tokens[6], 7, 22);
expectCharacterToken(tokens[7], 9, ']');
expectOperatorToken(tokens[8], 10, '+');
expectNumberToken(tokens[9], 11, 1.3);
expectOperatorToken(tokens[10], 14, '|');
expectIdentifierToken(tokens[11], 15, 'f');
expectCharacterToken(tokens[12], 16, ':');
expectStringToken(tokens[13], 17, 'a\'c');
expectCharacterToken(tokens[14], 23, ':');
expectStringToken(tokens[15], 24, 'd"e');
});
it('should tokenize undefined', () => {
const tokens: Token[] = lex('undefined');
expectKeywordToken(tokens[0], 0, 'undefined');
expect(tokens[0].isKeywordUndefined()).toBe(true);
});
it('should ignore whitespace', () => {
const tokens: Token[] = lex('a \t \n \r b');
expectIdentifierToken(tokens[0], 0, 'a');
expectIdentifierToken(tokens[1], 8, 'b');
});
it('should tokenize quoted string', () => {
const str = '[\'\\\'\', "\\""]';
const tokens: Token[] = lex(str);
expectStringToken(tokens[1], 1, '\'');
expectStringToken(tokens[3], 7, '"');
});
it('should tokenize escaped quoted string', () => {
const str = '"\\"\\n\\f\\r\\t\\v\\u00A0"';
const tokens: Token[] = lex(str);
expect(tokens.length).toEqual(1);
expect(tokens[0].toString()).toEqual('"\n\f\r\t\v\u00A0');
});
it('should tokenize unicode', () => {
const tokens: Token[] = lex('"\\u00A0"');
expect(tokens.length).toEqual(1);
expect(tokens[0].toString()).toEqual('\u00a0');
});
it('should tokenize relation', () => {
const tokens: Token[] = lex('! == != < > <= >= === !==');
expectOperatorToken(tokens[0], 0, '!');
expectOperatorToken(tokens[1], 2, '==');
expectOperatorToken(tokens[2], 5, '!=');
expectOperatorToken(tokens[3], 8, '<');
expectOperatorToken(tokens[4], 10, '>');
expectOperatorToken(tokens[5], 12, '<=');
expectOperatorToken(tokens[6], 15, '>=');
expectOperatorToken(tokens[7], 18, '===');
expectOperatorToken(tokens[8], 22, '!==');
});
it('should tokenize statements', () => {
const tokens: Token[] = lex('a;b;');
expectIdentifierToken(tokens[0], 0, 'a');
expectCharacterToken(tokens[1], 1, ';');
expectIdentifierToken(tokens[2], 2, 'b');
expectCharacterToken(tokens[3], 3, ';');
});
it('should tokenize function invocation', () => {
const tokens: Token[] = lex('a()');
expectIdentifierToken(tokens[0], 0, 'a');
expectCharacterToken(tokens[1], 1, '(');
expectCharacterToken(tokens[2], 2, ')');
});
it('should tokenize simple method invocations', () => {
const tokens: Token[] = lex('a.method()');
expectIdentifierToken(tokens[2], 2, 'method');
});
it('should tokenize method invocation', () => {
const tokens: Token[] = lex('a.b.c (d) - e.f()');
expectIdentifierToken(tokens[0], 0, 'a');
expectCharacterToken(tokens[1], 1, '.');
expectIdentifierToken(tokens[2], 2, 'b');
expectCharacterToken(tokens[3], 3, '.');
expectIdentifierToken(tokens[4], 4, 'c');
expectCharacterToken(tokens[5], 6, '(');
expectIdentifierToken(tokens[6], 7, 'd');
expectCharacterToken(tokens[7], 8, ')');
expectOperatorToken(tokens[8], 10, '-');
expectIdentifierToken(tokens[9], 12, 'e');
expectCharacterToken(tokens[10], 13, '.');
expectIdentifierToken(tokens[11], 14, 'f');
expectCharacterToken(tokens[12], 15, '(');
expectCharacterToken(tokens[13], 16, ')');
});
it('should tokenize number', () => { expectNumberToken(lex('0.5')[0], 0, 0.5); });
it('should tokenize number with exponent', () => {
let tokens: Token[] = lex('0.5E-10');
expect(tokens.length).toEqual(1);
expectNumberToken(tokens[0], 0, 0.5E-10);
tokens = lex('0.5E+10');
expectNumberToken(tokens[0], 0, 0.5E+10);
});
it('should return exception for invalid exponent', () => {
expectErrorToken(
lex('0.5E-')[0], 4, 'Lexer Error: Invalid exponent at column 4 in expression [0.5E-]');
expectErrorToken(
lex('0.5E-A')[0], 4,
'Lexer Error: Invalid exponent at column 4 in expression [0.5E-A]');
});
it('should tokenize number starting with a dot',
() => { expectNumberToken(lex('.5')[0], 0, 0.5); });
it('should throw error on invalid unicode', () => {
expectErrorToken(
lex('\'\\u1\'\'bla\'')[0], 2,
'Lexer Error: Invalid unicode escape [\\u1\'\'b] at column 2 in expression [\'\\u1\'\'bla\']');
});
it('should tokenize hash as operator', () => { expectOperatorToken(lex('#')[0], 0, '#'); });
it('should tokenize ?. as operator', () => { expectOperatorToken(lex('?.')[0], 0, '?.'); });
});
});
}

View File

@ -0,0 +1,590 @@
/**
* @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 {ASTWithSource, BindingPipe, Interpolation, ParserError, TemplateBinding} from '@angular/compiler/src/expression_parser/ast';
import {Lexer} from '@angular/compiler/src/expression_parser/lexer';
import {Parser, SplitInterpolation, TemplateBindingParseResult} from '@angular/compiler/src/expression_parser/parser';
import {expect} from '@angular/platform-browser/testing/matchers';
import {unparse} from './unparser';
import {validate} from './validator';
export function main() {
function createParser() { return new Parser(new Lexer()); }
function parseAction(text: string, location: any = null): ASTWithSource {
return createParser().parseAction(text, location);
}
function parseBinding(text: string, location: any = null): ASTWithSource {
return createParser().parseBinding(text, location);
}
function parseTemplateBindingsResult(
text: string, location: any = null, prefix?: string): TemplateBindingParseResult {
return createParser().parseTemplateBindings(prefix, text, location);
}
function parseTemplateBindings(
text: string, location: any = null, prefix?: string): TemplateBinding[] {
return parseTemplateBindingsResult(text, location, prefix).templateBindings;
}
function parseInterpolation(text: string, location: any = null): ASTWithSource {
return createParser().parseInterpolation(text, location);
}
function splitInterpolation(text: string, location: any = null): SplitInterpolation {
return createParser().splitInterpolation(text, location);
}
function parseSimpleBinding(text: string, location: any = null): ASTWithSource {
return createParser().parseSimpleBinding(text, location);
}
function checkInterpolation(exp: string, expected?: string) {
const ast = parseInterpolation(exp);
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function checkBinding(exp: string, expected?: string) {
const ast = parseBinding(exp);
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function checkAction(exp: string, expected?: string) {
const ast = parseAction(exp);
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function expectError(ast: {errors: ParserError[]}, message: string) {
for (const error of ast.errors) {
if (error.message.indexOf(message) >= 0) {
return;
}
}
const errMsgs = ast.errors.map(err => err.message).join('\n');
throw Error(
`Expected an error containing "${message}" to be reported, but got the errors:\n` +
errMsgs);
}
function expectActionError(text: string, message: string) {
expectError(validate(parseAction(text)), message);
}
function expectBindingError(text: string, message: string) {
expectError(validate(parseBinding(text)), message);
}
describe('parser', () => {
describe('parseAction', () => {
it('should parse numbers', () => { checkAction('1'); });
it('should parse strings', () => {
checkAction('\'1\'', '"1"');
checkAction('"1"');
});
it('should parse null', () => { checkAction('null'); });
it('should parse undefined', () => { checkAction('undefined'); });
it('should parse unary - expressions', () => {
checkAction('-1', '0 - 1');
checkAction('+1', '1');
});
it('should parse unary ! expressions', () => {
checkAction('!true');
checkAction('!!true');
checkAction('!!!true');
});
it('should parse multiplicative expressions',
() => { checkAction('3*4/2%5', '3 * 4 / 2 % 5'); });
it('should parse additive expressions', () => { checkAction('3 + 6 - 2'); });
it('should parse relational expressions', () => {
checkAction('2 < 3');
checkAction('2 > 3');
checkAction('2 <= 2');
checkAction('2 >= 2');
});
it('should parse equality expressions', () => {
checkAction('2 == 3');
checkAction('2 != 3');
});
it('should parse strict equality expressions', () => {
checkAction('2 === 3');
checkAction('2 !== 3');
});
it('should parse expressions', () => {
checkAction('true && true');
checkAction('true || false');
});
it('should parse grouped expressions', () => { checkAction('(1 + 2) * 3', '1 + 2 * 3'); });
it('should ignore comments in expressions', () => { checkAction('a //comment', 'a'); });
it('should retain // in string literals',
() => { checkAction(`"http://www.google.com"`, `"http://www.google.com"`); });
it('should parse an empty string', () => { checkAction(''); });
describe('literals', () => {
it('should parse array', () => {
checkAction('[1][0]');
checkAction('[[1]][0][0]');
checkAction('[]');
checkAction('[].length');
checkAction('[1, 2].length');
});
it('should parse map', () => {
checkAction('{}');
checkAction('{a: 1}[2]');
checkAction('{}["a"]');
});
it('should only allow identifier, string, or keyword as map key', () => {
expectActionError('{(:0}', 'expected identifier, keyword, or string');
expectActionError('{1234:0}', 'expected identifier, keyword, or string');
});
});
describe('member access', () => {
it('should parse field access', () => {
checkAction('a');
checkAction('this.a', 'a');
checkAction('a.a');
});
it('should only allow identifier or keyword as member names', () => {
expectActionError('x.(', 'identifier or keyword');
expectActionError('x. 1234', 'identifier or keyword');
expectActionError('x."foo"', 'identifier or keyword');
});
it('should parse safe field access', () => {
checkAction('a?.a');
checkAction('a.a?.a');
});
});
describe('method calls', () => {
it('should parse method calls', () => {
checkAction('fn()');
checkAction('add(1, 2)');
checkAction('a.add(1, 2)');
checkAction('fn().add(1, 2)');
});
});
describe('functional calls', () => {
it('should parse function calls', () => { checkAction('fn()(1, 2)'); });
});
describe('conditional', () => {
it('should parse ternary/conditional expressions', () => {
checkAction('7 == 3 + 4 ? 10 : 20');
checkAction('false ? 10 : 20');
});
it('should report incorrect ternary operator syntax', () => {
expectActionError('true?1', 'Conditional expression true?1 requires all 3 expressions');
});
});
describe('assignment', () => {
it('should support field assignments', () => {
checkAction('a = 12');
checkAction('a.a.a = 123');
checkAction('a = 123; b = 234;');
});
it('should report on safe field assignments',
() => { expectActionError('a?.a = 123', 'cannot be used in the assignment'); });
it('should support array updates', () => { checkAction('a[0] = 200'); });
});
it('should error when using pipes',
() => { expectActionError('x|blah', 'Cannot have a pipe'); });
it('should store the source in the result',
() => { expect(parseAction('someExpr', 'someExpr')); });
it('should store the passed-in location',
() => { expect(parseAction('someExpr', 'location').location).toBe('location'); });
it('should report when encountering interpolation', () => {
expectActionError('{{a()}}', 'Got interpolation ({{}}) where expression was expected');
});
});
describe('general error handling', () => {
it('should report an unexpected token',
() => { expectActionError('[1,2] trac', 'Unexpected token \'trac\''); });
it('should report reasonable error for unconsumed tokens',
() => { expectActionError(')', 'Unexpected token ) at column 1 in [)]'); });
it('should report a missing expected token', () => {
expectActionError('a(b', 'Missing expected ) at the end of the expression [a(b]');
});
});
describe('parseBinding', () => {
describe('pipes', () => {
it('should parse pipes', () => {
checkBinding('a(b | c)', 'a((b | c))');
checkBinding('a.b(c.d(e) | f)', 'a.b((c.d(e) | f))');
checkBinding('[1, 2, 3] | a', '([1, 2, 3] | a)');
checkBinding('{a: 1} | b', '({a: 1} | b)');
checkBinding('a[b] | c', '(a[b] | c)');
checkBinding('a?.b | c', '(a?.b | c)');
checkBinding('true | a', '(true | a)');
checkBinding('a | b:c | d', '((a | b:c) | d)');
checkBinding('a | b:(c | d)', '(a | b:(c | d))');
});
it('should only allow identifier or keyword as formatter names', () => {
expectBindingError('"Foo"|(', 'identifier or keyword');
expectBindingError('"Foo"|1234', 'identifier or keyword');
expectBindingError('"Foo"|"uppercase"', 'identifier or keyword');
});
it('should parse quoted expressions', () => { checkBinding('a:b', 'a:b'); });
it('should not crash when prefix part is not tokenizable',
() => { checkBinding('"a:b"', '"a:b"'); });
it('should ignore whitespace around quote prefix', () => { checkBinding(' a :b', 'a:b'); });
it('should refuse prefixes that are not single identifiers', () => {
expectBindingError('a + b:c', '');
expectBindingError('1:c', '');
});
});
it('should store the source in the result',
() => { expect(parseBinding('someExpr').source).toBe('someExpr'); });
it('should store the passed-in location',
() => { expect(parseBinding('someExpr', 'location').location).toBe('location'); });
it('should report chain expressions',
() => { expectError(parseBinding('1;2'), 'contain chained expression'); });
it('should report assignment',
() => { expectError(parseBinding('a=2'), 'contain assignments'); });
it('should report when encountering interpolation', () => {
expectBindingError('{{a.b}}', 'Got interpolation ({{}}) where expression was expected');
});
it('should parse conditional expression', () => { checkBinding('a < b ? a : b'); });
it('should ignore comments in bindings', () => { checkBinding('a //comment', 'a'); });
it('should retain // in string literals',
() => { checkBinding(`"http://www.google.com"`, `"http://www.google.com"`); });
it('should retain // in : microsyntax', () => { checkBinding('one:a//b', 'one:a//b'); });
});
describe('parseTemplateBindings', () => {
function keys(templateBindings: any[]) {
return templateBindings.map(binding => binding.key);
}
function keyValues(templateBindings: any[]) {
return templateBindings.map(binding => {
if (binding.keyIsVar) {
return 'let ' + binding.key + (binding.name == null ? '=null' : '=' + binding.name);
} else {
return binding.key + (binding.expression == null ? '' : `=${binding.expression}`);
}
});
}
function keySpans(source: string, templateBindings: TemplateBinding[]) {
return templateBindings.map(
binding => source.substring(binding.span.start, binding.span.end));
}
function exprSources(templateBindings: any[]) {
return templateBindings.map(
binding => binding.expression != null ? binding.expression.source : null);
}
it('should parse an empty string', () => { expect(parseTemplateBindings('')).toEqual([]); });
it('should parse a string without a value',
() => { expect(keys(parseTemplateBindings('a'))).toEqual(['a']); });
it('should only allow identifier, string, or keyword including dashes as keys', () => {
let bindings = parseTemplateBindings('a:\'b\'');
expect(keys(bindings)).toEqual(['a']);
bindings = parseTemplateBindings('\'a\':\'b\'');
expect(keys(bindings)).toEqual(['a']);
bindings = parseTemplateBindings('"a":\'b\'');
expect(keys(bindings)).toEqual(['a']);
bindings = parseTemplateBindings('a-b:\'c\'');
expect(keys(bindings)).toEqual(['a-b']);
expectError(parseTemplateBindingsResult('(:0'), 'expected identifier, keyword, or string');
expectError(
parseTemplateBindingsResult('1234:0'), 'expected identifier, keyword, or string');
});
it('should detect expressions as value', () => {
let bindings = parseTemplateBindings('a:b');
expect(exprSources(bindings)).toEqual(['b']);
bindings = parseTemplateBindings('a:1+1');
expect(exprSources(bindings)).toEqual(['1+1']);
});
it('should detect names as value', () => {
const bindings = parseTemplateBindings('a:let b');
expect(keyValues(bindings)).toEqual(['a', 'let b=\$implicit']);
});
it('should allow space and colon as separators', () => {
let bindings = parseTemplateBindings('a:b');
expect(keys(bindings)).toEqual(['a']);
expect(exprSources(bindings)).toEqual(['b']);
bindings = parseTemplateBindings('a b');
expect(keys(bindings)).toEqual(['a']);
expect(exprSources(bindings)).toEqual(['b']);
});
it('should allow multiple pairs', () => {
const bindings = parseTemplateBindings('a 1 b 2');
expect(keys(bindings)).toEqual(['a', 'aB']);
expect(exprSources(bindings)).toEqual(['1 ', '2']);
});
it('should store the sources in the result', () => {
const bindings = parseTemplateBindings('a 1,b 2');
expect(bindings[0].expression.source).toEqual('1');
expect(bindings[1].expression.source).toEqual('2');
});
it('should store the passed-in location', () => {
const bindings = parseTemplateBindings('a 1,b 2', 'location');
expect(bindings[0].expression.location).toEqual('location');
});
it('should support let notation', () => {
let bindings = parseTemplateBindings('let i');
expect(keyValues(bindings)).toEqual(['let i=\$implicit']);
bindings = parseTemplateBindings('let i');
expect(keyValues(bindings)).toEqual(['let i=\$implicit']);
bindings = parseTemplateBindings('let a; let b');
expect(keyValues(bindings)).toEqual(['let a=\$implicit', 'let b=\$implicit']);
bindings = parseTemplateBindings('let a; let b;');
expect(keyValues(bindings)).toEqual(['let a=\$implicit', 'let b=\$implicit']);
bindings = parseTemplateBindings('let i-a = k-a');
expect(keyValues(bindings)).toEqual(['let i-a=k-a']);
bindings = parseTemplateBindings('keyword let item; let i = k');
expect(keyValues(bindings)).toEqual(['keyword', 'let item=\$implicit', 'let i=k']);
bindings = parseTemplateBindings('keyword: let item; let i = k');
expect(keyValues(bindings)).toEqual(['keyword', 'let item=\$implicit', 'let i=k']);
bindings = parseTemplateBindings('directive: let item in expr; let a = b', 'location');
expect(keyValues(bindings)).toEqual([
'directive', 'let item=\$implicit', 'directiveIn=expr in location', 'let a=b'
]);
});
it('should parse pipes', () => {
const bindings = parseTemplateBindings('key value|pipe');
const ast = bindings[0].expression.ast;
expect(ast).toBeAnInstanceOf(BindingPipe);
});
describe('spans', () => {
it('should should support let', () => {
const source = 'let i';
expect(keySpans(source, parseTemplateBindings(source))).toEqual(['let i']);
});
it('should support multiple lets', () => {
const source = 'let item; let i=index; let e=even;';
expect(keySpans(source, parseTemplateBindings(source))).toEqual([
'let item', 'let i=index', 'let e=even'
]);
});
it('should support a prefix', () => {
const source = 'let person of people';
const prefix = 'ngFor';
const bindings = parseTemplateBindings(source, null, prefix);
expect(keyValues(bindings)).toEqual([
'ngFor', 'let person=$implicit', 'ngForOf=people in null'
]);
expect(keySpans(source, bindings)).toEqual(['', 'let person ', 'of people']);
});
});
});
describe('parseInterpolation', () => {
it('should return null if no interpolation',
() => { expect(parseInterpolation('nothing')).toBe(null); });
it('should parse no prefix/suffix interpolation', () => {
const ast = parseInterpolation('{{a}}').ast as Interpolation;
expect(ast.strings).toEqual(['', '']);
expect(ast.expressions.length).toEqual(1);
expect(ast.expressions[0].name).toEqual('a');
});
it('should parse prefix/suffix with multiple interpolation', () => {
const originalExp = 'before {{ a }} middle {{ b }} after';
const ast = parseInterpolation(originalExp).ast;
expect(unparse(ast)).toEqual(originalExp);
validate(ast);
});
it('should report empty interpolation expressions', () => {
expectError(
parseInterpolation('{{}}'),
'Blank expressions are not allowed in interpolated strings');
expectError(
parseInterpolation('foo {{ }}'),
'Parser Error: Blank expressions are not allowed in interpolated strings');
});
it('should parse conditional expression',
() => { checkInterpolation('{{ a < b ? a : b }}'); });
it('should parse expression with newline characters', () => {
checkInterpolation(`{{ 'foo' +\n 'bar' +\r 'baz' }}`, `{{ "foo" + "bar" + "baz" }}`);
});
it('should support custom interpolation', () => {
const parser = new Parser(new Lexer());
const ast = parser.parseInterpolation('{% a %}', null, {start: '{%', end: '%}'}).ast as any;
expect(ast.strings).toEqual(['', '']);
expect(ast.expressions.length).toEqual(1);
expect(ast.expressions[0].name).toEqual('a');
});
describe('comments', () => {
it('should ignore comments in interpolation expressions',
() => { checkInterpolation('{{a //comment}}', '{{ a }}'); });
it('should retain // in single quote strings', () => {
checkInterpolation(`{{ 'http://www.google.com' }}`, `{{ "http://www.google.com" }}`);
});
it('should retain // in double quote strings', () => {
checkInterpolation(`{{ "http://www.google.com" }}`, `{{ "http://www.google.com" }}`);
});
it('should ignore comments after string literals',
() => { checkInterpolation(`{{ "a//b" //comment }}`, `{{ "a//b" }}`); });
it('should retain // in complex strings', () => {
checkInterpolation(
`{{"//a\'//b\`//c\`//d\'//e" //comment}}`, `{{ "//a\'//b\`//c\`//d\'//e" }}`);
});
it('should retain // in nested, unterminated strings',
() => { checkInterpolation(`{{ "a\'b\`" //comment}}`, `{{ "a\'b\`" }}`); });
});
});
describe('parseSimpleBinding', () => {
it('should parse a field access', () => {
const p = parseSimpleBinding('name');
expect(unparse(p)).toEqual('name');
validate(p);
});
it('should report when encountering pipes', () => {
expectError(
validate(parseSimpleBinding('a | somePipe')),
'Host binding expression cannot contain pipes');
});
it('should report when encountering interpolation', () => {
expectError(
validate(parseSimpleBinding('{{exp}}')),
'Got interpolation ({{}}) where expression was expected');
});
it('should report when encountering field write', () => {
expectError(validate(parseSimpleBinding('a = b')), 'Bindings cannot contain assignments');
});
});
describe('wrapLiteralPrimitive', () => {
it('should wrap a literal primitive', () => {
expect(unparse(validate(createParser().wrapLiteralPrimitive('foo', null))))
.toEqual('"foo"');
});
});
describe('error recovery', () => {
function recover(text: string, expected?: string) {
const expr = validate(parseAction(text));
expect(unparse(expr)).toEqual(expected || text);
}
it('should be able to recover from an extra paren', () => recover('((a)))', 'a'));
it('should be able to recover from an extra bracket', () => recover('[[a]]]', '[[a]]'));
it('should be able to recover from a missing )', () => recover('(a;b', 'a; b;'));
it('should be able to recover from a missing ]', () => recover('[a,b', '[a, b]'));
it('should be able to recover from a missing selector', () => recover('a.'));
it('should be able to recover from a missing selector in a array literal',
() => recover('[[a.], b, c]'));
});
describe('offsets', () => {
it('should retain the offsets of an interpolation', () => {
const interpolations = splitInterpolation('{{a}} {{b}} {{c}}');
expect(interpolations.offsets).toEqual([2, 9, 16]);
});
it('should retain the offsets into the expression AST of interpolations', () => {
const source = parseInterpolation('{{a}} {{b}} {{c}}');
const interpolation = source.ast as Interpolation;
expect(interpolation.expressions.map(e => e.span.start)).toEqual([2, 9, 16]);
});
});
});
}

View File

@ -0,0 +1,188 @@
/**
* @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 {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '../../src/expression_parser/ast';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/ml_parser/interpolation_config';
class Unparser implements AstVisitor {
private static _quoteRegExp = /"/g;
private _expression: string;
private _interpolationConfig: InterpolationConfig;
unparse(ast: AST, interpolationConfig: InterpolationConfig) {
this._expression = '';
this._interpolationConfig = interpolationConfig;
this._visit(ast);
return this._expression;
}
visitPropertyRead(ast: PropertyRead, context: any) {
this._visit(ast.receiver);
this._expression += ast.receiver instanceof ImplicitReceiver ? `${ast.name}` : `.${ast.name}`;
}
visitPropertyWrite(ast: PropertyWrite, context: any) {
this._visit(ast.receiver);
this._expression +=
ast.receiver instanceof ImplicitReceiver ? `${ast.name} = ` : `.${ast.name} = `;
this._visit(ast.value);
}
visitBinary(ast: Binary, context: any) {
this._visit(ast.left);
this._expression += ` ${ast.operation} `;
this._visit(ast.right);
}
visitChain(ast: Chain, context: any) {
const len = ast.expressions.length;
for (let i = 0; i < len; i++) {
this._visit(ast.expressions[i]);
this._expression += i == len - 1 ? ';' : '; ';
}
}
visitConditional(ast: Conditional, context: any) {
this._visit(ast.condition);
this._expression += ' ? ';
this._visit(ast.trueExp);
this._expression += ' : ';
this._visit(ast.falseExp);
}
visitPipe(ast: BindingPipe, context: any) {
this._expression += '(';
this._visit(ast.exp);
this._expression += ` | ${ast.name}`;
ast.args.forEach(arg => {
this._expression += ':';
this._visit(arg);
});
this._expression += ')';
}
visitFunctionCall(ast: FunctionCall, context: any) {
this._visit(ast.target);
this._expression += '(';
let isFirst = true;
ast.args.forEach(arg => {
if (!isFirst) this._expression += ', ';
isFirst = false;
this._visit(arg);
});
this._expression += ')';
}
visitImplicitReceiver(ast: ImplicitReceiver, context: any) {}
visitInterpolation(ast: Interpolation, context: any) {
for (let i = 0; i < ast.strings.length; i++) {
this._expression += ast.strings[i];
if (i < ast.expressions.length) {
this._expression += `${this._interpolationConfig.start} `;
this._visit(ast.expressions[i]);
this._expression += ` ${this._interpolationConfig.end}`;
}
}
}
visitKeyedRead(ast: KeyedRead, context: any) {
this._visit(ast.obj);
this._expression += '[';
this._visit(ast.key);
this._expression += ']';
}
visitKeyedWrite(ast: KeyedWrite, context: any) {
this._visit(ast.obj);
this._expression += '[';
this._visit(ast.key);
this._expression += '] = ';
this._visit(ast.value);
}
visitLiteralArray(ast: LiteralArray, context: any) {
this._expression += '[';
let isFirst = true;
ast.expressions.forEach(expression => {
if (!isFirst) this._expression += ', ';
isFirst = false;
this._visit(expression);
});
this._expression += ']';
}
visitLiteralMap(ast: LiteralMap, context: any) {
this._expression += '{';
let isFirst = true;
for (let i = 0; i < ast.keys.length; i++) {
if (!isFirst) this._expression += ', ';
isFirst = false;
this._expression += `${ast.keys[i]}: `;
this._visit(ast.values[i]);
}
this._expression += '}';
}
visitLiteralPrimitive(ast: LiteralPrimitive, context: any) {
if (typeof ast.value === 'string') {
this._expression += `"${ast.value.replace( Unparser._quoteRegExp, '\"')}"`;
} else {
this._expression += `${ast.value}`;
}
}
visitMethodCall(ast: MethodCall, context: any) {
this._visit(ast.receiver);
this._expression += ast.receiver instanceof ImplicitReceiver ? `${ast.name}(` : `.${ast.name}(`;
let isFirst = true;
ast.args.forEach(arg => {
if (!isFirst) this._expression += ', ';
isFirst = false;
this._visit(arg);
});
this._expression += ')';
}
visitPrefixNot(ast: PrefixNot, context: any) {
this._expression += '!';
this._visit(ast.expression);
}
visitSafePropertyRead(ast: SafePropertyRead, context: any) {
this._visit(ast.receiver);
this._expression += `?.${ast.name}`;
}
visitSafeMethodCall(ast: SafeMethodCall, context: any) {
this._visit(ast.receiver);
this._expression += `?.${ast.name}(`;
let isFirst = true;
ast.args.forEach(arg => {
if (!isFirst) this._expression += ', ';
isFirst = false;
this._visit(arg);
});
this._expression += ')';
}
visitQuote(ast: Quote, context: any) {
this._expression += `${ast.prefix}:${ast.uninterpretedExpression}`;
}
private _visit(ast: AST) { ast.visit(this); }
}
const sharedUnparser = new Unparser();
export function unparse(
ast: AST, interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): string {
return sharedUnparser.unparse(ast, interpolationConfig);
}

View File

@ -0,0 +1,123 @@
/**
* @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 {AST, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead} from '../../src/expression_parser/ast';
import {unparse} from './unparser';
class ASTValidator extends RecursiveAstVisitor {
private parentSpan: ParseSpan|undefined;
visit(ast: AST) {
this.parentSpan = undefined;
ast.visit(this);
}
validate(ast: AST, cb: () => void): void {
if (!inSpan(ast.span, this.parentSpan)) {
if (this.parentSpan) {
const parentSpan = this.parentSpan as ParseSpan;
throw Error(
`Invalid AST span [expected (${ast.span.start}, ${ast.span.end}) to be in (${parentSpan.start}, ${parentSpan.end}) for ${unparse(ast)}`);
} else {
throw Error(`Invalid root AST span for ${unparse(ast)}`);
}
}
const oldParent = this.parentSpan;
this.parentSpan = ast.span;
cb();
this.parentSpan = oldParent;
}
visitBinary(ast: Binary, context: any): any {
this.validate(ast, () => super.visitBinary(ast, context));
}
visitChain(ast: Chain, context: any): any {
this.validate(ast, () => super.visitChain(ast, context));
}
visitConditional(ast: Conditional, context: any): any {
this.validate(ast, () => super.visitConditional(ast, context));
}
visitFunctionCall(ast: FunctionCall, context: any): any {
this.validate(ast, () => super.visitFunctionCall(ast, context));
}
visitImplicitReceiver(ast: ImplicitReceiver, context: any): any {
this.validate(ast, () => super.visitImplicitReceiver(ast, context));
}
visitInterpolation(ast: Interpolation, context: any): any {
this.validate(ast, () => super.visitInterpolation(ast, context));
}
visitKeyedRead(ast: KeyedRead, context: any): any {
this.validate(ast, () => super.visitKeyedRead(ast, context));
}
visitKeyedWrite(ast: KeyedWrite, context: any): any {
this.validate(ast, () => super.visitKeyedWrite(ast, context));
}
visitLiteralArray(ast: LiteralArray, context: any): any {
this.validate(ast, () => super.visitLiteralArray(ast, context));
}
visitLiteralMap(ast: LiteralMap, context: any): any {
this.validate(ast, () => super.visitLiteralMap(ast, context));
}
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): any {
this.validate(ast, () => super.visitLiteralPrimitive(ast, context));
}
visitMethodCall(ast: MethodCall, context: any): any {
this.validate(ast, () => super.visitMethodCall(ast, context));
}
visitPipe(ast: BindingPipe, context: any): any {
this.validate(ast, () => super.visitPipe(ast, context));
}
visitPrefixNot(ast: PrefixNot, context: any): any {
this.validate(ast, () => super.visitPrefixNot(ast, context));
}
visitPropertyRead(ast: PropertyRead, context: any): any {
this.validate(ast, () => super.visitPropertyRead(ast, context));
}
visitPropertyWrite(ast: PropertyWrite, context: any): any {
this.validate(ast, () => super.visitPropertyWrite(ast, context));
}
visitQuote(ast: Quote, context: any): any {
this.validate(ast, () => super.visitQuote(ast, context));
}
visitSafeMethodCall(ast: SafeMethodCall, context: any): any {
this.validate(ast, () => super.visitSafeMethodCall(ast, context));
}
visitSafePropertyRead(ast: SafePropertyRead, context: any): any {
this.validate(ast, () => super.visitSafePropertyRead(ast, context));
}
}
function inSpan(span: ParseSpan, parentSpan: ParseSpan | undefined): parentSpan is ParseSpan {
return !parentSpan || (span.start >= parentSpan.start && span.end <= parentSpan.end);
}
const sharedValidator = new ASTValidator();
export function validate<T extends AST>(ast: T): T {
sharedValidator.visit(ast);
return ast;
}

View File

@ -0,0 +1,119 @@
/**
* @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 {computeMsgId, digest, sha1} from '../../src/i18n/digest';
export function main(): void {
describe('digest', () => {
describe('digest', () => {
it('must return the ID if it\'s explicit', () => {
expect(digest({
id: 'i',
nodes: [],
placeholders: {},
placeholderToMessage: {},
meaning: '',
description: '',
})).toEqual('i');
});
});
describe('sha1', () => {
it('should work on empty strings',
() => { expect(sha1('')).toEqual('da39a3ee5e6b4b0d3255bfef95601890afd80709'); });
it('should returns the sha1 of "hello world"',
() => { expect(sha1('abc')).toEqual('a9993e364706816aba3e25717850c26c9cd0d89d'); });
it('should returns the sha1 of unicode strings',
() => { expect(sha1('你好,世界')).toEqual('3becb03b015ed48050611c8d7afe4b88f70d5a20'); });
it('should support arbitrary string size', () => {
// node.js reference code:
//
// var crypto = require('crypto');
//
// function sha1(string) {
// var shasum = crypto.createHash('sha1');
// shasum.update(string, 'utf8');
// return shasum.digest('hex', 'utf8');
// }
//
// var prefix = `你好,世界`;
// var result = sha1(prefix);
// for (var size = prefix.length; size < 5000; size += 101) {
// result = prefix + sha1(result);
// while (result.length < size) {
// result += result;
// }
// result = result.slice(-size);
// }
//
// console.log(sha1(result));
const prefix = `你好,世界`;
let result = sha1(prefix);
for (let size = prefix.length; size < 5000; size += 101) {
result = prefix + sha1(result);
while (result.length < size) {
result += result;
}
result = result.slice(-size);
}
expect(sha1(result)).toEqual('24c2dae5c1ac6f604dbe670a60290d7ce6320b45');
});
});
describe('decimal fingerprint', () => {
it('should work on well known inputs w/o meaning', () => {
const fixtures: {[msg: string]: string} = {
' Spaced Out ': '3976450302996657536',
'Last Name': '4407559560004943843',
'First Name': '6028371114637047813',
'View': '2509141182388535183',
'START_BOLDNUMEND_BOLD of START_BOLDmillionsEND_BOLD': '29997634073898638',
'The customer\'s credit card was authorized for AMOUNT and passed all risk checks.':
'6836487644149622036',
'Hello world!': '3022994926184248873',
'Jalape\u00f1o': '8054366208386598941',
'The set of SET_NAME is {XXX, ...}.': '135956960462609535',
'NAME took a trip to DESTINATION.': '768490705511913603',
'by AUTHOR (YEAR)': '7036633296476174078',
'': '4416290763660062288',
};
Object.keys(fixtures).forEach(
msg => { expect(computeMsgId(msg, '')).toEqual(fixtures[msg]); });
});
it('should work on well known inputs with meaning', () => {
const fixtures: {[msg: string]: [string, string]} = {
'7790835225175622807': ['Last Name', 'Gmail UI'],
'1809086297585054940': ['First Name', 'Gmail UI'],
'3993998469942805487': ['View', 'Gmail UI'],
};
Object.keys(fixtures).forEach(
id => { expect(computeMsgId(fixtures[id][0], fixtures[id][1])).toEqual(id); });
});
it('should support arbitrary string size', () => {
const prefix = `你好,世界`;
let result = computeMsgId(prefix, '');
for (let size = prefix.length; size < 5000; size += 101) {
result = prefix + computeMsgId(result, '');
while (result.length < size) {
result += result;
}
result = result.slice(-size);
}
expect(computeMsgId(result, '')).toEqual('2122606631351252558');
});
});
});
}

View File

@ -0,0 +1,524 @@
/**
* @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 {DEFAULT_INTERPOLATION_CONFIG, HtmlParser} from '@angular/compiler';
import {digest, serializeNodes as serializeI18nNodes} from '../../src/i18n/digest';
import {extractMessages, mergeTranslations} from '../../src/i18n/extractor_merger';
import * as i18n from '../../src/i18n/i18n_ast';
import {TranslationBundle} from '../../src/i18n/translation_bundle';
import * as html from '../../src/ml_parser/ast';
import {serializeNodes as serializeHtmlNodes} from '../ml_parser/ast_serializer_spec';
export function main() {
describe('Extractor', () => {
describe('elements', () => {
it('should extract from elements', () => {
expect(extract('<div i18n="m|d|e">text<span>nested</span></div>')).toEqual([
[
['text', '<ph tag name="START_TAG_SPAN">nested</ph name="CLOSE_TAG_SPAN">'], 'm', 'd|e',
''
],
]);
});
it('should extract from attributes', () => {
expect(
extract(
'<div i18n="m1|d1"><span i18n-title="m2|d2" title="single child">nested</span></div>'))
.toEqual([
[['<ph tag name="START_TAG_SPAN">nested</ph name="CLOSE_TAG_SPAN">'], 'm1', 'd1', ''],
[['single child'], 'm2', 'd2', ''],
]);
});
it('should extract from attributes with id', () => {
expect(
extract(
'<div i18n="m1|d1@@i1"><span i18n-title="m2|d2@@i2" title="single child">nested</span></div>'))
.toEqual([
[
['<ph tag name="START_TAG_SPAN">nested</ph name="CLOSE_TAG_SPAN">'], 'm1', 'd1',
'i1'
],
[['single child'], 'm2', 'd2', 'i2'],
]);
});
it('should extract from attributes without meaning and with id', () => {
expect(
extract(
'<div i18n="d1@@i1"><span i18n-title="d2@@i2" title="single child">nested</span></div>'))
.toEqual([
[['<ph tag name="START_TAG_SPAN">nested</ph name="CLOSE_TAG_SPAN">'], '', 'd1', 'i1'],
[['single child'], '', 'd2', 'i2'],
]);
});
it('should extract from attributes with id only', () => {
expect(
extract(
'<div i18n="@@i1"><span i18n-title="@@i2" title="single child">nested</span></div>'))
.toEqual([
[['<ph tag name="START_TAG_SPAN">nested</ph name="CLOSE_TAG_SPAN">'], '', '', 'i1'],
[['single child'], '', '', 'i2'],
]);
});
it('should extract from ICU messages', () => {
expect(
extract(
'<div i18n="m|d">{count, plural, =0 { <p i18n-title i18n-desc title="title" desc="desc"></p>}}</div>'))
.toEqual([
[
[
'{count, plural, =0 {[<ph tag name="START_PARAGRAPH"></ph name="CLOSE_PARAGRAPH">]}}'
],
'm', 'd', ''
],
[['title'], '', '', ''],
[['desc'], '', '', ''],
]);
});
it('should not create a message for empty elements',
() => { expect(extract('<div i18n="m|d"></div>')).toEqual([]); });
it('should ignore implicit elements in translatable elements', () => {
expect(extract('<div i18n="m|d"><p></p></div>', ['p'])).toEqual([
[['<ph tag name="START_PARAGRAPH"></ph name="CLOSE_PARAGRAPH">'], 'm', 'd', '']
]);
});
});
describe('blocks', () => {
it('should extract from blocks', () => {
expect(extract(`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
<!-- i18n: desc2 -->message2<!-- /i18n -->
<!-- i18n -->message3<!-- /i18n -->
<!-- i18n: meaning4|desc4@@id4 -->message4<!-- /i18n -->
<!-- i18n: @@id5 -->message5<!-- /i18n -->`))
.toEqual([
[['message1'], 'meaning1', 'desc1', ''], [['message2'], '', 'desc2', ''],
[['message3'], '', '', ''], [['message4'], 'meaning4', 'desc4', 'id4'],
[['message5'], '', '', 'id5']
]);
});
it('should ignore implicit elements in blocks', () => {
expect(extract('<!-- i18n:m|d --><p></p><!-- /i18n -->', ['p'])).toEqual([
[['<ph tag name="START_PARAGRAPH"></ph name="CLOSE_PARAGRAPH">'], 'm', 'd', '']
]);
});
it('should extract siblings', () => {
expect(
extract(
`<!-- i18n -->text<p>html<b>nested</b></p>{count, plural, =0 {<span>html</span>}}{{interp}}<!-- /i18n -->`))
.toEqual([
[
[
'{count, plural, =0 {[<ph tag name="START_TAG_SPAN">html</ph name="CLOSE_TAG_SPAN">]}}'
],
'', '', ''
],
[
[
'text', '<ph tag name="START_PARAGRAPH">html, <ph tag' +
' name="START_BOLD_TEXT">nested</ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">',
'<ph icu name="ICU">{count, plural, =0 {[<ph tag' +
' name="START_TAG_SPAN">html</ph name="CLOSE_TAG_SPAN">]}}</ph>',
'[<ph name="INTERPOLATION">interp</ph>]'
],
'', '', ''
],
]);
});
it('should ignore other comments', () => {
expect(extract(`<!-- i18n: meaning1|desc1@@id1 --><!-- other -->message1<!-- /i18n -->`))
.toEqual([
[['message1'], 'meaning1', 'desc1', 'id1'],
]);
});
it('should not create a message for empty blocks',
() => { expect(extract(`<!-- i18n: meaning1|desc1 --><!-- /i18n -->`)).toEqual([]); });
});
describe('ICU messages', () => {
it('should extract ICU messages from translatable elements', () => {
// single message when ICU is the only children
expect(extract('<div i18n="m|d">{count, plural, =0 {text}}</div>')).toEqual([
[['{count, plural, =0 {[text]}}'], 'm', 'd', ''],
]);
// single message when ICU is the only (implicit) children
expect(extract('<div>{count, plural, =0 {text}}</div>', ['div'])).toEqual([
[['{count, plural, =0 {[text]}}'], '', '', ''],
]);
// one message for the element content and one message for the ICU
expect(extract('<div i18n="m|d@@i">before{count, plural, =0 {text}}after</div>')).toEqual([
[
['before', '<ph icu name="ICU">{count, plural, =0 {[text]}}</ph>', 'after'], 'm', 'd',
'i'
],
[['{count, plural, =0 {[text]}}'], '', '', ''],
]);
});
it('should extract ICU messages from translatable block', () => {
// single message when ICU is the only children
expect(extract('<!-- i18n:m|d -->{count, plural, =0 {text}}<!-- /i18n -->')).toEqual([
[['{count, plural, =0 {[text]}}'], 'm', 'd', ''],
]);
// one message for the block content and one message for the ICU
expect(extract('<!-- i18n:m|d -->before{count, plural, =0 {text}}after<!-- /i18n -->'))
.toEqual([
[['{count, plural, =0 {[text]}}'], '', '', ''],
[
['before', '<ph icu name="ICU">{count, plural, =0 {[text]}}</ph>', 'after'], 'm',
'd', ''
],
]);
});
it('should not extract ICU messages outside of i18n sections',
() => { expect(extract('{count, plural, =0 {text}}')).toEqual([]); });
it('should ignore nested ICU messages', () => {
expect(extract('<div i18n="m|d">{count, plural, =0 { {sex, select, male {m}} }}</div>'))
.toEqual([
[['{count, plural, =0 {[{sex, select, male {[m]}}, ]}}'], 'm', 'd', ''],
]);
});
it('should ignore implicit elements in non translatable ICU messages', () => {
expect(extract(
'<div i18n="m|d@@i">{count, plural, =0 { {sex, select, male {<p>ignore</p>}}' +
' }}</div>',
['p']))
.toEqual([[
[
'{count, plural, =0 {[{sex, select, male {[<ph tag name="START_PARAGRAPH">ignore</ph name="CLOSE_PARAGRAPH">]}}, ]}}'
],
'm', 'd', 'i'
]]);
});
it('should ignore implicit elements in non translatable ICU messages', () => {
expect(extract('{count, plural, =0 { {sex, select, male {<p>ignore</p>}} }}', ['p']))
.toEqual([]);
});
});
describe('attributes', () => {
it('should extract from attributes outside of translatable sections', () => {
expect(extract('<div i18n-title="m|d@@i" title="msg"></div>')).toEqual([
[['msg'], 'm', 'd', 'i'],
]);
});
it('should extract from attributes in translatable elements', () => {
expect(extract('<div i18n><p><b i18n-title="m|d@@i" title="msg"></b></p></div>')).toEqual([
[
['<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph' +
' name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'],
'', '', ''
],
[['msg'], 'm', 'd', 'i'],
]);
});
it('should extract from attributes in translatable blocks', () => {
expect(extract('<!-- i18n --><p><b i18n-title="m|d" title="msg"></b></p><!-- /i18n -->'))
.toEqual([
[['msg'], 'm', 'd', ''],
[
['<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph' +
' name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'],
'', '', ''
],
]);
});
it('should extract from attributes in translatable ICUs', () => {
expect(extract(`<!-- i18n -->{count, plural, =0 {<p><b i18n-title="m|d@@i"
title="msg"></b></p>}}<!-- /i18n -->`))
.toEqual([
[['msg'], 'm', 'd', 'i'],
[
[
'{count, plural, =0 {[<ph tag name="START_PARAGRAPH"><ph tag' +
' name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">]}}'
],
'', '', ''
],
]);
});
it('should extract from attributes in non translatable ICUs', () => {
expect(extract('{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'))
.toEqual([
[['msg'], 'm', 'd', ''],
]);
});
it('should not create a message for empty attributes',
() => { expect(extract('<div i18n-title="m|d" title></div>')).toEqual([]); });
});
describe('implicit elements', () => {
it('should extract from implicit elements', () => {
expect(extract('<b>bold</b><i>italic</i>', ['b'])).toEqual([
[['bold'], '', '', ''],
]);
});
it('should allow nested implicit elements', () => {
let result: any[];
expect(() => {
result = extract('<div>outer<div>inner</div></div>', ['div']);
}).not.toThrow();
expect(result).toEqual([
[['outer', '<ph tag name="START_TAG_DIV">inner</ph name="CLOSE_TAG_DIV">'], '', '', ''],
]);
});
});
describe('implicit attributes', () => {
it('should extract implicit attributes', () => {
expect(extract('<b title="bb">bold</b><i title="ii">italic</i>', [], {'b': ['title']}))
.toEqual([
[['bb'], '', '', ''],
]);
});
});
describe('errors', () => {
describe('elements', () => {
it('should report nested translatable elements', () => {
expect(extractErrors(`<p i18n><b i18n></b></p>`)).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
]);
});
it('should report translatable elements in implicit elements', () => {
expect(extractErrors(`<p><b i18n></b></p>`, ['p'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
]);
});
it('should report translatable elements in translatable blocks', () => {
expect(extractErrors(`<!-- i18n --><b i18n></b><!-- /i18n -->`)).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
]);
});
});
describe('blocks', () => {
it('should report nested blocks', () => {
expect(extractErrors(`<!-- i18n --><!-- i18n --><!-- /i18n --><!-- /i18n -->`)).toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report unclosed blocks', () => {
expect(extractErrors(`<!-- i18n -->`)).toEqual([
['Unclosed block', '<!--'],
]);
});
it('should report translatable blocks in translatable elements', () => {
expect(extractErrors(`<p i18n><!-- i18n --><!-- /i18n --></p>`)).toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report translatable blocks in implicit elements', () => {
expect(extractErrors(`<p><!-- i18n --><!-- /i18n --></p>`, ['p'])).toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report when start and end of a block are not at the same level', () => {
expect(extractErrors(`<!-- i18n --><p><!-- /i18n --></p>`)).toEqual([
['I18N blocks should not cross element boundaries', '<!--'],
['Unclosed block', '<p>'],
]);
expect(extractErrors(`<p><!-- i18n --></p><!-- /i18n -->`)).toEqual([
['I18N blocks should not cross element boundaries', '<!--'],
['Unclosed block', '<!--'],
]);
});
});
});
});
describe('Merger', () => {
describe('elements', () => {
it('should merge elements', () => {
const HTML = `<p i18n="m|d">foo</p>`;
expect(fakeTranslate(HTML)).toEqual('<p>**foo**</p>');
});
it('should merge nested elements', () => {
const HTML = `<div>before<p i18n="m|d">foo</p><!-- comment --></div>`;
expect(fakeTranslate(HTML)).toEqual('<div>before<p>**foo**</p></div>');
});
it('should merge empty messages', () => {
const HTML = `<div i18n>some element</div>`;
const htmlNodes: html.Node[] = parseHtml(HTML);
const messages: i18n.Message[] =
extractMessages(htmlNodes, DEFAULT_INTERPOLATION_CONFIG, [], {}).messages;
expect(messages.length).toEqual(1);
const i18nMsgMap: {[id: string]: i18n.Node[]} = {};
i18nMsgMap[digest(messages[0])] = [];
const translations = new TranslationBundle(i18nMsgMap, null, digest);
const output =
mergeTranslations(htmlNodes, translations, DEFAULT_INTERPOLATION_CONFIG, [], {});
expect(output.errors).toEqual([]);
expect(serializeHtmlNodes(output.rootNodes).join('')).toEqual(`<div></div>`);
});
});
describe('blocks', () => {
it('should merge blocks', () => {
const HTML = `before<!-- i18n --><p>foo</p><span><i>bar</i></span><!-- /i18n -->after`;
expect(fakeTranslate(HTML))
.toEqual(
'before**[ph tag name="START_PARAGRAPH">foo[/ph name="CLOSE_PARAGRAPH">[ph tag' +
' name="START_TAG_SPAN">[ph tag name="START_ITALIC_TEXT">bar[/ph' +
' name="CLOSE_ITALIC_TEXT">[/ph name="CLOSE_TAG_SPAN">**after');
});
it('should merge nested blocks', () => {
const HTML =
`<div>before<!-- i18n --><p>foo</p><span><i>bar</i></span><!-- /i18n -->after</div>`;
expect(fakeTranslate(HTML))
.toEqual(
'<div>before**[ph tag name="START_PARAGRAPH">foo[/ph name="CLOSE_PARAGRAPH">[ph' +
' tag name="START_TAG_SPAN">[ph tag name="START_ITALIC_TEXT">bar[/ph' +
' name="CLOSE_ITALIC_TEXT">[/ph name="CLOSE_TAG_SPAN">**after</div>');
});
});
describe('attributes', () => {
it('should merge attributes', () => {
const HTML = `<p i18n-title="m|d" title="foo"></p>`;
expect(fakeTranslate(HTML)).toEqual('<p title="**foo**"></p>');
});
it('should merge nested attributes', () => {
const HTML = `<div>{count, plural, =0 {<p i18n-title title="foo"></p>}}</div>`;
expect(fakeTranslate(HTML))
.toEqual('<div>{count, plural, =0 {<p title="**foo**"></p>}}</div>');
});
it('should merge attributes without values', () => {
const HTML = `<p i18n-title="m|d" title=""></p>`;
expect(fakeTranslate(HTML)).toEqual('<p title=""></p>');
});
it('should merge empty attributes', () => {
const HTML = `<div i18n-title title="some attribute">some element</div>`;
const htmlNodes: html.Node[] = parseHtml(HTML);
const messages: i18n.Message[] =
extractMessages(htmlNodes, DEFAULT_INTERPOLATION_CONFIG, [], {}).messages;
expect(messages.length).toEqual(1);
const i18nMsgMap: {[id: string]: i18n.Node[]} = {};
i18nMsgMap[digest(messages[0])] = [];
const translations = new TranslationBundle(i18nMsgMap, null, digest);
const output =
mergeTranslations(htmlNodes, translations, DEFAULT_INTERPOLATION_CONFIG, [], {});
expect(output.errors).toEqual([]);
expect(serializeHtmlNodes(output.rootNodes).join(''))
.toEqual(`<div title="">some element</div>`);
});
});
});
}
function parseHtml(html: string): html.Node[] {
const htmlParser = new HtmlParser();
const parseResult = htmlParser.parse(html, 'extractor spec', true);
if (parseResult.errors.length > 1) {
throw new Error(`unexpected parse errors: ${parseResult.errors.join('\n')}`);
}
return parseResult.rootNodes;
}
function fakeTranslate(
content: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): string {
const htmlNodes: html.Node[] = parseHtml(content);
const messages: i18n.Message[] =
extractMessages(htmlNodes, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs)
.messages;
const i18nMsgMap: {[id: string]: i18n.Node[]} = {};
messages.forEach(message => {
const id = digest(message);
const text = serializeI18nNodes(message.nodes).join('').replace(/</g, '[');
i18nMsgMap[id] = [new i18n.Text(`**${text}**`, null)];
});
const translations = new TranslationBundle(i18nMsgMap, null, digest);
const output = mergeTranslations(
htmlNodes, translations, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs);
expect(output.errors).toEqual([]);
return serializeHtmlNodes(output.rootNodes).join('');
}
function extract(
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): [string[], string, string][] {
const result =
extractMessages(parseHtml(html), DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs);
if (result.errors.length > 0) {
throw new Error(`unexpected errors: ${result.errors.join('\n')}`);
}
// clang-format off
// https://github.com/angular/clang-format/issues/35
return result.messages.map(
message => [serializeI18nNodes(message.nodes), message.meaning, message.description, message.id]) as [string[], string, string][];
// clang-format on
}
function extractErrors(
html: string, implicitTags: string[] = [], implicitAttrs: {[k: string]: string[]} = {}): any[] {
const errors =
extractMessages(parseHtml(html), DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs)
.errors;
return errors.map((e): [string, string] => [e.msg, e.span.toString()]);
}

View File

@ -0,0 +1,47 @@
/**
* @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 {I18NHtmlParser} from '@angular/compiler/src/i18n/i18n_html_parser';
import {TranslationBundle} from '@angular/compiler/src/i18n/translation_bundle';
import {HtmlParser} from '@angular/compiler/src/ml_parser/html_parser';
import {ParseTreeResult} from '@angular/compiler/src/ml_parser/parser';
export function main() {
describe('I18N html parser', () => {
it('should return the html nodes when no translations are given', () => {
const htmlParser = new HtmlParser();
const i18nHtmlParser = new I18NHtmlParser(htmlParser);
const ptResult = new ParseTreeResult([], []);
spyOn(htmlParser, 'parse').and.returnValue(ptResult);
spyOn(i18nHtmlParser, 'parse').and.callThrough();
expect(i18nHtmlParser.parse('source', 'url')).toBe(ptResult);
expect(htmlParser.parse).toHaveBeenCalledTimes(1);
expect(htmlParser.parse)
.toHaveBeenCalledWith('source', 'url', jasmine.anything(), jasmine.anything());
});
// https://github.com/angular/angular/issues/14322
it('should parse the translations only once', () => {
const transBundle = new TranslationBundle({}, null, () => 'id');
spyOn(TranslationBundle, 'load').and.returnValue(transBundle);
const htmlParser = new HtmlParser();
const i18nHtmlParser = new I18NHtmlParser(htmlParser, 'translations');
expect(TranslationBundle.load).toHaveBeenCalledTimes(1);
i18nHtmlParser.parse('source', 'url');
i18nHtmlParser.parse('source', 'url');
expect(TranslationBundle.load).toHaveBeenCalledTimes(1);
});
});
}

View File

@ -0,0 +1,335 @@
/**
* @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 {digest, serializeNodes} from '@angular/compiler/src/i18n/digest';
import {extractMessages} from '@angular/compiler/src/i18n/extractor_merger';
import {Message} from '@angular/compiler/src/i18n/i18n_ast';
import {HtmlParser} from '@angular/compiler/src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '@angular/compiler/src/ml_parser/interpolation_config';
export function main() {
describe('I18nParser', () => {
describe('elements', () => {
it('should extract from elements', () => {
expect(_humanizeMessages('<div i18n="m|d">text</div>')).toEqual([
[['text'], 'm', 'd'],
]);
});
it('should extract from nested elements', () => {
expect(_humanizeMessages('<div i18n="m|d">text<span><b>nested</b></span></div>')).toEqual([
[
[
'text',
'<ph tag name="START_TAG_SPAN"><ph tag name="START_BOLD_TEXT">nested</ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_TAG_SPAN">'
],
'm', 'd'
],
]);
});
it('should not create a message for empty elements',
() => { expect(_humanizeMessages('<div i18n="m|d"></div>')).toEqual([]); });
it('should not create a message for plain elements',
() => { expect(_humanizeMessages('<div></div>')).toEqual([]); });
it('should suppoprt void elements', () => {
expect(_humanizeMessages('<div i18n="m|d"><p><br></p></div>')).toEqual([
[
[
'<ph tag name="START_PARAGRAPH"><ph tag name="LINE_BREAK"/></ph name="CLOSE_PARAGRAPH">'
],
'm', 'd'
],
]);
});
});
describe('attributes', () => {
it('should extract from attributes outside of translatable section', () => {
expect(_humanizeMessages('<div i18n-title="m|d" title="msg"></div>')).toEqual([
[['msg'], 'm', 'd'],
]);
});
it('should extract from attributes in translatable element', () => {
expect(_humanizeMessages('<div i18n><p><b i18n-title="m|d" title="msg"></b></p></div>'))
.toEqual([
[
[
'<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'
],
'', ''
],
[['msg'], 'm', 'd'],
]);
});
it('should extract from attributes in translatable block', () => {
expect(_humanizeMessages(
'<!-- i18n --><p><b i18n-title="m|d" title="msg"></b></p><!-- /i18n -->'))
.toEqual([
[['msg'], 'm', 'd'],
[
[
'<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'
],
'', ''
],
]);
});
it('should extract from attributes in translatable ICU', () => {
expect(
_humanizeMessages(
'<!-- i18n -->{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}<!-- /i18n -->'))
.toEqual([
[['msg'], 'm', 'd'],
[
[
'{count, plural, =0 {[<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">]}}'
],
'', ''
],
]);
});
it('should extract from attributes in non translatable ICU', () => {
expect(
_humanizeMessages('{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'))
.toEqual([
[['msg'], 'm', 'd'],
]);
});
it('should not create a message for empty attributes',
() => { expect(_humanizeMessages('<div i18n-title="m|d" title></div>')).toEqual([]); });
});
describe('interpolation', () => {
it('should replace interpolation with placeholder', () => {
expect(_humanizeMessages('<div i18n="m|d">before{{ exp }}after</div>')).toEqual([
[['[before, <ph name="INTERPOLATION"> exp </ph>, after]'], 'm', 'd'],
]);
});
it('should support named interpolation', () => {
expect(_humanizeMessages('<div i18n="m|d">before{{ exp //i18n(ph="teSt") }}after</div>'))
.toEqual([
[['[before, <ph name="TEST"> exp //i18n(ph="teSt") </ph>, after]'], 'm', 'd'],
]);
});
});
describe('blocks', () => {
it('should extract from blocks', () => {
expect(_humanizeMessages(`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
<!-- i18n: desc2 -->message2<!-- /i18n -->
<!-- i18n -->message3<!-- /i18n -->`))
.toEqual([
[['message1'], 'meaning1', 'desc1'],
[['message2'], '', 'desc2'],
[['message3'], '', ''],
]);
});
it('should extract all siblings', () => {
expect(_humanizeMessages(`<!-- i18n -->text<p>html<b>nested</b></p><!-- /i18n -->`)).toEqual([
[
[
'text',
'<ph tag name="START_PARAGRAPH">html, <ph tag name="START_BOLD_TEXT">nested</ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'
],
'', ''
],
]);
});
});
describe('ICU messages', () => {
it('should extract as ICU when single child of an element', () => {
expect(_humanizeMessages('<div i18n="m|d">{count, plural, =0 {zero}}</div>')).toEqual([
[['{count, plural, =0 {[zero]}}'], 'm', 'd'],
]);
});
it('should extract as ICU + ph when not single child of an element', () => {
expect(_humanizeMessages('<div i18n="m|d">b{count, plural, =0 {zero}}a</div>')).toEqual([
[['b', '<ph icu name="ICU">{count, plural, =0 {[zero]}}</ph>', 'a'], 'm', 'd'],
[['{count, plural, =0 {[zero]}}'], '', ''],
]);
});
it('should extract as ICU when single child of a block', () => {
expect(_humanizeMessages('<!-- i18n:m|d -->{count, plural, =0 {zero}}<!-- /i18n -->'))
.toEqual([
[['{count, plural, =0 {[zero]}}'], 'm', 'd'],
]);
});
it('should extract as ICU + ph when not single child of a block', () => {
expect(_humanizeMessages('<!-- i18n:m|d -->b{count, plural, =0 {zero}}a<!-- /i18n -->'))
.toEqual([
[['{count, plural, =0 {[zero]}}'], '', ''],
[['b', '<ph icu name="ICU">{count, plural, =0 {[zero]}}</ph>', 'a'], 'm', 'd'],
]);
});
it('should not extract nested ICU messages', () => {
expect(_humanizeMessages(
'<div i18n="m|d">b{count, plural, =0 {{sex, select, male {m}}}}a</div>'))
.toEqual([
[
[
'b', '<ph icu name="ICU">{count, plural, =0 {[{sex, select, male {[m]}}]}}</ph>',
'a'
],
'm', 'd'
],
[['{count, plural, =0 {[{sex, select, male {[m]}}]}}'], '', ''],
]);
});
});
describe('implicit elements', () => {
it('should extract from implicit elements', () => {
expect(_humanizeMessages('<b>bold</b><i>italic</i>', ['b'])).toEqual([
[['bold'], '', ''],
]);
});
});
describe('implicit attributes', () => {
it('should extract implicit attributes', () => {
expect(_humanizeMessages(
'<b title="bb">bold</b><i title="ii">italic</i>', [], {'b': ['title']}))
.toEqual([
[['bb'], '', ''],
]);
});
});
describe('placeholders', () => {
it('should reuse the same placeholder name for tags', () => {
const html = '<div i18n="m|d"><p>one</p><p>two</p><p other>three</p></div>';
expect(_humanizeMessages(html)).toEqual([
[
[
'<ph tag name="START_PARAGRAPH">one</ph name="CLOSE_PARAGRAPH">',
'<ph tag name="START_PARAGRAPH">two</ph name="CLOSE_PARAGRAPH">',
'<ph tag name="START_PARAGRAPH_1">three</ph name="CLOSE_PARAGRAPH">',
],
'm', 'd'
],
]);
expect(_humanizePlaceholders(html)).toEqual([
'START_PARAGRAPH=<p>, CLOSE_PARAGRAPH=</p>, START_PARAGRAPH_1=<p other>',
]);
});
it('should reuse the same placeholder name for interpolations', () => {
const html = '<div i18n="m|d">{{ a }}{{ a }}{{ b }}</div>';
expect(_humanizeMessages(html)).toEqual([
[
[
'[<ph name="INTERPOLATION"> a </ph>, <ph name="INTERPOLATION"> a </ph>, <ph name="INTERPOLATION_1"> b </ph>]'
],
'm', 'd'
],
]);
expect(_humanizePlaceholders(html)).toEqual([
'INTERPOLATION={{ a }}, INTERPOLATION_1={{ b }}',
]);
});
it('should reuse the same placeholder name for icu messages', () => {
const html =
'<div i18n="m|d">{count, plural, =0 {0}}{count, plural, =0 {0}}{count, plural, =1 {1}}</div>';
expect(_humanizeMessages(html)).toEqual([
[
[
'<ph icu name="ICU">{count, plural, =0 {[0]}}</ph>',
'<ph icu name="ICU">{count, plural, =0 {[0]}}</ph>',
'<ph icu name="ICU_1">{count, plural, =1 {[1]}}</ph>',
],
'm', 'd'
],
[['{count, plural, =0 {[0]}}'], '', ''],
[['{count, plural, =0 {[0]}}'], '', ''],
[['{count, plural, =1 {[1]}}'], '', ''],
]);
expect(_humanizePlaceholders(html)).toEqual([
'',
'VAR_PLURAL=count',
'VAR_PLURAL=count',
'VAR_PLURAL=count',
]);
expect(_humanizePlaceholdersToMessage(html)).toEqual([
'ICU=f0f76923009914f1b05f41042a5c7231b9496504, ICU_1=73693d1f78d0fc882f0bcbce4cb31a0aa1995cfe',
'',
'',
'',
]);
});
});
});
}
export function _humanizeMessages(
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): [string[], string, string][] {
// clang-format off
// https://github.com/angular/clang-format/issues/35
return _extractMessages(html, implicitTags, implicitAttrs).map(
message => [serializeNodes(message.nodes), message.meaning, message.description, ]) as [string[], string, string][];
// clang-format on
}
function _humanizePlaceholders(
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): string[] {
// clang-format off
// https://github.com/angular/clang-format/issues/35
return _extractMessages(html, implicitTags, implicitAttrs).map(
msg => Object.keys(msg.placeholders).map((name) => `${name}=${msg.placeholders[name]}`).join(', '));
// clang-format on
}
function _humanizePlaceholdersToMessage(
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): string[] {
// clang-format off
// https://github.com/angular/clang-format/issues/35
return _extractMessages(html, implicitTags, implicitAttrs).map(
msg => Object.keys(msg.placeholderToMessage).map(k => `${k}=${digest(msg.placeholderToMessage[k])}`).join(', '));
// clang-format on
}
export function _extractMessages(
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): Message[] {
const htmlParser = new HtmlParser();
const parseResult = htmlParser.parse(html, 'extractor spec', true);
if (parseResult.errors.length > 1) {
throw Error(`unexpected parse errors: ${parseResult.errors.join('\n')}`);
}
return extractMessages(
parseResult.rootNodes, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs)
.messages;
}

View File

@ -0,0 +1,253 @@
/**
* @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 {NgLocalization} from '@angular/common';
import {ResourceLoader} from '@angular/compiler';
import {MessageBundle} from '@angular/compiler/src/i18n/message_bundle';
import {Xmb} from '@angular/compiler/src/i18n/serializers/xmb';
import {HtmlParser} from '@angular/compiler/src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '@angular/compiler/src/ml_parser/interpolation_config';
import {Component, DebugElement, TRANSLATIONS, TRANSLATIONS_FORMAT} from '@angular/core';
import {TestBed, async} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {stringifyElement} from '@angular/platform-browser/testing/browser_util';
import {expect} from '@angular/platform-browser/testing/matchers';
import {SpyResourceLoader} from '../spies';
export function main() {
describe('i18n integration spec', () => {
beforeEach(async(() => {
TestBed.configureCompiler({
providers: [
{provide: ResourceLoader, useClass: SpyResourceLoader},
{provide: NgLocalization, useClass: FrLocalization},
{provide: TRANSLATIONS, useValue: XTB},
{provide: TRANSLATIONS_FORMAT, useValue: 'xtb'},
]
});
TestBed.configureTestingModule({declarations: [I18nComponent]});
}));
it('should extract from templates', () => {
const catalog = new MessageBundle(new HtmlParser, [], {});
const serializer = new Xmb();
catalog.updateFromTemplate(HTML, '', DEFAULT_INTERPOLATION_CONFIG);
expect(catalog.write(serializer)).toContain(XMB);
});
it('should translate templates', () => {
const tb = TestBed.overrideTemplate(I18nComponent, HTML).createComponent(I18nComponent);
const cmp = tb.componentInstance;
const el = tb.debugElement;
expectHtml(el, 'h1').toBe('<h1>attributs i18n sur les balises</h1>');
expectHtml(el, '#i18n-1').toBe('<div id="i18n-1"><p>imbriqué</p></div>');
expectHtml(el, '#i18n-2').toBe('<div id="i18n-2"><p>imbriqué</p></div>');
expectHtml(el, '#i18n-3')
.toBe('<div id="i18n-3"><p><i>avec des espaces réservés</i></p></div>');
expectHtml(el, '#i18n-3b')
.toBe(
'<div id="i18n-3b"><p><i class="preserved-on-placeholders">avec des espaces réservés</i></p></div>');
expectHtml(el, '#i18n-4')
.toBe('<p id="i18n-4" title="sur des balises non traductibles"></p>');
expectHtml(el, '#i18n-5').toBe('<p id="i18n-5" title="sur des balises traductibles"></p>');
expectHtml(el, '#i18n-6').toBe('<p id="i18n-6" title=""></p>');
cmp.count = 0;
tb.detectChanges();
expect(el.query(By.css('#i18n-7')).nativeElement).toHaveText('zero');
expect(el.query(By.css('#i18n-14')).nativeElement).toHaveText('zero');
cmp.count = 1;
tb.detectChanges();
expect(el.query(By.css('#i18n-7')).nativeElement).toHaveText('un');
expect(el.query(By.css('#i18n-14')).nativeElement).toHaveText('un');
expect(el.query(By.css('#i18n-17')).nativeElement).toHaveText('un');
cmp.count = 2;
tb.detectChanges();
expect(el.query(By.css('#i18n-7')).nativeElement).toHaveText('deux');
expect(el.query(By.css('#i18n-14')).nativeElement).toHaveText('deux');
expect(el.query(By.css('#i18n-17')).nativeElement).toHaveText('deux');
cmp.count = 3;
tb.detectChanges();
expect(el.query(By.css('#i18n-7')).nativeElement).toHaveText('beaucoup');
expect(el.query(By.css('#i18n-14')).nativeElement).toHaveText('beaucoup');
expect(el.query(By.css('#i18n-17')).nativeElement).toHaveText('beaucoup');
cmp.sex = 'm';
cmp.sexB = 'f';
tb.detectChanges();
expect(el.query(By.css('#i18n-8')).nativeElement).toHaveText('homme');
expect(el.query(By.css('#i18n-8b')).nativeElement).toHaveText('femme');
cmp.sex = 'f';
tb.detectChanges();
expect(el.query(By.css('#i18n-8')).nativeElement).toHaveText('femme');
cmp.count = 123;
tb.detectChanges();
expectHtml(el, '#i18n-9').toEqual('<div id="i18n-9">count = 123</div>');
cmp.sex = 'f';
tb.detectChanges();
expectHtml(el, '#i18n-10').toEqual('<div id="i18n-10">sexe = f</div>');
expectHtml(el, '#i18n-11').toEqual('<div id="i18n-11">custom name</div>');
expectHtml(el, '#i18n-12')
.toEqual('<h1 id="i18n-12">Balises dans les commentaires html</h1>');
expectHtml(el, '#i18n-13')
.toBe('<div id="i18n-13" title="dans une section traductible"></div>');
expectHtml(el, '#i18n-15').toMatch(/ca <b>devrait<\/b> marcher/);
expectHtml(el, '#i18n-16').toMatch(/avec un ID explicite/);
expectHtml(el, '#i18n-18')
.toEqual('<div id="i18n-18">FOO<a title="dans une section traductible">BAR</a></div>');
});
});
}
function expectHtml(el: DebugElement, cssSelector: string): any {
return expect(stringifyElement(el.query(By.css(cssSelector)).nativeElement));
}
@Component({
selector: 'i18n-cmp',
template: '',
})
class I18nComponent {
count: number;
sex: string;
sexB: string;
response: any = {getItemsList: (): any[] => []};
}
class FrLocalization extends NgLocalization {
getPluralCategory(value: number): string {
switch (value) {
case 0:
case 1:
return 'one';
default:
return 'other';
}
}
}
const XTB = `
<translationbundle>
<translation id="615790887472569365">attributs i18n sur les balises</translation>
<translation id="3707494640264351337">imbriqué</translation>
<translation id="5539162898278769904">imbriqué</translation>
<translation id="3780349238193953556"><ph name="START_ITALIC_TEXT"/>avec des espaces réservés<ph name="CLOSE_ITALIC_TEXT"/></translation>
<translation id="5525133077318024839">sur des balises non traductibles</translation>
<translation id="8670732454866344690">sur des balises traductibles</translation>
<translation id="4593805537723189714">{VAR_PLURAL, plural, =0 {zero} =1 {un} =2 {deux} other {<ph name="START_BOLD_TEXT"/>beaucoup<ph name="CLOSE_BOLD_TEXT"/>}}</translation>
<translation id="1746565782635215"><ph name="ICU"/></translation>
<translation id="5868084092545682515">{VAR_SELECT, select, m {homme} f {femme}}</translation>
<translation id="4851788426695310455"><ph name="INTERPOLATION"/></translation>
<translation id="9013357158046221374">sexe = <ph name="INTERPOLATION"/></translation>
<translation id="8324617391167353662"><ph name="CUSTOM_NAME"/></translation>
<translation id="7685649297917455806">dans une section traductible</translation>
<translation id="2387287228265107305">
<ph name="START_HEADING_LEVEL1"/>Balises dans les commentaires html<ph name="CLOSE_HEADING_LEVEL1"/>
<ph name="START_TAG_DIV"/><ph name="CLOSE_TAG_DIV"/>
<ph name="START_TAG_DIV_1"/><ph name="ICU"/><ph name="CLOSE_TAG_DIV"></ph>
</translation>
<translation id="1491627405349178954">ca <ph name="START_BOLD_TEXT"/>devrait<ph name="CLOSE_BOLD_TEXT"/> marcher</translation>
<translation id="i18n16">avec un ID explicite</translation>
<translation id="i18n17">{VAR_PLURAL, plural, =0 {zero} =1 {un} =2 {deux} other {<ph
name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>beaucoup<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>} }</translation>
<translation id="4085484936881858615">{VAR_PLURAL, plural, =0 {Pas de réponse} =1 {une réponse} other {<ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph> réponse} }</translation>
<translation id="4035252431381981115">FOO<ph name="START_LINK"><ex>&lt;a&gt;</ex></ph>BAR<ph name="CLOSE_LINK"><ex>&lt;/a&gt;</ex></ph></translation>
<translation id="5339604010413301604"><ph name="MAP_NAME"><ex>MAP_NAME</ex></ph></translation>
</translationbundle>`;
const XMB = ` <msg id="615790887472569365">i18n attribute on tags</msg>
<msg id="3707494640264351337">nested</msg>
<msg id="5539162898278769904" meaning="different meaning">nested</msg>
<msg id="3780349238193953556"><ph name="START_ITALIC_TEXT"><ex>&lt;i&gt;</ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex>&lt;/i&gt;</ex></ph></msg>
<msg id="5525133077318024839">on not translatable node</msg>
<msg id="8670732454866344690">on translatable node</msg>
<msg id="4593805537723189714">{VAR_PLURAL, plural, =0 {zero} =1 {one} =2 {two} other {<ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>} }</msg>
<msg id="1746565782635215">
<ph name="ICU"><ex>ICU</ex></ph>
</msg>
<msg id="5868084092545682515">{VAR_SELECT, select, m {male} f {female} }</msg>
<msg id="4851788426695310455"><ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph></msg>
<msg id="9013357158046221374">sex = <ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph></msg>
<msg id="8324617391167353662"><ph name="CUSTOM_NAME"><ex>CUSTOM_NAME</ex></ph></msg>
<msg id="7685649297917455806">in a translatable section</msg>
<msg id="2387287228265107305">
<ph name="START_HEADING_LEVEL1"><ex>&lt;h1&gt;</ex></ph>Markers in html comments<ph name="CLOSE_HEADING_LEVEL1"><ex>&lt;/h1&gt;</ex></ph>
<ph name="START_TAG_DIV"><ex>&lt;div&gt;</ex></ph><ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph>
<ph name="START_TAG_DIV_1"><ex>&lt;div&gt;</ex></ph><ph name="ICU"><ex>ICU</ex></ph><ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph>
</msg>
<msg id="1491627405349178954">it <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> work</msg>
<msg id="i18n16">with an explicit ID</msg>
<msg id="i18n17">{VAR_PLURAL, plural, =0 {zero} =1 {one} =2 {two} other {<ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>} }</msg>
<msg id="4085484936881858615" desc="desc">{VAR_PLURAL, plural, =0 {Found no results} =1 {Found one result} other {Found <ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph> results} }</msg>
<msg id="4035252431381981115">foo<ph name="START_LINK"><ex>&lt;a&gt;</ex></ph>bar<ph name="CLOSE_LINK"><ex>&lt;/a&gt;</ex></ph></msg>
<msg id="5339604010413301604"><ph name="MAP_NAME"><ex>MAP_NAME</ex></ph></msg>`;
const HTML = `
<div>
<h1 i18n>i18n attribute on tags</h1>
<div id="i18n-1"><p i18n>nested</p></div>
<div id="i18n-2"><p i18n="different meaning|">nested</p></div>
<div id="i18n-3"><p i18n><i>with placeholders</i></p></div>
<div id="i18n-3b"><p i18n><i class="preserved-on-placeholders">with placeholders</i></p></div>
<div>
<p id="i18n-4" i18n-title title="on not translatable node"></p>
<p id="i18n-5" i18n i18n-title title="on translatable node"></p>
<p id="i18n-6" i18n-title title></p>
</div>
<!-- no ph below because the ICU node is the only child of the div, i.e. no text nodes -->
<div i18n id="i18n-7">{count, plural, =0 {zero} =1 {one} =2 {two} other {<b>many</b>}}</div>
<div i18n id="i18n-8">
{sex, select, m {male} f {female}}
</div>
<div i18n id="i18n-8b">
{sexB, select, m {male} f {female}}
</div>
<div i18n id="i18n-9">{{ "count = " + count }}</div>
<div i18n id="i18n-10">sex = {{ sex }}</div>
<div i18n id="i18n-11">{{ "custom name" //i18n(ph="CUSTOM_NAME") }}</div>
</div>
<!-- i18n -->
<h1 id="i18n-12" >Markers in html comments</h1>
<div id="i18n-13" i18n-title title="in a translatable section"></div>
<div id="i18n-14">{count, plural, =0 {zero} =1 {one} =2 {two} other {<b>many</b>}}</div>
<!-- /i18n -->
<div id="i18n-15"><ng-container i18n>it <b>should</b> work</ng-container></div>
<div id="i18n-16" i18n="@@i18n16">with an explicit ID</div>
<div id="i18n-17" i18n="@@i18n17">{count, plural, =0 {zero} =1 {one} =2 {two} other {<b>many</b>}}</div>
<!-- make sure that ICU messages are not treated as text nodes -->
<div i18n="desc">{
response.getItemsList().length,
plural,
=0 {Found no results}
=1 {Found one result}
other {Found {{response.getItemsList().length}} results}
}</div>
<div i18n id="i18n-18">foo<a i18n-title title="in a translatable section">bar</a></div>
<div i18n>{{ 'test' //i18n(ph="map name") }}</div>
`;

View File

@ -0,0 +1,60 @@
/**
* @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 {serializeNodes} from '../../src/i18n/digest';
import * as i18n from '../../src/i18n/i18n_ast';
import {MessageBundle} from '../../src/i18n/message_bundle';
import {Serializer} from '../../src/i18n/serializers/serializer';
import {HtmlParser} from '../../src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
export function main(): void {
describe('MessageBundle', () => {
describe('Messages', () => {
let messages: MessageBundle;
beforeEach(() => { messages = new MessageBundle(new HtmlParser, [], {}); });
it('should extract the message to the catalog', () => {
messages.updateFromTemplate(
'<p i18n="m|d">Translate Me</p>', 'url', DEFAULT_INTERPOLATION_CONFIG);
expect(humanizeMessages(messages)).toEqual([
'Translate Me (m|d)',
]);
});
it('should extract and dedup messages', () => {
messages.updateFromTemplate(
'<p i18n="m|d@@1">Translate Me</p><p i18n="@@2">Translate Me</p><p i18n="@@2">Translate Me</p>',
'url', DEFAULT_INTERPOLATION_CONFIG);
expect(humanizeMessages(messages)).toEqual([
'Translate Me (m|d)',
'Translate Me (|)',
]);
});
});
});
}
class _TestSerializer extends Serializer {
write(messages: i18n.Message[]): string {
return messages.map(msg => `${serializeNodes(msg.nodes)} (${msg.meaning}|${msg.description})`)
.join('//');
}
load(content: string, url: string):
{locale: string | null, i18nNodesByMsgId: {[id: string]: i18n.Node[]}} {
return {locale: null, i18nNodesByMsgId: {}};
}
digest(msg: i18n.Message): string { return msg.id || `default`; }
}
function humanizeMessages(catalog: MessageBundle): string[] {
return catalog.write(new _TestSerializer()).split('//');
}

View File

@ -0,0 +1,66 @@
/**
* @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 * as i18n from '@angular/compiler/src/i18n/i18n_ast';
import {serializeNodes} from '../../../src/i18n/digest';
import {_extractMessages} from '../i18n_parser_spec';
export function main(): void {
describe('i18n AST', () => {
describe('CloneVisitor', () => {
it('should clone an AST', () => {
const messages = _extractMessages(
'<div i18n="m|d">b{count, plural, =0 {{sex, select, male {m}}}}a</div>');
const nodes = messages[0].nodes;
const text = serializeNodes(nodes).join('');
expect(text).toEqual(
'b<ph icu name="ICU">{count, plural, =0 {[{sex, select, male {[m]}}]}}</ph>a');
const visitor = new i18n.CloneVisitor();
const cloneNodes = nodes.map(n => n.visit(visitor));
expect(serializeNodes(nodes)).toEqual(serializeNodes(cloneNodes));
nodes.forEach((n: i18n.Node, i: number) => {
expect(n).toEqual(cloneNodes[i]);
expect(n).not.toBe(cloneNodes[i]);
});
});
});
describe('RecurseVisitor', () => {
it('should visit all nodes', () => {
const visitor = new RecurseVisitor();
const container = new i18n.Container(
[
new i18n.Text('', null),
new i18n.Placeholder('', '', null),
new i18n.IcuPlaceholder(null, '', null),
],
null);
const tag = new i18n.TagPlaceholder('', {}, '', '', [container], false, null);
const icu = new i18n.Icu('', '', {tag}, null);
icu.visit(visitor);
expect(visitor.textCount).toEqual(1);
expect(visitor.phCount).toEqual(1);
expect(visitor.icuPhCount).toEqual(1);
});
});
});
}
class RecurseVisitor extends i18n.RecurseVisitor {
textCount = 0;
phCount = 0;
icuPhCount = 0;
visitText(text: i18n.Text, context?: any): any { this.textCount++; }
visitPlaceholder(ph: i18n.Placeholder, context?: any): any { this.phCount++; }
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { this.icuPhCount++; }
}

View File

@ -0,0 +1,90 @@
/**
* @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 {PlaceholderRegistry} from '../../../src/i18n/serializers/placeholder';
export function main(): void {
describe('PlaceholderRegistry', () => {
let reg: PlaceholderRegistry;
beforeEach(() => { reg = new PlaceholderRegistry(); });
describe('tag placeholder', () => {
it('should generate names for well known tags', () => {
expect(reg.getStartTagPlaceholderName('p', {}, false)).toEqual('START_PARAGRAPH');
expect(reg.getCloseTagPlaceholderName('p')).toEqual('CLOSE_PARAGRAPH');
});
it('should generate names for custom tags', () => {
expect(reg.getStartTagPlaceholderName('my-cmp', {}, false)).toEqual('START_TAG_MY-CMP');
expect(reg.getCloseTagPlaceholderName('my-cmp')).toEqual('CLOSE_TAG_MY-CMP');
});
it('should generate the same name for the same tag', () => {
expect(reg.getStartTagPlaceholderName('p', {}, false)).toEqual('START_PARAGRAPH');
expect(reg.getStartTagPlaceholderName('p', {}, false)).toEqual('START_PARAGRAPH');
});
it('should be case sensitive for tag name', () => {
expect(reg.getStartTagPlaceholderName('p', {}, false)).toEqual('START_PARAGRAPH');
expect(reg.getStartTagPlaceholderName('P', {}, false)).toEqual('START_PARAGRAPH_1');
expect(reg.getCloseTagPlaceholderName('p')).toEqual('CLOSE_PARAGRAPH');
expect(reg.getCloseTagPlaceholderName('P')).toEqual('CLOSE_PARAGRAPH_1');
});
it('should generate the same name for the same tag with the same attributes', () => {
expect(reg.getStartTagPlaceholderName('p', {foo: 'a', bar: 'b'}, false))
.toEqual('START_PARAGRAPH');
expect(reg.getStartTagPlaceholderName('p', {foo: 'a', bar: 'b'}, false))
.toEqual('START_PARAGRAPH');
expect(reg.getStartTagPlaceholderName('p', {bar: 'b', foo: 'a'}, false))
.toEqual('START_PARAGRAPH');
});
it('should generate different names for the same tag with different attributes', () => {
expect(reg.getStartTagPlaceholderName('p', {foo: 'a', bar: 'b'}, false))
.toEqual('START_PARAGRAPH');
expect(reg.getStartTagPlaceholderName('p', {foo: 'a'}, false)).toEqual('START_PARAGRAPH_1');
});
it('should be case sensitive for attributes', () => {
expect(reg.getStartTagPlaceholderName('p', {foo: 'a', bar: 'b'}, false))
.toEqual('START_PARAGRAPH');
expect(reg.getStartTagPlaceholderName('p', {fOo: 'a', bar: 'b'}, false))
.toEqual('START_PARAGRAPH_1');
expect(reg.getStartTagPlaceholderName('p', {fOo: 'a', bAr: 'b'}, false))
.toEqual('START_PARAGRAPH_2');
});
it('should support void tags', () => {
expect(reg.getStartTagPlaceholderName('p', {}, true)).toEqual('PARAGRAPH');
expect(reg.getStartTagPlaceholderName('p', {}, true)).toEqual('PARAGRAPH');
expect(reg.getStartTagPlaceholderName('p', {other: 'true'}, true)).toEqual('PARAGRAPH_1');
});
});
describe('arbitrary placeholders', () => {
it('should generate the same name given the same name and content', () => {
expect(reg.getPlaceholderName('name', 'content')).toEqual('NAME');
expect(reg.getPlaceholderName('name', 'content')).toEqual('NAME');
});
it('should generate a different name given different content', () => {
expect(reg.getPlaceholderName('name', 'content1')).toEqual('NAME');
expect(reg.getPlaceholderName('name', 'content2')).toEqual('NAME_1');
expect(reg.getPlaceholderName('name', 'content3')).toEqual('NAME_2');
});
it('should generate a different name given different names', () => {
expect(reg.getPlaceholderName('name1', 'content')).toEqual('NAME1');
expect(reg.getPlaceholderName('name2', 'content')).toEqual('NAME2');
});
});
});
}

View File

@ -0,0 +1,252 @@
/**
* @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 {escapeRegExp} from '@angular/compiler/src/util';
import {serializeNodes} from '../../../src/i18n/digest';
import {MessageBundle} from '../../../src/i18n/message_bundle';
import {Xliff} from '../../../src/i18n/serializers/xliff';
import {HtmlParser} from '../../../src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/interpolation_config';
const HTML = `
<p i18n-title title="translatable attribute">not translatable</p>
<p i18n>translatable element <b>with placeholders</b> {{ interpolation}}</p>
<p i18n="m|d">foo</p>
<p i18n="m|d@@i">foo</p>
<p i18n="@@bar">foo</p>
<p i18n="ph names"><br><img><div></div></p>
`;
const WRITE_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="983775b9a51ce14b036be72d4cfd65d68d64e231" datatype="html">
<source>translatable attribute</source>
<target/>
</trans-unit>
<trans-unit id="ec1d033f2436133c14ab038286c4f5df4697484a" datatype="html">
<source>translatable element <x id="START_BOLD_TEXT" ctype="x-b"/>with placeholders<x id="CLOSE_BOLD_TEXT" ctype="x-b"/> <x id="INTERPOLATION"/></source>
<target/>
</trans-unit>
<trans-unit id="db3e0a6a5a96481f60aec61d98c3eecddef5ac23" datatype="html">
<source>foo</source>
<target/>
<note priority="1" from="description">d</note>
<note priority="1" from="meaning">m</note>
</trans-unit>
<trans-unit id="i" datatype="html">
<source>foo</source>
<target/>
<note priority="1" from="description">d</note>
<note priority="1" from="meaning">m</note>
</trans-unit>
<trans-unit id="bar" datatype="html">
<source>foo</source>
<target/>
</trans-unit>
<trans-unit id="d7fa2d59aaedcaa5309f13028c59af8c85b8c49d" datatype="html">
<source><x id="LINE_BREAK" ctype="lb"/><x id="TAG_IMG" ctype="image"/><x id="START_TAG_DIV" ctype="x-div"/><x id="CLOSE_TAG_DIV" ctype="x-div"/></source>
<target/>
<note priority="1" from="description">ph names</note>
</trans-unit>
</body>
</file>
</xliff>
`;
const LOAD_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="fr" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="983775b9a51ce14b036be72d4cfd65d68d64e231" datatype="html">
<source>translatable attribute</source>
<target>etubirtta elbatalsnart</target>
</trans-unit>
<trans-unit id="ec1d033f2436133c14ab038286c4f5df4697484a" datatype="html">
<source>translatable element <x id="START_BOLD_TEXT" ctype="b"/>with placeholders<x id="CLOSE_BOLD_TEXT" ctype="b"/> <x id="INTERPOLATION"/></source>
<target><x id="INTERPOLATION"/> footnemele elbatalsnart <x id="START_BOLD_TEXT" ctype="x-b"/>sredlohecalp htiw<x id="CLOSE_BOLD_TEXT" ctype="x-b"/></target>
</trans-unit>
<trans-unit id="db3e0a6a5a96481f60aec61d98c3eecddef5ac23" datatype="html">
<source>foo</source>
<target>oof</target>
<note priority="1" from="description">d</note>
<note priority="1" from="meaning">m</note>
</trans-unit>
<trans-unit id="i" datatype="html">
<source>foo</source>
<target>toto</target>
<note priority="1" from="description">d</note>
<note priority="1" from="meaning">m</note>
</trans-unit>
<trans-unit id="bar" datatype="html">
<source>foo</source>
<target>tata</target>
</trans-unit>
<trans-unit id="d7fa2d59aaedcaa5309f13028c59af8c85b8c49d" datatype="html">
<source><x id="LINE_BREAK" ctype="lb"/><x id="TAG_IMG" ctype="image"/><x id="START_TAG_DIV" ctype="x-div"/><x id="CLOSE_TAG_DIV" ctype="x-div"/></source>
<target><x id="START_TAG_DIV" ctype="x-div"/><x id="CLOSE_TAG_DIV" ctype="x-div"/><x id="TAG_IMG" ctype="image"/><x id="LINE_BREAK" ctype="lb"/></target>
<note priority="1" from="description">ph names</note>
</trans-unit>
<trans-unit id="empty target" datatype="html">
<source><x id="LINE_BREAK" ctype="lb"/><x id="TAG_IMG" ctype="image"/><x id="START_TAG_DIV" ctype="x-div"/><x id="CLOSE_TAG_DIV" ctype="x-div"/></source>
<target/>
<note priority="1" from="description">ph names</note>
</trans-unit>
</body>
</file>
</xliff>
`;
export function main(): void {
const serializer = new Xliff();
function toXliff(html: string, locale: string | null = null): string {
const catalog = new MessageBundle(new HtmlParser, [], {}, locale);
catalog.updateFromTemplate(html, '', DEFAULT_INTERPOLATION_CONFIG);
return catalog.write(serializer);
}
function loadAsMap(xliff: string): {[id: string]: string} {
const {i18nNodesByMsgId} = serializer.load(xliff, 'url');
const msgMap: {[id: string]: string} = {};
Object.keys(i18nNodesByMsgId)
.forEach(id => msgMap[id] = serializeNodes(i18nNodesByMsgId[id]).join(''));
return msgMap;
}
describe('XLIFF serializer', () => {
describe('write', () => {
it('should write a valid xliff file', () => { expect(toXliff(HTML)).toEqual(WRITE_XLIFF); });
it('should write a valid xliff file with a source language',
() => { expect(toXliff(HTML, 'fr')).toContain('file source-language="fr"'); });
});
describe('load', () => {
it('should load XLIFF files', () => {
expect(loadAsMap(LOAD_XLIFF)).toEqual({
'983775b9a51ce14b036be72d4cfd65d68d64e231': 'etubirtta elbatalsnart',
'ec1d033f2436133c14ab038286c4f5df4697484a':
'<ph name="INTERPOLATION"/> footnemele elbatalsnart <ph name="START_BOLD_TEXT"/>sredlohecalp htiw<ph name="CLOSE_BOLD_TEXT"/>',
'db3e0a6a5a96481f60aec61d98c3eecddef5ac23': 'oof',
'i': 'toto',
'bar': 'tata',
'd7fa2d59aaedcaa5309f13028c59af8c85b8c49d':
'<ph name="START_TAG_DIV"/><ph name="CLOSE_TAG_DIV"/><ph name="TAG_IMG"/><ph name="LINE_BREAK"/>',
'empty target': '',
});
});
it('should return the target locale',
() => { expect(serializer.load(LOAD_XLIFF, 'url').locale).toEqual('fr'); });
describe('structure errors', () => {
it('should throw when a trans-unit has no translation', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="missingtarget">
<source/>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => {
loadAsMap(XLIFF);
}).toThrowError(/Message missingtarget misses a translation/);
});
it('should throw when a trans-unit has no id attribute', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit datatype="html">
<source/>
<target/>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => {
loadAsMap(XLIFF);
}).toThrowError(/<trans-unit> misses the "id" attribute/);
});
it('should throw on duplicate trans-unit id', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="deadbeef">
<source/>
<target/>
</trans-unit>
<trans-unit id="deadbeef">
<source/>
<target/>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => {
loadAsMap(XLIFF);
}).toThrowError(/Duplicated translations for msg deadbeef/);
});
});
describe('message errors', () => {
it('should throw on unknown message tags', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="deadbeef" datatype="html">
<source/>
<target><b>msg should contain only ph tags</b></target>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => { loadAsMap(XLIFF); })
.toThrowError(
new RegExp(escapeRegExp(`[ERROR ->]<b>msg should contain only ph tags</b>`)));
});
it('should throw when a placeholder misses an id attribute', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="deadbeef" datatype="html">
<source/>
<target><x/></target>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => {
loadAsMap(XLIFF);
}).toThrowError(new RegExp(escapeRegExp(`<x> misses the "id" attribute`)));
});
});
});
});
}

View File

@ -0,0 +1,81 @@
/**
* @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 {MessageBundle} from '@angular/compiler/src/i18n/message_bundle';
import {Xmb} from '@angular/compiler/src/i18n/serializers/xmb';
import {HtmlParser} from '@angular/compiler/src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '@angular/compiler/src/ml_parser/interpolation_config';
export function main(): void {
describe('XMB serializer', () => {
const HTML = `
<p>not translatable</p>
<p i18n>translatable element <b>with placeholders</b> {{ interpolation}}</p>
<!-- i18n -->{ count, plural, =0 {<p>test</p>}}<!-- /i18n -->
<p i18n="m|d">foo</p>
<p i18n="m|d@@i">foo</p>
<p i18n="@@bar">foo</p>
<p i18n="@@baz">{ count, plural, =0 { { sex, select, other {<p>deeply nested</p>}} }}</p>
<p i18n>{ count, plural, =0 { { sex, select, other {<p>deeply nested</p>}} }}</p>`;
const XMB = `<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE messagebundle [
<!ELEMENT messagebundle (msg)*>
<!ATTLIST messagebundle class CDATA #IMPLIED>
<!ELEMENT msg (#PCDATA|ph|source)*>
<!ATTLIST msg id CDATA #IMPLIED>
<!ATTLIST msg seq CDATA #IMPLIED>
<!ATTLIST msg name CDATA #IMPLIED>
<!ATTLIST msg desc CDATA #IMPLIED>
<!ATTLIST msg meaning CDATA #IMPLIED>
<!ATTLIST msg obsolete (obsolete) #IMPLIED>
<!ATTLIST msg xml:space (default|preserve) "default">
<!ATTLIST msg is_hidden CDATA #IMPLIED>
<!ELEMENT source (#PCDATA)>
<!ELEMENT ph (#PCDATA|ex)*>
<!ATTLIST ph name CDATA #REQUIRED>
<!ELEMENT ex (#PCDATA)>
]>
<messagebundle>
<msg id="7056919470098446707">translatable element <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> <ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph></msg>
<msg id="2981514368455622387">{VAR_PLURAL, plural, =0 {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} }</msg>
<msg id="7999024498831672133" desc="d" meaning="m">foo</msg>
<msg id="i" desc="d" meaning="m">foo</msg>
<msg id="bar">foo</msg>
<msg id="baz">{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} } } }</msg>
<msg id="2015957479576096115">{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} } } }</msg>
</messagebundle>
`;
it('should write a valid xmb file', () => {
expect(toXmb(HTML)).toEqual(XMB);
// the locale is not specified in the xmb file
expect(toXmb(HTML, 'fr')).toEqual(XMB);
});
it('should throw when trying to load an xmb file', () => {
expect(() => {
const serializer = new Xmb();
serializer.load(XMB, 'url');
}).toThrowError(/Unsupported/);
});
});
}
function toXmb(html: string, locale: string | null = null): string {
const catalog = new MessageBundle(new HtmlParser, [], {}, locale);
const serializer = new Xmb();
catalog.updateFromTemplate(html, '', DEFAULT_INTERPOLATION_CONFIG);
return catalog.write(serializer);
}

View File

@ -0,0 +1,47 @@
/**
* @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 * as xml from '../../../src/i18n/serializers/xml_helper';
export function main(): void {
describe('XML helper', () => {
it('should serialize XML declaration', () => {
expect(xml.serialize([new xml.Declaration({version: '1.0'})]))
.toEqual('<?xml version="1.0" ?>');
});
it('should serialize text node',
() => { expect(xml.serialize([new xml.Text('foo bar')])).toEqual('foo bar'); });
it('should escape text nodes',
() => { expect(xml.serialize([new xml.Text('<>')])).toEqual('&lt;&gt;'); });
it('should serialize xml nodes without children', () => {
expect(xml.serialize([new xml.Tag('el', {foo: 'bar'}, [])])).toEqual('<el foo="bar"/>');
});
it('should serialize xml nodes with children', () => {
expect(xml.serialize([
new xml.Tag('parent', {}, [new xml.Tag('child', {}, [new xml.Text('content')])])
])).toEqual('<parent><child>content</child></parent>');
});
it('should serialize node lists', () => {
expect(xml.serialize([
new xml.Tag('el', {order: '0'}, []),
new xml.Tag('el', {order: '1'}, []),
])).toEqual('<el order="0"/><el order="1"/>');
});
it('should escape attribute values', () => {
expect(xml.serialize([new xml.Tag('el', {foo: '<">'}, [])]))
.toEqual('<el foo="&lt;&quot;&gt;"/>');
});
});
}

View File

@ -0,0 +1,192 @@
/**
* @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 {escapeRegExp} from '@angular/compiler/src/util';
import {serializeNodes} from '../../../src/i18n/digest';
import * as i18n from '../../../src/i18n/i18n_ast';
import {Xtb} from '../../../src/i18n/serializers/xtb';
export function main(): void {
describe('XTB serializer', () => {
const serializer = new Xtb();
function loadAsMap(xtb: string): {[id: string]: string} {
const {i18nNodesByMsgId} = serializer.load(xtb, 'url');
const msgMap: {[id: string]: string} = {};
Object.keys(i18nNodesByMsgId).forEach(id => {
msgMap[id] = serializeNodes(i18nNodesByMsgId[id]).join('');
});
return msgMap;
}
describe('load', () => {
it('should load XTB files with a doctype', () => {
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE translationbundle [<!ELEMENT translationbundle (translation)*>
<!ATTLIST translationbundle lang CDATA #REQUIRED>
<!ELEMENT translation (#PCDATA|ph)*>
<!ATTLIST translation id CDATA #REQUIRED>
<!ELEMENT ph EMPTY>
<!ATTLIST ph name CDATA #REQUIRED>
]>
<translationbundle>
<translation id="8841459487341224498">rab</translation>
</translationbundle>`;
expect(loadAsMap(XTB)).toEqual({'8841459487341224498': 'rab'});
});
it('should load XTB files without placeholders', () => {
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
<translationbundle>
<translation id="8841459487341224498">rab</translation>
</translationbundle>`;
expect(loadAsMap(XTB)).toEqual({'8841459487341224498': 'rab'});
});
it('should return the target locale', () => {
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
<translationbundle lang='fr'>
<translation id="8841459487341224498">rab</translation>
</translationbundle>`;
expect(serializer.load(XTB, 'url').locale).toEqual('fr');
});
it('should load XTB files with placeholders', () => {
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
<translationbundle>
<translation id="8877975308926375834"><ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/></translation>
</translationbundle>`;
expect(loadAsMap(XTB)).toEqual({
'8877975308926375834': '<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>'
});
});
it('should replace ICU placeholders with their translations', () => {
const XTB = `<?xml version="1.0" encoding="UTF-8" ?>
<translationbundle>
<translation id="7717087045075616176">*<ph name="ICU"/>*</translation>
<translation id="5115002811911870583">{VAR_PLURAL, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation>
</translationbundle>`;
expect(loadAsMap(XTB)).toEqual({
'7717087045075616176': `*<ph name="ICU"/>*`,
'5115002811911870583':
`{VAR_PLURAL, plural, =1 {[<ph name="START_PARAGRAPH"/>, rab, <ph name="CLOSE_PARAGRAPH"/>]}}`,
});
});
it('should load complex XTB files', () => {
const XTB = `<?xml version="1.0" encoding="UTF-8" ?>
<translationbundle>
<translation id="8281795707202401639"><ph name="INTERPOLATION"/><ph name="START_BOLD_TEXT"/>rab<ph name="CLOSE_BOLD_TEXT"/> oof</translation>
<translation id="5115002811911870583">{VAR_PLURAL, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation>
<translation id="130772889486467622">oof</translation>
<translation id="4739316421648347533">{VAR_PLURAL, plural, =1 {{VAR_GENDER, gender, male {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}} }}</translation>
</translationbundle>`;
expect(loadAsMap(XTB)).toEqual({
'8281795707202401639':
`<ph name="INTERPOLATION"/><ph name="START_BOLD_TEXT"/>rab<ph name="CLOSE_BOLD_TEXT"/> oof`,
'5115002811911870583':
`{VAR_PLURAL, plural, =1 {[<ph name="START_PARAGRAPH"/>, rab, <ph name="CLOSE_PARAGRAPH"/>]}}`,
'130772889486467622': `oof`,
'4739316421648347533':
`{VAR_PLURAL, plural, =1 {[{VAR_GENDER, gender, male {[<ph name="START_PARAGRAPH"/>, rab, <ph name="CLOSE_PARAGRAPH"/>]}}, ]}}`,
});
});
});
describe('errors', () => {
it('should be able to parse non-angular xtb files without error', () => {
const XTB = `<?xml version="1.0" encoding="UTF-8" ?>
<translationbundle>
<translation id="angular">is great</translation>
<translation id="non angular">is <invalid>less</invalid> {count, plural, =0 {{GREAT}}}</translation>
</translationbundle>`;
// Invalid messages should not cause the parser to throw
let i18nNodesByMsgId: {[id: string]: i18n.Node[]};
expect(() => {
i18nNodesByMsgId = serializer.load(XTB, 'url').i18nNodesByMsgId;
}).not.toThrow();
expect(Object.keys(i18nNodesByMsgId).length).toEqual(2);
expect(serializeNodes(i18nNodesByMsgId['angular']).join('')).toEqual('is great');
// Messages that contain unsupported feature should throw on access
expect(() => {
const read = i18nNodesByMsgId['non angular'];
}).toThrowError(/xtb parse errors/);
});
it('should throw on nested <translationbundle>', () => {
const XTB =
'<translationbundle><translationbundle></translationbundle></translationbundle>';
expect(() => {
loadAsMap(XTB);
}).toThrowError(/<translationbundle> elements can not be nested/);
});
it('should throw when a <translation> has no id attribute', () => {
const XTB = `<translationbundle>
<translation></translation>
</translationbundle>`;
expect(() => { loadAsMap(XTB); }).toThrowError(/<translation> misses the "id" attribute/);
});
it('should throw when a placeholder has no name attribute', () => {
const XTB = `<translationbundle>
<translation id="1186013544048295927"><ph /></translation>
</translationbundle>`;
expect(() => { loadAsMap(XTB); }).toThrowError(/<ph> misses the "name" attribute/);
});
it('should throw on unknown xtb tags', () => {
const XTB = `<what></what>`;
expect(() => {
loadAsMap(XTB);
}).toThrowError(new RegExp(escapeRegExp(`Unexpected tag ("[ERROR ->]<what></what>")`)));
});
it('should throw on unknown message tags', () => {
const XTB = `<translationbundle>
<translation id="1186013544048295927"><b>msg should contain only ph tags</b></translation>
</translationbundle>`;
expect(() => { loadAsMap(XTB); })
.toThrowError(
new RegExp(escapeRegExp(`[ERROR ->]<b>msg should contain only ph tags</b>`)));
});
it('should throw on duplicate message id', () => {
const XTB = `<translationbundle>
<translation id="1186013544048295927">msg1</translation>
<translation id="1186013544048295927">msg2</translation>
</translationbundle>`;
expect(() => {
loadAsMap(XTB);
}).toThrowError(/Duplicated translations for msg 1186013544048295927/);
});
it('should throw when trying to save an xtb file',
() => { expect(() => { serializer.write([], null); }).toThrowError(/Unsupported/); });
});
});
}

View File

@ -0,0 +1,155 @@
/**
* @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 {MissingTranslationStrategy} from '@angular/core';
import * as i18n from '../../src/i18n/i18n_ast';
import {TranslationBundle} from '../../src/i18n/translation_bundle';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util';
import {serializeNodes} from '../ml_parser/ast_serializer_spec';
import {_extractMessages} from './i18n_parser_spec';
export function main(): void {
describe('TranslationBundle', () => {
const file = new ParseSourceFile('content', 'url');
const location = new ParseLocation(file, 0, 0, 0);
const span = new ParseSourceSpan(location, null);
const srcNode = new i18n.Text('src', span);
it('should translate a plain message', () => {
const msgMap = {foo: [new i18n.Text('bar', null)]};
const tb = new TranslationBundle(msgMap, null, (_) => 'foo');
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
expect(serializeNodes(tb.get(msg))).toEqual(['bar']);
});
it('should translate a message with placeholder', () => {
const msgMap = {
foo: [
new i18n.Text('bar', null),
new i18n.Placeholder('', 'ph1', null),
]
};
const phMap = {
ph1: '*phContent*',
};
const tb = new TranslationBundle(msgMap, null, (_) => 'foo');
const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd', 'i');
expect(serializeNodes(tb.get(msg))).toEqual(['bar*phContent*']);
});
it('should translate a message with placeholder referencing messages', () => {
const msgMap = {
foo: [
new i18n.Text('--', null),
new i18n.Placeholder('', 'ph1', null),
new i18n.Text('++', null),
],
ref: [
new i18n.Text('*refMsg*', null),
],
};
const refMsg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd', 'i');
let count = 0;
const digest = (_: any) => count++ ? 'ref' : 'foo';
const tb = new TranslationBundle(msgMap, null, digest);
expect(serializeNodes(tb.get(msg))).toEqual(['--*refMsg*++']);
});
it('should use the original message or throw when a translation is not found', () => {
const src =
`<some-tag>some text{{ some_expression }}</some-tag>{count, plural, =0 {no} few {a <b>few</b>}}`;
const messages = _extractMessages(`<div i18n>${src}</div>`);
const digest = (_: any) => `no matching id`;
// Empty message map -> use source messages in Ignore mode
let tb = new TranslationBundle({}, null, digest, null, MissingTranslationStrategy.Ignore);
expect(serializeNodes(tb.get(messages[0])).join('')).toEqual(src);
// Empty message map -> use source messages in Warning mode
tb = new TranslationBundle({}, null, digest, null, MissingTranslationStrategy.Warning);
expect(serializeNodes(tb.get(messages[0])).join('')).toEqual(src);
// Empty message map -> throw in Error mode
tb = new TranslationBundle({}, null, digest, null, MissingTranslationStrategy.Error);
expect(() => serializeNodes(tb.get(messages[0])).join('')).toThrow();
});
describe('errors reporting', () => {
it('should report unknown placeholders', () => {
const msgMap = {
foo: [
new i18n.Text('bar', null),
new i18n.Placeholder('', 'ph1', span),
]
};
const tb = new TranslationBundle(msgMap, null, (_) => 'foo');
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
expect(() => tb.get(msg)).toThrowError(/Unknown placeholder/);
});
it('should report missing translation', () => {
const tb =
new TranslationBundle({}, null, (_) => 'foo', null, MissingTranslationStrategy.Error);
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
expect(() => tb.get(msg)).toThrowError(/Missing translation for message "foo"/);
});
it('should report missing translation with MissingTranslationStrategy.Warning', () => {
const log: string[] = [];
const console = {
log: (msg: string) => { throw `unexpected`; },
warn: (msg: string) => log.push(msg),
};
const tb = new TranslationBundle(
{}, 'en', (_) => 'foo', null, MissingTranslationStrategy.Warning, console);
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
expect(() => tb.get(msg)).not.toThrowError();
expect(log.length).toEqual(1);
expect(log[0]).toMatch(/Missing translation for message "foo" for locale "en"/);
});
it('should not report missing translation with MissingTranslationStrategy.Ignore', () => {
const tb =
new TranslationBundle({}, null, (_) => 'foo', null, MissingTranslationStrategy.Ignore);
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
expect(() => tb.get(msg)).not.toThrowError();
});
it('should report missing referenced message', () => {
const msgMap = {
foo: [new i18n.Placeholder('', 'ph1', span)],
};
const refMsg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd', 'i');
let count = 0;
const digest = (_: any) => count++ ? 'ref' : 'foo';
const tb =
new TranslationBundle(msgMap, null, digest, null, MissingTranslationStrategy.Error);
expect(() => tb.get(msg)).toThrowError(/Missing translation for message "ref"/);
});
it('should report invalid translated html', () => {
const msgMap = {
foo: [
new i18n.Text('text', null),
new i18n.Placeholder('', 'ph1', null),
]
};
const phMap = {
ph1: '</b>',
};
const tb = new TranslationBundle(msgMap, null, (_) => 'foo');
const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd', 'i');
expect(() => tb.get(msg)).toThrowError(/Unexpected closing tag "b"/);
});
});
});
}

View File

@ -0,0 +1,51 @@
/**
* @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 {Component, Directive, Input} from '@angular/core';
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {expect} from '@angular/platform-browser/testing/matchers';
export function main() {
describe('integration tests', () => {
let fixture: ComponentFixture<TestComponent>;
describe('directives', () => {
it('should support dotted selectors', async(() => {
@Directive({selector: '[dot.name]'})
class MyDir {
@Input('dot.name') value: string;
}
TestBed.configureTestingModule({
declarations: [
MyDir,
TestComponent,
],
});
const template = `<div [dot.name]="'foo'"></div>`;
fixture = createTestComponent(template);
fixture.detectChanges();
const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir);
expect(myDir.value).toEqual('foo');
}));
});
});
}
@Component({selector: 'test-cmp', template: ''})
class TestComponent {
}
function createTestComponent(template: string): ComponentFixture<TestComponent> {
return TestBed.overrideComponent(TestComponent, {set: {template: template}})
.createComponent(TestComponent);
}

View File

@ -0,0 +1,152 @@
/**
* @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 {expect} from '@angular/platform-browser/testing/matchers';
import {MetadataOverrider} from '../testing/metadata_overrider';
interface SomeMetadataType {
plainProp?: string;
getterProp?: string;
arrayProp?: any[];
}
interface OtherMetadataType extends SomeMetadataType {
otherPlainProp?: string;
}
class SomeMetadata implements SomeMetadataType {
plainProp: string;
private _getterProp: string;
get getterProp(): string { return this._getterProp; }
arrayProp: any[];
constructor(options: SomeMetadataType) {
this.plainProp = options.plainProp;
this._getterProp = options.getterProp;
this.arrayProp = options.arrayProp;
}
}
class OtherMetadata extends SomeMetadata implements OtherMetadataType {
otherPlainProp: string;
constructor(options: OtherMetadataType) {
super({
plainProp: options.plainProp,
getterProp: options.getterProp,
arrayProp: options.arrayProp
});
this.otherPlainProp = options.otherPlainProp;
}
}
export function main() {
describe('metadata overrider', () => {
let overrider: MetadataOverrider;
beforeEach(() => { overrider = new MetadataOverrider(); });
it('should return a new instance with the same values', () => {
const oldInstance = new SomeMetadata({plainProp: 'somePlainProp', getterProp: 'someInput'});
const newInstance = overrider.overrideMetadata(SomeMetadata, oldInstance, {});
expect(newInstance).not.toBe(oldInstance);
expect(newInstance).toBeAnInstanceOf(SomeMetadata);
expect(newInstance).toEqual(oldInstance);
});
it('should set individual properties and keep others', () => {
const oldInstance =
new SomeMetadata({plainProp: 'somePlainProp', getterProp: 'someGetterProp'});
const newInstance =
overrider.overrideMetadata(SomeMetadata, oldInstance, {set: {plainProp: 'newPlainProp'}});
expect(newInstance)
.toEqual(new SomeMetadata({plainProp: 'newPlainProp', getterProp: 'someGetterProp'}));
});
describe('add properties', () => {
it('should replace non array values', () => {
const oldInstance =
new SomeMetadata({plainProp: 'somePlainProp', getterProp: 'someGetterProp'});
const newInstance = overrider.overrideMetadata(
SomeMetadata, oldInstance, {add: {plainProp: 'newPlainProp'}});
expect(newInstance)
.toEqual(new SomeMetadata({plainProp: 'newPlainProp', getterProp: 'someGetterProp'}));
});
it('should add to array values', () => {
const oldInstance = new SomeMetadata({arrayProp: ['a']});
const newInstance =
overrider.overrideMetadata(SomeMetadata, oldInstance, {add: {arrayProp: ['b']}});
expect(newInstance).toEqual(new SomeMetadata({arrayProp: ['a', 'b']}));
});
});
describe('remove', () => {
it('should set values to undefined if their value matches', () => {
const oldInstance =
new SomeMetadata({plainProp: 'somePlainProp', getterProp: 'someGetterProp'});
const newInstance = overrider.overrideMetadata(
SomeMetadata, oldInstance, {remove: {plainProp: 'somePlainProp'}});
expect(newInstance)
.toEqual(new SomeMetadata({plainProp: undefined, getterProp: 'someGetterProp'}));
});
it('should leave values if their value does not match', () => {
const oldInstance =
new SomeMetadata({plainProp: 'somePlainProp', getterProp: 'someGetterProp'});
const newInstance = overrider.overrideMetadata(
SomeMetadata, oldInstance, {remove: {plainProp: 'newPlainProp'}});
expect(newInstance)
.toEqual(new SomeMetadata({plainProp: 'somePlainProp', getterProp: 'someGetterProp'}));
});
it('should remove a value from an array', () => {
const oldInstance =
new SomeMetadata({arrayProp: ['a', 'b', 'c'], getterProp: 'someGetterProp'});
const newInstance = overrider.overrideMetadata(
SomeMetadata, oldInstance, {remove: {arrayProp: ['a', 'c']}});
expect(newInstance)
.toEqual(new SomeMetadata({arrayProp: ['b'], getterProp: 'someGetterProp'}));
});
it('should support types as values', () => {
class Class1 {}
class Class2 {}
class Class3 {}
const instance1 = new SomeMetadata({arrayProp: [Class1, Class2, Class3]});
const instance2 =
overrider.overrideMetadata(SomeMetadata, instance1, {remove: {arrayProp: [Class1]}});
expect(instance2).toEqual(new SomeMetadata({arrayProp: [Class2, Class3]}));
const instance3 =
overrider.overrideMetadata(SomeMetadata, instance2, {remove: {arrayProp: [Class3]}});
expect(instance3).toEqual(new SomeMetadata({arrayProp: [Class2]}));
});
});
describe('subclasses', () => {
it('should set individual properties and keep others', () => {
const oldInstance = new OtherMetadata({
plainProp: 'somePlainProp',
getterProp: 'someGetterProp',
otherPlainProp: 'newOtherProp'
});
const newInstance = overrider.overrideMetadata(
OtherMetadata, oldInstance, {set: {plainProp: 'newPlainProp'}});
expect(newInstance).toEqual(new OtherMetadata({
plainProp: 'newPlainProp',
getterProp: 'someGetterProp',
otherPlainProp: 'newOtherProp'
}));
});
});
});
}

View File

@ -0,0 +1,13 @@
/**
* @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 {Component} from '@angular/core';
@Component({styles: <any>('foo'), template: ''})
export class MalformedStylesComponent {
}

View File

@ -0,0 +1,487 @@
/**
* @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 {TEST_COMPILER_PROVIDERS} from '@angular/compiler/testing/test_bindings';
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, Component, Directive, DoCheck, Injectable, NgModule, OnChanges, OnDestroy, OnInit, Pipe, SimpleChanges, ViewEncapsulation, ɵstringify as stringify} from '@angular/core';
import {LIFECYCLE_HOOKS_VALUES} from '@angular/core/src/metadata/lifecycle_hooks';
import {TestBed, async, inject} from '@angular/core/testing';
import {identifierName} from '../src/compile_metadata';
import {CompileMetadataResolver} from '../src/metadata_resolver';
import {ResourceLoader} from '../src/resource_loader';
import {MockResourceLoader} from '../testing/resource_loader_mock';
import {MalformedStylesComponent} from './metadata_resolver_fixture';
export function main() {
describe('CompileMetadataResolver', () => {
beforeEach(() => { TestBed.configureCompiler({providers: TEST_COMPILER_PROVIDERS}); });
it('should throw on the getDirectiveMetadata/getPipeMetadata methods if the module has not been loaded yet',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({})
class SomeModule {
}
@Pipe({name: 'pipe'})
class SomePipe {
}
expect(() => resolver.getDirectiveMetadata(ComponentWithEverythingInline))
.toThrowError(/Illegal state/);
expect(() => resolver.getPipeMetadata(SomePipe)).toThrowError(/Illegal state/);
}));
it('should read metadata in sync for components with inline resources',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [ComponentWithEverythingInline]})
class SomeModule {
}
resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true);
const meta = resolver.getDirectiveMetadata(ComponentWithEverythingInline);
expect(meta.selector).toEqual('someSelector');
expect(meta.exportAs).toEqual('someExportAs');
expect(meta.isComponent).toBe(true);
expect(meta.type.reference).toBe(ComponentWithEverythingInline);
expect(identifierName(meta.type)).toEqual(stringify(ComponentWithEverythingInline));
expect(meta.type.lifecycleHooks).toEqual(LIFECYCLE_HOOKS_VALUES);
expect(meta.changeDetection).toBe(ChangeDetectionStrategy.Default);
expect(meta.inputs).toEqual({'someProp': 'someProp'});
expect(meta.outputs).toEqual({'someEvent': 'someEvent'});
expect(meta.hostListeners).toEqual({'someHostListener': 'someHostListenerExpr'});
expect(meta.hostProperties).toEqual({'someHostProp': 'someHostPropExpr'});
expect(meta.hostAttributes).toEqual({'someHostAttr': 'someHostAttrValue'});
expect(meta.template.encapsulation).toBe(ViewEncapsulation.Emulated);
expect(meta.template.styles).toEqual(['someStyle']);
expect(meta.template.template).toEqual('someTemplate');
expect(meta.template.interpolation).toEqual(['{{', '}}']);
}));
it('should throw when reading metadata for component with external resources when sync=true is passed',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [ComponentWithExternalResources]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(
`Can't compile synchronously as ${stringify(ComponentWithExternalResources)} is still being loaded!`);
}));
it('should read external metadata when sync=false',
async(inject(
[CompileMetadataResolver, ResourceLoader],
(resolver: CompileMetadataResolver, resourceLoader: MockResourceLoader) => {
@NgModule({declarations: [ComponentWithExternalResources]})
class SomeModule {
}
resourceLoader.when('someTemplateUrl', 'someTemplate');
resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, false).then(() => {
const meta = resolver.getDirectiveMetadata(ComponentWithExternalResources);
expect(meta.selector).toEqual('someSelector');
expect(meta.template.styleUrls).toEqual(['someStyleUrl']);
expect(meta.template.templateUrl).toEqual('someTemplateUrl');
expect(meta.template.template).toEqual('someTemplate');
});
resourceLoader.flush();
})));
it('should use `./` as base url for templates during runtime compilation if no moduleId is given',
async(inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@Component({selector: 'someComponent', templateUrl: 'someUrl'})
class ComponentWithoutModuleId {
}
@NgModule({declarations: [ComponentWithoutModuleId]})
class SomeModule {
}
resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, false).then(() => {
const value: string =
resolver.getDirectiveMetadata(ComponentWithoutModuleId).template.templateUrl;
const expectedEndValue = './someUrl';
expect(value.endsWith(expectedEndValue)).toBe(true);
});
})));
it('should throw when the moduleId is not a string',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [ComponentWithInvalidModuleId]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(
`moduleId should be a string in "ComponentWithInvalidModuleId". See` +
` https://goo.gl/wIDDiL for more information.\n` +
`If you're using Webpack you should inline the template and the styles, see` +
` https://goo.gl/X2J8zc.`);
}));
it('should throw when metadata is incorrectly typed',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [MalformedStylesComponent]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(`Expected 'styles' to be an array of strings.`);
}));
it('should throw with descriptive error message when a module imports itself',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({imports: [SomeModule]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(`'SomeModule' module can't import itself`);
}));
it('should throw with descriptive error message when provider token can not be resolved',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [MyBrokenComp1]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(`Can't resolve all parameters for MyBrokenComp1: (?).`);
}));
it('should throw with descriptive error message when a directive is passed to imports',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({imports: [ComponentWithoutModuleId]})
class ModuleWithImportedComponent {
}
expect(
() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithImportedComponent, true))
.toThrowError(
`Unexpected directive 'ComponentWithoutModuleId' imported by the module 'ModuleWithImportedComponent'`);
}));
it('should throw with descriptive error message when a pipe is passed to imports',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@Pipe({name: 'somePipe'})
class SomePipe {
}
@NgModule({imports: [SomePipe]})
class ModuleWithImportedPipe {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithImportedPipe, true))
.toThrowError(
`Unexpected pipe 'SomePipe' imported by the module 'ModuleWithImportedPipe'`);
}));
it('should throw with descriptive error message when a module is passed to declarations',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({})
class SomeModule {
}
@NgModule({declarations: [SomeModule]})
class ModuleWithDeclaredModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithDeclaredModule, true))
.toThrowError(
`Unexpected module 'SomeModule' declared by the module 'ModuleWithDeclaredModule'`);
}));
it('should throw with descriptive error message when null is passed to declarations',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [null]})
class ModuleWithNullDeclared {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithNullDeclared, true))
.toThrowError(
`Unexpected value 'null' declared by the module 'ModuleWithNullDeclared'`);
}));
it('should throw with descriptive error message when null is passed to imports',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({imports: [null]})
class ModuleWithNullImported {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithNullImported, true))
.toThrowError(
`Unexpected value 'null' imported by the module 'ModuleWithNullImported'`);
}));
it('should throw with descriptive error message when a param token of a dependency is undefined',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [MyBrokenComp2]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(`Can't resolve all parameters for NonAnnotatedService: (?).`);
}));
it('should throw with descriptive error message when encounter invalid provider',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({providers: [{provide: SimpleService, useClass: undefined}]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(/Invalid provider for SimpleService. useClass cannot be undefined./);
}));
it('should throw with descriptive error message when provider is undefined',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({providers: [undefined]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(/Encountered undefined provider!/);
}));
it('should throw with descriptive error message when one of providers is not present',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [MyBrokenComp3]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(
`Invalid providers for "MyBrokenComp3" - only instances of Provider and Type are allowed, got: [SimpleService, ?null?, ...]`);
}));
it('should throw with descriptive error message when one of viewProviders is not present',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [MyBrokenComp4]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(
`Invalid viewProviders for "MyBrokenComp4" - only instances of Provider and Type are allowed, got: [?null?, ...]`);
}));
it('should throw with descriptive error message when null or undefined is passed to module bootstrap',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({bootstrap: [null]})
class ModuleWithNullBootstrap {
}
@NgModule({bootstrap: [undefined]})
class ModuleWithUndefinedBootstrap {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithNullBootstrap, true))
.toThrowError(
`Unexpected value 'null' used in the bootstrap property of module 'ModuleWithNullBootstrap'`);
expect(
() =>
resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithUndefinedBootstrap, true))
.toThrowError(
`Unexpected value 'undefined' used in the bootstrap property of module 'ModuleWithUndefinedBootstrap'`);
}));
it('should throw an error when the interpolation config has invalid symbols',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [ComponentWithInvalidInterpolation1]})
class Module1 {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module1, true))
.toThrowError(`[' ', ' '] contains unusable interpolation symbol.`);
@NgModule({declarations: [ComponentWithInvalidInterpolation2]})
class Module2 {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module2, true))
.toThrowError(`['{', '}'] contains unusable interpolation symbol.`);
@NgModule({declarations: [ComponentWithInvalidInterpolation3]})
class Module3 {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module3, true))
.toThrowError(`['<%', '%>'] contains unusable interpolation symbol.`);
@NgModule({declarations: [ComponentWithInvalidInterpolation4]})
class Module4 {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module4, true))
.toThrowError(`['&#', '}}'] contains unusable interpolation symbol.`);
@NgModule({declarations: [ComponentWithInvalidInterpolation5]})
class Module5 {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module5, true))
.toThrowError(`['&lbrace;', '}}'] contains unusable interpolation symbol.`);
}));
it(`should throw an error when a Pipe is added to module's bootstrap list`,
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@Pipe({name: 'pipe'})
class MyPipe {
}
@NgModule({declarations: [MyPipe], bootstrap: [MyPipe]})
class ModuleWithPipeInBootstrap {
}
expect(() => resolver.getNgModuleMetadata(ModuleWithPipeInBootstrap))
.toThrowError(`MyPipe cannot be used as an entry component.`);
}));
it(`should throw an error when a Service is added to module's bootstrap list`,
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [], bootstrap: [SimpleService]})
class ModuleWithServiceInBootstrap {
}
expect(() => resolver.getNgModuleMetadata(ModuleWithServiceInBootstrap))
.toThrowError(`SimpleService cannot be used as an entry component.`);
}));
it(`should throw an error when a Directive is added to module's bootstrap list`,
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@Directive({selector: 'directive'})
class MyDirective {
}
@NgModule({declarations: [], bootstrap: [MyDirective]})
class ModuleWithDirectiveInBootstrap {
}
expect(() => resolver.getNgModuleMetadata(ModuleWithDirectiveInBootstrap))
.toThrowError(`MyDirective cannot be used as an entry component.`);
}));
it(`should not throw an error when a Component is added to module's bootstrap list`,
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@Component({template: ''})
class MyComp {
}
@NgModule({declarations: [MyComp], bootstrap: [MyComp]})
class ModuleWithComponentInBootstrap {
}
expect(() => resolver.getNgModuleMetadata(ModuleWithComponentInBootstrap)).not.toThrow();
}));
});
it('should dedupe declarations in NgModule',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@Component({template: ''})
class MyComp {
}
@NgModule({declarations: [MyComp, MyComp]})
class MyModule {
}
const modMeta = resolver.getNgModuleMetadata(MyModule);
expect(modMeta.declaredDirectives.length).toBe(1);
expect(modMeta.declaredDirectives[0].reference).toBe(MyComp);
}));
}
@Component({selector: 'someComponent', template: ''})
class ComponentWithoutModuleId {
}
@Component({selector: 'someComponent', template: '', moduleId: <any>0})
class ComponentWithInvalidModuleId {
}
@Component({
selector: 'someSelector',
templateUrl: 'someTemplateUrl',
styleUrls: ['someStyleUrl'],
})
class ComponentWithExternalResources {
}
@Component({
selector: 'someSelector',
inputs: ['someProp'],
outputs: ['someEvent'],
host: {
'[someHostProp]': 'someHostPropExpr',
'(someHostListener)': 'someHostListenerExpr',
'someHostAttr': 'someHostAttrValue'
},
exportAs: 'someExportAs',
moduleId: 'someModuleId',
changeDetection: ChangeDetectionStrategy.Default,
template: 'someTemplate',
encapsulation: ViewEncapsulation.Emulated,
styles: ['someStyle'],
interpolation: ['{{', '}}']
})
class ComponentWithEverythingInline implements OnChanges,
OnInit, DoCheck, OnDestroy, AfterContentInit, AfterContentChecked, AfterViewInit,
AfterViewChecked {
ngOnChanges(changes: SimpleChanges): void {}
ngOnInit(): void {}
ngDoCheck(): void {}
ngOnDestroy(): void {}
ngAfterContentInit(): void {}
ngAfterContentChecked(): void {}
ngAfterViewInit(): void {}
ngAfterViewChecked(): void {}
}
@Component({selector: 'my-broken-comp', template: ''})
class MyBrokenComp1 {
constructor(public dependency: any) {}
}
class NonAnnotatedService {
constructor(dep: any) {}
}
@Component({selector: 'my-broken-comp', template: '', providers: [NonAnnotatedService]})
class MyBrokenComp2 {
constructor(dependency: NonAnnotatedService) {}
}
@Injectable()
class SimpleService {
}
@Component({selector: 'my-broken-comp', template: '', providers: [SimpleService, null, [null]]})
class MyBrokenComp3 {
}
@Component({selector: 'my-broken-comp', template: '', viewProviders: [null, SimpleService, [null]]})
class MyBrokenComp4 {
}
@Component({selector: 'someSelector', template: '', interpolation: [' ', ' ']})
class ComponentWithInvalidInterpolation1 {
}
@Component({selector: 'someSelector', template: '', interpolation: ['{', '}']})
class ComponentWithInvalidInterpolation2 {
}
@Component({selector: 'someSelector', template: '', interpolation: ['<%', '%>']})
class ComponentWithInvalidInterpolation3 {
}
@Component({selector: 'someSelector', template: '', interpolation: ['&#', '}}']})
class ComponentWithInvalidInterpolation4 {
}
@Component({selector: 'someSelector', template: '', interpolation: ['&lbrace;', '}}']})
class ComponentWithInvalidInterpolation5 {
}

View File

@ -0,0 +1,100 @@
/**
* @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 * as html from '@angular/compiler/src/ml_parser/ast';
import {HtmlParser} from '@angular/compiler/src/ml_parser/html_parser';
import {getHtmlTagDefinition} from '@angular/compiler/src/ml_parser/html_tags';
export function main() {
describe('Node serializer', () => {
let parser: HtmlParser;
beforeEach(() => { parser = new HtmlParser(); });
it('should support element', () => {
const html = '<p></p>';
const ast = parser.parse(html, 'url');
expect(serializeNodes(ast.rootNodes)).toEqual([html]);
});
it('should support attributes', () => {
const html = '<p k="value"></p>';
const ast = parser.parse(html, 'url');
expect(serializeNodes(ast.rootNodes)).toEqual([html]);
});
it('should support text', () => {
const html = 'some text';
const ast = parser.parse(html, 'url');
expect(serializeNodes(ast.rootNodes)).toEqual([html]);
});
it('should support expansion', () => {
const html = '{number, plural, =0 {none} =1 {one} other {many}}';
const ast = parser.parse(html, 'url', true);
expect(serializeNodes(ast.rootNodes)).toEqual([html]);
});
it('should support comment', () => {
const html = '<!--comment-->';
const ast = parser.parse(html, 'url', true);
expect(serializeNodes(ast.rootNodes)).toEqual([html]);
});
it('should support nesting', () => {
const html = `<div i18n="meaning|desc">
<span>{{ interpolation }}</span>
<!--comment-->
<p expansion="true">
{number, plural, =0 {{sex, select, other {<b>?</b>}}}}
</p>
</div>`;
const ast = parser.parse(html, 'url', true);
expect(serializeNodes(ast.rootNodes)).toEqual([html]);
});
});
}
class _SerializerVisitor implements html.Visitor {
visitElement(element: html.Element, context: any): any {
if (getHtmlTagDefinition(element.name).isVoid) {
return `<${element.name}${this._visitAll(element.attrs, ' ')}/>`;
}
return `<${element.name}${this._visitAll(element.attrs, ' ')}>${this._visitAll(element.children)}</${element.name}>`;
}
visitAttribute(attribute: html.Attribute, context: any): any {
return `${attribute.name}="${attribute.value}"`;
}
visitText(text: html.Text, context: any): any { return text.value; }
visitComment(comment: html.Comment, context: any): any { return `<!--${comment.value}-->`; }
visitExpansion(expansion: html.Expansion, context: any): any {
return `{${expansion.switchValue}, ${expansion.type},${this._visitAll(expansion.cases)}}`;
}
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any {
return ` ${expansionCase.value} {${this._visitAll(expansionCase.expression)}}`;
}
private _visitAll(nodes: html.Node[], join: string = ''): string {
if (nodes.length == 0) {
return '';
}
return join + nodes.map(a => a.visit(this, null)).join(join);
}
}
const serializerVisitor = new _SerializerVisitor();
export function serializeNodes(nodes: html.Node[]): string[] {
return nodes.map(node => node.visit(serializerVisitor, null));
}

View File

@ -0,0 +1,84 @@
/**
* @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 * as html from '../../src/ml_parser/ast';
import {ParseTreeResult} from '../../src/ml_parser/html_parser';
import {ParseLocation} from '../../src/parse_util';
export function humanizeDom(parseResult: ParseTreeResult, addSourceSpan: boolean = false): any[] {
if (parseResult.errors.length > 0) {
const errorString = parseResult.errors.join('\n');
throw new Error(`Unexpected parse errors:\n${errorString}`);
}
return humanizeNodes(parseResult.rootNodes, addSourceSpan);
}
export function humanizeDomSourceSpans(parseResult: ParseTreeResult): any[] {
return humanizeDom(parseResult, true);
}
export function humanizeNodes(nodes: html.Node[], addSourceSpan: boolean = false): any[] {
const humanizer = new _Humanizer(addSourceSpan);
html.visitAll(humanizer, nodes);
return humanizer.result;
}
export function humanizeLineColumn(location: ParseLocation): string {
return `${location.line}:${location.col}`;
}
class _Humanizer implements html.Visitor {
result: any[] = [];
elDepth: number = 0;
constructor(private includeSourceSpan: boolean){};
visitElement(element: html.Element, context: any): any {
const res = this._appendContext(element, [html.Element, element.name, this.elDepth++]);
this.result.push(res);
html.visitAll(this, element.attrs);
html.visitAll(this, element.children);
this.elDepth--;
}
visitAttribute(attribute: html.Attribute, context: any): any {
const res = this._appendContext(attribute, [html.Attribute, attribute.name, attribute.value]);
this.result.push(res);
}
visitText(text: html.Text, context: any): any {
const res = this._appendContext(text, [html.Text, text.value, this.elDepth]);
this.result.push(res);
}
visitComment(comment: html.Comment, context: any): any {
const res = this._appendContext(comment, [html.Comment, comment.value, this.elDepth]);
this.result.push(res);
}
visitExpansion(expansion: html.Expansion, context: any): any {
const res = this._appendContext(
expansion, [html.Expansion, expansion.switchValue, expansion.type, this.elDepth++]);
this.result.push(res);
html.visitAll(this, expansion.cases);
this.elDepth--;
}
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any {
const res =
this._appendContext(expansionCase, [html.ExpansionCase, expansionCase.value, this.elDepth]);
this.result.push(res);
}
private _appendContext(ast: html.Node, input: any[]): any[] {
if (!this.includeSourceSpan) return input;
input.push(ast.sourceSpan.toString());
return input;
}
}

View File

@ -0,0 +1,543 @@
/**
* @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 * as html from '../../src/ml_parser/ast';
import {HtmlParser, ParseTreeResult, TreeError} from '../../src/ml_parser/html_parser';
import {TokenType} from '../../src/ml_parser/lexer';
import {ParseError} from '../../src/parse_util';
import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spec_utils';
export function main() {
describe('HtmlParser', () => {
let parser: HtmlParser;
beforeEach(() => { parser = new HtmlParser(); });
describe('parse', () => {
describe('text nodes', () => {
it('should parse root level text nodes', () => {
expect(humanizeDom(parser.parse('a', 'TestComp'))).toEqual([[html.Text, 'a', 0]]);
});
it('should parse text nodes inside regular elements', () => {
expect(humanizeDom(parser.parse('<div>a</div>', 'TestComp'))).toEqual([
[html.Element, 'div', 0], [html.Text, 'a', 1]
]);
});
it('should parse text nodes inside <ng-template> elements', () => {
// deprecated in 4.0
expect(humanizeDom(parser.parse('<template>a</template>', 'TestComp'))).toEqual([
[html.Element, 'template', 0], [html.Text, 'a', 1]
]);
expect(humanizeDom(parser.parse('<ng-template>a</ng-template>', 'TestComp'))).toEqual([
[html.Element, 'ng-template', 0], [html.Text, 'a', 1]
]);
});
it('should parse CDATA', () => {
expect(humanizeDom(parser.parse('<![CDATA[text]]>', 'TestComp'))).toEqual([
[html.Text, 'text', 0]
]);
});
});
describe('elements', () => {
it('should parse root level elements', () => {
expect(humanizeDom(parser.parse('<div></div>', 'TestComp'))).toEqual([
[html.Element, 'div', 0]
]);
});
it('should parse elements inside of regular elements', () => {
expect(humanizeDom(parser.parse('<div><span></span></div>', 'TestComp'))).toEqual([
[html.Element, 'div', 0], [html.Element, 'span', 1]
]);
});
it('should parse elements inside <ng-template> elements', () => {
expect(humanizeDom(parser.parse('<template><span></span></template>', 'TestComp')))
.toEqual([[html.Element, 'template', 0], [html.Element, 'span', 1]]);
expect(humanizeDom(parser.parse('<ng-template><span></span></ng-template>', 'TestComp')))
.toEqual([[html.Element, 'ng-template', 0], [html.Element, 'span', 1]]);
});
it('should support void elements', () => {
expect(humanizeDom(parser.parse('<link rel="author license" href="/about">', 'TestComp')))
.toEqual([
[html.Element, 'link', 0],
[html.Attribute, 'rel', 'author license'],
[html.Attribute, 'href', '/about'],
]);
});
it('should not error on void elements from HTML5 spec',
() => { // http://www.w3.org/TR/html-markup/syntax.html#syntax-elements without:
// <base> - it can be present in head only
// <meta> - it can be present in head only
// <command> - obsolete
// <keygen> - obsolete
['<map><area></map>', '<div><br></div>', '<colgroup><col></colgroup>',
'<div><embed></div>', '<div><hr></div>', '<div><img></div>', '<div><input></div>',
'<object><param>/<object>', '<audio><source></audio>', '<audio><track></audio>',
'<p><wbr></p>',
].forEach((html) => { expect(parser.parse(html, 'TestComp').errors).toEqual([]); });
});
it('should close void elements on text nodes', () => {
expect(humanizeDom(parser.parse('<p>before<br>after</p>', 'TestComp'))).toEqual([
[html.Element, 'p', 0],
[html.Text, 'before', 1],
[html.Element, 'br', 1],
[html.Text, 'after', 1],
]);
});
it('should support optional end tags', () => {
expect(humanizeDom(parser.parse('<div><p>1<p>2</div>', 'TestComp'))).toEqual([
[html.Element, 'div', 0],
[html.Element, 'p', 1],
[html.Text, '1', 2],
[html.Element, 'p', 1],
[html.Text, '2', 2],
]);
});
it('should support nested elements', () => {
expect(humanizeDom(parser.parse('<ul><li><ul><li></li></ul></li></ul>', 'TestComp')))
.toEqual([
[html.Element, 'ul', 0],
[html.Element, 'li', 1],
[html.Element, 'ul', 2],
[html.Element, 'li', 3],
]);
});
it('should add the requiredParent', () => {
expect(
humanizeDom(parser.parse(
'<table><thead><tr head></tr></thead><tr noparent></tr><tbody><tr body></tr></tbody><tfoot><tr foot></tr></tfoot></table>',
'TestComp')))
.toEqual([
[html.Element, 'table', 0],
[html.Element, 'thead', 1],
[html.Element, 'tr', 2],
[html.Attribute, 'head', ''],
[html.Element, 'tbody', 1],
[html.Element, 'tr', 2],
[html.Attribute, 'noparent', ''],
[html.Element, 'tbody', 1],
[html.Element, 'tr', 2],
[html.Attribute, 'body', ''],
[html.Element, 'tfoot', 1],
[html.Element, 'tr', 2],
[html.Attribute, 'foot', ''],
]);
});
it('should append the required parent considering ng-container', () => {
expect(humanizeDom(parser.parse(
'<table><ng-container><tr></tr></ng-container></table>', 'TestComp')))
.toEqual([
[html.Element, 'table', 0],
[html.Element, 'tbody', 1],
[html.Element, 'ng-container', 2],
[html.Element, 'tr', 3],
]);
});
it('should special case ng-container when adding a required parent', () => {
expect(humanizeDom(parser.parse(
'<table><thead><ng-container><tr></tr></ng-container></thead></table>',
'TestComp')))
.toEqual([
[html.Element, 'table', 0],
[html.Element, 'thead', 1],
[html.Element, 'ng-container', 2],
[html.Element, 'tr', 3],
]);
});
it('should not add the requiredParent when the parent is a <ng-template>', () => {
expect(humanizeDom(parser.parse('<template><tr></tr></template>', 'TestComp'))).toEqual([
[html.Element, 'template', 0],
[html.Element, 'tr', 1],
]);
expect(humanizeDom(parser.parse('<ng-template><tr></tr></ng-template>', 'TestComp')))
.toEqual([
[html.Element, 'ng-template', 0],
[html.Element, 'tr', 1],
]);
});
// https://github.com/angular/angular/issues/5967
it('should not add the requiredParent to a template root element', () => {
expect(humanizeDom(parser.parse('<tr></tr>', 'TestComp'))).toEqual([
[html.Element, 'tr', 0],
]);
});
it('should support explicit mamespace', () => {
expect(humanizeDom(parser.parse('<myns:div></myns:div>', 'TestComp'))).toEqual([
[html.Element, ':myns:div', 0]
]);
});
it('should support implicit mamespace', () => {
expect(humanizeDom(parser.parse('<svg></svg>', 'TestComp'))).toEqual([
[html.Element, ':svg:svg', 0]
]);
});
it('should propagate the namespace', () => {
expect(humanizeDom(parser.parse('<myns:div><p></p></myns:div>', 'TestComp'))).toEqual([
[html.Element, ':myns:div', 0],
[html.Element, ':myns:p', 1],
]);
});
it('should match closing tags case sensitive', () => {
const errors = parser.parse('<DiV><P></p></dIv>', 'TestComp').errors;
expect(errors.length).toEqual(2);
expect(humanizeErrors(errors)).toEqual([
['p', 'Unexpected closing tag "p"', '0:8'],
['dIv', 'Unexpected closing tag "dIv"', '0:12'],
]);
});
it('should support self closing void elements', () => {
expect(humanizeDom(parser.parse('<input />', 'TestComp'))).toEqual([
[html.Element, 'input', 0]
]);
});
it('should support self closing foreign elements', () => {
expect(humanizeDom(parser.parse('<math />', 'TestComp'))).toEqual([
[html.Element, ':math:math', 0]
]);
});
it('should ignore LF immediately after textarea, pre and listing', () => {
expect(humanizeDom(parser.parse(
'<p>\n</p><textarea>\n</textarea><pre>\n\n</pre><listing>\n\n</listing>',
'TestComp')))
.toEqual([
[html.Element, 'p', 0],
[html.Text, '\n', 1],
[html.Element, 'textarea', 0],
[html.Element, 'pre', 0],
[html.Text, '\n', 1],
[html.Element, 'listing', 0],
[html.Text, '\n', 1],
]);
});
});
describe('attributes', () => {
it('should parse attributes on regular elements case sensitive', () => {
expect(humanizeDom(parser.parse('<div kEy="v" key2=v2></div>', 'TestComp'))).toEqual([
[html.Element, 'div', 0],
[html.Attribute, 'kEy', 'v'],
[html.Attribute, 'key2', 'v2'],
]);
});
it('should parse attributes without values', () => {
expect(humanizeDom(parser.parse('<div k></div>', 'TestComp'))).toEqual([
[html.Element, 'div', 0],
[html.Attribute, 'k', ''],
]);
});
it('should parse attributes on svg elements case sensitive', () => {
expect(humanizeDom(parser.parse('<svg viewBox="0"></svg>', 'TestComp'))).toEqual([
[html.Element, ':svg:svg', 0],
[html.Attribute, 'viewBox', '0'],
]);
});
it('should parse attributes on <ng-template> elements', () => {
expect(humanizeDom(parser.parse('<template k="v"></template>', 'TestComp'))).toEqual([
[html.Element, 'template', 0],
[html.Attribute, 'k', 'v'],
]);
expect(humanizeDom(parser.parse('<ng-template k="v"></ng-template>', 'TestComp')))
.toEqual([
[html.Element, 'ng-template', 0],
[html.Attribute, 'k', 'v'],
]);
});
it('should support namespace', () => {
expect(humanizeDom(parser.parse('<svg:use xlink:href="Port" />', 'TestComp'))).toEqual([
[html.Element, ':svg:use', 0],
[html.Attribute, ':xlink:href', 'Port'],
]);
});
});
describe('comments', () => {
it('should preserve comments', () => {
expect(humanizeDom(parser.parse('<!-- comment --><div></div>', 'TestComp'))).toEqual([
[html.Comment, 'comment', 0],
[html.Element, 'div', 0],
]);
});
});
describe('expansion forms', () => {
it('should parse out expansion forms', () => {
const parsed = parser.parse(
`<div>before{messages.length, plural, =0 {You have <b>no</b> messages} =1 {One {{message}}}}after</div>`,
'TestComp', true);
expect(humanizeDom(parsed)).toEqual([
[html.Element, 'div', 0],
[html.Text, 'before', 1],
[html.Expansion, 'messages.length', 'plural', 1],
[html.ExpansionCase, '=0', 2],
[html.ExpansionCase, '=1', 2],
[html.Text, 'after', 1],
]);
const cases = (<any>parsed.rootNodes[0]).children[1].cases;
expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([
[html.Text, 'You have ', 0],
[html.Element, 'b', 0],
[html.Text, 'no', 1],
[html.Text, ' messages', 0],
]);
expect(humanizeDom(new ParseTreeResult(cases[1].expression, [
]))).toEqual([[html.Text, 'One {{message}}', 0]]);
});
it('should parse out expansion forms', () => {
const parsed =
parser.parse(`<div><span>{a, plural, =0 {b}}</span></div>`, 'TestComp', true);
expect(humanizeDom(parsed)).toEqual([
[html.Element, 'div', 0],
[html.Element, 'span', 1],
[html.Expansion, 'a', 'plural', 2],
[html.ExpansionCase, '=0', 3],
]);
});
it('should parse out nested expansion forms', () => {
const parsed = parser.parse(
`{messages.length, plural, =0 { {p.gender, select, male {m}} }}`, 'TestComp', true);
expect(humanizeDom(parsed)).toEqual([
[html.Expansion, 'messages.length', 'plural', 0],
[html.ExpansionCase, '=0', 1],
]);
const firstCase = (<any>parsed.rootNodes[0]).cases[0];
expect(humanizeDom(new ParseTreeResult(firstCase.expression, []))).toEqual([
[html.Expansion, 'p.gender', 'select', 0],
[html.ExpansionCase, 'male', 1],
[html.Text, ' ', 0],
]);
});
it('should error when expansion form is not closed', () => {
const p = parser.parse(`{messages.length, plural, =0 {one}`, 'TestComp', true);
expect(humanizeErrors(p.errors)).toEqual([
[null, 'Invalid ICU message. Missing \'}\'.', '0:34']
]);
});
it('should error when expansion case is not closed', () => {
const p = parser.parse(`{messages.length, plural, =0 {one`, 'TestComp', true);
expect(humanizeErrors(p.errors)).toEqual([
[null, 'Invalid ICU message. Missing \'}\'.', '0:29']
]);
});
it('should error when invalid html in the case', () => {
const p = parser.parse(`{messages.length, plural, =0 {<b/>}`, 'TestComp', true);
expect(humanizeErrors(p.errors)).toEqual([
['b', 'Only void and foreign elements can be self closed "b"', '0:30']
]);
});
});
describe('source spans', () => {
it('should store the location', () => {
expect(humanizeDomSourceSpans(parser.parse(
'<div [prop]="v1" (e)="do()" attr="v2" noValue>\na\n</div>', 'TestComp')))
.toEqual([
[html.Element, 'div', 0, '<div [prop]="v1" (e)="do()" attr="v2" noValue>'],
[html.Attribute, '[prop]', 'v1', '[prop]="v1"'],
[html.Attribute, '(e)', 'do()', '(e)="do()"'],
[html.Attribute, 'attr', 'v2', 'attr="v2"'],
[html.Attribute, 'noValue', '', 'noValue'],
[html.Text, '\na\n', 1, '\na\n'],
]);
});
it('should set the start and end source spans', () => {
const node = <html.Element>parser.parse('<div>a</div>', 'TestComp').rootNodes[0];
expect(node.startSourceSpan.start.offset).toEqual(0);
expect(node.startSourceSpan.end.offset).toEqual(5);
expect(node.endSourceSpan.start.offset).toEqual(6);
expect(node.endSourceSpan.end.offset).toEqual(12);
});
it('should support expansion form', () => {
expect(humanizeDomSourceSpans(
parser.parse('<div>{count, plural, =0 {msg}}</div>', 'TestComp', true)))
.toEqual([
[html.Element, 'div', 0, '<div>'],
[html.Expansion, 'count', 'plural', 1, '{count, plural, =0 {msg}}'],
[html.ExpansionCase, '=0', 2, '=0 {msg}'],
]);
});
it('should not report a value span for an attribute without a value', () => {
const ast = parser.parse('<div bar></div>', 'TestComp');
expect((ast.rootNodes[0] as html.Element).attrs[0].valueSpan).toBeUndefined();
});
it('should report a value span for an attibute with a value', () => {
const ast = parser.parse('<div bar="12"></div>', 'TestComp');
const attr = (ast.rootNodes[0] as html.Element).attrs[0];
expect(attr.valueSpan.start.offset).toEqual(9);
expect(attr.valueSpan.end.offset).toEqual(13);
});
});
describe('visitor', () => {
it('should visit text nodes', () => {
const result = humanizeDom(parser.parse('text', 'TestComp'));
expect(result).toEqual([[html.Text, 'text', 0]]);
});
it('should visit element nodes', () => {
const result = humanizeDom(parser.parse('<div></div>', 'TestComp'));
expect(result).toEqual([[html.Element, 'div', 0]]);
});
it('should visit attribute nodes', () => {
const result = humanizeDom(parser.parse('<div id="foo"></div>', 'TestComp'));
expect(result).toContain([html.Attribute, 'id', 'foo']);
});
it('should visit all nodes', () => {
const result =
parser.parse('<div id="foo"><span id="bar">a</span><span>b</span></div>', 'TestComp');
const accumulator: html.Node[] = [];
const visitor = new class {
visit(node: html.Node, context: any) { accumulator.push(node); }
visitElement(element: html.Element, context: any): any {
html.visitAll(this, element.attrs);
html.visitAll(this, element.children);
}
visitAttribute(attribute: html.Attribute, context: any): any {}
visitText(text: html.Text, context: any): any {}
visitComment(comment: html.Comment, context: any): any {}
visitExpansion(expansion: html.Expansion, context: any): any {
html.visitAll(this, expansion.cases);
}
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any {}
};
html.visitAll(visitor, result.rootNodes);
expect(accumulator.map(n => n.constructor)).toEqual([
html.Element, html.Attribute, html.Element, html.Attribute, html.Text, html.Element,
html.Text
]);
});
it('should skip typed visit if visit() returns a truthy value', () => {
const visitor = new class {
visit(node: html.Node, context: any) { return true; }
visitElement(element: html.Element, context: any): any { throw Error('Unexpected'); }
visitAttribute(attribute: html.Attribute, context: any): any {
throw Error('Unexpected');
}
visitText(text: html.Text, context: any): any { throw Error('Unexpected'); }
visitComment(comment: html.Comment, context: any): any { throw Error('Unexpected'); }
visitExpansion(expansion: html.Expansion, context: any): any {
throw Error('Unexpected');
}
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any {
throw Error('Unexpected');
}
};
const result = parser.parse('<div id="foo"></div><div id="bar"></div>', 'TestComp');
const traversal = html.visitAll(visitor, result.rootNodes);
expect(traversal).toEqual([true, true]);
});
});
describe('errors', () => {
it('should report unexpected closing tags', () => {
const errors = parser.parse('<div></p></div>', 'TestComp').errors;
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors)).toEqual([['p', 'Unexpected closing tag "p"', '0:5']]);
});
it('should report subsequent open tags without proper close tag', () => {
const errors = parser.parse('<div</div>', 'TestComp').errors;
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors)).toEqual([['div', 'Unexpected closing tag "div"', '0:4']]);
});
it('should report closing tag for void elements', () => {
const errors = parser.parse('<input></input>', 'TestComp').errors;
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors)).toEqual([
['input', 'Void elements do not have end tags "input"', '0:7']
]);
});
it('should report self closing html element', () => {
const errors = parser.parse('<p />', 'TestComp').errors;
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors)).toEqual([
['p', 'Only void and foreign elements can be self closed "p"', '0:0']
]);
});
it('should report self closing custom element', () => {
const errors = parser.parse('<my-cmp />', 'TestComp').errors;
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors)).toEqual([
['my-cmp', 'Only void and foreign elements can be self closed "my-cmp"', '0:0']
]);
});
it('should also report lexer errors', () => {
const errors = parser.parse('<!-err--><div></p></div>', 'TestComp').errors;
expect(errors.length).toEqual(2);
expect(humanizeErrors(errors)).toEqual([
[TokenType.COMMENT_START, 'Unexpected character "e"', '0:3'],
['p', 'Unexpected closing tag "p"', '0:14']
]);
});
});
});
});
}
export function humanizeErrors(errors: ParseError[]): any[] {
return errors.map(e => {
if (e instanceof TreeError) {
// Parser errors
return [<any>e.elementName, e.msg, humanizeLineColumn(e.span.start)];
}
// Tokenizer errors
return [(<any>e).tokenType, e.msg, humanizeLineColumn(e.span.start)];
});
}

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 * as html from '../../src/ml_parser/ast';
import {HtmlParser} from '../../src/ml_parser/html_parser';
import {ExpansionResult, expandNodes} from '../../src/ml_parser/icu_ast_expander';
import {ParseError} from '../../src/parse_util';
import {humanizeNodes} from './ast_spec_utils';
export function main() {
describe('Expander', () => {
function expand(template: string): ExpansionResult {
const htmlParser = new HtmlParser();
const res = htmlParser.parse(template, 'url', true);
return expandNodes(res.rootNodes);
}
it('should handle the plural expansion form', () => {
const res = expand(`{messages.length, plural,=0 {zero<b>bold</b>}}`);
expect(humanizeNodes(res.nodes)).toEqual([
[html.Element, 'ng-container', 0],
[html.Attribute, '[ngPlural]', 'messages.length'],
[html.Element, 'ng-template', 1],
[html.Attribute, 'ngPluralCase', '=0'],
[html.Text, 'zero', 2],
[html.Element, 'b', 2],
[html.Text, 'bold', 3],
]);
});
it('should handle nested expansion forms', () => {
const res = expand(`{messages.length, plural, =0 { {p.gender, select, =m {m}} }}`);
expect(humanizeNodes(res.nodes)).toEqual([
[html.Element, 'ng-container', 0],
[html.Attribute, '[ngPlural]', 'messages.length'],
[html.Element, 'ng-template', 1],
[html.Attribute, 'ngPluralCase', '=0'],
[html.Element, 'ng-container', 2],
[html.Attribute, '[ngSwitch]', 'p.gender'],
[html.Element, 'ng-template', 3],
[html.Attribute, 'ngSwitchCase', '=m'],
[html.Text, 'm', 4],
[html.Text, ' ', 2],
]);
});
it('should correctly set source code positions', () => {
const nodes = expand(`{messages.length, plural,=0 {<b>bold</b>}}`).nodes;
const container: html.Element = <html.Element>nodes[0];
expect(container.sourceSpan.start.col).toEqual(0);
expect(container.sourceSpan.end.col).toEqual(42);
expect(container.startSourceSpan.start.col).toEqual(0);
expect(container.startSourceSpan.end.col).toEqual(42);
expect(container.endSourceSpan.start.col).toEqual(0);
expect(container.endSourceSpan.end.col).toEqual(42);
const switchExp = container.attrs[0];
expect(switchExp.sourceSpan.start.col).toEqual(1);
expect(switchExp.sourceSpan.end.col).toEqual(16);
const template: html.Element = <html.Element>container.children[0];
expect(template.sourceSpan.start.col).toEqual(25);
expect(template.sourceSpan.end.col).toEqual(41);
const switchCheck = template.attrs[0];
expect(switchCheck.sourceSpan.start.col).toEqual(25);
expect(switchCheck.sourceSpan.end.col).toEqual(28);
const b: html.Element = <html.Element>template.children[0];
expect(b.sourceSpan.start.col).toEqual(29);
expect(b.endSourceSpan.end.col).toEqual(40);
});
it('should handle other special forms', () => {
const res = expand(`{person.gender, select, male {m} other {default}}`);
expect(humanizeNodes(res.nodes)).toEqual([
[html.Element, 'ng-container', 0],
[html.Attribute, '[ngSwitch]', 'person.gender'],
[html.Element, 'ng-template', 1],
[html.Attribute, 'ngSwitchCase', 'male'],
[html.Text, 'm', 2],
[html.Element, 'ng-template', 1],
[html.Attribute, 'ngSwitchDefault', ''],
[html.Text, 'default', 2],
]);
});
it('should parse an expansion form as a tag single child', () => {
const res = expand(`<div><span>{a, b, =4 {c}}</span></div>`);
expect(humanizeNodes(res.nodes)).toEqual([
[html.Element, 'div', 0],
[html.Element, 'span', 1],
[html.Element, 'ng-container', 2],
[html.Attribute, '[ngSwitch]', 'a'],
[html.Element, 'ng-template', 3],
[html.Attribute, 'ngSwitchCase', '=4'],
[html.Text, 'c', 4],
]);
});
describe('errors', () => {
it('should error on unknown plural cases', () => {
expect(humanizeErrors(expand('{n, plural, unknown {-}}').errors)).toEqual([
`Plural cases should be "=<number>" or one of zero, one, two, few, many, other`,
]);
});
});
});
}
function humanizeErrors(errors: ParseError[]): string[] {
return errors.map(error => error.msg);
}

View File

@ -0,0 +1,830 @@
/**
* @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 {getHtmlTagDefinition} from '../../src/ml_parser/html_tags';
import {InterpolationConfig} from '../../src/ml_parser/interpolation_config';
import * as lex from '../../src/ml_parser/lexer';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util';
export function main() {
describe('HtmlLexer', () => {
describe('line/column numbers', () => {
it('should work without newlines', () => {
expect(tokenizeAndHumanizeLineColumn('<t>a</t>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '0:0'],
[lex.TokenType.TAG_OPEN_END, '0:2'],
[lex.TokenType.TEXT, '0:3'],
[lex.TokenType.TAG_CLOSE, '0:4'],
[lex.TokenType.EOF, '0:8'],
]);
});
it('should work with one newline', () => {
expect(tokenizeAndHumanizeLineColumn('<t>\na</t>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '0:0'],
[lex.TokenType.TAG_OPEN_END, '0:2'],
[lex.TokenType.TEXT, '0:3'],
[lex.TokenType.TAG_CLOSE, '1:1'],
[lex.TokenType.EOF, '1:5'],
]);
});
it('should work with multiple newlines', () => {
expect(tokenizeAndHumanizeLineColumn('<t\n>\na</t>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '0:0'],
[lex.TokenType.TAG_OPEN_END, '1:0'],
[lex.TokenType.TEXT, '1:1'],
[lex.TokenType.TAG_CLOSE, '2:1'],
[lex.TokenType.EOF, '2:5'],
]);
});
it('should work with CR and LF', () => {
expect(tokenizeAndHumanizeLineColumn('<t\n>\r\na\r</t>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '0:0'],
[lex.TokenType.TAG_OPEN_END, '1:0'],
[lex.TokenType.TEXT, '1:1'],
[lex.TokenType.TAG_CLOSE, '2:1'],
[lex.TokenType.EOF, '2:5'],
]);
});
});
describe('comments', () => {
it('should parse comments', () => {
expect(tokenizeAndHumanizeParts('<!--t\ne\rs\r\nt-->')).toEqual([
[lex.TokenType.COMMENT_START],
[lex.TokenType.RAW_TEXT, 't\ne\ns\nt'],
[lex.TokenType.COMMENT_END],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<!--t\ne\rs\r\nt-->')).toEqual([
[lex.TokenType.COMMENT_START, '<!--'],
[lex.TokenType.RAW_TEXT, 't\ne\rs\r\nt'],
[lex.TokenType.COMMENT_END, '-->'],
[lex.TokenType.EOF, ''],
]);
});
it('should report <!- without -', () => {
expect(tokenizeAndHumanizeErrors('<!-a')).toEqual([
[lex.TokenType.COMMENT_START, 'Unexpected character "a"', '0:3']
]);
});
it('should report missing end comment', () => {
expect(tokenizeAndHumanizeErrors('<!--')).toEqual([
[lex.TokenType.RAW_TEXT, 'Unexpected character "EOF"', '0:4']
]);
});
it('should accept comments finishing by too many dashes (even number)', () => {
expect(tokenizeAndHumanizeSourceSpans('<!-- test ---->')).toEqual([
[lex.TokenType.COMMENT_START, '<!--'],
[lex.TokenType.RAW_TEXT, ' test --'],
[lex.TokenType.COMMENT_END, '-->'],
[lex.TokenType.EOF, ''],
]);
});
it('should accept comments finishing by too many dashes (odd number)', () => {
expect(tokenizeAndHumanizeSourceSpans('<!-- test --->')).toEqual([
[lex.TokenType.COMMENT_START, '<!--'],
[lex.TokenType.RAW_TEXT, ' test -'],
[lex.TokenType.COMMENT_END, '-->'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('doctype', () => {
it('should parse doctypes', () => {
expect(tokenizeAndHumanizeParts('<!doctype html>')).toEqual([
[lex.TokenType.DOC_TYPE, 'doctype html'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<!doctype html>')).toEqual([
[lex.TokenType.DOC_TYPE, '<!doctype html>'],
[lex.TokenType.EOF, ''],
]);
});
it('should report missing end doctype', () => {
expect(tokenizeAndHumanizeErrors('<!')).toEqual([
[lex.TokenType.DOC_TYPE, 'Unexpected character "EOF"', '0:2']
]);
});
});
describe('CDATA', () => {
it('should parse CDATA', () => {
expect(tokenizeAndHumanizeParts('<![CDATA[t\ne\rs\r\nt]]>')).toEqual([
[lex.TokenType.CDATA_START],
[lex.TokenType.RAW_TEXT, 't\ne\ns\nt'],
[lex.TokenType.CDATA_END],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<![CDATA[t\ne\rs\r\nt]]>')).toEqual([
[lex.TokenType.CDATA_START, '<![CDATA['],
[lex.TokenType.RAW_TEXT, 't\ne\rs\r\nt'],
[lex.TokenType.CDATA_END, ']]>'],
[lex.TokenType.EOF, ''],
]);
});
it('should report <![ without CDATA[', () => {
expect(tokenizeAndHumanizeErrors('<![a')).toEqual([
[lex.TokenType.CDATA_START, 'Unexpected character "a"', '0:3']
]);
});
it('should report missing end cdata', () => {
expect(tokenizeAndHumanizeErrors('<![CDATA[')).toEqual([
[lex.TokenType.RAW_TEXT, 'Unexpected character "EOF"', '0:9']
]);
});
});
describe('open tags', () => {
it('should parse open tags without prefix', () => {
expect(tokenizeAndHumanizeParts('<test>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'test'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse namespace prefix', () => {
expect(tokenizeAndHumanizeParts('<ns1:test>')).toEqual([
[lex.TokenType.TAG_OPEN_START, 'ns1', 'test'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse void tags', () => {
expect(tokenizeAndHumanizeParts('<test/>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'test'],
[lex.TokenType.TAG_OPEN_END_VOID],
[lex.TokenType.EOF],
]);
});
it('should allow whitespace after the tag name', () => {
expect(tokenizeAndHumanizeParts('<test >')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'test'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<test>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '<test'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('attributes', () => {
it('should parse attributes without prefix', () => {
expect(tokenizeAndHumanizeParts('<t a>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with interpolation', () => {
expect(tokenizeAndHumanizeParts('<t a="{{v}}" b="s{{m}}e" c="s{{m//c}}e">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, '{{v}}'],
[lex.TokenType.ATTR_NAME, null, 'b'],
[lex.TokenType.ATTR_VALUE, 's{{m}}e'],
[lex.TokenType.ATTR_NAME, null, 'c'],
[lex.TokenType.ATTR_VALUE, 's{{m//c}}e'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with prefix', () => {
expect(tokenizeAndHumanizeParts('<t ns1:a>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, 'ns1', 'a'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes whose prefix is not valid', () => {
expect(tokenizeAndHumanizeParts('<t (ns1:a)>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, '(ns1:a)'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with single quote value', () => {
expect(tokenizeAndHumanizeParts('<t a=\'b\'>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with double quote value', () => {
expect(tokenizeAndHumanizeParts('<t a="b">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with unquoted value', () => {
expect(tokenizeAndHumanizeParts('<t a=b>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should allow whitespace', () => {
expect(tokenizeAndHumanizeParts('<t a = b >')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with entities in values', () => {
expect(tokenizeAndHumanizeParts('<t a="&#65;&#x41;">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'AA'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should not decode entities without trailing ";"', () => {
expect(tokenizeAndHumanizeParts('<t a="&amp" b="c&&d">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, '&amp'],
[lex.TokenType.ATTR_NAME, null, 'b'],
[lex.TokenType.ATTR_VALUE, 'c&&d'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with "&" in values', () => {
expect(tokenizeAndHumanizeParts('<t a="b && c &">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b && c &'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse values with CR and LF', () => {
expect(tokenizeAndHumanizeParts('<t a=\'t\ne\rs\r\nt\'>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 't\ne\ns\nt'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<t a=b>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '<t'],
[lex.TokenType.ATTR_NAME, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('closing tags', () => {
it('should parse closing tags without prefix', () => {
expect(tokenizeAndHumanizeParts('</test>')).toEqual([
[lex.TokenType.TAG_CLOSE, null, 'test'],
[lex.TokenType.EOF],
]);
});
it('should parse closing tags with prefix', () => {
expect(tokenizeAndHumanizeParts('</ns1:test>')).toEqual([
[lex.TokenType.TAG_CLOSE, 'ns1', 'test'],
[lex.TokenType.EOF],
]);
});
it('should allow whitespace', () => {
expect(tokenizeAndHumanizeParts('</ test >')).toEqual([
[lex.TokenType.TAG_CLOSE, null, 'test'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('</test>')).toEqual([
[lex.TokenType.TAG_CLOSE, '</test>'],
[lex.TokenType.EOF, ''],
]);
});
it('should report missing name after </', () => {
expect(tokenizeAndHumanizeErrors('</')).toEqual([
[lex.TokenType.TAG_CLOSE, 'Unexpected character "EOF"', '0:2']
]);
});
it('should report missing >', () => {
expect(tokenizeAndHumanizeErrors('</test')).toEqual([
[lex.TokenType.TAG_CLOSE, 'Unexpected character "EOF"', '0:6']
]);
});
});
describe('entities', () => {
it('should parse named entities', () => {
expect(tokenizeAndHumanizeParts('a&amp;b')).toEqual([
[lex.TokenType.TEXT, 'a&b'],
[lex.TokenType.EOF],
]);
});
it('should parse hexadecimal entities', () => {
expect(tokenizeAndHumanizeParts('&#x41;&#X41;')).toEqual([
[lex.TokenType.TEXT, 'AA'],
[lex.TokenType.EOF],
]);
});
it('should parse decimal entities', () => {
expect(tokenizeAndHumanizeParts('&#65;')).toEqual([
[lex.TokenType.TEXT, 'A'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('a&amp;b')).toEqual([
[lex.TokenType.TEXT, 'a&amp;b'],
[lex.TokenType.EOF, ''],
]);
});
it('should report malformed/unknown entities', () => {
expect(tokenizeAndHumanizeErrors('&tbo;')).toEqual([[
lex.TokenType.TEXT,
'Unknown entity "tbo" - use the "&#<decimal>;" or "&#x<hex>;" syntax', '0:0'
]]);
expect(tokenizeAndHumanizeErrors('&#asdf;')).toEqual([
[lex.TokenType.TEXT, 'Unexpected character "s"', '0:3']
]);
expect(tokenizeAndHumanizeErrors('&#xasdf;')).toEqual([
[lex.TokenType.TEXT, 'Unexpected character "s"', '0:4']
]);
expect(tokenizeAndHumanizeErrors('&#xABC')).toEqual([
[lex.TokenType.TEXT, 'Unexpected character "EOF"', '0:6']
]);
});
});
describe('regular text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts('a')).toEqual([
[lex.TokenType.TEXT, 'a'],
[lex.TokenType.EOF],
]);
});
it('should parse interpolation', () => {
expect(tokenizeAndHumanizeParts('{{ a }}b{{ c // comment }}')).toEqual([
[lex.TokenType.TEXT, '{{ a }}b{{ c // comment }}'],
[lex.TokenType.EOF],
]);
});
it('should parse interpolation with custom markers', () => {
expect(tokenizeAndHumanizeParts('{% a %}', null, {start: '{%', end: '%}'})).toEqual([
[lex.TokenType.TEXT, '{% a %}'],
[lex.TokenType.EOF],
]);
});
it('should handle CR & LF', () => {
expect(tokenizeAndHumanizeParts('t\ne\rs\r\nt')).toEqual([
[lex.TokenType.TEXT, 't\ne\ns\nt'],
[lex.TokenType.EOF],
]);
});
it('should parse entities', () => {
expect(tokenizeAndHumanizeParts('a&amp;b')).toEqual([
[lex.TokenType.TEXT, 'a&b'],
[lex.TokenType.EOF],
]);
});
it('should parse text starting with "&"', () => {
expect(tokenizeAndHumanizeParts('a && b &')).toEqual([
[lex.TokenType.TEXT, 'a && b &'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('a')).toEqual([
[lex.TokenType.TEXT, 'a'],
[lex.TokenType.EOF, ''],
]);
});
it('should allow "<" in text nodes', () => {
expect(tokenizeAndHumanizeParts('{{ a < b ? c : d }}')).toEqual([
[lex.TokenType.TEXT, '{{ a < b ? c : d }}'],
[lex.TokenType.EOF],
]);
expect(tokenizeAndHumanizeSourceSpans('<p>a<b</p>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '<p'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.TEXT, 'a<b'],
[lex.TokenType.TAG_CLOSE, '</p>'],
[lex.TokenType.EOF, ''],
]);
expect(tokenizeAndHumanizeParts('< a>')).toEqual([
[lex.TokenType.TEXT, '< a>'],
[lex.TokenType.EOF],
]);
});
it('should parse valid start tag in interpolation', () => {
expect(tokenizeAndHumanizeParts('{{ a <b && c > d }}')).toEqual([
[lex.TokenType.TEXT, '{{ a '],
[lex.TokenType.TAG_OPEN_START, null, 'b'],
[lex.TokenType.ATTR_NAME, null, '&&'],
[lex.TokenType.ATTR_NAME, null, 'c'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.TEXT, ' d }}'],
[lex.TokenType.EOF],
]);
});
it('should be able to escape {', () => {
expect(tokenizeAndHumanizeParts('{{ "{" }}')).toEqual([
[lex.TokenType.TEXT, '{{ "{" }}'],
[lex.TokenType.EOF],
]);
});
it('should be able to escape {{', () => {
expect(tokenizeAndHumanizeParts('{{ "{{" }}')).toEqual([
[lex.TokenType.TEXT, '{{ "{{" }}'],
[lex.TokenType.EOF],
]);
});
it('should treat expansion form as text when they are not parsed', () => {
expect(tokenizeAndHumanizeParts('<span>{a, b, =4 {c}}</span>', false)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'span'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.TEXT, '{a, b, =4 {c}}'],
[lex.TokenType.TAG_CLOSE, null, 'span'],
[lex.TokenType.EOF],
]);
});
});
describe('raw text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts(`<script>t\ne\rs\r\nt</script>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'script'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.RAW_TEXT, 't\ne\ns\nt'],
[lex.TokenType.TAG_CLOSE, null, 'script'],
[lex.TokenType.EOF],
]);
});
it('should not detect entities', () => {
expect(tokenizeAndHumanizeParts(`<script>&amp;</SCRIPT>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'script'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.RAW_TEXT, '&amp;'],
[lex.TokenType.TAG_CLOSE, null, 'script'],
[lex.TokenType.EOF],
]);
});
it('should ignore other opening tags', () => {
expect(tokenizeAndHumanizeParts(`<script>a<div></script>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'script'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.RAW_TEXT, 'a<div>'],
[lex.TokenType.TAG_CLOSE, null, 'script'],
[lex.TokenType.EOF],
]);
});
it('should ignore other closing tags', () => {
expect(tokenizeAndHumanizeParts(`<script>a</test></script>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'script'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.RAW_TEXT, 'a</test>'],
[lex.TokenType.TAG_CLOSE, null, 'script'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans(`<script>a</script>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, '<script'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.RAW_TEXT, 'a'],
[lex.TokenType.TAG_CLOSE, '</script>'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('escapable raw text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts(`<title>t\ne\rs\r\nt</title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'title'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.ESCAPABLE_RAW_TEXT, 't\ne\ns\nt'],
[lex.TokenType.TAG_CLOSE, null, 'title'],
[lex.TokenType.EOF],
]);
});
it('should detect entities', () => {
expect(tokenizeAndHumanizeParts(`<title>&amp;</title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'title'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.ESCAPABLE_RAW_TEXT, '&'],
[lex.TokenType.TAG_CLOSE, null, 'title'],
[lex.TokenType.EOF],
]);
});
it('should ignore other opening tags', () => {
expect(tokenizeAndHumanizeParts(`<title>a<div></title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'title'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.ESCAPABLE_RAW_TEXT, 'a<div>'],
[lex.TokenType.TAG_CLOSE, null, 'title'],
[lex.TokenType.EOF],
]);
});
it('should ignore other closing tags', () => {
expect(tokenizeAndHumanizeParts(`<title>a</test></title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'title'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.ESCAPABLE_RAW_TEXT, 'a</test>'],
[lex.TokenType.TAG_CLOSE, null, 'title'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans(`<title>a</title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, '<title'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.ESCAPABLE_RAW_TEXT, 'a'],
[lex.TokenType.TAG_CLOSE, '</title>'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('expansion forms', () => {
it('should parse an expansion form', () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four} =5 {five} foo {bar} }', true))
.toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'four'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_CASE_VALUE, '=5'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'five'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_CASE_VALUE, 'foo'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'bar'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.EOF],
]);
});
it('should parse an expansion form with text elements surrounding it', () => {
expect(tokenizeAndHumanizeParts('before{one.two, three, =4 {four}}after', true)).toEqual([
[lex.TokenType.TEXT, 'before'],
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'four'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.TEXT, 'after'],
[lex.TokenType.EOF],
]);
});
it('should parse an expansion form as a tag single child', () => {
expect(tokenizeAndHumanizeParts('<div><span>{a, b, =4 {c}}</span></div>', true)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'div'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.TAG_OPEN_START, null, 'span'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'a'],
[lex.TokenType.RAW_TEXT, 'b'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'c'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.TAG_CLOSE, null, 'span'],
[lex.TokenType.TAG_CLOSE, null, 'div'],
[lex.TokenType.EOF],
]);
});
it('should parse an expansion forms with elements in it', () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four <b>a</b>}}', true)).toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'four '],
[lex.TokenType.TAG_OPEN_START, null, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.TEXT, 'a'],
[lex.TokenType.TAG_CLOSE, null, 'b'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.EOF],
]);
});
it('should parse an expansion forms containing an interpolation', () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four {{a}}}}', true)).toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'four {{a}}'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.EOF],
]);
});
it('should parse nested expansion forms', () => {
expect(tokenizeAndHumanizeParts(`{one.two, three, =4 { {xx, yy, =x {one}} }}`, true))
.toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'xx'],
[lex.TokenType.RAW_TEXT, 'yy'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=x'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'one'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.TEXT, ' '],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.EOF],
]);
});
});
describe('errors', () => {
it('should report unescaped "{" on error', () => {
expect(tokenizeAndHumanizeErrors(`<p>before { after</p>`, true)).toEqual([[
lex.TokenType.RAW_TEXT,
`Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.)`,
'0:21',
]]);
});
it('should include 2 lines of context in message', () => {
const src = '111\n222\n333\nE\n444\n555\n666\n';
const file = new ParseSourceFile(src, 'file://');
const location = new ParseLocation(file, 12, 123, 456);
const span = new ParseSourceSpan(location, location);
const error = new lex.TokenError('**ERROR**', null, span);
expect(error.toString())
.toEqual(`**ERROR** ("\n222\n333\n[ERROR ->]E\n444\n555\n"): file://@123:456`);
});
});
describe('unicode characters', () => {
it('should support unicode characters', () => {
expect(tokenizeAndHumanizeSourceSpans(`<p>İ</p>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, '<p'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.TEXT, 'İ'],
[lex.TokenType.TAG_CLOSE, '</p>'],
[lex.TokenType.EOF, ''],
]);
});
});
});
}
function tokenizeWithoutErrors(
input: string, tokenizeExpansionForms: boolean = false,
interpolationConfig?: InterpolationConfig): lex.Token[] {
const tokenizeResult = lex.tokenize(
input, 'someUrl', getHtmlTagDefinition, tokenizeExpansionForms, interpolationConfig);
if (tokenizeResult.errors.length > 0) {
const errorString = tokenizeResult.errors.join('\n');
throw new Error(`Unexpected parse errors:\n${errorString}`);
}
return tokenizeResult.tokens;
}
function tokenizeAndHumanizeParts(
input: string, tokenizeExpansionForms: boolean = false,
interpolationConfig?: InterpolationConfig): any[] {
return tokenizeWithoutErrors(input, tokenizeExpansionForms, interpolationConfig)
.map(token => [<any>token.type].concat(token.parts));
}
function tokenizeAndHumanizeSourceSpans(input: string): any[] {
return tokenizeWithoutErrors(input).map(token => [<any>token.type, token.sourceSpan.toString()]);
}
function humanizeLineColumn(location: ParseLocation): string {
return `${location.line}:${location.col}`;
}
function tokenizeAndHumanizeLineColumn(input: string): any[] {
return tokenizeWithoutErrors(input).map(
token => [<any>token.type, humanizeLineColumn(token.sourceSpan.start)]);
}
function tokenizeAndHumanizeErrors(input: string, tokenizeExpansionForms: boolean = false): any[] {
return lex.tokenize(input, 'someUrl', getHtmlTagDefinition, tokenizeExpansionForms)
.errors.map(e => [<any>e.tokenType, e.msg, humanizeLineColumn(e.span.start)]);
}

View File

@ -0,0 +1,44 @@
/**
* @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 {Injector, NgModule} from '@angular/core';
import {beforeEach, describe, expect, inject, it} from '@angular/core/testing/testing_internal';
import {MockNgModuleResolver} from '../testing/index';
export function main() {
describe('MockNgModuleResolver', () => {
let ngModuleResolver: MockNgModuleResolver;
beforeEach(inject([Injector], (injector: Injector) => {
ngModuleResolver = new MockNgModuleResolver(injector);
}));
describe('NgModule overriding', () => {
it('should fallback to the default NgModuleResolver when templates are not overridden',
() => {
const ngModule = ngModuleResolver.resolve(SomeNgModule);
expect(ngModule.declarations).toEqual([SomeDirective]);
});
it('should allow overriding the @NgModule', () => {
ngModuleResolver.setNgModule(
SomeNgModule, new NgModule({declarations: [SomeOtherDirective]}));
const ngModule = ngModuleResolver.resolve(SomeNgModule);
expect(ngModule.declarations).toEqual([SomeOtherDirective]);
});
});
});
}
class SomeDirective {}
class SomeOtherDirective {}
@NgModule({declarations: [SomeDirective]})
class SomeNgModule {
}

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 {NgModuleResolver} from '@angular/compiler/src/ng_module_resolver';
import {ɵstringify as stringify} from '@angular/core';
import {NgModule} from '@angular/core/src/metadata';
class SomeClass1 {}
class SomeClass2 {}
class SomeClass3 {}
class SomeClass4 {}
class SomeClass5 {}
@NgModule({
declarations: [SomeClass1],
imports: [SomeClass2],
exports: [SomeClass3],
providers: [SomeClass4],
entryComponents: [SomeClass5]
})
class SomeModule {
}
class SimpleClass {}
export function main() {
describe('NgModuleResolver', () => {
let resolver: NgModuleResolver;
beforeEach(() => { resolver = new NgModuleResolver(); });
it('should read out the metadata from the class', () => {
const moduleMetadata = resolver.resolve(SomeModule);
expect(moduleMetadata).toEqual(new NgModule({
declarations: [SomeClass1],
imports: [SomeClass2],
exports: [SomeClass3],
providers: [SomeClass4],
entryComponents: [SomeClass5]
}));
});
it('should throw when simple class has no NgModule decorator', () => {
expect(() => resolver.resolve(SimpleClass))
.toThrowError(`No NgModule metadata found for '${stringify(SimpleClass)}'.`);
});
it('should support inheriting the metadata', function() {
@NgModule({id: 'p'})
class Parent {
}
class ChildNoDecorator extends Parent {}
@NgModule({id: 'c'})
class ChildWithDecorator extends Parent {
}
expect(resolver.resolve(ChildNoDecorator)).toEqual(new NgModule({id: 'p'}));
expect(resolver.resolve(ChildWithDecorator)).toEqual(new NgModule({id: 'c'}));
});
});
}

View File

@ -0,0 +1,148 @@
/**
* @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 {ParseLocation, ParseSourceFile, ParseSourceSpan} from '@angular/compiler';
import {EmitterVisitorContext} from '@angular/compiler/src/output/abstract_emitter';
import {SourceMap} from '@angular/compiler/src/output/source_map';
const SourceMapConsumer = require('source-map').SourceMapConsumer;
const b64 = require('base64-js');
export function main() {
describe('AbstractEmitter', () => {
describe('EmitterVisitorContext', () => {
const fileA = new ParseSourceFile('a0a1a2a3a4a5a6a7a8a9', 'a.js');
const fileB = new ParseSourceFile('b0b1b2b3b4b5b6b7b8b9', 'b.js');
let ctx: EmitterVisitorContext;
beforeEach(() => { ctx = EmitterVisitorContext.createRoot([]); });
it('should add source files to the source map', () => {
ctx.print(createSourceSpan(fileA, 0), 'o0');
ctx.print(createSourceSpan(fileA, 1), 'o1');
ctx.print(createSourceSpan(fileB, 0), 'o2');
ctx.print(createSourceSpan(fileB, 1), 'o3');
const sm = ctx.toSourceMapGenerator('o.js').toJSON();
expect(sm.sources).toEqual([fileA.url, fileB.url]);
expect(sm.sourcesContent).toEqual([fileA.content, fileB.content]);
});
it('should generate a valid mapping', () => {
ctx.print(createSourceSpan(fileA, 0), 'fileA-0');
ctx.println(createSourceSpan(fileB, 1), 'fileB-1');
ctx.print(createSourceSpan(fileA, 2), 'fileA-2');
expectMap(ctx, 0, 0, 'a.js', 0, 0);
expectMap(ctx, 0, 7, 'b.js', 0, 2);
expectMap(ctx, 1, 0, 'a.js', 0, 4);
});
it('should be able to shift the content', () => {
ctx.print(createSourceSpan(fileA, 0), 'fileA-0');
const sm = ctx.toSourceMapGenerator(null, 10).toJSON();
const smc = new SourceMapConsumer(sm);
expect(smc.originalPositionFor({line: 11, column: 0})).toEqual({
line: 1,
column: 0,
source: 'a.js',
name: null,
});
});
it('should not map leading segment without span', () => {
ctx.print(null, '....');
ctx.print(createSourceSpan(fileA, 0), 'fileA-0');
expectMap(ctx, 0, 0);
expectMap(ctx, 0, 4, 'a.js', 0, 0);
expect(nbSegmentsPerLine(ctx)).toEqual([1]);
});
it('should handle indent', () => {
ctx.incIndent();
ctx.println(createSourceSpan(fileA, 0), 'fileA-0');
ctx.incIndent();
ctx.println(createSourceSpan(fileA, 1), 'fileA-1');
ctx.decIndent();
ctx.println(createSourceSpan(fileA, 2), 'fileA-2');
expectMap(ctx, 0, 0);
expectMap(ctx, 0, 2, 'a.js', 0, 0);
expectMap(ctx, 1, 0);
expectMap(ctx, 1, 2);
expectMap(ctx, 1, 4, 'a.js', 0, 2);
expectMap(ctx, 2, 0);
expectMap(ctx, 2, 2, 'a.js', 0, 4);
expect(nbSegmentsPerLine(ctx)).toEqual([1, 1, 1]);
});
it('should coalesce identical span', () => {
const span = createSourceSpan(fileA, 0);
ctx.print(span, 'fileA-0');
ctx.print(null, '...');
ctx.print(span, 'fileA-0');
ctx.print(createSourceSpan(fileB, 0), 'fileB-0');
expectMap(ctx, 0, 0, 'a.js', 0, 0);
expectMap(ctx, 0, 7, 'a.js', 0, 0);
expectMap(ctx, 0, 10, 'a.js', 0, 0);
expectMap(ctx, 0, 17, 'b.js', 0, 0);
expect(nbSegmentsPerLine(ctx)).toEqual([2]);
});
});
});
}
// All lines / columns indexes are 0-based
// Note: source-map line indexes are 1-based, column 0-based
function expectMap(
ctx: EmitterVisitorContext, genLine: number, genCol: number, source: string = null,
srcLine: number = null, srcCol: number = null) {
const sm = ctx.toSourceMapGenerator().toJSON();
const smc = new SourceMapConsumer(sm);
const genPosition = {line: genLine + 1, column: genCol};
const origPosition = smc.originalPositionFor(genPosition);
expect(origPosition.source).toEqual(source);
expect(origPosition.line).toEqual(srcLine === null ? null : srcLine + 1);
expect(origPosition.column).toEqual(srcCol);
}
// returns the number of segments per line
function nbSegmentsPerLine(ctx: EmitterVisitorContext) {
const sm = ctx.toSourceMapGenerator().toJSON();
const lines = sm.mappings.split(';');
return lines.map(l => {
const m = l.match(/,/g);
return m === null ? 1 : m.length + 1;
});
}
function createSourceSpan(file: ParseSourceFile, idx: number) {
const col = 2 * idx;
const start = new ParseLocation(file, col, 0, col);
const end = new ParseLocation(file, col + 2, 0, col + 2);
const sourceSpan = new ParseSourceSpan(start, end);
return {sourceSpan};
}
export function extractSourceMap(source: string): SourceMap {
let idx = source.lastIndexOf('\n//#');
if (idx == -1) return null;
const smComment = source.slice(idx).trim();
const smB64 = smComment.split('sourceMappingURL=data:application/json;base64,')[1];
return smB64 ? JSON.parse(decodeB64String(smB64)) : null;
}
function decodeB64String(s: string): string {
return b64.toByteArray(s).reduce((s: string, c: number) => s + String.fromCharCode(c), '');
}

View File

@ -0,0 +1,40 @@
/**
* @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 {escapeIdentifier} from '@angular/compiler/src/output/abstract_emitter';
export function main() {
describe('AbstractEmitter', () => {
describe('escapeIdentifier', () => {
it('should escape single quotes',
() => { expect(escapeIdentifier(`'`, false)).toEqual(`'\\''`); });
it('should escape backslash',
() => { expect(escapeIdentifier('\\', false)).toEqual(`'\\\\'`); });
it('should escape newlines',
() => { expect(escapeIdentifier('\n', false)).toEqual(`'\\n'`); });
it('should escape carriage returns',
() => { expect(escapeIdentifier('\r', false)).toEqual(`'\\r'`); });
it('should escape $', () => { expect(escapeIdentifier('$', true)).toEqual(`'\\$'`); });
it('should not escape $', () => { expect(escapeIdentifier('$', false)).toEqual(`'$'`); });
it('should add quotes for non-identifiers',
() => { expect(escapeIdentifier('==', false, false)).toEqual(`'=='`); });
it('does not escape class (but it probably should)',
() => { expect(escapeIdentifier('class', false, false)).toEqual('class'); });
});
});
}
export function stripSourceMap(source: string): string {
const smi = source.lastIndexOf('\n//#');
if (smi == -1) return source;
return source.slice(0, smi);
}

View File

@ -0,0 +1,66 @@
/**
* @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 {StaticSymbol} from '@angular/compiler/src/aot/static_symbol';
import {CompileIdentifierMetadata} from '@angular/compiler/src/compile_metadata';
import {JavaScriptEmitter} from '@angular/compiler/src/output/js_emitter';
import * as o from '@angular/compiler/src/output/output_ast';
import {ImportResolver} from '@angular/compiler/src/output/path_util';
import {SourceMap} from '@angular/compiler/src/output/source_map';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '@angular/compiler/src/parse_util';
import {extractSourceMap} from './abstract_emitter_node_only_spec';
const SourceMapConsumer = require('source-map').SourceMapConsumer;
const someModuleUrl = 'somePackage/somePath';
class SimpleJsImportGenerator implements ImportResolver {
fileNameToModuleName(importedUrlStr: string, moduleUrlStr: string): string {
return importedUrlStr;
}
getImportAs(symbol: StaticSymbol): StaticSymbol { return null; }
getTypeArity(symbol: StaticSymbol): number /*|null*/ { return null; }
}
export function main() {
describe('JavaScriptEmitter', () => {
let importResolver: ImportResolver;
let emitter: JavaScriptEmitter;
let someVar: o.ReadVarExpr;
beforeEach(() => {
importResolver = new SimpleJsImportGenerator();
emitter = new JavaScriptEmitter(importResolver);
});
function emitSourceMap(
stmt: o.Statement | o.Statement[], exportedVars: string[] = null): SourceMap {
const stmts = Array.isArray(stmt) ? stmt : [stmt];
const source = emitter.emitStatements(someModuleUrl, stmts, exportedVars || []);
return extractSourceMap(source);
}
describe('source maps', () => {
it('should emit an inline source map', () => {
const source = new ParseSourceFile(';;;var', 'in.js');
const startLocation = new ParseLocation(source, 0, 0, 3);
const endLocation = new ParseLocation(source, 7, 0, 6);
const sourceSpan = new ParseSourceSpan(startLocation, endLocation);
const someVar = o.variable('someVar', null, sourceSpan);
const sm = emitSourceMap(someVar.toStmt());
const smc = new SourceMapConsumer(sm);
expect(sm.sources).toEqual(['in.js']);
expect(sm.sourcesContent).toEqual([';;;var']);
expect(smc.originalPositionFor({line: 1, column: 0}))
.toEqual({line: 1, column: 3, source: 'in.js', name: null});
});
});
});
}

View File

@ -0,0 +1,304 @@
/**
* @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 {StaticSymbol} from '@angular/compiler/src/aot/static_symbol';
import {CompileIdentifierMetadata} from '@angular/compiler/src/compile_metadata';
import {JavaScriptEmitter} from '@angular/compiler/src/output/js_emitter';
import * as o from '@angular/compiler/src/output/output_ast';
import {ImportResolver} from '@angular/compiler/src/output/path_util';
import {stripSourceMap} from './abstract_emitter_spec';
const someModuleUrl = 'somePackage/somePath';
const anotherModuleUrl = 'somePackage/someOtherPath';
const sameModuleIdentifier: CompileIdentifierMetadata = {
reference: new StaticSymbol(someModuleUrl, 'someLocalId', [])
};
const externalModuleIdentifier: CompileIdentifierMetadata = {
reference: new StaticSymbol(anotherModuleUrl, 'someExternalId', [])
};
class SimpleJsImportGenerator implements ImportResolver {
fileNameToModuleName(importedUrlStr: string, moduleUrlStr: string): string {
return importedUrlStr;
}
getImportAs(symbol: StaticSymbol): StaticSymbol { return null; }
getTypeArity(symbol: StaticSymbol): number /*|null*/ { return null; }
}
export function main() {
// Note supported features of our OutputAstin JavaScript / ES5:
// - types
// - declaring fields
describe('JavaScriptEmitter', () => {
let importResolver: ImportResolver;
let emitter: JavaScriptEmitter;
let someVar: o.ReadVarExpr;
beforeEach(() => {
importResolver = new SimpleJsImportGenerator();
emitter = new JavaScriptEmitter(importResolver);
someVar = o.variable('someVar');
});
function emitStmt(stmt: o.Statement, exportedVars: string[] = null): string {
const source = emitter.emitStatements(someModuleUrl, [stmt], exportedVars || []);
return stripSourceMap(source);
}
it('should declare variables', () => {
expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt())).toEqual(`var someVar = 1;`);
expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt(), ['someVar'])).toEqual([
'var someVar = 1;',
`Object.defineProperty(exports, 'someVar', { get: function() { return someVar; }});`
].join('\n'));
});
it('should read and write variables', () => {
expect(emitStmt(someVar.toStmt())).toEqual(`someVar;`);
expect(emitStmt(someVar.set(o.literal(1)).toStmt())).toEqual(`someVar = 1;`);
expect(emitStmt(someVar.set(o.variable('someOtherVar').set(o.literal(1))).toStmt()))
.toEqual(`someVar = (someOtherVar = 1);`);
});
it('should read and write keys', () => {
expect(emitStmt(o.variable('someMap').key(o.variable('someKey')).toStmt()))
.toEqual(`someMap[someKey];`);
expect(emitStmt(o.variable('someMap').key(o.variable('someKey')).set(o.literal(1)).toStmt()))
.toEqual(`someMap[someKey] = 1;`);
});
it('should read and write properties', () => {
expect(emitStmt(o.variable('someObj').prop('someProp').toStmt()))
.toEqual(`someObj.someProp;`);
expect(emitStmt(o.variable('someObj').prop('someProp').set(o.literal(1)).toStmt()))
.toEqual(`someObj.someProp = 1;`);
});
it('should invoke functions and methods and constructors', () => {
expect(emitStmt(o.variable('someFn').callFn([o.literal(1)]).toStmt())).toEqual('someFn(1);');
expect(emitStmt(o.variable('someObj').callMethod('someMethod', [o.literal(1)]).toStmt()))
.toEqual('someObj.someMethod(1);');
expect(emitStmt(o.variable('SomeClass').instantiate([o.literal(1)]).toStmt()))
.toEqual('new SomeClass(1);');
});
it('should support builtin methods', () => {
expect(emitStmt(o.variable('arr1')
.callMethod(o.BuiltinMethod.ConcatArray, [o.variable('arr2')])
.toStmt()))
.toEqual('arr1.concat(arr2);');
expect(emitStmt(o.variable('observable')
.callMethod(o.BuiltinMethod.SubscribeObservable, [o.variable('listener')])
.toStmt()))
.toEqual('observable.subscribe(listener);');
expect(
emitStmt(
o.variable('fn').callMethod(o.BuiltinMethod.Bind, [o.variable('someObj')]).toStmt()))
.toEqual('fn.bind(someObj);');
});
it('should support literals', () => {
expect(emitStmt(o.literal(0).toStmt())).toEqual('0;');
expect(emitStmt(o.literal(true).toStmt())).toEqual('true;');
expect(emitStmt(o.literal('someStr').toStmt())).toEqual(`'someStr';`);
expect(emitStmt(o.literalArr([o.literal(1)]).toStmt())).toEqual(`[1];`);
expect(emitStmt(o.literalMap([['someKey', o.literal(1)]]).toStmt())).toEqual(`{someKey: 1};`);
});
it('should support blank literals', () => {
expect(emitStmt(o.literal(null).toStmt())).toEqual('null;');
expect(emitStmt(o.literal(undefined).toStmt())).toEqual('undefined;');
});
it('should support external identifiers', () => {
expect(emitStmt(o.importExpr(sameModuleIdentifier).toStmt())).toEqual('someLocalId;');
expect(emitStmt(o.importExpr(externalModuleIdentifier).toStmt())).toEqual([
`var import0 = re` +
`quire('somePackage/someOtherPath');`,
`import0.someExternalId;`
].join('\n'));
});
it('should support `importAs` for external identifiers', () => {
spyOn(importResolver, 'getImportAs')
.and.returnValue(new StaticSymbol('somePackage/importAsModule', 'importAsName', []));
expect(emitStmt(o.importExpr(externalModuleIdentifier).toStmt())).toEqual([
`var import0 = re` +
`quire('somePackage/importAsModule');`,
`import0.importAsName;`
].join('\n'));
});
it('should support operators', () => {
const lhs = o.variable('lhs');
const rhs = o.variable('rhs');
expect(emitStmt(o.not(someVar).toStmt())).toEqual('!someVar;');
expect(
emitStmt(someVar.conditional(o.variable('trueCase'), o.variable('falseCase')).toStmt()))
.toEqual('(someVar? trueCase: falseCase);');
expect(emitStmt(lhs.equals(rhs).toStmt())).toEqual('(lhs == rhs);');
expect(emitStmt(lhs.notEquals(rhs).toStmt())).toEqual('(lhs != rhs);');
expect(emitStmt(lhs.identical(rhs).toStmt())).toEqual('(lhs === rhs);');
expect(emitStmt(lhs.notIdentical(rhs).toStmt())).toEqual('(lhs !== rhs);');
expect(emitStmt(lhs.minus(rhs).toStmt())).toEqual('(lhs - rhs);');
expect(emitStmt(lhs.plus(rhs).toStmt())).toEqual('(lhs + rhs);');
expect(emitStmt(lhs.divide(rhs).toStmt())).toEqual('(lhs / rhs);');
expect(emitStmt(lhs.multiply(rhs).toStmt())).toEqual('(lhs * rhs);');
expect(emitStmt(lhs.modulo(rhs).toStmt())).toEqual('(lhs % rhs);');
expect(emitStmt(lhs.and(rhs).toStmt())).toEqual('(lhs && rhs);');
expect(emitStmt(lhs.or(rhs).toStmt())).toEqual('(lhs || rhs);');
expect(emitStmt(lhs.lower(rhs).toStmt())).toEqual('(lhs < rhs);');
expect(emitStmt(lhs.lowerEquals(rhs).toStmt())).toEqual('(lhs <= rhs);');
expect(emitStmt(lhs.bigger(rhs).toStmt())).toEqual('(lhs > rhs);');
expect(emitStmt(lhs.biggerEquals(rhs).toStmt())).toEqual('(lhs >= rhs);');
});
it('should support function expressions', () => {
expect(emitStmt(o.fn([], []).toStmt())).toEqual(['function() {', '};'].join('\n'));
expect(emitStmt(o.fn([], [new o.ReturnStatement(o.literal(1))]).toStmt())).toEqual([
'function() {', ' return 1;\n};'
].join('\n'));
expect(emitStmt(o.fn([new o.FnParam('param1')], []).toStmt())).toEqual([
'function(param1) {', '};'
].join('\n'));
});
it('should support function statements', () => {
expect(emitStmt(new o.DeclareFunctionStmt('someFn', [], [
]))).toEqual(['function someFn() {', '}'].join('\n'));
expect(emitStmt(new o.DeclareFunctionStmt('someFn', [], []), ['someFn'])).toEqual([
'function someFn() {', '}',
`Object.defineProperty(exports, 'someFn', { get: function() { return someFn; }});`
].join('\n'));
expect(emitStmt(new o.DeclareFunctionStmt('someFn', [], [
new o.ReturnStatement(o.literal(1))
]))).toEqual(['function someFn() {', ' return 1;', '}'].join('\n'));
expect(emitStmt(new o.DeclareFunctionStmt('someFn', [new o.FnParam('param1')], [
]))).toEqual(['function someFn(param1) {', '}'].join('\n'));
});
it('should support comments', () => {
expect(emitStmt(new o.CommentStmt('a\nb'))).toEqual(['// a', '// b'].join('\n'));
});
it('should support if stmt', () => {
const trueCase = o.variable('trueCase').callFn([]).toStmt();
const falseCase = o.variable('falseCase').callFn([]).toStmt();
expect(emitStmt(new o.IfStmt(o.variable('cond'), [trueCase]))).toEqual([
'if (cond) { trueCase(); }'
].join('\n'));
expect(emitStmt(new o.IfStmt(o.variable('cond'), [trueCase], [falseCase]))).toEqual([
'if (cond) {', ' trueCase();', '} else {', ' falseCase();', '}'
].join('\n'));
});
it('should support try/catch', () => {
const bodyStmt = o.variable('body').callFn([]).toStmt();
const catchStmt =
o.variable('catchFn').callFn([o.CATCH_ERROR_VAR, o.CATCH_STACK_VAR]).toStmt();
expect(emitStmt(new o.TryCatchStmt([bodyStmt], [catchStmt]))).toEqual([
'try {', ' body();', '} catch (error) {', ' var stack = error.stack;',
' catchFn(error,stack);', '}'
].join('\n'));
});
it('should support support throwing',
() => { expect(emitStmt(new o.ThrowStmt(someVar))).toEqual('throw someVar;'); });
describe('classes', () => {
let callSomeMethod: o.Statement;
beforeEach(() => { callSomeMethod = o.THIS_EXPR.callMethod('someMethod', []).toStmt(); });
it('should support declaring classes', () => {
expect(emitStmt(new o.ClassStmt('SomeClass', null, [], [], null, [
]))).toEqual(['function SomeClass() {', '}'].join('\n'));
expect(emitStmt(new o.ClassStmt('SomeClass', null, [], [], null, []), ['SomeClass']))
.toEqual([
'function SomeClass() {', '}',
`Object.defineProperty(exports, 'SomeClass', { get: function() { return SomeClass; }});`
].join('\n'));
expect(
emitStmt(new o.ClassStmt('SomeClass', o.variable('SomeSuperClass'), [], [], null, [])))
.toEqual([
'function SomeClass() {', '}',
'SomeClass.prototype = Object.create(SomeSuperClass.prototype);'
].join('\n'));
});
it('should support declaring constructors', () => {
const superCall = o.SUPER_EXPR.callFn([o.variable('someParam')]).toStmt();
expect(emitStmt(
new o.ClassStmt('SomeClass', null, [], [], new o.ClassMethod(null, [], []), [])))
.toEqual(['function SomeClass() {', '}'].join('\n'));
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [], [],
new o.ClassMethod(null, [new o.FnParam('someParam')], []), [])))
.toEqual(['function SomeClass(someParam) {', '}'].join('\n'));
expect(emitStmt(new o.ClassStmt(
'SomeClass', o.variable('SomeSuperClass'), [], [],
new o.ClassMethod(null, [], [superCall]), [])))
.toEqual([
'function SomeClass() {', ' var self = this;',
' SomeSuperClass.call(this, someParam);', '}',
'SomeClass.prototype = Object.create(SomeSuperClass.prototype);'
].join('\n'));
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [], [], new o.ClassMethod(null, [], [callSomeMethod]), [])))
.toEqual([
'function SomeClass() {', ' var self = this;', ' self.someMethod();', '}'
].join('\n'));
});
it('should support declaring getters', () => {
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [], [new o.ClassGetter('someGetter', [])], null, [])))
.toEqual([
'function SomeClass() {', '}',
`Object.defineProperty(SomeClass.prototype, 'someGetter', { get: function() {`, `}});`
].join('\n'));
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [], [new o.ClassGetter('someGetter', [callSomeMethod])], null,
[])))
.toEqual([
'function SomeClass() {', '}',
`Object.defineProperty(SomeClass.prototype, 'someGetter', { get: function() {`,
` var self = this;`, ` self.someMethod();`, `}});`
].join('\n'));
});
it('should support methods', () => {
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [], [], null, [new o.ClassMethod('someMethod', [], [])])))
.toEqual([
'function SomeClass() {', '}', 'SomeClass.prototype.someMethod = function() {', '};'
].join('\n'));
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [], [], null,
[new o.ClassMethod('someMethod', [new o.FnParam('someParam')], [])])))
.toEqual([
'function SomeClass() {', '}',
'SomeClass.prototype.someMethod = function(someParam) {', '};'
].join('\n'));
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [], [], null,
[new o.ClassMethod('someMethod', [], [callSomeMethod])])))
.toEqual([
'function SomeClass() {', '}', 'SomeClass.prototype.someMethod = function() {',
' var self = this;', ' self.someMethod();', '};'
].join('\n'));
});
});
});
}

View File

@ -0,0 +1,130 @@
/**
* @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 {SourceMapGenerator, toBase64String} from '@angular/compiler/src/output/source_map';
export function main() {
describe('source map generation', () => {
describe('generation', () => {
it('should generate a valid source map', () => {
const map = new SourceMapGenerator('out.js')
.addSource('a.js', null)
.addLine()
.addMapping(0, 'a.js', 0, 0)
.addMapping(4, 'a.js', 0, 6)
.addMapping(5, 'a.js', 0, 7)
.addMapping(8, 'a.js', 0, 22)
.addMapping(9, 'a.js', 0, 23)
.addMapping(10, 'a.js', 0, 24)
.addLine()
.addMapping(0, 'a.js', 1, 0)
.addMapping(4, 'a.js', 1, 6)
.addMapping(5, 'a.js', 1, 7)
.addMapping(8, 'a.js', 1, 10)
.addMapping(9, 'a.js', 1, 11)
.addMapping(10, 'a.js', 1, 12)
.addLine()
.addMapping(0, 'a.js', 3, 0)
.addMapping(2, 'a.js', 3, 2)
.addMapping(3, 'a.js', 3, 3)
.addMapping(10, 'a.js', 3, 10)
.addMapping(11, 'a.js', 3, 11)
.addMapping(21, 'a.js', 3, 11)
.addMapping(22, 'a.js', 3, 12)
.addLine()
.addMapping(4, 'a.js', 4, 4)
.addMapping(11, 'a.js', 4, 11)
.addMapping(12, 'a.js', 4, 12)
.addMapping(15, 'a.js', 4, 15)
.addMapping(16, 'a.js', 4, 16)
.addMapping(21, 'a.js', 4, 21)
.addMapping(22, 'a.js', 4, 22)
.addMapping(23, 'a.js', 4, 23)
.addLine()
.addMapping(0, 'a.js', 5, 0)
.addMapping(1, 'a.js', 5, 1)
.addMapping(2, 'a.js', 5, 2)
.addMapping(3, 'a.js', 5, 2);
// Generated with https://sokra.github.io/source-map-visualization using a TS source map
expect(map.toJSON().mappings)
.toEqual(
'AAAA,IAAM,CAAC,GAAe,CAAC,CAAC;AACxB,IAAM,CAAC,GAAG,CAAC,CAAC;AAEZ,EAAE,CAAC,OAAO,CAAC,UAAA,CAAC;IACR,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC,CAAC,CAAA');
});
it('should include the files and their contents', () => {
const map = new SourceMapGenerator('out.js')
.addSource('inline.ts', 'inline')
.addSource('inline.ts', 'inline') // make sur the sources are dedup
.addSource('url.ts', null)
.addLine()
.addMapping(0, 'inline.ts', 0, 0)
.toJSON();
expect(map.file).toEqual('out.js');
expect(map.sources).toEqual(['inline.ts', 'url.ts']);
expect(map.sourcesContent).toEqual(['inline', null]);
});
it('should not generate source maps when there is no mapping', () => {
const smg = new SourceMapGenerator('out.js').addSource('inline.ts', 'inline').addLine();
expect(smg.toJSON()).toEqual(null);
expect(smg.toJsComment()).toEqual('');
});
});
describe('encodeB64String', () => {
it('should return the b64 encoded value', () => {
[['', ''], ['a', 'YQ=='], ['Foo', 'Rm9v'], ['Foo1', 'Rm9vMQ=='], ['Foo12', 'Rm9vMTI='],
['Foo123', 'Rm9vMTIz'],
].forEach(([src, b64]) => expect(toBase64String(src)).toEqual(b64));
});
});
describe('errors', () => {
it('should throw when mappings are added out of order', () => {
expect(() => {
new SourceMapGenerator('out.js')
.addSource('in.js')
.addLine()
.addMapping(10, 'in.js', 0, 0)
.addMapping(0, 'in.js', 0, 0);
}).toThrowError('Mapping should be added in output order');
});
it('should throw when adding segments before any line is created', () => {
expect(() => {
new SourceMapGenerator('out.js').addSource('in.js').addMapping(0, 'in.js', 0, 0);
}).toThrowError('A line must be added before mappings can be added');
});
it('should throw when adding segments referencing unknown sources', () => {
expect(() => {
new SourceMapGenerator('out.js').addSource('in.js').addLine().addMapping(
0, 'in_.js', 0, 0);
}).toThrowError('Unknown source file "in_.js"');
});
it('should throw when adding segments without column', () => {
expect(() => {
new SourceMapGenerator('out.js').addSource('in.js').addLine().addMapping(null);
}).toThrowError('The column in the generated code must be provided');
});
it('should throw when adding segments with a source url but no position', () => {
expect(() => {
new SourceMapGenerator('out.js').addSource('in.js').addLine().addMapping(0, 'in.js');
}).toThrowError('The source location must be provided when a source url is provided');
expect(() => {
new SourceMapGenerator('out.js').addSource('in.js').addLine().addMapping(0, 'in.js', 0);
}).toThrowError('The source location must be provided when a source url is provided');
});
});
});
}

View File

@ -0,0 +1,71 @@
/**
* @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 {ParseLocation, ParseSourceFile} from '@angular/compiler';
import {StaticSymbol} from '@angular/compiler/src/aot/static_symbol';
import * as o from '@angular/compiler/src/output/output_ast';
import {ImportResolver} from '@angular/compiler/src/output/path_util';
import {SourceMap} from '@angular/compiler/src/output/source_map';
import {TypeScriptEmitter} from '@angular/compiler/src/output/ts_emitter';
import {ParseSourceSpan} from '@angular/compiler/src/parse_util';
import {extractSourceMap} from './abstract_emitter_node_only_spec';
const SourceMapConsumer = require('source-map').SourceMapConsumer;
const someModuleUrl = 'somePackage/somePath';
class SimpleJsImportGenerator implements ImportResolver {
fileNameToModuleName(importedUrlStr: string, moduleUrlStr: string): string {
return importedUrlStr;
}
getImportAs(symbol: StaticSymbol): StaticSymbol { return null; }
getTypeArity(symbol: StaticSymbol): number /*|null*/ { return null; }
}
export function main() {
// Not supported features of our OutputAst in TS:
// - real `const` like in Dart
// - final fields
describe('TypeScriptEmitter', () => {
let importResolver: ImportResolver;
let emitter: TypeScriptEmitter;
let someVar: o.ReadVarExpr;
beforeEach(() => {
importResolver = new SimpleJsImportGenerator();
emitter = new TypeScriptEmitter(importResolver);
someVar = o.variable('someVar');
});
function emitSourceMap(
stmt: o.Statement | o.Statement[], exportedVars: string[] = null): SourceMap {
const stmts = Array.isArray(stmt) ? stmt : [stmt];
const source = emitter.emitStatements(someModuleUrl, stmts, exportedVars || []);
return extractSourceMap(source);
}
describe('source maps', () => {
it('should emit an inline source map', () => {
const source = new ParseSourceFile(';;;var', 'in.js');
const startLocation = new ParseLocation(source, 0, 0, 3);
const endLocation = new ParseLocation(source, 7, 0, 6);
const sourceSpan = new ParseSourceSpan(startLocation, endLocation);
const someVar = o.variable('someVar', null, sourceSpan);
const sm = emitSourceMap(someVar.toStmt());
const smc = new SourceMapConsumer(sm);
expect(sm.sources).toEqual(['in.js']);
expect(sm.sourcesContent).toEqual([';;;var']);
expect(smc.originalPositionFor({line: 1, column: 0}))
.toEqual({line: 1, column: 3, source: 'in.js', name: null});
});
});
});
}

View File

@ -0,0 +1,463 @@
/**
* @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 {StaticSymbol} from '@angular/compiler/src/aot/static_symbol';
import {CompileIdentifierMetadata} from '@angular/compiler/src/compile_metadata';
import * as o from '@angular/compiler/src/output/output_ast';
import {ImportResolver} from '@angular/compiler/src/output/path_util';
import {TypeScriptEmitter} from '@angular/compiler/src/output/ts_emitter';
import {stripSourceMap} from './abstract_emitter_spec';
const someModuleUrl = 'somePackage/somePath';
const anotherModuleUrl = 'somePackage/someOtherPath';
const sameModuleIdentifier: CompileIdentifierMetadata = {
reference: new StaticSymbol(someModuleUrl, 'someLocalId', [])
};
const externalModuleIdentifier: CompileIdentifierMetadata = {
reference: new StaticSymbol(anotherModuleUrl, 'someExternalId', [])
};
class SimpleJsImportGenerator implements ImportResolver {
fileNameToModuleName(importedUrlStr: string, moduleUrlStr: string): string {
return importedUrlStr;
}
getImportAs(symbol: StaticSymbol): StaticSymbol { return null; }
getTypeArity(symbol: StaticSymbol): number /*|null*/ { return null; }
}
export function main() {
// Not supported features of our OutputAst in TS:
// - real `const` like in Dart
// - final fields
describe('TypeScriptEmitter', () => {
let importResolver: ImportResolver;
let emitter: TypeScriptEmitter;
let someVar: o.ReadVarExpr;
beforeEach(() => {
importResolver = new SimpleJsImportGenerator();
emitter = new TypeScriptEmitter(importResolver);
someVar = o.variable('someVar');
});
function emitStmt(stmt: o.Statement | o.Statement[], exportedVars: string[] = null): string {
const stmts = Array.isArray(stmt) ? stmt : [stmt];
const source = emitter.emitStatements(someModuleUrl, stmts, exportedVars || []);
return stripSourceMap(source);
}
it('should declare variables', () => {
expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt())).toEqual(`var someVar:any = 1;`);
expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt(null, [o.StmtModifier.Final])))
.toEqual(`const someVar:any = 1;`);
expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt(), ['someVar']))
.toEqual(`export var someVar:any = 1;`);
expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt(o.INT_TYPE)))
.toEqual(`var someVar:number = 1;`);
expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt(o.INFERRED_TYPE)))
.toEqual(`var someVar = 1;`);
});
describe('declare variables with ExternExpressions as values', () => {
it('should create no reexport if the identifier is in the same module', () => {
// identifier is in the same module -> no reexport
expect(emitStmt(someVar.set(o.importExpr(sameModuleIdentifier)).toDeclStmt(), ['someVar']))
.toEqual('export var someVar:any = someLocalId;');
});
it('should create no reexport if the variable is not exported', () => {
expect(emitStmt(someVar.set(o.importExpr(externalModuleIdentifier)).toDeclStmt())).toEqual([
`import * as import0 from 'somePackage/someOtherPath';`,
`var someVar:any = import0.someExternalId;`
].join('\n'));
});
it('should create no reexport if the variable is typed', () => {
expect(emitStmt(
someVar.set(o.importExpr(externalModuleIdentifier)).toDeclStmt(o.DYNAMIC_TYPE),
['someVar']))
.toEqual([
`import * as import0 from 'somePackage/someOtherPath';`,
`export var someVar:any = import0.someExternalId;`
].join('\n'));
});
it('should create no reexport if the identifier has members', () => {
const externalModuleIdentifierWithMembers: CompileIdentifierMetadata = {
reference: new StaticSymbol(anotherModuleUrl, 'someExternalId', ['a'])
};
expect(emitStmt(
someVar.set(o.importExpr(externalModuleIdentifierWithMembers)).toDeclStmt(),
['someVar']))
.toEqual([
`import * as import0 from 'somePackage/someOtherPath';`,
`export var someVar:any = import0.someExternalId.a;`
].join('\n'));
});
it('should create a reexport', () => {
expect(
emitStmt(someVar.set(o.importExpr(externalModuleIdentifier)).toDeclStmt(), ['someVar']))
.toEqual([
`export {someExternalId as someVar} from 'somePackage/someOtherPath';`, ``
].join('\n'));
});
it('should create multiple reexports from the same file', () => {
const someVar2 = o.variable('someVar2');
const externalModuleIdentifier2: CompileIdentifierMetadata = {
reference: new StaticSymbol(anotherModuleUrl, 'someExternalId2', [])
};
expect(emitStmt(
[
someVar.set(o.importExpr(externalModuleIdentifier)).toDeclStmt(),
someVar2.set(o.importExpr(externalModuleIdentifier2)).toDeclStmt()
],
['someVar', 'someVar2']))
.toEqual([
`export {someExternalId as someVar,someExternalId2 as someVar2} from 'somePackage/someOtherPath';`,
``
].join('\n'));
});
it('should use `importAs` for reexports', () => {
spyOn(importResolver, 'getImportAs')
.and.returnValue(new StaticSymbol('somePackage/importAsModule', 'importAsName', []));
expect(
emitStmt(someVar.set(o.importExpr(externalModuleIdentifier)).toDeclStmt(), ['someVar']))
.toEqual([
`export {importAsName as someVar} from 'somePackage/importAsModule';`, ``
].join('\n'));
});
});
it('should read and write variables', () => {
expect(emitStmt(someVar.toStmt())).toEqual(`someVar;`);
expect(emitStmt(someVar.set(o.literal(1)).toStmt())).toEqual(`someVar = 1;`);
expect(emitStmt(someVar.set(o.variable('someOtherVar').set(o.literal(1))).toStmt()))
.toEqual(`someVar = (someOtherVar = 1);`);
});
it('should read and write keys', () => {
expect(emitStmt(o.variable('someMap').key(o.variable('someKey')).toStmt()))
.toEqual(`someMap[someKey];`);
expect(emitStmt(o.variable('someMap').key(o.variable('someKey')).set(o.literal(1)).toStmt()))
.toEqual(`someMap[someKey] = 1;`);
});
it('should read and write properties', () => {
expect(emitStmt(o.variable('someObj').prop('someProp').toStmt()))
.toEqual(`someObj.someProp;`);
expect(emitStmt(o.variable('someObj').prop('someProp').set(o.literal(1)).toStmt()))
.toEqual(`someObj.someProp = 1;`);
});
it('should invoke functions and methods and constructors', () => {
expect(emitStmt(o.variable('someFn').callFn([o.literal(1)]).toStmt())).toEqual('someFn(1);');
expect(emitStmt(o.variable('someObj').callMethod('someMethod', [o.literal(1)]).toStmt()))
.toEqual('someObj.someMethod(1);');
expect(emitStmt(o.variable('SomeClass').instantiate([o.literal(1)]).toStmt()))
.toEqual('new SomeClass(1);');
});
it('should support builtin methods', () => {
expect(emitStmt(o.variable('arr1')
.callMethod(o.BuiltinMethod.ConcatArray, [o.variable('arr2')])
.toStmt()))
.toEqual('arr1.concat(arr2);');
expect(emitStmt(o.variable('observable')
.callMethod(o.BuiltinMethod.SubscribeObservable, [o.variable('listener')])
.toStmt()))
.toEqual('observable.subscribe(listener);');
expect(
emitStmt(
o.variable('fn').callMethod(o.BuiltinMethod.Bind, [o.variable('someObj')]).toStmt()))
.toEqual('fn.bind(someObj);');
});
it('should support literals', () => {
expect(emitStmt(o.literal(0).toStmt())).toEqual('0;');
expect(emitStmt(o.literal(true).toStmt())).toEqual('true;');
expect(emitStmt(o.literal('someStr').toStmt())).toEqual(`'someStr';`);
expect(emitStmt(o.literalArr([o.literal(1)]).toStmt())).toEqual(`[1];`);
expect(emitStmt(o.literalMap([['someKey', o.literal(1)]]).toStmt())).toEqual(`{someKey: 1};`);
});
it('should apply quotes to each entry within a map produced with literalMap when true', () => {
expect(
emitStmt(
o.literalMap([['a', o.literal('a')], ['*', o.literal('star')]], null, true).toStmt())
.replace(/\s+/gm, ''))
.toEqual(`{'a':'a','*':'star'};`);
});
it('should support blank literals', () => {
expect(emitStmt(o.literal(null).toStmt())).toEqual('(null as any);');
expect(emitStmt(o.literal(undefined).toStmt())).toEqual('(undefined as any);');
expect(emitStmt(o.variable('a', null).isBlank().toStmt())).toEqual('(a == null);');
});
it('should support external identifiers', () => {
expect(emitStmt(o.importExpr(sameModuleIdentifier).toStmt())).toEqual('someLocalId;');
expect(emitStmt(o.importExpr(externalModuleIdentifier).toStmt())).toEqual([
`import * as import0 from 'somePackage/someOtherPath';`, `import0.someExternalId;`
].join('\n'));
});
it('should support `importAs` for external identifiers', () => {
spyOn(importResolver, 'getImportAs')
.and.returnValue(new StaticSymbol('somePackage/importAsModule', 'importAsName', []));
expect(emitStmt(o.importExpr(externalModuleIdentifier).toStmt())).toEqual([
`import * as import0 from 'somePackage/importAsModule';`, `import0.importAsName;`
].join('\n'));
});
it('should support operators', () => {
const lhs = o.variable('lhs');
const rhs = o.variable('rhs');
expect(emitStmt(someVar.cast(o.INT_TYPE).toStmt())).toEqual('(<number>someVar);');
expect(emitStmt(o.not(someVar).toStmt())).toEqual('!someVar;');
expect(
emitStmt(someVar.conditional(o.variable('trueCase'), o.variable('falseCase')).toStmt()))
.toEqual('(someVar? trueCase: falseCase);');
expect(emitStmt(lhs.equals(rhs).toStmt())).toEqual('(lhs == rhs);');
expect(emitStmt(lhs.notEquals(rhs).toStmt())).toEqual('(lhs != rhs);');
expect(emitStmt(lhs.identical(rhs).toStmt())).toEqual('(lhs === rhs);');
expect(emitStmt(lhs.notIdentical(rhs).toStmt())).toEqual('(lhs !== rhs);');
expect(emitStmt(lhs.minus(rhs).toStmt())).toEqual('(lhs - rhs);');
expect(emitStmt(lhs.plus(rhs).toStmt())).toEqual('(lhs + rhs);');
expect(emitStmt(lhs.divide(rhs).toStmt())).toEqual('(lhs / rhs);');
expect(emitStmt(lhs.multiply(rhs).toStmt())).toEqual('(lhs * rhs);');
expect(emitStmt(lhs.modulo(rhs).toStmt())).toEqual('(lhs % rhs);');
expect(emitStmt(lhs.and(rhs).toStmt())).toEqual('(lhs && rhs);');
expect(emitStmt(lhs.or(rhs).toStmt())).toEqual('(lhs || rhs);');
expect(emitStmt(lhs.lower(rhs).toStmt())).toEqual('(lhs < rhs);');
expect(emitStmt(lhs.lowerEquals(rhs).toStmt())).toEqual('(lhs <= rhs);');
expect(emitStmt(lhs.bigger(rhs).toStmt())).toEqual('(lhs > rhs);');
expect(emitStmt(lhs.biggerEquals(rhs).toStmt())).toEqual('(lhs >= rhs);');
});
it('should support function expressions', () => {
expect(emitStmt(o.fn([], []).toStmt())).toEqual(['():void => {', '};'].join('\n'));
expect(emitStmt(o.fn([], [new o.ReturnStatement(o.literal(1))], o.INT_TYPE).toStmt()))
.toEqual(['():number => {', ' return 1;\n};'].join('\n'));
expect(emitStmt(o.fn([new o.FnParam('param1', o.INT_TYPE)], []).toStmt())).toEqual([
'(param1:number):void => {', '};'
].join('\n'));
});
it('should support function statements', () => {
expect(emitStmt(new o.DeclareFunctionStmt('someFn', [], [
]))).toEqual(['function someFn():void {', '}'].join('\n'));
expect(emitStmt(new o.DeclareFunctionStmt('someFn', [], []), ['someFn'])).toEqual([
'export function someFn():void {', '}'
].join('\n'));
expect(emitStmt(new o.DeclareFunctionStmt(
'someFn', [], [new o.ReturnStatement(o.literal(1))], o.INT_TYPE)))
.toEqual(['function someFn():number {', ' return 1;', '}'].join('\n'));
expect(emitStmt(new o.DeclareFunctionStmt('someFn', [new o.FnParam('param1', o.INT_TYPE)], [
]))).toEqual(['function someFn(param1:number):void {', '}'].join('\n'));
});
it('should support comments', () => {
expect(emitStmt(new o.CommentStmt('a\nb'))).toEqual(['// a', '// b'].join('\n'));
});
it('should support if stmt', () => {
const trueCase = o.variable('trueCase').callFn([]).toStmt();
const falseCase = o.variable('falseCase').callFn([]).toStmt();
expect(emitStmt(new o.IfStmt(o.variable('cond'), [trueCase]))).toEqual([
'if (cond) { trueCase(); }'
].join('\n'));
expect(emitStmt(new o.IfStmt(o.variable('cond'), [trueCase], [falseCase]))).toEqual([
'if (cond) {', ' trueCase();', '} else {', ' falseCase();', '}'
].join('\n'));
});
it('should support try/catch', () => {
const bodyStmt = o.variable('body').callFn([]).toStmt();
const catchStmt =
o.variable('catchFn').callFn([o.CATCH_ERROR_VAR, o.CATCH_STACK_VAR]).toStmt();
expect(emitStmt(new o.TryCatchStmt([bodyStmt], [catchStmt]))).toEqual([
'try {', ' body();', '} catch (error) {', ' const stack:any = error.stack;',
' catchFn(error,stack);', '}'
].join('\n'));
});
it('should support support throwing',
() => { expect(emitStmt(new o.ThrowStmt(someVar))).toEqual('throw someVar;'); });
describe('classes', () => {
let callSomeMethod: o.Statement;
beforeEach(() => { callSomeMethod = o.THIS_EXPR.callMethod('someMethod', []).toStmt(); });
it('should support declaring classes', () => {
expect(emitStmt(new o.ClassStmt('SomeClass', null, [], [], null, [
]))).toEqual(['class SomeClass {', '}'].join('\n'));
expect(emitStmt(new o.ClassStmt('SomeClass', null, [], [], null, []), ['SomeClass']))
.toEqual(['export class SomeClass {', '}'].join('\n'));
expect(emitStmt(new o.ClassStmt('SomeClass', o.variable('SomeSuperClass'), [], [], null, [
]))).toEqual(['class SomeClass extends SomeSuperClass {', '}'].join('\n'));
});
it('should support declaring constructors', () => {
const superCall = o.SUPER_EXPR.callFn([o.variable('someParam')]).toStmt();
expect(emitStmt(
new o.ClassStmt('SomeClass', null, [], [], new o.ClassMethod(null, [], []), [])))
.toEqual(['class SomeClass {', ' constructor() {', ' }', '}'].join('\n'));
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [], [],
new o.ClassMethod(null, [new o.FnParam('someParam', o.INT_TYPE)], []), [])))
.toEqual(
['class SomeClass {', ' constructor(someParam:number) {', ' }', '}'].join('\n'));
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [], [], new o.ClassMethod(null, [], [superCall]), [])))
.toEqual([
'class SomeClass {', ' constructor() {', ' super(someParam);', ' }', '}'
].join('\n'));
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [], [], new o.ClassMethod(null, [], [callSomeMethod]), [])))
.toEqual([
'class SomeClass {', ' constructor() {', ' this.someMethod();', ' }', '}'
].join('\n'));
});
it('should support declaring fields', () => {
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [new o.ClassField('someField')], [], null, [])))
.toEqual(['class SomeClass {', ' someField:any;', '}'].join('\n'));
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [new o.ClassField('someField', o.INT_TYPE)], [], null, [])))
.toEqual(['class SomeClass {', ' someField:number;', '}'].join('\n'));
expect(emitStmt(new o.ClassStmt(
'SomeClass', null,
[new o.ClassField('someField', o.INT_TYPE, [o.StmtModifier.Private])], [], null,
[])))
.toEqual(['class SomeClass {', ' /*private*/ someField:number;', '}'].join('\n'));
});
it('should support declaring getters', () => {
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [], [new o.ClassGetter('someGetter', [])], null, [])))
.toEqual(['class SomeClass {', ' get someGetter():any {', ' }', '}'].join('\n'));
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [], [new o.ClassGetter('someGetter', [], o.INT_TYPE)], null,
[])))
.toEqual(['class SomeClass {', ' get someGetter():number {', ' }', '}'].join('\n'));
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [], [new o.ClassGetter('someGetter', [callSomeMethod])], null,
[])))
.toEqual([
'class SomeClass {', ' get someGetter():any {', ' this.someMethod();', ' }', '}'
].join('\n'));
expect(
emitStmt(new o.ClassStmt(
'SomeClass', null, [],
[new o.ClassGetter('someGetter', [], null, [o.StmtModifier.Private])], null, [])))
.toEqual(
['class SomeClass {', ' private get someGetter():any {', ' }', '}'].join('\n'));
});
it('should support methods', () => {
expect(emitStmt(new o.ClassStmt('SomeClass', null, [], [], null, [
new o.ClassMethod('someMethod', [], [])
]))).toEqual(['class SomeClass {', ' someMethod():void {', ' }', '}'].join('\n'));
expect(emitStmt(new o.ClassStmt('SomeClass', null, [], [], null, [
new o.ClassMethod('someMethod', [], [], o.INT_TYPE)
]))).toEqual(['class SomeClass {', ' someMethod():number {', ' }', '}'].join('\n'));
expect(
emitStmt(new o.ClassStmt(
'SomeClass', null, [], [], null,
[new o.ClassMethod('someMethod', [new o.FnParam('someParam', o.INT_TYPE)], [])])))
.toEqual([
'class SomeClass {', ' someMethod(someParam:number):void {', ' }', '}'
].join('\n'));
expect(emitStmt(new o.ClassStmt(
'SomeClass', null, [], [], null,
[new o.ClassMethod('someMethod', [], [callSomeMethod])])))
.toEqual([
'class SomeClass {', ' someMethod():void {', ' this.someMethod();', ' }', '}'
].join('\n'));
});
});
it('should support builtin types', () => {
const writeVarExpr = o.variable('a').set(o.NULL_EXPR);
expect(emitStmt(writeVarExpr.toDeclStmt(o.DYNAMIC_TYPE)))
.toEqual('var a:any = (null as any);');
expect(emitStmt(writeVarExpr.toDeclStmt(o.BOOL_TYPE)))
.toEqual('var a:boolean = (null as any);');
expect(emitStmt(writeVarExpr.toDeclStmt(o.INT_TYPE)))
.toEqual('var a:number = (null as any);');
expect(emitStmt(writeVarExpr.toDeclStmt(o.NUMBER_TYPE)))
.toEqual('var a:number = (null as any);');
expect(emitStmt(writeVarExpr.toDeclStmt(o.STRING_TYPE)))
.toEqual('var a:string = (null as any);');
expect(emitStmt(writeVarExpr.toDeclStmt(o.FUNCTION_TYPE)))
.toEqual('var a:Function = (null as any);');
});
it('should support external types', () => {
const writeVarExpr = o.variable('a').set(o.NULL_EXPR);
expect(emitStmt(writeVarExpr.toDeclStmt(o.importType(sameModuleIdentifier))))
.toEqual('var a:someLocalId = (null as any);');
expect(emitStmt(writeVarExpr.toDeclStmt(o.importType(externalModuleIdentifier)))).toEqual([
`import * as import0 from 'somePackage/someOtherPath';`,
`var a:import0.someExternalId = (null as any);`
].join('\n'));
});
it('should support `importAs` for external types', () => {
spyOn(importResolver, 'getImportAs')
.and.returnValue(new StaticSymbol('somePackage/importAsModule', 'importAsName', []));
const writeVarExpr = o.variable('a').set(o.NULL_EXPR);
expect(emitStmt(writeVarExpr.toDeclStmt(o.importType(externalModuleIdentifier)))).toEqual([
`import * as import0 from 'somePackage/importAsModule';`,
`var a:import0.importAsName = (null as any);`
].join('\n'));
});
it('should support expression types', () => {
expect(
emitStmt(o.variable('a').set(o.NULL_EXPR).toDeclStmt(o.expressionType(o.variable('b')))))
.toEqual('var a:b = (null as any);');
});
it('should support expressions with type parameters', () => {
expect(emitStmt(o.variable('a')
.set(o.NULL_EXPR)
.toDeclStmt(o.importType(externalModuleIdentifier, [o.STRING_TYPE]))))
.toEqual([
`import * as import0 from 'somePackage/someOtherPath';`,
`var a:import0.someExternalId<string> = (null as any);`
].join('\n'));
});
it('should support combined types', () => {
const writeVarExpr = o.variable('a').set(o.NULL_EXPR);
expect(emitStmt(writeVarExpr.toDeclStmt(new o.ArrayType(null))))
.toEqual('var a:any[] = (null as any);');
expect(emitStmt(writeVarExpr.toDeclStmt(new o.ArrayType(o.INT_TYPE))))
.toEqual('var a:number[] = (null as any);');
expect(emitStmt(writeVarExpr.toDeclStmt(new o.MapType(null))))
.toEqual('var a:{[key: string]:any} = (null as any);');
expect(emitStmt(writeVarExpr.toDeclStmt(new o.MapType(o.INT_TYPE))))
.toEqual('var a:{[key: string]:number} = (null as any);');
});
});
}

View File

@ -0,0 +1,37 @@
/**
* @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 {Injector, Pipe} from '@angular/core';
import {beforeEach, describe, expect, inject, it} from '@angular/core/testing/testing_internal';
import {MockPipeResolver} from '../testing/index';
export function main() {
describe('MockPipeResolver', () => {
let pipeResolver: MockPipeResolver;
beforeEach(inject(
[Injector], (injector: Injector) => { pipeResolver = new MockPipeResolver(injector); }));
describe('Pipe overriding', () => {
it('should fallback to the default PipeResolver when templates are not overridden', () => {
const pipe = pipeResolver.resolve(SomePipe);
expect(pipe.name).toEqual('somePipe');
});
it('should allow overriding the @Pipe', () => {
pipeResolver.setPipe(SomePipe, new Pipe({name: 'someOtherName'}));
const pipe = pipeResolver.resolve(SomePipe);
expect(pipe.name).toEqual('someOtherName');
});
});
});
}
@Pipe({name: 'somePipe'})
class SomePipe {
}

View File

@ -0,0 +1,52 @@
/**
* @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 {PipeResolver} from '@angular/compiler/src/pipe_resolver';
import {ɵstringify as stringify} from '@angular/core';
import {Pipe} from '@angular/core/src/metadata';
@Pipe({name: 'somePipe', pure: true})
class SomePipe {
}
class SimpleClass {}
export function main() {
describe('PipeResolver', () => {
let resolver: PipeResolver;
beforeEach(() => { resolver = new PipeResolver(); });
it('should read out the metadata from the class', () => {
const moduleMetadata = resolver.resolve(SomePipe);
expect(moduleMetadata).toEqual(new Pipe({name: 'somePipe', pure: true}));
});
it('should throw when simple class has no pipe decorator', () => {
expect(() => resolver.resolve(SimpleClass))
.toThrowError(`No Pipe decorator found on ${stringify(SimpleClass)}`);
});
it('should support inheriting the metadata', function() {
@Pipe({name: 'p'})
class Parent {
}
class ChildNoDecorator extends Parent {}
@Pipe({name: 'c'})
class ChildWithDecorator extends Parent {
}
expect(resolver.resolve(ChildNoDecorator)).toEqual(new Pipe({name: 'p'}));
expect(resolver.resolve(ChildWithDecorator)).toEqual(new Pipe({name: 'c'}));
});
});
}

View File

@ -0,0 +1,114 @@
/**
* @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 {MockResourceLoader} from '@angular/compiler/testing/resource_loader_mock';
import {AsyncTestCompleter, beforeEach, describe, expect, inject, it} from '@angular/core/testing/testing_internal';
export function main() {
describe('MockResourceLoader', () => {
let resourceLoader: MockResourceLoader;
beforeEach(() => { resourceLoader = new MockResourceLoader(); });
function expectResponse(
request: Promise<string>, url: string, response: string, done: () => void = null) {
function onResponse(text: string): string {
if (response === null) {
throw `Unexpected response ${url} -> ${text}`;
} else {
expect(text).toEqual(response);
if (done != null) done();
}
return text;
}
function onError(error: string): string {
if (response !== null) {
throw `Unexpected error ${url}`;
} else {
expect(error).toEqual(`Failed to load ${url}`);
if (done != null) done();
}
return error;
}
request.then(onResponse, onError);
}
it('should return a response from the definitions',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const url = '/foo';
const response = 'bar';
resourceLoader.when(url, response);
expectResponse(resourceLoader.get(url), url, response, () => async.done());
resourceLoader.flush();
}));
it('should return an error from the definitions',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const url = '/foo';
const response: string = null;
resourceLoader.when(url, response);
expectResponse(resourceLoader.get(url), url, response, () => async.done());
resourceLoader.flush();
}));
it('should return a response from the expectations',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const url = '/foo';
const response = 'bar';
resourceLoader.expect(url, response);
expectResponse(resourceLoader.get(url), url, response, () => async.done());
resourceLoader.flush();
}));
it('should return an error from the expectations',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const url = '/foo';
const response: string = null;
resourceLoader.expect(url, response);
expectResponse(resourceLoader.get(url), url, response, () => async.done());
resourceLoader.flush();
}));
it('should not reuse expectations', () => {
const url = '/foo';
const response = 'bar';
resourceLoader.expect(url, response);
resourceLoader.get(url);
resourceLoader.get(url);
expect(() => { resourceLoader.flush(); }).toThrowError('Unexpected request /foo');
});
it('should return expectations before definitions',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const url = '/foo';
resourceLoader.when(url, 'when');
resourceLoader.expect(url, 'expect');
expectResponse(resourceLoader.get(url), url, 'expect');
expectResponse(resourceLoader.get(url), url, 'when', () => async.done());
resourceLoader.flush();
}));
it('should throw when there is no definitions or expectations', () => {
resourceLoader.get('/foo');
expect(() => { resourceLoader.flush(); }).toThrowError('Unexpected request /foo');
});
it('should throw when flush is called without any pending requests', () => {
expect(() => { resourceLoader.flush(); }).toThrowError('No pending requests to flush');
});
it('should throw on unsatisfied expectations', () => {
resourceLoader.expect('/foo', 'bar');
resourceLoader.when('/bar', 'foo');
resourceLoader.get('/bar');
expect(() => { resourceLoader.flush(); }).toThrowError('Unsatisfied requests: /foo');
});
});
}

View File

@ -0,0 +1,168 @@
/**
* @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 {DirectiveResolver, ResourceLoader} from '@angular/compiler';
import {Compiler, Component, Injector, NgModule, NgModuleFactory, ɵViewMetadata as ViewMetadata, ɵstringify as stringify} from '@angular/core';
import {TestBed, async, fakeAsync, inject, tick} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/matchers';
import {MockDirectiveResolver} from '../testing/index';
import {SpyResourceLoader} from './spies';
@Component({selector: 'child-cmp'})
class ChildComp {
}
@Component({selector: 'some-cmp', template: 'someComp'})
class SomeComp {
}
@Component({selector: 'some-cmp', templateUrl: './someTpl'})
class SomeCompWithUrlTemplate {
}
export function main() {
describe('RuntimeCompiler', () => {
describe('compilerComponentSync', () => {
describe('never resolving loader', () => {
class StubResourceLoader {
get(url: string) { return new Promise(() => {}); }
}
beforeEach(() => {
TestBed.configureCompiler(
{providers: [{provide: ResourceLoader, useClass: StubResourceLoader}]});
});
it('should throw when using a templateUrl that has not been compiled before', async(() => {
TestBed.configureTestingModule({declarations: [SomeCompWithUrlTemplate]});
TestBed.compileComponents().then(() => {
expect(() => TestBed.createComponent(SomeCompWithUrlTemplate))
.toThrowError(
`Can't compile synchronously as ${stringify(SomeCompWithUrlTemplate)} is still being loaded!`);
});
}));
it('should throw when using a templateUrl in a nested component that has not been compiled before',
() => {
TestBed.configureTestingModule({declarations: [SomeComp, ChildComp]});
TestBed.overrideComponent(ChildComp, {set: {templateUrl: '/someTpl.html'}});
TestBed.overrideComponent(SomeComp, {set: {template: '<child-cmp></child-cmp>'}});
TestBed.compileComponents().then(() => {
expect(() => TestBed.createComponent(SomeComp))
.toThrowError(
`Can't compile synchronously as ${stringify(ChildComp)} is still being loaded!`);
});
});
});
describe('resolving loader', () => {
class StubResourceLoader {
get(url: string) { return Promise.resolve('hello'); }
}
beforeEach(() => {
TestBed.configureCompiler(
{providers: [{provide: ResourceLoader, useClass: StubResourceLoader}]});
});
it('should allow to use templateUrl components that have been loaded before', async(() => {
TestBed.configureTestingModule({declarations: [SomeCompWithUrlTemplate]});
TestBed.compileComponents().then(() => {
const fixture = TestBed.createComponent(SomeCompWithUrlTemplate);
expect(fixture.nativeElement).toHaveText('hello');
});
}));
});
});
});
describe('RuntimeCompiler', () => {
let compiler: Compiler;
let resourceLoader: SpyResourceLoader;
let dirResolver: MockDirectiveResolver;
let injector: Injector;
beforeEach(() => {
TestBed.configureCompiler(
{providers: [{provide: ResourceLoader, useClass: SpyResourceLoader}]});
});
beforeEach(fakeAsync(inject(
[Compiler, ResourceLoader, DirectiveResolver, Injector],
(_compiler: Compiler, _resourceLoader: SpyResourceLoader,
_dirResolver: MockDirectiveResolver, _injector: Injector) => {
compiler = _compiler;
resourceLoader = _resourceLoader;
dirResolver = _dirResolver;
injector = _injector;
})));
describe('compileModuleAsync', () => {
it('should allow to use templateUrl components', fakeAsync(() => {
@NgModule({
declarations: [SomeCompWithUrlTemplate],
entryComponents: [SomeCompWithUrlTemplate]
})
class SomeModule {
}
resourceLoader.spy('get').and.callFake(() => Promise.resolve('hello'));
let ngModuleFactory: NgModuleFactory<any>;
compiler.compileModuleAsync(SomeModule).then((f) => ngModuleFactory = f);
tick();
expect(ngModuleFactory.moduleType).toBe(SomeModule);
}));
});
describe('compileModuleSync', () => {
it('should throw when using a templateUrl that has not been compiled before', () => {
@NgModule(
{declarations: [SomeCompWithUrlTemplate], entryComponents: [SomeCompWithUrlTemplate]})
class SomeModule {
}
resourceLoader.spy('get').and.callFake(() => Promise.resolve(''));
expect(() => compiler.compileModuleSync(SomeModule))
.toThrowError(
`Can't compile synchronously as ${stringify(SomeCompWithUrlTemplate)} is still being loaded!`);
});
it('should throw when using a templateUrl in a nested component that has not been compiled before',
() => {
@NgModule({declarations: [SomeComp, ChildComp], entryComponents: [SomeComp]})
class SomeModule {
}
resourceLoader.spy('get').and.callFake(() => Promise.resolve(''));
dirResolver.setView(SomeComp, new ViewMetadata({template: ''}));
dirResolver.setView(ChildComp, new ViewMetadata({templateUrl: '/someTpl.html'}));
expect(() => compiler.compileModuleSync(SomeModule))
.toThrowError(
`Can't compile synchronously as ${stringify(ChildComp)} is still being loaded!`);
});
it('should allow to use templateUrl components that have been loaded before',
fakeAsync(() => {
@NgModule({
declarations: [SomeCompWithUrlTemplate],
entryComponents: [SomeCompWithUrlTemplate]
})
class SomeModule {
}
resourceLoader.spy('get').and.callFake(() => Promise.resolve('hello'));
compiler.compileModuleAsync(SomeModule);
tick();
const ngModuleFactory = compiler.compileModuleSync(SomeModule);
expect(ngModuleFactory).toBeTruthy();
}));
});
});
}

View File

@ -0,0 +1,235 @@
/**
* @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 {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry';
import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SecurityContext} from '@angular/core';
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
import {browserDetection} from '@angular/platform-browser/testing/browser_util';
import {Element} from '../../src/ml_parser/ast';
import {HtmlParser} from '../../src/ml_parser/html_parser';
import {extractSchema} from './schema_extractor';
export function main() {
describe('DOMElementSchema', () => {
let registry: DomElementSchemaRegistry;
beforeEach(() => { registry = new DomElementSchemaRegistry(); });
it('should detect elements', () => {
expect(registry.hasElement('div', [])).toBeTruthy();
expect(registry.hasElement('b', [])).toBeTruthy();
expect(registry.hasElement('ng-container', [])).toBeTruthy();
expect(registry.hasElement('ng-content', [])).toBeTruthy();
expect(registry.hasElement('my-cmp', [])).toBeFalsy();
expect(registry.hasElement('abc', [])).toBeFalsy();
});
// https://github.com/angular/angular/issues/11219
it('should detect elements missing from chrome', () => {
expect(registry.hasElement('data', [])).toBeTruthy();
expect(registry.hasElement('menuitem', [])).toBeTruthy();
expect(registry.hasElement('summary', [])).toBeTruthy();
expect(registry.hasElement('time', [])).toBeTruthy();
});
it('should detect properties on regular elements', () => {
expect(registry.hasProperty('div', 'id', [])).toBeTruthy();
expect(registry.hasProperty('div', 'title', [])).toBeTruthy();
expect(registry.hasProperty('h1', 'align', [])).toBeTruthy();
expect(registry.hasProperty('h2', 'align', [])).toBeTruthy();
expect(registry.hasProperty('h3', 'align', [])).toBeTruthy();
expect(registry.hasProperty('h4', 'align', [])).toBeTruthy();
expect(registry.hasProperty('h5', 'align', [])).toBeTruthy();
expect(registry.hasProperty('h6', 'align', [])).toBeTruthy();
expect(registry.hasProperty('h7', 'align', [])).toBeFalsy();
expect(registry.hasProperty('textarea', 'disabled', [])).toBeTruthy();
expect(registry.hasProperty('input', 'disabled', [])).toBeTruthy();
expect(registry.hasProperty('div', 'unknown', [])).toBeFalsy();
});
// https://github.com/angular/angular/issues/11219
it('should detect properties on elements missing from Chrome', () => {
expect(registry.hasProperty('data', 'value', [])).toBeTruthy();
expect(registry.hasProperty('menuitem', 'type', [])).toBeTruthy();
expect(registry.hasProperty('menuitem', 'default', [])).toBeTruthy();
expect(registry.hasProperty('time', 'dateTime', [])).toBeTruthy();
});
it('should detect different kinds of types', () => {
// inheritance: video => media => [HTMLElement] => [Element]
expect(registry.hasProperty('video', 'className', [])).toBeTruthy(); // from [Element]
expect(registry.hasProperty('video', 'id', [])).toBeTruthy(); // string
expect(registry.hasProperty('video', 'scrollLeft', [])).toBeTruthy(); // number
expect(registry.hasProperty('video', 'height', [])).toBeTruthy(); // number
expect(registry.hasProperty('video', 'autoplay', [])).toBeTruthy(); // boolean
expect(registry.hasProperty('video', 'classList', [])).toBeTruthy(); // object
// from *; but events are not properties
expect(registry.hasProperty('video', 'click', [])).toBeFalsy();
});
it('should treat custom elements as an unknown element by default', () => {
expect(registry.hasProperty('custom-like', 'unknown', [])).toBe(false);
expect(registry.hasProperty('custom-like', 'className', [])).toBeTruthy();
expect(registry.hasProperty('custom-like', 'style', [])).toBeTruthy();
expect(registry.hasProperty('custom-like', 'id', [])).toBeTruthy();
});
it('should return true for custom-like elements if the CUSTOM_ELEMENTS_SCHEMA was used', () => {
expect(registry.hasProperty('custom-like', 'unknown', [CUSTOM_ELEMENTS_SCHEMA])).toBeTruthy();
expect(registry.hasElement('custom-like', [CUSTOM_ELEMENTS_SCHEMA])).toBeTruthy();
});
it('should return true for all elements if the NO_ERRORS_SCHEMA was used', () => {
expect(registry.hasProperty('custom-like', 'unknown', [NO_ERRORS_SCHEMA])).toBeTruthy();
expect(registry.hasProperty('a', 'unknown', [NO_ERRORS_SCHEMA])).toBeTruthy();
expect(registry.hasElement('custom-like', [NO_ERRORS_SCHEMA])).toBeTruthy();
expect(registry.hasElement('unknown', [NO_ERRORS_SCHEMA])).toBeTruthy();
});
it('should re-map property names that are specified in DOM facade',
() => { expect(registry.getMappedPropName('readonly')).toEqual('readOnly'); });
it('should not re-map property names that are not specified in DOM facade', () => {
expect(registry.getMappedPropName('title')).toEqual('title');
expect(registry.getMappedPropName('exotic-unknown')).toEqual('exotic-unknown');
});
it('should return an error message when asserting event properties', () => {
let report = registry.validateProperty('onClick');
expect(report.error).toBeTruthy();
expect(report.msg)
.toEqual(
`Binding to event property 'onClick' is disallowed for security reasons, please use (Click)=...
If 'onClick' is a directive input, make sure the directive is imported by the current module.`);
report = registry.validateProperty('onAnything');
expect(report.error).toBeTruthy();
expect(report.msg)
.toEqual(
`Binding to event property 'onAnything' is disallowed for security reasons, please use (Anything)=...
If 'onAnything' is a directive input, make sure the directive is imported by the current module.`);
});
it('should return an error message when asserting event attributes', () => {
let report = registry.validateAttribute('onClick');
expect(report.error).toBeTruthy();
expect(report.msg)
.toEqual(
`Binding to event attribute 'onClick' is disallowed for security reasons, please use (Click)=...`);
report = registry.validateAttribute('onAnything');
expect(report.error).toBeTruthy();
expect(report.msg)
.toEqual(
`Binding to event attribute 'onAnything' is disallowed for security reasons, please use (Anything)=...`);
});
it('should not return an error message when asserting non-event properties or attributes',
() => {
let report = registry.validateProperty('title');
expect(report.error).toBeFalsy();
expect(report.msg).not.toBeDefined();
report = registry.validateProperty('exotic-unknown');
expect(report.error).toBeFalsy();
expect(report.msg).not.toBeDefined();
});
it('should return security contexts for elements', () => {
expect(registry.securityContext('iframe', 'srcdoc', false)).toBe(SecurityContext.HTML);
expect(registry.securityContext('p', 'innerHTML', false)).toBe(SecurityContext.HTML);
expect(registry.securityContext('a', 'href', false)).toBe(SecurityContext.URL);
expect(registry.securityContext('a', 'style', false)).toBe(SecurityContext.STYLE);
expect(registry.securityContext('ins', 'cite', false)).toBe(SecurityContext.URL);
expect(registry.securityContext('base', 'href', false)).toBe(SecurityContext.RESOURCE_URL);
});
it('should detect properties on namespaced elements', () => {
const htmlAst = new HtmlParser().parse('<svg:style>', 'TestComp');
const nodeName = (<Element>htmlAst.rootNodes[0]).name;
expect(registry.hasProperty(nodeName, 'type', [])).toBeTruthy();
});
it('should check security contexts case insensitive', () => {
expect(registry.securityContext('p', 'iNnErHtMl', false)).toBe(SecurityContext.HTML);
expect(registry.securityContext('p', 'formaction', false)).toBe(SecurityContext.URL);
expect(registry.securityContext('p', 'formAction', false)).toBe(SecurityContext.URL);
});
it('should check security contexts for attributes', () => {
expect(registry.securityContext('p', 'innerHtml', true)).toBe(SecurityContext.HTML);
expect(registry.securityContext('p', 'formaction', true)).toBe(SecurityContext.URL);
});
describe('Angular custom elements', () => {
it('should support <ng-container>',
() => { expect(registry.hasProperty('ng-container', 'id', [])).toBeFalsy(); });
it('should support <ng-content>', () => {
expect(registry.hasProperty('ng-content', 'id', [])).toBeFalsy();
expect(registry.hasProperty('ng-content', 'select', [])).toBeFalsy();
});
});
if (browserDetection.isChromeDesktop) {
it('generate a new schema', () => {
let schema = '\n';
extractSchema().forEach((props, name) => { schema += `'${name}|${props.join(',')}',\n`; });
// Uncomment this line to see:
// the generated schema which can then be pasted to the DomElementSchemaRegistry
// console.log(schema);
});
}
describe('normalizeAnimationStyleProperty', () => {
it('should normalize the given CSS property to camelCase', () => {
expect(registry.normalizeAnimationStyleProperty('border-radius')).toBe('borderRadius');
expect(registry.normalizeAnimationStyleProperty('zIndex')).toBe('zIndex');
expect(registry.normalizeAnimationStyleProperty('-webkit-animation'))
.toBe('WebkitAnimation');
});
});
describe('normalizeAnimationStyleValue', () => {
it('should normalize the given dimensional CSS style value to contain a PX value when numeric',
() => {
expect(
registry.normalizeAnimationStyleValue('borderRadius', 'border-radius', 10)['value'])
.toBe('10px');
});
it('should not normalize any values that are of zero', () => {
expect(registry.normalizeAnimationStyleValue('opacity', 'opacity', 0)['value']).toBe('0');
expect(registry.normalizeAnimationStyleValue('width', 'width', 0)['value']).toBe('0');
});
it('should retain the given dimensional CSS style value\'s unit if it already exists', () => {
expect(
registry.normalizeAnimationStyleValue('borderRadius', 'border-radius', '10em')['value'])
.toBe('10em');
});
it('should trim the provided CSS style value', () => {
expect(registry.normalizeAnimationStyleValue('color', 'color', ' red ')['value'])
.toBe('red');
});
it('should stringify all non dimensional numeric style values', () => {
expect(registry.normalizeAnimationStyleValue('zIndex', 'zIndex', 10)['value']).toBe('10');
expect(registry.normalizeAnimationStyleValue('opacity', 'opacity', 0.5)['value'])
.toBe('0.5');
});
});
});
}

View File

@ -0,0 +1,271 @@
/**
* @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
*/
const SVG_PREFIX = ':svg:';
// Element | Node interfaces
// see https://developer.mozilla.org/en-US/docs/Web/API/Element
// see https://developer.mozilla.org/en-US/docs/Web/API/Node
const ELEMENT_IF = '[Element]';
// HTMLElement interface
// see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
const HTMLELEMENT_IF = '[HTMLElement]';
const HTMLELEMENT_TAGS =
'abbr,address,article,aside,b,bdi,bdo,cite,code,dd,dfn,dt,em,figcaption,figure,footer,header,i,kbd,main,mark,nav,noscript,rb,rp,rt,rtc,ruby,s,samp,section,small,strong,sub,sup,u,var,wbr';
const ALL_HTML_TAGS =
// https://www.w3.org/TR/html5/index.html
'a,abbr,address,area,article,aside,audio,b,base,bdi,bdo,blockquote,body,br,button,canvas,caption,cite,code,col,colgroup,data,datalist,dd,del,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hr,html,i,iframe,img,input,ins,kbd,keygen,label,legend,li,link,main,map,mark,meta,meter,nav,noscript,object,ol,optgroup,option,output,p,param,pre,progress,q,rb,rp,rt,rtc,ruby,s,samp,script,section,select,small,source,span,strong,style,sub,sup,table,tbody,td,template,textarea,tfoot,th,thead,time,title,tr,track,u,ul,var,video,wbr,' +
// https://html.spec.whatwg.org/
'details,summary,menu,menuitem';
// Elements missing from Chrome (HtmlUnknownElement), to be manually added
const MISSING_FROM_CHROME: {[el: string]: string[]} = {
'data^[HTMLElement]': ['value'],
// TODO(vicb): Figure out why Chrome and WhatWG do not agree on the props
// 'menu^[HTMLElement]': ['type', 'label'],
'menuitem^[HTMLElement]':
['type', 'label', 'icon', '!disabled', '!checked', 'radiogroup', '!default'],
'summary^[HTMLElement]': [],
'time^[HTMLElement]': ['dateTime'],
};
const _G: any = global;
const document: any = typeof _G['document'] == 'object' ? _G['document'] : null;
export function extractSchema(): Map<string, string[]> {
if (!document) return null;
const SVGGraphicsElement = _G['SVGGraphicsElement'];
if (!SVGGraphicsElement) return null;
const SVGAnimationElement = _G['SVGAnimationElement'];
const SVGGeometryElement = _G['SVGGeometryElement'];
const SVGComponentTransferFunctionElement = _G['SVGComponentTransferFunctionElement'];
const SVGGradientElement = _G['SVGGradientElement'];
const SVGTextContentElement = _G['SVGTextContentElement'];
const SVGTextPositioningElement = _G['SVGTextPositioningElement'];
const element = document.createElement('video');
const svgAnimation = document.createElementNS('http://www.w3.org/2000/svg', 'set');
const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const svgFeFuncA = document.createElementNS('http://www.w3.org/2000/svg', 'feFuncA');
const svgGradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
const svgText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
const descMap: Map<string, string[]> = new Map();
const visited: {[name: string]: boolean} = {};
// HTML top level
extractProperties(Node, element, visited, descMap, ELEMENT_IF, '');
extractProperties(Element, element, visited, descMap, ELEMENT_IF, '');
extractProperties(HTMLElement, element, visited, descMap, HTMLELEMENT_IF, ELEMENT_IF);
extractProperties(HTMLElement, element, visited, descMap, HTMLELEMENT_TAGS, HTMLELEMENT_IF);
extractProperties(HTMLMediaElement, element, visited, descMap, 'media', HTMLELEMENT_IF);
// SVG top level
extractProperties(SVGElement, svgText, visited, descMap, SVG_PREFIX, HTMLELEMENT_IF);
extractProperties(
SVGGraphicsElement, svgText, visited, descMap, SVG_PREFIX + 'graphics', SVG_PREFIX);
extractProperties(
SVGAnimationElement, svgAnimation, visited, descMap, SVG_PREFIX + 'animation', SVG_PREFIX);
extractProperties(
SVGGeometryElement, svgPath, visited, descMap, SVG_PREFIX + 'geometry', SVG_PREFIX);
extractProperties(
SVGComponentTransferFunctionElement, svgFeFuncA, visited, descMap,
SVG_PREFIX + 'componentTransferFunction', SVG_PREFIX);
extractProperties(
SVGGradientElement, svgGradient, visited, descMap, SVG_PREFIX + 'gradient', SVG_PREFIX);
extractProperties(
SVGTextContentElement, svgText, visited, descMap, SVG_PREFIX + 'textContent',
SVG_PREFIX + 'graphics');
extractProperties(
SVGTextPositioningElement, svgText, visited, descMap, SVG_PREFIX + 'textPositioning',
SVG_PREFIX + 'textContent');
// Get all element types
const types = Object.getOwnPropertyNames(window).filter(k => /^(HTML|SVG).*?Element$/.test(k));
types.sort();
types.forEach(type => { extractRecursiveProperties(visited, descMap, (window as any)[type]); });
// Add elements missed by Chrome auto-detection
Object.keys(MISSING_FROM_CHROME).forEach(elHierarchy => {
descMap.set(elHierarchy, MISSING_FROM_CHROME[elHierarchy]);
});
assertNoMissingTags(descMap);
return descMap;
}
function assertNoMissingTags(descMap: Map<string, string[]>): void {
const extractedTags: string[] = [];
Array.from(descMap.keys()).forEach((key: string) => {
extractedTags.push(...key.split('|')[0].split('^')[0].split(','));
});
const missingTags = ALL_HTML_TAGS.split(',').filter(tag => extractedTags.indexOf(tag) == -1);
if (missingTags.length) {
throw new Error(`DOM schema misses tags: ${missingTags.join(',')}`);
}
}
function extractRecursiveProperties(
visited: {[name: string]: boolean}, descMap: Map<string, string[]>, type: Function): string {
const name = extractName(type);
if (visited[name]) {
return name;
}
let superName: string;
switch (name) {
case ELEMENT_IF:
// ELEMENT_IF is the top most interface (Element | Node)
superName = '';
break;
case HTMLELEMENT_IF:
superName = ELEMENT_IF;
break;
default:
superName =
extractRecursiveProperties(visited, descMap, type.prototype.__proto__.constructor);
}
let instance: HTMLElement = null;
name.split(',').forEach(tagName => {
instance = type['name'].startsWith('SVG') ?
document.createElementNS('http://www.w3.org/2000/svg', tagName.replace(SVG_PREFIX, '')) :
document.createElement(tagName);
let htmlType: Function;
switch (tagName) {
case 'cite':
// <cite> interface is `HTMLQuoteElement`
htmlType = HTMLElement;
break;
default:
htmlType = type;
}
if (!(instance instanceof htmlType)) {
throw new Error(`Tag <${tagName}> is not an instance of ${htmlType['name']}`);
}
});
extractProperties(type, instance, visited, descMap, name, superName);
return name;
}
function extractProperties(
type: Function, instance: any, visited: {[name: string]: boolean},
descMap: Map<string, string[]>, name: string, superName: string) {
if (!type) return;
visited[name] = true;
const fullName = name + (superName ? '^' + superName : '');
const props: string[] = descMap.has(fullName) ? descMap.get(fullName) : [];
const prototype = type.prototype;
const keys = Object.getOwnPropertyNames(prototype);
keys.sort();
keys.forEach((name) => {
if (name.startsWith('on')) {
props.push('*' + name.substr(2));
} else {
const typeCh = _TYPE_MNEMONICS[typeof instance[name]];
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
const isSetter = descriptor && descriptor.set;
if (typeCh !== void 0 && !name.startsWith('webkit') && isSetter) {
props.push(typeCh + name);
}
}
});
// There is no point in using `Node.nodeValue`, filter it out
descMap.set(fullName, type === Node ? props.filter(p => p != '%nodeValue') : props);
}
function extractName(type: Function): string {
let name = type['name'];
switch (name) {
// see https://www.w3.org/TR/html5/index.html
// TODO(vicb): generate this map from all the element types
case 'Element':
return ELEMENT_IF;
case 'HTMLElement':
return HTMLELEMENT_IF;
case 'HTMLImageElement':
return 'img';
case 'HTMLAnchorElement':
return 'a';
case 'HTMLDListElement':
return 'dl';
case 'HTMLDirectoryElement':
return 'dir';
case 'HTMLHeadingElement':
return 'h1,h2,h3,h4,h5,h6';
case 'HTMLModElement':
return 'ins,del';
case 'HTMLOListElement':
return 'ol';
case 'HTMLParagraphElement':
return 'p';
case 'HTMLQuoteElement':
return 'q,blockquote,cite';
case 'HTMLTableCaptionElement':
return 'caption';
case 'HTMLTableCellElement':
return 'th,td';
case 'HTMLTableColElement':
return 'col,colgroup';
case 'HTMLTableRowElement':
return 'tr';
case 'HTMLTableSectionElement':
return 'tfoot,thead,tbody';
case 'HTMLUListElement':
return 'ul';
case 'SVGGraphicsElement':
return SVG_PREFIX + 'graphics';
case 'SVGMPathElement':
return SVG_PREFIX + 'mpath';
case 'SVGSVGElement':
return SVG_PREFIX + 'svg';
case 'SVGTSpanElement':
return SVG_PREFIX + 'tspan';
default:
const isSVG = name.startsWith('SVG');
if (name.startsWith('HTML') || isSVG) {
name = name.replace('HTML', '').replace('SVG', '').replace('Element', '');
if (isSVG && name.startsWith('FE')) {
name = 'fe' + name.substring(2);
} else if (name) {
name = name.charAt(0).toLowerCase() + name.substring(1);
}
return isSVG ? SVG_PREFIX + name : name.toLowerCase();
}
}
return null;
}
const _TYPE_MNEMONICS: {[type: string]: string} = {
'string': '',
'number': '#',
'boolean': '!',
'object': '%',
};

View File

@ -0,0 +1,460 @@
/**
* @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 {CssSelector, SelectorMatcher} from '@angular/compiler/src/selector';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {el} from '@angular/platform-browser/testing/browser_util';
export function main() {
describe('SelectorMatcher', () => {
let matcher: SelectorMatcher;
let selectableCollector: (selector: CssSelector, context: any) => void;
let s1: any[], s2: any[], s3: any[], s4: any[];
let matched: any[];
function reset() { matched = []; }
beforeEach(() => {
reset();
s1 = s2 = s3 = s4 = null;
selectableCollector =
(selector: CssSelector, context: any) => { matched.push(selector, context); };
matcher = new SelectorMatcher();
});
it('should select by element name case sensitive', () => {
matcher.addSelectables(s1 = CssSelector.parse('someTag'), 1);
expect(matcher.match(getSelectorFor({tag: 'SOMEOTHERTAG'}), selectableCollector))
.toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(getSelectorFor({tag: 'SOMETAG'}), selectableCollector)).toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(getSelectorFor({tag: 'someTag'}), selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0], 1]);
});
it('should select by class name case insensitive', () => {
matcher.addSelectables(s1 = CssSelector.parse('.someClass'), 1);
matcher.addSelectables(s2 = CssSelector.parse('.someClass.class2'), 2);
expect(matcher.match(getSelectorFor({classes: 'SOMEOTHERCLASS'}), selectableCollector))
.toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(getSelectorFor({classes: 'SOMECLASS'}), selectableCollector))
.toEqual(true);
expect(matched).toEqual([s1[0], 1]);
reset();
expect(matcher.match(getSelectorFor({classes: 'someClass class2'}), selectableCollector))
.toEqual(true);
expect(matched).toEqual([s1[0], 1, s2[0], 2]);
});
it('should not throw for class name "constructor"', () => {
expect(matcher.match(getSelectorFor({classes: 'constructor'}), selectableCollector))
.toEqual(false);
expect(matched).toEqual([]);
});
it('should select by attr name case sensitive independent of the value', () => {
matcher.addSelectables(s1 = CssSelector.parse('[someAttr]'), 1);
matcher.addSelectables(s2 = CssSelector.parse('[someAttr][someAttr2]'), 2);
expect(matcher.match(getSelectorFor({attrs: [['SOMEOTHERATTR', '']]}), selectableCollector))
.toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(getSelectorFor({attrs: [['SOMEATTR', '']]}), selectableCollector))
.toEqual(false);
expect(matched).toEqual([]);
expect(
matcher.match(getSelectorFor({attrs: [['SOMEATTR', 'someValue']]}), selectableCollector))
.toEqual(false);
expect(matched).toEqual([]);
expect(
matcher.match(
getSelectorFor({attrs: [['someAttr', ''], ['someAttr2', '']]}), selectableCollector))
.toEqual(true);
expect(matched).toEqual([s1[0], 1, s2[0], 2]);
reset();
expect(matcher.match(
getSelectorFor({attrs: [['someAttr', 'someValue'], ['someAttr2', '']]}),
selectableCollector))
.toEqual(true);
expect(matched).toEqual([s1[0], 1, s2[0], 2]);
reset();
expect(matcher.match(
getSelectorFor({attrs: [['someAttr2', ''], ['someAttr', 'someValue']]}),
selectableCollector))
.toEqual(true);
expect(matched).toEqual([s1[0], 1, s2[0], 2]);
reset();
expect(matcher.match(
getSelectorFor({attrs: [['someAttr2', 'someValue'], ['someAttr', '']]}),
selectableCollector))
.toEqual(true);
expect(matched).toEqual([s1[0], 1, s2[0], 2]);
});
it('should support "." in attribute names', () => {
matcher.addSelectables(s1 = CssSelector.parse('[foo.bar]'), 1);
expect(matcher.match(getSelectorFor({attrs: [['barfoo', '']]}), selectableCollector))
.toEqual(false);
expect(matched).toEqual([]);
reset();
expect(matcher.match(getSelectorFor({attrs: [['foo.bar', '']]}), selectableCollector))
.toEqual(true);
expect(matched).toEqual([s1[0], 1]);
});
it('should select by attr name only once if the value is from the DOM', () => {
matcher.addSelectables(s1 = CssSelector.parse('[some-decor]'), 1);
const elementSelector = new CssSelector();
const element = el('<div attr></div>');
const empty = getDOM().getAttribute(element, 'attr');
elementSelector.addAttribute('some-decor', empty);
matcher.match(elementSelector, selectableCollector);
expect(matched).toEqual([s1[0], 1]);
});
it('should select by attr name case sensitive and value case insensitive', () => {
matcher.addSelectables(s1 = CssSelector.parse('[someAttr=someValue]'), 1);
expect(matcher.match(
getSelectorFor({attrs: [['SOMEATTR', 'SOMEOTHERATTR']]}), selectableCollector))
.toEqual(false);
expect(matched).toEqual([]);
expect(
matcher.match(getSelectorFor({attrs: [['SOMEATTR', 'SOMEVALUE']]}), selectableCollector))
.toEqual(false);
expect(matched).toEqual([]);
expect(
matcher.match(getSelectorFor({attrs: [['someAttr', 'SOMEVALUE']]}), selectableCollector))
.toEqual(true);
expect(matched).toEqual([s1[0], 1]);
});
it('should select by element name, class name and attribute name with value', () => {
matcher.addSelectables(s1 = CssSelector.parse('someTag.someClass[someAttr=someValue]'), 1);
expect(
matcher.match(
getSelectorFor(
{tag: 'someOtherTag', classes: 'someOtherClass', attrs: [['someOtherAttr', '']]}),
selectableCollector))
.toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(
getSelectorFor(
{tag: 'someTag', classes: 'someOtherClass', attrs: [['someOtherAttr', '']]}),
selectableCollector))
.toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(
getSelectorFor(
{tag: 'someTag', classes: 'someClass', attrs: [['someOtherAttr', '']]}),
selectableCollector))
.toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(
getSelectorFor({tag: 'someTag', classes: 'someClass', attrs: [['someAttr', '']]}),
selectableCollector))
.toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(
getSelectorFor(
{tag: 'someTag', classes: 'someClass', attrs: [['someAttr', 'someValue']]}),
selectableCollector))
.toEqual(true);
expect(matched).toEqual([s1[0], 1]);
});
it('should select by many attributes and independent of the value', () => {
matcher.addSelectables(s1 = CssSelector.parse('input[type=text][control]'), 1);
const cssSelector = new CssSelector();
cssSelector.setElement('input');
cssSelector.addAttribute('type', 'text');
cssSelector.addAttribute('control', 'one');
expect(matcher.match(cssSelector, selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0], 1]);
});
it('should select independent of the order in the css selector', () => {
matcher.addSelectables(s1 = CssSelector.parse('[someAttr].someClass'), 1);
matcher.addSelectables(s2 = CssSelector.parse('.someClass[someAttr]'), 2);
matcher.addSelectables(s3 = CssSelector.parse('.class1.class2'), 3);
matcher.addSelectables(s4 = CssSelector.parse('.class2.class1'), 4);
expect(matcher.match(CssSelector.parse('[someAttr].someClass')[0], selectableCollector))
.toEqual(true);
expect(matched).toEqual([s1[0], 1, s2[0], 2]);
reset();
expect(matcher.match(CssSelector.parse('.someClass[someAttr]')[0], selectableCollector))
.toEqual(true);
expect(matched).toEqual([s1[0], 1, s2[0], 2]);
reset();
expect(matcher.match(CssSelector.parse('.class1.class2')[0], selectableCollector))
.toEqual(true);
expect(matched).toEqual([s3[0], 3, s4[0], 4]);
reset();
expect(matcher.match(CssSelector.parse('.class2.class1')[0], selectableCollector))
.toEqual(true);
expect(matched).toEqual([s4[0], 4, s3[0], 3]);
});
it('should not select with a matching :not selector', () => {
matcher.addSelectables(CssSelector.parse('p:not(.someClass)'), 1);
matcher.addSelectables(CssSelector.parse('p:not([someAttr])'), 2);
matcher.addSelectables(CssSelector.parse(':not(.someClass)'), 3);
matcher.addSelectables(CssSelector.parse(':not(p)'), 4);
matcher.addSelectables(CssSelector.parse(':not(p[someAttr])'), 5);
expect(matcher.match(
getSelectorFor({tag: 'p', classes: 'someClass', attrs: [['someAttr', '']]}),
selectableCollector))
.toEqual(false);
expect(matched).toEqual([]);
});
it('should select with a non matching :not selector', () => {
matcher.addSelectables(s1 = CssSelector.parse('p:not(.someClass)'), 1);
matcher.addSelectables(s2 = CssSelector.parse('p:not(.someOtherClass[someAttr])'), 2);
matcher.addSelectables(s3 = CssSelector.parse(':not(.someClass)'), 3);
matcher.addSelectables(s4 = CssSelector.parse(':not(.someOtherClass[someAttr])'), 4);
expect(
matcher.match(
getSelectorFor({tag: 'p', attrs: [['someOtherAttr', '']], classes: 'someOtherClass'}),
selectableCollector))
.toEqual(true);
expect(matched).toEqual([s1[0], 1, s2[0], 2, s3[0], 3, s4[0], 4]);
});
it('should match * with :not selector', () => {
matcher.addSelectables(CssSelector.parse(':not([a])'), 1);
expect(matcher.match(getSelectorFor({tag: 'div'}), () => {})).toEqual(true);
});
it('should match with multiple :not selectors', () => {
matcher.addSelectables(s1 = CssSelector.parse('div:not([a]):not([b])'), 1);
expect(matcher.match(getSelectorFor({tag: 'div', attrs: [['a', '']]}), selectableCollector))
.toBe(false);
expect(matcher.match(getSelectorFor({tag: 'div', attrs: [['b', '']]}), selectableCollector))
.toBe(false);
expect(matcher.match(getSelectorFor({tag: 'div', attrs: [['c', '']]}), selectableCollector))
.toBe(true);
});
it('should select with one match in a list', () => {
matcher.addSelectables(s1 = CssSelector.parse('input[type=text], textbox'), 1);
expect(matcher.match(getSelectorFor({tag: 'textbox'}), selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[1], 1]);
reset();
expect(matcher.match(
getSelectorFor({tag: 'input', attrs: [['type', 'text']]}), selectableCollector))
.toEqual(true);
expect(matched).toEqual([s1[0], 1]);
});
it('should not select twice with two matches in a list', () => {
matcher.addSelectables(s1 = CssSelector.parse('input, .someClass'), 1);
expect(
matcher.match(getSelectorFor({tag: 'input', classes: 'someclass'}), selectableCollector))
.toEqual(true);
expect(matched.length).toEqual(2);
expect(matched).toEqual([s1[0], 1]);
});
});
describe('CssSelector.parse', () => {
it('should detect element names', () => {
const cssSelector = CssSelector.parse('sometag')[0];
expect(cssSelector.element).toEqual('sometag');
expect(cssSelector.toString()).toEqual('sometag');
});
it('should detect class names', () => {
const cssSelector = CssSelector.parse('.someClass')[0];
expect(cssSelector.classNames).toEqual(['someclass']);
expect(cssSelector.toString()).toEqual('.someclass');
});
it('should detect attr names', () => {
const cssSelector = CssSelector.parse('[attrname]')[0];
expect(cssSelector.attrs).toEqual(['attrname', '']);
expect(cssSelector.toString()).toEqual('[attrname]');
});
it('should detect attr values', () => {
const cssSelector = CssSelector.parse('[attrname=attrvalue]')[0];
expect(cssSelector.attrs).toEqual(['attrname', 'attrvalue']);
expect(cssSelector.toString()).toEqual('[attrname=attrvalue]');
});
it('should detect attr values with double quotes', () => {
const cssSelector = CssSelector.parse('[attrname="attrvalue"]')[0];
expect(cssSelector.attrs).toEqual(['attrname', 'attrvalue']);
expect(cssSelector.toString()).toEqual('[attrname=attrvalue]');
});
it('should detect attr values with single quotes', () => {
const cssSelector = CssSelector.parse('[attrname=\'attrvalue\']')[0];
expect(cssSelector.attrs).toEqual(['attrname', 'attrvalue']);
expect(cssSelector.toString()).toEqual('[attrname=attrvalue]');
});
it('should detect multiple parts', () => {
const cssSelector = CssSelector.parse('sometag[attrname=attrvalue].someclass')[0];
expect(cssSelector.element).toEqual('sometag');
expect(cssSelector.attrs).toEqual(['attrname', 'attrvalue']);
expect(cssSelector.classNames).toEqual(['someclass']);
expect(cssSelector.toString()).toEqual('sometag.someclass[attrname=attrvalue]');
});
it('should detect multiple attributes', () => {
const cssSelector = CssSelector.parse('input[type=text][control]')[0];
expect(cssSelector.element).toEqual('input');
expect(cssSelector.attrs).toEqual(['type', 'text', 'control', '']);
expect(cssSelector.toString()).toEqual('input[type=text][control]');
});
it('should detect :not', () => {
const cssSelector = CssSelector.parse('sometag:not([attrname=attrvalue].someclass)')[0];
expect(cssSelector.element).toEqual('sometag');
expect(cssSelector.attrs.length).toEqual(0);
expect(cssSelector.classNames.length).toEqual(0);
const notSelector = cssSelector.notSelectors[0];
expect(notSelector.element).toEqual(null);
expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']);
expect(notSelector.classNames).toEqual(['someclass']);
expect(cssSelector.toString()).toEqual('sometag:not(.someclass[attrname=attrvalue])');
});
it('should detect :not without truthy', () => {
const cssSelector = CssSelector.parse(':not([attrname=attrvalue].someclass)')[0];
expect(cssSelector.element).toEqual('*');
const notSelector = cssSelector.notSelectors[0];
expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']);
expect(notSelector.classNames).toEqual(['someclass']);
expect(cssSelector.toString()).toEqual('*:not(.someclass[attrname=attrvalue])');
});
it('should throw when nested :not', () => {
expect(() => {
CssSelector.parse('sometag:not(:not([attrname=attrvalue].someclass))')[0];
}).toThrowError('Nesting :not is not allowed in a selector');
});
it('should throw when multiple selectors in :not', () => {
expect(() => {
CssSelector.parse('sometag:not(a,b)');
}).toThrowError('Multiple selectors in :not are not supported');
});
it('should detect lists of selectors', () => {
const cssSelectors = CssSelector.parse('.someclass,[attrname=attrvalue], sometag');
expect(cssSelectors.length).toEqual(3);
expect(cssSelectors[0].classNames).toEqual(['someclass']);
expect(cssSelectors[1].attrs).toEqual(['attrname', 'attrvalue']);
expect(cssSelectors[2].element).toEqual('sometag');
});
it('should detect lists of selectors with :not', () => {
const cssSelectors =
CssSelector.parse('input[type=text], :not(textarea), textbox:not(.special)');
expect(cssSelectors.length).toEqual(3);
expect(cssSelectors[0].element).toEqual('input');
expect(cssSelectors[0].attrs).toEqual(['type', 'text']);
expect(cssSelectors[1].element).toEqual('*');
expect(cssSelectors[1].notSelectors[0].element).toEqual('textarea');
expect(cssSelectors[2].element).toEqual('textbox');
expect(cssSelectors[2].notSelectors[0].classNames).toEqual(['special']);
});
});
describe('CssSelector.getMatchingElementTemplate', () => {
it('should create an element with a tagName, classes, and attributes with the correct casing',
() => {
const selector = CssSelector.parse('Blink.neon.hotpink[Sweet][Dismissable=false]')[0];
const template = selector.getMatchingElementTemplate();
expect(template).toEqual('<Blink class="neon hotpink" Sweet Dismissable="false"></Blink>');
});
it('should create an element without a tag name', () => {
const selector = CssSelector.parse('[fancy]')[0];
const template = selector.getMatchingElementTemplate();
expect(template).toEqual('<div fancy></div>');
});
it('should ignore :not selectors', () => {
const selector = CssSelector.parse('grape:not(.red)')[0];
const template = selector.getMatchingElementTemplate();
expect(template).toEqual('<grape></grape>');
});
it('should support void tags', () => {
const selector = CssSelector.parse('input[fancy]')[0];
const template = selector.getMatchingElementTemplate();
expect(template).toEqual('<input fancy/>');
});
});
}
function getSelectorFor(
{tag = '', attrs = [], classes = ''}: {tag?: string, attrs?: any[], classes?: string} = {}):
CssSelector {
const selector = new CssSelector();
selector.setElement(tag);
attrs.forEach(nameValue => { selector.addAttribute(nameValue[0], nameValue[1]); });
classes.trim().split(/\s+/g).forEach(cName => { selector.addClassName(cName); });
return selector;
}

View File

@ -0,0 +1,316 @@
/**
* @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 {CssRule, ShadowCss, processRules} from '@angular/compiler/src/shadow_css';
import {normalizeCSS} from '@angular/platform-browser/testing/browser_util';
export function main() {
describe('ShadowCss', function() {
function s(css: string, contentAttr: string, hostAttr: string = '') {
const shadowCss = new ShadowCss();
const shim = shadowCss.shimCssText(css, contentAttr, hostAttr);
const nlRegexp = /\n/g;
return normalizeCSS(shim.replace(nlRegexp, ''));
}
it('should handle empty string', () => { expect(s('', 'a')).toEqual(''); });
it('should add an attribute to every rule', () => {
const css = 'one {color: red;}two {color: red;}';
const expected = 'one[a] {color:red;}two[a] {color:red;}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle invalid css', () => {
const css = 'one {color: red;}garbage';
const expected = 'one[a] {color:red;}garbage';
expect(s(css, 'a')).toEqual(expected);
});
it('should add an attribute to every selector', () => {
const css = 'one, two {color: red;}';
const expected = 'one[a], two[a] {color:red;}';
expect(s(css, 'a')).toEqual(expected);
});
it('should support newlines in the selector and content ', () => {
const css = 'one, \ntwo {\ncolor: red;}';
const expected = 'one[a], two[a] {color:red;}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle media rules', () => {
const css = '@media screen and (max-width:800px, max-height:100%) {div {font-size:50px;}}';
const expected =
'@media screen and (max-width:800px, max-height:100%) {div[a] {font-size:50px;}}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle page rules', () => {
const css = '@page {div {font-size:50px;}}';
const expected = '@page {div[a] {font-size:50px;}}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle document rules', () => {
const css = '@document url(http://www.w3.org/) {div {font-size:50px;}}';
const expected = '@document url(http://www.w3.org/) {div[a] {font-size:50px;}}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle media rules with simple rules', () => {
const css = '@media screen and (max-width: 800px) {div {font-size: 50px;}} div {}';
const expected = '@media screen and (max-width:800px) {div[a] {font-size:50px;}} div[a] {}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle support rules', () => {
const css = '@supports (display: flex) {section {display: flex;}}';
const expected = '@supports (display:flex) {section[a] {display:flex;}}';
expect(s(css, 'a')).toEqual(expected);
});
// Check that the browser supports unprefixed CSS animation
it('should handle keyframes rules', () => {
const css = '@keyframes foo {0% {transform:translate(-50%) scaleX(0);}}';
expect(s(css, 'a')).toEqual(css);
});
it('should handle -webkit-keyframes rules', () => {
const css = '@-webkit-keyframes foo {0% {-webkit-transform:translate(-50%) scaleX(0);}}';
expect(s(css, 'a')).toEqual(css);
});
it('should handle complicated selectors', () => {
expect(s('one::before {}', 'a')).toEqual('one[a]::before {}');
expect(s('one two {}', 'a')).toEqual('one[a] two[a] {}');
expect(s('one > two {}', 'a')).toEqual('one[a] > two[a] {}');
expect(s('one + two {}', 'a')).toEqual('one[a] + two[a] {}');
expect(s('one ~ two {}', 'a')).toEqual('one[a] ~ two[a] {}');
const res = s('.one.two > three {}', 'a'); // IE swap classes
expect(res == '.one.two[a] > three[a] {}' || res == '.two.one[a] > three[a] {}')
.toEqual(true);
expect(s('one[attr="value"] {}', 'a')).toEqual('one[attr="value"][a] {}');
expect(s('one[attr=value] {}', 'a')).toEqual('one[attr="value"][a] {}');
expect(s('one[attr^="value"] {}', 'a')).toEqual('one[attr^="value"][a] {}');
expect(s('one[attr$="value"] {}', 'a')).toEqual('one[attr$="value"][a] {}');
expect(s('one[attr*="value"] {}', 'a')).toEqual('one[attr*="value"][a] {}');
expect(s('one[attr|="value"] {}', 'a')).toEqual('one[attr|="value"][a] {}');
expect(s('one[attr~="value"] {}', 'a')).toEqual('one[attr~="value"][a] {}');
expect(s('one[attr="va lue"] {}', 'a')).toEqual('one[attr="va lue"][a] {}');
expect(s('one[attr] {}', 'a')).toEqual('one[attr][a] {}');
expect(s('[is="one"] {}', 'a')).toEqual('[is="one"][a] {}');
});
describe((':host'), () => {
it('should handle no context',
() => { expect(s(':host {}', 'a', 'a-host')).toEqual('[a-host] {}'); });
it('should handle tag selector',
() => { expect(s(':host(ul) {}', 'a', 'a-host')).toEqual('ul[a-host] {}'); });
it('should handle class selector',
() => { expect(s(':host(.x) {}', 'a', 'a-host')).toEqual('.x[a-host] {}'); });
it('should handle attribute selector', () => {
expect(s(':host([a="b"]) {}', 'a', 'a-host')).toEqual('[a="b"][a-host] {}');
expect(s(':host([a=b]) {}', 'a', 'a-host')).toEqual('[a="b"][a-host] {}');
});
it('should handle multiple tag selectors', () => {
expect(s(':host(ul,li) {}', 'a', 'a-host')).toEqual('ul[a-host], li[a-host] {}');
expect(s(':host(ul,li) > .z {}', 'a', 'a-host'))
.toEqual('ul[a-host] > .z[a], li[a-host] > .z[a] {}');
});
it('should handle multiple class selectors', () => {
expect(s(':host(.x,.y) {}', 'a', 'a-host')).toEqual('.x[a-host], .y[a-host] {}');
expect(s(':host(.x,.y) > .z {}', 'a', 'a-host'))
.toEqual('.x[a-host] > .z[a], .y[a-host] > .z[a] {}');
});
it('should handle multiple attribute selectors', () => {
expect(s(':host([a="b"],[c=d]) {}', 'a', 'a-host'))
.toEqual('[a="b"][a-host], [c="d"][a-host] {}');
});
it('should handle pseudo selectors', () => {
expect(s(':host(:before) {}', 'a', 'a-host')).toEqual('[a-host]:before {}');
expect(s(':host:before {}', 'a', 'a-host')).toEqual('[a-host]:before {}');
expect(s(':host:nth-child(8n+1) {}', 'a', 'a-host')).toEqual('[a-host]:nth-child(8n+1) {}');
expect(s(':host:nth-of-type(8n+1) {}', 'a', 'a-host'))
.toEqual('[a-host]:nth-of-type(8n+1) {}');
expect(s(':host(.class):before {}', 'a', 'a-host')).toEqual('.class[a-host]:before {}');
expect(s(':host.class:before {}', 'a', 'a-host')).toEqual('.class[a-host]:before {}');
expect(s(':host(:not(p)):before {}', 'a', 'a-host')).toEqual('[a-host]:not(p):before {}');
});
});
describe((':host-context'), () => {
it('should handle tag selector', () => {
expect(s(':host-context(div) {}', 'a', 'a-host')).toEqual('div[a-host], div [a-host] {}');
expect(s(':host-context(ul) > .y {}', 'a', 'a-host'))
.toEqual('ul[a-host] > .y[a], ul [a-host] > .y[a] {}');
});
it('should handle class selector', () => {
expect(s(':host-context(.x) {}', 'a', 'a-host')).toEqual('.x[a-host], .x [a-host] {}');
expect(s(':host-context(.x) > .y {}', 'a', 'a-host'))
.toEqual('.x[a-host] > .y[a], .x [a-host] > .y[a] {}');
});
it('should handle attribute selector', () => {
expect(s(':host-context([a="b"]) {}', 'a', 'a-host'))
.toEqual('[a="b"][a-host], [a="b"] [a-host] {}');
expect(s(':host-context([a=b]) {}', 'a', 'a-host'))
.toEqual('[a=b][a-host], [a="b"] [a-host] {}');
});
});
it('should support polyfill-next-selector', () => {
let css = s('polyfill-next-selector {content: \'x > y\'} z {}', 'a');
expect(css).toEqual('x[a] > y[a]{}');
css = s('polyfill-next-selector {content: "x > y"} z {}', 'a');
expect(css).toEqual('x[a] > y[a]{}');
css = s(`polyfill-next-selector {content: 'button[priority="1"]'} z {}`, 'a');
expect(css).toEqual('button[priority="1"][a]{}');
});
it('should support polyfill-unscoped-rule', () => {
let css = s('polyfill-unscoped-rule {content: \'#menu > .bar\';color: blue;}', 'a');
expect(css).toContain('#menu > .bar {;color:blue;}');
css = s('polyfill-unscoped-rule {content: "#menu > .bar";color: blue;}', 'a');
expect(css).toContain('#menu > .bar {;color:blue;}');
css = s(`polyfill-unscoped-rule {content: 'button[priority="1"]'}`, 'a');
expect(css).toContain('button[priority="1"] {}');
});
it('should support multiple instances polyfill-unscoped-rule', () => {
const css =
s('polyfill-unscoped-rule {content: \'foo\';color: blue;}' +
'polyfill-unscoped-rule {content: \'bar\';color: blue;}',
'a');
expect(css).toContain('foo {;color:blue;}');
expect(css).toContain('bar {;color:blue;}');
});
it('should support polyfill-rule', () => {
let css = s('polyfill-rule {content: \':host.foo .bar\';color: blue;}', 'a', 'a-host');
expect(css).toEqual('.foo[a-host] .bar[a] {;color:blue;}');
css = s('polyfill-rule {content: ":host.foo .bar";color:blue;}', 'a', 'a-host');
expect(css).toEqual('.foo[a-host] .bar[a] {;color:blue;}');
css = s(`polyfill-rule {content: 'button[priority="1"]'}`, 'a', 'a-host');
expect(css).toEqual('button[priority="1"][a] {}');
});
it('should handle ::shadow', () => {
const css = s('x::shadow > y {}', 'a');
expect(css).toEqual('x[a] > y[a] {}');
});
it('should handle /deep/', () => {
const css = s('x /deep/ y {}', 'a');
expect(css).toEqual('x[a] y {}');
});
it('should handle >>>', () => {
const css = s('x >>> y {}', 'a');
expect(css).toEqual('x[a] y {}');
});
it('should pass through @import directives', () => {
const styleStr = '@import url("https://fonts.googleapis.com/css?family=Roboto");';
const css = s(styleStr, 'a');
expect(css).toEqual(styleStr);
});
it('should shim rules after @import', () => {
const styleStr = '@import url("a"); div {}';
const css = s(styleStr, 'a');
expect(css).toEqual('@import url("a"); div[a] {}');
});
it('should leave calc() unchanged', () => {
const styleStr = 'div {height:calc(100% - 55px);}';
const css = s(styleStr, 'a');
expect(css).toEqual('div[a] {height:calc(100% - 55px);}');
});
it('should strip comments', () => { expect(s('/* x */b {c}', 'a')).toEqual('b[a] {c}'); });
it('should ignore special characters in comments',
() => { expect(s('/* {;, */b {c}', 'a')).toEqual('b[a] {c}'); });
it('should support multiline comments',
() => { expect(s('/* \n */b {c}', 'a')).toEqual('b[a] {c}'); });
it('should keep sourceMappingURL comments', () => {
expect(s('b {c}/*# sourceMappingURL=data:x */', 'a'))
.toEqual('b[a] {c}/*# sourceMappingURL=data:x */');
expect(s('b {c}/* #sourceMappingURL=data:x */', 'a'))
.toEqual('b[a] {c}/* #sourceMappingURL=data:x */');
});
});
describe('processRules', () => {
describe('parse rules', () => {
function captureRules(input: string): CssRule[] {
const result: CssRule[] = [];
processRules(input, (cssRule) => {
result.push(cssRule);
return cssRule;
});
return result;
}
it('should work with empty css', () => { expect(captureRules('')).toEqual([]); });
it('should capture a rule without body',
() => { expect(captureRules('a;')).toEqual([new CssRule('a', '')]); });
it('should capture css rules with body',
() => { expect(captureRules('a {b}')).toEqual([new CssRule('a', 'b')]); });
it('should capture css rules with nested rules', () => {
expect(captureRules('a {b {c}} d {e}')).toEqual([
new CssRule('a', 'b {c}'), new CssRule('d', 'e')
]);
});
it('should capture multiple rules where some have no body', () => {
expect(captureRules('@import a ; b {c}')).toEqual([
new CssRule('@import a', ''), new CssRule('b', 'c')
]);
});
});
describe('modify rules', () => {
it('should allow to change the selector while preserving whitespaces', () => {
expect(processRules(
'@import a; b {c {d}} e {f}',
(cssRule: CssRule) => new CssRule(cssRule.selector + '2', cssRule.content)))
.toEqual('@import a2; b2 {c {d}} e2 {f}');
});
it('should allow to change the content', () => {
expect(processRules(
'a {b}',
(cssRule: CssRule) => new CssRule(cssRule.selector, cssRule.content + '2')))
.toEqual('a {b2}');
});
});
});
}

View File

@ -0,0 +1,15 @@
/**
* @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 {ResourceLoader} from '@angular/compiler/src/resource_loader';
import {SpyObject} from '@angular/core/testing/testing_internal';
export class SpyResourceLoader extends SpyObject {
constructor() { super(ResourceLoader); }
}

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 {extractStyleUrls, isStyleUrlResolvable} from '@angular/compiler/src/style_url_resolver';
import {UrlResolver} from '@angular/compiler/src/url_resolver';
export function main() {
describe('extractStyleUrls', () => {
let urlResolver: UrlResolver;
beforeEach(() => { urlResolver = new UrlResolver(); });
it('should not resolve "url()" urls', () => {
const css = `
.foo {
background-image: url("double.jpg");
background-image: url('simple.jpg');
background-image: url(noquote.jpg);
}`;
const resolvedCss = extractStyleUrls(urlResolver, 'http://ng.io', css).style;
expect(resolvedCss).toEqual(css);
});
it('should extract "@import" urls', () => {
const css = `
@import '1.css';
@import "2.css";
`;
const styleWithImports = extractStyleUrls(urlResolver, 'http://ng.io', css);
expect(styleWithImports.style.trim()).toEqual('');
expect(styleWithImports.styleUrls).toEqual(['http://ng.io/1.css', 'http://ng.io/2.css']);
});
it('should ignore "@import" in comments', () => {
const css = `
@import '1.css';
/*@import '2.css';*/
`;
const styleWithImports = extractStyleUrls(urlResolver, 'http://ng.io', css);
expect(styleWithImports.style.trim()).toEqual('');
expect(styleWithImports.styleUrls).toContain('http://ng.io/1.css');
expect(styleWithImports.styleUrls).not.toContain('http://ng.io/2.css');
});
it('should extract "@import url()" urls', () => {
const css = `
@import url('3.css');
@import url("4.css");
@import url(5.css);
`;
const styleWithImports = extractStyleUrls(urlResolver, 'http://ng.io', css);
expect(styleWithImports.style.trim()).toEqual('');
expect(styleWithImports.styleUrls).toEqual([
'http://ng.io/3.css', 'http://ng.io/4.css', 'http://ng.io/5.css'
]);
});
it('should extract "@import urls and keep rules in the same line', () => {
const css = `@import url('some.css');div {color: red};`;
const styleWithImports = extractStyleUrls(urlResolver, 'http://ng.io', css);
expect(styleWithImports.style.trim()).toEqual('div {color: red};');
expect(styleWithImports.styleUrls).toEqual(['http://ng.io/some.css']);
});
it('should extract media query in "@import"', () => {
const css = `
@import 'print1.css' print;
@import url(print2.css) print;
`;
const styleWithImports = extractStyleUrls(urlResolver, 'http://ng.io', css);
expect(styleWithImports.style.trim()).toEqual('');
expect(styleWithImports.styleUrls).toEqual([
'http://ng.io/print1.css', 'http://ng.io/print2.css'
]);
});
it('should leave absolute non-package @import urls intact', () => {
const css = `@import url('http://server.com/some.css');`;
const styleWithImports = extractStyleUrls(urlResolver, 'http://ng.io', css);
expect(styleWithImports.style.trim()).toEqual(`@import url('http://server.com/some.css');`);
expect(styleWithImports.styleUrls).toEqual([]);
});
it('should resolve package @import urls', () => {
const css = `@import url('package:a/b/some.css');`;
const styleWithImports = extractStyleUrls(new FakeUrlResolver(), 'http://ng.io', css);
expect(styleWithImports.style.trim()).toEqual(``);
expect(styleWithImports.styleUrls).toEqual(['fake_resolved_url']);
});
});
describe('isStyleUrlResolvable', () => {
it('should resolve relative urls',
() => { expect(isStyleUrlResolvable('someUrl.css')).toBe(true); });
it('should resolve package: urls',
() => { expect(isStyleUrlResolvable('package:someUrl.css')).toBe(true); });
it('should not resolve empty urls', () => {
expect(isStyleUrlResolvable(null)).toBe(false);
expect(isStyleUrlResolvable('')).toBe(false);
});
it('should not resolve urls with other schema',
() => { expect(isStyleUrlResolvable('http://otherurl')).toBe(false); });
it('should not resolve urls with absolute paths', () => {
expect(isStyleUrlResolvable('/otherurl')).toBe(false);
expect(isStyleUrlResolvable('//otherurl')).toBe(false);
});
});
}
class FakeUrlResolver extends UrlResolver {
constructor() { super(); }
resolve(baseUrl: string, url: string): string { return 'fake_resolved_url'; }
}

View File

@ -0,0 +1,59 @@
/**
* @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 {SecurityContext} from '@angular/core';
import {inject} from '@angular/core/testing';
import {ElementSchemaRegistry} from '../../src/schema/element_schema_registry';
import {calcPossibleSecurityContexts} from '../../src/template_parser/binding_parser';
export function main() {
describe('BindingParser', () => {
let registry: ElementSchemaRegistry;
beforeEach(inject(
[ElementSchemaRegistry], (_registry: ElementSchemaRegistry) => { registry = _registry; }));
describe('possibleSecurityContexts', () => {
function hrefSecurityContexts(selector: string) {
return calcPossibleSecurityContexts(registry, selector, 'href', false);
}
it('should return a single security context if the selector as an element name',
() => { expect(hrefSecurityContexts('a')).toEqual([SecurityContext.URL]); });
it('should return the possible security contexts if the selector has no element name', () => {
expect(hrefSecurityContexts('[myDir]')).toEqual([
SecurityContext.NONE, SecurityContext.URL, SecurityContext.RESOURCE_URL
]);
});
it('should exclude possible elements via :not', () => {
expect(hrefSecurityContexts('[myDir]:not(link):not(base)')).toEqual([
SecurityContext.NONE, SecurityContext.URL
]);
});
it('should not exclude possible narrowed elements via :not', () => {
expect(hrefSecurityContexts('[myDir]:not(link.someClass):not(base.someClass)')).toEqual([
SecurityContext.NONE, SecurityContext.URL, SecurityContext.RESOURCE_URL
]);
});
it('should return SecurityContext.NONE if there are no possible elements',
() => { expect(hrefSecurityContexts('img:not(img)')).toEqual([SecurityContext.NONE]); });
it('should return the union of the possible security contexts if multiple selectors are specified',
() => {
expect(calcPossibleSecurityContexts(registry, 'a,link', 'href', false)).toEqual([
SecurityContext.URL, SecurityContext.RESOURCE_URL
]);
});
});
});
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
/**
* @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 {beforeEach, describe, expect, inject, it} from '../../../core/testing/testing_internal';
import {Element} from '../../src/ml_parser/ast';
import {HtmlParser} from '../../src/ml_parser/html_parser';
import {PreparsedElement, PreparsedElementType, preparseElement} from '../../src/template_parser/template_preparser';
export function main() {
describe('preparseElement', () => {
let htmlParser: HtmlParser;
beforeEach(inject([HtmlParser], (_htmlParser: HtmlParser) => { htmlParser = _htmlParser; }));
function preparse(html: string): PreparsedElement {
return preparseElement(htmlParser.parse(html, 'TestComp').rootNodes[0] as Element);
}
it('should detect script elements', inject([HtmlParser], (htmlParser: HtmlParser) => {
expect(preparse('<script>').type).toBe(PreparsedElementType.SCRIPT);
}));
it('should detect style elements', inject([HtmlParser], (htmlParser: HtmlParser) => {
expect(preparse('<style>').type).toBe(PreparsedElementType.STYLE);
}));
it('should detect stylesheet elements', inject([HtmlParser], (htmlParser: HtmlParser) => {
expect(preparse('<link rel="stylesheet">').type).toBe(PreparsedElementType.STYLESHEET);
expect(preparse('<link rel="stylesheet" href="someUrl">').hrefAttr).toEqual('someUrl');
expect(preparse('<link rel="someRel">').type).toBe(PreparsedElementType.OTHER);
}));
it('should detect ng-content elements', inject([HtmlParser], (htmlParser: HtmlParser) => {
expect(preparse('<ng-content>').type).toBe(PreparsedElementType.NG_CONTENT);
}));
it('should normalize ng-content.select attribute',
inject([HtmlParser], (htmlParser: HtmlParser) => {
expect(preparse('<ng-content>').selectAttr).toEqual('*');
expect(preparse('<ng-content select>').selectAttr).toEqual('*');
expect(preparse('<ng-content select="*">').selectAttr).toEqual('*');
}));
it('should extract ngProjectAs value', () => {
expect(preparse('<p ngProjectAs="el[attr].class"></p>').projectAs).toEqual('el[attr].class');
});
});
}

View File

@ -0,0 +1,117 @@
/**
* @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 {UrlResolver, createOfflineCompileUrlResolver} from '@angular/compiler/src/url_resolver';
import {beforeEach, describe, expect, inject, it} from '@angular/core/testing/testing_internal';
export function main() {
describe('UrlResolver', () => {
let resolver = new UrlResolver();
describe('absolute base url', () => {
it('should add a relative path to the base url', () => {
expect(resolver.resolve('http://www.foo.com', 'bar')).toEqual('http://www.foo.com/bar');
expect(resolver.resolve('http://www.foo.com/', 'bar')).toEqual('http://www.foo.com/bar');
expect(resolver.resolve('http://www.foo.com', './bar')).toEqual('http://www.foo.com/bar');
expect(resolver.resolve('http://www.foo.com/', './bar')).toEqual('http://www.foo.com/bar');
});
it('should replace the base path', () => {
expect(resolver.resolve('http://www.foo.com/baz', 'bar')).toEqual('http://www.foo.com/bar');
expect(resolver.resolve('http://www.foo.com/baz', './bar'))
.toEqual('http://www.foo.com/bar');
});
it('should append to the base path', () => {
expect(resolver.resolve('http://www.foo.com/baz/', 'bar'))
.toEqual('http://www.foo.com/baz/bar');
expect(resolver.resolve('http://www.foo.com/baz/', './bar'))
.toEqual('http://www.foo.com/baz/bar');
});
it('should support ".." in the path', () => {
expect(resolver.resolve('http://www.foo.com/baz/', '../bar'))
.toEqual('http://www.foo.com/bar');
expect(resolver.resolve('http://www.foo.com/1/2/3/', '../../bar'))
.toEqual('http://www.foo.com/1/bar');
expect(resolver.resolve('http://www.foo.com/1/2/3/', '../biz/bar'))
.toEqual('http://www.foo.com/1/2/biz/bar');
expect(resolver.resolve('http://www.foo.com/1/2/baz', '../../bar'))
.toEqual('http://www.foo.com/bar');
});
it('should ignore the base path when the url has a scheme', () => {
expect(resolver.resolve('http://www.foo.com', 'http://www.bar.com'))
.toEqual('http://www.bar.com');
});
it('should support absolute urls', () => {
expect(resolver.resolve('http://www.foo.com', '/bar')).toEqual('http://www.foo.com/bar');
expect(resolver.resolve('http://www.foo.com/', '/bar')).toEqual('http://www.foo.com/bar');
expect(resolver.resolve('http://www.foo.com/baz', '/bar'))
.toEqual('http://www.foo.com/bar');
expect(resolver.resolve('http://www.foo.com/baz/', '/bar'))
.toEqual('http://www.foo.com/bar');
});
});
describe('relative base url', () => {
it('should add a relative path to the base url', () => {
expect(resolver.resolve('foo/', './bar')).toEqual('foo/bar');
expect(resolver.resolve('foo/baz', './bar')).toEqual('foo/bar');
expect(resolver.resolve('foo/baz', 'bar')).toEqual('foo/bar');
});
it('should support ".." in the path', () => {
expect(resolver.resolve('foo/baz', '../bar')).toEqual('bar');
expect(resolver.resolve('foo/baz', '../biz/bar')).toEqual('biz/bar');
});
it('should support absolute urls', () => {
expect(resolver.resolve('foo/baz', '/bar')).toEqual('/bar');
expect(resolver.resolve('foo/baz/', '/bar')).toEqual('/bar');
});
it('should not resolve urls against the baseUrl when the url contains a scheme', () => {
resolver = new UrlResolver('my_packages_dir');
expect(resolver.resolve('base/', 'package:file')).toEqual('my_packages_dir/file');
expect(resolver.resolve('base/', 'http:super_file')).toEqual('http:super_file');
expect(resolver.resolve('base/', './mega_file')).toEqual('base/mega_file');
});
});
describe('packages', () => {
it('should resolve a url based on the application package', () => {
resolver = new UrlResolver('my_packages_dir');
expect(resolver.resolve(null, 'package:some/dir/file.txt'))
.toEqual('my_packages_dir/some/dir/file.txt');
expect(resolver.resolve(null, 'some/dir/file.txt')).toEqual('some/dir/file.txt');
});
it('should contain a default value of "/" when nothing is provided',
inject([UrlResolver], (resolver: UrlResolver) => {
expect(resolver.resolve(null, 'package:file')).toEqual('/file');
}));
it('should resolve a package value when present within the baseurl', () => {
resolver = new UrlResolver('/my_special_dir');
expect(resolver.resolve('package:some_dir/', 'matias.html'))
.toEqual('/my_special_dir/some_dir/matias.html');
});
});
describe('corner and error cases', () => {
it('should encode URLs before resolving',
() => {
expect(resolver.resolve('foo/baz', `<p #p>Hello
</p>`)).toEqual('foo/%3Cp%20#p%3EHello%0A%20%20%20%20%20%20%20%20%3C/p%3E');
});
});
});
}

View File

@ -0,0 +1,49 @@
/**
* @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 {fakeAsync} from '@angular/core/testing/fake_async';
import {describe, expect, it} from '@angular/core/testing/testing_internal';
import {SyncAsyncResult, escapeRegExp, splitAtColon} from '../src/util';
export function main() {
describe('util', () => {
describe('SyncAsyncResult', () => {
it('async value should default to Promise.resolve(syncValue)', fakeAsync(() => {
const syncValue = {};
const sar = new SyncAsyncResult(syncValue);
sar.asyncResult.then((v: any) => expect(v).toBe(syncValue));
}));
});
describe('splitAtColon', () => {
it('should split when a single ":" is present', () => {
expect(splitAtColon('a:b', [])).toEqual(['a', 'b']);
});
it('should trim parts', () => { expect(splitAtColon(' a : b ', [])).toEqual(['a', 'b']); });
it('should support multiple ":"', () => {
expect(splitAtColon('a:b:c', [])).toEqual(['a', 'b:c']);
});
it('should use the default value when no ":" is present', () => {
expect(splitAtColon('ab', ['c', 'd'])).toEqual(['c', 'd']);
});
});
describe('RegExp', () => {
it('should escape regexp', () => {
expect(new RegExp(escapeRegExp('b')).exec('abc')).toBeTruthy();
expect(new RegExp(escapeRegExp('b')).exec('adc')).toBeFalsy();
expect(new RegExp(escapeRegExp('a.b')).exec('a.b')).toBeTruthy();
expect(new RegExp(escapeRegExp('a.b')).exec('axb')).toBeFalsy();
});
});
});
}