Merge branch 'main' into teststep-attachments
This commit is contained in:
commit
acb0b9c12f
8
.github/workflows/tests_bidi.yml
vendored
8
.github/workflows/tests_bidi.yml
vendored
|
|
@ -7,6 +7,7 @@ on:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/tests_bidi.yml
|
- .github/workflows/tests_bidi.yml
|
||||||
|
- packages/playwright-core/src/server/bidi/*
|
||||||
schedule:
|
schedule:
|
||||||
# Run every day at midnight
|
# Run every day at midnight
|
||||||
- cron: '0 0 * * *'
|
- cron: '0 0 * * *'
|
||||||
|
|
@ -43,3 +44,10 @@ jobs:
|
||||||
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run biditest -- --project=${{ matrix.channel }}*
|
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run biditest -- --project=${{ matrix.channel }}*
|
||||||
env:
|
env:
|
||||||
PWTEST_USE_BIDI_EXPECTATIONS: '1'
|
PWTEST_USE_BIDI_EXPECTATIONS: '1'
|
||||||
|
- name: Upload csv report to GitHub
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: csv-report
|
||||||
|
path: test-results/report.csv
|
||||||
|
retention-days: 7
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ In case this browser is connected to, clears all created contexts belonging to t
|
||||||
browser server.
|
browser server.
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
This is similar to force quitting the browser. Therefore, you should call [`method: BrowserContext.close`] on any [BrowserContext]'s you explicitly created earlier with [`method: Browser.newContext`] **before** calling [`method: Browser.close`].
|
This is similar to force-quitting the browser. To close pages gracefully and ensure you receive page close events, call [`method: BrowserContext.close`] on any [BrowserContext] instances you explicitly created earlier using [`method: Browser.newContext`] **before** calling [`method: Browser.close`].
|
||||||
:::
|
:::
|
||||||
|
|
||||||
The [Browser] object itself is considered to be disposed and cannot be used anymore.
|
The [Browser] object itself is considered to be disposed and cannot be used anymore.
|
||||||
|
|
|
||||||
|
|
@ -1773,6 +1773,112 @@ Specifies a custom location for the step to be shown in test reports and trace v
|
||||||
|
|
||||||
Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout).
|
Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout).
|
||||||
|
|
||||||
|
## async method: Test.step.fail
|
||||||
|
* since: v1.50
|
||||||
|
- returns: <[void]>
|
||||||
|
|
||||||
|
Marks a test step as "should fail". Playwright runs this test step and ensures that it actually fails. This is useful for documentation purposes to acknowledge that some functionality is broken until it is fixed.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
If the step exceeds the timeout, a [TimeoutError] is thrown. This indicates the step did not fail as expected.
|
||||||
|
:::
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
You can declare a test step as failing, so that Playwright ensures it actually fails.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('my test', async ({ page }) => {
|
||||||
|
// ...
|
||||||
|
await test.step.fail('currently failing', async () => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### param: Test.step.fail.title
|
||||||
|
* since: v1.50
|
||||||
|
- `title` <[string]>
|
||||||
|
|
||||||
|
Step name.
|
||||||
|
|
||||||
|
### param: Test.step.fail.body
|
||||||
|
* since: v1.50
|
||||||
|
- `body` <[function]\(\):[Promise]<[any]>>
|
||||||
|
|
||||||
|
Step body.
|
||||||
|
|
||||||
|
### option: Test.step.fail.box
|
||||||
|
* since: v1.50
|
||||||
|
- `box` <boolean>
|
||||||
|
|
||||||
|
Whether to box the step in the report. Defaults to `false`. When the step is boxed, errors thrown from the step internals point to the step call site. See below for more details.
|
||||||
|
|
||||||
|
### option: Test.step.fail.location
|
||||||
|
* since: v1.50
|
||||||
|
- `location` <[Location]>
|
||||||
|
|
||||||
|
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.fail.timeout
|
||||||
|
* since: v1.50
|
||||||
|
- `timeout` <[float]>
|
||||||
|
|
||||||
|
Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout).
|
||||||
|
|
||||||
|
## async method: Test.step.fixme
|
||||||
|
* since: v1.50
|
||||||
|
- returns: <[void]>
|
||||||
|
|
||||||
|
Mark a test step as "fixme", with the intention to fix it. Playwright will not run the step.
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
You can declare a test step as failing, so that Playwright ensures it actually fails.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('my test', async ({ page }) => {
|
||||||
|
// ...
|
||||||
|
await test.step.fixme('not yet ready', async () => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### param: Test.step.fixme.title
|
||||||
|
* since: v1.50
|
||||||
|
- `title` <[string]>
|
||||||
|
|
||||||
|
Step name.
|
||||||
|
|
||||||
|
### param: Test.step.fixme.body
|
||||||
|
* since: v1.50
|
||||||
|
- `body` <[function]\(\):[Promise]<[any]>>
|
||||||
|
|
||||||
|
Step body.
|
||||||
|
|
||||||
|
### option: Test.step.fixme.box
|
||||||
|
* since: v1.50
|
||||||
|
- `box` <boolean>
|
||||||
|
|
||||||
|
Whether to box the step in the report. Defaults to `false`. When the step is boxed, errors thrown from the step internals point to the step call site. See below for more details.
|
||||||
|
|
||||||
|
### option: Test.step.fixme.location
|
||||||
|
* since: v1.50
|
||||||
|
- `location` <[Location]>
|
||||||
|
|
||||||
|
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.fixme.timeout
|
||||||
|
* since: v1.50
|
||||||
|
- `timeout` <[float]>
|
||||||
|
|
||||||
|
Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout).
|
||||||
|
|
||||||
## method: Test.use
|
## method: Test.use
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,13 @@ You can use the `globalSetup` option in the [configuration file](./test-configur
|
||||||
Similarly, use `globalTeardown` to run something once after all the tests. Alternatively, let `globalSetup` return a function that will be used as a global teardown. You can pass data such as port number, authentication tokens, etc. from your global setup to your tests using environment variables.
|
Similarly, use `globalTeardown` to run something once after all the tests. Alternatively, let `globalSetup` return a function that will be used as a global teardown. You can pass data such as port number, authentication tokens, etc. from your global setup to your tests using environment variables.
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
Using `globalSetup` and `globalTeardown` will not produce traces or artifacts, and options like `headless` or `testIdAttribute` specified in the config file are not applied. If you want to produce traces and artifacts and respect config options, use [project dependencies](#option-1-project-dependencies).
|
Beware of `globalSetup` and `globalTeardown` caveats:
|
||||||
|
|
||||||
|
- These methods will not produce traces or artifacts unless explictly enabled, as described in [Capturing trace of failures during global setup](#capturing-trace-of-failures-during-global-setup).
|
||||||
|
- Options sush as `headless` or `testIdAttribute` specified in the config file are not applied,
|
||||||
|
- An uncaught exception thrown in `globalSetup` will prevent Playwright from running tests, and no test results will appear in reporters.
|
||||||
|
|
||||||
|
Consider using [project dependencies](#option-1-project-dependencies) to produce traces, artifacts, respect config options and get test results in reporters even in case of a setup failure.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
```js title="playwright.config.ts"
|
```js title="playwright.config.ts"
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ export function useIsAnchored(id: AnchorID) {
|
||||||
export function Anchor({ id, children }: React.PropsWithChildren<{ id: AnchorID }>) {
|
export function Anchor({ id, children }: React.PropsWithChildren<{ id: AnchorID }>) {
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
const onAnchorReveal = React.useCallback(() => {
|
const onAnchorReveal = React.useCallback(() => {
|
||||||
requestAnimationFrame(() => ref.current?.scrollIntoView({ block: 'start', inline: 'start' }));
|
ref.current?.scrollIntoView({ block: 'start', inline: 'start' });
|
||||||
}, []);
|
}, []);
|
||||||
useAnchor(id, onAnchorReveal);
|
useAnchor(id, onAnchorReveal);
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,2 +0,0 @@
|
||||||
See building instructions at [`/browser_patches/winldd/README.md`](../../../browser_patches/winldd/README.md)
|
|
||||||
|
|
||||||
|
|
@ -9,9 +9,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "chromium-tip-of-tree",
|
"name": "chromium-tip-of-tree",
|
||||||
"revision": "1286",
|
"revision": "1288",
|
||||||
"installByDefault": false,
|
"installByDefault": false,
|
||||||
"browserVersion": "133.0.6891.0"
|
"browserVersion": "133.0.6905.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "firefox",
|
"name": "firefox",
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "webkit",
|
"name": "webkit",
|
||||||
"revision": "2119",
|
"revision": "2120",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"revisionOverrides": {
|
"revisionOverrides": {
|
||||||
"debian11-x64": "2105",
|
"debian11-x64": "2105",
|
||||||
|
|
@ -45,13 +45,18 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ffmpeg",
|
"name": "ffmpeg",
|
||||||
"revision": "1010",
|
"revision": "1011",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"revisionOverrides": {
|
"revisionOverrides": {
|
||||||
"mac12": "1010",
|
"mac12": "1010",
|
||||||
"mac12-arm64": "1010"
|
"mac12-arm64": "1010"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "winldd",
|
||||||
|
"revision": "1007",
|
||||||
|
"installByDefault": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "android",
|
"name": "android",
|
||||||
"revision": "1001",
|
"revision": "1001",
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,9 @@ function checkBrowsersToInstall(args: string[], options: { noShell?: boolean, on
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32')
|
||||||
|
executables.push(registry.findExecutable('winldd')!);
|
||||||
|
|
||||||
if (faultyArguments.length)
|
if (faultyArguments.length)
|
||||||
throw new Error(`Invalid installation targets: ${faultyArguments.map(name => `'${name}'`).join(', ')}. Expecting one of: ${suggestedBrowsersToInstall()}`);
|
throw new Error(`Invalid installation targets: ${faultyArguments.map(name => `'${name}'`).join(', ')}. Expecting one of: ${suggestedBrowsersToInstall()}`);
|
||||||
return executables;
|
return executables;
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import { debugLogger } from '../utils/debugLogger';
|
||||||
export type ClientType = 'controller' | 'launch-browser' | 'reuse-browser' | 'pre-launched-browser-or-android';
|
export type ClientType = 'controller' | 'launch-browser' | 'reuse-browser' | 'pre-launched-browser-or-android';
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
|
allowFSPaths: boolean,
|
||||||
socksProxyPattern: string | undefined,
|
socksProxyPattern: string | undefined,
|
||||||
browserName: string | null,
|
browserName: string | null,
|
||||||
launchOptions: LaunchOptions,
|
launchOptions: LaunchOptions,
|
||||||
|
|
@ -60,7 +61,7 @@ export class PlaywrightConnection {
|
||||||
this._ws = ws;
|
this._ws = ws;
|
||||||
this._preLaunched = preLaunched;
|
this._preLaunched = preLaunched;
|
||||||
this._options = options;
|
this._options = options;
|
||||||
options.launchOptions = filterLaunchOptions(options.launchOptions);
|
options.launchOptions = filterLaunchOptions(options.launchOptions, options.allowFSPaths);
|
||||||
if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser-or-android')
|
if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser-or-android')
|
||||||
assert(preLaunched.playwright);
|
assert(preLaunched.playwright);
|
||||||
if (clientType === 'pre-launched-browser-or-android')
|
if (clientType === 'pre-launched-browser-or-android')
|
||||||
|
|
@ -284,7 +285,7 @@ function launchOptionsHash(options: LaunchOptions) {
|
||||||
return JSON.stringify(copy);
|
return JSON.stringify(copy);
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterLaunchOptions(options: LaunchOptions): LaunchOptions {
|
function filterLaunchOptions(options: LaunchOptions, allowFSPaths: boolean): LaunchOptions {
|
||||||
return {
|
return {
|
||||||
channel: options.channel,
|
channel: options.channel,
|
||||||
args: options.args,
|
args: options.args,
|
||||||
|
|
@ -296,7 +297,8 @@ function filterLaunchOptions(options: LaunchOptions): LaunchOptions {
|
||||||
chromiumSandbox: options.chromiumSandbox,
|
chromiumSandbox: options.chromiumSandbox,
|
||||||
firefoxUserPrefs: options.firefoxUserPrefs,
|
firefoxUserPrefs: options.firefoxUserPrefs,
|
||||||
slowMo: options.slowMo,
|
slowMo: options.slowMo,
|
||||||
executablePath: isUnderTest() ? options.executablePath : undefined,
|
executablePath: (isUnderTest() || allowFSPaths) ? options.executablePath : undefined,
|
||||||
|
downloadsPath: allowFSPaths ? options.downloadsPath : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ export class PlaywrightServer {
|
||||||
return new PlaywrightConnection(
|
return new PlaywrightConnection(
|
||||||
semaphore.acquire(),
|
semaphore.acquire(),
|
||||||
clientType, ws,
|
clientType, ws,
|
||||||
{ socksProxyPattern: proxyValue, browserName, launchOptions },
|
{ socksProxyPattern: proxyValue, browserName, launchOptions, allowFSPaths: this._options.mode === 'extension' },
|
||||||
{
|
{
|
||||||
playwright: this._preLaunchedPlaywright,
|
playwright: this._preLaunchedPlaywright,
|
||||||
browser: this._options.preLaunchedBrowser,
|
browser: this._options.preLaunchedBrowser,
|
||||||
|
|
|
||||||
|
|
@ -399,7 +399,7 @@ export class BidiPage implements PageDelegate {
|
||||||
context: this._session.sessionId,
|
context: this._session.sessionId,
|
||||||
format: {
|
format: {
|
||||||
type: `image/${format === 'png' ? 'png' : 'jpeg'}`,
|
type: `image/${format === 'png' ? 'png' : 'jpeg'}`,
|
||||||
quality: quality || 80,
|
quality: quality ? quality / 100 : 0.8,
|
||||||
},
|
},
|
||||||
origin: documentRect ? 'document' : 'viewport',
|
origin: documentRect ? 'document' : 'viewport',
|
||||||
clip: {
|
clip: {
|
||||||
|
|
|
||||||
|
|
@ -171,8 +171,10 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
||||||
using var playwright = await Playwright.CreateAsync();
|
using var playwright = await Playwright.CreateAsync();
|
||||||
await using var browser = await playwright.${toPascal(options.browserName)}.LaunchAsync(${formatObject(options.launchOptions, ' ', 'BrowserTypeLaunchOptions')});
|
await using var browser = await playwright.${toPascal(options.browserName)}.LaunchAsync(${formatObject(options.launchOptions, ' ', 'BrowserTypeLaunchOptions')});
|
||||||
var context = await browser.NewContextAsync(${formatContextOptions(options.contextOptions, options.deviceName)});`);
|
var context = await browser.NewContextAsync(${formatContextOptions(options.contextOptions, options.deviceName)});`);
|
||||||
if (options.contextOptions.recordHar)
|
if (options.contextOptions.recordHar) {
|
||||||
formatter.add(` await context.RouteFromHARAsync(${quote(options.contextOptions.recordHar.path)});`);
|
const url = options.contextOptions.recordHar.urlFilter;
|
||||||
|
formatter.add(` await context.RouteFromHARAsync(${quote(options.contextOptions.recordHar.path)}${url ? `, ${formatObject({ url }, ' ', 'BrowserContextRouteFromHAROptions')}` : ''});`);
|
||||||
|
}
|
||||||
formatter.newLine();
|
formatter.newLine();
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
|
|
@ -198,8 +200,10 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
||||||
formatter.add(` [${this._mode === 'nunit' ? 'Test' : 'TestMethod'}]
|
formatter.add(` [${this._mode === 'nunit' ? 'Test' : 'TestMethod'}]
|
||||||
public async Task MyTest()
|
public async Task MyTest()
|
||||||
{`);
|
{`);
|
||||||
if (options.contextOptions.recordHar)
|
if (options.contextOptions.recordHar) {
|
||||||
formatter.add(` await context.RouteFromHARAsync(${quote(options.contextOptions.recordHar.path)});`);
|
const url = options.contextOptions.recordHar.urlFilter;
|
||||||
|
formatter.add(` await Context.RouteFromHARAsync(${quote(options.contextOptions.recordHar.path)}${url ? `, ${formatObject({ url }, ' ', 'BrowserContextRouteFromHAROptions')}` : ''});`);
|
||||||
|
}
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -150,28 +150,38 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
||||||
import com.microsoft.playwright.Page;
|
import com.microsoft.playwright.Page;
|
||||||
import com.microsoft.playwright.options.*;
|
import com.microsoft.playwright.options.*;
|
||||||
|
|
||||||
import org.junit.jupiter.api.*;
|
${options.contextOptions.recordHar ? `import java.nio.file.Paths;\n` : ''}import org.junit.jupiter.api.*;
|
||||||
import static com.microsoft.playwright.assertions.PlaywrightAssertions.*;
|
import static com.microsoft.playwright.assertions.PlaywrightAssertions.*;
|
||||||
|
|
||||||
@UsePlaywright
|
@UsePlaywright
|
||||||
public class TestExample {
|
public class TestExample {
|
||||||
@Test
|
@Test
|
||||||
void test(Page page) {`);
|
void test(Page page) {`);
|
||||||
|
if (options.contextOptions.recordHar) {
|
||||||
|
const url = options.contextOptions.recordHar.urlFilter;
|
||||||
|
const recordHarOptions = typeof url === 'string' ? `, new Page.RouteFromHAROptions()
|
||||||
|
.setUrl(${quote(url)})` : '';
|
||||||
|
formatter.add(` page.routeFromHAR(Paths.get(${quote(options.contextOptions.recordHar.path)})${recordHarOptions});`);
|
||||||
|
}
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
formatter.add(`
|
formatter.add(`
|
||||||
import com.microsoft.playwright.*;
|
import com.microsoft.playwright.*;
|
||||||
import com.microsoft.playwright.options.*;
|
import com.microsoft.playwright.options.*;
|
||||||
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
|
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
|
||||||
import java.util.*;
|
${options.contextOptions.recordHar ? `import java.nio.file.Paths;\n` : ''}import java.util.*;
|
||||||
|
|
||||||
public class Example {
|
public class Example {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
try (Playwright playwright = Playwright.create()) {
|
try (Playwright playwright = Playwright.create()) {
|
||||||
Browser browser = playwright.${options.browserName}().launch(${formatLaunchOptions(options.launchOptions)});
|
Browser browser = playwright.${options.browserName}().launch(${formatLaunchOptions(options.launchOptions)});
|
||||||
BrowserContext context = browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName)});`);
|
BrowserContext context = browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName)});`);
|
||||||
if (options.contextOptions.recordHar)
|
if (options.contextOptions.recordHar) {
|
||||||
formatter.add(` context.routeFromHAR(${quote(options.contextOptions.recordHar.path)});`);
|
const url = options.contextOptions.recordHar.urlFilter;
|
||||||
|
const recordHarOptions = typeof url === 'string' ? `, new BrowserContext.RouteFromHAROptions()
|
||||||
|
.setUrl(${quote(url)})` : '';
|
||||||
|
formatter.add(` context.routeFromHAR(Paths.get(${quote(options.contextOptions.recordHar.path)})${recordHarOptions});`);
|
||||||
|
}
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,8 +147,10 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
||||||
import { test, expect${options.deviceName ? ', devices' : ''} } from '@playwright/test';
|
import { test, expect${options.deviceName ? ', devices' : ''} } from '@playwright/test';
|
||||||
${useText ? '\ntest.use(' + useText + ');\n' : ''}
|
${useText ? '\ntest.use(' + useText + ');\n' : ''}
|
||||||
test('test', async ({ page }) => {`);
|
test('test', async ({ page }) => {`);
|
||||||
if (options.contextOptions.recordHar)
|
if (options.contextOptions.recordHar) {
|
||||||
formatter.add(` await page.routeFromHAR(${quote(options.contextOptions.recordHar.path)});`);
|
const url = options.contextOptions.recordHar.urlFilter;
|
||||||
|
formatter.add(` await page.routeFromHAR(${quote(options.contextOptions.recordHar.path)}${url ? `, ${formatOptions({ url }, false)}` : ''});`);
|
||||||
|
}
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
||||||
|
|
||||||
generateHeader(options: LanguageGeneratorOptions): string {
|
generateHeader(options: LanguageGeneratorOptions): string {
|
||||||
const formatter = new PythonFormatter();
|
const formatter = new PythonFormatter();
|
||||||
|
const recordHar = options.contextOptions.recordHar;
|
||||||
if (this._isPyTest) {
|
if (this._isPyTest) {
|
||||||
const contextOptions = formatContextOptions(options.contextOptions, options.deviceName, true /* asDict */);
|
const contextOptions = formatContextOptions(options.contextOptions, options.deviceName, true /* asDict */);
|
||||||
const fixture = contextOptions ? `
|
const fixture = contextOptions ? `
|
||||||
|
|
@ -146,13 +147,13 @@ def browser_context_args(browser_context_args, playwright) {
|
||||||
return {${contextOptions}}
|
return {${contextOptions}}
|
||||||
}
|
}
|
||||||
` : '';
|
` : '';
|
||||||
formatter.add(`${options.deviceName ? 'import pytest\n' : ''}import re
|
formatter.add(`${options.deviceName || contextOptions ? 'import pytest\n' : ''}import re
|
||||||
from playwright.sync_api import Page, expect
|
from playwright.sync_api import Page, expect
|
||||||
${fixture}
|
${fixture}
|
||||||
|
|
||||||
def test_example(page: Page) -> None {`);
|
def test_example(page: Page) -> None {`);
|
||||||
if (options.contextOptions.recordHar)
|
if (recordHar)
|
||||||
formatter.add(` page.route_from_har(${quote(options.contextOptions.recordHar.path)})`);
|
formatter.add(` page.route_from_har(${quote(recordHar.path)}${typeof recordHar.urlFilter === 'string' ? `, url=${quote(recordHar.urlFilter)}` : ''})`);
|
||||||
} else if (this._isAsync) {
|
} else if (this._isAsync) {
|
||||||
formatter.add(`
|
formatter.add(`
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
@ -163,8 +164,8 @@ from playwright.async_api import Playwright, async_playwright, expect
|
||||||
async def run(playwright: Playwright) -> None {
|
async def run(playwright: Playwright) -> None {
|
||||||
browser = await playwright.${options.browserName}.launch(${formatOptions(options.launchOptions, false)})
|
browser = await playwright.${options.browserName}.launch(${formatOptions(options.launchOptions, false)})
|
||||||
context = await browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
|
context = await browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
|
||||||
if (options.contextOptions.recordHar)
|
if (recordHar)
|
||||||
formatter.add(` await page.route_from_har(${quote(options.contextOptions.recordHar.path)})`);
|
formatter.add(` await context.route_from_har(${quote(recordHar.path)}${typeof recordHar.urlFilter === 'string' ? `, url=${quote(recordHar.urlFilter)}` : ''})`);
|
||||||
} else {
|
} else {
|
||||||
formatter.add(`
|
formatter.add(`
|
||||||
import re
|
import re
|
||||||
|
|
@ -174,8 +175,8 @@ from playwright.sync_api import Playwright, sync_playwright, expect
|
||||||
def run(playwright: Playwright) -> None {
|
def run(playwright: Playwright) -> None {
|
||||||
browser = playwright.${options.browserName}.launch(${formatOptions(options.launchOptions, false)})
|
browser = playwright.${options.browserName}.launch(${formatOptions(options.launchOptions, false)})
|
||||||
context = browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
|
context = browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
|
||||||
if (options.contextOptions.recordHar)
|
if (recordHar)
|
||||||
formatter.add(` context.route_from_har(${quote(options.contextOptions.recordHar.path)})`);
|
formatter.add(` context.route_from_har(${quote(recordHar.path)}${typeof recordHar.urlFilter === 'string' ? `, url=${quote(recordHar.urlFilter)}` : ''})`);
|
||||||
}
|
}
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -457,7 +457,8 @@ export class InjectedScript {
|
||||||
const queryAll = (root: SelectorRoot, body: string) => {
|
const queryAll = (root: SelectorRoot, body: string) => {
|
||||||
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
||||||
return [];
|
return [];
|
||||||
return isElementVisible(root as Element) === Boolean(body) ? [root as Element] : [];
|
const visible = body === 'true';
|
||||||
|
return isElementVisible(root as Element) === visible ? [root as Element] : [];
|
||||||
};
|
};
|
||||||
return { queryAll };
|
return { queryAll };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,10 @@ function yamlStringNeedsQuotes(str: string): boolean {
|
||||||
if (/[{}`]/.test(str))
|
if (/[{}`]/.test(str))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
// YAML array starts with [
|
||||||
|
if (/^\[/.test(str))
|
||||||
|
return true;
|
||||||
|
|
||||||
// Non-string types recognized by YAML
|
// Non-string types recognized by YAML
|
||||||
if (!isNaN(Number(str)) || ['y', 'n', 'yes', 'no', 'true', 'false', 'on', 'off', 'null'].includes(str.toLowerCase()))
|
if (!isNaN(Number(str)) || ['y', 'n', 'yes', 'no', 'true', 'false', 'on', 'off', 'null'].includes(str.toLowerCase()))
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -300,7 +300,6 @@ async function generateFrameSelectorInParent(parent: Frame, frame: Frame): Promi
|
||||||
}, frameElement);
|
}, frameElement);
|
||||||
return selector;
|
return selector;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return e.toString();
|
|
||||||
}
|
}
|
||||||
}, monotonicTime() + 2000);
|
}, monotonicTime() + 2000);
|
||||||
if (!result.timedOut && result.result)
|
if (!result.timedOut && result.result)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import childProcess from 'child_process';
|
||||||
import * as utils from '../../utils';
|
import * as utils from '../../utils';
|
||||||
import { spawnAsync } from '../../utils/spawnAsync';
|
import { spawnAsync } from '../../utils/spawnAsync';
|
||||||
import { hostPlatform, isOfficiallySupportedPlatform } from '../../utils/hostPlatform';
|
import { hostPlatform, isOfficiallySupportedPlatform } from '../../utils/hostPlatform';
|
||||||
import { buildPlaywrightCLICommand } from '.';
|
import { buildPlaywrightCLICommand, registry } from '.';
|
||||||
import { deps } from './nativeDeps';
|
import { deps } from './nativeDeps';
|
||||||
import { getPlaywrightVersion } from '../../utils/userAgent';
|
import { getPlaywrightVersion } from '../../utils/userAgent';
|
||||||
|
|
||||||
|
|
@ -122,12 +122,12 @@ export async function installDependenciesLinux(targets: Set<DependencyGroup>, dr
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateDependenciesWindows(windowsExeAndDllDirectories: string[]) {
|
export async function validateDependenciesWindows(sdkLanguage: string, windowsExeAndDllDirectories: string[]) {
|
||||||
const directoryPaths = windowsExeAndDllDirectories;
|
const directoryPaths = windowsExeAndDllDirectories;
|
||||||
const lddPaths: string[] = [];
|
const lddPaths: string[] = [];
|
||||||
for (const directoryPath of directoryPaths)
|
for (const directoryPath of directoryPaths)
|
||||||
lddPaths.push(...(await executablesOrSharedLibraries(directoryPath)));
|
lddPaths.push(...(await executablesOrSharedLibraries(directoryPath)));
|
||||||
const allMissingDeps = await Promise.all(lddPaths.map(lddPath => missingFileDependenciesWindows(lddPath)));
|
const allMissingDeps = await Promise.all(lddPaths.map(lddPath => missingFileDependenciesWindows(sdkLanguage, lddPath)));
|
||||||
const missingDeps: Set<string> = new Set();
|
const missingDeps: Set<string> = new Set();
|
||||||
for (const deps of allMissingDeps) {
|
for (const deps of allMissingDeps) {
|
||||||
for (const dep of deps)
|
for (const dep of deps)
|
||||||
|
|
@ -302,8 +302,8 @@ async function executablesOrSharedLibraries(directoryPath: string): Promise<stri
|
||||||
return executablersOrLibraries as string[];
|
return executablersOrLibraries as string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function missingFileDependenciesWindows(filePath: string): Promise<Array<string>> {
|
async function missingFileDependenciesWindows(sdkLanguage: string, filePath: string): Promise<Array<string>> {
|
||||||
const executable = path.join(__dirname, '..', '..', '..', 'bin', 'PrintDeps.exe');
|
const executable = registry.findExecutable('winldd')!.executablePathOrDie(sdkLanguage);
|
||||||
const dirname = path.dirname(filePath);
|
const dirname = path.dirname(filePath);
|
||||||
const { stdout, code } = await spawnAsync(executable, [filePath], {
|
const { stdout, code } = await spawnAsync(executable, [filePath], {
|
||||||
cwd: dirname,
|
cwd: dirname,
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,9 @@ const PACKAGE_PATH = path.join(__dirname, '..', '..', '..');
|
||||||
const BIN_PATH = path.join(__dirname, '..', '..', '..', 'bin');
|
const BIN_PATH = path.join(__dirname, '..', '..', '..', 'bin');
|
||||||
|
|
||||||
const PLAYWRIGHT_CDN_MIRRORS = [
|
const PLAYWRIGHT_CDN_MIRRORS = [
|
||||||
'https://playwright.azureedge.net/dbazure/download/playwright', // ESRP CDN
|
'https://cdn.playwright.dev/dbazure/download/playwright', // ESRP CDN
|
||||||
'https://playwright.download.prss.microsoft.com/dbazure/download/playwright', // Directly hit ESRP CDN
|
'https://playwright.download.prss.microsoft.com/dbazure/download/playwright', // Directly hit ESRP CDN
|
||||||
|
'https://cdn.playwright.dev', // Hit the Storage Bucket directly
|
||||||
// Old endpoints which hit the Storage Bucket directly:
|
|
||||||
'https://playwright.azureedge.net',
|
|
||||||
'https://playwright-akamai.azureedge.net', // Actually Edgio which will be retired Q4 2025.
|
|
||||||
'https://playwright-verizon.azureedge.net', // Actually Edgio which will be retired Q4 2025.
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (process.env.PW_TEST_CDN_THAT_SHOULD_WORK) {
|
if (process.env.PW_TEST_CDN_THAT_SHOULD_WORK) {
|
||||||
|
|
@ -83,6 +79,11 @@ const EXECUTABLE_PATHS = {
|
||||||
'mac': ['ffmpeg-mac'],
|
'mac': ['ffmpeg-mac'],
|
||||||
'win': ['ffmpeg-win64.exe'],
|
'win': ['ffmpeg-win64.exe'],
|
||||||
},
|
},
|
||||||
|
'winldd': {
|
||||||
|
'linux': undefined,
|
||||||
|
'mac': undefined,
|
||||||
|
'win': ['PrintDeps.exe'],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type DownloadPaths = Record<HostPlatform, string | undefined>;
|
type DownloadPaths = Record<HostPlatform, string | undefined>;
|
||||||
|
|
@ -319,6 +320,35 @@ const DOWNLOAD_PATHS: Record<BrowserName | InternalTool, DownloadPaths> = {
|
||||||
'mac15-arm64': 'builds/ffmpeg/%s/ffmpeg-mac-arm64.zip',
|
'mac15-arm64': 'builds/ffmpeg/%s/ffmpeg-mac-arm64.zip',
|
||||||
'win64': 'builds/ffmpeg/%s/ffmpeg-win64.zip',
|
'win64': 'builds/ffmpeg/%s/ffmpeg-win64.zip',
|
||||||
},
|
},
|
||||||
|
'winldd': {
|
||||||
|
'<unknown>': undefined,
|
||||||
|
'ubuntu18.04-x64': undefined,
|
||||||
|
'ubuntu20.04-x64': undefined,
|
||||||
|
'ubuntu22.04-x64': undefined,
|
||||||
|
'ubuntu24.04-x64': undefined,
|
||||||
|
'ubuntu18.04-arm64': undefined,
|
||||||
|
'ubuntu20.04-arm64': undefined,
|
||||||
|
'ubuntu22.04-arm64': undefined,
|
||||||
|
'ubuntu24.04-arm64': undefined,
|
||||||
|
'debian11-x64': undefined,
|
||||||
|
'debian11-arm64': undefined,
|
||||||
|
'debian12-x64': undefined,
|
||||||
|
'debian12-arm64': undefined,
|
||||||
|
'mac10.13': undefined,
|
||||||
|
'mac10.14': undefined,
|
||||||
|
'mac10.15': undefined,
|
||||||
|
'mac11': undefined,
|
||||||
|
'mac11-arm64': undefined,
|
||||||
|
'mac12': undefined,
|
||||||
|
'mac12-arm64': undefined,
|
||||||
|
'mac13': undefined,
|
||||||
|
'mac13-arm64': undefined,
|
||||||
|
'mac14': undefined,
|
||||||
|
'mac14-arm64': undefined,
|
||||||
|
'mac15': undefined,
|
||||||
|
'mac15-arm64': undefined,
|
||||||
|
'win64': 'builds/winldd/%s/winldd-win64.zip',
|
||||||
|
},
|
||||||
'android': {
|
'android': {
|
||||||
'<unknown>': 'builds/android/%s/android.zip',
|
'<unknown>': 'builds/android/%s/android.zip',
|
||||||
'ubuntu18.04-x64': undefined,
|
'ubuntu18.04-x64': undefined,
|
||||||
|
|
@ -446,7 +476,7 @@ function readDescriptors(browsersJSON: BrowsersJSON): BrowsersJSONDescriptor[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BrowserName = 'chromium' | 'firefox' | 'webkit' | 'bidi';
|
export type BrowserName = 'chromium' | 'firefox' | 'webkit' | 'bidi';
|
||||||
type InternalTool = 'ffmpeg' | 'firefox-beta' | 'chromium-tip-of-tree' | 'chromium-headless-shell' | 'chromium-tip-of-tree-headless-shell' | 'android';
|
type InternalTool = 'ffmpeg' | 'winldd' | 'firefox-beta' | 'chromium-tip-of-tree' | 'chromium-headless-shell' | 'chromium-tip-of-tree-headless-shell' | 'android';
|
||||||
type BidiChannel = 'bidi-firefox-stable' | 'bidi-firefox-beta' | 'bidi-firefox-nightly' | 'bidi-chrome-canary' | 'bidi-chrome-stable' | 'bidi-chromium';
|
type BidiChannel = 'bidi-firefox-stable' | 'bidi-firefox-beta' | 'bidi-firefox-nightly' | 'bidi-chrome-canary' | 'bidi-chrome-stable' | 'bidi-chromium';
|
||||||
type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary';
|
type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary';
|
||||||
const allDownloadable = ['android', 'chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree', 'chromium-headless-shell', 'chromium-tip-of-tree-headless-shell'];
|
const allDownloadable = ['android', 'chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree', 'chromium-headless-shell', 'chromium-tip-of-tree-headless-shell'];
|
||||||
|
|
@ -776,6 +806,22 @@ export class Registry {
|
||||||
_dependencyGroup: 'tools',
|
_dependencyGroup: 'tools',
|
||||||
_isHermeticInstallation: true,
|
_isHermeticInstallation: true,
|
||||||
});
|
});
|
||||||
|
const winldd = descriptors.find(d => d.name === 'winldd')!;
|
||||||
|
const winlddExecutable = findExecutablePath(winldd.dir, 'winldd');
|
||||||
|
this._executables.push({
|
||||||
|
type: 'tool',
|
||||||
|
name: 'winldd',
|
||||||
|
browserName: undefined,
|
||||||
|
directory: winldd.dir,
|
||||||
|
executablePath: () => winlddExecutable,
|
||||||
|
executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('winldd', winlddExecutable, winldd.installByDefault, sdkLanguage),
|
||||||
|
installType: process.platform === 'win32' ? 'download-by-default' : 'none',
|
||||||
|
_validateHostRequirements: () => Promise.resolve(),
|
||||||
|
downloadURLs: this._downloadURLs(winldd),
|
||||||
|
_install: () => this._downloadExecutable(winldd, winlddExecutable),
|
||||||
|
_dependencyGroup: 'tools',
|
||||||
|
_isHermeticInstallation: true,
|
||||||
|
});
|
||||||
const android = descriptors.find(d => d.name === 'android')!;
|
const android = descriptors.find(d => d.name === 'android')!;
|
||||||
this._executables.push({
|
this._executables.push({
|
||||||
type: 'tool',
|
type: 'tool',
|
||||||
|
|
@ -948,7 +994,7 @@ export class Registry {
|
||||||
if (os.platform() === 'linux')
|
if (os.platform() === 'linux')
|
||||||
return await validateDependenciesLinux(sdkLanguage, linuxLddDirectories.map(d => path.join(browserDirectory, d)), dlOpenLibraries);
|
return await validateDependenciesLinux(sdkLanguage, linuxLddDirectories.map(d => path.join(browserDirectory, d)), dlOpenLibraries);
|
||||||
if (os.platform() === 'win32' && os.arch() === 'x64')
|
if (os.platform() === 'win32' && os.arch() === 'x64')
|
||||||
return await validateDependenciesWindows(windowsExeAndDllDirectories.map(d => path.join(browserDirectory, d)));
|
return await validateDependenciesWindows(sdkLanguage, windowsExeAndDllDirectories.map(d => path.join(browserDirectory, d)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async installDeps(executablesToInstallDeps: Executable[], dryRun: boolean) {
|
async installDeps(executablesToInstallDeps: Executable[], dryRun: boolean) {
|
||||||
|
|
@ -1269,6 +1315,8 @@ export async function installBrowsersForNpmInstall(browsers: string[]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const executables: Executable[] = [];
|
const executables: Executable[] = [];
|
||||||
|
if (process.platform === 'win32')
|
||||||
|
executables.push(registry.findExecutable('winldd')!);
|
||||||
for (const browserName of browsers) {
|
for (const browserName of browsers) {
|
||||||
const executable = registry.findExecutable(browserName);
|
const executable = registry.findExecutable(browserName);
|
||||||
if (!executable || executable.installType === 'none')
|
if (!executable || executable.installType === 'none')
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export interface LocatorFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asLocator(lang: Language, selector: string, isFrameLocator: boolean = false): string {
|
export function asLocator(lang: Language, selector: string, isFrameLocator: boolean = false): string {
|
||||||
return asLocators(lang, selector, isFrameLocator)[0];
|
return asLocators(lang, selector, isFrameLocator, 1)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asLocators(lang: Language, selector: string, isFrameLocator: boolean = false, maxOutputSize = 20, preferredQuote?: Quote): string[] {
|
export function asLocators(lang: Language, selector: string, isFrameLocator: boolean = false, maxOutputSize = 20, preferredQuote?: Quote): string[] {
|
||||||
|
|
@ -220,7 +220,7 @@ function combineTokens(factory: LocatorFactory, tokens: string[][], maxOutputSiz
|
||||||
const visit = (index: number) => {
|
const visit = (index: number) => {
|
||||||
if (index === tokens.length) {
|
if (index === tokens.length) {
|
||||||
result.push(factory.chainLocators(currentTokens));
|
result.push(factory.chainLocators(currentTokens));
|
||||||
return currentTokens.length < maxOutputSize;
|
return result.length < maxOutputSize;
|
||||||
}
|
}
|
||||||
for (const taken of tokens[index]) {
|
for (const taken of tokens[index]) {
|
||||||
currentTokens[index] = taken;
|
currentTokens[index] = taken;
|
||||||
|
|
|
||||||
7
packages/playwright-core/types/types.d.ts
vendored
7
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -9589,10 +9589,11 @@ export interface Browser {
|
||||||
* In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from
|
* In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from
|
||||||
* the browser server.
|
* the browser server.
|
||||||
*
|
*
|
||||||
* **NOTE** This is similar to force quitting the browser. Therefore, you should call
|
* **NOTE** This is similar to force-quitting the browser. To close pages gracefully and ensure you receive page close
|
||||||
|
* events, call
|
||||||
* [browserContext.close([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-close) on
|
* [browserContext.close([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-close) on
|
||||||
* any [BrowserContext](https://playwright.dev/docs/api/class-browsercontext)'s you explicitly created earlier with
|
* any [BrowserContext](https://playwright.dev/docs/api/class-browsercontext) instances you explicitly created earlier
|
||||||
* [browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) **before**
|
* using [browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) **before**
|
||||||
* calling [browser.close([options])](https://playwright.dev/docs/api/class-browser#browser-close).
|
* calling [browser.close([options])](https://playwright.dev/docs/api/class-browser#browser-close).
|
||||||
*
|
*
|
||||||
* The [Browser](https://playwright.dev/docs/api/class-browser) object itself is considered to be disposed and cannot
|
* The [Browser](https://playwright.dev/docs/api/class-browser) object itself is considered to be disposed and cannot
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,9 @@ export class TestTypeImpl {
|
||||||
test.fail.only = wrapFunctionWithLocation(this._createTest.bind(this, 'fail.only'));
|
test.fail.only = wrapFunctionWithLocation(this._createTest.bind(this, 'fail.only'));
|
||||||
test.slow = wrapFunctionWithLocation(this._modifier.bind(this, 'slow'));
|
test.slow = wrapFunctionWithLocation(this._modifier.bind(this, 'slow'));
|
||||||
test.setTimeout = wrapFunctionWithLocation(this._setTimeout.bind(this));
|
test.setTimeout = wrapFunctionWithLocation(this._setTimeout.bind(this));
|
||||||
test.step = this._step.bind(this);
|
test.step = this._step.bind(this, 'pass');
|
||||||
|
test.step.fail = this._step.bind(this, 'fail');
|
||||||
|
test.step.fixme = this._step.bind(this, 'fixme');
|
||||||
test.use = wrapFunctionWithLocation(this._use.bind(this));
|
test.use = wrapFunctionWithLocation(this._use.bind(this));
|
||||||
test.extend = wrapFunctionWithLocation(this._extend.bind(this));
|
test.extend = wrapFunctionWithLocation(this._extend.bind(this));
|
||||||
test.info = () => {
|
test.info = () => {
|
||||||
|
|
@ -257,22 +259,40 @@ export class TestTypeImpl {
|
||||||
suite._use.push({ fixtures, location });
|
suite._use.push({ fixtures, location });
|
||||||
}
|
}
|
||||||
|
|
||||||
async _step<T>(title: string, body: () => T | Promise<T>, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise<T> {
|
async _step<T>(expectation: 'pass'|'fail'|'fixme', title: string, body: () => T | Promise<T>, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise<T> {
|
||||||
const testInfo = currentTestInfo();
|
const testInfo = currentTestInfo();
|
||||||
if (!testInfo)
|
if (!testInfo)
|
||||||
throw new Error(`test.step() can only be called from a test`);
|
throw new Error(`test.step() can only be called from a test`);
|
||||||
|
if (expectation === 'fixme')
|
||||||
|
return undefined as T;
|
||||||
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 });
|
||||||
return await zones.run('stepZone', step, async () => {
|
return await zones.run('stepZone', step, async () => {
|
||||||
|
let result;
|
||||||
|
let error;
|
||||||
try {
|
try {
|
||||||
const result = await raceAgainstDeadline(async () => body(), options.timeout ? monotonicTime() + options.timeout : 0);
|
result = await raceAgainstDeadline(async () => body(), options.timeout ? monotonicTime() + options.timeout : 0);
|
||||||
if (result.timedOut)
|
} catch (e) {
|
||||||
throw new errors.TimeoutError(`Step timeout ${options.timeout}ms exceeded.`);
|
error = e;
|
||||||
step.complete({});
|
}
|
||||||
return result.result;
|
if (result?.timedOut) {
|
||||||
} catch (error) {
|
const error = new errors.TimeoutError(`Step timeout ${options.timeout}ms exceeded.`);
|
||||||
step.complete({ error });
|
step.complete({ error });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
const expectedToFail = expectation === 'fail';
|
||||||
|
if (error) {
|
||||||
|
step.complete({ error });
|
||||||
|
if (expectedToFail)
|
||||||
|
return undefined as T;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (expectedToFail) {
|
||||||
|
error = new Error(`Step is expected to fail, but passed`);
|
||||||
|
step.complete({ error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
step.complete({});
|
||||||
|
return result!.result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,7 @@ export class BaseReporter implements ReporterV2 {
|
||||||
console.log(colors.yellow(' Slow test file: ') + file + colors.yellow(` (${milliseconds(duration)})`));
|
console.log(colors.yellow(' Slow test file: ') + file + colors.yellow(` (${milliseconds(duration)})`));
|
||||||
});
|
});
|
||||||
if (slowTests.length)
|
if (slowTests.length)
|
||||||
console.log(colors.yellow(' Consider splitting slow test files to speed up parallel execution'));
|
console.log(colors.yellow(' Consider running tests from slow files in parallel, see https://playwright.dev/docs/test-parallel.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _printSummary(summary: string) {
|
private _printSummary(summary: string) {
|
||||||
|
|
|
||||||
212
packages/playwright/types/test.d.ts
vendored
212
packages/playwright/types/test.d.ts
vendored
|
|
@ -5551,7 +5551,217 @@ export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
|
||||||
* @param body Step body.
|
* @param body Step body.
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
step<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>;
|
step: {
|
||||||
|
/**
|
||||||
|
* Declares a test step that is shown in the report.
|
||||||
|
*
|
||||||
|
* **Usage**
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* import { test, expect } from '@playwright/test';
|
||||||
|
*
|
||||||
|
* test('test', async ({ page }) => {
|
||||||
|
* await test.step('Log in', async () => {
|
||||||
|
* // ...
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* await test.step('Outer step', async () => {
|
||||||
|
* // ...
|
||||||
|
* // You can nest steps inside each other.
|
||||||
|
* await test.step('Inner step', async () => {
|
||||||
|
* // ...
|
||||||
|
* });
|
||||||
|
* });
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* **Details**
|
||||||
|
*
|
||||||
|
* The method returns the value returned by the step callback.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* import { test, expect } from '@playwright/test';
|
||||||
|
*
|
||||||
|
* test('test', async ({ page }) => {
|
||||||
|
* const user = await test.step('Log in', async () => {
|
||||||
|
* // ...
|
||||||
|
* return 'john';
|
||||||
|
* });
|
||||||
|
* expect(user).toBe('john');
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* **Decorator**
|
||||||
|
*
|
||||||
|
* You can use TypeScript method decorators to turn a method into a step. Each call to the decorated method will show
|
||||||
|
* up as a step in the report.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* function step(target: Function, context: ClassMethodDecoratorContext) {
|
||||||
|
* return function replacementMethod(...args: any) {
|
||||||
|
* const name = this.constructor.name + '.' + (context.name as string);
|
||||||
|
* return test.step(name, async () => {
|
||||||
|
* return await target.call(this, ...args);
|
||||||
|
* });
|
||||||
|
* };
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* class LoginPage {
|
||||||
|
* constructor(readonly page: Page) {}
|
||||||
|
*
|
||||||
|
* @step
|
||||||
|
* async login() {
|
||||||
|
* const account = { username: 'Alice', password: 's3cr3t' };
|
||||||
|
* await this.page.getByLabel('Username or email address').fill(account.username);
|
||||||
|
* await this.page.getByLabel('Password').fill(account.password);
|
||||||
|
* await this.page.getByRole('button', { name: 'Sign in' }).click();
|
||||||
|
* await expect(this.page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* test('example', async ({ page }) => {
|
||||||
|
* const loginPage = new LoginPage(page);
|
||||||
|
* await loginPage.login();
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* **Boxing**
|
||||||
|
*
|
||||||
|
* When something inside a step fails, you would usually see the error pointing to the exact action that failed. For
|
||||||
|
* example, consider the following login step:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* async function login(page) {
|
||||||
|
* await test.step('login', async () => {
|
||||||
|
* const account = { username: 'Alice', password: 's3cr3t' };
|
||||||
|
* await page.getByLabel('Username or email address').fill(account.username);
|
||||||
|
* await page.getByLabel('Password').fill(account.password);
|
||||||
|
* await page.getByRole('button', { name: 'Sign in' }).click();
|
||||||
|
* await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* test('example', async ({ page }) => {
|
||||||
|
* await page.goto('https://github.com/login');
|
||||||
|
* await login(page);
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ```txt
|
||||||
|
* Error: Timed out 5000ms waiting for expect(locator).toBeVisible()
|
||||||
|
* ... error details omitted ...
|
||||||
|
*
|
||||||
|
* 8 | await page.getByRole('button', { name: 'Sign in' }).click();
|
||||||
|
* > 9 | await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
|
||||||
|
* | ^
|
||||||
|
* 10 | });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* As we see above, the test may fail with an error pointing inside the step. If you would like the error to highlight
|
||||||
|
* the "login" step instead of its internals, use the `box` option. An error inside a boxed step points to the step
|
||||||
|
* call site.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* async function login(page) {
|
||||||
|
* await test.step('login', async () => {
|
||||||
|
* // ...
|
||||||
|
* }, { box: true }); // Note the "box" option here.
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ```txt
|
||||||
|
* Error: Timed out 5000ms waiting for expect(locator).toBeVisible()
|
||||||
|
* ... error details omitted ...
|
||||||
|
*
|
||||||
|
* 14 | await page.goto('https://github.com/login');
|
||||||
|
* > 15 | await login(page);
|
||||||
|
* | ^
|
||||||
|
* 16 | });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* You can also create a TypeScript decorator for a boxed step, similar to a regular step decorator above:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* function boxedStep(target: Function, context: ClassMethodDecoratorContext) {
|
||||||
|
* return function replacementMethod(...args: any) {
|
||||||
|
* const name = this.constructor.name + '.' + (context.name as string);
|
||||||
|
* return test.step(name, async () => {
|
||||||
|
* return await target.call(this, ...args);
|
||||||
|
* }, { box: true }); // Note the "box" option here.
|
||||||
|
* };
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* class LoginPage {
|
||||||
|
* constructor(readonly page: Page) {}
|
||||||
|
*
|
||||||
|
* @boxedStep
|
||||||
|
* async login() {
|
||||||
|
* // ....
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* test('example', async ({ page }) => {
|
||||||
|
* const loginPage = new LoginPage(page);
|
||||||
|
* await loginPage.login(); // <-- Error will be reported on this line.
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param title Step name.
|
||||||
|
* @param body Step body.
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>;
|
||||||
|
/**
|
||||||
|
* Mark a test step as "fixme", with the intention to fix it. Playwright will not run the step.
|
||||||
|
*
|
||||||
|
* **Usage**
|
||||||
|
*
|
||||||
|
* You can declare a test step as failing, so that Playwright ensures it actually fails.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* import { test, expect } from '@playwright/test';
|
||||||
|
*
|
||||||
|
* test('my test', async ({ page }) => {
|
||||||
|
* // ...
|
||||||
|
* await test.step.fixme('not yet ready', async () => {
|
||||||
|
* // ...
|
||||||
|
* });
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param title Step name.
|
||||||
|
* @param body Step body.
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
fixme(title: string, body: () => any | Promise<any>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Marks a test step as "should fail". Playwright runs this test step and ensures that it actually fails. This is
|
||||||
|
* useful for documentation purposes to acknowledge that some functionality is broken until it is fixed.
|
||||||
|
*
|
||||||
|
* **NOTE** If the step exceeds the timeout, a [TimeoutError](https://playwright.dev/docs/api/class-timeouterror) is
|
||||||
|
* thrown. This indicates the step did not fail as expected.
|
||||||
|
*
|
||||||
|
* **Usage**
|
||||||
|
*
|
||||||
|
* You can declare a test step as failing, so that Playwright ensures it actually fails.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* import { test, expect } from '@playwright/test';
|
||||||
|
*
|
||||||
|
* test('my test', async ({ page }) => {
|
||||||
|
* // ...
|
||||||
|
* await test.step.fail('currently failing', async () => {
|
||||||
|
* // ...
|
||||||
|
* });
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param title Step name.
|
||||||
|
* @param body Step body.
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
fail(title: string, body: () => any | Promise<any>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<void>;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* `expect` function can be used to create test assertions. Read more about [test assertions](https://playwright.dev/docs/test-assertions).
|
* `expect` function can be used to create test assertions. Read more about [test assertions](https://playwright.dev/docs/test-assertions).
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type zip from '@zip.js/zip.js';
|
import type * as zip from '@zip.js/zip.js';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import * as zipImport from '@zip.js/zip.js/lib/zip-no-worker-inflate.js';
|
import * as zipImport from '@zip.js/zip.js/lib/zip-no-worker-inflate.js';
|
||||||
import type { TraceModelBackend } from './traceModel';
|
import type { TraceModelBackend } from './traceModel';
|
||||||
|
|
|
||||||
|
|
@ -16,24 +16,25 @@
|
||||||
|
|
||||||
.settings-view {
|
.settings-view {
|
||||||
flex: none;
|
flex: none;
|
||||||
margin-top: 4px;
|
padding: 4px 0px;
|
||||||
|
row-gap: 8px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-view .setting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-view .setting label {
|
.settings-view .setting label {
|
||||||
display: flex;
|
text-overflow: ellipsis;
|
||||||
flex-direction: row;
|
white-space: nowrap;
|
||||||
align-items: center;
|
overflow: hidden;
|
||||||
margin: 4px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-view .setting:first-of-type label {
|
cursor: pointer;
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-view .setting:last-of-type label {
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-view .setting input {
|
.settings-view .setting input {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,22 +18,32 @@ import * as React from 'react';
|
||||||
import './settingsView.css';
|
import './settingsView.css';
|
||||||
|
|
||||||
export type Setting<T> = {
|
export type Setting<T> = {
|
||||||
value: T,
|
value: T;
|
||||||
set: (value: T) => void,
|
set: (value: T) => void;
|
||||||
title: string
|
name: string;
|
||||||
|
title?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsView: React.FunctionComponent<{
|
export const SettingsView: React.FunctionComponent<{
|
||||||
settings: Setting<boolean>[],
|
settings: Setting<boolean>[];
|
||||||
}> = ({ settings }) => {
|
}> = ({ settings }) => {
|
||||||
return <div className='vbox settings-view'>
|
return (
|
||||||
{settings.map(({ value, set, title }) => {
|
<div className='vbox settings-view'>
|
||||||
return <div key={title} className='setting'>
|
{settings.map(({ value, set, name, title }) => {
|
||||||
<label>
|
const labelId = `setting-${name}`;
|
||||||
<input type='checkbox' checked={value} onChange={() => set(!value)}/>
|
|
||||||
{title}
|
return (
|
||||||
</label>
|
<div key={name} className='setting' title={title}>
|
||||||
</div>;
|
<input
|
||||||
})}
|
type='checkbox'
|
||||||
</div>;
|
id={labelId}
|
||||||
|
checked={value}
|
||||||
|
onChange={() => set(!value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor={labelId}>{name}</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -508,9 +508,9 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
<div className='section-title'>Testing Options</div>
|
<div className='section-title'>Testing Options</div>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
{testingOptionsVisible && <SettingsView settings={[
|
{testingOptionsVisible && <SettingsView settings={[
|
||||||
{ value: singleWorker, set: setSingleWorker, title: 'Single worker' },
|
{ value: singleWorker, set: setSingleWorker, name: 'Single worker' },
|
||||||
{ value: showBrowser, set: setShowBrowser, title: 'Show browser' },
|
{ value: showBrowser, set: setShowBrowser, name: 'Show browser' },
|
||||||
{ value: updateSnapshots, set: setUpdateSnapshots, title: 'Update snapshots' },
|
{ value: updateSnapshots, set: setUpdateSnapshots, name: 'Update snapshots' },
|
||||||
]} />}
|
]} />}
|
||||||
</>}
|
</>}
|
||||||
<Toolbar noShadow={true} noMinHeight={true} className='settings-toolbar' onClick={() => setSettingsVisible(!settingsVisible)}>
|
<Toolbar noShadow={true} noMinHeight={true} className='settings-toolbar' onClick={() => setSettingsVisible(!settingsVisible)}>
|
||||||
|
|
@ -522,7 +522,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
<div className='section-title'>Settings</div>
|
<div className='section-title'>Settings</div>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
{settingsVisible && <SettingsView settings={[
|
{settingsVisible && <SettingsView settings={[
|
||||||
{ value: darkMode, set: setDarkMode, title: 'Dark mode' },
|
{ value: darkMode, set: setDarkMode, name: 'Dark mode' },
|
||||||
]} />}
|
]} />}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ansi2html } from '@web/ansi2html';
|
import { ansi2html } from '../ansi2html';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './errorMessage.css';
|
import './errorMessage.css';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import * as React from 'react';
|
||||||
import { ListView } from './listView';
|
import { ListView } from './listView';
|
||||||
import type { ListViewProps } from './listView';
|
import type { ListViewProps } from './listView';
|
||||||
import './gridView.css';
|
import './gridView.css';
|
||||||
import { ResizeView } from '@web/shared/resizeView';
|
import { ResizeView } from '../shared/resizeView';
|
||||||
|
|
||||||
export type Sorting<T> = { by: keyof T, negate: boolean };
|
export type Sorting<T> = { by: keyof T, negate: boolean };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './listView.css';
|
import './listView.css';
|
||||||
import { clsx, scrollIntoViewIfNeeded } from '@web/uiUtils';
|
import { clsx, scrollIntoViewIfNeeded } from '../uiUtils';
|
||||||
|
|
||||||
export type ListViewProps<T> = {
|
export type ListViewProps<T> = {
|
||||||
name: string,
|
name: string,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { clsx } from '@web/uiUtils';
|
import { clsx } from '../uiUtils';
|
||||||
import './tabbedPane.css';
|
import './tabbedPane.css';
|
||||||
import { Toolbar } from './toolbar';
|
import { Toolbar } from './toolbar';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { clsx } from '@web/uiUtils';
|
import { clsx } from '../uiUtils';
|
||||||
import './toolbar.css';
|
import './toolbar.css';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
import './toolbarButton.css';
|
import './toolbarButton.css';
|
||||||
import '../third_party/vscode/codicon.css';
|
import '../third_party/vscode/codicon.css';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { clsx } from '@web/uiUtils';
|
import { clsx } from '../uiUtils';
|
||||||
|
|
||||||
export interface ToolbarButtonProps {
|
export interface ToolbarButtonProps {
|
||||||
title: string,
|
title: string,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { clsx, scrollIntoViewIfNeeded } from '@web/uiUtils';
|
import { clsx, scrollIntoViewIfNeeded } from '../uiUtils';
|
||||||
import './treeView.css';
|
import './treeView.css';
|
||||||
|
|
||||||
export type TreeItem = {
|
export type TreeItem = {
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ import './xtermWrapper.css';
|
||||||
import type { ITheme, Terminal } from 'xterm';
|
import type { ITheme, Terminal } from 'xterm';
|
||||||
import type { FitAddon } from 'xterm-addon-fit';
|
import type { FitAddon } from 'xterm-addon-fit';
|
||||||
import type { XtermModule } from './xtermModule';
|
import type { XtermModule } from './xtermModule';
|
||||||
import { currentTheme, addThemeListener, removeThemeListener } from '@web/theme';
|
import { currentTheme, addThemeListener, removeThemeListener } from '../theme';
|
||||||
import { useMeasure } from '@web/uiUtils';
|
import { useMeasure } from '../uiUtils';
|
||||||
|
|
||||||
export type XtermDataSource = {
|
export type XtermDataSource = {
|
||||||
pending: (string | Uint8Array)[];
|
pending: (string | Uint8Array)[];
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ export function useMeasure<T extends Element>() {
|
||||||
const target = ref.current;
|
const target = ref.current;
|
||||||
if (!target)
|
if (!target)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const bounds = target.getBoundingClientRect();
|
||||||
|
|
||||||
|
setMeasure(new DOMRect(0, 0, bounds.width, bounds.height));
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver((entries: any) => {
|
const resizeObserver = new ResizeObserver((entries: any) => {
|
||||||
const entry = entries[entries.length - 1];
|
const entry = entries[entries.length - 1];
|
||||||
if (entry && entry.contentRect)
|
if (entry && entry.contentRect)
|
||||||
|
|
|
||||||
82
tests/bidi/csvReporter.ts
Normal file
82
tests/bidi/csvReporter.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
FullConfig, FullResult, Reporter, Suite
|
||||||
|
} from '@playwright/test/reporter';
|
||||||
|
import { stripAnsi } from '../config/utils';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
|
||||||
|
type ReporterOptions = {
|
||||||
|
outputFile?: string,
|
||||||
|
configDir: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
class CsvReporter implements Reporter {
|
||||||
|
private _suite: Suite;
|
||||||
|
private _options: ReporterOptions;
|
||||||
|
private _pendingWrite: Promise<void>;
|
||||||
|
|
||||||
|
constructor(options: ReporterOptions) {
|
||||||
|
this._options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
onBegin(config: FullConfig, suite: Suite) {
|
||||||
|
this._suite = suite;
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnd(result: FullResult) {
|
||||||
|
const rows = [['File Name', 'Test Name', 'Expected Status', 'Status', 'Error Message']];
|
||||||
|
for (const project of this._suite.suites) {
|
||||||
|
for (const file of project.suites) {
|
||||||
|
for (const test of file.allTests()) {
|
||||||
|
if (test.ok())
|
||||||
|
continue;
|
||||||
|
const row = [];
|
||||||
|
row.push(file.title);
|
||||||
|
row.push(csvEscape(test.title));
|
||||||
|
row.push(test.expectedStatus);
|
||||||
|
row.push(test.outcome());
|
||||||
|
const result = test.results.find(r => r.error);
|
||||||
|
const errorMessage = stripAnsi(result?.error?.message.replace(/\s+/g, ' ').trim().substring(0, 1024));
|
||||||
|
row.push(csvEscape(errorMessage ?? ''));
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const csv = rows.map(r => r.join(',')).join('\n');
|
||||||
|
const reportFile = path.resolve(this._options.configDir, this._options.outputFile || 'test-results.csv');
|
||||||
|
this._pendingWrite = fs.promises.writeFile(reportFile, csv);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onExit() {
|
||||||
|
await this._pendingWrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
printsToStdio(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function csvEscape(str) {
|
||||||
|
if (str.includes('"') || str.includes(',') || str.includes('\n'))
|
||||||
|
return `"${str.replace(/"/g, '""')}"`;
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CsvReporter;
|
||||||
|
|
@ -29,7 +29,7 @@ export async function createSkipTestPredicate(projectName: string): Promise<Shou
|
||||||
return (info: TestInfo) => {
|
return (info: TestInfo) => {
|
||||||
const key = info.titlePath.join(' › ');
|
const key = info.titlePath.join(' › ');
|
||||||
const expectation = expectationsMap.get(key);
|
const expectation = expectationsMap.get(key);
|
||||||
return expectation === 'fail' || expectation === 'timeout';
|
return expectation === 'timeout';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ const reporters = () => {
|
||||||
hasDebugOutput ? ['list'] : ['dot'],
|
hasDebugOutput ? ['list'] : ['dot'],
|
||||||
['json', { outputFile: path.join(outputDir, 'report.json') }],
|
['json', { outputFile: path.join(outputDir, 'report.json') }],
|
||||||
['blob', { fileName: `${process.env.PWTEST_BOT_NAME}.zip` }],
|
['blob', { fileName: `${process.env.PWTEST_BOT_NAME}.zip` }],
|
||||||
|
['./csvReporter', { outputFile: path.join(outputDir, 'report.csv') }],
|
||||||
] : [
|
] : [
|
||||||
['html', { open: 'on-failure' }],
|
['html', { open: 'on-failure' }],
|
||||||
['./expectationReporter', { rebase: false }],
|
['./expectationReporter', { rebase: false }],
|
||||||
|
|
@ -58,7 +59,7 @@ const config: Config<PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeW
|
||||||
workers: process.env.CI ? 2 : undefined,
|
workers: process.env.CI ? 2 : undefined,
|
||||||
fullyParallel: !process.env.CI,
|
fullyParallel: !process.env.CI,
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 3 : 0,
|
retries: 0, // No retries even on CI for now.
|
||||||
reporter: reporters(),
|
reporter: reporters(),
|
||||||
projects: [],
|
projects: [],
|
||||||
};
|
};
|
||||||
|
|
@ -66,7 +67,6 @@ const config: Config<PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeW
|
||||||
const executablePath = process.env.BIDIPATH;
|
const executablePath = process.env.BIDIPATH;
|
||||||
if (executablePath && !process.env.TEST_WORKER_INDEX)
|
if (executablePath && !process.env.TEST_WORKER_INDEX)
|
||||||
console.error(`Using executable at ${executablePath}`);
|
console.error(`Using executable at ${executablePath}`);
|
||||||
const testIgnore: RegExp[] = [];
|
|
||||||
const browserToChannels = {
|
const browserToChannels = {
|
||||||
'_bidiChromium': ['bidi-chromium', 'bidi-chrome-canary', 'bidi-chrome-stable'],
|
'_bidiChromium': ['bidi-chromium', 'bidi-chrome-canary', 'bidi-chrome-stable'],
|
||||||
'_bidiFirefox': ['bidi-firefox-nightly', 'bidi-firefox-beta', 'bidi-firefox-stable'],
|
'_bidiFirefox': ['bidi-firefox-nightly', 'bidi-firefox-beta', 'bidi-firefox-stable'],
|
||||||
|
|
@ -74,6 +74,17 @@ const browserToChannels = {
|
||||||
for (const [key, channels] of Object.entries(browserToChannels)) {
|
for (const [key, channels] of Object.entries(browserToChannels)) {
|
||||||
const browserName: any = key;
|
const browserName: any = key;
|
||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
|
const testIgnore: RegExp[] = [
|
||||||
|
/library\/debug-controller/,
|
||||||
|
/library\/inspector/,
|
||||||
|
/library\/trace-viewer.spec.ts/,
|
||||||
|
/library\/tracing.spec.ts/,
|
||||||
|
/page\/page-leaks.spec.ts/,
|
||||||
|
];
|
||||||
|
if (browserName.toLowerCase().includes('firefox'))
|
||||||
|
testIgnore.push(/chromium/);
|
||||||
|
if (browserName.toLowerCase().includes('chromium'))
|
||||||
|
testIgnore.push(/firefox/);
|
||||||
for (const folder of ['library', 'page']) {
|
for (const folder of ['library', 'page']) {
|
||||||
config.projects.push({
|
config.projects.push({
|
||||||
name: `${channel}-${folder}`,
|
name: `${channel}-${folder}`,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export class TestProxy {
|
||||||
|
|
||||||
connectHosts: string[] = [];
|
connectHosts: string[] = [];
|
||||||
requestUrls: string[] = [];
|
requestUrls: string[] = [];
|
||||||
|
wsUrls: string[] = [];
|
||||||
|
|
||||||
private readonly _server: ProxyServer;
|
private readonly _server: ProxyServer;
|
||||||
private readonly _sockets = new Set<net.Socket>();
|
private readonly _sockets = new Set<net.Socket>();
|
||||||
|
|
@ -58,11 +59,16 @@ export class TestProxy {
|
||||||
await new Promise(x => this._server.close(x));
|
await new Promise(x => this._server.close(x));
|
||||||
}
|
}
|
||||||
|
|
||||||
forwardTo(port: number, options?: { allowConnectRequests: boolean }) {
|
forwardTo(port: number, options?: { allowConnectRequests?: boolean, prefix?: string, preserveHostname?: boolean }) {
|
||||||
this._prependHandler('request', (req: IncomingMessage) => {
|
this._prependHandler('request', (req: IncomingMessage) => {
|
||||||
this.requestUrls.push(req.url);
|
this.requestUrls.push(req.url);
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||||
url.host = `127.0.0.1:${port}`;
|
if (options?.preserveHostname)
|
||||||
|
url.port = '' + port;
|
||||||
|
else
|
||||||
|
url.host = `127.0.0.1:${port}`;
|
||||||
|
if (options?.prefix)
|
||||||
|
url.pathname = url.pathname.replace(options.prefix, '');
|
||||||
req.url = url.toString();
|
req.url = url.toString();
|
||||||
});
|
});
|
||||||
this._prependHandler('connect', (req: IncomingMessage) => {
|
this._prependHandler('connect', (req: IncomingMessage) => {
|
||||||
|
|
@ -73,6 +79,17 @@ export class TestProxy {
|
||||||
this.connectHosts.push(req.url);
|
this.connectHosts.push(req.url);
|
||||||
req.url = `127.0.0.1:${port}`;
|
req.url = `127.0.0.1:${port}`;
|
||||||
});
|
});
|
||||||
|
this._prependHandler('upgrade', (req: IncomingMessage) => {
|
||||||
|
this.wsUrls.push(req.url);
|
||||||
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||||
|
if (options?.preserveHostname)
|
||||||
|
url.port = '' + port;
|
||||||
|
else
|
||||||
|
url.host = `127.0.0.1:${port}`;
|
||||||
|
if (options?.prefix)
|
||||||
|
url.pathname = url.pathname.replace(options.prefix, '');
|
||||||
|
req.url = url.toString();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setAuthHandler(handler: (req: IncomingMessage) => boolean) {
|
setAuthHandler(handler: (req: IncomingMessage) => boolean) {
|
||||||
|
|
|
||||||
|
|
@ -31,22 +31,22 @@ export const TMP_WORKSPACES = path.join(os.platform() === 'darwin' ? '/tmp' : os
|
||||||
const debug = debugLogger('itest');
|
const debug = debugLogger('itest');
|
||||||
|
|
||||||
const expect = _expect.extend({
|
const expect = _expect.extend({
|
||||||
toHaveLoggedSoftwareDownload(received: any, browsers: ('chromium' | 'chromium-headless-shell' | 'firefox' | 'webkit' | 'ffmpeg')[]) {
|
toHaveLoggedSoftwareDownload(received: string, browsers: ('chromium' | 'chromium-headless-shell' | 'firefox' | 'webkit' | 'winldd' |'ffmpeg')[]) {
|
||||||
if (typeof received !== 'string')
|
if (typeof received !== 'string')
|
||||||
throw new Error(`Expected argument to be a string.`);
|
throw new Error(`Expected argument to be a string.`);
|
||||||
|
|
||||||
const downloaded = new Set();
|
const downloaded = new Set();
|
||||||
let index = 0;
|
let index = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
const match = received.substring(index).match(/(chromium|chromium headless shell|firefox|webkit|ffmpeg)[\s\d\.]+\(?playwright build v\d+\)? downloaded/im);
|
const match = received.substring(index).match(/(chromium|chromium headless shell|firefox|webkit|winldd|ffmpeg)[\s\d\.]+\(?playwright build v\d+\)? downloaded/im);
|
||||||
if (!match)
|
if (!match)
|
||||||
break;
|
break;
|
||||||
downloaded.add(match[1].replace(/\s/g, '-').toLowerCase());
|
downloaded.add(match[1].replace(/\s/g, '-').toLowerCase());
|
||||||
index += match.index + 1;
|
index += match.index + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const expected = browsers;
|
const expected = new Set(browsers);
|
||||||
if (expected.length === downloaded.size && expected.every(browser => downloaded.has(browser))) {
|
if (expected.size === downloaded.size && [...expected].every(browser => downloaded.has(browser))) {
|
||||||
return {
|
return {
|
||||||
pass: true,
|
pass: true,
|
||||||
message: () => 'Expected not to download browsers, but did.'
|
message: () => 'Expected not to download browsers, but did.'
|
||||||
|
|
@ -75,7 +75,7 @@ type NPMTestFixtures = {
|
||||||
_auto: void;
|
_auto: void;
|
||||||
_browsersPath: string;
|
_browsersPath: string;
|
||||||
tmpWorkspace: string;
|
tmpWorkspace: string;
|
||||||
installedSoftwareOnDisk: () => Promise<string[]>;
|
checkInstalledSoftwareOnDisk: (browsers: string[]) => Promise<void>;
|
||||||
writeFiles: (nameToContents: Record<string, string>) => Promise<void>;
|
writeFiles: (nameToContents: Record<string, string>) => Promise<void>;
|
||||||
exec: (cmd: string, ...argsAndOrOptions: ArgsOrOptions) => Promise<string>;
|
exec: (cmd: string, ...argsAndOrOptions: ArgsOrOptions) => Promise<string>;
|
||||||
tsc: (args: string) => Promise<string>;
|
tsc: (args: string) => Promise<string>;
|
||||||
|
|
@ -146,10 +146,13 @@ export const test = _test
|
||||||
await use(registry);
|
await use(registry);
|
||||||
await registry.shutdown();
|
await registry.shutdown();
|
||||||
},
|
},
|
||||||
installedSoftwareOnDisk: async ({ isolateBrowsers, _browsersPath }, use) => {
|
checkInstalledSoftwareOnDisk: async ({ isolateBrowsers, _browsersPath }, use) => {
|
||||||
if (!isolateBrowsers)
|
await use(async expected => {
|
||||||
throw new Error(`Test that checks browser installation must set "isolateBrowsers" to true`);
|
if (!isolateBrowsers)
|
||||||
await use(async () => fs.promises.readdir(_browsersPath).catch(() => []).then(files => files.map(f => f.split('-')[0].replace(/_/g, '-')).filter(f => !f.startsWith('.'))));
|
throw new Error(`Test that checks browser installation must set "isolateBrowsers" to true`);
|
||||||
|
const actual = await fs.promises.readdir(_browsersPath).catch(() => []).then(files => files.map(f => f.split('-')[0].replace(/_/g, '-')).filter(f => !f.startsWith('.')));
|
||||||
|
expect(new Set(actual)).toEqual(new Set(expected));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
exec: async ({ tmpWorkspace, _browsersPath, isolateBrowsers }, use, testInfo) => {
|
exec: async ({ tmpWorkspace, _browsersPath, isolateBrowsers }, use, testInfo) => {
|
||||||
await use(async (cmd: string, ...argsAndOrOptions: [] | [...string[]] | [...string[], ExecOptions] | [ExecOptions]) => {
|
await use(async (cmd: string, ...argsAndOrOptions: [] | [...string[]] | [...string[], ExecOptions] | [ExecOptions]) => {
|
||||||
|
|
|
||||||
|
|
@ -17,26 +17,26 @@ import { test, expect } from './npmTest';
|
||||||
|
|
||||||
test.use({ isolateBrowsers: true, allowGlobalInstall: true });
|
test.use({ isolateBrowsers: true, allowGlobalInstall: true });
|
||||||
|
|
||||||
test('npx playwright --help should not download browsers', async ({ exec, installedSoftwareOnDisk }) => {
|
test('npx playwright --help should not download browsers', async ({ exec, checkInstalledSoftwareOnDisk }) => {
|
||||||
const result = await exec('npx playwright --help');
|
const result = await exec('npx playwright --help');
|
||||||
expect(result).toHaveLoggedSoftwareDownload([]);
|
expect(result).toHaveLoggedSoftwareDownload([]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual([]);
|
await checkInstalledSoftwareOnDisk([]);
|
||||||
expect(result).not.toContain(`To avoid unexpected behavior, please install your dependencies first`);
|
expect(result).not.toContain(`To avoid unexpected behavior, please install your dependencies first`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('npx playwright codegen', async ({ exec, installedSoftwareOnDisk }) => {
|
test('npx playwright codegen', async ({ exec, checkInstalledSoftwareOnDisk }) => {
|
||||||
const stdio = await exec('npx playwright codegen', { expectToExitWithError: true });
|
const stdio = await exec('npx playwright codegen', { expectToExitWithError: true });
|
||||||
expect(stdio).toHaveLoggedSoftwareDownload([]);
|
expect(stdio).toHaveLoggedSoftwareDownload([]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual([]);
|
await checkInstalledSoftwareOnDisk([]);
|
||||||
expect(stdio).toContain(`Please run the following command to download new browsers`);
|
expect(stdio).toContain(`Please run the following command to download new browsers`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('npx playwright install global', async ({ exec, installedSoftwareOnDisk }) => {
|
test('npx playwright install global', async ({ exec, checkInstalledSoftwareOnDisk }) => {
|
||||||
test.skip(process.platform === 'win32', 'isLikelyNpxGlobal() does not work in this setup on our bots');
|
test.skip(process.platform === 'win32', 'isLikelyNpxGlobal() does not work in this setup on our bots');
|
||||||
|
|
||||||
const result = await exec('npx playwright install');
|
const result = await exec('npx playwright install');
|
||||||
expect(result).toHaveLoggedSoftwareDownload(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
expect(result).toHaveLoggedSoftwareDownload(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
await checkInstalledSoftwareOnDisk(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
||||||
expect(result).not.toContain(`Please run the following command to download new browsers`);
|
expect(result).not.toContain(`Please run the following command to download new browsers`);
|
||||||
expect(result).toContain(`To avoid unexpected behavior, please install your dependencies first`);
|
expect(result).toContain(`To avoid unexpected behavior, please install your dependencies first`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,9 @@ import net from 'net';
|
||||||
import type { AddressInfo } from 'net';
|
import type { AddressInfo } from 'net';
|
||||||
|
|
||||||
const CDNS = [
|
const CDNS = [
|
||||||
'https://playwright.azureedge.net/dbazure/download/playwright', // ESRP
|
'https://cdn.playwright.dev/dbazure/download/playwright', // ESRP
|
||||||
'https://playwright.download.prss.microsoft.com/dbazure/download/playwright', // ESRP Fallback
|
'https://playwright.download.prss.microsoft.com/dbazure/download/playwright', // ESRP Fallback
|
||||||
'https://playwright.azureedge.net',
|
'https://cdn.playwright.dev',
|
||||||
'https://playwright-akamai.azureedge.net',
|
|
||||||
'https://playwright-verizon.azureedge.net',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const DL_STAT_BLOCK = /^.*from url: (.*)$\n^.*to location: (.*)$\n^.*response status code: (.*)$\n^.*total bytes: (\d+)$\n^.*download complete, size: (\d+)$\n^.*SUCCESS downloading (\w+) .*$/gm;
|
const DL_STAT_BLOCK = /^.*from url: (.*)$\n^.*to location: (.*)$\n^.*response status code: (.*)$\n^.*total bytes: (\d+)$\n^.*download complete, size: (\d+)$\n^.*SUCCESS downloading (\w+) .*$/gm;
|
||||||
|
|
@ -39,12 +37,14 @@ const parsedDownloads = (rawLogs: string) => {
|
||||||
|
|
||||||
test.use({ isolateBrowsers: true });
|
test.use({ isolateBrowsers: true });
|
||||||
|
|
||||||
|
const extraInstalledSoftware = process.platform === 'win32' ? ['winldd' as const] : [];
|
||||||
|
|
||||||
for (const cdn of CDNS) {
|
for (const cdn of CDNS) {
|
||||||
test(`playwright cdn failover should work (${cdn})`, async ({ exec, installedSoftwareOnDisk }) => {
|
test(`playwright cdn failover should work (${cdn})`, async ({ exec, checkInstalledSoftwareOnDisk }) => {
|
||||||
await exec('npm i playwright');
|
await exec('npm i playwright');
|
||||||
const result = await exec('npx playwright install', { env: { PW_TEST_CDN_THAT_SHOULD_WORK: cdn, DEBUG: 'pw:install' } });
|
const result = await exec('npx playwright install', { env: { PW_TEST_CDN_THAT_SHOULD_WORK: cdn, DEBUG: 'pw:install' } });
|
||||||
expect(result).toHaveLoggedSoftwareDownload(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
expect(result).toHaveLoggedSoftwareDownload(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit', ...extraInstalledSoftware]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
await checkInstalledSoftwareOnDisk((['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit', ...extraInstalledSoftware]));
|
||||||
const dls = parsedDownloads(result);
|
const dls = parsedDownloads(result);
|
||||||
for (const software of ['chromium', 'ffmpeg', 'firefox', 'webkit'])
|
for (const software of ['chromium', 'ffmpeg', 'firefox', 'webkit'])
|
||||||
expect(dls).toContainEqual({ status: 200, name: software, url: expect.stringContaining(cdn) });
|
expect(dls).toContainEqual({ status: 200, name: software, url: expect.stringContaining(cdn) });
|
||||||
|
|
|
||||||
|
|
@ -19,19 +19,21 @@ import path from 'path';
|
||||||
|
|
||||||
test.use({ isolateBrowsers: true });
|
test.use({ isolateBrowsers: true });
|
||||||
|
|
||||||
test('install command should work', async ({ exec, installedSoftwareOnDisk }) => {
|
const extraInstalledSoftware = process.platform === 'win32' ? ['winldd' as const] : [];
|
||||||
|
|
||||||
|
test('install command should work', async ({ exec, checkInstalledSoftwareOnDisk }) => {
|
||||||
await exec('npm i playwright');
|
await exec('npm i playwright');
|
||||||
|
|
||||||
await test.step('playwright install chromium', async () => {
|
await test.step('playwright install chromium', async () => {
|
||||||
const result = await exec('npx playwright install chromium');
|
const result = await exec('npx playwright install chromium');
|
||||||
expect(result).toHaveLoggedSoftwareDownload(['chromium', 'chromium-headless-shell', 'ffmpeg']);
|
expect(result).toHaveLoggedSoftwareDownload(['chromium', 'chromium-headless-shell', 'ffmpeg', ...extraInstalledSoftware]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual(['chromium', 'chromium-headless-shell', 'ffmpeg']);
|
await checkInstalledSoftwareOnDisk(['chromium', 'chromium-headless-shell', 'ffmpeg', ...extraInstalledSoftware]);
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('playwright install', async () => {
|
await test.step('playwright install', async () => {
|
||||||
const result = await exec('npx playwright install');
|
const result = await exec('npx playwright install');
|
||||||
expect(result).toHaveLoggedSoftwareDownload(['firefox', 'webkit']);
|
expect(result).toHaveLoggedSoftwareDownload(['firefox', 'webkit']);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
await checkInstalledSoftwareOnDisk(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit', ...extraInstalledSoftware]);
|
||||||
});
|
});
|
||||||
|
|
||||||
await exec('node sanity.js playwright', { env: { PLAYWRIGHT_BROWSERS_PATH: '0' } });
|
await exec('node sanity.js playwright', { env: { PLAYWRIGHT_BROWSERS_PATH: '0' } });
|
||||||
|
|
@ -48,12 +50,12 @@ test('install command should work', async ({ exec, installedSoftwareOnDisk }) =>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should be able to remove browsers', async ({ exec, installedSoftwareOnDisk }) => {
|
test('should be able to remove browsers', async ({ exec, checkInstalledSoftwareOnDisk }) => {
|
||||||
await exec('npm i playwright');
|
await exec('npm i playwright');
|
||||||
await exec('npx playwright install chromium');
|
await exec('npx playwright install chromium');
|
||||||
expect(await installedSoftwareOnDisk()).toEqual(['chromium', 'chromium-headless-shell', 'ffmpeg']);
|
await checkInstalledSoftwareOnDisk(['chromium', 'chromium-headless-shell', 'ffmpeg', ...extraInstalledSoftware]);
|
||||||
await exec('npx playwright uninstall');
|
await exec('npx playwright uninstall');
|
||||||
expect(await installedSoftwareOnDisk()).toEqual([]);
|
await checkInstalledSoftwareOnDisk([...extraInstalledSoftware]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should print the right install command without browsers', async ({ exec }) => {
|
test('should print the right install command without browsers', async ({ exec }) => {
|
||||||
|
|
@ -91,7 +93,7 @@ test('subsequent installs works', async ({ exec }) => {
|
||||||
await exec('node --unhandled-rejections=strict', path.join('node_modules', '@playwright', 'browser-chromium', 'install.js'));
|
await exec('node --unhandled-rejections=strict', path.join('node_modules', '@playwright', 'browser-chromium', 'install.js'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('install playwright-chromium should work', async ({ exec, installedSoftwareOnDisk }) => {
|
test('install playwright-chromium should work', async ({ exec }) => {
|
||||||
await exec('npm i playwright-chromium');
|
await exec('npm i playwright-chromium');
|
||||||
await exec('npx playwright install chromium');
|
await exec('npx playwright install chromium');
|
||||||
await exec('node sanity.js playwright-chromium chromium');
|
await exec('node sanity.js playwright-chromium chromium');
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,18 @@ import { test, expect } from './npmTest';
|
||||||
|
|
||||||
test.use({ isolateBrowsers: true });
|
test.use({ isolateBrowsers: true });
|
||||||
|
|
||||||
|
const extraInstalledSoftware = process.platform === 'win32' ? ['winldd' as const] : [];
|
||||||
|
|
||||||
for (const browser of ['chromium', 'firefox', 'webkit']) {
|
for (const browser of ['chromium', 'firefox', 'webkit']) {
|
||||||
test(`playwright-${browser} should work`, async ({ exec, installedSoftwareOnDisk }) => {
|
test(`playwright-${browser} should work`, async ({ exec, checkInstalledSoftwareOnDisk }) => {
|
||||||
const pkg = `playwright-${browser}`;
|
const pkg = `playwright-${browser}`;
|
||||||
const result = await exec('npm i --foreground-scripts', pkg);
|
const result = await exec('npm i --foreground-scripts', pkg);
|
||||||
const browserName = pkg.split('-')[1];
|
const browserName = pkg.split('-')[1];
|
||||||
const expectedSoftware = [browserName];
|
const expectedSoftware = [browserName, ...extraInstalledSoftware];
|
||||||
if (browserName === 'chromium')
|
if (browserName === 'chromium')
|
||||||
expectedSoftware.push('chromium-headless-shell', 'ffmpeg');
|
expectedSoftware.push('chromium-headless-shell', 'ffmpeg');
|
||||||
expect(result).toHaveLoggedSoftwareDownload(expectedSoftware as any);
|
expect(result).toHaveLoggedSoftwareDownload(expectedSoftware as any);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual(expectedSoftware);
|
await checkInstalledSoftwareOnDisk(expectedSoftware);
|
||||||
expect(result).not.toContain(`To avoid unexpected behavior, please install your dependencies first`);
|
expect(result).not.toContain(`To avoid unexpected behavior, please install your dependencies first`);
|
||||||
await exec('node sanity.js', pkg, browser);
|
await exec('node sanity.js', pkg, browser);
|
||||||
await exec('node', `esm-${pkg}.mjs`);
|
await exec('node', `esm-${pkg}.mjs`);
|
||||||
|
|
@ -35,75 +37,75 @@ for (const browser of ['chromium', 'firefox', 'webkit']) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const browser of ['chromium', 'firefox', 'webkit']) {
|
for (const browser of ['chromium', 'firefox', 'webkit']) {
|
||||||
test(`@playwright/browser-${browser} should work`, async ({ exec, installedSoftwareOnDisk }) => {
|
test(`@playwright/browser-${browser} should work`, async ({ exec, checkInstalledSoftwareOnDisk }) => {
|
||||||
const pkg = `@playwright/browser-${browser}`;
|
const pkg = `@playwright/browser-${browser}`;
|
||||||
const expectedSoftware = [browser];
|
const expectedSoftware = [browser, ...extraInstalledSoftware];
|
||||||
if (browser === 'chromium')
|
if (browser === 'chromium')
|
||||||
expectedSoftware.push('chromium-headless-shell', 'ffmpeg');
|
expectedSoftware.push('chromium-headless-shell', 'ffmpeg');
|
||||||
|
|
||||||
const result1 = await exec('npm i --foreground-scripts', pkg);
|
const result1 = await exec('npm i --foreground-scripts', pkg);
|
||||||
expect(result1).toHaveLoggedSoftwareDownload(expectedSoftware as any);
|
expect(result1).toHaveLoggedSoftwareDownload(expectedSoftware as any);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual(expectedSoftware);
|
await checkInstalledSoftwareOnDisk(expectedSoftware);
|
||||||
expect(result1).not.toContain(`To avoid unexpected behavior, please install your dependencies first`);
|
expect(result1).not.toContain(`To avoid unexpected behavior, please install your dependencies first`);
|
||||||
|
|
||||||
const result2 = await exec('npm i --foreground-scripts playwright');
|
const result2 = await exec('npm i --foreground-scripts playwright');
|
||||||
expect(result2).toHaveLoggedSoftwareDownload([]);
|
expect(result2).toHaveLoggedSoftwareDownload([]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual(expectedSoftware);
|
await checkInstalledSoftwareOnDisk(expectedSoftware);
|
||||||
|
|
||||||
await exec('node sanity.js playwright', browser);
|
await exec('node sanity.js playwright', browser);
|
||||||
await exec('node browser-only.js', pkg);
|
await exec('node browser-only.js', pkg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test(`playwright-core should work`, async ({ exec, installedSoftwareOnDisk }) => {
|
test(`playwright-core should work`, async ({ exec, checkInstalledSoftwareOnDisk }) => {
|
||||||
const result1 = await exec('npm i --foreground-scripts playwright-core');
|
const result1 = await exec('npm i --foreground-scripts playwright-core');
|
||||||
expect(result1).toHaveLoggedSoftwareDownload([]);
|
expect(result1).toHaveLoggedSoftwareDownload([]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual([]);
|
await checkInstalledSoftwareOnDisk([]);
|
||||||
const stdio = await exec('npx playwright-core', 'test', '-c', '.', { expectToExitWithError: true });
|
const stdio = await exec('npx playwright-core', 'test', '-c', '.', { expectToExitWithError: true });
|
||||||
expect(stdio).toContain(`Please install @playwright/test package`);
|
expect(stdio).toContain(`Please install @playwright/test package`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`playwright should work`, async ({ exec, installedSoftwareOnDisk }) => {
|
test(`playwright should work`, async ({ exec, checkInstalledSoftwareOnDisk }) => {
|
||||||
const result1 = await exec('npm i --foreground-scripts playwright');
|
const result1 = await exec('npm i --foreground-scripts playwright');
|
||||||
expect(result1).toHaveLoggedSoftwareDownload([]);
|
expect(result1).toHaveLoggedSoftwareDownload([]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual([]);
|
await checkInstalledSoftwareOnDisk([]);
|
||||||
|
|
||||||
const result2 = await exec('npx playwright install');
|
const result2 = await exec('npx playwright install');
|
||||||
expect(result2).toHaveLoggedSoftwareDownload(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
expect(result2).toHaveLoggedSoftwareDownload(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit', ...extraInstalledSoftware]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
await checkInstalledSoftwareOnDisk(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit', ...extraInstalledSoftware]);
|
||||||
|
|
||||||
await exec('node sanity.js playwright chromium firefox webkit');
|
await exec('node sanity.js playwright chromium firefox webkit');
|
||||||
await exec('node esm-playwright.mjs');
|
await exec('node esm-playwright.mjs');
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`playwright should work with chromium --no-shell`, async ({ exec, installedSoftwareOnDisk }) => {
|
test(`playwright should work with chromium --no-shell`, async ({ exec, checkInstalledSoftwareOnDisk }) => {
|
||||||
const result1 = await exec('npm i --foreground-scripts playwright');
|
const result1 = await exec('npm i --foreground-scripts playwright');
|
||||||
expect(result1).toHaveLoggedSoftwareDownload([]);
|
expect(result1).toHaveLoggedSoftwareDownload([]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual([]);
|
await checkInstalledSoftwareOnDisk([]);
|
||||||
const result2 = await exec('npx playwright install chromium --no-shell');
|
const result2 = await exec('npx playwright install chromium --no-shell');
|
||||||
expect(result2).toHaveLoggedSoftwareDownload(['chromium', 'ffmpeg']);
|
expect(result2).toHaveLoggedSoftwareDownload(['chromium', 'ffmpeg', ...extraInstalledSoftware]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual(['chromium', 'ffmpeg']);
|
await checkInstalledSoftwareOnDisk(['chromium', 'ffmpeg', ...extraInstalledSoftware]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`playwright should work with chromium --only-shell`, async ({ exec, installedSoftwareOnDisk }) => {
|
test(`playwright should work with chromium --only-shell`, async ({ exec, checkInstalledSoftwareOnDisk }) => {
|
||||||
const result1 = await exec('npm i --foreground-scripts playwright');
|
const result1 = await exec('npm i --foreground-scripts playwright');
|
||||||
expect(result1).toHaveLoggedSoftwareDownload([]);
|
expect(result1).toHaveLoggedSoftwareDownload([]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual([]);
|
await checkInstalledSoftwareOnDisk([]);
|
||||||
const result2 = await exec('npx playwright install --only-shell');
|
const result2 = await exec('npx playwright install --only-shell');
|
||||||
expect(result2).toHaveLoggedSoftwareDownload(['chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
expect(result2).toHaveLoggedSoftwareDownload(['chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit', ...extraInstalledSoftware]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual(['chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
await checkInstalledSoftwareOnDisk(['chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit', ...extraInstalledSoftware]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('@playwright/test should work', async ({ exec, installedSoftwareOnDisk }) => {
|
test('@playwright/test should work', async ({ exec, checkInstalledSoftwareOnDisk }) => {
|
||||||
const result1 = await exec('npm i --foreground-scripts @playwright/test');
|
const result1 = await exec('npm i --foreground-scripts @playwright/test');
|
||||||
expect(result1).toHaveLoggedSoftwareDownload([]);
|
expect(result1).toHaveLoggedSoftwareDownload([]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual([]);
|
await checkInstalledSoftwareOnDisk([]);
|
||||||
|
|
||||||
await exec('npx playwright test -c . sample.spec.js', { expectToExitWithError: true, message: 'should not be able to run tests without installing browsers' });
|
await exec('npx playwright test -c . sample.spec.js', { expectToExitWithError: true, message: 'should not be able to run tests without installing browsers' });
|
||||||
|
|
||||||
const result2 = await exec('npx playwright install');
|
const result2 = await exec('npx playwright install');
|
||||||
expect(result2).toHaveLoggedSoftwareDownload(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
expect(result2).toHaveLoggedSoftwareDownload(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit', ...extraInstalledSoftware]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
await checkInstalledSoftwareOnDisk(['chromium', 'chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit', ...extraInstalledSoftware]);
|
||||||
|
|
||||||
await exec('node sanity.js @playwright/test chromium firefox webkit');
|
await exec('node sanity.js @playwright/test chromium firefox webkit');
|
||||||
await exec('node', 'esm-playwright-test.mjs');
|
await exec('node', 'esm-playwright-test.mjs');
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ function patchPackageJsonForPreReleaseIfNeeded(tmpWorkspace: string) {
|
||||||
// Workaround per https://stackoverflow.com/questions/71479750/npm-install-pre-release-versions-for-peer-dependency.
|
// Workaround per https://stackoverflow.com/questions/71479750/npm-install-pre-release-versions-for-peer-dependency.
|
||||||
const pkg = JSON.parse(fs.readFileSync(path.resolve(tmpWorkspace, 'package.json'), 'utf-8'));
|
const pkg = JSON.parse(fs.readFileSync(path.resolve(tmpWorkspace, 'package.json'), 'utf-8'));
|
||||||
if (pkg.dependencies['@playwright/test'].match(/\d+\.\d+-\w+/)) {
|
if (pkg.dependencies['@playwright/test'].match(/\d+\.\d+-\w+/)) {
|
||||||
console.log(`Setting overrides in package.json to make pre-release version of peer dependency work.`);
|
|
||||||
pkg.overrides = { '@playwright/test': '$@playwright/test' };
|
pkg.overrides = { '@playwright/test': '$@playwright/test' };
|
||||||
fs.writeFileSync(path.resolve(tmpWorkspace, 'package.json'), JSON.stringify(pkg, null, 2));
|
fs.writeFileSync(path.resolve(tmpWorkspace, 'package.json'), JSON.stringify(pkg, null, 2));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,10 @@ import { test, expect } from './npmTest';
|
||||||
|
|
||||||
test.use({ isolateBrowsers: true });
|
test.use({ isolateBrowsers: true });
|
||||||
|
|
||||||
test('should skip browser installs', async ({ exec, installedSoftwareOnDisk }) => {
|
test('should skip browser installs', async ({ exec, checkInstalledSoftwareOnDisk }) => {
|
||||||
const result = await exec('npm i --foreground-scripts playwright @playwright/browser-firefox', { env: { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' } });
|
const result = await exec('npm i --foreground-scripts playwright @playwright/browser-firefox', { env: { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' } });
|
||||||
expect(result).toHaveLoggedSoftwareDownload([]);
|
expect(result).toHaveLoggedSoftwareDownload([]);
|
||||||
expect(await installedSoftwareOnDisk()).toEqual([]);
|
await checkInstalledSoftwareOnDisk([]);
|
||||||
expect(result).toContain(`Skipping browsers download because`);
|
expect(result).toContain(`Skipping browsers download because`);
|
||||||
|
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,20 @@ test('should work with --save-har', async ({ runCLI }, testInfo) => {
|
||||||
expect(json.log.creator.name).toBe('Playwright');
|
expect(json.log.creator.name).toBe('Playwright');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should work with --save-har and --save-har-glob', async ({ runCLI }, testInfo) => {
|
||||||
|
const harFileName = testInfo.outputPath('har.har');
|
||||||
|
const expectedResult = `await context.RouteFromHARAsync(${JSON.stringify(harFileName)}, new BrowserContextRouteFromHAROptions
|
||||||
|
{
|
||||||
|
Url = "**/*.js",
|
||||||
|
});`;
|
||||||
|
const cli = runCLI(['--target=csharp', `--save-har=${harFileName}`, '--save-har-glob=**/*.js'], {
|
||||||
|
autoExitWhen: expectedResult,
|
||||||
|
});
|
||||||
|
await cli.waitForCleanExit();
|
||||||
|
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
||||||
|
expect(json.log.creator.name).toBe('Playwright');
|
||||||
|
});
|
||||||
|
|
||||||
for (const testFramework of ['nunit', 'mstest'] as const) {
|
for (const testFramework of ['nunit', 'mstest'] as const) {
|
||||||
test(`should not print context options method override in ${testFramework} if no options were passed`, async ({ runCLI }) => {
|
test(`should not print context options method override in ${testFramework} if no options were passed`, async ({ runCLI }) => {
|
||||||
const cli = runCLI([`--target=csharp-${testFramework}`, emptyHTML]);
|
const cli = runCLI([`--target=csharp-${testFramework}`, emptyHTML]);
|
||||||
|
|
@ -201,7 +215,7 @@ for (const testFramework of ['nunit', 'mstest'] as const) {
|
||||||
|
|
||||||
test(`should work with --save-har in ${testFramework}`, async ({ runCLI }, testInfo) => {
|
test(`should work with --save-har in ${testFramework}`, async ({ runCLI }, testInfo) => {
|
||||||
const harFileName = testInfo.outputPath('har.har');
|
const harFileName = testInfo.outputPath('har.har');
|
||||||
const expectedResult = `await context.RouteFromHARAsync(${JSON.stringify(harFileName)});`;
|
const expectedResult = `await Context.RouteFromHARAsync(${JSON.stringify(harFileName)});`;
|
||||||
const cli = runCLI([`--target=csharp-${testFramework}`, `--save-har=${harFileName}`], {
|
const cli = runCLI([`--target=csharp-${testFramework}`, `--save-har=${harFileName}`], {
|
||||||
autoExitWhen: expectedResult,
|
autoExitWhen: expectedResult,
|
||||||
});
|
});
|
||||||
|
|
@ -209,6 +223,20 @@ for (const testFramework of ['nunit', 'mstest'] as const) {
|
||||||
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
||||||
expect(json.log.creator.name).toBe('Playwright');
|
expect(json.log.creator.name).toBe('Playwright');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(`should work with --save-har and --save-har-glob in ${testFramework}`, async ({ runCLI }, testInfo) => {
|
||||||
|
const harFileName = testInfo.outputPath('har.har');
|
||||||
|
const expectedResult = `await Context.RouteFromHARAsync(${JSON.stringify(harFileName)}, new BrowserContextRouteFromHAROptions
|
||||||
|
{
|
||||||
|
Url = "**/*.js",
|
||||||
|
});`;
|
||||||
|
const cli = runCLI([`--target=csharp-${testFramework}`, `--save-har=${harFileName}`, '--save-har-glob=**/*.js'], {
|
||||||
|
autoExitWhen: expectedResult,
|
||||||
|
});
|
||||||
|
await cli.waitForCleanExit();
|
||||||
|
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
||||||
|
expect(json.log.creator.name).toBe('Playwright');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test(`should print a valid basic program in mstest`, async ({ runCLI }) => {
|
test(`should print a valid basic program in mstest`, async ({ runCLI }) => {
|
||||||
|
|
|
||||||
|
|
@ -89,10 +89,24 @@ test('should print load/save storage_state', async ({ runCLI, browserName }, tes
|
||||||
await cli.waitFor(expectedResult2);
|
await cli.waitFor(expectedResult2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should work with --save-har', async ({ runCLI }, testInfo) => {
|
test('should work with --save-har and --save-har-glob as java-library', async ({ runCLI }, testInfo) => {
|
||||||
const harFileName = testInfo.outputPath('har.har');
|
const harFileName = testInfo.outputPath('har.har');
|
||||||
const expectedResult = `context.routeFromHAR(${JSON.stringify(harFileName)});`;
|
const expectedResult = `context.routeFromHAR(Paths.get(${JSON.stringify(harFileName)}), new BrowserContext.RouteFromHAROptions()
|
||||||
const cli = runCLI(['--target=java', `--save-har=${harFileName}`], {
|
.setUrl("**/*.js"));`;
|
||||||
|
const cli = runCLI(['--target=java', `--save-har=${harFileName}`, '--save-har-glob=**/*.js'], {
|
||||||
|
autoExitWhen: expectedResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
await cli.waitForCleanExit();
|
||||||
|
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
||||||
|
expect(json.log.creator.name).toBe('Playwright');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should work with --save-har and --save-har-glob as java-junit', async ({ runCLI }, testInfo) => {
|
||||||
|
const harFileName = testInfo.outputPath('har.har');
|
||||||
|
const expectedResult = `page.routeFromHAR(Paths.get(${JSON.stringify(harFileName)}), new Page.RouteFromHAROptions()
|
||||||
|
.setUrl("**/*.js"));`;
|
||||||
|
const cli = runCLI(['--target=java-junit', `--save-har=${harFileName}`, '--save-har-glob=**/*.js'], {
|
||||||
autoExitWhen: expectedResult,
|
autoExitWhen: expectedResult,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,3 +69,25 @@ def test_example(page: Page) -> None:
|
||||||
page.goto("${emptyHTML}")
|
page.goto("${emptyHTML}")
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should work with --save-har', async ({ runCLI }, testInfo) => {
|
||||||
|
const harFileName = testInfo.outputPath('har.har');
|
||||||
|
const expectedResult = `page.route_from_har(${JSON.stringify(harFileName)})`;
|
||||||
|
const cli = runCLI(['--target=python-pytest', `--save-har=${harFileName}`], {
|
||||||
|
autoExitWhen: expectedResult,
|
||||||
|
});
|
||||||
|
await cli.waitForCleanExit();
|
||||||
|
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
||||||
|
expect(json.log.creator.name).toBe('Playwright');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should work with --save-har and --save-har-glob', async ({ runCLI }, testInfo) => {
|
||||||
|
const harFileName = testInfo.outputPath('har.har');
|
||||||
|
const expectedResult = `page.route_from_har(${JSON.stringify(harFileName)}, url="**/*.js")`;
|
||||||
|
const cli = runCLI(['--target=python-pytest', `--save-har=${harFileName}`, '--save-har-glob=**/*.js'], {
|
||||||
|
autoExitWhen: expectedResult,
|
||||||
|
});
|
||||||
|
await cli.waitForCleanExit();
|
||||||
|
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
||||||
|
expect(json.log.creator.name).toBe('Playwright');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ asyncio.run(main())
|
||||||
|
|
||||||
test('should work with --save-har', async ({ runCLI }, testInfo) => {
|
test('should work with --save-har', async ({ runCLI }, testInfo) => {
|
||||||
const harFileName = testInfo.outputPath('har.har');
|
const harFileName = testInfo.outputPath('har.har');
|
||||||
const expectedResult = `await page.route_from_har(${JSON.stringify(harFileName)})`;
|
const expectedResult = `await context.route_from_har(${JSON.stringify(harFileName)})`;
|
||||||
const cli = runCLI(['--target=python-async', `--save-har=${harFileName}`], {
|
const cli = runCLI(['--target=python-async', `--save-har=${harFileName}`], {
|
||||||
autoExitWhen: expectedResult,
|
autoExitWhen: expectedResult,
|
||||||
});
|
});
|
||||||
|
|
@ -154,3 +154,14 @@ test('should work with --save-har', async ({ runCLI }, testInfo) => {
|
||||||
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
||||||
expect(json.log.creator.name).toBe('Playwright');
|
expect(json.log.creator.name).toBe('Playwright');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should work with --save-har and --save-har-glob', async ({ runCLI }, testInfo) => {
|
||||||
|
const harFileName = testInfo.outputPath('har.har');
|
||||||
|
const expectedResult = `await context.route_from_har(${JSON.stringify(harFileName)}, url="**/*.js")`;
|
||||||
|
const cli = runCLI(['--target=python-async', `--save-har=${harFileName}`, '--save-har-glob=**/*.js'], {
|
||||||
|
autoExitWhen: expectedResult,
|
||||||
|
});
|
||||||
|
await cli.waitForCleanExit();
|
||||||
|
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
||||||
|
expect(json.log.creator.name).toBe('Playwright');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -129,3 +129,25 @@ with sync_playwright() as playwright:
|
||||||
`;
|
`;
|
||||||
await cli.waitFor(expectedResult2);
|
await cli.waitFor(expectedResult2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should work with --save-har', async ({ runCLI }, testInfo) => {
|
||||||
|
const harFileName = testInfo.outputPath('har.har');
|
||||||
|
const expectedResult = `context.route_from_har(${JSON.stringify(harFileName)})`;
|
||||||
|
const cli = runCLI(['--target=python-async', `--save-har=${harFileName}`], {
|
||||||
|
autoExitWhen: expectedResult,
|
||||||
|
});
|
||||||
|
await cli.waitForCleanExit();
|
||||||
|
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
||||||
|
expect(json.log.creator.name).toBe('Playwright');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should work with --save-har and --save-har-glob', async ({ runCLI }, testInfo) => {
|
||||||
|
const harFileName = testInfo.outputPath('har.har');
|
||||||
|
const expectedResult = `context.route_from_har(${JSON.stringify(harFileName)}, url="**/*.js")`;
|
||||||
|
const cli = runCLI(['--target=python-async', `--save-har=${harFileName}`, '--save-har-glob=**/*.js'], {
|
||||||
|
autoExitWhen: expectedResult,
|
||||||
|
});
|
||||||
|
await cli.waitForCleanExit();
|
||||||
|
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
||||||
|
expect(json.log.creator.name).toBe('Playwright');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -108,3 +108,18 @@ test('should generate routeFromHAR with --save-har', async ({ runCLI }, testInfo
|
||||||
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
||||||
expect(json.log.creator.name).toBe('Playwright');
|
expect(json.log.creator.name).toBe('Playwright');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should generate routeFromHAR with --save-har and --save-har-glob', async ({ runCLI }, testInfo) => {
|
||||||
|
const harFileName = testInfo.outputPath('har.har');
|
||||||
|
const expectedResult = `test('test', async ({ page }) => {
|
||||||
|
await page.routeFromHAR('${harFileName.replace(/\\/g, '\\\\')}', {
|
||||||
|
url: '**/*.js'
|
||||||
|
});
|
||||||
|
});`;
|
||||||
|
const cli = runCLI(['--target=playwright-test', `--save-har=${harFileName}`, '--save-har-glob=**/*.js'], {
|
||||||
|
autoExitWhen: expectedResult,
|
||||||
|
});
|
||||||
|
await cli.waitForCleanExit();
|
||||||
|
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
|
||||||
|
expect(json.log.creator.name).toBe('Playwright');
|
||||||
|
});
|
||||||
|
|
@ -615,3 +615,27 @@ it('parseLocator frames', async () => {
|
||||||
expect.soft(parseLocator('java', `locator("iframe").contentFrame().getByText("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
|
expect.soft(parseLocator('java', `locator("iframe").contentFrame().getByText("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
|
||||||
expect.soft(parseLocator('java', `frameLocator("iframe").getByText("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
|
expect.soft(parseLocator('java', `frameLocator("iframe").getByText("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not oom in locator parser', async ({ page }) => {
|
||||||
|
const l = page.locator.bind(page);
|
||||||
|
const locator = page.locator('text=L1').or(l('text=L2').or(l('text=L3').or(l('text=L4')).or(l('#f0')
|
||||||
|
.contentFrame().locator('#f0_mid_0')
|
||||||
|
.contentFrame().locator('text=L5').or(l('text=L6'))).or(l('#f0')
|
||||||
|
.contentFrame().locator('#f0_mid_0')
|
||||||
|
.contentFrame().locator('text=L7')
|
||||||
|
.or(l('text=L8'))))).or(l('text=L9').or(l('text=L10').or(l('text=L11')).or(l('#f0')
|
||||||
|
.contentFrame().locator('#f0_mid_0')
|
||||||
|
.contentFrame().locator('text=L12').or(l('text=L13'))).or(l('#f0')
|
||||||
|
.contentFrame().locator('#f0_mid_0')
|
||||||
|
.contentFrame().locator('text=L14').or(l('text=L15'))))).or(l('text=L16').or(l('text=L17').or(l('text=L18')).or(l('#f0')
|
||||||
|
.contentFrame().locator('#f0_mid_0')
|
||||||
|
.contentFrame().locator('text=L19').or(l('text=L20'))).or(l('#f0')
|
||||||
|
.contentFrame().locator('#f0_mid_0')
|
||||||
|
.contentFrame().locator('text=L21').or(l('text=L22'))))).or(l('text=L23').or(l('text=L24').or(l('text=L25')).or(l('#f0')
|
||||||
|
.contentFrame().locator('#f0_mid_0')
|
||||||
|
.contentFrame().locator('text=L26').or(l('text=L27'))).or(l('#f0')
|
||||||
|
.contentFrame().locator('#f0_mid_0')
|
||||||
|
.contentFrame().locator('text=L28').or(l('text=L29')))));
|
||||||
|
const error = await locator.count().catch(e => e);
|
||||||
|
expect(error.message).toContain('Frame locators are not allowed inside composite locators');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -482,6 +482,7 @@ it('should escape yaml text in text nodes', async ({ page }) => {
|
||||||
{<a href="#">four</a>}
|
{<a href="#">four</a>}
|
||||||
[<a href="#">five</a>]
|
[<a href="#">five</a>]
|
||||||
</ul>
|
</ul>
|
||||||
|
<div>[Select all]</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await checkAndMatchSnapshot(page.locator('body'), `
|
await checkAndMatchSnapshot(page.locator('body'), `
|
||||||
|
|
@ -504,6 +505,7 @@ it('should escape yaml text in text nodes', async ({ page }) => {
|
||||||
- text: "} ["
|
- text: "} ["
|
||||||
- link "five"
|
- link "five"
|
||||||
- text: "]"
|
- text: "]"
|
||||||
|
- text: "[Select all]"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,18 @@ it('should work with >> visible=', async ({ page }) => {
|
||||||
expect(await page.$eval('div >> visible=true', div => div.id)).toBe('target2');
|
expect(await page.$eval('div >> visible=true', div => div.id)).toBe('target2');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should work with >> visible=false', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<section>
|
||||||
|
<div id=target1></div>
|
||||||
|
<div id=target2></div>
|
||||||
|
</section>
|
||||||
|
`);
|
||||||
|
await expect(page.locator('div >> visible=false')).toHaveCount(2);
|
||||||
|
await page.locator('#target2').evaluate(div => div.textContent = 'Now visible');
|
||||||
|
await expect(page.locator('div >> visible=false')).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('should work with :nth-match', async ({ page }) => {
|
it('should work with :nth-match', async ({ page }) => {
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
<section>
|
<section>
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,7 @@ for (const useIntermediateMergeReport of [false, true] as const) {
|
||||||
expect(result.output).toContain(`Slow test file: [bar] › dir${path.sep}a.test.js (`);
|
expect(result.output).toContain(`Slow test file: [bar] › dir${path.sep}a.test.js (`);
|
||||||
expect(result.output).toContain(`Slow test file: [baz] › dir${path.sep}a.test.js (`);
|
expect(result.output).toContain(`Slow test file: [baz] › dir${path.sep}a.test.js (`);
|
||||||
expect(result.output).toContain(`Slow test file: [qux] › dir${path.sep}a.test.js (`);
|
expect(result.output).toContain(`Slow test file: [qux] › dir${path.sep}a.test.js (`);
|
||||||
expect(result.output).toContain(`Consider splitting slow test files to speed up parallel execution`);
|
expect(result.output).toContain(`Consider running tests from slow files in parallel`);
|
||||||
expect(result.output).not.toContain(`Slow test file: [foo] › dir${path.sep}b.test.js (`);
|
expect(result.output).not.toContain(`Slow test file: [foo] › dir${path.sep}b.test.js (`);
|
||||||
expect(result.output).not.toContain(`Slow test file: [bar] › dir${path.sep}b.test.js (`);
|
expect(result.output).not.toContain(`Slow test file: [bar] › dir${path.sep}b.test.js (`);
|
||||||
expect(result.output).not.toContain(`Slow test file: [baz] › dir${path.sep}b.test.js (`);
|
expect(result.output).not.toContain(`Slow test file: [baz] › dir${path.sep}b.test.js (`);
|
||||||
|
|
|
||||||
|
|
@ -936,6 +936,9 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
await expect(attachment).not.toBeInViewport();
|
await expect(attachment).not.toBeInViewport();
|
||||||
await page.getByLabel('attach "foo-2"').getByTitle('link to attachment').click();
|
await page.getByLabel('attach "foo-2"').getByTitle('link to attachment').click();
|
||||||
await expect(attachment).toBeInViewport();
|
await expect(attachment).toBeInViewport();
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
await expect(attachment).toBeInViewport();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('steps with internal attachments have links', async ({ runInlineTest, page, showReport }) => {
|
test('steps with internal attachments have links', async ({ runInlineTest, page, showReport }) => {
|
||||||
|
|
|
||||||
|
|
@ -1494,3 +1494,103 @@ fixture | fixture: context
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('test.step.fail and test.step.fixme should work', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'reporter.ts': stepIndentReporter,
|
||||||
|
'playwright.config.ts': `module.exports = { reporter: './reporter' };`,
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({ }) => {
|
||||||
|
await test.step('outer step 1', async () => {
|
||||||
|
await test.step.fail('inner step 1.1', async () => {
|
||||||
|
throw new Error('inner step 1.1 failed');
|
||||||
|
});
|
||||||
|
await test.step.fixme('inner step 1.2', async () => {});
|
||||||
|
await test.step('inner step 1.3', async () => {});
|
||||||
|
});
|
||||||
|
await test.step('outer step 2', async () => {
|
||||||
|
await test.step.fixme('inner step 2.1', async () => {});
|
||||||
|
await test.step('inner step 2.2', async () => {
|
||||||
|
expect(1).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await test.step.fail('outer step 3', async () => {
|
||||||
|
throw new Error('outer step 3 failed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { reporter: '' });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.report.stats.expected).toBe(1);
|
||||||
|
expect(result.report.stats.unexpected).toBe(0);
|
||||||
|
expect(stripAnsi(result.output)).toBe(`
|
||||||
|
hook |Before Hooks
|
||||||
|
test.step |outer step 1 @ a.test.ts:4
|
||||||
|
test.step | inner step 1.1 @ a.test.ts:5
|
||||||
|
test.step | ↪ error: Error: inner step 1.1 failed
|
||||||
|
test.step | inner step 1.3 @ a.test.ts:9
|
||||||
|
test.step |outer step 2 @ a.test.ts:11
|
||||||
|
test.step | inner step 2.2 @ a.test.ts:13
|
||||||
|
expect | expect.toBe @ a.test.ts:14
|
||||||
|
test.step |outer step 3 @ a.test.ts:17
|
||||||
|
test.step |↪ error: Error: outer step 3 failed
|
||||||
|
hook |After Hooks
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('timeout inside test.step.fail is an error', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'reporter.ts': stepIndentReporter,
|
||||||
|
'playwright.config.ts': `module.exports = { reporter: './reporter' };`,
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test 2', async ({ }) => {
|
||||||
|
await test.step('outer step 2', async () => {
|
||||||
|
await test.step.fail('inner step 2', async () => {
|
||||||
|
await new Promise(() => {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { reporter: '', timeout: 2500 });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.report.stats.unexpected).toBe(1);
|
||||||
|
expect(stripAnsi(result.output)).toBe(`
|
||||||
|
hook |Before Hooks
|
||||||
|
test.step |outer step 2 @ a.test.ts:4
|
||||||
|
test.step | inner step 2 @ a.test.ts:5
|
||||||
|
hook |After Hooks
|
||||||
|
hook |Worker Cleanup
|
||||||
|
|Test timeout of 2500ms exceeded.
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skip test.step.fixme body', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'reporter.ts': stepIndentReporter,
|
||||||
|
'playwright.config.ts': `module.exports = { reporter: './reporter' };`,
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({ }) => {
|
||||||
|
let didRun = false;
|
||||||
|
await test.step('outer step 2', async () => {
|
||||||
|
await test.step.fixme('inner step 2', async () => {
|
||||||
|
didRun = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(didRun).toBe(false);
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { reporter: '' });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.report.stats.expected).toBe(1);
|
||||||
|
expect(stripAnsi(result.output)).toBe(`
|
||||||
|
hook |Before Hooks
|
||||||
|
test.step |outer step 2 @ a.test.ts:5
|
||||||
|
expect |expect.toBe @ a.test.ts:10
|
||||||
|
hook |After Hooks
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -204,3 +204,26 @@ test('step should inherit return type from its callback ', async ({ runTSC }) =>
|
||||||
});
|
});
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('step.fail and step.fixme return void ', async ({ runTSC }) => {
|
||||||
|
const result = await runTSC({
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test step.fail', async ({ }) => {
|
||||||
|
// @ts-expect-error
|
||||||
|
const bad1: string = await test.step.fail('my step', () => { });
|
||||||
|
const good: void = await test.step.fail('my step', async () => {
|
||||||
|
return 2024;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('test step.fixme', async ({ }) => {
|
||||||
|
// @ts-expect-error
|
||||||
|
const bad1: string = await test.step.fixme('my step', () => { });
|
||||||
|
const good: void = await test.step.fixme('my step', async () => {
|
||||||
|
return 2024;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -338,6 +338,38 @@ test('should show request source context id', async ({ runUITest, server }) => {
|
||||||
await expect(page.getByText('api#1')).toBeVisible();
|
await expect(page.getByText('api#1')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should work behind reverse proxy', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33705' } }, async ({ runUITest, proxyServer: reverseProxy }) => {
|
||||||
|
const { page } = await runUITest({
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('trace test', async ({ page }) => {
|
||||||
|
await page.setContent('<button>Submit</button>');
|
||||||
|
await page.getByRole('button').click();
|
||||||
|
expect(1).toBe(1);
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uiModeUrl = new URL(page.url());
|
||||||
|
reverseProxy.forwardTo(+uiModeUrl.port, { prefix: '/subdir', preserveHostname: true });
|
||||||
|
await page.goto(`${reverseProxy.URL}/subdir${uiModeUrl.pathname}?${uiModeUrl.searchParams}`);
|
||||||
|
|
||||||
|
await page.getByText('trace test').dblclick();
|
||||||
|
|
||||||
|
await expect(page.getByTestId('actions-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem /Before Hooks \\d+[hmsp]+/
|
||||||
|
- treeitem /page\\.setContent \\d+[hmsp]+/
|
||||||
|
- treeitem /locator\\.clickgetByRole\\('button'\\) \\d+[hmsp]+/
|
||||||
|
- treeitem /expect\\.toBe \\d+[hmsp]+/ [selected]
|
||||||
|
- treeitem /After Hooks \\d+[hmsp]+/
|
||||||
|
`);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.frameLocator('iframe.snapshot-visible[name=snapshot]').locator('button'),
|
||||||
|
).toHaveText('Submit');
|
||||||
|
});
|
||||||
|
|
||||||
test('should filter actions tab on double-click', async ({ runUITest, server }) => {
|
test('should filter actions tab on double-click', async ({ runUITest, server }) => {
|
||||||
const { page } = await runUITest({
|
const { page } = await runUITest({
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
|
|
|
||||||
27
tests/third_party/proxy/index.ts
vendored
27
tests/third_party/proxy/index.ts
vendored
|
|
@ -3,6 +3,7 @@ import * as net from 'net';
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
|
import { pipeline } from 'stream/promises';
|
||||||
|
|
||||||
const pkg = { version: '1.0.0' }
|
const pkg = { version: '1.0.0' }
|
||||||
|
|
||||||
|
|
@ -33,6 +34,7 @@ export function createProxy(server?: http.Server): ProxyServer {
|
||||||
if (!server) server = http.createServer();
|
if (!server) server = http.createServer();
|
||||||
server.on('request', onrequest);
|
server.on('request', onrequest);
|
||||||
server.on('connect', onconnect);
|
server.on('connect', onconnect);
|
||||||
|
server.on('upgrade', onupgrade);
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -465,4 +467,29 @@ function requestAuthorization(
|
||||||
};
|
};
|
||||||
res.writeHead(407, headers);
|
res.writeHead(407, headers);
|
||||||
res.end('Proxy authorization required');
|
res.end('Proxy authorization required');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onupgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
||||||
|
const proxyReq = http.request(req.url, {
|
||||||
|
method: req.method,
|
||||||
|
headers: req.headers,
|
||||||
|
localAddress: this.localAddress,
|
||||||
|
});
|
||||||
|
|
||||||
|
proxyReq.on('upgrade', async function (proxyRes, proxySocket, proxyHead) {
|
||||||
|
const header = ['HTTP/1.1 101 Switching Protocols'];
|
||||||
|
for (const [key, value] of Object.entries(proxyRes.headersDistinct))
|
||||||
|
header.push(`${key}: ${value}`);
|
||||||
|
socket.write(header.join('\r\n') + '\r\n\r\n');
|
||||||
|
if (proxyHead && proxyHead.length) proxySocket.unshift(proxyHead);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pipeline(proxySocket, socket, proxySocket);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "ECONNRESET")
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
proxyReq.end(head);
|
||||||
}
|
}
|
||||||
|
|
@ -362,7 +362,7 @@ function writeFile(filePath, content) {
|
||||||
fs.writeFileSync(filePath, content, 'utf8');
|
fs.writeFileSync(filePath, content, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFile(path.join(__dirname, '..', 'packages', 'protocol', 'src', 'channels.ts'), channels_ts.join('\n'));
|
writeFile(path.join(__dirname, '..', 'packages', 'protocol', 'src', 'channels.d.ts'), channels_ts.join('\n'));
|
||||||
writeFile(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'debug.ts'), debug_ts.join('\n'));
|
writeFile(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'debug.ts'), debug_ts.join('\n'));
|
||||||
writeFile(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'validator.ts'), validator_ts.join('\n'));
|
writeFile(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'validator.ts'), validator_ts.join('\n'));
|
||||||
process.exit(hasChanges ? 1 : 0);
|
process.exit(hasChanges ? 1 : 0);
|
||||||
|
|
|
||||||
6
utils/generate_types/overrides-test.d.ts
vendored
6
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -162,7 +162,11 @@ export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
|
||||||
afterAll(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
|
afterAll(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
|
||||||
afterAll(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
|
afterAll(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
|
||||||
use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void;
|
use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void;
|
||||||
step<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>;
|
step: {
|
||||||
|
<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>;
|
||||||
|
fixme(title: string, body: () => any | Promise<any>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<void>;
|
||||||
|
fail(title: string, body: () => any | Promise<any>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<void>;
|
||||||
|
}
|
||||||
expect: Expect<{}>;
|
expect: Expect<{}>;
|
||||||
extend<T extends {}, W extends {} = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;
|
extend<T extends {}, W extends {} = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;
|
||||||
info(): TestInfo;
|
info(): TestInfo;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue