upstream: Merge remote-tracking branch 'upstream/master' into merge-upstream

# Conflicts:
#	CHANGELOG.md
#	aio/content/examples/testing/src/app/app.component.router.spec.ts
#	aio/content/examples/testing/src/app/dashboard/dashboard-hero.component.spec.ts
#	aio/content/examples/testing/src/app/dashboard/dashboard.component.spec.ts
#	aio/content/examples/testing/src/app/hero/hero-detail.component.spec.ts
#	aio/content/examples/testing/src/app/hero/hero-list.component.spec.ts
#	aio/content/examples/testing/src/app/twain/twain.component.spec.ts
#	goldens/public-api/core/testing/testing.d.ts
#	goldens/size-tracking/aio-payloads.json
#	package.json
#	packages/core/test/bundling/forms/bundle.golden_symbols.json
#	packages/forms/test/form_group_spec.ts
This commit is contained in:
Michael Prentice 2020-08-15 20:48:38 -04:00
commit 575479a6a1
No known key found for this signature in database
GPG Key ID: A21110C72E90BFB7
159 changed files with 4962 additions and 2601 deletions

View File

@ -136,15 +136,6 @@ build:remote --remote_executor=remotebuildexecution.googleapis.com
# retry mechanism and we do not want to retry unnecessarily if Karma already tried multiple times.
test:saucelabs --flaky_test_attempts=1
###############################
# NodeJS rules settings
# These settings are required for rules_nodejs
###############################
# Turn on managed directories feature in Bazel
# This allows us to avoid installing a second copy of node_modules
common --experimental_allow_incremental_repository_updates
####################################################
# User bazel configuration
# NOTE: This needs to be the *last* entry in the config.

View File

@ -32,8 +32,8 @@ var_4_win: &cache_key_win_fallback v7-angular-win-node-12-{{ checksum ".bazelver
# Cache key for the `components-repo-unit-tests` job. **Note** when updating the SHA in the
# cache keys also update the SHA for the "COMPONENTS_REPO_COMMIT" environment variable.
var_5: &components_repo_unit_tests_cache_key v7-angular-components-f428c00465dfcf8a020237f22532480eedbd2cb6
var_6: &components_repo_unit_tests_cache_key_fallback v7-angular-components-
var_5: &components_repo_unit_tests_cache_key v9-angular-components-09e68db8ed5b1253f2fe38ff954ef0df019fc25a
var_6: &components_repo_unit_tests_cache_key_fallback v9-angular-components-
# Workspace initially persisted by the `setup` job, and then enhanced by `build-npm-packages` and
# `build-ivy-npm-packages`.

View File

@ -74,7 +74,7 @@ setPublicVar COMPONENTS_REPO_TMP_DIR "/tmp/angular-components-repo"
setPublicVar COMPONENTS_REPO_URL "https://github.com/angular/components.git"
setPublicVar COMPONENTS_REPO_BRANCH "master"
# **NOTE**: When updating the commit SHA, also update the cache key in the CircleCI `config.yml`.
setPublicVar COMPONENTS_REPO_COMMIT "f428c00465dfcf8a020237f22532480eedbd2cb6"
setPublicVar COMPONENTS_REPO_COMMIT "09e68db8ed5b1253f2fe38ff954ef0df019fc25a"
####################################################################################################

View File

@ -882,6 +882,7 @@ groups:
- *can-be-global-docs-approved
- >
contains_any_globs(files, [
'aio/content/guide/roadmap.md',
'aio/content/marketing/**',
'aio/content/images/bios/**',
'aio/content/images/marketing/**',

View File

@ -1,3 +1,31 @@
<a name="10.1.0-next.4"></a>
# 10.1.0-next.4 (2020-08-04)
### Bug Fixes
* **common:** narrow `NgIf` context variables in template type checker ([#36627](https://github.com/angular/angular/issues/36627)) ([9c8bc4a](https://github.com/angular/angular/commit/9c8bc4a))
* **compiler:** mark `NgModuleFactory` construction as not side effectful ([#38147](https://github.com/angular/angular/issues/38147)) ([7f8c222](https://github.com/angular/angular/commit/7f8c222))
### Features
* **core:** rename async to waitForAsync to avoid confusing ([#37583](https://github.com/angular/angular/issues/37583)) ([8f07429](https://github.com/angular/angular/commit/8f07429))
* **core:** update reference and doc to change `async` to `waitAsync`. ([#37583](https://github.com/angular/angular/issues/37583)) ([8fbf40b](https://github.com/angular/angular/commit/8fbf40b))
<a name="10.0.8"></a>
## 10.0.8 (2020-08-04)
### Bug Fixes
* **compiler:** add PURE annotation to getInheritedFactory calls ([#38291](https://github.com/angular/angular/issues/38291)) ([03d8e31](https://github.com/angular/angular/commit/03d8e31))
* **compiler:** update unparsable character reference entity error messages ([#38319](https://github.com/angular/angular/issues/38319)) ([cea4678](https://github.com/angular/angular/commit/cea4678)), closes [#26067](https://github.com/angular/angular/issues/26067)
<a name="10.0.7"></a>
## 10.0.7 (2020-07-30)

View File

@ -1,31 +1,31 @@
import { TestBed, async } from '@angular/core/testing';
import { TestBed, waitForAsync } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { TemplateModule } from './template/template.module';
import { ReactiveModule } from './reactive/reactive.module';
import { TemplateModule } from './template/template.module';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ReactiveModule, TemplateModule],
declarations: [
AppComponent
],
}).compileComponents();
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
imports: [ReactiveModule, TemplateModule],
declarations: [AppComponent],
})
.compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
it('should create the app', waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
}));
expect(app).toBeTruthy();
}));
it('should render title', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
it('should render title', waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Forms Overview');
}));
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Forms Overview');
}));
});

View File

