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
22 changes: 0 additions & 22 deletions docs/src/api/params.md
Original file line number Diff line number Diff line change
Expand Up @@ -1973,27 +1973,6 @@ In this config:
1. Since `snapshotPathTemplate` resolves to relative path, it will be resolved relative to `configDir`.
1. Forward slashes `"/"` can be used as path separators on any platform.

## test-config-web-server-options
* langs: js
- type: ?<[Object]|[Array]<[Object]>>
- `command` <[string]> Shell command to start. For example `npm run start`..
- `cwd` ?<[string]> Current working directory of the spawned process, defaults to the directory of the configuration file.
- `env` ?<[Object]<[string], [string]>> Environment variables to set for the command, `process.env` by default.
- `gracefulShutdown` ?<[Object]> How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGTERM', timeout: 500 }`, the process group is sent a `SIGTERM` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGINT` as the signal instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGTERM` and `SIGINT` signals, so this option is ignored on Windows. Note that shutting down a Docker container requires `SIGTERM`.
- `signal` <["SIGINT"|"SIGTERM"]>
- `timeout` <[int]>
- `ignoreHTTPSErrors` ?<[boolean]> Whether to ignore HTTPS errors when fetching the `url`. Defaults to `false`.
- `name` ?<[string]> Specifies a custom name for the web server. This name will be prefixed to log messages. Defaults to `[WebServer]`.
- `port` ?<[int]> The port that your http server is expected to appear on. It does wait until it accepts connections. Either `port` or `url` should be specified.
- `reuseExistingServer` ?<[boolean]> If true, it will re-use an existing server on the `port` or `url` when available. If no server is running on that `port` or `url`, it will run the command to start a new server. If `false`, it will throw if an existing process is listening on the `port` or `url`. This should be commonly set to `!process.env.CI` to allow the local dev server when running tests locally.
- `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`.
- `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`.
- `wait` ?<[Object]> Consider command started only when given output has been produced.
- `stdout` ?<[RegExp]> Regular expression to wait for in the `stdout` of the command output. Named capture groups are stored in the environment, for example `/Listening on port (?<my_server_port>\d+)/` will store the port number in `process.env['MY_SERVER_PORT']`.
- `stderr` ?<[RegExp]> Regular expression to wait for in the `stderr` of the command output. Named capture groups are stored in the environment, for example `/Listening on port (?<my_server_port>\d+)/` will store the port number in `process.env['MY_SERVER_PORT']`.
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
- `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified.

## response-security-details
- returns: <[null]|[Object]>
* alias: SecurityDetails
Expand All @@ -2014,4 +1993,3 @@ In this config:
* alias-java: ServerAddr
- `ipAddress` <[string]> IPv4 or IPV6 address of the server.
- `port` <[int]>

20 changes: 19 additions & 1 deletion docs/src/test-api/class-testconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -690,8 +690,26 @@ export default defineConfig({
});
```

## property: TestConfig.webServer = %%-test-config-web-server-options-%%
## property: TestConfig.webServer
* since: v1.10
- type: ?<[Object]|[Array]<[Object]>>
- `command` <[string]> Shell command to start. For example `npm run start`..
- `cwd` ?<[string]> Current working directory of the spawned process, defaults to the directory of the configuration file.
- `env` ?<[Object]<[string], [string]>> Environment variables to set for the command, `process.env` by default.
- `gracefulShutdown` ?<[Object]> How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGTERM', timeout: 500 }`, the process group is sent a `SIGTERM` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGINT` as the signal instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGTERM` and `SIGINT` signals, so this option is ignored on Windows. Note that shutting down a Docker container requires `SIGTERM`.
- `signal` <["SIGINT"|"SIGTERM"]>
- `timeout` <[int]>
- `ignoreHTTPSErrors` ?<[boolean]> Whether to ignore HTTPS errors when fetching the `url`. Defaults to `false`.
- `name` ?<[string]> Specifies a custom name for the web server. This name will be prefixed to log messages. Defaults to `[WebServer]`.
- `port` ?<[int]> The port that your http server is expected to appear on. It does wait until it accepts connections. Either `port` or `url` should be specified.
- `reuseExistingServer` ?<[boolean]> If true, it will re-use an existing server on the `port` or `url` when available. If no server is running on that `port` or `url`, it will run the command to start a new server. If `false`, it will throw if an existing process is listening on the `port` or `url`. This should be commonly set to `!process.env.CI` to allow the local dev server when running tests locally.
- `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`.
- `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`.
- `wait` ?<[Object]> Consider command started only when given output has been produced.
- `stdout` ?<[RegExp]> Regular expression to wait for in the `stdout` of the command output. Named capture groups are stored in the environment, for example `/Listening on port (?<my_server_port>\d+)/` will store the port number in `process.env['MY_SERVER_PORT']`.
- `stderr` ?<[RegExp]> Regular expression to wait for in the `stderr` of the command output. Named capture groups are stored in the environment, for example `/Listening on port (?<my_server_port>\d+)/` will store the port number in `process.env['MY_SERVER_PORT']`.
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
- `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified.

Launch a development web server (or multiple) during the tests.

Expand Down
42 changes: 0 additions & 42 deletions docs/src/test-api/class-testproject.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,48 +395,6 @@ export default defineConfig({

Use [`property: TestConfig.use`] to change this option for all projects.

## property: TestProject.webServer = %%-test-config-web-server-options-%%
* since: v1.61

Launch a development web server (or multiple) before running tests in this project. See [`property: TestConfig.webServer`] for the shape of each entry.

A per-project `webServer` is only launched when the project is selected (either directly via `--project` or indirectly through dependencies). This is useful when only a subset of your projects need a local backend, while others run against a deployed environment.

Per-project web servers are launched in addition to any top-level [`property: TestConfig.webServer`].

**Usage**

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

export default defineConfig({
projects: [
{
name: 'functional',
grepInvert: /@smoke/,
use: { baseURL: 'http://localhost:3000' },
webServer: [
{
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
{
command: 'npm run mock-server',
port: 3001,
reuseExistingServer: !process.env.CI,
},
],
},
{
name: 'smoke',
grep: /@smoke/,
use: { baseURL: 'https://production.app.com' },
},
],
});
```

## property: TestProject.workers
* since: v1.52
- type: ?<[int]|[string]>
Expand Down
3 changes: 1 addition & 2 deletions packages/injected/src/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ import type { Builtins } from './utilityScript';

export type FrameExpectParams = Omit<channels.FrameExpectParams, 'expectedValue' | 'timeout'> & {
expectedValue?: any;
noAutoWaiting?: boolean;
};

