@@ -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
`;
- 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: '