diff --git a/aio/scripts/test-production.sh b/aio/scripts/test-production.sh index e158d5e78d..313d9b946a 100755 --- a/aio/scripts/test-production.sh +++ b/aio/scripts/test-production.sh @@ -5,8 +5,7 @@ set +x -eu -o pipefail readonly thisDir="$(cd $(dirname ${BASH_SOURCE[0]}); pwd)" readonly aioDir="$(realpath $thisDir/..)" - readonly appPtorConf="$aioDir/tests/e2e/protractor.conf.js" - readonly cfgPtorConf="$aioDir/tests/deployment/e2e/protractor.conf.js" + readonly protractorConf="$aioDir/tests/deployment/e2e/protractor.conf.js" readonly minPwaScore="95" readonly urls=( "https://angular.io/" @@ -24,11 +23,8 @@ set +x -eu -o pipefail for url in "${urls[@]}"; do echo -e "\nChecking '$url'...\n-----" - # Run e2e tests. - yarn protractor "$appPtorConf" --baseUrl "$url" - - # Run deployment config tests. - yarn protractor "$cfgPtorConf" --baseUrl "$url" + # Run basic e2e and deployment config tests. + yarn protractor "$protractorConf" --baseUrl "$url" # Run PWA-score tests. yarn test-pwa-score "$url" "$minPwaScore" diff --git a/aio/tests/deployment/e2e/redirection.e2e-spec.ts b/aio/tests/deployment/e2e/redirection.e2e-spec.ts index 9c7b8d41d0..cd31d828bb 100644 --- a/aio/tests/deployment/e2e/redirection.e2e-spec.ts +++ b/aio/tests/deployment/e2e/redirection.e2e-spec.ts @@ -1,31 +1,18 @@ import { browser } from 'protractor'; +import { SitePage } from './site.po'; describe(browser.baseUrl, () => { - const sitemapUrls = browser.params.sitemapUrls; - const legacyUrls = browser.params.legacyUrls; - const goTo = async url => { - // Go to the specified URL and then unregister the ServiceWorker - // to ensure subsequent requests are passed through to the server. - await browser.get(url); - await browser.executeAsyncScript(cb => navigator.serviceWorker - .getRegistrations() - .then(regs => Promise.all(regs.map(reg => reg.unregister()))) - .then(cb)); - }; + const page = new SitePage(); - beforeAll(async done => { - // Make an initial request to unregister the ServiceWorker. - await goTo(browser.baseUrl); - done(); - }); + beforeAll(done => page.init().then(done)); beforeEach(() => browser.waitForAngularEnabled(false)); afterEach(() => browser.waitForAngularEnabled(true)); describe('(with sitemap URLs)', () => { - sitemapUrls.forEach((url, i) => { - it(`should not redirect '${url}' (${i + 1}/${sitemapUrls.length})`, async () => { - await goTo(url); + page.sitemapUrls.forEach((url, i) => { + it(`should not redirect '${url}' (${i + 1}/${page.sitemapUrls.length})`, async () => { + await page.goTo(url); const expectedUrl = browser.baseUrl + url; const actualUrl = (await browser.getCurrentUrl()).replace(/\?.*$/, ''); @@ -36,9 +23,9 @@ describe(browser.baseUrl, () => { }); describe('(with legacy URLs)', () => { - legacyUrls.forEach(([fromUrl, toUrl], i) => { - it(`should redirect '${fromUrl}' to '${toUrl}' (${i + 1}/${legacyUrls.length})`, async () => { - await goTo(fromUrl); + page.legacyUrls.forEach(([fromUrl, toUrl], i) => { + it(`should redirect '${fromUrl}' to '${toUrl}' (${i + 1}/${page.legacyUrls.length})`, async () => { + await page.goTo(fromUrl); const expectedUrl = (/^http/.test(toUrl) ? '' : browser.baseUrl.replace(/\/$/, '')) + toUrl; const actualUrl = (await browser.getCurrentUrl()).replace(/\?.*$/, ''); diff --git a/aio/tests/deployment/e2e/site.po.ts b/aio/tests/deployment/e2e/site.po.ts new file mode 100644 index 0000000000..32d1519dc2 --- /dev/null +++ b/aio/tests/deployment/e2e/site.po.ts @@ -0,0 +1,60 @@ +import { browser, by, element, ExpectedConditions } from 'protractor'; + +export class SitePage { + /** All URLs found in the app's `sitemap.xml` (i.e. valid URLs tha should not be redirected). */ + sitemapUrls: string[] = browser.params.sitemapUrls; + + /** A list of legacy URLs that should be redirected to new URLs (in the form `[fromUrl, toUrl]`). */ + legacyUrls: string[][] = browser.params.legacyUrls; + + /** + * Enter a query into the search field. + */ + async enterSearch(query: string) { + const searchInput = element(by.css('input[type=search]')); + await searchInput.clear(); + await searchInput.sendKeys(query); + } + + /** + * Get the text content of the `aio-doc-viewer` element (in lowercase). + */ + async getDocViewerText() { + const docViewer = element(by.css('aio-doc-viewer')); + const text = await docViewer.getText(); + return text.toLowerCase(); + } + + /** + * Get a list of text contents for all search result items found on the page. + */ + async getSearchResults() { + const results = element.all(by.css('.search-results li')); + await browser.wait(ExpectedConditions.presenceOf(results.first()), 8000); + return await results.map(link => link!.getText()); + } + + /** + * Navigate to a URL, disable animations, unregister the ServiceWorker, and wait for Angular. + * (The SW is unregistered to ensure that subsequent requests are passed through to the server.) + */ + async goTo(url: string) { + const unregisterServiceWorker = (cb: () => void) => navigator.serviceWorker + .getRegistrations() + .then(regs => Promise.all(regs.map(reg => reg.unregister()))) + .then(cb); + + await browser.get(url || browser.baseUrl); + await browser.executeScript('document.body.classList.add(\'no-animations\')'); + await browser.executeAsyncScript(unregisterServiceWorker); + await browser.waitForAngular(); + }; + + /** + * Initialize the page object and get it ready for further requests. + */ + async init() { + // Make an initial request to unregister the ServiceWorker. + await this.goTo(''); + } +} diff --git a/aio/tests/deployment/e2e/smoke-tests.e2e-spec.ts b/aio/tests/deployment/e2e/smoke-tests.e2e-spec.ts new file mode 100644 index 0000000000..b55aae136e --- /dev/null +++ b/aio/tests/deployment/e2e/smoke-tests.e2e-spec.ts @@ -0,0 +1,103 @@ +import { browser } from 'protractor'; +import { SitePage } from './site.po'; + +describe(browser.baseUrl, () => { + const page = new SitePage(); + + beforeAll(done => page.init().then(done)); + + beforeEach(() => browser.waitForAngularEnabled(false)); + afterEach(() => browser.waitForAngularEnabled(true)); + + describe('(smoke tests)', () => { + it('should show the home page', () => { + page.goTo(''); + const text = page.getDocViewerText(); + + expect(text).toContain('one framework'); + expect(text).toContain('mobile & desktop'); + }); + + describe('(marketing pages)', () => { + const textPerUrl = { + features: 'features & benefits', + docs: 'what is angular?', + events: 'events', + resources: 'explore angular resources', + }; + + Object.keys(textPerUrl).forEach(url => { + it(`should show the page at '${url}'`, () => { + page.goTo(url); + expect(page.getDocViewerText()).toContain(textPerUrl[url]); + }); + }); + }); + + describe('(docs pages)', () => { + const textPerUrl = { + api: 'api list', + 'guide/architecture': 'architecture', + 'guide/http': 'httpclient', + 'guide/quickstart': 'quickstart', + 'guide/security': 'security', + tutorial: 'tutorial', + }; + + Object.keys(textPerUrl).forEach(url => { + it(`should show the page at '${url}'`, () => { + page.goTo(url); + expect(page.getDocViewerText()).toContain(textPerUrl[url]); + }); + }); + }); + + describe('(api docs pages)', () => { + const textPerUrl = { + /* Class */ 'api/core/Injector': 'class injector', + /* Const */ 'api/forms/NG_VALIDATORS': 'const ng_validators', + /* Decorator */ 'api/core/Component': '@component', + /* Directive */ 'api/common/NgIf': 'class ngif', + /* Enum */ 'api/core/ChangeDetectionStrategy': 'enum changedetectionstrategy', + /* Function */ 'api/animations/animate': 'animate(', + /* Interface */ 'api/core/OnDestroy': 'interface ondestroy', + /* Pipe */ 'api/common/JsonPipe': '| json', + /* Type-Alias */ 'api/common/http/HttpEvent': 'type httpevent', + }; + + Object.keys(textPerUrl).forEach(url => { + it(`should show the page at '${url}'`, () => { + page.goTo(url); + expect(page.getDocViewerText()).toContain(textPerUrl[url]); + }); + }); + }); + + describe('(search results)', () => { + beforeEach(() => page.goTo('')); + + it('should find pages when searching by a partial word in the title', () => { + page.enterSearch('ngCont'); + expect(page.getSearchResults()).toContain('NgControl'); + }); + + it('should find API docs when searching for an instance member name', () => { + page.enterSearch('writeValue'); + expect(page.getSearchResults()).toContain('ControlValueAccessor'); + }); + + it('should find API docs when searching for a static member name', () => { + page.enterSearch('compose'); + expect(page.getSearchResults()).toContain('Validators'); + }); + }); + + it('should show relevant results on 404', () => { + page.goTo('http/router'); + const results = page.getSearchResults(); + + expect(results).toContain('HttpClient'); + expect(results).toContain('Router'); + }); + }); +});