Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/isomorphic/stringUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export function toSnakeCase(name: string): string {
}

export function formatObject(value: any, indent = ' ', mode: 'multiline' | 'oneline' = 'multiline'): string {
if (value === null)
return 'null';
if (typeof value === 'string')
return escapeWithQuotes(value, '\'');
if (Array.isArray(value))
Expand Down
50 changes: 50 additions & 0 deletions packages/playwright-core/src/tools/backend/emulation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as z from 'zod';
import { formatObject } from '@isomorphic/stringUtils';

import { defineTabTool } from './tool';

const emulateMedia = defineTabTool({
capability: 'core',
schema: {
name: 'browser_emulate_media',
title: 'Emulate media features',
description: 'Emulate CSS media features for the page, for example switch between the light and dark color scheme. Omitted parameters are left unchanged; null clears an override.',
inputSchema: z.object({
colorScheme: z.enum(['light', 'dark']).nullable().optional().describe('Emulates the prefers-color-scheme media feature'),
reducedMotion: z.enum(['reduce', 'no-preference']).nullable().optional().describe('Emulates the prefers-reduced-motion media feature'),
forcedColors: z.enum(['active', 'none']).nullable().optional().describe('Emulates the forced-colors media feature'),
contrast: z.enum(['more', 'no-preference']).nullable().optional().describe('Emulates the prefers-contrast media feature'),
media: z.enum(['screen', 'print']).nullable().optional().describe('Changes the CSS media type of the page'),
}),
type: 'action',
},

handle: async (tab, params, response) => {
if (Object.values(params).every(value => value === undefined)) {
response.addError('Error: Specify at least one media feature to emulate.');
return;
}
response.addCode(`await page.emulateMedia(${formatObject(params, ' ', 'oneline')});`);
await tab.page.emulateMedia(params);
},
});

export default [
emulateMedia,
];
2 changes: 2 additions & 0 deletions packages/playwright-core/src/tools/backend/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import console from './console';
import cookies from './cookies';
import devtools from './devtools';
import dialogs from './dialogs';
import emulation from './emulation';
import evaluate from './evaluate';
import files from './files';
import find from './find';
Expand Down Expand Up @@ -54,6 +55,7 @@ export const browserTools: Tool<any>[] = [
...cookies,
...devtools,
...dialogs,
...emulation,
...evaluate,
...files,
...find,
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/tools/cli-daemon/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import * as z from 'zod';
import type zodType from 'zod';

export type Category = 'core' | 'navigation' | 'keyboard' | 'mouse' | 'export' | 'storage' | 'tabs' | 'network' | 'devtools' | 'browsers' | 'config' | 'install' | 'webmcp';
export type Category = 'core' | 'navigation' | 'keyboard' | 'mouse' | 'export' | 'storage' | 'emulation' | 'tabs' | 'network' | 'devtools' | 'browsers' | 'config' | 'install' | 'webmcp';

export type CommandSchema<Args extends zodType.ZodTypeAny, Options extends zodType.ZodTypeAny> = {
name: string;
Expand Down
112 changes: 112 additions & 0 deletions packages/playwright-core/src/tools/cli-daemon/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,106 @@ const resize = declareCommand({
toolParams: ({ w: width, h: height }) => ({ width, height }),
});

const setColorScheme = declareCommand({
name: 'set-color-scheme',
description: 'Emulate the light or dark color scheme',
category: 'emulation',
args: z.object({
scheme: z.enum(['light', 'dark']).describe('Color scheme to emulate'),
}),
toolName: 'browser_emulate_media',
toolParams: ({ scheme: colorScheme }) => ({ colorScheme }),
});

const setReducedMotion = declareCommand({
name: 'set-reduced-motion',
description: 'Emulate the reduced motion preference',
category: 'emulation',
args: z.object({
motion: z.enum(['reduce', 'no-preference']).describe('Reduced motion preference to emulate'),
}),
toolName: 'browser_emulate_media',
toolParams: ({ motion: reducedMotion }) => ({ reducedMotion }),
});

const setForcedColors = declareCommand({
name: 'set-forced-colors',
description: 'Emulate forced colors mode',
category: 'emulation',
args: z.object({
colors: z.enum(['active', 'none']).describe('Forced colors mode to emulate'),
}),
toolName: 'browser_emulate_media',
toolParams: ({ colors: forcedColors }) => ({ forcedColors }),
});

const setContrast = declareCommand({
name: 'set-contrast',
description: 'Emulate the preferred contrast',
category: 'emulation',
args: z.object({
contrast: z.enum(['more', 'no-preference']).describe('Contrast preference to emulate'),
}),
toolName: 'browser_emulate_media',
toolParams: ({ contrast }) => ({ contrast }),
});

const setMedia = declareCommand({
name: 'set-media',
description: 'Emulate the CSS media type',
category: 'emulation',
args: z.object({
media: z.enum(['screen', 'print']).describe('CSS media type to emulate'),
}),
toolName: 'browser_emulate_media',
toolParams: ({ media }) => ({ media }),
});

const clearColorScheme = declareCommand({
name: 'clear-color-scheme',
description: 'Clear color scheme emulation',
category: 'emulation',
args: z.object({}),
toolName: 'browser_emulate_media',
toolParams: () => ({ colorScheme: null }),
});

const clearReducedMotion = declareCommand({
name: 'clear-reduced-motion',
description: 'Clear reduced motion emulation',
category: 'emulation',
args: z.object({}),
toolName: 'browser_emulate_media',
toolParams: () => ({ reducedMotion: null }),
});

const clearForcedColors = declareCommand({
name: 'clear-forced-colors',
description: 'Clear forced colors emulation',
category: 'emulation',
args: z.object({}),
toolName: 'browser_emulate_media',
toolParams: () => ({ forcedColors: null }),
});

const clearContrast = declareCommand({
name: 'clear-contrast',
description: 'Clear preferred contrast emulation',
category: 'emulation',
args: z.object({}),
toolName: 'browser_emulate_media',
toolParams: () => ({ contrast: null }),
});

const clearMedia = declareCommand({
name: 'clear-media',
description: 'Clear CSS media type emulation',
category: 'emulation',
args: z.object({}),
toolName: 'browser_emulate_media',
toolParams: () => ({ media: null }),
});

const runCode = declareCommand({
name: 'run-code',
description: 'Run Playwright code snippet',
Expand Down Expand Up @@ -1277,6 +1377,18 @@ const commandsArray: AnyCommandSchema[] = [
sessionStorageDelete,
sessionStorageClear,

// emulation category
setColorScheme,
setReducedMotion,
setForcedColors,
setContrast,
setMedia,
clearColorScheme,
clearReducedMotion,
clearForcedColors,
clearContrast,
clearMedia,

// network category
networkRequests,
networkRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const categories: { name: Category, title: string }[] = [
{ name: 'export', title: 'Save as' },
{ name: 'tabs', title: 'Tabs' },
{ name: 'storage', title: 'Storage' },
{ name: 'emulation', title: 'Emulation' },
{ name: 'network', title: 'Network' },
{ name: 'devtools', title: 'DevTools' },
{ name: 'webmcp', title: 'WebMCP' },
Expand Down
15 changes: 15 additions & 0 deletions packages/playwright-core/src/tools/skills/playwright-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,21 @@ playwright-cli sessionstorage-delete step
playwright-cli sessionstorage-clear
```

### Emulation

```bash
playwright-cli set-color-scheme dark
playwright-cli clear-color-scheme
playwright-cli set-reduced-motion reduce
playwright-cli clear-reduced-motion
playwright-cli set-forced-colors active
playwright-cli clear-forced-colors
playwright-cli set-contrast more
playwright-cli clear-contrast
playwright-cli set-media print
playwright-cli clear-media
```

### Network

```bash
Expand Down
1 change: 1 addition & 0 deletions tests/mcp/capabilities.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ test('test snapshot tool list', async ({ client }) => {
'browser_select_option',
'browser_type',
'browser_close',
'browser_emulate_media',
'browser_navigate_back',
'browser_navigate',
'browser_network_request',
Expand Down
51 changes: 51 additions & 0 deletions tests/mcp/cli-emulation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { test, expect } from './cli-fixtures';

test('set and clear media features', async ({ cli, server }) => {
await cli('open', server.PREFIX);

const expectMatches = async (query: string) => {
const { output } = await cli('eval', `() => matchMedia('${query}').matches`);
expect(output).toContain('### Result\ntrue');
};

const expectClear = async (command: string, option: string) => {
const { output } = await cli(command);
expect(output).toContain(`await page.emulateMedia({ ${option}: null });`);
};

await cli('set-color-scheme', 'dark');
await expectMatches('(prefers-color-scheme: dark)');
await expectClear('clear-color-scheme', 'colorScheme');

await cli('set-reduced-motion', 'reduce');
await expectMatches('(prefers-reduced-motion: reduce)');
await expectClear('clear-reduced-motion', 'reducedMotion');

await cli('set-forced-colors', 'active');
await expectMatches('(forced-colors: active)');
await expectClear('clear-forced-colors', 'forcedColors');

await cli('set-contrast', 'more');
await expectMatches('(prefers-contrast: more)');
await expectClear('clear-contrast', 'contrast');

await cli('set-media', 'print');
await expectMatches('print');
await expectClear('clear-media', 'media');
});
17 changes: 17 additions & 0 deletions tests/mcp/cli-help.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@ test('prints help', async ({ cli }) => {
expect(output).toContain('Usage: playwright-cli <command>');
});

test('prints emulation help after storage', async ({ cli }) => {
const { output } = await cli('--help');
const headings = output.split('\n').filter(line => /^[A-Z].*:$/.test(line));
expect(headings.indexOf('Emulation:')).toBe(headings.indexOf('Storage:') + 1);
const emulationHelp = output.slice(output.indexOf('\nEmulation:'), output.indexOf('\nNetwork:'));
expect(emulationHelp).toContain('set-color-scheme');
expect(emulationHelp).toContain('clear-color-scheme');
expect(emulationHelp).toContain('set-reduced-motion');
expect(emulationHelp).toContain('clear-reduced-motion');
expect(emulationHelp).toContain('set-forced-colors');
expect(emulationHelp).toContain('clear-forced-colors');
expect(emulationHelp).toContain('set-contrast');
expect(emulationHelp).toContain('clear-contrast');
expect(emulationHelp).toContain('set-media');
expect(emulationHelp).toContain('clear-media');
});

test('prints help by default', async ({ cli }) => {
const { output } = await cli();
expect(output).toContain('Usage: playwright-cli <command>');
Expand Down
17 changes: 17 additions & 0 deletions tests/mcp/config-resolve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,23 @@ test.describe('viewport', () => {
});
});

test('config file preserves context media preferences', async ({}, testInfo) => {
const configFile = testInfo.outputPath('config.json');
const fileConfig: Config = {
browser: {
contextOptions: {
colorScheme: 'dark',
contrast: 'more',
forcedColors: 'active',
reducedMotion: 'reduce',
},
},
};
await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv);
expect(config.browser.contextOptions).toMatchObject(fileConfig.browser!.contextOptions!);
});

test.describe('mobile', () => {
test('--mobile defaults to a Chromium mobile device', async () => {
const config = await resolveCLIConfigForMCP({ mobile: true }, emptyEnv);
Expand Down
Loading
Loading