diff --git a/aio/src/pwa-manifest.json b/aio/src/pwa-manifest.json index 832b749ec5..12f667f483 100644 --- a/aio/src/pwa-manifest.json +++ b/aio/src/pwa-manifest.json @@ -22,5 +22,31 @@ "purpose": "maskable" } ], - "start_url": "/?utm_source=homescreen" + "start_url": "/?utm_source=homescreen", + "shortcuts": [ + { + "name": "Go to API Reference", + "short_name": "API", + "description": "Go to the Angular API reference page.", + "url": "/api?utm_source=homescreen" + }, + { + "name": "Go to Glossary", + "short_name": "Glossary", + "description": "Go to the glossary page: A list of common Angular terms and their explanation.", + "url": "/guide/glossary?utm_source=homescreen" + }, + { + "name": "Go to Resources", + "short_name": "Resources", + "description": "Go to the resources page: A list of Angular resouces, such as development tooling, UI libraries, books, courses, community publications, podcasts, etc.", + "url": "/resources?utm_source=homescreen" + }, + { + "name": "Go to Tutorial: Tour of Heroes", + "short_name": "Tutorial", + "description": "Go to the \"Tour of Heroes\" tutorial page: Learn how to create your first Angular application.", + "url": "/tutorial?utm_source=homescreen" + } + ] } diff --git a/aio/tests/e2e/src/app.po.ts b/aio/tests/e2e/src/app.po.ts index 9904eb42ad..f10ab9fe6b 100644 --- a/aio/tests/e2e/src/app.po.ts +++ b/aio/tests/e2e/src/app.po.ts @@ -41,7 +41,7 @@ export class SitePage { async navigateTo(pageUrl: string) { // Navigate to the page, disable animations, and wait for Angular. - await browser.get(`/${pageUrl}`); + await browser.get(`/${pageUrl.replace(/^\//, '')}`); await browser.executeScript('document.body.classList.add(\'no-animations\')'); await browser.waitForAngular(); } diff --git a/aio/tests/e2e/src/pwa-manifest.e2e-spec.ts b/aio/tests/e2e/src/pwa-manifest.e2e-spec.ts new file mode 100644 index 0000000000..6296c4a55f --- /dev/null +++ b/aio/tests/e2e/src/pwa-manifest.e2e-spec.ts @@ -0,0 +1,29 @@ +import { PwaManifestPage, PwaShortcutItem } from './pwa-manifest.po'; + + +describe('PWA manifest', () => { + const page = new PwaManifestPage(); + + describe('shortcuts', () => { + let shortcuts: PwaShortcutItem[]; + + // Helpers + const pageExists = async (url: string) => { + await page.navigateTo(url); + const content = await page.getDocViewerText(); + return !/page not found/i.test(content); + }; + + beforeEach(async () => { + shortcuts = await page.getPwaShortcuts(); + }); + + it('should exist', async () => { + for (const {short_name, url} of shortcuts) { + expect(await pageExists(url)).toBe( + true, + `Page for shortcut '${short_name}' (from '${page.pwaManifestUrl}') does not exist. (URL: ${url})`); + } + }); + }); +}); diff --git a/aio/tests/e2e/src/pwa-manifest.po.ts b/aio/tests/e2e/src/pwa-manifest.po.ts new file mode 100644 index 0000000000..15040d9909 --- /dev/null +++ b/aio/tests/e2e/src/pwa-manifest.po.ts @@ -0,0 +1,74 @@ +import { get as httpGet } from 'http'; +import { get as httpsGet } from 'https'; +import { browser } from 'protractor'; +import { SitePage } from './app.po'; + + +export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +/** + * The shape of a PWA manifest. + * For simplicity, we only define types for the properties we care about in tests. + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest + */ +export type PwaManifest = Json & { + shortcuts?: PwaShortcutItem[], +}; + +/** + * The shape of an item in a PWA manifest's `shortcuts` list. + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/shortcuts + */ +export type PwaShortcutItem = Json & { + url: string, + name: string, + short_name?: string, + description?: string, + icons?: PwaImageResource[], +}; + +/** + * The shape of an item in a PWA manifest's icons list (such as the value of the top-level `icons` property or that of + * the `icons` property of a shortcut item). + * @see https://w3c.github.io/manifest/#manifestimageresource-and-its-members + */ +export type PwaImageResource = Json & { + src: string, + sizes?: string, + type?: string, + purpose?: string, +}; + + +export class PwaManifestPage extends SitePage { + /** The base URL with the trailing `/` stripped off (if any). */ + baseUrl = browser.baseUrl.replace(/\/$/, ''); + + /** The URL to the app's PWA manifest. */ + pwaManifestUrl = `${this.baseUrl}/pwa-manifest.json`; + + private pwaManifestText: string | null = null; + + /** Get the app's PWA manifest as an object. */ + async getPwaManifest(): Promise { + if (this.pwaManifestText === null) { + const get = /^https:/.test(this.pwaManifestUrl) ? httpsGet : httpGet; + + this.pwaManifestText = await new Promise((resolve, reject) => { + let responseText = ''; + get(this.pwaManifestUrl, res => res + .on('data', chunk => responseText += chunk) + .on('end', () => resolve(responseText)) + .on('error', reject)); + }); + } + + return JSON.parse(this.pwaManifestText); + } + + /** Get a list of PWA shortcuts as extracted from the app's PWA manifest. */ + async getPwaShortcuts(): Promise { + const {shortcuts = []} = await this.getPwaManifest(); + return shortcuts; + } +}