refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
1
packages/compiler/test/aot/README.md
Normal file
1
packages/compiler/test/aot/README.md
Normal file
@ -0,0 +1 @@
|
||||
Tests in this directory are excluded from running in the browser and only run in node.
|
248
packages/compiler/test/aot/compiler_spec.ts
Normal file
248
packages/compiler/test/aot/compiler_spec.ts
Normal 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 { }
|
||||
`
|
||||
}
|
||||
}
|
||||
};
|
1296
packages/compiler/test/aot/static_reflector_spec.ts
Normal file
1296
packages/compiler/test/aot/static_reflector_spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
487
packages/compiler/test/aot/static_symbol_resolver_spec.ts
Normal file
487
packages/compiler/test/aot/static_symbol_resolver_spec.ts
Normal 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'}]
|
||||
}
|
||||
};
|
111
packages/compiler/test/aot/summary_resolver_spec.ts
Normal file
111
packages/compiler/test/aot/summary_resolver_spec.ts
Normal 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]; }
|
||||
}
|
228
packages/compiler/test/aot/summary_serializer_spec.ts
Normal file
228
packages/compiler/test/aot/summary_serializer_spec.ts
Normal 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'}
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
431
packages/compiler/test/aot/test_util.ts
Normal file
431
packages/compiler/test/aot/test_util.ts
Normal 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';
|
||||
}
|
376
packages/compiler/test/css_parser/css_lexer_spec.ts
Normal file
376
packages/compiler/test/css_parser/css_lexer_spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
802
packages/compiler/test/css_parser/css_parser_spec.ts
Normal file
802
packages/compiler/test/css_parser/css_parser_spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
321
packages/compiler/test/css_parser/css_visitor_spec.ts
Normal file
321
packages/compiler/test/css_parser/css_visitor_spec.ts
Normal 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']);
|
||||
});
|
||||
});
|
||||
}
|
132
packages/compiler/test/directive_lifecycle_spec.ts
Normal file
132
packages/compiler/test/directive_lifecycle_spec.ts
Normal 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() {}
|
||||
}
|
553
packages/compiler/test/directive_normalizer_spec.ts
Normal file
553
packages/compiler/test/directive_normalizer_spec.ts
Normal 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 {}
|
94
packages/compiler/test/directive_resolver_mock_spec.ts
Normal file
94
packages/compiler/test/directive_resolver_mock_spec.ts
Normal 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 {
|
||||
}
|
425
packages/compiler/test/directive_resolver_spec.ts
Normal file
425
packages/compiler/test/directive_resolver_spec.ts
Normal 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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
248
packages/compiler/test/expression_parser/lexer_spec.ts
Normal file
248
packages/compiler/test/expression_parser/lexer_spec.ts
Normal 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, '?.'); });
|
||||
});
|
||||
});
|
||||
}
|
590
packages/compiler/test/expression_parser/parser_spec.ts
Normal file
590
packages/compiler/test/expression_parser/parser_spec.ts
Normal 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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
188
packages/compiler/test/expression_parser/unparser.ts
Normal file
188
packages/compiler/test/expression_parser/unparser.ts
Normal 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);
|
||||
}
|
123
packages/compiler/test/expression_parser/validator.ts
Normal file
123
packages/compiler/test/expression_parser/validator.ts
Normal 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;
|
||||
}
|
119
packages/compiler/test/i18n/digest_spec.ts
Normal file
119
packages/compiler/test/i18n/digest_spec.ts
Normal 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');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
524
packages/compiler/test/i18n/extractor_merger_spec.ts
Normal file
524
packages/compiler/test/i18n/extractor_merger_spec.ts
Normal 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()]);
|
||||
}
|
47
packages/compiler/test/i18n/i18n_html_parser_spec.ts
Normal file
47
packages/compiler/test/i18n/i18n_html_parser_spec.ts
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
335
packages/compiler/test/i18n/i18n_parser_spec.ts
Normal file
335
packages/compiler/test/i18n/i18n_parser_spec.ts
Normal 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;
|
||||
}
|
253
packages/compiler/test/i18n/integration_spec.ts
Normal file
253
packages/compiler/test/i18n/integration_spec.ts
Normal 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><b></ex></ph>beaucoup<ph name="CLOSE_BOLD_TEXT"><ex></b></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><a></ex></ph>BAR<ph name="CLOSE_LINK"><ex></a></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><i></ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex></i></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><b></ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex></b></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><h1></ex></ph>Markers in html comments<ph name="CLOSE_HEADING_LEVEL1"><ex></h1></ex></ph>
|
||||
<ph name="START_TAG_DIV"><ex><div></ex></ph><ph name="CLOSE_TAG_DIV"><ex></div></ex></ph>
|
||||
<ph name="START_TAG_DIV_1"><ex><div></ex></ph><ph name="ICU"><ex>ICU</ex></ph><ph name="CLOSE_TAG_DIV"><ex></div></ex></ph>
|
||||
</msg>
|
||||
<msg id="1491627405349178954">it <ph name="START_BOLD_TEXT"><ex><b></ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex></b></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><b></ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex></b></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><a></ex></ph>bar<ph name="CLOSE_LINK"><ex></a></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>
|
||||
`;
|
60
packages/compiler/test/i18n/message_bundle_spec.ts
Normal file
60
packages/compiler/test/i18n/message_bundle_spec.ts
Normal 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('//');
|
||||
}
|
66
packages/compiler/test/i18n/serializers/i18n_ast_spec.ts
Normal file
66
packages/compiler/test/i18n/serializers/i18n_ast_spec.ts
Normal 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++; }
|
||||
}
|
90
packages/compiler/test/i18n/serializers/placeholder_spec.ts
Normal file
90
packages/compiler/test/i18n/serializers/placeholder_spec.ts
Normal 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');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
252
packages/compiler/test/i18n/serializers/xliff_spec.ts
Normal file
252
packages/compiler/test/i18n/serializers/xliff_spec.ts
Normal 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`)));
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
81
packages/compiler/test/i18n/serializers/xmb_spec.ts
Normal file
81
packages/compiler/test/i18n/serializers/xmb_spec.ts
Normal 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><b></ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> <ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph></msg>
|
||||
<msg id="2981514368455622387">{VAR_PLURAL, plural, =0 {<ph name="START_PARAGRAPH"><ex><p></ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex></p></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><p></ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex></p></ex></ph>} } } }</msg>
|
||||
<msg id="2015957479576096115">{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {<ph name="START_PARAGRAPH"><ex><p></ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex></p></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);
|
||||
}
|
47
packages/compiler/test/i18n/serializers/xml_helper_spec.ts
Normal file
47
packages/compiler/test/i18n/serializers/xml_helper_spec.ts
Normal 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('<>'); });
|
||||
|
||||
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="<">"/>');
|
||||
});
|
||||
|
||||
});
|
||||
}
|
192
packages/compiler/test/i18n/serializers/xtb_spec.ts
Normal file
192
packages/compiler/test/i18n/serializers/xtb_spec.ts
Normal 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/); });
|
||||
|
||||
});
|
||||
});
|
||||
}
|
155
packages/compiler/test/i18n/translation_bundle_spec.ts
Normal file
155
packages/compiler/test/i18n/translation_bundle_spec.ts
Normal 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"/);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
51
packages/compiler/test/integration_spec.ts
Normal file
51
packages/compiler/test/integration_spec.ts
Normal 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);
|
||||
}
|
152
packages/compiler/test/metadata_overrider_spec.ts
Normal file
152
packages/compiler/test/metadata_overrider_spec.ts
Normal 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'
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
13
packages/compiler/test/metadata_resolver_fixture.ts
Normal file
13
packages/compiler/test/metadata_resolver_fixture.ts
Normal 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 {
|
||||
}
|
487
packages/compiler/test/metadata_resolver_spec.ts
Normal file
487
packages/compiler/test/metadata_resolver_spec.ts
Normal 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(`['{', '}}'] 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: ['{', '}}']})
|
||||
class ComponentWithInvalidInterpolation5 {
|
||||
}
|
100
packages/compiler/test/ml_parser/ast_serializer_spec.ts
Normal file
100
packages/compiler/test/ml_parser/ast_serializer_spec.ts
Normal 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));
|
||||
}
|
84
packages/compiler/test/ml_parser/ast_spec_utils.ts
Normal file
84
packages/compiler/test/ml_parser/ast_spec_utils.ts
Normal 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;
|
||||
}
|
||||
}
|
543
packages/compiler/test/ml_parser/html_parser_spec.ts
Normal file
543
packages/compiler/test/ml_parser/html_parser_spec.ts
Normal 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)];
|
||||
});
|
||||
}
|
124
packages/compiler/test/ml_parser/icu_ast_expander_spec.ts
Normal file
124
packages/compiler/test/ml_parser/icu_ast_expander_spec.ts
Normal 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);
|
||||
}
|
830
packages/compiler/test/ml_parser/lexer_spec.ts
Normal file
830
packages/compiler/test/ml_parser/lexer_spec.ts
Normal 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="AA">')).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="&" b="c&&d">')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'a'],
|
||||
[lex.TokenType.ATTR_VALUE, '&'],
|
||||
[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&b')).toEqual([
|
||||
[lex.TokenType.TEXT, 'a&b'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse hexadecimal entities', () => {
|
||||
expect(tokenizeAndHumanizeParts('AA')).toEqual([
|
||||
[lex.TokenType.TEXT, 'AA'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse decimal entities', () => {
|
||||
expect(tokenizeAndHumanizeParts('A')).toEqual([
|
||||
[lex.TokenType.TEXT, 'A'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('a&b')).toEqual([
|
||||
[lex.TokenType.TEXT, 'a&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('
sdf;')).toEqual([
|
||||
[lex.TokenType.TEXT, 'Unexpected character "s"', '0:4']
|
||||
]);
|
||||
|
||||
expect(tokenizeAndHumanizeErrors('઼')).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&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>&</SCRIPT>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'script'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.RAW_TEXT, '&'],
|
||||
[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>&</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)]);
|
||||
}
|
44
packages/compiler/test/ng_module_resolver_mock_spec.ts
Normal file
44
packages/compiler/test/ng_module_resolver_mock_spec.ts
Normal 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 {
|
||||
}
|
70
packages/compiler/test/ng_module_resolver_spec.ts
Normal file
70
packages/compiler/test/ng_module_resolver_spec.ts
Normal 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'}));
|
||||
});
|
||||
|
||||
});
|
||||
}
|
148
packages/compiler/test/output/abstract_emitter_node_only_spec.ts
Normal file
148
packages/compiler/test/output/abstract_emitter_node_only_spec.ts
Normal 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), '');
|
||||
}
|
40
packages/compiler/test/output/abstract_emitter_spec.ts
Normal file
40
packages/compiler/test/output/abstract_emitter_spec.ts
Normal 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);
|
||||
}
|
66
packages/compiler/test/output/js_emitter_node_only_spec.ts
Normal file
66
packages/compiler/test/output/js_emitter_node_only_spec.ts
Normal 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});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
304
packages/compiler/test/output/js_emitter_spec.ts
Normal file
304
packages/compiler/test/output/js_emitter_spec.ts
Normal 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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
130
packages/compiler/test/output/source_map_spec.ts
Normal file
130
packages/compiler/test/output/source_map_spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
71
packages/compiler/test/output/ts_emitter_node_only_spec.ts
Normal file
71
packages/compiler/test/output/ts_emitter_node_only_spec.ts
Normal 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});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
463
packages/compiler/test/output/ts_emitter_spec.ts
Normal file
463
packages/compiler/test/output/ts_emitter_spec.ts
Normal 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);');
|
||||
});
|
||||
});
|
||||
}
|
37
packages/compiler/test/pipe_resolver_mock_spec.ts
Normal file
37
packages/compiler/test/pipe_resolver_mock_spec.ts
Normal 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 {
|
||||
}
|
52
packages/compiler/test/pipe_resolver_spec.ts
Normal file
52
packages/compiler/test/pipe_resolver_spec.ts
Normal 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'}));
|
||||
});
|
||||
|
||||
});
|
||||
}
|
114
packages/compiler/test/resource_loader_mock_spec.ts
Normal file
114
packages/compiler/test/resource_loader_mock_spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
}
|
168
packages/compiler/test/runtime_compiler_spec.ts
Normal file
168
packages/compiler/test/runtime_compiler_spec.ts
Normal 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();
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
271
packages/compiler/test/schema/schema_extractor.ts
Normal file
271
packages/compiler/test/schema/schema_extractor.ts
Normal 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': '%',
|
||||
};
|
460
packages/compiler/test/selector_spec.ts
Normal file
460
packages/compiler/test/selector_spec.ts
Normal 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;
|
||||
}
|
316
packages/compiler/test/shadow_css_spec.ts
Normal file
316
packages/compiler/test/shadow_css_spec.ts
Normal 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}');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
15
packages/compiler/test/spies.ts
Normal file
15
packages/compiler/test/spies.ts
Normal 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); }
|
||||
}
|
124
packages/compiler/test/style_url_resolver_spec.ts
Normal file
124
packages/compiler/test/style_url_resolver_spec.ts
Normal 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'; }
|
||||
}
|
@ -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
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
2279
packages/compiler/test/template_parser/template_parser_spec.ts
Normal file
2279
packages/compiler/test/template_parser/template_parser_spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
117
packages/compiler/test/url_resolver_spec.ts
Normal file
117
packages/compiler/test/url_resolver_spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
49
packages/compiler/test/util_spec.ts
Normal file
49
packages/compiler/test/util_spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user