feat: add PWA automation tools (#2430)

## What / Why

Adds an opt-in Progressive Web Apps tool category that exposes the
installed web app lifecycle to MCP clients and agents.

The browser-level PWA APIs were introduced in Puppeteer 25.4.0
(puppeteer/puppeteer#15235). The current base uses Puppeteer 25.5.0,
which also enforces URL restrictions for browser-level PWA operations
(puppeteer/puppeteer#15271).

## Tools

- `install_pwa`
- `launch_pwa`
- `get_os_app_state`
- `uninstall_pwa`

The category is disabled by default and enabled with `--category-pwa`.

These are browser-scoped operations and do not require a selected page.
Callers provide the resolved manifest ID and, for installation, an
explicit page or bundle URL. `open_current_page_in_app` and automatic
current-page installation are intentionally deferred until Puppeteer
exposes page-native APIs with reliable loaded-document and
browser-context semantics.

## Compatibility and safety

- Requires a pipe-launched browser. The CLI rejects `--category-pwa`
when combined with `--auto-connect`, `--browser-url`, or
`--ws-endpoint`.
- Puppeteer rejects PWA operations when URL allow/block restrictions are
configured, preventing browser-internal PWA fetch and redirect flows
from bypassing the configured policy.
- File-based install bundles are validated against the MCP filesystem
roots.
- DevTools installation defaults to browser display mode unless
`displayMode: "standalone"` is supplied. Launch uses the app's saved
display preference.

## Testing

- PWA tests cover lifecycle/state, standalone launch, successful
explicit-URL launch, use without a selected page, file-bundle path
validation, OS-state output, and selected-page fallback after uninstall.
- Existing off-by-default category tests cover PWA tool registration and
opt-in exposure.
- Generated README, tool reference, CLI metadata, and telemetry metrics
are updated.
- Type checking, formatting, targeted tests, broad supporting repository
tests, third-party notice validation, and diff checks pass locally.
- Validated against Chrome for Testing 150 and a local Chromium/Edge
build.
- A real stdio MCP lifecycle passed end to end with the final four-tool
design: category gating, explicit install, OS state, standalone launch,
selecting and scripting the returned page, uninstall, selection
fallback, and post-uninstall state rejection.

Refs #2270
This commit is contained in:
Lu
2026-08-10 05:12:22 -07:00
committed by GitHub
parent 5c16940809
commit 0ea5c8d15b
12 changed files with 721 additions and 0 deletions
+10
View File
@@ -582,6 +582,11 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
- **WebMCP** (2 tools)
- [`execute_webmcp_tool`](docs/tool-reference.md#execute_webmcp_tool)
- [`list_webmcp_tools`](docs/tool-reference.md#list_webmcp_tools)
- **Progressive Web Apps** (4 tools)
- [`get_os_app_state`](docs/tool-reference.md#get_os_app_state)
- [`install_pwa`](docs/tool-reference.md#install_pwa)
- [`launch_pwa`](docs/tool-reference.md#launch_pwa)
- [`uninstall_pwa`](docs/tool-reference.md#uninstall_pwa)
<!-- END AUTO GENERATED TOOLS -->
@@ -747,6 +752,11 @@ The Chrome DevTools MCP server supports the following configuration option:
- **Type:** boolean
- **Default:** `false`
- **`--categoryPwa`/ `--category-pwa`**
Set to true to include tools for automating Progressive Web Apps (install, launch, uninstall, and OS state). This feature is only supported with a pipe connection; autoConnect, browserUrl, and wsEndpoint are not supported.
- **Type:** boolean
- **Default:** `false`
- **`--performanceCrux`/ `--performance-crux`**
Set to false to disable sending URLs from performance traces to CrUX API to get field performance data.
- **Type:** boolean
+52
View File
@@ -64,6 +64,11 @@
- **[WebMCP](#webmcp)** (2 tools)
- [`execute_webmcp_tool`](#execute_webmcp_tool)
- [`list_webmcp_tools`](#list_webmcp_tools)
- **[Progressive Web Apps](#progressive-web-apps)** (4 tools)
- [`get_os_app_state`](#get_os_app_state)
- [`install_pwa`](#install_pwa)
- [`launch_pwa`](#launch_pwa)
- [`uninstall_pwa`](#uninstall_pwa)
## Input automation
@@ -695,3 +700,50 @@ third-party developer tools with additional functionality. (requires flag: --cat
**Parameters:** None
---
## Progressive Web Apps
> NOTE: The Progressive Web Apps category is not active by default. Use the '--categoryPwa' flag.
### `get_os_app_state`
**Description:** Returns the OS integration state (badge count and registered file handlers) for an installed web app, identified by its manifest ID. (requires flag: --categoryPwa=true)
**Parameters:**
- **manifestId** (string) **(required)**: The manifest ID of the web app: the resolved `id` member of its manifest. If `id` is omitted, it defaults to the resolved `start_url` (e.g. "https://example.com/"). See https://w3c.github.io/manifest/#id-member.
---
### `install_pwa`
**Description:** Installs a Progressive Web App (PWA) identified by its manifest ID. This installs through the PWA CDP domain without a user gesture or install dialog. DevTools installs default to browser display mode. (requires flag: --categoryPwa=true)
**Parameters:**
- **installUrlOrBundleUrl** (string) **(required)**: The location of the app or bundle. For a normal site this is the page URL; for an Isolated Web App it can be a file:// or http(s):// signed web bundle.
- **manifestId** (string) **(required)**: The manifest ID of the web app: the resolved `id` member of its manifest. If `id` is omitted, it defaults to the resolved `start_url` (e.g. "https://example.com/"). See https://w3c.github.io/manifest/#id-member.
- **displayMode** (enum: "standalone", "browser") _(optional)_: Optional user display mode preference applied after install. "standalone" opens the app in its own window; "browser" opens it as a tab. Installs via the PWA CDP domain default to "browser" because they do not simulate the install dialog, so pass "standalone" to get an app-window experience.
---
### `launch_pwa`
**Description:** Launches an installed Progressive Web App using its saved display mode. Optionally opens a specific URL within the same app instead of the default start URL. (requires flag: --categoryPwa=true)
**Parameters:**
- **manifestId** (string) **(required)**: The manifest ID of the web app: the resolved `id` member of its manifest. If `id` is omitted, it defaults to the resolved `start_url` (e.g. "https://example.com/"). See https://w3c.github.io/manifest/#id-member.
- **url** (string) _(optional)_: Optional URL within the app to open instead of the default start URL.
---
### `uninstall_pwa`
**Description:** Uninstalls a Progressive Web App identified by its manifest ID and closes any open app windows. (requires flag: --categoryPwa=true)
**Parameters:**
- **manifestId** (string) **(required)**: The manifest ID of the web app: the resolved `id` member of its manifest. If `id` is omitted, it defaults to the resolved `start_url` (e.g. "https://example.com/"). See https://w3c.github.io/manifest/#id-member.
---
+21
View File
@@ -25,9 +25,14 @@ import {
type Browser,
type BrowserContext,
type ConsoleMessage,
type GetPWAStateOptions,
type InstallPWAOptions,
type LaunchPWAOptions,
type Page,
type PWAState,
type ScreenRecorder,
type Target,
type UninstallPWAOptions,
type Extension,
type Root,
type DevTools,
@@ -339,6 +344,22 @@ export class McpContext implements Context {
return !!(this.#options.allowList || this.#options.blocklist);
}
installPWA(options: InstallPWAOptions): Promise<string> {
return this.browser.installPWA(options);
}
uninstallPWA(options: UninstallPWAOptions): Promise<void> {
return this.browser.uninstallPWA(options);
}
launchPWA(options: LaunchPWAOptions): Promise<Page> {
return this.browser.launchPWA(options);
}
getPWAState(options: GetPWAStateOptions): Promise<PWAState> {
return this.browser.getPWAState(options);
}
setIsRunningPerformanceTrace(x: boolean): void {
this.#isRunningTrace = x;
}
+78
View File
@@ -642,6 +642,20 @@ export const commands: Commands = {
},
},
},
get_os_app_state: {
description:
'Returns the OS integration state (badge count and registered file handlers) for an installed web app, identified by its manifest ID. (requires flag: --categoryPwa=true)',
category: 'Progressive Web Apps',
args: {
manifestId: {
name: 'manifestId',
type: 'string',
description:
'The manifest ID of the web app: the resolved `id` member of its manifest. If `id` is omitted, it defaults to the resolved `start_url` (e.g. "https://example.com/"). See https://w3c.github.io/manifest/#id-member.',
required: true,
},
},
},
handle_dialog: {
description:
'If a browser dialog was opened, use this command to handle it',
@@ -695,6 +709,56 @@ export const commands: Commands = {
},
},
},
install_pwa: {
description:
'Installs a Progressive Web App (PWA) identified by its manifest ID. This installs through the PWA CDP domain without a user gesture or install dialog. DevTools installs default to browser display mode. (requires flag: --categoryPwa=true)',
category: 'Progressive Web Apps',
args: {
manifestId: {
name: 'manifestId',
type: 'string',
description:
'The manifest ID of the web app: the resolved `id` member of its manifest. If `id` is omitted, it defaults to the resolved `start_url` (e.g. "https://example.com/"). See https://w3c.github.io/manifest/#id-member.',
required: true,
},
installUrlOrBundleUrl: {
name: 'installUrlOrBundleUrl',
type: 'string',
description:
'The location of the app or bundle. For a normal site this is the page URL; for an Isolated Web App it can be a file:// or http(s):// signed web bundle.',
required: true,
},
displayMode: {
name: 'displayMode',
type: 'string',
description:
'Optional user display mode preference applied after install. "standalone" opens the app in its own window; "browser" opens it as a tab. Installs via the PWA CDP domain default to "browser" because they do not simulate the install dialog, so pass "standalone" to get an app-window experience.',
required: false,
enum: ['standalone', 'browser'],
},
},
},
launch_pwa: {
description:
'Launches an installed Progressive Web App using its saved display mode. Optionally opens a specific URL within the same app instead of the default start URL. (requires flag: --categoryPwa=true)',
category: 'Progressive Web Apps',
args: {
manifestId: {
name: 'manifestId',
type: 'string',
description:
'The manifest ID of the web app: the resolved `id` member of its manifest. If `id` is omitted, it defaults to the resolved `start_url` (e.g. "https://example.com/"). See https://w3c.github.io/manifest/#id-member.',
required: true,
},
url: {
name: 'url',
type: 'string',
description:
'Optional URL within the app to open instead of the default start URL.',
required: false,
},
},
},
lighthouse_audit: {
description:
'Get Lighthouse score and reports for accessibility, SEO, best practices, and agentic browsing. This excludes performance. For performance audits, run performance_start_trace',
@@ -1198,6 +1262,20 @@ export const commands: Commands = {
},
},
},
uninstall_pwa: {
description:
'Uninstalls a Progressive Web App identified by its manifest ID and closes any open app windows. (requires flag: --categoryPwa=true)',
category: 'Progressive Web Apps',
args: {
manifestId: {
name: 'manifestId',
type: 'string',
description:
'The manifest ID of the web app: the resolved `id` member of its manifest. If `id` is omitted, it defaults to the resolved `start_url` (e.g. "https://example.com/"). See https://w3c.github.io/manifest/#id-member.',
required: true,
},
},
},
upload_file: {
description: 'Upload a file through a provided element.',
category: 'Input automation',
@@ -258,6 +258,13 @@ export const cliOptions = {
describe:
'Set to true to enable third-party developer tools exposed by the inspected page itself',
},
categoryPwa: {
type: 'boolean',
hidden: false,
conflicts: ['autoConnect', 'browserUrl', 'wsEndpoint'],
describe:
'Set to true to include tools for automating Progressive Web Apps (install, launch, uninstall, and OS state). This feature is only supported with a pipe connection; autoConnect, browserUrl, and wsEndpoint are not supported.',
},
performanceCrux: {
type: 'boolean',
default: true,
+8
View File
@@ -371,5 +371,13 @@
{
"name": "allow_unrestricted_paths",
"flagType": "boolean"
},
{
"name": "category_pwa_present",
"flagType": "boolean"
},
{
"name": "category_pwa",
"flagType": "boolean"
}
]
+48
View File
@@ -913,5 +913,53 @@
"argType": "number"
}
]
},
{
"name": "get_os_app_state",
"args": [
{
"name": "manifest_id_length",
"argType": "number"
}
]
},
{
"name": "install_pwa",
"args": [
{
"name": "manifest_id_length",
"argType": "number"
},
{
"name": "install_url_or_bundle_url_length",
"argType": "number"
},
{
"name": "display_mode",
"argType": "string"
}
]
},
{
"name": "launch_pwa",
"args": [
{
"name": "manifest_id_length",
"argType": "number"
},
{
"name": "url_length",
"argType": "number"
}
]
},
{
"name": "uninstall_pwa",
"args": [
{
"name": "manifest_id_length",
"argType": "number"
}
]
}
]
+9
View File
@@ -17,7 +17,12 @@ import type {
Dialog,
ElementHandle,
Extension,
GetPWAStateOptions,
InstallPWAOptions,
LaunchPWAOptions,
PWAState,
ScreenRecorder,
UninstallPWAOptions,
Viewport,
DevTools,
Protocol,
@@ -195,6 +200,10 @@ export type SupportedExtensions =
*/
export type Context = Readonly<{
validatePath(filePath?: string): Promise<void>;
installPWA(options: InstallPWAOptions): Promise<string>;
uninstallPWA(options: UninstallPWAOptions): Promise<void>;
launchPWA(options: LaunchPWAOptions): Promise<Page>;
getPWAState(options: GetPWAStateOptions): Promise<PWAState>;
ensureExtension<Extension extends `.${string}`>(
filePath: string,
extension: Extension,
+3
View File
@@ -15,6 +15,7 @@ export enum ToolCategory {
THIRD_PARTY = 'experimentalThirdParty',
MEMORY = 'memory',
WEBMCP = 'experimentalWebmcp',
PWA = 'pwa',
}
export const labels = {
@@ -28,10 +29,12 @@ export const labels = {
[ToolCategory.THIRD_PARTY]: 'Third-party',
[ToolCategory.MEMORY]: 'Memory',
[ToolCategory.WEBMCP]: 'WebMCP',
[ToolCategory.PWA]: 'Progressive Web Apps',
};
export const OFF_BY_DEFAULT_CATEGORIES = [
ToolCategory.EXTENSIONS,
ToolCategory.THIRD_PARTY,
ToolCategory.WEBMCP,
ToolCategory.PWA,
];
+154
View File
@@ -0,0 +1,154 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {fileURLToPath} from 'node:url';
import {zod} from '../third_party/index.js';
import {ToolCategory} from './categories.js';
import {defineTool} from './ToolDefinition.js';
const manifestIdSchema = zod
.string()
.describe(
'The manifest ID of the web app: the resolved `id` member of its manifest. ' +
'If `id` is omitted, it defaults to the resolved `start_url` ' +
'(e.g. "https://example.com/"). See https://w3c.github.io/manifest/#id-member.',
);
const displayModeSchema = zod
.enum(['standalone', 'browser'])
.optional()
.describe(
'Optional user display mode preference applied after install. ' +
'"standalone" opens the app in its own window; "browser" opens it as a ' +
'tab. Installs via the PWA CDP domain default to "browser" because they ' +
'do not simulate the install dialog, so pass "standalone" to get an ' +
'app-window experience.',
);
export const installPwa = defineTool({
name: 'install_pwa',
description:
'Installs a Progressive Web App (PWA) identified by its manifest ID. ' +
'This installs through the PWA CDP domain without a user gesture or install ' +
'dialog. DevTools installs default to browser display mode.',
annotations: {
category: ToolCategory.PWA,
readOnlyHint: false,
},
schema: {
manifestId: manifestIdSchema,
installUrlOrBundleUrl: zod
.string()
.describe(
'The location of the app or bundle. For a normal site this is the page ' +
'URL; for an Isolated Web App it can be a file:// or http(s):// ' +
'signed web bundle.',
),
displayMode: displayModeSchema,
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const {manifestId, installUrlOrBundleUrl, displayMode} = request.params;
const installUrl = new URL(installUrlOrBundleUrl);
if (installUrl.protocol === 'file:') {
await context.validatePath(fileURLToPath(installUrl));
}
await context.installPWA({
manifestId,
installUrlOrBundleUrl,
displayMode,
});
response.appendResponseLine(
`Installed PWA with manifest ID: ${manifestId}`,
);
if (displayMode) {
response.appendResponseLine(`Display mode set to: ${displayMode}`);
}
},
});
export const uninstallPwa = defineTool({
name: 'uninstall_pwa',
description:
'Uninstalls a Progressive Web App identified by its manifest ID and ' +
'closes any open app windows.',
annotations: {
category: ToolCategory.PWA,
readOnlyHint: false,
},
schema: {
manifestId: manifestIdSchema,
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const {manifestId} = request.params;
await context.uninstallPWA({manifestId});
response.appendResponseLine(
`Uninstalled PWA with manifest ID: ${manifestId}`,
);
response.setIncludePages(true);
},
});
export const launchPwa = defineTool({
name: 'launch_pwa',
description:
'Launches an installed Progressive Web App using its saved display mode. ' +
'Optionally opens a specific URL within the same app instead of the default ' +
'start URL.',
annotations: {
category: ToolCategory.PWA,
readOnlyHint: false,
},
schema: {
manifestId: manifestIdSchema,
url: zod
.string()
.optional()
.describe(
'Optional URL within the app to open instead of the default start URL.',
),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const {manifestId, url} = request.params;
const page = await context.launchPWA({manifestId, url});
response.appendResponseLine(
`Launched PWA with manifest ID: ${manifestId} (${page.url()})`,
);
response.setIncludePages(true);
},
});
export const getOsAppState = defineTool({
name: 'get_os_app_state',
description:
'Returns the OS integration state (badge count and registered file ' +
'handlers) for an installed web app, identified by its manifest ID.',
annotations: {
category: ToolCategory.PWA,
readOnlyHint: true,
},
schema: {
manifestId: manifestIdSchema,
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const {manifestId} = request.params;
const state = await context.getPWAState({manifestId});
response.appendResponseLine(`OS app state for manifest ID: ${manifestId}`);
response.appendResponseLine(`Badge count: ${state.badgeCount}`);
response.appendResponseLine(
`File handlers: ${JSON.stringify(state.fileHandlers)}`,
);
},
});
+2
View File
@@ -15,6 +15,7 @@ import * as memoryTools from './memory.js';
import * as networkTools from './network.js';
import * as pagesTools from './pages.js';
import * as performanceTools from './performance.js';
import * as pwaTools from './pwa.js';
import * as screencastTools from './screencast.js';
import * as screenshotTools from './screenshot.js';
import * as scriptTools from './script.js';
@@ -37,6 +38,7 @@ export const createTools = (args: ParsedArguments) => {
...Object.values(networkTools),
...Object.values(pagesTools),
...Object.values(performanceTools),
...Object.values(pwaTools),
...Object.values(screencastTools),
...Object.values(screenshotTools),
...Object.values(scriptTools),
+329
View File
@@ -0,0 +1,329 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
import path from 'node:path';
import {afterEach, describe, it} from 'node:test';
import {pathToFileURL} from 'node:url';
import sinon from 'sinon';
import {
installPwa,
uninstallPwa,
launchPwa,
getOsAppState,
} from '../../src/tools/pwa.js';
import {serverHooks} from '../server.js';
import {getTextContent, withMcpContext} from '../utils.js';
// The `PWA` CDP domain is only available over a pipe connection, which
// `withMcpContext` uses by default.
const PWA_BROWSER_OPTIONS = {};
// Served verbatim: the `<link rel="manifest">` must live in the document head,
// so this cannot go through the `html` test helper (which re-wraps content in
// its own document body).
const INDEX_HTML = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="alternate manifest" href="manifest.webmanifest" />
<title>MCP Test PWA</title>
</head>
<body>
<h1>MCP Test PWA</h1>
</body>
</html>`;
const MANIFEST = JSON.stringify({
id: 'pwa/',
name: 'MCP Test PWA',
short_name: 'MCPPWA',
start_url: 'index.html',
scope: '/pwa/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#ffffff',
icons: [
{src: '/pptr.png', sizes: '192x192', type: 'image/png'},
{src: '/pptr.png', sizes: '512x512', type: 'image/png'},
],
});
describe('pwa', () => {
const server = serverHooks();
afterEach(() => {
sinon.restore();
});
function setupPwaRoutes(): {
manifestId: string;
startUrl: string;
explicitUrl: string;
} {
server.addRoute('/pwa/index.html', (_req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.statusCode = 200;
res.end(INDEX_HTML);
});
server.addRoute('/pwa/manifest.webmanifest', (_req, res) => {
res.setHeader('Content-Type', 'application/manifest+json');
res.statusCode = 200;
res.end(MANIFEST);
});
server.addHtmlRoute(
'/pwa/secondary.html',
'<h1>Explicit PWA launch URL</h1>',
);
return {
manifestId: `${server.baseUrl}/pwa/`,
startUrl: `${server.baseUrl}/pwa/index.html`,
explicitUrl: `${server.baseUrl}/pwa/secondary.html`,
};
}
it('installs a PWA, reads its OS state, and uninstalls it', async () => {
const {manifestId, startUrl} = setupPwaRoutes();
await withMcpContext(
async (response, context) => {
await installPwa.handler(
{params: {manifestId, installUrlOrBundleUrl: startUrl}},
response,
context,
);
assert.ok(
response.responseLines.some(l => l.includes(manifestId)),
'install response should reference the manifest ID',
);
response.resetResponseLineForTesting();
await getOsAppState.handler({params: {manifestId}}, response, context);
const stateOutput = response.responseLines.join('\n');
assert.ok(
stateOutput.includes('Badge count: 0'),
`expected a badge count in OS state, got: ${stateOutput}`,
);
assert.ok(
stateOutput.includes('File handlers: []'),
`expected file handlers in OS state, got: ${stateOutput}`,
);
const includePages = sinon.spy(response, 'setIncludePages');
await uninstallPwa.handler({params: {manifestId}}, response, context);
assert.ok(
includePages.calledOnce,
'uninstall should refresh the page list after closing app windows',
);
// Querying OS state after uninstall should reject.
await assert.rejects(
getOsAppState.handler({params: {manifestId}}, response, context),
);
},
PWA_BROWSER_OPTIONS,
{categoryPwa: true},
);
});
it('validates file bundle paths before installation', async () => {
await withMcpContext(
async (response, context) => {
const manifestId = 'isolated-app://example/';
const filePath = path.resolve('test-app.swbn');
const installUrlOrBundleUrl = pathToFileURL(filePath).href;
const validatePath = sinon.stub(context, 'validatePath').resolves();
const install = sinon.stub(context, 'installPWA').resolves(manifestId);
await installPwa.handler(
{params: {manifestId, installUrlOrBundleUrl}},
response,
context,
);
assert.ok(validatePath.calledOnceWithExactly(filePath));
assert.ok(install.calledOnce);
validatePath.reset();
validatePath.rejects(new Error('Path is outside configured roots'));
install.resetHistory();
await assert.rejects(
installPwa.handler(
{params: {manifestId, installUrlOrBundleUrl}},
response,
context,
),
/Path is outside configured roots/,
);
assert.ok(install.notCalled);
},
PWA_BROWSER_OPTIONS,
{categoryPwa: true},
);
});
it('does not require a selected page for browser-scoped operations', async () => {
const {manifestId, startUrl} = setupPwaRoutes();
await withMcpContext(
async (response, context) => {
const page = context.getSelectedMcpPage().pptrPage;
sinon
.stub(context, 'getSelectedMcpPage')
.throws(new Error('No page selected'));
const install = sinon.stub(context, 'installPWA').resolves(manifestId);
const launch = sinon.stub(context, 'launchPWA').resolves(page);
const getState = sinon
.stub(context, 'getPWAState')
.resolves({badgeCount: 0, fileHandlers: []});
const uninstall = sinon.stub(context, 'uninstallPWA').resolves();
await installPwa.handler(
{params: {manifestId, installUrlOrBundleUrl: startUrl}},
response,
context,
);
await launchPwa.handler({params: {manifestId}}, response, context);
await getOsAppState.handler({params: {manifestId}}, response, context);
await uninstallPwa.handler({params: {manifestId}}, response, context);
assert.ok(install.calledOnce);
assert.ok(launch.calledOnce);
assert.ok(getState.calledOnce);
assert.ok(uninstall.calledOnce);
},
PWA_BROWSER_OPTIONS,
{categoryPwa: true},
);
});
it('launches an installed PWA in a standalone window', async () => {
const {manifestId, startUrl} = setupPwaRoutes();
await withMcpContext(
async (response, context) => {
await installPwa.handler(
{
params: {
manifestId,
installUrlOrBundleUrl: startUrl,
displayMode: 'standalone',
},
},
response,
context,
);
response.resetResponseLineForTesting();
await launchPwa.handler({params: {manifestId}}, response, context);
assert.ok(
response.responseLines.some(l => l.includes(startUrl)),
`launch response should reference the app url, got: ${response.responseLines.join('\n')}`,
);
// The launched app window is a real page reachable via the browser,
// running in standalone display mode.
const appPage = context.browser
.targets()
.find(t => t.url() === startUrl);
assert.ok(appPage, 'the launched app page should be a browser target');
const page = await appPage.page();
assert.ok(page, 'the launched app target should resolve to a Page');
const isStandalone = await page.evaluate(() => {
return matchMedia('(display-mode: standalone)').matches;
});
assert.strictEqual(
isStandalone,
true,
'the launched app should be in standalone display mode',
);
await uninstallPwa.handler({params: {manifestId}}, response, context);
},
PWA_BROWSER_OPTIONS,
{categoryPwa: true},
);
});
it('launches an installed PWA at an explicit URL', async () => {
const {manifestId, startUrl, explicitUrl} = setupPwaRoutes();
await withMcpContext(
async (response, context) => {
await installPwa.handler(
{
params: {
manifestId,
installUrlOrBundleUrl: startUrl,
displayMode: 'standalone',
},
},
response,
context,
);
response.resetResponseLineForTesting();
await launchPwa.handler(
{params: {manifestId, url: explicitUrl}},
response,
context,
);
assert.ok(
response.responseLines.some(line => {
return line.includes(explicitUrl);
}),
`launch response should reference ${explicitUrl}`,
);
const appTarget = context.browser.targets().find(target => {
return target.url() === explicitUrl;
});
assert.ok(appTarget, 'the explicit launch target should exist');
await uninstallPwa.handler({params: {manifestId}}, response, context);
},
PWA_BROWSER_OPTIONS,
{categoryPwa: true},
);
});
it('selects a surviving page after uninstall closes the selected app', async () => {
const {manifestId, startUrl} = setupPwaRoutes();
await withMcpContext(
async (response, context) => {
const originalPage = context.getSelectedMcpPage();
await installPwa.handler(
{
params: {
manifestId,
installUrlOrBundleUrl: startUrl,
displayMode: 'standalone',
},
},
response,
context,
);
await launchPwa.handler({params: {manifestId}}, response, context);
await context.createPagesSnapshot();
const appPage = context.getPages().find(page => {
return page.pptrPage.url() === startUrl;
});
assert.ok(appPage, 'the launched app page should be listed');
context.selectPage(appPage);
response.resetResponseLineForTesting();
await uninstallPwa.handler({params: {manifestId}}, response, context);
const result = await response.handle(context);
assert.strictEqual(context.getSelectedMcpPage(), originalPage);
assert.match(
getTextContent(result.content[0]),
/previously selected page was closed/,
);
},
PWA_BROWSER_OPTIONS,
{categoryPwa: true},
);
});
});