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 docs/src/test-api/class-fullconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ See [`property: TestConfig.quiet`].

## property: FullConfig.reporter
* since: v1.10
- type: <[string]|[Array]<[Object]>|[BuiltInReporter]<"list"|"dot"|"line"|"github"|"json"|"junit"|"null"|"html">>
- type: <[string]|[Array]<[Object]>|[BuiltInReporter]<"list"|"dot"|"line"|"github"|"json"|"junit"|"null"|"html"|"chrome-trace">>
- `0` <[string]> Reporter name or module or file path
- `1` <[Object]> An object with reporter options if any

Expand Down
12 changes: 12 additions & 0 deletions docs/src/test-api/class-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -1844,6 +1844,12 @@ Whether to box the step in the report. Defaults to `false`. When the step is box

Specifies a custom location for the step to be shown in test reports and trace viewer. By default, location of the [`method: Test.step`] call is shown.

### option: Test.step.params
* since: v1.63
- `params` <[Object]<[string], [any]>>

Arbitrary serializable parameters describing the step. They are reported to the reporters as `testStep.params` and are shown in the trace viewer.

## async method: Test.step.skip
* since: v1.50
- returns: <[void]>
Expand Down Expand Up @@ -1891,6 +1897,12 @@ Whether to box the step in the report. Defaults to `false`. When the step is box

Specifies a custom location for the step to be shown in test reports and trace viewer. By default, location of the [`method: Test.step`] call is shown.

### option: Test.step.skip.params
* since: v1.63
- `params` <[Object]<[string], [any]>>

Arbitrary serializable parameters describing the step. They are reported to the reporters as `testStep.params` and are shown in the trace viewer.

