// args passed to router.navigateByUrl() spy
const spy = router.navigateByUrl as jasmine.Spy;
@@ -139,10 +131,8 @@ function tests(heroClick: () => void) {
// expecting to navigate to id of the component's first hero
const id = comp.heroes[0].id;
- expect(navArgs).toBe('/heroes/' + id,
- 'should nav to HeroDetail for first hero');
+ expect(navArgs).toBe('/heroes/' + id, 'should nav to HeroDetail for first hero');
});
// #enddocregion navigate-test
});
}
-
diff --git a/aio/content/examples/testing/src/app/demo/async-helper.spec.ts b/aio/content/examples/testing/src/app/demo/async-helper.spec.ts
index c96954d042..18a793d1a6 100644
--- a/aio/content/examples/testing/src/app/demo/async-helper.spec.ts
+++ b/aio/content/examples/testing/src/app/demo/async-helper.spec.ts
@@ -1,18 +1,23 @@
// tslint:disable-next-line:no-unused-variable
-import { async, fakeAsync, tick } from '@angular/core/testing';
+import { fakeAsync, tick, waitForAsync } from '@angular/core/testing';
import { interval, of } from 'rxjs';
import { delay, take } from 'rxjs/operators';
describe('Angular async helper', () => {
-
describe('async', () => {
let actuallyDone = false;
- beforeEach(() => { actuallyDone = false; });
+ beforeEach(() => {
+ actuallyDone = false;
+ });
- afterEach(() => { expect(actuallyDone).toBe(true, 'actuallyDone should be true'); });
+ afterEach(() => {
+ expect(actuallyDone).toBe(true, 'actuallyDone should be true');
+ });
- it('should run normal test', () => { actuallyDone = true; });
+ it('should run normal test', () => {
+ actuallyDone = true;
+ });
it('should run normal async test', (done: DoneFn) => {
setTimeout(() => {
@@ -21,39 +26,50 @@ describe('Angular async helper', () => {
}, 0);
});
- it('should run async test with task',
- async(() => { setTimeout(() => { actuallyDone = true; }, 0); }));
+ it('should run async test with task', waitForAsync(() => {
+ setTimeout(() => {
+ actuallyDone = true;
+ }, 0);
+ }));
- it('should run async test with task', async(() => {
+ it('should run async test with task', waitForAsync(() => {
const id = setInterval(() => {
actuallyDone = true;
clearInterval(id);
}, 100);
}));
- it('should run async test with successful promise', async(() => {
- const p = new Promise(resolve => { setTimeout(resolve, 10); });
- p.then(() => { actuallyDone = true; });
+ it('should run async test with successful promise', waitForAsync(() => {
+ const p = new Promise(resolve => {
+ setTimeout(resolve, 10);
+ });
+ p.then(() => {
+ actuallyDone = true;
+ });
}));
- it('should run async test with failed promise', async(() => {
- const p = new Promise((resolve, reject) => { setTimeout(reject, 10); });
- p.catch(() => { actuallyDone = true; });
+ it('should run async test with failed promise', waitForAsync(() => {
+ const p = new Promise((resolve, reject) => {
+ setTimeout(reject, 10);
+ });
+ p.catch(() => {
+ actuallyDone = true;
+ });
}));
// Use done. Can also use async or fakeAsync.
it('should run async test with successful delayed Observable', (done: DoneFn) => {
- const source = of (true).pipe(delay(10));
+ const source = of(true).pipe(delay(10));
source.subscribe(val => actuallyDone = true, err => fail(err), done);
});
- it('should run async test with successful delayed Observable', async(() => {
- const source = of (true).pipe(delay(10));
+ it('should run async test with successful delayed Observable', waitForAsync(() => {
+ const source = of(true).pipe(delay(10));
source.subscribe(val => actuallyDone = true, err => fail(err));
}));
it('should run async test with successful delayed Observable', fakeAsync(() => {
- const source = of (true).pipe(delay(10));
+ const source = of(true).pipe(delay(10));
source.subscribe(val => actuallyDone = true, err => fail(err));
tick(10);
@@ -64,7 +80,9 @@ describe('Angular async helper', () => {
// #docregion fake-async-test-tick
it('should run timeout callback with delay after call tick with millis', fakeAsync(() => {
let called = false;
- setTimeout(() => { called = true; }, 100);
+ setTimeout(() => {
+ called = true;
+ }, 100);
tick(100);
expect(called).toBe(true);
}));
@@ -73,7 +91,9 @@ describe('Angular async helper', () => {
// #docregion fake-async-test-tick-new-macro-task-sync
it('should run new macro task callback with delay after call tick with millis',
fakeAsync(() => {
- function nestedTimer(cb: () => any): void { setTimeout(() => setTimeout(() => cb())); }
+ function nestedTimer(cb: () => any): void {
+ setTimeout(() => setTimeout(() => cb()));
+ }
const callback = jasmine.createSpy('callback');
nestedTimer(callback);
expect(callback).not.toHaveBeenCalled();
@@ -86,7 +106,9 @@ describe('Angular async helper', () => {
// #docregion fake-async-test-tick-new-macro-task-async
it('should not run new macro task callback with delay after call tick with millis',
fakeAsync(() => {
- function nestedTimer(cb: () => any): void { setTimeout(() => setTimeout(() => cb())); }
+ function nestedTimer(cb: () => any): void {
+ setTimeout(() => setTimeout(() => cb()));
+ }
const callback = jasmine.createSpy('callback');
nestedTimer(callback);
expect(callback).not.toHaveBeenCalled();
@@ -112,7 +134,9 @@ describe('Angular async helper', () => {
// need to add `import 'zone.js/dist/zone-patch-rxjs-fake-async'
// to patch rxjs scheduler
let result = null;
- of ('hello').pipe(delay(1000)).subscribe(v => { result = v; });
+ of('hello').pipe(delay(1000)).subscribe(v => {
+ result = v;
+ });
expect(result).toBeNull();
tick(1000);
expect(result).toBe('hello');
@@ -133,12 +157,18 @@ describe('Angular async helper', () => {
describe('use jasmine.clock()', () => {
// need to config __zone_symbol__fakeAsyncPatchLock flag
// before loading zone.js/dist/zone-testing
- beforeEach(() => { jasmine.clock().install(); });
- afterEach(() => { jasmine.clock().uninstall(); });
+ beforeEach(() => {
+ jasmine.clock().install();
+ });
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
it('should auto enter fakeAsync', () => {
// is in fakeAsync now, don't need to call fakeAsync(testFn)
let called = false;
- setTimeout(() => { called = true; }, 100);
+ setTimeout(() => {
+ called = true;
+ }, 100);
jasmine.clock().tick(100);
expect(called).toBe(true);
});
@@ -152,7 +182,7 @@ describe('Angular async helper', () => {
}
// need to config __zone_symbol__supportWaitUnResolvedChainedPromise flag
// before loading zone.js/dist/zone-testing
- it('should wait until promise.then is called', async(() => {
+ it('should wait until promise.then is called', waitForAsync(() => {
let finished = false;
new Promise((res, rej) => {
jsonp('localhost:8080/jsonp', () => {
@@ -168,5 +198,4 @@ describe('Angular async helper', () => {
}));
});
// #enddocregion async-test-promise-then
-
});
diff --git a/aio/content/examples/testing/src/app/demo/demo.testbed.spec.ts b/aio/content/examples/testing/src/app/demo/demo.testbed.spec.ts
index cb5b20ea6a..a6bbd1aafc 100644
--- a/aio/content/examples/testing/src/app/demo/demo.testbed.spec.ts
+++ b/aio/content/examples/testing/src/app/demo/demo.testbed.spec.ts
@@ -24,17 +24,18 @@ import { FormsModule } from '@angular/forms';
// Forms symbols imported only for a specific test below
import { NgModel, NgControl } from '@angular/forms';
-import { async, ComponentFixture, fakeAsync, inject, TestBed, tick
+import {
+ ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync
} from '@angular/core/testing';
import { addMatchers, newEvent, click } from '../../testing';
-export class NotProvided extends ValueService { /* example below */}
-beforeEach( addMatchers );
+export class NotProvided extends ValueService { /* example below */ }
+beforeEach(addMatchers);
describe('demo (with TestBed):', () => {
-//////// Service Tests /////////////
+ //////// Service Tests /////////////
// #docregion ValueService
describe('ValueService', () => {
@@ -64,13 +65,13 @@ describe('demo (with TestBed):', () => {
// #enddocregion testbed-get-w-null
});
- it('test should wait for ValueService.getPromiseValue', async(() => {
+ it('test should wait for ValueService.getPromiseValue', waitForAsync(() => {
service.getPromiseValue().then(
value => expect(value).toBe('promise value')
);
}));
- it('test should wait for ValueService.getObservableValue', async(() => {
+ it('test should wait for ValueService.getObservableValue', waitForAsync(() => {
service.getObservableValue().subscribe(
value => expect(value).toBe('observable value')
);
@@ -150,7 +151,7 @@ describe('demo (with TestBed):', () => {
TestBed.configureTestingModule({ providers: [ValueService] });
});
- beforeEach(async(inject([ValueService], (service: ValueService) => {
+ beforeEach(waitForAsync(inject([ValueService], (service: ValueService) => {
service.getPromiseValue().then(value => serviceValue = value);
})));
@@ -159,11 +160,11 @@ describe('demo (with TestBed):', () => {
});
});
-/////////// Component Tests //////////////////
+ /////////// Component Tests //////////////////
describe('TestBed component tests', () => {
- beforeEach(async(() => {
+ beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
imports: [DemoModule],
@@ -235,7 +236,7 @@ describe('demo (with TestBed):', () => {
// #docregion ButtonComp
it('should support clicking a button', () => {
const fixture = TestBed.createComponent(LightswitchComponent);
- const btn = fixture.debugElement.query(By.css('button'));
+ const btn = fixture.debugElement.query(By.css('button'));
const span = fixture.debugElement.query(By.css('span')).nativeElement;
fixture.detectChanges();
@@ -248,7 +249,7 @@ describe('demo (with TestBed):', () => {
// #enddocregion ButtonComp
// ngModel is async so we must wait for it with promise-based `whenStable`
- it('should support entering text in input box (ngModel)', async(() => {
+ it('should support entering text in input box (ngModel)', waitForAsync(() => {
const expectedOrigName = 'John';
const expectedNewName = 'Sally';
@@ -278,10 +279,10 @@ describe('demo (with TestBed):', () => {
input.dispatchEvent(newEvent('input'));
return fixture.whenStable();
})
- .then(() => {
- expect(comp.name).toBe(expectedNewName,
- `After ngModel updates the model, comp.name should be ${expectedNewName} `);
- });
+ .then(() => {
+ expect(comp.name).toBe(expectedNewName,
+ `After ngModel updates the model, comp.name should be ${expectedNewName} `);
+ });
}));
// fakeAsync version of ngModel input test enables sync test style
@@ -327,9 +328,9 @@ describe('demo (with TestBed):', () => {
const fixture = TestBed.createComponent(ReversePipeComponent);
fixture.detectChanges();
- const comp = fixture.componentInstance;
+ const comp = fixture.componentInstance;
const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement;
- const span = fixture.debugElement.query(By.css('span')).nativeElement as HTMLElement;
+ const span = fixture.debugElement.query(By.css('span')).nativeElement as HTMLElement;
// simulate user entering new name in input
input.value = inputText;
@@ -381,12 +382,12 @@ describe('demo (with TestBed):', () => {
expect(el.styles.color).toBe(comp.color, 'color style');
expect(el.styles.width).toBe(comp.width + 'px', 'width style');
- // #enddocregion dom-attributes
+ // #enddocregion dom-attributes
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
// expect(el.properties['customProperty']).toBe(true, 'customProperty');
- // #docregion dom-attributes
+ // #docregion dom-attributes
});
// #enddocregion dom-attributes
@@ -400,10 +401,10 @@ describe('demo (with TestBed):', () => {
const fixture = TestBed.configureTestingModule({
declarations: [Child1Component],
})
- .overrideComponent(Child1Component, {
- set: { template: 'Fake' }
- })
- .createComponent(Child1Component);
+ .overrideComponent(Child1Component, {
+ set: { template: 'Fake' }
+ })
+ .createComponent(Child1Component);
fixture.detectChanges();
expect(fixture).toHaveText('Fake');
@@ -413,14 +414,14 @@ describe('demo (with TestBed):', () => {
const fixture = TestBed.configureTestingModule({
declarations: [TestProvidersComponent],
})
- .overrideComponent(TestProvidersComponent, {
- remove: { providers: [ValueService]},
- add: { providers: [{ provide: ValueService, useClass: FakeValueService }] },
+ .overrideComponent(TestProvidersComponent, {
+ remove: { providers: [ValueService] },
+ add: { providers: [{ provide: ValueService, useClass: FakeValueService }] },
- // Or replace them all (this component has only one provider)
- // set: { providers: [{ provide: ValueService, useClass: FakeValueService }] },
- })
- .createComponent(TestProvidersComponent);
+ // Or replace them all (this component has only one provider)
+ // set: { providers: [{ provide: ValueService, useClass: FakeValueService }] },
+ })
+ .createComponent(TestProvidersComponent);
fixture.detectChanges();
expect(fixture).toHaveText('injected value: faked value', 'text');
@@ -436,14 +437,14 @@ describe('demo (with TestBed):', () => {
const fixture = TestBed.configureTestingModule({
declarations: [TestViewProvidersComponent],
})
- .overrideComponent(TestViewProvidersComponent, {
- // remove: { viewProviders: [ValueService]},
- // add: { viewProviders: [{ provide: ValueService, useClass: FakeValueService }] },
+ .overrideComponent(TestViewProvidersComponent, {
+ // remove: { viewProviders: [ValueService]},
+ // add: { viewProviders: [{ provide: ValueService, useClass: FakeValueService }] },
- // Or replace them all (this component has only one viewProvider)
- set: { viewProviders: [{ provide: ValueService, useClass: FakeValueService }] },
- })
- .createComponent(TestViewProvidersComponent);
+ // Or replace them all (this component has only one viewProvider)
+ set: { viewProviders: [{ provide: ValueService, useClass: FakeValueService }] },
+ })
+ .createComponent(TestViewProvidersComponent);
fixture.detectChanges();
expect(fixture).toHaveText('injected value: faked value');
@@ -453,20 +454,20 @@ describe('demo (with TestBed):', () => {
// TestComponent is parent of TestProvidersComponent
@Component({ template: '' })
- class TestComponent {}
+ class TestComponent { }
// 3 levels of ValueService provider: module, TestCompomponent, TestProvidersComponent
const fixture = TestBed.configureTestingModule({
declarations: [TestComponent, TestProvidersComponent],
- providers: [ValueService]
+ providers: [ValueService]
})
- .overrideComponent(TestComponent, {
- set: { providers: [{ provide: ValueService, useValue: {} }] }
- })
- .overrideComponent(TestProvidersComponent, {
- set: { providers: [{ provide: ValueService, useClass: FakeValueService }] }
- })
- .createComponent(TestComponent);
+ .overrideComponent(TestComponent, {
+ set: { providers: [{ provide: ValueService, useValue: {} }] }
+ })
+ .overrideComponent(TestProvidersComponent, {
+ set: { providers: [{ provide: ValueService, useClass: FakeValueService }] }
+ })
+ .createComponent(TestComponent);
let testBedProvider: ValueService;
let tcProvider: ValueService;
@@ -489,10 +490,10 @@ describe('demo (with TestBed):', () => {
const fixture = TestBed.configureTestingModule({
declarations: [ShellComponent, NeedsContentComponent, Child1Component, Child2Component, Child3Component],
})
- .overrideComponent(ShellComponent, {
- set: {
- selector: 'test-shell',
- template: `
+ .overrideComponent(ShellComponent, {
+ set: {
+ selector: 'test-shell',
+ template: `
@@ -501,9 +502,9 @@ describe('demo (with TestBed):', () => {
!
`
- }
- })
- .createComponent(ShellComponent);
+ }
+ })
+ .createComponent(ShellComponent);
fixture.detectChanges();
@@ -615,7 +616,7 @@ describe('demo (with TestBed):', () => {
});
// must be async test to see child flow to parent
- it('changed child value flows to parent', async(() => {
+ it('changed child value flows to parent', waitForAsync(() => {
fixture.detectChanges();
getChild();
@@ -625,14 +626,14 @@ describe('demo (with TestBed):', () => {
// Wait one JS engine turn!
setTimeout(() => resolve(), 0);
})
- .then(() => {
- fixture.detectChanges();
+ .then(() => {
+ fixture.detectChanges();
- expect(child.ngOnChangesCounter).toBe(2,
- 'expected 2 changes: initial value and changed value');
- expect(parent.parentValue).toBe('bar',
- 'parentValue should eq changed parent value');
- });
+ expect(child.ngOnChangesCounter).toBe(2,
+ 'expected 2 changes: initial value and changed value');
+ expect(parent.parentValue).toBe('bar',
+ 'parentValue should eq changed parent value');
+ });
}));
diff --git a/aio/content/examples/testing/src/app/hero/hero-detail.component.spec.ts b/aio/content/examples/testing/src/app/hero/hero-detail.component.spec.ts
index 9fc287d137..0015c96d52 100644
--- a/aio/content/examples/testing/src/app/hero/hero-detail.component.spec.ts
+++ b/aio/content/examples/testing/src/app/hero/hero-detail.component.spec.ts
@@ -1,8 +1,5 @@
// #docplaster
-import {
- async, ComponentFixture, fakeAsync, inject, TestBed, tick
-} from '@angular/core/testing';
-
+import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { Router } from '@angular/router';
import {
@@ -36,59 +33,54 @@ describe('HeroDetailComponent', () => {
function overrideSetup() {
// #docregion hds-spy
class HeroDetailServiceSpy {
- testHero: Hero = {id: 42, name: 'Test Hero' };
+ testHero: Hero = {id: 42, name: 'Test Hero'};
/* emit cloned test hero */
getHero = jasmine.createSpy('getHero').and.callFake(
- () => asyncData(Object.assign({}, this.testHero))
- );
+ () => asyncData(Object.assign({}, this.testHero)));
/* emit clone of test hero, with changes merged in */
- saveHero = jasmine.createSpy('saveHero').and.callFake(
- (hero: Hero) => asyncData(Object.assign(this.testHero, hero))
- );
+ saveHero = jasmine.createSpy('saveHero')
+ .and.callFake((hero: Hero) => asyncData(Object.assign(this.testHero, hero)));
}
// #enddocregion hds-spy
// the `id` value is irrelevant because ignored by service stub
- beforeEach(() => activatedRoute.setParamMap({ id: 99999 }));
+ beforeEach(() => activatedRoute.setParamMap({id: 99999}));
// #docregion setup-override
- beforeEach(async(() => {
+ beforeEach(waitForAsync(() => {
const routerSpy = createRouterSpy();
- TestBed.configureTestingModule({
- imports: [ HeroModule ],
- providers: [
- { provide: ActivatedRoute, useValue: activatedRoute },
- { provide: Router, useValue: routerSpy},
+ TestBed
+ .configureTestingModule({
+ imports: [HeroModule],
+ providers: [
+ {provide: ActivatedRoute, useValue: activatedRoute},
+ {provide: Router, useValue: routerSpy},
// #enddocregion setup-override
- // HeroDetailService at this level is IRRELEVANT!
- { provide: HeroDetailService, useValue: {} }
+ // HeroDetailService at this level is IRRELEVANT!
+ {provide: HeroDetailService, useValue: {}}
// #docregion setup-override
- ]
- })
+ ]
+ })
- // Override component's own provider
- // #docregion override-component-method
- .overrideComponent(HeroDetailComponent, {
- set: {
- providers: [
- { provide: HeroDetailService, useClass: HeroDetailServiceSpy }
- ]
- }
- })
- // #enddocregion override-component-method
+ // Override component's own provider
+ // #docregion override-component-method
+ .overrideComponent(
+ HeroDetailComponent,
+ {set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]}})
+ // #enddocregion override-component-method
- .compileComponents();
+ .compileComponents();
}));
// #enddocregion setup-override
// #docregion override-tests
let hdsSpy: HeroDetailServiceSpy;
- beforeEach(async(() => {
+ beforeEach(waitForAsync(() => {
createComponent();
// get the component's injected HeroDetailServiceSpy
hdsSpy = fixture.debugElement.injector.get(HeroDetailService) as any;
@@ -103,33 +95,32 @@ function overrideSetup() {
});
it('should save stub hero change', fakeAsync(() => {
- const origName = hdsSpy.testHero.name;
- const newName = 'New Name';
+ const origName = hdsSpy.testHero.name;
+ const newName = 'New Name';
- page.nameInput.value = newName;
- page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
+ page.nameInput.value = newName;
+ page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
- expect(component.hero.name).toBe(newName, 'component hero has new name');
- expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save');
+ expect(component.hero.name).toBe(newName, 'component hero has new name');
+ expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save');
- click(page.saveBtn);
- expect(hdsSpy.saveHero.calls.count()).toBe(1, 'saveHero called once');
+ click(page.saveBtn);
+ expect(hdsSpy.saveHero.calls.count()).toBe(1, 'saveHero called once');
- tick(); // wait for async save to complete
- expect(hdsSpy.testHero.name).toBe(newName, 'service hero has new name after save');
- expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
- }));
+ tick(); // wait for async save to complete
+ expect(hdsSpy.testHero.name).toBe(newName, 'service hero has new name after save');
+ expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
+ }));
// #enddocregion override-tests
it('fixture injected service is not the component injected service',
- // inject gets the service from the fixture
- inject([HeroDetailService], (fixtureService: HeroDetailService) => {
+ // inject gets the service from the fixture
+ inject([HeroDetailService], (fixtureService: HeroDetailService) => {
+ // use `fixture.debugElement.injector` to get service from component
+ const componentService = fixture.debugElement.injector.get(HeroDetailService);
- // use `fixture.debugElement.injector` to get service from component
- const componentService = fixture.debugElement.injector.get(HeroDetailService);
-
- expect(fixtureService).not.toBe(componentService, 'service injected from fixture');
- }));
+ expect(fixtureService).not.toBe(componentService, 'service injected from fixture');
+ }));
}
////////////////////
@@ -139,21 +130,22 @@ const firstHero = getTestHeroes()[0];
function heroModuleSetup() {
// #docregion setup-hero-module
- beforeEach(async(() => {
+ beforeEach(waitForAsync(() => {
const routerSpy = createRouterSpy();
- TestBed.configureTestingModule({
- imports: [ HeroModule ],
- // #enddocregion setup-hero-module
- // declarations: [ HeroDetailComponent ], // NO! DOUBLE DECLARATION
- // #docregion setup-hero-module
- providers: [
- { provide: ActivatedRoute, useValue: activatedRoute },
- { provide: HeroService, useClass: TestHeroService },
- { provide: Router, useValue: routerSpy},
- ]
- })
- .compileComponents();
+ TestBed
+ .configureTestingModule({
+ imports: [HeroModule],
+ // #enddocregion setup-hero-module
+ // declarations: [ HeroDetailComponent ], // NO! DOUBLE DECLARATION
+ // #docregion setup-hero-module
+ providers: [
+ {provide: ActivatedRoute, useValue: activatedRoute},
+ {provide: HeroService, useClass: TestHeroService},
+ {provide: Router, useValue: routerSpy},
+ ]
+ })
+ .compileComponents();
}));
// #enddocregion setup-hero-module
@@ -161,17 +153,17 @@ function heroModuleSetup() {
describe('when navigate to existing hero', () => {
let expectedHero: Hero;
- beforeEach(async(() => {
+ beforeEach(waitForAsync(() => {
expectedHero = firstHero;
- activatedRoute.setParamMap({ id: expectedHero.id });
+ activatedRoute.setParamMap({id: expectedHero.id});
createComponent();
}));
- // #docregion selected-tests
+ // #docregion selected-tests
it('should display that hero\'s name', () => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
- // #enddocregion route-good-id
+ // #enddocregion route-good-id
it('should navigate when click cancel', () => {
click(page.cancelBtn);
@@ -190,10 +182,10 @@ function heroModuleSetup() {
});
it('should navigate when click save and save resolves', fakeAsync(() => {
- click(page.saveBtn);
- tick(); // wait for async save to complete
- expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
- }));
+ click(page.saveBtn);
+ tick(); // wait for async save to complete
+ expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
+ }));
// #docregion title-case-pipe
it('should convert hero name to Title Case', () => {
@@ -215,14 +207,14 @@ function heroModuleSetup() {
expect(nameDisplay.textContent).toBe('Quick Brown Fox');
});
// #enddocregion title-case-pipe
- // #enddocregion selected-tests
- // #docregion route-good-id
+ // #enddocregion selected-tests
+ // #docregion route-good-id
});
// #enddocregion route-good-id
// #docregion route-no-id
describe('when navigate with no hero id', () => {
- beforeEach(async( createComponent ));
+ beforeEach(waitForAsync(createComponent));
it('should have hero.id === 0', () => {
expect(component.hero.id).toBe(0);
@@ -236,8 +228,8 @@ function heroModuleSetup() {
// #docregion route-bad-id
describe('when navigate to non-existent hero id', () => {
- beforeEach(async(() => {
- activatedRoute.setParamMap({ id: 99999 });
+ beforeEach(waitForAsync(() => {
+ activatedRoute.setParamMap({id: 99999});
createComponent();
}));
@@ -253,11 +245,10 @@ function heroModuleSetup() {
let service: HeroDetailService;
fixture = TestBed.createComponent(HeroDetailComponent);
expect(
- // Throws because `inject` only has access to TestBed's injector
- // which is an ancestor of the component's injector
- inject([HeroDetailService], (hds: HeroDetailService) => service = hds )
- )
- .toThrowError(/No provider for HeroDetailService/);
+ // Throws because `inject` only has access to TestBed's injector
+ // which is an ancestor of the component's injector
+ inject([HeroDetailService], (hds: HeroDetailService) => service = hds))
+ .toThrowError(/No provider for HeroDetailService/);
// get `HeroDetailService` with component's own injector
service = fixture.debugElement.injector.get(HeroDetailService);
@@ -270,30 +261,31 @@ import { FormsModule } from '@angular/forms';
import { TitleCasePipe } from '../shared/title-case.pipe';
function formsModuleSetup() {
- // #docregion setup-forms-module
- beforeEach(async(() => {
+ // #docregion setup-forms-module
+ beforeEach(waitForAsync(() => {
const routerSpy = createRouterSpy();
- TestBed.configureTestingModule({
- imports: [ FormsModule ],
- declarations: [ HeroDetailComponent, TitleCasePipe ],
- providers: [
- { provide: ActivatedRoute, useValue: activatedRoute },
- { provide: HeroService, useClass: TestHeroService },
- { provide: Router, useValue: routerSpy},
- ]
- })
- .compileComponents();
+ TestBed
+ .configureTestingModule({
+ imports: [FormsModule],
+ declarations: [HeroDetailComponent, TitleCasePipe],
+ providers: [
+ {provide: ActivatedRoute, useValue: activatedRoute},
+ {provide: HeroService, useClass: TestHeroService},
+ {provide: Router, useValue: routerSpy},
+ ]
+ })
+ .compileComponents();
}));
// #enddocregion setup-forms-module
- it('should display 1st hero\'s name', async(() => {
- const expectedHero = firstHero;
- activatedRoute.setParamMap({ id: expectedHero.id });
- createComponent().then(() => {
- expect(page.nameDisplay.textContent).toBe(expectedHero.name);
- });
- }));
+ it('should display 1st hero\'s name', waitForAsync(() => {
+ const expectedHero = firstHero;
+ activatedRoute.setParamMap({id: expectedHero.id});
+ createComponent().then(() => {
+ expect(page.nameDisplay.textContent).toBe(expectedHero.name);
+ });
+ }));
}
///////////////////////
@@ -301,29 +293,30 @@ import { SharedModule } from '../shared/shared.module';
function sharedModuleSetup() {
// #docregion setup-shared-module
- beforeEach(async(() => {
+ beforeEach(waitForAsync(() => {
const routerSpy = createRouterSpy();
- TestBed.configureTestingModule({
- imports: [ SharedModule ],
- declarations: [ HeroDetailComponent ],
- providers: [
- { provide: ActivatedRoute, useValue: activatedRoute },
- { provide: HeroService, useClass: TestHeroService },
- { provide: Router, useValue: routerSpy},
- ]
- })
- .compileComponents();
+ TestBed
+ .configureTestingModule({
+ imports: [SharedModule],
+ declarations: [HeroDetailComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: activatedRoute},
+ {provide: HeroService, useClass: TestHeroService},
+ {provide: Router, useValue: routerSpy},
+ ]
+ })
+ .compileComponents();
}));
// #enddocregion setup-shared-module
- it('should display 1st hero\'s name', async(() => {
- const expectedHero = firstHero;
- activatedRoute.setParamMap({ id: expectedHero.id });
- createComponent().then(() => {
- expect(page.nameDisplay.textContent).toBe(expectedHero.name);
- });
- }));
+ it('should display 1st hero\'s name', waitForAsync(() => {
+ const expectedHero = firstHero;
+ activatedRoute.setParamMap({id: expectedHero.id});
+ createComponent().then(() => {
+ expect(page.nameDisplay.textContent).toBe(expectedHero.name);
+ });
+ }));
}
/////////// Helpers /////
@@ -347,11 +340,21 @@ function createComponent() {
// #docregion page
class Page {
// getter properties wait to query the DOM until called.
- get buttons() { return this.queryAll('button'); }
- get saveBtn() { return this.buttons[0]; }
- get cancelBtn() { return this.buttons[1]; }
- get nameDisplay() { return this.query('span'); }
- get nameInput() { return this.query('input'); }
+ get buttons() {
+ return this.queryAll('button');
+ }
+ get saveBtn() {
+ return this.buttons[0];
+ }
+ get cancelBtn() {
+ return this.buttons[1];
+ }
+ get nameDisplay() {
+ return this.query('span');
+ }
+ get nameInput() {
+ return this.query('input');
+ }
gotoListSpy: jasmine.Spy;
navigateSpy: jasmine.Spy;
diff --git a/aio/content/examples/testing/src/app/hero/hero-list.component.spec.ts b/aio/content/examples/testing/src/app/hero/hero-list.component.spec.ts
index 526be15c43..b8c0278bf6 100644
--- a/aio/content/examples/testing/src/app/hero/hero-list.component.spec.ts
+++ b/aio/content/examples/testing/src/app/hero/hero-list.component.spec.ts
@@ -1,4 +1,4 @@
-import { async, ComponentFixture, fakeAsync, TestBed, tick
+import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
@@ -7,13 +7,12 @@ import { DebugElement } from '@angular/core';
import { Router } from '@angular/router';
import { addMatchers, newEvent } from '../../testing';
-
+import { HeroService } from '../model/hero.service';
import { getTestHeroes, TestHeroService } from '../model/testing/test-hero.service';
import { HeroModule } from './hero.module';
import { HeroListComponent } from './hero-list.component';
import { HighlightDirective } from '../shared/highlight.directive';
-import { HeroService } from '../model/hero.service';
const HEROES = getTestHeroes();
@@ -24,20 +23,20 @@ let page: Page;
/////// Tests //////
describe('HeroListComponent', () => {
-
- beforeEach(async(() => {
+ beforeEach(waitForAsync(() => {
addMatchers();
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
- TestBed.configureTestingModule({
- imports: [HeroModule],
- providers: [
- { provide: HeroService, useClass: TestHeroService },
- { provide: Router, useValue: routerSpy}
- ]
- })
- .compileComponents()
- .then(createComponent);
+ TestBed
+ .configureTestingModule({
+ imports: [HeroModule],
+ providers: [
+ {provide: HeroService, useClass: TestHeroService},
+ {provide: Router, useValue: routerSpy}
+ ]
+ })
+ .compileComponents()
+ .then(createComponent);
}));
it('should display heroes', () => {
@@ -52,36 +51,35 @@ describe('HeroListComponent', () => {
});
it('should select hero on click', fakeAsync(() => {
- const expectedHero = HEROES[1];
- const li = page.heroRows[1];
- li.dispatchEvent(newEvent('click'));
- tick();
- // `.toEqual` because selectedHero is clone of expectedHero; see FakeHeroService
- expect(comp.selectedHero).toEqual(expectedHero);
- }));
+ const expectedHero = HEROES[1];
+ const li = page.heroRows[1];
+ li.dispatchEvent(newEvent('click'));
+ tick();
+ // `.toEqual` because selectedHero is clone of expectedHero; see FakeHeroService
+ expect(comp.selectedHero).toEqual(expectedHero);
+ }));
it('should navigate to selected hero detail on click', fakeAsync(() => {
- const expectedHero = HEROES[1];
- const li = page.heroRows[1];
- li.dispatchEvent(newEvent('click'));
- tick();
+ const expectedHero = HEROES[1];
+ const li = page.heroRows[1];
+ li.dispatchEvent(newEvent('click'));
+ tick();
- // should have navigated
- expect(page.navSpy.calls.any()).toBe(true, 'navigate called');
+ // should have navigated
+ expect(page.navSpy.calls.any()).toBe(true, 'navigate called');
- // composed hero detail will be URL like 'heroes/42'
- // expect link array with the route path and hero id
- // first argument to router.navigate is link array
- const navArgs = page.navSpy.calls.first().args[0];
- expect(navArgs[0]).toContain('heroes', 'nav to heroes detail URL');
- expect(navArgs[1]).toBe(expectedHero.id, 'expected hero.id');
-
- }));
+ // composed hero detail will be URL like 'heroes/42'
+ // expect link array with the route path and hero id
+ // first argument to router.navigate is link array
+ const navArgs = page.navSpy.calls.first().args[0];
+ expect(navArgs[0]).toContain('heroes', 'nav to heroes detail URL');
+ expect(navArgs[1]).toBe(expectedHero.id, 'expected hero.id');
+ }));
it('should find `HighlightDirective` with `By.directive', () => {
// #docregion by
// Can find DebugElement either by css selector or by directive
- const h2 = fixture.debugElement.query(By.css('h2'));
+ const h2 = fixture.debugElement.query(By.css('h2'));
const directive = fixture.debugElement.query(By.directive(HighlightDirective));
// #enddocregion by
expect(h2).toBe(directive);
diff --git a/aio/content/examples/testing/src/app/shared/canvas.component.spec.ts b/aio/content/examples/testing/src/app/shared/canvas.component.spec.ts
index 3ff225d33c..1c30c7dc6a 100644
--- a/aio/content/examples/testing/src/app/shared/canvas.component.spec.ts
+++ b/aio/content/examples/testing/src/app/shared/canvas.component.spec.ts
@@ -1,6 +1,7 @@
// #docplaster
// #docregion without-toBlob-macrotask
-import { TestBed, async, tick, fakeAsync } from '@angular/core/testing';
+import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
+
import { CanvasComponent } from './canvas.component';
describe('CanvasComponent', () => {
@@ -10,29 +11,29 @@ describe('CanvasComponent', () => {
(window as any).__zone_symbol__FakeAsyncTestMacroTask = [
{
source: 'HTMLCanvasElement.toBlob',
- callbackArgs: [{ size: 200 }],
+ callbackArgs: [{size: 200}],
},
];
});
// #enddocregion enable-toBlob-macrotask
// #docregion without-toBlob-macrotask
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [
- CanvasComponent
- ],
- }).compileComponents();
+ beforeEach(waitForAsync(() => {
+ TestBed
+ .configureTestingModule({
+ declarations: [CanvasComponent],
+ })
+ .compileComponents();
}));
it('should be able to generate blob data from canvas', fakeAsync(() => {
- const fixture = TestBed.createComponent(CanvasComponent);
- const canvasComp = fixture.componentInstance;
+ const fixture = TestBed.createComponent(CanvasComponent);
+ const canvasComp = fixture.componentInstance;
- fixture.detectChanges();
- expect(canvasComp.blobSize).toBe(0);
+ fixture.detectChanges();
+ expect(canvasComp.blobSize).toBe(0);
- tick();
- expect(canvasComp.blobSize).toBeGreaterThan(0);
- }));
+ tick();
+ expect(canvasComp.blobSize).toBeGreaterThan(0);
+ }));
});
// #enddocregion without-toBlob-macrotask
diff --git a/aio/content/examples/testing/src/app/twain/twain.component.spec.ts b/aio/content/examples/testing/src/app/twain/twain.component.spec.ts
index ced3bb0094..629b6a4d5b 100644
--- a/aio/content/examples/testing/src/app/twain/twain.component.spec.ts
+++ b/aio/content/examples/testing/src/app/twain/twain.component.spec.ts
@@ -1,14 +1,13 @@
// #docplaster
-import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
+import { fakeAsync, ComponentFixture, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { asyncData, asyncError } from '../../testing';
import { of, throwError } from 'rxjs';
-
import { last } from 'rxjs/operators';
-import { TwainService } from './twain.service';
import { TwainComponent } from './twain.component';
+import { TwainService } from './twain.service';
describe('TwainComponent', () => {
let component: TwainComponent;
@@ -32,14 +31,12 @@ describe('TwainComponent', () => {
// Create a fake TwainService object with a `getQuote()` spy
const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
// Make the spy return a synchronous Observable with the test data
- getQuoteSpy = twainService.getQuote.and.returnValue( of(testQuote) );
+ getQuoteSpy = twainService.getQuote.and.returnValue(of(testQuote));
// #enddocregion spy
TestBed.configureTestingModule({
- declarations: [ TwainComponent ],
- providers: [
- { provide: TwainService, useValue: twainService }
- ]
+ declarations: [TwainComponent],
+ providers: [{provide: TwainService, useValue: twainService}]
});
fixture = TestBed.createComponent(TwainComponent);
@@ -58,7 +55,7 @@ describe('TwainComponent', () => {
// The quote would not be immediately available if the service were truly async.
// #docregion sync-test
it('should show quote after component initialized', () => {
- fixture.detectChanges(); // onInit()
+ fixture.detectChanges(); // onInit()
// sync spy result shows testQuote immediately after init
expect(quoteEl.textContent).toBe(testQuote);
@@ -71,20 +68,19 @@ describe('TwainComponent', () => {
// Use `fakeAsync` because the component error calls `setTimeout`
// #docregion error-test
it('should display error when TwainService fails', fakeAsync(() => {
- // tell spy to return an error observable
- getQuoteSpy.and.returnValue(
- throwError('TwainService test failure'));
+ // tell spy to return an error observable
+ getQuoteSpy.and.returnValue(throwError('TwainService test failure'));
- fixture.detectChanges(); // onInit()
- // sync spy errors immediately after init
+ fixture.detectChanges(); // onInit()
+ // sync spy errors immediately after init
- tick(); // flush the component's setTimeout()
+ tick(); // flush the component's setTimeout()
- fixture.detectChanges(); // update errorMessage within setTimeout()
+ fixture.detectChanges(); // update errorMessage within setTimeout()
- expect(errorMessage()).toMatch(/test failure/, 'should display error');
- expect(quoteEl.textContent).toBe('...', 'should show placeholder');
- }));
+ expect(errorMessage()).toMatch(/test failure/, 'should display error');
+ expect(quoteEl.textContent).toBe('...', 'should show placeholder');
+ }));
// #enddocregion error-test
});
@@ -113,28 +109,28 @@ describe('TwainComponent', () => {
// #docregion fake-async-test
it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {
- fixture.detectChanges(); // ngOnInit()
- expect(quoteEl.textContent).toBe('...', 'should show placeholder');
+ fixture.detectChanges(); // ngOnInit()
+ expect(quoteEl.textContent).toBe('...', 'should show placeholder');
- tick(); // flush the observable to get the quote
- fixture.detectChanges(); // update view
+ tick(); // flush the observable to get the quote
+ fixture.detectChanges(); // update view
- expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
- expect(errorMessage()).toBeNull('should not show error');
- }));
+ expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
+ expect(errorMessage()).toBeNull('should not show error');
+ }));
// #enddocregion fake-async-test
// #docregion async-test
- it('should show quote after getQuote (async)', async(() => {
- fixture.detectChanges(); // ngOnInit()
- expect(quoteEl.textContent).toBe('...', 'should show placeholder');
+ it('should show quote after getQuote (async)', waitForAsync(() => {
+ fixture.detectChanges(); // ngOnInit()
+ expect(quoteEl.textContent).toBe('...', 'should show placeholder');
- fixture.whenStable().then(() => { // wait for async getQuote
- fixture.detectChanges(); // update view with quote
- expect(quoteEl.textContent).toBe(testQuote);
- expect(errorMessage()).toBeNull('should not show error');
- });
- }));
+ fixture.whenStable().then(() => { // wait for async getQuote
+ fixture.detectChanges(); // update view with quote
+ expect(quoteEl.textContent).toBe(testQuote);
+ expect(errorMessage()).toBeNull('should not show error');
+ });
+ }));
// #enddocregion async-test
@@ -142,8 +138,8 @@ describe('TwainComponent', () => {
it('should show last quote (quote done)', (done: DoneFn) => {
fixture.detectChanges();
- component.quote.pipe( last() ).subscribe(() => {
- fixture.detectChanges(); // update view with quote
+ component.quote.pipe(last()).subscribe(() => {
+ fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).toBeNull('should not show error');
done();
@@ -157,7 +153,7 @@ describe('TwainComponent', () => {
// the spy's most recent call returns the observable with the test quote
getQuoteSpy.calls.mostRecent().returnValue.subscribe(() => {
- fixture.detectChanges(); // update view with quote
+ fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).toBeNull('should not show error');
done();
@@ -167,16 +163,16 @@ describe('TwainComponent', () => {
// #docregion async-error-test
it('should display error when TwainService fails', fakeAsync(() => {
- // tell spy to return an async error observable
- getQuoteSpy.and.returnValue(asyncError('TwainService test failure'));
+ // tell spy to return an async error observable
+ getQuoteSpy.and.returnValue(asyncError('TwainService test failure'));
- fixture.detectChanges();
- tick(); // component shows error after a setTimeout()
- fixture.detectChanges(); // update error message
+ fixture.detectChanges();
+ tick(); // component shows error after a setTimeout()
+ fixture.detectChanges(); // update error message
- expect(errorMessage()).toMatch(/test failure/, 'should display error');
- expect(quoteEl.textContent).toBe('...', 'should show placeholder');
- }));
+ expect(errorMessage()).toMatch(/test failure/, 'should display error');
+ expect(quoteEl.textContent).toBe('...', 'should show placeholder');
+ }));
// #enddocregion async-error-test
});
});
diff --git a/aio/content/examples/toh-pt6/src/app/dashboard/dashboard.component.spec.ts b/aio/content/examples/toh-pt6/src/app/dashboard/dashboard.component.spec.ts
index 3926c21e9f..224f5db3ce 100644
--- a/aio/content/examples/toh-pt6/src/app/dashboard/dashboard.component.spec.ts
+++ b/aio/content/examples/toh-pt6/src/app/dashboard/dashboard.component.spec.ts
@@ -1,12 +1,12 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { DashboardComponent } from './dashboard.component';
-import { HeroSearchComponent } from '../hero-search/hero-search.component';
-
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
-import { HEROES } from '../mock-heroes';
+
+import { HeroSearchComponent } from '../hero-search/hero-search.component';
import { HeroService } from '../hero.service';
+import { HEROES } from '../mock-heroes';
+
+import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
@@ -14,23 +14,16 @@ describe('DashboardComponent', () => {
let heroService;
let getHeroesSpy;
- beforeEach(async(() => {
+ beforeEach(waitForAsync(() => {
heroService = jasmine.createSpyObj('HeroService', ['getHeroes']);
- getHeroesSpy = heroService.getHeroes.and.returnValue( of(HEROES) );
- TestBed.configureTestingModule({
- declarations: [
- DashboardComponent,
- HeroSearchComponent
- ],
- imports: [
- RouterTestingModule.withRoutes([])
- ],
- providers: [
- { provide: HeroService, useValue: heroService }
- ]
- })
- .compileComponents();
-
+ getHeroesSpy = heroService.getHeroes.and.returnValue(of(HEROES));
+ TestBed
+ .configureTestingModule({
+ declarations: [DashboardComponent, HeroSearchComponent],
+ imports: [RouterTestingModule.withRoutes([])],
+ providers: [{provide: HeroService, useValue: heroService}]
+ })
+ .compileComponents();
}));
beforeEach(() => {
@@ -47,12 +40,11 @@ describe('DashboardComponent', () => {
expect(fixture.nativeElement.querySelector('h3').textContent).toEqual('Top Heroes');
});
- it('should call heroService', async(() => {
- expect(getHeroesSpy.calls.any()).toBe(true);
- }));
-
- it('should display 4 links', async(() => {
- expect(fixture.nativeElement.querySelectorAll('a').length).toEqual(4);
- }));
+ it('should call heroService', waitForAsync(() => {
+ expect(getHeroesSpy.calls.any()).toBe(true);
+ }));
+ it('should display 4 links', waitForAsync(() => {
+ expect(fixture.nativeElement.querySelectorAll('a').length).toEqual(4);
+ }));
});
diff --git a/aio/content/examples/upgrade-phonecat-2-hybrid/app/phone-detail/phone-detail.component.spec.ts b/aio/content/examples/upgrade-phonecat-2-hybrid/app/phone-detail/phone-detail.component.spec.ts
index 607fad3184..4ebfdd344e 100644
--- a/aio/content/examples/upgrade-phonecat-2-hybrid/app/phone-detail/phone-detail.component.spec.ts
+++ b/aio/content/examples/upgrade-phonecat-2-hybrid/app/phone-detail/phone-detail.component.spec.ts
@@ -1,22 +1,16 @@
// #docregion
// #docregion activatedroute
+import { TestBed, waitForAsync } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
-
// #enddocregion activatedroute
import { Observable, of } from 'rxjs';
-import { async, TestBed } from '@angular/core/testing';
-
import { PhoneDetailComponent } from './phone-detail.component';
import { Phone, PhoneData } from '../core/phone/phone.service';
import { CheckmarkPipe } from '../core/checkmark/checkmark.pipe';
function xyzPhoneData(): PhoneData {
- return {
- name: 'phone xyz',
- snippet: '',
- images: ['image/url1.png', 'image/url2.png']
- };
+ return {name: 'phone xyz', snippet: '', images: ['image/url1.png', 'image/url2.png']};
}
class MockPhone {
@@ -34,10 +28,9 @@ class ActivatedRouteMock {
// #enddocregion activatedroute
describe('PhoneDetailComponent', () => {
-
// #docregion activatedroute
- beforeEach(async(() => {
+ beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ CheckmarkPipe, PhoneDetailComponent ],
providers: [
@@ -55,5 +48,4 @@ describe('PhoneDetailComponent', () => {
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain(xyzPhoneData().name);
});
-
});
diff --git a/aio/content/examples/upgrade-phonecat-2-hybrid/app/phone-list/phone-list.component.spec.ts b/aio/content/examples/upgrade-phonecat-2-hybrid/app/phone-list/phone-list.component.spec.ts
index 21bd8182b7..443fc65178 100644
--- a/aio/content/examples/upgrade-phonecat-2-hybrid/app/phone-list/phone-list.component.spec.ts
+++ b/aio/content/examples/upgrade-phonecat-2-hybrid/app/phone-list/phone-list.component.spec.ts
@@ -1,13 +1,14 @@
/* tslint:disable */
// #docregion
-import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-import { Observable, of } from 'rxjs';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { SpyLocation } from '@angular/common/testing';
+import {SpyLocation} from '@angular/common/testing';
+import {NO_ERRORS_SCHEMA} from '@angular/core';
+import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
+import {ActivatedRoute} from '@angular/router';
+import {Observable, of} from 'rxjs';
-import { PhoneListComponent } from './phone-list.component';
-import { Phone, PhoneData } from '../core/phone/phone.service';
+import {Phone, PhoneData} from '../core/phone/phone.service';
+
+import {PhoneListComponent} from './phone-list.component';
class ActivatedRouteMock {
constructor(public snapshot: any) {}
@@ -16,8 +17,7 @@ class ActivatedRouteMock {
class MockPhone {
query(): Observable {
return of([
- {name: 'Nexus S', snippet: '', images: []},
- {name: 'Motorola DROID', snippet: '', images: []}
+ {name: 'Nexus S', snippet: '', images: []}, {name: 'Motorola DROID', snippet: '', images: []}
]);
}
}
@@ -25,18 +25,18 @@ class MockPhone {
let fixture: ComponentFixture;
describe('PhoneList', () => {
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [ PhoneListComponent ],
- providers: [
- { provide: ActivatedRoute, useValue: new ActivatedRouteMock({ params: { 'phoneId': 1 } }) },
- { provide: Location, useClass: SpyLocation },
- { provide: Phone, useClass: MockPhone },
- ],
- schemas: [ NO_ERRORS_SCHEMA ]
- })
- .compileComponents();
+ beforeEach(waitForAsync(() => {
+ TestBed
+ .configureTestingModule({
+ declarations: [PhoneListComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: new ActivatedRouteMock({params: {'phoneId': 1}})},
+ {provide: Location, useClass: SpyLocation},
+ {provide: Phone, useClass: MockPhone},
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ })
+ .compileComponents();
}));
beforeEach(() => {
@@ -47,20 +47,15 @@ describe('PhoneList', () => {
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelectorAll('.phone-list-item').length).toBe(2);
- expect(
- compiled.querySelector('.phone-list-item:nth-child(1)').textContent
- ).toContain('Motorola DROID');
- expect(
- compiled.querySelector('.phone-list-item:nth-child(2)').textContent
- ).toContain('Nexus S');
+ expect(compiled.querySelector('.phone-list-item:nth-child(1)').textContent)
+ .toContain('Motorola DROID');
+ expect(compiled.querySelector('.phone-list-item:nth-child(2)').textContent)
+ .toContain('Nexus S');
});
xit('should set the default value of orderProp model', () => {
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
- expect(
- compiled.querySelector('select option:last-child').selected
- ).toBe(true);
+ expect(compiled.querySelector('select option:last-child').selected).toBe(true);
});
-
});
diff --git a/aio/content/examples/upgrade-phonecat-3-final/app/phone-detail/phone-detail.component.spec.ts b/aio/content/examples/upgrade-phonecat-3-final/app/phone-detail/phone-detail.component.spec.ts
index 607fad3184..4ebfdd344e 100644
--- a/aio/content/examples/upgrade-phonecat-3-final/app/phone-detail/phone-detail.component.spec.ts
+++ b/aio/content/examples/upgrade-phonecat-3-final/app/phone-detail/phone-detail.component.spec.ts
@@ -1,22 +1,16 @@
// #docregion
// #docregion activatedroute
+import { TestBed, waitForAsync } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
-
// #enddocregion activatedroute
import { Observable, of } from 'rxjs';
-import { async, TestBed } from '@angular/core/testing';
-
import { PhoneDetailComponent } from './phone-detail.component';
import { Phone, PhoneData } from '../core/phone/phone.service';
import { CheckmarkPipe } from '../core/checkmark/checkmark.pipe';
function xyzPhoneData(): PhoneData {
- return {
- name: 'phone xyz',
- snippet: '',
- images: ['image/url1.png', 'image/url2.png']
- };
+ return {name: 'phone xyz', snippet: '', images: ['image/url1.png', 'image/url2.png']};
}
class MockPhone {
@@ -34,10 +28,9 @@ class ActivatedRouteMock {
// #enddocregion activatedroute
describe('PhoneDetailComponent', () => {
-
// #docregion activatedroute
- beforeEach(async(() => {
+ beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ CheckmarkPipe, PhoneDetailComponent ],
providers: [
@@ -55,5 +48,4 @@ describe('PhoneDetailComponent', () => {
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain(xyzPhoneData().name);
});
-
});
diff --git a/aio/content/examples/upgrade-phonecat-3-final/app/phone-list/phone-list.component.spec.ts b/aio/content/examples/upgrade-phonecat-3-final/app/phone-list/phone-list.component.spec.ts
index 588eabe76e..cb5dd53084 100644
--- a/aio/content/examples/upgrade-phonecat-3-final/app/phone-list/phone-list.component.spec.ts
+++ b/aio/content/examples/upgrade-phonecat-3-final/app/phone-list/phone-list.component.spec.ts
@@ -1,13 +1,14 @@
/* tslint:disable */
// #docregion routestuff
-import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-import { Observable, of } from 'rxjs';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { SpyLocation } from '@angular/common/testing';
+import {SpyLocation} from '@angular/common/testing';
+import {NO_ERRORS_SCHEMA} from '@angular/core';
+import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
+import {ActivatedRoute} from '@angular/router';
+import {Observable, of} from 'rxjs';
-import { PhoneListComponent } from './phone-list.component';
-import { Phone, PhoneData } from '../core/phone/phone.service';
+import {Phone, PhoneData} from '../core/phone/phone.service';
+
+import {PhoneListComponent} from './phone-list.component';
// #enddocregion routestuff
@@ -18,8 +19,7 @@ class ActivatedRouteMock {
class MockPhone {
query(): Observable {
return of([
- {name: 'Nexus S', snippet: '', images: []},
- {name: 'Motorola DROID', snippet: '', images: []}
+ {name: 'Nexus S', snippet: '', images: []}, {name: 'Motorola DROID', snippet: '', images: []}
]);
}
}
@@ -27,20 +27,20 @@ class MockPhone {
let fixture: ComponentFixture;
describe('PhoneList', () => {
-
// #docregion routestuff
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [ PhoneListComponent ],
- providers: [
- { provide: ActivatedRoute, useValue: new ActivatedRouteMock({ params: { 'phoneId': 1 } }) },
- { provide: Location, useClass: SpyLocation },
- { provide: Phone, useClass: MockPhone },
- ],
- schemas: [ NO_ERRORS_SCHEMA ]
- })
- .compileComponents();
+ beforeEach(waitForAsync(() => {
+ TestBed
+ .configureTestingModule({
+ declarations: [PhoneListComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: new ActivatedRouteMock({params: {'phoneId': 1}})},
+ {provide: Location, useClass: SpyLocation},
+ {provide: Phone, useClass: MockPhone},
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ })
+ .compileComponents();
}));
beforeEach(() => {
@@ -52,20 +52,15 @@ describe('PhoneList', () => {
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelectorAll('.phone-list-item').length).toBe(2);
- expect(
- compiled.querySelector('.phone-list-item:nth-child(1)').textContent
- ).toContain('Motorola DROID');
- expect(
- compiled.querySelector('.phone-list-item:nth-child(2)').textContent
- ).toContain('Nexus S');
+ expect(compiled.querySelector('.phone-list-item:nth-child(1)').textContent)
+ .toContain('Motorola DROID');
+ expect(compiled.querySelector('.phone-list-item:nth-child(2)').textContent)
+ .toContain('Nexus S');
});
xit('should set the default value of orderProp model', () => {
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
- expect(
- compiled.querySelector('select option:last-child').selected
- ).toBe(true);
+ expect(compiled.querySelector('select option:last-child').selected).toBe(true);
});
-
});
diff --git a/aio/content/guide/architecture-components.md b/aio/content/guide/architecture-components.md
index c4f8e7f9de..bedc446a51 100644
--- a/aio/content/guide/architecture-components.md
+++ b/aio/content/guide/architecture-components.md
@@ -124,9 +124,9 @@ Data binding plays an important role in communication between a template and its
Angular pipes let you declare display-value transformations in your template HTML. A class with the `@Pipe` decorator defines a function that transforms input values to output values for display in a view.
-Angular defines various pipes, such as the [date](https://angular.io/api/common/DatePipe) pipe and [currency](https://angular.io/api/common/CurrencyPipe) pipe; for a complete list, see the [Pipes API list](https://angular.io/api?type=pipe). You can also define new pipes.
+Angular defines various pipes, such as the [date](api/common/DatePipe) pipe and [currency](api/common/CurrencyPipe) pipe; for a complete list, see the [Pipes API list](api?type=pipe). You can also define new pipes.
-To specify a value transformation in an HTML template, use the [pipe operator (|)](https://angular.io/guide/template-expression-operators#pipe).
+To specify a value transformation in an HTML template, use the [pipe operator (|)](guide/template-expression-operators#pipe).
`{{interpolated_value | pipe_name}}`
diff --git a/aio/content/guide/built-in-directives.md b/aio/content/guide/built-in-directives.md
index f1a65922a3..d1a954169d 100644
--- a/aio/content/guide/built-in-directives.md
+++ b/aio/content/guide/built-in-directives.md
@@ -140,7 +140,7 @@ Angular provides *value accessors* for all of the basic HTML form elements and t
You can't apply `[(ngModel)]` to a non-form native element or a
third-party custom component until you write a suitable value accessor. For more information, see
-the API documentation on [DefaultValueAccessor](https://angular.io/api/forms/DefaultValueAccessor).
+the API documentation on [DefaultValueAccessor](api/forms/DefaultValueAccessor).
You don't need a value accessor for an Angular component that
you write because you can name the value and event properties
diff --git a/aio/content/guide/cli-builder.md b/aio/content/guide/cli-builder.md
index 054a50b840..ab451235ff 100644
--- a/aio/content/guide/cli-builder.md
+++ b/aio/content/guide/cli-builder.md
@@ -42,11 +42,11 @@ For example, your `myBuilder` folder could contain the following files.
| `src/my-builder.ts` | Main source file for the builder definition. |
| `src/my-builder.spec.ts` | Source file for tests. |
| `src/schema.json` | Definition of builder input options. |
-| `builders.json` | Testing configuration. |
+| `builders.json` | Builders definition. |
| `package.json` | Dependencies. See https://docs.npmjs.com/files/package.json. |
| `tsconfig.json` | [TypeScript configuration](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). |
-You can publish the builder to `npm` (see [Publishing your Library](https://angular.io/guide/creating-libraries#publishing-your-library)). If you publish it as `@example/my-builder`, you can install it using the following command.
+You can publish the builder to `npm` (see [Publishing your Library](guide/creating-libraries#publishing-your-library)). If you publish it as `@example/my-builder`, you can install it using the following command.
diff --git a/aio/content/guide/deprecations.md b/aio/content/guide/deprecations.md
index c97f67b89c..d4e2320411 100644
--- a/aio/content/guide/deprecations.md
+++ b/aio/content/guide/deprecations.md
@@ -58,6 +58,7 @@ v9 - v12
| `@angular/core` | [`ANALYZE_FOR_ENTRY_COMPONENTS`](api/core/ANALYZE_FOR_ENTRY_COMPONENTS) | v11 |
| `@angular/router` | [`loadChildren` string syntax](#loadChildren) | v11 |
| `@angular/core/testing` | [`TestBed.get`](#testing) | v12 |
+| `@angular/core/testing` | [`async`](#testing) | v12 |
| `@angular/router` | [`ActivatedRoute` params and `queryParams` properties](#activatedroute-props) | unspecified |
| template syntax | [`/deep/`, `>>>`, and `::ng-deep`](#deep-component-style-selector) | unspecified |
| browser support | [`IE 9 and 10, IE mobile`](#ie-9-10-and-mobile) | v11 |
@@ -108,6 +109,7 @@ Tip: In the [API reference section](api) of this doc site, deprecated APIs are i
| API | Replacement | Deprecation announced | Notes |
| --- | ----------- | --------------------- | ----- |
| [`TestBed.get`](api/core/testing/TestBed#get) | [`TestBed.inject`](api/core/testing/TestBed#inject) | v9 | Same behavior, but type safe. |
+| [`async`](api/core/testing/async) | [`waitForAsync`](api/core/testing/waitForAsync) | v10 | Same behavior, but rename to avoid confusion. |
{@a forms}
@@ -477,7 +479,7 @@ The final decision was made on three key points:
{@a wrapped-value}
-### `WrappedValue`
+### `WrappedValue`
The purpose of `WrappedValue` is to allow the same object instance to be treated as different for the purposes of change detection.
It is commonly used with the `async` pipe in the case where the `Observable` produces the same instance of the value.
@@ -487,7 +489,7 @@ No replacement is planned for this deprecation.
If you rely on the behavior that the same object instance should cause change detection, you have two options:
- Clone the resulting value so that it has a new identity.
-- Explicitly call [`ChangeDetectorRef.detectChanges()`](api/core/ChangeDetectorRef#detectchanges) to force the update.
+- Explicitly call [`ChangeDetectorRef.detectChanges()`](api/core/ChangeDetectorRef#detectchanges) to force the update.
{@a deprecated-cli-flags}
## Deprecated CLI APIs and Options
diff --git a/aio/content/guide/glossary.md b/aio/content/guide/glossary.md
index e2ad149975..27d8d8049f 100644
--- a/aio/content/guide/glossary.md
+++ b/aio/content/guide/glossary.md
@@ -867,7 +867,7 @@ To learn more, see [Introduction to Services and Dependency Injection](guide/arc
## structural directives
-A category of [directive](#directive) that is responsible for shaping HTML layout by modifying the DOM&mdashthat is, adding, removing, or manipulating elements and their children.
+A category of [directive](#directive) that is responsible for shaping HTML layout by modifying the DOM—that is, adding, removing, or manipulating elements and their children.
To learn more, see [Structural Directives](guide/structural-directives).
diff --git a/aio/content/guide/i18n.md b/aio/content/guide/i18n.md
index db0162408f..ba0d175262 100644
--- a/aio/content/guide/i18n.md
+++ b/aio/content/guide/i18n.md
@@ -58,7 +58,7 @@ While following these steps, you can [explore the translated example app](#app-p
The following are optional practices that may be required in special cases:
-* [Set the source locale manually](#set-source-manually) if you need to set the [LOCALE_ID](https://angular.io/api/core/LOCALE_ID "API reference for LOCALE_ID") token.
+* [Set the source locale manually](#set-source-manually) if you need to set the [LOCALE_ID](api/core/LOCALE_ID "API reference for LOCALE_ID") token.
* [Import global variants of the locale data](#import-locale) for extra locale data.
* [Manage marked text with custom IDs](#custom-id) if you require more control over matching translations.
@@ -77,7 +77,7 @@ This command updates your project's `package.json` and `polyfills.ts` files to i
-For more information about `package.json` and polyfill packages, see [Workspace npm dependencies](https://angular.io/guide/npm-packages).
+For more information about `package.json` and polyfill packages, see [Workspace npm dependencies](guide/npm-packages).
@@ -804,7 +804,7 @@ The following tabs show the example app and its translation files:
The following are optional practices that may be required in special cases:
-* [Set the source locale manually](#set-source-manually) by setting the [LOCALE_ID](https://angular.io/api/core/LOCALE_ID "API reference for LOCALE_ID") token.
+* [Set the source locale manually](#set-source-manually) by setting the [LOCALE_ID](api/core/LOCALE_ID "API reference for LOCALE_ID") token.
* [Import global variants of the locale data](#import-locale) for extra locale data.
* [Manage marked text with custom IDs](#custom-id) if you require more control over matching translations.
diff --git a/aio/content/guide/roadmap.md b/aio/content/guide/roadmap.md
new file mode 100644
index 0000000000..1cc7eb356f
--- /dev/null
+++ b/aio/content/guide/roadmap.md
@@ -0,0 +1,93 @@
+# Angular Roadmap
+
+Angular receives a large number of feature requests, both from inside Google and from the broader open-source community. At the same time, our list of projects contains plenty of maintenance tasks, code refactorings, potential performance improvements, and so on. We bring together representatives from developer relations, product management, and engineering to prioritize this list. As new projects come into the queue, we regularly position them based on relative priority to other projects. As work gets done, projects will move up in the queue.
+
+The projects below are not associated with a particular Angular version. We'll release them on completion, and they will be part of a specific version based on our release schedule, following semantic versioning. For example, features are released in the next minor after they are complete, or the next major if they include breaking changes.
+
+## In Progress
+
+### Operation Bye Bye Backlog (aka Operation Byelog)
+
+We are actively investing up to 50% of our engineering capacity on triaging issues and PRs until we have a clear understanding of broader community needs. After that, we'll commit up to 20% of our engineering capacity to keep up with new submissions promptly.
+
+### Support TypeScript 4.0
+
+We're working on adding support for TypeScript 4.0 ahead of its stable release. We always want Angular to stay up-to-date with the latest version of TypeScript so that developers get the best the language has to offer.
+
+### Update our e2e testing strategy
+
+To ensure we provide a future-proof e2e testing strategy, we want to evaluate the state of Protractor, community innovations, e2e best practices, and explore novel opportunities.
+
+### Angular libraries use Ivy
+
+We are investing in the design and development of Ivy library distribution plan, which will include an update of the library package format to use Ivy compilation, unblock the deprecation of the View Engine library format, and [ngcc](guide/glossary#ngcc).
+
+### Evaluate future RxJS changes (v7 and beyond)
+
+We want to ensure Angular developers are taking advantage of the latest capabilities of RxJS and have a smooth transition to the next major releases of the framework. For this purpose, we will explore and document the scope of the changes in v7 and beyond of RxJS and plan an update strategy.
+
+### Angular language service uses Ivy
+
+Today the language service still uses the View Engine compiler and type checking, even for Ivy applications. We want to use the Ivy template parser and improved type checking for the Angular Language service to match application behavior. This migration will also be a step towards unblocking the removal of View Engine, which will simplify Angular, reduce the npm package size, and improve the framework's maintainability.
+
+### Expand component harnesses best practices
+
+Angular CDK introduced the concept of [component test harnesses](https://material.angular.io/cdk/test-harnesses) to Angular in version 9. Test harnesses allow component authors to create supported APIs for testing component interactions. We're continuing to improve this harness infrastructure and clarifying the best practices around using harnesses. We're also working to drive more harness adoption inside of Google.
+
+### Support native [Trusted Types](https://web.dev/trusted-types/) in Angular
+
+In collaboration with Google's security team, we're adding support for the new Trusted Types API. This web platform API will help developers build more secure web applications.
+
+### Integrate [MDC Web](https://material.io/develop/web/) into Angular Material
+
+MDC Web is a library created by Google's Material Design team that provides reusable primitives for building Material Design components. The Angular team is incorporating these primitives into Angular Material. Using MDC Web will align Angular Material more closely with the Material Design specification, expand accessibility, overall improve component quality, and improve our team's velocity.
+
+### Offer Google engineers better integration with Angular and Google's internal server stack
+
+This is an internal project to add support for Angular front-ends to Google's internal integrated server stack.
+
+### Angular versioning & branching
+
+We want to consolidate release management tooling between Angular's multiple GitHub repositories ([angular/angular](https://github.com/angular/angular), [angular/angular-cli](https://github.com/angular/angular-cli), and [angular/components](https://github.com/angular/components)). This effort will allow us to reuse infrastructure, unify and simplify processes, and improve our release process's reliability.
+
+## Future
+
+### Refresh introductory documentation
+
+We will redefine the user learning journeys and refresh the introductory documentation. We will clearly state the benefits of Angular, how to explore its capabilities, and provide guidance so developers can become proficient with the framework in as little time as possible.
+
+### Strict typing for `@angular/forms`
+
+We will work on implementing stricter type checking for reactive forms. This way, we will allow developers to catch more issues during development time, enable better text editor and IDE support, and improve the type checking for reactive forms.
+
+### webpack 5 in the Angular CLI
+
+Webpack 5 brings a lot of build speed and bundle size improvements. To make them available for Angular developers, we will invest in migrating Angular CLI from using deprecated and removed webpack APIs.
+
+### Commit message standardization
+
+We want to unify commit message requirements and conformance across Angular repositories ([angular/angular](https://github.com/angular/angular), [angular/components](https://github.com/angular/components), [angular/angular-cli](https://github.com/angular/angular-cli)) to bring consistency to our development process and reuse infrastructure tooling.
+
+### Optional Zone.js
+
+We are going to design and implement a plan to make Zone.js optional from Angular applications. This way, we will simplify the framework, improve debugging, and reduce application bundle size. Additionally, this will allow us to take advantage of native async/await syntax, which currently Zone.js does not support.
+
+### Remove legacy [View Engine](guide/ivy)
+
+After the transition of all our internal tooling to Ivy has completed, we want to remove the legacy View Engine for smaller Angular conceptual overhead, smaller package size, lower maintenance cost, and lower complexity of the codebase.
+
+### Angular DevTools
+
+We’ll be working on development tooling for Angular that will provide utilities for debugging and performance profiling. This project aims to help developers understand the component structure and the change detection in an Angular application.
+
+### Optional NgModules
+
+To simplify the Angular mental model and learning journey, we’ll be working on making NgModules optional. This work will allow developers to develop standalone components and implement an alternative API for declaring the component’s compilation scope.
+
+### Ergonomic component level code-splitting APIs
+
+A common problem of web applications is their slow initial load time. A way to improve it is to apply more granular code-splitting on a component level. To encourage this practice, we’ll be working on more ergonomic code-splitting APIs.
+
+### Migration to ESLint
+
+With the deprecation of TSLint we will be moving to ESLint. As part of the process, we will work on ensuring backward compatibility with our current recommended TSLint configuration, implement a migration strategy for existing Angular applications and introduce new tooling to the Angular CLI toolchain.
diff --git a/aio/content/guide/service-worker-config.md b/aio/content/guide/service-worker-config.md
index 58bce456d4..389033844e 100644
--- a/aio/content/guide/service-worker-config.md
+++ b/aio/content/guide/service-worker-config.md
@@ -53,14 +53,27 @@ This field contains an array of asset groups, each of which defines a set of ass
```json
{
- "assetGroups": [{
- ...
- }, {
- ...
- }]
+ "assetGroups": [
+ {
+ ...
+ },
+ {
+ ...
+ }
+ ]
}
```
+
+
+When the ServiceWorker handles a request, it checks asset groups in the order in which they appear in `ngsw-config.json`.
+The first asset group that matches the requested resource handles the request.
+
+It is recommended that you put the more specific asset groups higher in the list.
+For example, an asset group that matches `/foo.js` should appear before one that matches `*.js`.
+
+
+
Each asset group specifies both a group of resources and a policy that governs them. This policy determines when the resources are fetched and what happens when changes are detected.
Asset groups follow the Typescript interface shown here:
@@ -123,6 +136,31 @@ These options are used to modify the matching behavior of requests. They are pas
Unlike asset resources, data requests are not versioned along with the app. They're cached according to manually-configured policies that are more useful for situations such as API requests and other data dependencies.
+This field contains an array of data groups, each of which defines a set of data resources and the policy by which they are cached.
+
+```json
+{
+ "dataGroups": [
+ {
+ ...
+ },
+ {
+ ...
+ }
+ ]
+}
+```
+
+
+
+When the ServiceWorker handles a request, it checks data groups in the order in which they appear in `ngsw-config.json`.
+The first data group that matches the requested resource handles the request.
+
+It is recommended that you put the more specific data groups higher in the list.
+For example, a data group that matches `/api/foo.json` should appear before one that matches `/api/*.json`.
+
+
+
Data groups follow this Typescript interface:
```typescript
diff --git a/aio/content/guide/testing-components-basics.md b/aio/content/guide/testing-components-basics.md
index 01e0bec05a..ca70d13559 100644
--- a/aio/content/guide/testing-components-basics.md
+++ b/aio/content/guide/testing-components-basics.md
@@ -159,10 +159,10 @@ It also generates an initial test file for the component, `banner-external.compo
Because `compileComponents` is asynchronous, it uses
-the [`async`](api/core/testing/async) utility
+the [`waitForAsync`](api/core/testing/waitForAsync) utility
function imported from `@angular/core/testing`.
-Please refer to the [async](guide/testing-components-scenarios#async) section for more details.
+Please refer to the [waitForAsync](guide/testing-components-scenarios#waitForAsync) section for more details.
diff --git a/aio/content/guide/testing-components-scenarios.md b/aio/content/guide/testing-components-scenarios.md
index 7ddb1af097..fb0ec07f9e 100644
--- a/aio/content/guide/testing-components-scenarios.md
+++ b/aio/content/guide/testing-components-scenarios.md
@@ -402,7 +402,7 @@ There is no nested syntax (like a `Promise.then()`) to disrupt the flow of contr
Limitation: The `fakeAsync()` function won't work if the test body makes an `XMLHttpRequest` (XHR) call.
-XHR calls within a test are rare, but if you need to call XHR, see [`async()`](#async), below.
+XHR calls within a test are rare, but if you need to call XHR, see [`waitForAsync()`](#waitForAsync), below.
@@ -587,41 +587,41 @@ Then call `detectChanges()` to tell Angular to update the screen.
Then you can assert that the quote element displays the expected text.
-{@a async}
+{@a waitForAsync}
-#### Async test with _async()_
+#### Async test with _waitForAsync()_
-To use `async()` functionality, you must import `zone.js/dist/zone-testing` in your test setup file.
+To use `waitForAsync()` functionality, you must import `zone.js/dist/zone-testing` in your test setup file.
If you created your project with the Angular CLI, `zone-testing` is already imported in `src/test.ts`.
The `fakeAsync()` utility function has a few limitations.
In particular, it won't work if the test body makes an `XMLHttpRequest` (XHR) call.
XHR calls within a test are rare so you can generally stick with [`fakeAsync()`](#fake-async).
-But if you ever do need to call `XMLHttpRequest`, you'll want to know about `async()`.
+But if you ever do need to call `XMLHttpRequest`, you'll want to know about `waitForAsync()`.
The `TestBed.compileComponents()` method (see [below](#compile-components)) calls `XHR`
to read external template and css files during "just-in-time" compilation.
-Write tests that call `compileComponents()` with the `async()` utility.
+Write tests that call `compileComponents()` with the `waitForAsync()` utility.
-Here's the previous `fakeAsync()` test, re-written with the `async()` utility.
+Here's the previous `fakeAsync()` test, re-written with the `waitForAsync()` utility.
-The `async()` utility hides some asynchronous boilerplate by arranging for the tester's code
+The `waitForAsync()` utility hides some asynchronous boilerplate by arranging for the tester's code
to run in a special _async test zone_.
You don't need to pass Jasmine's `done()` into the test and call `done()` because it is `undefined` in promise or observable callbacks.
But the test's asynchronous nature is revealed by the call to `fixture.whenStable()`,
which breaks the linear flow of control.
-When using an `intervalTimer()` such as `setInterval()` in `async()`, remember to cancel the timer with `clearInterval()` after the test, otherwise the `async()` never ends.
+When using an `intervalTimer()` such as `setInterval()` in `waitForAsync()`, remember to cancel the timer with `clearInterval()` after the test, otherwise the `waitForAsync()` never ends.
{@a when-stable}
@@ -641,18 +641,18 @@ update the quote element with the expected text.
#### Jasmine _done()_
-While the `async()` and `fakeAsync()` functions greatly
+While the `waitForAsync()` and `fakeAsync()` functions greatly
simplify Angular asynchronous testing,
you can still fall back to the traditional technique
and pass `it` a function that takes a
[`done` callback](https://jasmine.github.io/2.0/introduction.html#section-Asynchronous_Support).
-You can't call `done()` in `async()` or `fakeAsync()` functions, because the `done parameter`
+You can't call `done()` in `waitForAsync()` or `fakeAsync()` functions, because the `done parameter`
is `undefined`.
Now you are responsible for chaining promises, handling errors, and calling `done()` at the appropriate moments.
-Writing test functions with `done()`, is more cumbersome than `async()`and `fakeAsync()`, but it is occasionally necessary when code involves the `intervalTimer()` like `setInterval`.
+Writing test functions with `done()`, is more cumbersome than `waitForAsync()`and `fakeAsync()`, but it is occasionally necessary when code involves the `intervalTimer()` like `setInterval`.
Here are two more versions of the previous test, written with `done()`.
The first one subscribes to the `Observable` exposed to the template by the component's `quote` property.
@@ -738,7 +738,7 @@ you tell the `TestScheduler` to _flush_ its queue of prepared tasks like this.
region="test-scheduler-flush">
This step serves a purpose analogous to [tick()](api/core/testing/tick) and `whenStable()` in the
-earlier `fakeAsync()` and `async()` examples.
+earlier `fakeAsync()` and `waitForAsync()` examples.
The balance of the test is the same as those examples.
#### Marble error testing
@@ -1535,7 +1535,7 @@ You must call `compileComponents()` within an asynchronous test function.
If you neglect to make the test function async
-(e.g., forget to use `async()` as described below),
+(e.g., forget to use `waitForAsync()` as described below),
you'll see this error message
@@ -1549,7 +1549,7 @@ A typical approach is to divide the setup logic into two separate `beforeEach()`
1. An async `beforeEach()` that compiles the components
1. A synchronous `beforeEach()` that performs the remaining setup.
-To follow this pattern, import the `async()` helper with the other testing symbols.
+To follow this pattern, import the `waitForAsync()` helper with the other testing symbols.
-The `async()` helper function takes a parameterless function with the body of the setup.
+The `waitForAsync()` helper function takes a parameterless function with the body of the setup.
The `TestBed.configureTestingModule()` method returns the `TestBed` class so you can chain
calls to other `TestBed` static methods such as `compileComponents()`.
diff --git a/aio/content/guide/testing-utility-apis.md b/aio/content/guide/testing-utility-apis.md
index 8152d75acf..0c1b6ddd12 100644
--- a/aio/content/guide/testing-utility-apis.md
+++ b/aio/content/guide/testing-utility-apis.md
@@ -25,7 +25,7 @@ Here's a summary of the stand-alone functions, in order of likely utility:
Runs the body of a test (`it`) or setup (`beforeEach`) function within a special _async test zone_.
- See [discussion above](guide/testing-components-scenarios#async).
+ See [discussion above](guide/testing-components-scenarios#waitForAsync).
diff --git a/aio/content/guide/upgrade-setup.md b/aio/content/guide/upgrade-setup.md
index 284076e42f..bf1e9da49e 100644
--- a/aio/content/guide/upgrade-setup.md
+++ b/aio/content/guide/upgrade-setup.md
@@ -306,9 +306,9 @@ If you develop angular locally with `ng serve`, a `websocket` connection is set
In Windows, by default, one application can only have 6 websocket connections, MSDN WebSocket Settings.
So when IE is refreshed (manually or automatically by `ng serve`), sometimes the websocket does not close properly. When websocket connections exceed the limitations, a `SecurityError` will be thrown. This error will not affect the angular application, you can just restart IE to clear this error, or modify the windows registry to update the limitations.
-## Appendix: Test using `fakeAsync()/async()`
+## Appendix: Test using `fakeAsync()/waitForAsync()`
-If you use the `fakeAsync()/async()` helper function to run unit tests (for details, read the [Testing guide](guide/testing-components-scenarios#fake-async)), you need to import `zone.js/dist/zone-testing` in your test setup file.
+If you use the `fakeAsync()/waitForAsync()` helper function to run unit tests (for details, read the [Testing guide](guide/testing-components-scenarios#fake-async)), you need to import `zone.js/dist/zone-testing` in your test setup file.
If you create project with `Angular/CLI`, it is already imported in `src/test.ts`.
diff --git a/aio/content/navigation.json b/aio/content/navigation.json
index 667f7bfc83..9257a2af5c 100644
--- a/aio/content/navigation.json
+++ b/aio/content/navigation.json
@@ -820,6 +820,11 @@
"title": "Release Practices",
"tooltip": "Angular versioning, release, support, and deprecation policies and practices."
},
+ {
+ "url": "guide/roadmap",
+ "title": "Roadmap",
+ "tooltip": "Roadmap of the Angular team."
+ },
{
"title": "Updating to Version 10",
"tooltip": "Support for updating your application from version 9 to 10.",
diff --git a/aio/src/app/shared/scroll.service.spec.ts b/aio/src/app/shared/scroll.service.spec.ts
index 28733e5574..bd3ffd71f1 100644
--- a/aio/src/app/shared/scroll.service.spec.ts
+++ b/aio/src/app/shared/scroll.service.spec.ts
@@ -1,10 +1,10 @@
-import { ReflectiveInjector } from '@angular/core';
-import { Location, LocationStrategy, PlatformLocation, ViewportScroller } from '@angular/common';
-import { DOCUMENT } from '@angular/common';
-import { MockLocationStrategy, SpyLocation } from '@angular/common/testing';
-import { fakeAsync, tick } from '@angular/core/testing';
+import {Location, LocationStrategy, PlatformLocation, ViewportScroller} from '@angular/common';
+import {DOCUMENT} from '@angular/common';
+import {MockLocationStrategy, SpyLocation} from '@angular/common/testing';
+import {ReflectiveInjector} from '@angular/core';
+import {fakeAsync, tick} from '@angular/core/testing';
-import { ScrollService, topMargin } from './scroll.service';
+import {ScrollService, topMargin} from './scroll.service';
describe('ScrollService', () => {
const scrollServiceInstances: ScrollService[] = [];
@@ -32,27 +32,25 @@ describe('ScrollService', () => {
}
class MockElement {
- getBoundingClientRect = jasmine.createSpy('Element getBoundingClientRect')
- .and.returnValue({top: 0});
+ getBoundingClientRect =
+ jasmine.createSpy('Element getBoundingClientRect').and.returnValue({top: 0});
scrollIntoView = jasmine.createSpy('Element scrollIntoView');
}
- const viewportScrollerStub = jasmine.createSpyObj(
- 'viewportScroller',
- ['getScrollPosition', 'scrollToPosition']);
+ const viewportScrollerStub =
+ jasmine.createSpyObj('viewportScroller', ['getScrollPosition', 'scrollToPosition']);
beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([
- {
- provide: ScrollService,
- useFactory: createScrollService,
- deps: [DOCUMENT, PlatformLocation, ViewportScroller, Location],
- },
- { provide: Location, useClass: SpyLocation },
- { provide: DOCUMENT, useClass: MockDocument },
- { provide: PlatformLocation, useClass: MockPlatformLocation },
- { provide: ViewportScroller, useValue: viewportScrollerStub },
- { provide: LocationStrategy, useClass: MockLocationStrategy }
+ {
+ provide: ScrollService,
+ useFactory: createScrollService,
+ deps: [DOCUMENT, PlatformLocation, ViewportScroller, Location],
+ },
+ {provide: Location, useClass: SpyLocation}, {provide: DOCUMENT, useClass: MockDocument},
+ {provide: PlatformLocation, useClass: MockPlatformLocation},
+ {provide: ViewportScroller, useValue: viewportScrollerStub},
+ {provide: LocationStrategy, useClass: MockLocationStrategy}
]);
platformLocation = injector.get(PlatformLocation);
document = injector.get(DOCUMENT);
@@ -68,18 +66,39 @@ describe('ScrollService', () => {
});
it('should debounce `updateScrollPositonInHistory()`', fakeAsync(() => {
- const updateScrollPositionInHistorySpy = spyOn(scrollService, 'updateScrollPositionInHistory');
+ const updateScrollPositionInHistorySpy =
+ spyOn(scrollService, 'updateScrollPositionInHistory');
- window.dispatchEvent(new Event('scroll'));
- tick(249);
- window.dispatchEvent(new Event('scroll'));
- tick(249);
- window.dispatchEvent(new Event('scroll'));
- tick(249);
- expect(updateScrollPositionInHistorySpy).not.toHaveBeenCalled();
- tick(1);
- expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(1);
- }));
+ window.dispatchEvent(new Event('scroll'));
+ tick(249);
+ window.dispatchEvent(new Event('scroll'));
+ tick(249);
+ window.dispatchEvent(new Event('scroll'));
+ tick(249);
+ expect(updateScrollPositionInHistorySpy).not.toHaveBeenCalled();
+ tick(1);
+ expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(1);
+ }));
+
+ it('should not support `manual` scrollRestoration when it is not writable', () => {
+ const original = Object.getOwnPropertyDescriptor(window.history, 'scrollRestoration');
+ try {
+ Object.defineProperty(window.history, 'scrollRestoration', {
+ value: 'auto',
+ configurable: true,
+ });
+ scrollService = createScrollService(
+ document, platformLocation as PlatformLocation, viewportScrollerStub, location);
+
+ expect(scrollService.supportManualScrollRestoration).toBe(false);
+ } finally {
+ if (original !== undefined) {
+ Object.defineProperty(window.history, 'scrollRestoration', original);
+ } else {
+ delete window.history.scrollRestoration;
+ }
+ }
+ });
it('should set `scrollRestoration` to `manual` if supported', () => {
if (scrollService.supportManualScrollRestoration) {
@@ -96,7 +115,9 @@ describe('ScrollService', () => {
try {
// Simulate `window.sessionStorage` being inaccessible, when cookies are disabled.
Object.defineProperty(window, 'sessionStorage', {
- get() { throw new Error('The operation is insecure'); },
+ get() {
+ throw new Error('The operation is insecure');
+ },
});
const platformLoc = platformLocation as PlatformLocation;
@@ -198,8 +219,7 @@ describe('ScrollService', () => {
platformLocation.hash = '';
const topOfPage = new MockElement();
- document.getElementById.and
- .callFake((id: string) => id === 'top-of-page' ? topOfPage : null);
+ document.getElementById.and.callFake((id: string) => id === 'top-of-page' ? topOfPage : null);
scrollService.scroll();
expect(topOfPage.scrollIntoView).toHaveBeenCalled();
@@ -227,7 +247,7 @@ describe('ScrollService', () => {
it('should scroll to the element whose id matches the hash with encoded characters', () => {
const element = new MockElement();
- platformLocation.hash = '%F0%9F%91%8D'; // 👍
+ platformLocation.hash = '%F0%9F%91%8D'; // 👍
document.getElementById.and.returnValue(element);
scrollService.scroll();
@@ -289,8 +309,7 @@ describe('ScrollService', () => {
it('should scroll to top', () => {
const topOfPageElement = new MockElement() as any as Element;
document.getElementById.and.callFake(
- (id: string) => id === 'top-of-page' ? topOfPageElement : null
- );
+ (id: string) => id === 'top-of-page' ? topOfPageElement : null);
scrollService.scrollToTop();
expect(topOfPageElement.scrollIntoView).toHaveBeenCalled();
@@ -312,58 +331,55 @@ describe('ScrollService', () => {
describe('#needToFixScrollPosition', async () => {
it('should return true when popState event was fired after a back navigation if the browser supports ' +
- 'scrollRestoration`. Otherwise, needToFixScrollPosition() returns false', () => {
+ 'scrollRestoration`. Otherwise, needToFixScrollPosition() returns false',
+ () => {
+ if (scrollService.supportManualScrollRestoration) {
+ location.go('/initial-url1');
+ // We simulate a scroll down
+ location.replaceState('/initial-url1', 'hack', {scrollPosition: [2000, 0]});
+ location.go('/initial-url2');
+ location.back();
- if (scrollService.supportManualScrollRestoration) {
- location.go('/initial-url1');
- // We simulate a scroll down
- location.replaceState('/initial-url1', 'hack', {scrollPosition: [2000, 0]});
- location.go('/initial-url2');
- location.back();
+ expect(scrollService.poppedStateScrollPosition).toEqual([2000, 0]);
+ expect(scrollService.needToFixScrollPosition()).toBe(true);
+ } else {
+ location.go('/initial-url1');
+ location.go('/initial-url2');
+ location.back();
- expect(scrollService.poppedStateScrollPosition).toEqual([2000, 0]);
- expect(scrollService.needToFixScrollPosition()).toBe(true);
- } else {
- location.go('/initial-url1');
- location.go('/initial-url2');
- location.back();
-
- expect(scrollService.poppedStateScrollPosition).toBe(null);
- expect(scrollService.needToFixScrollPosition()).toBe(false);
- }
-
- });
+ expect(scrollService.poppedStateScrollPosition).toBe(null);
+ expect(scrollService.needToFixScrollPosition()).toBe(false);
+ }
+ });
it('should return true when popState event was fired after a forward navigation if the browser supports ' +
- 'scrollRestoration`. Otherwise, needToFixScrollPosition() returns false', () => {
+ 'scrollRestoration`. Otherwise, needToFixScrollPosition() returns false',
+ () => {
+ if (scrollService.supportManualScrollRestoration) {
+ location.go('/initial-url1');
+ location.go('/initial-url2');
+ // We simulate a scroll down
+ location.replaceState('/initial-url1', 'hack', {scrollPosition: [2000, 0]});
- if (scrollService.supportManualScrollRestoration) {
- location.go('/initial-url1');
- location.go('/initial-url2');
- // We simulate a scroll down
- location.replaceState('/initial-url1', 'hack', {scrollPosition: [2000, 0]});
+ location.back();
+ scrollService.poppedStateScrollPosition = [0, 0];
+ location.forward();
- location.back();
- scrollService.poppedStateScrollPosition = [0, 0];
- location.forward();
+ expect(scrollService.poppedStateScrollPosition).toEqual([2000, 0]);
+ expect(scrollService.needToFixScrollPosition()).toBe(true);
+ } else {
+ location.go('/initial-url1');
+ location.go('/initial-url2');
+ location.back();
+ location.forward();
- expect(scrollService.poppedStateScrollPosition).toEqual([2000, 0]);
- expect(scrollService.needToFixScrollPosition()).toBe(true);
- } else {
- location.go('/initial-url1');
- location.go('/initial-url2');
- location.back();
- location.forward();
-
- expect(scrollService.poppedStateScrollPosition).toBe(null);
- expect(scrollService.needToFixScrollPosition()).toBe(false);
- }
-
- });
+ expect(scrollService.poppedStateScrollPosition).toBe(null);
+ expect(scrollService.needToFixScrollPosition()).toBe(false);
+ }
+ });
});
describe('#scrollAfterRender', async () => {
-
let scrollSpy: jasmine.Spy;
let scrollToTopSpy: jasmine.Spy;
let needToFixScrollPositionSpy: jasmine.Spy;
@@ -383,69 +399,69 @@ describe('ScrollService', () => {
it('should call `scroll` when we navigate to a location with anchor', fakeAsync(() => {
- needToFixScrollPositionSpy.and.returnValue(false);
- getStoredScrollPositionSpy.and.returnValue(null);
- isLocationWithHashSpy.and.returnValue(true);
+ needToFixScrollPositionSpy.and.returnValue(false);
+ getStoredScrollPositionSpy.and.returnValue(null);
+ isLocationWithHashSpy.and.returnValue(true);
- scrollService.scrollAfterRender(scrollDelay);
+ scrollService.scrollAfterRender(scrollDelay);
- expect(scrollSpy).not.toHaveBeenCalled();
- tick(scrollDelay);
- expect(scrollSpy).toHaveBeenCalled();
- }));
+ expect(scrollSpy).not.toHaveBeenCalled();
+ tick(scrollDelay);
+ expect(scrollSpy).toHaveBeenCalled();
+ }));
it('should call `scrollToTop` when we navigate to a location without anchor', fakeAsync(() => {
- needToFixScrollPositionSpy.and.returnValue(false);
- getStoredScrollPositionSpy.and.returnValue(null);
- isLocationWithHashSpy.and.returnValue(false);
+ needToFixScrollPositionSpy.and.returnValue(false);
+ getStoredScrollPositionSpy.and.returnValue(null);
+ isLocationWithHashSpy.and.returnValue(false);
- scrollService.scrollAfterRender(scrollDelay);
+ scrollService.scrollAfterRender(scrollDelay);
- expect(scrollToTopSpy).toHaveBeenCalled();
- tick(scrollDelay);
- expect(scrollSpy).not.toHaveBeenCalled();
- }));
+ expect(scrollToTopSpy).toHaveBeenCalled();
+ tick(scrollDelay);
+ expect(scrollSpy).not.toHaveBeenCalled();
+ }));
it('should call `viewportScroller.scrollToPosition` when we reload a page', fakeAsync(() => {
- getStoredScrollPositionSpy.and.returnValue([0, 1000]);
+ getStoredScrollPositionSpy.and.returnValue([0, 1000]);
- scrollService.scrollAfterRender(scrollDelay);
+ scrollService.scrollAfterRender(scrollDelay);
- expect(viewportScrollerStub.scrollToPosition).toHaveBeenCalled();
- expect(getStoredScrollPositionSpy).toHaveBeenCalled();
- }));
+ expect(viewportScrollerStub.scrollToPosition).toHaveBeenCalled();
+ expect(getStoredScrollPositionSpy).toHaveBeenCalled();
+ }));
it('should call `scrollToPosition` after a popState', fakeAsync(() => {
- needToFixScrollPositionSpy.and.returnValue(true);
- getStoredScrollPositionSpy.and.returnValue(null);
- scrollService.scrollAfterRender(scrollDelay);
- expect(scrollToPosition).toHaveBeenCalled();
- tick(scrollDelay);
- expect(scrollSpy).not.toHaveBeenCalled();
- expect(scrollToTopSpy).not.toHaveBeenCalled();
- }));
+ needToFixScrollPositionSpy.and.returnValue(true);
+ getStoredScrollPositionSpy.and.returnValue(null);
+ scrollService.scrollAfterRender(scrollDelay);
+ expect(scrollToPosition).toHaveBeenCalled();
+ tick(scrollDelay);
+ expect(scrollSpy).not.toHaveBeenCalled();
+ expect(scrollToTopSpy).not.toHaveBeenCalled();
+ }));
});
describe('once destroyed', () => {
it('should stop updating scroll position', fakeAsync(() => {
- const updateScrollPositionInHistorySpy =
- spyOn(scrollService, 'updateScrollPositionInHistory');
+ const updateScrollPositionInHistorySpy =
+ spyOn(scrollService, 'updateScrollPositionInHistory');
- window.dispatchEvent(new Event('scroll'));
- tick(250);
- expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(1);
+ window.dispatchEvent(new Event('scroll'));
+ tick(250);
+ expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(1);
- window.dispatchEvent(new Event('scroll'));
- tick(250);
- expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(2);
+ window.dispatchEvent(new Event('scroll'));
+ tick(250);
+ expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(2);
- updateScrollPositionInHistorySpy.calls.reset();
- scrollService.ngOnDestroy();
+ updateScrollPositionInHistorySpy.calls.reset();
+ scrollService.ngOnDestroy();
- window.dispatchEvent(new Event('scroll'));
- tick(250);
- expect(updateScrollPositionInHistorySpy).not.toHaveBeenCalled();
- }));
+ window.dispatchEvent(new Event('scroll'));
+ tick(250);
+ expect(updateScrollPositionInHistorySpy).not.toHaveBeenCalled();
+ }));
it('should stop updating the stored location href', () => {
const updateScrollLocationHrefSpy = spyOn(scrollService, 'updateScrollLocationHref');
diff --git a/aio/src/app/shared/scroll.service.ts b/aio/src/app/shared/scroll.service.ts
index 436040c8d3..ab5f299b66 100644
--- a/aio/src/app/shared/scroll.service.ts
+++ b/aio/src/app/shared/scroll.service.ts
@@ -1,7 +1,7 @@
-import { DOCUMENT, Location, PlatformLocation, PopStateEvent, ViewportScroller } from '@angular/common';
-import { Injectable, Inject, OnDestroy } from '@angular/core';
-import { fromEvent, Subject } from 'rxjs';
-import { debounceTime, takeUntil } from 'rxjs/operators';
+import {DOCUMENT, Location, PlatformLocation, PopStateEvent, ViewportScroller} from '@angular/common';
+import {Inject, Injectable, OnDestroy} from '@angular/core';
+import {fromEvent, Subject} from 'rxjs';
+import {debounceTime, takeUntil} from 'rxjs/operators';
type ScrollPosition = [number, number];
interface ScrollPositionPopStateEvent extends PopStateEvent {
@@ -15,18 +15,16 @@ export const topMargin = 16;
*/
@Injectable()
export class ScrollService implements OnDestroy {
-
- private _topOffset: number | null;
+ private _topOffset: number|null;
private _topOfPageElement: Element;
private onDestroy = new Subject();
private storage: Storage;
// The scroll position which has to be restored, after a `popstate` event.
- poppedStateScrollPosition: ScrollPosition | null = null;
+ poppedStateScrollPosition: ScrollPosition|null = null;
// Whether the browser supports the necessary features for manual scroll restoration.
- supportManualScrollRestoration: boolean =
- !!window && ('scrollTo' in window) && ('scrollX' in window) && ('scrollY' in window) &&
- !!history && ('scrollRestoration' in history);
+ supportManualScrollRestoration: boolean = !!window && ('scrollTo' in window) &&
+ ('scrollX' in window) && ('scrollY' in window) && isScrollRestorationWritable();
// Offset from the top of the document to bottom of any static elements
// at the top (e.g. toolbar) + some margin
@@ -46,10 +44,8 @@ export class ScrollService implements OnDestroy {
}
constructor(
- @Inject(DOCUMENT) private document: any,
- private platformLocation: PlatformLocation,
- private viewportScroller: ViewportScroller,
- private location: Location) {
+ @Inject(DOCUMENT) private document: any, private platformLocation: PlatformLocation,
+ private viewportScroller: ViewportScroller, private location: Location) {
try {
this.storage = window.sessionStorage;
} catch {
@@ -118,9 +114,7 @@ export class ScrollService implements OnDestroy {
*/
scroll() {
const hash = this.getCurrentHash();
- const element: HTMLElement = hash
- ? this.document.getElementById(hash)
- : this.topOfPageElement;
+ const element: HTMLElement = hash ? this.document.getElementById(hash) : this.topOfPageElement;
this.scrollToElement(element);
}
@@ -132,8 +126,8 @@ export class ScrollService implements OnDestroy {
}
/**
- * When we load a document, we have to scroll to the correct position depending on whether this is a new location,
- * a back/forward in the history, or a refresh
+ * When we load a document, we have to scroll to the correct position depending on whether this is
+ * a new location, a back/forward in the history, or a refresh
* @param delay before we scroll to the good position
*/
scrollAfterRender(delay: number) {
@@ -208,19 +202,22 @@ export class ScrollService implements OnDestroy {
updateScrollPositionInHistory() {
if (this.supportManualScrollRestoration) {
const currentScrollPosition = this.viewportScroller.getScrollPosition();
- this.location.replaceState(this.location.path(true), undefined, {scrollPosition: currentScrollPosition});
+ this.location.replaceState(
+ this.location.path(true), undefined, {scrollPosition: currentScrollPosition});
this.storage.setItem('scrollPosition', currentScrollPosition.join(','));
}
}
- getStoredScrollLocationHref(): string | null {
+ getStoredScrollLocationHref(): string|null {
const href = this.storage.getItem('scrollLocationHref');
return href || null;
}
- getStoredScrollPosition(): ScrollPosition | null {
+ getStoredScrollPosition(): ScrollPosition|null {
const position = this.storage.getItem('scrollPosition');
- if (!position) { return null; }
+ if (!position) {
+ return null;
+ }
const [x, y] = position.split(',');
return [+x, +y];
@@ -245,3 +242,20 @@ export class ScrollService implements OnDestroy {
return decodeURIComponent(this.platformLocation.hash.replace(/^#/, ''));
}
}
+
+/**
+ * We need to check whether we can write to `history.scrollRestoration`
+ *
+ * We do this by checking the property descriptor of the property, but
+ * it might actually be defined on the `history` prototype not the instance.
+ *
+ * In this context "writable" means either than the property is a `writable`
+ * data file or a property that has a setter.
+ */
+function isScrollRestorationWritable() {
+ const scrollRestorationDescriptor =
+ Object.getOwnPropertyDescriptor(history, 'scrollRestoration') ||
+ Object.getOwnPropertyDescriptor(Object.getPrototypeOf(history), 'scrollRestoration');
+ return scrollRestorationDescriptor !== undefined &&
+ !!(scrollRestorationDescriptor.writable || scrollRestorationDescriptor.set);
+}
diff --git a/aio/tools/transforms/cli-docs-package/processors/processCliCommands.js b/aio/tools/transforms/cli-docs-package/processors/processCliCommands.js
index 0de9909d7b..5b6e5448b1 100644
--- a/aio/tools/transforms/cli-docs-package/processors/processCliCommands.js
+++ b/aio/tools/transforms/cli-docs-package/processors/processCliCommands.js
@@ -4,11 +4,9 @@ module.exports = function processCliCommands(createDocMessage) {
$runBefore: ['rendering-docs'],
$process(docs) {
const navigationDoc = docs.find(doc => doc.docType === 'navigation-json');
- const navigationNode = navigationDoc &&
- navigationDoc.data['SideNav'].find(
- node => node.children && node.children.length && node.children[0].url === 'cli');
+ const cliCommandsNode = navigationDoc && findCliCommandsNode(navigationDoc.data['SideNav']);
- if (!navigationNode) {
+ if (!cliCommandsNode) {
throw new Error(createDocMessage(
'Missing `cli` url - CLI Commands must include a first child node with url set at `cli`',
navigationDoc));
@@ -24,13 +22,41 @@ module.exports = function processCliCommands(createDocMessage) {
doc.optionKeywords = Array.from(optionKeywords).join(' ');
// Add to navigation doc
- navigationNode.children.push({url: doc.path, title: `ng ${doc.name}`});
+ cliCommandsNode.children.push({url: doc.path, title: `ng ${doc.name}`});
}
});
}
};
};
+// Look for the `CLI Commands` navigation node. It is the node whose first child has `url: 'cli'`.
+// (NOTE: Using the URL instead of the title, because it is more robust.)
+function findCliCommandsNode(nodes) {
+ // We will "recursively" check all navigation nodes and their children (in breadth-first order),
+ // until we find the `CLI Commands` node. Keep a list of nodes lists to check.
+ // (NOTE: Each item in the list is a LIST of nodes.)
+ const nodesList = [nodes];
+
+ while (nodesList.length > 0) {
+ // Get the first item from the list of nodes lists.
+ const currentNodes = nodesList.shift();
+ const cliCommandsNode = currentNodes.find(isCliCommandsNode);
+
+ // One of the nodes in `currentNodes` was the `CLI Commands` node. Return it.
+ if (cliCommandsNode) return cliCommandsNode;
+
+ // The `CLI Commands` node is not in `currentNodes`. Check each node's children (if any).
+ currentNodes.forEach(node => node.children && nodesList.push(node.children));
+ }
+
+ // We checked all navigation nodes and their children and did not find the `CLI Commands` node.
+ return undefined;
+}
+
+function isCliCommandsNode(node) {
+ return node.children && node.children.length && node.children[0].url === 'cli';
+}
+
function processOptions(container, options, optionKeywords) {
container.positionalOptions = [];
container.namedOptions = [];
diff --git a/aio/tools/transforms/cli-docs-package/processors/processCliCommands.spec.js b/aio/tools/transforms/cli-docs-package/processors/processCliCommands.spec.js
index 97413d647e..79e0dbb0ec 100644
--- a/aio/tools/transforms/cli-docs-package/processors/processCliCommands.spec.js
+++ b/aio/tools/transforms/cli-docs-package/processors/processCliCommands.spec.js
@@ -258,14 +258,15 @@ describe('processCliCommands processor', () => {
docType: 'navigation-json',
data: {
SideNav: [
- {url: 'some/page', title: 'Some Page'}, {
+ {url: 'some/page', title: 'Some Page'},
+ {
title: 'CLI Commands',
tooltip: 'Angular CLI command reference',
- children: [{'title': 'Overview', 'url': 'cli'}]
+ children: [{'title': 'Overview', 'url': 'cli'}],
},
- {url: 'other/page', title: 'Other Page'}
- ]
- }
+ {url: 'other/page', title: 'Other Page'},
+ ],
+ },
};
processor.$process([command, navigation]);
expect(navigation.data.SideNav[1].title).toEqual('CLI Commands');
@@ -275,6 +276,54 @@ describe('processCliCommands processor', () => {
]);
});
+ it('should detect the CLI node if it is nested in another node (as long as there is a first child node with a `cli` url',
+ () => {
+ const command = {
+ docType: 'cli-command',
+ name: 'command1',
+ commandAliases: ['alias1', 'alias2'],
+ options: [],
+ path: 'cli/command1',
+ };
+ const navigation = {
+ docType: 'navigation-json',
+ data: {
+ SideNav: [
+ {url: 'some/page', title: 'Some Page'},
+ {
+ title: 'CLI Commands Grandparent',
+ children: [
+ {url: 'some/nested/page', title: 'Some Nested Page'},
+ {
+ title: 'CLI Commands Parent',
+ children: [
+ {url: 'some/more/nested/page', title: 'Some More Nested Page'},
+ {
+ title: 'CLI Commands',
+ tooltip: 'Angular CLI command reference',
+ children: [{'title': 'Overview', 'url': 'cli'}],
+ },
+ {url: 'other/more/nested/page', title: 'Other More Nested Page'},
+ ],
+ },
+ {url: 'other/nested/page', title: 'Other Nested Page'},
+ ],
+ },
+ {url: 'other/page', title: 'Other Page'},
+ ],
+ },
+ };
+
+ processor.$process([command, navigation]);
+
+ const cliCommandsNode = navigation.data.SideNav[1].children[1].children[1];
+ expect(cliCommandsNode.title).toEqual('CLI Commands');
+ expect(cliCommandsNode.children).toEqual([
+ {url: 'cli', title: 'Overview'},
+ {url: 'cli/command1', title: 'ng command1'},
+ ]);
+ });
+
it('should complain if there is no child with `cli` url', () => {
const command = {
docType: 'cli-command',
diff --git a/dev-infra/pr/merge/BUILD.bazel b/dev-infra/pr/merge/BUILD.bazel
index e17c742eef..ccfb9cb507 100644
--- a/dev-infra/pr/merge/BUILD.bazel
+++ b/dev-infra/pr/merge/BUILD.bazel
@@ -1,8 +1,12 @@
load("@npm_bazel_typescript//:index.bzl", "ts_library")
+load("//tools:defaults.bzl", "jasmine_node_test")
ts_library(
name = "merge",
- srcs = glob(["**/*.ts"]),
+ srcs = glob(
+ ["**/*.ts"],
+ exclude = ["**/*.spec.ts"],
+ ),
module_name = "@angular/dev-infra-private/pr/merge",
visibility = ["//dev-infra:__subpackages__"],
deps = [
@@ -11,8 +15,37 @@ ts_library(
"@npm//@octokit/rest",
"@npm//@types/inquirer",
"@npm//@types/node",
+ "@npm//@types/node-fetch",
"@npm//@types/semver",
"@npm//@types/yargs",
"@npm//chalk",
],
)
+
+ts_library(
+ name = "test_lib",
+ testonly = True,
+ srcs = glob(["**/*.spec.ts"]),
+ deps = [
+ ":merge",
+ "//dev-infra/utils",
+ "@npm//@types/jasmine",
+ "@npm//@types/node",
+ "@npm//@types/node-fetch",
+ "@npm//nock",
+ ],
+)
+
+jasmine_node_test(
+ name = "test",
+ # Disable the Bazel patched module resolution. It always loads ".mjs" files first. This
+ # breaks NodeJS execution for "node-fetch" as it uses experimental modules which are not
+ # enabled in NodeJS. TODO: Remove this with rules_nodejs 3.x where patching is optional.
+ # https://github.com/bazelbuild/rules_nodejs/commit/7d070ffadf9c3b41711382a4737b995f987c14fa.
+ args = ["--nobazel_patch_module_resolver"],
+ deps = [
+ ":test_lib",
+ "@npm//node-fetch",
+ "@npm//semver",
+ ],
+)
diff --git a/dev-infra/pr/merge/config.ts b/dev-infra/pr/merge/config.ts
index 73222436ef..ddf4f75dcb 100644
--- a/dev-infra/pr/merge/config.ts
+++ b/dev-infra/pr/merge/config.ts
@@ -6,10 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {getConfig, GitClientConfig, NgDevConfig} from '../../utils/config';
+import {GitClientConfig, NgDevConfig} from '../../utils/config';
+import {GithubClient} from '../../utils/git/github';
import {GithubApiMergeStrategyConfig} from './strategies/api-merge';
+/** Describes possible values that can be returned for `branches` of a target label. */
+export type TargetLabelBranchResult = string[]|Promise;
+
/**
* Possible merge methods supported by the Github API.
* https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button.
@@ -27,8 +31,11 @@ export interface TargetLabel {
* List of branches a pull request with this target label should be merged into.
* Can also be wrapped in a function that accepts the target branch specified in the
* Github Web UI. This is useful for supporting labels like `target: development-branch`.
+ *
+ * @throws {InvalidTargetLabelError} Invalid label has been applied to pull request.
+ * @throws {InvalidTargetBranchError} Invalid Github target branch has been selected.
*/
- branches: string[]|((githubTargetBranch: string) => string[]);
+ branches: TargetLabelBranchResult|((githubTargetBranch: string) => TargetLabelBranchResult);
}
/**
@@ -72,12 +79,13 @@ export interface MergeConfig {
* on branch name computations. We don't want to run these immediately whenever
* the dev-infra configuration is loaded as that could slow-down other commands.
*/
-export type DevInfraMergeConfig = NgDevConfig<{'merge': () => MergeConfig}>;
+export type DevInfraMergeConfig =
+ NgDevConfig<{'merge': (api: GithubClient) => MergeConfig | Promise}>;
/** Loads and validates the merge configuration. */
-export function loadAndValidateConfig(): {config?: MergeConfigWithRemote, errors?: string[]} {
- const config: Partial = getConfig();
-
+export async function loadAndValidateConfig(
+ config: Partial,
+ api: GithubClient): Promise<{config?: MergeConfig, errors?: string[]}> {
if (config.merge === undefined) {
return {errors: ['No merge configuration found. Set the `merge` configuration.']};
}
@@ -86,22 +94,14 @@ export function loadAndValidateConfig(): {config?: MergeConfigWithRemote, errors
return {errors: ['Expected merge configuration to be defined lazily through a function.']};
}
- const mergeConfig = config.merge();
+ const mergeConfig = await config.merge(api);
const errors = validateMergeConfig(mergeConfig);
if (errors.length) {
return {errors};
}
- if (mergeConfig.remote) {
- mergeConfig.remote = {...config.github, ...mergeConfig.remote};
- } else {
- mergeConfig.remote = config.github;
- }
-
- // We always set the `remote` option, so we can safely cast the
- // config to `MergeConfigWithRemote`.
- return {config: mergeConfig as MergeConfigWithRemote};
+ return {config: mergeConfig};
}
/** Validates the specified configuration. Returns a list of failure messages. */
diff --git a/dev-infra/pr/merge/defaults/branches.ts b/dev-infra/pr/merge/defaults/branches.ts
new file mode 100644
index 0000000000..0f40a2d0a2
--- /dev/null
+++ b/dev-infra/pr/merge/defaults/branches.ts
@@ -0,0 +1,212 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import * as semver from 'semver';
+import {GithubClient} from '../../../utils/git/github';
+
+/** Type describing a Github repository with corresponding API client. */
+export interface GithubRepo {
+ /** API client that can access the repository. */
+ api: GithubClient;
+ /** Owner login of the repository. */
+ owner: string;
+ /** Name of the repository. */
+ repo: string;
+ /**
+ * NPM package representing this repository. Angular repositories usually contain
+ * multiple packages in a monorepo scheme, but packages commonly are released with
+ * the same versions. This means that a single package can be used for querying
+ * NPM about previously published versions (e.g. to determine active LTS versions).
+ * */
+ npmPackageName: string;
+}
+
+/** Type describing a version-branch. */
+export interface VersionBranch {
+ /** Name of the branch in Git. e.g. `10.0.x`. */
+ name: string;
+ /**
+ * Parsed SemVer version for the version-branch. Version branches technically do
+ * not follow the SemVer format, but we can have representative SemVer versions
+ * that can be used for comparisons, sorting and other checks.
+ */
+ parsed: semver.SemVer;
+}
+
+/** Branch name for the `next` branch. */
+export const nextBranchName = 'master';
+
+/** Regular expression that matches version-branches for a release-train. */
+const releaseTrainBranchNameRegex = /(\d+)\.(\d+)\.x/;
+
+/**
+ * Fetches the active release train and its branches for the specified major version. i.e.
+ * the latest active release-train branch name is resolved and an optional version-branch for
+ * a currently active feature-freeze/release-candidate release-train.
+ */
+export async function fetchActiveReleaseTrainBranches(
+ repo: GithubRepo, nextVersion: semver.SemVer): Promise<{
+ /**
+ * Name of the currently active release-candidate branch. Null if no
+ * feature-freeze/release-candidate is currently active.
+ */
+ releaseCandidateBranch: string | null,
+ /** Name of the latest non-prerelease version branch (i.e. the patch branch). */
+ latestVersionBranch: string
+}> {
+ const majorVersionsToConsider: number[] = [];
+ let expectedReleaseCandidateMajor: number;
+
+ // If the `next` branch (i.e. `master` branch) is for an upcoming major version, we know
+ // that there is no patch branch or feature-freeze/release-candidate branch for this major
+ // digit. If the current `next` version is the first minor of a major version, we know that
+ // the feature-freeze/release-candidate branch can only be the actual major branch. The
+ // patch branch is based on that, either the actual major branch or the last minor from the
+ // preceding major version. In all other cases, the patch branch and feature-freeze or
+ // release-candidate branch are part of the same major version. Consider the following:
+ //
+ // CASE 1. next: 11.0.0-next.0: patch and feature-freeze/release-candidate can only be
+ // most recent `10.<>.x` branches. The FF/RC branch can only be the last-minor of v10.
+ // CASE 2. next: 11.1.0-next.0: patch can be either `11.0.x` or last-minor in v10 based
+ // on whether there is a feature-freeze/release-candidate branch (=> `11.0.x`).
+ // CASE 3. next: 10.6.0-next.0: patch can be either `10.5.x` or `10.4.x` based on whether
+ // there is a feature-freeze/release-candidate branch (=> `10.5.x`)
+ if (nextVersion.minor === 0) {
+ expectedReleaseCandidateMajor = nextVersion.major - 1;
+ majorVersionsToConsider.push(nextVersion.major - 1);
+ } else if (nextVersion.minor === 1) {
+ expectedReleaseCandidateMajor = nextVersion.major;
+ majorVersionsToConsider.push(nextVersion.major, nextVersion.major - 1);
+ } else {
+ expectedReleaseCandidateMajor = nextVersion.major;
+ majorVersionsToConsider.push(nextVersion.major);
+ }
+
+ // Collect all version-branches that should be considered for the latest version-branch,
+ // or the feature-freeze/release-candidate.
+ const branches = (await getBranchesForMajorVersions(repo, majorVersionsToConsider));
+ const {latestVersionBranch, releaseCandidateBranch} =
+ await findActiveVersionBranches(repo, nextVersion, branches, expectedReleaseCandidateMajor);
+
+ if (latestVersionBranch === null) {
+ throw Error(
+ `Unable to determine the latest release-train. The following branches ` +
+ `have been considered: [${branches.join(', ')}]`);
+ }
+
+ return {releaseCandidateBranch, latestVersionBranch};
+}
+
+/** Gets the version of a given branch by reading the `package.json` upstream. */
+export async function getVersionOfBranch(
+ repo: GithubRepo, branchName: string): Promise {
+ const {data} =
+ await repo.api.repos.getContents({...repo, path: '/package.json', ref: branchName});
+ const {version} = JSON.parse(Buffer.from(data.content, 'base64').toString());
+ const parsedVersion = semver.parse(version);
+ if (parsedVersion === null) {
+ throw Error(`Invalid version detected in following branch: ${branchName}.`);
+ }
+ return parsedVersion;
+}
+
+/** Whether the given branch corresponds to a release-train branch. */
+export function isReleaseTrainBranch(branchName: string): boolean {
+ return releaseTrainBranchNameRegex.test(branchName);
+}
+
+/**
+ * Converts a given version-branch into a SemVer version that can be used with SemVer
+ * utilities. e.g. to determine semantic order, extract major digit, compare.
+ *
+ * For example `10.0.x` will become `10.0.0` in SemVer. The patch digit is not
+ * relevant but needed for parsing. SemVer does not allow `x` as patch digit.
+ */
+export function getVersionForReleaseTrainBranch(branchName: string): semver.SemVer|null {
+ // Convert a given version-branch into a SemVer version that can be used
+ // with the SemVer utilities. i.e. to determine semantic order.
+ return semver.parse(branchName.replace(releaseTrainBranchNameRegex, '$1.$2.0'));
+}
+
+/**
+ * Gets the version branches for the specified major versions in descending
+ * order. i.e. latest version branches first.
+ */
+export async function getBranchesForMajorVersions(
+ repo: GithubRepo, majorVersions: number[]): Promise {
+ const {data: branchData} = await repo.api.repos.listBranches({...repo, protected: true});
+ const branches: VersionBranch[] = [];
+
+ for (const {name} of branchData) {
+ if (!isReleaseTrainBranch(name)) {
+ continue;
+ }
+ // Convert the version-branch into a SemVer version that can be used with the
+ // SemVer utilities. e.g. to determine semantic order, compare versions.
+ const parsed = getVersionForReleaseTrainBranch(name);
+ // Collect all version-branches that match the specified major versions.
+ if (parsed !== null && majorVersions.includes(parsed.major)) {
+ branches.push({name, parsed});
+ }
+ }
+
+ // Sort captured version-branches in descending order.
+ return branches.sort((a, b) => semver.rcompare(a.parsed, b.parsed));
+}
+
+export async function findActiveVersionBranches(
+ repo: GithubRepo, nextVersion: semver.SemVer, branches: VersionBranch[],
+ expectedReleaseCandidateMajor: number): Promise<{
+ latestVersionBranch: string | null,
+ releaseCandidateBranch: string | null,
+}> {
+ let latestVersionBranch: string|null = null;
+ let releaseCandidateBranch: string|null = null;
+
+ // Iterate through the captured branches and find the latest non-prerelease branch and a
+ // potential release candidate branch. From the collected branches we iterate descending
+ // order (most recent semantic version-branch first). The first branch is either the latest
+ // active version branch (i.e. patch) or a feature-freeze/release-candidate branch. A FF/RC
+ // branch cannot be older than the latest active version-branch, so we stop iterating once
+ // we found such a branch. Otherwise, if we found a FF/RC branch, we continue looking for the
+ // next version-branch as that one is supposed to be the latest active version-branch. If it
+ // is not, then an error will be thrown due to two FF/RC branches existing at the same time.
+ for (const {name, parsed} of branches) {
+ // It can happen that version branches that are more recent than the version in the next
+ // branch (i.e. `master`) have been created. We could ignore such branches silently, but
+ // it might actually be symptomatic for an outdated version in the `next` branch, or an
+ // accidentally created branch by the caretaker. In either way we want to raise awareness.
+ if (semver.gte(parsed, nextVersion)) {
+ throw Error(
+ `Discovered unexpected version-branch that is representing a minor ` +
+ `version more recent than the one in the "${nextBranchName}" branch. Consider ` +
+ `deleting the branch, or check if the version in "${nextBranchName}" is outdated.`);
+ }
+
+ const version = await getVersionOfBranch(repo, name);
+ const isPrerelease = version.prerelease[0] === 'rc' || version.prerelease[0] === 'next';
+ if (isPrerelease) {
+ if (releaseCandidateBranch !== null) {
+ throw Error(
+ `Unable to determine latest release-train. Found two consecutive ` +
+ `branches in feature-freeze/release-candidate phase. Did not expect both "${name}" ` +
+ `and "${releaseCandidateBranch}" to be in feature-freeze/release-candidate mode.`);
+ } else if (version.major !== expectedReleaseCandidateMajor) {
+ throw Error(
+ `Discovered unexpected old feature-freeze/release-candidate branch. Expected no ` +
+ `version-branch in feature-freeze/release-candidate mode for v${version.major}.`);
+ }
+ releaseCandidateBranch = name;
+ } else {
+ latestVersionBranch = name;
+ break;
+ }
+ }
+
+ return {releaseCandidateBranch, latestVersionBranch};
+}
diff --git a/dev-infra/pr/merge/defaults/index.ts b/dev-infra/pr/merge/defaults/index.ts
new file mode 100644
index 0000000000..b633d82825
--- /dev/null
+++ b/dev-infra/pr/merge/defaults/index.ts
@@ -0,0 +1,11 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+export * from './labels';
+export * from './branches';
+export * from './lts-branch';
diff --git a/dev-infra/pr/merge/defaults/integration.spec.ts b/dev-infra/pr/merge/defaults/integration.spec.ts
new file mode 100644
index 0000000000..d085422e90
--- /dev/null
+++ b/dev-infra/pr/merge/defaults/integration.spec.ts
@@ -0,0 +1,455 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import * as nock from 'nock';
+import * as nodeFetch from 'node-fetch';
+
+import {GithubConfig} from '../../../utils/config';
+import * as console from '../../../utils/console';
+import {GithubClient} from '../../../utils/git/github';
+import {TargetLabel} from '../config';
+import {getBranchesFromTargetLabel, getTargetLabelFromPullRequest} from '../target-label';
+
+import {getDefaultTargetLabelConfiguration} from './index';
+
+const API_ENDPOINT = `https://api.github.com`;
+
+describe('default target labels', () => {
+ let api: GithubClient;
+ let config: GithubConfig;
+ let npmPackageName: string;
+
+ beforeEach(() => {
+ api = new GithubClient();
+ config = {owner: 'angular', name: 'dev-infra-test'};
+ npmPackageName = '@angular/dev-infra-test-pkg';
+
+ // The label determination will print warn messages. These should not be
+ // printed to the console, so we turn `console.warn` into a spy.
+ spyOn(console, 'warn');
+ });
+
+ afterEach(() => nock.cleanAll());
+
+ async function computeTargetLabels(): Promise {
+ return getDefaultTargetLabelConfiguration(api, config, npmPackageName);
+ }
+
+ function getRepoApiRequestUrl(): string {
+ return `${API_ENDPOINT}/repos/${config.owner}/${config.name}`;
+ }
+
+ /**
+ * Mocks a branch `package.json` version API request.
+ * https://docs.github.com/en/rest/reference/repos#get-repository-content.
+ */
+ function interceptBranchVersionRequest(branchName: string, version: string) {
+ nock(getRepoApiRequestUrl())
+ .get('/contents//package.json')
+ .query(params => params.ref === branchName)
+ .reply(200, {content: Buffer.from(JSON.stringify({version})).toString('base64')});
+ }
+
+ /** Fakes a prompt confirm question with the given value. */
+ function fakePromptConfirmValue(returnValue: boolean) {
+ spyOn(console, 'promptConfirm').and.resolveTo(returnValue);
+ }
+
+ /** Fakes a NPM package query API request. */
+ function fakeNpmPackageQueryRequest(data: unknown) {
+ // Note: We only need to mock the `json` function for a `Response`. Types
+ // would expect us to mock more functions, so we need to cast to `any`.
+ spyOn(nodeFetch, 'default').and.resolveTo({json: async () => data} as any);
+ }
+
+ /**
+ * Mocks a repository branch list API request.
+ * https://docs.github.com/en/rest/reference/repos#list-branches.
+ */
+ function interceptBranchesListRequest(branches: string[]) {
+ nock(getRepoApiRequestUrl())
+ .get('/branches')
+ .query(true)
+ .reply(200, branches.map(name => ({name})));
+ }
+
+ async function getBranchesForLabel(
+ name: string, githubTargetBranch = 'master', labels?: TargetLabel[]): Promise {
+ if (labels === undefined) {
+ labels = await computeTargetLabels();
+ }
+ const label = getTargetLabelFromPullRequest({labels}, [name]);
+ if (label === null) {
+ return null;
+ }
+ return await getBranchesFromTargetLabel(label, githubTargetBranch);
+ }
+
+ it('should detect "master" as branch for target: minor', async () => {
+ interceptBranchVersionRequest('master', '11.0.0-next.0');
+ interceptBranchVersionRequest('10.2.x', '10.2.4');
+ interceptBranchesListRequest(['10.2.x']);
+
+ expect(await getBranchesForLabel('target: minor')).toEqual(['master']);
+ });
+
+ it('should error if non version-branch is targeted with "target: lts"', async () => {
+ interceptBranchVersionRequest('master', '11.0.0-next.0');
+ interceptBranchVersionRequest('10.2.x', '10.2.4');
+ interceptBranchesListRequest(['10.2.x']);
+
+ await expectAsync(getBranchesForLabel('target: lts', 'master'))
+ .toBeRejectedWith(jasmine.objectContaining({
+ failureMessage:
+ 'PR cannot be merged as it does not target a long-term support branch: "master"'
+ }));
+ });
+
+ it('should error if patch branch is targeted with "target: lts"', async () => {
+ interceptBranchVersionRequest('master', '11.0.0-next.0');
+ interceptBranchVersionRequest('10.2.x', '10.2.4');
+ interceptBranchesListRequest(['10.2.x']);
+
+ await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
+ .toBeRejectedWith(jasmine.objectContaining({
+ failureMessage:
+ 'PR cannot be merged with "target: lts" into patch branch. Consider changing the ' +
+ 'label to "target: patch" if this is intentional.'
+ }));
+ });
+
+ it('should error if feature-freeze branch is targeted with "target: lts"', async () => {
+ interceptBranchVersionRequest('master', '11.0.0-next.0');
+ interceptBranchVersionRequest('10.2.x', '10.2.0-next.0');
+ interceptBranchVersionRequest('10.1.x', '10.1.0');
+ interceptBranchesListRequest(['10.1.x', '10.2.x']);
+
+ await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
+ .toBeRejectedWith(jasmine.objectContaining({
+ failureMessage:
+ 'PR cannot be merged with "target: lts" into feature-freeze/release-candidate branch. ' +
+ 'Consider changing the label to "target: rc" if this is intentional.'
+ }));
+ });
+
+ it('should error if release-candidate branch is targeted with "target: lts"', async () => {
+ interceptBranchVersionRequest('master', '11.0.0-next.0');
+ interceptBranchVersionRequest('10.2.x', '10.2.0-rc.0');
+ interceptBranchVersionRequest('10.1.x', '10.1.0');
+ interceptBranchesListRequest(['10.1.x', '10.2.x']);
+
+ await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
+ .toBeRejectedWith(jasmine.objectContaining({
+ failureMessage:
+ 'PR cannot be merged with "target: lts" into feature-freeze/release-candidate branch. ' +
+ 'Consider changing the label to "target: rc" if this is intentional.'
+ }));
+ });
+
+ it('should error if branch targeted with "target: lts" is no longer active', async () => {
+ interceptBranchVersionRequest('master', '11.1.0-next.0');
+ interceptBranchVersionRequest('11.0.x', '11.0.0');
+ interceptBranchVersionRequest('10.5.x', '10.5.1');
+ interceptBranchesListRequest(['10.5.x', '11.0.x']);
+
+ // We support forcibly proceeding with merging if a given branch previously was in LTS mode
+ // but no longer is (after a period of time). In this test, we are not forcibly proceeding.
+ fakePromptConfirmValue(false);
+ fakeNpmPackageQueryRequest({
+ 'dist-tags': {
+ 'v10-lts': '10.5.1',
+ },
+ 'time': {
+ // v10 has been released at the given specified date. We pick a date that
+ // guarantees that the version is no longer considered as active LTS version.
+ '10.0.0': new Date(1912, 5, 23),
+ }
+ });
+
+ await expectAsync(getBranchesForLabel('target: lts', '10.5.x'))
+ .toBeRejectedWith(jasmine.objectContaining({
+ failureMessage:
+ 'Long-term supported ended for v10 on 12/23/1913. Pull request cannot be merged ' +
+ 'into the 10.5.x branch.'
+ }));
+ });
+
+ it('should error if branch targeted with "target: lts" is not latest LTS for given major',
+ async () => {
+ interceptBranchVersionRequest('master', '11.1.0-next.0');
+ interceptBranchVersionRequest('11.0.x', '11.0.0');
+ interceptBranchVersionRequest('10.5.x', '10.5.1');
+ interceptBranchVersionRequest('10.4.x', '10.4.4');
+ interceptBranchesListRequest(['10.4.x', '10.5.x', '11.0.x']);
+
+ fakeNpmPackageQueryRequest({
+ 'dist-tags': {
+ 'v10-lts': '10.5.1',
+ }
+ });
+
+ await expectAsync(getBranchesForLabel('target: lts', '10.4.x'))
+ .toBeRejectedWith(jasmine.objectContaining({
+ failureMessage:
+ 'Not using last-minor branch for v10 LTS version. PR should be updated to ' +
+ 'target: 10.5.x'
+ }));
+ });
+
+ it('should error if branch targeted with "target: lts" is not a major version with LTS',
+ async () => {
+ interceptBranchVersionRequest('master', '11.1.0-next.0');
+ interceptBranchVersionRequest('11.0.x', '11.0.0');
+ interceptBranchVersionRequest('10.5.x', '10.5.1');
+ interceptBranchesListRequest(['10.5.x', '11.0.x']);
+
+ fakeNpmPackageQueryRequest({'dist-tags': {}});
+
+ await expectAsync(getBranchesForLabel('target: lts', '10.5.x'))
+ .toBeRejectedWith(
+ jasmine.objectContaining({failureMessage: 'No LTS version tagged for v10 in NPM.'}));
+ });
+
+ it('should allow forcibly proceeding with merge if branch targeted with "target: lts" is no ' +
+ 'longer active',
+ async () => {
+ interceptBranchVersionRequest('master', '11.1.0-next.0');
+ interceptBranchVersionRequest('11.0.x', '11.0.0');
+ interceptBranchVersionRequest('10.5.x', '10.5.1');
+ interceptBranchesListRequest(['10.5.x', '11.0.x']);
+
+ // We support forcibly proceeding with merging if a given branch previously was in LTS mode
+ // but no longer is (after a period of time). In this test, we are forcibly proceeding and
+ // expect the Github target branch to be picked up as branch for the `target: lts` label.
+ fakePromptConfirmValue(true);
+ fakeNpmPackageQueryRequest({
+ 'dist-tags': {
+ 'v10-lts': '10.5.1',
+ },
+ 'time': {
+ // v10 has been released at the given specified date. We pick a date that
+ // guarantees that the version is no longer considered as active LTS version.
+ '10.0.0': new Date(1912, 5, 23),
+ }
+ });
+
+ expect(await getBranchesForLabel('target: lts', '10.5.x')).toEqual(['10.5.x']);
+ });
+
+ it('should use target branch for "target: lts" if it matches an active LTS branch', async () => {
+ interceptBranchVersionRequest('master', '11.1.0-next.0');
+ interceptBranchVersionRequest('11.0.x', '11.0.0');
+ interceptBranchVersionRequest('10.5.x', '10.5.1');
+ interceptBranchesListRequest(['10.5.x', '11.0.x']);
+
+ spyOn(require('node-fetch'), 'default').and.callFake(() => ({
+ json: () => ({
+ 'dist-tags': {
+ 'v10-lts': '10.5.1',
+ },
+ 'time': {
+ '10.0.0': new Date().toISOString(),
+ }
+ }),
+ }));
+
+ expect(await getBranchesForLabel('target: lts', '10.5.x')).toEqual(['10.5.x']);
+ });
+
+ it('should error if no active branch for given major version could be found', async () => {
+ interceptBranchVersionRequest('master', '12.0.0-next.0');
+ interceptBranchesListRequest(['9.0.x', '9.1.x']);
+
+ await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
+ .toBeRejectedWithError(
+ 'Unable to determine the latest release-train. The following branches have ' +
+ 'been considered: []');
+ });
+
+ it('should error if invalid version is set for version-branch', async () => {
+ interceptBranchVersionRequest('master', '11.2.0-next.0');
+ interceptBranchVersionRequest('11.1.x', '11.1.x');
+ interceptBranchesListRequest(['11.1.x']);
+
+ await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
+ .toBeRejectedWithError('Invalid version detected in following branch: 11.1.x.');
+ });
+
+ it('should error if branch more recent than version in "next" branch is found', async () => {
+ interceptBranchVersionRequest('master', '11.2.0-next.0');
+ interceptBranchVersionRequest('11.2.x', '11.2.0-next.0');
+ interceptBranchVersionRequest('11.1.x', '11.1.5');
+ interceptBranchesListRequest(['11.1.x', '11.2.x']);
+
+ await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
+ .toBeRejectedWithError(
+ 'Discovered unexpected version-branch that is representing a minor version more ' +
+ 'recent than the one in the "master" branch. Consider deleting the branch, or check ' +
+ 'if the version in "master" is outdated.');
+ });
+
+ it('should allow merging PR only into patch branch with "target: patch"', async () => {
+ interceptBranchVersionRequest('master', '11.2.0-next.0');
+ interceptBranchVersionRequest('11.1.x', '11.1.0');
+ interceptBranchesListRequest(['11.1.x']);
+
+ expect(await getBranchesForLabel('target: patch', '11.1.x')).toEqual(['11.1.x']);
+ });
+
+ describe('next: major release', () => {
+ it('should detect "master" as branch for target: major', async () => {
+ interceptBranchVersionRequest('master', '11.0.0-next.0');
+ interceptBranchVersionRequest('10.2.x', '10.2.4');
+ interceptBranchesListRequest(['10.2.x']);
+
+ expect(await getBranchesForLabel('target: major')).toEqual(['master']);
+ });
+
+ describe('without active release-candidate', () => {
+ it('should detect last-minor from previous major as branch for target: patch', async () => {
+ interceptBranchVersionRequest('master', '11.0.0-next.0');
+ interceptBranchVersionRequest('10.2.x', '10.2.4');
+ interceptBranchesListRequest(['10.0.x', '10.1.x', '10.2.x']);
+
+ expect(await getBranchesForLabel('target: patch')).toEqual(['master', '10.2.x']);
+ });
+
+ it('should error if "target: rc" is applied', async () => {
+ interceptBranchVersionRequest('master', '11.0.0-next.0');
+ interceptBranchVersionRequest('10.2.x', '10.2.4');
+ interceptBranchesListRequest(['10.0.x', '10.1.x', '10.2.x']);
+
+ await expectAsync(getBranchesForLabel('target: rc'))
+ .toBeRejectedWith(jasmine.objectContaining({
+ failureMessage:
+ 'No active feature-freeze/release-candidate branch. Unable to merge ' +
+ 'pull request using "target: rc" label.'
+ }));
+ });
+ });
+
+ describe('with active release-candidate', () => {
+ it('should detect most recent non-prerelease minor branch from previous major for ' +
+ 'target: patch',
+ async () => {
+ interceptBranchVersionRequest('master', '11.0.0-next.0');
+ interceptBranchVersionRequest('10.2.x', '10.2.0-rc.0');
+ interceptBranchVersionRequest('10.1.x', '10.2.3');
+ interceptBranchesListRequest(['10.1.x', '10.2.x']);
+
+ // Pull requests should also be merged into the RC and `next` (i.e. `master`) branch.
+ expect(await getBranchesForLabel('target: patch')).toEqual([
+ 'master', '10.1.x', '10.2.x'
+ ]);
+ });
+
+ it('should detect release-candidate branch for "target: rc"', async () => {
+ interceptBranchVersionRequest('master', '11.0.0-next.0');
+ interceptBranchVersionRequest('10.2.x', '10.2.0-rc.0');
+ interceptBranchVersionRequest('10.1.x', '10.1.0');
+ interceptBranchesListRequest(['10.0.x', '10.1.x', '10.2.x']);
+
+ expect(await getBranchesForLabel('target: rc')).toEqual(['master', '10.2.x']);
+ });
+
+ it('should detect feature-freeze branch with "target: rc"', async () => {
+ interceptBranchVersionRequest('master', '11.0.0-next.0');
+ interceptBranchVersionRequest('10.2.x', '10.2.0-next.0');
+ interceptBranchVersionRequest('10.1.x', '10.1.0');
+ interceptBranchesListRequest(['10.0.x', '10.1.x', '10.2.x']);
+
+ expect(await getBranchesForLabel('target: rc')).toEqual(['master', '10.2.x']);
+ });
+
+ it('should error if multiple consecutive release-candidate branches are found', async () => {
+ interceptBranchVersionRequest('master', '11.0.0-next.0');
+ interceptBranchVersionRequest('10.4.x', '10.4.0-next.0');
+ interceptBranchVersionRequest('10.3.x', '10.4.0-rc.5');
+ interceptBranchesListRequest(['10.3.x', '10.4.x']);
+
+ await expectAsync(getBranchesForLabel('target: patch'))
+ .toBeRejectedWithError(
+ 'Unable to determine latest release-train. Found two consecutive ' +
+ 'branches in feature-freeze/release-candidate phase. Did not expect both ' +
+ '"10.3.x" and "10.4.x" to be in feature-freeze/release-candidate mode.');
+ });
+ });
+ });
+
+ describe('next: minor release', () => {
+ it('should error if "target: major" is applied', async () => {
+ interceptBranchVersionRequest('master', '11.2.0-next.0');
+ interceptBranchVersionRequest('11.1.x', '11.1.4');
+ interceptBranchesListRequest(['11.1.x']);
+
+ await expectAsync(getBranchesForLabel('target: major'))
+ .toBeRejectedWith(jasmine.objectContaining({
+ failureMessage:
+ 'Unable to merge pull request. The "master" branch will be released as ' +
+ 'a minor version.',
+ }));
+ });
+
+ describe('without active release-candidate', () => {
+ it('should detect last-minor from previous major as branch for target: patch', async () => {
+ interceptBranchVersionRequest('master', '11.2.0-next.0');
+ interceptBranchVersionRequest('11.1.x', '11.1.0');
+ interceptBranchesListRequest(['11.1.x']);
+
+ expect(await getBranchesForLabel('target: patch')).toEqual(['master', '11.1.x']);
+ });
+
+ it('should error if "target: rc" is applied', async () => {
+ interceptBranchVersionRequest('master', '11.2.0-next.0');
+ interceptBranchVersionRequest('11.1.x', '11.1.0');
+ interceptBranchesListRequest(['11.1.x']);
+
+ await expectAsync(getBranchesForLabel('target: rc'))
+ .toBeRejectedWith(jasmine.objectContaining({
+ failureMessage:
+ 'No active feature-freeze/release-candidate branch. Unable to merge pull ' +
+ 'request using "target: rc" label.'
+ }));
+ });
+ });
+
+ describe('with active release-candidate', () => {
+ it('should detect most recent non-prerelease minor branch from previous major for ' +
+ 'target: patch',
+ async () => {
+ interceptBranchVersionRequest('master', '11.2.0-next.0');
+ interceptBranchVersionRequest('11.1.x', '11.1.0-rc.0');
+ interceptBranchVersionRequest('11.0.x', '11.0.0');
+ interceptBranchesListRequest(['11.0.x', '11.1.x']);
+
+ // Pull requests should also be merged into the RC and `next` (i.e. `master`) branch.
+ expect(await getBranchesForLabel('target: patch')).toEqual([
+ 'master', '11.0.x', '11.1.x'
+ ]);
+ });
+
+ it('should detect release-candidate branch for "target: rc"', async () => {
+ interceptBranchVersionRequest('master', '11.2.0-next.0');
+ interceptBranchVersionRequest('11.1.x', '11.1.0-rc.0');
+ interceptBranchVersionRequest('11.0.x', '10.0.0');
+ interceptBranchesListRequest(['11.0.x', '11.1.x']);
+
+ expect(await getBranchesForLabel('target: rc')).toEqual(['master', '11.1.x']);
+ });
+
+ it('should detect feature-freeze branch with "target: rc"', async () => {
+ interceptBranchVersionRequest('master', '11.2.0-next.0');
+ interceptBranchVersionRequest('11.1.x', '11.1.0-next.0');
+ interceptBranchVersionRequest('11.0.x', '10.0.0');
+ interceptBranchesListRequest(['11.0.x', '11.1.x']);
+
+ expect(await getBranchesForLabel('target: rc')).toEqual(['master', '11.1.x']);
+ });
+ });
+ });
+});
diff --git a/dev-infra/pr/merge/defaults/labels.ts b/dev-infra/pr/merge/defaults/labels.ts
new file mode 100644
index 0000000000..1ef039bdb9
--- /dev/null
+++ b/dev-infra/pr/merge/defaults/labels.ts
@@ -0,0 +1,124 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {GithubConfig} from '../../../utils/config';
+import {GithubClient} from '../../../utils/git/github';
+import {TargetLabel} from '../config';
+import {InvalidTargetBranchError, InvalidTargetLabelError} from '../target-label';
+
+import {fetchActiveReleaseTrainBranches, getVersionOfBranch, GithubRepo, isReleaseTrainBranch, nextBranchName} from './branches';
+import {assertActiveLtsBranch} from './lts-branch';
+
+/**
+ * Gets a label configuration for the merge tooling that reflects the default Angular
+ * organization-wide labeling and branching semantics as outlined in the specification.
+ *
+ * https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU
+ */
+export async function getDefaultTargetLabelConfiguration(
+ api: GithubClient, github: GithubConfig, npmPackageName: string): Promise {
+ const repo: GithubRepo = {owner: github.owner, repo: github.name, api, npmPackageName};
+ const nextVersion = await getVersionOfBranch(repo, nextBranchName);
+ const hasNextMajorTrain = nextVersion.minor === 0;
+ const {latestVersionBranch, releaseCandidateBranch} =
+ await fetchActiveReleaseTrainBranches(repo, nextVersion);
+
+ return [
+ {
+ pattern: 'target: major',
+ branches: () => {
+ // If `next` is currently not designated to be a major version, we do not
+ // allow merging of PRs with `target: major`.
+ if (!hasNextMajorTrain) {
+ throw new InvalidTargetLabelError(
+ `Unable to merge pull request. The "${nextBranchName}" branch will be ` +
+ `released as a minor version.`);
+ }
+ return [nextBranchName];
+ },
+ },
+ {
+ pattern: 'target: minor',
+ // Changes labeled with `target: minor` are merged most commonly into the next branch
+ // (i.e. `master`). In rare cases of an exceptional minor version while being already
+ // on a major release train, this would need to be overridden manually.
+ // TODO: Consider handling this automatically by checking if the NPM version matches
+ // the last-minor. If not, then an exceptional minor might be in progress. See:
+ // https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU/edit#heading=h.h7o5pjq6yqd0
+ branches: [nextBranchName],
+ },
+ {
+ pattern: 'target: patch',
+ branches: githubTargetBranch => {
+ // If a PR is targeting the latest active version-branch through the Github UI,
+ // and is also labeled with `target: patch`, then we merge it directly into the
+ // branch without doing any cherry-picking. This is useful if a PR could not be
+ // applied cleanly, and a separate PR for the patch branch has been created.
+ if (githubTargetBranch === latestVersionBranch) {
+ return [latestVersionBranch];
+ }
+ // Otherwise, patch changes are always merged into the next and patch branch.
+ const branches = [nextBranchName, latestVersionBranch];
+ // Additionally, if there is a release-candidate/feature-freeze release-train
+ // currently active, also merge the PR into that version-branch.
+ if (releaseCandidateBranch !== null) {
+ branches.push(releaseCandidateBranch);
+ }
+ return branches;
+ }
+ },
+ {
+ pattern: 'target: rc',
+ branches: githubTargetBranch => {
+ // The `target: rc` label cannot be applied if there is no active feature-freeze
+ // or release-candidate release train.
+ if (releaseCandidateBranch === null) {
+ throw new InvalidTargetLabelError(
+ `No active feature-freeze/release-candidate branch. ` +
+ `Unable to merge pull request using "target: rc" label.`);
+ }
+ // If the PR is targeting the active release-candidate/feature-freeze version branch
+ // directly through the Github UI and has the `target: rc` label applied, merge it
+ // only into the release candidate branch. This is useful if a PR did not apply cleanly
+ // into the release-candidate/feature-freeze branch, and a separate PR has been created.
+ if (githubTargetBranch === releaseCandidateBranch) {
+ return [releaseCandidateBranch];
+ }
+ // Otherwise, merge into the next and active release-candidate/feature-freeze branch.
+ return [nextBranchName, releaseCandidateBranch];
+ },
+ },
+ {
+ // LTS changes are rare enough that we won't worry about cherry-picking changes into all
+ // active LTS branches for PRs created against any other branch. Instead, PR authors need
+ // to manually create separate PRs for desired LTS branches. Additionally, active LT branches
+ // commonly diverge quickly. This makes cherry-picking not an option for LTS changes.
+ pattern: 'target: lts',
+ branches: async githubTargetBranch => {
+ if (!isReleaseTrainBranch(githubTargetBranch)) {
+ throw new InvalidTargetBranchError(
+ `PR cannot be merged as it does not target a long-term support ` +
+ `branch: "${githubTargetBranch}"`);
+ }
+ if (githubTargetBranch === latestVersionBranch) {
+ throw new InvalidTargetBranchError(
+ `PR cannot be merged with "target: lts" into patch branch. ` +
+ `Consider changing the label to "target: patch" if this is intentional.`);
+ }
+ if (githubTargetBranch === releaseCandidateBranch && releaseCandidateBranch !== null) {
+ throw new InvalidTargetBranchError(
+ `PR cannot be merged with "target: lts" into feature-freeze/release-candidate ` +
+ `branch. Consider changing the label to "target: rc" if this is intentional.`);
+ }
+ // Assert that the selected branch is an active LTS branch.
+ await assertActiveLtsBranch(repo, githubTargetBranch);
+ return [githubTargetBranch];
+ },
+ },
+ ];
+}
diff --git a/dev-infra/pr/merge/defaults/lts-branch.ts b/dev-infra/pr/merge/defaults/lts-branch.ts
new file mode 100644
index 0000000000..5db07f8ad4
--- /dev/null
+++ b/dev-infra/pr/merge/defaults/lts-branch.ts
@@ -0,0 +1,80 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import fetch from 'node-fetch';
+import * as semver from 'semver';
+
+import {promptConfirm, red, warn, yellow} from '../../../utils/console';
+import {InvalidTargetBranchError} from '../target-label';
+
+import {getVersionOfBranch, GithubRepo} from './branches';
+
+/**
+ * Number of months a major version in Angular is actively supported. See:
+ * https://angular.io/guide/releases#support-policy-and-schedule.
+ */
+const majorActiveSupportDuration = 6;
+
+/**
+ * Number of months a major version has active long-term support. See:
+ * https://angular.io/guide/releases#support-policy-and-schedule.
+ */
+const majorActiveTermSupportDuration = 12;
+
+/**
+ * Asserts that the given branch corresponds to an active LTS version-branch that can receive
+ * backported fixes. Throws an error if LTS expired or an invalid branch is selected.
+ */
+export async function assertActiveLtsBranch(repo: GithubRepo, branchName: string) {
+ const version = await getVersionOfBranch(repo, branchName);
+ const {'dist-tags': distTags, time} =
+ await (await fetch(`https://registry.npmjs.org/${repo.npmPackageName}`)).json();
+
+ // LTS versions should be tagged in NPM in the following format: `v{major}-lts`.
+ const ltsVersion = semver.parse(distTags[`v${version.major}-lts`]);
+
+ // Ensure that there is a LTS version tagged for the given version-branch major. e.g.
+ // if the version branch is `9.2.x` then we want to make sure that there is a LTS
+ // version tagged in NPM for `v9`, following the `v{major}-lts` tag convention.
+ if (ltsVersion === null) {
+ throw new InvalidTargetBranchError(`No LTS version tagged for v${version.major} in NPM.`);
+ }
+
+ // Ensure that the correct branch is used for the LTS version. We do not want to merge
+ // changes to older minor version branches that do not reflect the current LTS version.
+ if (branchName !== `${ltsVersion.major}.${ltsVersion.minor}.x`) {
+ throw new InvalidTargetBranchError(
+ `Not using last-minor branch for v${version.major} LTS version. PR ` +
+ `should be updated to target: ${ltsVersion.major}.${ltsVersion.minor}.x`);
+ }
+
+ const today = new Date();
+ const releaseDate = new Date(time[`${version.major}.0.0`]);
+ const ltsEndDate = new Date(
+ releaseDate.getFullYear(),
+ releaseDate.getMonth() + majorActiveSupportDuration + majorActiveTermSupportDuration,
+ releaseDate.getDate(), releaseDate.getHours(), releaseDate.getMinutes(),
+ releaseDate.getSeconds(), releaseDate.getMilliseconds());
+
+ // Check if LTS has already expired for the targeted major version. If so, we do not
+ // allow the merge as per our LTS guarantees. Can be forcibly overridden if desired.
+ // See: https://angular.io/guide/releases#support-policy-and-schedule.
+ if (today > ltsEndDate) {
+ const ltsEndDateText = ltsEndDate.toLocaleDateString();
+ warn(red(`Long-term support ended for v${version.major} on ${ltsEndDateText}.`));
+ warn(yellow(
+ `Merging of pull requests for this major is generally not ` +
+ `desired, but can be forcibly ignored.`));
+ if (await promptConfirm('Do you want to forcibly proceed with merging?')) {
+ return;
+ }
+ throw new InvalidTargetBranchError(
+ `Long-term supported ended for v${version.major} on ${ltsEndDateText}. ` +
+ `Pull request cannot be merged into the ${branchName} branch.`);
+ }
+}
diff --git a/dev-infra/pr/merge/determine-merge-branches.ts b/dev-infra/pr/merge/determine-merge-branches.ts
deleted file mode 100644
index bf4a479caa..0000000000
--- a/dev-infra/pr/merge/determine-merge-branches.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * @license
- * Copyright Google LLC All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.io/license
- */
-
-import * as semver from 'semver';
-import {exec} from '../../utils/shelljs';
-
-/**
- * Helper function that can be used to determine merge branches based on a given
- * project version. The function determines merge branches primarily through the
- * specified version, but falls back to consulting the NPM registry when needed.
- *
- * Consulting the NPM registry for determining the patch branch may slow down merging,
- * so whenever possible, the branches are determined statically based on the current
- * version. In some cases, consulting the NPM registry is inevitable because for major
- * pre-releases, we cannot determine the latest stable minor version from the current
- * pre-release version.
- */
-export function determineMergeBranches(
- currentVersion: string, npmPackageName: string): {minor: string, patch: string} {
- const projectVersion = semver.parse(currentVersion);
- if (projectVersion === null) {
- throw Error('Cannot parse version set in project "package.json" file.');
- }
- const {major, minor, patch, prerelease} = projectVersion;
- const isMajor = minor === 0 && patch === 0;
- const isMinor = minor !== 0 && patch === 0;
-
- // If there is no prerelease, then we compute patch and minor branches based
- // on the current version major and minor.
- if (prerelease.length === 0) {
- return {minor: `${major}.x`, patch: `${major}.${minor}.x`};
- }
-
- // If current version is set to a minor prerelease, we can compute the merge branches
- // statically. e.g. if we are set to `9.3.0-next.0`, then our merge branches should
- // be set to `9.x` and `9.2.x`.
- if (isMinor) {
- return {minor: `${major}.x`, patch: `${major}.${minor - 1}.x`};
- } else if (!isMajor) {
- throw Error('Unexpected version. Cannot have prerelease for patch version.');
- }
-
- // If we are set to a major prerelease, we cannot statically determine the stable patch
- // branch (as the latest minor segment is unknown). We determine it by looking in the NPM
- // registry for the latest stable release that will tell us about the current minor segment.
- // e.g. if the current major is `v10.0.0-next.0`, then we need to look for the latest release.
- // Let's say this is `v9.2.6`. Our patch branch will then be called `9.2.x`.
- const latestVersion = exec(`yarn -s info ${npmPackageName} dist-tags.latest`).trim();
- if (!latestVersion) {
- throw Error('Could not determine version of latest release.');
- }
- const expectedMajor = major - 1;
- const parsedLatestVersion = semver.parse(latestVersion);
- if (parsedLatestVersion === null) {
- throw Error(`Could not parse latest version from NPM registry: ${latestVersion}`);
- } else if (parsedLatestVersion.major !== expectedMajor) {
- throw Error(
- `Expected latest release to have major version: v${expectedMajor}, ` +
- `but got: v${latestVersion}`);
- }
-
- return {patch: `${expectedMajor}.${parsedLatestVersion.minor}.x`, minor: `${expectedMajor}.x`};
-}
diff --git a/dev-infra/pr/merge/index.ts b/dev-infra/pr/merge/index.ts
index 1b43aaf32b..816aa679d2 100644
--- a/dev-infra/pr/merge/index.ts
+++ b/dev-infra/pr/merge/index.ts
@@ -7,11 +7,12 @@
*/
-import {getRepoBaseDir} from '../../utils/config';
+import {getConfig, getRepoBaseDir} from '../../utils/config';
import {error, green, info, promptConfirm, red, yellow} from '../../utils/console';
-import {GithubApiRequestError} from '../../utils/git';
+import {GitClient} from '../../utils/git';
+import {GithubApiRequestError} from '../../utils/git/github';
-import {loadAndValidateConfig, MergeConfigWithRemote} from './config';
+import {loadAndValidateConfig, MergeConfig, MergeConfigWithRemote} from './config';
import {MergeResult, MergeStatus, PullRequestMergeTask} from './task';
/** URL to the Github page where personal access tokens can be generated. */
@@ -34,19 +35,7 @@ export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`;
export async function mergePullRequest(
prNumber: number, githubToken: string, projectRoot: string = getRepoBaseDir(),
config?: MergeConfigWithRemote) {
- // If no explicit configuration has been specified, we load and validate
- // the configuration from the shared dev-infra configuration.
- if (config === undefined) {
- const {config: _config, errors} = loadAndValidateConfig();
- if (errors) {
- error(red('Invalid configuration:'));
- errors.forEach(desc => error(yellow(` - ${desc}`)));
- process.exit(1);
- }
- config = _config!;
- }
-
- const api = new PullRequestMergeTask(projectRoot, config, githubToken);
+ const api = await createPullRequestMergeTask(githubToken, projectRoot, config);
// Perform the merge. Force mode can be activated through a command line flag.
// Alternatively, if the merge fails with non-fatal failures, the script
@@ -132,3 +121,33 @@ export async function mergePullRequest(
}
}
}
+
+/**
+ * Creates the pull request merge task from the given Github token, project root
+ * and optional explicit configuration. An explicit configuration can be specified
+ * when the merge script is used outside of a `ng-dev` configured repository.
+ */
+async function createPullRequestMergeTask(
+ githubToken: string, projectRoot: string, explicitConfig?: MergeConfigWithRemote) {
+ if (explicitConfig !== undefined) {
+ const git = new GitClient(githubToken, {github: explicitConfig.remote}, projectRoot);
+ return new PullRequestMergeTask(explicitConfig, git);
+ }
+
+ const devInfraConfig = getConfig();
+ const git = new GitClient(githubToken, devInfraConfig, projectRoot);
+ const {config, errors} = await loadAndValidateConfig(devInfraConfig, git.github);
+
+ if (errors) {
+ error(red('Invalid merge configuration:'));
+ errors.forEach(desc => error(yellow(` - ${desc}`)));
+ process.exit(1);
+ }
+
+ // Set the remote so that the merge tool has access to information about
+ // the remote it intends to merge to.
+ config!.remote = devInfraConfig.github;
+ // We can cast this to a merge config with remote because we always set the
+ // remote above.
+ return new PullRequestMergeTask(config! as MergeConfigWithRemote, git);
+}
diff --git a/dev-infra/pr/merge/pull-request.ts b/dev-infra/pr/merge/pull-request.ts
index 406105c5a4..40e1d646a9 100644
--- a/dev-infra/pr/merge/pull-request.ts
+++ b/dev-infra/pr/merge/pull-request.ts
@@ -12,7 +12,7 @@ import {GitClient} from '../../utils/git';
import {PullRequestFailure} from './failures';
import {matchesPattern} from './string-pattern';
-import {getBranchesFromTargetLabel, getTargetLabelFromPullRequest} from './target-label';
+import {getBranchesFromTargetLabel, getTargetLabelFromPullRequest, InvalidTargetBranchError, InvalidTargetLabelError} from './target-label';
import {PullRequestMergeTask} from './task';
/** Interface that describes a pull request. */
@@ -83,6 +83,20 @@ export async function loadAndValidatePullRequest(
labels.some(name => matchesPattern(name, config.commitMessageFixupLabel));
const hasCaretakerNote = !!config.caretakerNoteLabel &&
labels.some(name => matchesPattern(name, config.caretakerNoteLabel!));
+ let targetBranches: string[];
+
+ // If branches are determined for a given target label, capture errors that are
+ // thrown as part of branch computation. This is expected because a merge configuration
+ // can lazily compute branches for a target label and throw. e.g. if an invalid target
+ // label is applied, we want to exit the script gracefully with an error message.
+ try {
+ targetBranches = await getBranchesFromTargetLabel(targetLabel, githubTargetBranch);
+ } catch (error) {
+ if (error instanceof InvalidTargetBranchError || error instanceof InvalidTargetLabelError) {
+ return new PullRequestFailure(error.failureMessage);
+ }
+ throw error;
+ }
return {
url: prData.html_url,
@@ -92,8 +106,8 @@ export async function loadAndValidatePullRequest(
githubTargetBranch,
needsCommitMessageFixup,
hasCaretakerNote,
+ targetBranches,
title: prData.title,
- targetBranches: getBranchesFromTargetLabel(targetLabel, githubTargetBranch),
commitCount: prData.commits,
};
}
diff --git a/dev-infra/pr/merge/target-label.ts b/dev-infra/pr/merge/target-label.ts
index dec769b1ee..3a83fc4e89 100644
--- a/dev-infra/pr/merge/target-label.ts
+++ b/dev-infra/pr/merge/target-label.ts
@@ -9,9 +9,25 @@
import {MergeConfig, TargetLabel} from './config';
import {matchesPattern} from './string-pattern';
+/**
+ * Unique error that can be thrown in the merge configuration if an
+ * invalid branch is targeted.
+ */
+export class InvalidTargetBranchError {
+ constructor(public failureMessage: string) {}
+}
+
+/**
+ * Unique error that can be thrown in the merge configuration if an
+ * invalid label has been applied to a pull request.
+ */
+export class InvalidTargetLabelError {
+ constructor(public failureMessage: string) {}
+}
+
/** Gets the target label from the specified pull request labels. */
-export function getTargetLabelFromPullRequest(config: MergeConfig, labels: string[]): TargetLabel|
- null {
+export function getTargetLabelFromPullRequest(
+ config: Pick, labels: string[]): TargetLabel|null {
for (const label of labels) {
const match = config.labels.find(({pattern}) => matchesPattern(label, pattern));
if (match !== undefined) {
@@ -21,8 +37,14 @@ export function getTargetLabelFromPullRequest(config: MergeConfig, labels: strin
return null;
}
-/** Gets the branches from the specified target label. */
-export function getBranchesFromTargetLabel(
- label: TargetLabel, githubTargetBranch: string): string[] {
- return typeof label.branches === 'function' ? label.branches(githubTargetBranch) : label.branches;
+/**
+ * Gets the branches from the specified target label.
+ *
+ * @throws {InvalidTargetLabelError} Invalid label has been applied to pull request.
+ * @throws {InvalidTargetBranchError} Invalid Github target branch has been selected.
+ */
+export async function getBranchesFromTargetLabel(
+ label: TargetLabel, githubTargetBranch: string): Promise {
+ return typeof label.branches === 'function' ? await label.branches(githubTargetBranch) :
+ await label.branches;
}
diff --git a/dev-infra/pr/merge/task.ts b/dev-infra/pr/merge/task.ts
index 7b9a76ead2..4d20d2a338 100644
--- a/dev-infra/pr/merge/task.ts
+++ b/dev-infra/pr/merge/task.ts
@@ -9,7 +9,7 @@
import {promptConfirm} from '../../utils/console';
import {GitClient, GitCommandError} from '../../utils/git';
-import {MergeConfigWithRemote} from './config';
+import {MergeConfig, MergeConfigWithRemote} from './config';
import {PullRequestFailure} from './failures';
import {getCaretakerNotePromptMessage} from './messages';
import {isPullRequest, loadAndValidatePullRequest,} from './pull-request';
@@ -40,12 +40,7 @@ export interface MergeResult {
* labels that have been resolved through the merge script configuration.
*/
export class PullRequestMergeTask {
- /** Git client that can be used to execute Git commands. */
- git = new GitClient(this._githubToken, {github: this.config.remote});
-
- constructor(
- public projectRoot: string, public config: MergeConfigWithRemote,
- private _githubToken: string) {}
+ constructor(public config: MergeConfigWithRemote, public git: GitClient) {}
/**
* Merges the given pull request and pushes it upstream.
diff --git a/dev-infra/tmpl-package.json b/dev-infra/tmpl-package.json
index 5ff632c496..0b3048a665 100644
--- a/dev-infra/tmpl-package.json
+++ b/dev-infra/tmpl-package.json
@@ -19,6 +19,7 @@
"inquirer": "",
"minimatch": "",
"multimatch": "",
+ "node-fetch": "",
"node-uuid": "",
"semver": "",
"shelljs": "",
diff --git a/dev-infra/utils/git/_github.ts b/dev-infra/utils/git/github.ts
similarity index 81%
rename from dev-infra/utils/git/_github.ts
rename to dev-infra/utils/git/github.ts
index b467dcb9b4..85d0614358 100644
--- a/dev-infra/utils/git/_github.ts
+++ b/dev-infra/utils/git/github.ts
@@ -6,13 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
-/****************************************************************************
- ****************************************************************************
- ** DO NOT IMPORT THE GithubClient DIRECTLY, INSTEAD IMPORT GitClient from **
- ** ./index.ts and access the GithubClient via the `.github` member. **
- ****************************************************************************
- ****************************************************************************/
-
import {graphql} from '@octokit/graphql';
import * as Octokit from '@octokit/rest';
import {RequestParameters} from '@octokit/types';
@@ -28,10 +21,10 @@ export class GithubApiRequestError extends Error {
/**
* A Github client for interacting with the Github APIs.
*
- * Additionally, provides convienience methods for actions which require multiple requests, or
+ * Additionally, provides convenience methods for actions which require multiple requests, or
* would provide value from memoized style responses.
**/
-export class _GithubClient extends Octokit {
+export class GithubClient extends Octokit {
/** The Github GraphQL (v4) API. */
graqhql: GithubGraphqlClient;
diff --git a/dev-infra/utils/git/index.ts b/dev-infra/utils/git/index.ts
index 88c626dc20..030a25c4ca 100644
--- a/dev-infra/utils/git/index.ts
+++ b/dev-infra/utils/git/index.ts
@@ -11,10 +11,7 @@ import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process';
import {getConfig, getRepoBaseDir, NgDevConfig} from '../config';
import {info, yellow} from '../console';
-import {_GithubClient} from './_github';
-
-// Re-export GithubApiRequestError
-export {GithubApiRequestError} from './_github';
+import {GithubClient} from './github';
/** Github response type extended to include the `x-oauth-scopes` headers presence. */
type RateLimitResponseWithOAuthScopeHeader = Octokit.Response&{
@@ -54,10 +51,8 @@ export class GitClient {
`https://${this._githubToken}@github.com/${this.remoteConfig.owner}/${
this.remoteConfig.name}.git`;
/** Instance of the authenticated Github octokit API. */
- github = new _GithubClient(this._githubToken);
+ github = new GithubClient(this._githubToken);
- /** The file path of project's root directory. */
- private _projectRoot = getRepoBaseDir();
/** The OAuth scopes available for the provided Github token. */
private _oauthScopes: Promise|null = null;
/**
@@ -67,7 +62,8 @@ export class GitClient {
private _githubTokenRegex: RegExp|null = null;
constructor(
- private _githubToken?: string, private _config: Pick = getConfig()) {
+ private _githubToken?: string, private _config: Pick = getConfig(),
+ private _projectRoot = getRepoBaseDir()) {
// If a token has been specified (and is not empty), pass it to the Octokit API and
// also create a regular expression that can be used for sanitizing Git command output
// so that it does not print the token accidentally.
diff --git a/goldens/circular-deps/packages.json b/goldens/circular-deps/packages.json
index c34ed8bc11..924b196222 100644
--- a/goldens/circular-deps/packages.json
+++ b/goldens/circular-deps/packages.json
@@ -223,8 +223,23 @@
"packages/core/src/render3/assert.ts",
"packages/core/src/render3/interfaces/container.ts",
"packages/core/src/render3/interfaces/node.ts",
- "packages/core/src/render3/interfaces/definition.ts",
- "packages/core/src/core.ts",
+ "packages/core/src/render3/interfaces/view.ts",
+ "packages/core/src/di/injector.ts",
+ "packages/core/src/di/r3_injector.ts",
+ "packages/core/src/render3/definition.ts",
+ "packages/core/src/metadata/ng_module.ts"
+ ],
+ [
+ "packages/core/src/application_ref.ts",
+ "packages/core/src/application_tokens.ts",
+ "packages/core/src/linker/component_factory.ts",
+ "packages/core/src/change_detection/change_detection.ts",
+ "packages/core/src/change_detection/change_detector_ref.ts",
+ "packages/core/src/render3/view_engine_compatibility.ts",
+ "packages/core/src/render3/assert.ts",
+ "packages/core/src/render3/interfaces/container.ts",
+ "packages/core/src/render3/interfaces/node.ts",
+ "packages/core/src/render3/interfaces/view.ts",
"packages/core/src/metadata.ts",
"packages/core/src/di.ts",
"packages/core/src/di/index.ts",
@@ -247,25 +262,9 @@
"packages/core/src/render3/assert.ts",
"packages/core/src/render3/interfaces/container.ts",
"packages/core/src/render3/interfaces/node.ts",
- "packages/core/src/render3/interfaces/definition.ts",
"packages/core/src/render3/interfaces/view.ts",
- "packages/core/src/di/injector.ts",
- "packages/core/src/di/r3_injector.ts",
- "packages/core/src/render3/definition.ts",
- "packages/core/src/metadata/ng_module.ts"
- ],
- [
- "packages/core/src/application_ref.ts",
- "packages/core/src/application_tokens.ts",
- "packages/core/src/linker/component_factory.ts",
- "packages/core/src/change_detection/change_detection.ts",
- "packages/core/src/change_detection/change_detector_ref.ts",
- "packages/core/src/render3/view_engine_compatibility.ts",
- "packages/core/src/render3/assert.ts",
- "packages/core/src/render3/interfaces/container.ts",
- "packages/core/src/render3/interfaces/node.ts",
"packages/core/src/render3/interfaces/definition.ts",
- "packages/core/src/render3/interfaces/view.ts",
+ "packages/core/src/core.ts",
"packages/core/src/metadata.ts",
"packages/core/src/di.ts",
"packages/core/src/di/index.ts",
@@ -1766,27 +1765,25 @@
[
"packages/core/src/render3/interfaces/container.ts",
"packages/core/src/render3/interfaces/node.ts",
- "packages/core/src/render3/interfaces/definition.ts",
"packages/core/src/render3/interfaces/view.ts"
],
[
"packages/core/src/render3/interfaces/definition.ts",
- "packages/core/src/render3/interfaces/node.ts"
+ "packages/core/src/render3/interfaces/node.ts",
+ "packages/core/src/render3/interfaces/view.ts"
],
[
"packages/core/src/render3/interfaces/definition.ts",
"packages/core/src/render3/interfaces/view.ts"
],
[
- "packages/core/src/render3/interfaces/definition.ts",
- "packages/core/src/render3/interfaces/view.ts",
- "packages/core/src/render3/interfaces/node.ts"
+ "packages/core/src/render3/interfaces/node.ts",
+ "packages/core/src/render3/interfaces/view.ts"
],
[
- "packages/core/src/render3/interfaces/definition.ts",
+ "packages/core/src/render3/interfaces/node.ts",
"packages/core/src/render3/interfaces/view.ts",
- "packages/core/src/render3/interfaces/query.ts",
- "packages/core/src/render3/interfaces/node.ts"
+ "packages/core/src/render3/interfaces/query.ts"
],
[
"packages/core/src/render3/interfaces/query.ts",
@@ -1858,11 +1855,6 @@
"packages/forms/src/directives/ng_model.ts",
"packages/forms/src/directives/ng_model_group.ts"
],
- [
- "packages/forms/src/directives/normalize_validator.ts",
- "packages/forms/src/model.ts",
- "packages/forms/src/directives/shared.ts"
- ],
[
"packages/forms/src/directives/reactive_directives/form_control_directive.ts",
"packages/forms/src/directives/shared.ts",
diff --git a/goldens/public-api/core/testing/testing.d.ts b/goldens/public-api/core/testing/testing.d.ts
index b2ef64a7d7..8144bc5d30 100644
--- a/goldens/public-api/core/testing/testing.d.ts
+++ b/goldens/public-api/core/testing/testing.d.ts
@@ -1,6 +1,7 @@
/** @codeGenApi */
export declare const __core_private_testing_placeholder__ = "";
+/** @deprecated */
export declare function async(fn: Function): (done: any) => any;
export declare class ComponentFixture {
@@ -141,5 +142,7 @@ export declare function tick(millis?: number, tickOptions?: {
processNewMacroTasksSynchronously: boolean;
}): void;
+export declare function waitForAsync(fn: Function): (done: any) => any;
+
export declare function withModule(moduleDef: TestModuleMetadata): InjectSetupWrapper;
export declare function withModule(moduleDef: TestModuleMetadata, fn: Function): () => any;
diff --git a/goldens/size-tracking/aio-payloads.json b/goldens/size-tracking/aio-payloads.json
index b91c2891d6..8ebdb7da24 100755
--- a/goldens/size-tracking/aio-payloads.json
+++ b/goldens/size-tracking/aio-payloads.json
@@ -21,9 +21,9 @@
"master": {
"uncompressed": {
"runtime-es2015": 3097,
- "main-es2015": 429885,
+ "main-es2015": 430239,
"polyfills-es2015": 52195
}
}
}
-}
\ No newline at end of file
+}
diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json
index 6535e3d4b6..9a036944cf 100644
--- a/goldens/size-tracking/integration-payloads.json
+++ b/goldens/size-tracking/integration-payloads.json
@@ -49,9 +49,9 @@
"master": {
"uncompressed": {
"runtime-es2015": 2289,
- "main-es2015": 221897,
- "polyfills-es2015": 36938,
- "5-es2015": 779
+ "main-es2015": 221939,
+ "polyfills-es2015": 36723,
+ "5-es2015": 781
}
}
},
@@ -62,8 +62,8 @@
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
"bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338",
"bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info",
- "bundle": 1209659
+ "bundle": 1213130
}
}
}
-}
\ No newline at end of file
+}
diff --git a/integration/cli-hello-world-ivy-compat/src/app/app.component.spec.ts b/integration/cli-hello-world-ivy-compat/src/app/app.component.spec.ts
index 249cf07d32..bde049e345 100644
--- a/integration/cli-hello-world-ivy-compat/src/app/app.component.spec.ts
+++ b/integration/cli-hello-world-ivy-compat/src/app/app.component.spec.ts
@@ -1,13 +1,13 @@
-import { TestBed, async } from '@angular/core/testing';
-import { AppComponent } from './app.component';
+import {TestBed, waitForAsync} from '@angular/core/testing';
+import {AppComponent} from './app.component';
describe('AppComponent', () => {
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [
- AppComponent
- ],
- }).compileComponents();
+ beforeEach(waitForAsync(() => {
+ TestBed
+ .configureTestingModule({
+ declarations: [AppComponent],
+ })
+ .compileComponents();
}));
it('should create the app', () => {
@@ -26,6 +26,7 @@ describe('AppComponent', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
- expect(compiled.querySelector('.content span').textContent).toContain('cli-hello-world-ivy-compat app is running!');
+ expect(compiled.querySelector('.content span').textContent)
+ .toContain('cli-hello-world-ivy-compat app is running!');
});
});
diff --git a/integration/cli-hello-world-ivy-minimal/src/app/app.component.spec.ts b/integration/cli-hello-world-ivy-minimal/src/app/app.component.spec.ts
index 5f6eb96611..0efa90fe20 100644
--- a/integration/cli-hello-world-ivy-minimal/src/app/app.component.spec.ts
+++ b/integration/cli-hello-world-ivy-minimal/src/app/app.component.spec.ts
@@ -1,13 +1,13 @@
-import { TestBed, async } from '@angular/core/testing';
-import { AppComponent } from './app.component';
+import {TestBed, waitForAsync} from '@angular/core/testing';
+import {AppComponent} from './app.component';
describe('AppComponent', () => {
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [
- AppComponent
- ],
- }).compileComponents();
+ beforeEach(waitForAsync(() => {
+ TestBed
+ .configureTestingModule({
+ declarations: [AppComponent],
+ })
+ .compileComponents();
}));
it('should create the app', () => {
diff --git a/integration/cli-hello-world-lazy-rollup/src/app/app.component.spec.ts b/integration/cli-hello-world-lazy-rollup/src/app/app.component.spec.ts
index deeee48471..3275ebecc7 100644
--- a/integration/cli-hello-world-lazy-rollup/src/app/app.component.spec.ts
+++ b/integration/cli-hello-world-lazy-rollup/src/app/app.component.spec.ts
@@ -1,17 +1,15 @@
-import { TestBed, async } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-import { AppComponent } from './app.component';
+import {TestBed, waitForAsync} from '@angular/core/testing';
+import {RouterTestingModule} from '@angular/router/testing';
+import {AppComponent} from './app.component';
describe('AppComponent', () => {
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- imports: [
- RouterTestingModule
- ],
- declarations: [
- AppComponent
- ],
- }).compileComponents();
+ beforeEach(waitForAsync(() => {
+ TestBed
+ .configureTestingModule({
+ imports: [RouterTestingModule],
+ declarations: [AppComponent],
+ })
+ .compileComponents();
}));
it('should create the app', () => {
@@ -30,6 +28,7 @@ describe('AppComponent', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
- expect(compiled.querySelector('.content span').textContent).toContain('cli-hello-world-lazy-rollup app is running!');
+ expect(compiled.querySelector('.content span').textContent)
+ .toContain('cli-hello-world-lazy-rollup app is running!');
});
});
diff --git a/integration/cli-hello-world-lazy/src/app/app.component.spec.ts b/integration/cli-hello-world-lazy/src/app/app.component.spec.ts
index 6da00987a8..0bbc645160 100644
--- a/integration/cli-hello-world-lazy/src/app/app.component.spec.ts
+++ b/integration/cli-hello-world-lazy/src/app/app.component.spec.ts
@@ -1,17 +1,15 @@
-import { TestBed, async } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-import { AppComponent } from './app.component';
+import {TestBed, waitForAsync} from '@angular/core/testing';
+import {RouterTestingModule} from '@angular/router/testing';
+import {AppComponent} from './app.component';
describe('AppComponent', () => {
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- imports: [
- RouterTestingModule
- ],
- declarations: [
- AppComponent
- ],
- }).compileComponents();
+ beforeEach(waitForAsync(() => {
+ TestBed
+ .configureTestingModule({
+ imports: [RouterTestingModule],
+ declarations: [AppComponent],
+ })
+ .compileComponents();
}));
it('should create the app', () => {
@@ -30,6 +28,7 @@ describe('AppComponent', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
- expect(compiled.querySelector('.content span').textContent).toContain('cli-hello-world-lazy app is running!');
+ expect(compiled.querySelector('.content span').textContent)
+ .toContain('cli-hello-world-lazy app is running!');
});
});
diff --git a/integration/cli-hello-world/src/app/app.component.spec.ts b/integration/cli-hello-world/src/app/app.component.spec.ts
index 22488f4a4c..103a5b5c5b 100644
--- a/integration/cli-hello-world/src/app/app.component.spec.ts
+++ b/integration/cli-hello-world/src/app/app.component.spec.ts
@@ -1,13 +1,13 @@
-import { TestBed, async } from '@angular/core/testing';
-import { AppComponent } from './app.component';
+import {TestBed, waitForAsync} from '@angular/core/testing';
+import {AppComponent} from './app.component';
describe('AppComponent', () => {
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [
- AppComponent
- ],
- }).compileComponents();
+ beforeEach(waitForAsync(() => {
+ TestBed
+ .configureTestingModule({
+ declarations: [AppComponent],
+ })
+ .compileComponents();
}));
it('should create the app', () => {
@@ -26,6 +26,7 @@ describe('AppComponent', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
- expect(compiled.querySelector('.content span').textContent).toContain('cli-hello-world app is running!');
+ expect(compiled.querySelector('.content span').textContent)
+ .toContain('cli-hello-world app is running!');
});
});
diff --git a/integration/ng_update_migrations/src/app/app.component.spec.ts b/integration/ng_update_migrations/src/app/app.component.spec.ts
index 428143af15..1d9ff930df 100644
--- a/integration/ng_update_migrations/src/app/app.component.spec.ts
+++ b/integration/ng_update_migrations/src/app/app.component.spec.ts
@@ -1,13 +1,13 @@
-import { TestBed, async } from '@angular/core/testing';
-import { AppComponent } from './app.component';
+import {TestBed, waitForAsync} from '@angular/core/testing';
+import {AppComponent} from './app.component';
describe('AppComponent', () => {
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [
- AppComponent
- ],
- }).compileComponents();
+ beforeEach(waitForAsync(() => {
+ TestBed
+ .configureTestingModule({
+ declarations: [AppComponent],
+ })
+ .compileComponents();
}));
it('should create the app', () => {
diff --git a/package.json b/package.json
index b4a2b8cfa7..350be9de2d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "angular-srcs",
- "version": "10.1.0-next.3",
+ "version": "10.1.0-next.4",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@@ -82,6 +82,7 @@
"@types/jasminewd2": "^2.0.8",
"@types/minimist": "^1.2.0",
"@types/node": "^12.11.1",
+ "@types/node-fetch": "^2.5.7",
"@types/selenium-webdriver": "3.0.7",
"@types/semver": "^6.0.2",
"@types/shelljs": "^0.8.6",
@@ -127,6 +128,7 @@
"materialize-css": "1.0.0",
"minimatch": "^3.0.4",
"minimist": "1.2.0",
+ "node-fetch": "^2.6.0",
"node-uuid": "1.4.8",
"nodejs-websocket": "^1.7.2",
"protractor": "^5.4.2",
@@ -185,6 +187,7 @@
"madge": "^3.6.0",
"multimatch": "^4.0.0",
"mutation-observer": "^1.0.3",
+ "nock": "^13.0.3",
"rewire": "2.5.2",
"sauce-connect": "https://saucelabs.com/downloads/sc-4.5.1-linux.tar.gz",
"semver": "^6.3.0",
diff --git a/packages/common/src/viewport_scroller.ts b/packages/common/src/viewport_scroller.ts
index c2def3b91b..355b90ecc7 100644
--- a/packages/common/src/viewport_scroller.ts
+++ b/packages/common/src/viewport_scroller.ts
@@ -111,26 +111,10 @@ export class BrowserViewportScroller implements ViewportScroller {
*/
scrollToAnchor(anchor: string): void {
if (this.supportScrollRestoration()) {
- // Escape anything passed to `querySelector` as it can throw errors and stop the application
- // from working if invalid values are passed.
- if (this.window.CSS && this.window.CSS.escape) {
- anchor = this.window.CSS.escape(anchor);
- } else {
- anchor = anchor.replace(/(\"|\'\ |:|\.|\[|\]|,|=)/g, '\\$1');
- }
- try {
- const elSelectedById = this.document.querySelector(`#${anchor}`);
- if (elSelectedById) {
- this.scrollToElement(elSelectedById);
- return;
- }
- const elSelectedByName = this.document.querySelector(`[name='${anchor}']`);
- if (elSelectedByName) {
- this.scrollToElement(elSelectedByName);
- return;
- }
- } catch (e) {
- this.errorHandler.handleError(e);
+ const elSelected =
+ this.document.getElementById(anchor) || this.document.getElementsByName(anchor)[0];
+ if (elSelected) {
+ this.scrollToElement(elSelected);
}
}
}
@@ -165,13 +149,25 @@ export class BrowserViewportScroller implements ViewportScroller {
*/
private supportScrollRestoration(): boolean {
try {
- return !!this.window && !!this.window.scrollTo;
+ if (!this.window || !this.window.scrollTo) {
+ return false;
+ }
+ // The `scrollRestoration` property could be on the `history` instance or its prototype.
+ const scrollRestorationDescriptor = getScrollRestorationProperty(this.window.history) ||
+ getScrollRestorationProperty(Object.getPrototypeOf(this.window.history));
+ // We can write to the `scrollRestoration` property if it is a writable data field or it has a
+ // setter function.
+ return !!scrollRestorationDescriptor &&
+ !!(scrollRestorationDescriptor.writable || scrollRestorationDescriptor.set);
} catch {
return false;
}
}
}
+function getScrollRestorationProperty(obj: any): PropertyDescriptor|undefined {
+ return Object.getOwnPropertyDescriptor(obj, 'scrollRestoration');
+}
/**
* Provides an empty implementation of the viewport scroller. This will
diff --git a/packages/common/test/directives/ng_class_spec.ts b/packages/common/test/directives/ng_class_spec.ts
index b0eef33aad..66bd7b93df 100644
--- a/packages/common/test/directives/ng_class_spec.ts
+++ b/packages/common/test/directives/ng_class_spec.ts
@@ -7,7 +7,7 @@
*/
import {Component} from '@angular/core';
-import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
{
describe('binding to CSS class list', () => {
@@ -37,7 +37,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
});
});
- it('should clean up when the directive is destroyed', async(() => {
+ it('should clean up when the directive is destroyed', waitForAsync(() => {
fixture = createTestComponent('');
getComponent().items = [['0']];
@@ -47,21 +47,22 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
}));
describe('expressions evaluating to objects', () => {
- it('should add classes specified in an object literal', async(() => {
+ it('should add classes specified in an object literal', waitForAsync(() => {
fixture = createTestComponent('');
detectChangesAndExpectClassName('foo');
}));
it('should add classes specified in an object literal without change in class names',
- async(() => {
+ waitForAsync(() => {
fixture =
createTestComponent(``);
detectChangesAndExpectClassName('foo-bar fooBar');
}));
- it('should add and remove classes based on changes in object literal values', async(() => {
+ it('should add and remove classes based on changes in object literal values',
+ waitForAsync(() => {
fixture =
createTestComponent('');
@@ -71,7 +72,8 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
detectChangesAndExpectClassName('bar');
}));
- it('should add and remove classes based on changes to the expression object', async(() => {
+ it('should add and remove classes based on changes to the expression object',
+ waitForAsync(() => {
fixture = createTestComponent('');
const objExpr = getComponent().objExpr;
@@ -88,7 +90,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
}));
it('should add and remove classes based on reference changes to the expression object',
- async(() => {
+ waitForAsync(() => {
fixture = createTestComponent('');
detectChangesAndExpectClassName('foo');
@@ -100,7 +102,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
detectChangesAndExpectClassName('baz');
}));
- it('should remove active classes when expression evaluates to null', async(() => {
+ it('should remove active classes when expression evaluates to null', waitForAsync(() => {
fixture = createTestComponent('');
detectChangesAndExpectClassName('foo');
@@ -113,7 +115,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
}));
- it('should allow multiple classes per expression', async(() => {
+ it('should allow multiple classes per expression', waitForAsync(() => {
fixture = createTestComponent('');
getComponent().objExpr = {'bar baz': true, 'bar1 baz1': true};
@@ -123,7 +125,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
detectChangesAndExpectClassName('bar1 baz1');
}));
- it('should split by one or more spaces between classes', async(() => {
+ it('should split by one or more spaces between classes', waitForAsync(() => {
fixture = createTestComponent('');
getComponent().objExpr = {'foo bar baz': true};
@@ -132,14 +134,14 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
});
describe('expressions evaluating to lists', () => {
- it('should add classes specified in a list literal', async(() => {
+ it('should add classes specified in a list literal', waitForAsync(() => {
fixture =
createTestComponent(``);
detectChangesAndExpectClassName('foo bar foo-bar fooBar');
}));
- it('should add and remove classes based on changes to the expression', async(() => {
+ it('should add and remove classes based on changes to the expression', waitForAsync(() => {
fixture = createTestComponent('');
const arrExpr = getComponent().arrExpr;
detectChangesAndExpectClassName('foo');
@@ -154,7 +156,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
detectChangesAndExpectClassName('foo');
}));
- it('should add and remove classes when a reference changes', async(() => {
+ it('should add and remove classes when a reference changes', waitForAsync(() => {
fixture = createTestComponent('');
detectChangesAndExpectClassName('foo');
@@ -162,7 +164,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
detectChangesAndExpectClassName('bar');
}));
- it('should take initial classes into account when a reference changes', async(() => {
+ it('should take initial classes into account when a reference changes', waitForAsync(() => {
fixture = createTestComponent('');
detectChangesAndExpectClassName('foo');
@@ -170,13 +172,13 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
detectChangesAndExpectClassName('foo bar');
}));
- it('should ignore empty or blank class names', async(() => {
+ it('should ignore empty or blank class names', waitForAsync(() => {
fixture = createTestComponent('');
getComponent().arrExpr = ['', ' '];
detectChangesAndExpectClassName('foo');
}));
- it('should trim blanks from class names', async(() => {
+ it('should trim blanks from class names', waitForAsync(() => {
fixture = createTestComponent('');
getComponent().arrExpr = [' bar '];
@@ -184,7 +186,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
}));
- it('should allow multiple classes per item in arrays', async(() => {
+ it('should allow multiple classes per item in arrays', waitForAsync(() => {
fixture = createTestComponent('');
getComponent().arrExpr = ['foo bar baz', 'foo1 bar1 baz1'];
@@ -203,7 +205,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
});
describe('expressions evaluating to sets', () => {
- it('should add and remove classes if the set instance changed', async(() => {
+ it('should add and remove classes if the set instance changed', waitForAsync(() => {
fixture = createTestComponent('');
let setExpr = new Set();
setExpr.add('bar');
@@ -218,12 +220,12 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
});
describe('expressions evaluating to string', () => {
- it('should add classes specified in a string literal', async(() => {
+ it('should add classes specified in a string literal', waitForAsync(() => {
fixture = createTestComponent(``);
detectChangesAndExpectClassName('foo bar foo-bar fooBar');
}));
- it('should add and remove classes based on changes to the expression', async(() => {
+ it('should add and remove classes based on changes to the expression', waitForAsync(() => {
fixture = createTestComponent('');
detectChangesAndExpectClassName('foo');
@@ -235,7 +237,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
detectChangesAndExpectClassName('baz');
}));
- it('should remove active classes when switching from string to null', async(() => {
+ it('should remove active classes when switching from string to null', waitForAsync(() => {
fixture = createTestComponent(``);
detectChangesAndExpectClassName('foo');
@@ -244,7 +246,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
}));
it('should take initial classes into account when switching from string to null',
- async(() => {
+ waitForAsync(() => {
fixture = createTestComponent(``);
detectChangesAndExpectClassName('foo');
@@ -252,7 +254,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
detectChangesAndExpectClassName('foo');
}));
- it('should ignore empty and blank strings', async(() => {
+ it('should ignore empty and blank strings', waitForAsync(() => {
fixture = createTestComponent(``);
getComponent().strExpr = '';
detectChangesAndExpectClassName('foo');
@@ -260,7 +262,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
});
describe('cooperation with other class-changing constructs', () => {
- it('should co-operate with the class attribute', async(() => {
+ it('should co-operate with the class attribute', waitForAsync(() => {
fixture = createTestComponent('');
const objExpr = getComponent().objExpr;
@@ -274,7 +276,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
detectChangesAndExpectClassName('init foo');
}));
- it('should co-operate with the interpolated class attribute', async(() => {
+ it('should co-operate with the interpolated class attribute', waitForAsync(() => {
fixture = createTestComponent(``);
const objExpr = getComponent().objExpr;
@@ -289,7 +291,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
}));
it('should co-operate with the interpolated class attribute when interpolation changes',
- async(() => {
+ waitForAsync(() => {
fixture = createTestComponent(
``);
@@ -299,7 +301,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
detectChangesAndExpectClassName(`bar small`);
}));
- it('should co-operate with the class attribute and binding to it', async(() => {
+ it('should co-operate with the class attribute and binding to it', waitForAsync(() => {
fixture =
createTestComponent(``);
const objExpr = getComponent().objExpr;
@@ -314,7 +316,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
detectChangesAndExpectClassName(`init foo`);
}));
- it('should co-operate with the class attribute and class.name binding', async(() => {
+ it('should co-operate with the class attribute and class.name binding', waitForAsync(() => {
const template =
'';
fixture = createTestComponent(template);
@@ -333,7 +335,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
}));
it('should co-operate with initial class and class attribute binding when binding changes',
- async(() => {
+ waitForAsync(() => {
const template = '';
fixture = createTestComponent(template);
const cmp = getComponent();
diff --git a/packages/common/test/directives/ng_component_outlet_spec.ts b/packages/common/test/directives/ng_component_outlet_spec.ts
index cefe18a44a..c611247823 100644
--- a/packages/common/test/directives/ng_component_outlet_spec.ts
+++ b/packages/common/test/directives/ng_component_outlet_spec.ts
@@ -9,7 +9,7 @@
import {CommonModule} from '@angular/common';
import {NgComponentOutlet} from '@angular/common/src/directives/ng_component_outlet';
import {Compiler, Component, ComponentRef, Inject, InjectionToken, Injector, NgModule, NgModuleFactory, NO_ERRORS_SCHEMA, Optional, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
-import {async, TestBed} from '@angular/core/testing';
+import {TestBed, waitForAsync} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
describe('insert/remove', () => {
@@ -17,7 +17,7 @@ describe('insert/remove', () => {
TestBed.configureTestingModule({imports: [TestModule]});
});
- it('should do nothing if component is null', async(() => {
+ it('should do nothing if component is null', waitForAsync(() => {
const template = ``;
TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent);
@@ -28,7 +28,7 @@ describe('insert/remove', () => {
expect(fixture.nativeElement).toHaveText('');
}));
- it('should insert content specified by a component', async(() => {
+ it('should insert content specified by a component', waitForAsync(() => {
let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
@@ -40,7 +40,7 @@ describe('insert/remove', () => {
expect(fixture.nativeElement).toHaveText('foo');
}));
- it('should emit a ComponentRef once a component was created', async(() => {
+ it('should emit a ComponentRef once a component was created', waitForAsync(() => {
let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
@@ -56,7 +56,7 @@ describe('insert/remove', () => {
}));
- it('should clear view if component becomes null', async(() => {
+ it('should clear view if component becomes null', waitForAsync(() => {
let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
@@ -74,7 +74,7 @@ describe('insert/remove', () => {
}));
- it('should swap content if component changes', async(() => {
+ it('should swap content if component changes', waitForAsync(() => {
let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
@@ -91,7 +91,7 @@ describe('insert/remove', () => {
expect(fixture.nativeElement).toHaveText('bar');
}));
- it('should use the injector, if one supplied', async(() => {
+ it('should use the injector, if one supplied', waitForAsync(() => {
let fixture = TestBed.createComponent(TestComponent);
const uniqueValue = {};
@@ -107,7 +107,7 @@ describe('insert/remove', () => {
}));
- it('should resolve with an injector', async(() => {
+ it('should resolve with an injector', waitForAsync(() => {
let fixture = TestBed.createComponent(TestComponent);
// We are accessing a ViewChild (ngComponentOutlet) before change detection has run
@@ -120,7 +120,7 @@ describe('insert/remove', () => {
expect(cmpRef.instance.testToken).toBeNull();
}));
- it('should render projectable nodes, if supplied', async(() => {
+ it('should render projectable nodes, if supplied', waitForAsync(() => {
const template = `projected foo${TEST_CMP_TEMPLATE}`;
TestBed.overrideComponent(TestComponent, {set: {template: template}})
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
@@ -144,7 +144,7 @@ describe('insert/remove', () => {
expect(fixture.nativeElement).toHaveText('projected foo');
}));
- it('should resolve components from other modules, if supplied', async(() => {
+ it('should resolve components from other modules, if supplied', waitForAsync(() => {
const compiler = TestBed.inject(Compiler);
let fixture = TestBed.createComponent(TestComponent);
@@ -158,7 +158,7 @@ describe('insert/remove', () => {
expect(fixture.nativeElement).toHaveText('baz');
}));
- it('should clean up moduleRef, if supplied', async(() => {
+ it('should clean up moduleRef, if supplied', waitForAsync(() => {
let destroyed = false;
const compiler = TestBed.inject(Compiler);
const fixture = TestBed.createComponent(TestComponent);
@@ -174,7 +174,7 @@ describe('insert/remove', () => {
expect(moduleRef.destroy).toHaveBeenCalled();
}));
- it('should not re-create moduleRef when it didn\'t actually change', async(() => {
+ it('should not re-create moduleRef when it didn\'t actually change', waitForAsync(() => {
const compiler = TestBed.inject(Compiler);
const fixture = TestBed.createComponent(TestComponent);
@@ -191,7 +191,7 @@ describe('insert/remove', () => {
expect(moduleRef).toBe(fixture.componentInstance.ngComponentOutlet['_moduleRef']);
}));
- it('should re-create moduleRef when changed', async(() => {
+ it('should re-create moduleRef when changed', waitForAsync(() => {
const compiler = TestBed.inject(Compiler);
const fixture = TestBed.createComponent(TestComponent);
fixture.componentInstance.module = compiler.compileModuleSync(TestModule2);
diff --git a/packages/common/test/directives/ng_for_spec.ts b/packages/common/test/directives/ng_for_spec.ts
index 1b2fdb6f6c..549010ff8a 100644
--- a/packages/common/test/directives/ng_for_spec.ts
+++ b/packages/common/test/directives/ng_for_spec.ts
@@ -8,7 +8,7 @@
import {CommonModule} from '@angular/common';
import {Component} from '@angular/core';
-import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {expect} from '@angular/platform-browser/testing/src/matchers';
@@ -38,27 +38,27 @@ let thisArg: any;
});
});
- it('should reflect initial elements', async(() => {
+ it('should reflect initial elements', waitForAsync(() => {
fixture = createTestComponent();
detectChangesAndExpectText('1;2;');
}));
- it('should reflect added elements', async(() => {
+ it('should reflect added elements', waitForAsync(() => {
fixture = createTestComponent();
fixture.detectChanges();
getComponent().items.push(3);
detectChangesAndExpectText('1;2;3;');
}));
- it('should reflect removed elements', async(() => {
+ it('should reflect removed elements', waitForAsync(() => {
fixture = createTestComponent();
fixture.detectChanges();
getComponent().items.splice(1, 1);
detectChangesAndExpectText('1;');
}));
- it('should reflect moved elements', async(() => {
+ it('should reflect moved elements', waitForAsync(() => {
fixture = createTestComponent();
fixture.detectChanges();
getComponent().items.splice(0, 1);
@@ -66,7 +66,7 @@ let thisArg: any;
detectChangesAndExpectText('2;1;');
}));
- it('should reflect a mix of all changes (additions/removals/moves)', async(() => {
+ it('should reflect a mix of all changes (additions/removals/moves)', waitForAsync(() => {
fixture = createTestComponent();
getComponent().items = [0, 1, 2, 3, 4, 5];
@@ -77,7 +77,7 @@ let thisArg: any;
detectChangesAndExpectText('6;2;7;0;4;8;');
}));
- it('should iterate over an array of objects', async(() => {
+ it('should iterate over an array of objects', waitForAsync(() => {
const template = '
';
fixture = createTestComponent(template);
detectChangesAndExpectText('');
}));
- it('should gracefully handle ref changing to null and back', async(() => {
+ it('should gracefully handle ref changing to null and back', waitForAsync(() => {
fixture = createTestComponent();
detectChangesAndExpectText('1;2;');
@@ -114,7 +114,7 @@ let thisArg: any;
detectChangesAndExpectText('1;2;3;');
}));
- it('should throw on non-iterable ref and suggest using an array', async(() => {
+ it('should throw on non-iterable ref and suggest using an array', waitForAsync(() => {
fixture = createTestComponent();
getComponent().items = 'whaaa';
@@ -123,7 +123,7 @@ let thisArg: any;
/Cannot find a differ supporting object 'whaaa' of type 'string'. NgFor only supports binding to Iterables such as Arrays/);
}));
- it('should throw on ref changing to string', async(() => {
+ it('should throw on ref changing to string', waitForAsync(() => {
fixture = createTestComponent();
detectChangesAndExpectText('1;2;');
@@ -132,7 +132,7 @@ let thisArg: any;
expect(() => fixture.detectChanges()).toThrowError();
}));
- it('should works with duplicates', async(() => {
+ it('should works with duplicates', waitForAsync(() => {
fixture = createTestComponent();
const a = new Foo();
@@ -140,7 +140,7 @@ let thisArg: any;
detectChangesAndExpectText('foo;foo;');
}));
- it('should repeat over nested arrays', async(() => {
+ it('should repeat over nested arrays', waitForAsync(() => {
const template = '
' +
'
{{subitem}}-{{item.length}};
|' +
'
';
@@ -153,7 +153,7 @@ let thisArg: any;
detectChangesAndExpectText('e-1;|f-2;g-2;|');
}));
- it('should repeat over nested arrays with no intermediate element', async(() => {
+ it('should repeat over nested arrays with no intermediate element', waitForAsync(() => {
const template = '
' +
'
{{subitem}}-{{item.length}};
' +
'
';
@@ -166,7 +166,8 @@ let thisArg: any;
detectChangesAndExpectText('e-1;f-2;g-2;');
}));
- it('should repeat over nested ngIf that are the last node in the ngFor template', async(() => {
+ it('should repeat over nested ngIf that are the last node in the ngFor template',
+ waitForAsync(() => {
const template = `
` +
`
{{i}}|
` +
`
even|
` +
@@ -185,7 +186,7 @@ let thisArg: any;
detectChangesAndExpectText('0|even|1|2|even|');
}));
- it('should allow of saving the collection', async(() => {
+ it('should allow of saving the collection', waitForAsync(() => {
const template =
'
';
@@ -276,7 +277,7 @@ let thisArg: any;
detectChangesAndExpectText('0: a;1: b;2: c;');
}));
- it('should use a default template if a custom one is null', async(() => {
+ it('should use a default template if a custom one is null', waitForAsync(() => {
const template =
`
{{i}}: {{item}};
`;
fixture = createTestComponent(template);
@@ -285,7 +286,8 @@ let thisArg: any;
detectChangesAndExpectText('0: a;1: b;2: c;');
}));
- it('should use a custom template when both default and a custom one are present', async(() => {
+ it('should use a custom template when both default and a custom one are present',
+ waitForAsync(() => {
const template =
'{{i}};' +
'{{i}}: {{item}};';
@@ -296,7 +298,7 @@ let thisArg: any;
}));
describe('track by', () => {
- it('should console.warn if trackBy is not a function', async(() => {
+ it('should console.warn if trackBy is not a function', waitForAsync(() => {
// TODO(vicb): expect a warning message when we have a proper log service
const template = ``;
fixture = createTestComponent(template);
@@ -304,7 +306,7 @@ let thisArg: any;
fixture.detectChanges();
}));
- it('should track by identity when trackBy is to `null` or `undefined`', async(() => {
+ it('should track by identity when trackBy is to `null` or `undefined`', waitForAsync(() => {
// TODO(vicb): expect no warning message when we have a proper log service
const template = `
{{ item }}
`;
fixture = createTestComponent(template);
@@ -315,7 +317,7 @@ let thisArg: any;
detectChangesAndExpectText('abc');
}));
- it('should set the context to the component instance', async(() => {
+ it('should set the context to the component instance', waitForAsync(() => {
const template =
``;
fixture = createTestComponent(template);
@@ -325,7 +327,7 @@ let thisArg: any;
expect(thisArg).toBe(getComponent());
}));
- it('should not replace tracked items', async(() => {
+ it('should not replace tracked items', waitForAsync(() => {
const template =
`
{{items[i]}}
`;
fixture = createTestComponent(template);
@@ -341,7 +343,7 @@ let thisArg: any;
expect(finalP.nativeElement).toBe(firstP.nativeElement);
}));
- it('should update implicit local variable on view', async(() => {
+ it('should update implicit local variable on view', waitForAsync(() => {
const template =
`
{{item['color']}}
`;
fixture = createTestComponent(template);
@@ -353,7 +355,7 @@ let thisArg: any;
detectChangesAndExpectText('red');
}));
- it('should move items around and keep them updated ', async(() => {
+ it('should move items around and keep them updated ', waitForAsync(() => {
const template =
`
{{item['color']}}
`;
fixture = createTestComponent(template);
@@ -365,7 +367,8 @@ let thisArg: any;
detectChangesAndExpectText('orangered');
}));
- it('should handle added and removed items properly when tracking by index', async(() => {
+ it('should handle added and removed items properly when tracking by index',
+ waitForAsync(() => {
const template = `
{{item}}
`;
fixture = createTestComponent(template);
diff --git a/packages/common/test/directives/ng_if_spec.ts b/packages/common/test/directives/ng_if_spec.ts
index 38f526ec67..01f5d1e5a1 100644
--- a/packages/common/test/directives/ng_if_spec.ts
+++ b/packages/common/test/directives/ng_if_spec.ts
@@ -8,7 +8,7 @@
import {CommonModule, ɵgetDOM as getDOM} from '@angular/common';
import {Component} from '@angular/core';
-import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {expect} from '@angular/platform-browser/testing/src/matchers';
@@ -31,7 +31,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
});
});
- it('should work in a template attribute', async(() => {
+ it('should work in a template attribute', waitForAsync(() => {
const template = 'hello';
fixture = createTestComponent(template);
fixture.detectChanges();
@@ -39,14 +39,14 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
expect(fixture.nativeElement).toHaveText('hello');
}));
- it('should work on a template element', async(() => {
+ it('should work on a template element', waitForAsync(() => {
const template = 'hello2';
fixture = createTestComponent(template);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('hello2');
}));
- it('should toggle node when condition changes', async(() => {
+ it('should toggle node when condition changes', waitForAsync(() => {
const template = 'hello';
fixture = createTestComponent(template);
getComponent().booleanCondition = false;
@@ -65,7 +65,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
expect(fixture.nativeElement).toHaveText('');
}));
- it('should handle nested if correctly', async(() => {
+ it('should handle nested if correctly', waitForAsync(() => {
const template =
'
hello
';
@@ -97,7 +97,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
expect(fixture.nativeElement).toHaveText('');
}));
- it('should update several nodes with if', async(() => {
+ it('should update several nodes with if', waitForAsync(() => {
const template = '= 2">helloNumber' +
'helloString' +
'helloFunction';
@@ -120,7 +120,8 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
expect(fixture.nativeElement).toHaveText('helloNumber');
}));
- it('should not add the element twice if the condition goes from truthy to truthy', async(() => {
+ it('should not add the element twice if the condition goes from truthy to truthy',
+ waitForAsync(() => {
const template = 'hello';
fixture = createTestComponent(template);
@@ -141,7 +142,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
}));
describe('then/else templates', () => {
- it('should support else', async(() => {
+ it('should support else', waitForAsync(() => {
const template = 'TRUE' +
'FALSE';
@@ -155,7 +156,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
expect(fixture.nativeElement).toHaveText('FALSE');
}));
- it('should support then and else', async(() => {
+ it('should support then and else', waitForAsync(() => {
const template =
'IGNORE' +
'THEN' +
@@ -202,7 +203,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
expect(fixture.nativeElement).toHaveText('');
});
- it('should support dynamic else', async(() => {
+ it('should support dynamic else', waitForAsync(() => {
const template =
'TRUE' +
'FALSE1' +
@@ -222,7 +223,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
expect(fixture.nativeElement).toHaveText('FALSE2');
}));
- it('should support binding to variable using let', async(() => {
+ it('should support binding to variable using let', waitForAsync(() => {
const template = '{{v}}' +
'{{v}}';
@@ -236,7 +237,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
expect(fixture.nativeElement).toHaveText('false');
}));
- it('should support binding to variable using as', async(() => {
+ it('should support binding to variable using as', waitForAsync(() => {
const template = '{{v}}' +
'{{v}}';
@@ -252,7 +253,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
});
describe('Type guarding', () => {
- it('should throw when then block is not template', async(() => {
+ it('should throw when then block is not template', waitForAsync(() => {
const template = 'IGNORE' +
'
THEN
';
@@ -262,7 +263,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
.toThrowError(/ngIfThen must be a TemplateRef, but received/);
}));
- it('should throw when else block is not template', async(() => {
+ it('should throw when else block is not template', waitForAsync(() => {
const template = 'IGNORE' +
'
ELSE
';
diff --git a/packages/common/test/directives/ng_plural_spec.ts b/packages/common/test/directives/ng_plural_spec.ts
index 677c3240ed..7d6ab99295 100644
--- a/packages/common/test/directives/ng_plural_spec.ts
+++ b/packages/common/test/directives/ng_plural_spec.ts
@@ -8,7 +8,7 @@
import {CommonModule, NgLocalization} from '@angular/common';
import {Component, Injectable} from '@angular/core';
-import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
{
@@ -36,7 +36,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
});
});
- it('should display the template according to the exact value', async(() => {
+ it('should display the template according to the exact value', waitForAsync(() => {
const template = '
' +
'
you have no messages.
' +
'
you have one message.
' +
@@ -51,7 +51,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
detectChangesAndExpectText('you have one message.');
}));
- it('should display the template according to the exact numeric value', async(() => {
+ it('should display the template according to the exact numeric value', waitForAsync(() => {
const template = '
' +
'
' +
'
you have no messages.
' +
@@ -69,7 +69,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
// https://github.com/angular/angular/issues/9868
// https://github.com/angular/angular/issues/9882
- it('should not throw when ngPluralCase contains expressions', async(() => {
+ it('should not throw when ngPluralCase contains expressions', waitForAsync(() => {
const template = '
' +
'
{{ switchValue }}
' +
'
';
@@ -81,7 +81,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
}));
- it('should be applicable to elements', async(() => {
+ it('should be applicable to elements', waitForAsync(() => {
const template = '' +
'you have no messages.' +
'you have one message.' +
@@ -96,7 +96,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
detectChangesAndExpectText('you have one message.');
}));
- it('should display the template according to the category', async(() => {
+ it('should display the template according to the category', waitForAsync(() => {
const template = '
' +
'
you have a few messages.
' +
'
you have many messages.
' +
@@ -111,7 +111,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
detectChangesAndExpectText('you have many messages.');
}));
- it('should default to other when no matches are found', async(() => {
+ it('should default to other when no matches are found', waitForAsync(() => {
const template = '
' +
'
you have a few messages.
' +
'
default message.
' +
@@ -123,7 +123,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
detectChangesAndExpectText('default message.');
}));
- it('should prioritize value matches over category matches', async(() => {
+ it('should prioritize value matches over category matches', waitForAsync(() => {
const template = '
' +
'
you have a few messages.
' +
'you have two messages.' +
diff --git a/packages/common/test/directives/ng_style_spec.ts b/packages/common/test/directives/ng_style_spec.ts
index 095e5034e3..e9fd9fee8c 100644
--- a/packages/common/test/directives/ng_style_spec.ts
+++ b/packages/common/test/directives/ng_style_spec.ts
@@ -8,7 +8,7 @@
import {CommonModule} from '@angular/common';
import {Component} from '@angular/core';
-import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
{
describe('NgStyle', () => {
@@ -30,14 +30,14 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
TestBed.configureTestingModule({declarations: [TestComponent], imports: [CommonModule]});
});
- it('should add styles specified in an object literal', async(() => {
+ it('should add styles specified in an object literal', waitForAsync(() => {
const template = ``;
fixture = createTestComponent(template);
fixture.detectChanges();
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px'});
}));
- it('should add and change styles specified in an object expression', async(() => {
+ it('should add and change styles specified in an object expression', waitForAsync(() => {
const template = ``;
fixture = createTestComponent(template);
@@ -51,7 +51,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
expectNativeEl(fixture).toHaveCssStyle({'max-width': '30%'});
}));
- it('should add and remove styles specified using style.unit notation', async(() => {
+ it('should add and remove styles specified using style.unit notation', waitForAsync(() => {
const template = ``;
fixture = createTestComponent(template);
@@ -66,7 +66,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
}));
// https://github.com/angular/angular/issues/21064
- it('should add and remove styles which names are not dash-cased', async(() => {
+ it('should add and remove styles which names are not dash-cased', waitForAsync(() => {
fixture = createTestComponent(``);
getComponent().expr = 'green';
@@ -78,7 +78,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
expectNativeEl(fixture).not.toHaveCssStyle('color');
}));
- it('should update styles using style.unit notation when unit changes', async(() => {
+ it('should update styles using style.unit notation when unit changes', waitForAsync(() => {
const template = ``;
fixture = createTestComponent(template);
@@ -93,7 +93,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
}));
// keyValueDiffer is sensitive to key order #9115
- it('should change styles specified in an object expression', async(() => {
+ it('should change styles specified in an object expression', waitForAsync(() => {
const template = ``;
fixture = createTestComponent(template);
@@ -117,7 +117,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
expectNativeEl(fixture).toHaveCssStyle({'height': '5px', 'width': '5px'});
}));
- it('should remove styles when deleting a key in an object expression', async(() => {
+ it('should remove styles when deleting a key in an object expression', waitForAsync(() => {
const template = ``;
fixture = createTestComponent(template);
@@ -131,7 +131,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
expectNativeEl(fixture).not.toHaveCssStyle('max-width');
}));
- it('should co-operate with the style attribute', async(() => {
+ it('should co-operate with the style attribute', waitForAsync(() => {
const template = ``;
fixture = createTestComponent(template);
@@ -147,7 +147,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
}));
it('should co-operate with the style.[styleName]="expr" special-case in the compiler',
- async(() => {
+ waitForAsync(() => {
const template = ``;
fixture = createTestComponent(template);
diff --git a/packages/common/test/directives/ng_template_outlet_spec.ts b/packages/common/test/directives/ng_template_outlet_spec.ts
index 7a6f05771a..865f8c40da 100644
--- a/packages/common/test/directives/ng_template_outlet_spec.ts
+++ b/packages/common/test/directives/ng_template_outlet_spec.ts
@@ -8,7 +8,7 @@
import {CommonModule} from '@angular/common';
import {Component, ContentChildren, Directive, Injectable, NO_ERRORS_SCHEMA, OnDestroy, QueryList, TemplateRef} from '@angular/core';
-import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
describe('NgTemplateOutlet', () => {
@@ -36,7 +36,7 @@ describe('NgTemplateOutlet', () => {
});
// https://github.com/angular/angular/issues/14778
- it('should accept the component as the context', async(() => {
+ it('should accept the component as the context', waitForAsync(() => {
const template = `` +
`{{context.foo}}`;
@@ -44,20 +44,20 @@ describe('NgTemplateOutlet', () => {
detectChangesAndExpectText('bar');
}));
- it('should do nothing if templateRef is `null`', async(() => {
+ it('should do nothing if templateRef is `null`', waitForAsync(() => {
const template = ``;
fixture = createTestComponent(template);
detectChangesAndExpectText('');
}));
- it('should insert content specified by TemplateRef', async(() => {
+ it('should insert content specified by TemplateRef', waitForAsync(() => {
const template = `foo` +
``;
fixture = createTestComponent(template);
detectChangesAndExpectText('foo');
}));
- it('should clear content if TemplateRef becomes `null`', async(() => {
+ it('should clear content if TemplateRef becomes `null`', waitForAsync(() => {
const template = `foo` +
``;
fixture = createTestComponent(template);
@@ -71,7 +71,7 @@ describe('NgTemplateOutlet', () => {
detectChangesAndExpectText('');
}));
- it('should swap content if TemplateRef changes', async(() => {
+ it('should swap content if TemplateRef changes', waitForAsync(() => {
const template =
`foobar` +
``;
@@ -87,14 +87,14 @@ describe('NgTemplateOutlet', () => {
detectChangesAndExpectText('bar');
}));
- it('should display template if context is `null`', async(() => {
+ it('should display template if context is `null`', waitForAsync(() => {
const template = `foo` +
``;
fixture = createTestComponent(template);
detectChangesAndExpectText('foo');
}));
- it('should reflect initial context and changes', async(() => {
+ it('should reflect initial context and changes', waitForAsync(() => {
const template = `{{foo}}` +
``;
fixture = createTestComponent(template);
@@ -106,7 +106,7 @@ describe('NgTemplateOutlet', () => {
detectChangesAndExpectText('alter-bar');
}));
- it('should reflect user defined `$implicit` property in the context', async(() => {
+ it('should reflect user defined `$implicit` property in the context', waitForAsync(() => {
const template = `{{ctx.foo}}` +
``;
fixture = createTestComponent(template);
@@ -114,7 +114,7 @@ describe('NgTemplateOutlet', () => {
detectChangesAndExpectText('bra');
}));
- it('should reflect context re-binding', async(() => {
+ it('should reflect context re-binding', waitForAsync(() => {
const template = `{{shawshank}}` +
``;
fixture = createTestComponent(template);
@@ -222,7 +222,8 @@ describe('NgTemplateOutlet', () => {
}).not.toThrow();
});
- it('should not throw when switching from template to null and back to template', async(() => {
+ it('should not throw when switching from template to null and back to template',
+ waitForAsync(() => {
const template = `foo` +
``;
fixture = createTestComponent(template);
diff --git a/packages/common/test/directives/non_bindable_spec.ts b/packages/common/test/directives/non_bindable_spec.ts
index a1105efbc3..34ee43177a 100644
--- a/packages/common/test/directives/non_bindable_spec.ts
+++ b/packages/common/test/directives/non_bindable_spec.ts
@@ -9,7 +9,7 @@
import {ɵgetDOM as getDOM} from '@angular/common';
import {Component, Directive} from '@angular/core';
import {ElementRef} from '@angular/core/src/linker/element_ref';
-import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {hasClass} from '@angular/platform-browser/testing/src/browser_util';
import {expect} from '@angular/platform-browser/testing/src/matchers';
@@ -21,7 +21,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
});
});
- it('should not interpolate children', async(() => {
+ it('should not interpolate children', waitForAsync(() => {
const template = '
';
const fixture = createTestComponent(template);
fixture.detectChanges();
@@ -40,7 +40,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
expect(hasClass(span, 'compiled')).toBeFalsy();
}));
- it('should trigger directives on the same node', async(() => {
+ it('should trigger directives on the same node', waitForAsync(() => {
const template = '
{{text}}
';
const fixture = createTestComponent(template);
fixture.detectChanges();
diff --git a/packages/common/test/pipes/json_pipe_spec.ts b/packages/common/test/pipes/json_pipe_spec.ts
index fafa2445d3..f557cca878 100644
--- a/packages/common/test/pipes/json_pipe_spec.ts
+++ b/packages/common/test/pipes/json_pipe_spec.ts
@@ -8,7 +8,7 @@
import {CommonModule, JsonPipe} from '@angular/common';
import {Component} from '@angular/core';
-import {async, TestBed} from '@angular/core/testing';
+import {TestBed, waitForAsync} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
{
@@ -64,7 +64,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
TestBed.configureTestingModule({declarations: [TestComp], imports: [CommonModule]});
});
- it('should work with mutable objects', async(() => {
+ it('should work with mutable objects', waitForAsync(() => {
const fixture = TestBed.createComponent(TestComp);
const mutable: number[] = [1];
fixture.componentInstance.data = mutable;
diff --git a/packages/common/test/pipes/slice_pipe_spec.ts b/packages/common/test/pipes/slice_pipe_spec.ts
index a0832fd926..1ab7398c4d 100644
--- a/packages/common/test/pipes/slice_pipe_spec.ts
+++ b/packages/common/test/pipes/slice_pipe_spec.ts
@@ -8,7 +8,7 @@
import {CommonModule, SlicePipe} from '@angular/common';
import {Component} from '@angular/core';
-import {async, TestBed} from '@angular/core/testing';
+import {TestBed, waitForAsync} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
{
@@ -105,7 +105,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
TestBed.configureTestingModule({declarations: [TestComp], imports: [CommonModule]});
});
- it('should work with mutable arrays', async(() => {
+ it('should work with mutable arrays', waitForAsync(() => {
const fixture = TestBed.createComponent(TestComp);
const mutable: number[] = [1, 2];
fixture.componentInstance.data = mutable;
diff --git a/packages/common/test/viewport_scroller_spec.ts b/packages/common/test/viewport_scroller_spec.ts
index f3b6c907a8..510a24bad5 100644
--- a/packages/common/test/viewport_scroller_spec.ts
+++ b/packages/common/test/viewport_scroller_spec.ts
@@ -6,35 +6,61 @@
* found in the LICENSE file at https://angular.io/license
*/
-
-
-/**
- * @license
- * Copyright Google LLC All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.io/license
- */
-
import {describe, expect, it} from '@angular/core/testing/src/testing_internal';
import {BrowserViewportScroller, ViewportScroller} from '../src/viewport_scroller';
-{
- describe('BrowserViewportScroller', () => {
- let scroller: ViewportScroller;
- let documentSpy: any;
- beforeEach(() => {
- documentSpy = jasmine.createSpyObj('document', ['querySelector']);
- scroller = new BrowserViewportScroller(documentSpy, {scrollTo: 1}, null!);
- });
- it('escapes invalid characters selectors', () => {
- const invalidSelectorChars = `"' :.[],=`;
- // Double escaped to make sure we match the actual value passed to `querySelector`
- const escapedInvalids = `\\"\\' \\:\\.\\[\\]\\,\\=`;
- scroller.scrollToAnchor(`specials=${invalidSelectorChars}`);
- expect(documentSpy.querySelector).toHaveBeenCalledWith(`#specials\\=${escapedInvalids}`);
- expect(documentSpy.querySelector)
- .toHaveBeenCalledWith(`[name='specials\\=${escapedInvalids}']`);
+describe('BrowserViewportScroller', () => {
+ let scroller: ViewportScroller;
+ let documentSpy: any;
+ let windowSpy: any;
+
+ beforeEach(() => {
+ windowSpy = jasmine.createSpyObj('window', ['history']);
+ windowSpy.scrollTo = 1;
+ windowSpy.history.scrollRestoration = 'auto';
+ documentSpy = jasmine.createSpyObj('document', ['getElementById', 'getElementsByName']);
+ scroller = new BrowserViewportScroller(documentSpy, windowSpy, null!);
+ });
+
+ describe('setHistoryScrollRestoration', () => {
+ it('should not crash when scrollRestoration is not writable', () => {
+ Object.defineProperty(windowSpy.history, 'scrollRestoration', {
+ value: 'auto',
+ configurable: true,
+ });
+ expect(() => scroller.setHistoryScrollRestoration('manual')).not.toThrow();
});
});
-}
+
+ describe('scrollToAnchor', () => {
+ const anchor = 'anchor';
+ const el = document.createElement('a');
+
+ it('should only call getElementById when an element is found by id', () => {
+ documentSpy.getElementById.and.returnValue(el);
+ spyOn(scroller, 'scrollToElement');
+ scroller.scrollToAnchor(anchor);
+ expect(documentSpy.getElementById).toHaveBeenCalledWith(anchor);
+ expect(documentSpy.getElementsByName).not.toHaveBeenCalled();
+ expect((scroller as any).scrollToElement).toHaveBeenCalledWith(el);
+ });
+
+ it('should call getElementById and getElementsByName when an element is found by name', () => {
+ documentSpy.getElementsByName.and.returnValue([el]);
+ spyOn(scroller, 'scrollToElement');
+ scroller.scrollToAnchor(anchor);
+ expect(documentSpy.getElementById).toHaveBeenCalledWith(anchor);
+ expect(documentSpy.getElementsByName).toHaveBeenCalledWith(anchor);
+ expect((scroller as any).scrollToElement).toHaveBeenCalledWith(el);
+ });
+
+ it('should not call scrollToElement when an element is not found by its id or its name', () => {
+ documentSpy.getElementsByName.and.returnValue([]);
+ spyOn(scroller, 'scrollToElement');
+ scroller.scrollToAnchor(anchor);
+ expect(documentSpy.getElementById).toHaveBeenCalledWith(anchor);
+ expect(documentSpy.getElementsByName).toHaveBeenCalledWith(anchor);
+ expect((scroller as any).scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts
index 950ae78b98..115a9e7361 100644
--- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts
+++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts
@@ -307,7 +307,10 @@ export class NgModuleDecoratorHandler implements
});
if (this.factoryTracker !== null) {
- this.factoryTracker.track(node.getSourceFile(), analysis.factorySymbolName);
+ this.factoryTracker.track(node.getSourceFile(), {
+ name: analysis.factorySymbolName,
+ hasId: analysis.id !== null,
+ });
}
this.injectableRegistry.registerInjectable(node);
diff --git a/packages/compiler-cli/src/ngtsc/imports/src/core.ts b/packages/compiler-cli/src/ngtsc/imports/src/core.ts
index 122b399829..eae858337b 100644
--- a/packages/compiler-cli/src/ngtsc/imports/src/core.ts
+++ b/packages/compiler-cli/src/ngtsc/imports/src/core.ts
@@ -65,6 +65,7 @@ const CORE_SUPPORTED_SYMBOLS = new Map([
['ɵɵInjectorDef', 'ɵɵInjectorDef'],
['ɵɵNgModuleDefWithMeta', 'ɵɵNgModuleDefWithMeta'],
['ɵNgModuleFactory', 'NgModuleFactory'],
+ ['ɵnoSideEffects', 'ɵnoSideEffects'],
]);
const CORE_MODULE = '@angular/core';
diff --git a/packages/compiler-cli/src/ngtsc/shims/api.ts b/packages/compiler-cli/src/ngtsc/shims/api.ts
index 68cfbdb017..ec1e943c13 100644
--- a/packages/compiler-cli/src/ngtsc/shims/api.ts
+++ b/packages/compiler-cli/src/ngtsc/shims/api.ts
@@ -61,10 +61,15 @@ export interface PerFileShimGenerator {
export interface FactoryTracker {
readonly sourceInfo: Map;
- track(sf: ts.SourceFile, factorySymbolName: string): void;
+ track(sf: ts.SourceFile, moduleInfo: ModuleInfo): void;
}
export interface FactoryInfo {
sourceFilePath: string;
- moduleSymbolNames: Set;
-}
\ No newline at end of file
+ moduleSymbols: Map;
+}
+
+export interface ModuleInfo {
+ name: string;
+ hasId: boolean;
+}
diff --git a/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts b/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts
index 8e54e282a5..4ee78ec513 100644
--- a/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts
+++ b/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts
@@ -9,7 +9,7 @@ import * as ts from 'typescript';
import {absoluteFromSourceFile, AbsoluteFsPath, basename} from '../../file_system';
import {ImportRewriter} from '../../imports';
-import {FactoryInfo, FactoryTracker, PerFileShimGenerator} from '../api';
+import {FactoryInfo, FactoryTracker, ModuleInfo, PerFileShimGenerator} from '../api';
import {generatedModuleName} from './util';
@@ -22,7 +22,7 @@ const STRIP_NG_FACTORY = /(.*)NgFactory$/;
*/
export class FactoryGenerator implements PerFileShimGenerator, FactoryTracker {
readonly sourceInfo = new Map();
- private sourceToFactorySymbols = new Map>();
+ private sourceToFactorySymbols = new Map>();
readonly shouldEmit = true;
readonly extensionPrefix = 'ngfactory';
@@ -85,16 +85,19 @@ export class FactoryGenerator implements PerFileShimGenerator, FactoryTracker {
genFile.moduleName = generatedModuleName(sf.moduleName, sf.fileName, '.ngfactory');
}
- const moduleSymbolNames = new Set();
- this.sourceToFactorySymbols.set(absoluteSfPath, moduleSymbolNames);
- this.sourceInfo.set(genFilePath, {sourceFilePath: absoluteSfPath, moduleSymbolNames});
+ const moduleSymbols = new Map();
+ this.sourceToFactorySymbols.set(absoluteSfPath, moduleSymbols);
+ this.sourceInfo.set(genFilePath, {
+ sourceFilePath: absoluteSfPath,
+ moduleSymbols,
+ });
return genFile;
}
- track(sf: ts.SourceFile, factorySymbolName: string): void {
+ track(sf: ts.SourceFile, moduleInfo: ModuleInfo): void {
if (this.sourceToFactorySymbols.has(sf.fileName)) {
- this.sourceToFactorySymbols.get(sf.fileName)!.add(factorySymbolName);
+ this.sourceToFactorySymbols.get(sf.fileName)!.set(moduleInfo.name, moduleInfo);
}
}
}
@@ -123,7 +126,7 @@ function transformFactorySourceFile(
return file;
}
- const {moduleSymbolNames, sourceFilePath} = factoryMap.get(file.fileName)!;
+ const {moduleSymbols, sourceFilePath} = factoryMap.get(file.fileName)!;
file = ts.getMutableClone(file);
@@ -183,8 +186,24 @@ function transformFactorySourceFile(
// Otherwise, check if this export is a factory for a known NgModule, and retain it if so.
const match = STRIP_NG_FACTORY.exec(decl.name.text);
- if (match !== null && moduleSymbolNames.has(match[1])) {
- transformedStatements.push(stmt);
+ const module = match ? moduleSymbols.get(match[1]) : null;
+ if (module) {
+ // If the module can be tree shaken, then the factory should be wrapped in a
+ // `noSideEffects()` call which tells Closure to treat the expression as pure, allowing
+ // it to be removed if the result is not used.
+ //
+ // `NgModule`s with an `id` property will be lazy loaded. Google-internal lazy loading
+ // infra relies on a side effect from the `new NgModuleFactory()` call, which registers
+ // the module globally. Because of this, we **cannot** tree shake any module which has
+ // an `id` property. Doing so would cause lazy loaded modules to never be registered.
+ const moduleIsTreeShakable = !module.hasId;
+ const newStmt = !moduleIsTreeShakable ?
+ stmt :
+ updateInitializers(
+ stmt,
+ (init) => init ? wrapInNoSideEffects(init) : undefined,
+ );
+ transformedStatements.push(newStmt);
}
} else {
// Leave the statement alone, as it can't be understood.
@@ -263,3 +282,62 @@ function getFileoverviewComment(sourceFile: ts.SourceFile): string|null {
return commentText;
}
+
+/**
+ * Wraps the given expression in a call to `ɵnoSideEffects()`, which tells
+ * Closure we don't care about the side effects of this expression and it should
+ * be treated as "pure". Closure is free to tree shake this expression if its
+ * result is not used.
+ *
+ * Example: Takes `1 + 2` and returns `i0.ɵnoSideEffects(() => 1 + 2)`.
+ */
+function wrapInNoSideEffects(expr: ts.Expression): ts.Expression {
+ const noSideEffects = ts.createPropertyAccess(
+ ts.createIdentifier('i0'),
+ 'ɵnoSideEffects',
+ );
+
+ return ts.createCall(
+ noSideEffects,
+ /* typeArguments */[],
+ /* arguments */
+ [
+ ts.createFunctionExpression(
+ /* modifiers */[],
+ /* asteriskToken */ undefined,
+ /* name */ undefined,
+ /* typeParameters */[],
+ /* parameters */[],
+ /* type */ undefined,
+ /* body */ ts.createBlock([
+ ts.createReturn(expr),
+ ]),
+ ),
+ ],
+ );
+}
+
+/**
+ * Clones and updates the initializers for a given statement to use the new
+ * expression provided. Does not mutate the input statement.
+ */
+function updateInitializers(
+ stmt: ts.VariableStatement,
+ update: (initializer?: ts.Expression) => ts.Expression | undefined,
+ ): ts.VariableStatement {
+ return ts.updateVariableStatement(
+ stmt,
+ stmt.modifiers,
+ ts.updateVariableDeclarationList(
+ stmt.declarationList,
+ stmt.declarationList.declarations.map(
+ (decl) => ts.updateVariableDeclaration(
+ decl,
+ decl.name,
+ decl.type,
+ update(decl.initializer),
+ ),
+ ),
+ ),
+ );
+}
diff --git a/packages/compiler-cli/src/ngtsc/tsc_plugin.ts b/packages/compiler-cli/src/ngtsc/tsc_plugin.ts
index 7c87be7f22..54c6238521 100644
--- a/packages/compiler-cli/src/ngtsc/tsc_plugin.ts
+++ b/packages/compiler-cli/src/ngtsc/tsc_plugin.ts
@@ -79,7 +79,7 @@ export class NgTscPlugin implements TscPlugin {
}
wrapHost(
- host: ts.CompilerHost&UnifiedModulesHost, inputFiles: readonly string[],
+ host: ts.CompilerHost&Partial, inputFiles: readonly string[],
options: ts.CompilerOptions): PluginCompilerHost {
// TODO(alxhub): Eventually the `wrapHost()` API will accept the old `ts.Program` (if one is
// available). When it does, its `ts.SourceFile`s need to be re-tagged to enable proper
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts
index 139ec6a232..f80fd0fc69 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts
@@ -166,8 +166,8 @@ function constructTypeCtorParameter(
if (coercedKeys.length > 0) {
const coercedLiteral = ts.createTypeLiteralNode(coercedKeys);
- initType =
- initType !== null ? ts.createUnionTypeNode([initType, coercedLiteral]) : coercedLiteral;
+ initType = initType !== null ? ts.createIntersectionTypeNode([initType, coercedLiteral]) :
+ coercedLiteral;
}
if (initType === null) {
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts
index 54c399f1ba..f490cda7f5 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts
@@ -179,7 +179,7 @@ TestClass.ngTypeCtor({value: 'test'});
const typeCtor = TestClassWithCtor.members.find(isTypeCtor)!;
const ctorText = typeCtor.getText().replace(/[ \r\n]+/g, ' ');
expect(ctorText).toContain(
- 'init: Pick | { bar: typeof TestClass.ngAcceptInputType_bar; }');
+ 'init: Pick & { bar: typeof TestClass.ngAcceptInputType_bar; }');
});
});
});
diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
index 886e364fbc..5ae96b6e5c 100644
--- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
@@ -3538,7 +3538,9 @@ runInEachFileSystem(os => {
expect(factoryContents).toContain(`import * as i0 from '@angular/core';`);
expect(factoryContents).toContain(`import { NotAModule, TestModule } from './test';`);
expect(factoryContents)
- .toContain(`export var TestModuleNgFactory = new i0.\u0275NgModuleFactory(TestModule);`);
+ .toContain(
+ 'export var TestModuleNgFactory = i0.\u0275noSideEffects(function () { ' +
+ 'return new i0.\u0275NgModuleFactory(TestModule); });');
expect(factoryContents).not.toContain(`NotAModuleNgFactory`);
expect(factoryContents).not.toContain('\u0275NonEmptyModule');
@@ -3677,11 +3679,32 @@ runInEachFileSystem(os => {
env.driveMain();
const factoryContents = env.getContents('test.ngfactory.js');
- expect(normalize(factoryContents)).toBe(normalize(`
- import * as i0 from "./r3_symbols";
- import { TestModule } from './test';
- export var TestModuleNgFactory = new i0.NgModuleFactory(TestModule);
- `));
+ expect(factoryContents)
+ .toBe(
+ 'import * as i0 from "./r3_symbols";\n' +
+ 'import { TestModule } from \'./test\';\n' +
+ 'export var TestModuleNgFactory = i0.\u0275noSideEffects(function () {' +
+ ' return new i0.NgModuleFactory(TestModule); });\n');
+ });
+
+ it('should generate side effectful NgModuleFactory constructor when lazy loaded', () => {
+ env.tsconfig({'allowEmptyCodegenFiles': true});
+
+ env.write('test.ts', `
+ import {NgModule} from '@angular/core';
+
+ @NgModule({
+ id: 'test', // ID to use for lazy loading.
+ })
+ export class TestModule {}
+ `);
+
+ env.driveMain();
+
+ // Should **not** contain noSideEffects(), because the module is lazy loaded.
+ const factoryContents = env.getContents('test.ngfactory.js');
+ expect(factoryContents)
+ .toContain('export var TestModuleNgFactory = new i0.ɵNgModuleFactory(TestModule);');
});
describe('file-level comments', () => {
diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
index 377179e736..7c9ec4dbf4 100644
--- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
@@ -1498,6 +1498,39 @@ export declare class AnimationEvent {
expect(diags[0].messageText)
.toBe(`Type 'boolean' is not assignable to type 'string | number'.`);
});
+
+ it('should give an error for undefined bindings into regular inputs when coercion members are present',
+ () => {
+ env.tsconfig({strictTemplates: true});
+ env.write('test.ts', `
+ import {Component, Directive, NgModule, Input} from '@angular/core';
+
+ @Component({
+ selector: 'blah',
+ template: '',
+ })
+ export class FooCmp {
+ invalidType = true;
+ }
+
+ @Directive({selector: '[dir]'})
+ export class CoercionDir {
+ @Input() regular: string;
+ @Input() coerced: boolean;
+
+ static ngAcceptInputType_coerced: boolean|number;
+ }
+
+ @NgModule({
+ declarations: [FooCmp, CoercionDir],
+ })
+ export class FooModule {}
+ `);
+ const diags = env.driveDiagnostics();
+ expect(diags.length).toBe(1);
+ expect(diags[0].messageText)
+ .toBe(`Type 'undefined' is not assignable to type 'string'.`);
+ });
});
describe('legacy schema checking with the DOM schema', () => {
diff --git a/packages/compiler/test/i18n/integration_xliff2_spec.ts b/packages/compiler/test/i18n/integration_xliff2_spec.ts
index 184f471bec..9258a4e19e 100644
--- a/packages/compiler/test/i18n/integration_xliff2_spec.ts
+++ b/packages/compiler/test/i18n/integration_xliff2_spec.ts
@@ -7,15 +7,15 @@
*/
import {Xliff2} from '@angular/compiler/src/i18n/serializers/xliff2';
-import {async} from '@angular/core/testing';
+import {waitForAsync} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {configureCompiler, createComponent, HTML, serializeTranslations, validateHtml} from './integration_common';
describe('i18n XLIFF integration spec', () => {
describe('(with LF line endings)', () => {
- beforeEach(
- async(() => configureCompiler(XLIFF2_TOMERGE + LF_LINE_ENDING_XLIFF2_TOMERGE, 'xlf2')));
+ beforeEach(waitForAsync(
+ () => configureCompiler(XLIFF2_TOMERGE + LF_LINE_ENDING_XLIFF2_TOMERGE, 'xlf2')));
it('should extract from templates', () => {
const serializer = new Xliff2();
@@ -34,8 +34,8 @@ describe('i18n XLIFF integration spec', () => {
});
describe('(with CRLF line endings', () => {
- beforeEach(
- async(() => configureCompiler(XLIFF2_TOMERGE + CRLF_LINE_ENDING_XLIFF2_TOMERGE, 'xlf2')));
+ beforeEach(waitForAsync(
+ () => configureCompiler(XLIFF2_TOMERGE + CRLF_LINE_ENDING_XLIFF2_TOMERGE, 'xlf2')));
it('should extract from templates (with CRLF line endings)', () => {
const serializer = new Xliff2();
diff --git a/packages/compiler/test/i18n/integration_xliff_spec.ts b/packages/compiler/test/i18n/integration_xliff_spec.ts
index 6889a0d368..a7b5d9be22 100644
--- a/packages/compiler/test/i18n/integration_xliff_spec.ts
+++ b/packages/compiler/test/i18n/integration_xliff_spec.ts
@@ -7,14 +7,15 @@
*/
import {Xliff} from '@angular/compiler/src/i18n/serializers/xliff';
-import {async} from '@angular/core/testing';
+import {waitForAsync} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {configureCompiler, createComponent, HTML, serializeTranslations, validateHtml} from './integration_common';
describe('i18n XLIFF integration spec', () => {
describe('(with LF line endings)', () => {
- beforeEach(async(() => configureCompiler(XLIFF_TOMERGE + LF_LINE_ENDING_XLIFF_TOMERGE, 'xlf')));
+ beforeEach(
+ waitForAsync(() => configureCompiler(XLIFF_TOMERGE + LF_LINE_ENDING_XLIFF_TOMERGE, 'xlf')));
it('should extract from templates', () => {
const serializer = new Xliff();
@@ -33,8 +34,8 @@ describe('i18n XLIFF integration spec', () => {
});
describe('(with CRLF line endings', () => {
- beforeEach(
- async(() => configureCompiler(XLIFF_TOMERGE + CRLF_LINE_ENDING_XLIFF_TOMERGE, 'xlf')));
+ beforeEach(waitForAsync(
+ () => configureCompiler(XLIFF_TOMERGE + CRLF_LINE_ENDING_XLIFF_TOMERGE, 'xlf')));
it('should extract from templates (with CRLF line endings)', () => {
const serializer = new Xliff();
diff --git a/packages/compiler/test/i18n/integration_xmb_xtb_spec.ts b/packages/compiler/test/i18n/integration_xmb_xtb_spec.ts
index 20fecb9681..a214cc55ad 100644
--- a/packages/compiler/test/i18n/integration_xmb_xtb_spec.ts
+++ b/packages/compiler/test/i18n/integration_xmb_xtb_spec.ts
@@ -7,14 +7,14 @@
*/
import {Xmb} from '@angular/compiler/src/i18n/serializers/xmb';
-import {async} from '@angular/core/testing';
+import {waitForAsync} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {configureCompiler, createComponent, HTML, serializeTranslations, validateHtml} from './integration_common';
describe('i18n XMB/XTB integration spec', () => {
describe('(with LF line endings)', () => {
- beforeEach(async(() => configureCompiler(XTB + LF_LINE_ENDING_XTB, 'xtb')));
+ beforeEach(waitForAsync(() => configureCompiler(XTB + LF_LINE_ENDING_XTB, 'xtb')));
it('should extract from templates', () => {
const serializer = new Xmb();
@@ -33,7 +33,7 @@ describe('i18n XMB/XTB integration spec', () => {
});
describe('(with CRLF line endings', () => {
- beforeEach(async(() => configureCompiler(XTB + CRLF_LINE_ENDING_XTB, 'xtb')));
+ beforeEach(waitForAsync(() => configureCompiler(XTB + CRLF_LINE_ENDING_XTB, 'xtb')));
it('should extract from templates (with CRLF line endings)', () => {
const serializer = new Xmb();
diff --git a/packages/compiler/test/integration_spec.ts b/packages/compiler/test/integration_spec.ts
index de24253a37..ef5ddcb69c 100644
--- a/packages/compiler/test/integration_spec.ts
+++ b/packages/compiler/test/integration_spec.ts
@@ -7,7 +7,7 @@
*/
import {Component, Directive, Input} from '@angular/core';
-import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {expect} from '@angular/platform-browser/testing/src/matchers';
@@ -17,7 +17,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
let fixture: ComponentFixture;
describe('directives', () => {
- it('should support dotted selectors', async(() => {
+ it('should support dotted selectors', waitForAsync(() => {
@Directive({selector: '[dot.name]'})
class MyDir {
// TODO(issue/24571): remove '!'.
@@ -41,7 +41,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
describe('ng-container', () => {
if (browserDetection.isChromeDesktop) {
- it('should work regardless the namespace', async(() => {
+ it('should work regardless the namespace', waitForAsync(() => {
@Component({
selector: 'comp',
template:
diff --git a/packages/compiler/test/metadata_resolver_spec.ts b/packages/compiler/test/metadata_resolver_spec.ts
index f97125e4bd..040d075b4c 100644
--- a/packages/compiler/test/metadata_resolver_spec.ts
+++ b/packages/compiler/test/metadata_resolver_spec.ts
@@ -8,7 +8,7 @@
import {LIFECYCLE_HOOKS_VALUES, LifecycleHooks} from '@angular/compiler/src/lifecycle_reflector';
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, Component, Directive, DoCheck, Injectable, NgModule, OnChanges, OnDestroy, OnInit, Pipe, SimpleChanges, ViewEncapsulation, ɵstringify as stringify} from '@angular/core';
-import {async, inject, TestBed} from '@angular/core/testing';
+import {inject, TestBed, waitForAsync} from '@angular/core/testing';
import {CompileDiDependencyMetadata, identifierName} from '../src/compile_metadata';
import {CompileMetadataResolver} from '../src/metadata_resolver';
@@ -77,7 +77,7 @@ import {TEST_COMPILER_PROVIDERS} from './test_bindings';
}));
it('should read external metadata when sync=false',
- async(inject(
+ waitForAsync(inject(
[CompileMetadataResolver, ResourceLoader],
(resolver: CompileMetadataResolver, resourceLoader: MockResourceLoader) => {
@NgModule({declarations: [ComponentWithExternalResources]})
@@ -96,7 +96,7 @@ import {TEST_COMPILER_PROVIDERS} from './test_bindings';
})));
it('should use `./` as base url for templates during runtime compilation if no moduleId is given',
- async(inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
+ waitForAsync(inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@Component({selector: 'someComponent', templateUrl: 'someUrl'})
class ComponentWithoutModuleId {
}
diff --git a/packages/compiler/test/runtime_compiler_spec.ts b/packages/compiler/test/runtime_compiler_spec.ts
index 1cdc932c09..ce37f36e9f 100644
--- a/packages/compiler/test/runtime_compiler_spec.ts
+++ b/packages/compiler/test/runtime_compiler_spec.ts
@@ -8,7 +8,7 @@
import {DirectiveResolver, ResourceLoader} from '@angular/compiler';
import {Compiler, Component, Injector, NgModule, NgModuleFactory, ɵstringify as stringify} from '@angular/core';
-import {async, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
+import {fakeAsync, inject, TestBed, tick, waitForAsync} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {MockDirectiveResolver} from '../testing';
@@ -42,7 +42,8 @@ class SomeCompWithUrlTemplate {
{providers: [{provide: ResourceLoader, useClass: StubResourceLoader, deps: []}]});
});
- it('should throw when using a templateUrl that has not been compiled before', async(() => {
+ it('should throw when using a templateUrl that has not been compiled before',
+ waitForAsync(() => {
TestBed.configureTestingModule({declarations: [SomeCompWithUrlTemplate]});
TestBed.compileComponents().then(() => {
expect(() => TestBed.createComponent(SomeCompWithUrlTemplate))
@@ -76,7 +77,8 @@ class SomeCompWithUrlTemplate {
{providers: [{provide: ResourceLoader, useClass: StubResourceLoader, deps: []}]});
});
- it('should allow to use templateUrl components that have been loaded before', async(() => {
+ it('should allow to use templateUrl components that have been loaded before',
+ waitForAsync(() => {
TestBed.configureTestingModule({declarations: [SomeCompWithUrlTemplate]});
TestBed.compileComponents().then(() => {
const fixture = TestBed.createComponent(SomeCompWithUrlTemplate);
diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts
index d20a6449ac..ee09ad9f7a 100644
--- a/packages/core/src/core_render3_private_export.ts
+++ b/packages/core/src/core_render3_private_export.ts
@@ -292,5 +292,8 @@ export {
ɵɵsanitizeUrl,
ɵɵsanitizeUrlOrResourceUrl,
} from './sanitization/sanitization';
+export {
+ noSideEffects as ɵnoSideEffects,
+} from './util/closure';
// clang-format on
diff --git a/packages/core/src/r3_symbols.ts b/packages/core/src/r3_symbols.ts
index 391bb8bf56..1ee3762e83 100644
--- a/packages/core/src/r3_symbols.ts
+++ b/packages/core/src/r3_symbols.ts
@@ -28,6 +28,7 @@ export {ɵɵdefineNgModule} from './render3/definition';
export {ɵɵFactoryDef} from './render3/interfaces/definition';
export {setClassMetadata} from './render3/metadata';
export {NgModuleFactory} from './render3/ng_module_ref';
+export {noSideEffects as ɵnoSideEffects} from './util/closure';
diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts
index 1377482c80..ab8e7b4dfc 100644
--- a/packages/core/src/render3/i18n.ts
+++ b/packages/core/src/render3/i18n.ts
@@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import '../util/ng_i18n_closure_mode';
+import '../util/ng_dev_mode';
import {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization';
import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../sanitization/html_sanitizer';
@@ -16,8 +17,8 @@ import {assertDataInRange, assertDefined, assertEqual} from '../util/assert';
import {bindingUpdated} from './bindings';
import {attachPatchData} from './context_discovery';
+import {i18nMutateOpCodesToString, i18nUpdateOpCodesToString} from './i18n_debug';
import {setDelayProjection} from './instructions/all';
-import {attachI18nOpCodesDebug} from './instructions/lview_debug';
import {allocExpando, elementAttributeInternal, elementPropertyInternal, getOrCreateTNode, setInputsForProperty, setNgReflectProperties, textBindingInternal} from './instructions/shared';
import {LContainer, NATIVE} from './interfaces/container';
import {getDocument} from './interfaces/document';
@@ -29,6 +30,7 @@ import {isLContainer} from './interfaces/type_checks';
import {HEADER_OFFSET, LView, RENDERER, T_HOST, TVIEW, TView} from './interfaces/view';
import {appendChild, applyProjection, createTextNode, nativeRemoveNode} from './node_manipulation';
import {getBindingIndex, getIsParent, getLView, getPreviousOrParentTNode, getTView, nextBindingIndex, setIsNotParent, setPreviousOrParentTNode} from './state';
+import {attachDebugGetter} from './util/debug_utils';
import {renderStringify} from './util/misc_utils';
import {getNativeByIndex, getNativeByTNode, getTNode, load} from './util/view_utils';
@@ -267,6 +269,9 @@ function generateBindingUpdateOpCodes(
str: string, destinationNode: number, attrName?: string,
sanitizeFn: SanitizerFn|null = null): I18nUpdateOpCodes {
const updateOpCodes: I18nUpdateOpCodes = [null, null]; // Alloc space for mask and size
+ if (ngDevMode) {
+ attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
+ }
const textParts = str.split(BINDING_REGEXP);
let mask = 0;
@@ -395,6 +400,9 @@ function i18nStartFirstPass(
let parentIndexPointer = 0;
parentIndexStack[parentIndexPointer] = parentIndex;
const createOpCodes: I18nMutateOpCodes = [];
+ if (ngDevMode) {
+ attachDebugGetter(createOpCodes, i18nMutateOpCodesToString);
+ }
// If the previous node wasn't the direct parent then we have a translation without top level
// element and we need to keep a reference of the previous element if there is one. We should also
// keep track whether an element was a parent node or not, so that the logic that consumes
@@ -411,6 +419,9 @@ function i18nStartFirstPass(
createOpCodes.push(previousTNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select);
}
const updateOpCodes: I18nUpdateOpCodes = [];
+ if (ngDevMode) {
+ attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
+ }
const icuExpressions: TIcu[] = [];
if (message === '' && isRootTemplateMessage(subTemplateIndex)) {
@@ -507,10 +518,6 @@ function i18nStartFirstPass(
allocExpando(tView, lView, i18nVarsCount);
}
- ngDevMode &&
- attachI18nOpCodesDebug(
- createOpCodes, updateOpCodes, icuExpressions.length ? icuExpressions : null, lView);
-
// NOTE: local var needed to properly assert the type of `TI18n`.
const tI18n: TI18n = {
vars: i18nVarsCount,
@@ -751,6 +758,7 @@ function createDynamicNodeAtIndex(
const previousOrParentTNode = getPreviousOrParentTNode();
ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET);
lView[index + HEADER_OFFSET] = native;
+ // FIXME(misko): Why does this create A TNode??? I would not expect this to be here.
const tNode = getOrCreateTNode(tView, lView[T_HOST], index, type as any, name, null);
// We are creating a dynamic node, the previous tNode might not be pointing at this node.
@@ -780,7 +788,7 @@ function readCreateOpCodes(
visitedNodes.push(textNodeIndex);
setIsNotParent();
} else if (typeof opCode == 'number') {
- switch (opCode & I18nMutateOpCode.MASK_OPCODE) {
+ switch (opCode & I18nMutateOpCode.MASK_INSTRUCTION) {
case I18nMutateOpCode.AppendChild:
const destinationNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_PARENT;
let destinationTNode: TNode;
@@ -799,9 +807,10 @@ function readCreateOpCodes(
appendI18nNode(tView, currentTNode!, destinationTNode, previousTNode, lView);
break;
case I18nMutateOpCode.Select:
- // Negative indicies indicate that a given TNode is a sibling node, not a parent node
+ // Negative indices indicate that a given TNode is a sibling node, not a parent node
// (see `i18nStartFirstPass` for additional information).
const isParent = opCode >= 0;
+ // FIXME(misko): This SHIFT_REF looks suspect as it does not have mask.
const nodeIndex = (isParent ? opCode : ~opCode) >>> I18nMutateOpCode.SHIFT_REF;
visitedNodes.push(nodeIndex);
previousTNode = currentTNode;
@@ -874,7 +883,7 @@ function readCreateOpCodes(
function readUpdateOpCodes(
updateOpCodes: I18nUpdateOpCodes, icus: TIcu[]|null, bindingsStartIndex: number,
- changeMask: number, tView: TView, lView: LView, bypassCheckBit = false) {
+ changeMask: number, tView: TView, lView: LView, bypassCheckBit: boolean) {
let caseCreated = false;
for (let i = 0; i < updateOpCodes.length; i++) {
// bit code to check if we should apply the next update
@@ -890,13 +899,10 @@ function readUpdateOpCodes(
value += opCode;
} else if (typeof opCode == 'number') {
if (opCode < 0) {
- // It's a binding index whose value is negative
+ // Negative opCode represent `i18nExp` values offset.
value += renderStringify(lView[bindingsStartIndex - opCode]);
} else {
const nodeIndex = opCode >>> I18nUpdateOpCode.SHIFT_REF;
- let tIcuIndex: number;
- let tIcu: TIcu;
- let icuTNode: TIcuContainerNode;
switch (opCode & I18nUpdateOpCode.MASK_OPCODE) {
case I18nUpdateOpCode.Attr:
const propName = updateOpCodes[++j] as string;
@@ -909,56 +915,13 @@ function readUpdateOpCodes(
textBindingInternal(lView, nodeIndex, value);
break;
case I18nUpdateOpCode.IcuSwitch:
- tIcuIndex = updateOpCodes[++j] as number;
- tIcu = icus![tIcuIndex];
- icuTNode = getTNode(tView, nodeIndex) as TIcuContainerNode;
- // If there is an active case, delete the old nodes
- if (icuTNode.activeCaseIndex !== null) {
- const removeCodes = tIcu.remove[icuTNode.activeCaseIndex];
- for (let k = 0; k < removeCodes.length; k++) {
- const removeOpCode = removeCodes[k] as number;
- switch (removeOpCode & I18nMutateOpCode.MASK_OPCODE) {
- case I18nMutateOpCode.Remove:
- const nodeIndex = removeOpCode >>> I18nMutateOpCode.SHIFT_REF;
- // Remove DOM element, but do *not* mark TNode as detached, since we are
- // just switching ICU cases (while keeping the same TNode), so a DOM element
- // representing a new ICU case will be re-created.
- removeNode(tView, lView, nodeIndex, /* markAsDetached */ false);
- break;
- case I18nMutateOpCode.RemoveNestedIcu:
- const nestedIcuNodeIndex =
- removeCodes[k + 1] as number >>> I18nMutateOpCode.SHIFT_REF;
- const nestedIcuTNode =
- getTNode(tView, nestedIcuNodeIndex) as TIcuContainerNode;
- const activeIndex = nestedIcuTNode.activeCaseIndex;
- if (activeIndex !== null) {
- const nestedIcuTIndex = removeOpCode >>> I18nMutateOpCode.SHIFT_REF;
- const nestedTIcu = icus![nestedIcuTIndex];
- addAllToArray(nestedTIcu.remove[activeIndex], removeCodes);
- }
- break;
- }
- }
- }
-
- // Update the active caseIndex
- const caseIndex = getCaseIndex(tIcu, value);
- icuTNode.activeCaseIndex = caseIndex !== -1 ? caseIndex : null;
- if (caseIndex > -1) {
- // Add the nodes for the new case
- readCreateOpCodes(-1, tIcu.create[caseIndex], tView, lView);
- caseCreated = true;
- }
+ caseCreated = icuSwitchCase(
+ tView, updateOpCodes[++j] as number, nodeIndex, icus!, lView, value);
break;
case I18nUpdateOpCode.IcuUpdate:
- tIcuIndex = updateOpCodes[++j] as number;
- tIcu = icus![tIcuIndex];
- icuTNode = getTNode(tView, nodeIndex) as TIcuContainerNode;
- if (icuTNode.activeCaseIndex !== null) {
- readUpdateOpCodes(
- tIcu.update[icuTNode.activeCaseIndex], icus, bindingsStartIndex, changeMask,
- tView, lView, caseCreated);
- }
+ icuUpdateCase(
+ tView, lView, updateOpCodes[++j] as number, nodeIndex, bindingsStartIndex,
+ icus!, caseCreated);
break;
}
}
@@ -969,6 +932,70 @@ function readUpdateOpCodes(
}
}
+function icuUpdateCase(
+ tView: TView, lView: LView, tIcuIndex: number, nodeIndex: number, bindingsStartIndex: number,
+ tIcus: TIcu[], caseCreated: boolean) {
+ const tIcu = tIcus[tIcuIndex];
+ const icuTNode = getTNode(tView, nodeIndex) as TIcuContainerNode;
+ if (icuTNode.activeCaseIndex !== null) {
+ readUpdateOpCodes(
+ tIcu.update[icuTNode.activeCaseIndex], tIcus, bindingsStartIndex, changeMask, tView, lView,
+ caseCreated);
+ }
+}
+
+function icuSwitchCase(
+ tView: TView, tIcuIndex: number, nodeIndex: number, tIcus: TIcu[], lView: LView,
+ value: string): boolean {
+ const tIcu = tIcus[tIcuIndex];
+ const icuTNode = getTNode(tView, nodeIndex) as TIcuContainerNode;
+ let caseCreated = false;
+ // If there is an active case, delete the old nodes
+ if (icuTNode.activeCaseIndex !== null) {
+ const removeCodes = tIcu.remove[icuTNode.activeCaseIndex];
+ for (let k = 0; k < removeCodes.length; k++) {
+ const removeOpCode = removeCodes[k] as number;
+ const nodeOrIcuIndex = removeOpCode >>> I18nMutateOpCode.SHIFT_REF;
+ switch (removeOpCode & I18nMutateOpCode.MASK_INSTRUCTION) {
+ case I18nMutateOpCode.Remove:
+ // Remove DOM element, but do *not* mark TNode as detached, since we are
+ // just switching ICU cases (while keeping the same TNode), so a DOM element
+ // representing a new ICU case will be re-created.
+ removeNode(tView, lView, nodeOrIcuIndex, /* markAsDetached */ false);
+ break;
+ case I18nMutateOpCode.RemoveNestedIcu:
+ removeNestedIcu(
+ tView, tIcus, removeCodes, nodeOrIcuIndex,
+ removeCodes[k + 1] as number >>> I18nMutateOpCode.SHIFT_REF);
+ break;
+ }
+ }
+ }
+
+ // Update the active caseIndex
+ const caseIndex = getCaseIndex(tIcu, value);
+ icuTNode.activeCaseIndex = caseIndex !== -1 ? caseIndex : null;
+ if (caseIndex > -1) {
+ // Add the nodes for the new case
+ readCreateOpCodes(
+ -1 /* -1 means we don't have parent node */, tIcu.create[caseIndex], tView, lView);
+ caseCreated = true;
+ }
+ return caseCreated;
+}
+
+function removeNestedIcu(
+ tView: TView, tIcus: TIcu[], removeCodes: I18nMutateOpCodes, nodeIndex: number,
+ nestedIcuNodeIndex: number) {
+ const nestedIcuTNode = getTNode(tView, nestedIcuNodeIndex) as TIcuContainerNode;
+ const activeIndex = nestedIcuTNode.activeCaseIndex;
+ if (activeIndex !== null) {
+ const nestedTIcu = tIcus[nodeIndex];
+ // FIXME(misko): the fact that we are adding items to parent list looks very suspect!
+ addAllToArray(nestedTIcu.remove[activeIndex], removeCodes);
+ }
+}
+
function removeNode(tView: TView, lView: LView, index: number, markAsDetached: boolean) {
const removedPhTNode = getTNode(tView, index);
const removedPhRNode = getNativeByIndex(index, lView);
@@ -1044,6 +1071,9 @@ function i18nAttributesFirstPass(lView: LView, tView: TView, index: number, valu
const previousElement = getPreviousOrParentTNode();
const previousElementIndex = previousElement.index - HEADER_OFFSET;
const updateOpCodes: I18nUpdateOpCodes = [];
+ if (ngDevMode) {
+ attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
+ }
for (let i = 0; i < values.length; i += 2) {
const attrName = values[i];
const message = values[i + 1];
@@ -1134,7 +1164,7 @@ export function ɵɵi18nApply(index: number) {
}
const bindingsStartIndex = getBindingIndex() - shiftsCounter - 1;
const lView = getLView();
- readUpdateOpCodes(updateOpCodes, icus, bindingsStartIndex, changeMask, tView, lView);
+ readUpdateOpCodes(updateOpCodes, icus, bindingsStartIndex, changeMask, tView, lView, false);
// Reset changeMask & maskBit to default for the next update cycle
changeMask = 0b0;
@@ -1180,9 +1210,9 @@ function getCaseIndex(icuExpression: TIcu, bindingValue: string): number {
function icuStart(
tIcus: TIcu[], icuExpression: IcuExpression, startIndex: number,
expandoStartIndex: number): void {
- const createCodes = [];
- const removeCodes = [];
- const updateCodes = [];
+ const createCodes: I18nMutateOpCodes[] = [];
+ const removeCodes: I18nMutateOpCodes[] = [];
+ const updateCodes: I18nUpdateOpCodes[] = [];
const vars = [];
const childIcus: number[][] = [];
for (let i = 0; i < icuExpression.values.length; i++) {
@@ -1240,6 +1270,11 @@ function parseIcuCase(
}
const wrapper = getTemplateContent(inertBodyElement!) as Element || inertBodyElement;
const opCodes: IcuCase = {vars: 0, childIcus: [], create: [], remove: [], update: []};
+ if (ngDevMode) {
+ attachDebugGetter(opCodes.create, i18nMutateOpCodesToString);
+ attachDebugGetter(opCodes.remove, i18nMutateOpCodesToString);
+ attachDebugGetter(opCodes.update, i18nUpdateOpCodesToString);
+ }
parseNodes(wrapper.firstChild, opCodes, parentIndex, nestedIcus, tIcus, expandoStartIndex);
return opCodes;
}
@@ -1364,6 +1399,7 @@ function parseNodes(
3, // skip 3 opCodes if not changed
-1 - nestedIcu.mainBinding,
nestedIcuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch,
+ // FIXME(misko): Index should be part of the opcode
nestTIcuIndex,
mask, // mask of all the bindings of this ICU expression
2, // skip 2 opCodes if not changed
@@ -1371,6 +1407,7 @@ function parseNodes(
nestTIcuIndex);
icuCase.remove.push(
nestTIcuIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.RemoveNestedIcu,
+ // FIXME(misko): Index should be part of the opcode
nestedIcuNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
}
}
diff --git a/packages/core/src/render3/i18n_debug.ts b/packages/core/src/render3/i18n_debug.ts
new file mode 100644
index 0000000000..930f810428
--- /dev/null
+++ b/packages/core/src/render3/i18n_debug.ts
@@ -0,0 +1,186 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {assertNumber, assertString} from '../util/assert';
+
+import {COMMENT_MARKER, ELEMENT_MARKER, getInstructionFromI18nMutateOpCode, getParentFromI18nMutateOpCode, getRefFromI18nMutateOpCode, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes} from './interfaces/i18n';
+
+/**
+ * Converts `I18nUpdateOpCodes` array into a human readable format.
+ *
+ * This function is attached to the `I18nUpdateOpCodes.debug` property if `ngDevMode` is enabled.
+ * This function provides a human readable view of the opcodes. This is useful when debugging the
+ * application as well as writing more readable tests.
+ *
+ * @param this `I18nUpdateOpCodes` if attached as a method.
+ * @param opcodes `I18nUpdateOpCodes` if invoked as a function.
+ */
+export function i18nUpdateOpCodesToString(
+ this: I18nUpdateOpCodes|void, opcodes?: I18nUpdateOpCodes): string[] {
+ const parser = new OpCodeParser(opcodes || (Array.isArray(this) ? this : []));
+ let lines: string[] = [];
+
+ function consumeOpCode(value: number): string {
+ const ref = value >>> I18nUpdateOpCode.SHIFT_REF;
+ const opCode = value & I18nUpdateOpCode.MASK_OPCODE;
+ switch (opCode) {
+ case I18nUpdateOpCode.Text:
+ return `(lView[${ref}] as Text).textContent = $$$`;
+ case I18nUpdateOpCode.Attr:
+ const attrName = parser.consumeString();
+ const sanitizationFn = parser.consumeFunction();
+ const value = sanitizationFn ? `(${sanitizationFn})($$$)` : '$$$';
+ return `(lView[${ref}] as Element).setAttribute('${attrName}', ${value})`;
+ case I18nUpdateOpCode.IcuSwitch:
+ return `icuSwitchCase(lView[${ref}] as Comment, ${parser.consumeNumber()}, $$$)`;
+ case I18nUpdateOpCode.IcuUpdate:
+ return `icuUpdateCase(lView[${ref}] as Comment, ${parser.consumeNumber()})`;
+ }
+ throw new Error('unexpected OpCode');
+ }
+
+
+ while (parser.hasMore()) {
+ let mask = parser.consumeNumber();
+ let size = parser.consumeNumber();
+ const end = parser.i + size;
+ const statements: string[] = [];
+ let statement = '';
+ while (parser.i < end) {
+ let value = parser.consumeNumberOrString();
+ if (typeof value === 'string') {
+ statement += value;
+ } else if (value < 0) {
+ // Negative numbers are ref indexes
+ statement += '${lView[' + (0 - value) + ']}';
+ } else {
+ // Positive numbers are operations.
+ const opCodeText = consumeOpCode(value);
+ statements.push(opCodeText.replace('$$$', '`' + statement + '`') + ';');
+ statement = '';
+ }
+ }
+ lines.push(`if (mask & 0b${mask.toString(2)}) { ${statements.join(' ')} }`);
+ }
+ return lines;
+}
+
+/**
+ * Converts `I18nMutableOpCodes` array into a human readable format.
+ *
+ * This function is attached to the `I18nMutableOpCodes.debug` if `ngDevMode` is enabled. This
+ * function provides a human readable view of the opcodes. This is useful when debugging the
+ * application as well as writing more readable tests.
+ *
+ * @param this `I18nMutableOpCodes` if attached as a method.
+ * @param opcodes `I18nMutableOpCodes` if invoked as a function.
+ */
+export function i18nMutateOpCodesToString(
+ this: I18nMutateOpCodes|void, opcodes?: I18nMutateOpCodes): string[] {
+ const parser = new OpCodeParser(opcodes || (Array.isArray(this) ? this : []));
+ let lines: string[] = [];
+
+ function consumeOpCode(opCode: number): string {
+ const parent = getParentFromI18nMutateOpCode(opCode);
+ const ref = getRefFromI18nMutateOpCode(opCode);
+ switch (getInstructionFromI18nMutateOpCode(opCode)) {
+ case I18nMutateOpCode.Select:
+ lastRef = ref;
+ return '';
+ case I18nMutateOpCode.AppendChild:
+ return `(lView[${parent}] as Element).appendChild(lView[${lastRef}])`;
+ case I18nMutateOpCode.Remove:
+ return `(lView[${parent}] as Element).remove(lView[${ref}])`;
+ case I18nMutateOpCode.Attr:
+ return `(lView[${ref}] as Element).setAttribute("${parser.consumeString()}", "${
+ parser.consumeString()}")`;
+ case I18nMutateOpCode.ElementEnd:
+ return `setPreviousOrParentTNode(tView.data[${ref}] as TNode)`;
+ case I18nMutateOpCode.RemoveNestedIcu:
+ return `removeNestedICU(${ref})`;
+ }
+ throw new Error('Unexpected OpCode');
+ }
+
+ let lastRef = -1;
+ while (parser.hasMore()) {
+ let value = parser.consumeNumberStringOrMarker();
+ if (value === COMMENT_MARKER) {
+ const text = parser.consumeString();
+ lastRef = parser.consumeNumber();
+ lines.push(`lView[${lastRef}] = document.createComment("${text}")`);
+ } else if (value === ELEMENT_MARKER) {
+ const text = parser.consumeString();
+ lastRef = parser.consumeNumber();
+ lines.push(`lView[${lastRef}] = document.createElement("${text}")`);
+ } else if (typeof value === 'string') {
+ lastRef = parser.consumeNumber();
+ lines.push(`lView[${lastRef}] = document.createTextNode("${value}")`);
+ } else if (typeof value === 'number') {
+ const line = consumeOpCode(value);
+ line && lines.push(line);
+ } else {
+ throw new Error('Unexpected value');
+ }
+ }
+
+ return lines;
+}
+
+
+class OpCodeParser {
+ i: number = 0;
+ codes: any[];
+
+ constructor(codes: any[]) {
+ this.codes = codes;
+ }
+
+ hasMore() {
+ return this.i < this.codes.length;
+ }
+
+ consumeNumber(): number {
+ let value = this.codes[this.i++];
+ assertNumber(value, 'expecting number in OpCode');
+ return value;
+ }
+
+ consumeString(): string {
+ let value = this.codes[this.i++];
+ assertString(value, 'expecting string in OpCode');
+ return value;
+ }
+
+ consumeFunction(): Function|null {
+ let value = this.codes[this.i++];
+ if (value === null || typeof value === 'function') {
+ return value;
+ }
+ throw new Error('expecting function in OpCode');
+ }
+
+ consumeNumberOrString(): number|string {
+ let value = this.codes[this.i++];
+ if (typeof value === 'string') {
+ return value;
+ }
+ assertNumber(value, 'expecting number or string in OpCode');
+ return value;
+ }
+
+ consumeNumberStringOrMarker(): number|string|COMMENT_MARKER|ELEMENT_MARKER {
+ let value = this.codes[this.i++];
+ if (typeof value === 'string' || typeof value === 'number' || value == COMMENT_MARKER ||
+ value == ELEMENT_MARKER) {
+ return value;
+ }
+ assertNumber(value, 'expecting number, string, COMMENT_MARKER or ELEMENT_MARKER in OpCode');
+ return value;
+ }
+}
diff --git a/packages/core/src/render3/instructions/lview_debug.ts b/packages/core/src/render3/instructions/lview_debug.ts
index 1bd45018d0..7ea4307567 100644
--- a/packages/core/src/render3/instructions/lview_debug.ts
+++ b/packages/core/src/render3/instructions/lview_debug.ts
@@ -15,15 +15,14 @@ import {createNamedArrayType} from '../../util/named_array_type';
import {initNgDevMode} from '../../util/ng_dev_mode';
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS, NATIVE} from '../interfaces/container';
import {DirectiveDefList, PipeDefList, ViewQueriesFunction} from '../interfaces/definition';
-import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, TIcu} from '../interfaces/i18n';
-import {PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TViewNode} from '../interfaces/node';
+import {PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TNodeTypeAsString, TViewNode} from '../interfaces/node';
import {SelectorFlags} from '../interfaces/projection';
import {LQueries, TQueries} from '../interfaces/query';
import {RComment, RElement, Renderer3, RendererFactory3, RNode} from '../interfaces/renderer';
import {getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate, TStylingKey, TStylingRange} from '../interfaces/styling';
-import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, DestroyHookData, ExpandoInstructions, FLAGS, HEADER_OFFSET, HookData, HOST, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TVIEW, TView as ITView, TView, TViewType} from '../interfaces/view';
+import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DebugNode, DECLARATION_VIEW, DestroyHookData, ExpandoInstructions, FLAGS, HEADER_OFFSET, HookData, HOST, INJECTOR, LContainerDebug as ILContainerDebug, LView, LViewDebug as ILViewDebug, LViewDebugRange, LViewDebugRangeContent, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TView as ITView, TVIEW, TView, TViewType} from '../interfaces/view';
import {attachDebugObject} from '../util/debug_utils';
-import {getTNode, unwrapRNode} from '../util/view_utils';
+import {unwrapRNode} from '../util/view_utils';
const NG_DEV_MODE = ((typeof ngDevMode === 'undefined' || !!ngDevMode) && initNgDevMode());
@@ -143,7 +142,10 @@ export const TViewConstructor = class TView implements ITView {
public firstChild: ITNode|null, //
public schemas: SchemaMetadata[]|null, //
public consts: TConstants|null, //
- public incompleteFirstPass: boolean //
+ public incompleteFirstPass: boolean, //
+ public _decls: number, //
+ public _vars: number, //
+
) {}
get template_(): string {
@@ -335,9 +337,9 @@ export function attachLContainerDebug(lContainer: LContainer) {
attachDebugObject(lContainer, new LContainerDebug(lContainer));
}
-export function toDebug(obj: LView): LViewDebug;
-export function toDebug(obj: LView|null): LViewDebug|null;
-export function toDebug(obj: LView|LContainer|null): LViewDebug|LContainerDebug|null;
+export function toDebug(obj: LView): ILViewDebug;
+export function toDebug(obj: LView|null): ILViewDebug|null;
+export function toDebug(obj: LView|LContainer|null): ILViewDebug|ILContainerDebug|null;
export function toDebug(obj: any): any {
if (obj) {
const debug = (obj as any).debug;
@@ -375,7 +377,7 @@ function toHtml(value: any, includeChildren: boolean = false): string|null {
}
}
-export class LViewDebug {
+export class LViewDebug implements ILViewDebug {
constructor(private readonly _raw_lView: LView) {}
/**
@@ -396,10 +398,10 @@ export class LViewDebug {
indexWithinInitPhase: flags >> LViewFlags.IndexWithinInitPhaseShift,
};
}
- get parent(): LViewDebug|LContainerDebug|null {
+ get parent(): ILViewDebug|ILContainerDebug|null {
return toDebug(this._raw_lView[PARENT]);
}
- get host(): string|null {
+ get hostHTML(): string|null {
return toHtml(this._raw_lView[HOST], true);
}
get html(): string {
@@ -410,10 +412,9 @@ export class LViewDebug {
}
/**
* The tree of nodes associated with the current `LView`. The nodes have been normalized into
- * a
- * tree structure with relevant details pulled out for readability.
+ * a tree structure with relevant details pulled out for readability.
*/
- get nodes(): DebugNode[]|null {
+ get nodes(): DebugNode[] {
const lView = this._raw_lView;
const tNode = lView[TVIEW].firstChild;
return toDebugNodes(tNode, lView);
@@ -437,16 +438,16 @@ export class LViewDebug {
get sanitizer(): Sanitizer|null {
return this._raw_lView[SANITIZER];
}
- get childHead(): LViewDebug|LContainerDebug|null {
+ get childHead(): ILViewDebug|ILContainerDebug|null {
return toDebug(this._raw_lView[CHILD_HEAD]);
}
- get next(): LViewDebug|LContainerDebug|null {
+ get next(): ILViewDebug|ILContainerDebug|null {
return toDebug(this._raw_lView[NEXT]);
}
- get childTail(): LViewDebug|LContainerDebug|null {
+ get childTail(): ILViewDebug|ILContainerDebug|null {
return toDebug(this._raw_lView[CHILD_TAIL]);
}
- get declarationView(): LViewDebug|null {
+ get declarationView(): ILViewDebug|null {
return toDebug(this._raw_lView[DECLARATION_VIEW]);
}
get queries(): LQueries|null {
@@ -456,11 +457,35 @@ export class LViewDebug {
return this._raw_lView[T_HOST];
}
+ get decls(): LViewDebugRange {
+ const tView = this.tView as any as {_decls: number, _vars: number};
+ const start = HEADER_OFFSET;
+ return toLViewRange(this.tView, this._raw_lView, start, start + tView._decls);
+ }
+
+ get vars(): LViewDebugRange {
+ const tView = this.tView as any as {_decls: number, _vars: number};
+ const start = HEADER_OFFSET + tView._decls;
+ return toLViewRange(this.tView, this._raw_lView, start, start + tView._vars);
+ }
+
+ get i18n(): LViewDebugRange {
+ const tView = this.tView as any as {_decls: number, _vars: number};
+ const start = HEADER_OFFSET + tView._decls + tView._vars;
+ return toLViewRange(this.tView, this._raw_lView, start, this.tView.expandoStartIndex);
+ }
+
+ get expando(): LViewDebugRange {
+ const tView = this.tView as any as {_decls: number, _vars: number};
+ return toLViewRange(
+ this.tView, this._raw_lView, this.tView.expandoStartIndex, this._raw_lView.length);
+ }
+
/**
* Normalized view of child views (and containers) attached at this location.
*/
- get childViews(): Array {
- const childViews: Array = [];
+ get childViews(): Array {
+ const childViews: Array = [];
let child = this.childHead;
while (child) {
childViews.push(child);
@@ -470,11 +495,12 @@ export class LViewDebug {
}
}
-export interface DebugNode {
- html: string|null;
- native: Node;
- nodes: DebugNode[]|null;
- component: LViewDebug|null;
+function toLViewRange(tView: TView, lView: LView, start: number, end: number): LViewDebugRange {
+ let content: LViewDebugRangeContent[] = [];
+ for (let index = start; index < end; index++) {
+ content.push({index: index, t: tView.data[index], l: lView[index]});
+ }
+ return {start: start, end: end, length: end - start, content: content};
}
/**
@@ -483,7 +509,7 @@ export interface DebugNode {
* @param tNode
* @param lView
*/
-export function toDebugNodes(tNode: ITNode|null, lView: LView): DebugNode[]|null {
+export function toDebugNodes(tNode: ITNode|null, lView: LView): DebugNode[] {
if (tNode) {
const debugNodes: DebugNode[] = [];
let tNodeCursor: ITNode|null = tNode;
@@ -493,33 +519,32 @@ export function toDebugNodes(tNode: ITNode|null, lView: LView): DebugNode[]|null
}
return debugNodes;
} else {
- return null;
+ return [];
}
}
export function buildDebugNode(tNode: ITNode, lView: LView, nodeIndex: number): DebugNode {
const rawValue = lView[nodeIndex];
const native = unwrapRNode(rawValue);
- const componentLViewDebug = toDebug(readLViewValue(rawValue));
return {
html: toHtml(native),
+ type: TNodeTypeAsString[tNode.type],
native: native as any,
- nodes: toDebugNodes(tNode.child, lView),
- component: componentLViewDebug,
+ children: toDebugNodes(tNode.child, lView),
};
}
-export class LContainerDebug {
+export class LContainerDebug implements ILContainerDebug {
constructor(private readonly _raw_lContainer: LContainer) {}
get hasTransplantedViews(): boolean {
return this._raw_lContainer[HAS_TRANSPLANTED_VIEWS];
}
- get views(): LViewDebug[] {
+ get views(): ILViewDebug[] {
return this._raw_lContainer.slice(CONTAINER_HEADER_OFFSET)
- .map(toDebug as (l: LView) => LViewDebug);
+ .map(toDebug as (l: LView) => ILViewDebug);
}
- get parent(): LViewDebug|LContainerDebug|null {
+ get parent(): ILViewDebug|null {
return toDebug(this._raw_lContainer[PARENT]);
}
get movedViews(): LView[]|null {
@@ -550,206 +575,3 @@ export function readLViewValue(value: any): LView|null {
}
return null;
}
-
-export class I18NDebugItem {
- [key: string]: any;
-
- get tNode() {
- return getTNode(this._lView[TVIEW], this.nodeIndex);
- }
-
- constructor(
- public __raw_opCode: any, private _lView: LView, public nodeIndex: number,
- public type: string) {}
-}
-
-/**
- * Turns a list of "Create" & "Update" OpCodes into a human-readable list of operations for
- * debugging purposes.
- * @param mutateOpCodes mutation opCodes to read
- * @param updateOpCodes update opCodes to read
- * @param icus list of ICU expressions
- * @param lView The view the opCodes are acting on
- */
-export function attachI18nOpCodesDebug(
- mutateOpCodes: I18nMutateOpCodes, updateOpCodes: I18nUpdateOpCodes, icus: TIcu[]|null,
- lView: LView) {
- attachDebugObject(mutateOpCodes, new I18nMutateOpCodesDebug(mutateOpCodes, lView));
- attachDebugObject(updateOpCodes, new I18nUpdateOpCodesDebug(updateOpCodes, icus, lView));
-
- if (icus) {
- icus.forEach(icu => {
- icu.create.forEach(icuCase => {
- attachDebugObject(icuCase, new I18nMutateOpCodesDebug(icuCase, lView));
- });
- icu.update.forEach(icuCase => {
- attachDebugObject(icuCase, new I18nUpdateOpCodesDebug(icuCase, icus, lView));
- });
- });
- }
-}
-
-export class I18nMutateOpCodesDebug implements I18nOpCodesDebug {
- constructor(private readonly __raw_opCodes: I18nMutateOpCodes, private readonly __lView: LView) {}
-
- /**
- * A list of operation information about how the OpCodes will act on the view.
- */
- get operations() {
- const {__lView, __raw_opCodes} = this;
- const results: any[] = [];
-
- for (let i = 0; i < __raw_opCodes.length; i++) {
- const opCode = __raw_opCodes[i];
- let result: any;
- if (typeof opCode === 'string') {
- result = {
- __raw_opCode: opCode,
- type: 'Create Text Node',
- nodeIndex: __raw_opCodes[++i],
- text: opCode,
- };
- }
-
- if (typeof opCode === 'number') {
- switch (opCode & I18nMutateOpCode.MASK_OPCODE) {
- case I18nMutateOpCode.AppendChild:
- const destinationNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_PARENT;
- result = new I18NDebugItem(opCode, __lView, destinationNodeIndex, 'AppendChild');
- break;
- case I18nMutateOpCode.Select:
- const nodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
- result = new I18NDebugItem(opCode, __lView, nodeIndex, 'Select');
- break;
- case I18nMutateOpCode.ElementEnd:
- let elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
- result = new I18NDebugItem(opCode, __lView, elementIndex, 'ElementEnd');
- break;
- case I18nMutateOpCode.Attr:
- elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
- result = new I18NDebugItem(opCode, __lView, elementIndex, 'Attr');
- result['attrName'] = __raw_opCodes[++i];
- result['attrValue'] = __raw_opCodes[++i];
- break;
- }
- }
-
- if (!result) {
- switch (opCode) {
- case COMMENT_MARKER:
- result = {
- __raw_opCode: opCode,
- type: 'COMMENT_MARKER',
- commentValue: __raw_opCodes[++i],
- nodeIndex: __raw_opCodes[++i],
- };
- break;
- case ELEMENT_MARKER:
- result = {
- __raw_opCode: opCode,
- type: 'ELEMENT_MARKER',
- };
- break;
- }
- }
-
- if (!result) {
- result = {
- __raw_opCode: opCode,
- type: 'Unknown Op Code',
- code: opCode,
- };
- }
-
- results.push(result);
- }
-
- return results;
- }
-}
-
-export class I18nUpdateOpCodesDebug implements I18nOpCodesDebug {
- constructor(
- private readonly __raw_opCodes: I18nUpdateOpCodes, private readonly icus: TIcu[]|null,
- private readonly __lView: LView) {}
-
- /**
- * A list of operation information about how the OpCodes will act on the view.
- */
- get operations() {
- const {__lView, __raw_opCodes, icus} = this;
- const results: any[] = [];
-
- for (let i = 0; i < __raw_opCodes.length; i++) {
- // bit code to check if we should apply the next update
- const checkBit = __raw_opCodes[i] as number;
- // Number of opCodes to skip until next set of update codes
- const skipCodes = __raw_opCodes[++i] as number;
- let value = '';
- for (let j = i + 1; j <= (i + skipCodes); j++) {
- const opCode = __raw_opCodes[j];
- if (typeof opCode === 'string') {
- value += opCode;
- } else if (typeof opCode == 'number') {
- if (opCode < 0) {
- // It's a binding index whose value is negative
- // We cannot know the value of the binding so we only show the index
- value += `�${- opCode - 1}�`;
- } else {
- const nodeIndex = opCode >>> I18nUpdateOpCode.SHIFT_REF;
- let tIcuIndex: number;
- let tIcu: TIcu;
- switch (opCode & I18nUpdateOpCode.MASK_OPCODE) {
- case I18nUpdateOpCode.Attr:
- const attrName = __raw_opCodes[++j] as string;
- const sanitizeFn = __raw_opCodes[++j];
- results.push({
- __raw_opCode: opCode,
- checkBit,
- type: 'Attr',
- attrValue: value,
- attrName,
- sanitizeFn,
- });
- break;
- case I18nUpdateOpCode.Text:
- results.push({
- __raw_opCode: opCode,
- checkBit,
- type: 'Text',
- nodeIndex,
- text: value,
- });
- break;
- case I18nUpdateOpCode.IcuSwitch:
- tIcuIndex = __raw_opCodes[++j] as number;
- tIcu = icus![tIcuIndex];
- let result = new I18NDebugItem(opCode, __lView, nodeIndex, 'IcuSwitch');
- result['tIcuIndex'] = tIcuIndex;
- result['checkBit'] = checkBit;
- result['mainBinding'] = value;
- result['tIcu'] = tIcu;
- results.push(result);
- break;
- case I18nUpdateOpCode.IcuUpdate:
- tIcuIndex = __raw_opCodes[++j] as number;
- tIcu = icus![tIcuIndex];
- result = new I18NDebugItem(opCode, __lView, nodeIndex, 'IcuUpdate');
- result['tIcuIndex'] = tIcuIndex;
- result['checkBit'] = checkBit;
- result['tIcu'] = tIcu;
- results.push(result);
- break;
- }
- }
- }
- }
- i += skipCodes;
- }
- return results;
- }
-}
-
-export interface I18nOpCodesDebug {
- operations: any[];
-}
diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts
index ff8b4f0488..929a5fa78c 100644
--- a/packages/core/src/render3/instructions/shared.ts
+++ b/packages/core/src/render3/instructions/shared.ts
@@ -236,6 +236,13 @@ export function getOrCreateTNode(
const tNode = tView.data[adjustedIndex] as TNode ||
createTNodeAtIndex(tView, tHostNode, adjustedIndex, type, name, attrs);
setPreviousOrParentTNode(tNode, true);
+ if (ngDevMode) {
+ // For performance reasons it is important that the tNode retains the same shape during runtime.
+ // (To make sure that all of the code is monomorphic.) For this reason we seal the object to
+ // prevent class transitions.
+ // FIXME(misko): re-enable this once i18n code is compliant with this.
+ // Object.seal(tNode);
+ }
return tNode as TElementNode & TViewNode & TContainerNode & TElementContainerNode &
TProjectionNode & TIcuContainerNode;
}
@@ -692,7 +699,9 @@ export function createTView(
null, // firstChild: TNode|null,
schemas, // schemas: SchemaMetadata[]|null,
consts, // consts: TConstants|null
- false // incompleteFirstPass: boolean
+ false, // incompleteFirstPass: boolean
+ decls, // ngDevMode only: decls
+ vars, // ngDevMode only: vars
) :
{
type: type,
diff --git a/packages/core/src/render3/interfaces/i18n.ts b/packages/core/src/render3/interfaces/i18n.ts
index abb5327c86..f379ed9125 100644
--- a/packages/core/src/render3/interfaces/i18n.ts
+++ b/packages/core/src/render3/interfaces/i18n.ts
@@ -6,18 +6,31 @@
* found in the LICENSE file at https://angular.io/license
*/
+import {SanitizerFn} from './sanitization';
+
/**
* `I18nMutateOpCode` defines OpCodes for `I18nMutateOpCodes` array.
*
+ * OpCodes are efficient operations which can be applied to the DOM to update it. (For example to
+ * update to a new ICU case requires that we clean up previous elements and create new ones.)
+ *
* OpCodes contain three parts:
- * 1) Parent node index offset.
- * 2) Reference node index offset.
- * 3) The OpCode to execute.
+ * 1) Parent node index offset. (p)
+ * 2) Reference node index offset. (r)
+ * 3) The instruction to execute. (i)
+ *
+ * pppp pppp pppp pppp rrrr rrrr rrrr riii
+ * 3322 2222 2222 1111 1111 1110 0000 0000
+ * 1098 7654 3210 9876 5432 1098 7654 3210
+ *
+ * ```
+ * var parent = lView[opCode >>> SHIFT_PARENT];
+ * var refNode = lView[((opCode & MASK_REF) >>> SHIFT_REF)];
+ * var instruction = opCode & MASK_OPCODE;
+ * ```
*
* See: `I18nCreateOpCodes` for example of usage.
*/
-import {SanitizerFn} from './sanitization';
-
export const enum I18nMutateOpCode {
/**
* Stores shift amount for bits 17-3 that contain reference index.
@@ -30,36 +43,61 @@ export const enum I18nMutateOpCode {
/**
* Mask for OpCode
*/
- MASK_OPCODE = 0b111,
+ MASK_INSTRUCTION = 0b111,
/**
- * OpCode to select a node. (next OpCode will contain the operation.)
+ * Mask for the Reference node (bits 16-3)
+ */
+ // FIXME(misko): Why is this not used?
+ MASK_REF = 0b11111111111111000,
+ // 11111110000000000
+ // 65432109876543210
+
+ /**
+ * Instruction to select a node. (next OpCode will contain the operation.)
*/
Select = 0b000,
+
/**
- * OpCode to append the current node to `PARENT`.
+ * Instruction to append the current node to `PARENT`.
*/
AppendChild = 0b001,
+
/**
- * OpCode to remove the `REF` node from `PARENT`.
+ * Instruction to remove the `REF` node from `PARENT`.
*/
Remove = 0b011,
+
/**
- * OpCode to set the attribute of a node.
+ * Instruction to set the attribute of a node.
*/
Attr = 0b100,
+
/**
- * OpCode to simulate elementEnd()
+ * Instruction to simulate elementEnd()
*/
ElementEnd = 0b101,
+
/**
- * OpCode to read the remove OpCodes for the nested ICU
+ * Instruction to removed the nested ICU.
*/
RemoveNestedIcu = 0b110,
}
+export function getParentFromI18nMutateOpCode(mergedCode: number): number {
+ return mergedCode >>> I18nMutateOpCode.SHIFT_PARENT;
+}
+
+export function getRefFromI18nMutateOpCode(mergedCode: number): number {
+ return (mergedCode & I18nMutateOpCode.MASK_REF) >>> I18nMutateOpCode.SHIFT_REF;
+}
+
+export function getInstructionFromI18nMutateOpCode(mergedCode: number): number {
+ return mergedCode & I18nMutateOpCode.MASK_INSTRUCTION;
+}
+
/**
- * Marks that the next string is for element.
+ * Marks that the next string is an element name.
*
* See `I18nMutateOpCodes` documentation.
*/
@@ -71,7 +109,7 @@ export interface ELEMENT_MARKER {
}
/**
- * Marks that the next string is for comment.
+ * Marks that the next string is comment text.
*
* See `I18nMutateOpCodes` documentation.
*/
@@ -83,6 +121,18 @@ export interface COMMENT_MARKER {
marker: 'comment';
}
+export interface I18nDebug {
+ /**
+ * Human readable representation of the OpCode arrays.
+ *
+ * NOTE: This property only exists if `ngDevMode` is set to `true` and it is not present in
+ * production. Its presence is purely to help debug issue in development, and should not be relied
+ * on in production application.
+ */
+ debug?: string[];
+}
+
+
/**
* Array storing OpCode for dynamically creating `i18n` blocks.
*
@@ -92,50 +142,27 @@ export interface COMMENT_MARKER {
* // For adding text nodes
* // ---------------------
* // Equivalent to:
- * // const node = lView[index++] = document.createTextNode('abc');
- * // lView[1].insertBefore(node, lView[2]);
- * 'abc', 1 << SHIFT_PARENT | 2 << SHIFT_REF | InsertBefore,
- *
- * // Equivalent to:
- * // const node = lView[index++] = document.createTextNode('xyz');
- * // lView[1].appendChild(node);
- * 'xyz', 1 << SHIFT_PARENT | AppendChild,
+ * // lView[1].appendChild(lView[0] = document.createTextNode('xyz'));
+ * 'xyz', 0, 1 << SHIFT_PARENT | 0 << SHIFT_REF | AppendChild,
*
* // For adding element nodes
* // ---------------------
* // Equivalent to:
- * // const node = lView[index++] = document.createElement('div');
- * // lView[1].insertBefore(node, lView[2]);
- * ELEMENT_MARKER, 'div', 1 << SHIFT_PARENT | 2 << SHIFT_REF | InsertBefore,
- *
- * // Equivalent to:
- * // const node = lView[index++] = document.createElement('div');
- * // lView[1].appendChild(node);
- * ELEMENT_MARKER, 'div', 1 << SHIFT_PARENT | AppendChild,
+ * // lView[1].appendChild(lView[0] = document.createElement('div'));
+ * ELEMENT_MARKER, 'div', 0, 1 << SHIFT_PARENT | 0 << SHIFT_REF | AppendChild,
*
* // For adding comment nodes
* // ---------------------
* // Equivalent to:
- * // const node = lView[index++] = document.createComment('');
- * // lView[1].insertBefore(node, lView[2]);
- * COMMENT_MARKER, '', 1 << SHIFT_PARENT | 2 << SHIFT_REF | InsertBefore,
- *
- * // Equivalent to:
- * // const node = lView[index++] = document.createComment('');
- * // lView[1].appendChild(node);
- * COMMENT_MARKER, '', 1 << SHIFT_PARENT | AppendChild,
+ * // lView[1].appendChild(lView[0] = document.createComment(''));
+ * COMMENT_MARKER, '', 0, 1 << SHIFT_PARENT | 0 << SHIFT_REF | AppendChild,
*
* // For moving existing nodes to a different location
* // --------------------------------------------------
* // Equivalent to:
* // const node = lView[1];
- * // lView[2].insertBefore(node, lView[3]);
- * 1 << SHIFT_REF | Select, 2 << SHIFT_PARENT | 3 << SHIFT_REF | InsertBefore,
- *
- * // Equivalent to:
- * // const node = lView[1];
* // lView[2].appendChild(node);
- * 1 << SHIFT_REF | Select, 2 << SHIFT_PARENT | AppendChild,
+ * 1 << SHIFT_REF | Select, 2 << SHIFT_PARENT | 0 << SHIFT_REF | AppendChild,
*
* // For removing existing nodes
* // --------------------------------------------------
@@ -147,18 +174,14 @@ export interface COMMENT_MARKER {
* // --------------------------------------------------
* // const node = lView[1];
* // node.setAttribute('attr', 'value');
- * 1 << SHIFT_REF | Select, 'attr', 'value'
- * // NOTE: Select followed by two string (vs select followed by OpCode)
+ * 1 << SHIFT_REF | Attr, 'attr', 'value'
* ];
* ```
- * NOTE:
- * - `index` is initial location where the extra nodes should be stored in the EXPANDO section of
- * `LVIewData`.
*
* See: `applyI18nCreateOpCodes`;
*/
-export interface I18nMutateOpCodes extends Array {
-}
+export interface I18nMutateOpCodes extends Array,
+ I18nDebug {}
export const enum I18nUpdateOpCode {
/**
@@ -171,19 +194,19 @@ export const enum I18nUpdateOpCode {
MASK_OPCODE = 0b11,
/**
- * OpCode to update a text node.
+ * Instruction to update a text node.
*/
Text = 0b00,
/**
- * OpCode to update a attribute of a node.
+ * Instruction to update a attribute of a node.
*/
Attr = 0b01,
/**
- * OpCode to switch the current ICU case.
+ * Instruction to switch the current ICU case.
*/
IcuSwitch = 0b10,
/**
- * OpCode to update the current ICU case.
+ * Instruction to update the current ICU case.
*/
IcuUpdate = 0b11,
}
@@ -197,6 +220,10 @@ export const enum I18nUpdateOpCode {
* higher.) The OpCodes then compare its own change mask against the expression change mask to
* determine if the OpCodes should execute.
*
+ * NOTE: 32nd bit is special as it says 32nd or higher. This way if we have more than 32 bindings
+ * the code still works, but with lower efficiency. (it is unlikely that a translation would have
+ * more than 32 bindings.)
+ *
* These OpCodes can be used by both the i18n block as well as ICU sub-block.
*
* ## Example
@@ -220,8 +247,8 @@ export const enum I18nUpdateOpCode {
* // The following OpCodes represent: `
`
* // If `changeMask & 0b11`
* // has changed then execute update OpCodes.
- * // has NOT changed then skip `7` values and start processing next OpCodes.
- * 0b11, 7,
+ * // has NOT changed then skip `8` values and start processing next OpCodes.
+ * 0b11, 8,
* // Concatenate `newValue = 'pre'+lView[bindIndex-4]+'in'+lView[bindIndex-3]+'post';`.
* 'pre', -4, 'in', -3, 'post',
* // Update attribute: `elementAttribute(1, 'title', sanitizerFn(newValue));`
@@ -240,8 +267,8 @@ export const enum I18nUpdateOpCode {
* // The following OpCodes represent: `
{exp4, plural, ... }">`
* // If `changeMask & 0b1000`
* // has changed then execute update OpCodes.
- * // has NOT changed then skip `4` values and start processing next OpCodes.
- * 0b1000, 4,
+ * // has NOT changed then skip `2` values and start processing next OpCodes.
+ * 0b1000, 2,
* // Concatenate `newValue = lView[bindIndex -1];`.
* -1,
* // Switch ICU: `icuSwitchCase(lView[1], 0, newValue);`
@@ -256,7 +283,7 @@ export const enum I18nUpdateOpCode {
* ```
*
*/
-export interface I18nUpdateOpCodes extends Array {}
+export interface I18nUpdateOpCodes extends Array, I18nDebug {}
/**
* Store information for the i18n translation block.
diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts
index 58284f4c11..4684232065 100644
--- a/packages/core/src/render3/interfaces/node.ts
+++ b/packages/core/src/render3/interfaces/node.ts
@@ -7,14 +7,11 @@
*/
import {KeyValueArray} from '../../util/array_utils';
import {TStylingRange} from '../interfaces/styling';
-
-import {DirectiveDef} from './definition';
import {CssSelector} from './projection';
import {RNode} from './renderer';
import {LView, TView} from './view';
-
/**
* TNodeType corresponds to the {@link TNode} `type` property.
*/
@@ -45,6 +42,20 @@ export const enum TNodeType {
IcuContainer = 5,
}
+/**
+ * Converts `TNodeType` into human readable text.
+ * Make sure this matches with `TNodeType`
+ */
+export const TNodeTypeAsString = [
+ 'Container', // 0
+ 'Projection', // 1
+ 'View', // 2
+ 'Element', // 3
+ 'ElementContainer', // 4
+ 'IcuContainer' // 5
+] as const;
+
+
/**
* Corresponds to the TNode.flags property.
*/
@@ -701,7 +712,9 @@ export interface TIcuContainerNode extends TNode {
/**
* Indicates the current active case for an ICU expression.
* It is null when there is no active case.
+ *
*/
+ // FIXME(misko): This is at a wrong location as activeCase is `LView` (not `TView`) concern
activeCaseIndex: number|null;
}
diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts
index b51d1976f9..0aec9344b3 100644
--- a/packages/core/src/render3/interfaces/view.ts
+++ b/packages/core/src/render3/interfaces/view.ts
@@ -15,10 +15,10 @@ import {Sanitizer} from '../../sanitization/sanitizer';
import {LContainer} from './container';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefList, HostBindingsFunction, PipeDef, PipeDefList, ViewQueriesFunction} from './definition';
import {I18nUpdateOpCodes, TI18n} from './i18n';
-import {TConstants, TElementNode, TNode, TViewNode} from './node';
+import {TConstants, TElementNode, TNode, TNodeTypeAsString, TViewNode} from './node';
import {PlayerHandler} from './player';
import {LQueries, TQueries} from './query';
-import {RElement, Renderer3, RendererFactory3} from './renderer';
+import {RComment, RElement, Renderer3, RendererFactory3} from './renderer';
import {TStylingKey, TStylingRange} from './styling';
@@ -69,6 +69,15 @@ export interface OpaqueViewState {
* don't have to edit the data array based on which views are present.
*/
export interface LView extends Array {
+ /**
+ * Human readable representation of the `LView`.
+ *
+ * NOTE: This property only exists if `ngDevMode` is set to `true` and it is not present in
+ * production. Its presence is purely to help debug issue in development, and should not be relied
+ * on in production application.
+ */
+ debug?: LViewDebug;
+
/**
* The host node for this LView instance, if this is a component view.
* If this is an embedded view, HOST will be null.
@@ -826,3 +835,190 @@ export type TData =
// Note: This hack is necessary so we don't erroneously get a circular dependency
// failure based on types.
export const unusedValueExportToPlacateAjd = 1;
+
+/**
+ * Human readable version of the `LView`.
+ *
+ * `LView` is a data structure used internally to keep track of views. The `LView` is designed for
+ * efficiency and so at times it is difficult to read or write tests which assert on its values. For
+ * this reason when `ngDevMode` is true we patch a `LView.debug` property which points to
+ * `LViewDebug` for easier debugging and test writing. It is the intent of `LViewDebug` to be used
+ * in tests.
+ */
+export interface LViewDebug {
+ /**
+ * Flags associated with the `LView` unpacked into a more readable state.
+ *
+ * See `LViewFlags` for the flag meanings.
+ */
+ readonly flags: {
+ initPhaseState: number,
+ creationMode: boolean,
+ firstViewPass: boolean,
+ checkAlways: boolean,
+ dirty: boolean,
+ attached: boolean,
+ destroyed: boolean,
+ isRoot: boolean,
+ indexWithinInitPhase: number,
+ };
+
+ /**
+ * Parent view (or container)
+ */
+ readonly parent: LViewDebug|LContainerDebug|null;
+
+ /**
+ * Next sibling to the `LView`.
+ */
+ readonly next: LViewDebug|LContainerDebug|null;
+
+ /**
+ * The context used for evaluation of the `LView`
+ *
+ * (Usually the component)
+ */
+ readonly context: {}|null;
+
+ /**
+ * Hierarchical tree of nodes.
+ */
+ readonly nodes: DebugNode[];
+
+ /**
+ * HTML representation of the `LView`.
+ *
+ * This is only approximate to actual HTML as child `LView`s are removed.
+ */
+ readonly html: string;
+
+ /**
+ * The host element to which this `LView` is attached.
+ */
+ readonly hostHTML: string|null;
+
+ /**
+ * Child `LView`s
+ */
+ readonly childViews: Array;
+
+ /**
+ * Sub range of `LView` containing decls (DOM elements).
+ */
+ readonly decls: LViewDebugRange;
+
+ /**
+ * Sub range of `LView` containing vars (bindings).
+ */
+ readonly vars: LViewDebugRange;
+
+ /**
+ * Sub range of `LView` containing i18n (translated DOM elements).
+ */
+ readonly i18n: LViewDebugRange;
+
+ /**
+ * Sub range of `LView` containing expando (used by DI).
+ */
+ readonly expando: LViewDebugRange;
+}
+
+/**
+ * Human readable version of the `LContainer`
+ *
+ * `LContainer` is a data structure used internally to keep track of child views. The `LContainer`
+ * is designed for efficiency and so at times it is difficult to read or write tests which assert on
+ * its values. For this reason when `ngDevMode` is true we patch a `LContainer.debug` property which
+ * points to `LContainerDebug` for easier debugging and test writing. It is the intent of
+ * `LContainerDebug` to be used in tests.
+ */
+export interface LContainerDebug {
+ readonly native: RComment;
+ /**
+ * Child `LView`s.
+ */
+ readonly views: LViewDebug[];
+ readonly parent: LViewDebug|null;
+ readonly movedViews: LView[]|null;
+ readonly host: RElement|RComment|LView;
+ readonly next: LViewDebug|LContainerDebug|null;
+ readonly hasTransplantedViews: boolean;
+}
+
+
+
+/**
+ * `LView` is subdivided to ranges where the actual data is stored. Some of these ranges such as
+ * `decls` and `vars` are known at compile time. Other such as `i18n` and `expando` are runtime only
+ * concepts.
+ */
+export interface LViewDebugRange {
+ /**
+ * The starting index in `LView` where the range begins. (Inclusive)
+ */
+ start: number;
+
+ /**
+ * The ending index in `LView` where the range ends. (Exclusive)
+ */
+ end: number;
+
+ /**
+ * The length of the range
+ */
+ length: number;
+
+ /**
+ * The merged content of the range. `t` contains data from `TView.data` and `l` contains `LView`
+ * data at an index.
+ */
+ content: LViewDebugRangeContent[];
+}
+
+/**
+ * For convenience the static and instance portions of `TView` and `LView` are merged into a single
+ * object in `LViewRange`.
+ */
+export interface LViewDebugRangeContent {
+ /**
+ * Index into original `LView` or `TView.data`.
+ */
+ index: number;
+
+ /**
+ * Value from the `TView.data[index]` location.
+ */
+ t: any;
+
+ /**
+ * Value from the `LView[index]` location.
+ */
+ l: any;
+}
+
+
+/**
+ * A logical node which comprise into `LView`s.
+ *
+ */
+export interface DebugNode {
+ /**
+ * HTML representation of the node.
+ */
+ html: string|null;
+
+ /**
+ * Human readable node type.
+ */
+ type: typeof TNodeTypeAsString[number];
+
+ /**
+ * DOM native node.
+ */
+ native: Node;
+
+ /**
+ * Child nodes
+ */
+ children: DebugNode[];
+}
\ No newline at end of file
diff --git a/packages/core/src/render3/util/debug_utils.ts b/packages/core/src/render3/util/debug_utils.ts
index eda7e99db0..518b0738f6 100644
--- a/packages/core/src/render3/util/debug_utils.ts
+++ b/packages/core/src/render3/util/debug_utils.ts
@@ -5,6 +5,37 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
-export function attachDebugObject(obj: any, debug: any) {
- Object.defineProperty(obj, 'debug', {value: debug, enumerable: false});
+
+/**
+ * Patch a `debug` property on top of the existing object.
+ *
+ * NOTE: always call this method with `ngDevMode && attachDebugObject(...)`
+ *
+ * @param obj Object to patch
+ * @param debug Value to patch
+ */
+export function attachDebugObject(obj: any, debug: any): void {
+ if (ngDevMode) {
+ Object.defineProperty(obj, 'debug', {value: debug, enumerable: false});
+ } else {
+ throw new Error(
+ 'This method should be guarded with `ngDevMode` so that it can be tree shaken in production!');
+ }
+}
+
+/**
+ * Patch a `debug` property getter on top of the existing object.
+ *
+ * NOTE: always call this method with `ngDevMode && attachDebugObject(...)`
+ *
+ * @param obj Object to patch
+ * @param debugGetter Getter returning a value to patch
+ */
+export function attachDebugGetter(obj: any, debugGetter: () => any): void {
+ if (ngDevMode) {
+ Object.defineProperty(obj, 'debug', {get: debugGetter, enumerable: false});
+ } else {
+ throw new Error(
+ 'This method should be guarded with `ngDevMode` so that it can be tree shaken in production!');
+ }
}
diff --git a/packages/core/src/render3/util/discovery_utils.ts b/packages/core/src/render3/util/discovery_utils.ts
index 983bcdf0c9..94de7d03d5 100644
--- a/packages/core/src/render3/util/discovery_utils.ts
+++ b/packages/core/src/render3/util/discovery_utils.ts
@@ -10,12 +10,12 @@ import {Injector} from '../../di/injector';
import {assertLView} from '../assert';
import {discoverLocalRefs, getComponentAtNodeIndex, getDirectivesAtNodeIndex, getLContext} from '../context_discovery';
import {NodeInjector} from '../di';
-import {buildDebugNode, DebugNode} from '../instructions/lview_debug';
+import {buildDebugNode} from '../instructions/lview_debug';
import {LContext} from '../interfaces/context';
import {DirectiveDef} from '../interfaces/definition';
import {TElementNode, TNode, TNodeProviderIndexes} from '../interfaces/node';
import {isLView} from '../interfaces/type_checks';
-import {CLEANUP, CONTEXT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, T_HOST, TVIEW} from '../interfaces/view';
+import {CLEANUP, CONTEXT, DebugNode, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, T_HOST, TVIEW} from '../interfaces/view';
import {stringifyForError} from './misc_utils';
import {getLViewParent, getRootContext} from './view_traversal_utils';
diff --git a/packages/core/test/acceptance/BUILD.bazel b/packages/core/test/acceptance/BUILD.bazel
index b77a9a0169..a32706052a 100644
--- a/packages/core/test/acceptance/BUILD.bazel
+++ b/packages/core/test/acceptance/BUILD.bazel
@@ -20,6 +20,7 @@ ts_library(
"//packages/compiler/testing",
"//packages/core",
"//packages/core/src/util",
+ "//packages/core/test/render3:matchers",
"//packages/core/testing",
"//packages/localize",
"//packages/localize/init",
diff --git a/packages/core/test/acceptance/debug_spec.ts b/packages/core/test/acceptance/debug_spec.ts
index a64d6675e8..ef1e3d3ea3 100644
--- a/packages/core/test/acceptance/debug_spec.ts
+++ b/packages/core/test/acceptance/debug_spec.ts
@@ -8,13 +8,17 @@
import {Component} from '@angular/core';
import {getLContext} from '@angular/core/src/render3/context_discovery';
-import {LViewDebug, toDebug} from '@angular/core/src/render3/instructions/lview_debug';
+import {LViewDebug} from '@angular/core/src/render3/instructions/lview_debug';
+import {TNodeType} from '@angular/core/src/render3/interfaces/node';
+import {HEADER_OFFSET} from '@angular/core/src/render3/interfaces/view';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
-describe('Debug Representation', () => {
- onlyInIvy('Ivy specific').it('should generate a human readable version', () => {
+import {matchDomElement, matchDomText, matchTI18n, matchTNode} from '../render3/matchers';
+
+onlyInIvy('Ivy specific').describe('Debug Representation', () => {
+ it('should generate a human readable version', () => {
@Component({selector: 'my-comp', template: '