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: 1 addition & 1 deletion .claude/skills/playwright-triage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ result to report, not a non-finding.
match. (A version ending in `-next`, e.g. `1.62.0-next`, is **not** an npm version — it means
tip-of-tree, which is the `@next` build you already tried.)

To step through a test interactively, use the [playwright-cli](../playwright-cli/SKILL.md) skill.
To step through a test interactively, use the [playwright-cli](../../../packages/playwright-core/src/tools/skills/playwright-cli/SKILL.md) skill.

Reports sometimes target another part of the Playwright project — `@playwright/mcp` (its source is
in this repo), `playwright-vscode`, `playwright-python`, `playwright-java`, `playwright-dotnet`.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/browsers.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ npx playwright --version

## Configure Browsers

Playwright can run tests on Chromium, WebKit and Firefox browsers as well as branded browsers such as Google Chrome and Microsoft Edge. It can also run on emulated tablet and mobile devices. See the [registry of device parameters](https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json) for a complete list of selected desktop, tablet and mobile devices.
Playwright can run tests on Chromium, WebKit and Firefox browsers as well as branded browsers such as Google Chrome and Microsoft Edge. It can also run on emulated tablet and mobile devices. See the [registry of device parameters](https://github.com/microsoft/playwright/blob/main/packages/isomorphic/deviceDescriptorsSource.json) for a complete list of selected desktop, tablet and mobile devices.

### Run tests on different browsers
* langs: js
Expand Down
2 changes: 1 addition & 1 deletion docs/src/emulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ With Playwright you can test your app on any browser as well as emulate a real d
## Devices
* langs: js, csharp, python

Playwright comes with a [registry of device parameters](https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json) using [`property: Playwright.devices`] for selected desktop, tablet and mobile devices. It can be used to simulate browser behavior for a specific device such as user agent, screen size, viewport and if it has touch enabled. All tests will run with the specified device parameters.
Playwright comes with a [registry of device parameters](https://github.com/microsoft/playwright/blob/main/packages/isomorphic/deviceDescriptorsSource.json) using [`property: Playwright.devices`] for selected desktop, tablet and mobile devices. It can be used to simulate browser behavior for a specific device such as user agent, screen size, viewport and if it has touch enabled. All tests will run with the specified device parameters.

```js tab=js-test title="playwright.config.ts"
import { defineConfig, devices } from '@playwright/test'; // import devices
Expand Down
2 changes: 1 addition & 1 deletion docs/src/getting-started-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ playwright-cli check <ref> # check a checkbox or radio button
playwright-cli uncheck <ref> # uncheck a checkbox
playwright-cli hover <ref> # hover over element
playwright-cli drag <startRef> <endRef> # drag and drop between elements
playwright-cli upload <file> # upload files
playwright-cli upload <files...> # upload one or multiple files
playwright-cli close # close the page
```

Expand Down
2 changes: 1 addition & 1 deletion docs/src/test-projects-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ By setting up projects you can also run a group of tests with different timeouts

## Configure projects for multiple browsers

By using **projects** you can run your tests in multiple browsers such as chromium, webkit and firefox as well as branded browsers such as Google Chrome and Microsoft Edge. Playwright can also run on emulated tablet and mobile devices. See the [registry of device parameters](https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json) for a complete list of selected desktop, tablet and mobile devices.
By using **projects** you can run your tests in multiple browsers such as chromium, webkit and firefox as well as branded browsers such as Google Chrome and Microsoft Edge. Playwright can also run on emulated tablet and mobile devices. See the [registry of device parameters](https://github.com/microsoft/playwright/blob/main/packages/isomorphic/deviceDescriptorsSource.json) for a complete list of selected desktop, tablet and mobile devices.

```js
import { defineConfig, devices } from '@playwright/test';
Expand Down
1 change: 1 addition & 0 deletions packages/isomorphic/trace/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type ContextEntry = {
hasSource: boolean;
contextId: string;
testTimeout?: number;
annotations?: trace.TraceEventAnnotation[];
};

export type PageEntry = {
Expand Down
2 changes: 2 additions & 0 deletions packages/isomorphic/trace/traceModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export class TraceModel {
readonly actionCounters: Map<string, number>;
readonly traceUri: string;
readonly testTimeout?: number;
readonly annotations?: trace.TraceEventAnnotation[];
readonly pagerefToTitle = new Map<string, string>();
readonly contextToTitle = new Map<ContextEntry, string>();

Expand All @@ -106,6 +107,7 @@ export class TraceModel {
this.title = libraryContext?.title || '';
this.options = libraryContext?.options || {};
this.testTimeout = contexts.find(c => c.origin === 'testRunner')?.testTimeout;
this.annotations = contexts.find(c => c.origin === 'testRunner')?.annotations;
// Next call updates all timestamps for all events in library contexts, so it must be done first.
this.actions = mergeActionsAndUpdateTiming(contexts);
this.pages = ([] as PageEntry[]).concat(...contexts.map(c => c.pages));
Expand Down
1 change: 1 addition & 0 deletions packages/isomorphic/trace/traceModernizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export class TraceModernizer {
contextEntry.testIdAttributeName = event.testIdAttributeName;
contextEntry.contextId = event.contextId ?? '';
contextEntry.testTimeout = event.testTimeout;
contextEntry.annotations = event.annotations;
break;
}
case 'screencast-frame': {
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-core/browsers.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
"browsers": [
{
"name": "chromium",
"revision": "1235",
"revision": "1236",
"installByDefault": true,
"browserVersion": "151.0.7922.47",
"title": "Chrome for Testing"
},
{
"name": "chromium-headless-shell",
"revision": "1235",
"revision": "1236",
"installByDefault": true,
"browserVersion": "151.0.7922.47",
"title": "Chrome Headless Shell"
Expand All @@ -24,7 +24,7 @@
},
{
"name": "webkit",
"revision": "2340",
"revision": "2341",
"installByDefault": true,
"revisionOverrides": {
"mac14": "2251",
Expand Down
5 changes: 4 additions & 1 deletion packages/playwright-core/src/server/bidi/bidiBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,11 @@ export class BidiBrowserContext extends BrowserContext {
async doGrantPermissions(origin: string, permissions: string[]) {
if (origin === 'null')
return;
const protocolPermissions = permissions.flatMap(
permission => permission === 'local-network-access' ? ['local-network', 'loopback-network'] : permission
);
const currentPermissions = this._originToPermissions.get(origin) || [];
const toGrant = permissions.filter(permission => !currentPermissions.includes(permission));
const toGrant = protocolPermissions.filter(permission => !currentPermissions.includes(permission));
this._originToPermissions.set(origin, [...currentPermissions, ...toGrant]);
if (origin === '*') {
await Promise.all(this._bidiPages().flatMap(page =>
Expand Down
21 changes: 17 additions & 4 deletions packages/playwright-core/src/server/webkit/protocol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3571,6 +3571,7 @@ might return multiple quads for inline nodes.
* Unique script identifier.
*/
export type ScriptId = string;
export type ScriptType = "program"|"module"|"webassembly";
/**
* Call frame identifier.
*/
Expand All @@ -3588,7 +3589,7 @@ might return multiple quads for inline nodes.
*/
lineNumber: number;
/**
* Column number in the script (0-based).
* Column number in the script (0-based) or bytecode offset for WebAssembly modules (0-based).
*/
columnNumber?: number;
}
Expand Down Expand Up @@ -3791,9 +3792,17 @@ might return multiple quads for inline nodes.
*/
endLine: number;
/**
* Length of the last line of the script.
* Length of the last line of the script or the end bytecode offset for WebAssembly modules.
*/
endColumn: number;
/**
* Identifier of the execution context in which this script was parsed.
*/
executionContextId: Runtime.ExecutionContextId;
/**
* Type of script.
*/
scriptType: ScriptType;
/**
* Determines whether this script is a user extension script.
*/
Expand All @@ -3807,9 +3816,13 @@ might return multiple quads for inline nodes.
*/
sourceMapURL?: string;
/**
* True if this script was parsed as a module.
* Human-readable name of the script.
*/
module?: boolean;
displayName?: string;
/**
* Identifier of the network request associated with this script (if any).
*/
requestId?: Network.RequestId;
}
/**
* Fired when virtual machine fails to parse the script.
Expand Down
6 changes: 4 additions & 2 deletions packages/playwright-core/src/tools/cli-client/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ async function runInSession(entry: SessionFile, clientInfo: ClientInfo, args: Mi
delete args[globalOption];
const session = new Session(entry);
const result = await session.run(clientInfo, args, { raw, json: output.json });
if (result.isError)
process.exitCode = 1;
return result.text;
}

Expand Down Expand Up @@ -445,9 +447,9 @@ function validateFlags(args: MinimistArgs, command: { flags: Record<string, 'boo
output.errorUnknownOption(unknownFlags, command.help);
}

function validateArgs(args: MinimistArgs, command: { args: string[], help: string }, output: Output) {
function validateArgs(args: MinimistArgs, command: { args: string[], variadicArg?: boolean, help: string }, output: Output) {
const positional = args._.slice(1);
if (positional.length > command.args.length)
if (positional.length > command.args.length && !command.variadicArg)
output.errorTooManyArguments(command.args.length, positional.length, command.help);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/tools/cli-client/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class Session {
return compareSemver(clientInfo.version, this.config.version) >= 0;
}

async run(clientInfo: ClientInfo, args: MinimistArgs, options?: { raw?: boolean, json?: boolean }): Promise<{ text: string }> {
async run(clientInfo: ClientInfo, args: MinimistArgs, options?: { raw?: boolean, json?: boolean }): Promise<{ text: string, isError?: boolean }> {
if (!this.isCompatible(clientInfo))
throw new Error(`Client is v${clientInfo.version}, session '${this.name}' is v${this.config.version}. Run\n\n playwright-cli${this.name !== 'default' ? ` -s=${this.name}` : ''} open\n\nto restart the browser session.`);

Expand Down
31 changes: 27 additions & 4 deletions packages/playwright-core/src/tools/cli-daemon/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ export function declareCommand<Args extends zodType.ZodTypeAny, Options extends
const kEmptyOptions = z.object({});
const kEmptyArgs = z.object({});

// The last positional argument absorbs the remaining argv when its schema accepts an array.
export function isVariadicArg(schema: zodType.ZodTypeAny): boolean {
if (schema instanceof z.ZodArray)
return true;
if (schema instanceof z.ZodUnion)
return schema.options.some(option => isVariadicArg(option as zodType.ZodTypeAny));
if (schema instanceof z.ZodOptional)
return isVariadicArg(schema.unwrap() as zodType.ZodTypeAny);
if (schema instanceof z.ZodPipe)
return isVariadicArg(schema.in as zodType.ZodTypeAny);
return false;
}

export function parseCommand(command: AnyCommandSchema, args: Record<string, string> & { _: string[] }): { toolName: string, toolParams: any } {
const optionsObject = { ...args } as Record<string, string>;
delete optionsObject['_'];
Expand All @@ -49,11 +62,17 @@ export function parseCommand(command: AnyCommandSchema, args: Record<string, str
const argsSchema = (command.args ?? kEmptyArgs).strict();
const argNames = [...Object.keys(argsSchema.shape)];
const argv = args['_'].slice(1);
if (argv.length > argNames.length)
const variadic = argNames.length > 0 && isVariadicArg(argsSchema.shape[argNames[argNames.length - 1]]);
if (argv.length > argNames.length && !variadic)
throw new Error(`error: too many arguments: expected ${argNames.length}, received ${argv.length}`);
const argsObject: Record<string, string> = {};
argNames.forEach((name, index) => argsObject[name] = argv[index]);
const parsedArgsObject: Record<string, string> = zodParse(argsSchema, argsObject, 'argument');
const argsObject: Record<string, string | string[] | undefined> = {};
argNames.forEach((name, index) => {
if (variadic && index === argNames.length - 1)
argsObject[name] = index < argv.length ? argv.slice(index) : undefined;
else
argsObject[name] = argv[index];
});
const parsedArgsObject: Record<string, string | string[]> = zodParse(argsSchema, argsObject, 'argument');

const toolName = typeof command.toolName === 'function' ? command.toolName({ ...parsedArgsObject, ...options }) : command.toolName;
const toolParams = command.toolParams({ ...parsedArgsObject, ...options });
Expand All @@ -72,6 +91,10 @@ function zodParse(schema: zodType.ZodAny, data: unknown, type: 'option' | 'argum
switch (issue.code) {
case 'invalid_type':
return 'error: ' + label + ': ' + issue.message.replace(/Invalid input:/, '').trim();
case 'invalid_union': {
const message = issue.errors[0]?.[0]?.message ?? issue.message;
return 'error: ' + label + ': ' + message.replace(/Invalid input:/, '').trim();
}
case 'unrecognized_keys':
return 'error: unknown ' + label;
default:
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/tools/cli-daemon/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,10 +343,10 @@ const fileUpload = declareCommand({
description: 'Upload one or multiple files',
category: 'core',
args: z.object({
file: z.string().describe('The absolute paths to the files to upload'),
files: stringArrayArg.describe('The absolute paths to the files to upload'),
}),
toolName: 'browser_file_upload',
toolParams: ({ file }) => ({ paths: [file] }),
toolParams: ({ files }) => ({ paths: files }),
});

const check = declareCommand({
Expand Down
24 changes: 17 additions & 7 deletions packages/playwright-core/src/tools/cli-daemon/helpGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,33 @@

import * as z from 'zod';
import { commands } from './commands';
import { isVariadicArg } from './command';

import type zodType from 'zod';
import type { AnyCommandSchema, Category } from './command';

type CommandArg = { name: string, description: string, optional: boolean };
type CommandArg = { name: string, description: string, optional: boolean, variadic: boolean };

function commandArgs(command: AnyCommandSchema): CommandArg[] {
const args: CommandArg[] = [];
const shape = command.args ? (command.args as zodType.ZodObject<any>).shape : {};
const names = Object.keys(shape);
for (const [name, schema] of Object.entries(shape)) {
const zodSchema = schema as zodType.ZodTypeAny;
const description = zodSchema.description ?? '';
args.push({ name, description, optional: zodSchema.safeParse(undefined).success });
const variadic = name === names[names.length - 1] && isVariadicArg(zodSchema);
args.push({ name, description, optional: zodSchema.safeParse(undefined).success, variadic });
}
return args;
}

function commandArgText(a: CommandArg) {
const name = a.variadic ? `${a.name}...` : a.name;
return a.optional ? `[${name}]` : `<${name}>`;
}

function commandArgsText(args: CommandArg[]) {
return args.map(a => a.optional ? `[${a.name}]` : `<${a.name}>`).join(' ');
return args.map(commandArgText).join(' ');
}

function generateCommandHelp(command: AnyCommandSchema) {
Expand All @@ -48,7 +56,7 @@ function generateCommandHelp(command: AnyCommandSchema) {

if (args.length) {
lines.push('Arguments:');
lines.push(...args.map(a => formatWithGap(` ${a.optional ? `[${a.name}]` : `<${a.name}>`}`, a.description.toLowerCase())));
lines.push(...args.map(a => formatWithGap(` ${commandArgText(a)}`, a.description.toLowerCase())));
}

if (command.options) {
Expand Down Expand Up @@ -165,7 +173,7 @@ function isBooleanSchema(schema: zodType.ZodTypeAny): boolean {
export function generateHelpJSON() {
const booleanOptions = new Set<string>();

const commandEntries: Record<string, { help: string, flags: Record<string, 'boolean' | 'string'>, args: string[], raw?: boolean }> = {};
const commandEntries: Record<string, { help: string, flags: Record<string, 'boolean' | 'string'>, args: string[], variadicArg?: boolean, raw?: boolean }> = {};
for (const [name, command] of Object.entries(commands)) {
const flags: Record<string, 'boolean' | 'string'> = {};
if (command.options) {
Expand All @@ -177,8 +185,10 @@ export function generateHelpJSON() {
booleanOptions.add(flagName);
}
}
const args: string[] = command.args ? Object.keys((command.args as zodType.ZodObject<any>).shape) : [];
commandEntries[name] = { help: generateCommandHelp(command), flags, args };
const args = commandArgs(command);
commandEntries[name] = { help: generateCommandHelp(command), flags, args: args.map(a => a.name) };
if (args.some(a => a.variadic))
commandEntries[name].variadicArg = true;
if (command.raw)
commandEntries[name].raw = true;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export { compareSemver } from './utils/socketConnection';
export { extractTrace, DirTraceLoaderBackend } from './trace/traceParser';
export { decorateMCPCommand } from './mcp/program';
export { program as cliProgram } from './cli-client/program';
export { generateHelp, generateHelpJSON } from './cli-daemon/helpGenerator';
export { generateHelp, generateHelpJSON, generateReadme } from './cli-daemon/helpGenerator';
export { decorateProgram as decorateCliDaemonProgram, initWorkspace } from './cli-daemon/program';
export { allSkills, installSkills } from './utils/installSkills';
export { openDashboardApp, openDashboardForContext } from './dashboard/dashboardApp';
Expand Down
12 changes: 10 additions & 2 deletions packages/playwright/src/runner/lastRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import fs from 'fs';
import path from 'path';

import type { FullResult, Suite } from '../../types/testReporter';
import type { FullResult, Suite, TestCase } from '../../types/testReporter';
import type { config as commonConfig } from '../common';
import type { ReporterV2 } from '../reporters/reporterV2';

Expand All @@ -26,6 +26,14 @@ type LastRunInfo = {
failedTests: string[];
};

function didNotRun(test: TestCase): boolean {
if (test.outcome() !== 'skipped')
return false;
if (test.results.some(result => result.status === 'interrupted'))
return false;
return !test.results.length || test.expectedStatus !== 'skipped';
}

export class LastRunReporter implements ReporterV2 {
private _lastRunFile: string | undefined;
private _suite: Suite | undefined;
Expand Down Expand Up @@ -71,7 +79,7 @@ export class LastRunReporter implements ReporterV2 {
return;
const lastRunInfo: LastRunInfo = {
status: result.status,
failedTests: this._suite?.allTests().filter(t => !t.ok()).map(t => t.id) || [],
failedTests: this._suite?.allTests().filter(t => !t.ok() || didNotRun(t)).map(t => t.id) || [],
};
await fs.promises.mkdir(path.dirname(this._lastRunFile), { recursive: true });
await fs.promises.writeFile(this._lastRunFile, JSON.stringify(lastRunInfo, undefined, 2));
Expand Down
3 changes: 2 additions & 1 deletion packages/playwright/src/worker/testTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export class TestTracing {

async stopIfNeeded() {
this._contextCreatedEvent.testTimeout = this._testInfo.timeout;
this._contextCreatedEvent.annotations = this._testInfo.annotations.map(({ type, description }) => ({ type, description }));

if (!this._options)
return;
Expand Down Expand Up @@ -290,7 +291,7 @@ export class TestTracing {
});
}

appendAfterActionForStep(callId: string, error?: trace.SerializedError['error'], attachments: Attachment[] = [], annotations?: trace.AfterActionTraceEventAnnotation[]) {
appendAfterActionForStep(callId: string, error?: trace.SerializedError['error'], attachments: Attachment[] = [], annotations?: trace.TraceEventAnnotation[]) {
this._appendTraceEvent({
type: 'after',
callId,
Expand Down
2 changes: 1 addition & 1 deletion packages/trace-viewer/src/ui/uiModeTraceView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const TraceView: React.FC<{
fallbackLocation={item.testFile}
isLive={model?.isLive}
status={item.treeItem?.status}
annotations={item.testCase?.annotations ?? []}
defaultAnnotations={item.testCase?.annotations ?? []}
onOpenExternally={onOpenExternally}
revealSource={revealSource}
/>;
Expand Down
Loading
Loading