@ -1,19 +1,18 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { FavoriteColorComponent } from './favorite-color.component';
import { createNewEvent } from '../../shared/utils';
import { FavoriteColorComponent } from './favorite-color.component';
describe('Favorite Color Component', () => {
let component: FavoriteColorComponent;
let fixture: ComponentFixture<FavoriteColorComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ ReactiveFormsModule ],
declarations: [ FavoriteColorComponent ]
})
.compileComponents();
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule(
{imports: [ReactiveFormsModule], declarations: [FavoriteColorComponent]})
.compileComponents();
}));
beforeEach(() => {

View File

@ -1,19 +1,16 @@
import { async, ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { FavoriteColorComponent } from './favorite-color.component';
import { createNewEvent } from '../../shared/utils';
import { FavoriteColorComponent } from './favorite-color.component';
describe('FavoriteColorComponent', () => {
let component: FavoriteColorComponent;
let fixture: ComponentFixture<FavoriteColorComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ FavoriteColorComponent ]
})
.compileComponents();
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({imports: [FormsModule], declarations: [FavoriteColorComponent]})
.compileComponents();
}));
beforeEach(() => {
@ -28,29 +25,29 @@ describe('FavoriteColorComponent', () => {
// #docregion model-to-view
it('should update the favorite color on the input field', fakeAsync(() => {
component.favoriteColor = 'Blue';
component.favoriteColor = 'Blue';
fixture.detectChanges();
fixture.detectChanges();
tick();
tick();
const input = fixture.nativeElement.querySelector('input');
const input = fixture.nativeElement.querySelector('input');
expect(input.value).toBe('Blue');
}));
expect(input.value).toBe('Blue');
}));
// #enddocregion model-to-view
// #docregion view-to-model
it('should update the favorite color in the component', fakeAsync(() => {
const input = fixture.nativeElement.querySelector('input');
const event = createNewEvent('input');
const input = fixture.nativeElement.querySelector('input');
const event = createNewEvent('input');
input.value = 'Red';
input.dispatchEvent(event);
input.value = 'Red';
input.dispatchEvent(event);
fixture.detectChanges();
fixture.detectChanges();
expect(component.favoriteColor).toEqual('Red');
}));
expect(component.favoriteColor).toEqual('Red');
}));
// #enddocregion view-to-model
});

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MyLibComponent } from './my-lib.component';
@ -6,11 +6,8 @@ describe('MyLibComponent', () => {
let component: MyLibComponent;
let fixture: ComponentFixture<MyLibComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MyLibComponent ]
})
.compileComponents();
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({declarations: [MyLibComponent]}).compileComponents();
}));
beforeEach(() => {

View File

@ -1,19 +1,16 @@
import { AppComponent } from './app.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
let de: DebugElement;
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AppComponent ]
})
.compileComponents();
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({declarations: [AppComponent]}).compileComponents();
}));
beforeEach(() => {
@ -22,12 +19,11 @@ describe('AppComponent', () => {
de = fixture.debugElement.query(By.css('h1'));
});
it('should create component', () => expect(comp).toBeDefined() );
it('should create component', () => expect(comp).toBeDefined());
it('should have expected <h1> text', () => {
fixture.detectChanges();
const h1 = de.nativeElement;
expect(h1.textContent).toMatch(/angular/i,
'<h1> should say something about "Angular"');
expect(h1.textContent).toMatch(/angular/i, '<h1> should say something about "Angular"');
});
});

View File

@ -1,6 +1,6 @@
// #docplaster
// #docregion
import { TestBed, async } from '@angular/core/testing';
import { TestBed, waitForAsync } from '@angular/core/testing';
// #enddocregion
import { AppComponent } from './app-initial.component';
/*
@ -12,29 +12,29 @@ describe('AppComponent', () => {
*/
describe('AppComponent (initial CLI version)', () => {
// #docregion
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
declarations: [AppComponent],
})
.compileComponents();
}));
it('should create the app', waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title', waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
}));
});
// #enddocregion
@ -43,16 +43,13 @@ import { DebugElement } from '@angular/core';
import { ComponentFixture } from '@angular/core/testing';
describe('AppComponent (initial CLI version - as it should be)', () => {
let app: AppComponent;
let de: DebugElement;
let fixture: ComponentFixture<AppComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
declarations: [AppComponent],
});
fixture = TestBed.createComponent(AppComponent);
@ -70,7 +67,6 @@ describe('AppComponent (initial CLI version - as it should be)', () => {
it('should render title in an h1 tag', () => {
fixture.detectChanges();
expect(de.nativeElement.querySelector('h1').textContent)
.toContain('Welcome to app!');
expect(de.nativeElement.querySelector('h1').textContent).toContain('Welcome to app!');
});
});

View File

