diff --git a/.circleci/config.yml b/.circleci/config.yml index 0e69182d91..c0cc3f7eb4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -292,11 +292,21 @@ jobs: test_aio_local_ivy: <<: *job_defaults + docker: + # Needed because the AIO tests and the PWA score test depend on Chrome being available. + - image: *browsers_docker_image steps: - *attach_workspace - *init_environment # Build aio with Ivy (using local Angular packages) - run: yarn --cwd aio build-with-ivy --progress=false + # Run PWA-score tests + # (Run before unit and e2e tests, which destroy the `dist/` directory.) + - run: yarn --cwd aio test-pwa-score-localhost $CI_AIO_MIN_PWA_SCORE + # Run unit tests + - run: yarn --cwd aio test --progress=false --watch=false + # Run e2e tests + - run: yarn --cwd aio e2e --configuration=ci test_aio_tools: <<: *job_defaults diff --git a/aio/scripts/switch-to-ivy.js b/aio/scripts/switch-to-ivy.js index bafbbe3431..63fe90da14 100644 --- a/aio/scripts/switch-to-ivy.js +++ b/aio/scripts/switch-to-ivy.js @@ -33,7 +33,7 @@ function _main() { const oldTsConfigStr = readFileSync(tsConfigPath, 'utf8'); const oldTsConfigObj = parse(oldTsConfigStr); const newTsConfigObj = extend(true, oldTsConfigObj, NG_COMPILER_OPTS); - const newTsConfigStr = JSON.stringify(newTsConfigObj, null, 2); + const newTsConfigStr = `${JSON.stringify(newTsConfigObj, null, 2)}\n`; console.log(`\nNew config: ${newTsConfigStr}`); writeFileSync(tsConfigPath, newTsConfigStr); diff --git a/aio/src/app/custom-elements/code/code-example.component.spec.ts b/aio/src/app/custom-elements/code/code-example.component.spec.ts index 4784884c2b..ace165ed76 100644 --- a/aio/src/app/custom-elements/code/code-example.component.spec.ts +++ b/aio/src/app/custom-elements/code/code-example.component.spec.ts @@ -23,10 +23,10 @@ describe('CodeExampleComponent', () => { }); fixture = TestBed.createComponent(HostComponent); + fixture.detectChanges(); + hostComponent = fixture.componentInstance; codeExampleComponent = hostComponent.codeExampleComponent; - - fixture.detectChanges(); }); it('should be able to capture the code snippet provided in content', () => { diff --git a/aio/src/app/custom-elements/code/code-tabs.component.spec.ts b/aio/src/app/custom-elements/code/code-tabs.component.spec.ts index 7984d67bd1..1d9dc2b36d 100644 --- a/aio/src/app/custom-elements/code/code-tabs.component.spec.ts +++ b/aio/src/app/custom-elements/code/code-tabs.component.spec.ts @@ -23,10 +23,10 @@ describe('CodeTabsComponent', () => { }); fixture = TestBed.createComponent(HostComponent); + fixture.detectChanges(); + hostComponent = fixture.componentInstance; codeTabsComponent = hostComponent.codeTabsComponent; - - fixture.detectChanges(); }); it('should get correct tab info', () => { diff --git a/aio/src/app/custom-elements/toc/toc.component.spec.ts b/aio/src/app/custom-elements/toc/toc.component.spec.ts index f31bc26d99..dba9118445 100644 --- a/aio/src/app/custom-elements/toc/toc.component.spec.ts +++ b/aio/src/app/custom-elements/toc/toc.component.spec.ts @@ -63,178 +63,182 @@ describe('TocComponent', () => { expect(tocComponent.type).toEqual('None'); }); - it('should not display anything when no h2 or h3 TocItems', () => { - tocService.tocList.next([tocItem('H1', 'h1')]); - fixture.detectChanges(); - expect(tocComponentDe.children.length).toEqual(0); - }); + describe('(once the lifecycle hooks have run)', () => { + beforeEach(() => fixture.detectChanges()); - it('should update when the TocItems are updated', () => { - tocService.tocList.next([tocItem('Heading A')]); - fixture.detectChanges(); - expect(tocComponentDe.queryAll(By.css('li')).length).toBe(1); - - tocService.tocList.next([tocItem('Heading A'), tocItem('Heading B'), tocItem('Heading C')]); - fixture.detectChanges(); - expect(tocComponentDe.queryAll(By.css('li')).length).toBe(3); - }); - - it('should only display H2 and H3 TocItems', () => { - tocService.tocList.next([tocItem('Heading A', 'h1'), tocItem('Heading B'), tocItem('Heading C', 'h3')]); - fixture.detectChanges(); - - const tocItems = tocComponentDe.queryAll(By.css('li')); - const textContents = tocItems.map(item => item.nativeNode.textContent.trim()); - - expect(tocItems.length).toBe(2); - expect(textContents.find(text => text === 'Heading A')).toBeFalsy(); - expect(textContents.find(text => text === 'Heading B')).toBeTruthy(); - expect(textContents.find(text => text === 'Heading C')).toBeTruthy(); - expect(setPage().tocH1Heading).toBeFalsy(); - }); - - it('should stop listening for TocItems once destroyed', () => { - tocService.tocList.next([tocItem('Heading A')]); - fixture.detectChanges(); - expect(tocComponentDe.queryAll(By.css('li')).length).toBe(1); - - tocComponent.ngOnDestroy(); - tocService.tocList.next([tocItem('Heading A', 'h1'), tocItem('Heading B'), tocItem('Heading C')]); - fixture.detectChanges(); - expect(tocComponentDe.queryAll(By.css('li')).length).toBe(1); - }); - - describe('when fewer than `maxPrimary` TocItems', () => { - - beforeEach(() => { - tocService.tocList.next([tocItem('Heading A'), tocItem('Heading B'), tocItem('Heading C'), tocItem('Heading D')]); + it('should not display anything when no h2 or h3 TocItems', () => { + tocService.tocList.next([tocItem('H1', 'h1')]); fixture.detectChanges(); - page = setPage(); + expect(tocComponentDe.children.length).toEqual(0); }); - it('should have four displayed items', () => { - expect(page.listItems.length).toEqual(4); - }); - - it('should not have secondary items', () => { - expect(tocComponent.type).toEqual('EmbeddedSimple'); - const aSecond = page.listItems.find(item => item.classes.secondary); - expect(aSecond).toBeFalsy('should not find a secondary'); - }); - - it('should not display expando buttons', () => { - expect(page.tocHeadingButtonEmbedded).toBeFalsy('top expand/collapse button'); - expect(page.tocMoreButton).toBeFalsy('bottom more button'); - }); - }); - - describe('when many TocItems', () => { - let scrollToTopSpy: jasmine.Spy; - - beforeEach(() => { + it('should update when the TocItems are updated', () => { + tocService.tocList.next([tocItem('Heading A')]); fixture.detectChanges(); - page = setPage(); - scrollToTopSpy = TestBed.get(ScrollService).scrollToTop; + expect(tocComponentDe.queryAll(By.css('li')).length).toBe(1); + + tocService.tocList.next([tocItem('Heading A'), tocItem('Heading B'), tocItem('Heading C')]); + fixture.detectChanges(); + expect(tocComponentDe.queryAll(By.css('li')).length).toBe(3); }); - it('should have more than 4 displayed items', () => { - expect(page.listItems.length).toBeGreaterThan(4); + it('should only display H2 and H3 TocItems', () => { + tocService.tocList.next([tocItem('Heading A', 'h1'), tocItem('Heading B'), tocItem('Heading C', 'h3')]); + fixture.detectChanges(); + + const tocItems = tocComponentDe.queryAll(By.css('li')); + const textContents = tocItems.map(item => item.nativeNode.textContent.trim()); + + expect(tocItems.length).toBe(2); + expect(textContents.find(text => text === 'Heading A')).toBeFalsy(); + expect(textContents.find(text => text === 'Heading B')).toBeTruthy(); + expect(textContents.find(text => text === 'Heading C')).toBeTruthy(); + expect(setPage().tocH1Heading).toBeFalsy(); }); - it('should not display the h1 item', () => { - expect(page.listItems.find(item => item.classes.h1)).toBeFalsy('should not find h1 item'); + it('should stop listening for TocItems once destroyed', () => { + tocService.tocList.next([tocItem('Heading A')]); + fixture.detectChanges(); + expect(tocComponentDe.queryAll(By.css('li')).length).toBe(1); + + tocComponent.ngOnDestroy(); + tocService.tocList.next([tocItem('Heading A', 'h1'), tocItem('Heading B'), tocItem('Heading C')]); + fixture.detectChanges(); + expect(tocComponentDe.queryAll(By.css('li')).length).toBe(1); }); - it('should be in "collapsed" (not expanded) state at the start', () => { - expect(tocComponent.isCollapsed).toBeTruthy(); - }); - - it('should have "collapsed" class at the start', () => { - expect(tocComponentDe.children[0].classes.collapsed).toEqual(true); - }); - - it('should display expando buttons', () => { - expect(page.tocHeadingButtonEmbedded).toBeTruthy('top expand/collapse button'); - expect(page.tocMoreButton).toBeTruthy('bottom more button'); - }); - - it('should have secondary items', () => { - expect(tocComponent.type).toEqual('EmbeddedExpandable'); - }); - - // CSS will hide items with the secondary class when collapsed - it('should have secondary item with a secondary class', () => { - const aSecondary = page.listItems.find(item => item.classes.secondary); - expect(aSecondary).toBeTruthy('should find a secondary'); - }); - - describe('after click tocHeading button', () => { + describe('when fewer than `maxPrimary` TocItems', () => { beforeEach(() => { - page.tocHeadingButtonEmbedded.nativeElement.click(); + tocService.tocList.next([tocItem('Heading A'), tocItem('Heading B'), tocItem('Heading C'), tocItem('Heading D')]); fixture.detectChanges(); + page = setPage(); }); - it('should not be "collapsed"', () => { - expect(tocComponent.isCollapsed).toEqual(false); + it('should have four displayed items', () => { + expect(page.listItems.length).toEqual(4); }); - it('should not have "collapsed" class', () => { - expect(tocComponentDe.children[0].classes.collapsed).toBeFalsy(); + it('should not have secondary items', () => { + expect(tocComponent.type).toEqual('EmbeddedSimple'); + const aSecond = page.listItems.find(item => item.classes.secondary); + expect(aSecond).toBeFalsy('should not find a secondary'); }); - it('should not scroll', () => { - expect(scrollToTopSpy).not.toHaveBeenCalled(); - }); - - it('should be "collapsed" after clicking again', () => { - page.tocHeadingButtonEmbedded.nativeElement.click(); - fixture.detectChanges(); - expect(tocComponent.isCollapsed).toEqual(true); - }); - - it('should not scroll after clicking again', () => { - page.tocHeadingButtonEmbedded.nativeElement.click(); - fixture.detectChanges(); - expect(scrollToTopSpy).not.toHaveBeenCalled(); + it('should not display expando buttons', () => { + expect(page.tocHeadingButtonEmbedded).toBeFalsy('top expand/collapse button'); + expect(page.tocMoreButton).toBeFalsy('bottom more button'); }); }); - describe('after click tocMore button', () => { + describe('when many TocItems', () => { + let scrollToTopSpy: jasmine.Spy; beforeEach(() => { - page.tocMoreButton.nativeElement.click(); fixture.detectChanges(); + page = setPage(); + scrollToTopSpy = TestBed.get(ScrollService).scrollToTop; }); - it('should not be "collapsed"', () => { - expect(tocComponent.isCollapsed).toEqual(false); + it('should have more than 4 displayed items', () => { + expect(page.listItems.length).toBeGreaterThan(4); }); - it('should not have "collapsed" class', () => { - expect(tocComponentDe.children[0].classes.collapsed).toBeFalsy(); + it('should not display the h1 item', () => { + expect(page.listItems.find(item => item.classes.h1)).toBeFalsy('should not find h1 item'); }); - it('should not scroll', () => { - expect(scrollToTopSpy).not.toHaveBeenCalled(); + it('should be in "collapsed" (not expanded) state at the start', () => { + expect(tocComponent.isCollapsed).toBeTruthy(); }); - it('should be "collapsed" after clicking again', () => { - page.tocMoreButton.nativeElement.click(); - fixture.detectChanges(); - expect(tocComponent.isCollapsed).toEqual(true); + it('should have "collapsed" class at the start', () => { + expect(tocComponentDe.children[0].classes.collapsed).toEqual(true); }); - it('should be "collapsed" after clicking tocHeadingButton', () => { - page.tocMoreButton.nativeElement.click(); - fixture.detectChanges(); - expect(tocComponent.isCollapsed).toEqual(true); + it('should display expando buttons', () => { + expect(page.tocHeadingButtonEmbedded).toBeTruthy('top expand/collapse button'); + expect(page.tocMoreButton).toBeTruthy('bottom more button'); }); - it('should scroll after clicking again', () => { - page.tocMoreButton.nativeElement.click(); - fixture.detectChanges(); - expect(scrollToTopSpy).toHaveBeenCalled(); + it('should have secondary items', () => { + expect(tocComponent.type).toEqual('EmbeddedExpandable'); + }); + + // CSS will hide items with the secondary class when collapsed + it('should have secondary item with a secondary class', () => { + const aSecondary = page.listItems.find(item => item.classes.secondary); + expect(aSecondary).toBeTruthy('should find a secondary'); + }); + + describe('after click tocHeading button', () => { + + beforeEach(() => { + page.tocHeadingButtonEmbedded.nativeElement.click(); + fixture.detectChanges(); + }); + + it('should not be "collapsed"', () => { + expect(tocComponent.isCollapsed).toEqual(false); + }); + + it('should not have "collapsed" class', () => { + expect(tocComponentDe.children[0].classes.collapsed).toBeFalsy(); + }); + + it('should not scroll', () => { + expect(scrollToTopSpy).not.toHaveBeenCalled(); + }); + + it('should be "collapsed" after clicking again', () => { + page.tocHeadingButtonEmbedded.nativeElement.click(); + fixture.detectChanges(); + expect(tocComponent.isCollapsed).toEqual(true); + }); + + it('should not scroll after clicking again', () => { + page.tocHeadingButtonEmbedded.nativeElement.click(); + fixture.detectChanges(); + expect(scrollToTopSpy).not.toHaveBeenCalled(); + }); + }); + + describe('after click tocMore button', () => { + + beforeEach(() => { + page.tocMoreButton.nativeElement.click(); + fixture.detectChanges(); + }); + + it('should not be "collapsed"', () => { + expect(tocComponent.isCollapsed).toEqual(false); + }); + + it('should not have "collapsed" class', () => { + expect(tocComponentDe.children[0].classes.collapsed).toBeFalsy(); + }); + + it('should not scroll', () => { + expect(scrollToTopSpy).not.toHaveBeenCalled(); + }); + + it('should be "collapsed" after clicking again', () => { + page.tocMoreButton.nativeElement.click(); + fixture.detectChanges(); + expect(tocComponent.isCollapsed).toEqual(true); + }); + + it('should be "collapsed" after clicking tocHeadingButton', () => { + page.tocMoreButton.nativeElement.click(); + fixture.detectChanges(); + expect(tocComponent.isCollapsed).toEqual(true); + }); + + it('should scroll after clicking again', () => { + page.tocMoreButton.nativeElement.click(); + fixture.detectChanges(); + expect(scrollToTopSpy).toHaveBeenCalled(); + }); }); }); }); diff --git a/aio/src/app/search/search-box/search-box.component.spec.ts b/aio/src/app/search/search-box/search-box.component.spec.ts index 4fabfc54a7..1c41741a24 100644 --- a/aio/src/app/search/search-box/search-box.component.spec.ts +++ b/aio/src/app/search/search-box/search-box.component.spec.ts @@ -38,7 +38,7 @@ describe('SearchBoxComponent', () => { it('should get the current search query from the location service', fakeAsync(inject([LocationService], (location: MockLocationService) => { location.search.and.returnValue({ search: 'initial search' }); - component.ngOnInit(); + component.ngAfterViewInit(); expect(location.search).toHaveBeenCalled(); tick(300); expect(host.searchHandler).toHaveBeenCalledWith('initial search'); diff --git a/aio/src/app/search/search-box/search-box.component.ts b/aio/src/app/search/search-box/search-box.component.ts index 59334c13b1..7033d0b2d3 100644 --- a/aio/src/app/search/search-box/search-box.component.ts +++ b/aio/src/app/search/search-box/search-box.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewChild, ElementRef, EventEmitter, Output } from '@angular/core'; +import { AfterViewInit, Component, ViewChild, ElementRef, EventEmitter, Output } from '@angular/core'; import { LocationService } from 'app/shared/location.service'; import { Subject } from 'rxjs'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; @@ -24,7 +24,7 @@ import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; (focus)="doFocus()" (click)="doSearch()">` }) -export class SearchBoxComponent implements OnInit { +export class SearchBoxComponent implements AfterViewInit { private searchDebounce = 300; private searchSubject = new Subject(); @@ -40,7 +40,7 @@ export class SearchBoxComponent implements OnInit { /** * When we first show this search box we trigger a search if there is a search query in the URL */ - ngOnInit() { + ngAfterViewInit() { const query = this.locationService.search()['search']; if (query) { this.query = query; diff --git a/aio/tests/e2e/src/app.e2e-spec.ts b/aio/tests/e2e/src/app.e2e-spec.ts index 3fc8c44b4b..48f68b904a 100644 --- a/aio/tests/e2e/src/app.e2e-spec.ts +++ b/aio/tests/e2e/src/app.e2e-spec.ts @@ -239,7 +239,9 @@ describe('site App', function() { /* tslint:disable:max-line-length */ expect(page.ghLinks.get(0).getAttribute('href')) .toMatch(/https:\/\/github\.com\/angular\/angular\/edit\/master\/aio\/content\/guide\/http\.md\?message=docs%3A%20describe%20your%20change\.\.\./); - }); + // TODO(gkalpak): This test often times out with Ivy (because loading `guide/http` takes a lot of time). + // Remove the timeout once the performance issues have been fixed. + }, 60000); it('should not be present on top level pages', () => { page.navigateTo('features');