Merge branch 'main' into sharding-algorithm

This commit is contained in:
Mathias Leppich 2024-05-29 11:30:17 +02:00
commit ef2d35f34a
47 changed files with 233 additions and 129 deletions

View file

@ -60,7 +60,7 @@ jobs:
git checkout -b "$BRANCH_NAME"
git push origin $BRANCH_NAME
- name: Create Pull Request
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
script: |

View file

@ -58,7 +58,7 @@ jobs:
path: '.'
- name: Comment on PR
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View file

@ -15,11 +15,13 @@ jobs:
runs-on: ubuntu-20.04
if: github.repository == 'microsoft/playwright'
steps:
- uses: actions/checkout@v4
- name: Create GitHub issue
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
script: |
const currentPlaywrightVersion = require('./package.json').version.match(/\d+\.\d+/)[0];
const { data } = await github.rest.git.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
@ -27,10 +29,10 @@ jobs:
});
const commitHeader = data.message.split('\n')[0];
const title = '[Ports]: Backport client side changes';
const title = '[Ports]: Backport client side changes for ' + currentPlaywrightVersion;
for (const repo of ['playwright-python', 'playwright-java', 'playwright-dotnet']) {
const { data: issuesData } = await github.rest.search.issuesAndPullRequests({
q: `is:issue is:open repo:microsoft/${repo} in:title "${title}"`
q: `is:issue is:open repo:microsoft/${repo} in:title "${title}" author:playwrightmachine"`
})
let issueNumber = null;
let issueBody = '';

View file

@ -38,7 +38,7 @@ jobs:
git commit -m "feat(${{ github.event.client_payload.browser }}): roll to r${{ github.event.client_payload.revision }}"
git push origin $BRANCH_NAME
- name: Create Pull Request
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
script: |

View file

@ -35,7 +35,7 @@ jobs:
git push origin $BRANCH_NAME
- name: Create Pull Request
if: ${{ steps.prepare-branch.outputs.HAS_CHANGES == '1' }}
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
script: |

View file

@ -202,6 +202,12 @@ Prevents automatic playwright driver installation on attach. Assumes that the dr
Optional device serial number to launch the browser on. If not specified, it will
throw if multiple devices are connected.
### option: Android.launchServer.host
* since: v1.45
- `host` <[string]>
Host to use for the web socket. It is optional and if it is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available, or the unspecified IPv4 address (0.0.0.0) otherwise. Consider hardening it with picking a specific interface.
### option: Android.launchServer.port
* since: v1.28
- `port` <[int]>

View file

@ -31,3 +31,5 @@ Browser websocket url.
Browser websocket endpoint which can be used as an argument to [`method: BrowserType.connect`] to establish connection
to the browser.
Note that if the listen `host` option in `launchServer` options is not specified, localhost will be output anyway, even if the actual listening address is an unspecified address.

View file

@ -380,6 +380,12 @@ const { chromium } = require('playwright'); // Or 'webkit' or 'firefox'.
### option: BrowserType.launchServer.logger = %%-browser-option-logger-%%
* since: v1.8
### option: BrowserType.launchServer.host
* since: v1.45
- `host` <[string]>
Host to use for the web socket. It is optional and if it is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available, or the unspecified IPv4 address (0.0.0.0) otherwise. Consider hardening it with picking a specific interface.
### option: BrowserType.launchServer.port
* since: v1.8
- `port` <[int]>

View file

@ -127,8 +127,6 @@ When you have finished interacting with the page, press the **record** button to
Use the **clear** button to clear the code to start recording again. Once finished close the Playwright inspector window or stop the terminal command.
To learn more about generating tests check out or detailed guide on [Codegen](./codegen.md).
### Generating locators
You can generate [locators](/locators.md) with the test generator.

View file

@ -164,7 +164,7 @@ export default defineConfig({
* since: v1.10
- type: ?<[RegExp]|[Array]<[RegExp]>>
Filter to only run tests with a title matching one of the patterns. For example, passing `grep: /cart/` should only run tests with "cart" in the title. Also available in the [command line](../test-cli.md) with the `-g` option. The regular expression will be tested against the string that consists of the test file name, `test.describe` name (if any) and the test name divided by spaces, e.g. `my-test.spec.ts my-suite my-test`.
Filter to only run tests with a title matching one of the patterns. For example, passing `grep: /cart/` should only run tests with "cart" in the title. Also available in the [command line](../test-cli.md) with the `-g` option. The regular expression will be tested against the string that consists of the project name, the test file name, the `test.describe` name (if any), the test name and the test tags divided by spaces, e.g. `chromium my-test.spec.ts my-suite my-test`.
`grep` option is also useful for [tagging tests](../test-annotations.md#tag-tests).

View file

@ -559,7 +559,7 @@ Whether to record trace for each test. Defaults to `'off'`.
* `'on-first-retry'`: Record trace only when retrying a test for the first time.
* `'on-all-retries'`: Record trace only when retrying a test.
* `'retain-on-failure'`: Record trace for each test. When test run passes, remove the recorded trace.
* `'retain-on-first-failure'`: Record trace for the first run of each test, but not for retires. When test run passes, remove the recorded trace.
* `'retain-on-first-failure'`: Record trace for the first run of each test, but not for retries. When test run passes, remove the recorded trace.
For more control, pass an object that specifies `mode` and trace features to enable.

View file

@ -123,7 +123,7 @@ You can configure entire test project to concurrently run all tests in all files
* since: v1.10
- type: ?<[RegExp]|[Array]<[RegExp]>>
Filter to only run tests with a title matching one of the patterns. For example, passing `grep: /cart/` should only run tests with "cart" in the title. Also available globally and in the [command line](../test-cli.md) with the `-g` option. The regular expression will be tested against the string that consists of the test file name, `test.describe` name (if any) and the test name divided by spaces, e.g. `my-test.spec.ts my-suite my-test`.
Filter to only run tests with a title matching one of the patterns. For example, passing `grep: /cart/` should only run tests with "cart" in the title. Also available globally and in the [command line](../test-cli.md) with the `-g` option. The regular expression will be tested against the string that consists of the project name, the test file name, the `test.describe` name (if any), the test name and the test tags divided by spaces, e.g. `chromium my-test.spec.ts my-suite my-test`.
`grep` option is also useful for [tagging tests](../test-annotations.md#tag-tests).

View file

@ -100,7 +100,6 @@ Complete set of Playwright Test options is available in the [configuration file]
| `--reporter <reporter>` | Choose a reporter: minimalist `dot`, concise `line` or detailed `list`. See [reporters](./test-reporters.md) for more information. You can also pass a path to a [custom reporter](./test-reporters.md#custom-reporters) file. |
| `--retries <number>` | The maximum number of [retries](./test-retries.md#retries) for flaky tests, defaults to zero (no retries). |
| `--shard <shard>` | [Shard](./test-parallel.md#shard-tests-between-multiple-machines) tests and execute only selected shard, specified in the form `current/all`, 1-based, for example `3/5`.|
| `--tag <tag>` | Only run tests with a tag matching this tag expression. Learn more about [tagging](./test-annotations.md#tag-tests). |
| `--timeout <number>` | Maximum timeout in milliseconds for each test, defaults to 30 seconds. Learn more about [various timeouts](./test-timeouts.md).|
| `--trace <mode>` | Force tracing mode, can be `on`, `off`, `on-first-retry`, `on-all-retries`, `retain-on-failure` |
| `--update-snapshots` or `-u` | Whether to update [snapshots](./test-snapshots.md) with actual results instead of comparing them. Use this when snapshot expectations have changed.|

View file

@ -926,9 +926,9 @@ export default defineConfig<MyOptions>({
Each fixture has a setup and teardown phase separated by the `await use()` call in the fixture. Setup is executed before the fixture is used by the test/hook, and teardown is executed when the fixture will not be used by the test/hook anymore.
Fixtures follow these rules to determine the execution order:
* When fixture A depends on fixture B: B is always set up before A and teared down after A.
* When fixture A depends on fixture B: B is always set up before A and torn down after A.
* Non-automatic fixtures are executed lazily, only when the test/hook needs them.
* Test-scoped fixtures are teared down after each test, while worker-scoped fixtures are only teared down when the worker process executing tests is shutdown.
* Test-scoped fixtures are torn down after each test, while worker-scoped fixtures are only torn down when the worker process executing tests is shutdown.
Consider the following example:
@ -1036,8 +1036,8 @@ Normally, if all tests pass and no errors are thrown, the order of execution is
* `beforeEach` runs.
* `first test` runs.
* `afterEach` runs.
* `page` teardown because it is a test-scoped fixture and should be teared down after the test finishes.
* `autoTestFixture` teardown because it is a test-scoped fixture and should be teared down after the test finishes.
* `page` teardown because it is a test-scoped fixture and should be torn down after the test finishes.
* `autoTestFixture` teardown because it is a test-scoped fixture and should be torn down after the test finishes.
* `second test` section:
* `autoTestFixture` setup because automatic test fixtures are always set up before test and `beforeEach` hooks.
* `page` setup because it is required in `beforeEach` hook.
@ -1046,20 +1046,20 @@ Normally, if all tests pass and no errors are thrown, the order of execution is
* `testFixture` setup because it is required by the `second test`.
* `second test` runs.
* `afterEach` runs.
* `testFixture` teardown because it is a test-scoped fixture and should be teared down after the test finishes.
* `page` teardown because it is a test-scoped fixture and should be teared down after the test finishes.
* `autoTestFixture` teardown because it is a test-scoped fixture and should be teared down after the test finishes.
* `testFixture` teardown because it is a test-scoped fixture and should be torn down after the test finishes.
* `page` teardown because it is a test-scoped fixture and should be torn down after the test finishes.
* `autoTestFixture` teardown because it is a test-scoped fixture and should be torn down after the test finishes.
* `afterAll` and worker teardown section:
* `afterAll` runs.
* `workerFixture` teardown because it is a workers-scoped fixture and should be teared down once at the end.
* `autoWorkerFixture` teardown because it is a workers-scoped fixture and should be teared down once at the end.
* `browser` teardown because it is a workers-scoped fixture and should be teared down once at the end.
* `workerFixture` teardown because it is a workers-scoped fixture and should be torn down once at the end.
* `autoWorkerFixture` teardown because it is a workers-scoped fixture and should be torn down once at the end.
* `browser` teardown because it is a workers-scoped fixture and should be torn down once at the end.
A few observations:
* `page` and `autoTestFixture` are set up and teared down for each test, as test-scoped fixtures.
* `page` and `autoTestFixture` are set up and torn down for each test, as test-scoped fixtures.
* `unusedFixture` is never set up because it is not used by any tests/hooks.
* `testFixture` depends on `workerFixture` and triggers its setup.
* `workerFixture` is lazily set up before the second test, but teared down once during worker shutdown, as a worker-scoped fixture.
* `workerFixture` is lazily set up before the second test, but torn down once during worker shutdown, as a worker-scoped fixture.
* `autoWorkerFixture` is set up for `beforeAll` hook, but `autoTestFixture` is not.
## Combine custom fixtures from multiple modules

View file

@ -9,9 +9,9 @@
},
{
"name": "chromium-tip-of-tree",
"revision": "1223",
"revision": "1226",
"installByDefault": false,
"browserVersion": "127.0.6492.0"
"browserVersion": "127.0.6505.0"
},
{
"name": "firefox",
@ -27,7 +27,7 @@
},
{
"name": "webkit",
"revision": "2012",
"revision": "2013",
"installByDefault": true,
"revisionOverrides": {
"mac10.14": "1446",

View file

@ -50,7 +50,7 @@ export class AndroidServerLauncherImpl {
// 2. Start the server
const server = new PlaywrightServer({ mode: 'launchServer', path, maxConnections: 1, preLaunchedAndroidDevice: device });
const wsEndpoint = await server.listen(options.port);
const wsEndpoint = await server.listen(options.port, options.host);
// 3. Return the BrowserServer interface
const browserServer = new ws.EventEmitter() as (BrowserServer & WebSocketEventEmitter);

View file

@ -58,7 +58,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
// 2. Start the server
const server = new PlaywrightServer({ mode: 'launchServer', path, maxConnections: Infinity, preLaunchedBrowser: browser, preLaunchedSocksProxy: socksProxy });
const wsEndpoint = await server.listen(options.port);
const wsEndpoint = await server.listen(options.port, options.host);
// 3. Return the BrowserServer interface
const browserServer = new ws.EventEmitter() as (BrowserServer & WebSocketEventEmitter);

View file

@ -235,11 +235,11 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
context.setDefaultTimeout(this._defaultContextTimeout);
if (this._defaultContextNavigationTimeout !== undefined)
context.setDefaultNavigationTimeout(this._defaultContextNavigationTimeout);
await this._instrumentation.onDidCreateBrowserContext(context);
await this._instrumentation.runAfterCreateBrowserContext(context);
}
async _willCloseContext(context: BrowserContext) {
this._contexts.delete(context);
await this._instrumentation.onWillCloseBrowserContext(context);
await this._instrumentation.runBeforeCloseBrowserContext(context);
}
}

View file

@ -24,21 +24,23 @@ export interface ClientInstrumentation {
removeAllListeners(): void;
onApiCallBegin(apiCall: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }): void;
onApiCallEnd(userData: any, error?: Error): void;
onDidCreateBrowserContext(context: BrowserContext): Promise<void>;
onDidCreateRequestContext(context: APIRequestContext): Promise<void>;
onWillPause(): void;
onWillCloseBrowserContext(context: BrowserContext): Promise<void>;
onWillCloseRequestContext(context: APIRequestContext): Promise<void>;
runAfterCreateBrowserContext(context: BrowserContext): Promise<void>;
runAfterCreateRequestContext(context: APIRequestContext): Promise<void>;
runBeforeCloseBrowserContext(context: BrowserContext): Promise<void>;
runBeforeCloseRequestContext(context: APIRequestContext): Promise<void>;
}
export interface ClientInstrumentationListener {
onApiCallBegin?(apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }): void;
onApiCallEnd?(userData: any, error?: Error): void;
onDidCreateBrowserContext?(context: BrowserContext): Promise<void>;
onDidCreateRequestContext?(context: APIRequestContext): Promise<void>;
onWillPause?(): void;
onWillCloseBrowserContext?(context: BrowserContext): Promise<void>;
onWillCloseRequestContext?(context: APIRequestContext): Promise<void>;
runAfterCreateBrowserContext?(context: BrowserContext): Promise<void>;
runAfterCreateRequestContext?(context: APIRequestContext): Promise<void>;
runBeforeCloseBrowserContext?(context: BrowserContext): Promise<void>;
runBeforeCloseRequestContext?(context: APIRequestContext): Promise<void>;
}
export function createInstrumentation(): ClientInstrumentation {
@ -53,12 +55,19 @@ export function createInstrumentation(): ClientInstrumentation {
return (listener: ClientInstrumentationListener) => listeners.splice(listeners.indexOf(listener), 1);
if (prop === 'removeAllListeners')
return () => listeners.splice(0, listeners.length);
if (!prop.startsWith('on'))
return obj[prop];
return async (...params: any[]) => {
for (const listener of listeners)
await (listener as any)[prop]?.(...params);
};
if (prop.startsWith('run')) {
return async (...params: any[]) => {
for (const listener of listeners)
await (listener as any)[prop]?.(...params);
};
}
if (prop.startsWith('on')) {
return (...params: any[]) => {
for (const listener of listeners)
(listener as any)[prop]?.(...params);
};
}
return obj[prop];
},
});
}

View file

@ -77,7 +77,7 @@ export class APIRequest implements api.APIRequest {
this._contexts.add(context);
context._request = this;
context._tracing._tracesDir = tracesDir;
await context._instrumentation.onDidCreateRequestContext(context);
await context._instrumentation.runAfterCreateRequestContext(context);
return context;
}
}
@ -102,7 +102,7 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
async dispose(options: { reason?: string } = {}): Promise<void> {
this._closeReason = options.reason;
await this._instrumentation.onWillCloseRequestContext(this);
await this._instrumentation.runBeforeCloseRequestContext(this);
try {
await this._channel.dispose(options);
} catch (e) {

View file

@ -111,6 +111,7 @@ export type LaunchServerOptions = {
},
downloadsPath?: string,
chromiumSandbox?: boolean,
host?: string,
port?: number,
wsPath?: string,
logger?: Logger,
@ -122,6 +123,7 @@ export type LaunchAndroidServerOptions = {
adbHost?: string,
adbPort?: number,
omitDriverInstall?: boolean,
host?: string,
port?: number,
wsPath?: string,
};

View file

@ -13745,6 +13745,13 @@ export interface BrowserType<Unused = {}> {
*/
headless?: boolean;
/**
* Host to use for the web socket. It is optional and if it is omitted, the server will accept connections on the
* unspecified IPv6 address (::) when IPv6 is available, or the unspecified IPv4 address (0.0.0.0) otherwise. Consider
* hardening it with picking a specific interface.
*/
host?: string;
/**
* If `true`, Playwright does not pass its own configurations args and only uses the ones from `args`. If an array is
* given, then filters out the given default arguments. Dangerous option; use with care. Defaults to `false`.
@ -14621,6 +14628,13 @@ export interface Android {
*/
deviceSerialNumber?: string;
/**
* Host to use for the web socket. It is optional and if it is omitted, the server will accept connections on the
* unspecified IPv6 address (::) when IPv6 is available, or the unspecified IPv4 address (0.0.0.0) otherwise. Consider
* hardening it with picking a specific interface.
*/
host?: string;
/**
* Prevents automatic playwright driver installation on attach. Assumes that the drivers have been installed already.
*/
@ -17201,6 +17215,9 @@ export interface BrowserServer {
* Browser websocket endpoint which can be used as an argument to
* [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect)
* to establish connection to the browser.
*
* Note that if the listen `host` option in `launchServer` options is not specified, localhost will be output anyway,
* even if the actual listening address is an unspecified address.
*/
wsEndpoint(): string;

View file

@ -102,6 +102,7 @@ async function innerMount(page: Page, componentRef: JsxComponent | ImportRef, op
const selector = await page.evaluate(async ({ component, hooksConfig }) => {
component = await window.__pwUnwrapObject(component);
hooksConfig = await window.__pwUnwrapObject(hooksConfig);
let rootElement = document.getElementById('root');
if (!rootElement) {
rootElement = document.createElement('div');

View file

@ -14,11 +14,6 @@
* limitations under the License.
*/
type JsonPrimitive = string | number | boolean | null;
type JsonValue = JsonPrimitive | JsonObject | JsonArray;
type JsonArray = JsonValue[];
export type JsonObject = { [Key in string]?: JsonValue };
export type JsxComponent = {
__pw_type: 'jsx',
type: any,
@ -47,10 +42,10 @@ declare global {
playwrightMount(component: Component, rootElement: Element, hooksConfig?: any): Promise<void>;
playwrightUnmount(rootElement: Element): Promise<void>;
playwrightUpdate(rootElement: Element, component: Component): Promise<void>;
__pw_hooks_before_mount?: (<HooksConfig extends JsonObject = JsonObject>(
__pw_hooks_before_mount?: (<HooksConfig>(
params: { hooksConfig?: HooksConfig; [key: string]: any }
) => Promise<any>)[];
__pw_hooks_after_mount?: (<HooksConfig extends JsonObject = JsonObject>(
__pw_hooks_after_mount?: (<HooksConfig>(
params: { hooksConfig?: HooksConfig; [key: string]: any }
) => Promise<void>)[];
// Can't start with __pw due to core reuse bindings logic for __pw*.

View file

@ -14,11 +14,9 @@
* limitations under the License.
*/
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
export declare function beforeMount<HooksConfig extends JsonObject>(
export declare function beforeMount<HooksConfig>(
callback: (params: { hooksConfig?: HooksConfig; App: () => JSX.Element }) => Promise<void | JSX.Element>
): void;
export declare function afterMount<HooksConfig extends JsonObject>(
export declare function afterMount<HooksConfig>(
callback: (params: { hooksConfig?: HooksConfig }) => Promise<void>
): void;

View file

@ -15,10 +15,9 @@
*/
import type { Locator } from 'playwright/test';
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
import type { TestType } from '@playwright/experimental-ct-core';
export interface MountOptions<HooksConfig extends JsonObject> {
export interface MountOptions<HooksConfig> {
hooksConfig?: HooksConfig;
}
@ -28,7 +27,7 @@ export interface MountResult extends Locator {
}
export const test: TestType<{
mount<HooksConfig extends JsonObject>(
mount<HooksConfig>(
component: JSX.Element,
options?: MountOptions<HooksConfig>
): Promise<MountResult>;

View file

@ -14,11 +14,9 @@
* limitations under the License.
*/
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
export declare function beforeMount<HooksConfig extends JsonObject>(
export declare function beforeMount<HooksConfig>(
callback: (params: { hooksConfig?: HooksConfig; App: () => JSX.Element }) => Promise<void | JSX.Element>
): void;
export declare function afterMount<HooksConfig extends JsonObject>(
export declare function afterMount<HooksConfig>(
callback: (params: { hooksConfig?: HooksConfig }) => Promise<void>
): void;

View file

@ -15,10 +15,9 @@
*/
import type { Locator } from 'playwright/test';
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
import type { TestType } from '@playwright/experimental-ct-core';
export interface MountOptions<HooksConfig extends JsonObject> {
export interface MountOptions<HooksConfig> {
hooksConfig?: HooksConfig;
}
@ -28,7 +27,7 @@ export interface MountResult extends Locator {
}
export const test: TestType<{
mount<HooksConfig extends JsonObject>(
mount<HooksConfig>(
component: JSX.Element,
options?: MountOptions<HooksConfig>
): Promise<MountResult>;

View file

@ -14,12 +14,11 @@
* limitations under the License.
*/
import { JSXElement } from "solid-js";
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
import { JSXElement } from 'solid-js';
export declare function beforeMount<HooksConfig extends JsonObject>(
export declare function beforeMount<HooksConfig>(
callback: (params: { hooksConfig?: HooksConfig, App: () => JSXElement }) => Promise<void | JSXElement>
): void;
export declare function afterMount<HooksConfig extends JsonObject>(
export declare function afterMount<HooksConfig>(
callback: (params: { hooksConfig?: HooksConfig }) => Promise<void>
): void;

View file

@ -15,10 +15,9 @@
*/
import type { Locator } from 'playwright/test';
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
import type { TestType } from '@playwright/experimental-ct-core';
export interface MountOptions<HooksConfig extends JsonObject> {
export interface MountOptions<HooksConfig> {
hooksConfig?: HooksConfig;
}
@ -28,7 +27,7 @@ export interface MountResult extends Locator {
}
export const test: TestType<{
mount<HooksConfig extends JsonObject>(
mount<HooksConfig>(
component: JSX.Element,
options?: MountOptions<HooksConfig>
): Promise<MountResult>;

View file

@ -15,15 +15,14 @@
*/
import type { ComponentConstructorOptions, SvelteComponent } from 'svelte';
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
export declare function beforeMount<HooksConfig extends JsonObject>(
export declare function beforeMount<HooksConfig>(
callback: (params: {
hooksConfig?: HooksConfig,
App: new (options: Partial<ComponentConstructorOptions>) => SvelteComponent
}) => Promise<SvelteComponent | void>
): void;
export declare function afterMount<HooksConfig extends JsonObject>(
export declare function afterMount<HooksConfig>(
callback: (params: {
hooksConfig?: HooksConfig;
svelteComponent: SvelteComponent;

View file

@ -15,7 +15,6 @@
*/
import type { Locator } from 'playwright/test';
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
import type { SvelteComponent, ComponentProps } from 'svelte/types/runtime';
import type { TestType } from '@playwright/experimental-ct-core';
@ -23,7 +22,7 @@ type ComponentSlot = string | string[];
type ComponentSlots = Record<string, ComponentSlot> & { default?: ComponentSlot };
type ComponentEvents = Record<string, Function>;
export interface MountOptions<HooksConfig extends JsonObject, Component extends SvelteComponent> {
export interface MountOptions<HooksConfig, Component extends SvelteComponent> {
props?: ComponentProps<Component>;
slots?: ComponentSlots;
on?: ComponentEvents;
@ -39,7 +38,7 @@ export interface MountResult<Component extends SvelteComponent> extends Locator
}
export const test: TestType<{
mount<HooksConfig extends JsonObject, Component extends SvelteComponent = SvelteComponent>(
mount<HooksConfig, Component extends SvelteComponent = SvelteComponent>(
component: new (...args: any[]) => Component,
options?: MountOptions<HooksConfig, Component>
): Promise<MountResult<Component>>;

View file

@ -15,12 +15,11 @@
*/
import type { App, ComponentPublicInstance } from 'vue';
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
export declare function beforeMount<HooksConfig extends JsonObject>(
export declare function beforeMount<HooksConfig>(
callback: (params: { app: App; hooksConfig?: HooksConfig }) => Promise<void>
): void;
export declare function afterMount<HooksConfig extends JsonObject>(
export declare function afterMount<HooksConfig>(
callback: (params: {
app: App;
hooksConfig?: HooksConfig;

View file

@ -15,7 +15,6 @@
*/
import type { Locator } from 'playwright/test';
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
import type { TestType } from '@playwright/experimental-ct-core';
type ComponentSlot = string | string[];
@ -29,14 +28,14 @@ type ComponentProps<T> =
T extends (props: infer P, ...args: any) => any ? P :
{};
export interface MountOptions<HooksConfig extends JsonObject, Component> {
export interface MountOptions<HooksConfig, Component> {
props?: ComponentProps<Component>;
slots?: ComponentSlots;
on?: ComponentEvents;
hooksConfig?: HooksConfig;
}
export interface MountOptionsJsx<HooksConfig extends JsonObject> {
export interface MountOptionsJsx<HooksConfig> {
hooksConfig?: HooksConfig;
}
@ -55,11 +54,11 @@ export interface MountResultJsx extends Locator {
}
export const test: TestType<{
mount<HooksConfig extends JsonObject>(
mount<HooksConfig>(
component: JSX.Element,
options: MountOptionsJsx<HooksConfig>
): Promise<MountResultJsx>;
mount<HooksConfig extends JsonObject, Component = unknown>(
mount<HooksConfig, Component = unknown>(
component: Component,
options?: MountOptions<HooksConfig, Component>
): Promise<MountResult<Component>>;

View file

@ -14,17 +14,16 @@
* limitations under the License.
*/
import { ComponentOptions } from 'vue';
import { CombinedVueInstance, Vue, VueConstructor } from 'vue/types/vue';
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
import type { ComponentOptions } from 'vue';
import type { CombinedVueInstance, Vue, VueConstructor } from 'vue/types/vue';
export declare function beforeMount<HooksConfig extends JsonObject>(
callback: (params: {
hooksConfig?: HooksConfig,
Vue: VueConstructor<Vue>,
export declare function beforeMount<HooksConfig>(
callback: (params: {
hooksConfig?: HooksConfig,
Vue: VueConstructor<Vue>,
}) => Promise<void | ComponentOptions<Vue> & Record<string, unknown>>
): void;
export declare function afterMount<HooksConfig extends JsonObject>(
export declare function afterMount<HooksConfig>(
callback: (params: {
hooksConfig?: HooksConfig;
instance: CombinedVueInstance<

View file

@ -15,7 +15,6 @@
*/
import type { Locator } from 'playwright/test';
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
import type { TestType } from '@playwright/experimental-ct-core';
type Slot = string | string[];
@ -29,14 +28,14 @@ type ComponentProps<T> =
T extends (props: infer P, ...args: any) => any ? P :
{};
export interface MountOptions<HooksConfig extends JsonObject, Component> {
export interface MountOptions<HooksConfig, Component> {
props?: ComponentProps<Component>;
slots?: ComponentSlots;
on?: ComponentEvents;
hooksConfig?: HooksConfig;
}
export interface MountOptionsJsx<HooksConfig extends JsonObject> {
export interface MountOptionsJsx<HooksConfig> {
hooksConfig?: HooksConfig;
}
@ -55,11 +54,11 @@ export interface MountResultJsx extends Locator {
}
export const test: TestType<{
mount<HooksConfig extends JsonObject>(
mount<HooksConfig>(
component: JSX.Element,
options?: MountOptionsJsx<HooksConfig>
): Promise<MountResultJsx>;
mount<HooksConfig extends JsonObject, Component = unknown>(
mount<HooksConfig, Component = unknown>(
component: Component,
options?: MountOptions<HooksConfig, Component>
): Promise<MountResult<Component>>;

View file

@ -268,19 +268,19 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
onWillPause: () => {
currentTestInfo()?.setTimeout(0);
},
onDidCreateBrowserContext: async (context: BrowserContext) => {
runAfterCreateBrowserContext: async (context: BrowserContext) => {
await artifactsRecorder?.didCreateBrowserContext(context);
const testInfo = currentTestInfo();
if (testInfo)
attachConnectedHeaderIfNeeded(testInfo, context.browser());
},
onDidCreateRequestContext: async (context: APIRequestContext) => {
runAfterCreateRequestContext: async (context: APIRequestContext) => {
await artifactsRecorder?.didCreateRequestContext(context);
},
onWillCloseBrowserContext: async (context: BrowserContext) => {
runBeforeCloseBrowserContext: async (context: BrowserContext) => {
await artifactsRecorder?.willCloseBrowserContext(context);
},
onWillCloseRequestContext: async (context: APIRequestContext) => {
runBeforeCloseRequestContext: async (context: APIRequestContext) => {
await artifactsRecorder?.willCloseRequestContext(context);
},
};

View file

@ -32,6 +32,9 @@ export class TeleReporterEmitter implements ReporterV2 {
private _messageSink: (message: teleReceiver.JsonEvent) => void;
private _rootDir!: string;
private _emitterOptions: TeleReporterEmitterOptions;
// In case there is blob reporter and UI mode, make sure one does override
// the id assigned by the other.
private readonly _idSymbol = Symbol('id');
constructor(messageSink: (message: teleReceiver.JsonEvent) => void, options: TeleReporterEmitterOptions = {}) {
this._messageSink = messageSink;
@ -55,7 +58,7 @@ export class TeleReporterEmitter implements ReporterV2 {
}
onTestBegin(test: reporterTypes.TestCase, result: reporterTypes.TestResult): void {
(result as any)[idSymbol] = createGuid();
(result as any)[this._idSymbol] = createGuid();
this._messageSink({
method: 'onTestBegin',
params: {
@ -82,12 +85,12 @@ export class TeleReporterEmitter implements ReporterV2 {
}
onStepBegin(test: reporterTypes.TestCase, result: reporterTypes.TestResult, step: reporterTypes.TestStep): void {
(step as any)[idSymbol] = createGuid();
(step as any)[this._idSymbol] = createGuid();
this._messageSink({
method: 'onStepBegin',
params: {
testId: test.id,
resultId: (result as any)[idSymbol],
resultId: (result as any)[this._idSymbol],
step: this._serializeStepStart(step)
}
});
@ -98,7 +101,7 @@ export class TeleReporterEmitter implements ReporterV2 {
method: 'onStepEnd',
params: {
testId: test.id,
resultId: (result as any)[idSymbol],
resultId: (result as any)[this._idSymbol],
step: this._serializeStepEnd(step)
}
});
@ -126,7 +129,7 @@ export class TeleReporterEmitter implements ReporterV2 {
const data = isBase64 ? chunk.toString('base64') : chunk;
this._messageSink({
method: 'onStdIO',
params: { testId: test?.id, resultId: result ? (result as any)[idSymbol] : undefined, type, data, isBase64 }
params: { testId: test?.id, resultId: result ? (result as any)[this._idSymbol] : undefined, type, data, isBase64 }
});
}
@ -214,7 +217,7 @@ export class TeleReporterEmitter implements ReporterV2 {
private _serializeResultStart(result: reporterTypes.TestResult): teleReceiver.JsonTestResultStart {
return {
id: (result as any)[idSymbol],
id: (result as any)[this._idSymbol],
retry: result.retry,
workerIndex: result.workerIndex,
parallelIndex: result.parallelIndex,
@ -224,7 +227,7 @@ export class TeleReporterEmitter implements ReporterV2 {
private _serializeResultEnd(result: reporterTypes.TestResult): teleReceiver.JsonTestResultEnd {
return {
id: (result as any)[idSymbol],
id: (result as any)[this._idSymbol],
duration: result.duration,
status: result.status,
errors: result.errors,
@ -244,8 +247,8 @@ export class TeleReporterEmitter implements ReporterV2 {
private _serializeStepStart(step: reporterTypes.TestStep): teleReceiver.JsonTestStepStart {
return {
id: (step as any)[idSymbol],
parentStepId: (step.parent as any)?.[idSymbol],
id: (step as any)[this._idSymbol],
parentStepId: (step.parent as any)?.[this._idSymbol],
title: step.title,
category: step.category,
startTime: +step.startTime,
@ -255,7 +258,7 @@ export class TeleReporterEmitter implements ReporterV2 {
private _serializeStepEnd(step: reporterTypes.TestStep): teleReceiver.JsonTestStepEnd {
return {
id: (step as any)[idSymbol],
id: (step as any)[this._idSymbol],
duration: step.duration,
error: step.error,
};
@ -280,5 +283,3 @@ export class TeleReporterEmitter implements ReporterV2 {
return path.relative(this._rootDir, absolutePath);
}
}
const idSymbol = Symbol('id');

View file

@ -266,8 +266,9 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
/**
* Filter to only run tests with a title matching one of the patterns. For example, passing `grep: /cart/` should only
* run tests with "cart" in the title. Also available globally and in the [command line](https://playwright.dev/docs/test-cli) with the `-g`
* option. The regular expression will be tested against the string that consists of the test file name,
* `test.describe` name (if any) and the test name divided by spaces, e.g. `my-test.spec.ts my-suite my-test`.
* option. The regular expression will be tested against the string that consists of the project name, the test file
* name, the `test.describe` name (if any), the test name and the test tags divided by spaces, e.g. `chromium
* my-test.spec.ts my-suite my-test`.
*
* `grep` option is also useful for [tagging tests](https://playwright.dev/docs/test-annotations#tag-tests).
*/
@ -1130,8 +1131,9 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
/**
* Filter to only run tests with a title matching one of the patterns. For example, passing `grep: /cart/` should only
* run tests with "cart" in the title. Also available in the [command line](https://playwright.dev/docs/test-cli) with the `-g` option. The
* regular expression will be tested against the string that consists of the test file name, `test.describe` name (if
* any) and the test name divided by spaces, e.g. `my-test.spec.ts my-suite my-test`.
* regular expression will be tested against the string that consists of the project name, the test file name, the
* `test.describe` name (if any), the test name and the test tags divided by spaces, e.g. `chromium my-test.spec.ts
* my-suite my-test`.
*
* `grep` option is also useful for [tagging tests](https://playwright.dev/docs/test-annotations#tag-tests).
*
@ -5088,7 +5090,7 @@ export interface PlaywrightWorkerOptions {
* - `'on-first-retry'`: Record trace only when retrying a test for the first time.
* - `'on-all-retries'`: Record trace only when retrying a test.
* - `'retain-on-failure'`: Record trace for each test. When test run passes, remove the recorded trace.
* - `'retain-on-first-failure'`: Record trace for the first run of each test, but not for retires. When test run
* - `'retain-on-first-failure'`: Record trace for the first run of each test, but not for retries. When test run
* passes, remove the recorded trace.
*
* For more control, pass an object that specifies `mode` and trace features to enable.

View file

@ -26,7 +26,7 @@
const traceUrl = new URL(location.href).searchParams.get('trace');
const params = new URLSearchParams();
params.set('trace', traceUrl);
await fetch('context?' + params.toString()).then(r => r.json());
await fetch('contexts?' + params.toString()).then(r => r.json());
await location.reload();
})();
</script>

View file

@ -30,6 +30,17 @@ test('android.launchServer should connect to a device', async ({ playwright }) =
await browserServer.close();
});
test('android.launchServer should work with host', async ({ playwright }) => {
const host = '0.0.0.0';
const browserServer = await playwright._android.launchServer({ host });
expect(browserServer.wsEndpoint()).toContain(String(host));
const device = await playwright._android.connect(browserServer.wsEndpoint());
const output = await device.shell('echo 123');
expect(output.toString()).toBe('123\n');
await device.close();
await browserServer.close();
});
test('android.launchServer should handle close event correctly', async ({ playwright }) => {
const receivedEvents: string[] = [];
const browserServer = await playwright._android.launchServer();

View file

@ -4,14 +4,17 @@ import Button from '../src/components/Button.vue';
import '../src/assets/index.css';
export type HooksConfig = {
route?: string;
routing?: boolean;
components?: Record<string, any>;
}
beforeMount<HooksConfig>(async ({ app, hooksConfig }) => {
if (hooksConfig?.routing)
if (hooksConfig?.routing)
app.use(router as any); // TODO: remove any and fix the various installed conflicting Vue versions
app.component('Button', Button);
for (const [name, component] of Object.entries(hooksConfig?.components || {}))
app.component(name, component);
console.log(`Before mount: ${JSON.stringify(hooksConfig)}, app: ${!!app}`);
});

View file

@ -1,6 +1,7 @@
import { test, expect } from '@playwright/experimental-ct-vue';
import DefaultSlot from '@/components/DefaultSlot.vue';
import NamedSlots from '@/components/NamedSlots.vue';
import Button from '@/components/Button.vue';
test('render a default slot', async ({ mount }) => {
const component = await mount(DefaultSlot, {
@ -16,6 +17,9 @@ test('render a component as slot', async ({ mount }) => {
slots: {
default: '<Button title="Submit" />', // component is registered globally in /playwright/index.ts
},
hooksConfig: {
components: { Button }
}
});
await expect(component).toContainText('Submit');
});

View file

@ -1,6 +1,7 @@
import { test, expect } from '@playwright/experimental-ct-vue';
import DefaultSlot from '@/components/DefaultSlot.vue';
import NamedSlots from '@/components/NamedSlots.vue';
import Button from '@/components/Button.vue';
test('render a default slot', async ({ mount }) => {
const component = await mount(DefaultSlot, {
@ -16,6 +17,9 @@ test('render a component as slot', async ({ mount }) => {
slots: {
default: '<Button title="Submit" />', // component is registered globally in /playwright/index.ts
},
hooksConfig: {
components: { Button }
}
});
await expect(component).toContainText('Submit');
});

View file

@ -26,6 +26,13 @@ it.describe('launch server', () => {
await browserServer.close();
});
it('should work with host', async ({ browserType }) => {
const host = '0.0.0.0';
const browserServer = await browserType.launchServer({ host });
expect(browserServer.wsEndpoint()).toContain(String(host));
await browserServer.close();
});
it('should work with port', async ({ browserType }, testInfo) => {
const port = 8800 + testInfo.workerIndex;
const browserServer = await browserType.launchServer({ port });

View file

@ -68,7 +68,7 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s
// Go through instrumentation to exercise reentrant stack traces.
const csi = {
onWillCloseBrowserContext: async () => {
runBeforeCloseBrowserContext: async () => {
await page.hover('body');
await page.close();
traceFile = path.join(workerInfo.project.outputDir, String(workerInfo.workerIndex), browserName, 'trace.zip');
@ -1207,3 +1207,20 @@ test('should remove noscript when javaScriptEnabled is set to true', async ({ br
await expect(frame.getByText('Always visible')).toBeVisible();
await expect(frame.getByText('Enable JavaScript to run this app.')).toBeHidden();
});
test('should open snapshot in new browser context', async ({ browser, page, runAndTrace, server }) => {
const traceViewer = await runAndTrace(async () => {
await page.goto(server.EMPTY_PAGE);
await page.setContent('hello');
});
await traceViewer.snapshotFrame('page.setContent');
const popupPromise = traceViewer.page.context().waitForEvent('page');
await traceViewer.page.getByTitle('Open snapshot in a new tab').click();
const popup = await popupPromise;
// doesn't share sw.bundle.js
const newPage = await browser.newPage();
await newPage.goto(popup.url());
await expect(newPage.getByText('hello')).toBeVisible();
await newPage.close();
});

View file

@ -292,6 +292,38 @@ test('should merge blob into blob', async ({ runInlineTest, mergeReports, showRe
}
});
test('should produce consistent step ids', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31023' },
}, async ({ runInlineTest, mergeReports, showReport, page }) => {
const files = {
'playwright.config.ts': `
module.exports = {
retries: 1,
reporter: [
['blob', { outputFile: 'blob-report/report-1.zip' }],
['blob', { outputFile: 'blob-report/report-2.zip' }]
]
};
`,
'a.test.js': `
import { test, expect } from '@playwright/test';
test('math 1', async ({}) => {
expect(1 + 1).toBe(2);
});
`
};
await runInlineTest(files);
const reportDir = test.info().outputPath('blob-report');
const reportFiles = await fs.promises.readdir(reportDir);
reportFiles.sort();
expect(reportFiles).toEqual(['report-1.zip', 'report-2.zip']);
const { exitCode } = await mergeReports(reportDir, { 'PLAYWRIGHT_HTML_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html,json'] });
expect(exitCode).toBe(0);
await showReport();
await expect(page.locator('.subnav-item:has-text("All") .counter')).toHaveText('2');
await expect(page.locator('.subnav-item:has-text("Passed") .counter')).toHaveText('2');
});
test('be able to merge incomplete shards', async ({ runInlineTest, mergeReports, showReport, page }) => {
const reportDir = test.info().outputPath('blob-report');
const files = {