This commit is contained in:
JacksonLei123 2024-12-18 21:13:24 -05:00
commit ad28bcba36
38 changed files with 736 additions and 63 deletions

View file

@ -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

View file

@ -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"

View file

@ -143,7 +143,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);

View file

@ -9,9 +9,9 @@
}, },
{ {
"name": "chromium-tip-of-tree", "name": "chromium-tip-of-tree",
"revision": "1286", "revision": "1287",
"installByDefault": false, "installByDefault": false,
"browserVersion": "133.0.6891.0" "browserVersion": "133.0.6901.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",

View file

@ -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,
}; };
} }

View file

@ -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,

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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) {

View file

@ -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;
}); });
} }

View file

@ -5570,7 +5570,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).
* *

View file

@ -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';

View file

@ -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 };

View file

@ -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,

View file

@ -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';

View file

@ -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';

View file

@ -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,

View file

@ -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 = {

View file

@ -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)[];

View file

@ -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}`);
if (options?.preserveHostname)
url.port = '' + port;
else
url.host = `127.0.0.1:${port}`; 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) {

View file

@ -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;

View file

@ -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 }) => {

View file

@ -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,
}); });

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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('should highlight textual diff', async ({ runInlineTest, showReport, page }) => { test('should highlight textual diff', async ({ runInlineTest, showReport, page }) => {

View file

@ -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
`);
});

View file

@ -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);
});

View file

@ -340,6 +340,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': `

View file

@ -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;
} }
@ -466,3 +468,28 @@ 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);
}

View file

@ -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;