From 41667de778322ac946ed9e8a1d6647a096f1b371 Mon Sep 17 00:00:00 2001 From: JiaLiPassion Date: Tue, 31 Mar 2020 00:22:25 +0900 Subject: [PATCH] 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 --- packages/common/http/test/xhr_spec.ts | 12 +- .../ngcc/test/integration/ngcc_spec.ts | 2 +- .../unlocker_spec.ts | 1 + .../test/cached_file_system_spec.ts | 6 +- .../test/node_js_file_system_spec.ts | 37 +- packages/compiler-cli/test/ngc_spec.ts | 32 +- .../compiler/test/output/value_util_spec.ts | 4 +- .../animation_query_integration_spec.ts | 4929 ++++++++--------- .../change_detection_integration_spec.ts | 3256 +++++------ .../core/test/render3/global_utils_spec.ts | 39 +- .../render3/node_selector_matcher_spec.ts | 40 +- packages/elements/test/utils_spec.ts | 15 +- .../di/ts/forward_ref/forward_ref_spec.ts | 6 +- .../test/dom/events/hammer_gestures_spec.ts | 14 +- .../test/dom/events/key_events_spec.ts | 2 - packages/router/test/integration.spec.ts | 3 +- packages/service-worker/test/comm_spec.ts | 73 +- packages/service-worker/test/module_spec.ts | 22 +- .../service-worker/worker/test/happy_spec.ts | 2817 +++++----- .../upgrade/src/dynamic/test/upgrade_spec.ts | 412 +- packages/zone.js/test/extra/bluebird.spec.ts | 8 - packages/zone.js/test/node/crypto.spec.ts | 8 +- 22 files changed, 5993 insertions(+), 5745 deletions(-) diff --git a/packages/common/http/test/xhr_spec.ts b/packages/common/http/test/xhr_spec.ts index 0d08c3a91e..d7b4147a2b 100644 --- a/packages/common/http/test/xhr_spec.ts +++ b/packages/common/http/test/xhr_spec.ts @@ -29,8 +29,8 @@ const XSSI_PREFIX = ')]}\'\n'; { describe('XhrBackend', () => { - let factory: MockXhrFactory = null !; - let backend: HttpXhrBackend = null !; + let factory: MockXhrFactory = null!; + let backend: HttpXhrBackend = null!; beforeEach(() => { factory = new MockXhrFactory(); backend = new HttpXhrBackend(factory); @@ -92,7 +92,7 @@ const XSSI_PREFIX = ')]}\'\n'; factory.mock.mockFlush(200, 'OK', JSON.stringify({data: 'some data'})); expect(events.length).toBe(2); 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', () => { 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'})); expect(events.length).toBe(2); 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', () => { const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'}))); factory.mock.mockFlush(500, 'Error', XSSI_PREFIX + JSON.stringify({data: 'some data'})); expect(events.length).toBe(2); 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', () => { 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'})); expect(events.length).toBe(2); 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 => { backend.handle(TEST_POST).subscribe(undefined, (err: HttpErrorResponse) => { diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index be342392a5..cab8b9852d 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -38,7 +38,7 @@ runInEachFileSystem(() => { initMockFileSystem(fs, testFiles); // 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', () => { diff --git a/packages/compiler-cli/ngcc/test/locking/lockfile_with_child_process/unlocker_spec.ts b/packages/compiler-cli/ngcc/test/locking/lockfile_with_child_process/unlocker_spec.ts index 0e99ed7417..80ad758291 100644 --- a/packages/compiler-cli/ngcc/test/locking/lockfile_with_child_process/unlocker_spec.ts +++ b/packages/compiler-cli/ngcc/test/locking/lockfile_with_child_process/unlocker_spec.ts @@ -13,6 +13,7 @@ describe('unlocker', () => { spyOn(process, 'on'); require('../../../src/locking/lock_file_with_child_process/unlocker'); // 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)); }); }); diff --git a/packages/compiler-cli/src/ngtsc/file_system/test/cached_file_system_spec.ts b/packages/compiler-cli/src/ngtsc/file_system/test/cached_file_system_spec.ts index bcba05ed8f..d8bc40dfd5 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/test/cached_file_system_spec.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/test/cached_file_system_spec.ts @@ -47,7 +47,7 @@ describe('CachedFileSystem', () => { let lstatSpy: jasmine.Spy; beforeEach(() => { // 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', () => { @@ -93,7 +93,7 @@ describe('CachedFileSystem', () => { describe('invalidateCaches()', () => { 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'); fs.readFile(abcPath); // Call once to fill the cache spy.calls.reset(); @@ -230,7 +230,7 @@ describe('CachedFileSystem', () => { describe('moveFile()', () => { beforeEach(() => { // `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', () => { diff --git a/packages/compiler-cli/src/ngtsc/file_system/test/node_js_file_system_spec.ts b/packages/compiler-cli/src/ngtsc/file_system/test/node_js_file_system_spec.ts index 71babf1878..eca3261239 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/test/node_js_file_system_spec.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/test/node_js_file_system_spec.ts @@ -69,6 +69,7 @@ describe('NodeJSFileSystem', () => { const result = fs.readdir(abcPath); expect(result).toEqual([relativeFrom('x'), relativeFrom('y/z')]); // 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); }); }); @@ -90,6 +91,7 @@ describe('NodeJSFileSystem', () => { const result = fs.stat(abcPath); expect(result).toBe(stats); // 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); }); }); @@ -173,12 +175,14 @@ describe('NodeJSFileSystem', () => { } return false; }); - spyOn(fs, 'stat').and.returnValue({ isDirectory: () => true } as any); - const mkdirSyncSpy = spyOn(realFs, 'mkdirSync').and.callFake(((path: string) => { - if (path === abcPath) { - throw new Error('It exists already. Supposedly.'); - } - }) as any); + spyOn(fs, 'stat').and.returnValue({isDirectory: () => true} as any); + const mkdirSyncSpy = + spyOn(realFs, 'mkdirSync').and.callFake(((path: string) => { + if (path === abcPath) { + throw new Error( + 'It exists already. Supposedly.'); + } + }) as any); fs.ensureDir(abcPath); expect(mkdirSyncSpy).toHaveBeenCalledTimes(3); @@ -188,11 +192,12 @@ describe('NodeJSFileSystem', () => { it('should fail if creating the directory throws and the directory does not exist', () => { spyOn(fs, 'exists').and.returnValue(false); - spyOn(realFs, 'mkdirSync').and.callFake(((path: string) => { - if (path === abcPath) { - throw new Error('Unable to create directory (for whatever reason).'); - } - }) as any); + spyOn(realFs, 'mkdirSync') + .and.callFake(((path: string) => { + if (path === abcPath) { + throw new Error('Unable to create directory (for whatever reason).'); + } + }) as any); expect(() => fs.ensureDir(abcPath)) .toThrowError('Unable to create directory (for whatever reason).'); @@ -212,12 +217,12 @@ describe('NodeJSFileSystem', () => { } 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) => { - if (path === abcPath) { - throw new Error('It exists already. Supposedly.'); - } - }) as any); + if (path === abcPath) { + throw new Error('It exists already. Supposedly.'); + } + }) as any); expect(() => fs.ensureDir(abcPath)).toThrowError('It exists already. Supposedly.'); expect(isDirectorySpy).toHaveBeenCalledTimes(1); diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 93ccda8f6c..d2c783ad6c 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -41,7 +41,9 @@ describe('ngc transformer command-line', () => { basePath = support.basePath; outDir = path.join(basePath, 'built'); 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', `{ "compilerOptions": { @@ -96,8 +98,9 @@ describe('ngc transformer command-line', () => { }); describe('errors', () => { - - beforeEach(() => { errorSpy.and.stub(); }); + beforeEach(() => { + errorSpy.and.stub(); + }); it('should not print the stack trace if user input file does not exist', () => { writeConfig(`{ @@ -231,7 +234,6 @@ describe('ngc transformer command-line', () => { }); describe('compile ngfactory files', () => { - it('should compile ngfactory files that are not referenced by root files', () => { writeConfig(`{ "extends": "./tsconfig-base.json", @@ -1122,7 +1124,6 @@ describe('ngc transformer command-line', () => { }); describe('with external symbol re-exports enabled', () => { - it('should be able to compile multiple libraries with summaries', () => { // Note: we need to emit the generated code for the libraries // 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; const timerToken = 100; // 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) => { timer = callback; return timerToken; }); // 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) => { if (token == timerToken) { 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) { writeConfig(`{ @@ -1672,11 +1677,13 @@ describe('ngc transformer command-line', () => { `); })); - it('should recompile when the html file changes', - expectRecompile(() => { write('greet.html', '

Hello {{name}} again!

'); })); + it('should recompile when the html file changes', expectRecompile(() => { + write('greet.html', '

Hello {{name}} again!

'); + })); - it('should recompile when the css file changes', - expectRecompile(() => { write('greet.css', `p.greeting { color: blue }`); })); + it('should recompile when the css file changes', expectRecompile(() => { + write('greet.css', `p.greeting { color: blue }`); + })); }); describe('regressions', () => { @@ -2041,8 +2048,8 @@ describe('ngc transformer command-line', () => { expect(exitCode).toBe(1, 'Compile was expected to fail'); const srcPathWithSep = `lib/`; expect(messages[0]) - .toEqual( - `${srcPathWithSep}test.component.ts(6,21): Error during template compile of 'TestComponent' + .toEqual(`${ + srcPathWithSep}test.component.ts(6,21): Error during template compile of 'TestComponent' Tagged template expressions are not supported in metadata in 't1' 't1' references 't2' at ${srcPathWithSep}indirect1.ts(3,27) 't2' contains the error at ${srcPathWithSep}indirect2.ts(4,27). @@ -2051,7 +2058,6 @@ describe('ngc transformer command-line', () => { }); describe('tree shakeable services', () => { - function compileService(source: string): string { write('service.ts', source); diff --git a/packages/compiler/test/output/value_util_spec.ts b/packages/compiler/test/output/value_util_spec.ts index 536cf81527..0098608151 100644 --- a/packages/compiler/test/output/value_util_spec.ts +++ b/packages/compiler/test/output/value_util_spec.ts @@ -13,8 +13,8 @@ describe('convertValueToOutputAst', () => { it('should convert all array elements, including undefined', () => { const ctx = null; const value = new Array(3).concat('foo'); - const expr = convertValueToOutputAst(ctx !, value) as o.LiteralArrayExpr; - expect(expr instanceof o.LiteralArrayExpr); + const expr = convertValueToOutputAst(ctx!, value) as o.LiteralArrayExpr; + expect(expr instanceof o.LiteralArrayExpr).toBe(true); expect(expr.entries.length).toBe(4); for (let i = 0; i < 4; ++i) { expect(expr.entries[i] instanceof o.Expression).toBe(true); diff --git a/packages/core/test/animation/animation_query_integration_spec.ts b/packages/core/test/animation/animation_query_integration_spec.ts index 4917316870..ff17a0f9aa 100644 --- a/packages/core/test/animation/animation_query_integration_spec.ts +++ b/packages/core/test/animation/animation_query_integration_spec.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {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 {matchesElement} from '@angular/animations/browser/src/render/shared'; import {TransitionAnimationPlayer} from '@angular/animations/browser/src/render/transition_animation_engine'; @@ -13,35 +13,37 @@ import {ENTER_CLASSNAME, LEAVE_CLASSNAME} from '@angular/animations/browser/src/ import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing'; import {CommonModule} from '@angular/common'; 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 {HostListener} from '../../src/metadata/directives'; (function() { - // these tests are only mean't to be run within the DOM (for now) - if (isNode) return; +// these tests are only mean't to be run within the DOM (for now) +if (isNode) return; - describe('animation query tests', function() { - function getLog(): MockAnimationPlayer[] { - return MockAnimationDriver.log as MockAnimationPlayer[]; - } +describe('animation query tests', function() { + function getLog(): MockAnimationPlayer[] { + return MockAnimationDriver.log as MockAnimationPlayer[]; + } - function resetLog() { MockAnimationDriver.log = []; } + function resetLog() { + MockAnimationDriver.log = []; + } - beforeEach(() => { - resetLog(); - TestBed.configureTestingModule({ - providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}], - imports: [BrowserAnimationsModule, CommonModule] - }); + beforeEach(() => { + resetLog(); + TestBed.configureTestingModule({ + providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}], + imports: [BrowserAnimationsModule, CommonModule] }); + }); - describe('query()', () => { - it('should be able to query all elements that contain animation triggers via @*', () => { - @Component({ - selector: 'ani-cmp', - template: ` + describe('query()', () => { + it('should be able to query all elements that contain animation triggers via @*', () => { + @Component({ + selector: 'ani-cmp', + template: `
@@ -50,75 +52,71 @@ import {HostListener} from '../../src/metadata/directives';
`, - animations: [ - trigger( - 'parent', - [ - transition( - '* => go', - [ - query( - '@*', - [ - style({ backgroundColor: 'blue' }), - animate(1000, style({backgroundColor: 'red'})), - ]), - ]), - ]), - trigger( - 'a', - [ - transition('* => 1', [ - animate(1000, style({ opacity: 0 })) - ]), - ]), - trigger( - 'b', - [ - transition('* => 1', [ - animate(1000, style({ opacity: 0 })), - query('.b-inner', [ - animate(1000, style({ opacity: 0 })) + animations: [ + trigger( + 'parent', + [ + transition( + '* => go', + [ + query( + '@*', + [ + style({backgroundColor: 'blue'}), + animate(1000, style({backgroundColor: 'red'})), + ]), ]), - ]), - ]), - trigger( - 'c', - [ - transition('* => 1', [ - animate(1000, style({ opacity: 0 })) - ]), - ]), - ] - }) - class Cmp { - public exp0: any; - public exp1: any; - public exp2: any; - } + ]), + trigger( + 'a', + [ + transition('* => 1', [animate(1000, style({opacity: 0}))]), + ]), + trigger( + 'b', + [ + transition( + '* => 1', + [ + animate(1000, style({opacity: 0})), + query('.b-inner', [animate(1000, style({opacity: 0}))]), + ]), + ]), + trigger( + 'c', + [ + transition('* => 1', [animate(1000, style({opacity: 0}))]), + ]), + ] + }) + class Cmp { + public exp0: any; + public exp1: any; + public exp2: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp0 = 'go'; - fixture.detectChanges(); + cmp.exp0 = 'go'; + fixture.detectChanges(); - let players = getLog(); - expect(players.length).toEqual(3); // a,b,c - resetLog(); + let players = getLog(); + expect(players.length).toEqual(3); // a,b,c + resetLog(); - const [p1, p2, p3] = players; - expect(p1.element.classList.contains('a')).toBeTruthy(); - expect(p2.element.classList.contains('b')).toBeTruthy(); - expect(p3.element.classList.contains('c')).toBeTruthy(); - }); + const [p1, p2, p3] = players; + expect(p1.element.classList.contains('a')).toBeTruthy(); + expect(p2.element.classList.contains('b')).toBeTruthy(); + expect(p3.element.classList.contains('c')).toBeTruthy(); + }); - it('should be able to query currently animating elements via :animating', () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should be able to query currently animating elements via :animating', () => { + @Component({ + selector: 'ani-cmp', + template: `
@@ -127,95 +125,91 @@ import {HostListener} from '../../src/metadata/directives';
`, - animations: [ - trigger( - 'parent', - [ - transition( - '* => go', - [ - query( - ':animating', - [ - style({ backgroundColor: 'blue' }), - animate(1000, style({backgroundColor: 'red'})), - ]), - ]), - ]), - trigger( - 'a', - [ - transition('* => 1', [ - animate(1000, style({ opacity: 0 })) - ]), - ]), - trigger( - 'b', - [ - transition('* => 1', [ - animate(1000, style({ opacity: 0 })), - query('.b-inner', [ - animate(1000, style({ opacity: 0 })) + animations: [ + trigger( + 'parent', + [ + transition( + '* => go', + [ + query( + ':animating', + [ + style({backgroundColor: 'blue'}), + animate(1000, style({backgroundColor: 'red'})), + ]), ]), - ]), - ]), - trigger( - 'c', - [ - transition('* => 1', [ - animate(1000, style({ opacity: 0 })) - ]), - ]), - ] - }) - class Cmp { - public exp0: any; - public exp1: any; - public exp2: any; - public exp3: any; - } + ]), + trigger( + 'a', + [ + transition('* => 1', [animate(1000, style({opacity: 0}))]), + ]), + trigger( + 'b', + [ + transition( + '* => 1', + [ + animate(1000, style({opacity: 0})), + query('.b-inner', [animate(1000, style({opacity: 0}))]), + ]), + ]), + trigger( + 'c', + [ + transition('* => 1', [animate(1000, style({opacity: 0}))]), + ]), + ] + }) + class Cmp { + public exp0: any; + public exp1: any; + public exp2: any; + public exp3: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp0 = ''; - cmp.exp1 = 1; - cmp.exp2 = 1; - // note that exp3 is skipped here - fixture.detectChanges(); + cmp.exp0 = ''; + cmp.exp1 = 1; + cmp.exp2 = 1; + // note that exp3 is skipped here + fixture.detectChanges(); - let players = getLog(); - expect(players.length).toEqual(3); // a,b,b-inner and not c - resetLog(); + let players = getLog(); + expect(players.length).toEqual(3); // a,b,b-inner and not c + resetLog(); - cmp.exp0 = 'go'; - fixture.detectChanges(); + cmp.exp0 = 'go'; + fixture.detectChanges(); - const expectedKeyframes = [ - {backgroundColor: 'blue', offset: 0}, - {backgroundColor: 'red', offset: 1}, - ]; + const expectedKeyframes = [ + {backgroundColor: 'blue', offset: 0}, + {backgroundColor: 'red', offset: 1}, + ]; - players = getLog(); - expect(players.length).toEqual(3); - const [p1, p2, p3] = players; + players = getLog(); + expect(players.length).toEqual(3); + const [p1, p2, p3] = players; - expect(p1.element.classList.contains('a')).toBeTruthy(); - expect(p1.keyframes).toEqual(expectedKeyframes); + expect(p1.element.classList.contains('a')).toBeTruthy(); + expect(p1.keyframes).toEqual(expectedKeyframes); - expect(p2.element.classList.contains('b')).toBeTruthy(); - expect(p2.keyframes).toEqual(expectedKeyframes); + expect(p2.element.classList.contains('b')).toBeTruthy(); + expect(p2.keyframes).toEqual(expectedKeyframes); - expect(p3.element.classList.contains('b-inner')).toBeTruthy(); - expect(p3.keyframes).toEqual(expectedKeyframes); - }); + expect(p3.element.classList.contains('b-inner')).toBeTruthy(); + expect(p3.keyframes).toEqual(expectedKeyframes); + }); - it('should be able to query triggers directly by name', () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should be able to query triggers directly by name', () => { + @Component({ + selector: 'ani-cmp', + template: `
@@ -225,147 +219,146 @@ import {HostListener} from '../../src/metadata/directives';
`, - animations: [ - trigger('foo', []), - trigger('bar', []), - trigger( - 'myAnimation', - [ - transition( - '* => foo', - [ - query( - '@foo', - [ - animate(1000, style({color: 'red'})), - ]), - ]), - transition( - '* => bar', - [ - query( - '@bar', - [ - animate(1000, style({color: 'blue'})), - ]), - ]) - ]), - ] - }) - class Cmp { - public exp0: any; - public exp1: any; - public exp2: any; - } + animations: [ + trigger('foo', []), + trigger('bar', []), + trigger( + 'myAnimation', + [ + transition( + '* => foo', + [ + query( + '@foo', + [ + animate(1000, style({color: 'red'})), + ]), + ]), + transition( + '* => bar', + [ + query( + '@bar', + [ + animate(1000, style({color: 'blue'})), + ]), + ]) + ]), + ] + }) + class Cmp { + public exp0: any; + public exp1: any; + public exp2: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - fixture.detectChanges(); - engine.flush(); - resetLog(); + fixture.detectChanges(); + engine.flush(); + resetLog(); - cmp.exp0 = 'foo'; - fixture.detectChanges(); - engine.flush(); + cmp.exp0 = 'foo'; + fixture.detectChanges(); + engine.flush(); - let players = getLog(); - expect(players.length).toEqual(3); - const [p1, p2, p3] = players; - resetLog(); + let players = getLog(); + expect(players.length).toEqual(3); + const [p1, p2, p3] = players; + resetLog(); - expect(p1.element.classList.contains('f1')).toBeTruthy(); - expect(p2.element.classList.contains('f2')).toBeTruthy(); - expect(p3.element.classList.contains('f3')).toBeTruthy(); + expect(p1.element.classList.contains('f1')).toBeTruthy(); + expect(p2.element.classList.contains('f2')).toBeTruthy(); + expect(p3.element.classList.contains('f3')).toBeTruthy(); - cmp.exp0 = 'bar'; - fixture.detectChanges(); - engine.flush(); + cmp.exp0 = 'bar'; + fixture.detectChanges(); + engine.flush(); - players = getLog(); - expect(players.length).toEqual(3); - const [p4, p5, p6] = players; - resetLog(); + players = getLog(); + expect(players.length).toEqual(3); + const [p4, p5, p6] = players; + resetLog(); - expect(p4.element.classList.contains('b1')).toBeTruthy(); - expect(p5.element.classList.contains('b2')).toBeTruthy(); - expect(p6.element.classList.contains('b3')).toBeTruthy(); - }); + expect(p4.element.classList.contains('b1')).toBeTruthy(); + expect(p5.element.classList.contains('b2')).toBeTruthy(); + expect(p6.element.classList.contains('b3')).toBeTruthy(); + }); - it('should be able to query all active animations using :animating in a query', () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should be able to query all active animations using :animating in a query', () => { + @Component({ + selector: 'ani-cmp', + template: `
`, - animations: [ - trigger( - 'myAnimation', - [ - transition( - '* => a', - [ - query( - '.item:nth-child(odd)', - [ - style({opacity: 0}), - animate(1000, style({opacity: 1})), - ]), - ]), - transition( - '* => b', - [ - query( - '.item:animating', - [ - style({opacity: 1}), - animate(1000, style({opacity: 0})), - ]), - ]), - ]), - ] - }) - class Cmp { - public exp: any; - public items: number[] = [0, 1, 2, 3, 4]; - } + animations: [ + trigger( + 'myAnimation', + [ + transition( + '* => a', + [ + query( + '.item:nth-child(odd)', + [ + style({opacity: 0}), + animate(1000, style({opacity: 1})), + ]), + ]), + transition( + '* => b', + [ + query( + '.item:animating', + [ + style({opacity: 1}), + animate(1000, style({opacity: 0})), + ]), + ]), + ]), + ] + }) + class Cmp { + public exp: any; + public items: number[] = [0, 1, 2, 3, 4]; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = 'a'; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'a'; + fixture.detectChanges(); + engine.flush(); - let players = getLog(); - expect(players.length).toEqual(3); - resetLog(); + let players = getLog(); + expect(players.length).toEqual(3); + resetLog(); - cmp.exp = 'b'; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'b'; + fixture.detectChanges(); + engine.flush(); - players = getLog(); - expect(players.length).toEqual(3); - expect(players[0].element.classList.contains('e-0')).toBeTruthy(); - expect(players[1].element.classList.contains('e-2')).toBeTruthy(); - expect(players[2].element.classList.contains('e-4')).toBeTruthy(); - }); + players = getLog(); + expect(players.length).toEqual(3); + expect(players[0].element.classList.contains('e-0')).toBeTruthy(); + expect(players[1].element.classList.contains('e-2')).toBeTruthy(); + expect(players[2].element.classList.contains('e-4')).toBeTruthy(); + }); - it('should be able to query all actively queued animation triggers via `@*:animating`', - () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should be able to query all actively queued animation triggers via `@*:animating`', () => { + @Component({ + selector: 'ani-cmp', + template: `
@@ -374,88 +367,89 @@ import {HostListener} from '../../src/metadata/directives';
`, - animations: [ - trigger( - 'parent', - [ - transition( - '* => *', - [ - query( - '@*:animating', [animate(1000, style({background: 'red'}))], - {optional: true}), - ]), - ]), - trigger( - 'child', - [ - transition('* => *', []), - ]) - ] - }) - class Cmp { - public exp0: any; - public exp1: any; - public exp2: any; - public exp3: any; - public exp4: any; - public exp5: any; - } + animations: [ + trigger( + 'parent', + [ + transition( + '* => *', + [ + query( + '@*:animating', [animate(1000, style({background: 'red'}))], + {optional: true}), + ]), + ]), + trigger( + 'child', + [ + transition('* => *', []), + ]) + ] + }) + class Cmp { + public exp0: any; + public exp1: any; + public exp2: any; + public exp3: any; + public exp4: any; + public exp5: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp1 = 0; - cmp.exp2 = 0; - cmp.exp3 = 0; - cmp.exp4 = 0; - cmp.exp5 = 0; - fixture.detectChanges(); + cmp.exp1 = 0; + cmp.exp2 = 0; + cmp.exp3 = 0; + cmp.exp4 = 0; + cmp.exp5 = 0; + fixture.detectChanges(); - cmp.exp0 = 0; - fixture.detectChanges(); + cmp.exp0 = 0; + fixture.detectChanges(); - let players = engine.players; - cancelAllPlayers(players); + let players = engine.players; + cancelAllPlayers(players); - cmp.exp2 = 1; - cmp.exp4 = 1; - fixture.detectChanges(); + cmp.exp2 = 1; + cmp.exp4 = 1; + fixture.detectChanges(); - cmp.exp0 = 1; - fixture.detectChanges(); + cmp.exp0 = 1; + fixture.detectChanges(); - players = engine.players; - cancelAllPlayers(players); - expect(players.length).toEqual(3); + players = engine.players; + cancelAllPlayers(players); + expect(players.length).toEqual(3); - cmp.exp1 = 2; - cmp.exp2 = 2; - cmp.exp3 = 2; - cmp.exp4 = 2; - cmp.exp5 = 2; - fixture.detectChanges(); + cmp.exp1 = 2; + cmp.exp2 = 2; + cmp.exp3 = 2; + cmp.exp4 = 2; + cmp.exp5 = 2; + fixture.detectChanges(); - cmp.exp0 = 2; - fixture.detectChanges(); + cmp.exp0 = 2; + fixture.detectChanges(); - players = engine.players; - cancelAllPlayers(players); - expect(players.length).toEqual(6); + players = engine.players; + cancelAllPlayers(players); + expect(players.length).toEqual(6); - cmp.exp0 = 3; - fixture.detectChanges(); + cmp.exp0 = 3; + fixture.detectChanges(); - players = engine.players; - cancelAllPlayers(players); - expect(players.length).toEqual(1); - }); + players = engine.players; + cancelAllPlayers(players); + expect(players.length).toEqual(1); + }); - it('should collect styles for the same elements between queries', () => { - @Component({ + it( + 'should collect styles for the same elements between queries', () => { + @Component({ selector: 'ani-cmp', template: `
@@ -478,110 +472,109 @@ import {HostListener} from '../../src/metadata/directives'; ] }) class Cmp { - public exp: any; - public items: any[] = [0, 1, 2]; - } + public exp: any; + public items: any[] = [0, 1, 2]; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = 'go'; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'go'; + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(6); + const players = getLog(); + expect(players.length).toEqual(6); - const [p1, p2, p3, p4, p5, p6] = players; + const [p1, p2, p3, p4, p5, p6] = players; - expect(p1.delay).toEqual(0); - expect(p1.duration).toEqual(0); - expect(p1.keyframes).toEqual([ - {opacity: '0.01', offset: 0}, - {opacity: '0.01', offset: 1}, - ]); + expect(p1.delay).toEqual(0); + expect(p1.duration).toEqual(0); + expect(p1.keyframes).toEqual([ + {opacity: '0.01', offset: 0}, + {opacity: '0.01', offset: 1}, + ]); - expect(p2.delay).toEqual(0); - expect(p2.duration).toEqual(0); - expect(p2.keyframes).toEqual([ - {opacity: '0.01', offset: 0}, - {opacity: '0.01', offset: 1}, - ]); + expect(p2.delay).toEqual(0); + expect(p2.duration).toEqual(0); + expect(p2.keyframes).toEqual([ + {opacity: '0.01', offset: 0}, + {opacity: '0.01', offset: 1}, + ]); - expect(p3.delay).toEqual(0); - expect(p3.duration).toEqual(0); - expect(p3.keyframes).toEqual([ - {opacity: '0.01', offset: 0}, - {opacity: '0.01', offset: 1}, - ]); + expect(p3.delay).toEqual(0); + expect(p3.duration).toEqual(0); + expect(p3.keyframes).toEqual([ + {opacity: '0.01', offset: 0}, + {opacity: '0.01', offset: 1}, + ]); - expect(p4.delay).toEqual(0); - expect(p4.duration).toEqual(1000); - expect(p4.keyframes).toEqual([ - {opacity: '0.01', offset: 0}, - {opacity: '1', offset: 1}, - ]); + expect(p4.delay).toEqual(0); + expect(p4.duration).toEqual(1000); + expect(p4.keyframes).toEqual([ + {opacity: '0.01', offset: 0}, + {opacity: '1', offset: 1}, + ]); - expect(p5.delay).toEqual(1000); - expect(p5.duration).toEqual(1000); - expect(p5.keyframes).toEqual([ - {opacity: '0.01', offset: 0}, - {opacity: '1', offset: 1}, - ]); + expect(p5.delay).toEqual(1000); + expect(p5.duration).toEqual(1000); + expect(p5.keyframes).toEqual([ + {opacity: '0.01', offset: 0}, + {opacity: '1', offset: 1}, + ]); - expect(p6.delay).toEqual(1500); - expect(p6.duration).toEqual(1000); - expect(p6.keyframes).toEqual([ - {opacity: '0.01', offset: 0}, - {opacity: '1', offset: 1}, - ]); - }); + expect(p6.delay).toEqual(1500); + expect(p6.duration).toEqual(1000); + expect(p6.keyframes).toEqual([ + {opacity: '0.01', offset: 0}, + {opacity: '1', offset: 1}, + ]); + }); - it('should retain style values when :self is used inside of a query', () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should retain style values when :self is used inside of a query', () => { + @Component({ + selector: 'ani-cmp', + template: `
`, - animations: [trigger('myAnimation', [transition( - '* => go', - [ - query(':self', style({opacity: '0.5'})), - animate(1000, style({opacity: '1'})) - ])])] - }) - class Cmp { - public exp: any; - } + animations: [trigger( + 'myAnimation', + [transition( + '* => go', + [query(':self', style({opacity: '0.5'})), animate(1000, style({opacity: '1'}))])])] + }) + class Cmp { + public exp: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = 'go'; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'go'; + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(2); + const players = getLog(); + expect(players.length).toEqual(2); - const [p1, p2] = players; - expect(p1.delay).toEqual(0); - expect(p1.duration).toEqual(0); - expect(p1.keyframes).toEqual([{opacity: '0.5', offset: 0}, {opacity: '0.5', offset: 1}]); + const [p1, p2] = players; + expect(p1.delay).toEqual(0); + expect(p1.duration).toEqual(0); + expect(p1.keyframes).toEqual([{opacity: '0.5', offset: 0}, {opacity: '0.5', offset: 1}]); - expect(p2.delay).toEqual(0); - expect(p2.duration).toEqual(1000); - expect(p2.keyframes).toEqual([{opacity: '0.5', offset: 0}, {opacity: '1', offset: 1}]); - }); + expect(p2.delay).toEqual(0); + expect(p2.duration).toEqual(1000); + expect(p2.keyframes).toEqual([{opacity: '0.5', offset: 0}, {opacity: '1', offset: 1}]); + }); - it('should properly apply stagger after various other steps within a query', () => { - @Component({ + it('should properly apply stagger after various other steps within a query', () => { + @Component({ selector: 'ani-cmp', template: `
@@ -605,101 +598,101 @@ import {HostListener} from '../../src/metadata/directives'; ] }) class Cmp { - public exp: any; - public items: any[] = [0, 1, 2]; - } + public exp: any; + public items: any[] = [0, 1, 2]; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = 'go'; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'go'; + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(3); + const players = getLog(); + expect(players.length).toEqual(3); - const [p1, p2, p3] = players; + const [p1, p2, p3] = players; - expect(p1.delay).toEqual(0); - expect(p1.duration).toEqual(3000); - expect(p2.delay).toEqual(0); - expect(p2.duration).toEqual(3500); - expect(p3.delay).toEqual(0); - expect(p3.duration).toEqual(4000); - }); + expect(p1.delay).toEqual(0); + expect(p1.duration).toEqual(3000); + expect(p2.delay).toEqual(0); + expect(p2.duration).toEqual(3500); + expect(p3.delay).toEqual(0); + expect(p3.duration).toEqual(4000); + }); - it('should properly apply pre styling before a stagger is issued', () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should properly apply pre styling before a stagger is issued', () => { + @Component({ + selector: 'ani-cmp', + template: `
{{ item }}
`, - animations: [ - trigger( - 'myAnimation', - [ - transition( - '* => go', - [ - query( - ':enter', - [ - style({opacity: 0}), - stagger( - 100, - [ - animate(1000, style({opacity: 1})), - ]), - ]), - ]), - ]), - ] - }) - class Cmp { - public exp: any; - public items: any[] = [0, 1, 2, 3, 4]; + animations: [ + trigger( + 'myAnimation', + [ + transition( + '* => go', + [ + query( + ':enter', + [ + style({opacity: 0}), + stagger( + 100, + [ + animate(1000, style({opacity: 1})), + ]), + ]), + ]), + ]), + ] + }) + class Cmp { + public exp: any; + public items: any[] = [0, 1, 2, 3, 4]; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + + cmp.exp = 'go'; + fixture.detectChanges(); + engine.flush(); + + const players = getLog(); + expect(players.length).toEqual(5); + + for (let i = 0; i < players.length; i++) { + const player = players[i]; + const kf = player.keyframes; + const limit = kf.length - 1; + const staggerDelay = 100 * i; + const duration = 1000 + staggerDelay; + + expect(kf[0]).toEqual({opacity: '0', offset: 0}); + if (limit > 1) { + const offsetAtStaggerDelay = staggerDelay / duration; + expect(kf[1]).toEqual({opacity: '0', offset: offsetAtStaggerDelay}); } + expect(kf[limit]).toEqual({opacity: '1', offset: 1}); + expect(player.duration).toEqual(duration); + } + }); - TestBed.configureTestingModule({declarations: [Cmp]}); - - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - - cmp.exp = 'go'; - fixture.detectChanges(); - engine.flush(); - - const players = getLog(); - expect(players.length).toEqual(5); - - for (let i = 0; i < players.length; i++) { - const player = players[i]; - const kf = player.keyframes; - const limit = kf.length - 1; - const staggerDelay = 100 * i; - const duration = 1000 + staggerDelay; - - expect(kf[0]).toEqual({opacity: '0', offset: 0}); - if (limit > 1) { - const offsetAtStaggerDelay = staggerDelay / duration; - expect(kf[1]).toEqual({opacity: '0', offset: offsetAtStaggerDelay}); - } - expect(kf[limit]).toEqual({opacity: '1', offset: 1}); - expect(player.duration).toEqual(duration); - } - }); - - it('should apply a full stagger step delay if the timing data is left undefined', () => { - @Component({ + it('should apply a full stagger step delay if the timing data is left undefined', () => { + @Component({ selector: 'ani-cmp', template: `
@@ -717,146 +710,144 @@ import {HostListener} from '../../src/metadata/directives'; ])])])])] }) class Cmp { - public exp: any; - public items: any[] = [0, 1, 2, 3, 4]; - } + public exp: any; + public items: any[] = [0, 1, 2, 3, 4]; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = 'go'; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'go'; + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(5); + const players = getLog(); + expect(players.length).toEqual(5); - const [p1, p2, p3, p4, p5] = players; - expect(p1.delay).toEqual(0); - expect(p2.delay).toEqual(1500); - expect(p3.delay).toEqual(3000); - expect(p4.delay).toEqual(4500); - expect(p5.delay).toEqual(6000); - }); + const [p1, p2, p3, p4, p5] = players; + expect(p1.delay).toEqual(0); + expect(p2.delay).toEqual(1500); + expect(p3.delay).toEqual(3000); + expect(p4.delay).toEqual(4500); + expect(p5.delay).toEqual(6000); + }); - it('should persist inner sub trigger styles once their animation is complete', () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should persist inner sub trigger styles once their animation is complete', () => { + @Component({ + selector: 'ani-cmp', + template: `
`, - animations: [ - trigger( - 'parent', - [ - transition( - ':enter', - [ - query( - '.child', - [ - animateChild(), - ]), - ]), - ]), - trigger( - 'child', - [ - state('*, void', style({height: '0px'})), - state('b', style({height: '444px'})), - transition('* => *', animate(500)), - ]), - ] - }) - class Cmp { - public exp1: any; - public exp2: any; - } + animations: [ + trigger( + 'parent', + [ + transition( + ':enter', + [ + query( + '.child', + [ + animateChild(), + ]), + ]), + ]), + trigger( + 'child', + [ + state('*, void', style({height: '0px'})), + state('b', style({height: '444px'})), + transition('* => *', animate(500)), + ]), + ] + }) + class Cmp { + public exp1: any; + public exp2: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp1 = true; - cmp.exp2 = 'b'; - fixture.detectChanges(); - engine.flush(); + cmp.exp1 = true; + cmp.exp2 = 'b'; + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(1); - const player = players[0]; + const players = getLog(); + expect(players.length).toEqual(1); + const player = players[0]; - expect(player.keyframes).toEqual([ - {height: '0px', offset: 0}, {height: '444px', offset: 1} - ]); - player.finish(); + expect(player.keyframes).toEqual([{height: '0px', offset: 0}, {height: '444px', offset: 1}]); + player.finish(); - expect(player.element.style.height).toEqual('444px'); - }); + expect(player.element.style.height).toEqual('444px'); + }); - it('should find newly inserted items in the component via :enter', () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should find newly inserted items in the component via :enter', () => { + @Component({ + selector: 'ani-cmp', + template: `
{{ item }}
`, - animations: [trigger( - 'myAnimation', - [ - transition( - ':enter', - [ - query( - ':enter', - [ - style({opacity: 0}), - animate(1000, style({opacity: .5})), - ]), - ]), - ])] - }) - class Cmp { - public items: any[] = [0, 1, 2]; - } + animations: [trigger( + 'myAnimation', + [ + transition( + ':enter', + [ + query( + ':enter', + [ + style({opacity: 0}), + animate(1000, style({opacity: .5})), + ]), + ]), + ])] + }) + class Cmp { + public items: any[] = [0, 1, 2]; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - fixture.detectChanges(); - engine.flush(); + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(3); + const players = getLog(); + expect(players.length).toEqual(3); - const [p1, p2, p3] = players; - expect(p1.element.innerText.trim()).toEqual('0'); - expect(p2.element.innerText.trim()).toEqual('1'); - expect(p3.element.innerText.trim()).toEqual('2'); + const [p1, p2, p3] = players; + expect(p1.element.innerText.trim()).toEqual('0'); + expect(p2.element.innerText.trim()).toEqual('1'); + expect(p3.element.innerText.trim()).toEqual('2'); - players.forEach(p => { - expect(p.keyframes).toEqual([{opacity: '0', offset: 0}, {opacity: '0.5', offset: 1}]); - }); + players.forEach(p => { + expect(p.keyframes).toEqual([{opacity: '0', offset: 0}, {opacity: '0.5', offset: 1}]); }); + }); - it('should cleanup :enter and :leave artifacts from nodes when any animation sequences fail to be built', - () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should cleanup :enter and :leave artifacts from nodes when any animation sequences fail to be built', + () => { + @Component({ + selector: 'ani-cmp', + template: `
{{ item }} @@ -864,161 +855,163 @@ import {HostListener} from '../../src/metadata/directives';
Leave!
`, - animations: [ - trigger( - 'myAnimation', - [ - transition('* => 0', []), - transition( - '* => *', - [ - query( - '.child:enter', - [ - style({opacity: 0}), - animate(1000, style({opacity: 1})), - ]), - query( - '.incorrect-child:leave', - [ - animate(1000, style({opacity: 0})), - ]), - ]), - ]), - ] - }) - class Cmp { - @ViewChild('container') public container: any; - public items: any[] = []; - } + animations: [ + trigger( + 'myAnimation', + [ + transition('* => 0', []), + transition( + '* => *', + [ + query( + '.child:enter', + [ + style({opacity: 0}), + animate(1000, style({opacity: 1})), + ]), + query( + '.incorrect-child:leave', + [ + animate(1000, style({opacity: 0})), + ]), + ]), + ]), + ] + }) + class Cmp { + @ViewChild('container') public container: any; + public items: any[] = []; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.items = []; + cmp.items = []; + fixture.detectChanges(); + + cmp.items = [0, 1, 2, 3, 4]; + + expect(() => { fixture.detectChanges(); + }).toThrow(); - cmp.items = [0, 1, 2, 3, 4]; + const children = cmp.container.nativeElement.querySelectorAll('.child'); + expect(children.length).toEqual(5); - expect(() => { fixture.detectChanges(); }).toThrow(); + for (let i = 0; i < children.length; i++) { + let child = children[i]; + expect(child.classList.contains(ENTER_CLASSNAME)).toBe(false); + expect(child.classList.contains(LEAVE_CLASSNAME)).toBe(false); + } + }); - const children = cmp.container.nativeElement.querySelectorAll('.child'); - expect(children.length).toEqual(5); - - for (let i = 0; i < children.length; i++) { - let child = children[i]; - expect(child.classList.contains(ENTER_CLASSNAME)).toBe(false); - expect(child.classList.contains(LEAVE_CLASSNAME)).toBe(false); - } - }); - - it('should find elements that have been removed via :leave', () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should find elements that have been removed via :leave', () => { + @Component({ + selector: 'ani-cmp', + template: `
{{ item }}
`, - animations: [trigger( - 'myAnimation', - [ - transition( - 'a => b', - [query(':leave', [style({opacity: 1}), animate(1000, style({opacity: .5}))])]), - ])] - }) - class Cmp { - public exp: any; - public items: any[] = [4, 2, 0]; - } + animations: [trigger( + 'myAnimation', + [ + transition( + 'a => b', + [query(':leave', [style({opacity: 1}), animate(1000, style({opacity: .5}))])]), + ])] + }) + class Cmp { + public exp: any; + public items: any[] = [4, 2, 0]; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = 'a'; - fixture.detectChanges(); - engine.flush(); - resetLog(); + cmp.exp = 'a'; + fixture.detectChanges(); + engine.flush(); + resetLog(); - cmp.exp = 'b'; - cmp.items = []; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'b'; + cmp.items = []; + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(3); + const players = getLog(); + expect(players.length).toEqual(3); - const [p1, p2, p3] = players; - expect(p1.element.innerText.trim()).toEqual('4'); - expect(p2.element.innerText.trim()).toEqual('2'); - expect(p3.element.innerText.trim()).toEqual('0'); + const [p1, p2, p3] = players; + expect(p1.element.innerText.trim()).toEqual('4'); + expect(p2.element.innerText.trim()).toEqual('2'); + expect(p3.element.innerText.trim()).toEqual('0'); - players.forEach(p => { - expect(p.keyframes).toEqual([{opacity: '1', offset: 0}, {opacity: '0.5', offset: 1}]); - }); + players.forEach(p => { + expect(p.keyframes).toEqual([{opacity: '1', offset: 0}, {opacity: '0.5', offset: 1}]); }); + }); - it('should find :enter nodes that have been inserted around non enter nodes', () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should find :enter nodes that have been inserted around non enter nodes', () => { + @Component({ + selector: 'ani-cmp', + template: `
{{ item }}
`, - animations: [trigger( - 'myAnimation', - [ - transition( - '* => go', - [query(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))])]), - ])] - }) - class Cmp { - public exp: any; - public items: any[] = []; - } + animations: [trigger( + 'myAnimation', + [ + transition( + '* => go', + [query(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))])]), + ])] + }) + class Cmp { + public exp: any; + public items: any[] = []; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = 'no'; - cmp.items = [2]; - fixture.detectChanges(); - engine.flush(); - resetLog(); + cmp.exp = 'no'; + cmp.items = [2]; + fixture.detectChanges(); + engine.flush(); + resetLog(); - cmp.exp = 'go'; - cmp.items = [0, 1, 2, 3, 4]; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'go'; + cmp.items = [0, 1, 2, 3, 4]; + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(4); + const players = getLog(); + expect(players.length).toEqual(4); - const [p1, p2, p3, p4] = players; - expect(p1.element.innerText.trim()).toEqual('0'); - expect(p2.element.innerText.trim()).toEqual('1'); - expect(p3.element.innerText.trim()).toEqual('3'); - expect(p4.element.innerText.trim()).toEqual('4'); - }); + const [p1, p2, p3, p4] = players; + expect(p1.element.innerText.trim()).toEqual('0'); + expect(p2.element.innerText.trim()).toEqual('1'); + expect(p3.element.innerText.trim()).toEqual('3'); + expect(p4.element.innerText.trim()).toEqual('4'); + }); - it('should find :enter/:leave nodes that are nested inside of ng-container elements', () => { - @Component({ + it('should find :enter/:leave nodes that are nested inside of ng-container elements', () => { + @Component({ selector: 'ani-cmp', template: `
@@ -1048,64 +1041,64 @@ import {HostListener} from '../../src/metadata/directives'; ])] }) class Cmp { - // TODO(issue/24571): remove '!'. - public items !: any[]; - } + // TODO(issue/24571): remove '!'. + public items!: any[]; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.items = []; - fixture.detectChanges(); - engine.flush(); - resetLog(); + cmp.items = []; + fixture.detectChanges(); + engine.flush(); + resetLog(); - cmp.items = [0, 1, 2, 3, 4]; - fixture.detectChanges(); - engine.flush(); + cmp.items = [0, 1, 2, 3, 4]; + fixture.detectChanges(); + engine.flush(); - let players = getLog(); - expect(players.length).toEqual(5); + let players = getLog(); + expect(players.length).toEqual(5); - for (let i = 0; i < 5; i++) { - let player = players[i] !; - expect(player.keyframes).toEqual([ - {opacity: '0', offset: 0}, - {opacity: '1', offset: 1}, - ]); + for (let i = 0; i < 5; i++) { + let player = players[i]!; + expect(player.keyframes).toEqual([ + {opacity: '0', offset: 0}, + {opacity: '1', offset: 1}, + ]); - let elm = player.element; - let text = i % 2 == 0 ? `even ${i}` : `odd ${i}`; - expect(elm.innerText.trim()).toEqual(text); - } + let elm = player.element; + let text = i % 2 == 0 ? `even ${i}` : `odd ${i}`; + expect(elm.innerText.trim()).toEqual(text); + } - resetLog(); - cmp.items = []; - fixture.detectChanges(); - engine.flush(); + resetLog(); + cmp.items = []; + fixture.detectChanges(); + engine.flush(); - players = getLog(); - expect(players.length).toEqual(5); + players = getLog(); + expect(players.length).toEqual(5); - for (let i = 0; i < 5; i++) { - let player = players[i] !; - expect(player.keyframes).toEqual([ - {opacity: '1', offset: 0}, - {opacity: '0', offset: 1}, - ]); + for (let i = 0; i < 5; i++) { + let player = players[i]!; + expect(player.keyframes).toEqual([ + {opacity: '1', offset: 0}, + {opacity: '0', offset: 1}, + ]); - let elm = player.element; - let text = i % 2 == 0 ? `even ${i}` : `odd ${i}`; - expect(elm.innerText.trim()).toEqual(text); - } - }); + let elm = player.element; + let text = i % 2 == 0 ? `even ${i}` : `odd ${i}`; + expect(elm.innerText.trim()).toEqual(text); + } + }); - it('should properly cancel items that were queried into a former animation and pass in the associated styles into the follow-up players per element', - () => { - @Component({ + it('should properly cancel items that were queried into a former animation and pass in the associated styles into the follow-up players per element', + () => { + @Component({ selector: 'ani-cmp', template: `
@@ -1128,52 +1121,52 @@ import {HostListener} from '../../src/metadata/directives'; ])] }) class Cmp { - public exp: any; - // TODO(issue/24571): remove '!'. - public items !: any[]; - } + public exp: any; + // TODO(issue/24571): remove '!'. + public items!: any[]; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = 'on'; - cmp.items = [0, 1, 2, 3, 4]; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'on'; + cmp.items = [0, 1, 2, 3, 4]; + fixture.detectChanges(); + engine.flush(); - const previousPlayers = getLog(); - expect(previousPlayers.length).toEqual(10); - resetLog(); + const previousPlayers = getLog(); + expect(previousPlayers.length).toEqual(10); + resetLog(); - cmp.exp = 'off'; - cmp.items = [0, 1, 2]; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'off'; + cmp.items = [0, 1, 2]; + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(4); + const players = getLog(); + expect(players.length).toEqual(4); - const [p1, p2, p3, p4] = players; + const [p1, p2, p3, p4] = players; - // p1 && p2 are the starting players for item3 and item4 - expect(p1.previousStyles) - .toEqual({opacity: AUTO_STYLE, width: AUTO_STYLE, height: AUTO_STYLE}); - expect(p2.previousStyles) - .toEqual({opacity: AUTO_STYLE, width: AUTO_STYLE, height: AUTO_STYLE}); + // p1 && p2 are the starting players for item3 and item4 + expect(p1.previousStyles) + .toEqual({opacity: AUTO_STYLE, width: AUTO_STYLE, height: AUTO_STYLE}); + expect(p2.previousStyles) + .toEqual({opacity: AUTO_STYLE, width: AUTO_STYLE, height: AUTO_STYLE}); - // p3 && p4 are the following players for item3 and item4 - expect(p3.previousStyles).toEqual({}); - expect(p4.previousStyles).toEqual({}); - }); + // p3 && p4 are the following players for item3 and item4 + expect(p3.previousStyles).toEqual({}); + expect(p4.previousStyles).toEqual({}); + }); - it('should not remove a parent container if its contents are queried into by an ancestor element', - () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should not remove a parent container if its contents are queried into by an ancestor element', + () => { + @Component({ + selector: 'ani-cmp', + template: `
@@ -1181,67 +1174,67 @@ import {HostListener} from '../../src/metadata/directives';
`, - animations: [ - trigger( - 'myAnimation', - [ - transition( - '* => go', - [ - query( - '.child', - [ - style({opacity: 0}), - animate(1000, style({opacity: 1})), - ]), - ]), - ]), - ] - }) - class Cmp { - public exp1: any = ''; - public exp2: any = true; + animations: [ + trigger( + 'myAnimation', + [ + transition( + '* => go', + [ + query( + '.child', + [ + style({opacity: 0}), + animate(1000, style({opacity: 1})), + ]), + ]), + ]), + ] + }) + class Cmp { + public exp1: any = ''; + public exp2: any = true; - @ViewChild('ancestor') public ancestorElm: any; + @ViewChild('ancestor') public ancestorElm: any; - @ViewChild('parent') public parentElm: any; - } + @ViewChild('parent') public parentElm: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - fixture.detectChanges(); - engine.flush(); - resetLog(); + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + fixture.detectChanges(); + engine.flush(); + resetLog(); - const ancestorElm = cmp.ancestorElm.nativeElement; - const parentElm = cmp.parentElm.nativeElement; + const ancestorElm = cmp.ancestorElm.nativeElement; + const parentElm = cmp.parentElm.nativeElement; - cmp.exp1 = 'go'; - cmp.exp2 = false; - fixture.detectChanges(); - engine.flush(); + cmp.exp1 = 'go'; + cmp.exp2 = false; + fixture.detectChanges(); + engine.flush(); - expect(ancestorElm.contains(parentElm)).toBe(true); + expect(ancestorElm.contains(parentElm)).toBe(true); - const players = getLog(); - expect(players.length).toEqual(2); - const [p1, p2] = players; - expect(parentElm.contains(p1.element)).toBe(true); - expect(parentElm.contains(p2.element)).toBe(true); + const players = getLog(); + expect(players.length).toEqual(2); + const [p1, p2] = players; + expect(parentElm.contains(p1.element)).toBe(true); + expect(parentElm.contains(p2.element)).toBe(true); - cancelAllPlayers(players); + cancelAllPlayers(players); - expect(ancestorElm.contains(parentElm)).toBe(false); - }); + expect(ancestorElm.contains(parentElm)).toBe(false); + }); - it('should only retain a to-be-removed node if the inner queried items are apart of an animation issued by an ancestor', - fakeAsync(() => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should only retain a to-be-removed node if the inner queried items are apart of an animation issued by an ancestor', + fakeAsync(() => { + @Component({ + selector: 'ani-cmp', + template: `
hello
@@ -1249,191 +1242,195 @@ import {HostListener} from '../../src/metadata/directives';
`, - animations: [ - trigger( - 'one', - [ - transition( - '* => go', - [ - query( - '.child', - [ - style({height: '100px'}), - animate(1000, style({height: '0px'})), - ]), - ]), - ]), - trigger( - 'two', - [ - transition('* => go', [query( - 'header', - [ - style({width: '100px'}), - animate(1000, style({width: '0px'})), - ])]), - ]), - ] - }) - class Cmp { - public exp1: any = ''; - public exp2: any = ''; - public parentExp: any = true; + animations: [ + trigger( + 'one', + [ + transition( + '* => go', + [ + query( + '.child', + [ + style({height: '100px'}), + animate(1000, style({height: '0px'})), + ]), + ]), + ]), + trigger( + 'two', + [ + transition('* => go', [query( + 'header', + [ + style({width: '100px'}), + animate(1000, style({width: '0px'})), + ])]), + ]), + ] + }) + class Cmp { + public exp1: any = ''; + public exp2: any = ''; + public parentExp: any = true; - @ViewChild('ancestor') public ancestorElm: any; + @ViewChild('ancestor') public ancestorElm: any; - @ViewChild('parent') public parentElm: any; - } + @ViewChild('parent') public parentElm: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - fixture.detectChanges(); - engine.flush(); - resetLog(); + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + fixture.detectChanges(); + engine.flush(); + resetLog(); - const ancestorElm = cmp.ancestorElm.nativeElement; - const parentElm = cmp.parentElm.nativeElement; - expect(ancestorElm.contains(parentElm)).toBe(true); + const ancestorElm = cmp.ancestorElm.nativeElement; + const parentElm = cmp.parentElm.nativeElement; + expect(ancestorElm.contains(parentElm)).toBe(true); - cmp.exp1 = 'go'; - fixture.detectChanges(); - engine.flush(); + cmp.exp1 = 'go'; + fixture.detectChanges(); + engine.flush(); - expect(ancestorElm.contains(parentElm)).toBe(true); + expect(ancestorElm.contains(parentElm)).toBe(true); - const onePlayers = getLog(); - expect(onePlayers.length).toEqual(1); // element.child - const [childPlayer] = onePlayers; + const onePlayers = getLog(); + expect(onePlayers.length).toEqual(1); // element.child + const [childPlayer] = onePlayers; - let childPlayerComplete = false; - childPlayer.onDone(() => childPlayerComplete = true); - resetLog(); - flushMicrotasks(); + let childPlayerComplete = false; + childPlayer.onDone(() => childPlayerComplete = true); + resetLog(); + flushMicrotasks(); - expect(childPlayerComplete).toBe(false); + expect(childPlayerComplete).toBe(false); - cmp.exp2 = 'go'; - cmp.parentExp = false; - fixture.detectChanges(); - engine.flush(); + cmp.exp2 = 'go'; + cmp.parentExp = false; + fixture.detectChanges(); + engine.flush(); - const twoPlayers = getLog(); - expect(twoPlayers.length).toEqual(1); // the header element - expect(ancestorElm.contains(parentElm)).toBe(false); - expect(childPlayerComplete).toBe(true); - })); + const twoPlayers = getLog(); + expect(twoPlayers.length).toEqual(1); // the header element + expect(ancestorElm.contains(parentElm)).toBe(false); + expect(childPlayerComplete).toBe(true); + })); - it('should finish queried players in an animation when the next animation takes over', () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should finish queried players in an animation when the next animation takes over', () => { + @Component({ + selector: 'ani-cmp', + template: `
{{ item }}
`, - animations: [trigger( - 'myAnimation', - [ - transition( - '* => on', - [ - query(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))]), - ]), - transition('* => off', []) - ])] - }) - class Cmp { - public exp: any; - // TODO(issue/24571): remove '!'. - public items !: any[]; - } + animations: [trigger( + 'myAnimation', + [ + transition( + '* => on', + [ + query(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))]), + ]), + transition('* => off', []) + ])] + }) + class Cmp { + public exp: any; + // TODO(issue/24571): remove '!'. + public items!: any[]; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = 'on'; - cmp.items = [0, 1, 2, 3, 4]; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'on'; + cmp.items = [0, 1, 2, 3, 4]; + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(5); + const players = getLog(); + expect(players.length).toEqual(5); - let count = 0; - players.forEach(p => { p.onDone(() => count++); }); - - expect(count).toEqual(0); - - cmp.exp = 'off'; - fixture.detectChanges(); - engine.flush(); - - expect(count).toEqual(5); + let count = 0; + players.forEach(p => { + p.onDone(() => count++); }); - it('should finish queried players when the previous player is finished', () => { - @Component({ - selector: 'ani-cmp', - template: ` + expect(count).toEqual(0); + + cmp.exp = 'off'; + fixture.detectChanges(); + engine.flush(); + + expect(count).toEqual(5); + }); + + it('should finish queried players when the previous player is finished', () => { + @Component({ + selector: 'ani-cmp', + template: `
{{ item }}
`, - animations: [trigger( - 'myAnimation', - [ - transition( - '* => on', - [ - query(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))]), - ]), - transition('* => off', []) - ])] - }) - class Cmp { - public exp: any; - // TODO(issue/24571): remove '!'. - public items !: any[]; - } + animations: [trigger( + 'myAnimation', + [ + transition( + '* => on', + [ + query(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))]), + ]), + transition('* => off', []) + ])] + }) + class Cmp { + public exp: any; + // TODO(issue/24571): remove '!'. + public items!: any[]; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = 'on'; - cmp.items = [0, 1, 2, 3, 4]; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'on'; + cmp.items = [0, 1, 2, 3, 4]; + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(5); + const players = getLog(); + expect(players.length).toEqual(5); - let count = 0; - players.forEach(p => { p.onDone(() => count++); }); - - expect(count).toEqual(0); - - expect(engine.players.length).toEqual(1); - engine.players[0].finish(); - - expect(count).toEqual(5); + let count = 0; + players.forEach(p => { + p.onDone(() => count++); }); - it('should allow multiple triggers to animate on queried elements at the same time', () => { - @Component({ + expect(count).toEqual(0); + + expect(engine.players.length).toEqual(1); + engine.players[0].finish(); + + expect(count).toEqual(5); + }); + + it('should allow multiple triggers to animate on queried elements at the same time', () => { + @Component({ selector: 'ani-cmp', template: `
@@ -1464,111 +1461,117 @@ import {HostListener} from '../../src/metadata/directives'; ] }) class Cmp { - public exp1: any; - public exp2: any; - // TODO(issue/24571): remove '!'. - public items !: any[]; - } + public exp1: any; + public exp2: any; + // TODO(issue/24571): remove '!'. + public items!: any[]; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp1 = 'on'; - cmp.items = [0, 1, 2, 3, 4]; - fixture.detectChanges(); - engine.flush(); + cmp.exp1 = 'on'; + cmp.items = [0, 1, 2, 3, 4]; + fixture.detectChanges(); + engine.flush(); - let players = getLog(); - expect(players.length).toEqual(5); + let players = getLog(); + expect(players.length).toEqual(5); - let count = 0; - players.forEach(p => { p.onDone(() => count++); }); - - resetLog(); - - expect(count).toEqual(0); - - cmp.exp2 = 'on'; - fixture.detectChanges(); - engine.flush(); - - expect(count).toEqual(0); - - players = getLog(); - expect(players.length).toEqual(3); - - players.forEach(p => { p.onDone(() => count++); }); - - cmp.exp1 = 'off'; - fixture.detectChanges(); - engine.flush(); - - expect(count).toEqual(5); - - cmp.exp2 = 'off'; - fixture.detectChanges(); - engine.flush(); - - expect(count).toEqual(8); + let count = 0; + players.forEach(p => { + p.onDone(() => count++); }); - it('should cancel inner queried animations if a trigger state value changes, but isn\'t detected as a valid transition', - () => { - @Component({ - selector: 'ani-cmp', - template: ` + resetLog(); + + expect(count).toEqual(0); + + cmp.exp2 = 'on'; + fixture.detectChanges(); + engine.flush(); + + expect(count).toEqual(0); + + players = getLog(); + expect(players.length).toEqual(3); + + players.forEach(p => { + p.onDone(() => count++); + }); + + cmp.exp1 = 'off'; + fixture.detectChanges(); + engine.flush(); + + expect(count).toEqual(5); + + cmp.exp2 = 'off'; + fixture.detectChanges(); + engine.flush(); + + expect(count).toEqual(8); + }); + + it('should cancel inner queried animations if a trigger state value changes, but isn\'t detected as a valid transition', + () => { + @Component({ + selector: 'ani-cmp', + template: `
{{ item }}
`, - animations: [trigger( - 'myAnimation', - [transition( - '* => on', - [ - query(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))]), - ])])] - }) - class Cmp { - public exp: any; - // TODO(issue/24571): remove '!'. - public items !: any[]; - } + animations: [trigger( + 'myAnimation', + [transition( + '* => on', + [ + query(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))]), + ])])] + }) + class Cmp { + public exp: any; + // TODO(issue/24571): remove '!'. + public items!: any[]; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = 'on'; - cmp.items = [0, 1, 2, 3, 4]; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'on'; + cmp.items = [0, 1, 2, 3, 4]; + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(5); + const players = getLog(); + expect(players.length).toEqual(5); - let count = 0; - players.forEach(p => { p.onDone(() => count++); }); - - expect(count).toEqual(0); - - cmp.exp = 'off'; - fixture.detectChanges(); - engine.flush(); - - expect(count).toEqual(5); + let count = 0; + players.forEach(p => { + p.onDone(() => count++); }); - it('should allow for queried items to restore their styling back to the original state via animate(time, "*")', - () => { - @Component({ + expect(count).toEqual(0); + + cmp.exp = 'off'; + fixture.detectChanges(); + engine.flush(); + + expect(count).toEqual(5); + }); + + it('should allow for queried items to restore their styling back to the original state via animate(time, "*")', + () => { + @Component({ selector: 'ani-cmp', template: `
@@ -1590,301 +1593,299 @@ import {HostListener} from '../../src/metadata/directives'; ] }) class Cmp { + public exp: any; + // TODO(issue/24571): remove '!'. + public items!: any[]; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + + cmp.exp = 'on'; + cmp.items = [0, 1, 2]; + fixture.detectChanges(); + engine.flush(); + + const players = getLog(); + expect(players.length).toEqual(3); + + players.forEach(p => { + expect(p.keyframes).toEqual([ + {opacity: '0', width: '0px', height: '0px', offset: 0}, + {opacity: '1', width: '0px', height: '0px', offset: .5}, + {opacity: AUTO_STYLE, width: AUTO_STYLE, height: '200px', offset: 1} + ]); + }); + }); + + it('should query elements in sub components that do not contain animations using the :enter selector', + () => { + @Component({ + selector: 'parent-cmp', + template: ` +
+ +
+ `, + animations: [trigger( + 'myAnimation', + [transition( + '* => on', + [query(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])])] + }) + class ParentCmp { + public exp: any; + + @ViewChild('child') public child: any; + } + + @Component({ + selector: 'child-cmp', + template: ` +
+ {{ item }} +
+ ` + }) + class ChildCmp { + public items: any[] = []; + } + + TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); + const fixture = TestBed.createComponent(ParentCmp); + const cmp = fixture.componentInstance; + fixture.detectChanges(); + + cmp.exp = 'on'; + cmp.child.items = [1, 2, 3]; + fixture.detectChanges(); + + const players = getLog() as any[]; + expect(players.length).toEqual(3); + + expect(players[0].element.innerText.trim()).toEqual('1'); + expect(players[1].element.innerText.trim()).toEqual('2'); + expect(players[2].element.innerText.trim()).toEqual('3'); + }); + + it('should query elements in sub components that do not contain animations using the :leave selector', + () => { + @Component({ + selector: 'parent-cmp', + template: ` +
+ +
+ `, + animations: [trigger( + 'myAnimation', + [transition('* => on', [query(':leave', [animate(1000, style({opacity: 0}))])])])] + }) + class ParentCmp { + public exp: any; + + @ViewChild('child', {static: true}) public child: any; + } + + @Component({ + selector: 'child-cmp', + template: ` +
+ {{ item }} +
+ ` + }) + class ChildCmp { + public items: any[] = []; + } + + TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); + const fixture = TestBed.createComponent(ParentCmp); + const cmp = fixture.componentInstance; + + cmp.child.items = [4, 5, 6]; + fixture.detectChanges(); + + cmp.exp = 'on'; + cmp.child.items = []; + fixture.detectChanges(); + + const players = getLog() as any[]; + expect(players.length).toEqual(3); + + expect(players[0].element.innerText.trim()).toEqual('4'); + expect(players[1].element.innerText.trim()).toEqual('5'); + expect(players[2].element.innerText.trim()).toEqual('6'); + }); + + describe('options.limit', () => { + it('should limit results when a limit value is passed into the query options', () => { + @Component({ + selector: 'cmp', + template: ` +
+
+ {{ item }} +
+
+ `, + animations: [ + trigger( + 'myAnimation', + [ + transition( + '* => go', + [ + query( + '.item', + [ + style({opacity: 0}), + animate('1s', style({opacity: 1})), + ], + {limit: 2}), + ]), + ]), + ] + }) + class Cmp { + public exp: any; + public items: any[] = []; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + cmp.items = ['a', 'b', 'c', 'd', 'e']; + fixture.detectChanges(); + + cmp.exp = 'go'; + fixture.detectChanges(); + + const players = getLog() as any[]; + expect(players.length).toEqual(2); + expect(players[0].element.innerText.trim()).toEqual('a'); + expect(players[1].element.innerText.trim()).toEqual('b'); + }); + + it('should support negative limit values by pulling in elements from the end of the query', + () => { + @Component({ + selector: 'cmp', + template: ` +
+
+ {{ item }} +
+
+ `, + animations: [ + trigger( + 'myAnimation', + [ + transition( + '* => go', + [ + query( + '.item', + [ + style({opacity: 0}), + animate('1s', style({opacity: 1})), + ], + {limit: -3}), + ]), + ]), + ] + }) + class Cmp { public exp: any; - // TODO(issue/24571): remove '!'. - public items !: any[]; + public items: any[] = []; } TestBed.configureTestingModule({declarations: [Cmp]}); - - const engine = TestBed.inject(ɵAnimationEngine); const fixture = TestBed.createComponent(Cmp); const cmp = fixture.componentInstance; - - cmp.exp = 'on'; - cmp.items = [0, 1, 2]; - fixture.detectChanges(); - engine.flush(); - - const players = getLog(); - expect(players.length).toEqual(3); - - players.forEach(p => { - expect(p.keyframes).toEqual([ - {opacity: '0', width: '0px', height: '0px', offset: 0}, - {opacity: '1', width: '0px', height: '0px', offset: .5}, - {opacity: AUTO_STYLE, width: AUTO_STYLE, height: '200px', offset: 1} - ]); - }); - }); - - it('should query elements in sub components that do not contain animations using the :enter selector', - () => { - @Component({ - selector: 'parent-cmp', - template: ` -
- -
- `, - animations: [trigger( - 'myAnimation', - [transition( - '* => on', - [query( - ':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])])] - }) - class ParentCmp { - public exp: any; - - @ViewChild('child') public child: any; - } - - @Component({ - selector: 'child-cmp', - template: ` -
- {{ item }} -
- ` - }) - class ChildCmp { - public items: any[] = []; - } - - TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); - const fixture = TestBed.createComponent(ParentCmp); - const cmp = fixture.componentInstance; + cmp.items = ['a', 'b', 'c', 'd', 'e']; fixture.detectChanges(); - cmp.exp = 'on'; - cmp.child.items = [1, 2, 3]; + cmp.exp = 'go'; fixture.detectChanges(); const players = getLog() as any[]; expect(players.length).toEqual(3); - - expect(players[0].element.innerText.trim()).toEqual('1'); - expect(players[1].element.innerText.trim()).toEqual('2'); - expect(players[2].element.innerText.trim()).toEqual('3'); + expect(players[0].element.innerText.trim()).toEqual('c'); + expect(players[1].element.innerText.trim()).toEqual('d'); + expect(players[2].element.innerText.trim()).toEqual('e'); }); - - it('should query elements in sub components that do not contain animations using the :leave selector', - () => { - @Component({ - selector: 'parent-cmp', - template: ` -
- -
- `, - animations: [trigger( - 'myAnimation', - [transition( - '* => on', [query(':leave', [animate(1000, style({opacity: 0}))])])])] - }) - class ParentCmp { - public exp: any; - - @ViewChild('child', {static: true}) public child: any; - } - - @Component({ - selector: 'child-cmp', - template: ` -
- {{ item }} -
- ` - }) - class ChildCmp { - public items: any[] = []; - } - - TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); - const fixture = TestBed.createComponent(ParentCmp); - const cmp = fixture.componentInstance; - - cmp.child.items = [4, 5, 6]; - fixture.detectChanges(); - - cmp.exp = 'on'; - cmp.child.items = []; - fixture.detectChanges(); - - const players = getLog() as any[]; - expect(players.length).toEqual(3); - - expect(players[0].element.innerText.trim()).toEqual('4'); - expect(players[1].element.innerText.trim()).toEqual('5'); - expect(players[2].element.innerText.trim()).toEqual('6'); - }); - - describe('options.limit', () => { - it('should limit results when a limit value is passed into the query options', () => { - @Component({ - selector: 'cmp', - template: ` -
-
- {{ item }} -
-
- `, - animations: [ - trigger( - 'myAnimation', - [ - transition( - '* => go', - [ - query( - '.item', - [ - style({opacity: 0}), - animate('1s', style({opacity: 1})), - ], - {limit: 2}), - ]), - ]), - ] - }) - class Cmp { - public exp: any; - public items: any[] = []; - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - cmp.items = ['a', 'b', 'c', 'd', 'e']; - fixture.detectChanges(); - - cmp.exp = 'go'; - fixture.detectChanges(); - - const players = getLog() as any[]; - expect(players.length).toEqual(2); - expect(players[0].element.innerText.trim()).toEqual('a'); - expect(players[1].element.innerText.trim()).toEqual('b'); - }); - - it('should support negative limit values by pulling in elements from the end of the query', - () => { - @Component({ - selector: 'cmp', - template: ` -
-
- {{ item }} -
-
- `, - animations: [ - trigger( - 'myAnimation', - [ - transition( - '* => go', - [ - query( - '.item', - [ - style({opacity: 0}), - animate('1s', style({opacity: 1})), - ], - {limit: -3}), - ]), - ]), - ] - }) - class Cmp { - public exp: any; - public items: any[] = []; - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - cmp.items = ['a', 'b', 'c', 'd', 'e']; - fixture.detectChanges(); - - cmp.exp = 'go'; - fixture.detectChanges(); - - const players = getLog() as any[]; - expect(players.length).toEqual(3); - expect(players[0].element.innerText.trim()).toEqual('c'); - expect(players[1].element.innerText.trim()).toEqual('d'); - expect(players[2].element.innerText.trim()).toEqual('e'); - }); - }); }); + }); - describe('sub triggers', () => { - it('should animate a sub trigger that exists in an inner element in the template', () => { - @Component({ - selector: 'ani-cmp', - template: ` + describe('sub triggers', () => { + it('should animate a sub trigger that exists in an inner element in the template', () => { + @Component({ + selector: 'ani-cmp', + template: `
`, - animations: [ - trigger('parent', [transition( - '* => go1', - [ - style({width: '0px'}), animate(1000, style({width: '100px'})), - query('.child', [animateChild()]) - ])]), - trigger('child', [transition( - '* => go2', - [ - style({height: '0px'}), - animate(1000, style({height: '100px'})), - ])]) - ] - }) - class Cmp { - public exp1: any; - public exp2: any; + animations: [ + trigger('parent', [transition( + '* => go1', + [ + style({width: '0px'}), animate(1000, style({width: '100px'})), + query('.child', [animateChild()]) + ])]), + trigger('child', [transition( + '* => go2', + [ + style({height: '0px'}), + animate(1000, style({height: '100px'})), + ])]) + ] + }) + class Cmp { + public exp1: any; + public exp2: any; - @ViewChild('parent') public elm1: any; + @ViewChild('parent') public elm1: any; - @ViewChild('child') public elm2: any; - } + @ViewChild('child') public elm2: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp1 = 'go1'; - cmp.exp2 = 'go2'; - fixture.detectChanges(); - engine.flush(); + cmp.exp1 = 'go1'; + cmp.exp2 = 'go2'; + fixture.detectChanges(); + engine.flush(); - const elm1 = cmp.elm1; - const elm2 = cmp.elm2; + const elm1 = cmp.elm1; + const elm2 = cmp.elm2; - const [p1, p2] = getLog(); - expect(p1.delay).toEqual(0); - expect(p1.element).toEqual(elm1.nativeElement); - expect(p1.duration).toEqual(1000); - expect(p1.keyframes).toEqual([{width: '0px', offset: 0}, {width: '100px', offset: 1}]); + const [p1, p2] = getLog(); + expect(p1.delay).toEqual(0); + expect(p1.element).toEqual(elm1.nativeElement); + expect(p1.duration).toEqual(1000); + expect(p1.keyframes).toEqual([{width: '0px', offset: 0}, {width: '100px', offset: 1}]); - expect(p2.delay).toEqual(0); - expect(p2.element).toEqual(elm2.nativeElement); - expect(p2.duration).toEqual(2000); - expect(p2.keyframes).toEqual([ - {height: '0px', offset: 0}, {height: '0px', offset: .5}, {height: '100px', offset: 1} - ]); - }); + expect(p2.delay).toEqual(0); + expect(p2.element).toEqual(elm2.nativeElement); + expect(p2.duration).toEqual(2000); + expect(p2.keyframes).toEqual([ + {height: '0px', offset: 0}, {height: '0px', offset: .5}, {height: '100px', offset: 1} + ]); + }); - it('should run and operate a series of triggers on a list of elements with overridden timing data', - () => { - @Component({ + it('should run and operate a series of triggers on a list of elements with overridden timing data', + () => { + @Component({ selector: 'ani-cmp', template: `
@@ -1908,284 +1909,275 @@ import {HostListener} from '../../src/metadata/directives'; ] }) class Cmp { - public exp: any; - public items: any[] = [0, 1, 2, 3, 4]; + public exp: any; + public items: any[] = [0, 1, 2, 3, 4]; - @ViewChild('parent') public elm: any; - } + @ViewChild('parent') public elm: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = 'go'; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'go'; + fixture.detectChanges(); + engine.flush(); - const parent = cmp.elm.nativeElement; - const elements = parent.querySelectorAll('.item'); + const parent = cmp.elm.nativeElement; + const elements = parent.querySelectorAll('.item'); - const players = getLog(); - expect(players.length).toEqual(7); - const [pA, pc1, pc2, pc3, pc4, pc5, pZ] = players; + const players = getLog(); + expect(players.length).toEqual(7); + const [pA, pc1, pc2, pc3, pc4, pc5, pZ] = players; - expect(pA.element).toEqual(parent); - expect(pA.delay).toEqual(0); - expect(pA.duration).toEqual(1000); + expect(pA.element).toEqual(parent); + expect(pA.delay).toEqual(0); + expect(pA.duration).toEqual(1000); - expect(pc1.element).toEqual(elements[0]); - expect(pc1.delay).toEqual(0); - expect(pc1.duration).toEqual(4000); + expect(pc1.element).toEqual(elements[0]); + expect(pc1.delay).toEqual(0); + expect(pc1.duration).toEqual(4000); - expect(pc2.element).toEqual(elements[1]); - expect(pc2.delay).toEqual(0); - expect(pc2.duration).toEqual(4000); + expect(pc2.element).toEqual(elements[1]); + expect(pc2.delay).toEqual(0); + expect(pc2.duration).toEqual(4000); - expect(pc3.element).toEqual(elements[2]); - expect(pc3.delay).toEqual(0); - expect(pc3.duration).toEqual(4000); + expect(pc3.element).toEqual(elements[2]); + expect(pc3.delay).toEqual(0); + expect(pc3.duration).toEqual(4000); - expect(pc4.element).toEqual(elements[3]); - expect(pc4.delay).toEqual(0); - expect(pc4.duration).toEqual(4000); + expect(pc4.element).toEqual(elements[3]); + expect(pc4.delay).toEqual(0); + expect(pc4.duration).toEqual(4000); - expect(pc5.element).toEqual(elements[4]); - expect(pc5.delay).toEqual(0); - expect(pc5.duration).toEqual(4000); + expect(pc5.element).toEqual(elements[4]); + expect(pc5.delay).toEqual(0); + expect(pc5.duration).toEqual(4000); - expect(pZ.element).toEqual(parent); - expect(pZ.delay).toEqual(4000); - expect(pZ.duration).toEqual(1000); - }); + expect(pZ.element).toEqual(parent); + expect(pZ.delay).toEqual(4000); + expect(pZ.duration).toEqual(1000); + }); - it('should silently continue if a sub trigger is animated that doesn\'t exist', () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should silently continue if a sub trigger is animated that doesn\'t exist', () => { + @Component({ + selector: 'ani-cmp', + template: `
`, - animations: - [trigger('parent', [transition( - '* => go', - [ - style({opacity: 0}), animate(1000, style({opacity: 1})), - query('.child', [animateChild({duration: '1s'})]), - animate(1000, style({opacity: 0})) - ])])] - }) - class Cmp { - public exp: any; - public items: any[] = [0, 1, 2, 3, 4]; + animations: + [trigger('parent', [transition( + '* => go', + [ + style({opacity: 0}), animate(1000, style({opacity: 1})), + query('.child', [animateChild({duration: '1s'})]), + animate(1000, style({opacity: 0})) + ])])] + }) + class Cmp { + public exp: any; + public items: any[] = [0, 1, 2, 3, 4]; - @ViewChild('parent') public elm: any; - } + @ViewChild('parent') public elm: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = 'go'; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 'go'; + fixture.detectChanges(); + engine.flush(); - const parent = cmp.elm.nativeElement; - const players = getLog(); - expect(players.length).toEqual(2); + const parent = cmp.elm.nativeElement; + const players = getLog(); + expect(players.length).toEqual(2); - const [pA, pZ] = players; - expect(pA.element).toEqual(parent); - expect(pA.delay).toEqual(0); - expect(pA.duration).toEqual(1000); + const [pA, pZ] = players; + expect(pA.element).toEqual(parent); + expect(pA.delay).toEqual(0); + expect(pA.duration).toEqual(1000); - expect(pZ.element).toEqual(parent); - expect(pZ.delay).toEqual(1000); - expect(pZ.duration).toEqual(1000); - }); + expect(pZ.element).toEqual(parent); + expect(pZ.delay).toEqual(1000); + expect(pZ.duration).toEqual(1000); + }); - it('should silently continue if a sub trigger is animated that doesn\'t contain a trigger that is setup for animation', - () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should silently continue if a sub trigger is animated that doesn\'t contain a trigger that is setup for animation', + () => { + @Component({ + selector: 'ani-cmp', + template: `
`, - animations: [ - trigger('child', [transition( - 'a => z', - [style({opacity: 0}), animate(1000, style({opacity: 1}))])]), - trigger('parent', [transition( - 'a => z', - [ - style({opacity: 0}), animate(1000, style({opacity: 1})), - query('.child', [animateChild({duration: '1s'})]), - animate(1000, style({opacity: 0})) - ])]) - ] - }) - class Cmp { - public exp1: any; - public exp2: any; + animations: [ + trigger( + 'child', + [transition('a => z', [style({opacity: 0}), animate(1000, style({opacity: 1}))])]), + trigger('parent', [transition( + 'a => z', + [ + style({opacity: 0}), animate(1000, style({opacity: 1})), + query('.child', [animateChild({duration: '1s'})]), + animate(1000, style({opacity: 0})) + ])]) + ] + }) + class Cmp { + public exp1: any; + public exp2: any; - @ViewChild('parent') public elm: any; - } + @ViewChild('parent') public elm: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp1 = 'a'; - cmp.exp2 = 'a'; - fixture.detectChanges(); - engine.flush(); - resetLog(); + cmp.exp1 = 'a'; + cmp.exp2 = 'a'; + fixture.detectChanges(); + engine.flush(); + resetLog(); - cmp.exp1 = 'z'; - fixture.detectChanges(); - engine.flush(); + cmp.exp1 = 'z'; + fixture.detectChanges(); + engine.flush(); - const parent = cmp.elm.nativeElement; - const players = getLog(); - expect(players.length).toEqual(2); + const parent = cmp.elm.nativeElement; + const players = getLog(); + expect(players.length).toEqual(2); - const [pA, pZ] = players; - expect(pA.element).toEqual(parent); - expect(pA.delay).toEqual(0); - expect(pA.duration).toEqual(1000); + const [pA, pZ] = players; + expect(pA.element).toEqual(parent); + expect(pA.delay).toEqual(0); + expect(pA.duration).toEqual(1000); - expect(pZ.element).toEqual(parent); - expect(pZ.delay).toEqual(1000); - expect(pZ.duration).toEqual(1000); - }); + expect(pZ.element).toEqual(parent); + expect(pZ.delay).toEqual(1000); + expect(pZ.duration).toEqual(1000); + }); - it('should animate all sub triggers on the element at the same time', () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should animate all sub triggers on the element at the same time', () => { + @Component({ + selector: 'ani-cmp', + template: `
`, - animations: [ - trigger('w', [ - transition('* => go', [ - style({ width: 0 }), - animate(1800, style({ width: '100px' })) - ]) - ]), - trigger('h', [ - transition('* => go', [ - style({ height: 0 }), - animate(1500, style({ height: '100px' })) - ]) - ]), - trigger('parent', [ - transition('* => go', [ - style({ opacity: 0 }), - animate(1000, style({ opacity: 1 })), - query('.child', [ - animateChild() - ]), - animate(1000, style({ opacity: 0 })) - ]) - ]) - ] - }) - class Cmp { - public exp1: any; - public exp2: any; + animations: [ + trigger( + 'w', + [transition('* => go', [style({width: 0}), animate(1800, style({width: '100px'}))])]), + trigger( + 'h', [transition( + '* => go', [style({height: 0}), animate(1500, style({height: '100px'}))])]), + trigger( + 'parent', [transition( + '* => go', + [ + style({opacity: 0}), animate(1000, style({opacity: 1})), + query('.child', [animateChild()]), animate(1000, style({opacity: 0})) + ])]) + ] + }) + class Cmp { + public exp1: any; + public exp2: any; - @ViewChild('parent') public elm: any; - } + @ViewChild('parent') public elm: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp1 = 'go'; - cmp.exp2 = 'go'; - fixture.detectChanges(); - engine.flush(); + cmp.exp1 = 'go'; + cmp.exp2 = 'go'; + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(4); - const [pA, pc1, pc2, pZ] = players; + const players = getLog(); + expect(players.length).toEqual(4); + const [pA, pc1, pc2, pZ] = players; - expect(pc1.delay).toEqual(0); - expect(pc1.duration).toEqual(2800); + expect(pc1.delay).toEqual(0); + expect(pc1.duration).toEqual(2800); - expect(pc2.delay).toEqual(0); - expect(pc2.duration).toEqual(2500); + expect(pc2.delay).toEqual(0); + expect(pc2.duration).toEqual(2500); - expect(pZ.delay).toEqual(2800); - expect(pZ.duration).toEqual(1000); - }); + expect(pZ.delay).toEqual(2800); + expect(pZ.duration).toEqual(1000); + }); - it('should skip a sub animation when a zero duration value is passed in', () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should skip a sub animation when a zero duration value is passed in', () => { + @Component({ + selector: 'ani-cmp', + template: `
`, - animations: [ - trigger('child', [transition( - '* => go', - [style({width: 0}), animate(1800, style({width: '100px'}))])]), - trigger('parent', [transition( - '* => go', - [ - style({opacity: 0}), animate(1000, style({opacity: 1})), - query('.child', [animateChild({duration: '0'})]), - animate(1000, style({opacity: 0})) - ])]) - ] - }) - class Cmp { - public exp1: any; - public exp2: any; + animations: [ + trigger( + 'child', + [transition('* => go', [style({width: 0}), animate(1800, style({width: '100px'}))])]), + trigger('parent', [transition( + '* => go', + [ + style({opacity: 0}), animate(1000, style({opacity: 1})), + query('.child', [animateChild({duration: '0'})]), + animate(1000, style({opacity: 0})) + ])]) + ] + }) + class Cmp { + public exp1: any; + public exp2: any; - @ViewChild('parent') public elm: any; - } + @ViewChild('parent') public elm: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp1 = 'go'; - cmp.exp2 = 'go'; - fixture.detectChanges(); - engine.flush(); + cmp.exp1 = 'go'; + cmp.exp2 = 'go'; + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(2); - const [pA, pZ] = players; + const players = getLog(); + expect(players.length).toEqual(2); + const [pA, pZ] = players; - expect(pA.delay).toEqual(0); - expect(pA.duration).toEqual(1000); + expect(pA.delay).toEqual(0); + expect(pA.duration).toEqual(1000); - expect(pZ.delay).toEqual(1000); - expect(pZ.duration).toEqual(1000); - }); + expect(pZ.delay).toEqual(1000); + expect(pZ.duration).toEqual(1000); + }); - it('should only allow a sub animation to be used up by a parent trigger once', () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should only allow a sub animation to be used up by a parent trigger once', () => { + @Component({ + selector: 'ani-cmp', + template: `
@@ -2193,272 +2185,272 @@ import {HostListener} from '../../src/metadata/directives';
`, - animations: [ - trigger('parent', [transition( - '* => go', - [ - style({opacity: 0}), animate(1000, style({opacity: 1})), - query('.child', animateChild()) - ])]), - trigger('child', [transition( - '* => go', - [style({opacity: 0}), animate(1800, style({opacity: 1}))])]) - ] - }) - class Cmp { - public exp1: any; - public exp2: any; + animations: [ + trigger('parent', [transition( + '* => go', + [ + style({opacity: 0}), animate(1000, style({opacity: 1})), + query('.child', animateChild()) + ])]), + trigger( + 'child', + [transition('* => go', [style({opacity: 0}), animate(1800, style({opacity: 1}))])]) + ] + }) + class Cmp { + public exp1: any; + public exp2: any; - @ViewChild('parent') public elm: any; - } + @ViewChild('parent') public elm: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp1 = 'go'; - cmp.exp2 = 'go'; - fixture.detectChanges(); - engine.flush(); + cmp.exp1 = 'go'; + cmp.exp2 = 'go'; + fixture.detectChanges(); + engine.flush(); - const players = getLog(); - expect(players.length).toEqual(3); + const players = getLog(); + expect(players.length).toEqual(3); - const [p1, p2, p3] = players; + const [p1, p2, p3] = players; - // parent2 is evaluated first because it is inside of parent1 - expect(p1.element.classList.contains('parent2')).toBeTruthy(); - expect(p2.element.classList.contains('child')).toBeTruthy(); - expect(p3.element.classList.contains('parent1')).toBeTruthy(); - }); + // parent2 is evaluated first because it is inside of parent1 + expect(p1.element.classList.contains('parent2')).toBeTruthy(); + expect(p2.element.classList.contains('child')).toBeTruthy(); + expect(p3.element.classList.contains('parent1')).toBeTruthy(); + }); - it('should emulate a leave animation on the nearest sub host elements when a parent is removed', - fakeAsync(() => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should emulate a leave animation on the nearest sub host elements when a parent is removed', + fakeAsync(() => { + @Component({ + selector: 'ani-cmp', + template: `
`, - animations: [ - trigger( - 'leave', - [ - transition(':leave', [animate(1000, style({color: 'gold'}))]), - ]), - trigger( - 'parent', - [ - transition(':leave', [query(':leave', animateChild())]), - ]), - ] - }) - class ParentCmp { - public exp: boolean = true; - @ViewChild('child') public childElm: any; + animations: [ + trigger( + 'leave', + [ + transition(':leave', [animate(1000, style({color: 'gold'}))]), + ]), + trigger( + 'parent', + [ + transition(':leave', [query(':leave', animateChild())]), + ]), + ] + }) + class ParentCmp { + public exp: boolean = true; + @ViewChild('child') public childElm: any; - public childEvent: any; + public childEvent: any; - animateStart(event: any) { - if (event.toState == 'void') { - this.childEvent = event; - } + animateStart(event: any) { + if (event.toState == 'void') { + this.childEvent = event; } } + } - @Component({ - selector: 'child-cmp', - template: '...', - animations: [ - trigger( - 'child', - [ - transition(':leave', [animate(1000, style({color: 'gold'}))]), - ]), - ] - }) - class ChildCmp { - public childEvent: any; + @Component({ + selector: 'child-cmp', + template: '...', + animations: [ + trigger( + 'child', + [ + transition(':leave', [animate(1000, style({color: 'gold'}))]), + ]), + ] + }) + class ChildCmp { + public childEvent: any; - @HostBinding('@child') public animate = true; + @HostBinding('@child') public animate = true; - @HostListener('@child.start', ['$event']) - animateStart(event: any) { - if (event.toState == 'void') { - this.childEvent = event; - } + @HostListener('@child.start', ['$event']) + animateStart(event: any) { + if (event.toState == 'void') { + this.childEvent = event; } } + } - TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); - const fixture = TestBed.createComponent(ParentCmp); - const cmp = fixture.componentInstance; + TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); + const fixture = TestBed.createComponent(ParentCmp); + const cmp = fixture.componentInstance; - fixture.detectChanges(); + fixture.detectChanges(); - const childCmp = cmp.childElm; + const childCmp = cmp.childElm; - cmp.exp = false; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp = false; + fixture.detectChanges(); + flushMicrotasks(); - expect(cmp.childEvent.toState).toEqual('void'); - expect(cmp.childEvent.totalTime).toEqual(1000); - expect(childCmp.childEvent.toState).toEqual('void'); - expect(childCmp.childEvent.totalTime).toEqual(1000); - })); + expect(cmp.childEvent.toState).toEqual('void'); + expect(cmp.childEvent.totalTime).toEqual(1000); + expect(childCmp.childEvent.toState).toEqual('void'); + expect(childCmp.childEvent.totalTime).toEqual(1000); + })); - it('should emulate a leave animation on a sub component\'s inner elements when a parent leave animation occurs with animateChild', - () => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should emulate a leave animation on a sub component\'s inner elements when a parent leave animation occurs with animateChild', + () => { + @Component({ + selector: 'ani-cmp', + template: `
`, - animations: [ - trigger( - 'myAnimation', - [ - transition( - ':leave', - [ - query('@*', animateChild()), - ]), - ]), - ] - }) - class ParentCmp { - public exp: boolean = true; - } + animations: [ + trigger( + 'myAnimation', + [ + transition( + ':leave', + [ + query('@*', animateChild()), + ]), + ]), + ] + }) + class ParentCmp { + public exp: boolean = true; + } - @Component({ - selector: 'child-cmp', - template: ` + @Component({ + selector: 'child-cmp', + template: `
`, - animations: [ - trigger( - 'myChildAnimation', - [ - transition( - ':leave', - [ - style({opacity: 0}), - animate('1s', style({opacity: 1})), - ]), - ]), - ] - }) - class ChildCmp { - } + animations: [ + trigger( + 'myChildAnimation', + [ + transition( + ':leave', + [ + style({opacity: 0}), + animate('1s', style({opacity: 1})), + ]), + ]), + ] + }) + class ChildCmp { + } - TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); + TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(ParentCmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(ParentCmp); + const cmp = fixture.componentInstance; - cmp.exp = true; - fixture.detectChanges(); + cmp.exp = true; + fixture.detectChanges(); - cmp.exp = false; - fixture.detectChanges(); + cmp.exp = false; + fixture.detectChanges(); - let players = getLog(); - expect(players.length).toEqual(1); - const [player] = players; + let players = getLog(); + expect(players.length).toEqual(1); + const [player] = players; - expect(player.element.classList.contains('inner-div')).toBeTruthy(); - expect(player.keyframes).toEqual([ - {opacity: '0', offset: 0}, - {opacity: '1', offset: 1}, - ]); - }); + expect(player.element.classList.contains('inner-div')).toBeTruthy(); + expect(player.keyframes).toEqual([ + {opacity: '0', offset: 0}, + {opacity: '1', offset: 1}, + ]); + }); - it('should not cause a removal of inner @trigger DOM nodes when a parent animation occurs', - fakeAsync(() => { - @Component({ - selector: 'ani-cmp', - template: ` + it('should not cause a removal of inner @trigger DOM nodes when a parent animation occurs', + fakeAsync(() => { + @Component({ + selector: 'ani-cmp', + template: `
this
child
`, - animations: [ - trigger( - 'parent', - [ - transition( - ':leave', - [ - style({opacity: 0}), - animate('1s', style({opacity: 1})), - ]), - ]), - trigger( - 'child', - [ - transition( - '* => something', - [ - style({opacity: 0}), - animate('1s', style({opacity: 1})), - ]), - ]), - ] - }) - class Cmp { - public exp: boolean = true; - } + animations: [ + trigger( + 'parent', + [ + transition( + ':leave', + [ + style({opacity: 0}), + animate('1s', style({opacity: 1})), + ]), + ]), + trigger( + 'child', + [ + transition( + '* => something', + [ + style({opacity: 0}), + animate('1s', style({opacity: 1})), + ]), + ]), + ] + }) + class Cmp { + public exp: boolean = true; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = true; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp = true; + fixture.detectChanges(); + flushMicrotasks(); - cmp.exp = false; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp = false; + fixture.detectChanges(); + flushMicrotasks(); - const players = getLog(); - expect(players.length).toEqual(1); + const players = getLog(); + expect(players.length).toEqual(1); - const element = players[0] !.element; - expect(element.innerText.trim()).toMatch(/this\s+child/mg); - })); + const element = players[0]!.element; + expect(element.innerText.trim()).toMatch(/this\s+child/mg); + })); - it('should only mark outermost *directive nodes :enter and :leave when inserts and removals occur', - () => { - @Component({ - selector: 'ani-cmp', - animations: [ - trigger( - 'anim', - [ - transition( - '* => enter', - [ - query(':enter', [animate(1000, style({color: 'red'}))]), - ]), - transition( - '* => leave', - [ - query(':leave', [animate(1000, style({color: 'blue'}))]), - ]), - ]), - ], - template: ` + it('should only mark outermost *directive nodes :enter and :leave when inserts and removals occur', + () => { + @Component({ + selector: 'ani-cmp', + animations: [ + trigger( + 'anim', + [ + transition( + '* => enter', + [ + query(':enter', [animate(1000, style({color: 'red'}))]), + ]), + transition( + '* => leave', + [ + query(':leave', [animate(1000, style({color: 'blue'}))]), + ]), + ]), + ], + template: `
@@ -2474,61 +2466,59 @@ import {HostListener} from '../../src/metadata/directives';
` - }) - class Cmp { - // TODO(issue/24571): remove '!'. - public exp !: boolean; - } + }) + class Cmp { + // TODO(issue/24571): remove '!'. + public exp!: boolean; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - const container = fixture.elementRef.nativeElement; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + const container = fixture.elementRef.nativeElement; - cmp.exp = true; - fixture.detectChanges(); - engine.flush(); + cmp.exp = true; + fixture.detectChanges(); + engine.flush(); - let players = getLog(); - resetLog(); - expect(players.length).toEqual(2); - const [p1, p2] = players; + let players = getLog(); + resetLog(); + expect(players.length).toEqual(2); + const [p1, p2] = players; - expect(p1.element.classList.contains('a')).toBeTrue(); - expect(p2.element.classList.contains('d')).toBeTrue(); + expect(p1.element.classList.contains('a')).toBeTrue(); + expect(p2.element.classList.contains('d')).toBeTrue(); - cmp.exp = false; - fixture.detectChanges(); - engine.flush(); + cmp.exp = false; + fixture.detectChanges(); + engine.flush(); - players = getLog(); - resetLog(); - expect(players.length).toEqual(2); - const [p3, p4] = players; + players = getLog(); + resetLog(); + expect(players.length).toEqual(2); + const [p3, p4] = players; - expect(p3.element.classList.contains('a')).toBeTrue(); - expect(p4.element.classList.contains('d')).toBeTrue(); - }); + expect(p3.element.classList.contains('a')).toBeTrue(); + expect(p4.element.classList.contains('d')).toBeTrue(); + }); - it('should collect multiple root levels of :enter and :leave nodes', () => { - @Component({ - selector: 'ani-cmp', - animations: [ - trigger('pageAnimation', [ + it('should collect multiple root levels of :enter and :leave nodes', () => { + @Component({ + selector: 'ani-cmp', + animations: [trigger( + 'pageAnimation', + [ transition(':enter', []), - transition('* => *', [ - query(':leave', [ - animate('1s', style({ opacity: 0 })) - ], { optional: true }), - query(':enter', [ - animate('1s', style({ opacity: 1 })) - ], { optional: true }) - ]) - ]) - ], - template: ` + transition( + '* => *', + [ + query(':leave', [animate('1s', style({opacity: 0}))], {optional: true}), + query(':enter', [animate('1s', style({opacity: 1}))], {optional: true}) + ]) + ])], + template: `
{{ title }}
@@ -2546,209 +2536,208 @@ import {HostListener} from '../../src/metadata/directives';
` - }) - class Cmp { - get title() { - if (this.page1) { - return 'hello from page1'; - } - return 'greetings from page2'; - } - - page1 = false; - page2 = false; - loading = false; - - get status() { - if (this.loading) return 'loading'; - if (this.page1) return 'page1'; - if (this.page2) return 'page2'; - return ''; + }) + class Cmp { + get title() { + if (this.page1) { + return 'hello from page1'; } + return 'greetings from page2'; } - TestBed.configureTestingModule({declarations: [Cmp]}); + page1 = false; + page2 = false; + loading = false; - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - cmp.loading = true; - fixture.detectChanges(); - engine.flush(); + get status() { + if (this.loading) return 'loading'; + if (this.page1) return 'page1'; + if (this.page2) return 'page2'; + return ''; + } + } - let players = getLog(); - resetLog(); - cancelAllPlayers(players); + TestBed.configureTestingModule({declarations: [Cmp]}); - cmp.page1 = true; - cmp.loading = false; - fixture.detectChanges(); - engine.flush(); + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + cmp.loading = true; + fixture.detectChanges(); + engine.flush(); - let p1: MockAnimationPlayer; - let p2: MockAnimationPlayer; - let p3: MockAnimationPlayer; + let players = getLog(); + resetLog(); + cancelAllPlayers(players); - players = getLog(); - expect(players.length).toEqual(3); - [p1, p2, p3] = players; + cmp.page1 = true; + cmp.loading = false; + fixture.detectChanges(); + engine.flush(); - expect(p1.element.classList.contains('loading')).toBe(true); - expect(p2.element.classList.contains('title')).toBe(true); - expect(p3.element.classList.contains('page1')).toBe(true); + let p1: MockAnimationPlayer; + let p2: MockAnimationPlayer; + let p3: MockAnimationPlayer; - resetLog(); - cancelAllPlayers(players); + players = getLog(); + expect(players.length).toEqual(3); + [p1, p2, p3] = players; - cmp.page1 = false; - cmp.loading = true; - fixture.detectChanges(); + expect(p1.element.classList.contains('loading')).toBe(true); + expect(p2.element.classList.contains('title')).toBe(true); + expect(p3.element.classList.contains('page1')).toBe(true); - players = getLog(); - cancelAllPlayers(players); + resetLog(); + cancelAllPlayers(players); - expect(players.length).toEqual(3); - [p1, p2, p3] = players; + cmp.page1 = false; + cmp.loading = true; + fixture.detectChanges(); - expect(p1.element.classList.contains('title')).toBe(true); - expect(p2.element.classList.contains('page1')).toBe(true); - expect(p3.element.classList.contains('loading')).toBe(true); + players = getLog(); + cancelAllPlayers(players); - resetLog(); - cancelAllPlayers(players); + expect(players.length).toEqual(3); + [p1, p2, p3] = players; - cmp.page2 = true; - cmp.loading = false; - fixture.detectChanges(); - engine.flush(); + expect(p1.element.classList.contains('title')).toBe(true); + expect(p2.element.classList.contains('page1')).toBe(true); + expect(p3.element.classList.contains('loading')).toBe(true); - players = getLog(); - expect(players.length).toEqual(3); - [p1, p2, p3] = players; + resetLog(); + cancelAllPlayers(players); - expect(p1.element.classList.contains('loading')).toBe(true); - expect(p2.element.classList.contains('title')).toBe(true); - expect(p3.element.classList.contains('page2')).toBe(true); - }); + cmp.page2 = true; + cmp.loading = false; + fixture.detectChanges(); + engine.flush(); - it('should emulate leave animation callbacks for all sub elements that have leave triggers within the component', - fakeAsync(() => { - @Component({ - selector: 'ani-cmp', - animations: [ - trigger('parent', []), trigger('child', []), - trigger( - 'childWithAnimation', - [ - transition( - ':leave', - [ - animate(1000, style({background: 'red'})), - ]), - ]) - ], - template: ` + players = getLog(); + expect(players.length).toEqual(3); + [p1, p2, p3] = players; + + expect(p1.element.classList.contains('loading')).toBe(true); + expect(p2.element.classList.contains('title')).toBe(true); + expect(p3.element.classList.contains('page2')).toBe(true); + }); + + it('should emulate leave animation callbacks for all sub elements that have leave triggers within the component', + fakeAsync(() => { + @Component({ + selector: 'ani-cmp', + animations: [ + trigger('parent', []), trigger('child', []), + trigger( + 'childWithAnimation', + [ + transition( + ':leave', + [ + animate(1000, style({background: 'red'})), + ]), + ]) + ], + template: `
` - }) - class Cmp { - // TODO(issue/24571): remove '!'. - public exp !: boolean; - public log: string[] = []; - callback(event: any) { - this.log.push(event.element.getAttribute('data-name') + '-' + event.phaseName); - } + }) + class Cmp { + // TODO(issue/24571): remove '!'. + public exp!: boolean; + public log: string[] = []; + callback(event: any) { + this.log.push(event.element.getAttribute('data-name') + '-' + event.phaseName); } + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = true; - fixture.detectChanges(); - flushMicrotasks(); - cmp.log = []; + cmp.exp = true; + fixture.detectChanges(); + flushMicrotasks(); + cmp.log = []; - cmp.exp = false; - fixture.detectChanges(); - flushMicrotasks(); - expect(cmp.log).toEqual([ - 'c1-start', 'c1-done', 'c2-start', 'c2-done', 'p-start', 'c3-start', 'c3-done', - 'p-done' - ]); - })); + cmp.exp = false; + fixture.detectChanges(); + flushMicrotasks(); + expect(cmp.log).toEqual([ + 'c1-start', 'c1-done', 'c2-start', 'c2-done', 'p-start', 'c3-start', 'c3-done', 'p-done' + ]); + })); - it('should build, but not run sub triggers when a parent animation is scheduled', () => { - @Component({ - selector: 'parent-cmp', - animations: - [trigger('parent', [transition('* => *', [animate(1000, style({opacity: 0}))])])], - template: '
' - }) - class ParentCmp { - public exp: any; + it('should build, but not run sub triggers when a parent animation is scheduled', () => { + @Component({ + selector: 'parent-cmp', + animations: + [trigger('parent', [transition('* => *', [animate(1000, style({opacity: 0}))])])], + template: '
' + }) + class ParentCmp { + public exp: any; - @ViewChild('child') public childCmp: any; - } + @ViewChild('child') public childCmp: any; + } - @Component({ - selector: 'child-cmp', - animations: - [trigger('child', [transition('* => *', [animate(1000, style({color: 'red'}))])])], - template: '
' - }) - class ChildCmp { - public exp: any; - } + @Component({ + selector: 'child-cmp', + animations: + [trigger('child', [transition('* => *', [animate(1000, style({color: 'red'}))])])], + template: '
' + }) + class ChildCmp { + public exp: any; + } - TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); + TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(ParentCmp); - fixture.detectChanges(); - engine.flush(); - resetLog(); + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(ParentCmp); + fixture.detectChanges(); + engine.flush(); + resetLog(); - const cmp = fixture.componentInstance; - const childCmp = cmp.childCmp; + const cmp = fixture.componentInstance; + const childCmp = cmp.childCmp; - cmp.exp = 1; - childCmp.exp = 1; - fixture.detectChanges(); - engine.flush(); + cmp.exp = 1; + childCmp.exp = 1; + fixture.detectChanges(); + engine.flush(); - // we have 2 players, but the child is not used even though - // it is created. - const players = getLog(); - expect(players.length).toEqual(2); - expect(engine.players.length).toEqual(1); + // we have 2 players, but the child is not used even though + // it is created. + const players = getLog(); + expect(players.length).toEqual(2); + expect(engine.players.length).toEqual(1); - expect((engine.players[0] as TransitionAnimationPlayer).getRealPlayer()).toBe(players[1]); - }); + expect((engine.players[0] as TransitionAnimationPlayer).getRealPlayer()).toBe(players[1]); + }); - it('should fire and synchronize the start/done callbacks on sub triggers even if they are not allowed to animate within the animation', - fakeAsync(() => { - @Component({ - selector: 'parent-cmp', - animations: [ - trigger( - 'parent', - [ - transition( - '* => go', - [ - style({height: '0px'}), - animate(1000, style({height: '100px'})), - ]), - ]), - ], - template: ` + it('should fire and synchronize the start/done callbacks on sub triggers even if they are not allowed to animate within the animation', + fakeAsync(() => { + @Component({ + selector: 'parent-cmp', + animations: [ + trigger( + 'parent', + [ + transition( + '* => go', + [ + style({height: '0px'}), + animate(1000, style({height: '100px'})), + ]), + ]), + ], + template: `
` - }) - class ParentCmp { - @ViewChild('child') public childCmp: any; + }) + class ParentCmp { + @ViewChild('child') public childCmp: any; - public exp: any; - public log: string[] = []; - public remove = false; + public exp: any; + public log: string[] = []; + public remove = false; - track(event: any) { this.log.push(`${event.triggerName}-${event.phaseName}`); } + track(event: any) { + this.log.push(`${event.triggerName}-${event.phaseName}`); } + } - @Component({ - selector: 'child-cmp', - animations: [ - trigger( - 'child', - [ - transition( - '* => go', - [ - style({width: '0px'}), - animate(1000, style({width: '100px'})), - ]), - ]), - ], - template: ` + @Component({ + selector: 'child-cmp', + animations: [ + trigger( + 'child', + [ + transition( + '* => go', + [ + style({width: '0px'}), + animate(1000, style({width: '100px'})), + ]), + ]), + ], + template: `
` - }) - class ChildCmp { - public exp: any; - public log: string[] = []; - track(event: any) { this.log.push(`${event.triggerName}-${event.phaseName}`); } + }) + class ChildCmp { + public exp: any; + public log: string[] = []; + track(event: any) { + this.log.push(`${event.triggerName}-${event.phaseName}`); } + } - TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(ParentCmp); - fixture.detectChanges(); - flushMicrotasks(); + TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(ParentCmp); + fixture.detectChanges(); + flushMicrotasks(); - const cmp = fixture.componentInstance; - const child = cmp.childCmp; + const cmp = fixture.componentInstance; + const child = cmp.childCmp; - expect(cmp.log).toEqual(['parent-start', 'parent-done']); - expect(child.log).toEqual(['child-start', 'child-done']); + expect(cmp.log).toEqual(['parent-start', 'parent-done']); + expect(child.log).toEqual(['child-start', 'child-done']); - cmp.log = []; - child.log = []; - cmp.exp = 'go'; - cmp.childCmp.exp = 'go'; - fixture.detectChanges(); - flushMicrotasks(); + cmp.log = []; + child.log = []; + cmp.exp = 'go'; + cmp.childCmp.exp = 'go'; + fixture.detectChanges(); + flushMicrotasks(); - expect(cmp.log).toEqual(['parent-start']); - expect(child.log).toEqual(['child-start']); + expect(cmp.log).toEqual(['parent-start']); + expect(child.log).toEqual(['child-start']); - const players = engine.players; - expect(players.length).toEqual(1); - players[0].finish(); + const players = engine.players; + expect(players.length).toEqual(1); + players[0].finish(); - expect(cmp.log).toEqual(['parent-start', 'parent-done']); - expect(child.log).toEqual(['child-start', 'child-done']); + expect(cmp.log).toEqual(['parent-start', 'parent-done']); + expect(child.log).toEqual(['child-start', 'child-done']); - cmp.log = []; - child.log = []; - cmp.remove = true; - fixture.detectChanges(); - flushMicrotasks(); + cmp.log = []; + child.log = []; + cmp.remove = true; + fixture.detectChanges(); + flushMicrotasks(); - expect(cmp.log).toEqual(['parent-start', 'parent-done']); - expect(child.log).toEqual(['child-start', 'child-done']); - })); + expect(cmp.log).toEqual(['parent-start', 'parent-done']); + expect(child.log).toEqual(['child-start', 'child-done']); + })); - it('should fire and synchronize the start/done callbacks on multiple blocked sub triggers', - fakeAsync(() => { - @Component({ - selector: 'cmp', - animations: [ - trigger( - 'parent1', - [ - transition( - '* => go, * => go-again', - [ - style({opacity: 0}), - animate('1s', style({opacity: 1})), - ]), - ]), - trigger( - 'parent2', - [ - transition( - '* => go, * => go-again', - [ - style({lineHeight: '0px'}), - animate('1s', style({lineHeight: '10px'})), - ]), - ]), - trigger( - 'child1', - [ - transition( - '* => go, * => go-again', - [ - style({width: '0px'}), - animate('1s', style({width: '100px'})), - ]), - ]), - trigger( - 'child2', - [ - transition( - '* => go, * => go-again', - [ - style({height: '0px'}), - animate('1s', style({height: '100px'})), - ]), - ]), - ], - template: ` + it('should fire and synchronize the start/done callbacks on multiple blocked sub triggers', + fakeAsync(() => { + @Component({ + selector: 'cmp', + animations: [ + trigger( + 'parent1', + [ + transition( + '* => go, * => go-again', + [ + style({opacity: 0}), + animate('1s', style({opacity: 1})), + ]), + ]), + trigger( + 'parent2', + [ + transition( + '* => go, * => go-again', + [ + style({lineHeight: '0px'}), + animate('1s', style({lineHeight: '10px'})), + ]), + ]), + trigger( + 'child1', + [ + transition( + '* => go, * => go-again', + [ + style({width: '0px'}), + animate('1s', style({width: '100px'})), + ]), + ]), + trigger( + 'child2', + [ + transition( + '* => go, * => go-again', + [ + style({height: '0px'}), + animate('1s', style({height: '100px'})), + ]), + ]), + ], + template: `
` - }) - class Cmp { - public parent1Exp = ''; - public parent2Exp = ''; - public child1Exp = ''; - public child2Exp = ''; - public log: string[] = []; + }) + class Cmp { + public parent1Exp = ''; + public parent2Exp = ''; + public child1Exp = ''; + public child2Exp = ''; + 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]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - fixture.detectChanges(); - flushMicrotasks(); + TestBed.configureTestingModule({declarations: [Cmp]}); + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + flushMicrotasks(); - const cmp = fixture.componentInstance; - cmp.log = []; - cmp.parent1Exp = 'go'; - cmp.parent2Exp = 'go'; - cmp.child1Exp = 'go'; - cmp.child2Exp = 'go'; - fixture.detectChanges(); - flushMicrotasks(); + const cmp = fixture.componentInstance; + cmp.log = []; + cmp.parent1Exp = 'go'; + cmp.parent2Exp = 'go'; + cmp.child1Exp = 'go'; + cmp.child2Exp = 'go'; + fixture.detectChanges(); + flushMicrotasks(); - expect(cmp.log).toEqual( - ['parent1-start', 'parent2-start', 'child1-start', 'child2-start']); + expect(cmp.log).toEqual( + ['parent1-start', 'parent2-start', 'child1-start', 'child2-start']); - cmp.parent1Exp = 'go-again'; - cmp.parent2Exp = 'go-again'; - cmp.child1Exp = 'go-again'; - cmp.child2Exp = 'go-again'; - fixture.detectChanges(); - flushMicrotasks(); - })); + cmp.parent1Exp = 'go-again'; + cmp.parent2Exp = 'go-again'; + cmp.child1Exp = 'go-again'; + cmp.child2Exp = 'go-again'; + fixture.detectChanges(); + flushMicrotasks(); + })); - it('should stretch the starting keyframe of a child animation queries are issued by the parent', - () => { - @Component({ - selector: 'parent-cmp', - animations: [trigger( - 'parent', - [transition( - '* => *', - [animate(1000, style({color: 'red'})), query('@child', animateChild())])])], - template: '
' - }) - class ParentCmp { - public exp: any; + it('should stretch the starting keyframe of a child animation queries are issued by the parent', + () => { + @Component({ + selector: 'parent-cmp', + animations: [trigger( + 'parent', + [transition( + '* => *', + [animate(1000, style({color: 'red'})), query('@child', animateChild())])])], + template: '
' + }) + class ParentCmp { + public exp: any; - @ViewChild('child') public childCmp: any; + @ViewChild('child') public childCmp: any; + } + + @Component({ + selector: 'child-cmp', + animations: [trigger( + 'child', + [transition( + '* => *', [style({color: 'blue'}), animate(1000, style({color: 'red'}))])])], + template: '
' + }) + class ChildCmp { + public exp: any; + } + + TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); + + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(ParentCmp); + fixture.detectChanges(); + engine.flush(); + resetLog(); + + const cmp = fixture.componentInstance; + const childCmp = cmp.childCmp; + + cmp.exp = 1; + childCmp.exp = 1; + fixture.detectChanges(); + engine.flush(); + + expect(engine.players.length).toEqual(1); // child player, parent cover, parent player + const groupPlayer = (engine.players[0] as TransitionAnimationPlayer).getRealPlayer() as + AnimationGroupPlayer; + const childPlayer = groupPlayer.players.find(player => { + if (player instanceof MockAnimationPlayer) { + return matchesElement(player.element, '.child'); } + return false; + }) as MockAnimationPlayer; - @Component({ - selector: 'child-cmp', - animations: [trigger( - 'child', - [transition( - '* => *', [style({color: 'blue'}), animate(1000, style({color: 'red'}))])])], - template: '
' - }) - class ChildCmp { - public exp: any; - } - - TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); - - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(ParentCmp); - fixture.detectChanges(); - engine.flush(); - resetLog(); - - const cmp = fixture.componentInstance; - const childCmp = cmp.childCmp; - - cmp.exp = 1; - childCmp.exp = 1; - fixture.detectChanges(); - engine.flush(); - - expect(engine.players.length).toEqual(1); // child player, parent cover, parent player - const groupPlayer = (engine.players[0] as TransitionAnimationPlayer) - .getRealPlayer() as AnimationGroupPlayer; - const childPlayer = groupPlayer.players.find(player => { - if (player instanceof MockAnimationPlayer) { - return matchesElement(player.element, '.child'); - } - return false; - }) as MockAnimationPlayer; - - const keyframes = childPlayer.keyframes.map(kf => { - delete kf['offset']; - return kf; - }); - - expect(keyframes.length).toEqual(3); - - const [k1, k2, k3] = keyframes; - expect(k1).toEqual(k2); + const keyframes = childPlayer.keyframes.map(kf => { + delete kf['offset']; + return kf; }); - it('should allow a parent trigger to control child triggers across multiple template boundaries even if there are no animations in between', - () => { - @Component({ - selector: 'parent-cmp', - animations: [ - trigger( - 'parentAnimation', - [ - transition( - '* => go', - [ - query(':self, @grandChildAnimation', style({opacity: 0})), - animate(1000, style({opacity: 1})), - query( - '@grandChildAnimation', - [ - animate(1000, style({opacity: 1})), - animateChild(), - ]), - ]), - ]), - ], - template: '
' - }) - class ParentCmp { - public exp: any; + expect(keyframes.length).toEqual(3); - @ViewChild('child') public innerCmp: any; - } + const [k1, k2, k3] = keyframes; + expect(k1).toEqual(k2); + }); - @Component( - {selector: 'child-cmp', template: ''}) - class ChildCmp { - @ViewChild('grandchild') public innerCmp: any; - } + it('should allow a parent trigger to control child triggers across multiple template boundaries even if there are no animations in between', + () => { + @Component({ + selector: 'parent-cmp', + animations: [ + trigger( + 'parentAnimation', + [ + transition( + '* => go', + [ + query(':self, @grandChildAnimation', style({opacity: 0})), + animate(1000, style({opacity: 1})), + query( + '@grandChildAnimation', + [ + animate(1000, style({opacity: 1})), + animateChild(), + ]), + ]), + ]), + ], + template: '
' + }) + class ParentCmp { + public exp: any; - @Component({ - selector: 'grandchild-cmp', - animations: [ - trigger( - 'grandChildAnimation', - [ - transition( - '* => go', - [ - style({width: '0px'}), - animate(1000, style({width: '200px'})), - ]), - ]), - ], - template: '
' - }) - class GrandChildCmp { - public exp: any; - } + @ViewChild('child') public innerCmp: any; + } - TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp, GrandChildCmp]}); + @Component( + {selector: 'child-cmp', template: ''}) + class ChildCmp { + @ViewChild('grandchild') public innerCmp: any; + } - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(ParentCmp); - fixture.detectChanges(); - engine.flush(); - resetLog(); + @Component({ + selector: 'grandchild-cmp', + animations: [ + trigger( + 'grandChildAnimation', + [ + transition( + '* => go', + [ + style({width: '0px'}), + animate(1000, style({width: '200px'})), + ]), + ]), + ], + template: '
' + }) + class GrandChildCmp { + public exp: any; + } - const cmp = fixture.componentInstance; - const grandChildCmp = cmp.innerCmp.innerCmp; + TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp, GrandChildCmp]}); - cmp.exp = 'go'; - grandChildCmp.exp = 'go'; + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(ParentCmp); + fixture.detectChanges(); + engine.flush(); + resetLog(); - fixture.detectChanges(); - engine.flush(); - const players = getLog(); - expect(players.length).toEqual(5); - const [p1, p2, p3, p4, p5] = players; + const cmp = fixture.componentInstance; + const grandChildCmp = cmp.innerCmp.innerCmp; - expect(p5.keyframes).toEqual([ - {offset: 0, width: '0px'}, {offset: .67, width: '0px'}, {offset: 1, width: '200px'} - ]); - }); + cmp.exp = 'go'; + grandChildCmp.exp = 'go'; - it('should scope :enter queries between sub animations', () => { - @Component({ - selector: 'cmp', - animations: [ - trigger( - 'parent', - [ - transition(':enter', group([ - sequence([ - style({opacity: 0}), - animate(1000, style({opacity: 1})), - ]), - query(':enter @child', animateChild()), - ])), - ]), - trigger( - 'child', - [ - transition( - ':enter', - [ - query( - ':enter .item', - [style({opacity: 0}), animate(1000, style({opacity: 1}))]), - ]), - ]), - ], - template: ` + fixture.detectChanges(); + engine.flush(); + const players = getLog(); + expect(players.length).toEqual(5); + const [p1, p2, p3, p4, p5] = players; + + expect(p5.keyframes).toEqual([ + {offset: 0, width: '0px'}, {offset: .67, width: '0px'}, {offset: 1, width: '200px'} + ]); + }); + + it('should scope :enter queries between sub animations', () => { + @Component({ + selector: 'cmp', + animations: [ + trigger( + 'parent', + [ + transition(':enter', group([ + sequence([ + style({opacity: 0}), + animate(1000, style({opacity: 1})), + ]), + query(':enter @child', animateChild()), + ])), + ]), + trigger( + 'child', + [ + transition( + ':enter', + [ + query( + ':enter .item', + [style({opacity: 0}), animate(1000, style({opacity: 1}))]), + ]), + ]), + ], + template: `
@@ -3107,107 +3102,107 @@ import {HostListener} from '../../src/metadata/directives';
` - }) - class Cmp { - public exp1: any; - public exp2: any; - public exp3: any; - } + }) + class Cmp { + public exp1: any; + public exp2: any; + public exp3: any; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - fixture.detectChanges(); - resetLog(); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + resetLog(); - const cmp = fixture.componentInstance; - cmp.exp1 = true; - cmp.exp2 = true; - cmp.exp3 = true; - fixture.detectChanges(); + const cmp = fixture.componentInstance; + cmp.exp1 = true; + cmp.exp2 = true; + cmp.exp3 = true; + fixture.detectChanges(); - const players = getLog(); - expect(players.length).toEqual(2); + const players = getLog(); + expect(players.length).toEqual(2); - const [p1, p2] = players; - expect(p1.element.classList.contains('container')).toBeTruthy(); - expect(p2.element.classList.contains('item')).toBeTruthy(); - }); - - it('should scope :leave queries between sub animations', () => { - @Component({ - selector: 'cmp', - animations: [ - trigger( - 'parent', - [ - transition(':leave', group([ - sequence([ - style({opacity: 0}), - animate(1000, style({opacity: 1})), - ]), - query(':leave @child', animateChild()), - ])), - ]), - trigger( - 'child', - [ - transition( - ':leave', - [ - query( - ':leave .item', - [style({opacity: 0}), animate(1000, style({opacity: 1}))]), - ]), - ]), - ], - template: ` -
-
-
-
-
-
-
-
-
- ` - }) - class Cmp { - public exp1: any; - public exp2: any; - public exp3: any; - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - cmp.exp1 = true; - cmp.exp2 = true; - cmp.exp3 = true; - fixture.detectChanges(); - resetLog(); - - cmp.exp1 = false; - fixture.detectChanges(); - - const players = getLog(); - expect(players.length).toEqual(2); - - const [p1, p2] = players; - expect(p1.element.classList.contains('container')).toBeTruthy(); - expect(p2.element.classList.contains('item')).toBeTruthy(); - }); + const [p1, p2] = players; + expect(p1.element.classList.contains('container')).toBeTruthy(); + expect(p2.element.classList.contains('item')).toBeTruthy(); }); - describe('animation control flags', () => { - describe('[@.disabled]', () => { - it('should allow a parent animation to query and animate inner nodes that are in a disabled region', - () => { - @Component({ - selector: 'some-cmp', - template: ` + it('should scope :leave queries between sub animations', () => { + @Component({ + selector: 'cmp', + animations: [ + trigger( + 'parent', + [ + transition(':leave', group([ + sequence([ + style({opacity: 0}), + animate(1000, style({opacity: 1})), + ]), + query(':leave @child', animateChild()), + ])), + ]), + trigger( + 'child', + [ + transition( + ':leave', + [ + query( + ':leave .item', + [style({opacity: 0}), animate(1000, style({opacity: 1}))]), + ]), + ]), + ], + template: ` +
+
+
+
+
+
+
+
+
+ ` + }) + class Cmp { + public exp1: any; + public exp2: any; + public exp3: any; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + cmp.exp1 = true; + cmp.exp2 = true; + cmp.exp3 = true; + fixture.detectChanges(); + resetLog(); + + cmp.exp1 = false; + fixture.detectChanges(); + + const players = getLog(); + expect(players.length).toEqual(2); + + const [p1, p2] = players; + expect(p1.element.classList.contains('container')).toBeTruthy(); + expect(p2.element.classList.contains('item')).toBeTruthy(); + }); + }); + + describe('animation control flags', () => { + describe('[@.disabled]', () => { + it('should allow a parent animation to query and animate inner nodes that are in a disabled region', + () => { + @Component({ + selector: 'some-cmp', + template: `
@@ -3215,101 +3210,101 @@ import {HostListener} from '../../src/metadata/directives';
`, - animations: [ - trigger( - 'myAnimation', - [ - transition( - '* => go', - [ - query('.header', animate(750, style({opacity: 0}))), - query('.footer', animate(250, style({opacity: 0}))), - ]), - ]), - ] - }) - class Cmp { - exp: any = ''; - disableExp = false; - } + animations: [ + trigger( + 'myAnimation', + [ + transition( + '* => go', + [ + query('.header', animate(750, style({opacity: 0}))), + query('.footer', animate(250, style({opacity: 0}))), + ]), + ]), + ] + }) + class Cmp { + exp: any = ''; + disableExp = false; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - cmp.disableExp = true; - fixture.detectChanges(); - resetLog(); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + cmp.disableExp = true; + fixture.detectChanges(); + resetLog(); - cmp.exp = 'go'; - fixture.detectChanges(); - const players = getLog(); - expect(players.length).toEqual(2); + cmp.exp = 'go'; + fixture.detectChanges(); + const players = getLog(); + expect(players.length).toEqual(2); - const [p1, p2] = players; - expect(p1.duration).toEqual(750); - expect(p1.element.classList.contains('header')).toBeTrue(); - expect(p2.duration).toEqual(250); - expect(p2.element.classList.contains('footer')).toBeTrue(); - }); + const [p1, p2] = players; + expect(p1.duration).toEqual(750); + expect(p1.element.classList.contains('header')).toBeTrue(); + expect(p2.duration).toEqual(250); + expect(p2.element.classList.contains('footer')).toBeTrue(); + }); - it('should allow a parent animation to query and animate sub animations that are in a disabled region', - () => { - @Component({ - selector: 'some-cmp', - template: ` + it('should allow a parent animation to query and animate sub animations that are in a disabled region', + () => { + @Component({ + selector: 'some-cmp', + template: `
`, - animations: [ - trigger( - 'parentAnimation', - [ - transition( - '* => go', - [ - query('@childAnimation', animateChild()), - animate(1000, style({opacity: 0})) - ]), - ]), - trigger( - 'childAnimation', - [ - transition('* => go', [animate(500, style({opacity: 0}))]), - ]), - ] - }) - class Cmp { - exp: any = ''; - disableExp = false; - } + animations: [ + trigger( + 'parentAnimation', + [ + transition( + '* => go', + [ + query('@childAnimation', animateChild()), + animate(1000, style({opacity: 0})) + ]), + ]), + trigger( + 'childAnimation', + [ + transition('* => go', [animate(500, style({opacity: 0}))]), + ]), + ] + }) + class Cmp { + exp: any = ''; + disableExp = false; + } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - cmp.disableExp = true; - fixture.detectChanges(); - resetLog(); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + cmp.disableExp = true; + fixture.detectChanges(); + resetLog(); - cmp.exp = 'go'; - fixture.detectChanges(); + cmp.exp = 'go'; + fixture.detectChanges(); - const players = getLog(); - expect(players.length).toEqual(2); + const players = getLog(); + expect(players.length).toEqual(2); - const [p1, p2] = players; - expect(p1.duration).toEqual(500); - expect(p1.element.classList.contains('child')).toBeTrue(); - expect(p2.duration).toEqual(1000); - expect(p2.element.classList.contains('parent')).toBeTrue(); - }); - }); + const [p1, p2] = players; + expect(p1.duration).toEqual(500); + expect(p1.element.classList.contains('child')).toBeTrue(); + expect(p2.duration).toEqual(1000); + expect(p2.element.classList.contains('parent')).toBeTrue(); + }); }); }); +}); })(); function cancelAllPlayers(players: AnimationPlayer[]) { diff --git a/packages/core/test/linker/change_detection_integration_spec.ts b/packages/core/test/linker/change_detection_integration_spec.ts index ed67524610..e9bd1f37fa 100644 --- a/packages/core/test/linker/change_detection_integration_spec.ts +++ b/packages/core/test/linker/change_detection_integration_spec.ts @@ -9,7 +9,7 @@ import {ResourceLoader, UrlResolver} from '@angular/compiler'; 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 {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 {isTextNode} from '@angular/platform-browser/testing/src/browser_util'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -26,1667 +26,1697 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ (function() { - let renderLog: RenderLog; - let directiveLog: DirectiveLog; +let renderLog: RenderLog; +let directiveLog: DirectiveLog; - function createCompFixture(template: string): ComponentFixture; - function createCompFixture(template: string, compType: Type): ComponentFixture; - function createCompFixture( - template: string, compType: Type = TestComponent): ComponentFixture { - TestBed.overrideComponent(compType, {set: new Component({template})}); +function createCompFixture(template: string): ComponentFixture; +function createCompFixture(template: string, compType: Type): ComponentFixture; +function createCompFixture( + template: string, compType: Type = TestComponent): ComponentFixture { + TestBed.overrideComponent(compType, {set: new Component({template})}); - initHelpers(); + initHelpers(); - return TestBed.createComponent(compType); - } + return TestBed.createComponent(compType); +} - function initHelpers(): void { - renderLog = TestBed.inject(RenderLog); - directiveLog = TestBed.inject(DirectiveLog); - patchLoggingRenderer2(TestBed.inject(RendererFactory2), renderLog); - } +function initHelpers(): void { + renderLog = TestBed.inject(RenderLog); + directiveLog = TestBed.inject(DirectiveLog); + patchLoggingRenderer2(TestBed.inject(RendererFactory2), renderLog); +} - function queryDirs(el: DebugElement, dirType: Type): any { - const nodes = el.queryAllNodes(By.directive(dirType)); - return nodes.map(node => node.injector.get(dirType)); - } +function queryDirs(el: DebugElement, dirType: Type): any { + const nodes = el.queryAllNodes(By.directive(dirType)); + return nodes.map(node => node.injector.get(dirType)); +} - function _bindSimpleProp(bindAttr: string): ComponentFixture; - function _bindSimpleProp(bindAttr: string, compType: Type): ComponentFixture; - function _bindSimpleProp( - bindAttr: string, compType: Type = TestComponent): ComponentFixture { - const template = `
`; - return createCompFixture(template, compType); - } +function _bindSimpleProp(bindAttr: string): ComponentFixture; +function _bindSimpleProp(bindAttr: string, compType: Type): ComponentFixture; +function _bindSimpleProp( + bindAttr: string, compType: Type = TestComponent): ComponentFixture { + const template = `
`; + return createCompFixture(template, compType); +} - function _bindSimpleValue(expression: any): ComponentFixture; - function _bindSimpleValue(expression: any, compType: Type): ComponentFixture; - function _bindSimpleValue( - expression: any, compType: Type = TestComponent): ComponentFixture { - return _bindSimpleProp(`[id]='${expression}'`, compType); - } +function _bindSimpleValue(expression: any): ComponentFixture; +function _bindSimpleValue(expression: any, compType: Type): ComponentFixture; +function _bindSimpleValue( + expression: any, compType: Type = TestComponent): ComponentFixture { + return _bindSimpleProp(`[id]='${expression}'`, compType); +} - function _bindAndCheckSimpleValue( - expression: any, compType: Type = TestComponent): string[] { - const ctx = _bindSimpleValue(expression, compType); - ctx.detectChanges(false); - return renderLog.log; - } +function _bindAndCheckSimpleValue(expression: any, compType: Type = TestComponent): string[] { + const ctx = _bindSimpleValue(expression, compType); + ctx.detectChanges(false); + return renderLog.log; +} - describe(`ChangeDetection`, () => { - - beforeEach(() => { - TestBed.configureCompiler({providers: TEST_COMPILER_PROVIDERS}); - TestBed.configureTestingModule({ - declarations: [ - TestData, - TestDirective, - TestComponent, - AnotherComponent, - TestLocals, - CompWithRef, - WrapCompWithRef, - EmitterDirective, - PushComp, - OnDestroyDirective, - OrderCheckDirective2, - OrderCheckDirective0, - OrderCheckDirective1, - Gh9882, - Uninitialized, - Person, - PersonHolder, - PersonHolderHolder, - CountingPipe, - CountingImpurePipe, - MultiArgPipe, - PipeWithOnDestroy, - IdentityPipe, - WrappedPipe, - ], - providers: [ - RenderLog, - DirectiveLog, - ], - }); +describe(`ChangeDetection`, () => { + beforeEach(() => { + TestBed.configureCompiler({providers: TEST_COMPILER_PROVIDERS}); + TestBed.configureTestingModule({ + declarations: [ + TestData, + TestDirective, + TestComponent, + AnotherComponent, + TestLocals, + CompWithRef, + WrapCompWithRef, + EmitterDirective, + PushComp, + OnDestroyDirective, + OrderCheckDirective2, + OrderCheckDirective0, + OrderCheckDirective1, + Gh9882, + Uninitialized, + Person, + PersonHolder, + PersonHolderHolder, + CountingPipe, + CountingImpurePipe, + MultiArgPipe, + PipeWithOnDestroy, + IdentityPipe, + WrappedPipe, + ], + providers: [ + RenderLog, + DirectiveLog, + ], }); + }); - describe('expressions', () => { - it('should support literals', - fakeAsync(() => { expect(_bindAndCheckSimpleValue(10)).toEqual(['id=10']); })); + describe('expressions', () => { + it('should support literals', fakeAsync(() => { + expect(_bindAndCheckSimpleValue(10)).toEqual(['id=10']); + })); - it('should strip quotes from literals', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('"str"')).toEqual(['id=str']); })); + it('should strip quotes from literals', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('"str"')).toEqual(['id=str']); + })); - it('should support newlines in literals', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('"a\n\nb"')).toEqual(['id=a\n\nb']); })); + it('should support newlines in literals', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('"a\n\nb"')).toEqual(['id=a\n\nb']); + })); - it('should support + operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 + 2')).toEqual(['id=12']); })); + it('should support + operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('10 + 2')).toEqual(['id=12']); + })); - it('should support - operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 - 2')).toEqual(['id=8']); })); + it('should support - operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('10 - 2')).toEqual(['id=8']); + })); - it('should support * operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 * 2')).toEqual(['id=20']); })); + it('should support * operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('10 * 2')).toEqual(['id=20']); + })); - it('should support / operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('10 / 2')).toEqual([`id=${5.0}`]); - })); // dart exp=5.0, js exp=5 + it('should support / operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('10 / 2')).toEqual([`id=${5.0}`]); + })); // dart exp=5.0, js exp=5 - it('should support % operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('11 % 2')).toEqual(['id=1']); })); + it('should support % operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('11 % 2')).toEqual(['id=1']); + })); - it('should support == operations on identical', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 == 1')).toEqual(['id=true']); })); + it('should support == operations on identical', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('1 == 1')).toEqual(['id=true']); + })); - it('should support != operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 != 1')).toEqual(['id=false']); })); + it('should support != operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('1 != 1')).toEqual(['id=false']); + })); - it('should support == operations on coerceible', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 == true')).toEqual([`id=true`]); })); + it('should support == operations on coerceible', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('1 == true')).toEqual([`id=true`]); + })); - it('should support === operations on identical', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 === 1')).toEqual(['id=true']); })); + it('should support === operations on identical', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('1 === 1')).toEqual(['id=true']); + })); - it('should support !== operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 !== 1')).toEqual(['id=false']); })); + it('should support !== operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('1 !== 1')).toEqual(['id=false']); + })); - it('should support === operations on coerceible', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('1 === true')).toEqual(['id=false']); - })); + it('should support === operations on coerceible', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('1 === true')).toEqual(['id=false']); + })); - it('should support true < operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 < 2')).toEqual(['id=true']); })); + it('should support true < operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('1 < 2')).toEqual(['id=true']); + })); - it('should support false < operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 < 1')).toEqual(['id=false']); })); + it('should support false < operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('2 < 1')).toEqual(['id=false']); + })); - it('should support false > operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 > 2')).toEqual(['id=false']); })); + it('should support false > operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('1 > 2')).toEqual(['id=false']); + })); - it('should support true > operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 > 1')).toEqual(['id=true']); })); + it('should support true > operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('2 > 1')).toEqual(['id=true']); + })); - it('should support true <= operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 <= 2')).toEqual(['id=true']); })); + it('should support true <= operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('1 <= 2')).toEqual(['id=true']); + })); - it('should support equal <= operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 <= 2')).toEqual(['id=true']); })); + it('should support equal <= operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('2 <= 2')).toEqual(['id=true']); + })); - it('should support false <= operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 <= 1')).toEqual(['id=false']); })); + it('should support false <= operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('2 <= 1')).toEqual(['id=false']); + })); - it('should support true >= operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 >= 1')).toEqual(['id=true']); })); + it('should support true >= operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('2 >= 1')).toEqual(['id=true']); + })); - it('should support equal >= operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 >= 2')).toEqual(['id=true']); })); + it('should support equal >= operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('2 >= 2')).toEqual(['id=true']); + })); - it('should support false >= operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 >= 2')).toEqual(['id=false']); })); + it('should support false >= operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('1 >= 2')).toEqual(['id=false']); + })); - it('should support true && operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('true && true')).toEqual(['id=true']); - })); + it('should support true && operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('true && true')).toEqual(['id=true']); + })); - it('should support false && operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('true && false')).toEqual(['id=false']); - })); + it('should support false && operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('true && false')).toEqual(['id=false']); + })); - it('should support true || operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('true || false')).toEqual(['id=true']); - })); + it('should support true || operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('true || false')).toEqual(['id=true']); + })); - it('should support false || operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('false || false')).toEqual(['id=false']); - })); + it('should support false || operations', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('false || false')).toEqual(['id=false']); + })); - it('should support negate', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('!true')).toEqual(['id=false']); })); + it('should support negate', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('!true')).toEqual(['id=false']); + })); - it('should support double negate', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('!!true')).toEqual(['id=true']); })); + it('should support double negate', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('!!true')).toEqual(['id=true']); + })); - it('should support true conditionals', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 < 2 ? 1 : 2')).toEqual(['id=1']); })); + it('should support true conditionals', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('1 < 2 ? 1 : 2')).toEqual(['id=1']); + })); - it('should support false conditionals', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 > 2 ? 1 : 2')).toEqual(['id=2']); })); + it('should support false conditionals', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('1 > 2 ? 1 : 2')).toEqual(['id=2']); + })); - it('should support keyed access to a list item', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('["foo", "bar"][0]')).toEqual(['id=foo']); - })); + it('should support keyed access to a list item', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('["foo", "bar"][0]')).toEqual(['id=foo']); + })); - it('should support keyed access to a map item', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('{"foo": "bar"}["foo"]')).toEqual(['id=bar']); - })); + it('should support keyed access to a map item', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('{"foo": "bar"}["foo"]')).toEqual(['id=bar']); + })); - it('should report all changes on the first run including uninitialized values', - fakeAsync(() => { - expect(_bindAndCheckSimpleValue('value', Uninitialized)).toEqual(['id=null']); - })); + it('should report all changes on the first run including uninitialized values', + fakeAsync(() => { + expect(_bindAndCheckSimpleValue('value', Uninitialized)).toEqual(['id=null']); + })); - it('should report all changes on the first run including null values', fakeAsync(() => { - const ctx = _bindSimpleValue('a', TestData); - ctx.componentInstance.a = null; + it('should report all changes on the first run including null values', fakeAsync(() => { + const ctx = _bindSimpleValue('a', TestData); + ctx.componentInstance.a = null; + ctx.detectChanges(false); + expect(renderLog.log).toEqual(['id=null']); + })); + + it('should support simple chained property access', fakeAsync(() => { + const ctx = _bindSimpleValue('address.city', Person); + ctx.componentInstance.name = 'Victor'; + ctx.componentInstance.address = new Address('Grenoble'); + ctx.detectChanges(false); + expect(renderLog.log).toEqual(['id=Grenoble']); + })); + + describe('safe navigation operator', () => { + it('should support reading properties of nulls', fakeAsync(() => { + const ctx = _bindSimpleValue('address?.city', Person); + ctx.componentInstance.address = null!; ctx.detectChanges(false); expect(renderLog.log).toEqual(['id=null']); })); - it('should support simple chained property access', fakeAsync(() => { - const ctx = _bindSimpleValue('address.city', Person); - ctx.componentInstance.name = 'Victor'; - ctx.componentInstance.address = new Address('Grenoble'); + it('should support calling methods on nulls', fakeAsync(() => { + const ctx = _bindSimpleValue('address?.toString()', Person); + ctx.componentInstance.address = null!; ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=Grenoble']); + expect(renderLog.log).toEqual(['id=null']); })); - describe('safe navigation operator', () => { - it('should support reading properties of nulls', fakeAsync(() => { - const ctx = _bindSimpleValue('address?.city', Person); - ctx.componentInstance.address = null !; - ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=null']); - })); - - it('should support calling methods on nulls', fakeAsync(() => { - const ctx = _bindSimpleValue('address?.toString()', Person); - ctx.componentInstance.address = null !; - ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=null']); - })); - - it('should support reading properties on non nulls', fakeAsync(() => { - const ctx = _bindSimpleValue('address?.city', Person); - ctx.componentInstance.address = new Address('MTV'); - ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=MTV']); - })); - - it('should support calling methods on non nulls', fakeAsync(() => { - const ctx = _bindSimpleValue('address?.toString()', Person); - ctx.componentInstance.address = new Address('MTV'); - ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=MTV']); - })); - - it('should support short-circuting safe navigation', fakeAsync(() => { - const ctx = _bindSimpleValue('value?.address.city', PersonHolder); - ctx.componentInstance.value = null !; - ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=null']); - })); - - it('should support nested short-circuting safe navigation', fakeAsync(() => { - const ctx = _bindSimpleValue('value.value?.address.city', PersonHolderHolder); - ctx.componentInstance.value = new PersonHolder(); - ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=null']); - })); - - it('should support chained short-circuting safe navigation', fakeAsync(() => { - const ctx = _bindSimpleValue('value?.value?.address.city', PersonHolderHolder); - ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=null']); - })); - - it('should support short-circuting array index operations', fakeAsync(() => { - const ctx = _bindSimpleValue('value?.phones[0]', PersonHolder); - ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=null']); - })); - - it('should still throw if right-side would throw', fakeAsync(() => { - expect(() => { - const ctx = _bindSimpleValue('value?.address.city', PersonHolder); - const person = new Person(); - person.address = null !; - ctx.componentInstance.value = person; - ctx.detectChanges(false); - }).toThrow(); - })); - }); - - it('should support method calls', fakeAsync(() => { - const ctx = _bindSimpleValue('sayHi("Jim")', Person); - ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=Hi, Jim']); - })); - - it('should support function calls', fakeAsync(() => { - const ctx = _bindSimpleValue('a()(99)', TestData); - ctx.componentInstance.a = () => (a: any) => a; - ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=99']); - })); - - it('should support chained method calls', fakeAsync(() => { - const ctx = _bindSimpleValue('address.toString()', Person); + it('should support reading properties on non nulls', fakeAsync(() => { + const ctx = _bindSimpleValue('address?.city', Person); ctx.componentInstance.address = new Address('MTV'); ctx.detectChanges(false); expect(renderLog.log).toEqual(['id=MTV']); })); - it('should support NaN', fakeAsync(() => { - const ctx = _bindSimpleValue('age', Person); - ctx.componentInstance.age = NaN; + it('should support calling methods on non nulls', fakeAsync(() => { + const ctx = _bindSimpleValue('address?.toString()', Person); + ctx.componentInstance.address = new Address('MTV'); + ctx.detectChanges(false); + expect(renderLog.log).toEqual(['id=MTV']); + })); + + it('should support short-circuting safe navigation', fakeAsync(() => { + const ctx = _bindSimpleValue('value?.address.city', PersonHolder); + ctx.componentInstance.value = null!; + ctx.detectChanges(false); + expect(renderLog.log).toEqual(['id=null']); + })); + + it('should support nested short-circuting safe navigation', fakeAsync(() => { + const ctx = _bindSimpleValue('value.value?.address.city', PersonHolderHolder); + ctx.componentInstance.value = new PersonHolder(); + ctx.detectChanges(false); + expect(renderLog.log).toEqual(['id=null']); + })); + + it('should support chained short-circuting safe navigation', fakeAsync(() => { + const ctx = _bindSimpleValue('value?.value?.address.city', PersonHolderHolder); + ctx.detectChanges(false); + expect(renderLog.log).toEqual(['id=null']); + })); + + it('should support short-circuting array index operations', fakeAsync(() => { + const ctx = _bindSimpleValue('value?.phones[0]', PersonHolder); + ctx.detectChanges(false); + expect(renderLog.log).toEqual(['id=null']); + })); + + it('should still throw if right-side would throw', fakeAsync(() => { + expect(() => { + const ctx = _bindSimpleValue('value?.address.city', PersonHolder); + const person = new Person(); + person.address = null!; + ctx.componentInstance.value = person; + ctx.detectChanges(false); + }).toThrow(); + })); + }); + + it('should support method calls', fakeAsync(() => { + const ctx = _bindSimpleValue('sayHi("Jim")', Person); + ctx.detectChanges(false); + expect(renderLog.log).toEqual(['id=Hi, Jim']); + })); + + it('should support function calls', fakeAsync(() => { + const ctx = _bindSimpleValue('a()(99)', TestData); + ctx.componentInstance.a = () => (a: any) => a; + ctx.detectChanges(false); + expect(renderLog.log).toEqual(['id=99']); + })); + + it('should support chained method calls', fakeAsync(() => { + const ctx = _bindSimpleValue('address.toString()', Person); + ctx.componentInstance.address = new Address('MTV'); + ctx.detectChanges(false); + expect(renderLog.log).toEqual(['id=MTV']); + })); + + it('should support NaN', fakeAsync(() => { + const ctx = _bindSimpleValue('age', Person); + ctx.componentInstance.age = NaN; + ctx.detectChanges(false); + + expect(renderLog.log).toEqual(['id=NaN']); + renderLog.clear(); + + ctx.detectChanges(false); + expect(renderLog.log).toEqual([]); + })); + + it('should do simple watching', fakeAsync(() => { + const ctx = _bindSimpleValue('name', Person); + ctx.componentInstance.name = 'misko'; + + ctx.detectChanges(false); + expect(renderLog.log).toEqual(['id=misko']); + renderLog.clear(); + + ctx.detectChanges(false); + expect(renderLog.log).toEqual([]); + renderLog.clear(); + + ctx.componentInstance.name = 'Misko'; + ctx.detectChanges(false); + expect(renderLog.log).toEqual(['id=Misko']); + })); + + it('should support literal array made of literals', fakeAsync(() => { + const ctx = _bindSimpleValue('[1, 2]'); + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual([[1, 2]]); + })); + + it('should support empty literal array', fakeAsync(() => { + const ctx = _bindSimpleValue('[]'); + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual([[]]); + })); + + it('should support literal array made of expressions', fakeAsync(() => { + const ctx = _bindSimpleValue('[1, a]', TestData); + ctx.componentInstance.a = 2; + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual([[1, 2]]); + })); + + it('should not recreate literal arrays unless their content changed', fakeAsync(() => { + const ctx = _bindSimpleValue('[1, a]', TestData); + ctx.componentInstance.a = 2; + ctx.detectChanges(false); + ctx.detectChanges(false); + ctx.componentInstance.a = 3; + ctx.detectChanges(false); + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual([[1, 2], [1, 3]]); + })); + + it('should support literal maps made of literals', fakeAsync(() => { + const ctx = _bindSimpleValue('{z: 1}'); + ctx.detectChanges(false); + expect(renderLog.loggedValues[0]['z']).toEqual(1); + })); + + it('should support empty literal map', fakeAsync(() => { + const ctx = _bindSimpleValue('{}'); + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual([{}]); + })); + + it('should support literal maps made of expressions', fakeAsync(() => { + const ctx = _bindSimpleValue('{z: a}'); + ctx.componentInstance.a = 1; + ctx.detectChanges(false); + expect(renderLog.loggedValues[0]['z']).toEqual(1); + })); + + it('should not recreate literal maps unless their content changed', fakeAsync(() => { + const ctx = _bindSimpleValue('{z: a}'); + ctx.componentInstance.a = 1; + ctx.detectChanges(false); + ctx.detectChanges(false); + ctx.componentInstance.a = 2; + ctx.detectChanges(false); + ctx.detectChanges(false); + expect(renderLog.loggedValues.length).toBe(2); + expect(renderLog.loggedValues[0]['z']).toEqual(1); + expect(renderLog.loggedValues[1]['z']).toEqual(2); + })); + + + it('should ignore empty bindings', fakeAsync(() => { + const ctx = _bindSimpleProp('[id]', TestData); + ctx.componentInstance.a = 'value'; + ctx.detectChanges(false); + + expect(renderLog.log).toEqual([]); + })); + + it('should support interpolation', fakeAsync(() => { + const ctx = _bindSimpleProp('id="B{{a}}A"', TestData); + ctx.componentInstance.a = 'value'; + ctx.detectChanges(false); + + expect(renderLog.log).toEqual(['id=BvalueA']); + })); + + it('should output empty strings for null values in interpolation', fakeAsync(() => { + const ctx = _bindSimpleProp('id="B{{a}}A"', TestData); + ctx.componentInstance.a = null; + ctx.detectChanges(false); + + expect(renderLog.log).toEqual(['id=BA']); + })); + + it('should escape values in literals that indicate interpolation', fakeAsync(() => { + expect(_bindAndCheckSimpleValue('"$"')).toEqual(['id=$']); + })); + + it('should read locals', fakeAsync(() => { + const ctx = createCompFixture( + '{{local}}'); + ctx.detectChanges(false); + + expect(renderLog.log).toEqual(['{{someLocalValue}}']); + })); + + describe('pipes', () => { + it('should use the return value of the pipe', fakeAsync(() => { + const ctx = _bindSimpleValue('name | countingPipe', Person); + ctx.componentInstance.name = 'bob'; + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual(['bob state:0']); + })); + + it('should support arguments in pipes', fakeAsync(() => { + const ctx = _bindSimpleValue('name | multiArgPipe:"one":address.city', Person); + ctx.componentInstance.name = 'value'; + ctx.componentInstance.address = new Address('two'); + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual(['value one two default']); + })); + + it('should associate pipes right-to-left', fakeAsync(() => { + const ctx = _bindSimpleValue('name | multiArgPipe:"a":"b" | multiArgPipe:0:1', Person); + ctx.componentInstance.name = 'value'; + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual(['value a b default 0 1 default']); + })); + + it('should support calling pure pipes with different number of arguments', fakeAsync(() => { + const ctx = _bindSimpleValue('name | multiArgPipe:"a":"b" | multiArgPipe:0:1:2', Person); + ctx.componentInstance.name = 'value'; + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual(['value a b default 0 1 2']); + })); + + it('should do nothing when no change', fakeAsync(() => { + const ctx = _bindSimpleValue('"Megatron" | identityPipe', Person); + ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=NaN']); + expect(renderLog.log).toEqual(['id=Megatron']); + renderLog.clear(); - - ctx.detectChanges(false); - expect(renderLog.log).toEqual([]); - })); - - it('should do simple watching', fakeAsync(() => { - const ctx = _bindSimpleValue('name', Person); - ctx.componentInstance.name = 'misko'; - - ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=misko']); - renderLog.clear(); - - ctx.detectChanges(false); - expect(renderLog.log).toEqual([]); - renderLog.clear(); - - ctx.componentInstance.name = 'Misko'; - ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=Misko']); - })); - - it('should support literal array made of literals', fakeAsync(() => { - const ctx = _bindSimpleValue('[1, 2]'); - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual([[1, 2]]); - })); - - it('should support empty literal array', fakeAsync(() => { - const ctx = _bindSimpleValue('[]'); - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual([[]]); - })); - - it('should support literal array made of expressions', fakeAsync(() => { - const ctx = _bindSimpleValue('[1, a]', TestData); - ctx.componentInstance.a = 2; - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual([[1, 2]]); - })); - - it('should not recreate literal arrays unless their content changed', fakeAsync(() => { - const ctx = _bindSimpleValue('[1, a]', TestData); - ctx.componentInstance.a = 2; - ctx.detectChanges(false); - ctx.detectChanges(false); - ctx.componentInstance.a = 3; - ctx.detectChanges(false); - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual([[1, 2], [1, 3]]); - })); - - it('should support literal maps made of literals', fakeAsync(() => { - const ctx = _bindSimpleValue('{z: 1}'); - ctx.detectChanges(false); - expect(renderLog.loggedValues[0]['z']).toEqual(1); - })); - - it('should support empty literal map', fakeAsync(() => { - const ctx = _bindSimpleValue('{}'); - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual([{}]); - })); - - it('should support literal maps made of expressions', fakeAsync(() => { - const ctx = _bindSimpleValue('{z: a}'); - ctx.componentInstance.a = 1; - ctx.detectChanges(false); - expect(renderLog.loggedValues[0]['z']).toEqual(1); - })); - - it('should not recreate literal maps unless their content changed', fakeAsync(() => { - const ctx = _bindSimpleValue('{z: a}'); - ctx.componentInstance.a = 1; - ctx.detectChanges(false); - ctx.detectChanges(false); - ctx.componentInstance.a = 2; - ctx.detectChanges(false); - ctx.detectChanges(false); - expect(renderLog.loggedValues.length).toBe(2); - expect(renderLog.loggedValues[0]['z']).toEqual(1); - expect(renderLog.loggedValues[1]['z']).toEqual(2); - })); - - - it('should ignore empty bindings', fakeAsync(() => { - const ctx = _bindSimpleProp('[id]', TestData); - ctx.componentInstance.a = 'value'; ctx.detectChanges(false); expect(renderLog.log).toEqual([]); })); - it('should support interpolation', fakeAsync(() => { - const ctx = _bindSimpleProp('id="B{{a}}A"', TestData); - ctx.componentInstance.a = 'value'; + it('should unwrap the wrapped value and force a change', fakeAsync(() => { + const ctx = _bindSimpleValue('"Megatron" | wrappedPipe', Person); + ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=BvalueA']); - })); + expect(renderLog.log).toEqual(['id=Megatron']); - it('should output empty strings for null values in interpolation', fakeAsync(() => { - const ctx = _bindSimpleProp('id="B{{a}}A"', TestData); - ctx.componentInstance.a = null; + renderLog.clear(); ctx.detectChanges(false); - expect(renderLog.log).toEqual(['id=BA']); + expect(renderLog.log).toEqual(['id=Megatron']); })); - it('should escape values in literals that indicate interpolation', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('"$"')).toEqual(['id=$']); })); - - it('should read locals', fakeAsync(() => { + it('should record unwrapped values via ngOnChanges', fakeAsync(() => { const ctx = createCompFixture( - '{{local}}'); + '
'); + const dir: TestDirective = queryDirs(ctx.debugElement, TestDirective)[0]; + ctx.detectChanges(false); + dir.changes = {}; ctx.detectChanges(false); - expect(renderLog.log).toEqual(['{{someLocalValue}}']); - })); - - describe('pipes', () => { - it('should use the return value of the pipe', fakeAsync(() => { - const ctx = _bindSimpleValue('name | countingPipe', Person); - ctx.componentInstance.name = 'bob'; - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual(['bob state:0']); - })); - - it('should support arguments in pipes', fakeAsync(() => { - const ctx = _bindSimpleValue('name | multiArgPipe:"one":address.city', Person); - ctx.componentInstance.name = 'value'; - ctx.componentInstance.address = new Address('two'); - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual(['value one two default']); - })); - - it('should associate pipes right-to-left', fakeAsync(() => { - const ctx = _bindSimpleValue('name | multiArgPipe:"a":"b" | multiArgPipe:0:1', Person); - ctx.componentInstance.name = 'value'; - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual(['value a b default 0 1 default']); - })); - - it('should support calling pure pipes with different number of arguments', fakeAsync(() => { - const ctx = - _bindSimpleValue('name | multiArgPipe:"a":"b" | multiArgPipe:0:1:2', Person); - ctx.componentInstance.name = 'value'; - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual(['value a b default 0 1 2']); - })); - - it('should do nothing when no change', fakeAsync(() => { - const ctx = _bindSimpleValue('"Megatron" | identityPipe', Person); - - ctx.detectChanges(false); - - expect(renderLog.log).toEqual(['id=Megatron']); - - renderLog.clear(); - ctx.detectChanges(false); - - expect(renderLog.log).toEqual([]); - })); - - it('should unwrap the wrapped value and force a change', fakeAsync(() => { - const ctx = _bindSimpleValue('"Megatron" | wrappedPipe', Person); - - ctx.detectChanges(false); - - expect(renderLog.log).toEqual(['id=Megatron']); - - renderLog.clear(); - ctx.detectChanges(false); - - expect(renderLog.log).toEqual(['id=Megatron']); - })); - - it('should record unwrapped values via ngOnChanges', fakeAsync(() => { - const ctx = createCompFixture( - '
'); - const dir: TestDirective = queryDirs(ctx.debugElement, TestDirective)[0]; - ctx.detectChanges(false); - dir.changes = {}; - ctx.detectChanges(false); - - // Note: the binding for `a` did not change and has no ValueWrapper, - // and should therefore stay unchanged. - expect(dir.changes).toEqual({ - 'name': new SimpleChange('aName', 'aName', false), - 'b': new SimpleChange(2, 2, false) - }); - - ctx.detectChanges(false); - expect(dir.changes).toEqual({ - 'name': new SimpleChange('aName', 'aName', false), - 'b': new SimpleChange(2, 2, false) - }); - })); - - it('should call pure pipes only if the arguments change', fakeAsync(() => { - const ctx = _bindSimpleValue('name | countingPipe', Person); - // change from undefined -> null - ctx.componentInstance.name = null !; - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual(['null state:0']); - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual(['null state:0']); - - // change from null -> some value - ctx.componentInstance.name = 'bob'; - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual(['null state:0', 'bob state:1']); - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual(['null state:0', 'bob state:1']); - - // change from some value -> some other value - ctx.componentInstance.name = 'bart'; - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual([ - 'null state:0', 'bob state:1', 'bart state:2' - ]); - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual([ - 'null state:0', 'bob state:1', 'bart state:2' - ]); - - })); - - modifiedInIvy('Pure pipes are instantiated differently in view engine and ivy') - .it('should call pure pipes that are used multiple times only when the arguments change and share state between pipe instances', - fakeAsync(() => { - const ctx = createCompFixture( - `
` + - '
', - Person); - ctx.componentInstance.name = 'a'; - ctx.componentInstance.age = 10; - ctx.componentInstance.address = new Address('mtv'); - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual([ - 'mtv state:0', 'mtv state:1', 'a state:2', '10 state:3' - ]); - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual([ - 'mtv state:0', 'mtv state:1', 'a state:2', '10 state:3' - ]); - ctx.componentInstance.age = 11; - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual([ - 'mtv state:0', 'mtv state:1', 'a state:2', '10 state:3', '11 state:4' - ]); - })); - - // this is the ivy version of the above tests - the difference is in pure pipe instantiation - // logic and binding execution order - ivyEnabled && - it('should call pure pipes that are used multiple times only when the arguments change', - fakeAsync(() => { - const ctx = createCompFixture( - `
` + - '
', - Person); - ctx.componentInstance.name = 'a'; - ctx.componentInstance.age = 10; - ctx.componentInstance.address = new Address('mtv'); - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual([ - 'a state:0', '10 state:0', 'mtv state:0', 'mtv state:0' - ]); - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual([ - 'a state:0', '10 state:0', 'mtv state:0', 'mtv state:0' - ]); - ctx.componentInstance.age = 11; - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual([ - 'a state:0', '10 state:0', 'mtv state:0', 'mtv state:0', '11 state:1' - ]); - })); - - it('should call impure pipes on each change detection run', fakeAsync(() => { - const ctx = _bindSimpleValue('name | countingImpurePipe', Person); - ctx.componentInstance.name = 'bob'; - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual(['bob state:0']); - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual(['bob state:0', 'bob state:1']); - })); - }); - - describe('event expressions', () => { - it('should support field assignments', fakeAsync(() => { - const ctx = _bindSimpleProp('(event)="b=a=$event"'); - const childEl = ctx.debugElement.children[0]; - const evt = 'EVENT'; - childEl.triggerEventHandler('event', evt); - - expect(ctx.componentInstance.a).toEqual(evt); - expect(ctx.componentInstance.b).toEqual(evt); - })); - - it('should support keyed assignments', fakeAsync(() => { - const ctx = _bindSimpleProp('(event)="a[0]=$event"'); - const childEl = ctx.debugElement.children[0]; - ctx.componentInstance.a = ['OLD']; - const evt = 'EVENT'; - childEl.triggerEventHandler('event', evt); - expect(ctx.componentInstance.a).toEqual([evt]); - })); - - it('should support chains', fakeAsync(() => { - const ctx = _bindSimpleProp('(event)="a=a+1; a=a+1;"'); - const childEl = ctx.debugElement.children[0]; - ctx.componentInstance.a = 0; - childEl.triggerEventHandler('event', 'EVENT'); - expect(ctx.componentInstance.a).toEqual(2); - })); - - it('should support empty literals', fakeAsync(() => { - const ctx = _bindSimpleProp('(event)="a=[{},[]]"'); - const childEl = ctx.debugElement.children[0]; - childEl.triggerEventHandler('event', 'EVENT'); - - expect(ctx.componentInstance.a).toEqual([{}, []]); - })); - - it('should throw when trying to assign to a local', fakeAsync(() => { - expect(() => { _bindSimpleProp('(event)="$event=1"'); }) - .toThrowError(new RegExp( - 'Cannot assign value (.*) to template variable (.*). Template variables are read-only.')); - })); - - it('should support short-circuiting', fakeAsync(() => { - const ctx = _bindSimpleProp('(event)="true ? a = a + 1 : a = a + 1"'); - const childEl = ctx.debugElement.children[0]; - ctx.componentInstance.a = 0; - childEl.triggerEventHandler('event', 'EVENT'); - expect(ctx.componentInstance.a).toEqual(1); - })); - }); - - }); - - describe('RendererFactory', () => { - it('should call the begin and end methods on the renderer factory when change detection is called', - fakeAsync(() => { - const ctx = createCompFixture('
'); - const rf = TestBed.inject(RendererFactory2); - // TODO: @JiaLiPassion, need to wait @types/jasmine to fix the - // optional method infer issue. - spyOn(rf as any, 'begin'); - spyOn(rf as any, 'end'); - expect(rf.begin).not.toHaveBeenCalled(); - expect(rf.end).not.toHaveBeenCalled(); - - ctx.detectChanges(false); - expect(rf.begin).toHaveBeenCalled(); - expect(rf.end).toHaveBeenCalled(); - })); - }); - - describe('change notification', () => { - describe('updating directives', () => { - it('should happen without invoking the renderer', fakeAsync(() => { - const ctx = createCompFixture('
'); - ctx.detectChanges(false); - expect(renderLog.log).toEqual([]); - expect(queryDirs(ctx.debugElement, TestDirective)[0].a).toEqual(42); - })); - }); - - describe('reading directives', () => { - it('should read directive properties', fakeAsync(() => { - const ctx = createCompFixture( - '
'); - ctx.detectChanges(false); - expect(renderLog.loggedValues).toEqual([42]); - })); - }); - - describe('ngOnChanges', () => { - it('should notify the directive when a group of records changes', fakeAsync(() => { - const ctx = createCompFixture( - '
'); - ctx.detectChanges(false); - - const dirs = queryDirs(ctx.debugElement, TestDirective); - expect(dirs[0].changes).toEqual({ - 'a': new SimpleChange(undefined, 1, true), - 'b': new SimpleChange(undefined, 2, true), - 'name': new SimpleChange(undefined, 'aName', true) - }); - expect(dirs[1].changes).toEqual({ - 'a': new SimpleChange(undefined, 4, true), - 'name': new SimpleChange(undefined, 'bName', true) - }); - })); - }); - }); - - describe('lifecycle', () => { - function createCompWithContentAndViewChild(): ComponentFixture { - TestBed.overrideComponent(AnotherComponent, { - set: new Component({ - selector: 'other-cmp', - template: '
', - }) - }); - - return createCompFixture( - '
', - TestComponent); - } - - describe('ngOnInit', () => { - it('should be called after ngOnChanges', fakeAsync(() => { - const ctx = createCompFixture('
'); - expect(directiveLog.filter(['ngOnInit', 'ngOnChanges'])).toEqual([]); - - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngOnInit', 'ngOnChanges'])).toEqual([ - 'dir.ngOnChanges', 'dir.ngOnInit' - ]); - directiveLog.clear(); - - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngOnInit'])).toEqual([]); - })); - - it('should only be called only once', fakeAsync(() => { - const ctx = createCompFixture('
'); - - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngOnInit'])).toEqual(['dir.ngOnInit']); - - // reset directives - directiveLog.clear(); - - // Verify that checking should not call them. - ctx.checkNoChanges(); - - expect(directiveLog.filter(['ngOnInit'])).toEqual([]); - - // re-verify that changes should not call them - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngOnInit'])).toEqual([]); - })); - - it('should not call ngOnInit again if it throws', fakeAsync(() => { - const ctx = createCompFixture('
'); - - let errored = false; - // First pass fails, but ngOnInit should be called. - try { - ctx.detectChanges(false); - } catch (e) { - expect(e.message).toBe('Boom!'); - errored = true; - } - expect(errored).toBe(true); - - expect(directiveLog.filter(['ngOnInit'])).toEqual(['dir.ngOnInit']); - directiveLog.clear(); - - // Second change detection also fails, but this time ngOnInit should not be called. - try { - ctx.detectChanges(false); - } catch (e) { - expect(e.message).toBe('Boom!'); - throw new Error('Second detectChanges() should not have called ngOnInit.'); - } - expect(directiveLog.filter(['ngOnInit'])).toEqual([]); - })); - }); - - describe('ngDoCheck', () => { - it('should be called after ngOnInit', fakeAsync(() => { - const ctx = createCompFixture('
'); - - ctx.detectChanges(false); - expect(directiveLog.filter(['ngDoCheck', 'ngOnInit'])).toEqual([ - 'dir.ngOnInit', 'dir.ngDoCheck' - ]); - })); - - it('should be called on every detectChanges run, except for checkNoChanges', - fakeAsync(() => { - const ctx = createCompFixture('
'); - - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngDoCheck'])).toEqual(['dir.ngDoCheck']); - - // reset directives - directiveLog.clear(); - - // Verify that checking should not call them. - ctx.checkNoChanges(); - - expect(directiveLog.filter(['ngDoCheck'])).toEqual([]); - - // re-verify that changes are still detected - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngDoCheck'])).toEqual(['dir.ngDoCheck']); - })); - }); - - describe('ngAfterContentInit', () => { - it('should be called after processing the content children but before the view children', - fakeAsync(() => { - const ctx = createCompWithContentAndViewChild(); - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngDoCheck', 'ngAfterContentInit'])).toEqual([ - 'parent.ngDoCheck', 'contentChild.ngDoCheck', 'contentChild.ngAfterContentInit', - 'parent.ngAfterContentInit', 'viewChild.ngDoCheck', 'viewChild.ngAfterContentInit' - ]); - })); - - it('should only be called only once', fakeAsync(() => { - const ctx = createCompFixture('
'); - - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngAfterContentInit'])).toEqual([ - 'dir.ngAfterContentInit' - ]); - - // reset directives - directiveLog.clear(); - - // Verify that checking should not call them. - ctx.checkNoChanges(); - - expect(directiveLog.filter(['ngAfterContentInit'])).toEqual([]); - - // re-verify that changes should not call them - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngAfterContentInit'])).toEqual([]); - })); - - it('should not call ngAfterContentInit again if it throws', fakeAsync(() => { - const ctx = - createCompFixture('
'); - - let errored = false; - // First pass fails, but ngAfterContentInit should be called. - try { - ctx.detectChanges(false); - } catch (e) { - errored = true; - } - expect(errored).toBe(true); - - expect(directiveLog.filter(['ngAfterContentInit'])).toEqual([ - 'dir.ngAfterContentInit' - ]); - directiveLog.clear(); - - // Second change detection also fails, but this time ngAfterContentInit should not be - // called. - try { - ctx.detectChanges(false); - } catch (e) { - throw new Error('Second detectChanges() should not have run detection.'); - } - expect(directiveLog.filter(['ngAfterContentInit'])).toEqual([]); - })); - }); - - describe('ngAfterContentChecked', () => { - it('should be called after the content children but before the view children', - fakeAsync(() => { - const ctx = createCompWithContentAndViewChild(); - - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngDoCheck', 'ngAfterContentChecked'])).toEqual([ - 'parent.ngDoCheck', 'contentChild.ngDoCheck', 'contentChild.ngAfterContentChecked', - 'parent.ngAfterContentChecked', 'viewChild.ngDoCheck', - 'viewChild.ngAfterContentChecked' - ]); - })); - - it('should be called on every detectChanges run, except for checkNoChanges', - fakeAsync(() => { - const ctx = createCompFixture('
'); - - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngAfterContentChecked'])).toEqual([ - 'dir.ngAfterContentChecked' - ]); - - // reset directives - directiveLog.clear(); - - // Verify that checking should not call them. - ctx.checkNoChanges(); - - expect(directiveLog.filter(['ngAfterContentChecked'])).toEqual([]); - - // re-verify that changes are still detected - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngAfterContentChecked'])).toEqual([ - 'dir.ngAfterContentChecked' - ]); - })); - - it('should be called in reverse order so the child is always notified before the parent', - fakeAsync(() => { - const ctx = createCompFixture( - '
'); - - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngAfterContentChecked'])).toEqual([ - 'child.ngAfterContentChecked', 'parent.ngAfterContentChecked', - 'sibling.ngAfterContentChecked' - ]); - })); - }); - - - describe('ngAfterViewInit', () => { - it('should be called after processing the view children', fakeAsync(() => { - const ctx = createCompWithContentAndViewChild(); - - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngDoCheck', 'ngAfterViewInit'])).toEqual([ - 'parent.ngDoCheck', 'contentChild.ngDoCheck', 'contentChild.ngAfterViewInit', - 'viewChild.ngDoCheck', 'viewChild.ngAfterViewInit', 'parent.ngAfterViewInit' - ]); - })); - - it('should only be called only once', fakeAsync(() => { - const ctx = createCompFixture('
'); - - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngAfterViewInit'])).toEqual(['dir.ngAfterViewInit']); - - // reset directives - directiveLog.clear(); - - // Verify that checking should not call them. - ctx.checkNoChanges(); - - expect(directiveLog.filter(['ngAfterViewInit'])).toEqual([]); - - // re-verify that changes should not call them - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngAfterViewInit'])).toEqual([]); - })); - - it('should not call ngAfterViewInit again if it throws', fakeAsync(() => { - const ctx = - createCompFixture('
'); - - let errored = false; - // First pass fails, but ngAfterViewInit should be called. - try { - ctx.detectChanges(false); - } catch (e) { - errored = true; - } - expect(errored).toBe(true); - - expect(directiveLog.filter(['ngAfterViewInit'])).toEqual(['dir.ngAfterViewInit']); - directiveLog.clear(); - - // Second change detection also fails, but this time ngAfterViewInit should not be - // called. - try { - ctx.detectChanges(false); - } catch (e) { - throw new Error('Second detectChanges() should not have run detection.'); - } - expect(directiveLog.filter(['ngAfterViewInit'])).toEqual([]); - })); - }); - - describe('ngAfterViewChecked', () => { - it('should be called after processing the view children', fakeAsync(() => { - const ctx = createCompWithContentAndViewChild(); - - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngDoCheck', 'ngAfterViewChecked'])).toEqual([ - 'parent.ngDoCheck', 'contentChild.ngDoCheck', 'contentChild.ngAfterViewChecked', - 'viewChild.ngDoCheck', 'viewChild.ngAfterViewChecked', 'parent.ngAfterViewChecked' - ]); - })); - - it('should be called on every detectChanges run, except for checkNoChanges', - fakeAsync(() => { - const ctx = createCompFixture('
'); - - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual([ - 'dir.ngAfterViewChecked' - ]); - - // reset directives - directiveLog.clear(); - - // Verify that checking should not call them. - ctx.checkNoChanges(); - - expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual([]); - - // re-verify that changes are still detected - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual([ - 'dir.ngAfterViewChecked' - ]); - })); - - it('should be called in reverse order so the child is always notified before the parent', - fakeAsync(() => { - const ctx = createCompFixture( - '
'); - - ctx.detectChanges(false); - - expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual([ - 'child.ngAfterViewChecked', 'parent.ngAfterViewChecked', 'sibling.ngAfterViewChecked' - ]); - })); - }); - - describe('ngOnDestroy', () => { - it('should be called on view destruction', fakeAsync(() => { - const ctx = createCompFixture('
'); - ctx.detectChanges(false); - - ctx.destroy(); - - expect(directiveLog.filter(['ngOnDestroy'])).toEqual(['dir.ngOnDestroy']); - })); - - it('should be called after processing the content and view children', fakeAsync(() => { - TestBed.overrideComponent(AnotherComponent, { - set: new Component( - {selector: 'other-cmp', template: '
'}) - }); - - const ctx = createCompFixture( - '
' + - '
', - TestComponent); - - ctx.detectChanges(false); - ctx.destroy(); - - expect(directiveLog.filter(['ngOnDestroy'])).toEqual([ - 'contentChild0.ngOnDestroy', 'contentChild1.ngOnDestroy', 'viewChild.ngOnDestroy', - 'parent.ngOnDestroy' - ]); - })); - - it('should be called in reverse order so the child is always notified before the parent', - fakeAsync(() => { - const ctx = createCompFixture( - '
'); - - ctx.detectChanges(false); - ctx.destroy(); - - expect(directiveLog.filter(['ngOnDestroy'])).toEqual([ - 'child.ngOnDestroy', 'parent.ngOnDestroy', 'sibling.ngOnDestroy' - ]); - })); - - it('should deliver synchronous events to parent', fakeAsync(() => { - const ctx = createCompFixture('
'); - - ctx.detectChanges(false); - ctx.destroy(); - - expect(ctx.componentInstance.a).toEqual('destroyed'); - })); - - - it('should call ngOnDestroy on pipes', fakeAsync(() => { - const ctx = createCompFixture('{{true | pipeWithOnDestroy }}'); - - ctx.detectChanges(false); - ctx.destroy(); - - expect(directiveLog.filter(['ngOnDestroy'])).toEqual([ - 'pipeWithOnDestroy.ngOnDestroy' - ]); - })); - - it('should call ngOnDestroy on an injectable class', fakeAsync(() => { - TestBed.overrideDirective( - TestDirective, {set: {providers: [InjectableWithLifecycle]}}); - - const ctx = createCompFixture('
', TestComponent); - - ctx.debugElement.children[0].injector.get(InjectableWithLifecycle); - ctx.detectChanges(false); - - ctx.destroy(); - - // We don't care about the exact order in this test. - expect(directiveLog.filter(['ngOnDestroy']).sort()).toEqual([ - 'dir.ngOnDestroy', 'injectable.ngOnDestroy' - ]); - })); - }); - }); - - describe('enforce no new changes', () => { - it('should throw when a record gets changed after it has been checked', fakeAsync(() => { - @Directive({selector: '[changed]'}) - class ChangingDirective { - @Input() changed: any; - } - - TestBed.configureTestingModule({declarations: [ChangingDirective]}); - - const ctx = createCompFixture('
', TestData); - - ctx.componentInstance.b = 1; - const errMsgRegExp = ivyEnabled ? - /Previous value: 'undefined'\. Current value: '1'/g : - /Previous value: 'changed: undefined'\. Current value: 'changed: 1'/g; - expect(() => ctx.checkNoChanges()).toThrowError(errMsgRegExp); - })); - - - it('should throw when a record gets changed after the first change detection pass', - fakeAsync(() => { - @Directive({selector: '[changed]'}) - class ChangingDirective { - @Input() changed: any; - } - - TestBed.configureTestingModule({declarations: [ChangingDirective]}); - - const ctx = createCompFixture('
', TestData); - - ctx.componentInstance.b = 1; - ctx.detectChanges(); - - ctx.componentInstance.b = 2; - const errMsgRegExp = ivyEnabled ? - /Previous value: '1'\. Current value: '2'/g : - /Previous value: 'changed: 1'\. Current value: 'changed: 2'/g; - expect(() => ctx.checkNoChanges()).toThrowError(errMsgRegExp); - })); - - it('should warn when the view has been created in a cd hook', fakeAsync(() => { - const ctx = createCompFixture('
{{ a }}
', TestData); - ctx.componentInstance.a = 1; - expect(() => ctx.detectChanges()) - .toThrowError( - /It seems like the view has been created after its parent and its children have been dirty checked/); - - // subsequent change detection should run without issues - ctx.detectChanges(); - })); - - it('should not throw when two arrays are structurally the same', fakeAsync(() => { - const ctx = _bindSimpleValue('a', TestData); - ctx.componentInstance.a = ['value']; - ctx.detectChanges(false); - ctx.componentInstance.a = ['value']; - expect(() => ctx.checkNoChanges()).not.toThrow(); - })); - - it('should not break the next run', fakeAsync(() => { - const ctx = _bindSimpleValue('a', TestData); - ctx.componentInstance.a = 'value'; - expect(() => ctx.checkNoChanges()).toThrow(); - - ctx.detectChanges(); - expect(renderLog.loggedValues).toEqual(['value']); - })); - - it('should not break the next run (view engine and ivy)', fakeAsync(() => { - const ctx = _bindSimpleValue('a', TestData); - - ctx.detectChanges(); - renderLog.clear(); - - ctx.componentInstance.a = 'value'; - expect(() => ctx.checkNoChanges()).toThrow(); - - ctx.detectChanges(); - expect(renderLog.loggedValues).toEqual(['value']); - })); - }); - - describe('mode', () => { - it('Detached', fakeAsync(() => { - const ctx = createCompFixture(''); - const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0]; - cmp.value = 'hello'; - cmp.changeDetectorRef.detach(); - - ctx.detectChanges(); - - expect(renderLog.log).toEqual([]); - })); - - it('Detached should disable OnPush', fakeAsync(() => { - const ctx = createCompFixture(''); - ctx.componentInstance.value = 0; - ctx.detectChanges(); - renderLog.clear(); - - const cmp: CompWithRef = queryDirs(ctx.debugElement, PushComp)[0]; - cmp.changeDetectorRef.detach(); - - ctx.componentInstance.value = 1; - ctx.detectChanges(); - - expect(renderLog.log).toEqual([]); - })); - - it('Detached view can be checked locally', fakeAsync(() => { - const ctx = createCompFixture(''); - const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0]; - cmp.value = 'hello'; - cmp.changeDetectorRef.detach(); - expect(renderLog.log).toEqual([]); - - ctx.detectChanges(); - - expect(renderLog.log).toEqual([]); - - cmp.changeDetectorRef.detectChanges(); - - expect(renderLog.log).toEqual(['{{hello}}']); - })); - - - it('Reattaches', fakeAsync(() => { - const ctx = createCompFixture(''); - const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0]; - - cmp.value = 'hello'; - cmp.changeDetectorRef.detach(); - - ctx.detectChanges(); - - expect(renderLog.log).toEqual([]); - - cmp.changeDetectorRef.reattach(); - - ctx.detectChanges(); - - expect(renderLog.log).toEqual(['{{hello}}']); - - })); - - it('Reattaches in the original cd mode', fakeAsync(() => { - const ctx = createCompFixture(''); - const cmp: PushComp = queryDirs(ctx.debugElement, PushComp)[0]; - cmp.changeDetectorRef.detach(); - cmp.changeDetectorRef.reattach(); - - // renderCount should NOT be incremented with each CD as CD mode - // should be resetted to - // on-push - ctx.detectChanges(); - expect(cmp.renderCount).toBeGreaterThan(0); - const count = cmp.renderCount; - - ctx.detectChanges(); - expect(cmp.renderCount).toBe(count); - })); - - }); - - describe('multi directive order', () => { - modifiedInIvy('order of bindings to directive inputs is different in ivy') - .it('should follow the DI order for the same element', fakeAsync(() => { - const ctx = createCompFixture( - '
'); - - ctx.detectChanges(false); - ctx.destroy(); - - expect(directiveLog.filter(['set'])).toEqual(['0.set', '1.set', '2.set']); - })); - }); - - describe('nested view recursion', () => { - it('should recurse into nested components even if there are no bindings in the component view', - () => { - @Component({selector: 'nested', template: '{{name}}'}) - class Nested { - name = 'Tom'; - } - - TestBed.configureTestingModule({declarations: [Nested]}); - - const ctx = createCompFixture(''); - ctx.detectChanges(); - expect(renderLog.loggedValues).toEqual(['Tom']); - }); - - it('should recurse into nested view containers even if there are no bindings in the component view', - () => { - @Component({template: '{{name}}'}) - class Comp { - name = 'Tom'; - // TODO(issue/24571): remove '!'. - @ViewChild('vc', {read: ViewContainerRef, static: true}) vc !: ViewContainerRef; - // TODO(issue/24571): remove '!'. - @ViewChild(TemplateRef, {static: true}) template !: TemplateRef; - } - - TestBed.configureTestingModule({declarations: [Comp]}); - initHelpers(); - - const ctx = TestBed.createComponent(Comp); - ctx.detectChanges(); - expect(renderLog.loggedValues).toEqual([]); - - ctx.componentInstance.vc.createEmbeddedView(ctx.componentInstance.template); - ctx.detectChanges(); - expect(renderLog.loggedValues).toEqual(['Tom']); - }); - - describe('projected views', () => { - let log: string[]; - - @Directive({selector: '[i]'}) - class DummyDirective { - @Input() - i: any; - } - - @Component({ - selector: 'main-cmp', - template: - `` - }) - class MainComp { - constructor(public cdRef: ChangeDetectorRef) {} - log(id: string) { log.push(`main-${id}`); } - } - - @Component({ - selector: 'outer-cmp', - template: - `` - }) - class OuterComp { - // TODO(issue/24571): remove '!'. - @ContentChild(TemplateRef, {static: true}) - tpl !: TemplateRef; - - constructor(public cdRef: ChangeDetectorRef) {} - log(id: string) { log.push(`outer-${id}`); } - } - - @Component({ - selector: 'inner-cmp', - template: - `>` - }) - class InnerComp { - // TODO(issue/24571): remove '!'. - @ContentChild(TemplateRef, {static: true}) - tpl !: TemplateRef; - - // TODO(issue/24571): remove '!'. - @Input() - outerTpl !: TemplateRef; - - constructor(public cdRef: ChangeDetectorRef) {} - log(id: string) { log.push(`inner-${id}`); } - } - - let ctx: ComponentFixture; - let mainComp: MainComp; - let outerComp: OuterComp; - let innerComp: InnerComp; - - beforeEach(() => { - log = []; - ctx = TestBed - .configureTestingModule( - {declarations: [MainComp, OuterComp, InnerComp, DummyDirective]}) - .createComponent(MainComp); - mainComp = ctx.componentInstance; - outerComp = ctx.debugElement.query(By.directive(OuterComp)).injector.get(OuterComp); - innerComp = ctx.debugElement.query(By.directive(InnerComp)).injector.get(InnerComp); - }); - - it('should dirty check projected views in regular order', () => { - ctx.detectChanges(false); - expect(log).toEqual( - ['main-start', 'outer-start', 'inner-start', 'main-tpl', 'outer-tpl']); - - log = []; - ctx.detectChanges(false); - expect(log).toEqual( - ['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', - () => { - ctx.detectChanges(false); - log = []; - mainComp.cdRef.detach(); - ctx.detectChanges(false); - - expect(log).toEqual([]); + // Note: the binding for `a` did not change and has no ValueWrapper, + // and should therefore stay unchanged. + expect(dir.changes).toEqual({ + 'name': new SimpleChange('aName', 'aName', false), + 'b': new SimpleChange(2, 2, false) }); - it('should dirty check projected views if the insertion place is dirty checked', () => { - ctx.detectChanges(false); - log = []; + ctx.detectChanges(false); + expect(dir.changes).toEqual({ + 'name': new SimpleChange('aName', 'aName', false), + 'b': new SimpleChange(2, 2, false) + }); + })); - innerComp.cdRef.detectChanges(); - expect(log).toEqual(['inner-start', 'main-tpl', 'outer-tpl']); - }); + it('should call pure pipes only if the arguments change', fakeAsync(() => { + const ctx = _bindSimpleValue('name | countingPipe', Person); + // change from undefined -> null + ctx.componentInstance.name = null!; + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual(['null state:0']); + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual(['null state:0']); - modifiedInIvy('Views should not be dirty checked if inserted into CD-detached view tree') - .it('should dirty check projected views if the declaration place is dirty checked', - () => { - ctx.detectChanges(false); - log = []; - innerComp.cdRef.detach(); - mainComp.cdRef.detectChanges(); + // change from null -> some value + ctx.componentInstance.name = 'bob'; + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual(['null state:0', 'bob state:1']); + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual(['null state:0', 'bob state:1']); - expect(log).toEqual(['main-start', 'outer-start', 'main-tpl', 'outer-tpl']); + // change from some value -> some other value + ctx.componentInstance.name = 'bart'; + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual(['null state:0', 'bob state:1', 'bart state:2']); + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual(['null state:0', 'bob state:1', 'bart state:2']); + })); - log = []; - outerComp.cdRef.detectChanges(); + modifiedInIvy('Pure pipes are instantiated differently in view engine and ivy') + .it('should call pure pipes that are used multiple times only when the arguments change and share state between pipe instances', + fakeAsync(() => { + const ctx = createCompFixture( + `
` + + '
', + Person); + ctx.componentInstance.name = 'a'; + ctx.componentInstance.age = 10; + ctx.componentInstance.address = new Address('mtv'); + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual([ + 'mtv state:0', 'mtv state:1', 'a state:2', '10 state:3' + ]); + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual([ + 'mtv state:0', 'mtv state:1', 'a state:2', '10 state:3' + ]); + ctx.componentInstance.age = 11; + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual([ + 'mtv state:0', 'mtv state:1', 'a state:2', '10 state:3', '11 state:4' + ]); + })); - expect(log).toEqual(['outer-start', 'outer-tpl']); + // this is the ivy version of the above tests - the difference is in pure pipe instantiation + // logic and binding execution order + ivyEnabled && + it('should call pure pipes that are used multiple times only when the arguments change', + fakeAsync(() => { + const ctx = createCompFixture( + `
` + + '
', + Person); + ctx.componentInstance.name = 'a'; + ctx.componentInstance.age = 10; + ctx.componentInstance.address = new Address('mtv'); + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual([ + 'a state:0', '10 state:0', 'mtv state:0', 'mtv state:0' + ]); + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual([ + 'a state:0', '10 state:0', 'mtv state:0', 'mtv state:0' + ]); + ctx.componentInstance.age = 11; + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual([ + 'a state:0', '10 state:0', 'mtv state:0', 'mtv state:0', '11 state:1' + ]); + })); - log = []; - outerComp.cdRef.detach(); - mainComp.cdRef.detectChanges(); - - expect(log).toEqual(['main-start', 'main-tpl']); - }); - - onlyInIvy('Views should not be dirty checked if inserted into CD-detached view tree') - .it('should not dirty check views that are inserted into a detached tree, even if the declaration place is dirty checked', - () => { - ctx.detectChanges(false); - log = []; - innerComp.cdRef.detach(); - mainComp.cdRef.detectChanges(); - - expect(log).toEqual(['main-start', 'outer-start']); - - log = []; - outerComp.cdRef.detectChanges(); - - expect(log).toEqual(['outer-start']); - - log = []; - outerComp.cdRef.detach(); - mainComp.cdRef.detectChanges(); - - expect(log).toEqual(['main-start']); - }); - }); + it('should call impure pipes on each change detection run', fakeAsync(() => { + const ctx = _bindSimpleValue('name | countingImpurePipe', Person); + ctx.componentInstance.name = 'bob'; + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual(['bob state:0']); + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual(['bob state:0', 'bob state:1']); + })); }); - describe('class binding', () => { - it('should coordinate class attribute and class host binding', () => { - @Component({template: `
`}) - class Comp { - initClasses = 'init'; + describe('event expressions', () => { + it('should support field assignments', fakeAsync(() => { + const ctx = _bindSimpleProp('(event)="b=a=$event"'); + const childEl = ctx.debugElement.children[0]; + const evt = 'EVENT'; + childEl.triggerEventHandler('event', evt); + + expect(ctx.componentInstance.a).toEqual(evt); + expect(ctx.componentInstance.b).toEqual(evt); + })); + + it('should support keyed assignments', fakeAsync(() => { + const ctx = _bindSimpleProp('(event)="a[0]=$event"'); + const childEl = ctx.debugElement.children[0]; + ctx.componentInstance.a = ['OLD']; + const evt = 'EVENT'; + childEl.triggerEventHandler('event', evt); + expect(ctx.componentInstance.a).toEqual([evt]); + })); + + it('should support chains', fakeAsync(() => { + const ctx = _bindSimpleProp('(event)="a=a+1; a=a+1;"'); + const childEl = ctx.debugElement.children[0]; + ctx.componentInstance.a = 0; + childEl.triggerEventHandler('event', 'EVENT'); + expect(ctx.componentInstance.a).toEqual(2); + })); + + it('should support empty literals', fakeAsync(() => { + const ctx = _bindSimpleProp('(event)="a=[{},[]]"'); + const childEl = ctx.debugElement.children[0]; + childEl.triggerEventHandler('event', 'EVENT'); + + expect(ctx.componentInstance.a).toEqual([{}, []]); + })); + + it('should throw when trying to assign to a local', fakeAsync(() => { + expect(() => { + _bindSimpleProp('(event)="$event=1"'); + }) + .toThrowError(new RegExp( + 'Cannot assign value (.*) to template variable (.*). Template variables are read-only.')); + })); + + it('should support short-circuiting', fakeAsync(() => { + const ctx = _bindSimpleProp('(event)="true ? a = a + 1 : a = a + 1"'); + const childEl = ctx.debugElement.children[0]; + ctx.componentInstance.a = 0; + childEl.triggerEventHandler('event', 'EVENT'); + expect(ctx.componentInstance.a).toEqual(1); + })); + }); + }); + + describe('RendererFactory', () => { + it('should call the begin and end methods on the renderer factory when change detection is called', + fakeAsync(() => { + const ctx = createCompFixture('
'); + const rf = TestBed.inject(RendererFactory2); + // TODO: @JiaLiPassion, need to wait @types/jasmine to fix the + // optional method infer issue. + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486 + spyOn(rf as any, 'begin'); + spyOn(rf as any, 'end'); + expect(rf.begin).not.toHaveBeenCalled(); + expect(rf.end).not.toHaveBeenCalled(); + + ctx.detectChanges(false); + expect(rf.begin).toHaveBeenCalled(); + expect(rf.end).toHaveBeenCalled(); + })); + }); + + describe('change notification', () => { + describe('updating directives', () => { + it('should happen without invoking the renderer', fakeAsync(() => { + const ctx = createCompFixture('
'); + ctx.detectChanges(false); + expect(renderLog.log).toEqual([]); + expect(queryDirs(ctx.debugElement, TestDirective)[0].a).toEqual(42); + })); + }); + + describe('reading directives', () => { + it('should read directive properties', fakeAsync(() => { + const ctx = createCompFixture( + '
'); + ctx.detectChanges(false); + expect(renderLog.loggedValues).toEqual([42]); + })); + }); + + describe('ngOnChanges', () => { + it('should notify the directive when a group of records changes', fakeAsync(() => { + const ctx = createCompFixture( + '
'); + ctx.detectChanges(false); + + const dirs = queryDirs(ctx.debugElement, TestDirective); + expect(dirs[0].changes).toEqual({ + 'a': new SimpleChange(undefined, 1, true), + 'b': new SimpleChange(undefined, 2, true), + 'name': new SimpleChange(undefined, 'aName', true) + }); + expect(dirs[1].changes).toEqual({ + 'a': new SimpleChange(undefined, 4, true), + 'name': new SimpleChange(undefined, 'bName', true) + }); + })); + }); + }); + + describe('lifecycle', () => { + function createCompWithContentAndViewChild(): ComponentFixture { + TestBed.overrideComponent(AnotherComponent, { + set: new Component({ + selector: 'other-cmp', + template: '
', + }) + }); + + return createCompFixture( + '
', + TestComponent); + } + + describe('ngOnInit', () => { + it('should be called after ngOnChanges', fakeAsync(() => { + const ctx = createCompFixture('
'); + expect(directiveLog.filter(['ngOnInit', 'ngOnChanges'])).toEqual([]); + + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngOnInit', 'ngOnChanges'])).toEqual([ + 'dir.ngOnChanges', 'dir.ngOnInit' + ]); + directiveLog.clear(); + + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngOnInit'])).toEqual([]); + })); + + it('should only be called only once', fakeAsync(() => { + const ctx = createCompFixture('
'); + + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngOnInit'])).toEqual(['dir.ngOnInit']); + + // reset directives + directiveLog.clear(); + + // Verify that checking should not call them. + ctx.checkNoChanges(); + + expect(directiveLog.filter(['ngOnInit'])).toEqual([]); + + // re-verify that changes should not call them + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngOnInit'])).toEqual([]); + })); + + it('should not call ngOnInit again if it throws', fakeAsync(() => { + const ctx = createCompFixture('
'); + + let errored = false; + // First pass fails, but ngOnInit should be called. + try { + ctx.detectChanges(false); + } catch (e) { + expect(e.message).toBe('Boom!'); + errored = true; + } + expect(errored).toBe(true); + + expect(directiveLog.filter(['ngOnInit'])).toEqual(['dir.ngOnInit']); + directiveLog.clear(); + + // Second change detection also fails, but this time ngOnInit should not be called. + try { + ctx.detectChanges(false); + } catch (e) { + expect(e.message).toBe('Boom!'); + throw new Error('Second detectChanges() should not have called ngOnInit.'); + } + expect(directiveLog.filter(['ngOnInit'])).toEqual([]); + })); + }); + + describe('ngDoCheck', () => { + it('should be called after ngOnInit', fakeAsync(() => { + const ctx = createCompFixture('
'); + + ctx.detectChanges(false); + expect(directiveLog.filter(['ngDoCheck', 'ngOnInit'])).toEqual([ + 'dir.ngOnInit', 'dir.ngDoCheck' + ]); + })); + + it('should be called on every detectChanges run, except for checkNoChanges', fakeAsync(() => { + const ctx = createCompFixture('
'); + + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngDoCheck'])).toEqual(['dir.ngDoCheck']); + + // reset directives + directiveLog.clear(); + + // Verify that checking should not call them. + ctx.checkNoChanges(); + + expect(directiveLog.filter(['ngDoCheck'])).toEqual([]); + + // re-verify that changes are still detected + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngDoCheck'])).toEqual(['dir.ngDoCheck']); + })); + }); + + describe('ngAfterContentInit', () => { + it('should be called after processing the content children but before the view children', + fakeAsync(() => { + const ctx = createCompWithContentAndViewChild(); + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngDoCheck', 'ngAfterContentInit'])).toEqual([ + 'parent.ngDoCheck', 'contentChild.ngDoCheck', 'contentChild.ngAfterContentInit', + 'parent.ngAfterContentInit', 'viewChild.ngDoCheck', 'viewChild.ngAfterContentInit' + ]); + })); + + it('should only be called only once', fakeAsync(() => { + const ctx = createCompFixture('
'); + + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngAfterContentInit'])).toEqual(['dir.ngAfterContentInit']); + + // reset directives + directiveLog.clear(); + + // Verify that checking should not call them. + ctx.checkNoChanges(); + + expect(directiveLog.filter(['ngAfterContentInit'])).toEqual([]); + + // re-verify that changes should not call them + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngAfterContentInit'])).toEqual([]); + })); + + it('should not call ngAfterContentInit again if it throws', fakeAsync(() => { + const ctx = + createCompFixture('
'); + + let errored = false; + // First pass fails, but ngAfterContentInit should be called. + try { + ctx.detectChanges(false); + } catch (e) { + errored = true; + } + expect(errored).toBe(true); + + expect(directiveLog.filter(['ngAfterContentInit'])).toEqual(['dir.ngAfterContentInit']); + directiveLog.clear(); + + // Second change detection also fails, but this time ngAfterContentInit should not be + // called. + try { + ctx.detectChanges(false); + } catch (e) { + throw new Error('Second detectChanges() should not have run detection.'); + } + expect(directiveLog.filter(['ngAfterContentInit'])).toEqual([]); + })); + }); + + describe('ngAfterContentChecked', () => { + it('should be called after the content children but before the view children', + fakeAsync(() => { + const ctx = createCompWithContentAndViewChild(); + + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngDoCheck', 'ngAfterContentChecked'])).toEqual([ + 'parent.ngDoCheck', 'contentChild.ngDoCheck', 'contentChild.ngAfterContentChecked', + 'parent.ngAfterContentChecked', 'viewChild.ngDoCheck', + 'viewChild.ngAfterContentChecked' + ]); + })); + + it('should be called on every detectChanges run, except for checkNoChanges', fakeAsync(() => { + const ctx = createCompFixture('
'); + + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngAfterContentChecked'])).toEqual([ + 'dir.ngAfterContentChecked' + ]); + + // reset directives + directiveLog.clear(); + + // Verify that checking should not call them. + ctx.checkNoChanges(); + + expect(directiveLog.filter(['ngAfterContentChecked'])).toEqual([]); + + // re-verify that changes are still detected + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngAfterContentChecked'])).toEqual([ + 'dir.ngAfterContentChecked' + ]); + })); + + it('should be called in reverse order so the child is always notified before the parent', + fakeAsync(() => { + const ctx = createCompFixture( + '
'); + + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngAfterContentChecked'])).toEqual([ + 'child.ngAfterContentChecked', 'parent.ngAfterContentChecked', + 'sibling.ngAfterContentChecked' + ]); + })); + }); + + + describe('ngAfterViewInit', () => { + it('should be called after processing the view children', fakeAsync(() => { + const ctx = createCompWithContentAndViewChild(); + + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngDoCheck', 'ngAfterViewInit'])).toEqual([ + 'parent.ngDoCheck', 'contentChild.ngDoCheck', 'contentChild.ngAfterViewInit', + 'viewChild.ngDoCheck', 'viewChild.ngAfterViewInit', 'parent.ngAfterViewInit' + ]); + })); + + it('should only be called only once', fakeAsync(() => { + const ctx = createCompFixture('
'); + + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngAfterViewInit'])).toEqual(['dir.ngAfterViewInit']); + + // reset directives + directiveLog.clear(); + + // Verify that checking should not call them. + ctx.checkNoChanges(); + + expect(directiveLog.filter(['ngAfterViewInit'])).toEqual([]); + + // re-verify that changes should not call them + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngAfterViewInit'])).toEqual([]); + })); + + it('should not call ngAfterViewInit again if it throws', fakeAsync(() => { + const ctx = + createCompFixture('
'); + + let errored = false; + // First pass fails, but ngAfterViewInit should be called. + try { + ctx.detectChanges(false); + } catch (e) { + errored = true; + } + expect(errored).toBe(true); + + expect(directiveLog.filter(['ngAfterViewInit'])).toEqual(['dir.ngAfterViewInit']); + directiveLog.clear(); + + // Second change detection also fails, but this time ngAfterViewInit should not be + // called. + try { + ctx.detectChanges(false); + } catch (e) { + throw new Error('Second detectChanges() should not have run detection.'); + } + expect(directiveLog.filter(['ngAfterViewInit'])).toEqual([]); + })); + }); + + describe('ngAfterViewChecked', () => { + it('should be called after processing the view children', fakeAsync(() => { + const ctx = createCompWithContentAndViewChild(); + + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngDoCheck', 'ngAfterViewChecked'])).toEqual([ + 'parent.ngDoCheck', 'contentChild.ngDoCheck', 'contentChild.ngAfterViewChecked', + 'viewChild.ngDoCheck', 'viewChild.ngAfterViewChecked', 'parent.ngAfterViewChecked' + ]); + })); + + it('should be called on every detectChanges run, except for checkNoChanges', fakeAsync(() => { + const ctx = createCompFixture('
'); + + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual(['dir.ngAfterViewChecked']); + + // reset directives + directiveLog.clear(); + + // Verify that checking should not call them. + ctx.checkNoChanges(); + + expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual([]); + + // re-verify that changes are still detected + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual(['dir.ngAfterViewChecked']); + })); + + it('should be called in reverse order so the child is always notified before the parent', + fakeAsync(() => { + const ctx = createCompFixture( + '
'); + + ctx.detectChanges(false); + + expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual([ + 'child.ngAfterViewChecked', 'parent.ngAfterViewChecked', 'sibling.ngAfterViewChecked' + ]); + })); + }); + + describe('ngOnDestroy', () => { + it('should be called on view destruction', fakeAsync(() => { + const ctx = createCompFixture('
'); + ctx.detectChanges(false); + + ctx.destroy(); + + expect(directiveLog.filter(['ngOnDestroy'])).toEqual(['dir.ngOnDestroy']); + })); + + it('should be called after processing the content and view children', fakeAsync(() => { + TestBed.overrideComponent(AnotherComponent, { + set: new Component( + {selector: 'other-cmp', template: '
'}) + }); + + const ctx = createCompFixture( + '
' + + '
', + TestComponent); + + ctx.detectChanges(false); + ctx.destroy(); + + expect(directiveLog.filter(['ngOnDestroy'])).toEqual([ + 'contentChild0.ngOnDestroy', 'contentChild1.ngOnDestroy', 'viewChild.ngOnDestroy', + 'parent.ngOnDestroy' + ]); + })); + + it('should be called in reverse order so the child is always notified before the parent', + fakeAsync(() => { + const ctx = createCompFixture( + '
'); + + ctx.detectChanges(false); + ctx.destroy(); + + expect(directiveLog.filter(['ngOnDestroy'])).toEqual([ + 'child.ngOnDestroy', 'parent.ngOnDestroy', 'sibling.ngOnDestroy' + ]); + })); + + it('should deliver synchronous events to parent', fakeAsync(() => { + const ctx = createCompFixture('
'); + + ctx.detectChanges(false); + ctx.destroy(); + + expect(ctx.componentInstance.a).toEqual('destroyed'); + })); + + + it('should call ngOnDestroy on pipes', fakeAsync(() => { + const ctx = createCompFixture('{{true | pipeWithOnDestroy }}'); + + ctx.detectChanges(false); + ctx.destroy(); + + expect(directiveLog.filter(['ngOnDestroy'])).toEqual(['pipeWithOnDestroy.ngOnDestroy']); + })); + + it('should call ngOnDestroy on an injectable class', fakeAsync(() => { + TestBed.overrideDirective(TestDirective, {set: {providers: [InjectableWithLifecycle]}}); + + const ctx = createCompFixture('
', TestComponent); + + ctx.debugElement.children[0].injector.get(InjectableWithLifecycle); + ctx.detectChanges(false); + + ctx.destroy(); + + // We don't care about the exact order in this test. + expect(directiveLog.filter(['ngOnDestroy']).sort()).toEqual([ + 'dir.ngOnDestroy', 'injectable.ngOnDestroy' + ]); + })); + }); + }); + + describe('enforce no new changes', () => { + it('should throw when a record gets changed after it has been checked', fakeAsync(() => { + @Directive({selector: '[changed]'}) + class ChangingDirective { + @Input() changed: any; + } + + TestBed.configureTestingModule({declarations: [ChangingDirective]}); + + const ctx = createCompFixture('
', TestData); + + ctx.componentInstance.b = 1; + const errMsgRegExp = ivyEnabled ? + /Previous value: 'undefined'\. Current value: '1'/g : + /Previous value: 'changed: undefined'\. Current value: 'changed: 1'/g; + expect(() => ctx.checkNoChanges()).toThrowError(errMsgRegExp); + })); + + + it('should throw when a record gets changed after the first change detection pass', + fakeAsync(() => { + @Directive({selector: '[changed]'}) + class ChangingDirective { + @Input() changed: any; + } + + TestBed.configureTestingModule({declarations: [ChangingDirective]}); + + const ctx = createCompFixture('
', TestData); + + ctx.componentInstance.b = 1; + ctx.detectChanges(); + + ctx.componentInstance.b = 2; + const errMsgRegExp = ivyEnabled ? + /Previous value: '1'\. Current value: '2'/g : + /Previous value: 'changed: 1'\. Current value: 'changed: 2'/g; + expect(() => ctx.checkNoChanges()).toThrowError(errMsgRegExp); + })); + + it('should warn when the view has been created in a cd hook', fakeAsync(() => { + const ctx = createCompFixture('
{{ a }}
', TestData); + ctx.componentInstance.a = 1; + expect(() => ctx.detectChanges()) + .toThrowError( + /It seems like the view has been created after its parent and its children have been dirty checked/); + + // subsequent change detection should run without issues + ctx.detectChanges(); + })); + + it('should not throw when two arrays are structurally the same', fakeAsync(() => { + const ctx = _bindSimpleValue('a', TestData); + ctx.componentInstance.a = ['value']; + ctx.detectChanges(false); + ctx.componentInstance.a = ['value']; + expect(() => ctx.checkNoChanges()).not.toThrow(); + })); + + it('should not break the next run', fakeAsync(() => { + const ctx = _bindSimpleValue('a', TestData); + ctx.componentInstance.a = 'value'; + expect(() => ctx.checkNoChanges()).toThrow(); + + ctx.detectChanges(); + expect(renderLog.loggedValues).toEqual(['value']); + })); + + it('should not break the next run (view engine and ivy)', fakeAsync(() => { + const ctx = _bindSimpleValue('a', TestData); + + ctx.detectChanges(); + renderLog.clear(); + + ctx.componentInstance.a = 'value'; + expect(() => ctx.checkNoChanges()).toThrow(); + + ctx.detectChanges(); + expect(renderLog.loggedValues).toEqual(['value']); + })); + }); + + describe('mode', () => { + it('Detached', fakeAsync(() => { + const ctx = createCompFixture(''); + const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0]; + cmp.value = 'hello'; + cmp.changeDetectorRef.detach(); + + ctx.detectChanges(); + + expect(renderLog.log).toEqual([]); + })); + + it('Detached should disable OnPush', fakeAsync(() => { + const ctx = createCompFixture(''); + ctx.componentInstance.value = 0; + ctx.detectChanges(); + renderLog.clear(); + + const cmp: CompWithRef = queryDirs(ctx.debugElement, PushComp)[0]; + cmp.changeDetectorRef.detach(); + + ctx.componentInstance.value = 1; + ctx.detectChanges(); + + expect(renderLog.log).toEqual([]); + })); + + it('Detached view can be checked locally', fakeAsync(() => { + const ctx = createCompFixture(''); + const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0]; + cmp.value = 'hello'; + cmp.changeDetectorRef.detach(); + expect(renderLog.log).toEqual([]); + + ctx.detectChanges(); + + expect(renderLog.log).toEqual([]); + + cmp.changeDetectorRef.detectChanges(); + + expect(renderLog.log).toEqual(['{{hello}}']); + })); + + + it('Reattaches', fakeAsync(() => { + const ctx = createCompFixture(''); + const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0]; + + cmp.value = 'hello'; + cmp.changeDetectorRef.detach(); + + ctx.detectChanges(); + + expect(renderLog.log).toEqual([]); + + cmp.changeDetectorRef.reattach(); + + ctx.detectChanges(); + + expect(renderLog.log).toEqual(['{{hello}}']); + })); + + it('Reattaches in the original cd mode', fakeAsync(() => { + const ctx = createCompFixture(''); + const cmp: PushComp = queryDirs(ctx.debugElement, PushComp)[0]; + cmp.changeDetectorRef.detach(); + cmp.changeDetectorRef.reattach(); + + // renderCount should NOT be incremented with each CD as CD mode + // should be resetted to + // on-push + ctx.detectChanges(); + expect(cmp.renderCount).toBeGreaterThan(0); + const count = cmp.renderCount; + + ctx.detectChanges(); + expect(cmp.renderCount).toBe(count); + })); + }); + + describe('multi directive order', () => { + modifiedInIvy('order of bindings to directive inputs is different in ivy') + .it('should follow the DI order for the same element', fakeAsync(() => { + const ctx = + createCompFixture('
'); + + ctx.detectChanges(false); + ctx.destroy(); + + expect(directiveLog.filter(['set'])).toEqual(['0.set', '1.set', '2.set']); + })); + }); + + describe('nested view recursion', () => { + it('should recurse into nested components even if there are no bindings in the component view', + () => { + @Component({selector: 'nested', template: '{{name}}'}) + class Nested { + name = 'Tom'; + } + + TestBed.configureTestingModule({declarations: [Nested]}); + + const ctx = createCompFixture(''); + ctx.detectChanges(); + expect(renderLog.loggedValues).toEqual(['Tom']); + }); + + it('should recurse into nested view containers even if there are no bindings in the component view', + () => { + @Component({template: '{{name}}'}) + class Comp { + name = 'Tom'; + // TODO(issue/24571): remove '!'. + @ViewChild('vc', {read: ViewContainerRef, static: true}) vc!: ViewContainerRef; + // TODO(issue/24571): remove '!'. + @ViewChild(TemplateRef, {static: true}) template !: TemplateRef; + } + + TestBed.configureTestingModule({declarations: [Comp]}); + initHelpers(); + + const ctx = TestBed.createComponent(Comp); + ctx.detectChanges(); + expect(renderLog.loggedValues).toEqual([]); + + ctx.componentInstance.vc.createEmbeddedView(ctx.componentInstance.template); + ctx.detectChanges(); + expect(renderLog.loggedValues).toEqual(['Tom']); + }); + + describe('projected views', () => { + let log: string[]; + + @Directive({selector: '[i]'}) + class DummyDirective { + @Input() i: any; + } + + @Component({ + selector: 'main-cmp', + template: + `` + }) + class MainComp { + constructor(public cdRef: ChangeDetectorRef) {} + log(id: string) { + log.push(`main-${id}`); + } + } + + @Component({ + selector: 'outer-cmp', + template: + `` + }) + class OuterComp { + // TODO(issue/24571): remove '!'. + @ContentChild(TemplateRef, {static: true}) tpl!: TemplateRef; + + constructor(public cdRef: ChangeDetectorRef) {} + log(id: string) { + log.push(`outer-${id}`); + } + } + + @Component({ + selector: 'inner-cmp', + template: + `>` + }) + class InnerComp { + // TODO(issue/24571): remove '!'. + @ContentChild(TemplateRef, {static: true}) tpl!: TemplateRef; + + // TODO(issue/24571): remove '!'. + @Input() outerTpl!: TemplateRef; + + constructor(public cdRef: ChangeDetectorRef) {} + log(id: string) { + log.push(`inner-${id}`); + } + } + + let ctx: ComponentFixture; + let mainComp: MainComp; + let outerComp: OuterComp; + let innerComp: InnerComp; + + beforeEach(() => { + log = []; + ctx = TestBed + .configureTestingModule( + {declarations: [MainComp, OuterComp, InnerComp, DummyDirective]}) + .createComponent(MainComp); + mainComp = ctx.componentInstance; + outerComp = ctx.debugElement.query(By.directive(OuterComp)).injector.get(OuterComp); + innerComp = ctx.debugElement.query(By.directive(InnerComp)).injector.get(InnerComp); + }); + + it('should dirty check projected views in regular order', () => { + ctx.detectChanges(false); + expect(log).toEqual(['main-start', 'outer-start', 'inner-start', 'main-tpl', 'outer-tpl']); + + log = []; + ctx.detectChanges(false); + expect(log).toEqual(['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', + () => { + ctx.detectChanges(false); + log = []; + mainComp.cdRef.detach(); + ctx.detectChanges(false); + + expect(log).toEqual([]); + }); + + it('should dirty check projected views if the insertion place is dirty checked', () => { + ctx.detectChanges(false); + log = []; + + innerComp.cdRef.detectChanges(); + expect(log).toEqual(['inner-start', 'main-tpl', 'outer-tpl']); + }); + + modifiedInIvy('Views should not be dirty checked if inserted into CD-detached view tree') + .it('should dirty check projected views if the declaration place is dirty checked', + () => { + ctx.detectChanges(false); + log = []; + innerComp.cdRef.detach(); + mainComp.cdRef.detectChanges(); + + expect(log).toEqual(['main-start', 'outer-start', 'main-tpl', 'outer-tpl']); + + log = []; + outerComp.cdRef.detectChanges(); + + expect(log).toEqual(['outer-start', 'outer-tpl']); + + log = []; + outerComp.cdRef.detach(); + mainComp.cdRef.detectChanges(); + + expect(log).toEqual(['main-start', 'main-tpl']); + }); + + onlyInIvy('Views should not be dirty checked if inserted into CD-detached view tree') + .it('should not dirty check views that are inserted into a detached tree, even if the declaration place is dirty checked', + () => { + ctx.detectChanges(false); + log = []; + innerComp.cdRef.detach(); + mainComp.cdRef.detectChanges(); + + expect(log).toEqual(['main-start', 'outer-start']); + + log = []; + outerComp.cdRef.detectChanges(); + + expect(log).toEqual(['outer-start']); + + log = []; + outerComp.cdRef.detach(); + mainComp.cdRef.detectChanges(); + + expect(log).toEqual(['main-start']); + }); + }); + }); + + describe('class binding', () => { + it('should coordinate class attribute and class host binding', () => { + @Component({template: `
`}) + class Comp { + initClasses = 'init'; + } + + @Directive({selector: '[someDir]'}) + class SomeDir { + @HostBinding('class.foo') fooClass = true; + } + + const ctx = + TestBed.configureTestingModule({declarations: [Comp, SomeDir]}).createComponent(Comp); + + ctx.detectChanges(); + + const divEl = ctx.debugElement.children[0]; + expect(divEl.nativeElement).toHaveCssClass('init'); + expect(divEl.nativeElement).toHaveCssClass('foo'); + }); + }); + + describe('lifecycle asserts', () => { + let logged: string[]; + + function log(value: string) { + logged.push(value); + } + function clearLog() { + logged = []; + } + + function expectOnceAndOnlyOnce(log: string) { + expect(logged.indexOf(log) >= 0) + .toBeTruthy(`'${log}' not logged. Log was ${JSON.stringify(logged)}`); + expect(logged.lastIndexOf(log) === logged.indexOf(log)) + .toBeTruthy(`'${log}' logged more than once. Log was ${JSON.stringify(logged)}`); + } + + beforeEach(() => { + clearLog(); + }); + + enum LifetimeMethods { + None = 0, + ngOnInit = 1 << 0, + ngOnChanges = 1 << 1, + ngAfterViewInit = 1 << 2, + ngAfterContentInit = 1 << 3, + ngDoCheck = 1 << 4, + InitMethods = ngOnInit | ngAfterViewInit | ngAfterContentInit, + InitMethodsAndChanges = InitMethods | ngOnChanges, + All = InitMethodsAndChanges | ngDoCheck, + } + + function forEachMethod(methods: LifetimeMethods, cb: (method: LifetimeMethods) => void) { + if (methods & LifetimeMethods.ngOnInit) cb(LifetimeMethods.ngOnInit); + if (methods & LifetimeMethods.ngOnChanges) cb(LifetimeMethods.ngOnChanges); + if (methods & LifetimeMethods.ngAfterContentInit) cb(LifetimeMethods.ngAfterContentInit); + if (methods & LifetimeMethods.ngAfterViewInit) cb(LifetimeMethods.ngAfterViewInit); + if (methods & LifetimeMethods.ngDoCheck) cb(LifetimeMethods.ngDoCheck); + } + + interface Options { + childRecursion: LifetimeMethods; + childThrows: LifetimeMethods; + } + + describe('calling init', () => { + function initialize(options: Options) { + @Component({selector: 'my-child', template: ''}) + class MyChild { + private thrown = LifetimeMethods.None; + + // TODO(issue/24571): remove '!'. + @Input() inp!: boolean; + @Output() outp = new EventEmitter(); + + constructor() {} + + ngDoCheck() { + this.check(LifetimeMethods.ngDoCheck); + } + ngOnInit() { + this.check(LifetimeMethods.ngOnInit); + } + ngOnChanges() { + this.check(LifetimeMethods.ngOnChanges); + } + ngAfterViewInit() { + this.check(LifetimeMethods.ngAfterViewInit); + } + ngAfterContentInit() { + this.check(LifetimeMethods.ngAfterContentInit); + } + + private check(method: LifetimeMethods) { + log(`MyChild::${LifetimeMethods[method]}()`); + + if ((options.childRecursion & method) !== 0) { + if (logged.length < 20) { + this.outp.emit(null); + } else { + fail(`Unexpected MyChild::${LifetimeMethods[method]} recursion`); + } + } + if ((options.childThrows & method) !== 0) { + if ((this.thrown & method) === 0) { + this.thrown |= method; + log(`()`); + throw new Error(`Throw from MyChild::${LifetimeMethods[method]}`); + } + } + } } - @Directive({selector: '[someDir]'}) - class SomeDir { - @HostBinding('class.foo') - fooClass = true; + @Component({ + selector: 'my-component', + template: `` + }) + class MyComponent { + constructor(private changeDetectionRef: ChangeDetectorRef) {} + ngDoCheck() { + this.check(LifetimeMethods.ngDoCheck); + } + ngOnInit() { + this.check(LifetimeMethods.ngOnInit); + } + ngAfterViewInit() { + this.check(LifetimeMethods.ngAfterViewInit); + } + ngAfterContentInit() { + this.check(LifetimeMethods.ngAfterContentInit); + } + onOutp() { + log(''); + this.changeDetectionRef.detectChanges(); + log(''); + } + + private check(method: LifetimeMethods) { + log(`MyComponent::${LifetimeMethods[method]}()`); + } } - const ctx = - TestBed.configureTestingModule({declarations: [Comp, SomeDir]}).createComponent(Comp); + TestBed.configureTestingModule({declarations: [MyChild, MyComponent]}); + return createCompFixture(``); + } + + function ensureOneInit(options: Options) { + const ctx = initialize(options); + + + const throws = options.childThrows != LifetimeMethods.None; + if (throws) { + log(``); + expect(() => { + // Expect child to throw. + ctx.detectChanges(); + }).toThrow(); + log(``); + log(``); + } ctx.detectChanges(); + if (throws) log(``); + expectOnceAndOnlyOnce('MyComponent::ngOnInit()'); + expectOnceAndOnlyOnce('MyChild::ngOnInit()'); + expectOnceAndOnlyOnce('MyComponent::ngAfterViewInit()'); + expectOnceAndOnlyOnce('MyComponent::ngAfterContentInit()'); + expectOnceAndOnlyOnce('MyChild::ngAfterViewInit()'); + expectOnceAndOnlyOnce('MyChild::ngAfterContentInit()'); + } - const divEl = ctx.debugElement.children[0]; - expect(divEl.nativeElement).toHaveCssClass('init'); - expect(divEl.nativeElement).toHaveCssClass('foo'); + forEachMethod(LifetimeMethods.InitMethodsAndChanges, 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. + ensureOneInit({childRecursion: method, childThrows: LifetimeMethods.None}); + }); }); - }); - - describe('lifecycle asserts', () => { - let logged: string[]; - - function log(value: string) { logged.push(value); } - function clearLog() { logged = []; } - - function expectOnceAndOnlyOnce(log: string) { - expect(logged.indexOf(log) >= 0) - .toBeTruthy(`'${log}' not logged. Log was ${JSON.stringify(logged)}`); - expect(logged.lastIndexOf(log) === logged.indexOf(log)) - .toBeTruthy(`'${log}' logged more than once. Log was ${JSON.stringify(logged)}`); - } - - beforeEach(() => { clearLog(); }); - - enum LifetimeMethods { - None = 0, - ngOnInit = 1 << 0, - ngOnChanges = 1 << 1, - ngAfterViewInit = 1 << 2, - ngAfterContentInit = 1 << 3, - ngDoCheck = 1 << 4, - InitMethods = ngOnInit | ngAfterViewInit | ngAfterContentInit, - InitMethodsAndChanges = InitMethods | ngOnChanges, - All = InitMethodsAndChanges | ngDoCheck, - } - - function forEachMethod(methods: LifetimeMethods, cb: (method: LifetimeMethods) => void) { - if (methods & LifetimeMethods.ngOnInit) cb(LifetimeMethods.ngOnInit); - if (methods & LifetimeMethods.ngOnChanges) cb(LifetimeMethods.ngOnChanges); - if (methods & LifetimeMethods.ngAfterContentInit) cb(LifetimeMethods.ngAfterContentInit); - if (methods & LifetimeMethods.ngAfterViewInit) cb(LifetimeMethods.ngAfterViewInit); - if (methods & LifetimeMethods.ngDoCheck) cb(LifetimeMethods.ngDoCheck); - } - - interface Options { - childRecursion: LifetimeMethods; - childThrows: LifetimeMethods; - } - - describe('calling init', () => { - function initialize(options: Options) { - @Component({selector: 'my-child', template: ''}) - class MyChild { - private thrown = LifetimeMethods.None; - - // TODO(issue/24571): remove '!'. - @Input() inp !: boolean; - @Output() outp = new EventEmitter(); - - constructor() {} - - ngDoCheck() { this.check(LifetimeMethods.ngDoCheck); } - ngOnInit() { this.check(LifetimeMethods.ngOnInit); } - ngOnChanges() { this.check(LifetimeMethods.ngOnChanges); } - ngAfterViewInit() { this.check(LifetimeMethods.ngAfterViewInit); } - ngAfterContentInit() { this.check(LifetimeMethods.ngAfterContentInit); } - - private check(method: LifetimeMethods) { - log(`MyChild::${LifetimeMethods[method]}()`); - - if ((options.childRecursion & method) !== 0) { - if (logged.length < 20) { - this.outp.emit(null); - } else { - fail(`Unexpected MyChild::${LifetimeMethods[method]} recursion`); - } - } - if ((options.childThrows & method) !== 0) { - if ((this.thrown & method) === 0) { - this.thrown |= method; - log(`()`); - throw new Error(`Throw from MyChild::${LifetimeMethods[method]}`); - } - } - } - } - - @Component({ - selector: 'my-component', - template: `` - }) - class MyComponent { - constructor(private changeDetectionRef: ChangeDetectorRef) {} - ngDoCheck() { this.check(LifetimeMethods.ngDoCheck); } - ngOnInit() { this.check(LifetimeMethods.ngOnInit); } - ngAfterViewInit() { this.check(LifetimeMethods.ngAfterViewInit); } - ngAfterContentInit() { this.check(LifetimeMethods.ngAfterContentInit); } - onOutp() { - log(''); - this.changeDetectionRef.detectChanges(); - log(''); - } - - private check(method: LifetimeMethods) { - log(`MyComponent::${LifetimeMethods[method]}()`); - } - } - - TestBed.configureTestingModule({declarations: [MyChild, MyComponent]}); - - return createCompFixture(``); - } - - function ensureOneInit(options: Options) { - const ctx = initialize(options); - - - const throws = options.childThrows != LifetimeMethods.None; - if (throws) { - log(``); - expect(() => { - // Expect child to throw. - ctx.detectChanges(); - }).toThrow(); - log(``); - log(``); - } - ctx.detectChanges(); - if (throws) log(``); - expectOnceAndOnlyOnce('MyComponent::ngOnInit()'); - expectOnceAndOnlyOnce('MyChild::ngOnInit()'); - expectOnceAndOnlyOnce('MyComponent::ngAfterViewInit()'); - expectOnceAndOnlyOnce('MyComponent::ngAfterContentInit()'); - expectOnceAndOnlyOnce('MyChild::ngAfterViewInit()'); - expectOnceAndOnlyOnce('MyChild::ngAfterContentInit()'); - } - - forEachMethod(LifetimeMethods.InitMethodsAndChanges, 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. - ensureOneInit({childRecursion: method, childThrows: LifetimeMethods.None}); - }); - }); - forEachMethod(LifetimeMethods.All, 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. - // the first cycle throws but the next cycle should complete the inits. - ensureOneInit({childRecursion: LifetimeMethods.None, childThrows: method}); - }); - }); + forEachMethod(LifetimeMethods.All, 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. + // the first cycle throws but the next cycle should complete the inits. + ensureOneInit({childRecursion: LifetimeMethods.None, childThrows: method}); + }); }); }); }); +}); })(); @Injectable() @@ -1750,7 +1780,9 @@ class DirectiveLog { this.entries.push(new DirectiveLogEntry(directiveName, method)); } - clear() { this.entries = []; } + clear() { + this.entries = []; + } filter(methods: string[]): string[] { return this.entries.filter((entry) => methods.indexOf(entry.method) !== -1) @@ -1762,32 +1794,44 @@ class DirectiveLog { @Pipe({name: 'countingPipe'}) class CountingPipe implements PipeTransform { state: number = 0; - transform(value: any) { return `${value} state:${this.state++}`; } + transform(value: any) { + return `${value} state:${this.state++}`; + } } @Pipe({name: 'countingImpurePipe', pure: false}) class CountingImpurePipe implements PipeTransform { state: number = 0; - transform(value: any) { return `${value} state:${this.state++}`; } + transform(value: any) { + return `${value} state:${this.state++}`; + } } @Pipe({name: 'pipeWithOnDestroy'}) class PipeWithOnDestroy implements PipeTransform, OnDestroy { 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'}) class IdentityPipe implements PipeTransform { - transform(value: any) { return value; } + transform(value: any) { + return value; + } } @Pipe({name: 'wrappedPipe'}) class WrappedPipe implements PipeTransform { - transform(value: any) { return WrappedValue.wrap(value); } + transform(value: any) { + return WrappedValue.wrap(value); + } } @Pipe({name: 'multiArgPipe'}) @@ -1856,30 +1900,36 @@ class Gh9882 implements AfterContentInit { constructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef) { } - ngAfterContentInit(): any { this._viewContainer.createEmbeddedView(this._templateRef); } + ngAfterContentInit(): any { + this._viewContainer.createEmbeddedView(this._templateRef); + } } @Directive({selector: '[testDirective]', exportAs: 'testDirective'}) class TestDirective implements OnInit, DoCheck, OnChanges, AfterContentInit, AfterContentChecked, - AfterViewInit, AfterViewChecked, OnDestroy { + AfterViewInit, AfterViewChecked, OnDestroy { @Input() a: any; @Input() b: any; // TODO(issue/24571): remove '!'. - changes !: SimpleChanges; + changes!: SimpleChanges; event: any; eventEmitter: EventEmitter = new EventEmitter(); // TODO(issue/24571): remove '!'. - @Input('testDirective') name !: string; + @Input('testDirective') name!: string; // TODO(issue/24571): remove '!'. - @Input() throwOn !: string; + @Input() throwOn!: string; 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() { this.log.add(this.name, 'ngOnInit'); @@ -1937,20 +1987,24 @@ class InjectableWithLifecycle { name = 'injectable'; constructor(public log: DirectiveLog) {} - ngOnDestroy() { this.log.add(this.name, 'ngOnDestroy'); } + ngOnDestroy() { + this.log.add(this.name, 'ngOnDestroy'); + } } @Directive({selector: '[onDestroyDirective]'}) class OnDestroyDirective implements OnDestroy { @Output('destroy') emitter = new EventEmitter(false); - ngOnDestroy() { this.emitter.emit('destroyed'); } + ngOnDestroy() { + this.emitter.emit('destroyed'); + } } @Directive({selector: '[orderCheck0]'}) class OrderCheckDirective0 { // TODO(issue/24571): remove '!'. - private _name !: string; + private _name!: string; @Input('orderCheck0') set name(value: string) { @@ -1964,7 +2018,7 @@ class OrderCheckDirective0 { @Directive({selector: '[orderCheck1]'}) class OrderCheckDirective1 { // TODO(issue/24571): remove '!'. - private _name !: string; + private _name!: string; @Input('orderCheck1') set name(value: string) { @@ -1978,7 +2032,7 @@ class OrderCheckDirective1 { @Directive({selector: '[orderCheck2]'}) class OrderCheckDirective2 { // TODO(issue/24571): remove '!'. - private _name !: string; + private _name!: string; @Input('orderCheck2') set name(value: string) { @@ -2003,21 +2057,25 @@ class TestLocals { @Component({selector: 'root', template: 'empty'}) class Person { // TODO(issue/24571): remove '!'. - age !: number; + age!: number; // TODO(issue/24571): remove '!'. - name !: string; + name!: string; address: Address|null = null; // TODO(issue/24571): remove '!'. - phones !: number[]; + phones!: number[]; init(name: string, address: Address|null = null) { this.name = name; 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 { const address = this.address == null ? '' : ' address=' + this.address.toString(); @@ -2042,11 +2100,17 @@ class Address { 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'}) @@ -2063,14 +2127,16 @@ class TestData { @Component({selector: 'root', template: 'empty'}) class TestDataWithGetter { // TODO(issue/24571): remove '!'. - public fn !: Function; + public fn!: Function; - get a() { return this.fn(); } + get a() { + return this.fn(); + } } class Holder { // TODO(issue/24571): remove '!'. - value !: T; + value!: T; } @Component({selector: 'root', template: 'empty'}) diff --git a/packages/core/test/render3/global_utils_spec.ts b/packages/core/test/render3/global_utils_spec.ts index c3df0032c0..a95cb61d22 100644 --- a/packages/core/test/render3/global_utils_spec.ts +++ b/packages/core/test/render3/global_utils_spec.ts @@ -25,26 +25,41 @@ describe('global utils', () => { describe('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', - () => { assertPublished('getOwningComponent', getOwningComponent); }); + it('should publish getOwningComponent', () => { + assertPublished('getOwningComponent', getOwningComponent); + }); - it('should publish getRootComponents', - () => { assertPublished('getRootComponents', getRootComponents); }); + it('should publish getRootComponents', () => { + assertPublished('getRootComponents', getRootComponents); + }); - it('should publish getDirectives', () => { assertPublished('getDirectives', getDirectives); }); + it('should publish getDirectives', () => { + assertPublished('getDirectives', getDirectives); + }); - it('should publish getHostComponent', - () => { assertPublished('getHostElement', getHostElement); }); + it('should publish getHostComponent', () => { + 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); + }); }); }); diff --git a/packages/core/test/render3/node_selector_matcher_spec.ts b/packages/core/test/render3/node_selector_matcher_spec.ts index 92abc30c09..5565f8a867 100644 --- a/packages/core/test/render3/node_selector_matcher_spec.ts +++ b/packages/core/test/render3/node_selector_matcher_spec.ts @@ -12,24 +12,21 @@ import {AttributeMarker, TAttributes, TNode, TNodeType} from '../../src/render3/ import {CssSelector, CssSelectorList, SelectorFlags} from '../../src/render3/interfaces/projection'; import {extractAttrsAndClassesFromSelector, getProjectAsAttrValue, isNodeMatchingSelector, isNodeMatchingSelectorList, stringifyCSSSelectorList} from '../../src/render3/node_selector_matcher'; -function testLStaticData(tagName: string, attrs: TAttributes | null): TNode { - return createTNode(null !, null, TNodeType.Element, 0, tagName, attrs); +function testLStaticData(tagName: string, attrs: TAttributes|null): TNode { + return createTNode(null!, null, TNodeType.Element, 0, tagName, attrs); } describe('css selector matching', () => { 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)) ? - createTNode(null !, null, TNodeType.Element, 0, tagName, attrsOrTNode as TAttributes) : + createTNode(null!, null, TNodeType.Element, 0, tagName, attrsOrTNode as TAttributes) : (attrsOrTNode as TNode); return isNodeMatchingSelector(tNode, selector, true); } describe('isNodeMatchingSimpleSelector', () => { - - describe('element matching', () => { - it('should match element name only if names are the same', () => { expect(isMatching('span', null, ['span'])) .toBeTruthy(`Selector 'span' should match `); @@ -55,11 +52,9 @@ describe('css selector matching', () => { }); describe('attributes matching', () => { - // TODO: do we need to differentiate no value and empty value? that is: title vs. title="" ? it('should match single attribute without value', () => { - expect(isMatching('span', ['title', ''], [ '', 'title', '' ])).toBeTruthy(`Selector '[title]' should match `); @@ -81,7 +76,8 @@ describe('css selector matching', () => { ])).toBeFalsy(`Selector '[other]' should NOT match '`); }); - // 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', () => { expect(isMatching( 'span', [AttributeMarker.NamespaceURI, 'http://some/uri', 'title', 'name'], @@ -228,7 +224,6 @@ describe('css selector matching', () => { }); describe('class matching', () => { - it('should match with a class selector when an element has multiple classes', () => { expect(isMatching('span', ['class', 'foo bar'], [ '', SelectorFlags.CLASS, 'foo' @@ -328,7 +323,6 @@ describe('css selector matching', () => { }); describe('negations', () => { - it('should match when negation part is null', () => { expect(isMatching('span', null, ['span'])).toBeTruthy(`Selector 'span' should match `); }); @@ -436,13 +430,11 @@ describe('css selector matching', () => { expect(isMatching('div', ['name', 'name', 'title', '', 'class', 'foo bar'], selector)) .toBeFalsy(); }); - }); describe('isNodeMatchingSelectorList', () => { - 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); } @@ -468,16 +460,18 @@ describe('css selector matching', () => { }); describe('reading the ngProjectAs attribute value', function() { - - function testTNode(attrs: TAttributes | null) { return testLStaticData('tag', attrs); } + function testTNode(attrs: TAttributes|null) { + return testLStaticData('tag', attrs); + } it('should get ngProjectAs value if present', function() { expect(getProjectAsAttrValue(testTNode([AttributeMarker.ProjectAs, ['tag', 'foo', 'bar']]))) .toEqual(['tag', 'foo', 'bar']); }); - it('should return null if there are no attributes', - function() { expect(getProjectAsAttrValue(testTNode(null))).toBe(null); }); + it('should return null if there are no attributes', function() { + expect(getProjectAsAttrValue(testTNode(null))).toBe(null); + }); it('should return if ngProjectAs is not present', function() { 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() { expect(getProjectAsAttrValue(testTNode(['foo', AttributeMarker.ProjectAs]))).toBe(null); }); - }); - }); describe('stringifyCSSSelectorList', () => { - - it('should stringify selector with a tag name only', - () => { expect(stringifyCSSSelectorList([['button']])).toBe('button'); }); + it('should stringify selector with a tag name only', () => { + expect(stringifyCSSSelectorList([['button']])).toBe('button'); + }); it('should stringify selector with attributes', () => { expect(stringifyCSSSelectorList([['', 'id', '']])).toBe('[id]'); diff --git a/packages/elements/test/utils_spec.ts b/packages/elements/test/utils_spec.ts index fb7a0a326e..97cc62b7a5 100644 --- a/packages/elements/test/utils_spec.ts +++ b/packages/elements/test/utils_spec.ts @@ -17,6 +17,7 @@ describe('utils', () => { beforeEach(() => { // TODO: @JiaLiPassion, need to wait @types/jasmine to fix the wrong return // type infer issue. + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486 setTimeoutSpy = spyOn(window, 'setTimeout').and.returnValue(42 as any); clearTimeoutSpy = spyOn(window, 'clearTimeout'); }); @@ -83,8 +84,9 @@ describe('utils', () => { expect(camelToDashCase('foo1Bar2Baz3Qux4')).toBe('foo1-bar2-baz3-qux4'); }); - it('should keep existing dashes', - () => { expect(camelToDashCase('fooBar-baz-Qux')).toBe('foo-bar-baz--qux'); }); + it('should keep existing dashes', () => { + expect(camelToDashCase('fooBar-baz-Qux')).toBe('foo-bar-baz--qux'); + }); }); describe('createCustomEvent()', () => { @@ -99,7 +101,6 @@ describe('utils', () => { expect(event.cancelable).toBe(false); expect(event.detail).toEqual(value); }); - }); describe('isElement()', () => { @@ -131,7 +132,7 @@ describe('utils', () => { it('should return true for functions', () => { const obj = {foo: function() {}, bar: () => null, baz() {}}; const fns = [ - function(){}, + function() {}, () => null, obj.foo, obj.bar, @@ -182,7 +183,7 @@ describe('utils', () => { `; - li = div.querySelector('li') !; + li = div.querySelector('li')!; }); it('should return whether the element matches the selector', () => { @@ -218,7 +219,9 @@ describe('utils', () => { ]; 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); + }); }); }); diff --git a/packages/examples/core/di/ts/forward_ref/forward_ref_spec.ts b/packages/examples/core/di/ts/forward_ref/forward_ref_spec.ts index b16e869307..a9e9ad5d7e 100644 --- a/packages/examples/core/di/ts/forward_ref/forward_ref_spec.ts +++ b/packages/examples/core/di/ts/forward_ref/forward_ref_spec.ts @@ -6,7 +6,7 @@ * 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', () => { @@ -26,7 +26,9 @@ import {Inject, ReflectiveInjector, forwardRef, resolveForwardRef} from '@angula // Door attempts to inject Lock, despite it not being defined yet. // 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. diff --git a/packages/platform-browser/test/dom/events/hammer_gestures_spec.ts b/packages/platform-browser/test/dom/events/hammer_gestures_spec.ts index e631a7c7ab..fd0c99c35e 100644 --- a/packages/platform-browser/test/dom/events/hammer_gestures_spec.ts +++ b/packages/platform-browser/test/dom/events/hammer_gestures_spec.ts @@ -17,7 +17,9 @@ import {HammerGestureConfig, HammerGesturesPlugin,} from '@angular/platform-brow let fakeConsole: any; if (isNode) return; - beforeEach(() => { fakeConsole = {warn: jasmine.createSpy('console.warn')}; }); + beforeEach(() => { + fakeConsole = {warn: jasmine.createSpy('console.warn')}; + }); describe('with no custom loader', () => { 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 // EventManager. let ngZone: NgZone; - beforeEach(inject([NgZone], (z: NgZone) => { ngZone = z; })); + beforeEach(inject([NgZone], (z: NgZone) => { + ngZone = z; + })); beforeEach(() => { originalHammerGlobal = (window as any).Hammer; @@ -84,13 +88,15 @@ import {HammerGestureConfig, HammerGesturesPlugin,} from '@angular/platform-brow plugin = new HammerGesturesPlugin(document, hammerConfig, fakeConsole, loader); // 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'); someListener = () => {}; }); - afterEach(() => { (window as any).Hammer = originalHammerGlobal; }); + afterEach(() => { + (window as any).Hammer = originalHammerGlobal; + }); it('should not log a warning when HammerJS is not loaded', () => { plugin.addEventListener(someElement, 'swipe', () => {}); diff --git a/packages/platform-browser/test/dom/events/key_events_spec.ts b/packages/platform-browser/test/dom/events/key_events_spec.ts index 0c9b14d19d..ba97308756 100644 --- a/packages/platform-browser/test/dom/events/key_events_spec.ts +++ b/packages/platform-browser/test/dom/events/key_events_spec.ts @@ -51,7 +51,6 @@ import {KeyEventsPlugin} from '@angular/platform-browser/src/dom/events/key_even .toEqual({'domEventName': 'keydown', 'fullKey': 'control.shift'}); expect(KeyEventsPlugin.parseEventName('keyup.control.shift')) .toEqual({'domEventName': 'keyup', 'fullKey': 'control.shift'}); - }); 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', () => {})) .not.toThrowError(); }); - }); } diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index 867f550bb5..358f4ccc46 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -3708,8 +3708,7 @@ describe('Integration', () => { router.navigate(['/user/:fedor']); advance(fixture); - expect(navigateSpy.calls.mostRecent().args[1] !.queryParams); - + expect(navigateSpy.calls.mostRecent().args[1]!.queryParams); }))); }); diff --git a/packages/service-worker/test/comm_spec.ts b/packages/service-worker/test/comm_spec.ts index 839deada20..64f6e67601 100644 --- a/packages/service-worker/test/comm_spec.ts +++ b/packages/service-worker/test/comm_spec.ts @@ -9,7 +9,7 @@ import {PLATFORM_ID} from '@angular/core'; import {TestBed} from '@angular/core/testing'; 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 {SwUpdate} from '@angular/service-worker/src/update'; import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockServiceWorkerRegistration, patchDecodeBase64} from '@angular/service-worker/testing/mock'; @@ -32,14 +32,18 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS 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 => { const mock = new MockServiceWorkerContainer(); const comm = new NgswCommChannel(mock as any); 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(); }); @@ -158,7 +162,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS }); 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'}); expect(promise).toEqual(jasmine.any(Promise)); @@ -166,7 +170,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS 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) => Array.from(charCodeArr).map(c => String.fromCharCode(c)).join(''); @@ -183,12 +187,12 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS userVisibleOnly: true, }); - const actualAppServerKey = pmSubscribeSpy.calls.first().args[0] !.applicationServerKey; + const actualAppServerKey = pmSubscribeSpy.calls.first().args[0]!.applicationServerKey; const actualAppServerKeyStr = decode(actualAppServerKey as Uint8Array); 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'); push.subscription.subscribe(subscriptionSpy); const sub = await push.requestSubscription({serverPublicKey: 'test'}); @@ -204,7 +208,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS 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 { await push.unsubscribe(); 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.unsubscribe(); expect(psUnsubscribeSpy).toHaveBeenCalledTimes(1); }); - it('rejects if `PushSubscription.unsubscribe()` fails', async() => { - psUnsubscribeSpy.and.callFake(() => { throw new Error('foo'); }); + it('rejects if `PushSubscription.unsubscribe()` fails', async () => { + psUnsubscribeSpy.and.callFake(() => { + throw new Error('foo'); + }); try { 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)); 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'); push.subscription.subscribe(subscriptionSpy); @@ -254,7 +260,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS 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 initialSubEmit = new Promise(resolve => subscriptionSpy.and.callFake(resolve)); @@ -271,7 +277,9 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS subscriptionSpy.calls.reset(); // 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); expect(subscriptionSpy).not.toHaveBeenCalled(); @@ -338,7 +346,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS 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`. await nextSubEmitPromise; expect(subscriptionSpy).toHaveBeenCalledTimes(1); @@ -353,7 +361,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS 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; subscriptionSpy.calls.reset(); @@ -391,11 +399,16 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS }); 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', - done => { push.unsubscribe().catch(err => { done(); }); }); + it('gives an error when unsubscribing', done => { + push.unsubscribe().catch(err => { + done(); + }); + }); }); }); @@ -461,7 +474,9 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS }); }); return update.activateUpdate() - .catch(err => { expect(err.message).toEqual('Failed to activate'); }) + .catch(err => { + expect(err.message).toEqual('Failed to activate'); + }) .then(() => done()) .catch(err => done.fail(err)); }); @@ -475,8 +490,12 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS expect(() => TestBed.inject(SwUpdate)).not.toThrow(); }); describe('with no SW', () => { - beforeEach(() => { comm = new NgswCommChannel(undefined); }); - it('can be instantiated', () => { update = new SwUpdate(comm); }); + beforeEach(() => { + comm = new NgswCommChannel(undefined); + }); + it('can be instantiated', () => { + update = new SwUpdate(comm); + }); it('does not crash on subscription to observables', () => { update = new SwUpdate(comm); 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 => { update = new SwUpdate(comm); - update.checkForUpdate().catch(err => { done(); }); + update.checkForUpdate().catch(err => { + done(); + }); }); it('gives an error when activating updates', done => { update = new SwUpdate(comm); - update.activateUpdate().catch(err => { done(); }); + update.activateUpdate().catch(err => { + done(); + }); }); }); }); diff --git a/packages/service-worker/test/module_spec.ts b/packages/service-worker/test/module_spec.ts index 480fe837f1..495debec5b 100644 --- a/packages/service-worker/test/module_spec.ts +++ b/packages/service-worker/test/module_spec.ts @@ -7,7 +7,7 @@ */ 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 {filter, take} from 'rxjs/operators'; @@ -33,7 +33,7 @@ describe('ServiceWorkerModule', () => { spyOn(navigator.serviceWorker, 'register').and.returnValue(Promise.resolve(null as any))); describe('register()', () => { - const configTestBed = async(opts: SwRegistrationOptions) => { + const configTestBed = async (opts: SwRegistrationOptions) => { TestBed.configureTestingModule({ imports: [ServiceWorkerModule.register('sw.js', opts)], providers: [{provide: PLATFORM_ID, useValue: 'browser'}], @@ -42,35 +42,35 @@ describe('ServiceWorkerModule', () => { await untilStable(); }; - it('sets the registration options', async() => { + it('sets the registration options', async () => { await configTestBed({enabled: true, scope: 'foo'}); expect(TestBed.inject(SwRegistrationOptions)).toEqual({enabled: true, 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}); expect(TestBed.inject(SwUpdate).isEnabled).toBe(false); expect(swRegisterSpy).not.toHaveBeenCalled(); }); - it('can enable the SW', async() => { + it('can enable the SW', async () => { await configTestBed({enabled: true}); expect(TestBed.inject(SwUpdate).isEnabled).toBe(true); expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); }); - it('defaults to enabling the SW', async() => { + it('defaults to enabling the SW', async () => { await configTestBed({}); expect(TestBed.inject(SwUpdate).isEnabled).toBe(true); 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'); 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'}); await untilStable(); @@ -100,7 +100,7 @@ describe('ServiceWorkerModule', () => { expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: 'provider'}); }); - it('can disable the SW', async() => { + it('can disable the SW', async () => { configTestBed({enabled: false}, {enabled: true}); await untilStable(); @@ -108,7 +108,7 @@ describe('ServiceWorkerModule', () => { expect(swRegisterSpy).not.toHaveBeenCalled(); }); - it('can enable the SW', async() => { + it('can enable the SW', async () => { configTestBed({enabled: true}, {enabled: false}); await untilStable(); @@ -116,7 +116,7 @@ describe('ServiceWorkerModule', () => { expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); }); - it('defaults to enabling the SW', async() => { + it('defaults to enabling the SW', async () => { configTestBed({}, {enabled: false}); await untilStable(); diff --git a/packages/service-worker/worker/test/happy_spec.ts b/packages/service-worker/worker/test/happy_spec.ts index ba29e7dfbf..320240c555 100644 --- a/packages/service-worker/worker/test/happy_spec.ts +++ b/packages/service-worker/worker/test/happy_spec.ts @@ -11,60 +11,77 @@ import {CacheDatabase} from '../src/db-cache'; import {Driver, DriverReadyState} from '../src/driver'; import {AssetGroupConfig, DataGroupConfig, Manifest} from '../src/manifest'; import {sha1} from '../src/sha1'; -import {MockCache, clearAllCaches} from '../testing/cache'; +import {clearAllCaches, MockCache} from '../testing/cache'; import {MockRequest, MockResponse} from '../testing/fetch'; import {MockFileSystemBuilder, MockServerStateBuilder, tmpHashTableForFs} from '../testing/mock'; import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope'; (function() { - // Skip environments that don't support the minimum APIs needed to run the SW tests. - if (!SwTestHarness.envIsSupported()) { - return; - } +// Skip environments that don't support the minimum APIs needed to run the SW tests. +if (!SwTestHarness.envIsSupported()) { + return; +} - const dist = - new MockFileSystemBuilder() - .addFile('/foo.txt', 'this is foo') - .addFile('/bar.txt', 'this is bar') - .addFile('/baz.txt', 'this is baz') - .addFile('/qux.txt', 'this is qux') - .addFile('/quux.txt', 'this is quux') - .addFile('/quuux.txt', 'this is quuux') - .addFile('/lazy/unchanged1.txt', 'this is unchanged (1)') - .addFile('/lazy/unchanged2.txt', 'this is unchanged (2)') - .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('/api/foo', 'this is api foo', {'Cache-Control': 'no-cache'}) - .addUnhashedFile( - '/api-static/bar', 'this is static api bar', {'Cache-Control': 'no-cache'}) - .build(); +const dist = + new MockFileSystemBuilder() + .addFile('/foo.txt', 'this is foo') + .addFile('/bar.txt', 'this is bar') + .addFile('/baz.txt', 'this is baz') + .addFile('/qux.txt', 'this is qux') + .addFile('/quux.txt', 'this is quux') + .addFile('/quuux.txt', 'this is quuux') + .addFile('/lazy/unchanged1.txt', 'this is unchanged (1)') + .addFile('/lazy/unchanged2.txt', 'this is unchanged (2)') + .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('/api/foo', 'this is api foo', {'Cache-Control': 'no-cache'}) + .addUnhashedFile('/api-static/bar', 'this is static api bar', {'Cache-Control': 'no-cache'}) + .build(); - const distUpdate = - new MockFileSystemBuilder() - .addFile('/foo.txt', 'this is foo v2') - .addFile('/bar.txt', 'this is bar') - .addFile('/baz.txt', 'this is baz v2') - .addFile('/qux.txt', 'this is qux v2') - .addFile('/quux.txt', 'this is quux v2') - .addFile('/quuux.txt', 'this is quuux v2') - .addFile('/lazy/unchanged1.txt', 'this is unchanged (1)') - .addFile('/lazy/unchanged2.txt', 'this is unchanged (2)') - .addUnhashedFile( - '/unhashed/a.txt', 'this is unhashed v2', {'Cache-Control': 'max-age=10'}) - .addUnhashedFile('/ignored/file1', 'this is not handled by the SW') - .addUnhashedFile('/ignored/dir/file2', 'this is not handled by the SW either') - .build(); +const distUpdate = + new MockFileSystemBuilder() + .addFile('/foo.txt', 'this is foo v2') + .addFile('/bar.txt', 'this is bar') + .addFile('/baz.txt', 'this is baz v2') + .addFile('/qux.txt', 'this is qux v2') + .addFile('/quux.txt', 'this is quux v2') + .addFile('/quuux.txt', 'this is quuux v2') + .addFile('/lazy/unchanged1.txt', 'this is unchanged (1)') + .addFile('/lazy/unchanged2.txt', 'this is unchanged (2)') + .addUnhashedFile('/unhashed/a.txt', 'this is unhashed v2', {'Cache-Control': 'max-age=10'}) + .addUnhashedFile('/ignored/file1', 'this is not handled by the SW') + .addUnhashedFile('/ignored/dir/file2', 'this is not handled by the SW either') + .build(); - const brokenFs = new MockFileSystemBuilder() - .addFile('/foo.txt', 'this is foo (broken)') - .addFile('/bar.txt', 'this is bar (broken)') - .build(); +const brokenFs = new MockFileSystemBuilder() + .addFile('/foo.txt', 'this is foo (broken)') + .addFile('/bar.txt', 'this is bar (broken)') + .build(); - const brokenManifest: Manifest = { - configVersion: 1, - timestamp: 1234567890123, - index: '/foo.txt', - assetGroups: [{ +const brokenManifest: Manifest = { + configVersion: 1, + timestamp: 1234567890123, + index: '/foo.txt', + assetGroups: [{ + name: 'assets', + installMode: 'prefetch', + updateMode: 'prefetch', + urls: [ + '/foo.txt', + ], + patterns: [], + }], + dataGroups: [], + navigationUrls: processNavigationUrls(''), + hashTable: tmpHashTableForFs(brokenFs, {'/foo.txt': true}), +}; + +const brokenLazyManifest: Manifest = { + configVersion: 1, + timestamp: 1234567890123, + index: '/foo.txt', + assetGroups: [ + { name: 'assets', installMode: 'prefetch', updateMode: 'prefetch', @@ -72,1441 +89,1419 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope'; '/foo.txt', ], patterns: [], - }], - dataGroups: [], - navigationUrls: processNavigationUrls(''), - hashTable: tmpHashTableForFs(brokenFs, {'/foo.txt': true}), - }; - - const brokenLazyManifest: Manifest = { - configVersion: 1, - timestamp: 1234567890123, - index: '/foo.txt', - assetGroups: [ - { - name: 'assets', - installMode: 'prefetch', - updateMode: 'prefetch', - urls: [ - '/foo.txt', - ], - patterns: [], - }, - { - name: 'lazy-assets', - installMode: 'lazy', - updateMode: 'lazy', - urls: [ - '/bar.txt', - ], - patterns: [], - }, - ], - dataGroups: [], - navigationUrls: processNavigationUrls(''), - hashTable: tmpHashTableForFs(brokenFs, {'/bar.txt': true}), - }; - - // Manifest without navigation urls to test backward compatibility with - // versions < 6.0.0. - interface ManifestV5 { - configVersion: number; - appData?: {[key: string]: string}; - index: string; - assetGroups?: AssetGroupConfig[]; - dataGroups?: DataGroupConfig[]; - hashTable: {[url: string]: string}; - } - - // To simulate versions < 6.0.0 - const manifestOld: ManifestV5 = { - configVersion: 1, - index: '/foo.txt', - hashTable: tmpHashTableForFs(dist), - }; - - const manifest: Manifest = { - configVersion: 1, - timestamp: 1234567890123, - appData: { - version: 'original', }, - index: '/foo.txt', - assetGroups: [ - { - name: 'assets', - installMode: 'prefetch', - updateMode: 'prefetch', - urls: [ - '/foo.txt', - '/bar.txt', - '/redirected.txt', - ], - patterns: [ - '/unhashed/.*', - ], - }, - { - name: 'other', - installMode: 'lazy', - updateMode: 'lazy', - urls: [ - '/baz.txt', - '/qux.txt', - ], - patterns: [], - }, - { - name: 'lazy_prefetch', - installMode: 'lazy', - updateMode: 'prefetch', - urls: [ - '/quux.txt', - '/quuux.txt', - '/lazy/unchanged1.txt', - '/lazy/unchanged2.txt', - ], - patterns: [], - } - ], - dataGroups: [ - { - name: 'api', - version: 42, - maxAge: 3600000, - maxSize: 100, - strategy: 'freshness', - patterns: [ - '/api/.*', - ], - }, - { - name: 'api-static', - version: 43, - maxAge: 3600000, - maxSize: 100, - strategy: 'performance', - patterns: [ - '/api-static/.*', - ], - }, - ], - navigationUrls: processNavigationUrls(''), - hashTable: tmpHashTableForFs(dist), - }; - - const manifestUpdate: Manifest = { - configVersion: 1, - timestamp: 1234567890123, - appData: { - version: 'update', + { + name: 'lazy-assets', + installMode: 'lazy', + updateMode: 'lazy', + urls: [ + '/bar.txt', + ], + patterns: [], }, - index: '/foo.txt', - assetGroups: [ + ], + dataGroups: [], + navigationUrls: processNavigationUrls(''), + hashTable: tmpHashTableForFs(brokenFs, {'/bar.txt': true}), +}; + +// Manifest without navigation urls to test backward compatibility with +// versions < 6.0.0. +interface ManifestV5 { + configVersion: number; + appData?: {[key: string]: string}; + index: string; + assetGroups?: AssetGroupConfig[]; + dataGroups?: DataGroupConfig[]; + hashTable: {[url: string]: string}; +} + +// To simulate versions < 6.0.0 +const manifestOld: ManifestV5 = { + configVersion: 1, + index: '/foo.txt', + hashTable: tmpHashTableForFs(dist), +}; + +const manifest: Manifest = { + configVersion: 1, + timestamp: 1234567890123, + appData: { + version: 'original', + }, + index: '/foo.txt', + assetGroups: [ + { + name: 'assets', + installMode: 'prefetch', + updateMode: 'prefetch', + urls: [ + '/foo.txt', + '/bar.txt', + '/redirected.txt', + ], + patterns: [ + '/unhashed/.*', + ], + }, + { + name: 'other', + installMode: 'lazy', + updateMode: 'lazy', + urls: [ + '/baz.txt', + '/qux.txt', + ], + patterns: [], + }, + { + name: 'lazy_prefetch', + installMode: 'lazy', + updateMode: 'prefetch', + urls: [ + '/quux.txt', + '/quuux.txt', + '/lazy/unchanged1.txt', + '/lazy/unchanged2.txt', + ], + patterns: [], + } + ], + dataGroups: [ + { + name: 'api', + version: 42, + maxAge: 3600000, + maxSize: 100, + strategy: 'freshness', + patterns: [ + '/api/.*', + ], + }, + { + name: 'api-static', + version: 43, + maxAge: 3600000, + maxSize: 100, + strategy: 'performance', + patterns: [ + '/api-static/.*', + ], + }, + ], + navigationUrls: processNavigationUrls(''), + hashTable: tmpHashTableForFs(dist), +}; + +const manifestUpdate: Manifest = { + configVersion: 1, + timestamp: 1234567890123, + appData: { + version: 'update', + }, + index: '/foo.txt', + assetGroups: [ + { + name: 'assets', + installMode: 'prefetch', + updateMode: 'prefetch', + urls: [ + '/foo.txt', + '/bar.txt', + '/redirected.txt', + ], + patterns: [ + '/unhashed/.*', + ], + }, + { + name: 'other', + installMode: 'lazy', + updateMode: 'lazy', + urls: [ + '/baz.txt', + '/qux.txt', + ], + patterns: [], + }, + { + name: 'lazy_prefetch', + installMode: 'lazy', + updateMode: 'prefetch', + urls: [ + '/quux.txt', + '/quuux.txt', + '/lazy/unchanged1.txt', + '/lazy/unchanged2.txt', + ], + patterns: [], + } + ], + navigationUrls: processNavigationUrls( + '', + [ + '/**/file1', + '/**/file2', + '!/ignored/file1', + '!/ignored/dir/**', + ]), + hashTable: tmpHashTableForFs(distUpdate), +}; + +const serverBuilderBase = + new MockServerStateBuilder() + .withStaticFiles(dist) + .withRedirect('/redirected.txt', '/redirect-target.txt', 'this was a redirect') + .withError('/error.txt'); + +const server = serverBuilderBase.withManifest(manifest).build(); + +const serverRollback = + serverBuilderBase.withManifest({...manifest, timestamp: manifest.timestamp + 1}).build(); + +const serverUpdate = + new MockServerStateBuilder() + .withStaticFiles(distUpdate) + .withManifest(manifestUpdate) + .withRedirect('/redirected.txt', '/redirect-target.txt', 'this was a redirect') + .build(); + +const brokenServer = + new MockServerStateBuilder().withStaticFiles(brokenFs).withManifest(brokenManifest).build(); + +const brokenLazyServer = + new MockServerStateBuilder().withStaticFiles(brokenFs).withManifest(brokenLazyManifest).build(); + +const server404 = new MockServerStateBuilder().withStaticFiles(dist).build(); + +const manifestHash = sha1(JSON.stringify(manifest)); +const manifestUpdateHash = sha1(JSON.stringify(manifestUpdate)); + + +describe('Driver', () => { + let scope: SwTestHarness; + let driver: Driver; + + beforeEach(() => { + server.reset(); + serverUpdate.reset(); + server404.reset(); + brokenServer.reset(); + + scope = new SwTestHarnessBuilder().withServerState(server).build(); + driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); + }); + + it('activates without waiting', async () => { + const skippedWaiting = await scope.startup(true); + expect(skippedWaiting).toBe(true); + }); + + it('claims all clients, after activation', async () => { + const claimSpy = spyOn(scope.clients, 'claim'); + + await scope.startup(true); + expect(claimSpy).toHaveBeenCalledTimes(1); + }); + + it('cleans up old `@angular/service-worker` caches, after activation', async () => { + const claimSpy = spyOn(scope.clients, 'claim'); + const cleanupOldSwCachesSpy = spyOn(driver, 'cleanupOldSwCaches'); + + // Automatically advance time to trigger idle tasks as they are added. + scope.autoAdvanceTime = true; + await scope.startup(true); + await scope.resolveSelfMessages(); + scope.autoAdvanceTime = false; + + expect(cleanupOldSwCachesSpy).toHaveBeenCalledTimes(1); + expect(claimSpy).toHaveBeenCalledBefore(cleanupOldSwCachesSpy); + }); + + it('does not blow up if cleaning up old `@angular/service-worker` caches fails', async () => { + spyOn(driver, 'cleanupOldSwCaches').and.callFake(() => Promise.reject('Ooops')); + + // Automatically advance time to trigger idle tasks as they are added. + scope.autoAdvanceTime = true; + await scope.startup(true); + await scope.resolveSelfMessages(); + scope.autoAdvanceTime = false; + + server.clearRequests(); + + expect(driver.state).toBe(DriverReadyState.NORMAL); + expect(await makeRequest(scope, '/foo.txt')).toBe('this is foo'); + server.assertNoOtherRequests(); + }); + + it('initializes prefetched content correctly, after activation', async () => { + // Automatically advance time to trigger idle tasks as they are added. + scope.autoAdvanceTime = true; + await scope.startup(true); + await scope.resolveSelfMessages(); + scope.autoAdvanceTime = false; + + server.assertSawRequestFor('ngsw.json'); + server.assertSawRequestFor('/foo.txt'); + server.assertSawRequestFor('/bar.txt'); + server.assertSawRequestFor('/redirected.txt'); + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + expect(await makeRequest(scope, '/bar.txt')).toEqual('this is bar'); + server.assertNoOtherRequests(); + }); + + it('initializes prefetched content correctly, after a request kicks it off', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + server.assertSawRequestFor('ngsw.json'); + server.assertSawRequestFor('/foo.txt'); + server.assertSawRequestFor('/bar.txt'); + server.assertSawRequestFor('/redirected.txt'); + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + expect(await makeRequest(scope, '/bar.txt')).toEqual('this is bar'); + server.assertNoOtherRequests(); + }); + + it('initializes the service worker on fetch if it has not yet been initialized', async () => { + // Driver is initially uninitialized. + expect(driver.initialized).toBeNull(); + expect(driver['latestHash']).toBeNull(); + + // Making a request initializes the driver (fetches assets). + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + expect(driver['latestHash']).toEqual(jasmine.any(String)); + server.assertSawRequestFor('ngsw.json'); + server.assertSawRequestFor('/foo.txt'); + server.assertSawRequestFor('/bar.txt'); + server.assertSawRequestFor('/redirected.txt'); + + // Once initialized, cached resources are served without network requests. + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + expect(await makeRequest(scope, '/bar.txt')).toEqual('this is bar'); + server.assertNoOtherRequests(); + }); + + it('initializes the service worker on message if it has not yet been initialized', async () => { + // Driver is initially uninitialized. + expect(driver.initialized).toBeNull(); + expect(driver['latestHash']).toBeNull(); + + // Pushing a message initializes the driver (fetches assets). + await scope.handleMessage({action: 'foo'}, 'someClient'); + expect(driver['latestHash']).toEqual(jasmine.any(String)); + server.assertSawRequestFor('ngsw.json'); + server.assertSawRequestFor('/foo.txt'); + server.assertSawRequestFor('/bar.txt'); + server.assertSawRequestFor('/redirected.txt'); + + // Once initialized, pushed messages are handled without re-initializing. + await scope.handleMessage({action: 'bar'}, 'someClient'); + server.assertNoOtherRequests(); + + // Once initialized, cached resources are served without network requests. + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + expect(await makeRequest(scope, '/bar.txt')).toEqual('this is bar'); + server.assertNoOtherRequests(); + }); + + it('handles non-relative URLs', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + server.clearRequests(); + expect(await makeRequest(scope, 'http://localhost/foo.txt')).toEqual('this is foo'); + server.assertNoOtherRequests(); + }); + + it('handles actual errors from the browser', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + server.clearRequests(); + + const [resPromise, done] = scope.handleFetch(new MockRequest('/error.txt'), 'default'); + await done; + const res = (await resPromise)!; + expect(res.status).toEqual(504); + expect(res.statusText).toEqual('Gateway Timeout'); + }); + + it('handles redirected responses', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + server.clearRequests(); + expect(await makeRequest(scope, '/redirected.txt')).toEqual('this was a redirect'); + server.assertNoOtherRequests(); + }); + + it('caches lazy content on-request', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + server.clearRequests(); + expect(await makeRequest(scope, '/baz.txt')).toEqual('this is baz'); + server.assertSawRequestFor('/baz.txt'); + server.assertNoOtherRequests(); + expect(await makeRequest(scope, '/baz.txt')).toEqual('this is baz'); + server.assertNoOtherRequests(); + expect(await makeRequest(scope, '/qux.txt')).toEqual('this is qux'); + server.assertSawRequestFor('/qux.txt'); + server.assertNoOtherRequests(); + }); + + it('updates to new content when requested', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + + const client = scope.clients.getMock('default')!; + expect(client.messages).toEqual([]); + + scope.updateServerState(serverUpdate); + expect(await driver.checkForUpdate()).toEqual(true); + serverUpdate.assertSawRequestFor('ngsw.json'); + serverUpdate.assertSawRequestFor('/foo.txt'); + serverUpdate.assertSawRequestFor('/redirected.txt'); + serverUpdate.assertNoOtherRequests(); + + expect(client.messages).toEqual([{ + type: 'UPDATE_AVAILABLE', + current: {hash: manifestHash, appData: {version: 'original'}}, + available: {hash: manifestUpdateHash, appData: {version: 'update'}}, + }]); + + // Default client is still on the old version of the app. + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + + // Sending a new client id should result in the updated version being returned. + expect(await makeRequest(scope, '/foo.txt', 'new')).toEqual('this is foo v2'); + + // Of course, the old version should still work. + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + + expect(await makeRequest(scope, '/bar.txt')).toEqual('this is bar'); + serverUpdate.assertNoOtherRequests(); + }); + + it('detects new version even if only `manifest.timestamp` is different', async () => { + expect(await makeRequest(scope, '/foo.txt', 'newClient')).toEqual('this is foo'); + await driver.initialized; + + scope.updateServerState(serverUpdate); + expect(await driver.checkForUpdate()).toEqual(true); + expect(await makeRequest(scope, '/foo.txt', 'newerClient')).toEqual('this is foo v2'); + + scope.updateServerState(serverRollback); + expect(await driver.checkForUpdate()).toEqual(true); + expect(await makeRequest(scope, '/foo.txt', 'newestClient')).toEqual('this is foo'); + }); + + it('updates a specific client to new content on request', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + + const client = scope.clients.getMock('default')!; + expect(client.messages).toEqual([]); + + scope.updateServerState(serverUpdate); + expect(await driver.checkForUpdate()).toEqual(true); + serverUpdate.clearRequests(); + await driver.updateClient(client as any as Client); + + expect(client.messages).toEqual([ { - name: 'assets', - installMode: 'prefetch', - updateMode: 'prefetch', - urls: [ - '/foo.txt', - '/bar.txt', - '/redirected.txt', - ], - patterns: [ - '/unhashed/.*', - ], - }, - { - name: 'other', - installMode: 'lazy', - updateMode: 'lazy', - urls: [ - '/baz.txt', - '/qux.txt', - ], - patterns: [], - }, - { - name: 'lazy_prefetch', - installMode: 'lazy', - updateMode: 'prefetch', - urls: [ - '/quux.txt', - '/quuux.txt', - '/lazy/unchanged1.txt', - '/lazy/unchanged2.txt', - ], - patterns: [], - } - ], - navigationUrls: processNavigationUrls( - '', - [ - '/**/file1', - '/**/file2', - '!/ignored/file1', - '!/ignored/dir/**', - ]), - hashTable: tmpHashTableForFs(distUpdate), - }; - - const serverBuilderBase = - new MockServerStateBuilder() - .withStaticFiles(dist) - .withRedirect('/redirected.txt', '/redirect-target.txt', 'this was a redirect') - .withError('/error.txt'); - - const server = serverBuilderBase.withManifest(manifest).build(); - - const serverRollback = - serverBuilderBase.withManifest({...manifest, timestamp: manifest.timestamp + 1}).build(); - - const serverUpdate = - new MockServerStateBuilder() - .withStaticFiles(distUpdate) - .withManifest(manifestUpdate) - .withRedirect('/redirected.txt', '/redirect-target.txt', 'this was a redirect') - .build(); - - const brokenServer = - new MockServerStateBuilder().withStaticFiles(brokenFs).withManifest(brokenManifest).build(); - - const brokenLazyServer = new MockServerStateBuilder() - .withStaticFiles(brokenFs) - .withManifest(brokenLazyManifest) - .build(); - - const server404 = new MockServerStateBuilder().withStaticFiles(dist).build(); - - const manifestHash = sha1(JSON.stringify(manifest)); - const manifestUpdateHash = sha1(JSON.stringify(manifestUpdate)); - - - describe('Driver', () => { - let scope: SwTestHarness; - let driver: Driver; - - beforeEach(() => { - server.reset(); - serverUpdate.reset(); - server404.reset(); - brokenServer.reset(); - - scope = new SwTestHarnessBuilder().withServerState(server).build(); - driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); - }); - - it('activates without waiting', async() => { - const skippedWaiting = await scope.startup(true); - expect(skippedWaiting).toBe(true); - }); - - it('claims all clients, after activation', async() => { - const claimSpy = spyOn(scope.clients, 'claim'); - - await scope.startup(true); - expect(claimSpy).toHaveBeenCalledTimes(1); - }); - - it('cleans up old `@angular/service-worker` caches, after activation', async() => { - const claimSpy = spyOn(scope.clients, 'claim'); - const cleanupOldSwCachesSpy = spyOn(driver, 'cleanupOldSwCaches'); - - // Automatically advance time to trigger idle tasks as they are added. - scope.autoAdvanceTime = true; - await scope.startup(true); - await scope.resolveSelfMessages(); - scope.autoAdvanceTime = false; - - expect(cleanupOldSwCachesSpy).toHaveBeenCalledTimes(1); - expect(claimSpy).toHaveBeenCalledBefore(cleanupOldSwCachesSpy); - }); - - it('does not blow up if cleaning up old `@angular/service-worker` caches fails', async() => { - spyOn(driver, 'cleanupOldSwCaches').and.callFake(() => Promise.reject('Ooops')); - - // Automatically advance time to trigger idle tasks as they are added. - scope.autoAdvanceTime = true; - await scope.startup(true); - await scope.resolveSelfMessages(); - scope.autoAdvanceTime = false; - - server.clearRequests(); - - expect(driver.state).toBe(DriverReadyState.NORMAL); - expect(await makeRequest(scope, '/foo.txt')).toBe('this is foo'); - server.assertNoOtherRequests(); - }); - - it('initializes prefetched content correctly, after activation', async() => { - // Automatically advance time to trigger idle tasks as they are added. - scope.autoAdvanceTime = true; - await scope.startup(true); - await scope.resolveSelfMessages(); - scope.autoAdvanceTime = false; - - server.assertSawRequestFor('ngsw.json'); - server.assertSawRequestFor('/foo.txt'); - server.assertSawRequestFor('/bar.txt'); - server.assertSawRequestFor('/redirected.txt'); - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - expect(await makeRequest(scope, '/bar.txt')).toEqual('this is bar'); - server.assertNoOtherRequests(); - }); - - it('initializes prefetched content correctly, after a request kicks it off', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.assertSawRequestFor('ngsw.json'); - server.assertSawRequestFor('/foo.txt'); - server.assertSawRequestFor('/bar.txt'); - server.assertSawRequestFor('/redirected.txt'); - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - expect(await makeRequest(scope, '/bar.txt')).toEqual('this is bar'); - server.assertNoOtherRequests(); - }); - - it('initializes the service worker on fetch if it has not yet been initialized', async() => { - // Driver is initially uninitialized. - expect(driver.initialized).toBeNull(); - expect(driver['latestHash']).toBeNull(); - - // Making a request initializes the driver (fetches assets). - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - expect(driver['latestHash']).toEqual(jasmine.any(String)); - server.assertSawRequestFor('ngsw.json'); - server.assertSawRequestFor('/foo.txt'); - server.assertSawRequestFor('/bar.txt'); - server.assertSawRequestFor('/redirected.txt'); - - // Once initialized, cached resources are served without network requests. - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - expect(await makeRequest(scope, '/bar.txt')).toEqual('this is bar'); - server.assertNoOtherRequests(); - }); - - it('initializes the service worker on message if it has not yet been initialized', async() => { - // Driver is initially uninitialized. - expect(driver.initialized).toBeNull(); - expect(driver['latestHash']).toBeNull(); - - // Pushing a message initializes the driver (fetches assets). - await scope.handleMessage({action: 'foo'}, 'someClient'); - expect(driver['latestHash']).toEqual(jasmine.any(String)); - server.assertSawRequestFor('ngsw.json'); - server.assertSawRequestFor('/foo.txt'); - server.assertSawRequestFor('/bar.txt'); - server.assertSawRequestFor('/redirected.txt'); - - // Once initialized, pushed messages are handled without re-initializing. - await scope.handleMessage({action: 'bar'}, 'someClient'); - server.assertNoOtherRequests(); - - // Once initialized, cached resources are served without network requests. - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - expect(await makeRequest(scope, '/bar.txt')).toEqual('this is bar'); - server.assertNoOtherRequests(); - }); - - it('handles non-relative URLs', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.clearRequests(); - expect(await makeRequest(scope, 'http://localhost/foo.txt')).toEqual('this is foo'); - server.assertNoOtherRequests(); - }); - - it('handles actual errors from the browser', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.clearRequests(); - - const [resPromise, done] = scope.handleFetch(new MockRequest('/error.txt'), 'default'); - await done; - const res = (await resPromise) !; - expect(res.status).toEqual(504); - expect(res.statusText).toEqual('Gateway Timeout'); - }); - - it('handles redirected responses', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.clearRequests(); - expect(await makeRequest(scope, '/redirected.txt')).toEqual('this was a redirect'); - server.assertNoOtherRequests(); - }); - - it('caches lazy content on-request', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.clearRequests(); - expect(await makeRequest(scope, '/baz.txt')).toEqual('this is baz'); - server.assertSawRequestFor('/baz.txt'); - server.assertNoOtherRequests(); - expect(await makeRequest(scope, '/baz.txt')).toEqual('this is baz'); - server.assertNoOtherRequests(); - expect(await makeRequest(scope, '/qux.txt')).toEqual('this is qux'); - server.assertSawRequestFor('/qux.txt'); - server.assertNoOtherRequests(); - }); - - it('updates to new content when requested', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - - const client = scope.clients.getMock('default') !; - expect(client.messages).toEqual([]); - - scope.updateServerState(serverUpdate); - expect(await driver.checkForUpdate()).toEqual(true); - serverUpdate.assertSawRequestFor('ngsw.json'); - serverUpdate.assertSawRequestFor('/foo.txt'); - serverUpdate.assertSawRequestFor('/redirected.txt'); - serverUpdate.assertNoOtherRequests(); - - expect(client.messages).toEqual([{ type: 'UPDATE_AVAILABLE', current: {hash: manifestHash, appData: {version: 'original'}}, available: {hash: manifestUpdateHash, appData: {version: 'update'}}, - }]); + }, + { + type: 'UPDATE_ACTIVATED', + previous: {hash: manifestHash, appData: {version: 'original'}}, + current: {hash: manifestUpdateHash, appData: {version: 'update'}}, + } + ]); - // Default client is still on the old version of the app. - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo v2'); + }); - // Sending a new client id should result in the updated version being returned. - expect(await makeRequest(scope, '/foo.txt', 'new')).toEqual('this is foo v2'); + it('handles empty client ID', async () => { + // Initialize the SW. + expect(await makeNavigationRequest(scope, '/foo/file1', '')).toEqual('this is foo'); + expect(await makeNavigationRequest(scope, '/bar/file2', null)).toEqual('this is foo'); + await driver.initialized; - // Of course, the old version should still work. - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + // Update to a new version. + scope.updateServerState(serverUpdate); + expect(await driver.checkForUpdate()).toEqual(true); - expect(await makeRequest(scope, '/bar.txt')).toEqual('this is bar'); - serverUpdate.assertNoOtherRequests(); + // Correctly handle navigation requests, even if `clientId` is null/empty. + expect(await makeNavigationRequest(scope, '/foo/file1', '')).toEqual('this is foo v2'); + expect(await makeNavigationRequest(scope, '/bar/file2', null)).toEqual('this is foo v2'); + }); + + it('checks for updates on restart', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + + scope = new SwTestHarnessBuilder() + .withCacheState(scope.caches.dehydrate()) + .withServerState(serverUpdate) + .build(); + driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + serverUpdate.assertNoOtherRequests(); + + scope.advance(12000); + await driver.idle.empty; + + serverUpdate.assertSawRequestFor('ngsw.json'); + serverUpdate.assertSawRequestFor('/foo.txt'); + serverUpdate.assertSawRequestFor('/redirected.txt'); + serverUpdate.assertNoOtherRequests(); + }); + + it('checks for updates on navigation', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + server.clearRequests(); + + expect(await makeNavigationRequest(scope, '/foo.txt')).toEqual('this is foo'); + + scope.advance(12000); + await driver.idle.empty; + + server.assertSawRequestFor('ngsw.json'); + }); + + it('does not make concurrent checks for updates on navigation', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + server.clearRequests(); + + expect(await makeNavigationRequest(scope, '/foo.txt')).toEqual('this is foo'); + + expect(await makeNavigationRequest(scope, '/foo.txt')).toEqual('this is foo'); + + scope.advance(12000); + await driver.idle.empty; + + server.assertSawRequestFor('ngsw.json'); + server.assertNoOtherRequests(); + }); + + it('preserves multiple client assignments across restarts', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + + scope.updateServerState(serverUpdate); + expect(await driver.checkForUpdate()).toEqual(true); + expect(await makeRequest(scope, '/foo.txt', 'new')).toEqual('this is foo v2'); + serverUpdate.clearRequests(); + + scope = new SwTestHarnessBuilder() + .withCacheState(scope.caches.dehydrate()) + .withServerState(serverUpdate) + .build(); + driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); + + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + expect(await makeRequest(scope, '/foo.txt', 'new')).toEqual('this is foo v2'); + serverUpdate.assertNoOtherRequests(); + }); + + it('updates when refreshed', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + + const client = scope.clients.getMock('default')!; + + scope.updateServerState(serverUpdate); + expect(await driver.checkForUpdate()).toEqual(true); + serverUpdate.clearRequests(); + + expect(await makeNavigationRequest(scope, '/file1')).toEqual('this is foo v2'); + + expect(client.messages).toEqual([ + { + type: 'UPDATE_AVAILABLE', + current: {hash: manifestHash, appData: {version: 'original'}}, + available: {hash: manifestUpdateHash, appData: {version: 'update'}}, + }, + { + type: 'UPDATE_ACTIVATED', + previous: {hash: manifestHash, appData: {version: 'original'}}, + current: {hash: manifestUpdateHash, appData: {version: 'update'}}, + } + ]); + serverUpdate.assertNoOtherRequests(); + }); + + it('cleans up properly when manually requested', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + + scope.updateServerState(serverUpdate); + expect(await driver.checkForUpdate()).toEqual(true); + serverUpdate.clearRequests(); + + expect(await makeRequest(scope, '/foo.txt', 'new')).toEqual('this is foo v2'); + + // Delete the default client. + scope.clients.remove('default'); + + // After this, the old version should no longer be cached. + await driver.cleanupCaches(); + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo v2'); + + serverUpdate.assertNoOtherRequests(); + }); + + it('cleans up properly on restart', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + + scope = new SwTestHarnessBuilder() + .withCacheState(scope.caches.dehydrate()) + .withServerState(serverUpdate) + .build(); + driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + serverUpdate.assertNoOtherRequests(); + + let keys = await scope.caches.keys(); + let hasOriginalCaches = keys.some(name => name.startsWith(`ngsw:/:${manifestHash}:`)); + expect(hasOriginalCaches).toEqual(true); + + scope.clients.remove('default'); + + scope.advance(12000); + await driver.idle.empty; + serverUpdate.clearRequests(); + + driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo v2'); + + keys = await scope.caches.keys(); + hasOriginalCaches = keys.some(name => name.startsWith(`ngsw:/:${manifestHash}:`)); + expect(hasOriginalCaches).toEqual(false); + }); + + it('shows notifications for push notifications', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + await scope.handlePush({ + notification: { + title: 'This is a test', + body: 'Test body', + } }); - - it('detects new version even if only `manifest.timestamp` is different', async() => { - expect(await makeRequest(scope, '/foo.txt', 'newClient')).toEqual('this is foo'); - await driver.initialized; - - scope.updateServerState(serverUpdate); - expect(await driver.checkForUpdate()).toEqual(true); - expect(await makeRequest(scope, '/foo.txt', 'newerClient')).toEqual('this is foo v2'); - - scope.updateServerState(serverRollback); - expect(await driver.checkForUpdate()).toEqual(true); - expect(await makeRequest(scope, '/foo.txt', 'newestClient')).toEqual('this is foo'); - }); - - it('updates a specific client to new content on request', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - - const client = scope.clients.getMock('default') !; - expect(client.messages).toEqual([]); - - scope.updateServerState(serverUpdate); - expect(await driver.checkForUpdate()).toEqual(true); - serverUpdate.clearRequests(); - await driver.updateClient(client as any as Client); - - expect(client.messages).toEqual([ - { - type: 'UPDATE_AVAILABLE', - current: {hash: manifestHash, appData: {version: 'original'}}, - available: {hash: manifestUpdateHash, appData: {version: 'update'}}, - }, - { - type: 'UPDATE_ACTIVATED', - previous: {hash: manifestHash, appData: {version: 'original'}}, - current: {hash: manifestUpdateHash, appData: {version: 'update'}}, - } - ]); - - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo v2'); - }); - - it('handles empty client ID', async() => { - // Initialize the SW. - expect(await makeNavigationRequest(scope, '/foo/file1', '')).toEqual('this is foo'); - expect(await makeNavigationRequest(scope, '/bar/file2', null)).toEqual('this is foo'); - await driver.initialized; - - // Update to a new version. - scope.updateServerState(serverUpdate); - expect(await driver.checkForUpdate()).toEqual(true); - - // Correctly handle navigation requests, even if `clientId` is null/empty. - expect(await makeNavigationRequest(scope, '/foo/file1', '')).toEqual('this is foo v2'); - expect(await makeNavigationRequest(scope, '/bar/file2', null)).toEqual('this is foo v2'); - }); - - it('checks for updates on restart', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - - scope = new SwTestHarnessBuilder() - .withCacheState(scope.caches.dehydrate()) - .withServerState(serverUpdate) - .build(); - driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - serverUpdate.assertNoOtherRequests(); - - scope.advance(12000); - await driver.idle.empty; - - serverUpdate.assertSawRequestFor('ngsw.json'); - serverUpdate.assertSawRequestFor('/foo.txt'); - serverUpdate.assertSawRequestFor('/redirected.txt'); - serverUpdate.assertNoOtherRequests(); - }); - - it('checks for updates on navigation', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.clearRequests(); - - expect(await makeNavigationRequest(scope, '/foo.txt')).toEqual('this is foo'); - - scope.advance(12000); - await driver.idle.empty; - - server.assertSawRequestFor('ngsw.json'); - }); - - it('does not make concurrent checks for updates on navigation', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.clearRequests(); - - expect(await makeNavigationRequest(scope, '/foo.txt')).toEqual('this is foo'); - - expect(await makeNavigationRequest(scope, '/foo.txt')).toEqual('this is foo'); - - scope.advance(12000); - await driver.idle.empty; - - server.assertSawRequestFor('ngsw.json'); - server.assertNoOtherRequests(); - }); - - it('preserves multiple client assignments across restarts', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - - scope.updateServerState(serverUpdate); - expect(await driver.checkForUpdate()).toEqual(true); - expect(await makeRequest(scope, '/foo.txt', 'new')).toEqual('this is foo v2'); - serverUpdate.clearRequests(); - - scope = new SwTestHarnessBuilder() - .withCacheState(scope.caches.dehydrate()) - .withServerState(serverUpdate) - .build(); - driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); - - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - expect(await makeRequest(scope, '/foo.txt', 'new')).toEqual('this is foo v2'); - serverUpdate.assertNoOtherRequests(); - }); - - it('updates when refreshed', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - - const client = scope.clients.getMock('default') !; - - scope.updateServerState(serverUpdate); - expect(await driver.checkForUpdate()).toEqual(true); - serverUpdate.clearRequests(); - - expect(await makeNavigationRequest(scope, '/file1')).toEqual('this is foo v2'); - - expect(client.messages).toEqual([ - { - type: 'UPDATE_AVAILABLE', - current: {hash: manifestHash, appData: {version: 'original'}}, - available: {hash: manifestUpdateHash, appData: {version: 'update'}}, - }, - { - type: 'UPDATE_ACTIVATED', - previous: {hash: manifestHash, appData: {version: 'original'}}, - current: {hash: manifestUpdateHash, appData: {version: 'update'}}, - } - ]); - serverUpdate.assertNoOtherRequests(); - }); - - it('cleans up properly when manually requested', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - - scope.updateServerState(serverUpdate); - expect(await driver.checkForUpdate()).toEqual(true); - serverUpdate.clearRequests(); - - expect(await makeRequest(scope, '/foo.txt', 'new')).toEqual('this is foo v2'); - - // Delete the default client. - scope.clients.remove('default'); - - // After this, the old version should no longer be cached. - await driver.cleanupCaches(); - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo v2'); - - serverUpdate.assertNoOtherRequests(); - }); - - it('cleans up properly on restart', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - - scope = new SwTestHarnessBuilder() - .withCacheState(scope.caches.dehydrate()) - .withServerState(serverUpdate) - .build(); - driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - serverUpdate.assertNoOtherRequests(); - - let keys = await scope.caches.keys(); - let hasOriginalCaches = keys.some(name => name.startsWith(`ngsw:/:${manifestHash}:`)); - expect(hasOriginalCaches).toEqual(true); - - scope.clients.remove('default'); - - scope.advance(12000); - await driver.idle.empty; - serverUpdate.clearRequests(); - - driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo v2'); - - keys = await scope.caches.keys(); - hasOriginalCaches = keys.some(name => name.startsWith(`ngsw:/:${manifestHash}:`)); - expect(hasOriginalCaches).toEqual(false); - }); - - it('shows notifications for push notifications', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - await scope.handlePush({ + expect(scope.notifications).toEqual([{ + title: 'This is a test', + options: {title: 'This is a test', body: 'Test body'}, + }]); + expect(scope.clients.getMock('default')!.messages).toEqual([{ + type: 'PUSH', + data: { notification: { title: 'This is a test', body: 'Test body', - } - }); - expect(scope.notifications).toEqual([{ - title: 'This is a test', - options: {title: 'This is a test', body: 'Test body'}, - }]); - expect(scope.clients.getMock('default') !.messages).toEqual([{ - type: 'PUSH', - data: { - notification: { - title: 'This is a test', - body: 'Test body', - }, }, - }]); + }, + }]); + }); + + it('broadcasts notification click events with action', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + await scope.handleClick( + {title: 'This is a test with action', body: 'Test body with action'}, 'button'); + const message: any = scope.clients.getMock('default')!.messages[0]; + + expect(message.type).toEqual('NOTIFICATION_CLICK'); + expect(message.data.action).toEqual('button'); + expect(message.data.notification.title).toEqual('This is a test with action'); + expect(message.data.notification.body).toEqual('Test body with action'); + }); + + it('broadcasts notification click events without action', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + await scope.handleClick( + {title: 'This is a test without action', body: 'Test body without action'}); + const message: any = scope.clients.getMock('default')!.messages[0]; + + expect(message.type).toEqual('NOTIFICATION_CLICK'); + expect(message.data.action).toBeUndefined(); + expect(message.data.notification.title).toEqual('This is a test without action'); + expect(message.data.notification.body).toEqual('Test body without action'); + }); + + it('prefetches updates to lazy cache when set', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + + // Fetch some files from the `lazy_prefetch` asset group. + expect(await makeRequest(scope, '/quux.txt')).toEqual('this is quux'); + expect(await makeRequest(scope, '/lazy/unchanged1.txt')).toEqual('this is unchanged (1)'); + + // Install update. + scope.updateServerState(serverUpdate); + expect(await driver.checkForUpdate()).toBe(true); + + // Previously requested and changed: Fetch from network. + serverUpdate.assertSawRequestFor('/quux.txt'); + // Never requested and changed: Don't fetch. + serverUpdate.assertNoRequestFor('/quuux.txt'); + // Previously requested and unchanged: Fetch from cache. + serverUpdate.assertNoRequestFor('/lazy/unchanged1.txt'); + // Never requested and unchanged: Don't fetch. + serverUpdate.assertNoRequestFor('/lazy/unchanged2.txt'); + + serverUpdate.clearRequests(); + + // Update client. + await driver.updateClient(await scope.clients.get('default')); + + // Already cached. + expect(await makeRequest(scope, '/quux.txt')).toBe('this is quux v2'); + serverUpdate.assertNoOtherRequests(); + + // Not cached: Fetch from network. + expect(await makeRequest(scope, '/quuux.txt')).toBe('this is quuux v2'); + serverUpdate.assertSawRequestFor('/quuux.txt'); + + // Already cached (copied from old cache). + expect(await makeRequest(scope, '/lazy/unchanged1.txt')).toBe('this is unchanged (1)'); + serverUpdate.assertNoOtherRequests(); + + // Not cached: Fetch from network. + expect(await makeRequest(scope, '/lazy/unchanged2.txt')).toBe('this is unchanged (2)'); + serverUpdate.assertSawRequestFor('/lazy/unchanged2.txt'); + + serverUpdate.assertNoOtherRequests(); + }); + + it('should bypass serviceworker on ngsw-bypass parameter', async () => { + await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': 'true'}}); + server.assertNoRequestFor('/foo.txt'); + + await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': 'anything'}}); + server.assertNoRequestFor('/foo.txt'); + + await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': null!}}); + server.assertNoRequestFor('/foo.txt'); + + await makeRequest(scope, '/foo.txt', undefined, {headers: {'NGSW-bypass': 'upperCASE'}}); + server.assertNoRequestFor('/foo.txt'); + + await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypasss': 'anything'}}); + server.assertSawRequestFor('/foo.txt'); + + server.clearRequests(); + + await makeRequest(scope, '/bar.txt?ngsw-bypass=true'); + server.assertNoRequestFor('/bar.txt'); + + await makeRequest(scope, '/bar.txt?ngsw-bypasss=true'); + server.assertSawRequestFor('/bar.txt'); + + server.clearRequests(); + + await makeRequest(scope, '/bar.txt?ngsw-bypaSS=something'); + server.assertNoRequestFor('/bar.txt'); + + await makeRequest(scope, '/bar.txt?testparam=test&ngsw-byPASS=anything'); + server.assertNoRequestFor('/bar.txt'); + + await makeRequest(scope, '/bar.txt?testparam=test&angsw-byPASS=anything'); + server.assertSawRequestFor('/bar.txt'); + + server.clearRequests(); + + await makeRequest(scope, '/bar&ngsw-bypass=true.txt?testparam=test&angsw-byPASS=anything'); + server.assertSawRequestFor('/bar&ngsw-bypass=true.txt'); + + server.clearRequests(); + + await makeRequest(scope, '/bar&ngsw-bypass=true.txt'); + server.assertSawRequestFor('/bar&ngsw-bypass=true.txt'); + + server.clearRequests(); + + await makeRequest( + scope, '/bar&ngsw-bypass=true.txt?testparam=test&ngSW-BYPASS=SOMETHING&testparam2=test'); + server.assertNoRequestFor('/bar&ngsw-bypass=true.txt'); + + await makeRequest(scope, '/bar?testparam=test&ngsw-bypass'); + server.assertNoRequestFor('/bar'); + + await makeRequest(scope, '/bar?testparam=test&ngsw-bypass&testparam2'); + server.assertNoRequestFor('/bar'); + + await makeRequest(scope, '/bar?ngsw-bypass&testparam2'); + server.assertNoRequestFor('/bar'); + + await makeRequest(scope, '/bar?ngsw-bypass=&foo=ngsw-bypass'); + server.assertNoRequestFor('/bar'); + + await makeRequest(scope, '/bar?ngsw-byapass&testparam2'); + server.assertSawRequestFor('/bar'); + }); + + it('unregisters when manifest 404s', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + + scope.updateServerState(server404); + expect(await driver.checkForUpdate()).toEqual(false); + expect(scope.unregistered).toEqual(true); + expect(await scope.caches.keys()).toEqual([]); + }); + + it('does not unregister or change state when offline (i.e. manifest 504s)', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + server.online = false; + + expect(await driver.checkForUpdate()).toEqual(false); + expect(driver.state).toEqual(DriverReadyState.NORMAL); + expect(scope.unregistered).toBeFalsy(); + expect(await scope.caches.keys()).not.toEqual([]); + }); + + it('does not unregister or change state when status code is 503 (service unavailable)', + async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + spyOn(server, 'fetch').and.callFake(async (req: Request) => new MockResponse(null, { + status: 503, + statusText: 'Service Unavailable' + })); + + expect(await driver.checkForUpdate()).toEqual(false); + expect(driver.state).toEqual(DriverReadyState.NORMAL); + expect(scope.unregistered).toBeFalsy(); + expect(await scope.caches.keys()).not.toEqual([]); + }); + + describe('cache naming', () => { + // Helpers + const cacheKeysFor = (baseHref: string) => + [`ngsw:${baseHref}:db:control`, + `ngsw:${baseHref}:${manifestHash}:assets:assets:cache`, + `ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:assets:meta`, + `ngsw:${baseHref}:${manifestHash}:assets:other:cache`, + `ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:other:meta`, + `ngsw:${baseHref}:${manifestHash}:assets:lazy_prefetch:cache`, + `ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:lazy_prefetch:meta`, + `ngsw:${baseHref}:42:data:dynamic:api:cache`, + `ngsw:${baseHref}:db:ngsw:${baseHref}:42:data:dynamic:api:lru`, + `ngsw:${baseHref}:db:ngsw:${baseHref}:42:data:dynamic:api:age`, + `ngsw:${baseHref}:43:data:dynamic:api-static:cache`, + `ngsw:${baseHref}:db:ngsw:${baseHref}:43:data:dynamic:api-static:lru`, + `ngsw:${baseHref}:db:ngsw:${baseHref}:43:data:dynamic:api-static:age`, + ]; + + const getClientAssignments = async (sw: SwTestHarness, baseHref: string) => { + const cache = await sw.caches.open(`ngsw:${baseHref}:db:control`) as unknown as MockCache; + const dehydrated = cache.dehydrate(); + return JSON.parse(dehydrated['/assignments'].body!); + }; + + const initializeSwFor = + async (baseHref: string, initialCacheState = '{}', serverState = server) => { + const newScope = new SwTestHarnessBuilder(`http://localhost${baseHref}`) + .withCacheState(initialCacheState) + .withServerState(serverState) + .build(); + const newDriver = new Driver(newScope, newScope, new CacheDatabase(newScope, newScope)); + + await makeRequest(newScope, '/foo.txt', baseHref.replace(/\//g, '_')); + await newDriver.initialized; + + return newScope; + }; + + it('includes the SW scope in all cache names', async () => { + // Default SW with scope `/`. + await makeRequest(scope, '/foo.txt'); + await driver.initialized; + const cacheNames = await scope.caches.keys(); + + expect(cacheNames).toEqual(cacheKeysFor('/')); + expect(cacheNames.every(name => name.includes('/'))).toBe(true); + + // SW with scope `/foo/`. + const fooScope = await initializeSwFor('/foo/'); + const fooCacheNames = await fooScope.caches.keys(); + + expect(fooCacheNames).toEqual(cacheKeysFor('/foo/')); + expect(fooCacheNames.every(name => name.includes('/foo/'))).toBe(true); }); - it('broadcasts notification click events with action', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - await scope.handleClick( - {title: 'This is a test with action', body: 'Test body with action'}, 'button'); - const message: any = scope.clients.getMock('default') !.messages[0]; + it('does not affect caches from other scopes', async () => { + // Create SW with scope `/foo/`. + const fooScope = await initializeSwFor('/foo/'); + const fooAssignments = await getClientAssignments(fooScope, '/foo/'); - expect(message.type).toEqual('NOTIFICATION_CLICK'); - expect(message.data.action).toEqual('button'); - expect(message.data.notification.title).toEqual('This is a test with action'); - expect(message.data.notification.body).toEqual('Test body with action'); + expect(fooAssignments).toEqual({_foo_: manifestHash}); + + // Add new SW with different scope. + const barScope = await initializeSwFor('/bar/', await fooScope.caches.dehydrate()); + const barCacheNames = await barScope.caches.keys(); + const barAssignments = await getClientAssignments(barScope, '/bar/'); + + expect(barAssignments).toEqual({_bar_: manifestHash}); + expect(barCacheNames).toEqual([ + ...cacheKeysFor('/foo/'), + ...cacheKeysFor('/bar/'), + ]); + + // The caches for `/foo/` should be intact. + const fooAssignments2 = await getClientAssignments(barScope, '/foo/'); + expect(fooAssignments2).toEqual({_foo_: manifestHash}); }); - it('broadcasts notification click events without action', async() => { + it('updates existing caches for same scope', async () => { + // Create SW with scope `/foo/`. + const fooScope = await initializeSwFor('/foo/'); + await makeRequest(fooScope, '/foo.txt', '_bar_'); + const fooAssignments = await getClientAssignments(fooScope, '/foo/'); + + expect(fooAssignments).toEqual({ + _foo_: manifestHash, + _bar_: manifestHash, + }); + + expect(await makeRequest(fooScope, '/baz.txt', '_foo_')).toBe('this is baz'); + expect(await makeRequest(fooScope, '/baz.txt', '_bar_')).toBe('this is baz'); + + // Add new SW with same scope. + const fooScope2 = + await initializeSwFor('/foo/', await fooScope.caches.dehydrate(), serverUpdate); + await fooScope2.handleMessage({action: 'CHECK_FOR_UPDATES'}, '_foo_'); + await fooScope2.handleMessage({action: 'ACTIVATE_UPDATE'}, '_foo_'); + const fooAssignments2 = await getClientAssignments(fooScope2, '/foo/'); + + expect(fooAssignments2).toEqual({ + _foo_: manifestUpdateHash, + _bar_: manifestHash, + }); + + // Everything should still work as expected. + expect(await makeRequest(fooScope2, '/foo.txt', '_foo_')).toBe('this is foo v2'); + expect(await makeRequest(fooScope2, '/foo.txt', '_bar_')).toBe('this is foo'); + + expect(await makeRequest(fooScope2, '/baz.txt', '_foo_')).toBe('this is baz v2'); + expect(await makeRequest(fooScope2, '/baz.txt', '_bar_')).toBe('this is baz'); + }); + }); + + describe('unhashed requests', () => { + beforeEach(async () => { expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); await driver.initialized; - await scope.handleClick( - {title: 'This is a test without action', body: 'Test body without action'}); - const message: any = scope.clients.getMock('default') !.messages[0]; - - expect(message.type).toEqual('NOTIFICATION_CLICK'); - expect(message.data.action).toBeUndefined(); - expect(message.data.notification.title).toEqual('This is a test without action'); - expect(message.data.notification.body).toEqual('Test body without action'); + server.clearRequests(); }); - it('prefetches updates to lazy cache when set', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; + it('are cached appropriately', async () => { + expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); + server.assertSawRequestFor('/unhashed/a.txt'); + expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); + server.assertNoOtherRequests(); + }); - // Fetch some files from the `lazy_prefetch` asset group. - expect(await makeRequest(scope, '/quux.txt')).toEqual('this is quux'); - expect(await makeRequest(scope, '/lazy/unchanged1.txt')).toEqual('this is unchanged (1)'); + it(`doesn't error when 'Cache-Control' is 'no-cache'`, async () => { + expect(await makeRequest(scope, '/unhashed/b.txt')).toEqual('this is unhashed b'); + server.assertSawRequestFor('/unhashed/b.txt'); + expect(await makeRequest(scope, '/unhashed/b.txt')).toEqual('this is unhashed b'); + server.assertNoOtherRequests(); + }); - // Install update. + it('avoid opaque responses', async () => { + expect(await makeRequest(scope, '/unhashed/a.txt', 'default', { + credentials: 'include' + })).toEqual('this is unhashed'); + server.assertSawRequestFor('/unhashed/a.txt'); + expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); + server.assertNoOtherRequests(); + }); + + it('expire according to Cache-Control headers', async () => { + expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); + server.clearRequests(); + + // Update the resource on the server. scope.updateServerState(serverUpdate); - expect(await driver.checkForUpdate()).toBe(true); - // Previously requested and changed: Fetch from network. - serverUpdate.assertSawRequestFor('/quux.txt'); - // Never requested and changed: Don't fetch. - serverUpdate.assertNoRequestFor('/quuux.txt'); - // Previously requested and unchanged: Fetch from cache. - serverUpdate.assertNoRequestFor('/lazy/unchanged1.txt'); - // Never requested and unchanged: Don't fetch. - serverUpdate.assertNoRequestFor('/lazy/unchanged2.txt'); + // Move ahead by 15 seconds. + scope.advance(15000); + expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); + serverUpdate.assertNoOtherRequests(); + + // Another 6 seconds. + scope.advance(6000); + await driver.idle.empty; + serverUpdate.assertSawRequestFor('/unhashed/a.txt'); + + // Now the new version of the resource should be served. + expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed v2'); + server.assertNoOtherRequests(); + }); + + it('survive serialization', async () => { + expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); + server.clearRequests(); + + const state = scope.caches.dehydrate(); + scope = new SwTestHarnessBuilder().withCacheState(state).withServerState(server).build(); + driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + server.assertNoRequestFor('/unhashed/a.txt'); + server.clearRequests(); + + expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); + server.assertNoOtherRequests(); + + // Advance the clock by 6 seconds, triggering the idle tasks. If an idle task + // was scheduled from the request above, it means that the metadata was not + // properly saved. + scope.advance(6000); + await driver.idle.empty; + server.assertNoRequestFor('/unhashed/a.txt'); + }); + + it('get carried over during updates', async () => { + expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); + server.clearRequests(); + + scope = new SwTestHarnessBuilder() + .withCacheState(scope.caches.dehydrate()) + .withServerState(serverUpdate) + .build(); + driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + + scope.advance(15000); + await driver.idle.empty; + serverUpdate.assertNoRequestFor('/unhashed/a.txt'); serverUpdate.clearRequests(); - // Update client. - await driver.updateClient(await scope.clients.get('default')); - - // Already cached. - expect(await makeRequest(scope, '/quux.txt')).toBe('this is quux v2'); + expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); serverUpdate.assertNoOtherRequests(); - // Not cached: Fetch from network. - expect(await makeRequest(scope, '/quuux.txt')).toBe('this is quuux v2'); - serverUpdate.assertSawRequestFor('/quuux.txt'); - - // Already cached (copied from old cache). - expect(await makeRequest(scope, '/lazy/unchanged1.txt')).toBe('this is unchanged (1)'); - serverUpdate.assertNoOtherRequests(); - - // Not cached: Fetch from network. - expect(await makeRequest(scope, '/lazy/unchanged2.txt')).toBe('this is unchanged (2)'); - serverUpdate.assertSawRequestFor('/lazy/unchanged2.txt'); + scope.advance(15000); + await driver.idle.empty; + serverUpdate.assertSawRequestFor('/unhashed/a.txt'); + expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed v2'); serverUpdate.assertNoOtherRequests(); }); + }); - it('should bypass serviceworker on ngsw-bypass parameter', async() => { - await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': 'true'}}); - server.assertNoRequestFor('/foo.txt'); + describe('routing', () => { + const navRequest = (url: string, init = {}) => + makeNavigationRequest(scope, url, undefined, init); - await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': 'anything'}}); - server.assertNoRequestFor('/foo.txt'); - - await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': null !}}); - server.assertNoRequestFor('/foo.txt'); - - await makeRequest(scope, '/foo.txt', undefined, {headers: {'NGSW-bypass': 'upperCASE'}}); - server.assertNoRequestFor('/foo.txt'); - - await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypasss': 'anything'}}); - server.assertSawRequestFor('/foo.txt'); - - server.clearRequests(); - - await makeRequest(scope, '/bar.txt?ngsw-bypass=true'); - server.assertNoRequestFor('/bar.txt'); - - await makeRequest(scope, '/bar.txt?ngsw-bypasss=true'); - server.assertSawRequestFor('/bar.txt'); - - server.clearRequests(); - - await makeRequest(scope, '/bar.txt?ngsw-bypaSS=something'); - server.assertNoRequestFor('/bar.txt'); - - await makeRequest(scope, '/bar.txt?testparam=test&ngsw-byPASS=anything'); - server.assertNoRequestFor('/bar.txt'); - - await makeRequest(scope, '/bar.txt?testparam=test&angsw-byPASS=anything'); - server.assertSawRequestFor('/bar.txt'); - - server.clearRequests(); - - await makeRequest(scope, '/bar&ngsw-bypass=true.txt?testparam=test&angsw-byPASS=anything'); - server.assertSawRequestFor('/bar&ngsw-bypass=true.txt'); - - server.clearRequests(); - - await makeRequest(scope, '/bar&ngsw-bypass=true.txt'); - server.assertSawRequestFor('/bar&ngsw-bypass=true.txt'); - - server.clearRequests(); - - await makeRequest( - scope, '/bar&ngsw-bypass=true.txt?testparam=test&ngSW-BYPASS=SOMETHING&testparam2=test'); - server.assertNoRequestFor('/bar&ngsw-bypass=true.txt'); - - await makeRequest(scope, '/bar?testparam=test&ngsw-bypass'); - server.assertNoRequestFor('/bar'); - - await makeRequest(scope, '/bar?testparam=test&ngsw-bypass&testparam2'); - server.assertNoRequestFor('/bar'); - - await makeRequest(scope, '/bar?ngsw-bypass&testparam2'); - server.assertNoRequestFor('/bar'); - - await makeRequest(scope, '/bar?ngsw-bypass=&foo=ngsw-bypass'); - server.assertNoRequestFor('/bar'); - - await makeRequest(scope, '/bar?ngsw-byapass&testparam2'); - server.assertSawRequestFor('/bar'); - - }); - - it('unregisters when manifest 404s', async() => { + beforeEach(async () => { expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); await driver.initialized; - - scope.updateServerState(server404); - expect(await driver.checkForUpdate()).toEqual(false); - expect(scope.unregistered).toEqual(true); - expect(await scope.caches.keys()).toEqual([]); + server.clearRequests(); }); - it('does not unregister or change state when offline (i.e. manifest 504s)', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.online = false; - - expect(await driver.checkForUpdate()).toEqual(false); - expect(driver.state).toEqual(DriverReadyState.NORMAL); - expect(scope.unregistered).toBeFalsy(); - expect(await scope.caches.keys()).not.toEqual([]); + it('redirects to index on a route-like request', async () => { + expect(await navRequest('/baz')).toEqual('this is foo'); + server.assertNoOtherRequests(); }); - it('does not unregister or change state when status code is 503 (service unavailable)', - async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - spyOn(server, 'fetch').and.callFake(async(req: Request) => new MockResponse(null, { - status: 503, - statusText: 'Service Unavailable' - })); - - expect(await driver.checkForUpdate()).toEqual(false); - expect(driver.state).toEqual(DriverReadyState.NORMAL); - expect(scope.unregistered).toBeFalsy(); - expect(await scope.caches.keys()).not.toEqual([]); - }); - - describe('cache naming', () => { - // Helpers - const cacheKeysFor = (baseHref: string) => - [`ngsw:${baseHref}:db:control`, `ngsw:${baseHref}:${manifestHash}:assets:assets:cache`, - `ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:assets:meta`, - `ngsw:${baseHref}:${manifestHash}:assets:other:cache`, - `ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:other:meta`, - `ngsw:${baseHref}:${manifestHash}:assets:lazy_prefetch:cache`, - `ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:lazy_prefetch:meta`, - `ngsw:${baseHref}:42:data:dynamic:api:cache`, - `ngsw:${baseHref}:db:ngsw:${baseHref}:42:data:dynamic:api:lru`, - `ngsw:${baseHref}:db:ngsw:${baseHref}:42:data:dynamic:api:age`, - `ngsw:${baseHref}:43:data:dynamic:api-static:cache`, - `ngsw:${baseHref}:db:ngsw:${baseHref}:43:data:dynamic:api-static:lru`, - `ngsw:${baseHref}:db:ngsw:${baseHref}:43:data:dynamic:api-static:age`, - ]; - - const getClientAssignments = async(sw: SwTestHarness, baseHref: string) => { - const cache = await sw.caches.open(`ngsw:${baseHref}:db:control`) as unknown as MockCache; - const dehydrated = cache.dehydrate(); - return JSON.parse(dehydrated['/assignments'].body !); - }; - - const initializeSwFor = - async(baseHref: string, initialCacheState = '{}', serverState = server) => { - const newScope = new SwTestHarnessBuilder(`http://localhost${baseHref}`) - .withCacheState(initialCacheState) - .withServerState(serverState) - .build(); - const newDriver = new Driver(newScope, newScope, new CacheDatabase(newScope, newScope)); - - await makeRequest(newScope, '/foo.txt', baseHref.replace(/\//g, '_')); - await newDriver.initialized; - - return newScope; - }; - - it('includes the SW scope in all cache names', async() => { - // Default SW with scope `/`. - await makeRequest(scope, '/foo.txt'); - await driver.initialized; - const cacheNames = await scope.caches.keys(); - - expect(cacheNames).toEqual(cacheKeysFor('/')); - expect(cacheNames.every(name => name.includes('/'))).toBe(true); - - // SW with scope `/foo/`. - const fooScope = await initializeSwFor('/foo/'); - const fooCacheNames = await fooScope.caches.keys(); - - expect(fooCacheNames).toEqual(cacheKeysFor('/foo/')); - expect(fooCacheNames.every(name => name.includes('/foo/'))).toBe(true); - }); - - it('does not affect caches from other scopes', async() => { - // Create SW with scope `/foo/`. - const fooScope = await initializeSwFor('/foo/'); - const fooAssignments = await getClientAssignments(fooScope, '/foo/'); - - expect(fooAssignments).toEqual({_foo_: manifestHash}); - - // Add new SW with different scope. - const barScope = await initializeSwFor('/bar/', await fooScope.caches.dehydrate()); - const barCacheNames = await barScope.caches.keys(); - const barAssignments = await getClientAssignments(barScope, '/bar/'); - - expect(barAssignments).toEqual({_bar_: manifestHash}); - expect(barCacheNames).toEqual([ - ...cacheKeysFor('/foo/'), - ...cacheKeysFor('/bar/'), - ]); - - // The caches for `/foo/` should be intact. - const fooAssignments2 = await getClientAssignments(barScope, '/foo/'); - expect(fooAssignments2).toEqual({_foo_: manifestHash}); - }); - - it('updates existing caches for same scope', async() => { - // Create SW with scope `/foo/`. - const fooScope = await initializeSwFor('/foo/'); - await makeRequest(fooScope, '/foo.txt', '_bar_'); - const fooAssignments = await getClientAssignments(fooScope, '/foo/'); - - expect(fooAssignments).toEqual({ - _foo_: manifestHash, - _bar_: manifestHash, - }); - - expect(await makeRequest(fooScope, '/baz.txt', '_foo_')).toBe('this is baz'); - expect(await makeRequest(fooScope, '/baz.txt', '_bar_')).toBe('this is baz'); - - // Add new SW with same scope. - const fooScope2 = - await initializeSwFor('/foo/', await fooScope.caches.dehydrate(), serverUpdate); - await fooScope2.handleMessage({action: 'CHECK_FOR_UPDATES'}, '_foo_'); - await fooScope2.handleMessage({action: 'ACTIVATE_UPDATE'}, '_foo_'); - const fooAssignments2 = await getClientAssignments(fooScope2, '/foo/'); - - expect(fooAssignments2).toEqual({ - _foo_: manifestUpdateHash, - _bar_: manifestHash, - }); - - // Everything should still work as expected. - expect(await makeRequest(fooScope2, '/foo.txt', '_foo_')).toBe('this is foo v2'); - expect(await makeRequest(fooScope2, '/foo.txt', '_bar_')).toBe('this is foo'); - - expect(await makeRequest(fooScope2, '/baz.txt', '_foo_')).toBe('this is baz v2'); - expect(await makeRequest(fooScope2, '/baz.txt', '_bar_')).toBe('this is baz'); - }); + it('redirects to index on a request to the origin URL request', async () => { + expect(await navRequest('http://localhost/')).toEqual('this is foo'); + server.assertNoOtherRequests(); }); - describe('unhashed requests', () => { - beforeEach(async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.clearRequests(); - }); + it('does not redirect to index on a non-navigation request', async () => { + expect(await navRequest('/baz', {mode: undefined})).toBeNull(); + server.assertSawRequestFor('/baz'); + }); - it('are cached appropriately', async() => { - expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); - server.assertSawRequestFor('/unhashed/a.txt'); - expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); - server.assertNoOtherRequests(); - }); + it('does not redirect to index on a request that does not accept HTML', async () => { + expect(await navRequest('/baz', {headers: {}})).toBeNull(); + server.assertSawRequestFor('/baz'); - it(`doesn't error when 'Cache-Control' is 'no-cache'`, async() => { - expect(await makeRequest(scope, '/unhashed/b.txt')).toEqual('this is unhashed b'); - server.assertSawRequestFor('/unhashed/b.txt'); - expect(await makeRequest(scope, '/unhashed/b.txt')).toEqual('this is unhashed b'); - server.assertNoOtherRequests(); - }); + expect(await navRequest('/qux', {headers: {'Accept': 'text/plain'}})).toBeNull(); + server.assertSawRequestFor('/qux'); + }); - it('avoid opaque responses', async() => { - expect(await makeRequest(scope, '/unhashed/a.txt', 'default', { - credentials: 'include' - })).toEqual('this is unhashed'); - server.assertSawRequestFor('/unhashed/a.txt'); - expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); - server.assertNoOtherRequests(); - }); + it('does not redirect to index on a request with an extension', async () => { + expect(await navRequest('/baz.html')).toBeNull(); + server.assertSawRequestFor('/baz.html'); - it('expire according to Cache-Control headers', async() => { - expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); - server.clearRequests(); + // Only considers the last path segment when checking for a file extension. + expect(await navRequest('/baz.html/qux')).toBe('this is foo'); + server.assertNoOtherRequests(); + }); - // Update the resource on the server. + it('does not redirect to index if the URL contains `__`', async () => { + expect(await navRequest('/baz/x__x')).toBeNull(); + server.assertSawRequestFor('/baz/x__x'); + + expect(await navRequest('/baz/x__x/qux')).toBeNull(); + server.assertSawRequestFor('/baz/x__x/qux'); + + expect(await navRequest('/baz/__')).toBeNull(); + server.assertSawRequestFor('/baz/__'); + + expect(await navRequest('/baz/__/qux')).toBeNull(); + server.assertSawRequestFor('/baz/__/qux'); + }); + + describe('(with custom `navigationUrls`)', () => { + beforeEach(async () => { scope.updateServerState(serverUpdate); - - // Move ahead by 15 seconds. - scope.advance(15000); - - expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); - serverUpdate.assertNoOtherRequests(); - - // Another 6 seconds. - scope.advance(6000); - await driver.idle.empty; - serverUpdate.assertSawRequestFor('/unhashed/a.txt'); - - // Now the new version of the resource should be served. - expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed v2'); - server.assertNoOtherRequests(); - }); - - it('survive serialization', async() => { - expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); - server.clearRequests(); - - const state = scope.caches.dehydrate(); - scope = new SwTestHarnessBuilder().withCacheState(state).withServerState(server).build(); - driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.assertNoRequestFor('/unhashed/a.txt'); - server.clearRequests(); - - expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); - server.assertNoOtherRequests(); - - // Advance the clock by 6 seconds, triggering the idle tasks. If an idle task - // was scheduled from the request above, it means that the metadata was not - // properly saved. - scope.advance(6000); - await driver.idle.empty; - server.assertNoRequestFor('/unhashed/a.txt'); - }); - - it('get carried over during updates', async() => { - expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); - server.clearRequests(); - - scope = new SwTestHarnessBuilder() - .withCacheState(scope.caches.dehydrate()) - .withServerState(serverUpdate) - .build(); - driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - - scope.advance(15000); - await driver.idle.empty; - serverUpdate.assertNoRequestFor('/unhashed/a.txt'); + await driver.checkForUpdate(); serverUpdate.clearRequests(); + }); - expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed'); + it('redirects to index on a request that matches any positive pattern', async () => { + expect(await navRequest('/foo/file0')).toBeNull(); + serverUpdate.assertSawRequestFor('/foo/file0'); + + expect(await navRequest('/foo/file1')).toBe('this is foo v2'); serverUpdate.assertNoOtherRequests(); - scope.advance(15000); - await driver.idle.empty; - serverUpdate.assertSawRequestFor('/unhashed/a.txt'); - - expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed v2'); + expect(await navRequest('/bar/file2')).toBe('this is foo v2'); serverUpdate.assertNoOtherRequests(); }); - }); - describe('routing', () => { - const navRequest = (url: string, init = {}) => - makeNavigationRequest(scope, url, undefined, init); + 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'); + serverUpdate.assertSawRequestFor('/ignored/file1'); - beforeEach(async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.clearRequests(); + expect(await navRequest('/ignored/dir/file2')).toBe('this is not handled by the SW either'); + serverUpdate.assertSawRequestFor('/ignored/dir/file2'); + + expect(await navRequest('/ignored/directory/file2')).toBe('this is foo v2'); + serverUpdate.assertNoOtherRequests(); }); - it('redirects to index on a route-like request', async() => { - expect(await navRequest('/baz')).toEqual('this is foo'); - server.assertNoOtherRequests(); + it('strips URL query before checking `navigationUrls`', async () => { + expect(await navRequest('/foo/file1?query=/a/b')).toBe('this is foo v2'); + serverUpdate.assertNoOtherRequests(); + + expect(await navRequest('/ignored/file1?query=/a/b')).toBe('this is not handled by the SW'); + serverUpdate.assertSawRequestFor('/ignored/file1'); + + expect(await navRequest('/ignored/dir/file2?query=/a/b')) + .toBe('this is not handled by the SW either'); + serverUpdate.assertSawRequestFor('/ignored/dir/file2'); }); - it('redirects to index on a request to the origin URL request', async() => { - expect(await navRequest('http://localhost/')).toEqual('this is foo'); - server.assertNoOtherRequests(); - }); - - it('does not redirect to index on a non-navigation request', async() => { - expect(await navRequest('/baz', {mode: undefined})).toBeNull(); - server.assertSawRequestFor('/baz'); - }); - - it('does not redirect to index on a request that does not accept HTML', async() => { - expect(await navRequest('/baz', {headers: {}})).toBeNull(); - server.assertSawRequestFor('/baz'); - - expect(await navRequest('/qux', {headers: {'Accept': 'text/plain'}})).toBeNull(); - server.assertSawRequestFor('/qux'); - }); - - it('does not redirect to index on a request with an extension', async() => { - expect(await navRequest('/baz.html')).toBeNull(); - server.assertSawRequestFor('/baz.html'); - - // Only considers the last path segment when checking for a file extension. - expect(await navRequest('/baz.html/qux')).toBe('this is foo'); - server.assertNoOtherRequests(); - }); - - it('does not redirect to index if the URL contains `__`', async() => { - expect(await navRequest('/baz/x__x')).toBeNull(); - server.assertSawRequestFor('/baz/x__x'); - - expect(await navRequest('/baz/x__x/qux')).toBeNull(); - server.assertSawRequestFor('/baz/x__x/qux'); - - expect(await navRequest('/baz/__')).toBeNull(); - server.assertSawRequestFor('/baz/__'); - - expect(await navRequest('/baz/__/qux')).toBeNull(); - server.assertSawRequestFor('/baz/__/qux'); - }); - - describe('(with custom `navigationUrls`)', () => { - beforeEach(async() => { - scope.updateServerState(serverUpdate); - await driver.checkForUpdate(); - serverUpdate.clearRequests(); - }); - - it('redirects to index on a request that matches any positive pattern', async() => { - expect(await navRequest('/foo/file0')).toBeNull(); - serverUpdate.assertSawRequestFor('/foo/file0'); - - expect(await navRequest('/foo/file1')).toBe('this is foo v2'); - serverUpdate.assertNoOtherRequests(); - - expect(await navRequest('/bar/file2')).toBe('this is foo v2'); - serverUpdate.assertNoOtherRequests(); - }); - - 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'); - serverUpdate.assertSawRequestFor('/ignored/file1'); - - expect(await navRequest('/ignored/dir/file2')) - .toBe('this is not handled by the SW either'); - serverUpdate.assertSawRequestFor('/ignored/dir/file2'); - - expect(await navRequest('/ignored/directory/file2')).toBe('this is foo v2'); - serverUpdate.assertNoOtherRequests(); - }); - - it('strips URL query before checking `navigationUrls`', async() => { - expect(await navRequest('/foo/file1?query=/a/b')).toBe('this is foo v2'); - serverUpdate.assertNoOtherRequests(); - - expect(await navRequest('/ignored/file1?query=/a/b')) - .toBe('this is not handled by the SW'); - serverUpdate.assertSawRequestFor('/ignored/file1'); - - expect(await navRequest('/ignored/dir/file2?query=/a/b')) - .toBe('this is not handled by the SW either'); - serverUpdate.assertSawRequestFor('/ignored/dir/file2'); - }); - - it('strips registration scope before checking `navigationUrls`', async() => { - expect(await navRequest('http://localhost/ignored/file1')) - .toBe('this is not handled by the SW'); - serverUpdate.assertSawRequestFor('/ignored/file1'); - }); - }); - }); - - describe('cleanupOldSwCaches()', () => { - it('should delete the correct caches', async() => { - const oldSwCacheNames = [ - // Example cache names from the beta versions of `@angular/service-worker`. - 'ngsw:active', - 'ngsw:staged', - 'ngsw:manifest:a1b2c3:super:duper', - // Example cache names from the beta versions of `@angular/service-worker`. - 'ngsw:a1b2c3:assets:foo', - 'ngsw:db:a1b2c3:assets:bar', - ]; - const otherCacheNames = [ - 'ngsuu:active', - 'not:ngsw:active', - 'NgSw:StAgEd', - 'ngsw:/:active', - 'ngsw:/foo/:staged', - ]; - const allCacheNames = oldSwCacheNames.concat(otherCacheNames); - - await Promise.all(allCacheNames.map(name => scope.caches.open(name))); - expect(await scope.caches.keys()).toEqual(allCacheNames); - - await driver.cleanupOldSwCaches(); - expect(await scope.caches.keys()).toEqual(otherCacheNames); - }); - - 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 deleteSpy = spyOn(scope.caches, 'delete') - .and.callFake( - (cacheName: string) => - Promise.reject(`Failed to delete cache '${cacheName}'.`)); - - await Promise.all(oldSwCacheNames.map(name => scope.caches.open(name))); - const error = await driver.cleanupOldSwCaches().catch(err => err); - - expect(error).toBe('Failed to delete cache \'ngsw:active\'.'); - expect(deleteSpy).toHaveBeenCalledTimes(3); - oldSwCacheNames.forEach(name => expect(deleteSpy).toHaveBeenCalledWith(name)); - }); - }); - - describe('bugs', () => { - it('does not crash with bad index hash', async() => { - scope = new SwTestHarnessBuilder().withServerState(brokenServer).build(); - (scope.registration as any).scope = 'http://site.com'; - driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); - - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo (broken)'); - }); - - it('enters degraded mode when update has a bad index', async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.clearRequests(); - - scope = new SwTestHarnessBuilder() - .withCacheState(scope.caches.dehydrate()) - .withServerState(brokenServer) - .build(); - driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); - await driver.checkForUpdate(); - - scope.advance(12000); - await driver.idle.empty; - - expect(driver.state).toEqual(DriverReadyState.EXISTING_CLIENTS_ONLY); - }); - - it('enters degraded mode when failing to write to cache', async() => { - // Initialize the SW. - await makeRequest(scope, '/foo.txt'); - await driver.initialized; - expect(driver.state).toBe(DriverReadyState.NORMAL); - - server.clearRequests(); - - // Operate normally. - expect(await makeRequest(scope, '/foo.txt')).toBe('this is foo'); - server.assertNoOtherRequests(); - - // Clear the caches and make them unwritable. - await clearAllCaches(scope.caches); - spyOn(MockCache.prototype, 'put').and.throwError('Can\'t touch this'); - - // Enter degraded mode and serve from network. - expect(await makeRequest(scope, '/foo.txt')).toBe('this is foo'); - expect(driver.state).toBe(DriverReadyState.EXISTING_CLIENTS_ONLY); - server.assertSawRequestFor('/foo.txt'); - }); - - it('keeps serving api requests with freshness strategy when failing to write to cache', - async() => { - // Initialize the SW. - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.clearRequests(); - - // Make the caches unwritable. - spyOn(MockCache.prototype, 'put').and.throwError('Can\'t touch this'); - spyOn(driver.debugger, 'log'); - - expect(await makeRequest(scope, '/api/foo')).toEqual('this is api foo'); - expect(driver.state).toBe(DriverReadyState.NORMAL); - // Since we are swallowing an error here, make sure it is at least properly logged - expect(driver.debugger.log) - .toHaveBeenCalledWith( - new Error('Can\'t touch this'), - 'DataGroup(api@42).safeCacheResponse(/api/foo, status: 200)'); - server.assertSawRequestFor('/api/foo'); - }); - - it('keeps serving api requests with performance strategy when failing to write to cache', - async() => { - // Initialize the SW. - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.clearRequests(); - - // Make the caches unwritable. - spyOn(MockCache.prototype, 'put').and.throwError('Can\'t touch this'); - spyOn(driver.debugger, 'log'); - - expect(await makeRequest(scope, '/api-static/bar')).toEqual('this is static api bar'); - expect(driver.state).toBe(DriverReadyState.NORMAL); - // Since we are swallowing an error here, make sure it is at least properly logged - expect(driver.debugger.log) - .toHaveBeenCalledWith( - new Error('Can\'t touch this'), - 'DataGroup(api-static@43).safeCacheResponse(/api-static/bar, status: 200)'); - server.assertSawRequestFor('/api-static/bar'); - }); - - 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 - async() => { - // Initialize the SW. - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - server.clearRequests(); - - // Make the caches unwritable. - spyOn(MockCache.prototype, 'put').and.throwError('Can\'t touch this'); - spyOn(driver.debugger, 'log'); - expect(await makeRequest(scope, '/api/foo', 'default', { - method: 'post' - })).toEqual('this is api foo'); - expect(driver.state).toBe(DriverReadyState.NORMAL); - // Since we are swallowing an error here, make sure it is at least properly logged - expect(driver.debugger.log) - .toHaveBeenCalledWith(new Error('Can\'t touch this'), 'DataGroup(api@42).syncLru()'); - server.assertSawRequestFor('/api/foo'); - }); - - it('enters degraded mode when something goes wrong with the latest version', async() => { - await driver.initialized; - - // Two clients on initial version. - expect(await makeRequest(scope, '/foo.txt', 'client1')).toBe('this is foo'); - expect(await makeRequest(scope, '/foo.txt', 'client2')).toBe('this is foo'); - - // Install a broken version (`bar.txt` has invalid hash). - scope.updateServerState(brokenLazyServer); - await driver.checkForUpdate(); - - // Update `client1` but not `client2`. - await makeNavigationRequest(scope, '/', 'client1'); - server.clearRequests(); - brokenLazyServer.clearRequests(); - - expect(await makeRequest(scope, '/foo.txt', 'client1')).toBe('this is foo (broken)'); - expect(await makeRequest(scope, '/foo.txt', 'client2')).toBe('this is foo'); - server.assertNoOtherRequests(); - brokenLazyServer.assertNoOtherRequests(); - - // Trying to fetch `bar.txt` (which has an invalid hash) should invalidate the latest - // version, enter degraded mode and "forget" clients that are on that version (i.e. - // `client1`). - expect(await makeRequest(scope, '/bar.txt', 'client1')).toBe('this is bar (broken)'); - expect(driver.state).toBe(DriverReadyState.EXISTING_CLIENTS_ONLY); - brokenLazyServer.sawRequestFor('/bar.txt'); - brokenLazyServer.clearRequests(); - - // `client1` should not be served from the network. - expect(await makeRequest(scope, '/foo.txt', 'client1')).toBe('this is foo (broken)'); - brokenLazyServer.sawRequestFor('/foo.txt'); - - // `client2` should still be served from the old version (since it never updated). - expect(await makeRequest(scope, '/foo.txt', 'client2')).toBe('this is foo'); - server.assertNoOtherRequests(); - brokenLazyServer.assertNoOtherRequests(); - }); - - it('recovers from degraded `EXISTING_CLIENTS_ONLY` mode as soon as there is a valid update', - async() => { - await driver.initialized; - expect(driver.state).toBe(DriverReadyState.NORMAL); - - // Install a broken version. - scope.updateServerState(brokenServer); - await driver.checkForUpdate(); - expect(driver.state).toBe(DriverReadyState.EXISTING_CLIENTS_ONLY); - - // Install a good version. - scope.updateServerState(serverUpdate); - await driver.checkForUpdate(); - expect(driver.state).toBe(DriverReadyState.NORMAL); - }); - - it('ignores invalid `only-if-cached` requests ', async() => { - const requestFoo = (cache: RequestCache | 'only-if-cached', mode: RequestMode) => - makeRequest(scope, '/foo.txt', undefined, {cache, mode}); - - expect(await requestFoo('default', 'no-cors')).toBe('this is foo'); - expect(await requestFoo('only-if-cached', 'same-origin')).toBe('this is foo'); - expect(await requestFoo('only-if-cached', 'no-cors')).toBeNull(); - }); - - it('ignores passive mixed content requests ', async() => { - const scopeFetchSpy = spyOn(scope, 'fetch').and.callThrough(); - const getRequestUrls = () => - (scopeFetchSpy.calls.allArgs() as[Request][]).map(args => args[0].url); - - const httpScopeUrl = 'http://mock.origin.dev'; - const httpsScopeUrl = 'https://mock.origin.dev'; - const httpRequestUrl = 'http://other.origin.sh/unknown.png'; - const httpsRequestUrl = 'https://other.origin.sh/unknown.pnp'; - - // Registration scope: `http:` - (scope.registration.scope as string) = httpScopeUrl; - - await makeRequest(scope, httpRequestUrl); - await makeRequest(scope, httpsRequestUrl); - const requestUrls1 = getRequestUrls(); - - expect(requestUrls1).toContain(httpRequestUrl); - expect(requestUrls1).toContain(httpsRequestUrl); - - scopeFetchSpy.calls.reset(); - - // Registration scope: `https:` - (scope.registration.scope as string) = httpsScopeUrl; - - await makeRequest(scope, httpRequestUrl); - await makeRequest(scope, httpsRequestUrl); - const requestUrls2 = getRequestUrls(); - - expect(requestUrls2).not.toContain(httpRequestUrl); - expect(requestUrls2).toContain(httpsRequestUrl); - }); - - describe('backwards compatibility with v5', () => { - beforeEach(() => { - const serverV5 = new MockServerStateBuilder() - .withStaticFiles(dist) - .withManifest(manifestOld) - .build(); - - scope = new SwTestHarnessBuilder().withServerState(serverV5).build(); - driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); - }); - - // Test this bug: https://github.com/angular/angular/issues/27209 - it('fills previous versions of manifests with default navigation urls for backwards compatibility', - async() => { - expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); - await driver.initialized; - scope.updateServerState(serverUpdate); - expect(await driver.checkForUpdate()).toEqual(true); - }); + it('strips registration scope before checking `navigationUrls`', async () => { + expect(await navRequest('http://localhost/ignored/file1')) + .toBe('this is not handled by the SW'); + serverUpdate.assertSawRequestFor('/ignored/file1'); }); }); }); + + describe('cleanupOldSwCaches()', () => { + it('should delete the correct caches', async () => { + const oldSwCacheNames = [ + // Example cache names from the beta versions of `@angular/service-worker`. + 'ngsw:active', + 'ngsw:staged', + 'ngsw:manifest:a1b2c3:super:duper', + // Example cache names from the beta versions of `@angular/service-worker`. + 'ngsw:a1b2c3:assets:foo', + 'ngsw:db:a1b2c3:assets:bar', + ]; + const otherCacheNames = [ + 'ngsuu:active', + 'not:ngsw:active', + 'NgSw:StAgEd', + 'ngsw:/:active', + 'ngsw:/foo/:staged', + ]; + const allCacheNames = oldSwCacheNames.concat(otherCacheNames); + + await Promise.all(allCacheNames.map(name => scope.caches.open(name))); + expect(await scope.caches.keys()).toEqual(allCacheNames); + + await driver.cleanupOldSwCaches(); + expect(await scope.caches.keys()).toEqual(otherCacheNames); + }); + + 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 deleteSpy = + spyOn(scope.caches, 'delete') + .and.callFake( + (cacheName: string) => Promise.reject(`Failed to delete cache '${cacheName}'.`)); + + await Promise.all(oldSwCacheNames.map(name => scope.caches.open(name))); + const error = await driver.cleanupOldSwCaches().catch(err => err); + + expect(error).toBe('Failed to delete cache \'ngsw:active\'.'); + expect(deleteSpy).toHaveBeenCalledTimes(3); + oldSwCacheNames.forEach(name => expect(deleteSpy).toHaveBeenCalledWith(name)); + }); + }); + + describe('bugs', () => { + it('does not crash with bad index hash', async () => { + scope = new SwTestHarnessBuilder().withServerState(brokenServer).build(); + (scope.registration as any).scope = 'http://site.com'; + driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); + + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo (broken)'); + }); + + it('enters degraded mode when update has a bad index', async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + server.clearRequests(); + + scope = new SwTestHarnessBuilder() + .withCacheState(scope.caches.dehydrate()) + .withServerState(brokenServer) + .build(); + driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); + await driver.checkForUpdate(); + + scope.advance(12000); + await driver.idle.empty; + + expect(driver.state).toEqual(DriverReadyState.EXISTING_CLIENTS_ONLY); + }); + + it('enters degraded mode when failing to write to cache', async () => { + // Initialize the SW. + await makeRequest(scope, '/foo.txt'); + await driver.initialized; + expect(driver.state).toBe(DriverReadyState.NORMAL); + + server.clearRequests(); + + // Operate normally. + expect(await makeRequest(scope, '/foo.txt')).toBe('this is foo'); + server.assertNoOtherRequests(); + + // Clear the caches and make them unwritable. + await clearAllCaches(scope.caches); + spyOn(MockCache.prototype, 'put').and.throwError('Can\'t touch this'); + + // Enter degraded mode and serve from network. + expect(await makeRequest(scope, '/foo.txt')).toBe('this is foo'); + expect(driver.state).toBe(DriverReadyState.EXISTING_CLIENTS_ONLY); + server.assertSawRequestFor('/foo.txt'); + }); + + it('keeps serving api requests with freshness strategy when failing to write to cache', + async () => { + // Initialize the SW. + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + server.clearRequests(); + + // Make the caches unwritable. + spyOn(MockCache.prototype, 'put').and.throwError('Can\'t touch this'); + spyOn(driver.debugger, 'log'); + + expect(await makeRequest(scope, '/api/foo')).toEqual('this is api foo'); + expect(driver.state).toBe(DriverReadyState.NORMAL); + // Since we are swallowing an error here, make sure it is at least properly logged + expect(driver.debugger.log) + .toHaveBeenCalledWith( + new Error('Can\'t touch this'), + 'DataGroup(api@42).safeCacheResponse(/api/foo, status: 200)'); + server.assertSawRequestFor('/api/foo'); + }); + + it('keeps serving api requests with performance strategy when failing to write to cache', + async () => { + // Initialize the SW. + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + server.clearRequests(); + + // Make the caches unwritable. + spyOn(MockCache.prototype, 'put').and.throwError('Can\'t touch this'); + spyOn(driver.debugger, 'log'); + + expect(await makeRequest(scope, '/api-static/bar')).toEqual('this is static api bar'); + expect(driver.state).toBe(DriverReadyState.NORMAL); + // Since we are swallowing an error here, make sure it is at least properly logged + expect(driver.debugger.log) + .toHaveBeenCalledWith( + new Error('Can\'t touch this'), + 'DataGroup(api-static@43).safeCacheResponse(/api-static/bar, status: 200)'); + server.assertSawRequestFor('/api-static/bar'); + }); + + 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 + async () => { + // Initialize the SW. + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + server.clearRequests(); + + // Make the caches unwritable. + spyOn(MockCache.prototype, 'put').and.throwError('Can\'t touch this'); + spyOn(driver.debugger, 'log'); + expect(await makeRequest(scope, '/api/foo', 'default', { + method: 'post' + })).toEqual('this is api foo'); + expect(driver.state).toBe(DriverReadyState.NORMAL); + // Since we are swallowing an error here, make sure it is at least properly logged + expect(driver.debugger.log) + .toHaveBeenCalledWith(new Error('Can\'t touch this'), 'DataGroup(api@42).syncLru()'); + server.assertSawRequestFor('/api/foo'); + }); + + it('enters degraded mode when something goes wrong with the latest version', async () => { + await driver.initialized; + + // Two clients on initial version. + expect(await makeRequest(scope, '/foo.txt', 'client1')).toBe('this is foo'); + expect(await makeRequest(scope, '/foo.txt', 'client2')).toBe('this is foo'); + + // Install a broken version (`bar.txt` has invalid hash). + scope.updateServerState(brokenLazyServer); + await driver.checkForUpdate(); + + // Update `client1` but not `client2`. + await makeNavigationRequest(scope, '/', 'client1'); + server.clearRequests(); + brokenLazyServer.clearRequests(); + + expect(await makeRequest(scope, '/foo.txt', 'client1')).toBe('this is foo (broken)'); + expect(await makeRequest(scope, '/foo.txt', 'client2')).toBe('this is foo'); + server.assertNoOtherRequests(); + brokenLazyServer.assertNoOtherRequests(); + + // Trying to fetch `bar.txt` (which has an invalid hash) should invalidate the latest + // version, enter degraded mode and "forget" clients that are on that version (i.e. + // `client1`). + expect(await makeRequest(scope, '/bar.txt', 'client1')).toBe('this is bar (broken)'); + expect(driver.state).toBe(DriverReadyState.EXISTING_CLIENTS_ONLY); + brokenLazyServer.sawRequestFor('/bar.txt'); + brokenLazyServer.clearRequests(); + + // `client1` should not be served from the network. + expect(await makeRequest(scope, '/foo.txt', 'client1')).toBe('this is foo (broken)'); + brokenLazyServer.sawRequestFor('/foo.txt'); + + // `client2` should still be served from the old version (since it never updated). + expect(await makeRequest(scope, '/foo.txt', 'client2')).toBe('this is foo'); + server.assertNoOtherRequests(); + brokenLazyServer.assertNoOtherRequests(); + }); + + it('recovers from degraded `EXISTING_CLIENTS_ONLY` mode as soon as there is a valid update', + async () => { + await driver.initialized; + expect(driver.state).toBe(DriverReadyState.NORMAL); + + // Install a broken version. + scope.updateServerState(brokenServer); + await driver.checkForUpdate(); + expect(driver.state).toBe(DriverReadyState.EXISTING_CLIENTS_ONLY); + + // Install a good version. + scope.updateServerState(serverUpdate); + await driver.checkForUpdate(); + expect(driver.state).toBe(DriverReadyState.NORMAL); + }); + + it('ignores invalid `only-if-cached` requests ', async () => { + const requestFoo = (cache: RequestCache|'only-if-cached', mode: RequestMode) => + makeRequest(scope, '/foo.txt', undefined, {cache, mode}); + + expect(await requestFoo('default', 'no-cors')).toBe('this is foo'); + expect(await requestFoo('only-if-cached', 'same-origin')).toBe('this is foo'); + expect(await requestFoo('only-if-cached', 'no-cors')).toBeNull(); + }); + + it('ignores passive mixed content requests ', async () => { + const scopeFetchSpy = spyOn(scope, 'fetch').and.callThrough(); + const getRequestUrls = () => + (scopeFetchSpy.calls.allArgs() as [Request][]).map(args => args[0].url); + + const httpScopeUrl = 'http://mock.origin.dev'; + const httpsScopeUrl = 'https://mock.origin.dev'; + const httpRequestUrl = 'http://other.origin.sh/unknown.png'; + const httpsRequestUrl = 'https://other.origin.sh/unknown.pnp'; + + // Registration scope: `http:` + (scope.registration.scope as string) = httpScopeUrl; + + await makeRequest(scope, httpRequestUrl); + await makeRequest(scope, httpsRequestUrl); + const requestUrls1 = getRequestUrls(); + + expect(requestUrls1).toContain(httpRequestUrl); + expect(requestUrls1).toContain(httpsRequestUrl); + + scopeFetchSpy.calls.reset(); + + // Registration scope: `https:` + (scope.registration.scope as string) = httpsScopeUrl; + + await makeRequest(scope, httpRequestUrl); + await makeRequest(scope, httpsRequestUrl); + const requestUrls2 = getRequestUrls(); + + expect(requestUrls2).not.toContain(httpRequestUrl); + expect(requestUrls2).toContain(httpsRequestUrl); + }); + + describe('backwards compatibility with v5', () => { + beforeEach(() => { + const serverV5 = new MockServerStateBuilder() + .withStaticFiles(dist) + .withManifest(manifestOld) + .build(); + + scope = new SwTestHarnessBuilder().withServerState(serverV5).build(); + driver = new Driver(scope, scope, new CacheDatabase(scope, scope)); + }); + + // Test this bug: https://github.com/angular/angular/issues/27209 + it('fills previous versions of manifests with default navigation urls for backwards compatibility', + async () => { + expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); + await driver.initialized; + scope.updateServerState(serverUpdate); + expect(await driver.checkForUpdate()).toEqual(true); + }); + }); + }); +}); })(); async function makeRequest( - scope: SwTestHarness, url: string, clientId: string | null = 'default', init?: Object): - Promise { - const [resPromise, done] = scope.handleFetch(new MockRequest(url, init), clientId); - await done; - const res = await resPromise; - if (res !== undefined && res.ok) { - return res.text(); - } - return null; - } + scope: SwTestHarness, url: string, clientId: string|null = 'default', + init?: Object): Promise { + const [resPromise, done] = scope.handleFetch(new MockRequest(url, init), clientId); + await done; + const res = await resPromise; + if (res !== undefined && res.ok) { + return res.text(); + } + return null; +} function makeNavigationRequest( - scope: SwTestHarness, url: string, clientId?: string | null, init: Object = {}): - Promise { - return makeRequest(scope, url, clientId, { - headers: { - Accept: 'text/plain, text/html, text/css', - ...(init as any).headers, - }, - mode: 'navigate', ...init, - }); - } + scope: SwTestHarness, url: string, clientId?: string|null, + init: Object = {}): Promise { + return makeRequest(scope, url, clientId, { + headers: { + Accept: 'text/plain, text/html, text/css', + ...(init as any).headers, + }, + mode: 'navigate', + ...init, + }); +} diff --git a/packages/upgrade/src/dynamic/test/upgrade_spec.ts b/packages/upgrade/src/dynamic/test/upgrade_spec.ts index 981690fe34..71f6ed9fec 100644 --- a/packages/upgrade/src/dynamic/test/upgrade_spec.ts +++ b/packages/upgrade/src/dynamic/test/upgrade_spec.ts @@ -6,7 +6,7 @@ * 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 {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; @@ -23,7 +23,6 @@ declare global { withEachNg1Version(() => { describe('adapter: ng1 to ng2', () => { - beforeEach(() => destroyPlatform()); afterEach(() => destroyPlatform()); @@ -232,7 +231,9 @@ withEachNg1Version(() => { }) class Ng2 { l: any; - constructor() { this.l = l; } + constructor() { + this.l = l; + } } @NgModule({ @@ -262,7 +263,9 @@ withEachNg1Version(() => { @Component({selector: 'my-app', template: ''}) class AppComponent { value?: number; - constructor() { appComponent = this; } + constructor() { + appComponent = this; + } } @Component({ @@ -272,7 +275,9 @@ withEachNg1Version(() => { class ChildComponent { valueFromPromise?: number; @Input() - set value(v: number) { expect(NgZone.isInAngularZone()).toBe(true); } + set value(v: number) { + expect(NgZone.isInAngularZone()).toBe(true); + } constructor(private zone: NgZone) {} @@ -352,14 +357,15 @@ withEachNg1Version(() => { const element = html(''); 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(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); - const ng1Module = - angular.module_('ng1', []).value($EXCEPTION_HANDLER, (err: any) => { throw err; }); + const ng1Module = angular.module_('ng1', []).value($EXCEPTION_HANDLER, (err: any) => { + throw err; + }); ng1Module.run(($rootScope: any) => { $rootScope.name = 'world'; @@ -409,8 +415,8 @@ withEachNg1Version(() => { } const actValue = changes[prop].currentValue; if (actValue != value) { - throw new Error( - `Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`); + throw new Error(`Expected changes record for'${prop}' to be '${ + value}' but was '${actValue}'`); } }; @@ -458,7 +464,7 @@ withEachNg1Version(() => { | modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}}; `); adapter.bootstrap(element, ['ng1']).ready((ref) => { - expect(multiTrim(document.body.textContent !)) + expect(multiTrim(document.body.textContent!)) .toEqual( 'ignore: -; ' + 'literal: Text; interpolate: Hello world; ' + @@ -466,7 +472,7 @@ withEachNg1Version(() => { 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); ref.ng1RootScope.$apply('name = "everyone"'); - expect(multiTrim(document.body.textContent !)) + expect(multiTrim(document.body.textContent!)) .toEqual( 'ignore: -; ' + 'literal: Text; interpolate: Hello everyone; ' + @@ -475,7 +481,6 @@ withEachNg1Version(() => { ref.dispose(); }); - })); it('should support two-way binding and event listener', async(() => { @@ -541,9 +546,9 @@ withEachNg1Version(() => { ngOnChangesCount = 0; firstChangesCount = 0; // TODO(issue/24571): remove '!'. - initialValue !: string; + initialValue!: string; // TODO(issue/24571): remove '!'. - @Input() foo !: string; + @Input() foo!: string; ngOnChanges(changes: SimpleChanges) { this.ngOnChangesCount++; @@ -590,7 +595,9 @@ withEachNg1Version(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); 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; @Component({selector: 'ng2', template: '{{_value}}'}) @@ -598,11 +605,21 @@ withEachNg1Version(() => { private _value: any = ''; private _onChangeCallback: (_: any) => void = () => {}; private _onTouchedCallback: () => void = () => {}; - constructor() { ng2Instance = this; } - writeValue(value: any) { this._value = value; } - registerOnChange(fn: any) { this._onChangeCallback = fn; } - registerOnTouched(fn: any) { this._onTouchedCallback = fn; } - doTouch() { this._onTouchedCallback(); } + constructor() { + ng2Instance = this; + } + writeValue(value: any) { + this._value = value; + } + registerOnChange(fn: any) { + this._onChangeCallback = fn; + } + registerOnTouched(fn: any) { + this._onTouchedCallback = fn; + } + doTouch() { + this._onTouchedCallback(); + } doChange(newValue: string) { this._value = newValue; this._onChangeCallback(newValue); @@ -653,14 +670,18 @@ withEachNg1Version(() => { return { template: '
', controller: function($rootScope: any, $timeout: Function) { - $timeout(() => { $rootScope.destroyIt = true; }); + $timeout(() => { + $rootScope.destroyIt = true; + }); } }; }); @Component({selector: 'ng2', template: 'test'}) class Ng2 { - ngOnDestroy() { onDestroyed.emit('destroyed'); } + ngOnDestroy() { + onDestroyed.emit('destroyed'); + } } @NgModule({ @@ -673,7 +694,9 @@ withEachNg1Version(() => { ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(''); 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'}) class Ng2InnerComponent implements OnDestroy { - ngOnDestroy() { destroyed = true; } + ngOnDestroy() { + destroyed = true; + } } @NgModule({ @@ -789,7 +814,7 @@ withEachNg1Version(() => { @Component({selector: 'ng2', template: 'ng2-{{ itemId }}()'}) class Ng2Component { // TODO(issue/24571): remove '!'. - @Input() itemId !: string; + @Input() itemId!: string; } @NgModule({imports: [BrowserModule], declarations: [Ng2Component]}) @@ -838,7 +863,7 @@ withEachNg1Version(() => { ng1Module.directive('rootComponent', adapter.downgradeNg2Component(RootComponent)); document.body.innerHTML = ''; - adapter.bootstrap(document.body.firstElementChild !, ['myExample']).ready((ref) => { + adapter.bootstrap(document.body.firstElementChild!, ['myExample']).ready((ref) => { expect(multiTrim(document.body.textContent)).toEqual('It works!'); ref.dispose(); }); @@ -868,7 +893,9 @@ withEachNg1Version(() => { dataA = 'foo'; dataB = 'bar'; - constructor() { ng2ComponentInstance = this; } + constructor() { + ng2ComponentInstance = this; + } } // Define `ng1Module` @@ -888,8 +915,8 @@ withEachNg1Version(() => { const element = html(``); adapter.bootstrap(element, ['ng1Module']).ready(ref => { - const ng1 = element.querySelector('ng1') !; - const ng1Controller = angular.element(ng1).controller !('ng1'); + const ng1 = element.querySelector('ng1')!; + const ng1Controller = angular.element(ng1).controller!('ng1'); expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar'); @@ -932,7 +959,9 @@ withEachNg1Version(() => { dataA = {value: 'foo'}; dataB = {value: 'bar'}; - constructor() { ng2ComponentInstance = this; } + constructor() { + ng2ComponentInstance = this; + } } // Define `ng1Module` @@ -952,8 +981,8 @@ withEachNg1Version(() => { const element = html(``); adapter.bootstrap(element, ['ng1Module']).ready(ref => { - const ng1 = element.querySelector('ng1') !; - const ng1Controller = angular.element(ng1).controller !('ng1'); + const ng1 = element.querySelector('ng1')!; + const ng1Controller = angular.element(ng1).controller!('ng1'); expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar'); @@ -996,7 +1025,9 @@ withEachNg1Version(() => { dataA = {value: 'foo'}; dataB = {value: 'bar'}; - constructor() { ng2ComponentInstance = this; } + constructor() { + ng2ComponentInstance = this; + } } // Define `ng1Module` @@ -1016,8 +1047,8 @@ withEachNg1Version(() => { const element = html(``); adapter.bootstrap(element, ['ng1Module']).ready(ref => { - const ng1 = element.querySelector('ng1') !; - const ng1Controller = angular.element(ng1).controller !('ng1'); + const ng1 = element.querySelector('ng1')!; + const ng1Controller = angular.element(ng1).controller!('ng1'); expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar'); @@ -1077,8 +1108,8 @@ withEachNg1Version(() => { const element = html(``); adapter.bootstrap(element, ['ng1Module']).ready(ref => { - const ng1 = element.querySelector('ng1') !; - const ng1Controller = angular.element(ng1).controller !('ng1'); + const ng1 = element.querySelector('ng1')!; + const ng1Controller = angular.element(ng1).controller!('ng1'); expect(multiTrim(element.textContent)).toBe('Inside: - | Outside: foo, bar'); @@ -1204,7 +1235,9 @@ withEachNg1Version(() => { restrict: 'E', template: '{{someText}} - Length: {{data.length}}', 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', template: '{{someText}} - Length: {{data.length}}', 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}`); }); - const ng1 = () => { return {templateUrl: 'url.html'}; }; + const ng1 = () => { + return {templateUrl: 'url.html'}; + }; ng1Module.directive('ng1', ng1); @Component({selector: 'ng2', template: ''}) class Ng2 { @@ -1320,7 +1357,13 @@ withEachNg1Version(() => { cbFn(200, `${method}:${url}`); }); - const ng1 = () => { return {templateUrl() { return 'url.html'; }}; }; + const ng1 = () => { + return { + templateUrl() { + return 'url.html'; + } + }; + }; ng1Module.directive('ng1', ng1); @Component({selector: 'ng2', template: ''}) class Ng2 { @@ -1345,7 +1388,9 @@ withEachNg1Version(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const ng1Module = angular.module_('ng1', []); - const ng1 = () => { return {template: ''}; }; + const ng1 = () => { + return {template: ''}; + }; ng1Module.directive('ng1', ng1); @Component({selector: 'ng2', template: ''}) @@ -1371,7 +1416,13 @@ withEachNg1Version(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const ng1Module = angular.module_('ng1', []); - const ng1 = () => { return {template() { return ''; }}; }; + const ng1 = () => { + return { + template() { + return ''; + } + }; + }; ng1Module.directive('ng1', ng1); @Component({selector: 'ng2', template: ''}) @@ -1398,7 +1449,9 @@ withEachNg1Version(() => { const ng1Module = angular.module_('ng1', []); 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); @Component({selector: 'ng2', template: ''}) @@ -1431,13 +1484,20 @@ withEachNg1Version(() => { '{{ctl.scope}}; {{ctl.isClass}}; {{ctl.hasElement}}; {{ctl.isPublished()}}', controllerAs: 'ctl', controller: class { - scope: any; hasElement: string; $element: any; isClass: any; + scope: any; + hasElement: string; + $element: any; + isClass: any; constructor($scope: any, $element: any) { this.verifyIAmAClass(); this.scope = $scope.$parent.$parent == $scope.$root ? 'scope' : 'wrong-scope'; this.hasElement = $element[0].nodeName; this.$element = $element; - } verifyIAmAClass() { this.isClass = 'isClass'; } isPublished() { + } + verifyIAmAClass() { + this.isClass = 'isClass'; + } + isPublished() { return this.$element.controller('ng1') == this ? 'published' : 'not-published'; } } @@ -1543,7 +1603,9 @@ withEachNg1Version(() => { template: '{{ctl.status}}', require: 'ng1', controllerAs: 'ctrl', - controller: class {status = 'WORKS';}, + controller: class { + status = 'WORKS'; + }, link: function(scope: any, element: any, attrs: any, linkController: any) { expect(scope.$root).toEqual($rootScope); expect(element[0].nodeName).toEqual('NG1'); @@ -1577,7 +1639,13 @@ withEachNg1Version(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const ng1Module = angular.module_('ng1', []); - const parent = () => { return {controller: class {parent = 'PARENT';}}; }; + const parent = () => { + return { + controller: class { + parent = 'PARENT'; + } + }; + }; const ng1 = () => { return { scope: {title: '@'}, @@ -1585,7 +1653,9 @@ withEachNg1Version(() => { template: '{{parent.parent}}:{{ng1.status}}', require: ['ng1', '^parent', '?^^notFound'], controllerAs: 'ctrl', - controller: class {status = 'WORKS';}, + controller: class { + status = 'WORKS'; + }, link: function(scope: any, element: any, attrs: any, linkControllers: any) { expect(linkControllers[0].status).toEqual('WORKS'); expect(linkControllers[1].parent).toEqual('PARENT'); @@ -1633,16 +1703,21 @@ withEachNg1Version(() => { scope: {}, bindToController: true, controllerAs: '$ctrl', - controller: class {$onInit() { $onInitSpyA(); }} + controller: class { + $onInit() { + $onInitSpyA(); + } + } + })) + .directive('ng1B', () => ({ + template: '', + scope: {}, + bindToController: false, + controllerAs: '$ctrl', + controller: function(this: any) { + this.$onInit = $onInitSpyB; + } })) - .directive( - 'ng1B', () => ({ - template: '', - scope: {}, - bindToController: false, - controllerAs: '$ctrl', - controller: function(this: any) { this.$onInit = $onInitSpyB; } - })) .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); @NgModule({ @@ -1718,7 +1793,9 @@ withEachNg1Version(() => { @Component({selector: 'ng2', template: ' | '}) class Ng2Component { - constructor(cd: ChangeDetectorRef) { changeDetector = cd; } + constructor(cd: ChangeDetectorRef) { + changeDetector = cd; + } } angular.module_('ng1', []) @@ -1727,16 +1804,21 @@ withEachNg1Version(() => { scope: {}, bindToController: true, controllerAs: '$ctrl', - controller: class {$doCheck() { $doCheckSpyA(); }} + controller: class { + $doCheck() { + $doCheckSpyA(); + } + } + })) + .directive('ng1B', () => ({ + template: '', + scope: {}, + bindToController: false, + controllerAs: '$ctrl', + controller: function(this: any) { + this.$doCheck = $doCheckSpyB; + } })) - .directive( - 'ng1B', () => ({ - template: '', - scope: {}, - bindToController: false, - controllerAs: '$ctrl', - controller: function(this: any) { this.$doCheck = $doCheckSpyB; } - })) .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); @NgModule({ @@ -1773,7 +1855,9 @@ withEachNg1Version(() => { @Component({selector: 'ng2', template: ' | '}) class Ng2Component { - constructor(cd: ChangeDetectorRef) { changeDetector = cd; } + constructor(cd: ChangeDetectorRef) { + changeDetector = cd; + } } angular.module_('ng1', []) @@ -1835,16 +1919,21 @@ withEachNg1Version(() => { scope: {}, bindToController: true, controllerAs: '$ctrl', - controller: class {$postLink() { $postLinkSpyA(); }} + controller: class { + $postLink() { + $postLinkSpyA(); + } + } + })) + .directive('ng1B', () => ({ + template: '', + scope: {}, + bindToController: false, + controllerAs: '$ctrl', + controller: function(this: any) { + this.$postLink = $postLinkSpyB; + } })) - .directive( - 'ng1B', () => ({ - template: '', - scope: {}, - bindToController: false, - controllerAs: '$ctrl', - controller: function(this: any) { this.$postLink = $postLinkSpyB; } - })) .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); @NgModule({ @@ -1924,7 +2013,9 @@ withEachNg1Version(() => { template: ' | ' }) class Ng2Component { - constructor() { ng2Instance = this; } + constructor() { + ng2Instance = this; + } } angular.module_('ng1', []) @@ -1937,17 +2028,17 @@ withEachNg1Version(() => { this.$onChanges = $onChangesControllerSpyA; } })) - .directive( - 'ng1B', - () => ({ - template: '', - scope: {valB: '<'}, - bindToController: false, - controllerAs: '$ctrl', - controller: class { - $onChanges(changes: SimpleChanges) { $onChangesControllerSpyB(changes); } - } - })) + .directive('ng1B', () => ({ + template: '', + scope: {valB: '<'}, + bindToController: false, + controllerAs: '$ctrl', + controller: class { + $onChanges(changes: SimpleChanges) { + $onChangesControllerSpyB(changes); + } + } + })) .directive('ng2', adapter.downgradeNg2Component(Ng2Component)) .run(($rootScope: angular.IRootScopeService) => { Object.getPrototypeOf($rootScope).$onChanges = $onChangesScopeSpy; @@ -2022,7 +2113,9 @@ withEachNg1Version(() => { }) class Ng2Component { ng2Destroy: boolean = false; - constructor() { ng2ComponentInstance = this; } + constructor() { + ng2ComponentInstance = this; + } } // On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3), @@ -2036,16 +2129,21 @@ withEachNg1Version(() => { scope: {}, bindToController: true, controllerAs: '$ctrl', - controller: class {$onDestroy() { $onDestroySpyA(); }} + controller: class { + $onDestroy() { + $onDestroySpyA(); + } + } + })) + .directive('ng1B', () => ({ + template: '', + scope: {}, + bindToController: false, + controllerAs: '$ctrl', + controller: function(this: any) { + this.$onDestroy = $onDestroySpyB; + } })) - .directive( - 'ng1B', () => ({ - template: '', - scope: {}, - bindToController: false, - controllerAs: '$ctrl', - controller: function(this: any) { this.$onDestroy = $onDestroySpyB; } - })) .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); @NgModule({ @@ -2112,7 +2210,9 @@ withEachNg1Version(() => { }) class Ng2Component { ng2Destroy: boolean = false; - constructor() { ng2ComponentInstance = this; } + constructor() { + ng2ComponentInstance = this; + } } // On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3), @@ -2187,7 +2287,9 @@ withEachNg1Version(() => { @Component({selector: 'ng2', template: '
'}) class Ng2Component { ng2Destroy: boolean = false; - constructor() { ng2ComponentInstance = this; } + constructor() { + ng2ComponentInstance = this; + } } // On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3), @@ -2233,7 +2335,9 @@ withEachNg1Version(() => { @Component({selector: 'ng2', template: '
'}) class Ng2Component { ng2Destroy: boolean = false; - constructor() { ng2ComponentInstance = this; } + constructor() { + ng2ComponentInstance = this; + } } // On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3), @@ -2245,8 +2349,8 @@ withEachNg1Version(() => { .component('ng1', { controller: class { constructor(private $element: angular.IAugmentedJQuery) {} $onInit() { - this.$element.on !('$destroy', elementDestroyListener); - this.$element.contents !().on !('$destroy', descendantDestroyListener); + this.$element.on!('$destroy', elementDestroyListener); + this.$element.contents!().on!('$destroy', descendantDestroyListener); } }, template: '
' @@ -2287,8 +2391,8 @@ withEachNg1Version(() => { const ng1Component: angular.IComponent = { controller: class { constructor(private $element: angular.IAugmentedJQuery) {} $onInit() { - this.$element.data !('test', 1); - this.$element.contents !().data !('test', 2); + this.$element.data!('test', 1); + this.$element.contents!().data!('test', 2); ng1ComponentElement = this.$element; } @@ -2301,7 +2405,9 @@ withEachNg1Version(() => { class Ng2ComponentA { destroyIt = false; - constructor() { ng2ComponentAInstance = this; } + constructor() { + ng2ComponentAInstance = this; + } } @Component({selector: 'ng2B', template: ''}) @@ -2330,15 +2436,15 @@ withEachNg1Version(() => { const $rootScope = ref.ng1RootScope as any; tick(); $rootScope.$digest(); - expect(ng1ComponentElement.data !('test')).toBe(1); - expect(ng1ComponentElement.contents !().data !('test')).toBe(2); + expect(ng1ComponentElement.data!('test')).toBe(1); + expect(ng1ComponentElement.contents!().data!('test')).toBe(2); ng2ComponentAInstance.destroyIt = true; tick(); $rootScope.$digest(); - expect(ng1ComponentElement.data !('test')).toBeUndefined(); - expect(ng1ComponentElement.contents !().data !('test')).toBeUndefined(); + expect(ng1ComponentElement.data!('test')).toBeUndefined(); + expect(ng1ComponentElement.contents!().data!('test')).toBeUndefined(); }); })); @@ -2353,10 +2459,10 @@ withEachNg1Version(() => { const ng1Component: angular.IComponent = { controller: class { constructor(private $element: angular.IAugmentedJQuery) {} $onInit() { - ng1DescendantElement = this.$element.contents !(); + ng1DescendantElement = this.$element.contents!(); - this.$element.on !('click', elementClickListener); - ng1DescendantElement.on !('click', descendantClickListener); + this.$element.on!('click', elementClickListener); + ng1DescendantElement.on!('click', descendantClickListener); } }, template: '
' @@ -2367,7 +2473,9 @@ withEachNg1Version(() => { class Ng2ComponentA { destroyIt = false; - constructor() { ng2ComponentAInstance = this; } + constructor() { + ng2ComponentAInstance = this; + } } @Component({selector: 'ng2B', template: ''}) @@ -2420,7 +2528,11 @@ withEachNg1Version(() => { const ng1Directive: angular.IDirective = { template: '', link: {pre: () => log.push('ng1-pre')}, - controller: class {constructor() { log.push('ng1-ctrl'); }} + controller: class { + constructor() { + log.push('ng1-ctrl'); + } + } }; // Define `Ng2Component` @@ -2577,7 +2689,11 @@ withEachNg1Version(() => { const ng1Directive: angular.IDirective = { template: '', link: () => log.push('ng1-post'), - controller: class {$postLink() { log.push('ng1-$post'); }} + controller: class { + $postLink() { + log.push('ng1-$post'); + } + } }; // Define `Ng2Component` @@ -2627,13 +2743,17 @@ withEachNg1Version(() => { class Ng2ComponentA { value = 'foo'; showB = false; - constructor() { ng2ComponentAInstance = this; } + constructor() { + ng2ComponentAInstance = this; + } } @Component({selector: 'ng2B', template: 'ng2B({{ value }})'}) class Ng2ComponentB { value = 'bar'; - constructor() { ng2ComponentBInstance = this; } + constructor() { + ng2ComponentBInstance = this; + } } // Define `ng1Module` @@ -2678,7 +2798,10 @@ withEachNg1Version(() => { template: 'ng1(
{{ $ctrl.value }}
)', transclude: true, controller: class { - value = 'from-ng1'; constructor() { ng1ControllerInstances.push(this); } + value = 'from-ng1'; + constructor() { + ng1ControllerInstances.push(this); + } } }; @@ -2697,7 +2820,9 @@ withEachNg1Version(() => { }) class Ng2Component { value = 'from-ng2'; - constructor() { ng2ComponentInstance = this; } + constructor() { + ng2ComponentInstance = this; + } } // Define `ng1Module` @@ -2756,7 +2881,9 @@ withEachNg1Version(() => { class Ng2Component { x = 'foo'; y = 'bar'; - constructor() { ng2ComponentInstance = this; } + constructor() { + ng2ComponentInstance = this; + } } // Define `ng1Module` @@ -2798,8 +2925,12 @@ withEachNg1Version(() => { const ng1Component: angular.IComponent = { template: 'ng1(default(
fallback-{{ $ctrl.value }}
))', transclude: {slotX: 'contentX', slotY: 'contentY'}, - controller: - class {value = 'ng1'; constructor() { ng1ControllerInstances.push(this); }} + controller: class { + value = 'ng1'; + constructor() { + ng1ControllerInstances.push(this); + } + } }; // Define `Ng2Component` @@ -2830,7 +2961,9 @@ withEachNg1Version(() => { class Ng2Component { x = 'foo'; y = 'bar'; - constructor() { ng2ComponentInstance = this; } + constructor() { + ng2ComponentInstance = this; + } } // Define `ng1Module` @@ -2880,7 +3013,11 @@ withEachNg1Version(() => { )`, transclude: {slotX: '?contentX', slotY: '?contentY'}, 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 { x = 'ng2X'; y = 'ng2Y'; - constructor() { ng2ComponentInstance = this; } + constructor() { + ng2ComponentInstance = this; + } } // Define `ng1Module` @@ -3000,7 +3139,9 @@ withEachNg1Version(() => { x = 'foo'; y = 'bar'; show = true; - constructor() { ng2ComponentInstance = this; } + constructor() { + ng2ComponentInstance = this; + } } // Define `ng1Module` @@ -3202,13 +3343,18 @@ withEachNg1Version(() => { const ng1Module = angular.module_('ng1', []); let a1Injector: angular.IInjectorService|undefined; ng1Module.run([ - '$injector', function($injector: angular.IInjectorService) { a1Injector = $injector; } + '$injector', + function($injector: angular.IInjectorService) { + a1Injector = $injector; + } ]); const element = html('
'); 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); @@ -3275,7 +3421,7 @@ withEachNg1Version(() => { document.body.innerHTML = 'project'; - adapter.bootstrap(document.body.firstElementChild !, ['myExample']).ready((ref) => { + adapter.bootstrap(document.body.firstElementChild!, ['myExample']).ready((ref) => { expect(multiTrim(document.body.textContent)) .toEqual('ng2[ng1[Hello World!](transclude)](project)'); ref.dispose(); diff --git a/packages/zone.js/test/extra/bluebird.spec.ts b/packages/zone.js/test/extra/bluebird.spec.ts index f88fcc9ece..a7db2845ee 100644 --- a/packages/zone.js/test/extra/bluebird.spec.ts +++ b/packages/zone.js/test/extra/bluebird.spec.ts @@ -284,11 +284,7 @@ describe('bluebird promise', () => { .each( BluebirdPromise.map(arr, (item: number) => BluebirdPromise.resolve(item)), (r: number, idx: number) => { -<<<<<<< HEAD expect(r).toBe(arr[idx]); -======= - expect(r === arr[idx]).toBeTrue(); ->>>>>>> 253023848d... build: update jasmine to 3.5 expect(Zone.current.name).toEqual('bluebird'); }) .then((r: any) => { @@ -309,11 +305,7 @@ describe('bluebird promise', () => { .mapSeries( BluebirdPromise.map(arr, (item: number) => BluebirdPromise.resolve(item)), (r: number, idx: number) => { -<<<<<<< HEAD expect(r).toBe(arr[idx]); -======= - expect(r === arr[idx]).toBeTrue(); ->>>>>>> 253023848d... build: update jasmine to 3.5 expect(Zone.current.name).toEqual('bluebird'); }) .then((r: any) => { diff --git a/packages/zone.js/test/node/crypto.spec.ts b/packages/zone.js/test/node/crypto.spec.ts index 3c38d484e7..c09205049f 100644 --- a/packages/zone.js/test/node/crypto.spec.ts +++ b/packages/zone.js/test/node/crypto.spec.ts @@ -21,7 +21,9 @@ describe('crypto test', () => { const zoneASpec = { name: 'A', 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); spyOn(zoneASpec, 'onScheduleTask').and.callThrough(); @@ -44,7 +46,9 @@ describe('crypto test', () => { const zoneASpec = { name: 'A', 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); spyOn(zoneASpec, 'onScheduleTask').and.callThrough();