fix(zone.js): add issue numbers of @types/jasmine to the test cases (#34625)

Some cases will still need to use `spy as any` cast, because `@types/jasmine` have some issues,
1. The issue jasmine doesn't handle optional method properties, https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
2. The issue jasmine doesn't handle overload method correctly, https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42455

PR Close #34625
This commit is contained in:
JiaLiPassion 2020-03-31 00:22:25 +09:00 committed by Kara Erickson
parent b28a5f6eef
commit 421b6a97d6
22 changed files with 5993 additions and 5745 deletions

View File

@ -29,8 +29,8 @@ const XSSI_PREFIX = ')]}\'\n';
{ {
describe('XhrBackend', () => { describe('XhrBackend', () => {
let factory: MockXhrFactory = null !; let factory: MockXhrFactory = null!;
let backend: HttpXhrBackend = null !; let backend: HttpXhrBackend = null!;
beforeEach(() => { beforeEach(() => {
factory = new MockXhrFactory(); factory = new MockXhrFactory();
backend = new HttpXhrBackend(factory); backend = new HttpXhrBackend(factory);
@ -92,7 +92,7 @@ const XSSI_PREFIX = ')]}\'\n';
factory.mock.mockFlush(200, 'OK', JSON.stringify({data: 'some data'})); factory.mock.mockFlush(200, 'OK', JSON.stringify({data: 'some data'}));
expect(events.length).toBe(2); expect(events.length).toBe(2);
const res = events[1] as HttpResponse<{data: string}>; const res = events[1] as HttpResponse<{data: string}>;
expect(res.body !.data).toBe('some data'); expect(res.body!.data).toBe('some data');
}); });
it('handles a blank json response', () => { it('handles a blank json response', () => {
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'}))); const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
@ -106,14 +106,14 @@ const XSSI_PREFIX = ')]}\'\n';
factory.mock.mockFlush(500, 'Error', JSON.stringify({data: 'some data'})); factory.mock.mockFlush(500, 'Error', JSON.stringify({data: 'some data'}));
expect(events.length).toBe(2); expect(events.length).toBe(2);
const res = events[1] as any as HttpErrorResponse; const res = events[1] as any as HttpErrorResponse;
expect(res.error !.data).toBe('some data'); expect(res.error!.data).toBe('some data');
}); });
it('handles a json error response with XSSI prefix', () => { it('handles a json error response with XSSI prefix', () => {
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'}))); const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
factory.mock.mockFlush(500, 'Error', XSSI_PREFIX + JSON.stringify({data: 'some data'})); factory.mock.mockFlush(500, 'Error', XSSI_PREFIX + JSON.stringify({data: 'some data'}));
expect(events.length).toBe(2); expect(events.length).toBe(2);
const res = events[1] as any as HttpErrorResponse; const res = events[1] as any as HttpErrorResponse;
expect(res.error !.data).toBe('some data'); expect(res.error!.data).toBe('some data');
}); });
it('handles a json string response', () => { it('handles a json string response', () => {
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'}))); const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
@ -128,7 +128,7 @@ const XSSI_PREFIX = ')]}\'\n';
factory.mock.mockFlush(200, 'OK', XSSI_PREFIX + JSON.stringify({data: 'some data'})); factory.mock.mockFlush(200, 'OK', XSSI_PREFIX + JSON.stringify({data: 'some data'}));
expect(events.length).toBe(2); expect(events.length).toBe(2);
const res = events[1] as HttpResponse<{data: string}>; const res = events[1] as HttpResponse<{data: string}>;
expect(res.body !.data).toBe('some data'); expect(res.body!.data).toBe('some data');
}); });
it('emits unsuccessful responses via the error path', done => { it('emits unsuccessful responses via the error path', done => {
backend.handle(TEST_POST).subscribe(undefined, (err: HttpErrorResponse) => { backend.handle(TEST_POST).subscribe(undefined, (err: HttpErrorResponse) => {

View File

@ -38,7 +38,7 @@ runInEachFileSystem(() => {
initMockFileSystem(fs, testFiles); initMockFileSystem(fs, testFiles);
// Force single-process execution in unit tests by mocking available CPUs to 1. // Force single-process execution in unit tests by mocking available CPUs to 1.
spyOn(os, 'cpus').and.returnValue([{ model: 'Mock CPU' } as any]); spyOn(os, 'cpus').and.returnValue([{model: 'Mock CPU'} as any]);
}); });
it('should run ngcc without errors for esm2015', () => { it('should run ngcc without errors for esm2015', () => {

View File

@ -13,6 +13,7 @@ describe('unlocker', () => {
spyOn(process, 'on'); spyOn(process, 'on');
require('../../../src/locking/lock_file_with_child_process/unlocker'); require('../../../src/locking/lock_file_with_child_process/unlocker');
// TODO: @JiaLiPassion, need to wait for @types/jasmine to handle the override case // TODO: @JiaLiPassion, need to wait for @types/jasmine to handle the override case
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42455
expect(process.on).toHaveBeenCalledWith('disconnect' as any, jasmine.any(Function)); expect(process.on).toHaveBeenCalledWith('disconnect' as any, jasmine.any(Function));
}); });
}); });

View File

@ -47,7 +47,7 @@ describe('CachedFileSystem', () => {
let lstatSpy: jasmine.Spy; let lstatSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
// For most of the tests the files are not symbolic links. // For most of the tests the files are not symbolic links.
lstatSpy = spyOn(delegate, 'lstat').and.returnValue({ isSymbolicLink: () => false } as any); lstatSpy = spyOn(delegate, 'lstat').and.returnValue({isSymbolicLink: () => false} as any);
}); });
it('should call delegate if not in cache', () => { it('should call delegate if not in cache', () => {
@ -93,7 +93,7 @@ describe('CachedFileSystem', () => {
describe('invalidateCaches()', () => { describe('invalidateCaches()', () => {
it('should call the delegate `readFile()` if the path for the cached file has been invalidated', it('should call the delegate `readFile()` if the path for the cached file has been invalidated',
() => { () => {
spyOn(delegate, 'lstat').and.returnValue({ isSymbolicLink: () => false } as any); spyOn(delegate, 'lstat').and.returnValue({isSymbolicLink: () => false} as any);
const spy = spyOn(delegate, 'readFile').and.returnValue('Some contents'); const spy = spyOn(delegate, 'readFile').and.returnValue('Some contents');
fs.readFile(abcPath); // Call once to fill the cache fs.readFile(abcPath); // Call once to fill the cache
spy.calls.reset(); spy.calls.reset();
@ -230,7 +230,7 @@ describe('CachedFileSystem', () => {
describe('moveFile()', () => { describe('moveFile()', () => {
beforeEach(() => { beforeEach(() => {
// `moveFile()` relies upon `readFile` which calls through to `lstat()`, so stub it out. // `moveFile()` relies upon `readFile` which calls through to `lstat()`, so stub it out.
spyOn(delegate, 'lstat').and.returnValue({ isSymbolicLink: () => false } as any); spyOn(delegate, 'lstat').and.returnValue({isSymbolicLink: () => false} as any);
}); });
it('should call delegate', () => { it('should call delegate', () => {

View File

@ -69,6 +69,7 @@ describe('NodeJSFileSystem', () => {
const result = fs.readdir(abcPath); const result = fs.readdir(abcPath);
expect(result).toEqual([relativeFrom('x'), relativeFrom('y/z')]); expect(result).toEqual([relativeFrom('x'), relativeFrom('y/z')]);
// TODO: @JiaLiPassion need to wait for @types/jasmine update to handle optional parameters. // TODO: @JiaLiPassion need to wait for @types/jasmine update to handle optional parameters.
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
expect(spy as any).toHaveBeenCalledWith(abcPath); expect(spy as any).toHaveBeenCalledWith(abcPath);
}); });
}); });
@ -90,6 +91,7 @@ describe('NodeJSFileSystem', () => {
const result = fs.stat(abcPath); const result = fs.stat(abcPath);
expect(result).toBe(stats); expect(result).toBe(stats);
// TODO: @JiaLiPassion need to wait for @types/jasmine update to handle optional parameters. // TODO: @JiaLiPassion need to wait for @types/jasmine update to handle optional parameters.
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
expect(spy as any).toHaveBeenCalledWith(abcPath); expect(spy as any).toHaveBeenCalledWith(abcPath);
}); });
}); });
@ -173,10 +175,12 @@ describe('NodeJSFileSystem', () => {
} }
return false; return false;
}); });
spyOn(fs, 'stat').and.returnValue({ isDirectory: () => true } as any); spyOn(fs, 'stat').and.returnValue({isDirectory: () => true} as any);
const mkdirSyncSpy = spyOn(realFs, 'mkdirSync').and.callFake(((path: string) => { const mkdirSyncSpy =
spyOn(realFs, 'mkdirSync').and.callFake(((path: string) => {
if (path === abcPath) { if (path === abcPath) {
throw new Error('It exists already. Supposedly.'); throw new Error(
'It exists already. Supposedly.');
} }
}) as any); }) as any);
@ -188,7 +192,8 @@ describe('NodeJSFileSystem', () => {
it('should fail if creating the directory throws and the directory does not exist', () => { it('should fail if creating the directory throws and the directory does not exist', () => {
spyOn(fs, 'exists').and.returnValue(false); spyOn(fs, 'exists').and.returnValue(false);
spyOn(realFs, 'mkdirSync').and.callFake(((path: string) => { spyOn(realFs, 'mkdirSync')
.and.callFake(((path: string) => {
if (path === abcPath) { if (path === abcPath) {
throw new Error('Unable to create directory (for whatever reason).'); throw new Error('Unable to create directory (for whatever reason).');
} }
@ -212,7 +217,7 @@ describe('NodeJSFileSystem', () => {
} }
return false; return false;
}); });
spyOn(fs, 'stat').and.returnValue({ isDirectory: isDirectorySpy } as any); spyOn(fs, 'stat').and.returnValue({isDirectory: isDirectorySpy} as any);
spyOn(realFs, 'mkdirSync').and.callFake(((path: string) => { spyOn(realFs, 'mkdirSync').and.callFake(((path: string) => {
if (path === abcPath) { if (path === abcPath) {
throw new Error('It exists already. Supposedly.'); throw new Error('It exists already. Supposedly.');

View File

@ -41,7 +41,9 @@ describe('ngc transformer command-line', () => {
basePath = support.basePath; basePath = support.basePath;
outDir = path.join(basePath, 'built'); outDir = path.join(basePath, 'built');
process.chdir(basePath); process.chdir(basePath);
write = (fileName: string, content: string) => { support.write(fileName, content); }; write = (fileName: string, content: string) => {
support.write(fileName, content);
};
write('tsconfig-base.json', `{ write('tsconfig-base.json', `{
"compilerOptions": { "compilerOptions": {
@ -96,8 +98,9 @@ describe('ngc transformer command-line', () => {
}); });
describe('errors', () => { describe('errors', () => {
beforeEach(() => {
beforeEach(() => { errorSpy.and.stub(); }); errorSpy.and.stub();
});
it('should not print the stack trace if user input file does not exist', () => { it('should not print the stack trace if user input file does not exist', () => {
writeConfig(`{ writeConfig(`{
@ -231,7 +234,6 @@ describe('ngc transformer command-line', () => {
}); });
describe('compile ngfactory files', () => { describe('compile ngfactory files', () => {
it('should compile ngfactory files that are not referenced by root files', () => { it('should compile ngfactory files that are not referenced by root files', () => {
writeConfig(`{ writeConfig(`{
"extends": "./tsconfig-base.json", "extends": "./tsconfig-base.json",
@ -1122,7 +1124,6 @@ describe('ngc transformer command-line', () => {
}); });
describe('with external symbol re-exports enabled', () => { describe('with external symbol re-exports enabled', () => {
it('should be able to compile multiple libraries with summaries', () => { it('should be able to compile multiple libraries with summaries', () => {
// Note: we need to emit the generated code for the libraries // Note: we need to emit the generated code for the libraries
// into the node_modules, as that is the only way that we // into the node_modules, as that is the only way that we
@ -1560,11 +1561,13 @@ describe('ngc transformer command-line', () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
const timerToken = 100; const timerToken = 100;
// TODO: @JiaLiPassion, need to wait @types/jasmine to handle optional method case // TODO: @JiaLiPassion, need to wait @types/jasmine to handle optional method case
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
spyOn(ts.sys as any, 'setTimeout').and.callFake((callback: () => void) => { spyOn(ts.sys as any, 'setTimeout').and.callFake((callback: () => void) => {
timer = callback; timer = callback;
return timerToken; return timerToken;
}); });
// TODO: @JiaLiPassion, need to wait @types/jasmine to handle optional method case // TODO: @JiaLiPassion, need to wait @types/jasmine to handle optional method case
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
spyOn(ts.sys as any, 'clearTimeout').and.callFake((token: number) => { spyOn(ts.sys as any, 'clearTimeout').and.callFake((token: number) => {
if (token == timerToken) { if (token == timerToken) {
timer = undefined; timer = undefined;
@ -1617,7 +1620,9 @@ describe('ngc transformer command-line', () => {
`); `);
}); });
afterEach(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; }); afterEach(() => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
});
function writeAppConfig(location: string) { function writeAppConfig(location: string) {
writeConfig(`{ writeConfig(`{
@ -1672,11 +1677,13 @@ describe('ngc transformer command-line', () => {
`); `);
})); }));
it('should recompile when the html file changes', it('should recompile when the html file changes', expectRecompile(() => {
expectRecompile(() => { write('greet.html', '<p> Hello {{name}} again!</p>'); })); write('greet.html', '<p> Hello {{name}} again!</p>');
}));
it('should recompile when the css file changes', it('should recompile when the css file changes', expectRecompile(() => {
expectRecompile(() => { write('greet.css', `p.greeting { color: blue }`); })); write('greet.css', `p.greeting { color: blue }`);
}));
}); });
describe('regressions', () => { describe('regressions', () => {
@ -2041,8 +2048,8 @@ describe('ngc transformer command-line', () => {
expect(exitCode).toBe(1, 'Compile was expected to fail'); expect(exitCode).toBe(1, 'Compile was expected to fail');
const srcPathWithSep = `lib/`; const srcPathWithSep = `lib/`;
expect(messages[0]) expect(messages[0])
.toEqual( .toEqual(`${
`${srcPathWithSep}test.component.ts(6,21): Error during template compile of 'TestComponent' srcPathWithSep}test.component.ts(6,21): Error during template compile of 'TestComponent'
Tagged template expressions are not supported in metadata in 't1' Tagged template expressions are not supported in metadata in 't1'
't1' references 't2' at ${srcPathWithSep}indirect1.ts(3,27) 't1' references 't2' at ${srcPathWithSep}indirect1.ts(3,27)
't2' contains the error at ${srcPathWithSep}indirect2.ts(4,27). 't2' contains the error at ${srcPathWithSep}indirect2.ts(4,27).
@ -2051,7 +2058,6 @@ describe('ngc transformer command-line', () => {
}); });
describe('tree shakeable services', () => { describe('tree shakeable services', () => {
function compileService(source: string): string { function compileService(source: string): string {
write('service.ts', source); write('service.ts', source);

View File

@ -13,8 +13,8 @@ describe('convertValueToOutputAst', () => {
it('should convert all array elements, including undefined', () => { it('should convert all array elements, including undefined', () => {
const ctx = null; const ctx = null;
const value = new Array(3).concat('foo'); const value = new Array(3).concat('foo');
const expr = convertValueToOutputAst(ctx !, value) as o.LiteralArrayExpr; const expr = convertValueToOutputAst(ctx!, value) as o.LiteralArrayExpr;
expect(expr instanceof o.LiteralArrayExpr); expect(expr instanceof o.LiteralArrayExpr).toBe(true);
expect(expr.entries.length).toBe(4); expect(expr.entries.length).toBe(4);
for (let i = 0; i < 4; ++i) { for (let i = 0; i < 4; ++i) {
expect(expr.entries[i] instanceof o.Expression).toBe(true); expect(expr.entries[i] instanceof o.Expression).toBe(true);

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {AUTO_STYLE, AnimationPlayer, animate, animateChild, group, query, sequence, stagger, state, style, transition, trigger, ɵAnimationGroupPlayer as AnimationGroupPlayer} from '@angular/animations'; import {animate, animateChild, AnimationPlayer, AUTO_STYLE, group, query, sequence, stagger, state, style, transition, trigger, ɵAnimationGroupPlayer as AnimationGroupPlayer} from '@angular/animations';
import {AnimationDriver, ɵAnimationEngine} from '@angular/animations/browser'; import {AnimationDriver, ɵAnimationEngine} from '@angular/animations/browser';
import {matchesElement} from '@angular/animations/browser/src/render/shared'; import {matchesElement} from '@angular/animations/browser/src/render/shared';
import {TransitionAnimationPlayer} from '@angular/animations/browser/src/render/transition_animation_engine'; import {TransitionAnimationPlayer} from '@angular/animations/browser/src/render/transition_animation_engine';
@ -13,21 +13,23 @@ import {ENTER_CLASSNAME, LEAVE_CLASSNAME} from '@angular/animations/browser/src/
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing'; import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {Component, HostBinding, ViewChild} from '@angular/core'; import {Component, HostBinding, ViewChild} from '@angular/core';
import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HostListener} from '../../src/metadata/directives'; import {HostListener} from '../../src/metadata/directives';
(function() { (function() {
// these tests are only mean't to be run within the DOM (for now) // these tests are only mean't to be run within the DOM (for now)
if (isNode) return; if (isNode) return;
describe('animation query tests', function() { describe('animation query tests', function() {
function getLog(): MockAnimationPlayer[] { function getLog(): MockAnimationPlayer[] {
return MockAnimationDriver.log as MockAnimationPlayer[]; return MockAnimationDriver.log as MockAnimationPlayer[];
} }
function resetLog() { MockAnimationDriver.log = []; } function resetLog() {
MockAnimationDriver.log = [];
}
beforeEach(() => { beforeEach(() => {
resetLog(); resetLog();
@ -60,7 +62,7 @@ import {HostListener} from '../../src/metadata/directives';
query( query(
'@*', '@*',
[ [
style({ backgroundColor: 'blue' }), style({backgroundColor: 'blue'}),
animate(1000, style({backgroundColor: 'red'})), animate(1000, style({backgroundColor: 'red'})),
]), ]),
]), ]),
@ -68,26 +70,22 @@ import {HostListener} from '../../src/metadata/directives';
trigger( trigger(
'a', 'a',
[ [
transition('* => 1', [ transition('* => 1', [animate(1000, style({opacity: 0}))]),
animate(1000, style({ opacity: 0 }))
]),
]), ]),
trigger( trigger(
'b', 'b',
[ [
transition('* => 1', [ transition(
animate(1000, style({ opacity: 0 })), '* => 1',
query('.b-inner', [ [
animate(1000, style({ opacity: 0 })) animate(1000, style({opacity: 0})),
]), query('.b-inner', [animate(1000, style({opacity: 0}))]),
]), ]),
]), ]),
trigger( trigger(
'c', 'c',
[ [
transition('* => 1', [ transition('* => 1', [animate(1000, style({opacity: 0}))]),
animate(1000, style({ opacity: 0 }))
]),
]), ]),
] ]
}) })
@ -137,7 +135,7 @@ import {HostListener} from '../../src/metadata/directives';
query( query(
':animating', ':animating',
[ [
style({ backgroundColor: 'blue' }), style({backgroundColor: 'blue'}),
animate(1000, style({backgroundColor: 'red'})), animate(1000, style({backgroundColor: 'red'})),
]), ]),
]), ]),
@ -145,26 +143,22 @@ import {HostListener} from '../../src/metadata/directives';
trigger( trigger(
'a', 'a',
[ [
transition('* => 1', [ transition('* => 1', [animate(1000, style({opacity: 0}))]),
animate(1000, style({ opacity: 0 }))
]),
]), ]),
trigger( trigger(
'b', 'b',
[ [
transition('* => 1', [ transition(
animate(1000, style({ opacity: 0 })), '* => 1',
query('.b-inner', [ [
animate(1000, style({ opacity: 0 })) animate(1000, style({opacity: 0})),
]), query('.b-inner', [animate(1000, style({opacity: 0}))]),
]), ]),
]), ]),
trigger( trigger(
'c', 'c',
[ [
transition('* => 1', [ transition('* => 1', [animate(1000, style({opacity: 0}))]),
animate(1000, style({ opacity: 0 }))
]),
]), ]),
] ]
}) })
@ -361,8 +355,7 @@ import {HostListener} from '../../src/metadata/directives';
expect(players[2].element.classList.contains('e-4')).toBeTruthy(); expect(players[2].element.classList.contains('e-4')).toBeTruthy();
}); });
it('should be able to query all actively queued animation triggers via `@*:animating`', it('should be able to query all actively queued animation triggers via `@*:animating`', () => {
() => {
@Component({ @Component({
selector: 'ani-cmp', selector: 'ani-cmp',
template: ` template: `
@ -454,7 +447,8 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(1); expect(players.length).toEqual(1);
}); });
it('should collect styles for the same elements between queries', () => { it(
'should collect styles for the same elements between queries', () => {
@Component({ @Component({
selector: 'ani-cmp', selector: 'ani-cmp',
template: ` template: `
@ -546,12 +540,11 @@ import {HostListener} from '../../src/metadata/directives';
template: ` template: `
<div [@myAnimation]="exp"></div> <div [@myAnimation]="exp"></div>
`, `,
animations: [trigger('myAnimation', [transition( animations: [trigger(
'myAnimation',
[transition(
'* => go', '* => go',
[ [query(':self', style({opacity: '0.5'})), animate(1000, style({opacity: '1'}))])])]
query(':self', style({opacity: '0.5'})),
animate(1000, style({opacity: '1'}))
])])]
}) })
class Cmp { class Cmp {
public exp: any; public exp: any;
@ -793,9 +786,7 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(1); expect(players.length).toEqual(1);
const player = players[0]; const player = players[0];
expect(player.keyframes).toEqual([ expect(player.keyframes).toEqual([{height: '0px', offset: 0}, {height: '444px', offset: 1}]);
{height: '0px', offset: 0}, {height: '444px', offset: 1}
]);
player.finish(); player.finish();
expect(player.element.style.height).toEqual('444px'); expect(player.element.style.height).toEqual('444px');
@ -903,7 +894,9 @@ import {HostListener} from '../../src/metadata/directives';
cmp.items = [0, 1, 2, 3, 4]; cmp.items = [0, 1, 2, 3, 4];
expect(() => { fixture.detectChanges(); }).toThrow(); expect(() => {
fixture.detectChanges();
}).toThrow();
const children = cmp.container.nativeElement.querySelectorAll('.child'); const children = cmp.container.nativeElement.querySelectorAll('.child');
expect(children.length).toEqual(5); expect(children.length).toEqual(5);
@ -1049,7 +1042,7 @@ import {HostListener} from '../../src/metadata/directives';
}) })
class Cmp { class Cmp {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
public items !: any[]; public items!: any[];
} }
TestBed.configureTestingModule({declarations: [Cmp]}); TestBed.configureTestingModule({declarations: [Cmp]});
@ -1071,7 +1064,7 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(5); expect(players.length).toEqual(5);
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
let player = players[i] !; let player = players[i]!;
expect(player.keyframes).toEqual([ expect(player.keyframes).toEqual([
{opacity: '0', offset: 0}, {opacity: '0', offset: 0},
{opacity: '1', offset: 1}, {opacity: '1', offset: 1},
@ -1091,7 +1084,7 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(5); expect(players.length).toEqual(5);
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
let player = players[i] !; let player = players[i]!;
expect(player.keyframes).toEqual([ expect(player.keyframes).toEqual([
{opacity: '1', offset: 0}, {opacity: '1', offset: 0},
{opacity: '0', offset: 1}, {opacity: '0', offset: 1},
@ -1130,7 +1123,7 @@ import {HostListener} from '../../src/metadata/directives';
class Cmp { class Cmp {
public exp: any; public exp: any;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
public items !: any[]; public items!: any[];
} }
TestBed.configureTestingModule({declarations: [Cmp]}); TestBed.configureTestingModule({declarations: [Cmp]});
@ -1351,7 +1344,7 @@ import {HostListener} from '../../src/metadata/directives';
class Cmp { class Cmp {
public exp: any; public exp: any;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
public items !: any[]; public items!: any[];
} }
TestBed.configureTestingModule({declarations: [Cmp]}); TestBed.configureTestingModule({declarations: [Cmp]});
@ -1369,7 +1362,9 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(5); expect(players.length).toEqual(5);
let count = 0; let count = 0;
players.forEach(p => { p.onDone(() => count++); }); players.forEach(p => {
p.onDone(() => count++);
});
expect(count).toEqual(0); expect(count).toEqual(0);
@ -1404,7 +1399,7 @@ import {HostListener} from '../../src/metadata/directives';
class Cmp { class Cmp {
public exp: any; public exp: any;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
public items !: any[]; public items!: any[];
} }
TestBed.configureTestingModule({declarations: [Cmp]}); TestBed.configureTestingModule({declarations: [Cmp]});
@ -1422,7 +1417,9 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(5); expect(players.length).toEqual(5);
let count = 0; let count = 0;
players.forEach(p => { p.onDone(() => count++); }); players.forEach(p => {
p.onDone(() => count++);
});
expect(count).toEqual(0); expect(count).toEqual(0);
@ -1467,7 +1464,7 @@ import {HostListener} from '../../src/metadata/directives';
public exp1: any; public exp1: any;
public exp2: any; public exp2: any;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
public items !: any[]; public items!: any[];
} }
TestBed.configureTestingModule({declarations: [Cmp]}); TestBed.configureTestingModule({declarations: [Cmp]});
@ -1485,7 +1482,9 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(5); expect(players.length).toEqual(5);
let count = 0; let count = 0;
players.forEach(p => { p.onDone(() => count++); }); players.forEach(p => {
p.onDone(() => count++);
});
resetLog(); resetLog();
@ -1500,7 +1499,9 @@ import {HostListener} from '../../src/metadata/directives';
players = getLog(); players = getLog();
expect(players.length).toEqual(3); expect(players.length).toEqual(3);
players.forEach(p => { p.onDone(() => count++); }); players.forEach(p => {
p.onDone(() => count++);
});
cmp.exp1 = 'off'; cmp.exp1 = 'off';
fixture.detectChanges(); fixture.detectChanges();
@ -1537,7 +1538,7 @@ import {HostListener} from '../../src/metadata/directives';
class Cmp { class Cmp {
public exp: any; public exp: any;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
public items !: any[]; public items!: any[];
} }
TestBed.configureTestingModule({declarations: [Cmp]}); TestBed.configureTestingModule({declarations: [Cmp]});
@ -1555,7 +1556,9 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(5); expect(players.length).toEqual(5);
let count = 0; let count = 0;
players.forEach(p => { p.onDone(() => count++); }); players.forEach(p => {
p.onDone(() => count++);
});
expect(count).toEqual(0); expect(count).toEqual(0);
@ -1592,7 +1595,7 @@ import {HostListener} from '../../src/metadata/directives';
class Cmp { class Cmp {
public exp: any; public exp: any;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
public items !: any[]; public items!: any[];
} }
TestBed.configureTestingModule({declarations: [Cmp]}); TestBed.configureTestingModule({declarations: [Cmp]});
@ -1631,8 +1634,7 @@ import {HostListener} from '../../src/metadata/directives';
'myAnimation', 'myAnimation',
[transition( [transition(
'* => on', '* => on',
[query( [query(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])])]
':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])])]
}) })
class ParentCmp { class ParentCmp {
public exp: any; public exp: any;
@ -1680,8 +1682,7 @@ import {HostListener} from '../../src/metadata/directives';
`, `,
animations: [trigger( animations: [trigger(
'myAnimation', 'myAnimation',
[transition( [transition('* => on', [query(':leave', [animate(1000, style({opacity: 0}))])])])]
'* => on', [query(':leave', [animate(1000, style({opacity: 0}))])])])]
}) })
class ParentCmp { class ParentCmp {
public exp: any; public exp: any;
@ -2018,9 +2019,9 @@ import {HostListener} from '../../src/metadata/directives';
</div> </div>
`, `,
animations: [ animations: [
trigger('child', [transition( trigger(
'a => z', 'child',
[style({opacity: 0}), animate(1000, style({opacity: 1}))])]), [transition('a => z', [style({opacity: 0}), animate(1000, style({opacity: 1}))])]),
trigger('parent', [transition( trigger('parent', [transition(
'a => z', 'a => z',
[ [
@ -2076,28 +2077,19 @@ import {HostListener} from '../../src/metadata/directives';
</div> </div>
`, `,
animations: [ animations: [
trigger('w', [ trigger(
transition('* => go', [ 'w',
style({ width: 0 }), [transition('* => go', [style({width: 0}), animate(1800, style({width: '100px'}))])]),
animate(1800, style({ width: '100px' })) trigger(
]) 'h', [transition(
]), '* => go', [style({height: 0}), animate(1500, style({height: '100px'}))])]),
trigger('h', [ trigger(
transition('* => go', [ 'parent', [transition(
style({ height: 0 }), '* => go',
animate(1500, style({ height: '100px' })) [
]) style({opacity: 0}), animate(1000, style({opacity: 1})),
]), query('.child', [animateChild()]), animate(1000, style({opacity: 0}))
trigger('parent', [ ])])
transition('* => go', [
style({ opacity: 0 }),
animate(1000, style({ opacity: 1 })),
query('.child', [
animateChild()
]),
animate(1000, style({ opacity: 0 }))
])
])
] ]
}) })
class Cmp { class Cmp {
@ -2141,9 +2133,9 @@ import {HostListener} from '../../src/metadata/directives';
</div> </div>
`, `,
animations: [ animations: [
trigger('child', [transition( trigger(
'* => go', 'child',
[style({width: 0}), animate(1800, style({width: '100px'}))])]), [transition('* => go', [style({width: 0}), animate(1800, style({width: '100px'}))])]),
trigger('parent', [transition( trigger('parent', [transition(
'* => go', '* => go',
[ [
@ -2200,9 +2192,9 @@ import {HostListener} from '../../src/metadata/directives';
style({opacity: 0}), animate(1000, style({opacity: 1})), style({opacity: 0}), animate(1000, style({opacity: 1})),
query('.child', animateChild()) query('.child', animateChild())
])]), ])]),
trigger('child', [transition( trigger(
'* => go', 'child',
[style({opacity: 0}), animate(1800, style({opacity: 1}))])]) [transition('* => go', [style({opacity: 0}), animate(1800, style({opacity: 1}))])])
] ]
}) })
class Cmp { class Cmp {
@ -2434,7 +2426,7 @@ import {HostListener} from '../../src/metadata/directives';
const players = getLog(); const players = getLog();
expect(players.length).toEqual(1); expect(players.length).toEqual(1);
const element = players[0] !.element; const element = players[0]!.element;
expect(element.innerText.trim()).toMatch(/this\s+child/mg); expect(element.innerText.trim()).toMatch(/this\s+child/mg);
})); }));
@ -2477,7 +2469,7 @@ import {HostListener} from '../../src/metadata/directives';
}) })
class Cmp { class Cmp {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
public exp !: boolean; public exp!: boolean;
} }
TestBed.configureTestingModule({declarations: [Cmp]}); TestBed.configureTestingModule({declarations: [Cmp]});
@ -2515,19 +2507,17 @@ import {HostListener} from '../../src/metadata/directives';
it('should collect multiple root levels of :enter and :leave nodes', () => { it('should collect multiple root levels of :enter and :leave nodes', () => {
@Component({ @Component({
selector: 'ani-cmp', selector: 'ani-cmp',
animations: [ animations: [trigger(
trigger('pageAnimation', [ 'pageAnimation',
[
transition(':enter', []), transition(':enter', []),
transition('* => *', [ transition(
query(':leave', [ '* => *',
animate('1s', style({ opacity: 0 })) [
], { optional: true }), query(':leave', [animate('1s', style({opacity: 0}))], {optional: true}),
query(':enter', [ query(':enter', [animate('1s', style({opacity: 1}))], {optional: true})
animate('1s', style({ opacity: 1 }))
], { optional: true })
]) ])
]) ])],
],
template: ` template: `
<div [@pageAnimation]="status"> <div [@pageAnimation]="status">
<header> <header>
@ -2657,7 +2647,7 @@ import {HostListener} from '../../src/metadata/directives';
}) })
class Cmp { class Cmp {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
public exp !: boolean; public exp!: boolean;
public log: string[] = []; public log: string[] = [];
callback(event: any) { callback(event: any) {
this.log.push(event.element.getAttribute('data-name') + '-' + event.phaseName); this.log.push(event.element.getAttribute('data-name') + '-' + event.phaseName);
@ -2679,8 +2669,7 @@ import {HostListener} from '../../src/metadata/directives';
fixture.detectChanges(); fixture.detectChanges();
flushMicrotasks(); flushMicrotasks();
expect(cmp.log).toEqual([ expect(cmp.log).toEqual([
'c1-start', 'c1-done', 'c2-start', 'c2-done', 'p-start', 'c3-start', 'c3-done', 'c1-start', 'c1-done', 'c2-start', 'c2-done', 'p-start', 'c3-start', 'c3-done', 'p-done'
'p-done'
]); ]);
})); }));
@ -2764,7 +2753,9 @@ import {HostListener} from '../../src/metadata/directives';
public log: string[] = []; public log: string[] = [];
public remove = false; public remove = false;
track(event: any) { this.log.push(`${event.triggerName}-${event.phaseName}`); } track(event: any) {
this.log.push(`${event.triggerName}-${event.phaseName}`);
}
} }
@Component({ @Component({
@ -2790,7 +2781,9 @@ import {HostListener} from '../../src/metadata/directives';
class ChildCmp { class ChildCmp {
public exp: any; public exp: any;
public log: string[] = []; public log: string[] = [];
track(event: any) { this.log.push(`${event.triggerName}-${event.phaseName}`); } track(event: any) {
this.log.push(`${event.triggerName}-${event.phaseName}`);
}
} }
TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});
@ -2893,7 +2886,9 @@ import {HostListener} from '../../src/metadata/directives';
public child2Exp = ''; public child2Exp = '';
public log: string[] = []; public log: string[] = [];
track(event: any) { this.log.push(`${event.triggerName}-${event.phaseName}`); } track(event: any) {
this.log.push(`${event.triggerName}-${event.phaseName}`);
}
} }
TestBed.configureTestingModule({declarations: [Cmp]}); TestBed.configureTestingModule({declarations: [Cmp]});
@ -2968,8 +2963,8 @@ import {HostListener} from '../../src/metadata/directives';
engine.flush(); engine.flush();
expect(engine.players.length).toEqual(1); // child player, parent cover, parent player expect(engine.players.length).toEqual(1); // child player, parent cover, parent player
const groupPlayer = (engine.players[0] as TransitionAnimationPlayer) const groupPlayer = (engine.players[0] as TransitionAnimationPlayer).getRealPlayer() as
.getRealPlayer() as AnimationGroupPlayer; AnimationGroupPlayer;
const childPlayer = groupPlayer.players.find(player => { const childPlayer = groupPlayer.players.find(player => {
if (player instanceof MockAnimationPlayer) { if (player instanceof MockAnimationPlayer) {
return matchesElement(player.element, '.child'); return matchesElement(player.element, '.child');
@ -3309,7 +3304,7 @@ import {HostListener} from '../../src/metadata/directives';
}); });
}); });
}); });
}); });
})(); })();
function cancelAllPlayers(players: AnimationPlayer[]) { function cancelAllPlayers(players: AnimationPlayer[]) {

View File

@ -9,7 +9,7 @@
import {ResourceLoader, UrlResolver} from '@angular/compiler'; import {ResourceLoader, UrlResolver} from '@angular/compiler';
import {MockResourceLoader} from '@angular/compiler/testing'; import {MockResourceLoader} from '@angular/compiler/testing';
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, DebugElement, Directive, DoCheck, EventEmitter, HostBinding, Inject, Injectable, Input, OnChanges, OnDestroy, OnInit, Output, Pipe, PipeTransform, Provider, RendererFactory2, RendererType2, SimpleChange, SimpleChanges, TemplateRef, Type, ViewChild, ViewContainerRef, WrappedValue} from '@angular/core'; import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, DebugElement, Directive, DoCheck, EventEmitter, HostBinding, Inject, Injectable, Input, OnChanges, OnDestroy, OnInit, Output, Pipe, PipeTransform, Provider, RendererFactory2, RendererType2, SimpleChange, SimpleChanges, TemplateRef, Type, ViewChild, ViewContainerRef, WrappedValue} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync} from '@angular/core/testing'; import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by'; import {By} from '@angular/platform-browser/src/dom/debug/by';
import {isTextNode} from '@angular/platform-browser/testing/src/browser_util'; import {isTextNode} from '@angular/platform-browser/testing/src/browser_util';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
@ -26,55 +26,53 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
(function() { (function() {
let renderLog: RenderLog; let renderLog: RenderLog;
let directiveLog: DirectiveLog; let directiveLog: DirectiveLog;
function createCompFixture<T>(template: string): ComponentFixture<TestComponent>; function createCompFixture<T>(template: string): ComponentFixture<TestComponent>;
function createCompFixture<T>(template: string, compType: Type<T>): ComponentFixture<T>; function createCompFixture<T>(template: string, compType: Type<T>): ComponentFixture<T>;
function createCompFixture<T>( function createCompFixture<T>(
template: string, compType: Type<T> = <any>TestComponent): ComponentFixture<T> { template: string, compType: Type<T> = <any>TestComponent): ComponentFixture<T> {
TestBed.overrideComponent(compType, {set: new Component({template})}); TestBed.overrideComponent(compType, {set: new Component({template})});
initHelpers(); initHelpers();
return TestBed.createComponent(compType); return TestBed.createComponent(compType);
} }
function initHelpers(): void { function initHelpers(): void {
renderLog = TestBed.inject(RenderLog); renderLog = TestBed.inject(RenderLog);
directiveLog = TestBed.inject(DirectiveLog); directiveLog = TestBed.inject(DirectiveLog);
patchLoggingRenderer2(TestBed.inject(RendererFactory2), renderLog); patchLoggingRenderer2(TestBed.inject(RendererFactory2), renderLog);
} }
function queryDirs(el: DebugElement, dirType: Type<any>): any { function queryDirs(el: DebugElement, dirType: Type<any>): any {
const nodes = el.queryAllNodes(By.directive(dirType)); const nodes = el.queryAllNodes(By.directive(dirType));
return nodes.map(node => node.injector.get(dirType)); return nodes.map(node => node.injector.get(dirType));
} }
function _bindSimpleProp<T>(bindAttr: string): ComponentFixture<TestComponent>; function _bindSimpleProp<T>(bindAttr: string): ComponentFixture<TestComponent>;
function _bindSimpleProp<T>(bindAttr: string, compType: Type<T>): ComponentFixture<T>; function _bindSimpleProp<T>(bindAttr: string, compType: Type<T>): ComponentFixture<T>;
function _bindSimpleProp<T>( function _bindSimpleProp<T>(
bindAttr: string, compType: Type<T> = <any>TestComponent): ComponentFixture<T> { bindAttr: string, compType: Type<T> = <any>TestComponent): ComponentFixture<T> {
const template = `<div ${bindAttr}></div>`; const template = `<div ${bindAttr}></div>`;
return createCompFixture(template, compType); return createCompFixture(template, compType);
} }
function _bindSimpleValue(expression: any): ComponentFixture<TestComponent>; function _bindSimpleValue(expression: any): ComponentFixture<TestComponent>;
function _bindSimpleValue<T>(expression: any, compType: Type<T>): ComponentFixture<T>; function _bindSimpleValue<T>(expression: any, compType: Type<T>): ComponentFixture<T>;
function _bindSimpleValue<T>( function _bindSimpleValue<T>(
expression: any, compType: Type<T> = <any>TestComponent): ComponentFixture<T> { expression: any, compType: Type<T> = <any>TestComponent): ComponentFixture<T> {
return _bindSimpleProp(`[id]='${expression}'`, compType); return _bindSimpleProp(`[id]='${expression}'`, compType);
} }
function _bindAndCheckSimpleValue( function _bindAndCheckSimpleValue(expression: any, compType: Type<any> = TestComponent): string[] {
expression: any, compType: Type<any> = TestComponent): string[] {
const ctx = _bindSimpleValue(expression, compType); const ctx = _bindSimpleValue(expression, compType);
ctx.detectChanges(false); ctx.detectChanges(false);
return renderLog.log; return renderLog.log;
} }
describe(`ChangeDetection`, () => {
describe(`ChangeDetection`, () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureCompiler({providers: TEST_COMPILER_PROVIDERS}); TestBed.configureCompiler({providers: TEST_COMPILER_PROVIDERS});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -112,79 +110,101 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
}); });
describe('expressions', () => { describe('expressions', () => {
it('should support literals', it('should support literals', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue(10)).toEqual(['id=10']); })); expect(_bindAndCheckSimpleValue(10)).toEqual(['id=10']);
}));
it('should strip quotes from literals', it('should strip quotes from literals', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('"str"')).toEqual(['id=str']); })); expect(_bindAndCheckSimpleValue('"str"')).toEqual(['id=str']);
}));
it('should support newlines in literals', it('should support newlines in literals', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('"a\n\nb"')).toEqual(['id=a\n\nb']); })); expect(_bindAndCheckSimpleValue('"a\n\nb"')).toEqual(['id=a\n\nb']);
}));
it('should support + operations', it('should support + operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 + 2')).toEqual(['id=12']); })); expect(_bindAndCheckSimpleValue('10 + 2')).toEqual(['id=12']);
}));
it('should support - operations', it('should support - operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 - 2')).toEqual(['id=8']); })); expect(_bindAndCheckSimpleValue('10 - 2')).toEqual(['id=8']);
}));
it('should support * operations', it('should support * operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 * 2')).toEqual(['id=20']); })); expect(_bindAndCheckSimpleValue('10 * 2')).toEqual(['id=20']);
}));
it('should support / operations', fakeAsync(() => { it('should support / operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('10 / 2')).toEqual([`id=${5.0}`]); expect(_bindAndCheckSimpleValue('10 / 2')).toEqual([`id=${5.0}`]);
})); // dart exp=5.0, js exp=5 })); // dart exp=5.0, js exp=5
it('should support % operations', it('should support % operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('11 % 2')).toEqual(['id=1']); })); expect(_bindAndCheckSimpleValue('11 % 2')).toEqual(['id=1']);
}));
it('should support == operations on identical', it('should support == operations on identical', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 == 1')).toEqual(['id=true']); })); expect(_bindAndCheckSimpleValue('1 == 1')).toEqual(['id=true']);
}));
it('should support != operations', it('should support != operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 != 1')).toEqual(['id=false']); })); expect(_bindAndCheckSimpleValue('1 != 1')).toEqual(['id=false']);
}));
it('should support == operations on coerceible', it('should support == operations on coerceible', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 == true')).toEqual([`id=true`]); })); expect(_bindAndCheckSimpleValue('1 == true')).toEqual([`id=true`]);
}));
it('should support === operations on identical', it('should support === operations on identical', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 === 1')).toEqual(['id=true']); })); expect(_bindAndCheckSimpleValue('1 === 1')).toEqual(['id=true']);
}));
it('should support !== operations', it('should support !== operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 !== 1')).toEqual(['id=false']); })); expect(_bindAndCheckSimpleValue('1 !== 1')).toEqual(['id=false']);
}));
it('should support === operations on coerceible', fakeAsync(() => { it('should support === operations on coerceible', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('1 === true')).toEqual(['id=false']); expect(_bindAndCheckSimpleValue('1 === true')).toEqual(['id=false']);
})); }));
it('should support true < operations', it('should support true < operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 < 2')).toEqual(['id=true']); })); expect(_bindAndCheckSimpleValue('1 < 2')).toEqual(['id=true']);
}));
it('should support false < operations', it('should support false < operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 < 1')).toEqual(['id=false']); })); expect(_bindAndCheckSimpleValue('2 < 1')).toEqual(['id=false']);
}));
it('should support false > operations', it('should support false > operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 > 2')).toEqual(['id=false']); })); expect(_bindAndCheckSimpleValue('1 > 2')).toEqual(['id=false']);
}));
it('should support true > operations', it('should support true > operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 > 1')).toEqual(['id=true']); })); expect(_bindAndCheckSimpleValue('2 > 1')).toEqual(['id=true']);
}));
it('should support true <= operations', it('should support true <= operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 <= 2')).toEqual(['id=true']); })); expect(_bindAndCheckSimpleValue('1 <= 2')).toEqual(['id=true']);
}));
it('should support equal <= operations', it('should support equal <= operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 <= 2')).toEqual(['id=true']); })); expect(_bindAndCheckSimpleValue('2 <= 2')).toEqual(['id=true']);
}));
it('should support false <= operations', it('should support false <= operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 <= 1')).toEqual(['id=false']); })); expect(_bindAndCheckSimpleValue('2 <= 1')).toEqual(['id=false']);
}));
it('should support true >= operations', it('should support true >= operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 >= 1')).toEqual(['id=true']); })); expect(_bindAndCheckSimpleValue('2 >= 1')).toEqual(['id=true']);
}));
it('should support equal >= operations', it('should support equal >= operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 >= 2')).toEqual(['id=true']); })); expect(_bindAndCheckSimpleValue('2 >= 2')).toEqual(['id=true']);
}));
it('should support false >= operations', it('should support false >= operations', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 >= 2')).toEqual(['id=false']); })); expect(_bindAndCheckSimpleValue('1 >= 2')).toEqual(['id=false']);
}));
it('should support true && operations', fakeAsync(() => { it('should support true && operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('true && true')).toEqual(['id=true']); expect(_bindAndCheckSimpleValue('true && true')).toEqual(['id=true']);
@ -202,17 +222,21 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
expect(_bindAndCheckSimpleValue('false || false')).toEqual(['id=false']); expect(_bindAndCheckSimpleValue('false || false')).toEqual(['id=false']);
})); }));
it('should support negate', it('should support negate', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('!true')).toEqual(['id=false']); })); expect(_bindAndCheckSimpleValue('!true')).toEqual(['id=false']);
}));
it('should support double negate', it('should support double negate', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('!!true')).toEqual(['id=true']); })); expect(_bindAndCheckSimpleValue('!!true')).toEqual(['id=true']);
}));
it('should support true conditionals', it('should support true conditionals', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 < 2 ? 1 : 2')).toEqual(['id=1']); })); expect(_bindAndCheckSimpleValue('1 < 2 ? 1 : 2')).toEqual(['id=1']);
}));
it('should support false conditionals', it('should support false conditionals', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 > 2 ? 1 : 2')).toEqual(['id=2']); })); expect(_bindAndCheckSimpleValue('1 > 2 ? 1 : 2')).toEqual(['id=2']);
}));
it('should support keyed access to a list item', fakeAsync(() => { it('should support keyed access to a list item', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('["foo", "bar"][0]')).toEqual(['id=foo']); expect(_bindAndCheckSimpleValue('["foo", "bar"][0]')).toEqual(['id=foo']);
@ -245,14 +269,14 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
describe('safe navigation operator', () => { describe('safe navigation operator', () => {
it('should support reading properties of nulls', fakeAsync(() => { it('should support reading properties of nulls', fakeAsync(() => {
const ctx = _bindSimpleValue('address?.city', Person); const ctx = _bindSimpleValue('address?.city', Person);
ctx.componentInstance.address = null !; ctx.componentInstance.address = null!;
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['id=null']); expect(renderLog.log).toEqual(['id=null']);
})); }));
it('should support calling methods on nulls', fakeAsync(() => { it('should support calling methods on nulls', fakeAsync(() => {
const ctx = _bindSimpleValue('address?.toString()', Person); const ctx = _bindSimpleValue('address?.toString()', Person);
ctx.componentInstance.address = null !; ctx.componentInstance.address = null!;
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['id=null']); expect(renderLog.log).toEqual(['id=null']);
})); }));
@ -273,7 +297,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
it('should support short-circuting safe navigation', fakeAsync(() => { it('should support short-circuting safe navigation', fakeAsync(() => {
const ctx = _bindSimpleValue('value?.address.city', PersonHolder); const ctx = _bindSimpleValue('value?.address.city', PersonHolder);
ctx.componentInstance.value = null !; ctx.componentInstance.value = null!;
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['id=null']); expect(renderLog.log).toEqual(['id=null']);
})); }));
@ -301,7 +325,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
expect(() => { expect(() => {
const ctx = _bindSimpleValue('value?.address.city', PersonHolder); const ctx = _bindSimpleValue('value?.address.city', PersonHolder);
const person = new Person(); const person = new Person();
person.address = null !; person.address = null!;
ctx.componentInstance.value = person; ctx.componentInstance.value = person;
ctx.detectChanges(false); ctx.detectChanges(false);
}).toThrow(); }).toThrow();
@ -444,8 +468,9 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
expect(renderLog.log).toEqual(['id=BA']); expect(renderLog.log).toEqual(['id=BA']);
})); }));
it('should escape values in literals that indicate interpolation', it('should escape values in literals that indicate interpolation', fakeAsync(() => {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('"$"')).toEqual(['id=$']); })); expect(_bindAndCheckSimpleValue('"$"')).toEqual(['id=$']);
}));
it('should read locals', fakeAsync(() => { it('should read locals', fakeAsync(() => {
const ctx = createCompFixture( const ctx = createCompFixture(
@ -479,8 +504,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
})); }));
it('should support calling pure pipes with different number of arguments', fakeAsync(() => { it('should support calling pure pipes with different number of arguments', fakeAsync(() => {
const ctx = const ctx = _bindSimpleValue('name | multiArgPipe:"a":"b" | multiArgPipe:0:1:2', Person);
_bindSimpleValue('name | multiArgPipe:"a":"b" | multiArgPipe:0:1:2', Person);
ctx.componentInstance.name = 'value'; ctx.componentInstance.name = 'value';
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.loggedValues).toEqual(['value a b default 0 1 2']); expect(renderLog.loggedValues).toEqual(['value a b default 0 1 2']);
@ -537,7 +561,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
it('should call pure pipes only if the arguments change', fakeAsync(() => { it('should call pure pipes only if the arguments change', fakeAsync(() => {
const ctx = _bindSimpleValue('name | countingPipe', Person); const ctx = _bindSimpleValue('name | countingPipe', Person);
// change from undefined -> null // change from undefined -> null
ctx.componentInstance.name = null !; ctx.componentInstance.name = null!;
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.loggedValues).toEqual(['null state:0']); expect(renderLog.loggedValues).toEqual(['null state:0']);
ctx.detectChanges(false); ctx.detectChanges(false);
@ -553,14 +577,9 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
// change from some value -> some other value // change from some value -> some other value
ctx.componentInstance.name = 'bart'; ctx.componentInstance.name = 'bart';
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.loggedValues).toEqual([ expect(renderLog.loggedValues).toEqual(['null state:0', 'bob state:1', 'bart state:2']);
'null state:0', 'bob state:1', 'bart state:2'
]);
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.loggedValues).toEqual([ expect(renderLog.loggedValues).toEqual(['null state:0', 'bob state:1', 'bart state:2']);
'null state:0', 'bob state:1', 'bart state:2'
]);
})); }));
modifiedInIvy('Pure pipes are instantiated differently in view engine and ivy') modifiedInIvy('Pure pipes are instantiated differently in view engine and ivy')
@ -662,7 +681,9 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
})); }));
it('should throw when trying to assign to a local', fakeAsync(() => { it('should throw when trying to assign to a local', fakeAsync(() => {
expect(() => { _bindSimpleProp('(event)="$event=1"'); }) expect(() => {
_bindSimpleProp('(event)="$event=1"');
})
.toThrowError(new RegExp( .toThrowError(new RegExp(
'Cannot assign value (.*) to template variable (.*). Template variables are read-only.')); 'Cannot assign value (.*) to template variable (.*). Template variables are read-only.'));
})); }));
@ -675,7 +696,6 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
expect(ctx.componentInstance.a).toEqual(1); expect(ctx.componentInstance.a).toEqual(1);
})); }));
}); });
}); });
describe('RendererFactory', () => { describe('RendererFactory', () => {
@ -685,6 +705,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
const rf = TestBed.inject(RendererFactory2); const rf = TestBed.inject(RendererFactory2);
// TODO: @JiaLiPassion, need to wait @types/jasmine to fix the // TODO: @JiaLiPassion, need to wait @types/jasmine to fix the
// optional method infer issue. // optional method infer issue.
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
spyOn(rf as any, 'begin'); spyOn(rf as any, 'begin');
spyOn(rf as any, 'end'); spyOn(rf as any, 'end');
expect(rf.begin).not.toHaveBeenCalled(); expect(rf.begin).not.toHaveBeenCalled();
@ -824,8 +845,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
]); ]);
})); }));
it('should be called on every detectChanges run, except for checkNoChanges', it('should be called on every detectChanges run, except for checkNoChanges', fakeAsync(() => {
fakeAsync(() => {
const ctx = createCompFixture('<div testDirective="dir"></div>'); const ctx = createCompFixture('<div testDirective="dir"></div>');
ctx.detectChanges(false); ctx.detectChanges(false);
@ -864,9 +884,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
ctx.detectChanges(false); ctx.detectChanges(false);
expect(directiveLog.filter(['ngAfterContentInit'])).toEqual([ expect(directiveLog.filter(['ngAfterContentInit'])).toEqual(['dir.ngAfterContentInit']);
'dir.ngAfterContentInit'
]);
// reset directives // reset directives
directiveLog.clear(); directiveLog.clear();
@ -895,9 +913,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
} }
expect(errored).toBe(true); expect(errored).toBe(true);
expect(directiveLog.filter(['ngAfterContentInit'])).toEqual([ expect(directiveLog.filter(['ngAfterContentInit'])).toEqual(['dir.ngAfterContentInit']);
'dir.ngAfterContentInit'
]);
directiveLog.clear(); directiveLog.clear();
// Second change detection also fails, but this time ngAfterContentInit should not be // Second change detection also fails, but this time ngAfterContentInit should not be
@ -925,8 +941,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
]); ]);
})); }));
it('should be called on every detectChanges run, except for checkNoChanges', it('should be called on every detectChanges run, except for checkNoChanges', fakeAsync(() => {
fakeAsync(() => {
const ctx = createCompFixture('<div testDirective="dir"></div>'); const ctx = createCompFixture('<div testDirective="dir"></div>');
ctx.detectChanges(false); ctx.detectChanges(false);
@ -1038,15 +1053,12 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
]); ]);
})); }));
it('should be called on every detectChanges run, except for checkNoChanges', it('should be called on every detectChanges run, except for checkNoChanges', fakeAsync(() => {
fakeAsync(() => {
const ctx = createCompFixture('<div testDirective="dir"></div>'); const ctx = createCompFixture('<div testDirective="dir"></div>');
ctx.detectChanges(false); ctx.detectChanges(false);
expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual([ expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual(['dir.ngAfterViewChecked']);
'dir.ngAfterViewChecked'
]);
// reset directives // reset directives
directiveLog.clear(); directiveLog.clear();
@ -1059,9 +1071,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
// re-verify that changes are still detected // re-verify that changes are still detected
ctx.detectChanges(false); ctx.detectChanges(false);
expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual([ expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual(['dir.ngAfterViewChecked']);
'dir.ngAfterViewChecked'
]);
})); }));
it('should be called in reverse order so the child is always notified before the parent', it('should be called in reverse order so the child is always notified before the parent',
@ -1136,14 +1146,11 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
ctx.detectChanges(false); ctx.detectChanges(false);
ctx.destroy(); ctx.destroy();
expect(directiveLog.filter(['ngOnDestroy'])).toEqual([ expect(directiveLog.filter(['ngOnDestroy'])).toEqual(['pipeWithOnDestroy.ngOnDestroy']);
'pipeWithOnDestroy.ngOnDestroy'
]);
})); }));
it('should call ngOnDestroy on an injectable class', fakeAsync(() => { it('should call ngOnDestroy on an injectable class', fakeAsync(() => {
TestBed.overrideDirective( TestBed.overrideDirective(TestDirective, {set: {providers: [InjectableWithLifecycle]}});
TestDirective, {set: {providers: [InjectableWithLifecycle]}});
const ctx = createCompFixture('<div testDirective="dir"></div>', TestComponent); const ctx = createCompFixture('<div testDirective="dir"></div>', TestComponent);
@ -1302,7 +1309,6 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
ctx.detectChanges(); ctx.detectChanges();
expect(renderLog.log).toEqual(['{{hello}}']); expect(renderLog.log).toEqual(['{{hello}}']);
})); }));
it('Reattaches in the original cd mode', fakeAsync(() => { it('Reattaches in the original cd mode', fakeAsync(() => {
@ -1321,14 +1327,13 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
ctx.detectChanges(); ctx.detectChanges();
expect(cmp.renderCount).toBe(count); expect(cmp.renderCount).toBe(count);
})); }));
}); });
describe('multi directive order', () => { describe('multi directive order', () => {
modifiedInIvy('order of bindings to directive inputs is different in ivy') modifiedInIvy('order of bindings to directive inputs is different in ivy')
.it('should follow the DI order for the same element', fakeAsync(() => { .it('should follow the DI order for the same element', fakeAsync(() => {
const ctx = createCompFixture( const ctx =
'<div orderCheck2="2" orderCheck0="0" orderCheck1="1"></div>'); createCompFixture('<div orderCheck2="2" orderCheck0="0" orderCheck1="1"></div>');
ctx.detectChanges(false); ctx.detectChanges(false);
ctx.destroy(); ctx.destroy();
@ -1358,7 +1363,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
class Comp { class Comp {
name = 'Tom'; name = 'Tom';
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@ViewChild('vc', {read: ViewContainerRef, static: true}) vc !: ViewContainerRef; @ViewChild('vc', {read: ViewContainerRef, static: true}) vc!: ViewContainerRef;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@ViewChild(TemplateRef, {static: true}) template !: TemplateRef<any>; @ViewChild(TemplateRef, {static: true}) template !: TemplateRef<any>;
} }
@ -1380,8 +1385,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
@Directive({selector: '[i]'}) @Directive({selector: '[i]'})
class DummyDirective { class DummyDirective {
@Input() @Input() i: any;
i: any;
} }
@Component({ @Component({
@ -1391,7 +1395,9 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
}) })
class MainComp { class MainComp {
constructor(public cdRef: ChangeDetectorRef) {} constructor(public cdRef: ChangeDetectorRef) {}
log(id: string) { log.push(`main-${id}`); } log(id: string) {
log.push(`main-${id}`);
}
} }
@Component({ @Component({
@ -1401,11 +1407,12 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
}) })
class OuterComp { class OuterComp {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@ContentChild(TemplateRef, {static: true}) @ContentChild(TemplateRef, {static: true}) tpl!: TemplateRef<any>;
tpl !: TemplateRef<any>;
constructor(public cdRef: ChangeDetectorRef) {} constructor(public cdRef: ChangeDetectorRef) {}
log(id: string) { log.push(`outer-${id}`); } log(id: string) {
log.push(`outer-${id}`);
}
} }
@Component({ @Component({
@ -1415,15 +1422,15 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
}) })
class InnerComp { class InnerComp {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@ContentChild(TemplateRef, {static: true}) @ContentChild(TemplateRef, {static: true}) tpl!: TemplateRef<any>;
tpl !: TemplateRef<any>;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input() @Input() outerTpl!: TemplateRef<any>;
outerTpl !: TemplateRef<any>;
constructor(public cdRef: ChangeDetectorRef) {} constructor(public cdRef: ChangeDetectorRef) {}
log(id: string) { log.push(`inner-${id}`); } log(id: string) {
log.push(`inner-${id}`);
}
} }
let ctx: ComponentFixture<MainComp>; let ctx: ComponentFixture<MainComp>;
@ -1444,13 +1451,11 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
it('should dirty check projected views in regular order', () => { it('should dirty check projected views in regular order', () => {
ctx.detectChanges(false); ctx.detectChanges(false);
expect(log).toEqual( expect(log).toEqual(['main-start', 'outer-start', 'inner-start', 'main-tpl', 'outer-tpl']);
['main-start', 'outer-start', 'inner-start', 'main-tpl', 'outer-tpl']);
log = []; log = [];
ctx.detectChanges(false); ctx.detectChanges(false);
expect(log).toEqual( expect(log).toEqual(['main-start', 'outer-start', 'inner-start', 'main-tpl', 'outer-tpl']);
['main-start', 'outer-start', 'inner-start', 'main-tpl', 'outer-tpl']);
}); });
it('should not dirty check projected views if neither the declaration nor the insertion place is dirty checked', it('should not dirty check projected views if neither the declaration nor the insertion place is dirty checked',
@ -1526,8 +1531,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
@Directive({selector: '[someDir]'}) @Directive({selector: '[someDir]'})
class SomeDir { class SomeDir {
@HostBinding('class.foo') @HostBinding('class.foo') fooClass = true;
fooClass = true;
} }
const ctx = const ctx =
@ -1544,8 +1548,12 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
describe('lifecycle asserts', () => { describe('lifecycle asserts', () => {
let logged: string[]; let logged: string[];
function log(value: string) { logged.push(value); } function log(value: string) {
function clearLog() { logged = []; } logged.push(value);
}
function clearLog() {
logged = [];
}
function expectOnceAndOnlyOnce(log: string) { function expectOnceAndOnlyOnce(log: string) {
expect(logged.indexOf(log) >= 0) expect(logged.indexOf(log) >= 0)
@ -1554,7 +1562,9 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
.toBeTruthy(`'${log}' logged more than once. Log was ${JSON.stringify(logged)}`); .toBeTruthy(`'${log}' logged more than once. Log was ${JSON.stringify(logged)}`);
} }
beforeEach(() => { clearLog(); }); beforeEach(() => {
clearLog();
});
enum LifetimeMethods { enum LifetimeMethods {
None = 0, None = 0,
@ -1588,16 +1598,26 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
private thrown = LifetimeMethods.None; private thrown = LifetimeMethods.None;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input() inp !: boolean; @Input() inp!: boolean;
@Output() outp = new EventEmitter<any>(); @Output() outp = new EventEmitter<any>();
constructor() {} constructor() {}
ngDoCheck() { this.check(LifetimeMethods.ngDoCheck); } ngDoCheck() {
ngOnInit() { this.check(LifetimeMethods.ngOnInit); } this.check(LifetimeMethods.ngDoCheck);
ngOnChanges() { this.check(LifetimeMethods.ngOnChanges); } }
ngAfterViewInit() { this.check(LifetimeMethods.ngAfterViewInit); } ngOnInit() {
ngAfterContentInit() { this.check(LifetimeMethods.ngAfterContentInit); } this.check(LifetimeMethods.ngOnInit);
}
ngOnChanges() {
this.check(LifetimeMethods.ngOnChanges);
}
ngAfterViewInit() {
this.check(LifetimeMethods.ngAfterViewInit);
}
ngAfterContentInit() {
this.check(LifetimeMethods.ngAfterContentInit);
}
private check(method: LifetimeMethods) { private check(method: LifetimeMethods) {
log(`MyChild::${LifetimeMethods[method]}()`); log(`MyChild::${LifetimeMethods[method]}()`);
@ -1625,10 +1645,18 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
}) })
class MyComponent { class MyComponent {
constructor(private changeDetectionRef: ChangeDetectorRef) {} constructor(private changeDetectionRef: ChangeDetectorRef) {}
ngDoCheck() { this.check(LifetimeMethods.ngDoCheck); } ngDoCheck() {
ngOnInit() { this.check(LifetimeMethods.ngOnInit); } this.check(LifetimeMethods.ngDoCheck);
ngAfterViewInit() { this.check(LifetimeMethods.ngAfterViewInit); } }
ngAfterContentInit() { this.check(LifetimeMethods.ngAfterContentInit); } ngOnInit() {
this.check(LifetimeMethods.ngOnInit);
}
ngAfterViewInit() {
this.check(LifetimeMethods.ngAfterViewInit);
}
ngAfterContentInit() {
this.check(LifetimeMethods.ngAfterContentInit);
}
onOutp() { onOutp() {
log('<RECURSION START>'); log('<RECURSION START>');
this.changeDetectionRef.detectChanges(); this.changeDetectionRef.detectChanges();
@ -1670,14 +1698,16 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
} }
forEachMethod(LifetimeMethods.InitMethodsAndChanges, method => { forEachMethod(LifetimeMethods.InitMethodsAndChanges, method => {
it(`should ensure that init hooks are called once an only once with recursion in ${LifetimeMethods[method]} `, it(`should ensure that init hooks are called once an only once with recursion in ${
LifetimeMethods[method]} `,
() => { () => {
// Ensure all the init methods are called once. // Ensure all the init methods are called once.
ensureOneInit({childRecursion: method, childThrows: LifetimeMethods.None}); ensureOneInit({childRecursion: method, childThrows: LifetimeMethods.None});
}); });
}); });
forEachMethod(LifetimeMethods.All, method => { forEachMethod(LifetimeMethods.All, method => {
it(`should ensure that init hooks are called once an only once with a throw in ${LifetimeMethods[method]} `, it(`should ensure that init hooks are called once an only once with a throw in ${
LifetimeMethods[method]} `,
() => { () => {
// Ensure all the init methods are called once. // Ensure all the init methods are called once.
// the first cycle throws but the next cycle should complete the inits. // the first cycle throws but the next cycle should complete the inits.
@ -1686,7 +1716,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
}); });
}); });
}); });
}); });
})(); })();
@Injectable() @Injectable()
@ -1750,7 +1780,9 @@ class DirectiveLog {
this.entries.push(new DirectiveLogEntry(directiveName, method)); this.entries.push(new DirectiveLogEntry(directiveName, method));
} }
clear() { this.entries = []; } clear() {
this.entries = [];
}
filter(methods: string[]): string[] { filter(methods: string[]): string[] {
return this.entries.filter((entry) => methods.indexOf(entry.method) !== -1) return this.entries.filter((entry) => methods.indexOf(entry.method) !== -1)
@ -1762,32 +1794,44 @@ class DirectiveLog {
@Pipe({name: 'countingPipe'}) @Pipe({name: 'countingPipe'})
class CountingPipe implements PipeTransform { class CountingPipe implements PipeTransform {
state: number = 0; state: number = 0;
transform(value: any) { return `${value} state:${this.state++}`; } transform(value: any) {
return `${value} state:${this.state++}`;
}
} }
@Pipe({name: 'countingImpurePipe', pure: false}) @Pipe({name: 'countingImpurePipe', pure: false})
class CountingImpurePipe implements PipeTransform { class CountingImpurePipe implements PipeTransform {
state: number = 0; state: number = 0;
transform(value: any) { return `${value} state:${this.state++}`; } transform(value: any) {
return `${value} state:${this.state++}`;
}
} }
@Pipe({name: 'pipeWithOnDestroy'}) @Pipe({name: 'pipeWithOnDestroy'})
class PipeWithOnDestroy implements PipeTransform, OnDestroy { class PipeWithOnDestroy implements PipeTransform, OnDestroy {
constructor(private directiveLog: DirectiveLog) {} constructor(private directiveLog: DirectiveLog) {}
ngOnDestroy() { this.directiveLog.add('pipeWithOnDestroy', 'ngOnDestroy'); } ngOnDestroy() {
this.directiveLog.add('pipeWithOnDestroy', 'ngOnDestroy');
}
transform(value: any): any { return null; } transform(value: any): any {
return null;
}
} }
@Pipe({name: 'identityPipe'}) @Pipe({name: 'identityPipe'})
class IdentityPipe implements PipeTransform { class IdentityPipe implements PipeTransform {
transform(value: any) { return value; } transform(value: any) {
return value;
}
} }
@Pipe({name: 'wrappedPipe'}) @Pipe({name: 'wrappedPipe'})
class WrappedPipe implements PipeTransform { class WrappedPipe implements PipeTransform {
transform(value: any) { return WrappedValue.wrap(value); } transform(value: any) {
return WrappedValue.wrap(value);
}
} }
@Pipe({name: 'multiArgPipe'}) @Pipe({name: 'multiArgPipe'})
@ -1856,7 +1900,9 @@ class Gh9882 implements AfterContentInit {
constructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef<Object>) { constructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef<Object>) {
} }
ngAfterContentInit(): any { this._viewContainer.createEmbeddedView(this._templateRef); } ngAfterContentInit(): any {
this._viewContainer.createEmbeddedView(this._templateRef);
}
} }
@Directive({selector: '[testDirective]', exportAs: 'testDirective'}) @Directive({selector: '[testDirective]', exportAs: 'testDirective'})
@ -1865,21 +1911,25 @@ class TestDirective implements OnInit, DoCheck, OnChanges, AfterContentInit, Aft
@Input() a: any; @Input() a: any;
@Input() b: any; @Input() b: any;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
changes !: SimpleChanges; changes!: SimpleChanges;
event: any; event: any;
eventEmitter: EventEmitter<string> = new EventEmitter<string>(); eventEmitter: EventEmitter<string> = new EventEmitter<string>();
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input('testDirective') name !: string; @Input('testDirective') name!: string;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input() throwOn !: string; @Input() throwOn!: string;
constructor(public log: DirectiveLog) {} constructor(public log: DirectiveLog) {}
onEvent(event: any) { this.event = event; } onEvent(event: any) {
this.event = event;
}
ngDoCheck() { this.log.add(this.name, 'ngDoCheck'); } ngDoCheck() {
this.log.add(this.name, 'ngDoCheck');
}
ngOnInit() { ngOnInit() {
this.log.add(this.name, 'ngOnInit'); this.log.add(this.name, 'ngOnInit');
@ -1937,20 +1987,24 @@ class InjectableWithLifecycle {
name = 'injectable'; name = 'injectable';
constructor(public log: DirectiveLog) {} constructor(public log: DirectiveLog) {}
ngOnDestroy() { this.log.add(this.name, 'ngOnDestroy'); } ngOnDestroy() {
this.log.add(this.name, 'ngOnDestroy');
}
} }
@Directive({selector: '[onDestroyDirective]'}) @Directive({selector: '[onDestroyDirective]'})
class OnDestroyDirective implements OnDestroy { class OnDestroyDirective implements OnDestroy {
@Output('destroy') emitter = new EventEmitter<string>(false); @Output('destroy') emitter = new EventEmitter<string>(false);
ngOnDestroy() { this.emitter.emit('destroyed'); } ngOnDestroy() {
this.emitter.emit('destroyed');
}
} }
@Directive({selector: '[orderCheck0]'}) @Directive({selector: '[orderCheck0]'})
class OrderCheckDirective0 { class OrderCheckDirective0 {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
private _name !: string; private _name!: string;
@Input('orderCheck0') @Input('orderCheck0')
set name(value: string) { set name(value: string) {
@ -1964,7 +2018,7 @@ class OrderCheckDirective0 {
@Directive({selector: '[orderCheck1]'}) @Directive({selector: '[orderCheck1]'})
class OrderCheckDirective1 { class OrderCheckDirective1 {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
private _name !: string; private _name!: string;
@Input('orderCheck1') @Input('orderCheck1')
set name(value: string) { set name(value: string) {
@ -1978,7 +2032,7 @@ class OrderCheckDirective1 {
@Directive({selector: '[orderCheck2]'}) @Directive({selector: '[orderCheck2]'})
class OrderCheckDirective2 { class OrderCheckDirective2 {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
private _name !: string; private _name!: string;
@Input('orderCheck2') @Input('orderCheck2')
set name(value: string) { set name(value: string) {
@ -2003,21 +2057,25 @@ class TestLocals {
@Component({selector: 'root', template: 'empty'}) @Component({selector: 'root', template: 'empty'})
class Person { class Person {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
age !: number; age!: number;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
name !: string; name!: string;
address: Address|null = null; address: Address|null = null;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
phones !: number[]; phones!: number[];
init(name: string, address: Address|null = null) { init(name: string, address: Address|null = null) {
this.name = name; this.name = name;
this.address = address; this.address = address;
} }
sayHi(m: any): string { return `Hi, ${m}`; } sayHi(m: any): string {
return `Hi, ${m}`;
}
passThrough(val: any): any { return val; } passThrough(val: any): any {
return val;
}
toString(): string { toString(): string {
const address = this.address == null ? '' : ' address=' + this.address.toString(); const address = this.address == null ? '' : ' address=' + this.address.toString();
@ -2042,11 +2100,17 @@ class Address {
return this._zipcode; return this._zipcode;
} }
set city(v) { this._city = v; } set city(v) {
this._city = v;
}
set zipcode(v) { this._zipcode = v; } set zipcode(v) {
this._zipcode = v;
}
toString(): string { return this.city || '-'; } toString(): string {
return this.city || '-';
}
} }
@Component({selector: 'root', template: 'empty'}) @Component({selector: 'root', template: 'empty'})
@ -2063,14 +2127,16 @@ class TestData {
@Component({selector: 'root', template: 'empty'}) @Component({selector: 'root', template: 'empty'})
class TestDataWithGetter { class TestDataWithGetter {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
public fn !: Function; public fn!: Function;
get a() { return this.fn(); } get a() {
return this.fn();
}
} }
class Holder<T> { class Holder<T> {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
value !: T; value!: T;
} }
@Component({selector: 'root', template: 'empty'}) @Component({selector: 'root', template: 'empty'})

View File

@ -25,26 +25,41 @@ describe('global utils', () => {
describe('publishDefaultGlobalUtils', () => { describe('publishDefaultGlobalUtils', () => {
beforeEach(() => publishDefaultGlobalUtils()); beforeEach(() => publishDefaultGlobalUtils());
it('should publish getComponent', () => { assertPublished('getComponent', getComponent); }); it('should publish getComponent', () => {
assertPublished('getComponent', getComponent);
});
it('should publish getContext', () => { assertPublished('getContext', getContext); }); it('should publish getContext', () => {
assertPublished('getContext', getContext);
});
it('should publish getListeners', () => { assertPublished('getListeners', getListeners); }); it('should publish getListeners', () => {
assertPublished('getListeners', getListeners);
});
it('should publish getOwningComponent', it('should publish getOwningComponent', () => {
() => { assertPublished('getOwningComponent', getOwningComponent); }); assertPublished('getOwningComponent', getOwningComponent);
});
it('should publish getRootComponents', it('should publish getRootComponents', () => {
() => { assertPublished('getRootComponents', getRootComponents); }); assertPublished('getRootComponents', getRootComponents);
});
it('should publish getDirectives', () => { assertPublished('getDirectives', getDirectives); }); it('should publish getDirectives', () => {
assertPublished('getDirectives', getDirectives);
});
it('should publish getHostComponent', it('should publish getHostComponent', () => {
() => { assertPublished('getHostElement', getHostElement); }); assertPublished('getHostElement', getHostElement);
});
it('should publish getInjector', () => { assertPublished('getInjector', getInjector); }); it('should publish getInjector', () => {
assertPublished('getInjector', getInjector);
});
it('should publish applyChanges', () => { assertPublished('applyChanges', applyChanges); }); it('should publish applyChanges', () => {
assertPublished('applyChanges', applyChanges);
});
}); });
}); });

View File

@ -12,24 +12,21 @@ import {AttributeMarker, TAttributes, TNode, TNodeType} from '../../src/render3/
import {CssSelector, CssSelectorList, SelectorFlags} from '../../src/render3/interfaces/projection'; import {CssSelector, CssSelectorList, SelectorFlags} from '../../src/render3/interfaces/projection';
import {extractAttrsAndClassesFromSelector, getProjectAsAttrValue, isNodeMatchingSelector, isNodeMatchingSelectorList, stringifyCSSSelectorList} from '../../src/render3/node_selector_matcher'; import {extractAttrsAndClassesFromSelector, getProjectAsAttrValue, isNodeMatchingSelector, isNodeMatchingSelectorList, stringifyCSSSelectorList} from '../../src/render3/node_selector_matcher';
function testLStaticData(tagName: string, attrs: TAttributes | null): TNode { function testLStaticData(tagName: string, attrs: TAttributes|null): TNode {
return createTNode(null !, null, TNodeType.Element, 0, tagName, attrs); return createTNode(null!, null, TNodeType.Element, 0, tagName, attrs);
} }
describe('css selector matching', () => { describe('css selector matching', () => {
function isMatching( function isMatching(
tagName: string, attrsOrTNode: TAttributes | TNode | null, selector: CssSelector): boolean { tagName: string, attrsOrTNode: TAttributes|TNode|null, selector: CssSelector): boolean {
const tNode = (!attrsOrTNode || Array.isArray(attrsOrTNode)) ? const tNode = (!attrsOrTNode || Array.isArray(attrsOrTNode)) ?
createTNode(null !, null, TNodeType.Element, 0, tagName, attrsOrTNode as TAttributes) : createTNode(null!, null, TNodeType.Element, 0, tagName, attrsOrTNode as TAttributes) :
(attrsOrTNode as TNode); (attrsOrTNode as TNode);
return isNodeMatchingSelector(tNode, selector, true); return isNodeMatchingSelector(tNode, selector, true);
} }
describe('isNodeMatchingSimpleSelector', () => { describe('isNodeMatchingSimpleSelector', () => {
describe('element matching', () => { describe('element matching', () => {
it('should match element name only if names are the same', () => { it('should match element name only if names are the same', () => {
expect(isMatching('span', null, ['span'])) expect(isMatching('span', null, ['span']))
.toBeTruthy(`Selector 'span' should match <span>`); .toBeTruthy(`Selector 'span' should match <span>`);
@ -55,11 +52,9 @@ describe('css selector matching', () => {
}); });
describe('attributes matching', () => { describe('attributes matching', () => {
// TODO: do we need to differentiate no value and empty value? that is: title vs. title="" ? // TODO: do we need to differentiate no value and empty value? that is: title vs. title="" ?
it('should match single attribute without value', () => { it('should match single attribute without value', () => {
expect(isMatching('span', ['title', ''], [ expect(isMatching('span', ['title', ''], [
'', 'title', '' '', 'title', ''
])).toBeTruthy(`Selector '[title]' should match <span title>`); ])).toBeTruthy(`Selector '[title]' should match <span title>`);
@ -81,7 +76,8 @@ describe('css selector matching', () => {
])).toBeFalsy(`Selector '[other]' should NOT match <span title="">'`); ])).toBeFalsy(`Selector '[other]' should NOT match <span title="">'`);
}); });
// TODO: Not sure how to fix this cases. // TODO: this case will not work, need more discussion
// https://github.com/angular/angular/pull/34625#discussion_r401791275
xit('should match namespaced attributes', () => { xit('should match namespaced attributes', () => {
expect(isMatching( expect(isMatching(
'span', [AttributeMarker.NamespaceURI, 'http://some/uri', 'title', 'name'], 'span', [AttributeMarker.NamespaceURI, 'http://some/uri', 'title', 'name'],
@ -228,7 +224,6 @@ describe('css selector matching', () => {
}); });
describe('class matching', () => { describe('class matching', () => {
it('should match with a class selector when an element has multiple classes', () => { it('should match with a class selector when an element has multiple classes', () => {
expect(isMatching('span', ['class', 'foo bar'], [ expect(isMatching('span', ['class', 'foo bar'], [
'', SelectorFlags.CLASS, 'foo' '', SelectorFlags.CLASS, 'foo'
@ -328,7 +323,6 @@ describe('css selector matching', () => {
}); });
describe('negations', () => { describe('negations', () => {
it('should match when negation part is null', () => { it('should match when negation part is null', () => {
expect(isMatching('span', null, ['span'])).toBeTruthy(`Selector 'span' should match <span>`); expect(isMatching('span', null, ['span'])).toBeTruthy(`Selector 'span' should match <span>`);
}); });
@ -436,13 +430,11 @@ describe('css selector matching', () => {
expect(isMatching('div', ['name', 'name', 'title', '', 'class', 'foo bar'], selector)) expect(isMatching('div', ['name', 'name', 'title', '', 'class', 'foo bar'], selector))
.toBeFalsy(); .toBeFalsy();
}); });
}); });
describe('isNodeMatchingSelectorList', () => { describe('isNodeMatchingSelectorList', () => {
function isAnyMatching( function isAnyMatching(
tagName: string, attrs: string[] | null, selector: CssSelectorList): boolean { tagName: string, attrs: string[]|null, selector: CssSelectorList): boolean {
return isNodeMatchingSelectorList(testLStaticData(tagName, attrs), selector, false); return isNodeMatchingSelectorList(testLStaticData(tagName, attrs), selector, false);
} }
@ -468,16 +460,18 @@ describe('css selector matching', () => {
}); });
describe('reading the ngProjectAs attribute value', function() { describe('reading the ngProjectAs attribute value', function() {
function testTNode(attrs: TAttributes|null) {
function testTNode(attrs: TAttributes | null) { return testLStaticData('tag', attrs); } return testLStaticData('tag', attrs);
}
it('should get ngProjectAs value if present', function() { it('should get ngProjectAs value if present', function() {
expect(getProjectAsAttrValue(testTNode([AttributeMarker.ProjectAs, ['tag', 'foo', 'bar']]))) expect(getProjectAsAttrValue(testTNode([AttributeMarker.ProjectAs, ['tag', 'foo', 'bar']])))
.toEqual(['tag', 'foo', 'bar']); .toEqual(['tag', 'foo', 'bar']);
}); });
it('should return null if there are no attributes', it('should return null if there are no attributes', function() {
function() { expect(getProjectAsAttrValue(testTNode(null))).toBe(null); }); expect(getProjectAsAttrValue(testTNode(null))).toBe(null);
});
it('should return if ngProjectAs is not present', function() { it('should return if ngProjectAs is not present', function() {
expect(getProjectAsAttrValue(testTNode(['foo', 'bar']))).toBe(null); expect(getProjectAsAttrValue(testTNode(['foo', 'bar']))).toBe(null);
@ -486,15 +480,13 @@ describe('css selector matching', () => {
it('should not accidentally identify ngProjectAs in attribute values', function() { it('should not accidentally identify ngProjectAs in attribute values', function() {
expect(getProjectAsAttrValue(testTNode(['foo', AttributeMarker.ProjectAs]))).toBe(null); expect(getProjectAsAttrValue(testTNode(['foo', AttributeMarker.ProjectAs]))).toBe(null);
}); });
}); });
}); });
describe('stringifyCSSSelectorList', () => { describe('stringifyCSSSelectorList', () => {
it('should stringify selector with a tag name only', () => {
it('should stringify selector with a tag name only', expect(stringifyCSSSelectorList([['button']])).toBe('button');
() => { expect(stringifyCSSSelectorList([['button']])).toBe('button'); }); });
it('should stringify selector with attributes', () => { it('should stringify selector with attributes', () => {
expect(stringifyCSSSelectorList([['', 'id', '']])).toBe('[id]'); expect(stringifyCSSSelectorList([['', 'id', '']])).toBe('[id]');

View File

@ -17,6 +17,7 @@ describe('utils', () => {
beforeEach(() => { beforeEach(() => {
// TODO: @JiaLiPassion, need to wait @types/jasmine to fix the wrong return // TODO: @JiaLiPassion, need to wait @types/jasmine to fix the wrong return
// type infer issue. // type infer issue.
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
setTimeoutSpy = spyOn(window, 'setTimeout').and.returnValue(42 as any); setTimeoutSpy = spyOn(window, 'setTimeout').and.returnValue(42 as any);
clearTimeoutSpy = spyOn(window, 'clearTimeout'); clearTimeoutSpy = spyOn(window, 'clearTimeout');
}); });
@ -83,8 +84,9 @@ describe('utils', () => {
expect(camelToDashCase('foo1Bar2Baz3Qux4')).toBe('foo1-bar2-baz3-qux4'); expect(camelToDashCase('foo1Bar2Baz3Qux4')).toBe('foo1-bar2-baz3-qux4');
}); });
it('should keep existing dashes', it('should keep existing dashes', () => {
() => { expect(camelToDashCase('fooBar-baz-Qux')).toBe('foo-bar-baz--qux'); }); expect(camelToDashCase('fooBar-baz-Qux')).toBe('foo-bar-baz--qux');
});
}); });
describe('createCustomEvent()', () => { describe('createCustomEvent()', () => {
@ -99,7 +101,6 @@ describe('utils', () => {
expect(event.cancelable).toBe(false); expect(event.cancelable).toBe(false);
expect(event.detail).toEqual(value); expect(event.detail).toEqual(value);
}); });
}); });
describe('isElement()', () => { describe('isElement()', () => {
@ -131,7 +132,7 @@ describe('utils', () => {
it('should return true for functions', () => { it('should return true for functions', () => {
const obj = {foo: function() {}, bar: () => null, baz() {}}; const obj = {foo: function() {}, bar: () => null, baz() {}};
const fns = [ const fns = [
function(){}, function() {},
() => null, () => null,
obj.foo, obj.foo,
obj.bar, obj.bar,
@ -182,7 +183,7 @@ describe('utils', () => {
</ul> </ul>
</div> </div>
`; `;
li = div.querySelector('li') !; li = div.querySelector('li')!;
}); });
it('should return whether the element matches the selector', () => { it('should return whether the element matches the selector', () => {
@ -218,7 +219,9 @@ describe('utils', () => {
]; ];
values.forEach((v1, i) => { values.forEach((v1, i) => {
values.forEach((v2, j) => { expect(strictEquals(v1, v2)).toBe(i === j); }); values.forEach((v2, j) => {
expect(strictEquals(v1, v2)).toBe(i === j);
});
}); });
}); });

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Inject, ReflectiveInjector, forwardRef, resolveForwardRef} from '@angular/core'; import {forwardRef, Inject, ReflectiveInjector, resolveForwardRef} from '@angular/core';
{ {
describe('forwardRef examples', () => { describe('forwardRef examples', () => {
@ -26,7 +26,9 @@ import {Inject, ReflectiveInjector, forwardRef, resolveForwardRef} from '@angula
// Door attempts to inject Lock, despite it not being defined yet. // Door attempts to inject Lock, despite it not being defined yet.
// forwardRef makes this possible. // forwardRef makes this possible.
constructor(@Inject(forwardRef(() => Lock)) lock: Lock) { this.lock = lock; } constructor(@Inject(forwardRef(() => Lock)) lock: Lock) {
this.lock = lock;
}
} }
// Only at this point Lock is defined. // Only at this point Lock is defined.

View File

@ -17,7 +17,9 @@ import {HammerGestureConfig, HammerGesturesPlugin,} from '@angular/platform-brow
let fakeConsole: any; let fakeConsole: any;
if (isNode) return; if (isNode) return;
beforeEach(() => { fakeConsole = {warn: jasmine.createSpy('console.warn')}; }); beforeEach(() => {
fakeConsole = {warn: jasmine.createSpy('console.warn')};
});
describe('with no custom loader', () => { describe('with no custom loader', () => {
beforeEach(() => { beforeEach(() => {
@ -61,7 +63,9 @@ import {HammerGestureConfig, HammerGesturesPlugin,} from '@angular/platform-brow
// Inject the NgZone so that we can make it available to the plugin through a fake // Inject the NgZone so that we can make it available to the plugin through a fake
// EventManager. // EventManager.
let ngZone: NgZone; let ngZone: NgZone;
beforeEach(inject([NgZone], (z: NgZone) => { ngZone = z; })); beforeEach(inject([NgZone], (z: NgZone) => {
ngZone = z;
}));
beforeEach(() => { beforeEach(() => {
originalHammerGlobal = (window as any).Hammer; originalHammerGlobal = (window as any).Hammer;
@ -84,13 +88,15 @@ import {HammerGestureConfig, HammerGesturesPlugin,} from '@angular/platform-brow
plugin = new HammerGesturesPlugin(document, hammerConfig, fakeConsole, loader); plugin = new HammerGesturesPlugin(document, hammerConfig, fakeConsole, loader);
// Use a fake EventManager that has access to the NgZone. // Use a fake EventManager that has access to the NgZone.
plugin.manager = { getZone: () => ngZone } as EventManager; plugin.manager = {getZone: () => ngZone} as EventManager;
someElement = document.createElement('div'); someElement = document.createElement('div');
someListener = () => {}; someListener = () => {};
}); });
afterEach(() => { (window as any).Hammer = originalHammerGlobal; }); afterEach(() => {
(window as any).Hammer = originalHammerGlobal;
});
it('should not log a warning when HammerJS is not loaded', () => { it('should not log a warning when HammerJS is not loaded', () => {
plugin.addEventListener(someElement, 'swipe', () => {}); plugin.addEventListener(someElement, 'swipe', () => {});

View File

@ -51,7 +51,6 @@ import {KeyEventsPlugin} from '@angular/platform-browser/src/dom/events/key_even
.toEqual({'domEventName': 'keydown', 'fullKey': 'control.shift'}); .toEqual({'domEventName': 'keydown', 'fullKey': 'control.shift'});
expect(KeyEventsPlugin.parseEventName('keyup.control.shift')) expect(KeyEventsPlugin.parseEventName('keyup.control.shift'))
.toEqual({'domEventName': 'keyup', 'fullKey': 'control.shift'}); .toEqual({'domEventName': 'keyup', 'fullKey': 'control.shift'});
}); });
it('should alias esc to escape', () => { it('should alias esc to escape', () => {
@ -67,6 +66,5 @@ import {KeyEventsPlugin} from '@angular/platform-browser/src/dom/events/key_even
expect(() => plugin.addGlobalEventListener('window', 'keyup.control.esc', () => {})) expect(() => plugin.addGlobalEventListener('window', 'keyup.control.esc', () => {}))
.not.toThrowError(); .not.toThrowError();
}); });
}); });
} }

View File

@ -3708,8 +3708,7 @@ describe('Integration', () => {
router.navigate(['/user/:fedor']); router.navigate(['/user/:fedor']);
advance(fixture); advance(fixture);
expect(navigateSpy.calls.mostRecent().args[1] !.queryParams); expect(navigateSpy.calls.mostRecent().args[1]!.queryParams);
}))); })));
}); });

View File

@ -9,7 +9,7 @@
import {PLATFORM_ID} from '@angular/core'; import {PLATFORM_ID} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {NgswCommChannel} from '@angular/service-worker/src/low_level'; import {NgswCommChannel} from '@angular/service-worker/src/low_level';
import {SwRegistrationOptions, ngswCommChannelFactory} from '@angular/service-worker/src/module'; import {ngswCommChannelFactory, SwRegistrationOptions} from '@angular/service-worker/src/module';
import {SwPush} from '@angular/service-worker/src/push'; import {SwPush} from '@angular/service-worker/src/push';
import {SwUpdate} from '@angular/service-worker/src/update'; import {SwUpdate} from '@angular/service-worker/src/update';
import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockServiceWorkerRegistration, patchDecodeBase64} from '@angular/service-worker/testing/mock'; import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockServiceWorkerRegistration, patchDecodeBase64} from '@angular/service-worker/testing/mock';
@ -32,14 +32,18 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
mock.setupSw(); mock.setupSw();
(comm as any).registration.subscribe((reg: any) => { done(); }); (comm as any).registration.subscribe((reg: any) => {
done();
});
}); });
it('can access the registration when it comes after subscription', done => { it('can access the registration when it comes after subscription', done => {
const mock = new MockServiceWorkerContainer(); const mock = new MockServiceWorkerContainer();
const comm = new NgswCommChannel(mock as any); const comm = new NgswCommChannel(mock as any);
const regPromise = mock.getRegistration() as any as MockServiceWorkerRegistration; const regPromise = mock.getRegistration() as any as MockServiceWorkerRegistration;
(comm as any).registration.subscribe((reg: any) => { done(); }); (comm as any).registration.subscribe((reg: any) => {
done();
});
mock.setupSw(); mock.setupSw();
}); });
@ -158,7 +162,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
}); });
describe('requestSubscription()', () => { describe('requestSubscription()', () => {
it('returns a promise that resolves to the subscription', async() => { it('returns a promise that resolves to the subscription', async () => {
const promise = push.requestSubscription({serverPublicKey: 'test'}); const promise = push.requestSubscription({serverPublicKey: 'test'});
expect(promise).toEqual(jasmine.any(Promise)); expect(promise).toEqual(jasmine.any(Promise));
@ -166,7 +170,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
expect(sub).toEqual(jasmine.any(MockPushSubscription)); expect(sub).toEqual(jasmine.any(MockPushSubscription));
}); });
it('calls `PushManager.subscribe()` (with appropriate options)', async() => { it('calls `PushManager.subscribe()` (with appropriate options)', async () => {
const decode = (charCodeArr: Uint8Array) => const decode = (charCodeArr: Uint8Array) =>
Array.from(charCodeArr).map(c => String.fromCharCode(c)).join(''); Array.from(charCodeArr).map(c => String.fromCharCode(c)).join('');
@ -183,12 +187,12 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
userVisibleOnly: true, userVisibleOnly: true,
}); });
const actualAppServerKey = pmSubscribeSpy.calls.first().args[0] !.applicationServerKey; const actualAppServerKey = pmSubscribeSpy.calls.first().args[0]!.applicationServerKey;
const actualAppServerKeyStr = decode(actualAppServerKey as Uint8Array); const actualAppServerKeyStr = decode(actualAppServerKey as Uint8Array);
expect(actualAppServerKeyStr).toBe(appServerKeyStr); expect(actualAppServerKeyStr).toBe(appServerKeyStr);
}); });
it('emits the new `PushSubscription` on `SwPush.subscription`', async() => { it('emits the new `PushSubscription` on `SwPush.subscription`', async () => {
const subscriptionSpy = jasmine.createSpy('subscriptionSpy'); const subscriptionSpy = jasmine.createSpy('subscriptionSpy');
push.subscription.subscribe(subscriptionSpy); push.subscription.subscribe(subscriptionSpy);
const sub = await push.requestSubscription({serverPublicKey: 'test'}); const sub = await push.requestSubscription({serverPublicKey: 'test'});
@ -204,7 +208,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
psUnsubscribeSpy = spyOn(MockPushSubscription.prototype, 'unsubscribe').and.callThrough(); psUnsubscribeSpy = spyOn(MockPushSubscription.prototype, 'unsubscribe').and.callThrough();
}); });
it('rejects if currently not subscribed to push notifications', async() => { it('rejects if currently not subscribed to push notifications', async () => {
try { try {
await push.unsubscribe(); await push.unsubscribe();
throw new Error('`unsubscribe()` should fail'); throw new Error('`unsubscribe()` should fail');
@ -213,15 +217,17 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
} }
}); });
it('calls `PushSubscription.unsubscribe()`', async() => { it('calls `PushSubscription.unsubscribe()`', async () => {
await push.requestSubscription({serverPublicKey: 'test'}); await push.requestSubscription({serverPublicKey: 'test'});
await push.unsubscribe(); await push.unsubscribe();
expect(psUnsubscribeSpy).toHaveBeenCalledTimes(1); expect(psUnsubscribeSpy).toHaveBeenCalledTimes(1);
}); });
it('rejects if `PushSubscription.unsubscribe()` fails', async() => { it('rejects if `PushSubscription.unsubscribe()` fails', async () => {
psUnsubscribeSpy.and.callFake(() => { throw new Error('foo'); }); psUnsubscribeSpy.and.callFake(() => {
throw new Error('foo');
});
try { try {
await push.requestSubscription({serverPublicKey: 'test'}); await push.requestSubscription({serverPublicKey: 'test'});
@ -232,7 +238,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
} }
}); });
it('rejects if `PushSubscription.unsubscribe()` returns false', async() => { it('rejects if `PushSubscription.unsubscribe()` returns false', async () => {
psUnsubscribeSpy.and.returnValue(Promise.resolve(false)); psUnsubscribeSpy.and.returnValue(Promise.resolve(false));
try { try {
@ -244,7 +250,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
} }
}); });
it('emits `null` on `SwPush.subscription`', async() => { it('emits `null` on `SwPush.subscription`', async () => {
const subscriptionSpy = jasmine.createSpy('subscriptionSpy'); const subscriptionSpy = jasmine.createSpy('subscriptionSpy');
push.subscription.subscribe(subscriptionSpy); push.subscription.subscribe(subscriptionSpy);
@ -254,7 +260,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
expect(subscriptionSpy).toHaveBeenCalledWith(null); expect(subscriptionSpy).toHaveBeenCalledWith(null);
}); });
it('does not emit on `SwPush.subscription` on failure', async() => { it('does not emit on `SwPush.subscription` on failure', async () => {
const subscriptionSpy = jasmine.createSpy('subscriptionSpy'); const subscriptionSpy = jasmine.createSpy('subscriptionSpy');
const initialSubEmit = new Promise(resolve => subscriptionSpy.and.callFake(resolve)); const initialSubEmit = new Promise(resolve => subscriptionSpy.and.callFake(resolve));
@ -271,7 +277,9 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
subscriptionSpy.calls.reset(); subscriptionSpy.calls.reset();
// Error due to `PushSubscription.unsubscribe()` error. // Error due to `PushSubscription.unsubscribe()` error.
psUnsubscribeSpy.and.callFake(() => { throw new Error('foo'); }); psUnsubscribeSpy.and.callFake(() => {
throw new Error('foo');
});
await push.unsubscribe().catch(() => undefined); await push.unsubscribe().catch(() => undefined);
expect(subscriptionSpy).not.toHaveBeenCalled(); expect(subscriptionSpy).not.toHaveBeenCalled();
@ -338,7 +346,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
push.subscription.subscribe(subscriptionSpy); push.subscription.subscribe(subscriptionSpy);
}); });
it('emits on worker-driven changes (i.e. when the controller changes)', async() => { it('emits on worker-driven changes (i.e. when the controller changes)', async () => {
// Initial emit for the current `ServiceWorkerController`. // Initial emit for the current `ServiceWorkerController`.
await nextSubEmitPromise; await nextSubEmitPromise;
expect(subscriptionSpy).toHaveBeenCalledTimes(1); expect(subscriptionSpy).toHaveBeenCalledTimes(1);
@ -353,7 +361,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
expect(subscriptionSpy).toHaveBeenCalledWith(null); expect(subscriptionSpy).toHaveBeenCalledWith(null);
}); });
it('emits on subscription changes (i.e. when subscribing/unsubscribing)', async() => { it('emits on subscription changes (i.e. when subscribing/unsubscribing)', async () => {
await nextSubEmitPromise; await nextSubEmitPromise;
subscriptionSpy.calls.reset(); subscriptionSpy.calls.reset();
@ -391,11 +399,16 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
}); });
it('gives an error when registering', done => { it('gives an error when registering', done => {
push.requestSubscription({serverPublicKey: 'test'}).catch(err => { done(); }); push.requestSubscription({serverPublicKey: 'test'}).catch(err => {
done();
});
}); });
it('gives an error when unsubscribing', it('gives an error when unsubscribing', done => {
done => { push.unsubscribe().catch(err => { done(); }); }); push.unsubscribe().catch(err => {
done();
});
});
}); });
}); });
@ -461,7 +474,9 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
}); });
}); });
return update.activateUpdate() return update.activateUpdate()
.catch(err => { expect(err.message).toEqual('Failed to activate'); }) .catch(err => {
expect(err.message).toEqual('Failed to activate');
})
.then(() => done()) .then(() => done())
.catch(err => done.fail(err)); .catch(err => done.fail(err));
}); });
@ -475,8 +490,12 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
expect(() => TestBed.inject(SwUpdate)).not.toThrow(); expect(() => TestBed.inject(SwUpdate)).not.toThrow();
}); });
describe('with no SW', () => { describe('with no SW', () => {
beforeEach(() => { comm = new NgswCommChannel(undefined); }); beforeEach(() => {
it('can be instantiated', () => { update = new SwUpdate(comm); }); comm = new NgswCommChannel(undefined);
});
it('can be instantiated', () => {
update = new SwUpdate(comm);
});
it('does not crash on subscription to observables', () => { it('does not crash on subscription to observables', () => {
update = new SwUpdate(comm); update = new SwUpdate(comm);
update.available.toPromise().catch(err => fail(err)); update.available.toPromise().catch(err => fail(err));
@ -484,11 +503,15 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
}); });
it('gives an error when checking for updates', done => { it('gives an error when checking for updates', done => {
update = new SwUpdate(comm); update = new SwUpdate(comm);
update.checkForUpdate().catch(err => { done(); }); update.checkForUpdate().catch(err => {
done();
});
}); });
it('gives an error when activating updates', done => { it('gives an error when activating updates', done => {
update = new SwUpdate(comm); update = new SwUpdate(comm);
update.activateUpdate().catch(err => { done(); }); update.activateUpdate().catch(err => {
done();
});
}); });
}); });
}); });

View File

@ -7,7 +7,7 @@
*/ */
import {ApplicationRef, PLATFORM_ID} from '@angular/core'; import {ApplicationRef, PLATFORM_ID} from '@angular/core';
import {TestBed, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; import {fakeAsync, flushMicrotasks, TestBed, tick} from '@angular/core/testing';
import {Subject} from 'rxjs'; import {Subject} from 'rxjs';
import {filter, take} from 'rxjs/operators'; import {filter, take} from 'rxjs/operators';
@ -33,7 +33,7 @@ describe('ServiceWorkerModule', () => {
spyOn(navigator.serviceWorker, 'register').and.returnValue(Promise.resolve(null as any))); spyOn(navigator.serviceWorker, 'register').and.returnValue(Promise.resolve(null as any)));
describe('register()', () => { describe('register()', () => {
const configTestBed = async(opts: SwRegistrationOptions) => { const configTestBed = async (opts: SwRegistrationOptions) => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ServiceWorkerModule.register('sw.js', opts)], imports: [ServiceWorkerModule.register('sw.js', opts)],
providers: [{provide: PLATFORM_ID, useValue: 'browser'}], providers: [{provide: PLATFORM_ID, useValue: 'browser'}],
@ -42,35 +42,35 @@ describe('ServiceWorkerModule', () => {
await untilStable(); await untilStable();
}; };
it('sets the registration options', async() => { it('sets the registration options', async () => {
await configTestBed({enabled: true, scope: 'foo'}); await configTestBed({enabled: true, scope: 'foo'});
expect(TestBed.inject(SwRegistrationOptions)).toEqual({enabled: true, scope: 'foo'}); expect(TestBed.inject(SwRegistrationOptions)).toEqual({enabled: true, scope: 'foo'});
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: 'foo'}); expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: 'foo'});
}); });
it('can disable the SW', async() => { it('can disable the SW', async () => {
await configTestBed({enabled: false}); await configTestBed({enabled: false});
expect(TestBed.inject(SwUpdate).isEnabled).toBe(false); expect(TestBed.inject(SwUpdate).isEnabled).toBe(false);
expect(swRegisterSpy).not.toHaveBeenCalled(); expect(swRegisterSpy).not.toHaveBeenCalled();
}); });
it('can enable the SW', async() => { it('can enable the SW', async () => {
await configTestBed({enabled: true}); await configTestBed({enabled: true});
expect(TestBed.inject(SwUpdate).isEnabled).toBe(true); expect(TestBed.inject(SwUpdate).isEnabled).toBe(true);
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
}); });
it('defaults to enabling the SW', async() => { it('defaults to enabling the SW', async () => {
await configTestBed({}); await configTestBed({});
expect(TestBed.inject(SwUpdate).isEnabled).toBe(true); expect(TestBed.inject(SwUpdate).isEnabled).toBe(true);
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
}); });
it('catches and a logs registration errors', async() => { it('catches and a logs registration errors', async () => {
const consoleErrorSpy = spyOn(console, 'error'); const consoleErrorSpy = spyOn(console, 'error');
swRegisterSpy.and.returnValue(Promise.reject('no reason')); swRegisterSpy.and.returnValue(Promise.reject('no reason'));
@ -92,7 +92,7 @@ describe('ServiceWorkerModule', () => {
}); });
}; };
it('sets the registration options (and overwrites those set via `.register()`', async() => { it('sets the registration options (and overwrites those set via `.register()`', async () => {
configTestBed({enabled: true, scope: 'provider'}); configTestBed({enabled: true, scope: 'provider'});
await untilStable(); await untilStable();
@ -100,7 +100,7 @@ describe('ServiceWorkerModule', () => {
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: 'provider'}); expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: 'provider'});
}); });
it('can disable the SW', async() => { it('can disable the SW', async () => {
configTestBed({enabled: false}, {enabled: true}); configTestBed({enabled: false}, {enabled: true});
await untilStable(); await untilStable();
@ -108,7 +108,7 @@ describe('ServiceWorkerModule', () => {
expect(swRegisterSpy).not.toHaveBeenCalled(); expect(swRegisterSpy).not.toHaveBeenCalled();
}); });
it('can enable the SW', async() => { it('can enable the SW', async () => {
configTestBed({enabled: true}, {enabled: false}); configTestBed({enabled: true}, {enabled: false});
await untilStable(); await untilStable();
@ -116,7 +116,7 @@ describe('ServiceWorkerModule', () => {
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
}); });
it('defaults to enabling the SW', async() => { it('defaults to enabling the SW', async () => {
configTestBed({}, {enabled: false}); configTestBed({}, {enabled: false});
await untilStable(); await untilStable();

View File

@ -11,18 +11,18 @@ import {CacheDatabase} from '../src/db-cache';
import {Driver, DriverReadyState} from '../src/driver'; import {Driver, DriverReadyState} from '../src/driver';
import {AssetGroupConfig, DataGroupConfig, Manifest} from '../src/manifest'; import {AssetGroupConfig, DataGroupConfig, Manifest} from '../src/manifest';
import {sha1} from '../src/sha1'; import {sha1} from '../src/sha1';
import {MockCache, clearAllCaches} from '../testing/cache'; import {clearAllCaches, MockCache} from '../testing/cache';
import {MockRequest, MockResponse} from '../testing/fetch'; import {MockRequest, MockResponse} from '../testing/fetch';
import {MockFileSystemBuilder, MockServerStateBuilder, tmpHashTableForFs} from '../testing/mock'; import {MockFileSystemBuilder, MockServerStateBuilder, tmpHashTableForFs} from '../testing/mock';
import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope'; import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
(function() { (function() {
// Skip environments that don't support the minimum APIs needed to run the SW tests. // Skip environments that don't support the minimum APIs needed to run the SW tests.
if (!SwTestHarness.envIsSupported()) { if (!SwTestHarness.envIsSupported()) {
return; return;
} }
const dist = const dist =
new MockFileSystemBuilder() new MockFileSystemBuilder()
.addFile('/foo.txt', 'this is foo') .addFile('/foo.txt', 'this is foo')
.addFile('/bar.txt', 'this is bar') .addFile('/bar.txt', 'this is bar')
@ -35,11 +35,10 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
.addUnhashedFile('/unhashed/a.txt', 'this is unhashed', {'Cache-Control': 'max-age=10'}) .addUnhashedFile('/unhashed/a.txt', 'this is unhashed', {'Cache-Control': 'max-age=10'})
.addUnhashedFile('/unhashed/b.txt', 'this is unhashed b', {'Cache-Control': 'no-cache'}) .addUnhashedFile('/unhashed/b.txt', 'this is unhashed b', {'Cache-Control': 'no-cache'})
.addUnhashedFile('/api/foo', 'this is api foo', {'Cache-Control': 'no-cache'}) .addUnhashedFile('/api/foo', 'this is api foo', {'Cache-Control': 'no-cache'})
.addUnhashedFile( .addUnhashedFile('/api-static/bar', 'this is static api bar', {'Cache-Control': 'no-cache'})
'/api-static/bar', 'this is static api bar', {'Cache-Control': 'no-cache'})
.build(); .build();
const distUpdate = const distUpdate =
new MockFileSystemBuilder() new MockFileSystemBuilder()
.addFile('/foo.txt', 'this is foo v2') .addFile('/foo.txt', 'this is foo v2')
.addFile('/bar.txt', 'this is bar') .addFile('/bar.txt', 'this is bar')
@ -49,18 +48,17 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
.addFile('/quuux.txt', 'this is quuux v2') .addFile('/quuux.txt', 'this is quuux v2')
.addFile('/lazy/unchanged1.txt', 'this is unchanged (1)') .addFile('/lazy/unchanged1.txt', 'this is unchanged (1)')
.addFile('/lazy/unchanged2.txt', 'this is unchanged (2)') .addFile('/lazy/unchanged2.txt', 'this is unchanged (2)')
.addUnhashedFile( .addUnhashedFile('/unhashed/a.txt', 'this is unhashed v2', {'Cache-Control': 'max-age=10'})
'/unhashed/a.txt', 'this is unhashed v2', {'Cache-Control': 'max-age=10'})
.addUnhashedFile('/ignored/file1', 'this is not handled by the SW') .addUnhashedFile('/ignored/file1', 'this is not handled by the SW')
.addUnhashedFile('/ignored/dir/file2', 'this is not handled by the SW either') .addUnhashedFile('/ignored/dir/file2', 'this is not handled by the SW either')
.build(); .build();
const brokenFs = new MockFileSystemBuilder() const brokenFs = new MockFileSystemBuilder()
.addFile('/foo.txt', 'this is foo (broken)') .addFile('/foo.txt', 'this is foo (broken)')
.addFile('/bar.txt', 'this is bar (broken)') .addFile('/bar.txt', 'this is bar (broken)')
.build(); .build();
const brokenManifest: Manifest = { const brokenManifest: Manifest = {
configVersion: 1, configVersion: 1,
timestamp: 1234567890123, timestamp: 1234567890123,
index: '/foo.txt', index: '/foo.txt',
@ -76,9 +74,9 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
dataGroups: [], dataGroups: [],
navigationUrls: processNavigationUrls(''), navigationUrls: processNavigationUrls(''),
hashTable: tmpHashTableForFs(brokenFs, {'/foo.txt': true}), hashTable: tmpHashTableForFs(brokenFs, {'/foo.txt': true}),
}; };
const brokenLazyManifest: Manifest = { const brokenLazyManifest: Manifest = {
configVersion: 1, configVersion: 1,
timestamp: 1234567890123, timestamp: 1234567890123,
index: '/foo.txt', index: '/foo.txt',
@ -105,27 +103,27 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
dataGroups: [], dataGroups: [],
navigationUrls: processNavigationUrls(''), navigationUrls: processNavigationUrls(''),
hashTable: tmpHashTableForFs(brokenFs, {'/bar.txt': true}), hashTable: tmpHashTableForFs(brokenFs, {'/bar.txt': true}),
}; };
// Manifest without navigation urls to test backward compatibility with // Manifest without navigation urls to test backward compatibility with
// versions < 6.0.0. // versions < 6.0.0.
interface ManifestV5 { interface ManifestV5 {
configVersion: number; configVersion: number;
appData?: {[key: string]: string}; appData?: {[key: string]: string};
index: string; index: string;
assetGroups?: AssetGroupConfig[]; assetGroups?: AssetGroupConfig[];
dataGroups?: DataGroupConfig[]; dataGroups?: DataGroupConfig[];
hashTable: {[url: string]: string}; hashTable: {[url: string]: string};
} }
// To simulate versions < 6.0.0 // To simulate versions < 6.0.0
const manifestOld: ManifestV5 = { const manifestOld: ManifestV5 = {
configVersion: 1, configVersion: 1,
index: '/foo.txt', index: '/foo.txt',
hashTable: tmpHashTableForFs(dist), hashTable: tmpHashTableForFs(dist),
}; };
const manifest: Manifest = { const manifest: Manifest = {
configVersion: 1, configVersion: 1,
timestamp: 1234567890123, timestamp: 1234567890123,
appData: { appData: {
@ -193,9 +191,9 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
], ],
navigationUrls: processNavigationUrls(''), navigationUrls: processNavigationUrls(''),
hashTable: tmpHashTableForFs(dist), hashTable: tmpHashTableForFs(dist),
}; };
const manifestUpdate: Manifest = { const manifestUpdate: Manifest = {
configVersion: 1, configVersion: 1,
timestamp: 1234567890123, timestamp: 1234567890123,
appData: { appData: {
@ -248,41 +246,39 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
'!/ignored/dir/**', '!/ignored/dir/**',
]), ]),
hashTable: tmpHashTableForFs(distUpdate), hashTable: tmpHashTableForFs(distUpdate),
}; };
const serverBuilderBase = const serverBuilderBase =
new MockServerStateBuilder() new MockServerStateBuilder()
.withStaticFiles(dist) .withStaticFiles(dist)
.withRedirect('/redirected.txt', '/redirect-target.txt', 'this was a redirect') .withRedirect('/redirected.txt', '/redirect-target.txt', 'this was a redirect')
.withError('/error.txt'); .withError('/error.txt');
const server = serverBuilderBase.withManifest(manifest).build(); const server = serverBuilderBase.withManifest(manifest).build();
const serverRollback = const serverRollback =
serverBuilderBase.withManifest({...manifest, timestamp: manifest.timestamp + 1}).build(); serverBuilderBase.withManifest({...manifest, timestamp: manifest.timestamp + 1}).build();
const serverUpdate = const serverUpdate =
new MockServerStateBuilder() new MockServerStateBuilder()
.withStaticFiles(distUpdate) .withStaticFiles(distUpdate)
.withManifest(manifestUpdate) .withManifest(manifestUpdate)
.withRedirect('/redirected.txt', '/redirect-target.txt', 'this was a redirect') .withRedirect('/redirected.txt', '/redirect-target.txt', 'this was a redirect')
.build(); .build();
const brokenServer = const brokenServer =
new MockServerStateBuilder().withStaticFiles(brokenFs).withManifest(brokenManifest).build(); new MockServerStateBuilder().withStaticFiles(brokenFs).withManifest(brokenManifest).build();
const brokenLazyServer = new MockServerStateBuilder() const brokenLazyServer =
.withStaticFiles(brokenFs) new MockServerStateBuilder().withStaticFiles(brokenFs).withManifest(brokenLazyManifest).build();
.withManifest(brokenLazyManifest)
.build();
const server404 = new MockServerStateBuilder().withStaticFiles(dist).build(); const server404 = new MockServerStateBuilder().withStaticFiles(dist).build();
const manifestHash = sha1(JSON.stringify(manifest)); const manifestHash = sha1(JSON.stringify(manifest));
const manifestUpdateHash = sha1(JSON.stringify(manifestUpdate)); const manifestUpdateHash = sha1(JSON.stringify(manifestUpdate));
describe('Driver', () => { describe('Driver', () => {
let scope: SwTestHarness; let scope: SwTestHarness;
let driver: Driver; let driver: Driver;
@ -296,19 +292,19 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
}); });
it('activates without waiting', async() => { it('activates without waiting', async () => {
const skippedWaiting = await scope.startup(true); const skippedWaiting = await scope.startup(true);
expect(skippedWaiting).toBe(true); expect(skippedWaiting).toBe(true);
}); });
it('claims all clients, after activation', async() => { it('claims all clients, after activation', async () => {
const claimSpy = spyOn(scope.clients, 'claim'); const claimSpy = spyOn(scope.clients, 'claim');
await scope.startup(true); await scope.startup(true);
expect(claimSpy).toHaveBeenCalledTimes(1); expect(claimSpy).toHaveBeenCalledTimes(1);
}); });
it('cleans up old `@angular/service-worker` caches, after activation', async() => { it('cleans up old `@angular/service-worker` caches, after activation', async () => {
const claimSpy = spyOn(scope.clients, 'claim'); const claimSpy = spyOn(scope.clients, 'claim');
const cleanupOldSwCachesSpy = spyOn(driver, 'cleanupOldSwCaches'); const cleanupOldSwCachesSpy = spyOn(driver, 'cleanupOldSwCaches');
@ -322,7 +318,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(claimSpy).toHaveBeenCalledBefore(cleanupOldSwCachesSpy); expect(claimSpy).toHaveBeenCalledBefore(cleanupOldSwCachesSpy);
}); });
it('does not blow up if cleaning up old `@angular/service-worker` caches fails', async() => { it('does not blow up if cleaning up old `@angular/service-worker` caches fails', async () => {
spyOn(driver, 'cleanupOldSwCaches').and.callFake(() => Promise.reject('Ooops')); spyOn(driver, 'cleanupOldSwCaches').and.callFake(() => Promise.reject('Ooops'));
// Automatically advance time to trigger idle tasks as they are added. // Automatically advance time to trigger idle tasks as they are added.
@ -338,7 +334,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('initializes prefetched content correctly, after activation', async() => { it('initializes prefetched content correctly, after activation', async () => {
// Automatically advance time to trigger idle tasks as they are added. // Automatically advance time to trigger idle tasks as they are added.
scope.autoAdvanceTime = true; scope.autoAdvanceTime = true;
await scope.startup(true); await scope.startup(true);
@ -354,7 +350,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('initializes prefetched content correctly, after a request kicks it off', async() => { it('initializes prefetched content correctly, after a request kicks it off', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
server.assertSawRequestFor('ngsw.json'); server.assertSawRequestFor('ngsw.json');
@ -366,7 +362,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('initializes the service worker on fetch if it has not yet been initialized', async() => { it('initializes the service worker on fetch if it has not yet been initialized', async () => {
// Driver is initially uninitialized. // Driver is initially uninitialized.
expect(driver.initialized).toBeNull(); expect(driver.initialized).toBeNull();
expect(driver['latestHash']).toBeNull(); expect(driver['latestHash']).toBeNull();
@ -385,7 +381,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('initializes the service worker on message if it has not yet been initialized', async() => { it('initializes the service worker on message if it has not yet been initialized', async () => {
// Driver is initially uninitialized. // Driver is initially uninitialized.
expect(driver.initialized).toBeNull(); expect(driver.initialized).toBeNull();
expect(driver['latestHash']).toBeNull(); expect(driver['latestHash']).toBeNull();
@ -408,7 +404,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('handles non-relative URLs', async() => { it('handles non-relative URLs', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
server.clearRequests(); server.clearRequests();
@ -416,19 +412,19 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('handles actual errors from the browser', async() => { it('handles actual errors from the browser', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
server.clearRequests(); server.clearRequests();
const [resPromise, done] = scope.handleFetch(new MockRequest('/error.txt'), 'default'); const [resPromise, done] = scope.handleFetch(new MockRequest('/error.txt'), 'default');
await done; await done;
const res = (await resPromise) !; const res = (await resPromise)!;
expect(res.status).toEqual(504); expect(res.status).toEqual(504);
expect(res.statusText).toEqual('Gateway Timeout'); expect(res.statusText).toEqual('Gateway Timeout');
}); });
it('handles redirected responses', async() => { it('handles redirected responses', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
server.clearRequests(); server.clearRequests();
@ -436,7 +432,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('caches lazy content on-request', async() => { it('caches lazy content on-request', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
server.clearRequests(); server.clearRequests();
@ -450,11 +446,11 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('updates to new content when requested', async() => { it('updates to new content when requested', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
const client = scope.clients.getMock('default') !; const client = scope.clients.getMock('default')!;
expect(client.messages).toEqual([]); expect(client.messages).toEqual([]);
scope.updateServerState(serverUpdate); scope.updateServerState(serverUpdate);
@ -483,7 +479,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertNoOtherRequests(); serverUpdate.assertNoOtherRequests();
}); });
it('detects new version even if only `manifest.timestamp` is different', async() => { it('detects new version even if only `manifest.timestamp` is different', async () => {
expect(await makeRequest(scope, '/foo.txt', 'newClient')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt', 'newClient')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
@ -496,11 +492,11 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(await makeRequest(scope, '/foo.txt', 'newestClient')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt', 'newestClient')).toEqual('this is foo');
}); });
it('updates a specific client to new content on request', async() => { it('updates a specific client to new content on request', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
const client = scope.clients.getMock('default') !; const client = scope.clients.getMock('default')!;
expect(client.messages).toEqual([]); expect(client.messages).toEqual([]);
scope.updateServerState(serverUpdate); scope.updateServerState(serverUpdate);
@ -524,7 +520,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo v2'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo v2');
}); });
it('handles empty client ID', async() => { it('handles empty client ID', async () => {
// Initialize the SW. // Initialize the SW.
expect(await makeNavigationRequest(scope, '/foo/file1', '')).toEqual('this is foo'); expect(await makeNavigationRequest(scope, '/foo/file1', '')).toEqual('this is foo');
expect(await makeNavigationRequest(scope, '/bar/file2', null)).toEqual('this is foo'); expect(await makeNavigationRequest(scope, '/bar/file2', null)).toEqual('this is foo');
@ -539,7 +535,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(await makeNavigationRequest(scope, '/bar/file2', null)).toEqual('this is foo v2'); expect(await makeNavigationRequest(scope, '/bar/file2', null)).toEqual('this is foo v2');
}); });
it('checks for updates on restart', async() => { it('checks for updates on restart', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
@ -561,7 +557,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertNoOtherRequests(); serverUpdate.assertNoOtherRequests();
}); });
it('checks for updates on navigation', async() => { it('checks for updates on navigation', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
server.clearRequests(); server.clearRequests();
@ -574,7 +570,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertSawRequestFor('ngsw.json'); server.assertSawRequestFor('ngsw.json');
}); });
it('does not make concurrent checks for updates on navigation', async() => { it('does not make concurrent checks for updates on navigation', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
server.clearRequests(); server.clearRequests();
@ -590,7 +586,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('preserves multiple client assignments across restarts', async() => { it('preserves multiple client assignments across restarts', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
@ -610,11 +606,11 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertNoOtherRequests(); serverUpdate.assertNoOtherRequests();
}); });
it('updates when refreshed', async() => { it('updates when refreshed', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
const client = scope.clients.getMock('default') !; const client = scope.clients.getMock('default')!;
scope.updateServerState(serverUpdate); scope.updateServerState(serverUpdate);
expect(await driver.checkForUpdate()).toEqual(true); expect(await driver.checkForUpdate()).toEqual(true);
@ -637,7 +633,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertNoOtherRequests(); serverUpdate.assertNoOtherRequests();
}); });
it('cleans up properly when manually requested', async() => { it('cleans up properly when manually requested', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
@ -657,7 +653,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertNoOtherRequests(); serverUpdate.assertNoOtherRequests();
}); });
it('cleans up properly on restart', async() => { it('cleans up properly on restart', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
@ -688,7 +684,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(hasOriginalCaches).toEqual(false); expect(hasOriginalCaches).toEqual(false);
}); });
it('shows notifications for push notifications', async() => { it('shows notifications for push notifications', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
await scope.handlePush({ await scope.handlePush({
@ -701,7 +697,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
title: 'This is a test', title: 'This is a test',
options: {title: 'This is a test', body: 'Test body'}, options: {title: 'This is a test', body: 'Test body'},
}]); }]);
expect(scope.clients.getMock('default') !.messages).toEqual([{ expect(scope.clients.getMock('default')!.messages).toEqual([{
type: 'PUSH', type: 'PUSH',
data: { data: {
notification: { notification: {
@ -712,12 +708,12 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
}]); }]);
}); });
it('broadcasts notification click events with action', async() => { it('broadcasts notification click events with action', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
await scope.handleClick( await scope.handleClick(
{title: 'This is a test with action', body: 'Test body with action'}, 'button'); {title: 'This is a test with action', body: 'Test body with action'}, 'button');
const message: any = scope.clients.getMock('default') !.messages[0]; const message: any = scope.clients.getMock('default')!.messages[0];
expect(message.type).toEqual('NOTIFICATION_CLICK'); expect(message.type).toEqual('NOTIFICATION_CLICK');
expect(message.data.action).toEqual('button'); expect(message.data.action).toEqual('button');
@ -725,12 +721,12 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(message.data.notification.body).toEqual('Test body with action'); expect(message.data.notification.body).toEqual('Test body with action');
}); });
it('broadcasts notification click events without action', async() => { it('broadcasts notification click events without action', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
await scope.handleClick( await scope.handleClick(
{title: 'This is a test without action', body: 'Test body without action'}); {title: 'This is a test without action', body: 'Test body without action'});
const message: any = scope.clients.getMock('default') !.messages[0]; const message: any = scope.clients.getMock('default')!.messages[0];
expect(message.type).toEqual('NOTIFICATION_CLICK'); expect(message.type).toEqual('NOTIFICATION_CLICK');
expect(message.data.action).toBeUndefined(); expect(message.data.action).toBeUndefined();
@ -738,7 +734,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(message.data.notification.body).toEqual('Test body without action'); expect(message.data.notification.body).toEqual('Test body without action');
}); });
it('prefetches updates to lazy cache when set', async() => { it('prefetches updates to lazy cache when set', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
@ -783,14 +779,14 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertNoOtherRequests(); serverUpdate.assertNoOtherRequests();
}); });
it('should bypass serviceworker on ngsw-bypass parameter', async() => { it('should bypass serviceworker on ngsw-bypass parameter', async () => {
await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': 'true'}}); await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': 'true'}});
server.assertNoRequestFor('/foo.txt'); server.assertNoRequestFor('/foo.txt');
await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': 'anything'}}); await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': 'anything'}});
server.assertNoRequestFor('/foo.txt'); server.assertNoRequestFor('/foo.txt');
await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': null !}}); await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': null!}});
server.assertNoRequestFor('/foo.txt'); server.assertNoRequestFor('/foo.txt');
await makeRequest(scope, '/foo.txt', undefined, {headers: {'NGSW-bypass': 'upperCASE'}}); await makeRequest(scope, '/foo.txt', undefined, {headers: {'NGSW-bypass': 'upperCASE'}});
@ -848,10 +844,9 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
await makeRequest(scope, '/bar?ngsw-byapass&testparam2'); await makeRequest(scope, '/bar?ngsw-byapass&testparam2');
server.assertSawRequestFor('/bar'); server.assertSawRequestFor('/bar');
}); });
it('unregisters when manifest 404s', async() => { it('unregisters when manifest 404s', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
@ -861,7 +856,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(await scope.caches.keys()).toEqual([]); expect(await scope.caches.keys()).toEqual([]);
}); });
it('does not unregister or change state when offline (i.e. manifest 504s)', async() => { it('does not unregister or change state when offline (i.e. manifest 504s)', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
server.online = false; server.online = false;
@ -873,10 +868,10 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
}); });
it('does not unregister or change state when status code is 503 (service unavailable)', it('does not unregister or change state when status code is 503 (service unavailable)',
async() => { async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
spyOn(server, 'fetch').and.callFake(async(req: Request) => new MockResponse(null, { spyOn(server, 'fetch').and.callFake(async (req: Request) => new MockResponse(null, {
status: 503, status: 503,
statusText: 'Service Unavailable' statusText: 'Service Unavailable'
})); }));
@ -890,7 +885,8 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
describe('cache naming', () => { describe('cache naming', () => {
// Helpers // Helpers
const cacheKeysFor = (baseHref: string) => const cacheKeysFor = (baseHref: string) =>
[`ngsw:${baseHref}:db:control`, `ngsw:${baseHref}:${manifestHash}:assets:assets:cache`, [`ngsw:${baseHref}:db:control`,
`ngsw:${baseHref}:${manifestHash}:assets:assets:cache`,
`ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:assets:meta`, `ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:assets:meta`,
`ngsw:${baseHref}:${manifestHash}:assets:other:cache`, `ngsw:${baseHref}:${manifestHash}:assets:other:cache`,
`ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:other:meta`, `ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:other:meta`,
@ -904,14 +900,14 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
`ngsw:${baseHref}:db:ngsw:${baseHref}:43:data:dynamic:api-static:age`, `ngsw:${baseHref}:db:ngsw:${baseHref}:43:data:dynamic:api-static:age`,
]; ];
const getClientAssignments = async(sw: SwTestHarness, baseHref: string) => { const getClientAssignments = async (sw: SwTestHarness, baseHref: string) => {
const cache = await sw.caches.open(`ngsw:${baseHref}:db:control`) as unknown as MockCache; const cache = await sw.caches.open(`ngsw:${baseHref}:db:control`) as unknown as MockCache;
const dehydrated = cache.dehydrate(); const dehydrated = cache.dehydrate();
return JSON.parse(dehydrated['/assignments'].body !); return JSON.parse(dehydrated['/assignments'].body!);
}; };
const initializeSwFor = const initializeSwFor =
async(baseHref: string, initialCacheState = '{}', serverState = server) => { async (baseHref: string, initialCacheState = '{}', serverState = server) => {
const newScope = new SwTestHarnessBuilder(`http://localhost${baseHref}`) const newScope = new SwTestHarnessBuilder(`http://localhost${baseHref}`)
.withCacheState(initialCacheState) .withCacheState(initialCacheState)
.withServerState(serverState) .withServerState(serverState)
@ -924,7 +920,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
return newScope; return newScope;
}; };
it('includes the SW scope in all cache names', async() => { it('includes the SW scope in all cache names', async () => {
// Default SW with scope `/`. // Default SW with scope `/`.
await makeRequest(scope, '/foo.txt'); await makeRequest(scope, '/foo.txt');
await driver.initialized; await driver.initialized;
@ -941,7 +937,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(fooCacheNames.every(name => name.includes('/foo/'))).toBe(true); expect(fooCacheNames.every(name => name.includes('/foo/'))).toBe(true);
}); });
it('does not affect caches from other scopes', async() => { it('does not affect caches from other scopes', async () => {
// Create SW with scope `/foo/`. // Create SW with scope `/foo/`.
const fooScope = await initializeSwFor('/foo/'); const fooScope = await initializeSwFor('/foo/');
const fooAssignments = await getClientAssignments(fooScope, '/foo/'); const fooAssignments = await getClientAssignments(fooScope, '/foo/');
@ -964,7 +960,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(fooAssignments2).toEqual({_foo_: manifestHash}); expect(fooAssignments2).toEqual({_foo_: manifestHash});
}); });
it('updates existing caches for same scope', async() => { it('updates existing caches for same scope', async () => {
// Create SW with scope `/foo/`. // Create SW with scope `/foo/`.
const fooScope = await initializeSwFor('/foo/'); const fooScope = await initializeSwFor('/foo/');
await makeRequest(fooScope, '/foo.txt', '_bar_'); await makeRequest(fooScope, '/foo.txt', '_bar_');
@ -1000,27 +996,27 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
}); });
describe('unhashed requests', () => { describe('unhashed requests', () => {
beforeEach(async() => { beforeEach(async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
server.clearRequests(); server.clearRequests();
}); });
it('are cached appropriately', async() => { it('are cached appropriately', async () => {
expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed');
server.assertSawRequestFor('/unhashed/a.txt'); server.assertSawRequestFor('/unhashed/a.txt');
expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed');
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it(`doesn't error when 'Cache-Control' is 'no-cache'`, async() => { it(`doesn't error when 'Cache-Control' is 'no-cache'`, async () => {
expect(await makeRequest(scope, '/unhashed/b.txt')).toEqual('this is unhashed b'); expect(await makeRequest(scope, '/unhashed/b.txt')).toEqual('this is unhashed b');
server.assertSawRequestFor('/unhashed/b.txt'); server.assertSawRequestFor('/unhashed/b.txt');
expect(await makeRequest(scope, '/unhashed/b.txt')).toEqual('this is unhashed b'); expect(await makeRequest(scope, '/unhashed/b.txt')).toEqual('this is unhashed b');
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('avoid opaque responses', async() => { it('avoid opaque responses', async () => {
expect(await makeRequest(scope, '/unhashed/a.txt', 'default', { expect(await makeRequest(scope, '/unhashed/a.txt', 'default', {
credentials: 'include' credentials: 'include'
})).toEqual('this is unhashed'); })).toEqual('this is unhashed');
@ -1029,7 +1025,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('expire according to Cache-Control headers', async() => { it('expire according to Cache-Control headers', async () => {
expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed');
server.clearRequests(); server.clearRequests();
@ -1052,7 +1048,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('survive serialization', async() => { it('survive serialization', async () => {
expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed');
server.clearRequests(); server.clearRequests();
@ -1075,7 +1071,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoRequestFor('/unhashed/a.txt'); server.assertNoRequestFor('/unhashed/a.txt');
}); });
it('get carried over during updates', async() => { it('get carried over during updates', async () => {
expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed');
server.clearRequests(); server.clearRequests();
@ -1108,28 +1104,28 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
const navRequest = (url: string, init = {}) => const navRequest = (url: string, init = {}) =>
makeNavigationRequest(scope, url, undefined, init); makeNavigationRequest(scope, url, undefined, init);
beforeEach(async() => { beforeEach(async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
server.clearRequests(); server.clearRequests();
}); });
it('redirects to index on a route-like request', async() => { it('redirects to index on a route-like request', async () => {
expect(await navRequest('/baz')).toEqual('this is foo'); expect(await navRequest('/baz')).toEqual('this is foo');
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('redirects to index on a request to the origin URL request', async() => { it('redirects to index on a request to the origin URL request', async () => {
expect(await navRequest('http://localhost/')).toEqual('this is foo'); expect(await navRequest('http://localhost/')).toEqual('this is foo');
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('does not redirect to index on a non-navigation request', async() => { it('does not redirect to index on a non-navigation request', async () => {
expect(await navRequest('/baz', {mode: undefined})).toBeNull(); expect(await navRequest('/baz', {mode: undefined})).toBeNull();
server.assertSawRequestFor('/baz'); server.assertSawRequestFor('/baz');
}); });
it('does not redirect to index on a request that does not accept HTML', async() => { it('does not redirect to index on a request that does not accept HTML', async () => {
expect(await navRequest('/baz', {headers: {}})).toBeNull(); expect(await navRequest('/baz', {headers: {}})).toBeNull();
server.assertSawRequestFor('/baz'); server.assertSawRequestFor('/baz');
@ -1137,7 +1133,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertSawRequestFor('/qux'); server.assertSawRequestFor('/qux');
}); });
it('does not redirect to index on a request with an extension', async() => { it('does not redirect to index on a request with an extension', async () => {
expect(await navRequest('/baz.html')).toBeNull(); expect(await navRequest('/baz.html')).toBeNull();
server.assertSawRequestFor('/baz.html'); server.assertSawRequestFor('/baz.html');
@ -1146,7 +1142,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests(); server.assertNoOtherRequests();
}); });
it('does not redirect to index if the URL contains `__`', async() => { it('does not redirect to index if the URL contains `__`', async () => {
expect(await navRequest('/baz/x__x')).toBeNull(); expect(await navRequest('/baz/x__x')).toBeNull();
server.assertSawRequestFor('/baz/x__x'); server.assertSawRequestFor('/baz/x__x');
@ -1161,13 +1157,13 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
}); });
describe('(with custom `navigationUrls`)', () => { describe('(with custom `navigationUrls`)', () => {
beforeEach(async() => { beforeEach(async () => {
scope.updateServerState(serverUpdate); scope.updateServerState(serverUpdate);
await driver.checkForUpdate(); await driver.checkForUpdate();
serverUpdate.clearRequests(); serverUpdate.clearRequests();
}); });
it('redirects to index on a request that matches any positive pattern', async() => { it('redirects to index on a request that matches any positive pattern', async () => {
expect(await navRequest('/foo/file0')).toBeNull(); expect(await navRequest('/foo/file0')).toBeNull();
serverUpdate.assertSawRequestFor('/foo/file0'); serverUpdate.assertSawRequestFor('/foo/file0');
@ -1178,24 +1174,22 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertNoOtherRequests(); serverUpdate.assertNoOtherRequests();
}); });
it('does not redirect to index on a request that matches any negative pattern', async() => { it('does not redirect to index on a request that matches any negative pattern', async () => {
expect(await navRequest('/ignored/file1')).toBe('this is not handled by the SW'); expect(await navRequest('/ignored/file1')).toBe('this is not handled by the SW');
serverUpdate.assertSawRequestFor('/ignored/file1'); serverUpdate.assertSawRequestFor('/ignored/file1');
expect(await navRequest('/ignored/dir/file2')) expect(await navRequest('/ignored/dir/file2')).toBe('this is not handled by the SW either');
.toBe('this is not handled by the SW either');
serverUpdate.assertSawRequestFor('/ignored/dir/file2'); serverUpdate.assertSawRequestFor('/ignored/dir/file2');
expect(await navRequest('/ignored/directory/file2')).toBe('this is foo v2'); expect(await navRequest('/ignored/directory/file2')).toBe('this is foo v2');
serverUpdate.assertNoOtherRequests(); serverUpdate.assertNoOtherRequests();
}); });
it('strips URL query before checking `navigationUrls`', async() => { it('strips URL query before checking `navigationUrls`', async () => {
expect(await navRequest('/foo/file1?query=/a/b')).toBe('this is foo v2'); expect(await navRequest('/foo/file1?query=/a/b')).toBe('this is foo v2');
serverUpdate.assertNoOtherRequests(); serverUpdate.assertNoOtherRequests();
expect(await navRequest('/ignored/file1?query=/a/b')) expect(await navRequest('/ignored/file1?query=/a/b')).toBe('this is not handled by the SW');
.toBe('this is not handled by the SW');
serverUpdate.assertSawRequestFor('/ignored/file1'); serverUpdate.assertSawRequestFor('/ignored/file1');
expect(await navRequest('/ignored/dir/file2?query=/a/b')) expect(await navRequest('/ignored/dir/file2?query=/a/b'))
@ -1203,7 +1197,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertSawRequestFor('/ignored/dir/file2'); serverUpdate.assertSawRequestFor('/ignored/dir/file2');
}); });
it('strips registration scope before checking `navigationUrls`', async() => { it('strips registration scope before checking `navigationUrls`', async () => {
expect(await navRequest('http://localhost/ignored/file1')) expect(await navRequest('http://localhost/ignored/file1'))
.toBe('this is not handled by the SW'); .toBe('this is not handled by the SW');
serverUpdate.assertSawRequestFor('/ignored/file1'); serverUpdate.assertSawRequestFor('/ignored/file1');
@ -1212,7 +1206,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
}); });
describe('cleanupOldSwCaches()', () => { describe('cleanupOldSwCaches()', () => {
it('should delete the correct caches', async() => { it('should delete the correct caches', async () => {
const oldSwCacheNames = [ const oldSwCacheNames = [
// Example cache names from the beta versions of `@angular/service-worker`. // Example cache names from the beta versions of `@angular/service-worker`.
'ngsw:active', 'ngsw:active',
@ -1238,12 +1232,12 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(await scope.caches.keys()).toEqual(otherCacheNames); expect(await scope.caches.keys()).toEqual(otherCacheNames);
}); });
it('should delete other caches even if deleting one of them fails', async() => { it('should delete other caches even if deleting one of them fails', async () => {
const oldSwCacheNames = ['ngsw:active', 'ngsw:staged', 'ngsw:manifest:a1b2c3:super:duper']; const oldSwCacheNames = ['ngsw:active', 'ngsw:staged', 'ngsw:manifest:a1b2c3:super:duper'];
const deleteSpy = spyOn(scope.caches, 'delete') const deleteSpy =
spyOn(scope.caches, 'delete')
.and.callFake( .and.callFake(
(cacheName: string) => (cacheName: string) => Promise.reject(`Failed to delete cache '${cacheName}'.`));
Promise.reject(`Failed to delete cache '${cacheName}'.`));
await Promise.all(oldSwCacheNames.map(name => scope.caches.open(name))); await Promise.all(oldSwCacheNames.map(name => scope.caches.open(name)));
const error = await driver.cleanupOldSwCaches().catch(err => err); const error = await driver.cleanupOldSwCaches().catch(err => err);
@ -1255,7 +1249,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
}); });
describe('bugs', () => { describe('bugs', () => {
it('does not crash with bad index hash', async() => { it('does not crash with bad index hash', async () => {
scope = new SwTestHarnessBuilder().withServerState(brokenServer).build(); scope = new SwTestHarnessBuilder().withServerState(brokenServer).build();
(scope.registration as any).scope = 'http://site.com'; (scope.registration as any).scope = 'http://site.com';
driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
@ -1263,7 +1257,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo (broken)'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo (broken)');
}); });
it('enters degraded mode when update has a bad index', async() => { it('enters degraded mode when update has a bad index', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
server.clearRequests(); server.clearRequests();
@ -1281,7 +1275,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(driver.state).toEqual(DriverReadyState.EXISTING_CLIENTS_ONLY); expect(driver.state).toEqual(DriverReadyState.EXISTING_CLIENTS_ONLY);
}); });
it('enters degraded mode when failing to write to cache', async() => { it('enters degraded mode when failing to write to cache', async () => {
// Initialize the SW. // Initialize the SW.
await makeRequest(scope, '/foo.txt'); await makeRequest(scope, '/foo.txt');
await driver.initialized; await driver.initialized;
@ -1304,7 +1298,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
}); });
it('keeps serving api requests with freshness strategy when failing to write to cache', it('keeps serving api requests with freshness strategy when failing to write to cache',
async() => { async () => {
// Initialize the SW. // Initialize the SW.
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
@ -1325,7 +1319,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
}); });
it('keeps serving api requests with performance strategy when failing to write to cache', it('keeps serving api requests with performance strategy when failing to write to cache',
async() => { async () => {
// Initialize the SW. // Initialize the SW.
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
@ -1347,7 +1341,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
it('keeps serving mutating api requests when failing to write to cache', it('keeps serving mutating api requests when failing to write to cache',
// sw can invalidate LRU cache entry and try to write to cache storage on mutating request // sw can invalidate LRU cache entry and try to write to cache storage on mutating request
async() => { async () => {
// Initialize the SW. // Initialize the SW.
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
@ -1366,7 +1360,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertSawRequestFor('/api/foo'); server.assertSawRequestFor('/api/foo');
}); });
it('enters degraded mode when something goes wrong with the latest version', async() => { it('enters degraded mode when something goes wrong with the latest version', async () => {
await driver.initialized; await driver.initialized;
// Two clients on initial version. // Two clients on initial version.
@ -1406,7 +1400,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
}); });
it('recovers from degraded `EXISTING_CLIENTS_ONLY` mode as soon as there is a valid update', it('recovers from degraded `EXISTING_CLIENTS_ONLY` mode as soon as there is a valid update',
async() => { async () => {
await driver.initialized; await driver.initialized;
expect(driver.state).toBe(DriverReadyState.NORMAL); expect(driver.state).toBe(DriverReadyState.NORMAL);
@ -1421,8 +1415,8 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(driver.state).toBe(DriverReadyState.NORMAL); expect(driver.state).toBe(DriverReadyState.NORMAL);
}); });
it('ignores invalid `only-if-cached` requests ', async() => { it('ignores invalid `only-if-cached` requests ', async () => {
const requestFoo = (cache: RequestCache | 'only-if-cached', mode: RequestMode) => const requestFoo = (cache: RequestCache|'only-if-cached', mode: RequestMode) =>
makeRequest(scope, '/foo.txt', undefined, {cache, mode}); makeRequest(scope, '/foo.txt', undefined, {cache, mode});
expect(await requestFoo('default', 'no-cors')).toBe('this is foo'); expect(await requestFoo('default', 'no-cors')).toBe('this is foo');
@ -1430,10 +1424,10 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(await requestFoo('only-if-cached', 'no-cors')).toBeNull(); expect(await requestFoo('only-if-cached', 'no-cors')).toBeNull();
}); });
it('ignores passive mixed content requests ', async() => { it('ignores passive mixed content requests ', async () => {
const scopeFetchSpy = spyOn(scope, 'fetch').and.callThrough(); const scopeFetchSpy = spyOn(scope, 'fetch').and.callThrough();
const getRequestUrls = () => const getRequestUrls = () =>
(scopeFetchSpy.calls.allArgs() as[Request][]).map(args => args[0].url); (scopeFetchSpy.calls.allArgs() as [Request][]).map(args => args[0].url);
const httpScopeUrl = 'http://mock.origin.dev'; const httpScopeUrl = 'http://mock.origin.dev';
const httpsScopeUrl = 'https://mock.origin.dev'; const httpsScopeUrl = 'https://mock.origin.dev';
@ -1476,7 +1470,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
// Test this bug: https://github.com/angular/angular/issues/27209 // Test this bug: https://github.com/angular/angular/issues/27209
it('fills previous versions of manifests with default navigation urls for backwards compatibility', it('fills previous versions of manifests with default navigation urls for backwards compatibility',
async() => { async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized; await driver.initialized;
scope.updateServerState(serverUpdate); scope.updateServerState(serverUpdate);
@ -1484,12 +1478,12 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
}); });
}); });
}); });
}); });
})(); })();
async function makeRequest( async function makeRequest(
scope: SwTestHarness, url: string, clientId: string | null = 'default', init?: Object): scope: SwTestHarness, url: string, clientId: string|null = 'default',
Promise<string|null> { init?: Object): Promise<string|null> {
const [resPromise, done] = scope.handleFetch(new MockRequest(url, init), clientId); const [resPromise, done] = scope.handleFetch(new MockRequest(url, init), clientId);
await done; await done;
const res = await resPromise; const res = await resPromise;
@ -1497,16 +1491,17 @@ async function makeRequest(
return res.text(); return res.text();
} }
return null; return null;
} }
function makeNavigationRequest( function makeNavigationRequest(
scope: SwTestHarness, url: string, clientId?: string | null, init: Object = {}): scope: SwTestHarness, url: string, clientId?: string|null,
Promise<string|null> { init: Object = {}): Promise<string|null> {
return makeRequest(scope, url, clientId, { return makeRequest(scope, url, clientId, {
headers: { headers: {
Accept: 'text/plain, text/html, text/css', Accept: 'text/plain, text/html, text/css',
...(init as any).headers, ...(init as any).headers,
}, },
mode: 'navigate', ...init, mode: 'navigate',
...init,
}); });
} }

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ChangeDetectorRef, Component, EventEmitter, Input, NO_ERRORS_SCHEMA, NgModule, NgModuleFactory, NgZone, OnChanges, OnDestroy, Output, SimpleChange, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core'; import {ChangeDetectorRef, Component, destroyPlatform, EventEmitter, forwardRef, Input, NgModule, NgModuleFactory, NgZone, NO_ERRORS_SCHEMA, OnChanges, OnDestroy, Output, SimpleChange, SimpleChanges, Testability} from '@angular/core';
import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
@ -23,7 +23,6 @@ declare global {
withEachNg1Version(() => { withEachNg1Version(() => {
describe('adapter: ng1 to ng2', () => { describe('adapter: ng1 to ng2', () => {
beforeEach(() => destroyPlatform()); beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform()); afterEach(() => destroyPlatform());
@ -232,7 +231,9 @@ withEachNg1Version(() => {
}) })
class Ng2 { class Ng2 {
l: any; l: any;
constructor() { this.l = l; } constructor() {
this.l = l;
}
} }
@NgModule({ @NgModule({
@ -262,7 +263,9 @@ withEachNg1Version(() => {
@Component({selector: 'my-app', template: '<my-child [value]="value"></my-child>'}) @Component({selector: 'my-app', template: '<my-child [value]="value"></my-child>'})
class AppComponent { class AppComponent {
value?: number; value?: number;
constructor() { appComponent = this; } constructor() {
appComponent = this;
}
} }
@Component({ @Component({
@ -272,7 +275,9 @@ withEachNg1Version(() => {
class ChildComponent { class ChildComponent {
valueFromPromise?: number; valueFromPromise?: number;
@Input() @Input()
set value(v: number) { expect(NgZone.isInAngularZone()).toBe(true); } set value(v: number) {
expect(NgZone.isInAngularZone()).toBe(true);
}
constructor(private zone: NgZone) {} constructor(private zone: NgZone) {}
@ -352,14 +357,15 @@ withEachNg1Version(() => {
const element = html('<ng2></ng2>'); const element = html('<ng2></ng2>');
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(document.body.textContent !)).toBe('It works'); expect(multiTrim(document.body.textContent!)).toBe('It works');
}); });
})); }));
it('should bind properties, events', async(() => { it('should bind properties, events', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = const ng1Module = angular.module_('ng1', []).value($EXCEPTION_HANDLER, (err: any) => {
angular.module_('ng1', []).value($EXCEPTION_HANDLER, (err: any) => { throw err; }); throw err;
});
ng1Module.run(($rootScope: any) => { ng1Module.run(($rootScope: any) => {
$rootScope.name = 'world'; $rootScope.name = 'world';
@ -409,8 +415,8 @@ withEachNg1Version(() => {
} }
const actValue = changes[prop].currentValue; const actValue = changes[prop].currentValue;
if (actValue != value) { if (actValue != value) {
throw new Error( throw new Error(`Expected changes record for'${prop}' to be '${
`Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`); value}' but was '${actValue}'`);
} }
}; };
@ -458,7 +464,7 @@ withEachNg1Version(() => {
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}}; | modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
</div>`); </div>`);
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(document.body.textContent !)) expect(multiTrim(document.body.textContent!))
.toEqual( .toEqual(
'ignore: -; ' + 'ignore: -; ' +
'literal: Text; interpolate: Hello world; ' + 'literal: Text; interpolate: Hello world; ' +
@ -466,7 +472,7 @@ withEachNg1Version(() => {
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
ref.ng1RootScope.$apply('name = "everyone"'); ref.ng1RootScope.$apply('name = "everyone"');
expect(multiTrim(document.body.textContent !)) expect(multiTrim(document.body.textContent!))
.toEqual( .toEqual(
'ignore: -; ' + 'ignore: -; ' +
'literal: Text; interpolate: Hello everyone; ' + 'literal: Text; interpolate: Hello everyone; ' +
@ -475,7 +481,6 @@ withEachNg1Version(() => {
ref.dispose(); ref.dispose();
}); });
})); }));
it('should support two-way binding and event listener', async(() => { it('should support two-way binding and event listener', async(() => {
@ -541,9 +546,9 @@ withEachNg1Version(() => {
ngOnChangesCount = 0; ngOnChangesCount = 0;
firstChangesCount = 0; firstChangesCount = 0;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
initialValue !: string; initialValue!: string;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input() foo !: string; @Input() foo!: string;
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
this.ngOnChangesCount++; this.ngOnChangesCount++;
@ -590,7 +595,9 @@ withEachNg1Version(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module_('ng1', []); const ng1Module = angular.module_('ng1', []);
ng1Module.run(($rootScope: any /** TODO #9100 */) => { $rootScope.modelA = 'A'; }); ng1Module.run(($rootScope: any /** TODO #9100 */) => {
$rootScope.modelA = 'A';
});
let ng2Instance: Ng2; let ng2Instance: Ng2;
@Component({selector: 'ng2', template: '{{_value}}'}) @Component({selector: 'ng2', template: '{{_value}}'})
@ -598,11 +605,21 @@ withEachNg1Version(() => {
private _value: any = ''; private _value: any = '';
private _onChangeCallback: (_: any) => void = () => {}; private _onChangeCallback: (_: any) => void = () => {};
private _onTouchedCallback: () => void = () => {}; private _onTouchedCallback: () => void = () => {};
constructor() { ng2Instance = this; } constructor() {
writeValue(value: any) { this._value = value; } ng2Instance = this;
registerOnChange(fn: any) { this._onChangeCallback = fn; } }
registerOnTouched(fn: any) { this._onTouchedCallback = fn; } writeValue(value: any) {
doTouch() { this._onTouchedCallback(); } this._value = value;
}
registerOnChange(fn: any) {
this._onChangeCallback = fn;
}
registerOnTouched(fn: any) {
this._onTouchedCallback = fn;
}
doTouch() {
this._onTouchedCallback();
}
doChange(newValue: string) { doChange(newValue: string) {
this._value = newValue; this._value = newValue;
this._onChangeCallback(newValue); this._onChangeCallback(newValue);
@ -653,14 +670,18 @@ withEachNg1Version(() => {
return { return {
template: '<div ng-if="!destroyIt"><ng2></ng2></div>', template: '<div ng-if="!destroyIt"><ng2></ng2></div>',
controller: function($rootScope: any, $timeout: Function) { controller: function($rootScope: any, $timeout: Function) {
$timeout(() => { $rootScope.destroyIt = true; }); $timeout(() => {
$rootScope.destroyIt = true;
});
} }
}; };
}); });
@Component({selector: 'ng2', template: 'test'}) @Component({selector: 'ng2', template: 'test'})
class Ng2 { class Ng2 {
ngOnDestroy() { onDestroyed.emit('destroyed'); } ngOnDestroy() {
onDestroyed.emit('destroyed');
}
} }
@NgModule({ @NgModule({
@ -673,7 +694,9 @@ withEachNg1Version(() => {
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
const element = html('<ng1></ng1>'); const element = html('<ng1></ng1>');
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
onDestroyed.subscribe(() => { ref.dispose(); }); onDestroyed.subscribe(() => {
ref.dispose();
});
}); });
})); }));
@ -689,7 +712,9 @@ withEachNg1Version(() => {
@Component({selector: 'ng2-inner', template: 'test'}) @Component({selector: 'ng2-inner', template: 'test'})
class Ng2InnerComponent implements OnDestroy { class Ng2InnerComponent implements OnDestroy {
ngOnDestroy() { destroyed = true; } ngOnDestroy() {
destroyed = true;
}
} }
@NgModule({ @NgModule({
@ -789,7 +814,7 @@ withEachNg1Version(() => {
@Component({selector: 'ng2', template: 'ng2-{{ itemId }}(<ng-content></ng-content>)'}) @Component({selector: 'ng2', template: 'ng2-{{ itemId }}(<ng-content></ng-content>)'})
class Ng2Component { class Ng2Component {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input() itemId !: string; @Input() itemId!: string;
} }
@NgModule({imports: [BrowserModule], declarations: [Ng2Component]}) @NgModule({imports: [BrowserModule], declarations: [Ng2Component]})
@ -838,7 +863,7 @@ withEachNg1Version(() => {
ng1Module.directive('rootComponent', adapter.downgradeNg2Component(RootComponent)); ng1Module.directive('rootComponent', adapter.downgradeNg2Component(RootComponent));
document.body.innerHTML = '<root-component></root-component>'; document.body.innerHTML = '<root-component></root-component>';
adapter.bootstrap(document.body.firstElementChild !, ['myExample']).ready((ref) => { adapter.bootstrap(document.body.firstElementChild!, ['myExample']).ready((ref) => {
expect(multiTrim(document.body.textContent)).toEqual('It works!'); expect(multiTrim(document.body.textContent)).toEqual('It works!');
ref.dispose(); ref.dispose();
}); });
@ -868,7 +893,9 @@ withEachNg1Version(() => {
dataA = 'foo'; dataA = 'foo';
dataB = 'bar'; dataB = 'bar';
constructor() { ng2ComponentInstance = this; } constructor() {
ng2ComponentInstance = this;
}
} }
// Define `ng1Module` // Define `ng1Module`
@ -888,8 +915,8 @@ withEachNg1Version(() => {
const element = html(`<ng2></ng2>`); const element = html(`<ng2></ng2>`);
adapter.bootstrap(element, ['ng1Module']).ready(ref => { adapter.bootstrap(element, ['ng1Module']).ready(ref => {
const ng1 = element.querySelector('ng1') !; const ng1 = element.querySelector('ng1')!;
const ng1Controller = angular.element(ng1).controller !('ng1'); const ng1Controller = angular.element(ng1).controller!('ng1');
expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar'); expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar');
@ -932,7 +959,9 @@ withEachNg1Version(() => {
dataA = {value: 'foo'}; dataA = {value: 'foo'};
dataB = {value: 'bar'}; dataB = {value: 'bar'};
constructor() { ng2ComponentInstance = this; } constructor() {
ng2ComponentInstance = this;
}
} }
// Define `ng1Module` // Define `ng1Module`
@ -952,8 +981,8 @@ withEachNg1Version(() => {
const element = html(`<ng2></ng2>`); const element = html(`<ng2></ng2>`);
adapter.bootstrap(element, ['ng1Module']).ready(ref => { adapter.bootstrap(element, ['ng1Module']).ready(ref => {
const ng1 = element.querySelector('ng1') !; const ng1 = element.querySelector('ng1')!;
const ng1Controller = angular.element(ng1).controller !('ng1'); const ng1Controller = angular.element(ng1).controller!('ng1');
expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar'); expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar');
@ -996,7 +1025,9 @@ withEachNg1Version(() => {
dataA = {value: 'foo'}; dataA = {value: 'foo'};
dataB = {value: 'bar'}; dataB = {value: 'bar'};
constructor() { ng2ComponentInstance = this; } constructor() {
ng2ComponentInstance = this;
}
} }
// Define `ng1Module` // Define `ng1Module`
@ -1016,8 +1047,8 @@ withEachNg1Version(() => {
const element = html(`<ng2></ng2>`); const element = html(`<ng2></ng2>`);
adapter.bootstrap(element, ['ng1Module']).ready(ref => { adapter.bootstrap(element, ['ng1Module']).ready(ref => {
const ng1 = element.querySelector('ng1') !; const ng1 = element.querySelector('ng1')!;
const ng1Controller = angular.element(ng1).controller !('ng1'); const ng1Controller = angular.element(ng1).controller!('ng1');
expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar'); expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar');
@ -1077,8 +1108,8 @@ withEachNg1Version(() => {
const element = html(`<ng2></ng2>`); const element = html(`<ng2></ng2>`);
adapter.bootstrap(element, ['ng1Module']).ready(ref => { adapter.bootstrap(element, ['ng1Module']).ready(ref => {
const ng1 = element.querySelector('ng1') !; const ng1 = element.querySelector('ng1')!;
const ng1Controller = angular.element(ng1).controller !('ng1'); const ng1Controller = angular.element(ng1).controller!('ng1');
expect(multiTrim(element.textContent)).toBe('Inside: - | Outside: foo, bar'); expect(multiTrim(element.textContent)).toBe('Inside: - | Outside: foo, bar');
@ -1204,7 +1235,9 @@ withEachNg1Version(() => {
restrict: 'E', restrict: 'E',
template: '{{someText}} - Length: {{data.length}}', template: '{{someText}} - Length: {{data.length}}',
scope: {data: '='}, scope: {data: '='},
controller: function($scope: any) { $scope.someText = 'ng1 - Data: ' + $scope.data; } controller: function($scope: any) {
$scope.someText = 'ng1 - Data: ' + $scope.data;
}
}; };
}; };
@ -1248,7 +1281,9 @@ withEachNg1Version(() => {
restrict: 'E', restrict: 'E',
template: '{{someText}} - Length: {{data.length}}', template: '{{someText}} - Length: {{data.length}}',
scope: {data: '='}, scope: {data: '='},
link: function($scope: any) { $scope.someText = 'ng1 - Data: ' + $scope.data; } link: function($scope: any) {
$scope.someText = 'ng1 - Data: ' + $scope.data;
}
}; };
}; };
@ -1291,7 +1326,9 @@ withEachNg1Version(() => {
cbFn(200, `${method}:${url}`); cbFn(200, `${method}:${url}`);
}); });
const ng1 = () => { return {templateUrl: 'url.html'}; }; const ng1 = () => {
return {templateUrl: 'url.html'};
};
ng1Module.directive('ng1', ng1); ng1Module.directive('ng1', ng1);
@Component({selector: 'ng2', template: '<ng1></ng1>'}) @Component({selector: 'ng2', template: '<ng1></ng1>'})
class Ng2 { class Ng2 {
@ -1320,7 +1357,13 @@ withEachNg1Version(() => {
cbFn(200, `${method}:${url}`); cbFn(200, `${method}:${url}`);
}); });
const ng1 = () => { return {templateUrl() { return 'url.html'; }}; }; const ng1 = () => {
return {
templateUrl() {
return 'url.html';
}
};
};
ng1Module.directive('ng1', ng1); ng1Module.directive('ng1', ng1);
@Component({selector: 'ng2', template: '<ng1></ng1>'}) @Component({selector: 'ng2', template: '<ng1></ng1>'})
class Ng2 { class Ng2 {
@ -1345,7 +1388,9 @@ withEachNg1Version(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module_('ng1', []); const ng1Module = angular.module_('ng1', []);
const ng1 = () => { return {template: ''}; }; const ng1 = () => {
return {template: ''};
};
ng1Module.directive('ng1', ng1); ng1Module.directive('ng1', ng1);
@Component({selector: 'ng2', template: '<ng1></ng1>'}) @Component({selector: 'ng2', template: '<ng1></ng1>'})
@ -1371,7 +1416,13 @@ withEachNg1Version(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module_('ng1', []); const ng1Module = angular.module_('ng1', []);
const ng1 = () => { return {template() { return ''; }}; }; const ng1 = () => {
return {
template() {
return '';
}
};
};
ng1Module.directive('ng1', ng1); ng1Module.directive('ng1', ng1);
@Component({selector: 'ng2', template: '<ng1></ng1>'}) @Component({selector: 'ng2', template: '<ng1></ng1>'})
@ -1398,7 +1449,9 @@ withEachNg1Version(() => {
const ng1Module = angular.module_('ng1', []); const ng1Module = angular.module_('ng1', []);
ng1Module.run(($templateCache: any) => $templateCache.put('url.html', 'WORKS')); ng1Module.run(($templateCache: any) => $templateCache.put('url.html', 'WORKS'));
const ng1 = () => { return {templateUrl: 'url.html'}; }; const ng1 = () => {
return {templateUrl: 'url.html'};
};
ng1Module.directive('ng1', ng1); ng1Module.directive('ng1', ng1);
@Component({selector: 'ng2', template: '<ng1></ng1>'}) @Component({selector: 'ng2', template: '<ng1></ng1>'})
@ -1431,13 +1484,20 @@ withEachNg1Version(() => {
'{{ctl.scope}}; {{ctl.isClass}}; {{ctl.hasElement}}; {{ctl.isPublished()}}', '{{ctl.scope}}; {{ctl.isClass}}; {{ctl.hasElement}}; {{ctl.isPublished()}}',
controllerAs: 'ctl', controllerAs: 'ctl',
controller: class { controller: class {
scope: any; hasElement: string; $element: any; isClass: any; scope: any;
hasElement: string;
$element: any;
isClass: any;
constructor($scope: any, $element: any) { constructor($scope: any, $element: any) {
this.verifyIAmAClass(); this.verifyIAmAClass();
this.scope = $scope.$parent.$parent == $scope.$root ? 'scope' : 'wrong-scope'; this.scope = $scope.$parent.$parent == $scope.$root ? 'scope' : 'wrong-scope';
this.hasElement = $element[0].nodeName; this.hasElement = $element[0].nodeName;
this.$element = $element; this.$element = $element;
} verifyIAmAClass() { this.isClass = 'isClass'; } isPublished() { }
verifyIAmAClass() {
this.isClass = 'isClass';
}
isPublished() {
return this.$element.controller('ng1') == this ? 'published' : 'not-published'; return this.$element.controller('ng1') == this ? 'published' : 'not-published';
} }
} }
@ -1543,7 +1603,9 @@ withEachNg1Version(() => {
template: '{{ctl.status}}', template: '{{ctl.status}}',
require: 'ng1', require: 'ng1',
controllerAs: 'ctrl', controllerAs: 'ctrl',
controller: class {status = 'WORKS';}, controller: class {
status = 'WORKS';
},
link: function(scope: any, element: any, attrs: any, linkController: any) { link: function(scope: any, element: any, attrs: any, linkController: any) {
expect(scope.$root).toEqual($rootScope); expect(scope.$root).toEqual($rootScope);
expect(element[0].nodeName).toEqual('NG1'); expect(element[0].nodeName).toEqual('NG1');
@ -1577,7 +1639,13 @@ withEachNg1Version(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module_('ng1', []); const ng1Module = angular.module_('ng1', []);
const parent = () => { return {controller: class {parent = 'PARENT';}}; }; const parent = () => {
return {
controller: class {
parent = 'PARENT';
}
};
};
const ng1 = () => { const ng1 = () => {
return { return {
scope: {title: '@'}, scope: {title: '@'},
@ -1585,7 +1653,9 @@ withEachNg1Version(() => {
template: '{{parent.parent}}:{{ng1.status}}', template: '{{parent.parent}}:{{ng1.status}}',
require: ['ng1', '^parent', '?^^notFound'], require: ['ng1', '^parent', '?^^notFound'],
controllerAs: 'ctrl', controllerAs: 'ctrl',
controller: class {status = 'WORKS';}, controller: class {
status = 'WORKS';
},
link: function(scope: any, element: any, attrs: any, linkControllers: any) { link: function(scope: any, element: any, attrs: any, linkControllers: any) {
expect(linkControllers[0].status).toEqual('WORKS'); expect(linkControllers[0].status).toEqual('WORKS');
expect(linkControllers[1].parent).toEqual('PARENT'); expect(linkControllers[1].parent).toEqual('PARENT');
@ -1633,15 +1703,20 @@ withEachNg1Version(() => {
scope: {}, scope: {},
bindToController: true, bindToController: true,
controllerAs: '$ctrl', controllerAs: '$ctrl',
controller: class {$onInit() { $onInitSpyA(); }} controller: class {
$onInit() {
$onInitSpyA();
}
}
})) }))
.directive( .directive('ng1B', () => ({
'ng1B', () => ({
template: '', template: '',
scope: {}, scope: {},
bindToController: false, bindToController: false,
controllerAs: '$ctrl', controllerAs: '$ctrl',
controller: function(this: any) { this.$onInit = $onInitSpyB; } controller: function(this: any) {
this.$onInit = $onInitSpyB;
}
})) }))
.directive('ng2', adapter.downgradeNg2Component(Ng2Component)); .directive('ng2', adapter.downgradeNg2Component(Ng2Component));
@ -1718,7 +1793,9 @@ withEachNg1Version(() => {
@Component({selector: 'ng2', template: '<ng1-a></ng1-a> | <ng1-b></ng1-b>'}) @Component({selector: 'ng2', template: '<ng1-a></ng1-a> | <ng1-b></ng1-b>'})
class Ng2Component { class Ng2Component {
constructor(cd: ChangeDetectorRef) { changeDetector = cd; } constructor(cd: ChangeDetectorRef) {
changeDetector = cd;
}
} }
angular.module_('ng1', []) angular.module_('ng1', [])
@ -1727,15 +1804,20 @@ withEachNg1Version(() => {
scope: {}, scope: {},
bindToController: true, bindToController: true,
controllerAs: '$ctrl', controllerAs: '$ctrl',
controller: class {$doCheck() { $doCheckSpyA(); }} controller: class {
$doCheck() {
$doCheckSpyA();
}
}
})) }))
.directive( .directive('ng1B', () => ({
'ng1B', () => ({
template: '', template: '',
scope: {}, scope: {},
bindToController: false, bindToController: false,
controllerAs: '$ctrl', controllerAs: '$ctrl',
controller: function(this: any) { this.$doCheck = $doCheckSpyB; } controller: function(this: any) {
this.$doCheck = $doCheckSpyB;
}
})) }))
.directive('ng2', adapter.downgradeNg2Component(Ng2Component)); .directive('ng2', adapter.downgradeNg2Component(Ng2Component));
@ -1773,7 +1855,9 @@ withEachNg1Version(() => {
@Component({selector: 'ng2', template: '<ng1-a></ng1-a> | <ng1-b></ng1-b>'}) @Component({selector: 'ng2', template: '<ng1-a></ng1-a> | <ng1-b></ng1-b>'})
class Ng2Component { class Ng2Component {
constructor(cd: ChangeDetectorRef) { changeDetector = cd; } constructor(cd: ChangeDetectorRef) {
changeDetector = cd;
}
} }
angular.module_('ng1', []) angular.module_('ng1', [])
@ -1835,15 +1919,20 @@ withEachNg1Version(() => {
scope: {}, scope: {},
bindToController: true, bindToController: true,
controllerAs: '$ctrl', controllerAs: '$ctrl',
controller: class {$postLink() { $postLinkSpyA(); }} controller: class {
$postLink() {
$postLinkSpyA();
}
}
})) }))
.directive( .directive('ng1B', () => ({
'ng1B', () => ({
template: '', template: '',
scope: {}, scope: {},
bindToController: false, bindToController: false,
controllerAs: '$ctrl', controllerAs: '$ctrl',
controller: function(this: any) { this.$postLink = $postLinkSpyB; } controller: function(this: any) {
this.$postLink = $postLinkSpyB;
}
})) }))
.directive('ng2', adapter.downgradeNg2Component(Ng2Component)); .directive('ng2', adapter.downgradeNg2Component(Ng2Component));
@ -1924,7 +2013,9 @@ withEachNg1Version(() => {
template: '<ng1-a [valA]="val"></ng1-a> | <ng1-b [valB]="val"></ng1-b>' template: '<ng1-a [valA]="val"></ng1-a> | <ng1-b [valB]="val"></ng1-b>'
}) })
class Ng2Component { class Ng2Component {
constructor() { ng2Instance = this; } constructor() {
ng2Instance = this;
}
} }
angular.module_('ng1', []) angular.module_('ng1', [])
@ -1937,15 +2028,15 @@ withEachNg1Version(() => {
this.$onChanges = $onChangesControllerSpyA; this.$onChanges = $onChangesControllerSpyA;
} }
})) }))
.directive( .directive('ng1B', () => ({
'ng1B',
() => ({
template: '', template: '',
scope: {valB: '<'}, scope: {valB: '<'},
bindToController: false, bindToController: false,
controllerAs: '$ctrl', controllerAs: '$ctrl',
controller: class { controller: class {
$onChanges(changes: SimpleChanges) { $onChangesControllerSpyB(changes); } $onChanges(changes: SimpleChanges) {
$onChangesControllerSpyB(changes);
}
} }
})) }))
.directive('ng2', adapter.downgradeNg2Component(Ng2Component)) .directive('ng2', adapter.downgradeNg2Component(Ng2Component))
@ -2022,7 +2113,9 @@ withEachNg1Version(() => {
}) })
class Ng2Component { class Ng2Component {
ng2Destroy: boolean = false; ng2Destroy: boolean = false;
constructor() { ng2ComponentInstance = this; } constructor() {
ng2ComponentInstance = this;
}
} }
// On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3), // On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3),
@ -2036,15 +2129,20 @@ withEachNg1Version(() => {
scope: {}, scope: {},
bindToController: true, bindToController: true,
controllerAs: '$ctrl', controllerAs: '$ctrl',
controller: class {$onDestroy() { $onDestroySpyA(); }} controller: class {
$onDestroy() {
$onDestroySpyA();
}
}
})) }))
.directive( .directive('ng1B', () => ({
'ng1B', () => ({
template: '', template: '',
scope: {}, scope: {},
bindToController: false, bindToController: false,
controllerAs: '$ctrl', controllerAs: '$ctrl',
controller: function(this: any) { this.$onDestroy = $onDestroySpyB; } controller: function(this: any) {
this.$onDestroy = $onDestroySpyB;
}
})) }))
.directive('ng2', adapter.downgradeNg2Component(Ng2Component)); .directive('ng2', adapter.downgradeNg2Component(Ng2Component));
@ -2112,7 +2210,9 @@ withEachNg1Version(() => {
}) })
class Ng2Component { class Ng2Component {
ng2Destroy: boolean = false; ng2Destroy: boolean = false;
constructor() { ng2ComponentInstance = this; } constructor() {
ng2ComponentInstance = this;
}
} }
// On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3), // On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3),
@ -2187,7 +2287,9 @@ withEachNg1Version(() => {
@Component({selector: 'ng2', template: '<div *ngIf="!ng2Destroy"><ng1></ng1></div>'}) @Component({selector: 'ng2', template: '<div *ngIf="!ng2Destroy"><ng1></ng1></div>'})
class Ng2Component { class Ng2Component {
ng2Destroy: boolean = false; ng2Destroy: boolean = false;
constructor() { ng2ComponentInstance = this; } constructor() {
ng2ComponentInstance = this;
}
} }
// On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3), // On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3),
@ -2233,7 +2335,9 @@ withEachNg1Version(() => {
@Component({selector: 'ng2', template: '<div *ngIf="!ng2Destroy"><ng1></ng1></div>'}) @Component({selector: 'ng2', template: '<div *ngIf="!ng2Destroy"><ng1></ng1></div>'})
class Ng2Component { class Ng2Component {
ng2Destroy: boolean = false; ng2Destroy: boolean = false;
constructor() { ng2ComponentInstance = this; } constructor() {
ng2ComponentInstance = this;
}
} }
// On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3), // On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3),
@ -2245,8 +2349,8 @@ withEachNg1Version(() => {
.component('ng1', { .component('ng1', {
controller: class { controller: class {
constructor(private $element: angular.IAugmentedJQuery) {} $onInit() { constructor(private $element: angular.IAugmentedJQuery) {} $onInit() {
this.$element.on !('$destroy', elementDestroyListener); this.$element.on!('$destroy', elementDestroyListener);
this.$element.contents !().on !('$destroy', descendantDestroyListener); this.$element.contents!().on!('$destroy', descendantDestroyListener);
} }
}, },
template: '<div></div>' template: '<div></div>'
@ -2287,8 +2391,8 @@ withEachNg1Version(() => {
const ng1Component: angular.IComponent = { const ng1Component: angular.IComponent = {
controller: class { controller: class {
constructor(private $element: angular.IAugmentedJQuery) {} $onInit() { constructor(private $element: angular.IAugmentedJQuery) {} $onInit() {
this.$element.data !('test', 1); this.$element.data!('test', 1);
this.$element.contents !().data !('test', 2); this.$element.contents!().data!('test', 2);
ng1ComponentElement = this.$element; ng1ComponentElement = this.$element;
} }
@ -2301,7 +2405,9 @@ withEachNg1Version(() => {
class Ng2ComponentA { class Ng2ComponentA {
destroyIt = false; destroyIt = false;
constructor() { ng2ComponentAInstance = this; } constructor() {
ng2ComponentAInstance = this;
}
} }
@Component({selector: 'ng2B', template: '<ng1></ng1>'}) @Component({selector: 'ng2B', template: '<ng1></ng1>'})
@ -2330,15 +2436,15 @@ withEachNg1Version(() => {
const $rootScope = ref.ng1RootScope as any; const $rootScope = ref.ng1RootScope as any;
tick(); tick();
$rootScope.$digest(); $rootScope.$digest();
expect(ng1ComponentElement.data !('test')).toBe(1); expect(ng1ComponentElement.data!('test')).toBe(1);
expect(ng1ComponentElement.contents !().data !('test')).toBe(2); expect(ng1ComponentElement.contents!().data!('test')).toBe(2);
ng2ComponentAInstance.destroyIt = true; ng2ComponentAInstance.destroyIt = true;
tick(); tick();
$rootScope.$digest(); $rootScope.$digest();
expect(ng1ComponentElement.data !('test')).toBeUndefined(); expect(ng1ComponentElement.data!('test')).toBeUndefined();
expect(ng1ComponentElement.contents !().data !('test')).toBeUndefined(); expect(ng1ComponentElement.contents!().data!('test')).toBeUndefined();
}); });
})); }));
@ -2353,10 +2459,10 @@ withEachNg1Version(() => {
const ng1Component: angular.IComponent = { const ng1Component: angular.IComponent = {
controller: class { controller: class {
constructor(private $element: angular.IAugmentedJQuery) {} $onInit() { constructor(private $element: angular.IAugmentedJQuery) {} $onInit() {
ng1DescendantElement = this.$element.contents !(); ng1DescendantElement = this.$element.contents!();
this.$element.on !('click', elementClickListener); this.$element.on!('click', elementClickListener);
ng1DescendantElement.on !('click', descendantClickListener); ng1DescendantElement.on!('click', descendantClickListener);
} }
}, },
template: '<div></div>' template: '<div></div>'
@ -2367,7 +2473,9 @@ withEachNg1Version(() => {
class Ng2ComponentA { class Ng2ComponentA {
destroyIt = false; destroyIt = false;
constructor() { ng2ComponentAInstance = this; } constructor() {
ng2ComponentAInstance = this;
}
} }
@Component({selector: 'ng2B', template: '<ng1></ng1>'}) @Component({selector: 'ng2B', template: '<ng1></ng1>'})
@ -2420,7 +2528,11 @@ withEachNg1Version(() => {
const ng1Directive: angular.IDirective = { const ng1Directive: angular.IDirective = {
template: '', template: '',
link: {pre: () => log.push('ng1-pre')}, link: {pre: () => log.push('ng1-pre')},
controller: class {constructor() { log.push('ng1-ctrl'); }} controller: class {
constructor() {
log.push('ng1-ctrl');
}
}
}; };
// Define `Ng2Component` // Define `Ng2Component`
@ -2577,7 +2689,11 @@ withEachNg1Version(() => {
const ng1Directive: angular.IDirective = { const ng1Directive: angular.IDirective = {
template: '', template: '',
link: () => log.push('ng1-post'), link: () => log.push('ng1-post'),
controller: class {$postLink() { log.push('ng1-$post'); }} controller: class {
$postLink() {
log.push('ng1-$post');
}
}
}; };
// Define `Ng2Component` // Define `Ng2Component`
@ -2627,13 +2743,17 @@ withEachNg1Version(() => {
class Ng2ComponentA { class Ng2ComponentA {
value = 'foo'; value = 'foo';
showB = false; showB = false;
constructor() { ng2ComponentAInstance = this; } constructor() {
ng2ComponentAInstance = this;
}
} }
@Component({selector: 'ng2B', template: 'ng2B({{ value }})'}) @Component({selector: 'ng2B', template: 'ng2B({{ value }})'})
class Ng2ComponentB { class Ng2ComponentB {
value = 'bar'; value = 'bar';
constructor() { ng2ComponentBInstance = this; } constructor() {
ng2ComponentBInstance = this;
}
} }
// Define `ng1Module` // Define `ng1Module`
@ -2678,7 +2798,10 @@ withEachNg1Version(() => {
template: 'ng1(<div ng-transclude>{{ $ctrl.value }}</div>)', template: 'ng1(<div ng-transclude>{{ $ctrl.value }}</div>)',
transclude: true, transclude: true,
controller: class { controller: class {
value = 'from-ng1'; constructor() { ng1ControllerInstances.push(this); } value = 'from-ng1';
constructor() {
ng1ControllerInstances.push(this);
}
} }
}; };
@ -2697,7 +2820,9 @@ withEachNg1Version(() => {
}) })
class Ng2Component { class Ng2Component {
value = 'from-ng2'; value = 'from-ng2';
constructor() { ng2ComponentInstance = this; } constructor() {
ng2ComponentInstance = this;
}
} }
// Define `ng1Module` // Define `ng1Module`
@ -2756,7 +2881,9 @@ withEachNg1Version(() => {
class Ng2Component { class Ng2Component {
x = 'foo'; x = 'foo';
y = 'bar'; y = 'bar';
constructor() { ng2ComponentInstance = this; } constructor() {
ng2ComponentInstance = this;
}
} }
// Define `ng1Module` // Define `ng1Module`
@ -2798,8 +2925,12 @@ withEachNg1Version(() => {
const ng1Component: angular.IComponent = { const ng1Component: angular.IComponent = {
template: 'ng1(default(<div ng-transclude="">fallback-{{ $ctrl.value }}</div>))', template: 'ng1(default(<div ng-transclude="">fallback-{{ $ctrl.value }}</div>))',
transclude: {slotX: 'contentX', slotY: 'contentY'}, transclude: {slotX: 'contentX', slotY: 'contentY'},
controller: controller: class {
class {value = 'ng1'; constructor() { ng1ControllerInstances.push(this); }} value = 'ng1';
constructor() {
ng1ControllerInstances.push(this);
}
}
}; };
// Define `Ng2Component` // Define `Ng2Component`
@ -2830,7 +2961,9 @@ withEachNg1Version(() => {
class Ng2Component { class Ng2Component {
x = 'foo'; x = 'foo';
y = 'bar'; y = 'bar';
constructor() { ng2ComponentInstance = this; } constructor() {
ng2ComponentInstance = this;
}
} }
// Define `ng1Module` // Define `ng1Module`
@ -2880,7 +3013,11 @@ withEachNg1Version(() => {
)`, )`,
transclude: {slotX: '?contentX', slotY: '?contentY'}, transclude: {slotX: '?contentX', slotY: '?contentY'},
controller: class { controller: class {
x = 'ng1X'; y = 'ng1Y'; constructor() { ng1ControllerInstances.push(this); } x = 'ng1X';
y = 'ng1Y';
constructor() {
ng1ControllerInstances.push(this);
}
} }
}; };
@ -2896,7 +3033,9 @@ withEachNg1Version(() => {
class Ng2Component { class Ng2Component {
x = 'ng2X'; x = 'ng2X';
y = 'ng2Y'; y = 'ng2Y';
constructor() { ng2ComponentInstance = this; } constructor() {
ng2ComponentInstance = this;
}
} }
// Define `ng1Module` // Define `ng1Module`
@ -3000,7 +3139,9 @@ withEachNg1Version(() => {
x = 'foo'; x = 'foo';
y = 'bar'; y = 'bar';
show = true; show = true;
constructor() { ng2ComponentInstance = this; } constructor() {
ng2ComponentInstance = this;
}
} }
// Define `ng1Module` // Define `ng1Module`
@ -3202,13 +3343,18 @@ withEachNg1Version(() => {
const ng1Module = angular.module_('ng1', []); const ng1Module = angular.module_('ng1', []);
let a1Injector: angular.IInjectorService|undefined; let a1Injector: angular.IInjectorService|undefined;
ng1Module.run([ ng1Module.run([
'$injector', function($injector: angular.IInjectorService) { a1Injector = $injector; } '$injector',
function($injector: angular.IInjectorService) {
a1Injector = $injector;
}
]); ]);
const element = html('<div></div>'); const element = html('<div></div>');
window.name = 'NG_DEFER_BOOTSTRAP!' + window.name; window.name = 'NG_DEFER_BOOTSTRAP!' + window.name;
adapter.bootstrap(element, [ng1Module.name]).ready((ref) => { ref.dispose(); }); adapter.bootstrap(element, [ng1Module.name]).ready((ref) => {
ref.dispose();
});
tick(100); tick(100);
@ -3275,7 +3421,7 @@ withEachNg1Version(() => {
document.body.innerHTML = '<ng2 name="World">project</ng2>'; document.body.innerHTML = '<ng2 name="World">project</ng2>';
adapter.bootstrap(document.body.firstElementChild !, ['myExample']).ready((ref) => { adapter.bootstrap(document.body.firstElementChild!, ['myExample']).ready((ref) => {
expect(multiTrim(document.body.textContent)) expect(multiTrim(document.body.textContent))
.toEqual('ng2[ng1[Hello World!](transclude)](project)'); .toEqual('ng2[ng1[Hello World!](transclude)](project)');
ref.dispose(); ref.dispose();

View File

@ -284,11 +284,7 @@ describe('bluebird promise', () => {
.each( .each(
BluebirdPromise.map(arr, (item: number) => BluebirdPromise.resolve(item)), BluebirdPromise.map(arr, (item: number) => BluebirdPromise.resolve(item)),
(r: number, idx: number) => { (r: number, idx: number) => {
<<<<<<< HEAD
expect(r).toBe(arr[idx]); expect(r).toBe(arr[idx]);
=======
expect(r === arr[idx]).toBeTrue();
>>>>>>> 253023848d... build: update jasmine to 3.5
expect(Zone.current.name).toEqual('bluebird'); expect(Zone.current.name).toEqual('bluebird');
}) })
.then((r: any) => { .then((r: any) => {
@ -309,11 +305,7 @@ describe('bluebird promise', () => {
.mapSeries( .mapSeries(
BluebirdPromise.map(arr, (item: number) => BluebirdPromise.resolve(item)), BluebirdPromise.map(arr, (item: number) => BluebirdPromise.resolve(item)),
(r: number, idx: number) => { (r: number, idx: number) => {
<<<<<<< HEAD
expect(r).toBe(arr[idx]); expect(r).toBe(arr[idx]);
=======
expect(r === arr[idx]).toBeTrue();
>>>>>>> 253023848d... build: update jasmine to 3.5
expect(Zone.current.name).toEqual('bluebird'); expect(Zone.current.name).toEqual('bluebird');
}) })
.then((r: any) => { .then((r: any) => {

View File

@ -21,7 +21,9 @@ describe('crypto test', () => {
const zoneASpec = { const zoneASpec = {
name: 'A', name: 'A',
onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
Task => { return delegate.scheduleTask(targetZone, task); } Task => {
return delegate.scheduleTask(targetZone, task);
}
}; };
const zoneA = Zone.current.fork(zoneASpec); const zoneA = Zone.current.fork(zoneASpec);
spyOn(zoneASpec, 'onScheduleTask').and.callThrough(); spyOn(zoneASpec, 'onScheduleTask').and.callThrough();
@ -44,7 +46,9 @@ describe('crypto test', () => {
const zoneASpec = { const zoneASpec = {
name: 'A', name: 'A',
onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
Task => { return delegate.scheduleTask(targetZone, task); } Task => {
return delegate.scheduleTask(targetZone, task);
}
}; };
const zoneA = Zone.current.fork(zoneASpec); const zoneA = Zone.current.fork(zoneASpec);
spyOn(zoneASpec, 'onScheduleTask').and.callThrough(); spyOn(zoneASpec, 'onScheduleTask').and.callThrough();