@ -1,7 +1,7 @@
// For more examples:
// https://github.com/angular/angular/blob/master/modules/@angular/router/test/integration.spec.ts
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { waitForAsync, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { asyncData } from '../testing';
@ -21,9 +21,9 @@ import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { AboutComponent } from './about/about.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { TwainService } from './twain/twain.service';
import { HeroService, TestHeroService } from './model/testing/test-hero.service';
import { TwainService } from './twain/twain.service';
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
@ -32,54 +32,51 @@ let router: Router;
let location: SpyLocation;
describe('AppComponent & RouterTestingModule', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
AppModule,
RouterTestingModule.withRoutes(routes),
],
providers: [
{ provide: HeroService, useClass: TestHeroService }
]
})
.compileComponents();
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
imports: [
AppModule,
RouterTestingModule.withRoutes(routes),
],
providers: [{provide: HeroService, useClass: TestHeroService}]
})
.compileComponents();
}));
it('should navigate to "Dashboard" immediately', fakeAsync(() => {
createComponent();
tick(); // wait for async data to arrive
expectPathToBe('/dashboard', 'after initialNavigation()');
expectElementOf(DashboardComponent);
}));
createComponent();
tick(); // wait for async data to arrive
expectPathToBe('/dashboard', 'after initialNavigation()');
expectElementOf(DashboardComponent);
}));
it('should navigate to "About" on click', fakeAsync(() => {
createComponent();
click(page.aboutLinkDe);
// page.aboutLinkDe.nativeElement.click(); // ok but fails in phantom
createComponent();
click(page.aboutLinkDe);
// page.aboutLinkDe.nativeElement.click(); // ok but fails in phantom
advance();
expectPathToBe('/about');
expectElementOf(AboutComponent);
}));
advance();
expectPathToBe('/about');
expectElementOf(AboutComponent);
}));
it('should navigate to "About" w/ browser location URL change', fakeAsync(() => {
createComponent();
location.simulateHashChange('/about');
// location.go('/about'); // also works ... except, perhaps, in Stackblitz
advance();
expectPathToBe('/about');
expectElementOf(AboutComponent);
}));
createComponent();
location.simulateHashChange('/about');
// location.go('/about'); // also works ... except, perhaps, in Stackblitz
advance();
expectPathToBe('/about');
expectElementOf(AboutComponent);
}));
// Can't navigate to lazy loaded modules with this technique
xit('should navigate to "Heroes" on click (not working yet)', fakeAsync(() => {
createComponent();
page.heroesLinkDe.nativeElement.click();
advance();
expectPathToBe('/heroes');
}));
createComponent();
page.heroesLinkDe.nativeElement.click();
advance();
expectPathToBe('/heroes');
}));
});
@ -94,37 +91,37 @@ let loader: SpyNgModuleFactoryLoader;
///////// Can't get lazy loaded Heroes to work yet
xdescribe('AppComponent & Lazy Loading (not working yet)', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
AppModule,
RouterTestingModule.withRoutes(routes),
],
})
.compileComponents();
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
imports: [
AppModule,
RouterTestingModule.withRoutes(routes),
],
})
.compileComponents();
}));
beforeEach(fakeAsync(() => {
createComponent();
loader = TestBed.inject(NgModuleFactoryLoader) as SpyNgModuleFactoryLoader;
loader.stubbedModules = { expected: HeroModule };
loader.stubbedModules = {expected: HeroModule};
router.resetConfig([{path: 'heroes', loadChildren: 'expected'}]);
}));
it('should navigate to "Heroes" on click', async(() => {
page.heroesLinkDe.nativeElement.click();
advance();
expectPathToBe('/heroes');
expectElementOf(HeroListComponent);
}));
it('should navigate to "Heroes" on click', waitForAsync(() => {
page.heroesLinkDe.nativeElement.click();
advance();
expectPathToBe('/heroes');
expectElementOf(HeroListComponent);
}));
it('can navigate to "Heroes" w/ browser location URL change', fakeAsync(() => {
location.go('/heroes');
advance();
expectPathToBe('/heroes');
expectElementOf(HeroListComponent);
}));
location.go('/heroes');
advance();
expectPathToBe('/heroes');
expectElementOf(HeroListComponent);
}));
});
////// Helpers /////////
@ -134,9 +131,9 @@ xdescribe('AppComponent & Lazy Loading (not working yet)', () => {
* Wait a tick, then detect changes, and tick again
*/
function advance(): void {
tick(); // wait while navigating
fixture.detectChanges(); // update view
tick(); // wait for async data to arrive
tick(); // wait while navigating
fixture.detectChanges(); // update view
tick(); // wait for async data to arrive
}
function createComponent() {
@ -148,8 +145,8 @@ function createComponent() {
router = injector.get(Router);
router.initialNavigation();
spyOn(injector.get(TwainService), 'getQuote')
// fake fast async observable
.and.returnValue(asyncData('Test Quote'));
// fake fast async observable
.and.returnValue(asyncData('Test Quote'));
advance();
page = new Page();
@ -168,14 +165,14 @@ class Page {
constructor() {
const links = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
this.aboutLinkDe = links[2];
this.aboutLinkDe = links[2];
this.dashboardLinkDe = links[0];
this.heroesLinkDe = links[1];
this.heroesLinkDe = links[1];
// for debugging
this.comp = comp;
this.comp = comp;
this.fixture = fixture;
this.router = router;
this.router = router;
}
}

View File

@ -1,66 +1,70 @@
// #docplaster
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { RouterLinkDirectiveStub } from '../testing';
import { AppComponent } from './app.component';
// #docregion component-stubs
@Component({selector: 'app-banner', template: ''})
class BannerStubComponent {}
class BannerStubComponent {
}
@Component({selector: 'router-outlet', template: ''})
class RouterOutletStubComponent { }
class RouterOutletStubComponent {
}
@Component({selector: 'app-welcome', template: ''})
class WelcomeStubComponent {}
class WelcomeStubComponent {
}
// #enddocregion component-stubs
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
describe('AppComponent & TestModule', () => {
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
// #docregion testbed-stubs
TestBed.configureTestingModule({
declarations: [
AppComponent,
RouterLinkDirectiveStub,
BannerStubComponent,
RouterOutletStubComponent,
WelcomeStubComponent
]
})
// #enddocregion testbed-stubs
.compileComponents().then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
TestBed
.configureTestingModule({
declarations: [
AppComponent, RouterLinkDirectiveStub, BannerStubComponent, RouterOutletStubComponent,
WelcomeStubComponent
]
})
// #enddocregion testbed-stubs
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
tests();
});
//////// Testing w/ NO_ERRORS_SCHEMA //////
describe('AppComponent & NO_ERRORS_SCHEMA', () => {
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
// #docregion no-errors-schema, mixed-setup
TestBed.configureTestingModule({
declarations: [
AppComponent,
// #enddocregion no-errors-schema
BannerStubComponent,
// #docregion no-errors-schema
RouterLinkDirectiveStub
],
schemas: [ NO_ERRORS_SCHEMA ]
})
// #enddocregion no-errors-schema, mixed-setup
.compileComponents().then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
TestBed
.configureTestingModule({
declarations: [
AppComponent,
// #enddocregion no-errors-schema
BannerStubComponent,
// #docregion no-errors-schema
RouterLinkDirectiveStub
],
schemas: [NO_ERRORS_SCHEMA]
})
// #enddocregion no-errors-schema, mixed-setup
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
tests();
});
@ -72,30 +76,23 @@ import { AppModule } from './app.module';
import { AppRoutingModule } from './app-routing.module';
describe('AppComponent & AppModule', () => {
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({imports: [AppModule]})
beforeEach(async(() => {
// Get rid of app's Router configuration otherwise many failures.
// Doing so removes Router declarations; add the Router stubs
.overrideModule(AppModule, {
remove: {imports: [AppRoutingModule]},
add: {declarations: [RouterLinkDirectiveStub, RouterOutletStubComponent]}
})
TestBed.configureTestingModule({
imports: [ AppModule ]
})
.compileComponents()
// Get rid of app's Router configuration otherwise many failures.
// Doing so removes Router declarations; add the Router stubs
.overrideModule(AppModule, {
remove: {
imports: [ AppRoutingModule ]
},
add: {
declarations: [ RouterLinkDirectiveStub, RouterOutletStubComponent ]
}
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
.then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
tests();
@ -107,11 +104,10 @@ function tests() {
// #docregion test-setup
beforeEach(() => {
fixture.detectChanges(); // trigger initial data binding
fixture.detectChanges(); // trigger initial data binding
// find DebugElements with an attached RouterLinkStubDirective
linkDes = fixture.debugElement
.queryAll(By.directive(RouterLinkDirectiveStub));
linkDes = fixture.debugElement.queryAll(By.directive(RouterLinkDirectiveStub));
// get attached link directive instances
// using each DebugElement's injector
@ -132,8 +128,8 @@ function tests() {
});
it('can click Heroes link in template', () => {
const heroesLinkDe = linkDes[1]; // heroes link DebugElement
const heroesLink = routerLinks[1]; // heroes link directive
const heroesLinkDe = linkDes[1]; // heroes link DebugElement
const heroesLink = routerLinks[1]; // heroes link directive
expect(heroesLink.navigatedTo).toBeNull('should not have navigated yet');

View File

@ -1,6 +1,6 @@
// #docplaster
// #docregion import-async
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
// #enddocregion import-async
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
@ -14,11 +14,12 @@ describe('BannerComponent (external files)', () => {
describe('Two beforeEach', () => {
// #docregion async-before-each
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
})
.compileComponents(); // compile template and css
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
declarations: [BannerComponent],
})
.compileComponents(); // compile template and css
}));
// #enddocregion async-before-each
@ -26,7 +27,7 @@ describe('BannerComponent (external files)', () => {
// #docregion sync-before-each
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance; // BannerComponent test instance
component = fixture.componentInstance; // BannerComponent test instance
h1 = fixture.nativeElement.querySelector('h1');
});
// #enddocregion sync-before-each
@ -36,16 +37,17 @@ describe('BannerComponent (external files)', () => {
describe('One beforeEach', () => {
// #docregion one-before-each
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
h1 = fixture.nativeElement.querySelector('h1');
});
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
declarations: [BannerComponent],
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
h1 = fixture.nativeElement.querySelector('h1');
});
}));
// #enddocregion one-before-each
@ -69,4 +71,3 @@ describe('BannerComponent (external files)', () => {
});
}
});

View File

@ -1,14 +1,16 @@
// #docplaster
// #docregion import-by
import { By } from '@angular/platform-browser';
// #enddocregion import-by
// #docregion import-debug-element
import { DebugElement } from '@angular/core';
// #enddocregion import-debug-element
// #docregion v1
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
// #enddocregion v1
import { BannerComponent } from './banner-initial.component';
/*
// #docregion v1
import { BannerComponent } from './banner.component';
@ -17,15 +19,12 @@ describe('BannerComponent', () => {
// #enddocregion v1
*/
describe('BannerComponent (initial CLI generated)', () => {
// #docregion v1
// #docregion v1
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ]
})
.compileComponents();
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({declarations: [BannerComponent]}).compileComponents();
}));
beforeEach(() => {
@ -44,9 +43,7 @@ describe('BannerComponent (initial CLI generated)', () => {
describe('BannerComponent (minimal)', () => {
it('should create', () => {
// #docregion configureTestingModule
TestBed.configureTestingModule({
declarations: [ BannerComponent ]
});
TestBed.configureTestingModule({declarations: [BannerComponent]});
// #enddocregion configureTestingModule
// #docregion createComponent
const fixture = TestBed.createComponent(BannerComponent);
@ -65,9 +62,7 @@ describe('BannerComponent (with beforeEach)', () => {
let fixture: ComponentFixture<BannerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ]
});
TestBed.configureTestingModule({declarations: [BannerComponent]});
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
});

View File

@ -1,22 +1,21 @@
// #docplaster
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { addMatchers, click } from '../../testing';
import { Hero } from '../model/hero';
import { DashboardHeroComponent } from './dashboard-hero.component';
beforeEach( addMatchers );
beforeEach(addMatchers);
describe('DashboardHeroComponent class only', () => {
// #docregion class-only
it('raises the selected event when clicked', () => {
const comp = new DashboardHeroComponent();
const hero: Hero = { id: 42, name: 'Test' };
const hero: Hero = {id: 42, name: 'Test'};
comp.hero = hero;
comp.selected.subscribe((selectedHero: Hero) => expect(selectedHero).toBe(hero));
@ -26,33 +25,31 @@ describe('DashboardHeroComponent class only', () => {
});
describe('DashboardHeroComponent when tested directly', () => {
let comp: DashboardHeroComponent;
let expectedHero: Hero;
let fixture: ComponentFixture<DashboardHeroComponent>;
let heroDe: DebugElement;
let heroEl: HTMLElement;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
// #docregion setup, config-testbed
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent ]
})
// #enddocregion setup, config-testbed
.compileComponents();
TestBed
.configureTestingModule({declarations: [DashboardHeroComponent]})
// #enddocregion setup, config-testbed
.compileComponents();
}));
beforeEach(() => {
// #docregion setup
fixture = TestBed.createComponent(DashboardHeroComponent);
comp = fixture.componentInstance;
comp = fixture.componentInstance;
// find the hero's DebugElement and element
heroDe = fixture.debugElement.query(By.css('.hero'));
heroDe = fixture.debugElement.query(By.css('.hero'));
heroEl = heroDe.nativeElement;
// mock the hero supplied by the parent component
expectedHero = { id: 42, name: 'Test Name' };
expectedHero = {id: 42, name: 'Test Name'};
// simulate the parent setting the input property with that hero
comp.hero = expectedHero;
@ -96,8 +93,8 @@ describe('DashboardHeroComponent when tested directly', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
click(heroDe); // click helper with DebugElement
click(heroEl); // click helper with native element
click(heroDe); // click helper with DebugElement
click(heroEl); // click helper with native element
expect(selectedHero).toBe(expectedHero);
});
@ -111,22 +108,21 @@ describe('DashboardHeroComponent when inside a test host', () => {
let fixture: ComponentFixture<TestHostComponent>;
let heroEl: HTMLElement;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
// #docregion test-host-setup
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent, TestHostComponent ]
})
// #enddocregion test-host-setup
.compileComponents();
TestBed
.configureTestingModule({declarations: [DashboardHeroComponent, TestHostComponent]})
// #enddocregion test-host-setup
.compileComponents();
}));
beforeEach(() => {
// #docregion test-host-setup
// create TestHostComponent instead of DashboardHeroComponent
fixture = TestBed.createComponent(TestHostComponent);
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
heroEl = fixture.nativeElement.querySelector('.hero');
fixture.detectChanges(); // trigger initial data binding
heroEl = fixture.nativeElement.querySelector('.hero');
fixture.detectChanges(); // trigger initial data binding
// #enddocregion test-host-setup
});
@ -155,8 +151,10 @@ import { Component } from '@angular/core';
</dashboard-hero>`
})
class TestHostComponent {
hero: Hero = {id: 42, name: 'Test Name' };
hero: Hero = {id: 42, name: 'Test Name'};
selectedHero: Hero;
onSelected(hero: Hero) { this.selectedHero = hero; }
onSelected(hero: Hero) {
this.selectedHero = hero;
}
}
// #enddocregion test-host

View File

@ -1,6 +1,5 @@
// #docplaster
import { async, inject, ComponentFixture, TestBed
} from '@angular/core/testing';
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { addMatchers, asyncData, click } from '../../testing';
import { HeroService } from '../model/hero.service';
@ -12,7 +11,7 @@ import { Router } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { DashboardModule } from './dashboard.module';
beforeEach ( addMatchers );
beforeEach(addMatchers);
let comp: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
@ -21,9 +20,7 @@ let fixture: ComponentFixture<DashboardComponent>;
describe('DashboardComponent (deep)', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ DashboardModule ]
});
TestBed.configureTestingModule({imports: [DashboardModule]});
});
compileAndCreate();
@ -43,10 +40,8 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('DashboardComponent (shallow)', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ DashboardComponent ],
schemas: [NO_ERRORS_SCHEMA]
});
TestBed.configureTestingModule(
{declarations: [DashboardComponent], schemas: [NO_ERRORS_SCHEMA]});
});
compileAndCreate();
@ -63,25 +58,26 @@ describe('DashboardComponent (shallow)', () => {
/** Add TestBed providers, compile, and create DashboardComponent */
function compileAndCreate() {
// #docregion compile-and-create-body
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
// #docregion router-spy
const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
const heroServiceSpy = jasmine.createSpyObj('HeroService', ['getHeroes']);
TestBed.configureTestingModule({
providers: [
{ provide: HeroService, useValue: heroServiceSpy },
{ provide: Router, useValue: routerSpy }
]
})
// #enddocregion router-spy
.compileComponents().then(() => {
fixture = TestBed.createComponent(DashboardComponent);
comp = fixture.componentInstance;
TestBed
.configureTestingModule({
providers: [
{provide: HeroService, useValue: heroServiceSpy}, {provide: Router, useValue: routerSpy}
]
})
// #enddocregion router-spy
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(DashboardComponent);
comp = fixture.componentInstance;
// getHeroes spy returns observable of test heroes
heroServiceSpy.getHeroes.and.returnValue(asyncData(getTestHeroes()));
});
// getHeroes spy returns observable of test heroes
heroServiceSpy.getHeroes.and.returnValue(asyncData(getTestHeroes()));
});
// #enddocregion compile-and-create-body
}));
}
@ -93,23 +89,20 @@ function compileAndCreate() {
function tests(heroClick: () => void) {
it('should NOT have heroes before ngOnInit', () => {
expect(comp.heroes.length).toBe(0,
'should not have heroes before ngOnInit');
expect(comp.heroes.length).toBe(0, 'should not have heroes before ngOnInit');
});
it('should NOT have heroes immediately after ngOnInit', () => {
fixture.detectChanges(); // runs initial lifecycle hooks
fixture.detectChanges(); // runs initial lifecycle hooks
expect(comp.heroes.length).toBe(0,
'should not have heroes until service promise resolves');
expect(comp.heroes.length).toBe(0, 'should not have heroes until service promise resolves');
});
describe('after get dashboard heroes', () => {
let router: Router;
// Trigger component so it gets heroes and binds to them
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
router = fixture.debugElement.injector.get(Router);
fixture.detectChanges(); // runs ngOnInit -> getHeroes
fixture.whenStable() // No need for the `lastPromise` hack!
@ -117,8 +110,8 @@ function tests(heroClick: () => void) {
}));
it('should HAVE heroes', () => {
expect(comp.heroes.length).toBeGreaterThan(0,
'should have heroes after service promise resolves');
expect(comp.heroes.length)
.toBeGreaterThan(0, 'should have heroes after service promise resolves');
});
it('should DISPLAY heroes', () => {
@ -130,8 +123,7 @@ function tests(heroClick: () => void) {
// #docregion navigate-test
it('should tell ROUTER to navigate when hero clicked', () => {
heroClick(); // trigger click on first inner <div class="hero">
heroClick(); // trigger click on first inner <div class="hero">
// 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
});
}

View File

@ -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
});

View File

@ -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: '<span>Fake</span>' }
})
.createComponent(Child1Component);
.overrideComponent(Child1Component, {
set: { template: '<span>Fake</span>' }
})
.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: '<my-service-comp></my-service-comp>' })
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: `
<needs-content #nc>
<child-1 #content text="My"></child-1>
<child-2 #content text="dog"></child-2>
@ -501,9 +502,9 @@ describe('demo (with TestBed):', () => {
<div #content>!</div>
</needs-content>
`
}
})
.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');
});
}));

View File

@ -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<HTMLButtonElement>('button'); }
get saveBtn() { return this.buttons[0]; }
get cancelBtn() { return this.buttons[1]; }
get nameDisplay() { return this.query<HTMLElement>('span'); }
get nameInput() { return this.query<HTMLInputElement>('input'); }
get buttons() {
return this.queryAll<HTMLButtonElement>('button');
}
get saveBtn() {
return this.buttons[0];
}
get cancelBtn() {
return this.buttons[1];
}
get nameDisplay() {
return this.query<HTMLElement>('span');
}
get nameInput() {
return this.query<HTMLInputElement>('input');
}
gotoListSpy: jasmine.Spy;
navigateSpy: jasmine.Spy;

View File

@ -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);

View File

@ -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

View File

@ -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<string>('TwainService test failure'));
// tell spy to return an async error observable
getQuoteSpy.and.returnValue(asyncError<string>('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
});
});

View File

@ -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);
}));
});

View File

@ -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);
});
});

View File

@ -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<PhoneData[]> {
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<PhoneListComponent>;
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);
});
});

View File

@ -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);
});
});

View File

@ -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<PhoneData[]> {
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<PhoneListComponent>;
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);
});
});

View File

@ -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}}`

View File

@ -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

View File

@ -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.
<code-example language="sh">

View File

@ -58,6 +58,7 @@ v9 - v12
| `@angular/core` | [`ANALYZE_FOR_ENTRY_COMPONENTS`](api/core/ANALYZE_FOR_ENTRY_COMPONENTS) | <!--v9--> v11 |
| `@angular/router` | [`loadChildren` string syntax](#loadChildren) | <!--v9--> v11 |
| `@angular/core/testing` | [`TestBed.get`](#testing) | <!--v9--> v12 |
| `@angular/core/testing` | [`async`](#testing) | <!--v9--> v12 |
| `@angular/router` | [`ActivatedRoute` params and `queryParams` properties](#activatedroute-props) | unspecified |
| template syntax | [`/deep/`, `>>>`, and `::ng-deep`](#deep-component-style-selector) | <!--v7--> unspecified |
| browser support | [`IE 9 and 10, IE mobile`](#ie-9-10-and-mobile) | <!--v10--> 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

View File

@ -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&mdash;that is, adding, removing, or manipulating elements and their children.
To learn more, see [Structural Directives](guide/structural-directives).

View File

@ -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
<div class="alert is-helpful">
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).
</div>
@ -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.

View File

@ -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
Well 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, well be working on making NgModules optional. This work will allow developers to develop standalone components and implement an alternative API for declaring the components 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, well 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.

View File

@ -53,14 +53,27 @@ This field contains an array of asset groups, each of which defines a set of ass
```json
{
"assetGroups": [{
...
}, {
...
}]
"assetGroups": [
{
...
},
{
...
}
]
}
```
<div class="alert is-helpful">
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`.
</div>
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": [
{
...
},
{
...
}
]
}
```
<div class="alert is-helpful">
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`.
</div>
Data groups follow this Typescript interface:
```typescript

View File

@ -159,10 +159,10 @@ It also generates an initial test file for the component, `banner-external.compo
<div class="alert is-helpful">
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.
</div>

View File

@ -402,7 +402,7 @@ There is no nested syntax (like a `Promise.then()`) to disrupt the flow of contr
<div class="alert is-helpful">
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.
</div>
@ -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()`.
<div class="alert is-helpful">
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.
</div>
Here's the previous `fakeAsync()` test, re-written with the `async()` utility.
Here's the previous `fakeAsync()` test, re-written with the `waitForAsync()` utility.
<code-example
path="testing/src/app/twain/twain.component.spec.ts"
region="async-test">
</code-example>
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"></code-example>
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.
<div class="alert is-critical">
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
<code-example language="sh" class="code-shell" hideCopy>
@ -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.
<code-example
path="testing/src/app/banner/banner-external.component.spec.ts"
@ -1565,7 +1565,7 @@ Write the first async `beforeEach` like this.
region="async-before-each"
header="app/banner/banner-external.component.spec.ts (async beforeEach)"></code-example>
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()`.

View File

@ -25,7 +25,7 @@ Here's a summary of the stand-alone functions, in order of likely utility:
<td>
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).
</td>
</tr>

View File

@ -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, <a href="https://msdn.microsoft.com/library/ee330736%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396#websocket_maxconn" title="MSDN WebSocket settings">MSDN WebSocket Settings</a>.
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.
<div class="alert is-important">
If you create project with `Angular/CLI`, it is already imported in `src/test.ts`.

View File

@ -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.",

View File

@ -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');

View File

@ -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<void>();
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);
}

View File

@ -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 = [];

View File

@ -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',

View File

@ -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",
],
)

View File

@ -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<string[]>;
/**
* 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<MergeConfig>}>;
/** Loads and validates the merge configuration. */
export function loadAndValidateConfig(): {config?: MergeConfigWithRemote, errors?: string[]} {
const config: Partial<DevInfraMergeConfig> = getConfig();
export async function loadAndValidateConfig(
config: Partial<DevInfraMergeConfig>,
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. */

View File

@ -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<semver.SemVer> {
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<VersionBranch[]> {
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};
}

View File

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

View File

@ -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<TargetLabel[]> {
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<string[]|null> {
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']);
});
});
});
});

View File

@ -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<TargetLabel[]> {
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];
},
},
];
}

View File

@ -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.`);
}
}

View File

@ -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`};
}

View File

@ -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);
}

View File

@ -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,
};
}

View File

@ -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<MergeConfig, 'labels'>, 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<string[]> {
return typeof label.branches === 'function' ? await label.branches(githubTargetBranch) :
await label.branches;
}

View File

@ -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.

View File

@ -19,6 +19,7 @@
"inquirer": "<from-root>",
"minimatch": "<from-root>",
"multimatch": "<from-root>",
"node-fetch": "<from-root>",
"node-uuid": "<from-root>",
"semver": "<from-root>",
"shelljs": "<from-root>",

View File

@ -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;

View File

@ -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<Octokit.RateLimitGetResponse>&{
@ -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<string[]>|null = null;
/**
@ -67,7 +62,8 @@ export class GitClient {
private _githubTokenRegex: RegExp|null = null;
constructor(
private _githubToken?: string, private _config: Pick<NgDevConfig, 'github'> = getConfig()) {
private _githubToken?: string, private _config: Pick<NgDevConfig, 'github'> = 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.

View File

@ -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",

View File

@ -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<T> {
@ -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;

View File

@ -21,9 +21,9 @@
"master": {
"uncompressed": {
"runtime-es2015": 3097,
"main-es2015": 429885,
"main-es2015": 430239,
"polyfills-es2015": 52195
}
}
}
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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!');
});
});

View File

@ -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', () => {

View File

@ -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!');
});
});

View File

@ -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!');
});
});

View File

@ -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!');
});
});

View File

@ -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', () => {

View File

@ -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",

View File

@ -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

View File

@ -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('<div *ngFor="let item of items" [ngClass]="item"></div>');
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('<div [ngClass]="{foo: true, bar: false}"></div>');
detectChangesAndExpectClassName('foo');
}));
it('should add classes specified in an object literal without change in class names',
async(() => {
waitForAsync(() => {
fixture =
createTestComponent(`<div [ngClass]="{'foo-bar': true, 'fooBar': true}"></div>`);
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('<div [ngClass]="{foo: condition, bar: !condition}"></div>');
@ -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('<div [ngClass]="objExpr"></div>');
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('<div [ngClass]="objExpr"></div>');
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('<div [ngClass]="objExpr"></div>');
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('<div [ngClass]="objExpr"></div>');
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('<div [ngClass]="objExpr"></div>');
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(`<div [ngClass]="['foo', 'bar', 'foo-bar', 'fooBar']"></div>`);
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('<div [ngClass]="arrExpr"></div>');
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('<div [ngClass]="arrExpr"></div>');
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('<div class="foo" [ngClass]="arrExpr"></div>');
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('<div class="foo" [ngClass]="arrExpr"></div>');
getComponent().arrExpr = ['', ' '];
detectChangesAndExpectClassName('foo');
}));
it('should trim blanks from class names', async(() => {
it('should trim blanks from class names', waitForAsync(() => {
fixture = createTestComponent('<div class="foo" [ngClass]="arrExpr"></div>');
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('<div [ngClass]="arrExpr"></div>');
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('<div [ngClass]="setExpr"></div>');
let setExpr = new Set<string>();
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(`<div [ngClass]="'foo bar foo-bar fooBar'"></div>`);
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('<div [ngClass]="strExpr"></div>');
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(`<div [ngClass]="strExpr"></div>`);
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(`<div class="foo" [ngClass]="strExpr"></div>`);
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(`<div class="foo" [ngClass]="strExpr"></div>`);
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('<div [ngClass]="objExpr" class="init foo"></div>');
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(`<div [ngClass]="objExpr" class="{{'init foo'}}"></div>`);
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(
`<div [ngClass]="{large: false, small: true}" class="{{strExpr}}"></div>`);
@ -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(`<div [ngClass]="objExpr" class="init" [class]="'foo'"></div>`);
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 =
'<div class="init foo" [ngClass]="objExpr" [class.baz]="condition"></div>';
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 = '<div class="init" [ngClass]="objExpr" [class]="strExpr"></div>';
fixture = createTestComponent(template);
const cmp = getComponent();

View File

@ -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 = `<ng-template *ngComponentOutlet="currentComponent"></ng-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 = `<ng-template>projected foo</ng-template>${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);

View File

@ -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 = '<ul><li *ngFor="let item of items">{{item["name"]}};</li></ul>';
fixture = createTestComponent(template);
@ -95,14 +95,14 @@ let thisArg: any;
detectChangesAndExpectText('shyam;');
}));
it('should gracefully handle nulls', async(() => {
it('should gracefully handle nulls', waitForAsync(() => {
const template = '<ul><li *ngFor="let item of null">{{item}};</li></ul>';
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 = <any>'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 = '<div *ngFor="let item of items">' +
'<div *ngFor="let subitem of item">{{subitem}}-{{item.length}};</div>|' +
'</div>';
@ -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 = '<div *ngFor="let item of items">' +
'<div *ngFor="let subitem of item">{{subitem}}-{{item.length}};</div>' +
'</div>';
@ -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 = `<div *ngFor="let item of items; let i=index">` +
`<div>{{i}}|</div>` +
`<div *ngIf="i % 2 == 0">even|</div>` +
@ -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 =
'<ul><li *ngFor="let item of items as collection; index as i">{{i}}/{{collection.length}} - {{item}};</li></ul>';
fixture = createTestComponent(template);
@ -196,7 +197,7 @@ let thisArg: any;
detectChangesAndExpectText('0/3 - 1;1/3 - 2;2/3 - 3;');
}));
it('should display indices correctly', async(() => {
it('should display indices correctly', waitForAsync(() => {
const template = '<span *ngFor ="let item of items; let i=index">{{i.toString()}}</span>';
fixture = createTestComponent(template);
@ -207,7 +208,7 @@ let thisArg: any;
detectChangesAndExpectText('0123456789');
}));
it('should display count correctly', async(() => {
it('should display count correctly', waitForAsync(() => {
const template = '<span *ngFor="let item of items; let len=count">{{len}}</span>';
fixture = createTestComponent(template);
@ -218,7 +219,7 @@ let thisArg: any;
detectChangesAndExpectText('666666');
}));
it('should display first item correctly', async(() => {
it('should display first item correctly', waitForAsync(() => {
const template =
'<span *ngFor="let item of items; let isFirst=first">{{isFirst.toString()}}</span>';
fixture = createTestComponent(template);
@ -230,7 +231,7 @@ let thisArg: any;
detectChangesAndExpectText('truefalse');
}));
it('should display last item correctly', async(() => {
it('should display last item correctly', waitForAsync(() => {
const template =
'<span *ngFor="let item of items; let isLast=last">{{isLast.toString()}}</span>';
fixture = createTestComponent(template);
@ -242,7 +243,7 @@ let thisArg: any;
detectChangesAndExpectText('falsetrue');
}));
it('should display even items correctly', async(() => {
it('should display even items correctly', waitForAsync(() => {
const template =
'<span *ngFor="let item of items; let isEven=even">{{isEven.toString()}}</span>';
fixture = createTestComponent(template);
@ -254,7 +255,7 @@ let thisArg: any;
detectChangesAndExpectText('truefalse');
}));
it('should display odd items correctly', async(() => {
it('should display odd items correctly', waitForAsync(() => {
const template =
'<span *ngFor="let item of items; let isOdd=odd">{{isOdd.toString()}}</span>';
fixture = createTestComponent(template);
@ -266,7 +267,7 @@ let thisArg: any;
detectChangesAndExpectText('falsetrue');
}));
it('should allow to use a custom template', async(() => {
it('should allow to use a custom template', waitForAsync(() => {
const template =
'<ng-container *ngFor="let item of items; template: tpl"></ng-container>' +
'<ng-template let-item let-i="index" #tpl><p>{{i}}: {{item}};</p></ng-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 =
`<ul><ng-container *ngFor="let item of items; template: null; let i=index">{{i}}: {{item}};</ng-container></ul>`;
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 =
'<ng-container *ngFor="let item of items; template: tpl">{{i}};</ng-container>' +
'<ng-template let-item let-i="index" #tpl>{{i}}: {{item}};</ng-template>';
@ -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 = `<p *ngFor="let item of items; trackBy: value"></p>`;
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 = `<p *ngFor="let item of items; trackBy: value">{{ item }}</p>`;
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 =
`<p *ngFor="let item of items; trackBy: trackByContext.bind(this)"></p>`;
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 =
`<p *ngFor="let item of items; trackBy: trackById; let i=index">{{items[i]}}</p>`;
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 =
`<div *ngFor="let item of items; trackBy: trackById">{{item['color']}}</div>`;
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 =
`<div *ngFor="let item of items; trackBy: trackById">{{item['color']}}</div>`;
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 = `<div *ngFor="let item of items; trackBy: trackByIndex">{{item}}</div>`;
fixture = createTestComponent(template);

View File

@ -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 = '<span *ngIf="booleanCondition">hello</span>';
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 = '<ng-template [ngIf]="booleanCondition">hello2</ng-template>';
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 = '<span *ngIf="booleanCondition">hello</span>';
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 =
'<div *ngIf="booleanCondition"><span *ngIf="nestedBooleanCondition">hello</span></div>';
@ -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 = '<span *ngIf="numberCondition + 1 >= 2">helloNumber</span>' +
'<span *ngIf="stringCondition == \'foo\'">helloString</span>' +
'<span *ngIf="functionCondition(stringCondition, numberCondition)">helloFunction</span>';
@ -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 = '<span *ngIf="numberCondition">hello</span>';
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 = '<span *ngIf="booleanCondition; else elseBlock">TRUE</span>' +
'<ng-template #elseBlock>FALSE</ng-template>';
@ -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 =
'<span *ngIf="booleanCondition; then thenBlock; else elseBlock">IGNORE</span>' +
'<ng-template #thenBlock>THEN</ng-template>' +
@ -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 =
'<span *ngIf="booleanCondition; else nestedBooleanCondition ? b1 : b2">TRUE</span>' +
'<ng-template #b1>FALSE1</ng-template>' +
@ -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 = '<span *ngIf="booleanCondition; else elseBlock; let v">{{v}}</span>' +
'<ng-template #elseBlock let-v>{{v}}</ng-template>';
@ -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 = '<span *ngIf="booleanCondition as v; else elseBlock">{{v}}</span>' +
'<ng-template #elseBlock let-v>{{v}}</ng-template>';
@ -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 = '<span *ngIf="booleanCondition; then thenBlock">IGNORE</span>' +
'<div #thenBlock>THEN</div>';
@ -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 = '<span *ngIf="booleanCondition; else elseBlock">IGNORE</span>' +
'<div #elseBlock>ELSE</div>';

View File

@ -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 = '<ul [ngPlural]="switchValue">' +
'<ng-template ngPluralCase="=0"><li>you have no messages.</li></ng-template>' +
'<ng-template ngPluralCase="=1"><li>you have one message.</li></ng-template>' +
@ -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 = '<div>' +
'<ul [ngPlural]="switchValue">' +
'<ng-template ngPluralCase="0"><li>you have no messages.</li></ng-template>' +
@ -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 = '<ul [ngPlural]="switchValue">' +
'<ng-template ngPluralCase="=0"><li>{{ switchValue }}</li></ng-template>' +
'</ul>';
@ -81,7 +81,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
}));
it('should be applicable to <ng-container> elements', async(() => {
it('should be applicable to <ng-container> elements', waitForAsync(() => {
const template = '<ng-container [ngPlural]="switchValue">' +
'<ng-template ngPluralCase="=0">you have no messages.</ng-template>' +
'<ng-template ngPluralCase="=1">you have one message.</ng-template>' +
@ -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 = '<ul [ngPlural]="switchValue">' +
'<ng-template ngPluralCase="few"><li>you have a few messages.</li></ng-template>' +
'<ng-template ngPluralCase="many"><li>you have many messages.</li></ng-template>' +
@ -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 = '<ul [ngPlural]="switchValue">' +
'<ng-template ngPluralCase="few"><li>you have a few messages.</li></ng-template>' +
'<ng-template ngPluralCase="other"><li>default message.</li></ng-template>' +
@ -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 = '<ul [ngPlural]="switchValue">' +
'<ng-template ngPluralCase="few"><li>you have a few messages.</li></ng-template>' +
'<ng-template ngPluralCase="=2">you have two messages.</ng-template>' +

View File

@ -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 = `<div [ngStyle]="{'max-width': '40px'}"></div>`;
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 = `<div [ngStyle]="expr"></div>`;
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 = `<div [ngStyle]="{'max-width.px': expr}"></div>`;
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(`<div [ngStyle]="{'color': expr}"></div>`);
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 = `<div [ngStyle]="expr"></div>`;
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 = `<div [ngStyle]="expr"></div>`;
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 = `<div [ngStyle]="expr"></div>`;
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 = `<div style="font-size: 12px" [ngStyle]="expr"></div>`;
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 = `<div [style.font-size.px]="12" [ngStyle]="expr"></div>`;
fixture = createTestComponent(template);

View File

@ -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 = `<ng-container *ngTemplateOutlet="tpl; context: this"></ng-container>` +
`<ng-template #tpl>{{context.foo}}</ng-template>`;
@ -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 = `<ng-container [ngTemplateOutlet]="null"></ng-container>`;
fixture = createTestComponent(template);
detectChangesAndExpectText('');
}));
it('should insert content specified by TemplateRef', async(() => {
it('should insert content specified by TemplateRef', waitForAsync(() => {
const template = `<ng-template #tpl>foo</ng-template>` +
`<ng-container [ngTemplateOutlet]="tpl"></ng-container>`;
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 = `<tpl-refs #refs="tplRefs"><ng-template>foo</ng-template></tpl-refs>` +
`<ng-container [ngTemplateOutlet]="currentTplRef"></ng-container>`;
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 =
`<tpl-refs #refs="tplRefs"><ng-template>foo</ng-template><ng-template>bar</ng-template></tpl-refs>` +
`<ng-container [ngTemplateOutlet]="currentTplRef"></ng-container>`;
@ -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 = `<ng-template #tpl>foo</ng-template>` +
`<ng-container *ngTemplateOutlet="tpl; context: null"></ng-container>`;
fixture = createTestComponent(template);
detectChangesAndExpectText('foo');
}));
it('should reflect initial context and changes', async(() => {
it('should reflect initial context and changes', waitForAsync(() => {
const template = `<ng-template let-foo="foo" #tpl>{{foo}}</ng-template>` +
`<ng-container *ngTemplateOutlet="tpl; context: context"></ng-container>`;
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 = `<ng-template let-ctx #tpl>{{ctx.foo}}</ng-template>` +
`<ng-container *ngTemplateOutlet="tpl; context: context"></ng-container>`;
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 = `<ng-template let-shawshank="shawshank" #tpl>{{shawshank}}</ng-template>` +
`<ng-container *ngTemplateOutlet="tpl; context: context"></ng-container>`;
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 = `<tpl-refs #refs="tplRefs"><ng-template>foo</ng-template></tpl-refs>` +
`<ng-container [ngTemplateOutlet]="currentTplRef"></ng-container>`;
fixture = createTestComponent(template);

View File

@ -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 = '<div>{{text}}<span ngNonBindable>{{text}}</span></div>';
const fixture = createTestComponent(template);
@ -29,7 +29,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
expect(fixture.nativeElement).toHaveText('foo{{text}}');
}));
it('should ignore directives on child nodes', async(() => {
it('should ignore directives on child nodes', waitForAsync(() => {
const template = '<div ngNonBindable><span id=child test-dec>{{text}}</span></div>';
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 = '<div><span id=child ngNonBindable test-dec>{{text}}</span></div>';
const fixture = createTestComponent(template);
fixture.detectChanges();

View File

@ -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;

View File

@ -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;

View File

@ -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<any>(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<any>(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<any>(scroller, 'scrollToElement');
scroller.scrollToAnchor(anchor);
expect(documentSpy.getElementById).toHaveBeenCalledWith(anchor);
expect(documentSpy.getElementsByName).toHaveBeenCalledWith(anchor);
expect((scroller as any).scrollToElement).not.toHaveBeenCalled();
});
});
});

View File

@ -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);

View File

@ -65,6 +65,7 @@ const CORE_SUPPORTED_SYMBOLS = new Map<string, string>([
['ɵɵInjectorDef', 'ɵɵInjectorDef'],
['ɵɵNgModuleDefWithMeta', 'ɵɵNgModuleDefWithMeta'],
['ɵNgModuleFactory', 'NgModuleFactory'],
['ɵnoSideEffects', 'ɵnoSideEffects'],
]);
const CORE_MODULE = '@angular/core';

View File

@ -61,10 +61,15 @@ export interface PerFileShimGenerator {
export interface FactoryTracker {
readonly sourceInfo: Map<string, FactoryInfo>;
track(sf: ts.SourceFile, factorySymbolName: string): void;
track(sf: ts.SourceFile, moduleInfo: ModuleInfo): void;
}
export interface FactoryInfo {
sourceFilePath: string;
moduleSymbolNames: Set<string>;
}
moduleSymbols: Map<string, ModuleInfo>;
}
export interface ModuleInfo {
name: string;
hasId: boolean;
}

View File

@ -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<string, FactoryInfo>();
private sourceToFactorySymbols = new Map<string, Set<string>>();
private sourceToFactorySymbols = new Map<string, Map<string, ModuleInfo>>();
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<string>();
this.sourceToFactorySymbols.set(absoluteSfPath, moduleSymbolNames);
this.sourceInfo.set(genFilePath, {sourceFilePath: absoluteSfPath, moduleSymbolNames});
const moduleSymbols = new Map<string, ModuleInfo>();
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),
),
),
),
);
}

View File

@ -79,7 +79,7 @@ export class NgTscPlugin implements TscPlugin {
}
wrapHost(
host: ts.CompilerHost&UnifiedModulesHost, inputFiles: readonly string[],
host: ts.CompilerHost&Partial<UnifiedModulesHost>, 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

View File

@ -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) {

View File

@ -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<TestClass, "foo"> | { bar: typeof TestClass.ngAcceptInputType_bar; }');
'init: Pick<TestClass, "foo"> & { bar: typeof TestClass.ngAcceptInputType_bar; }');
});
});
});

View File

@ -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', () => {

View File

@ -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: '<input dir [regular]="undefined" [coerced]="1">',
})
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', () => {

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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<TestComponent>;
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:

View File

@ -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 {
}

View File

@ -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);

View File

@ -292,5 +292,8 @@ export {
ɵɵsanitizeUrl,
ɵɵsanitizeUrlOrResourceUrl,
} from './sanitization/sanitization';
export {
noSideEffects as ɵnoSideEffects,
} from './util/closure';
// clang-format on

View File

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

Some files were not shown because too many files have changed in this diff Show More