### option: Test.step.skip.timeout
* since: v1.50
- `timeout` <[float]>
Expand Down
2 changes: 1 addition & 1 deletion docs/src/test-api/class-testconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ export default defineConfig({

## property: TestConfig.reporter
* since: v1.10
- type: ?<[string]|[Array]<[Object]>|[BuiltInReporter]<"list"|"dot"|"line"|"github"|"json"|"junit"|"null"|"html">>
- type: ?<[string]|[Array]<[Object]>|[BuiltInReporter]<"list"|"dot"|"line"|"github"|"json"|"junit"|"null"|"html"|"chrome-trace">>
- `0` <[string]> Reporter name or module or file path
- `1` <[Object]> An object with reporter options if any

Expand Down
28 changes: 28 additions & 0 deletions docs/src/test-reporter-api/class-teststep.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,34 @@ Error thrown during the step execution, if any.

Parent step, if any.

## property: TestStep.params
* since: v1.63
- type: ?<[Object]<[string], [any]>>

Step-dependent parameters, when available. For example, steps produced by the Playwright API calls contain the target
`locator` and the call arguments such as `url`, while [`method: Test.step`] steps contain the parameters passed by the
test author.

```js
// { locator: 'getByRole(\'button\')' }
await page.getByRole('button').click();

// { url: 'https://example.com' }
await page.goto('https://example.com');

// { locator: 'getByLabel(\'Password\')', value: 'secret' }
await page.getByLabel('Password').fill('secret');

// { orderId: 42 }
await test.step('checkout', async () => {
// ...
}, { params: { orderId: 42 } });
```

To keep the reports small, Playwright API calls only report a curated set of arguments per call, and long string values
are truncated. Unbounded arguments such as the page content, evaluated expressions or request bodies are never
reported.

## property: TestStep.startTime
* since: v1.10
- type: <[Date]>
Expand Down
27 changes: 27 additions & 0 deletions docs/src/test-reporters-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,33 @@ JUnit report supports following configuration options and environment variables:
| `PLAYWRIGHT_JUNIT_SUITE_ID` | | Value of the `id` attribute on the root `<testsuites/>` report entry. | Empty string.
| `PLAYWRIGHT_JUNIT_SUITE_NAME` | | Value of the `name` attribute on the root `<testsuites/>` report entry. | Empty string.

### Chrome tracing reporter

Chrome tracing reporter produces a [Trace Event Format](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview) json file that can be opened in [`chrome://tracing`](chrome://tracing) or in the [Perfetto UI](https://ui.perfetto.dev). It renders the test run as a timeline with a lane per worker, where every test is a slice containing its before/after hooks, fixtures and steps as nested slices. Every slice carries the details of the test or step in its trace event arguments, including source locations, [`property: TestStep.params`], tags, annotations, errors, stdio and paths to the attachment files.

```bash
npx playwright test --reporter=chrome-trace
```

By default the report is written to `test-results/chrome-trace.json`. When the output file name ends with `.gz`, the
report is gzipped on the fly, which both viewers accept and which is worth it for large test runs.

```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';

export default defineConfig({
reporter: [['chrome-trace', { outputFile: 'chrome-trace.json.gz' }]],
});
```

Chrome tracing report supports following configuration options and environment variables:

| Environment Variable Name | Reporter Config Option| Description | Default
|---|---|---|---|
| `PLAYWRIGHT_CHROME_TRACE_OUTPUT_DIR` | | Directory to save the output file. Ignored if output file is specified. | `test-results`
| `PLAYWRIGHT_CHROME_TRACE_OUTPUT_NAME` | | Base file name for the output, relative to the output dir. | `chrome-trace.json`
| `PLAYWRIGHT_CHROME_TRACE_OUTPUT_FILE` | `outputFile` | Full path to the output file. If defined, `PLAYWRIGHT_CHROME_TRACE_OUTPUT_DIR` and `PLAYWRIGHT_CHROME_TRACE_OUTPUT_NAME` will be ignored. | `undefined`

### GitHub Actions annotations

You can use the built in `github` reporter to get automatic failure annotations when running in GitHub actions.
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ export function toReporters(reporters: BuiltInReporter | ReporterDescription[] |
return reporters;
}

export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html', 'blob'] as const;
export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html', 'blob', 'chrome-trace'] as const;
export type BuiltInReporter = typeof builtInReporters[number];

export type ContextReuseMode = 'none' | 'when-possible';
Expand Down
1 change: 1 addition & 0 deletions packages/playwright/src/common/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export type StepBeginPayload = {
parentStepId: string | undefined;
title: string;
category: string;
params?: Record<string, any>;
wallTime: number; // milliseconds since unix epoch
location?: { file: string, line: number, column: number };
};
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright/src/common/testType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,12 +274,12 @@ export class TestTypeImpl {
suite._use.push({ fixtures, location });
}

async _step<T>(expectation: 'pass'|'skip', title: string, body: (step: TestStepInfo) => T | Promise<T>, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise<T> {
async _step<T>(expectation: 'pass'|'skip', title: string, body: (step: TestStepInfo) => T | Promise<T>, options: {box?: boolean, location?: Location, timeout?: number, params?: Record<string, any> } = {}): Promise<T> {
const testInfo = currentTestInfo();
if (!testInfo)
throw new Error(`test.step() can only be called from a test`);
await testInfo._onUserStepBegin?.(title);
const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box });
const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box, params: options.params });
return await currentZone().with('stepZone', step).run(async () => {
try {
let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;
Expand Down
126 changes: 119 additions & 7 deletions packages/playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,12 @@ const utilityFixtures: Fixtures<UtilityTestFixtures, UtilityWorkerFixtures> = {
}

// In the general case, create a step for each api call and connect them through the stepId.
const params = renderParams(channel.type, channel.method, channel.params);
const step = testInfo._addStep({
location: data.frames[0],
category: 'pw:api',
title: renderTitle(channel.type, channel.method, channel.params, data.title),
params: channel.params,
title: renderTitle(channel.type, channel.method, channel.params, data.title, params?.locator),
params,
group: getActionGroup({ type: channel.type, method: channel.method }),
}, tracingGroupSteps[tracingGroupSteps.length - 1]);
data.stepId = step.stepId;
Expand Down Expand Up @@ -919,12 +920,123 @@ function createTestOverlay(parts: string[], position: string, fontSize: number)
</div>`;
}

function renderTitle(type: string, method: string, params: Record<string, string> | undefined, title?: string) {
function renderTitle(type: string, method: string, params: Record<string, string> | undefined, title: string | undefined, locator: string | undefined) {
const prefix = renderTitleForCall({ title, type, method, params });
let selector;
if (params?.['selector'] && typeof params.selector === 'string')
selector = asLocatorDescription('javascript', params.selector);
return prefix + (selector ? ` ${selector}` : '');
return prefix + (locator ? ` ${locator}` : '');
}

const kMaxParamLength = 200;

// Curated per-call parameters, keyed the same way as the protocol metainfo. Only the
// arguments that say what the call actually did are reported, and only when they are
// bounded in size: page content, evaluated expressions, request bodies and the like are
// never reported, and neither are options that repeat their default on every call.
function renderCallParams(type: string, method: string, params: Record<string, any>): Record<string, any> | undefined {
switch (`${type}.${method}`) {
case 'APIRequestContext.fetch':
return { url: params.url, method: params.method };
case 'Frame.goto':
case 'Frame.addScriptTag':
case 'Frame.addStyleTag':
return { url: params.url };

case 'Frame.click':
case 'Frame.dblclick':
case 'ElementHandle.click':
case 'ElementHandle.dblclick':
return { button: params.button, clickCount: params.clickCount, modifiers: params.modifiers, position: params.position };
case 'Frame.hover':
case 'Frame.tap':
case 'ElementHandle.hover':
case 'ElementHandle.tap':
return { modifiers: params.modifiers, position: params.position };
case 'Frame.check':
case 'Frame.uncheck':
case 'ElementHandle.check':
case 'ElementHandle.uncheck':
return { position: params.position };
case 'Frame.dragAndDrop':
return { source: renderLocator(params.source), target: renderLocator(params.target) };
case 'Frame.fill':
case 'ElementHandle.fill':
return { value: params.value };
case 'Frame.press':
case 'ElementHandle.press':
case 'Page.keyboardDown':
case 'Page.keyboardUp':
case 'Page.keyboardPress':
return { key: params.key };
case 'Frame.type':
case 'ElementHandle.type':
case 'Page.keyboardType':
case 'Page.keyboardInsertText':
return { text: params.text };
case 'Frame.dispatchEvent':
case 'ElementHandle.dispatchEvent':
return { type: params.type };
case 'Frame.selectOption':
case 'ElementHandle.selectOption':
return { options: params.options };
case 'Frame.setInputFiles':
case 'ElementHandle.setInputFiles':
return { files: params.localPaths };

case 'Page.mouseClick':
return { x: params.x, y: params.y, button: params.button, clickCount: params.clickCount };
case 'Page.mouseMove':
case 'Page.touchscreenTap':
return { x: params.x, y: params.y };
case 'Page.mouseDown':
case 'Page.mouseUp':
return { button: params.button, clickCount: params.clickCount };
case 'Page.mouseWheel':
return { deltaX: params.deltaX, deltaY: params.deltaY };

case 'Frame.waitForSelector':
case 'ElementHandle.waitForSelector':
case 'ElementHandle.waitForElementState':
return { state: params.state };
case 'Frame.waitForTimeout':
return { timeout: params.waitTimeout };

case 'Page.emulateMedia':
return { media: params.media, colorScheme: params.colorScheme, reducedMotion: params.reducedMotion, forcedColors: params.forcedColors, contrast: params.contrast };
case 'Page.setViewportSize':
return params.viewportSize;
case 'Page.screenshot':
case 'ElementHandle.screenshot':
return { type: params.type, fullPage: params.fullPage };
case 'BrowserContext.setOffline':
return { offline: params.offline };
case 'Dialog.accept':
return { promptText: params.promptText };
case 'Tracing.tracingGroup':
return { name: params.name };
}
}

function renderParams(type: string, method: string, params: Record<string, any> | undefined): Record<string, any> | undefined {
if (!params)
return undefined;
const result: Record<string, any> = {};
const locator = renderLocator(params.selector);
if (locator !== undefined)
result.locator = locator;
for (const [name, value] of Object.entries(renderCallParams(type, method, params) ?? {})) {
if (value !== undefined)
result[name] = typeof value === 'string' ? truncateParam(value) : value;
}
return Object.keys(result).length ? result : undefined;
}

function renderLocator(selector: any): string | undefined {
if (typeof selector !== 'string')
return undefined;
return truncateParam(asLocatorDescription('javascript', selector));
}

function truncateParam(value: string): string {
return value.length > kMaxParamLength ? value.substring(0, kMaxParamLength) + '\u2026' : value;
}

function tracing() {
Expand Down
3 changes: 3 additions & 0 deletions packages/playwright/src/isomorphic/teleReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export type JsonTestStepStart = {
parentStepId?: string;
title: string;
category: string,
params?: Record<string, any>;
startTime: number;
location?: reporterTypes.Location;
};
Expand Down Expand Up @@ -724,6 +725,7 @@ export class TeleTestCase implements reporterTypes.TestCase {
class TeleTestStep implements reporterTypes.TestStep {
title: string;
category: string;
params: Record<string, any> | undefined;
location: reporterTypes.Location | undefined;
parent: reporterTypes.TestStep | undefined;
duration: number = -1;
Expand All @@ -738,6 +740,7 @@ class TeleTestStep implements reporterTypes.TestStep {
constructor(payload: JsonTestStepStart, parentStep: reporterTypes.TestStep | undefined, location: reporterTypes.Location | undefined, result: TeleTestResult) {
this.title = payload.title;
this.category = payload.category;
this.params = payload.params;
this.location = location;
this.parent = parentStep;
this._startTime = payload.startTime;
Expand Down
5 changes: 4 additions & 1 deletion packages/playwright/src/matchers/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,12 +336,15 @@ function callMatcherAsStep(matcherName: string, info: ExpectMetaInfo, actual: un
// This looks like it is unnecessary, but it isn't - we need to filter
// out all the frames that belong to the test runner from caught runtime errors.
const stackFrames = expectConfig().filteredStackTrace(captureRawStack());
const params: Record<string, any> = { ...suffixes.params };
if (args[0])
params.expected = args[0];
const stepData = {
category: 'expect' as const,
title: longTitle,
shortTitle,
location: stackFrames[0],
params: args[0] ? { expected: args[0] } : undefined,
params: Object.keys(params).length ? params : undefined,
};
const step = testInfo?._addStep(stepData);

Expand Down
5 changes: 3 additions & 2 deletions packages/playwright/src/matchers/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,14 +529,15 @@ export async function toPass(
return { pass: !this.isNot, message: () => '' };
}

export function computeMatcherTitleSuffix(matcherName: string, receiver: any, args: any[]): { short?: string, long?: string } {
export function computeMatcherTitleSuffix(matcherName: string, receiver: any, args: any[]): { short?: string, long?: string, params?: Record<string, any> } {
if (matcherName === 'toHaveScreenshot') {
const title = toHaveScreenshotStepTitle(...args);
return { short: title ? `(${title})` : '' };
}
if (receiver && typeof receiver === 'object' && (receiver as any)._apiName === 'Locator') {
try {
return { long: ' ' + asLocatorDescription('javascript', (receiver as LocatorEx)._selector) };
const locator = asLocatorDescription('javascript', (receiver as LocatorEx)._selector);
return { long: ' ' + locator, params: { locator } };
} catch {
}
}
Expand Down
Loading
Loading