export type ElementState = 'visible' | 'hidden' | 'enabled' | 'disabled' | 'editable' | 'checked' | 'unchecked' | 'indeterminate' | 'stable';
Expand Down Expand Up @@ -1447,7 +1446,7 @@ export class InjectedScript {

async expect(element: Element | undefined, options: FrameExpectParams, elements: Element[]): Promise<{ matches: boolean, received?: ExpectReceived, missingReceived?: boolean }> {
const core = await this._expectCore(element, options, elements);
const ariaSnapshot = this._ariaSnapshotForExpect(element, options);
const ariaSnapshot = core.matches !== options.isNot ? undefined : this._ariaSnapshotForExpect(element, options);
if (core.received === undefined && ariaSnapshot === undefined)
return { matches: core.matches, missingReceived: core.missingReceived };
return { matches: core.matches, received: { value: core.received, ariaSnapshot }, missingReceived: core.missingReceived };
Expand Down
8 changes: 6 additions & 2 deletions packages/playwright-core/src/client/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { Stream } from './stream';
import { Tracing } from './tracing';
import { Worker } from './worker';
import { WritableStream } from './writableStream';
import { ValidationError, findValidator } from '../protocol/validator';
import { ValidationError, findValidator, maybeFindValidator } from '../protocol/validator';
import type { ClientInstrumentation } from './clientInstrumentation';
import type { HeadersArray } from './types';
import type { ValidatorContext } from '../protocol/validator';
Expand Down Expand Up @@ -214,7 +214,7 @@ export class Connection extends EventEmitter {
if (this._closedError)
return;

const { id, guid, method, params, result, error, log } = message as any;
const { id, guid, method, params, result, error, errorDetails, log } = message as any;
if (id) {
if (this._platform.isLogEnabled('channel'))
this._platform.log('channel', '<RECV ' + JSON.stringify(message));
Expand All @@ -224,7 +224,11 @@ export class Connection extends EventEmitter {
this._callbacks.delete(id);
if (error && !result) {
const parsedError = parseError(error);
parsedError.log = log || [];
rewriteErrorMessage(parsedError, parsedError.message + formatCallLog(this._platform, log));
const detailsValidator = maybeFindValidator(callback.type, callback.method, 'ErrorDetails');
if (detailsValidator)
parsedError.details = detailsValidator(errorDetails ?? {}, '', this._validatorFromWireContext());
callback.reject(parsedError);
} else {
const validator = findValidator(callback.type, callback.method, 'Result');
Expand Down
30 changes: 15 additions & 15 deletions packages/playwright-core/src/client/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@ import { parseSerializedValue, serializeValue } from '../protocol/serializers';

import type { SerializedError } from '@protocol/channels';

export class TimeoutError extends Error {
export class PlaywrightError extends Error {
log: string[] = [];
details?: any; // As declared in the protocol.
}

export class TimeoutError extends PlaywrightError {
constructor(message: string) {
super(message);
this.name = 'TimeoutError';
}
}

export class TargetClosedError extends Error {
export class TargetClosedError extends PlaywrightError {
constructor(cause?: string) {
super(cause || 'Target page, context or browser has been closed');
}
Expand All @@ -42,24 +47,19 @@ export function serializeError(e: any): SerializedError {
return { value: serializeValue(e, value => ({ fallThrough: value })) };
}

export function parseError(error: SerializedError): Error {
export function parseError(error: SerializedError): PlaywrightError {
if (!error.error) {
if (error.value === undefined)
throw new Error('Serialized error must have either an error or a value');
return parseSerializedValue(error.value, undefined);
}
if (error.error.name === 'TimeoutError') {
const e = new TimeoutError(error.error.message);
e.stack = error.error.stack || '';
return e;
}
if (error.error.name === 'TargetClosedError') {
const e = new TargetClosedError(error.error.message);
e.stack = error.error.stack || '';
return e;
}
const e = new Error(error.error.message);
let e: PlaywrightError;
if (error.error.name === 'TimeoutError')
e = new TimeoutError(error.error.message);
else if (error.error.name === 'TargetClosedError')
e = new TargetClosedError(error.error.message);
else
e = Object.assign(new PlaywrightError(error.error.message), { name: error.error.name });
e.stack = error.error.stack || '';
e.name = error.error.name;
return e;
}
30 changes: 18 additions & 12 deletions packages/playwright-core/src/client/frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { EventEmitter } from './eventEmitter';
import { ChannelOwner } from './channelOwner';
import { addSourceUrlToScript } from './clientHelper';
import { ElementHandle, convertInputFiles, convertSelectOptionValues } from './elementHandle';
import { PlaywrightError } from './errors';
import { Events } from './events';
import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle';
import { FrameLocator, Locator, testIdAttributeName } from './locator';
Expand Down Expand Up @@ -493,20 +494,25 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
async _expect(expression: string, options: Omit<channels.FrameExpectParams, 'expression'>): Promise<ExpectResult> {
const params: channels.FrameExpectParams = { expression, ...options, isNot: !!options.isNot };
params.expectedValue = serializeArgument(options.expectedValue);
const channelResult = await this._channel.expect(params);
const result: ExpectResult = {
matches: channelResult.matches,
log: channelResult.log,
timedOut: channelResult.timedOut,
errorMessage: channelResult.errorMessage,
};
if (channelResult.received !== undefined && channelResult.matches === !!options.isNot) {
result.received = {
value: channelResult.received.value !== undefined ? parseResult(channelResult.received.value) : undefined,
ariaSnapshot: channelResult.received.ariaSnapshot,
try {
await this._channel.expect(params);
return { matches: !params.isNot };
} catch (e) {
if (!(e instanceof PlaywrightError))
throw e;
const details = e.details as channels.FrameExpectErrorDetails;
const received = details.received ? {
value: details.received.value !== undefined ? parseResult(details.received.value) : undefined,
ariaSnapshot: details.received.ariaSnapshot,
} : undefined;
return {
matches: !!params.isNot,
received,
log: e.log,
timedOut: details.timedOut,
errorMessage: details.customErrorMessage ? 'Error: ' + details.customErrorMessage : undefined,
};
}
return result;
}
}

Expand Down
22 changes: 15 additions & 7 deletions packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { Coverage } from './coverage';
import { DisposableObject, DisposableStub } from './disposable';
import { Download } from './download';
import { ElementHandle, determineScreenshotType } from './elementHandle';
import { TargetClosedError, isTargetClosedError, parseError, serializeError } from './errors';
import { PlaywrightError, TargetClosedError, isTargetClosedError, parseError, serializeError } from './errors';
import { Events } from './events';
import { FileChooser } from './fileChooser';
import { Frame, verifyLoadState } from './frame';
Expand Down Expand Up @@ -627,12 +627,20 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
frame: (options.locator as Locator)._frame._channel,
selector: (options.locator as Locator)._selector,
} : undefined;
return await this._channel.expectScreenshot({
...options,
isNot: !!options.isNot,
locator,
mask,
});
try {
const result = await this._channel.expectScreenshot({
...options,
isNot: !!options.isNot,
locator,
mask,
});
return { actual: result.actual };
} catch (e) {
if (!(e instanceof PlaywrightError))
throw e;
const details = e.details as channels.PageExpectScreenshotErrorDetails;
return { ...details, errorMessage: details.customErrorMessage };
}
}

async title(): Promise<string> {
Expand Down
12 changes: 7 additions & 5 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1658,15 +1658,14 @@ scheme.FrameExpectParams = tObject({
isNot: tBoolean,
timeout: tFloat,
});
scheme.FrameExpectResult = tObject({
matches: tBoolean,
scheme.FrameExpectResult = tOptional(tObject({}));
scheme.FrameExpectErrorDetails = tObject({
received: tOptional(tObject({
value: tOptional(tType('SerializedValue')),
ariaSnapshot: tOptional(tString),
})),
timedOut: tOptional(tBoolean),
errorMessage: tOptional(tString),
log: tOptional(tArray(tString)),
customErrorMessage: tOptional(tString),
});
scheme.JSHandleInitializer = tObject({
preview: tString,
Expand Down Expand Up @@ -2421,8 +2420,11 @@ scheme.PageExpectScreenshotParams = tObject({
style: tOptional(tString),
});
scheme.PageExpectScreenshotResult = tObject({
actual: tOptional(tBinary),
});
scheme.PageExpectScreenshotErrorDetails = tObject({
diff: tOptional(tBinary),
errorMessage: tOptional(tString),
customErrorMessage: tOptional(tString),
actual: tOptional(tBinary),
previous: tOptional(tBinary),
timedOut: tOptional(tBoolean),
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/protocol/validatorPrimitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ export type ValidatorContext = {
};
export const scheme: { [key: string]: Validator } = {};

export function findValidator(type: string, method: string, kind: 'Initializer' | 'Event' | 'Params' | 'Result'): Validator {
export function findValidator(type: string, method: string, kind: 'Initializer' | 'Event' | 'Params' | 'Result' | 'ErrorDetails'): Validator {
const validator = maybeFindValidator(type, method, kind);
if (!validator)
throw new ValidationError(`Unknown scheme for ${kind}: ${type}.${method}`);
return validator;
}
export function maybeFindValidator(type: string, method: string, kind: 'Initializer' | 'Event' | 'Params' | 'Result'): Validator | undefined {
export function maybeFindValidator(type: string, method: string, kind: 'Initializer' | 'Event' | 'Params' | 'Result' | 'ErrorDetails'): Validator | undefined {
const schemeName = type + (kind === 'Initializer' ? '' : method[0].toUpperCase() + method.substring(1)) + kind;
return scheme[schemeName];
}
Expand Down
11 changes: 6 additions & 5 deletions packages/playwright-core/src/server/chromium/crBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { CRPage } from './crPage';
import { saveProtocolStream } from './crProtocolHelper';
import { CRServiceWorker } from './crServiceWorker';

import type { InitScript, Worker } from '../page';
import type { InitScript } from '../page';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';
import type { CDPSession, CRSession } from './crConnection';
Expand Down Expand Up @@ -496,9 +496,10 @@ export class CRBrowserContext extends BrowserContext<CREventsMap> {

async setUserAgent(userAgent: string | undefined): Promise<void> {
this._options.userAgent = userAgent;
for (const page of this.pages())
await (page.delegate as CRPage).updateUserAgent();
// TODO: service workers don't have Emulation domain?
await Promise.all([
...this.pages().map(page => (page.delegate as CRPage).updateUserAgent()),
...this.serviceWorkers().map(sw => sw.updateUserAgent()),
]);
}

async doUpdateOffline(): Promise<void> {
Expand Down Expand Up @@ -593,7 +594,7 @@ export class CRBrowserContext extends BrowserContext<CREventsMap> {
});
}

serviceWorkers(): Worker[] {
serviceWorkers(): CRServiceWorker[] {
return Array.from(this._browser._serviceWorkers.values()).filter(serviceWorker => serviceWorker.browserContext === this);
}

Expand Down
Loading
Loading