diff --git a/.github/workflows/tests_others.yml b/.github/workflows/tests_others.yml
index ed18e1541c635..b35a61d3b234d 100644
--- a/.github/workflows/tests_others.yml
+++ b/.github/workflows/tests_others.yml
@@ -134,6 +134,27 @@ jobs:
env:
PW_CLOCK: ${{ matrix.clock }}
+ test_legacy_progress_timeouts:
+ name: legacy progress timeouts
+ environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
+ permissions:
+ id-token: write # This is required for OIDC login (azure/login) to succeed
+ contents: read # This is required for actions/checkout to succeed
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/actions/run-test
+ with:
+ node-version: 20
+ browsers-to-install: chromium
+ command: npm run test -- --project=chromium-*
+ bot-name: "legacy-progress-timeouts-linux"
+ flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
+ flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
+ flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
+ env:
+ PLAYWRIGHT_LEGACY_TIMEOUTS: 1
+
test_electron:
name: Electron - ${{ matrix.os }}
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
diff --git a/README.md b/README.md
index eb576b1d38d78..96ab31796073a 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# 🎠Playwright
-[](https://www.npmjs.com/package/playwright) [](https://www.chromium.org/Home) [](https://www.mozilla.org/en-US/firefox/new/) [](https://webkit.org/) [](https://aka.ms/playwright/discord)
+[](https://www.npmjs.com/package/playwright) [](https://www.chromium.org/Home) [](https://www.mozilla.org/en-US/firefox/new/) [](https://webkit.org/) [](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
-| Chromium 138.0.7204.15 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
+| Chromium 138.0.7204.35 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit 18.5 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox 139.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md
index f9dda43e11a29..375b6a48b507a 100644
--- a/docs/src/api/class-browsercontext.md
+++ b/docs/src/api/class-browsercontext.md
@@ -987,6 +987,7 @@ Here are some permissions that may be supported by some browsers:
* `'notifications'`
* `'payment-handler'`
* `'storage-access'`
+* `'local-fonts'`
### option: BrowserContext.grantPermissions.origin
* since: v1.8
diff --git a/docs/src/clock.md b/docs/src/clock.md
index 44582f450befb..e898899b18251 100644
--- a/docs/src/clock.md
+++ b/docs/src/clock.md
@@ -64,6 +64,47 @@ await page.clock.setFixedTime(new Date('2024-02-02T10:30:00'));
await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM');
```
+```python async
+await page.clock.set_fixed_time(datetime.datetime(2024, 2, 2, 10, 0, 0))
+await page.goto("http://localhost:3333")
+await expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM")
+
+await page.clock.set_fixed_time(datetime.datetime(2024, 2, 2, 10, 30, 0))
+# We know that the page has a timer that updates the time every second.
+await expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM")
+```
+
+```python sync
+page.clock.set_fixed_time(datetime.datetime(2024, 2, 2, 10, 0, 0))
+page.goto("http://localhost:3333")
+expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM")
+page.clock.set_fixed_time(datetime.datetime(2024, 2, 2, 10, 30, 0))
+# We know that the page has a timer that updates the time every second.
+expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM")
+```
+
+```java
+SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
+page.clock().setFixedTime(format.parse("2024-02-02T10:00:00"));
+page.navigate("http://localhost:3333");
+Locator locator = page.getByTestId("current-time");
+assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
+page.clock().setFixedTime(format.parse("2024-02-02T10:30:00"));
+// We know that the page has a timer that updates the time every second.
+assertThat(locator).hasText("2/2/2024, 10:30:00 AM");
+```
+
+```csharp
+// Set the fixed time for the clock.
+await Page.Clock.SetFixedTimeAsync(new DateTime(2024, 2, 2, 10, 0, 0));
+await Page.GotoAsync("http://localhost:3333");
+await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:00:00 AM");
+// Set the fixed time for the clock.
+await Page.Clock.SetFixedTimeAsync(new DateTime(2024, 2, 2, 10, 30, 0));
+// We know that the page has a timer that updates the time every second.
+await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:30:00 AM");
+```
+
## Consistent time and timers
Sometimes your timers depend on `Date.now` and are confused when the `Date.now` value does not change over time.
diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md
index 2511f412d2871..d976058aef18d 100644
--- a/docs/src/test-api/class-test.md
+++ b/docs/src/test-api/class-test.md
@@ -1425,7 +1425,7 @@ Timeout in milliseconds.
Skip a test. Playwright will not run the test past the `test.skip()` call.
-Skipped tests are not supposed to be ever run. If you intent to fix the test, use [`method: Test.fixme`] instead.
+Skipped tests are not supposed to be ever run. If you intend to fix the test, use [`method: Test.fixme`] instead.
To declare a skipped test:
* `test.skip(title, body)`
diff --git a/docs/src/test-reporters-js.md b/docs/src/test-reporters-js.md
index 294f0466ddb79..e5fe3f526d632 100644
--- a/docs/src/test-reporters-js.md
+++ b/docs/src/test-reporters-js.md
@@ -102,7 +102,7 @@ List report supports the following configuration options and environment variabl
| Environment Variable Name | Reporter Config Option| Description | Default
|---|---|---|---|
| `PLAYWRIGHT_LIST_PRINT_STEPS` | `printSteps` | Whether to print each step on its own line. | `false`
-| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. If a number is specified, it will also be used as the terminal width. | `true` when terminal is in TTY mode, `false` otherwise.
+| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. Supports `true`, `1`, `false`, `0`, `[WIDTH]`, and `[WIDTH]x[HEIGHT]`. `[WIDTH]` and `[WIDTH]x[HEIGHT]` specifies the TTY dimensions. | `true` when terminal is in TTY mode, `false` otherwise.
| `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise.
@@ -140,7 +140,7 @@ Line report supports the following configuration options and environment variabl
| Environment Variable Name | Reporter Config Option| Description | Default
|---|---|---|---|
-| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. If a number is specified, it will also be used as the terminal width. | `true` when terminal is in TTY mode, `false` otherwise.
+| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. Supports `true`, `1`, `false`, `0`, `[WIDTH]`, and `[WIDTH]x[HEIGHT]`. `[WIDTH]` and `[WIDTH]x[HEIGHT]` specifies the TTY dimensions. | `true` when terminal is in TTY mode, `false` otherwise.
| `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise.
@@ -182,7 +182,7 @@ Dot report supports the following configuration options and environment variable
| Environment Variable Name | Reporter Config Option| Description | Default
|---|---|---|---|
-| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. If a number is specified, it will also be used as the terminal width. | `true` when terminal is in TTY mode, `false` otherwise.
+| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. Supports `true`, `1`, `false`, `0`, `[WIDTH]`, and `[WIDTH]x[HEIGHT]`. `[WIDTH]` and `[WIDTH]x[HEIGHT]` specifies the TTY dimensions. | `true` when terminal is in TTY mode, `false` otherwise.
| `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise.
### HTML reporter
diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts
index 135336e869907..04034c8ceb3fc 100644
--- a/packages/html-reporter/src/filter.ts
+++ b/packages/html-reporter/src/filter.ts
@@ -29,7 +29,10 @@ export class Filter {
annotations: FilterToken[] = [];
empty(): boolean {
- return this.project.length + this.status.length + this.text.length === 0;
+ return (
+ this.project.length + this.status.length + this.text.length +
+ this.labels.length + this.annotations.length
+ ) === 0;
}
static parse(expression: string): Filter {
diff --git a/packages/html-reporter/src/headerView.tsx b/packages/html-reporter/src/headerView.tsx
index bea84bcdd0e43..0b80590212b8d 100644
--- a/packages/html-reporter/src/headerView.tsx
+++ b/packages/html-reporter/src/headerView.tsx
@@ -23,6 +23,7 @@ import * as icons from './icons';
import { Link, navigate, SearchParamsContext } from './links';
import { statusIcon } from './statusIcon';
import { filterWithToken } from './filter';
+import { linkifyText } from '@web/renderUtils';
export const HeaderView: React.FC<{
title: string | undefined,
@@ -35,7 +36,7 @@ export const HeaderView: React.FC<{
{rightSuperHeader}
- {title &&
{title}
}
+ {title &&
{linkifyText(title)}
}
;
};
@@ -60,13 +61,16 @@ export const GlobalFilterView: React.FC<{
event => {
event.preventDefault();
const url = new URL(window.location.href);
- url.hash = filterText ? '?' + new URLSearchParams({ q: filterText }) : '';
+ // If onSubmit happens immediately after onChange, the filterText state is not updated yet.
+ // Using FormData here is a workaround to get the latest value.
+ const q = new FormData(event.target as HTMLFormElement).get('q') as string;
+ url.hash = q ? '?' + new URLSearchParams({ q }) : '';
navigate(url);
}
}>
{icons.search()}
{/* Use navigationId to reset defaultValue */}
- {
+ {
setFilterText(e.target.value);
}}>
diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts
index a75a5cd185d6c..c1f2ab4bdc411 100644
--- a/packages/injected/src/ariaSnapshot.ts
+++ b/packages/injected/src/ariaSnapshot.ts
@@ -16,7 +16,7 @@
import { escapeRegExp, longestCommonSubstring, normalizeWhiteSpace } from '@isomorphic/stringUtils';
-import { box, getElementComputedStyle, getGlobalOptions, isElementVisible } from './domUtils';
+import { box, getElementComputedStyle, isElementVisible } from './domUtils';
import * as roleUtils from './roleUtils';
import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded } from './yaml';
@@ -214,7 +214,7 @@ function toAriaNode(element: Element, options?: { forAI?: boolean, refPrefix?: s
result.selected = roleUtils.getAriaSelected(element);
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
- if (element.type !== 'checkbox' && element.type !== 'radio' && (element.type !== 'file' || getGlobalOptions().inputFileRoleTextbox))
+ if (element.type !== 'checkbox' && element.type !== 'radio' && element.type !== 'file')
result.children = [element.value];
}
diff --git a/packages/injected/src/domUtils.ts b/packages/injected/src/domUtils.ts
index eb97e04a8b322..e11d6476949cb 100644
--- a/packages/injected/src/domUtils.ts
+++ b/packages/injected/src/domUtils.ts
@@ -16,7 +16,6 @@
type GlobalOptions = {
browserNameForWorkarounds?: string;
- inputFileRoleTextbox?: boolean;
};
let globalOptions: GlobalOptions = {};
export function setGlobalOptions(options: GlobalOptions) {
diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts
index 05a1c9eb97f88..bcc6be892e4e9 100644
--- a/packages/injected/src/injectedScript.ts
+++ b/packages/injected/src/injectedScript.ts
@@ -53,9 +53,8 @@ export type ElementState = 'visible' | 'hidden' | 'enabled' | 'disabled' | 'edit
export type ElementStateWithoutStable = Exclude;
export type ElementStateQueryResult = { matches: boolean, received?: string | 'error:notconnected' };
-export type HitTargetError = { hitTargetDescription: string, hasPositionStickyOrFixed: boolean };
export type HitTargetInterceptionResult = {
- stop: () => 'done' | HitTargetError;
+ stop: () => 'done' | { hitTargetDescription: string };
};
interface WebKitLegacyDeviceOrientationEvent extends DeviceOrientationEvent {
@@ -73,7 +72,6 @@ export type InjectedScriptOptions = {
testIdAttributeName: string;
stableRafCount: number;
browserName: string;
- inputFileRoleTextbox: boolean;
customEngines: { name: string, source: string }[];
};
@@ -236,7 +234,7 @@ export class InjectedScript {
this._stableRafCount = options.stableRafCount;
this._browserName = options.browserName;
- setGlobalOptions({ browserNameForWorkarounds: options.browserName, inputFileRoleTextbox: options.inputFileRoleTextbox });
+ setGlobalOptions({ browserNameForWorkarounds: options.browserName });
this._setupGlobalListenersRemovalDetection();
this._setupHitTargetInterceptors();
@@ -924,7 +922,7 @@ export class InjectedScript {
input.dispatchEvent(new Event('change', { bubbles: true }));
}
- expectHitTarget(hitPoint: { x: number, y: number }, targetElement: Element): 'done' | HitTargetError {
+ expectHitTarget(hitPoint: { x: number, y: number }, targetElement: Element) {
const roots: (Document | ShadowRoot)[] = [];
// Get all component roots leading to the target element.
@@ -977,21 +975,14 @@ export class InjectedScript {
// Check whether hit target is the target or its descendant.
const hitParents: Element[] = [];
- const isHitParentPositionStickyOrFixed: boolean[] = [];
while (hitElement && hitElement !== targetElement) {
hitParents.push(hitElement);
- isHitParentPositionStickyOrFixed.push(['sticky', 'fixed'].includes(this.window.getComputedStyle(hitElement).position));
hitElement = parentElementOrShadowHost(hitElement);
}
if (hitElement === targetElement)
return 'done';
- // The description of the element that was hit instead of the target element.
const hitTargetDescription = this.previewNode(hitParents[0] || this.document.documentElement);
- // Whether any ancestor of the hit target has position: static. In this case, it could be
- // beneficial to scroll the target element into different positions to reveal it.
- let hasPositionStickyOrFixed = isHitParentPositionStickyOrFixed.some(x => x);
-
// Root is the topmost element in the hitTarget's chain that is not in the
// element's chain. For example, it might be a dialog element that overlays
// the target.
@@ -1002,14 +993,13 @@ export class InjectedScript {
if (index !== -1) {
if (index > 1)
rootHitTargetDescription = this.previewNode(hitParents[index - 1]);
- hasPositionStickyOrFixed = isHitParentPositionStickyOrFixed.slice(0, index).some(x => x);
break;
}
element = parentElementOrShadowHost(element);
}
if (rootHitTargetDescription)
- return { hitTargetDescription: `${hitTargetDescription} from ${rootHitTargetDescription} subtree`, hasPositionStickyOrFixed };
- return { hitTargetDescription, hasPositionStickyOrFixed };
+ return { hitTargetDescription: `${hitTargetDescription} from ${rootHitTargetDescription} subtree` };
+ return { hitTargetDescription };
}
// Life of a pointer action, for example click.
@@ -1042,7 +1032,7 @@ export class InjectedScript {
// 2k. (injected) Event interceptor is removed.
// 2l. All navigations triggered between 2g-2k are awaited to be either committed or canceled.
// 2m. If failed, wait for increasing amount of time before the next retry.
- setupHitTargetInterceptor(node: Node, action: 'hover' | 'tap' | 'mouse' | 'drag', hitPoint: { x: number, y: number } | undefined, blockAllEvents: boolean): HitTargetInterceptionResult | 'error:notconnected' | string /* JSON.stringify(hitTargetDescription) */ {
+ setupHitTargetInterceptor(node: Node, action: 'hover' | 'tap' | 'mouse' | 'drag', hitPoint: { x: number, y: number } | undefined, blockAllEvents: boolean): HitTargetInterceptionResult | 'error:notconnected' | string /* hitTargetDescription */ {
const element = this.retarget(node, 'button-link');
if (!element || !element.isConnected)
return 'error:notconnected';
@@ -1052,7 +1042,7 @@ export class InjectedScript {
// intercepting the action.
const preliminaryResult = this.expectHitTarget(hitPoint, element);
if (preliminaryResult !== 'done')
- return JSON.stringify(preliminaryResult);
+ return preliminaryResult.hitTargetDescription;
}
// When dropping, the "element that is being dragged" often stays under the cursor,
@@ -1067,7 +1057,7 @@ export class InjectedScript {
'tap': this._tapHitTargetInterceptorEvents,
'mouse': this._mouseHitTargetInterceptorEvents,
}[action];
- let result: 'done' | HitTargetError | undefined;
+ let result: 'done' | { hitTargetDescription: string } | undefined;
const listener = (event: PointerEvent | MouseEvent | TouchEvent) => {
// Ignore events that we do not expect to intercept.
@@ -1349,6 +1339,16 @@ export class InjectedScript {
// expect(locator).not.toBeInViewport() passes when there is no element.
if (options.isNot && options.expression === 'to.be.in.viewport')
return { matches: false };
+ if (options.expression === 'to.have.title' && options?.expectedText?.[0]) {
+ const matcher = new ExpectedTextMatcher(options.expectedText[0]);
+ const received = this.document.title;
+ return { received, matches: matcher.matches(received) };
+ }
+ if (options.expression === 'to.have.url' && options?.expectedText?.[0]) {
+ const matcher = new ExpectedTextMatcher(options.expectedText[0]);
+ const received = this.document.location.href;
+ return { received, matches: matcher.matches(received) };
+ }
// When none of the above applies, expect does not match.
return { matches: options.isNot, missingReceived: true };
}
@@ -1498,10 +1498,6 @@ export class InjectedScript {
received = getElementAccessibleErrorMessage(element);
} else if (expression === 'to.have.role') {
received = getAriaRole(element) || '';
- } else if (expression === 'to.have.title') {
- received = this.document.title;
- } else if (expression === 'to.have.url') {
- received = this.document.location.href;
} else if (expression === 'to.have.value') {
element = this.retarget(element, 'follow-label')!;
if (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT')
diff --git a/packages/injected/src/roleUtils.ts b/packages/injected/src/roleUtils.ts
index b9bdd333a01c8..139b26bb6394b 100644
--- a/packages/injected/src/roleUtils.ts
+++ b/packages/injected/src/roleUtils.ts
@@ -16,7 +16,7 @@
import * as css from '@isomorphic/cssTokenizer';
-import { getGlobalOptions, closestCrossShadow, elementSafeTagName, enclosingShadowRootOrDocument, getElementComputedStyle, isElementStyleVisibilityVisible, isVisibleTextNode, parentElementOrShadowHost } from './domUtils';
+import { closestCrossShadow, elementSafeTagName, enclosingShadowRootOrDocument, getElementComputedStyle, isElementStyleVisibilityVisible, isVisibleTextNode, parentElementOrShadowHost } from './domUtils';
import type { AriaRole } from '@isomorphic/ariaSnapshot';
@@ -135,7 +135,7 @@ const kImplicitRoleByTagName: { [tagName: string]: (e: Element) => AriaRole | nu
// File inputs do not have a role by the spec: https://www.w3.org/TR/html-aam-1.0/#el-input-file.
// However, there are open issues about fixing it: https://github.com/w3c/aria/issues/1926.
// All browsers report it as a button, and it is rendered as a button, so we do "button".
- if (type === 'file' && !getGlobalOptions().inputFileRoleTextbox)
+ if (type === 'file')
return 'button';
return inputTypeToRole[type] || 'textbox';
},
@@ -707,7 +707,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
// There is no spec for this, but Chromium/WebKit do "Choose File" so we follow that.
// All browsers respect labels, aria-labelledby and aria-label.
// No browsers respect the title attribute, although w3c accname tests disagree. We follow browsers.
- if (!getGlobalOptions().inputFileRoleTextbox && tagName === 'INPUT' && (element as HTMLInputElement).type === 'file') {
+ if (tagName === 'INPUT' && (element as HTMLInputElement).type === 'file') {
options.visitedElements.add(element);
const labels = (element as HTMLInputElement).labels || [];
if (labels.length && !options.embeddedInLabelledBy)
diff --git a/packages/injected/src/selectorGenerator.ts b/packages/injected/src/selectorGenerator.ts
index 8410d7dc7d6c1..a7dad6644fb0b 100644
--- a/packages/injected/src/selectorGenerator.ts
+++ b/packages/injected/src/selectorGenerator.ts
@@ -578,6 +578,25 @@ function escapeNodeName(node: Node): string {
}
function escapeClassName(className: string): string {
- // We are escaping it for document.querySelectorAll, not for usage in CSS file.
- return className.replace(/[:\.]/g, char => '\\' + char);
+ // We are escaping class names for document.querySelectorAll by following CSS.escape() rules.
+ let result = '';
+ for (let i = 0; i < className.length; i++)
+ result += cssEscapeCharacter(className, i);
+ return result;
+}
+
+function cssEscapeCharacter(s: string, i: number): string {
+ // https://drafts.csswg.org/cssom/#serialize-an-identifier
+ const c = s.charCodeAt(i);
+ if (c === 0x0000)
+ return '\uFFFD';
+ if ((c >= 0x0001 && c <= 0x001f) ||
+ (c >= 0x0030 && c <= 0x0039 && (i === 0 || (i === 1 && s.charCodeAt(0) === 0x002d))))
+ return '\\' + c.toString(16) + ' ';
+ if (i === 0 && c === 0x002d && s.length === 1)
+ return '\\' + s.charAt(i);
+ if (c >= 0x0080 || c === 0x002d || c === 0x005f || (c >= 0x0030 && c <= 0x0039) ||
+ (c >= 0x0041 && c <= 0x005a) || (c >= 0x0061 && c <= 0x007a))
+ return s.charAt(i);
+ return '\\' + s.charAt(i);
}
diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts
index dbd022dcc3388..f788301765da1 100644
--- a/packages/playwright-client/types/types.d.ts
+++ b/packages/playwright-client/types/types.d.ts
@@ -8993,6 +8993,7 @@ export interface BrowserContext {
* - `'notifications'`
* - `'payment-handler'`
* - `'storage-access'`
+ * - `'local-fonts'`
* @param options
*/
grantPermissions(permissions: ReadonlyArray, options?: {
diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json
index e7a5eaf4c0c2e..3b75b5f38177c 100644
--- a/packages/playwright-core/browsers.json
+++ b/packages/playwright-core/browsers.json
@@ -3,43 +3,43 @@
"browsers": [
{
"name": "chromium",
- "revision": "1178",
+ "revision": "1180",
"installByDefault": true,
- "browserVersion": "138.0.7204.15"
+ "browserVersion": "138.0.7204.35"
},
{
"name": "chromium-headless-shell",
- "revision": "1178",
+ "revision": "1180",
"installByDefault": true,
- "browserVersion": "138.0.7204.15"
+ "browserVersion": "138.0.7204.35"
},
{
"name": "chromium-tip-of-tree",
- "revision": "1338",
+ "revision": "1342",
"installByDefault": false,
- "browserVersion": "139.0.7219.3"
+ "browserVersion": "139.0.7248.0"
},
{
"name": "chromium-tip-of-tree-headless-shell",
- "revision": "1338",
+ "revision": "1342",
"installByDefault": false,
- "browserVersion": "139.0.7219.3"
+ "browserVersion": "139.0.7248.0"
},
{
"name": "firefox",
- "revision": "1487",
+ "revision": "1488",
"installByDefault": true,
"browserVersion": "139.0"
},
{
"name": "firefox-beta",
- "revision": "1482",
+ "revision": "1484",
"installByDefault": false,
- "browserVersion": "138.0b10"
+ "browserVersion": "140.0b7"
},
{
"name": "webkit",
- "revision": "2183",
+ "revision": "2186",
"installByDefault": true,
"revisionOverrides": {
"debian11-x64": "2105",
diff --git a/packages/playwright-core/src/DEPS.list b/packages/playwright-core/src/DEPS.list
index d21eb103c4ad9..c4f50cb22e158 100644
--- a/packages/playwright-core/src/DEPS.list
+++ b/packages/playwright-core/src/DEPS.list
@@ -4,6 +4,8 @@ server/
server/utils
utils/isomorphic/
utilsBundle.ts
+protocol/validator.ts
+protocol/validatorPrimitives.ts
[androidServerImpl.ts]
remote/
diff --git a/packages/playwright-core/src/browserServerImpl.ts b/packages/playwright-core/src/browserServerImpl.ts
index 927bb6d68dedf..9a8b295202e40 100644
--- a/packages/playwright-core/src/browserServerImpl.ts
+++ b/packages/playwright-core/src/browserServerImpl.ts
@@ -14,15 +14,16 @@
* limitations under the License.
*/
-import { SocksProxy } from './server/utils/socksProxy';
import { PlaywrightServer } from './remote/playwrightServer';
import { helper } from './server/helper';
import { serverSideCallMetadata } from './server/instrumentation';
import { createPlaywright } from './server/playwright';
import { createGuid } from './server/utils/crypto';
+import { isUnderTest } from './server/utils/debug';
import { rewriteErrorMessage } from './utils/isomorphic/stackTrace';
import { DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT } from './utils/isomorphic/time';
import { ws } from './utilsBundle';
+import * as validatorPrimitives from './protocol/validatorPrimitives';
import type { BrowserServer, BrowserServerLauncher } from './client/browserType';
import type { LaunchServerOptions, Logger, Env } from './client/types';
@@ -31,33 +32,41 @@ import type { WebSocketEventEmitter } from './utilsBundle';
import type { Browser } from './server/browser';
export class BrowserServerLauncherImpl implements BrowserServerLauncher {
- private _browserName: 'chromium' | 'firefox' | 'webkit' | 'bidiFirefox' | 'bidiChromium';
+ private _browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiFirefox' | '_bidiChromium';
- constructor(browserName: 'chromium' | 'firefox' | 'webkit' | 'bidiFirefox' | 'bidiChromium') {
+ constructor(browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiFirefox' | '_bidiChromium') {
this._browserName = browserName;
}
async launchServer(options: LaunchServerOptions & { _sharedBrowser?: boolean, _userDataDir?: string } = {}): Promise {
const playwright = createPlaywright({ sdkLanguage: 'javascript', isServer: true });
- // TODO: enable socks proxy once ipv6 is supported.
- const socksProxy = false ? new SocksProxy() : undefined;
- playwright.options.socksProxyPort = await socksProxy?.listen(0);
-
// 1. Pre-launch the browser
const metadata = serverSideCallMetadata();
- const launchOptions = {
+ const validatorContext = {
+ tChannelImpl: (names: '*' | string[], arg: any, path: string) => {
+ throw new validatorPrimitives.ValidationError(`${path}: channels are not expected in launchServer`);
+ },
+ binary: 'buffer',
+ isUnderTest,
+ } satisfies validatorPrimitives.ValidatorContext;
+ let launchOptions = {
...options,
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
env: options.env ? envObjectToArray(options.env) : undefined,
timeout: options.timeout ?? DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT,
};
+
let browser: Browser;
try {
if (options._userDataDir !== undefined) {
+ const validator = validatorPrimitives.scheme['BrowserTypeLaunchPersistentContextParams'];
+ launchOptions = validator({ ...launchOptions, userDataDir: options._userDataDir }, '', validatorContext);
const context = await playwright[this._browserName].launchPersistentContext(metadata, options._userDataDir, launchOptions);
browser = context._browser;
} else {
+ const validator = validatorPrimitives.scheme['BrowserTypeLaunchParams'];
+ launchOptions = validator(launchOptions, '', validatorContext);
browser = await playwright[this._browserName].launch(metadata, launchOptions, toProtocolLogger(options.logger));
}
} catch (e) {
@@ -69,7 +78,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
const path = options.wsPath ? (options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`) : `/${createGuid()}`;
// 2. Start the server
- const server = new PlaywrightServer({ mode: options._sharedBrowser ? 'launchServerShared' : 'launchServer', path, maxConnections: Infinity, preLaunchedBrowser: browser, preLaunchedSocksProxy: socksProxy });
+ const server = new PlaywrightServer({ mode: options._sharedBrowser ? 'launchServerShared' : 'launchServer', path, maxConnections: Infinity, preLaunchedBrowser: browser });
const wsEndpoint = await server.listen(options.port, options.host);
// 3. Return the BrowserServer interface
@@ -82,7 +91,6 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
(browserServer as any)._disconnectForTest = () => server.close();
(browserServer as any)._userDataDirForTest = (browser as any)._userDataDirForTest;
browser.options.browserProcess.onclose = (exitCode, signal) => {
- socksProxy?.close().catch(() => {});
server.close();
browserServer.emit('close', exitCode, signal);
};
diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts
index a9116101b6cab..e41b0db69a325 100644
--- a/packages/playwright-core/src/client/browserType.ts
+++ b/packages/playwright-core/src/client/browserType.ts
@@ -119,8 +119,8 @@ export class BrowserType extends ChannelOwner imple
});
}
- connect(options: api.ConnectOptions & { wsEndpoint: string }): Promise;
- connect(wsEndpoint: string, options?: api.ConnectOptions): Promise;
+ connect(options: api.ConnectOptions & { wsEndpoint: string }): Promise;
+ connect(wsEndpoint: string, options?: api.ConnectOptions): Promise;
async connect(optionsOrWsEndpoint: string | (api.ConnectOptions & { wsEndpoint: string }), options?: api.ConnectOptions): Promise{
if (typeof optionsOrWsEndpoint === 'string')
return await this._connect({ ...options, wsEndpoint: optionsOrWsEndpoint });
diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts
index 32fa6b0997240..780d3680eed40 100644
--- a/packages/playwright-core/src/client/frame.ts
+++ b/packages/playwright-core/src/client/frame.ts
@@ -460,6 +460,15 @@ export class Frame extends ChannelOwner implements api.Fr
async title(): Promise {
return (await this._channel.title()).value;
}
+
+ async _expect(expression: string, options: Omit): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> {
+ const params: channels.FrameExpectParams = { expression, ...options, isNot: !!options.isNot };
+ params.expectedValue = serializeArgument(options.expectedValue);
+ const result = (await this._channel.expect(params));
+ if (result.received !== undefined)
+ result.received = parseResult(result.received);
+ return result;
+ }
}
export function verifyLoadState(name: string, waitUntil: LifecycleEvent): LifecycleEvent {
diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts
index 9e759c2f34220..ea780c2fbe34d 100644
--- a/packages/playwright-core/src/client/locator.ts
+++ b/packages/playwright-core/src/client/locator.ts
@@ -15,7 +15,6 @@
*/
import { ElementHandle } from './elementHandle';
-import { parseResult, serializeArgument } from './jsHandle';
import { asLocator } from '../utils/isomorphic/locatorGenerators';
import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../utils/isomorphic/locatorUtils';
import { escapeForTextSelector } from '../utils/isomorphic/stringUtils';
@@ -374,12 +373,10 @@ export class Locator implements api.Locator {
}
async _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> {
- const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot };
- params.expectedValue = serializeArgument(options.expectedValue);
- const result = (await this._frame._channel.expect(params));
- if (result.received !== undefined)
- result.received = parseResult(result.received);
- return result;
+ return this._frame._expect(expression, {
+ ...options,
+ selector: this._selector,
+ });
}
private _inspect() {
diff --git a/packages/playwright-core/src/client/playwright.ts b/packages/playwright-core/src/client/playwright.ts
index cc58a7b9e52b7..48c6349080f15 100644
--- a/packages/playwright-core/src/client/playwright.ts
+++ b/packages/playwright-core/src/client/playwright.ts
@@ -58,14 +58,13 @@ export class Playwright extends ChannelOwner {
this._android._playwright = this;
this._electron = Electron.from(initializer.electron);
this._electron._playwright = this;
- this._bidiChromium = BrowserType.from(initializer.bidiChromium);
+ this._bidiChromium = BrowserType.from(initializer._bidiChromium);
this._bidiChromium._playwright = this;
- this._bidiFirefox = BrowserType.from(initializer.bidiFirefox);
+ this._bidiFirefox = BrowserType.from(initializer._bidiFirefox);
this._bidiFirefox._playwright = this;
this.devices = this._connection.localUtils()?.devices ?? {};
this.selectors = new Selectors(this._connection._platform);
this.errors = { TimeoutError };
- (global as any)._playwrightInstance = this;
}
static from(channel: channels.PlaywrightChannel): Playwright {
diff --git a/packages/playwright-core/src/inProcessFactory.ts b/packages/playwright-core/src/inProcessFactory.ts
index 0bb1d89d4b7b6..0dac70918cb08 100644
--- a/packages/playwright-core/src/inProcessFactory.ts
+++ b/packages/playwright-core/src/inProcessFactory.ts
@@ -42,8 +42,8 @@ export function createInProcessPlaywright(): PlaywrightAPI {
playwrightAPI.firefox._serverLauncher = new BrowserServerLauncherImpl('firefox');
playwrightAPI.webkit._serverLauncher = new BrowserServerLauncherImpl('webkit');
playwrightAPI._android._serverLauncher = new AndroidServerLauncherImpl();
- playwrightAPI._bidiChromium._serverLauncher = new BrowserServerLauncherImpl('bidiChromium');
- playwrightAPI._bidiFirefox._serverLauncher = new BrowserServerLauncherImpl('bidiFirefox');
+ playwrightAPI._bidiChromium._serverLauncher = new BrowserServerLauncherImpl('_bidiChromium');
+ playwrightAPI._bidiFirefox._serverLauncher = new BrowserServerLauncherImpl('_bidiFirefox');
// Switch to async dispatch after we got Playwright object.
dispatcherConnection.onmessage = message => setImmediate(() => clientConnection.dispatch(message));
diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts
index 71903879cec9f..2475526a43eb5 100644
--- a/packages/playwright-core/src/protocol/validator.ts
+++ b/packages/playwright-core/src/protocol/validator.ts
@@ -376,8 +376,8 @@ scheme.PlaywrightInitializer = tObject({
chromium: tChannel(['BrowserType']),
firefox: tChannel(['BrowserType']),
webkit: tChannel(['BrowserType']),
- bidiChromium: tChannel(['BrowserType']),
- bidiFirefox: tChannel(['BrowserType']),
+ _bidiChromium: tChannel(['BrowserType']),
+ _bidiFirefox: tChannel(['BrowserType']),
android: tChannel(['Android']),
electron: tChannel(['Electron']),
utils: tOptional(tChannel(['LocalUtils'])),
@@ -1882,7 +1882,7 @@ scheme.FrameWaitForSelectorResult = tObject({
element: tOptional(tChannel(['ElementHandle'])),
});
scheme.FrameExpectParams = tObject({
- selector: tString,
+ selector: tOptional(tString),
expression: tString,
expressionArg: tOptional(tAny),
expectedText: tOptional(tArray(tType('ExpectedTextValue'))),
diff --git a/packages/playwright-core/src/remote/playwrightConnection.ts b/packages/playwright-core/src/remote/playwrightConnection.ts
index 2f9abfbf01154..8a529f4b7a48a 100644
--- a/packages/playwright-core/src/remote/playwrightConnection.ts
+++ b/packages/playwright-core/src/remote/playwrightConnection.ts
@@ -14,64 +14,39 @@
* limitations under the License.
*/
-import { SocksProxy } from '../server/utils/socksProxy';
-import { DispatcherConnection, PlaywrightDispatcher, RootDispatcher, createPlaywright } from '../server';
+import { DispatcherConnection, PlaywrightDispatcher, RootDispatcher } from '../server';
import { AndroidDevice } from '../server/android/android';
import { Browser } from '../server/browser';
import { DebugControllerDispatcher } from '../server/dispatchers/debugControllerDispatcher';
-import { serverSideCallMetadata } from '../server/instrumentation';
-import { assert } from '../utils/isomorphic/assert';
-import { isUnderTest } from '../server/utils/debug';
import { startProfiling, stopProfiling } from '../server/utils/profiler';
-import { monotonicTime } from '../utils';
+import { monotonicTime, Semaphore } from '../utils';
import { debugLogger } from '../server/utils/debugLogger';
+import { PlaywrightDispatcherOptions } from '../server/dispatchers/playwrightDispatcher';
import type { DispatcherScope, Playwright } from '../server';
-import type { LaunchOptions } from '../server/types';
import type { WebSocket } from '../utilsBundle';
-import type * as channels from '@protocol/channels';
-export type ClientType = 'controller' | 'launch-browser' | 'reuse-browser' | 'pre-launched-browser-or-android';
-
-type Options = {
- allowFSPaths: boolean,
- socksProxyPattern: string | undefined,
- browserName: string | null,
- launchOptions: LaunchOptions,
- sharedBrowser?: boolean,
-};
-
-type PreLaunched = {
- playwright?: Playwright | undefined;
- browser?: Browser | undefined;
- androidDevice?: AndroidDevice | undefined;
- socksProxy?: SocksProxy | undefined;
-};
+export interface PlaywrightInitializeResult extends PlaywrightDispatcherOptions {
+ dispose?(): Promise;
+}
export class PlaywrightConnection {
private _ws: WebSocket;
- private _onClose: () => void;
+ private _semaphore: Semaphore;
private _dispatcherConnection: DispatcherConnection;
private _cleanups: (() => Promise)[] = [];
private _id: string;
private _disconnected = false;
- private _preLaunched: PreLaunched;
- private _options: Options;
private _root: DispatcherScope;
private _profileName: string;
- constructor(lock: Promise, clientType: ClientType, ws: WebSocket, options: Options, preLaunched: PreLaunched, id: string, onClose: () => void) {
+ constructor(semaphore: Semaphore, ws: WebSocket, controller: boolean, playwright: Playwright, initialize: () => Promise, id: string) {
this._ws = ws;
- this._preLaunched = preLaunched;
- this._options = options;
- options.launchOptions = filterLaunchOptions(options.launchOptions, options.allowFSPaths);
- if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser-or-android')
- assert(preLaunched.playwright);
- if (clientType === 'pre-launched-browser-or-android')
- assert(preLaunched.browser || preLaunched.androidDevice);
- this._onClose = onClose;
+ this._semaphore = semaphore;
this._id = id;
- this._profileName = `${new Date().toISOString()}-${clientType}`;
+ this._profileName = new Date().toISOString();
+
+ const lock = this._semaphore.acquire();
this._dispatcherConnection = new DispatcherConnection();
this._dispatcherConnection.onmessage = async message => {
@@ -99,158 +74,39 @@ export class PlaywrightConnection {
ws.on('close', () => this._onDisconnect());
ws.on('error', (error: Error) => this._onDisconnect(error));
- if (clientType === 'controller') {
- this._root = this._initDebugControllerMode();
+ if (controller) {
+ debugLogger.log('server', `[${this._id}] engaged reuse controller mode`);
+ this._root = new DebugControllerDispatcher(this._dispatcherConnection, playwright.debugController);
return;
}
- this._root = new RootDispatcher(this._dispatcherConnection, async (scope, options) => {
+ this._root = new RootDispatcher(this._dispatcherConnection, async (scope, params) => {
await startProfiling();
- if (clientType === 'reuse-browser')
- return await this._initReuseBrowsersMode(scope);
- if (clientType === 'pre-launched-browser-or-android')
- return this._preLaunched.browser ? await this._initPreLaunchedBrowserMode(scope) : await this._initPreLaunchedAndroidMode(scope);
- if (clientType === 'launch-browser')
- return await this._initLaunchBrowserMode(scope, options);
- throw new Error('Unsupported client type: ' + clientType);
- });
- }
-
- private async _initLaunchBrowserMode(scope: RootDispatcher, options: channels.RootInitializeParams) {
- debugLogger.log('server', `[${this._id}] engaged launch mode for "${this._options.browserName}"`);
- const playwright = createPlaywright({ sdkLanguage: options.sdkLanguage, isServer: true });
-
- const ownedSocksProxy = await this._createOwnedSocksProxy(playwright);
- let browserName = this._options.browserName;
- if ('bidi' === browserName) {
- if (this._options.launchOptions?.channel?.toLocaleLowerCase().includes('firefox'))
- browserName = 'bidiFirefox';
- else
- browserName = 'bidiChromium';
- }
- const browser = await playwright[browserName as 'chromium'].launch(serverSideCallMetadata(), this._options.launchOptions);
-
- this._cleanups.push(async () => {
- for (const browser of playwright.allBrowsers())
- await browser.close({ reason: 'Connection terminated' });
- });
- browser.on(Browser.Events.Disconnected, () => {
- // Underlying browser did close for some reason - force disconnect the client.
- this.close({ code: 1001, reason: 'Browser closed' });
- });
- return new PlaywrightDispatcher(scope, playwright, { socksProxy: ownedSocksProxy, preLaunchedBrowser: browser });
- }
-
- private async _initPreLaunchedBrowserMode(scope: RootDispatcher) {
- debugLogger.log('server', `[${this._id}] engaged pre-launched (browser) mode`);
- const playwright = this._preLaunched.playwright!;
-
- // Note: connected client owns the socks proxy and configures the pattern.
- this._preLaunched.socksProxy?.setPattern(this._options.socksProxyPattern);
-
- const browser = this._preLaunched.browser!;
- browser.on(Browser.Events.Disconnected, () => {
- // Underlying browser did close for some reason - force disconnect the client.
- this.close({ code: 1001, reason: 'Browser closed' });
- });
-
- const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, {
- socksProxy: this._preLaunched.socksProxy,
- preLaunchedBrowser: browser,
- sharedBrowser: this._options.sharedBrowser,
- });
- // In pre-launched mode, keep only the pre-launched browser.
- for (const b of playwright.allBrowsers()) {
- if (b !== browser)
- await b.close({ reason: 'Connection terminated' });
- }
- this._cleanups.push(() => playwrightDispatcher.cleanup());
- return playwrightDispatcher;
- }
-
- private async _initPreLaunchedAndroidMode(scope: RootDispatcher) {
- debugLogger.log('server', `[${this._id}] engaged pre-launched (Android) mode`);
- const playwright = this._preLaunched.playwright!;
- const androidDevice = this._preLaunched.androidDevice!;
- androidDevice.on(AndroidDevice.Events.Close, () => {
- // Underlying browser did close for some reason - force disconnect the client.
- this.close({ code: 1001, reason: 'Android device disconnected' });
- });
- const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, { preLaunchedAndroidDevice: androidDevice });
- this._cleanups.push(() => playwrightDispatcher.cleanup());
- return playwrightDispatcher;
- }
-
- private _initDebugControllerMode(): DebugControllerDispatcher {
- debugLogger.log('server', `[${this._id}] engaged reuse controller mode`);
- const playwright = this._preLaunched.playwright!;
- // Always create new instance based on the reused Playwright instance.
- return new DebugControllerDispatcher(this._dispatcherConnection, playwright.debugController);
- }
-
- private async _initReuseBrowsersMode(scope: RootDispatcher) {
- // Note: reuse browser mode does not support socks proxy, because
- // clients come and go, while the browser stays the same.
-
- debugLogger.log('server', `[${this._id}] engaged reuse browsers mode for ${this._options.browserName}`);
- const playwright = this._preLaunched.playwright!;
-
- const requestedOptions = launchOptionsHash(this._options.launchOptions);
- let browser = playwright.allBrowsers().find(b => {
- if (b.options.name !== this._options.browserName)
- return false;
- const existingOptions = launchOptionsHash(b.options.originalLaunchOptions);
- return existingOptions === requestedOptions;
- });
-
- // Close remaining browsers of this type+channel. Keep different browser types for the speed.
- for (const b of playwright.allBrowsers()) {
- if (b === browser)
- continue;
- if (b.options.name === this._options.browserName && b.options.channel === this._options.launchOptions.channel)
- await b.close({ reason: 'Connection terminated' });
- }
-
- if (!browser) {
- browser = await playwright[(this._options.browserName || 'chromium') as 'chromium'].launch(serverSideCallMetadata(), {
- ...this._options.launchOptions,
- headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS,
- });
- browser.on(Browser.Events.Disconnected, () => {
- // Underlying browser did close for some reason - force disconnect the client.
- this.close({ code: 1001, reason: 'Browser closed' });
- });
- }
-
- this._cleanups.push(async () => {
- // Don't close the pages so that user could debug them,
- // but close all the empty browsers and contexts to clean up.
- for (const browser of playwright.allBrowsers()) {
- for (const context of browser.contexts()) {
- if (!context.pages().length)
- await context.close({ reason: 'Connection terminated' });
- else
- await context.stopPendingOperations('Connection closed');
- }
- if (!browser.contexts())
- await browser.close({ reason: 'Connection terminated' });
+ const options = await initialize();
+ if (options.preLaunchedBrowser) {
+ const browser = options.preLaunchedBrowser;
+ browser.options.sdkLanguage = params.sdkLanguage;
+ browser.on(Browser.Events.Disconnected, () => {
+ // Underlying browser did close for some reason - force disconnect the client.
+ this.close({ code: 1001, reason: 'Browser closed' });
+ });
}
- });
+ if (options.preLaunchedAndroidDevice) {
+ const androidDevice = options.preLaunchedAndroidDevice;
+ androidDevice.on(AndroidDevice.Events.Close, () => {
+ // Underlying android device did close for some reason - force disconnect the client.
+ this.close({ code: 1001, reason: 'Android device disconnected' });
+ });
+ }
+ if (options.dispose)
+ this._cleanups.push(options.dispose);
- const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, { preLaunchedBrowser: browser });
- return playwrightDispatcher;
- }
+ const dispatcher = new PlaywrightDispatcher(scope, playwright, options);
+ this._cleanups.push(() => dispatcher.cleanup());
- private async _createOwnedSocksProxy(playwright: Playwright): Promise {
- if (!this._options.socksProxyPattern)
- return;
- const socksProxy = new SocksProxy();
- socksProxy.setPattern(this._options.socksProxyPattern);
- playwright.options.socksProxyPort = await socksProxy.listen(0);
- debugLogger.log('server', `[${this._id}] started socks proxy on port ${playwright.options.socksProxyPort}`);
- this._cleanups.push(() => socksProxy.close());
- return socksProxy;
+ return dispatcher;
+ });
}
private async _onDisconnect(error?: Error) {
@@ -261,7 +117,7 @@ export class PlaywrightConnection {
for (const cleanup of this._cleanups)
await cleanup().catch(() => {});
await stopProfiling(this._profileName);
- this._onClose();
+ this._semaphore.release();
debugLogger.log('server', `[${this._id}] finished cleanup`);
}
@@ -286,47 +142,3 @@ export class PlaywrightConnection {
}
}
}
-
-function launchOptionsHash(options: LaunchOptions) {
- const copy = { ...options };
- for (const k of Object.keys(copy)) {
- const key = k as keyof LaunchOptions;
- if (copy[key] === defaultLaunchOptions[key])
- delete copy[key];
- }
- for (const key of optionsThatAllowBrowserReuse)
- delete copy[key];
- return JSON.stringify(copy);
-}
-
-function filterLaunchOptions(options: LaunchOptions, allowFSPaths: boolean): LaunchOptions {
- return {
- channel: options.channel,
- args: options.args,
- ignoreAllDefaultArgs: options.ignoreAllDefaultArgs,
- ignoreDefaultArgs: options.ignoreDefaultArgs,
- timeout: options.timeout,
- headless: options.headless,
- proxy: options.proxy,
- chromiumSandbox: options.chromiumSandbox,
- firefoxUserPrefs: options.firefoxUserPrefs,
- slowMo: options.slowMo,
- executablePath: (isUnderTest() || allowFSPaths) ? options.executablePath : undefined,
- downloadsPath: allowFSPaths ? options.downloadsPath : undefined,
- };
-}
-
-const defaultLaunchOptions: Partial = {
- ignoreAllDefaultArgs: false,
- handleSIGINT: false,
- handleSIGTERM: false,
- handleSIGHUP: false,
- headless: true,
- devtools: false,
-};
-
-const optionsThatAllowBrowserReuse: (keyof LaunchOptions)[] = [
- 'headless',
- 'timeout',
- 'tracesDir',
-];
diff --git a/packages/playwright-core/src/remote/playwrightServer.ts b/packages/playwright-core/src/remote/playwrightServer.ts
index 9a564c4252c83..9839cff9b3fcc 100644
--- a/packages/playwright-core/src/remote/playwrightServer.ts
+++ b/packages/playwright-core/src/remote/playwrightServer.ts
@@ -14,19 +14,19 @@
* limitations under the License.
*/
-import { PlaywrightConnection } from './playwrightConnection';
+import { PlaywrightConnection, PlaywrightInitializeResult } from './playwrightConnection';
import { createPlaywright } from '../server/playwright';
-import { debugLogger } from '../server/utils/debugLogger';
import { Semaphore } from '../utils/isomorphic/semaphore';
import { DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT } from '../utils/isomorphic/time';
import { WSServer } from '../server/utils/wsServer';
import { wrapInASCIIBox } from '../server/utils/ascii';
import { getPlaywrightVersion } from '../server/utils/userAgent';
+import { debugLogger, isUnderTest } from '../utils';
+import { serverSideCallMetadata } from '../server';
+import { SocksProxy } from '../server/utils/socksProxy';
+import { Browser } from '../server/browser';
-import type { ClientType } from './playwrightConnection';
-import type { SocksProxy } from '../server/utils/socksProxy';
import type { AndroidDevice } from '../server/android/android';
-import type { Browser } from '../server/browser';
import type { Playwright } from '../server/playwright';
import type { LaunchOptions } from '../server/types';
@@ -41,22 +41,38 @@ type ServerOptions = {
};
export class PlaywrightServer {
- private _preLaunchedPlaywright: Playwright | undefined;
+ private _playwright: Playwright;
private _options: ServerOptions;
private _wsServer: WSServer;
+ private _dontReuseBrowsers = new Set();
+
constructor(options: ServerOptions) {
this._options = options;
- if (options.preLaunchedBrowser)
- this._preLaunchedPlaywright = options.preLaunchedBrowser.attribution.playwright;
+ if (options.preLaunchedBrowser) {
+ this._playwright = options.preLaunchedBrowser.attribution.playwright;
+ this._dontReuse(options.preLaunchedBrowser);
+ }
if (options.preLaunchedAndroidDevice)
- this._preLaunchedPlaywright = options.preLaunchedAndroidDevice._android.attribution.playwright;
+ this._playwright = options.preLaunchedAndroidDevice._android.attribution.playwright;
+ this._playwright ??= createPlaywright({ sdkLanguage: 'javascript', isServer: true });
const browserSemaphore = new Semaphore(this._options.maxConnections);
const controllerSemaphore = new Semaphore(1);
const reuseBrowserSemaphore = new Semaphore(1);
this._wsServer = new WSServer({
+ onRequest: (request, response) => {
+ if (request.method === 'GET' && request.url === '/json') {
+ response.setHeader('Content-Type', 'application/json');
+ response.end(JSON.stringify({
+ wsEndpointPath: this._options.path,
+ }));
+ return;
+ }
+ response.end('Running');
+ },
+
onUpgrade: (request, socket) => {
const uaError = userAgentVersionMatchesErrorMessage(request.headers['user-agent'] || '');
if (uaError)
@@ -83,51 +99,204 @@ export class PlaywrightServer {
} catch (e) {
}
- // Instantiate playwright for the extension modes.
const isExtension = this._options.mode === 'extension';
+ const allowFSPaths = isExtension;
+ launchOptions = filterLaunchOptions(launchOptions, allowFSPaths);
+
+ if (process.env.PW_BROWSER_SERVER && url.searchParams.has('connect')) {
+ const filter = url.searchParams.get('connect');
+ if (filter !== 'first')
+ throw new Error(`Unknown connect filter: ${filter}`);
+ return new PlaywrightConnection(
+ browserSemaphore,
+ ws,
+ false,
+ this._playwright,
+ () => this._initConnectMode(id, filter, browserName, launchOptions),
+ id,
+ );
+ }
+
if (isExtension) {
- if (!this._preLaunchedPlaywright)
- this._preLaunchedPlaywright = createPlaywright({ sdkLanguage: 'javascript', isServer: true });
+ if (url.searchParams.has('debug-controller')) {
+ return new PlaywrightConnection(
+ controllerSemaphore,
+ ws,
+ true,
+ this._playwright,
+ async () => { throw new Error('shouldnt be used'); },
+ id,
+ );
+ }
+ return new PlaywrightConnection(
+ reuseBrowserSemaphore,
+ ws,
+ false,
+ this._playwright,
+ () => this._initReuseBrowsersMode(browserName, launchOptions, id),
+ id,
+ );
}
- let clientType: ClientType = 'launch-browser';
- let semaphore: Semaphore = browserSemaphore;
- if (isExtension && url.searchParams.has('debug-controller')) {
- clientType = 'controller';
- semaphore = controllerSemaphore;
- } else if (isExtension) {
- clientType = 'reuse-browser';
- semaphore = reuseBrowserSemaphore;
- } else if (this._options.mode === 'launchServer' || this._options.mode === 'launchServerShared') {
- clientType = 'pre-launched-browser-or-android';
- semaphore = browserSemaphore;
+ if (this._options.mode === 'launchServer' || this._options.mode === 'launchServerShared') {
+ if (this._options.preLaunchedBrowser) {
+ return new PlaywrightConnection(
+ browserSemaphore,
+ ws,
+ false,
+ this._playwright,
+ () => this._initPreLaunchedBrowserMode(id),
+ id,
+ );
+ }
+
+ return new PlaywrightConnection(
+ browserSemaphore,
+ ws,
+ false,
+ this._playwright,
+ () => this._initPreLaunchedAndroidMode(id),
+ id,
+ );
}
return new PlaywrightConnection(
- semaphore.acquire(),
- clientType, ws,
- {
- socksProxyPattern: proxyValue,
- browserName,
- launchOptions,
- allowFSPaths: this._options.mode === 'extension',
- sharedBrowser: this._options.mode === 'launchServerShared',
- },
- {
- playwright: this._preLaunchedPlaywright,
- browser: this._options.preLaunchedBrowser,
- androidDevice: this._options.preLaunchedAndroidDevice,
- socksProxy: this._options.preLaunchedSocksProxy,
- },
- id, () => semaphore.release());
+ browserSemaphore,
+ ws,
+ false,
+ this._playwright,
+ () => this._initLaunchBrowserMode(browserName, proxyValue, launchOptions, id),
+ id,
+ );
},
+ });
+ }
+
+ private async _initReuseBrowsersMode(browserName: string | null, launchOptions: LaunchOptions, id: string): Promise {
+ // Note: reuse browser mode does not support socks proxy, because
+ // clients come and go, while the browser stays the same.
- onClose: async () => {
- debugLogger.log('server', 'closing browsers');
- if (this._preLaunchedPlaywright)
- await Promise.all(this._preLaunchedPlaywright.allBrowsers().map(browser => browser.close({ reason: 'Playwright Server stopped' })));
- debugLogger.log('server', 'closed browsers');
+ debugLogger.log('server', `[${id}] engaged reuse browsers mode for ${browserName}`);
+
+ const requestedOptions = launchOptionsHash(launchOptions);
+ let browser = this._playwright.allBrowsers().find(b => {
+ if (b.options.name !== browserName)
+ return false;
+ if (this._dontReuseBrowsers.has(b))
+ return false;
+ const existingOptions = launchOptionsHash(b.options.originalLaunchOptions);
+ return existingOptions === requestedOptions;
+ });
+
+ // Close remaining browsers of this type+channel. Keep different browser types for the speed.
+ for (const b of this._playwright.allBrowsers()) {
+ if (b === browser)
+ continue;
+ if (this._dontReuseBrowsers.has(b))
+ continue;
+ if (b.options.name === browserName && b.options.channel === launchOptions.channel)
+ await b.close({ reason: 'Connection terminated' });
+ }
+
+ if (!browser) {
+ browser = await this._playwright[(browserName || 'chromium') as 'chromium'].launch(serverSideCallMetadata(), {
+ ...launchOptions,
+ headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS,
+ });
+ }
+
+ return {
+ preLaunchedBrowser: browser,
+ denyLaunch: true,
+ dispose: async () => {
+ // Don't close the pages so that user could debug them,
+ // but close all the empty contexts to clean up.
+ // keep around browser so it can be reused by the next connection.
+ for (const context of browser.contexts()) {
+ if (!context.pages().length)
+ await context.close({ reason: 'Connection terminated' });
+ else
+ await context.stopPendingOperations('Connection closed');
+ }
}
+ };
+ }
+
+ private async _initConnectMode(id: string, filter: 'first', browserName: string | null, launchOptions: LaunchOptions): Promise {
+ browserName ??= 'chromium';
+
+ debugLogger.log('server', `[${id}] engaged connect mode`);
+
+ let browser = this._playwright.allBrowsers().find(b => b.options.name === browserName);
+ if (!browser) {
+ browser = await this._playwright[browserName as 'chromium'].launch(serverSideCallMetadata(), launchOptions);
+ this._dontReuse(browser);
+ }
+
+ return {
+ preLaunchedBrowser: browser,
+ denyLaunch: true,
+ sharedBrowser: true,
+ };
+ }
+
+ private async _initPreLaunchedBrowserMode(id: string): Promise {
+ debugLogger.log('server', `[${id}] engaged pre-launched (browser) mode`);
+
+ const browser = this._options.preLaunchedBrowser!;
+
+ // In pre-launched mode, keep only the pre-launched browser.
+ for (const b of this._playwright.allBrowsers()) {
+ if (b !== browser)
+ await b.close({ reason: 'Connection terminated' });
+ }
+
+ return {
+ preLaunchedBrowser: browser,
+ socksProxy: this._options.preLaunchedSocksProxy,
+ sharedBrowser: this._options.mode === 'launchServerShared',
+ denyLaunch: true,
+ };
+ }
+
+ private async _initPreLaunchedAndroidMode(id: string): Promise {
+ debugLogger.log('server', `[${id}] engaged pre-launched (Android) mode`);
+ const androidDevice = this._options.preLaunchedAndroidDevice!;
+ return {
+ preLaunchedAndroidDevice: androidDevice,
+ denyLaunch: true,
+ };
+ }
+
+ private async _initLaunchBrowserMode(browserName: string | null, proxyValue: string | undefined, launchOptions: LaunchOptions, id: string): Promise {
+ debugLogger.log('server', `[${id}] engaged launch mode for "${browserName}"`);
+ let socksProxy: SocksProxy | undefined;
+ if (proxyValue) {
+ socksProxy = new SocksProxy();
+ socksProxy.setPattern(proxyValue);
+ launchOptions.socksProxyPort = await socksProxy.listen(0);
+ debugLogger.log('server', `[${id}] started socks proxy on port ${launchOptions.socksProxyPort}`);
+ } else {
+ launchOptions.socksProxyPort = undefined;
+ }
+ const browser = await this._playwright[browserName as 'chromium'].launch(serverSideCallMetadata(), launchOptions);
+ this._dontReuseBrowsers.add(browser);
+ return {
+ preLaunchedBrowser: browser,
+ socksProxy,
+ sharedBrowser: true,
+ denyLaunch: true,
+ dispose: async () => {
+ await browser.close({ reason: 'Connection terminated' });
+ socksProxy?.close();
+ },
+ };
+ }
+
+ private _dontReuse(browser: Browser) {
+ this._dontReuseBrowsers.add(browser);
+ browser.on(Browser.Events.Disconnected, () => {
+ this._dontReuseBrowsers.delete(browser);
});
}
@@ -164,3 +333,47 @@ function userAgentVersionMatchesErrorMessage(userAgent: string) {
].join('\n'), 1);
}
}
+
+function launchOptionsHash(options: LaunchOptions) {
+ const copy = { ...options };
+ for (const k of Object.keys(copy)) {
+ const key = k as keyof LaunchOptions;
+ if (copy[key] === defaultLaunchOptions[key])
+ delete copy[key];
+ }
+ for (const key of optionsThatAllowBrowserReuse)
+ delete copy[key];
+ return JSON.stringify(copy);
+}
+
+function filterLaunchOptions(options: LaunchOptions, allowFSPaths: boolean): LaunchOptions {
+ return {
+ channel: options.channel,
+ args: options.args,
+ ignoreAllDefaultArgs: options.ignoreAllDefaultArgs,
+ ignoreDefaultArgs: options.ignoreDefaultArgs,
+ timeout: options.timeout,
+ headless: options.headless,
+ proxy: options.proxy,
+ chromiumSandbox: options.chromiumSandbox,
+ firefoxUserPrefs: options.firefoxUserPrefs,
+ slowMo: options.slowMo,
+ executablePath: (isUnderTest() || allowFSPaths) ? options.executablePath : undefined,
+ downloadsPath: allowFSPaths ? options.downloadsPath : undefined,
+ };
+}
+
+const defaultLaunchOptions: Partial = {
+ ignoreAllDefaultArgs: false,
+ handleSIGINT: false,
+ handleSIGTERM: false,
+ handleSIGHUP: false,
+ headless: true,
+ devtools: false,
+};
+
+const optionsThatAllowBrowserReuse: (keyof LaunchOptions)[] = [
+ 'headless',
+ 'timeout',
+ 'tracesDir',
+];
diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts
index 70a95b720a717..69452e4e3b442 100644
--- a/packages/playwright-core/src/server/android/android.ts
+++ b/packages/playwright-core/src/server/android/android.ts
@@ -32,9 +32,9 @@ import { chromiumSwitches } from '../chromium/chromiumSwitches';
import { CRBrowser } from '../chromium/crBrowser';
import { removeFolders } from '../utils/fileUtils';
import { helper } from '../helper';
-import { SdkObject, serverSideCallMetadata } from '../instrumentation';
+import { CallMetadata, SdkObject } from '../instrumentation';
import { gracefullyCloseSet } from '../utils/processLauncher';
-import { ProgressController } from '../progress';
+import { Progress, ProgressController } from '../progress';
import { registry } from '../registry';
import type { BrowserOptions, BrowserProcess } from '../browser';
@@ -59,7 +59,6 @@ export interface DeviceBackend {
}
export interface SocketBackend extends EventEmitter {
- guid: string;
write(data: Buffer): Promise;
close(): void;
}
@@ -123,6 +122,7 @@ export class AndroidDevice extends SdkObject {
this.model = model;
this.serial = backend.serial;
this._options = options;
+ this.logName = 'browser';
}
static async create(android: Android, backend: DeviceBackend, options: channels.AndroidDevicesOptions): Promise {
@@ -259,18 +259,21 @@ export class AndroidDevice extends SdkObject {
this.emit(AndroidDevice.Events.Close);
}
- async launchBrowser(pkg: string = 'com.android.chrome', options: channels.AndroidDeviceLaunchBrowserParams): Promise {
- debug('pw:android')('Force-stopping', pkg);
- await this._backend.runCommand(`shell:am force-stop ${pkg}`);
- const socketName = isUnderTest() ? 'webview_devtools_remote_playwright_test' : ('playwright_' + createGuid() + '_devtools_remote');
- const commandLine = this._defaultArgs(options, socketName).join(' ');
- debug('pw:android')('Starting', pkg, commandLine);
- // encode commandLine to base64 to avoid issues (bash encoding) with special characters
- await this._backend.runCommand(`shell:echo "${Buffer.from(commandLine).toString('base64')}" | base64 -d > /data/local/tmp/chrome-command-line`);
- await this._backend.runCommand(`shell:am start -a android.intent.action.VIEW -d about:blank ${pkg}`);
- const browserContext = await this._connectToBrowser(socketName, options);
- await this._backend.runCommand(`shell:rm /data/local/tmp/chrome-command-line`);
- return browserContext;
+ async launchBrowser(metadata: CallMetadata, pkg: string = 'com.android.chrome', options: channels.AndroidDeviceLaunchBrowserParams): Promise {
+ const controller = new ProgressController(metadata, this);
+ return controller.run(async progress => {
+ debug('pw:android')('Force-stopping', pkg);
+ await this._backend.runCommand(`shell:am force-stop ${pkg}`);
+ const socketName = isUnderTest() ? 'webview_devtools_remote_playwright_test' : ('playwright_' + createGuid() + '_devtools_remote');
+ const commandLine = this._defaultArgs(options, socketName).join(' ');
+ debug('pw:android')('Starting', pkg, commandLine);
+ // encode commandLine to base64 to avoid issues (bash encoding) with special characters
+ await progress.race(this._backend.runCommand(`shell:echo "${Buffer.from(commandLine).toString('base64')}" | base64 -d > /data/local/tmp/chrome-command-line`));
+ await progress.race(this._backend.runCommand(`shell:am start -a android.intent.action.VIEW -d about:blank ${pkg}`));
+ const browserContext = await this._connectToBrowser(progress, socketName, options);
+ await progress.race(this._backend.runCommand(`shell:rm /data/local/tmp/chrome-command-line`));
+ return browserContext;
+ });
}
private _defaultArgs(options: channels.AndroidDeviceLaunchBrowserParams, socketName: string): string[] {
@@ -302,25 +305,30 @@ export class AndroidDevice extends SdkObject {
return chromeArguments;
}
- async connectToWebView(socketName: string): Promise {
- const webView = this._webViews.get(socketName);
- if (!webView)
- throw new Error('WebView has been closed');
- return await this._connectToBrowser(socketName);
+ async connectToWebView(metadata: CallMetadata, socketName: string): Promise {
+ const controller = new ProgressController(metadata, this);
+ return controller.run(async progress => {
+ const webView = this._webViews.get(socketName);
+ if (!webView)
+ throw new Error('WebView has been closed');
+ return await this._connectToBrowser(progress, socketName);
+ });
}
- private async _connectToBrowser(socketName: string, options: types.BrowserContextOptions = {}): Promise {
- const socket = await this._waitForLocalAbstract(socketName);
+ private async _connectToBrowser(progress: Progress, socketName: string, options: types.BrowserContextOptions = {}): Promise {
+ const socket = await progress.race(this._waitForLocalAbstract(socketName));
const androidBrowser = new AndroidBrowser(this, socket);
- await androidBrowser._init();
+ progress.cleanupWhenAborted(() => androidBrowser.close());
+ await progress.race(androidBrowser._init());
this._browserConnections.add(androidBrowser);
- const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER);
+ const artifactsDir = await progress.race(fs.promises.mkdtemp(ARTIFACTS_FOLDER));
const cleanupArtifactsDir = async () => {
const errors = (await removeFolders([artifactsDir])).filter(Boolean);
for (let i = 0; i < (errors || []).length; ++i)
debug('pw:android')(`exception while removing ${artifactsDir}: ${errors[i]}`);
};
+ progress.cleanupWhenAborted(cleanupArtifactsDir);
gracefullyCloseSet.add(cleanupArtifactsDir);
socket.on('close', async () => {
gracefullyCloseSet.delete(cleanupArtifactsDir);
@@ -342,12 +350,9 @@ export class AndroidDevice extends SdkObject {
};
validateBrowserContextOptions(options, browserOptions);
- const browser = await CRBrowser.connect(this.attribution.playwright, androidBrowser, browserOptions);
- const controller = new ProgressController(serverSideCallMetadata(), this);
+ const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, androidBrowser, browserOptions));
const defaultContext = browser._defaultContext!;
- await controller.run(async progress => {
- await defaultContext._loadDefaultContextAsIs(progress);
- });
+ await defaultContext._loadDefaultContextAsIs(progress);
return defaultContext;
}
diff --git a/packages/playwright-core/src/server/android/backendAdb.ts b/packages/playwright-core/src/server/android/backendAdb.ts
index 88b7c8896abe8..b25b5a24f36fe 100644
--- a/packages/playwright-core/src/server/android/backendAdb.ts
+++ b/packages/playwright-core/src/server/android/backendAdb.ts
@@ -18,7 +18,6 @@ import { EventEmitter } from 'events';
import net from 'net';
import { assert } from '../../utils/isomorphic/assert';
-import { createGuid } from '../utils/crypto';
import { debug } from '../../utilsBundle';
import type { Backend, DeviceBackend, SocketBackend } from './android';
@@ -117,7 +116,6 @@ function encodeMessage(message: string): Buffer {
}
class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
- readonly guid = createGuid();
private _socket: net.Socket;
private _buffer = Buffer.from([]);
private _isSocket = false;
diff --git a/packages/playwright-core/src/server/bidi/bidiChromium.ts b/packages/playwright-core/src/server/bidi/bidiChromium.ts
index 53fc05cb9ce93..38a23580c4e40 100644
--- a/packages/playwright-core/src/server/bidi/bidiChromium.ts
+++ b/packages/playwright-core/src/server/bidi/bidiChromium.ts
@@ -34,7 +34,7 @@ import type * as types from '../types';
export class BidiChromium extends BrowserType {
constructor(parent: SdkObject) {
- super(parent, 'bidi');
+ super(parent, '_bidiChromium');
}
override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions, browserLogsCollector: RecentLogsCollector): Promise {
@@ -144,14 +144,14 @@ export class BidiChromium extends BrowserType {
const proxyURL = new URL(proxy.server);
const isSocks = proxyURL.protocol === 'socks5:';
// https://www.chromium.org/developers/design-documents/network-settings
- if (isSocks && !this.attribution.playwright.options.socksProxyPort) {
+ if (isSocks && !options.socksProxyPort) {
// https://www.chromium.org/developers/design-documents/network-stack/socks-proxy
chromeArguments.push(`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"`);
}
chromeArguments.push(`--proxy-server=${proxy.server}`);
const proxyBypassRules = [];
// https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
- if (this.attribution.playwright.options.socksProxyPort)
+ if (options.socksProxyPort)
proxyBypassRules.push('<-loopback>');
if (proxy.bypass)
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
diff --git a/packages/playwright-core/src/server/bidi/bidiFirefox.ts b/packages/playwright-core/src/server/bidi/bidiFirefox.ts
index 28c2af7c1db86..f10808a13dd36 100644
--- a/packages/playwright-core/src/server/bidi/bidiFirefox.ts
+++ b/packages/playwright-core/src/server/bidi/bidiFirefox.ts
@@ -35,7 +35,11 @@ import type { RecentLogsCollector } from '../utils/debugLogger';
export class BidiFirefox extends BrowserType {
constructor(parent: SdkObject) {
- super(parent, 'bidi');
+ super(parent, '_bidiFirefox');
+ }
+
+ override executablePath(): string {
+ return '';
}
override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise {
diff --git a/packages/playwright-core/src/server/bidi/bidiInput.ts b/packages/playwright-core/src/server/bidi/bidiInput.ts
index e67c07ba8ff64..30a6439745197 100644
--- a/packages/playwright-core/src/server/bidi/bidiInput.ts
+++ b/packages/playwright-core/src/server/bidi/bidiInput.ts
@@ -21,6 +21,7 @@ import * as bidi from './third_party/bidiProtocol';
import type * as input from '../input';
import type * as types from '../types';
import type { BidiSession } from './bidiConnection';
+import type { Progress } from '../progress';
export class RawKeyboardImpl implements input.RawKeyboard {
private _session: BidiSession;
@@ -33,32 +34,32 @@ export class RawKeyboardImpl implements input.RawKeyboard {
this._session = session;
}
- async keydown(modifiers: Set, keyName: string, description: input.KeyDescription, autoRepeat: boolean): Promise {
+ async keydown(progress: Progress, modifiers: Set, keyName: string, description: input.KeyDescription, autoRepeat: boolean): Promise {
keyName = resolveSmartModifierString(keyName);
const actions: bidi.Input.KeySourceAction[] = [];
actions.push({ type: 'keyDown', value: getBidiKeyValue(keyName) });
- await this._performActions(actions);
+ await this._performActions(progress, actions);
}
- async keyup(modifiers: Set, keyName: string, description: input.KeyDescription): Promise {
+ async keyup(progress: Progress, modifiers: Set, keyName: string, description: input.KeyDescription): Promise {
keyName = resolveSmartModifierString(keyName);
const actions: bidi.Input.KeySourceAction[] = [];
actions.push({ type: 'keyUp', value: getBidiKeyValue(keyName) });
- await this._performActions(actions);
+ await this._performActions(progress, actions);
}
- async sendText(text: string): Promise {
+ async sendText(progress: Progress, text: string): Promise {
const actions: bidi.Input.KeySourceAction[] = [];
for (const char of text) {
const value = getBidiKeyValue(char);
actions.push({ type: 'keyDown', value });
actions.push({ type: 'keyUp', value });
}
- await this._performActions(actions);
+ await this._performActions(progress, actions);
}
- private async _performActions(actions: bidi.Input.KeySourceAction[]) {
- await this._session.send('input.performActions', {
+ private async _performActions(progress: Progress, actions: bidi.Input.KeySourceAction[]) {
+ await progress.race(this._session.send('input.performActions', {
context: this._session.sessionId,
actions: [
{
@@ -67,7 +68,7 @@ export class RawKeyboardImpl implements input.RawKeyboard {
actions,
}
]
- });
+ }));
}
}
@@ -78,23 +79,23 @@ export class RawMouseImpl implements input.RawMouse {
this._session = session;
}
- async move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set, modifiers: Set, forClick: boolean): Promise {
- await this._performActions([{ type: 'pointerMove', x, y }]);
+ async move(progress: Progress, x: number, y: number, button: types.MouseButton | 'none', buttons: Set, modifiers: Set, forClick: boolean): Promise {
+ await this._performActions(progress, [{ type: 'pointerMove', x, y }]);
}
- async down(x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set, clickCount: number): Promise {
- await this._performActions([{ type: 'pointerDown', button: toBidiButton(button) }]);
+ async down(progress: Progress, x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set, clickCount: number): Promise {
+ await this._performActions(progress, [{ type: 'pointerDown', button: toBidiButton(button) }]);
}
- async up(x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set, clickCount: number): Promise {
- await this._performActions([{ type: 'pointerUp', button: toBidiButton(button) }]);
+ async up(progress: Progress, x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set, clickCount: number): Promise {
+ await this._performActions(progress, [{ type: 'pointerUp', button: toBidiButton(button) }]);
}
- async wheel(x: number, y: number, buttons: Set, modifiers: Set, deltaX: number, deltaY: number): Promise {
+ async wheel(progress: Progress, x: number, y: number, buttons: Set, modifiers: Set, deltaX: number, deltaY: number): Promise {
// Bidi throws when x/y are not integers.
x = Math.floor(x);
y = Math.floor(y);
- await this._session.send('input.performActions', {
+ await progress.race(this._session.send('input.performActions', {
context: this._session.sessionId,
actions: [
{
@@ -103,11 +104,11 @@ export class RawMouseImpl implements input.RawMouse {
actions: [{ type: 'scroll', x, y, deltaX, deltaY }],
}
]
- });
+ }));
}
- private async _performActions(actions: bidi.Input.PointerSourceAction[]) {
- await this._session.send('input.performActions', {
+ private async _performActions(progress: Progress, actions: bidi.Input.PointerSourceAction[]) {
+ await progress.race(this._session.send('input.performActions', {
context: this._session.sessionId,
actions: [
{
@@ -119,7 +120,7 @@ export class RawMouseImpl implements input.RawMouse {
actions,
}
]
- });
+ }));
}
}
@@ -130,7 +131,7 @@ export class RawTouchscreenImpl implements input.RawTouchscreen {
this._session = session;
}
- async tap(x: number, y: number, modifiers: Set) {
+ async tap(progress: Progress, x: number, y: number, modifiers: Set) {
}
}
diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts
index f8d379d179034..606d6fb416f4b 100644
--- a/packages/playwright-core/src/server/bidi/bidiPage.ts
+++ b/packages/playwright-core/src/server/bidi/bidiPage.ts
@@ -368,7 +368,7 @@ export class BidiPage implements PageDelegate {
async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise {
const rect = (documentRect || viewportRect)!;
- const { data } = await this._session.send('browsingContext.captureScreenshot', {
+ const { data } = await progress.race(this._session.send('browsingContext.captureScreenshot', {
context: this._session.sessionId,
format: {
type: `image/${format === 'png' ? 'png' : 'jpeg'}`,
@@ -379,7 +379,7 @@ export class BidiPage implements PageDelegate {
type: 'box',
...rect,
}
- });
+ }));
return Buffer.from(data, 'base64');
}
@@ -510,7 +510,7 @@ export class BidiPage implements PageDelegate {
async inputActionEpilogue(): Promise {
}
- async resetForReuse(): Promise {
+ async resetForReuse(progress: Progress): Promise {
}
async pdf(options: channels.PagePdfParams): Promise {
diff --git a/packages/playwright-core/src/server/browser.ts b/packages/playwright-core/src/server/browser.ts
index e165de972952e..f19c124afdfeb 100644
--- a/packages/playwright-core/src/server/browser.ts
+++ b/packages/playwright-core/src/server/browser.ts
@@ -20,6 +20,7 @@ import { Download } from './download';
import { SdkObject } from './instrumentation';
import { Page } from './page';
import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
+import { ProgressController } from './progress';
import type { CallMetadata } from './instrumentation';
import type * as types from './types';
@@ -27,6 +28,8 @@ import type { ProxySettings } from './types';
import type { RecentLogsCollector } from './utils/debugLogger';
import type * as channels from '@protocol/channels';
import type { ChildProcess } from 'child_process';
+import type { Language } from '../utils';
+import type { Progress } from './progress';
export interface BrowserProcess {
@@ -52,6 +55,7 @@ export type BrowserOptions = {
browserLogsCollector: RecentLogsCollector,
slowMo?: number;
wsEndpoint?: string; // Only there when connected over web socket.
+ sdkLanguage?: Language;
originalLaunchOptions: types.LaunchOptions;
};
@@ -84,25 +88,30 @@ export abstract class Browser extends SdkObject {
abstract version(): string;
abstract userAgent(): string;
- async newContext(metadata: CallMetadata, options: types.BrowserContextOptions): Promise {
+ sdkLanguage() {
+ return this.options.sdkLanguage || this.attribution.playwright.options.sdkLanguage;
+ }
+
+ newContextFromMetadata(metadata: CallMetadata, options: types.BrowserContextOptions): Promise {
+ const controller = new ProgressController(metadata, this);
+ return controller.run(progress => this.newContext(progress, options));
+ }
+
+ async newContext(progress: Progress, options: types.BrowserContextOptions): Promise {
validateBrowserContextOptions(options, this.options);
let clientCertificatesProxy: ClientCertificatesProxy | undefined;
if (options.clientCertificates?.length) {
- clientCertificatesProxy = new ClientCertificatesProxy(options);
+ clientCertificatesProxy = await progress.raceWithCleanup(ClientCertificatesProxy.create(options), proxy => proxy.close());
options = { ...options };
- options.proxyOverride = await clientCertificatesProxy.listen();
+ options.proxyOverride = clientCertificatesProxy.proxySettings();
options.internalIgnoreHTTPSErrors = true;
}
- let context;
- try {
- context = await this.doCreateNewContext(options);
- } catch (error) {
- await clientCertificatesProxy?.close();
- throw error;
- }
+ const context = await progress.raceWithCleanup(this.doCreateNewContext(options), context => context.close({ reason: 'Failed to create context' }));
context._clientCertificatesProxy = clientCertificatesProxy;
+ if ((options as any).__testHookBeforeSetStorageState)
+ await progress.race((options as any).__testHookBeforeSetStorageState());
if (options.storageState)
- await context.setStorageState(metadata, options.storageState);
+ await context.setStorageState(progress, options.storageState);
this.emit(Browser.Events.Context, context);
return context;
}
@@ -112,7 +121,7 @@ export abstract class Browser extends SdkObject {
if (!this._contextForReuse || hash !== this._contextForReuse.hash || !this._contextForReuse.context.canResetForReuse()) {
if (this._contextForReuse)
await this._contextForReuse.context.close({ reason: 'Context reused' });
- this._contextForReuse = { context: await this.newContext(metadata, params), hash };
+ this._contextForReuse = { context: await this.newContextFromMetadata(metadata, params), hash };
return { context: this._contextForReuse.context, needsReset: false };
}
await this._contextForReuse.context.stopPendingOperations('Context recreated');
diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts
index 5d9857d61ae83..3615c3b1bdcab 100644
--- a/packages/playwright-core/src/server/browserContext.ts
+++ b/packages/playwright-core/src/server/browserContext.ts
@@ -36,13 +36,14 @@ import { RecorderApp } from './recorder/recorderApp';
import { Selectors } from './selectors';
import { Tracing } from './trace/recorder/tracing';
import * as rawStorageSource from '../generated/storageScriptSource';
+import { ProgressController } from './progress';
import type { Artifact } from './artifact';
import type { Browser, BrowserOptions } from './browser';
import type { Download } from './download';
import type * as frames from './frames';
import type { CallMetadata } from './instrumentation';
-import type { Progress, ProgressController } from './progress';
+import type { Progress } from './progress';
import type { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
import type { SerializedStorage } from '@injected/storageScript';
import type * as types from './types';
@@ -167,8 +168,7 @@ export abstract class BrowserContext extends SdkObject {
async stopPendingOperations(reason: string) {
// When using context reuse, stop pending operations to gracefully terminate all the actions
// with a user-friendly error message containing operation log.
- for (const controller of this._activeProgressControllers)
- controller.abort(new Error(reason));
+ await Promise.all(Array.from(this._activeProgressControllers).map(controller => controller.abort(reason)));
// Let rejections in microtask generate events before returning.
await new Promise(f => setTimeout(f, 0));
}
@@ -191,7 +191,12 @@ export abstract class BrowserContext extends SdkObject {
}
async resetForReuse(metadata: CallMetadata, params: channels.BrowserNewContextForReuseParams | null) {
- await this.tracing.resetForReuse();
+ const controller = new ProgressController(metadata, this);
+ return controller.run(progress => this.resetForReuseImpl(progress, params));
+ }
+
+ async resetForReuseImpl(progress: Progress, params: channels.BrowserNewContextForReuseParams | null) {
+ await progress.race(this.tracing.resetForReuse());
if (params) {
for (const key of paramsThatAllowContextReuse)
@@ -204,30 +209,34 @@ export abstract class BrowserContext extends SdkObject {
let page: Page | undefined = this.pages()[0];
const [, ...otherPages] = this.pages();
for (const p of otherPages)
- await p.close(metadata);
+ await p.close();
if (page && page.hasCrashed()) {
- await page.close(metadata);
+ await page.close();
page = undefined;
}
// Navigate to about:blank first to ensure no page scripts are running after this point.
- await page?.mainFrame().goto(metadata, 'about:blank', { timeout: 0 });
-
- await this._resetStorage();
- await this.clock.resetForReuse();
- // TODO: following can be optimized to not perform noops.
- if (this._options.permissions)
- await this.grantPermissions(this._options.permissions);
- else
- await this.clearPermissions();
- await this.setExtraHTTPHeaders(this._options.extraHTTPHeaders || []);
- await this.setGeolocation(this._options.geolocation);
- await this.setOffline(!!this._options.offline);
- await this.setUserAgent(this._options.userAgent);
- await this.clearCache();
- await this._resetCookies();
+ await page?.mainFrame().gotoImpl(progress, 'about:blank', {});
+
+ await this._resetStorage(progress);
+
+ const resetOptions = async () => {
+ await this.clock.resetForReuse();
+ // TODO: following can be optimized to not perform noops.
+ if (this._options.permissions)
+ await this.grantPermissions(this._options.permissions);
+ else
+ await this.clearPermissions();
+ await this.setExtraHTTPHeaders(this._options.extraHTTPHeaders || []);
+ await this.setGeolocation(this._options.geolocation);
+ await this.setOffline(!!this._options.offline);
+ await this.setUserAgent(this._options.userAgent);
+ await this.clearCache();
+ await this._resetCookies();
+ };
+ await progress.race(resetOptions());
- await page?.resetForReuse(metadata);
+ await page?.resetForReuse(progress);
}
_browserClosed() {
@@ -374,14 +383,13 @@ export abstract class BrowserContext extends SdkObject {
async _loadDefaultContextAsIs(progress: Progress): Promise {
if (!this.possiblyUninitializedPages().length) {
const waitForEvent = helper.waitForEvent(progress, this, BrowserContext.Events.Page);
- progress.cleanupWhenAborted(() => waitForEvent.dispose);
// Race against BrowserContext.close
await Promise.race([waitForEvent.promise, this._closePromise]);
}
const page = this.possiblyUninitializedPages()[0];
if (!page)
return;
- const pageOrError = await page.waitForInitializedOrError();
+ const pageOrError = await progress.race(page.waitForInitializedOrError());
if (pageOrError instanceof Error)
throw pageOrError;
await page.mainFrame()._waitForLoadState(progress, 'load');
@@ -397,8 +405,8 @@ export abstract class BrowserContext extends SdkObject {
// Workaround for:
// - chromium fails to change isMobile for existing page;
// - webkit fails to change locale for existing page.
- await this.newPage(progress.metadata);
- await defaultPage.close(progress.metadata);
+ await this.newPage(progress, false);
+ await defaultPage.close();
}
}
@@ -506,9 +514,14 @@ export abstract class BrowserContext extends SdkObject {
await this._closePromise;
}
- async newPage(metadata: CallMetadata): Promise {
- const page = await this.doCreateNewPage(metadata.isServerSide);
- const pageOrError = await page.waitForInitializedOrError();
+ newPageFromMetadata(metadata: CallMetadata): Promise {
+ const contoller = new ProgressController(metadata, this);
+ return contoller.run(progress => this.newPage(progress, false));
+ }
+
+ async newPage(progress: Progress, isServerSide: boolean): Promise {
+ const page = await progress.raceWithCleanup(this.doCreateNewPage(isServerSide), page => page.close());
+ const pageOrError = await progress.race(page.waitForInitializedOrError());
if (pageOrError instanceof Page) {
if (pageOrError.isClosed())
throw new Error('Page has been closed.');
@@ -521,7 +534,12 @@ export abstract class BrowserContext extends SdkObject {
this._origins.add(origin);
}
- async storageState(indexedDB = false): Promise {
+ storageState(indexedDB = false): Promise {
+ const controller = new ProgressController(serverSideCallMetadata(), this);
+ return controller.run(progress => this.storageStateImpl(progress, indexedDB));
+ }
+
+ async storageStateImpl(progress: Progress, indexedDB: boolean): Promise {
const result: channels.BrowserContextStorageStateResult = {
cookies: await this.cookies(),
origins: []
@@ -552,46 +570,43 @@ export abstract class BrowserContext extends SdkObject {
// If there are still origins to save, create a blank page to iterate over origins.
if (originsToSave.size) {
- const internalMetadata = serverSideCallMetadata();
- const page = await this.newPage(internalMetadata);
- page.addRequestInterceptor(route => {
+ const page = await this.newPage(progress, true);
+ await progress.race(page.addRequestInterceptor(route => {
route.fulfill({ body: '' }).catch(() => {});
- }, 'prepend');
+ }, 'prepend'));
for (const origin of originsToSave) {
const frame = page.mainFrame();
- await frame.goto(internalMetadata, origin, { timeout: 0 });
- const storage: SerializedStorage = await frame.evaluateExpression(collectScript, { world: 'utility' });
+ await frame.gotoImpl(progress, origin, {});
+ const storage: SerializedStorage = await progress.race(frame.evaluateExpression(collectScript, { world: 'utility' }));
if (storage.localStorage.length || storage.indexedDB?.length)
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB });
}
- await page.close(internalMetadata);
+ await page.close();
}
return result;
}
- async _resetStorage() {
+ async _resetStorage(progress: Progress) {
const oldOrigins = this._origins;
const newOrigins = new Map(this._options.storageState?.origins?.map(p => [p.origin, p]) || []);
if (!oldOrigins.size && !newOrigins.size)
return;
let page = this.pages()[0];
- const internalMetadata = serverSideCallMetadata();
- page = page || await this.newPage({
- ...internalMetadata,
- // Do not mark this page as internal, because we will leave it for later reuse
- // as a user-visible page.
- isServerSide: false,
- });
+ // Do not mark this page as internal, because we will leave it for later reuse
+ // as a user-visible page.
+ page = page || await this.newPage(progress, false);
const interceptor = (route: network.Route) => {
route.fulfill({ body: '' }).catch(() => {});
};
- await page.addRequestInterceptor(interceptor, 'prepend');
+
+ progress.cleanupWhenAborted(() => page.removeRequestInterceptor(interceptor));
+ await progress.race(page.addRequestInterceptor(interceptor, 'prepend'));
for (const origin of new Set([...oldOrigins, ...newOrigins.keys()])) {
const frame = page.mainFrame();
- await frame.goto(internalMetadata, origin, { timeout: 0 });
- await frame.resetStorageForCurrentOriginBestEffort(newOrigins.get(origin));
+ await frame.gotoImpl(progress, origin, {});
+ await progress.race(frame.resetStorageForCurrentOriginBestEffort(newOrigins.get(origin)));
}
await page.removeRequestInterceptor(interceptor);
@@ -610,29 +625,28 @@ export abstract class BrowserContext extends SdkObject {
return this._settingStorageState;
}
- async setStorageState(metadata: CallMetadata, state: NonNullable) {
+ async setStorageState(progress: Progress, state: NonNullable) {
this._settingStorageState = true;
try {
if (state.cookies)
- await this.addCookies(state.cookies);
+ await progress.race(this.addCookies(state.cookies));
if (state.origins && state.origins.length) {
- const internalMetadata = serverSideCallMetadata();
- const page = await this.newPage(internalMetadata);
- await page.addRequestInterceptor(route => {
+ const page = await this.newPage(progress, true);
+ await progress.race(page.addRequestInterceptor(route => {
route.fulfill({ body: '' }).catch(() => {});
- }, 'prepend');
+ }, 'prepend'));
for (const originState of state.origins) {
const frame = page.mainFrame();
- await frame.goto(metadata, originState.origin, { timeout: 0 });
+ await frame.gotoImpl(progress, originState.origin, {});
const restoreScript = `(() => {
const module = {};
${rawStorageSource.source}
const script = new (module.exports.StorageScript())(${this._browser.options.name === 'firefox'});
return script.restore(${JSON.stringify(originState)});
})()`;
- await frame.evaluateExpression(restoreScript, { world: 'utility' });
+ await progress.race(frame.evaluateExpression(restoreScript, { world: 'utility' }));
}
- await page.close(internalMetadata);
+ await page.close();
}
} finally {
this._settingStorageState = false;
diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts
index 8564f44081bbb..ba0cfa4aa148e 100644
--- a/packages/playwright-core/src/server/browserType.ts
+++ b/packages/playwright-core/src/server/browserType.ts
@@ -23,7 +23,7 @@ import { debugMode } from './utils/debug';
import { assert } from '../utils/isomorphic/assert';
import { ManualPromise } from '../utils/isomorphic/manualPromise';
import { DEFAULT_PLAYWRIGHT_TIMEOUT } from '../utils/isomorphic/time';
-import { existsAsync } from './utils/fileUtils';
+import { existsAsync, removeFolders } from './utils/fileUtils';
import { helper } from './helper';
import { SdkObject } from './instrumentation';
import { PipeTransport } from './pipeTransport';
@@ -56,6 +56,7 @@ export abstract class BrowserType extends SdkObject {
super(parent, 'browser-type');
this.attribution.browserType = this;
this._name = browserName;
+ this.logName = 'browser';
}
executablePath(): string {
@@ -69,7 +70,6 @@ export abstract class BrowserType extends SdkObject {
async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise {
options = this._validateLaunchOptions(options);
const controller = new ProgressController(metadata, this);
- controller.setLogName('browser');
const browser = await controller.run(progress => {
const seleniumHubUrl = (options as any).__testHookSeleniumRemoteURL || process.env.SELENIUM_REMOTE_URL;
if (seleniumHubUrl)
@@ -79,20 +79,18 @@ export abstract class BrowserType extends SdkObject {
return browser;
}
- async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { timeout: number, cdpPort?: number, internalIgnoreHTTPSErrors?: boolean }): Promise {
+ async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { timeout: number, cdpPort?: number, internalIgnoreHTTPSErrors?: boolean, socksProxyPort?: number }): Promise {
const launchOptions = this._validateLaunchOptions(options);
const controller = new ProgressController(metadata, this);
- controller.setLogName('browser');
const browser = await controller.run(async progress => {
// Note: Any initial TLS requests will fail since we rely on the Page/Frames initialize which sets ignoreHTTPSErrors.
let clientCertificatesProxy: ClientCertificatesProxy | undefined;
if (options.clientCertificates?.length) {
- clientCertificatesProxy = new ClientCertificatesProxy(options);
- launchOptions.proxyOverride = await clientCertificatesProxy?.listen();
+ clientCertificatesProxy = await progress.raceWithCleanup(ClientCertificatesProxy.create(options), proxy => proxy.close());
+ launchOptions.proxyOverride = clientCertificatesProxy.proxySettings();
options = { ...options };
options.internalIgnoreHTTPSErrors = true;
}
- progress.cleanupWhenAborted(() => clientCertificatesProxy?.close());
const browser = await this._innerLaunchWithRetries(progress, launchOptions, options, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); });
browser._defaultContext!._clientCertificatesProxy = clientCertificatesProxy;
return browser;
@@ -119,7 +117,7 @@ export abstract class BrowserType extends SdkObject {
const browserLogsCollector = new RecentLogsCollector();
const { browserProcess, userDataDir, artifactsDir, transport } = await this._launchProcess(progress, options, !!persistent, browserLogsCollector, maybeUserDataDir);
if ((options as any).__testHookBeforeCreateBrowser)
- await (options as any).__testHookBeforeCreateBrowser();
+ await progress.race((options as any).__testHookBeforeCreateBrowser());
const browserOptions: BrowserOptions = {
name: this._name,
isChromium: this._name === 'chromium',
@@ -141,7 +139,7 @@ export abstract class BrowserType extends SdkObject {
if (persistent)
validateBrowserContextOptions(persistent, browserOptions);
copyTestHooks(options, browserOptions);
- const browser = await this.connectToTransport(transport, browserOptions, browserLogsCollector);
+ const browser = await progress.race(this.connectToTransport(transport, browserOptions, browserLogsCollector));
(browser as any)._userDataDirForTest = userDataDir;
// We assume no control when using custom arguments, and do not prepare the default context in that case.
if (persistent && !options.ignoreAllDefaultArgs)
@@ -149,22 +147,16 @@ export abstract class BrowserType extends SdkObject {
return browser;
}
- private async _launchProcess(progress: Progress, options: types.LaunchOptions, isPersistent: boolean, browserLogsCollector: RecentLogsCollector, userDataDir?: string): Promise<{ browserProcess: BrowserProcess, artifactsDir: string, userDataDir: string, transport: ConnectionTransport }> {
+ private async _prepareToLaunch(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string | undefined) {
const {
ignoreDefaultArgs,
ignoreAllDefaultArgs,
args = [],
executablePath = null,
- handleSIGINT = true,
- handleSIGTERM = true,
- handleSIGHUP = true,
} = options;
-
- const env = options.env ? envArrayToObject(options.env) : process.env;
-
await this._createArtifactDirs(options);
- const tempDirectories = [];
+ const tempDirectories: string[] = [];
const artifactsDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-artifacts-'));
tempDirectories.push(artifactsDir);
@@ -179,7 +171,7 @@ export abstract class BrowserType extends SdkObject {
}
await this.prepareUserDataDir(options, userDataDir);
- const browserArguments = [];
+ const browserArguments: string[] = [];
if (ignoreAllDefaultArgs)
browserArguments.push(...args);
else if (ignoreDefaultArgs)
@@ -200,15 +192,29 @@ export abstract class BrowserType extends SdkObject {
await registry.validateHostRequirementsForExecutablesIfNeeded([registryExecutable], this.attribution.playwright.options.sdkLanguage);
}
+ return { executable, browserArguments, userDataDir, artifactsDir, tempDirectories };
+ }
+
+ private async _launchProcess(progress: Progress, options: types.LaunchOptions, isPersistent: boolean, browserLogsCollector: RecentLogsCollector, userDataDir?: string): Promise<{ browserProcess: BrowserProcess, artifactsDir: string, userDataDir: string, transport: ConnectionTransport }> {
+ const {
+ handleSIGINT = true,
+ handleSIGTERM = true,
+ handleSIGHUP = true,
+ } = options;
+
+ const env = options.env ? envArrayToObject(options.env) : process.env;
+ const prepared = await progress.race(this._prepareToLaunch(options, isPersistent, userDataDir));
+ progress.cleanupWhenAborted(() => removeFolders(prepared.tempDirectories));
+
// Note: it is important to define these variables before launchProcess, so that we don't get
// "Cannot access 'browserServer' before initialization" if something went wrong.
let transport: ConnectionTransport | undefined = undefined;
let browserProcess: BrowserProcess | undefined = undefined;
const exitPromise = new ManualPromise();
const { launchedProcess, gracefullyClose, kill } = await launchProcess({
- command: executable,
- args: browserArguments,
- env: this.amendEnvironment(env, userDataDir, executable, browserArguments),
+ command: prepared.executable,
+ args: prepared.browserArguments,
+ env: this.amendEnvironment(env, prepared.userDataDir, prepared.executable, prepared.browserArguments),
handleSIGINT,
handleSIGTERM,
handleSIGHUP,
@@ -217,7 +223,7 @@ export abstract class BrowserType extends SdkObject {
browserLogsCollector.log(message);
},
stdio: 'pipe',
- tempDirectories,
+ tempDirectories: prepared.tempDirectories,
attemptToGracefullyClose: async () => {
if ((options as any).__testHookGracefullyClose)
await (options as any).__testHookGracefullyClose();
@@ -253,8 +259,8 @@ export abstract class BrowserType extends SdkObject {
close: () => closeOrKill((options as any).__testHookBrowserCloseTimeout || DEFAULT_PLAYWRIGHT_TIMEOUT),
kill
};
- progress.cleanupWhenAborted(() => closeOrKill(progress.timeUntilDeadline()));
- const { wsEndpoint } = await Promise.race([
+ progress.cleanupWhenAborted(() => closeOrKill(DEFAULT_PLAYWRIGHT_TIMEOUT));
+ const { wsEndpoint } = await progress.race([
this.waitForReadyState(options, browserLogsCollector),
exitPromise.then(() => ({ wsEndpoint: undefined })),
]);
@@ -264,7 +270,8 @@ export abstract class BrowserType extends SdkObject {
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
transport = new PipeTransport(stdio[3], stdio[4]);
}
- return { browserProcess, artifactsDir, userDataDir, transport };
+ progress.cleanupWhenAborted(() => transport.close());
+ return { browserProcess, artifactsDir: prepared.artifactsDir, userDataDir: prepared.userDataDir, transport };
}
async _createArtifactDirs(options: types.LaunchOptions): Promise {
@@ -289,8 +296,8 @@ export abstract class BrowserType extends SdkObject {
headless = false;
if (downloadsPath && !path.isAbsolute(downloadsPath))
downloadsPath = path.join(process.cwd(), downloadsPath);
- if (this.attribution.playwright.options.socksProxyPort)
- proxy = { server: `socks5://127.0.0.1:${this.attribution.playwright.options.socksProxyPort}` };
+ if (options.socksProxyPort)
+ proxy = { server: `socks5://127.0.0.1:${options.socksProxyPort}` };
return { ...options, devtools, headless, downloadsPath, proxy };
}
diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts
index 61c4b0cc72795..8ae2cdc864629 100644
--- a/packages/playwright-core/src/server/chromium/chromium.ts
+++ b/packages/playwright-core/src/server/chromium/chromium.ts
@@ -64,7 +64,6 @@ export class Chromium extends BrowserType {
override async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray, timeout: number }) {
const controller = new ProgressController(metadata, this);
- controller.setLogName('browser');
return controller.run(async progress => {
return await this._connectOverCDPInternal(progress, endpointURL, options);
}, options.timeout);
@@ -80,12 +79,11 @@ export class Chromium extends BrowserType {
else if (headersMap && !Object.keys(headersMap).some(key => key.toLowerCase() === 'user-agent'))
headersMap['User-Agent'] = getUserAgent();
- const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER);
+ const artifactsDir = await progress.race(fs.promises.mkdtemp(ARTIFACTS_FOLDER));
const wsEndpoint = await urlToWSEndpoint(progress, endpointURL, headersMap);
- progress.throwIfAborted();
-
const chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint, { headers: headersMap });
+ progress.cleanupWhenAborted(() => chromeTransport.close());
const cleanedUp = new ManualPromise();
const doCleanup = async () => {
await removeFolders([artifactsDir]);
@@ -112,8 +110,7 @@ export class Chromium extends BrowserType {
originalLaunchOptions: { timeout: options.timeout },
};
validateBrowserContextOptions(persistent, browserOptions);
- progress.throwIfAborted();
- const browser = await CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions);
+ const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions));
browser._isCollocatedWithServer = false;
browser.on(Browser.Events.Disconnected, doCleanup);
return browser;
@@ -175,7 +172,7 @@ export class Chromium extends BrowserType {
}
override async _launchWithSeleniumHub(progress: Progress, hubUrl: string, options: types.LaunchOptions): Promise {
- await this._createArtifactDirs(options);
+ await progress.race(this._createArtifactDirs(options));
if (!hubUrl.endsWith('/'))
hubUrl = hubUrl + '/';
@@ -202,7 +199,7 @@ export class Chromium extends BrowserType {
}
progress.log(` connecting to ${hubUrl}`);
- const response = await fetchData({
+ const response = await fetchData(progress, {
url: hubUrl + 'session',
method: 'POST',
headers: {
@@ -212,7 +209,6 @@ export class Chromium extends BrowserType {
data: JSON.stringify({
capabilities: { alwaysMatch: desiredCapabilities }
}),
- timeout: progress.timeUntilDeadline(),
}, seleniumErrorHandler);
const value = JSON.parse(response).value;
const sessionId = value.sessionId;
@@ -220,7 +216,8 @@ export class Chromium extends BrowserType {
const disconnectFromSelenium = async () => {
progress.log(` disconnecting from sessionId=${sessionId}`);
- await fetchData({
+ // Do not pass "progress" to disconnect even after the progress has aborted.
+ await fetchData(undefined, {
url: hubUrl + 'session/' + sessionId,
method: 'DELETE',
headers,
@@ -256,10 +253,9 @@ export class Chromium extends BrowserType {
if (endpointURL.hostname === 'localhost' || endpointURL.hostname === '127.0.0.1') {
const sessionInfoUrl = new URL(hubUrl).origin + '/grid/api/testsession?session=' + sessionId;
try {
- const sessionResponse = await fetchData({
+ const sessionResponse = await fetchData(progress, {
url: sessionInfoUrl,
method: 'GET',
- timeout: progress.timeUntilDeadline(),
headers,
}, seleniumErrorHandler);
const proxyId = JSON.parse(sessionResponse).proxyId;
@@ -332,14 +328,14 @@ export class Chromium extends BrowserType {
const proxyURL = new URL(proxy.server);
const isSocks = proxyURL.protocol === 'socks5:';
// https://www.chromium.org/developers/design-documents/network-settings
- if (isSocks && !this.attribution.playwright.options.socksProxyPort) {
+ if (isSocks && !options.socksProxyPort) {
// https://www.chromium.org/developers/design-documents/network-stack/socks-proxy
chromeArguments.push(`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"`);
}
chromeArguments.push(`--proxy-server=${proxy.server}`);
const proxyBypassRules = [];
// https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
- if (this.attribution.playwright.options.socksProxyPort)
+ if (options.socksProxyPort)
proxyBypassRules.push('<-loopback>');
if (proxy.bypass)
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
@@ -385,10 +381,12 @@ async function urlToWSEndpoint(progress: Progress, endpointURL: string, headers:
return endpointURL;
progress.log(` retrieving websocket url from ${endpointURL}`);
const url = new URL(endpointURL);
+ if (!url.pathname.endsWith('/'))
+ url.pathname = url.pathname + '/';
url.pathname += 'json/version/';
const httpURL = url.toString();
- const json = await fetchData({
+ const json = await fetchData(progress, {
url: httpURL,
headers,
}, async (_, resp) => new Error(`Unexpected status ${resp.statusCode} when connecting to ${httpURL}.\n` +
diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts
index 0557ea11cf977..0c640c9879ab9 100644
--- a/packages/playwright-core/src/server/chromium/crBrowser.ts
+++ b/packages/playwright-core/src/server/chromium/crBrowser.ts
@@ -58,7 +58,7 @@ export class CRBrowser extends Browser {
static async connect(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions, devtools?: CRDevTools): Promise {
// Make a copy in case we need to update `headful` property below.
options = { ...options };
- const connection = new CRConnection(transport, options.protocolLogger, options.browserLogsCollector);
+ const connection = new CRConnection(parent, transport, options.protocolLogger, options.browserLogsCollector);
const browser = new CRBrowser(parent, connection, options);
browser._devtools = devtools;
if (browser.isClank())
@@ -455,6 +455,7 @@ export class CRBrowserContext extends BrowserContext {
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
['storage-access', 'storageAccess'],
+ ['local-fonts', 'localFonts'],
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
diff --git a/packages/playwright-core/src/server/chromium/crConnection.ts b/packages/playwright-core/src/server/chromium/crConnection.ts
index 98c566c82abf1..0a8d3427fcc01 100644
--- a/packages/playwright-core/src/server/chromium/crConnection.ts
+++ b/packages/playwright-core/src/server/chromium/crConnection.ts
@@ -15,12 +15,11 @@
* limitations under the License.
*/
-import { EventEmitter } from 'events';
-
import { assert, eventsHelper } from '../../utils';
import { debugLogger } from '../utils/debugLogger';
import { helper } from '../helper';
import { ProtocolError } from '../protocolError';
+import { SdkObject } from '../instrumentation';
import type { RegisteredListener } from '../../utils';
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
@@ -37,7 +36,7 @@ export const ConnectionEvents = {
// should ignore.
export const kBrowserCloseMessageId = -9999;
-export class CRConnection extends EventEmitter {
+export class CRConnection extends SdkObject {
private _lastId = 0;
private readonly _transport: ConnectionTransport;
readonly _sessions = new Map();
@@ -47,8 +46,8 @@ export class CRConnection extends EventEmitter {
readonly rootSession: CRSession;
_closed = false;
- constructor(transport: ConnectionTransport, protocolLogger: ProtocolLogger, browserLogsCollector: RecentLogsCollector) {
- super();
+ constructor(parent: SdkObject, transport: ConnectionTransport, protocolLogger: ProtocolLogger, browserLogsCollector: RecentLogsCollector) {
+ super(parent, 'cr-connection');
this.setMaxListeners(0);
this._transport = transport;
this._protocolLogger = protocolLogger;
@@ -101,7 +100,7 @@ export class CRConnection extends EventEmitter {
type SessionEventListener = (method: string, params?: Object) => void;
-export class CRSession extends EventEmitter {
+export class CRSession extends SdkObject {
private readonly _connection: CRConnection;
private _eventListener?: SessionEventListener;
private readonly _callbacks = new Map void, reject: (e: ProtocolError) => void, error: ProtocolError }>();
@@ -116,7 +115,7 @@ export class CRSession extends EventEmitter {
override once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
constructor(connection: CRConnection, parentSession: CRSession | null, sessionId: string, eventListener?: SessionEventListener) {
- super();
+ super(connection, 'cr-session');
this.setMaxListeners(0);
this._connection = connection;
this._parentSession = parentSession;
@@ -203,19 +202,17 @@ export class CRSession extends EventEmitter {
}
}
-export class CDPSession extends EventEmitter {
+export class CDPSession extends SdkObject {
static Events = {
Event: 'event',
Closed: 'close',
};
- readonly guid: string;
private _session: CRSession;
private _listeners: RegisteredListener[] = [];
constructor(parentSession: CRSession, sessionId: string) {
- super();
- this.guid = `cdp-session@${sessionId}`;
+ super(parentSession, 'cdp-session');
this._session = parentSession.createChildSession(sessionId, (method, params) => this.emit(CDPSession.Events.Event, { method, params }));
this._listeners = [eventsHelper.addEventListener(parentSession, 'Target.detachedFromTarget', (event: Protocol.Target.detachedFromTargetPayload) => {
if (event.sessionId === sessionId)
diff --git a/packages/playwright-core/src/server/chromium/crDragDrop.ts b/packages/playwright-core/src/server/chromium/crDragDrop.ts
index f94a30670110d..b54fe50f3f707 100644
--- a/packages/playwright-core/src/server/chromium/crDragDrop.ts
+++ b/packages/playwright-core/src/server/chromium/crDragDrop.ts
@@ -19,6 +19,7 @@ import { assert } from '../../utils';
import type { CRPage } from './crPage';
import type * as types from '../types';
import type { Protocol } from './protocol';
+import type { Progress } from '../progress';
declare global {
@@ -51,16 +52,16 @@ export class DragManager {
return true;
}
- async interceptDragCausedByMove(x: number, y: number, button: types.MouseButton | 'none', buttons: Set, modifiers: Set, moveCallback: () => Promise): Promise {
+ async interceptDragCausedByMove(progress: Progress, x: number, y: number, button: types.MouseButton | 'none', buttons: Set, modifiers: Set, moveCallback: () => Promise): Promise {
this._lastPosition = { x, y };
if (this._dragState) {
- await this._crPage._mainFrameSession._client.send('Input.dispatchDragEvent', {
+ await progress.race(this._crPage._mainFrameSession._client.send('Input.dispatchDragEvent', {
type: 'dragOver',
x,
y,
data: this._dragState,
modifiers: toModifiersMask(modifiers),
- });
+ }));
return;
}
if (button !== 'left')
@@ -91,6 +92,7 @@ export class DragManager {
}
await this._crPage._page.safeNonStallingEvaluateInAllFrames(`(${setupDragListeners.toString()})()`, 'utility');
+ progress.cleanupWhenAborted(() => this._crPage._page.safeNonStallingEvaluateInAllFrames('window.__cleanupDrag && window.__cleanupDrag()', 'utility'));
client.on('Input.dragIntercepted', onDragIntercepted!);
try {
@@ -108,17 +110,16 @@ export class DragManager {
}))).some(x => x);
this._dragState = expectingDrag ? (await dragInterceptedPromise).data : null;
client.off('Input.dragIntercepted', onDragIntercepted!);
- await client.send('Input.setInterceptDrags', { enabled: false });
-
+ await progress.race(client.send('Input.setInterceptDrags', { enabled: false }));
if (this._dragState) {
- await this._crPage._mainFrameSession._client.send('Input.dispatchDragEvent', {
+ await progress.race(this._crPage._mainFrameSession._client.send('Input.dispatchDragEvent', {
type: 'dragEnter',
x,
y,
data: this._dragState,
modifiers: toModifiersMask(modifiers),
- });
+ }));
}
}
@@ -126,15 +127,15 @@ export class DragManager {
return !!this._dragState;
}
- async drop(x: number, y: number, modifiers: Set) {
+ async drop(progress: Progress, x: number, y: number, modifiers: Set) {
assert(this._dragState, 'missing drag state');
- await this._crPage._mainFrameSession._client.send('Input.dispatchDragEvent', {
+ await progress.race(this._crPage._mainFrameSession._client.send('Input.dispatchDragEvent', {
type: 'drop',
x,
y,
data: this._dragState,
modifiers: toModifiersMask(modifiers),
- });
+ }));
this._dragState = null;
}
}
diff --git a/packages/playwright-core/src/server/chromium/crInput.ts b/packages/playwright-core/src/server/chromium/crInput.ts
index 2ec52432fbc3e..beb030c9bff9a 100644
--- a/packages/playwright-core/src/server/chromium/crInput.ts
+++ b/packages/playwright-core/src/server/chromium/crInput.ts
@@ -24,6 +24,7 @@ import type * as types from '../types';
import type { CRSession } from './crConnection';
import type { DragManager } from './crDragDrop';
import type { CRPage } from './crPage';
+import type { Progress } from '../progress';
export class RawKeyboardImpl implements input.RawKeyboard {
@@ -52,12 +53,12 @@ export class RawKeyboardImpl implements input.RawKeyboard {
return commands.map(c => c.substring(0, c.length - 1));
}
- async keydown(modifiers: Set, keyName: string, description: input.KeyDescription, autoRepeat: boolean): Promise {
+ async keydown(progress: Progress, modifiers: Set, keyName: string, description: input.KeyDescription, autoRepeat: boolean): Promise {
const { code, key, location, text } = description;
- if (code === 'Escape' && await this._dragManger.cancelDrag())
+ if (code === 'Escape' && await progress.race(this._dragManger.cancelDrag()))
return;
const commands = this._commandsForCode(code, modifiers);
- await this._client.send('Input.dispatchKeyEvent', {
+ await progress.race(this._client.send('Input.dispatchKeyEvent', {
type: text ? 'keyDown' : 'rawKeyDown',
modifiers: toModifiersMask(modifiers),
windowsVirtualKeyCode: description.keyCodeWithoutLocation,
@@ -69,23 +70,23 @@ export class RawKeyboardImpl implements input.RawKeyboard {
autoRepeat,
location,
isKeypad: location === input.keypadLocation
- });
+ }));
}
- async keyup(modifiers: Set, keyName: string, description: input.KeyDescription): Promise {
+ async keyup(progress: Progress, modifiers: Set, keyName: string, description: input.KeyDescription): Promise {
const { code, key, location } = description;
- await this._client.send('Input.dispatchKeyEvent', {
+ await progress.race(this._client.send('Input.dispatchKeyEvent', {
type: 'keyUp',
modifiers: toModifiersMask(modifiers),
key,
windowsVirtualKeyCode: description.keyCodeWithoutLocation,
code,
location
- });
+ }));
}
- async sendText(text: string): Promise {
- await this._client.send('Input.insertText', { text });
+ async sendText(progress: Progress, text: string): Promise {
+ await progress.race(this._client.send('Input.insertText', { text }));
}
}
@@ -100,9 +101,9 @@ export class RawMouseImpl implements input.RawMouse {
this._dragManager = dragManager;
}
- async move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set, modifiers: Set, forClick: boolean): Promise {
+ async move(progress: Progress, x: number, y: number, button: types.MouseButton | 'none', buttons: Set, modifiers: Set, forClick: boolean): Promise {
const actualMove = async () => {
- await this._client.send('Input.dispatchMouseEvent', {
+ await progress.race(this._client.send('Input.dispatchMouseEvent', {
type: 'mouseMoved',
button,
buttons: toButtonsMask(buttons),
@@ -110,20 +111,21 @@ export class RawMouseImpl implements input.RawMouse {
y,
modifiers: toModifiersMask(modifiers),
force: buttons.size > 0 ? 0.5 : 0,
- });
+ }));
};
if (forClick) {
// Avoid extra protocol calls related to drag and drop, because click relies on
// move-down-up protocol commands being sent synchronously.
- return actualMove();
+ await actualMove();
+ return;
}
- await this._dragManager.interceptDragCausedByMove(x, y, button, buttons, modifiers, actualMove);
+ await this._dragManager.interceptDragCausedByMove(progress, x, y, button, buttons, modifiers, actualMove);
}
- async down(x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set, clickCount: number): Promise {
+ async down(progress: Progress, x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set, clickCount: number): Promise {
if (this._dragManager.isDragging())
return;
- await this._client.send('Input.dispatchMouseEvent', {
+ await progress.race(this._client.send('Input.dispatchMouseEvent', {
type: 'mousePressed',
button,
buttons: toButtonsMask(buttons),
@@ -132,15 +134,15 @@ export class RawMouseImpl implements input.RawMouse {
modifiers: toModifiersMask(modifiers),
clickCount,
force: buttons.size > 0 ? 0.5 : 0,
- });
+ }));
}
- async up(x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set, clickCount: number): Promise {
+ async up(progress: Progress, x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set, clickCount: number): Promise {
if (this._dragManager.isDragging()) {
- await this._dragManager.drop(x, y, modifiers);
+ await this._dragManager.drop(progress, x, y, modifiers);
return;
}
- await this._client.send('Input.dispatchMouseEvent', {
+ await progress.race(this._client.send('Input.dispatchMouseEvent', {
type: 'mouseReleased',
button,
buttons: toButtonsMask(buttons),
@@ -148,18 +150,18 @@ export class RawMouseImpl implements input.RawMouse {
y,
modifiers: toModifiersMask(modifiers),
clickCount
- });
+ }));
}
- async wheel(x: number, y: number, buttons: Set, modifiers: Set, deltaX: number, deltaY: number): Promise {
- await this._client.send('Input.dispatchMouseEvent', {
+ async wheel(progress: Progress, x: number, y: number, buttons: Set, modifiers: Set, deltaX: number, deltaY: number): Promise {
+ await progress.race(this._client.send('Input.dispatchMouseEvent', {
type: 'mouseWheel',
x,
y,
modifiers: toModifiersMask(modifiers),
deltaX,
deltaY,
- });
+ }));
}
}
@@ -169,8 +171,8 @@ export class RawTouchscreenImpl implements input.RawTouchscreen {
constructor(client: CRSession) {
this._client = client;
}
- async tap(x: number, y: number, modifiers: Set) {
- await Promise.all([
+ async tap(progress: Progress, x: number, y: number, modifiers: Set) {
+ await progress.race(Promise.all([
this._client.send('Input.dispatchTouchEvent', {
type: 'touchStart',
modifiers: toModifiersMask(modifiers),
@@ -183,6 +185,6 @@ export class RawTouchscreenImpl implements input.RawTouchscreen {
modifiers: toModifiersMask(modifiers),
touchPoints: []
}),
- ]);
+ ]));
}
}
diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts
index 6a9f4d6613981..502d135c81cad 100644
--- a/packages/playwright-core/src/server/chromium/crPage.ts
+++ b/packages/playwright-core/src/server/chromium/crPage.ts
@@ -256,7 +256,7 @@ export class CRPage implements PageDelegate {
}
async takeScreenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise {
- const { visualViewport } = await this._mainFrameSession._client.send('Page.getLayoutMetrics');
+ const { visualViewport } = await progress.race(this._mainFrameSession._client.send('Page.getLayoutMetrics'));
if (!documentRect) {
documentRect = {
x: visualViewport.pageX + viewportRect!.x,
@@ -274,8 +274,7 @@ export class CRPage implements PageDelegate {
const deviceScaleFactor = this._browserContext._options.deviceScaleFactor || 1;
clip.scale /= deviceScaleFactor;
}
- progress.throwIfAborted();
- const result = await this._mainFrameSession._client.send('Page.captureScreenshot', { format, quality, clip, captureBeyondViewport: !fitsViewport });
+ const result = await progress.race(this._mainFrameSession._client.send('Page.captureScreenshot', { format, quality, clip, captureBeyondViewport: !fitsViewport }));
return Buffer.from(result.data, 'base64');
}
@@ -339,9 +338,9 @@ export class CRPage implements PageDelegate {
await this._mainFrameSession._client.send('Page.enable').catch(e => {});
}
- async resetForReuse(): Promise {
+ async resetForReuse(progress: Progress): Promise {
// See https://github.com/microsoft/playwright/issues/22432.
- await this.rawMouse.move(-1, -1, 'none', new Set(), new Set(), true);
+ await this.rawMouse.move(progress, -1, -1, 'none', new Set(), new Set(), true);
}
async pdf(options: channels.PagePdfParams): Promise {
@@ -894,7 +893,7 @@ class FrameSession {
async _createVideoRecorder(screencastId: string, options: types.PageScreencastOptions): Promise {
assert(!this._screencastId);
- const ffmpegPath = registry.findExecutable('ffmpeg')!.executablePathOrDie(this._page.attribution.playwright.options.sdkLanguage);
+ const ffmpegPath = registry.findExecutable('ffmpeg')!.executablePathOrDie(this._page.browserContext._browser.sdkLanguage());
this._videoRecorder = await VideoRecorder.launch(this._crPage._page, ffmpegPath, options);
this._screencastId = screencastId;
}
diff --git a/packages/playwright-core/src/server/chromium/videoRecorder.ts b/packages/playwright-core/src/server/chromium/videoRecorder.ts
index 160fd354812b6..59dc50c837c07 100644
--- a/packages/playwright-core/src/server/chromium/videoRecorder.ts
+++ b/packages/playwright-core/src/server/chromium/videoRecorder.ts
@@ -46,6 +46,7 @@ export class VideoRecorder {
controller.setLogName('browser');
return await controller.run(async progress => {
const recorder = new VideoRecorder(page, ffmpegPath, progress);
+ progress.cleanupWhenAborted(() => recorder.stop());
await recorder._launch(options);
return recorder;
});
diff --git a/packages/playwright-core/src/server/cookieStore.ts b/packages/playwright-core/src/server/cookieStore.ts
index 3e24425b3997a..c8839cebd9d71 100644
--- a/packages/playwright-core/src/server/cookieStore.ts
+++ b/packages/playwright-core/src/server/cookieStore.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { kMaxCookieExpiresDateInSeconds } from './network';
+import { isLocalHostname, kMaxCookieExpiresDateInSeconds } from './network';
import type * as channels from '@protocol/channels';
@@ -30,7 +30,7 @@ export class Cookie {
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.4
matches(url: URL): boolean {
- if (this._raw.secure && (url.protocol !== 'https:' && url.hostname !== 'localhost'))
+ if (this._raw.secure && (url.protocol !== 'https:' && !isLocalHostname(url.hostname)))
return false;
if (!domainMatches(url.hostname, this._raw.domain))
return false;
diff --git a/packages/playwright-core/src/server/debugController.ts b/packages/playwright-core/src/server/debugController.ts
index 9338b59406dd5..92b94211a612f 100644
--- a/packages/playwright-core/src/server/debugController.ts
+++ b/packages/playwright-core/src/server/debugController.ts
@@ -106,7 +106,7 @@ export class DebugController extends SdkObject {
if (!pages.length) {
const [browser] = this._playwright.allBrowsers();
const { context } = await browser.newContextForReuse({}, internalMetadata);
- await context.newPage(internalMetadata);
+ await context.newPageFromMetadata(internalMetadata);
}
// Update test id attribute.
if (params.testIdAttributeName) {
diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json
index b595e0347d417..f260c0d5e39d9 100644
--- a/packages/playwright-core/src/server/deviceDescriptorsSource.json
+++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json
@@ -110,7 +110,7 @@
"defaultBrowserType": "webkit"
},
"Galaxy S5": {
- "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@@ -121,7 +121,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S5 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@@ -132,7 +132,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 740
@@ -143,7 +143,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 740,
"height": 360
@@ -154,7 +154,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 320,
"height": 658
@@ -165,7 +165,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+ landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 658,
"height": 320
@@ -176,7 +176,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S24": {
- "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 480,
"height": 1040
@@ -187,7 +187,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S24 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 1040,
"height": 480
@@ -198,7 +198,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy A55": {
- "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 480,
"height": 1040
@@ -209,7 +209,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy A55 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 1040,
"height": 480
@@ -220,7 +220,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Safari/537.36",
"viewport": {
"width": 712,
"height": 1138
@@ -231,7 +231,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Safari/537.36",
"viewport": {
"width": 1138,
"height": 712
@@ -242,7 +242,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S9": {
- "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Safari/537.36",
"viewport": {
"width": 640,
"height": 1024
@@ -253,7 +253,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S9 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Safari/537.36",
"viewport": {
"width": 1024,
"height": 640
@@ -1208,7 +1208,7 @@
"defaultBrowserType": "webkit"
},
"LG Optimus L70": {
- "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@@ -1219,7 +1219,7 @@
"defaultBrowserType": "chromium"
},
"LG Optimus L70 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@@ -1230,7 +1230,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@@ -1241,7 +1241,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550 landscape": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@@ -1252,7 +1252,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@@ -1263,7 +1263,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950 landscape": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@@ -1274,7 +1274,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Safari/537.36",
"viewport": {
"width": 800,
"height": 1280
@@ -1285,7 +1285,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Safari/537.36",
"viewport": {
"width": 1280,
"height": 800
@@ -1296,7 +1296,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@@ -1307,7 +1307,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@@ -1318,7 +1318,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@@ -1329,7 +1329,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@@ -1340,7 +1340,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@@ -1351,7 +1351,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@@ -1362,7 +1362,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@@ -1373,7 +1373,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@@ -1384,7 +1384,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@@ -1395,7 +1395,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@@ -1406,7 +1406,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Safari/537.36",
"viewport": {
"width": 600,
"height": 960
@@ -1417,7 +1417,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Safari/537.36",
"viewport": {
"width": 960,
"height": 600
@@ -1472,7 +1472,7 @@
"defaultBrowserType": "webkit"
},
"Pixel 2": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 731
@@ -1483,7 +1483,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 731,
"height": 411
@@ -1494,7 +1494,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 823
@@ -1505,7 +1505,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 823,
"height": 411
@@ -1516,7 +1516,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3": {
- "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 393,
"height": 786
@@ -1527,7 +1527,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 786,
"height": 393
@@ -1538,7 +1538,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 353,
"height": 745
@@ -1549,7 +1549,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 745,
"height": 353
@@ -1560,7 +1560,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G)": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 892
@@ -1575,7 +1575,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G) landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"screen": {
"height": 892,
"width": 412
@@ -1590,7 +1590,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"screen": {
"width": 393,
"height": 851
@@ -1605,7 +1605,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"screen": {
"width": 851,
"height": 393
@@ -1620,7 +1620,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7": {
- "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 915
@@ -1635,7 +1635,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"screen": {
"width": 915,
"height": 412
@@ -1650,7 +1650,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@@ -1661,7 +1661,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@@ -1672,7 +1672,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Chrome HiDPI": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Safari/537.36",
"screen": {
"width": 1792,
"height": 1120
@@ -1687,7 +1687,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge HiDPI": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Safari/537.36 Edg/138.0.7204.15",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Safari/537.36 Edg/138.0.7204.35",
"screen": {
"width": 1792,
"height": 1120
@@ -1732,7 +1732,7 @@
"defaultBrowserType": "webkit"
},
"Desktop Chrome": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Safari/537.36",
"screen": {
"width": 1920,
"height": 1080
@@ -1747,7 +1747,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.15 Safari/537.36 Edg/138.0.7204.15",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.35 Safari/537.36 Edg/138.0.7204.35",
"screen": {
"width": 1920,
"height": 1080
diff --git a/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts b/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts
index 25398ce05f164..6972d50f2d21c 100644
--- a/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts
@@ -17,6 +17,8 @@
import { BrowserContextDispatcher } from './browserContextDispatcher';
import { Dispatcher } from './dispatcher';
import { AndroidDevice } from '../android/android';
+import { eventsHelper } from '../utils/eventsHelper';
+import { SdkObject } from '../instrumentation';
import type { RootDispatcher } from './dispatcher';
import type { Android, SocketBackend } from '../android/android';
@@ -25,8 +27,10 @@ import type * as channels from '@protocol/channels';
export class AndroidDispatcher extends Dispatcher implements channels.AndroidChannel {
_type_Android = true;
- constructor(scope: RootDispatcher, android: Android) {
+ _denyLaunch: boolean;
+ constructor(scope: RootDispatcher, android: Android, denyLaunch: boolean) {
super(scope, android, 'Android', {});
+ this._denyLaunch = denyLaunch;
}
async devices(params: channels.AndroidDevicesParams): Promise {
@@ -145,7 +149,7 @@ export class AndroidDeviceDispatcher extends Dispatcher {
const socket = await this._object.open(params.command);
- return { socket: new AndroidSocketDispatcher(this, socket) };
+ return { socket: new AndroidSocketDispatcher(this, new SocketSdkObject(this._object, socket)) };
}
async installApk(params: channels.AndroidDeviceInstallApkParams) {
@@ -156,8 +160,10 @@ export class AndroidDeviceDispatcher extends Dispatcher {
- const context = await this._object.launchBrowser(params.pkg, params);
+ async launchBrowser(params: channels.AndroidDeviceLaunchBrowserParams, metadata: CallMetadata): Promise {
+ if (this.parentScope()._denyLaunch)
+ throw new Error(`Launching more browsers is not allowed.`);
+ const context = await this._object.launchBrowser(metadata, params.pkg, params);
return { context: BrowserContextDispatcher.from(this, context) };
}
@@ -165,15 +171,42 @@ export class AndroidDeviceDispatcher extends Dispatcher {
- return { context: BrowserContextDispatcher.from(this, await this._object.connectToWebView(params.socketName)) };
+ async connectToWebView(params: channels.AndroidDeviceConnectToWebViewParams, metadata: CallMetadata): Promise {
+ if (this.parentScope()._denyLaunch)
+ throw new Error(`Launching more browsers is not allowed.`);
+ return { context: BrowserContextDispatcher.from(this, await this._object.connectToWebView(metadata, params.socketName)) };
}
}
-export class AndroidSocketDispatcher extends Dispatcher implements channels.AndroidSocketChannel {
+class SocketSdkObject extends SdkObject implements SocketBackend {
+ private _socket: SocketBackend;
+ private _eventListeners;
+
+ constructor(parent: SdkObject, socket: SocketBackend) {
+ super(parent, 'socket');
+ this._socket = socket;
+ this._eventListeners = [
+ eventsHelper.addEventListener(socket, 'data', data => this.emit('data', data)),
+ eventsHelper.addEventListener(socket, 'close', () => {
+ eventsHelper.removeEventListeners(this._eventListeners);
+ this.emit('close');
+ }),
+ ];
+ }
+
+ async write(data: Buffer) {
+ await this._socket.write(data);
+ }
+
+ close() {
+ this._socket.close();
+ }
+}
+
+export class AndroidSocketDispatcher extends Dispatcher implements channels.AndroidSocketChannel {
_type_AndroidSocket = true;
- constructor(scope: AndroidDeviceDispatcher, socket: SocketBackend) {
+ constructor(scope: AndroidDeviceDispatcher, socket: SocketSdkObject) {
super(scope, socket, 'AndroidSocket', {});
this.addObjectListener('data', (data: Buffer) => this._dispatchEvent('data', { data }));
this.addObjectListener('close', () => {
diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts
index 033efda435e5d..9609d62a26730 100644
--- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts
@@ -245,7 +245,7 @@ export class BrowserContextDispatcher extends Dispatcher {
- return { page: PageDispatcher.from(this, await this._context.newPage(metadata)) };
+ return { page: PageDispatcher.from(this, await this._context.newPageFromMetadata(metadata)) };
}
async cookies(params: channels.BrowserContextCookiesParams): Promise {
diff --git a/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts
index 186ec73faa03c..448440069a939 100644
--- a/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts
@@ -60,14 +60,14 @@ export class BrowserDispatcher extends Dispatcher {
if (!this._options.isolateContexts) {
- const context = await this._object.newContext(metadata, params);
+ const context = await this._object.newContextFromMetadata(metadata, params);
const contextDispatcher = BrowserContextDispatcher.from(this, context);
return { context: contextDispatcher };
}
if (params.recordVideo)
params.recordVideo.dir = this._object.options.artifactsDir;
- const context = await this._object.newContext(metadata, params);
+ const context = await this._object.newContextFromMetadata(metadata, params);
this._isolatedContexts.add(context);
context.on(BrowserContext.Events.Close, () => this._isolatedContexts.delete(context));
const contextDispatcher = BrowserContextDispatcher.from(this, context);
diff --git a/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts
index e97b7cdc757d0..3b71e04072ac7 100644
--- a/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts
@@ -25,19 +25,27 @@ import type * as channels from '@protocol/channels';
export class BrowserTypeDispatcher extends Dispatcher implements channels.BrowserTypeChannel {
_type_BrowserType = true;
- constructor(scope: RootDispatcher, browserType: BrowserType) {
+ private readonly _denyLaunch: boolean;
+ constructor(scope: RootDispatcher, browserType: BrowserType, denyLaunch: boolean) {
super(scope, browserType, 'BrowserType', {
executablePath: browserType.executablePath(),
name: browserType.name()
});
+ this._denyLaunch = denyLaunch;
}
async launch(params: channels.BrowserTypeLaunchParams, metadata: CallMetadata): Promise {
+ if (this._denyLaunch)
+ throw new Error(`Launching more browsers is not allowed.`);
+
const browser = await this._object.launch(metadata, params);
return { browser: new BrowserDispatcher(this, browser) };
}
async launchPersistentContext(params: channels.BrowserTypeLaunchPersistentContextParams, metadata: CallMetadata): Promise {
+ if (this._denyLaunch)
+ throw new Error(`Launching more browsers is not allowed.`);
+
const browserContext = await this._object.launchPersistentContext(metadata, params.userDataDir, params);
const browserDispatcher = new BrowserDispatcher(this, browserContext._browser);
const contextDispatcher = BrowserContextDispatcher.from(browserDispatcher, browserContext);
@@ -45,6 +53,9 @@ export class BrowserTypeDispatcher extends Dispatcher {
+ if (this._denyLaunch)
+ throw new Error(`Launching more browsers is not allowed.`);
+
const browser = await this._object.connectOverCDP(metadata, params.endpointURL, params);
const browserDispatcher = new BrowserDispatcher(this, browser);
return {
diff --git a/packages/playwright-core/src/server/dispatchers/dispatcher.ts b/packages/playwright-core/src/server/dispatchers/dispatcher.ts
index ca1db256e9ed8..cc38a398dfc43 100644
--- a/packages/playwright-core/src/server/dispatchers/dispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/dispatcher.ts
@@ -21,7 +21,7 @@ import { ValidationError, createMetadataValidator, findValidator } from '../../
import { LongStandingScope, assert, monotonicTime, rewriteErrorMessage } from '../../utils';
import { isUnderTest } from '../utils/debug';
import { TargetClosedError, isTargetClosedError, serializeError } from '../errors';
-import { SdkObject } from '../instrumentation';
+import { createRootSdkObject, SdkObject } from '../instrumentation';
import { isProtocolError } from '../protocolError';
import { compressCallLog } from '../callLog';
import { methodMetainfo } from '../../utils/isomorphic/protocolMetainfo';
@@ -45,7 +45,7 @@ function maxDispatchersForBucket(gcBucket: string) {
}[gcBucket] ?? 10000;
}
-export class Dispatcher extends EventEmitter implements channels.Channel {
+export class Dispatcher extends EventEmitter implements channels.Channel {
readonly connection: DispatcherConnection;
// Parent is always "isScope".
private _parent: ParentScopeType | undefined;
@@ -162,13 +162,13 @@ export class Dispatcher;
+export type DispatcherScope = Dispatcher;
-export class RootDispatcher extends Dispatcher<{ guid: '' }, any, any> {
+export class RootDispatcher extends Dispatcher {
private _initialized = false;
constructor(connection: DispatcherConnection, private readonly createPlaywright?: (scope: RootDispatcher, options: channels.RootInitializeParams) => Promise) {
- super(connection, { guid: '' }, 'Root', {});
+ super(connection, createRootSdkObject(), 'Root', {});
}
async initialize(params: channels.RootInitializeParams): Promise {
@@ -311,16 +311,16 @@ export class DispatcherConnection {
validMetadata.internal = true;
}
- const sdkObject = dispatcher._object instanceof SdkObject ? dispatcher._object : undefined;
+ const sdkObject = dispatcher._object;
const callMetadata: CallMetadata = {
id: `call@${id}`,
location: validMetadata.location,
title: validMetadata.title,
internal: validMetadata.internal,
stepId: validMetadata.stepId,
- objectId: sdkObject?.guid,
- pageId: sdkObject?.attribution?.page?.guid,
- frameId: sdkObject?.attribution?.frame?.guid,
+ objectId: sdkObject.guid,
+ pageId: sdkObject.attribution?.page?.guid,
+ frameId: sdkObject.attribution?.frame?.guid,
startTime: monotonicTime(),
endTime: 0,
type: dispatcher._type,
@@ -329,7 +329,7 @@ export class DispatcherConnection {
log: [],
};
- if (sdkObject && params?.info?.waitId) {
+ if (params?.info?.waitId) {
// Process logs for waitForNavigation/waitForLoadState/etc.
const info = params.info;
switch (info.phase) {
@@ -356,7 +356,7 @@ export class DispatcherConnection {
}
}
- await sdkObject?.instrumentation.onBeforeCall(sdkObject, callMetadata);
+ await sdkObject.instrumentation.onBeforeCall(sdkObject, callMetadata);
const response: any = { id };
try {
const result = await dispatcher._handleCommand(callMetadata, method, validParams);
@@ -364,24 +364,22 @@ export class DispatcherConnection {
response.result = validator(result, '', this._validatorToWireContext());
callMetadata.result = result;
} catch (e) {
- if (isTargetClosedError(e) && sdkObject) {
+ if (isTargetClosedError(e)) {
const reason = closeReason(sdkObject);
if (reason)
rewriteErrorMessage(e, reason);
} else if (isProtocolError(e)) {
- if (e.type === 'closed') {
- const reason = sdkObject ? closeReason(sdkObject) : undefined;
- e = new TargetClosedError(reason, e.browserLogMessage());
- } else if (e.type === 'crashed') {
+ if (e.type === 'closed')
+ e = new TargetClosedError(closeReason(sdkObject), e.browserLogMessage());
+ else if (e.type === 'crashed')
rewriteErrorMessage(e, 'Target crashed ' + e.browserLogMessage());
- }
}
response.error = serializeError(e);
// The command handler could have set error in the metadata, do not reset it if there was no exception.
callMetadata.error = response.error;
} finally {
callMetadata.endTime = monotonicTime();
- await sdkObject?.instrumentation.onAfterCall(sdkObject, callMetadata);
+ await sdkObject.instrumentation.onAfterCall(sdkObject, callMetadata);
}
if (response.error)
diff --git a/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts b/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts
index 4eab4fcfc2438..8ca84caadeca0 100644
--- a/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts
@@ -24,17 +24,22 @@ import type { PageDispatcher } from './pageDispatcher';
import type { ConsoleMessage } from '../console';
import type { Electron } from '../electron/electron';
import type * as channels from '@protocol/channels';
+import type { CallMetadata } from '@protocol/callMetadata';
export class ElectronDispatcher extends Dispatcher implements channels.ElectronChannel {
_type_Electron = true;
+ _denyLaunch: boolean;
- constructor(scope: RootDispatcher, electron: Electron) {
+ constructor(scope: RootDispatcher, electron: Electron, denyLaunch: boolean) {
super(scope, electron, 'Electron', {});
+ this._denyLaunch = denyLaunch;
}
- async launch(params: channels.ElectronLaunchParams): Promise {
- const electronApplication = await this._object.launch(params);
+ async launch(params: channels.ElectronLaunchParams, metadata: CallMetadata): Promise {
+ if (this._denyLaunch)
+ throw new Error(`Launching more browsers is not allowed.`);
+ const electronApplication = await this._object.launch(metadata, params);
return { electronApplication: new ElectronApplicationDispatcher(this, electronApplication) };
}
}
diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts
index 3afab78c254f7..38c31056f2304 100644
--- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts
@@ -246,7 +246,7 @@ export class FrameDispatcher extends Dispatcher {
- return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this, await this._frame._waitForFunctionExpression(metadata, params.expression, params.isFunction, parseArgument(params.arg), params)) };
+ return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this, await this._frame.waitForFunctionExpression(metadata, params.expression, params.isFunction, parseArgument(params.arg), params)) };
}
async title(params: channels.FrameTitleParams, metadata: CallMetadata): Promise {
diff --git a/packages/playwright-core/src/server/dispatchers/jsonPipeDispatcher.ts b/packages/playwright-core/src/server/dispatchers/jsonPipeDispatcher.ts
index 75c1c89fad2fa..522df815be1b2 100644
--- a/packages/playwright-core/src/server/dispatchers/jsonPipeDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/jsonPipeDispatcher.ts
@@ -15,15 +15,15 @@
*/
import { Dispatcher } from './dispatcher';
-import { createGuid } from '../utils/crypto';
+import { SdkObject } from '../instrumentation';
import type { LocalUtilsDispatcher } from './localUtilsDispatcher';
import type * as channels from '@protocol/channels';
-export class JsonPipeDispatcher extends Dispatcher<{ guid: string }, channels.JsonPipeChannel, LocalUtilsDispatcher> implements channels.JsonPipeChannel {
+export class JsonPipeDispatcher extends Dispatcher implements channels.JsonPipeChannel {
_type_JsonPipe = true;
constructor(scope: LocalUtilsDispatcher) {
- super(scope, { guid: 'jsonPipe@' + createGuid() }, 'JsonPipe', {});
+ super(scope, new SdkObject(scope._object, 'jsonPipe'), 'JsonPipe', {});
}
async send(params: channels.JsonPipeSendParams): Promise {
diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts
index 5a3dc562df4a6..850a339564323 100644
--- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts
@@ -34,13 +34,14 @@ import type * as channels from '@protocol/channels';
import type * as http from 'http';
import type { HTTPRequestParams } from '../utils/network';
-export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel {
+export class LocalUtilsDispatcher extends Dispatcher implements channels.LocalUtilsChannel {
_type_LocalUtils: boolean;
private _harBackends = new Map();
private _stackSessions = new Map();
constructor(scope: RootDispatcher, playwright: Playwright) {
const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils');
+ localUtils.logName = 'browser';
const deviceDescriptors = Object.entries(descriptors)
.map(([name, descriptor]) => ({ name, descriptor }));
super(scope, localUtils, 'LocalUtils', {
@@ -82,8 +83,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
}
async connect(params: channels.LocalUtilsConnectParams, metadata: CallMetadata): Promise {
- const controller = new ProgressController(metadata, this._object as SdkObject);
- controller.setLogName('browser');
+ const controller = new ProgressController(metadata, this._object);
return await controller.run(async progress => {
const wsHeaders = {
'User-Agent': getUserAgent(),
@@ -137,16 +137,14 @@ async function urlToWSEndpoint(progress: Progress, endpointURL: string): Promise
if (!fetchUrl.pathname.endsWith('/'))
fetchUrl.pathname += '/';
fetchUrl.pathname += 'json';
- const json = await fetchData({
+ const json = await fetchData(progress, {
url: fetchUrl.toString(),
method: 'GET',
- timeout: progress.timeUntilDeadline(),
headers: { 'User-Agent': getUserAgent() },
}, async (params: HTTPRequestParams, response: http.IncomingMessage) => {
return new Error(`Unexpected status ${response.statusCode} when connecting to ${fetchUrl.toString()}.\n` +
`This does not look like a Playwright server, try connecting via ws://.`);
});
- progress.throwIfAborted();
const wsUrl = new URL(endpointURL);
let wsEndpointPath = JSON.parse(json).wsEndpointPath;
diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts
index 365a3239977fb..4d633c7f2cfa3 100644
--- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts
@@ -25,7 +25,7 @@ import { RequestDispatcher } from './networkDispatchers';
import { ResponseDispatcher } from './networkDispatchers';
import { RouteDispatcher, WebSocketDispatcher } from './networkDispatchers';
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
-import { createGuid } from '../utils/crypto';
+import { SdkObject } from '../instrumentation';
import { urlMatches } from '../../utils/isomorphic/urlMatch';
import type { Artifact } from '../artifact';
@@ -238,7 +238,7 @@ export class PageDispatcher extends Dispatcher {
if (!params.runBeforeUnload)
metadata.potentiallyClosesScope = true;
- await this._page.close(metadata, params);
+ await this._page.close(params);
}
async updateSubscription(params: channels.PageUpdateSubscriptionParams): Promise {
@@ -251,47 +251,52 @@ export class PageDispatcher extends Dispatcher {
- await this._page.keyboard.down(params.key);
+ await this._page.keyboard.down(metadata, params.key);
}
async keyboardUp(params: channels.PageKeyboardUpParams, metadata: CallMetadata): Promise {
- await this._page.keyboard.up(params.key);
+ await this._page.keyboard.up(metadata, params.key);
}
async keyboardInsertText(params: channels.PageKeyboardInsertTextParams, metadata: CallMetadata): Promise {
- await this._page.keyboard.insertText(params.text);
+ await this._page.keyboard.insertText(metadata, params.text);
}
async keyboardType(params: channels.PageKeyboardTypeParams, metadata: CallMetadata): Promise {
- await this._page.keyboard.type(params.text, params);
+ await this._page.keyboard.type(metadata, params.text, params);
}
async keyboardPress(params: channels.PageKeyboardPressParams, metadata: CallMetadata): Promise {
- await this._page.keyboard.press(params.key, params);
+ await this._page.keyboard.press(metadata, params.key, params);
}
async mouseMove(params: channels.PageMouseMoveParams, metadata: CallMetadata): Promise {
- await this._page.mouse.move(params.x, params.y, params, metadata);
+ metadata.point = { x: params.x, y: params.y };
+ await this._page.mouse.move(metadata, params.x, params.y, params);
}
async mouseDown(params: channels.PageMouseDownParams, metadata: CallMetadata): Promise {
- await this._page.mouse.down(params, metadata);
+ metadata.point = this._page.mouse.currentPoint();
+ await this._page.mouse.down(metadata, params);
}
async mouseUp(params: channels.PageMouseUpParams, metadata: CallMetadata): Promise {
- await this._page.mouse.up(params, metadata);
+ metadata.point = this._page.mouse.currentPoint();
+ await this._page.mouse.up(metadata, params);
}
async mouseClick(params: channels.PageMouseClickParams, metadata: CallMetadata): Promise {
- await this._page.mouse.click(params.x, params.y, params, metadata);
+ metadata.point = { x: params.x, y: params.y };
+ await this._page.mouse.click(metadata, params.x, params.y, params);
}
async mouseWheel(params: channels.PageMouseWheelParams, metadata: CallMetadata): Promise {
- await this._page.mouse.wheel(params.deltaX, params.deltaY);
+ await this._page.mouse.wheel(metadata, params.deltaX, params.deltaY);
}
async touchscreenTap(params: channels.PageTouchscreenTapParams, metadata: CallMetadata): Promise {
- await this._page.touchscreen.tap(params.x, params.y, metadata);
+ metadata.point = { x: params.x, y: params.y };
+ await this._page.touchscreen.tap(metadata, params.x, params.y);
}
async accessibilitySnapshot(params: channels.PageAccessibilitySnapshotParams, metadata: CallMetadata): Promise {
@@ -403,7 +408,7 @@ export class WorkerDispatcher extends Dispatcher implements channels.BindingCallChannel {
+export class BindingCallDispatcher extends Dispatcher implements channels.BindingCallChannel {
_type_BindingCall = true;
private _resolve: ((arg: any) => void) | undefined;
private _reject: ((error: any) => void) | undefined;
@@ -411,7 +416,7 @@ export class BindingCallDispatcher extends Dispatcher<{ guid: string }, channels
constructor(scope: PageDispatcher, name: string, needsHandle: boolean, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) {
const frameDispatcher = FrameDispatcher.from(scope.parentScope(), source.frame);
- super(scope, { guid: 'bindingCall@' + createGuid() }, 'BindingCall', {
+ super(scope, new SdkObject(scope._object, 'bindingCall'), 'BindingCall', {
frame: frameDispatcher,
name,
args: needsHandle ? undefined : args.map(serializeResult),
diff --git a/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts b/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts
index f87f1ca85b471..a80aa49456299 100644
--- a/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts
@@ -24,7 +24,7 @@ import { Dispatcher } from './dispatcher';
import { ElectronDispatcher } from './electronDispatcher';
import { LocalUtilsDispatcher } from './localUtilsDispatcher';
import { APIRequestContextDispatcher } from './networkDispatchers';
-import { createGuid } from '../utils/crypto';
+import { SdkObject } from '../instrumentation';
import { eventsHelper } from '../utils/eventsHelper';
import type { RootDispatcher } from './dispatcher';
@@ -35,8 +35,9 @@ import type { Browser } from '../browser';
import type { Playwright } from '../playwright';
import type * as channels from '@protocol/channels';
-type PlaywrightDispatcherOptions = {
+export type PlaywrightDispatcherOptions = {
socksProxy?: SocksProxy;
+ denyLaunch?: boolean;
preLaunchedBrowser?: Browser;
preLaunchedAndroidDevice?: AndroidDevice;
sharedBrowser?: boolean;
@@ -47,34 +48,28 @@ export class PlaywrightDispatcher extends Dispatcher implements channels.SocksSupportChannel {
+class SocksSupportDispatcher extends Dispatcher implements channels.SocksSupportChannel {
_type_SocksSupport: boolean;
private _socksProxy: SocksProxy;
private _socksListeners: RegisteredListener[];
- constructor(scope: RootDispatcher, socksProxy: SocksProxy) {
- super(scope, { guid: 'socksSupport@' + createGuid() }, 'SocksSupport', {});
+ constructor(scope: RootDispatcher, parent: SdkObject, socksProxy: SocksProxy) {
+ super(scope, new SdkObject(parent, 'socksSupport'), 'SocksSupport', {});
this._type_SocksSupport = true;
this._socksProxy = socksProxy;
this._socksListeners = [
diff --git a/packages/playwright-core/src/server/dispatchers/streamDispatcher.ts b/packages/playwright-core/src/server/dispatchers/streamDispatcher.ts
index f3a0a413b7e77..82b5319c5983d 100644
--- a/packages/playwright-core/src/server/dispatchers/streamDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/streamDispatcher.ts
@@ -16,18 +16,27 @@
import { Dispatcher } from './dispatcher';
import { ManualPromise } from '../../utils/isomorphic/manualPromise';
-import { createGuid } from '../utils/crypto';
+import { SdkObject } from '../instrumentation';
import type { ArtifactDispatcher } from './artifactDispatcher';
import type * as channels from '@protocol/channels';
import type * as stream from 'stream';
-export class StreamDispatcher extends Dispatcher<{ guid: string, stream: stream.Readable }, channels.StreamChannel, ArtifactDispatcher> implements channels.StreamChannel {
+class StreamSdkObject extends SdkObject {
+ readonly stream: stream.Readable;
+
+ constructor(parent: SdkObject, stream: stream.Readable) {
+ super(parent, 'stream');
+ this.stream = stream;
+ }
+}
+
+export class StreamDispatcher extends Dispatcher implements channels.StreamChannel {
_type_Stream = true;
private _ended: boolean = false;
constructor(scope: ArtifactDispatcher, stream: stream.Readable) {
- super(scope, { guid: 'stream@' + createGuid(), stream }, 'Stream', {});
+ super(scope, new StreamSdkObject(scope._object, stream), 'Stream', {});
// In Node v12.9.0+ we can use readableEnded.
stream.once('end', () => this._ended = true);
stream.once('error', () => this._ended = true);
diff --git a/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts b/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts
index 9bb05849ae9a8..975f2d2082eff 100644
--- a/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts
@@ -18,7 +18,7 @@ import { Page } from '../page';
import { Dispatcher } from './dispatcher';
import { PageDispatcher } from './pageDispatcher';
import * as rawWebSocketMockSource from '../../generated/webSocketMockSource';
-import { createGuid } from '../utils/crypto';
+import { SdkObject } from '../instrumentation';
import { urlMatches } from '../../utils/isomorphic/urlMatch';
import { eventsHelper } from '../utils/eventsHelper';
@@ -29,14 +29,14 @@ import type { Frame } from '../frames';
import type * as ws from '@injected/webSocketMock';
import type * as channels from '@protocol/channels';
-export class WebSocketRouteDispatcher extends Dispatcher<{ guid: string }, channels.WebSocketRouteChannel, PageDispatcher | BrowserContextDispatcher> implements channels.WebSocketRouteChannel {
+export class WebSocketRouteDispatcher extends Dispatcher implements channels.WebSocketRouteChannel {
_type_WebSocketRoute = true;
private _id: string;
private _frame: Frame;
private static _idToDispatcher = new Map();
constructor(scope: PageDispatcher | BrowserContextDispatcher, id: string, url: string, frame: Frame) {
- super(scope, { guid: 'webSocketRoute@' + createGuid() }, 'WebSocketRoute', { url });
+ super(scope, new SdkObject(scope._object, 'webSocketRoute'), 'WebSocketRoute', { url });
this._id = id;
this._frame = frame;
this._eventListeners.push(
diff --git a/packages/playwright-core/src/server/dispatchers/writableStreamDispatcher.ts b/packages/playwright-core/src/server/dispatchers/writableStreamDispatcher.ts
index 7bd6f1fd745ce..1595cae757854 100644
--- a/packages/playwright-core/src/server/dispatchers/writableStreamDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/writableStreamDispatcher.ts
@@ -17,18 +17,27 @@
import fs from 'fs';
import { Dispatcher } from './dispatcher';
-import { createGuid } from '../utils/crypto';
+import { SdkObject } from '../instrumentation';
import type { BrowserContextDispatcher } from './browserContextDispatcher';
import type * as channels from '@protocol/channels';
-export class WritableStreamDispatcher extends Dispatcher<{ guid: string, streamOrDirectory: fs.WriteStream | string }, channels.WritableStreamChannel, BrowserContextDispatcher> implements channels.WritableStreamChannel {
+class WritableStreamSdkObject extends SdkObject {
+ readonly streamOrDirectory: fs.WriteStream | string;
+ readonly lastModifiedMs: number | undefined;
+
+ constructor(parent: SdkObject, streamOrDirectory: fs.WriteStream | string, lastModifiedMs: number | undefined) {
+ super(parent, 'stream');
+ this.streamOrDirectory = streamOrDirectory;
+ this.lastModifiedMs = lastModifiedMs;
+ }
+}
+
+export class WritableStreamDispatcher extends Dispatcher implements channels.WritableStreamChannel {
_type_WritableStream = true;
- private _lastModifiedMs: number | undefined;
constructor(scope: BrowserContextDispatcher, streamOrDirectory: fs.WriteStream | string, lastModifiedMs?: number) {
- super(scope, { guid: 'writableStream@' + createGuid(), streamOrDirectory }, 'WritableStream', {});
- this._lastModifiedMs = lastModifiedMs;
+ super(scope, new WritableStreamSdkObject(scope._object, streamOrDirectory, lastModifiedMs), 'WritableStream', {});
}
async write(params: channels.WritableStreamWriteParams): Promise {
@@ -50,8 +59,8 @@ export class WritableStreamDispatcher extends Dispatcher<{ guid: string, streamO
throw new Error('Cannot close a directory');
const stream = this._object.streamOrDirectory;
await new Promise(fulfill => stream.end(fulfill));
- if (this._lastModifiedMs)
- await fs.promises.utimes(this.path(), new Date(this._lastModifiedMs), new Date(this._lastModifiedMs));
+ if (this._object.lastModifiedMs)
+ await fs.promises.utimes(this.path(), new Date(this._object.lastModifiedMs), new Date(this._object.lastModifiedMs));
}
path(): string {
diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts
index ed7cb37cef660..f1f234864dfe9 100644
--- a/packages/playwright-core/src/server/dom.ts
+++ b/packages/playwright-core/src/server/dom.ts
@@ -20,11 +20,10 @@ import * as js from './javascript';
import { ProgressController } from './progress';
import { asLocator, isUnderTest } from '../utils';
import { prepareFilesForUpload } from './fileUploadUtils';
-import { isSessionClosedError } from './protocolError';
import * as rawInjectedScriptSource from '../generated/injectedScriptSource';
import type * as frames from './frames';
-import type { ElementState, HitTargetError, HitTargetInterceptionResult, InjectedScript, InjectedScriptOptions } from '@injected/injectedScript';
+import type { ElementState, HitTargetInterceptionResult, InjectedScript, InjectedScriptOptions } from '@injected/injectedScript';
import type { CallMetadata } from './instrumentation';
import type { Page } from './page';
import type { Progress } from './progress';
@@ -39,7 +38,7 @@ export type InputFilesItems = {
};
type ActionName = 'click' | 'hover' | 'dblclick' | 'tap' | 'move and up' | 'move and down';
-type PerformActionResult = 'error:notvisible' | 'error:notconnected' | 'error:notinviewport' | 'error:optionsnotfound' | { missingState: ElementState } | HitTargetError | 'done';
+type PerformActionResult = 'error:notvisible' | 'error:notconnected' | 'error:notinviewport' | 'error:optionsnotfound' | { missingState: ElementState } | { hitTargetDescription: string } | 'done';
export class NonRecoverableDOMError extends Error {
}
@@ -87,14 +86,13 @@ export class FrameExecutionContext extends js.ExecutionContext {
const selectorsRegistry = this.frame._page.browserContext.selectors();
for (const [name, { source }] of selectorsRegistry._engines)
customEngines.push({ name, source: `(${source})` });
- const sdkLanguage = this.frame.attribution.playwright.options.sdkLanguage;
+ const sdkLanguage = this.frame._page.browserContext._browser.sdkLanguage();
const options: InjectedScriptOptions = {
isUnderTest: isUnderTest(),
sdkLanguage,
testIdAttributeName: selectorsRegistry.testIdAttributeName(),
stableRafCount: this.frame._page.delegate.rafCountForStablePosition(),
browserName: this.frame._page.browserContext._browser.options.name,
- inputFileRoleTextbox: process.env.PLAYWRIGHT_INPUT_FILE_TEXTBOX ? true : false,
customEngines,
};
const source = `
@@ -142,7 +140,7 @@ export class ElementHandle extends js.JSHandle {
const utility = await this._frame._utilityContext();
return await utility.evaluate(pageFunction, [await utility.injectedScript(), this, arg]);
} catch (e) {
- if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e))
+ if (this._frame.isNonRetriableError(e))
throw e;
return 'error:notconnected';
}
@@ -153,7 +151,7 @@ export class ElementHandle extends js.JSHandle {
const utility = await this._frame._utilityContext();
return await utility.evaluateHandle(pageFunction, [await utility.injectedScript(), this, arg]);
} catch (e) {
- if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e))
+ if (this._frame.isNonRetriableError(e))
throw e;
return 'error:notconnected';
}
@@ -241,19 +239,19 @@ export class ElementHandle extends js.JSHandle {
return this._frame.dispatchEvent(metadata, ':scope', type, eventInit, { timeout: 0 }, this);
}
- async _scrollRectIntoViewIfNeeded(rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'> {
- return await this._page.delegate.scrollRectIntoViewIfNeeded(this, rect);
+ async _scrollRectIntoViewIfNeeded(progress: Progress, rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'> {
+ return await progress.race(this._page.delegate.scrollRectIntoViewIfNeeded(this, rect));
}
async _waitAndScrollIntoViewIfNeeded(progress: Progress, waitForVisible: boolean): Promise {
const result = await this._retryAction(progress, 'scroll into view', async () => {
progress.log(` waiting for element to be stable`);
- const waitResult = await this.evaluateInUtility(async ([injected, node, { waitForVisible }]) => {
+ const waitResult = await progress.race(this.evaluateInUtility(async ([injected, node, { waitForVisible }]) => {
return await injected.checkElementStates(node, waitForVisible ? ['visible', 'stable'] : ['stable']);
- }, { waitForVisible });
+ }, { waitForVisible }));
if (waitResult)
return waitResult;
- return await this._scrollRectIntoViewIfNeeded();
+ return await this._scrollRectIntoViewIfNeeded(progress);
}, {});
assertDone(throwRetargetableDOMError(result));
}
@@ -332,18 +330,18 @@ export class ElementHandle extends js.JSHandle {
};
}
- async _retryAction(progress: Progress, actionName: string, action: () => Promise, options: { trial?: boolean, force?: boolean, skipActionPreChecks?: boolean }): Promise<'error:notconnected' | 'done'> {
+ async _retryAction(progress: Progress, actionName: string, action: (retry: number) => Promise, options: { trial?: boolean, force?: boolean, skipActionPreChecks?: boolean }): Promise<'error:notconnected' | 'done'> {
let retry = 0;
// We progressively wait longer between retries, up to 500ms.
const waitTime = [0, 20, 100, 100, 500];
- while (progress.isRunning()) {
+ while (true) {
if (retry) {
progress.log(`retrying ${actionName} action${options.trial ? ' (trial run)' : ''}`);
const timeout = waitTime[Math.min(retry - 1, waitTime.length - 1)];
if (timeout) {
progress.log(` waiting ${timeout}ms`);
- const result = await this.evaluateInUtility(([injected, node, timeout]) => new Promise(f => setTimeout(f, timeout)), timeout);
+ const result = await progress.race(this.evaluateInUtility(([injected, node, timeout]) => new Promise(f => setTimeout(f, timeout)), timeout));
if (result === 'error:notconnected')
return result;
}
@@ -352,7 +350,7 @@ export class ElementHandle extends js.JSHandle {
}
if (!options.skipActionPreChecks && !options.force)
await this._frame._page.performActionPreChecks(progress);
- const result = await action();
+ const result = await action(retry);
++retry;
if (result === 'error:notvisible') {
if (options.force)
@@ -380,32 +378,25 @@ export class ElementHandle extends js.JSHandle {
}
return result;
}
- return 'done';
}
async _retryPointerAction(progress: Progress, actionName: ActionName, waitForEnabled: boolean, action: (point: types.Point) => Promise,
- options: { waitAfter: boolean | 'disabled' } & types.PointerActionOptions & types.PointerActionWaitOptions): Promise<'error:notconnected' | 'done'> {
+ options: Omit<{ waitAfter: boolean | 'disabled' } & types.PointerActionOptions & types.PointerActionWaitOptions, 'timeout'>): Promise<'error:notconnected' | 'done'> {
// Note: do not perform locator handlers checkpoint to avoid moving the mouse in the middle of a drag operation.
const skipActionPreChecks = actionName === 'move and up';
- // By default, we scroll with protocol method to reveal the action point.
- // However, that might not work to scroll from under position:sticky and position:fixed elements
- // that overlay the target element. To fight this, we cycle through different
- // scroll alignments. This works in most scenarios.
- const scrollOptions: (ScrollIntoViewOptions | undefined)[] = [
- undefined,
- { block: 'end', inline: 'end' },
- { block: 'center', inline: 'center' },
- { block: 'start', inline: 'start' },
- ];
- let scrollOptionIndex = 0;
- return await this._retryAction(progress, actionName, async () => {
- const forceScrollOptions = scrollOptions[scrollOptionIndex % scrollOptions.length];
- const result = await this._performPointerAction(progress, actionName, waitForEnabled, action, forceScrollOptions, options);
- if (typeof result === 'object' && 'hasPositionStickyOrFixed' in result && result.hasPositionStickyOrFixed)
- ++scrollOptionIndex;
- else
- scrollOptionIndex = 0;
- return result;
+ return await this._retryAction(progress, actionName, async retry => {
+ // By default, we scroll with protocol method to reveal the action point.
+ // However, that might not work to scroll from under position:sticky elements
+ // that overlay the target element. To fight this, we cycle through different
+ // scroll alignments. This works in most scenarios.
+ const scrollOptions: (ScrollIntoViewOptions | undefined)[] = [
+ undefined,
+ { block: 'end', inline: 'end' },
+ { block: 'center', inline: 'center' },
+ { block: 'start', inline: 'start' },
+ ];
+ const forceScrollOptions = scrollOptions[retry % scrollOptions.length];
+ return await this._performPointerAction(progress, actionName, waitForEnabled, action, forceScrollOptions, options);
}, { ...options, skipActionPreChecks });
}
@@ -415,7 +406,7 @@ export class ElementHandle extends js.JSHandle {
waitForEnabled: boolean,
action: (point: types.Point) => Promise,
forceScrollOptions: ScrollIntoViewOptions | undefined,
- options: { waitAfter: boolean | 'disabled' } & types.PointerActionOptions & types.PointerActionWaitOptions,
+ options: Omit<{ waitAfter: boolean | 'disabled' } & types.PointerActionOptions & types.PointerActionWaitOptions, 'timeout'>,
): Promise {
const { force = false, position } = options;
@@ -427,68 +418,66 @@ export class ElementHandle extends js.JSHandle {
return 'done' as const;
}, forceScrollOptions);
}
- return await this._scrollRectIntoViewIfNeeded(position ? { x: position.x, y: position.y, width: 0, height: 0 } : undefined);
+ return await this._scrollRectIntoViewIfNeeded(progress, position ? { x: position.x, y: position.y, width: 0, height: 0 } : undefined);
};
if (this._frame.parentFrame()) {
// Best-effort scroll to make sure any iframes containing this element are scrolled
// into view and visible, so they are not throttled.
// See https://github.com/microsoft/playwright/issues/27196 for an example.
- progress.throwIfAborted(); // Avoid action that has side-effects.
- await doScrollIntoView().catch(() => {});
+ await progress.race(doScrollIntoView().catch(() => {}));
}
if ((options as any).__testHookBeforeStable)
- await (options as any).__testHookBeforeStable();
+ await progress.race((options as any).__testHookBeforeStable());
if (!force) {
const elementStates: ElementState[] = waitForEnabled ? ['visible', 'enabled', 'stable'] : ['visible', 'stable'];
progress.log(` waiting for element to be ${waitForEnabled ? 'visible, enabled and stable' : 'visible and stable'}`);
- const result = await this.evaluateInUtility(async ([injected, node, { elementStates }]) => {
+ const result = await progress.race(this.evaluateInUtility(async ([injected, node, { elementStates }]) => {
return await injected.checkElementStates(node, elementStates);
- }, { elementStates });
+ }, { elementStates }));
if (result)
return result;
progress.log(` element is ${waitForEnabled ? 'visible, enabled and stable' : 'visible and stable'}`);
}
if ((options as any).__testHookAfterStable)
- await (options as any).__testHookAfterStable();
+ await progress.race((options as any).__testHookAfterStable());
progress.log(' scrolling into view if needed');
- progress.throwIfAborted(); // Avoid action that has side-effects.
- const scrolled = await doScrollIntoView();
+ const scrolled = await progress.race(doScrollIntoView());
if (scrolled !== 'done')
return scrolled;
progress.log(' done scrolling');
- const maybePoint = position ? await this._offsetPoint(position) : await this._clickablePoint();
+ const maybePoint = position ? await progress.race(this._offsetPoint(position)) : await progress.race(this._clickablePoint());
if (typeof maybePoint === 'string')
return maybePoint;
const point = roundPoint(maybePoint);
progress.metadata.point = point;
- await this.instrumentation.onBeforeInputAction(this, progress.metadata);
+ await progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata));
let hitTargetInterceptionHandle: js.JSHandle | undefined;
if (force) {
progress.log(` forcing action`);
} else {
if ((options as any).__testHookBeforeHitTarget)
- await (options as any).__testHookBeforeHitTarget();
+ await progress.race((options as any).__testHookBeforeHitTarget());
- const frameCheckResult = await this._checkFrameIsHitTarget(point);
+ const frameCheckResult = await progress.race(this._checkFrameIsHitTarget(point));
if (frameCheckResult === 'error:notconnected' || ('hitTargetDescription' in frameCheckResult))
return frameCheckResult;
const hitPoint = frameCheckResult.framePoint;
const actionType = actionName === 'move and up' ? 'drag' : ((actionName === 'hover' || actionName === 'tap') ? actionName : 'mouse');
- const handle = await this.evaluateHandleInUtility(([injected, node, { actionType, hitPoint, trial }]) => injected.setupHitTargetInterceptor(node, actionType, hitPoint, trial), { actionType, hitPoint, trial: !!options.trial } as const);
+ const handle = await progress.race(this.evaluateHandleInUtility(([injected, node, { actionType, hitPoint, trial }]) => injected.setupHitTargetInterceptor(node, actionType, hitPoint, trial), { actionType, hitPoint, trial: !!options.trial } as const));
if (handle === 'error:notconnected')
return handle;
if (!handle._objectId) {
const error = handle.rawValue() as string;
if (error === 'error:notconnected')
return error;
- return JSON.parse(error) as HitTargetError; // It is safe to parse, because we evaluated in utility.
+ return { hitTargetDescription: error };
}
hitTargetInterceptionHandle = handle as any;
progress.cleanupWhenAborted(() => {
@@ -501,15 +490,14 @@ export class ElementHandle extends js.JSHandle {
const actionResult = await this._page.frameManager.waitForSignalsCreatedBy(progress, options.waitAfter === true, async () => {
if ((options as any).__testHookBeforePointerAction)
- await (options as any).__testHookBeforePointerAction();
- progress.throwIfAborted(); // Avoid action that has side-effects.
+ await progress.race((options as any).__testHookBeforePointerAction());
let restoreModifiers: types.KeyboardModifier[] | undefined;
if (options && options.modifiers)
- restoreModifiers = await this._page.keyboard.ensureModifiers(options.modifiers);
+ restoreModifiers = await this._page.keyboard.ensureModifiers(progress, options.modifiers);
progress.log(` performing ${actionName} action`);
await action(point);
if (restoreModifiers)
- await this._page.keyboard.ensureModifiers(restoreModifiers);
+ await this._page.keyboard.ensureModifiers(progress, restoreModifiers);
if (hitTargetInterceptionHandle) {
const stopHitTargetInterception = this._frame.raceAgainstEvaluationStallingEvents(() => {
return hitTargetInterceptionHandle.evaluate(h => h.stop());
@@ -519,7 +507,7 @@ export class ElementHandle extends js.JSHandle {
if (options.waitAfter !== false) {
// When noWaitAfter is passed, we do not want to accidentally stall on
// non-committed navigation blocking the evaluate.
- const hitTargetResult = await stopHitTargetInterception;
+ const hitTargetResult = await progress.race(stopHitTargetInterception);
if (hitTargetResult !== 'done')
return hitTargetResult;
}
@@ -527,7 +515,7 @@ export class ElementHandle extends js.JSHandle {
progress.log(` ${options.trial ? 'trial ' : ''}${actionName} action done`);
progress.log(' waiting for scheduled navigations to finish');
if ((options as any).__testHookAfterPointerAction)
- await (options as any).__testHookAfterPointerAction();
+ await progress.race((options as any).__testHookAfterPointerAction());
return 'done';
});
if (actionResult !== 'done')
@@ -536,71 +524,71 @@ export class ElementHandle extends js.JSHandle {
return 'done';
}
- private async _markAsTargetElement(metadata: CallMetadata) {
- if (!metadata.id)
+ private async _markAsTargetElement(progress: Progress) {
+ if (!progress.metadata.id)
return;
- await this.evaluateInUtility(([injected, node, callId]) => {
+ await progress.race(this.evaluateInUtility(([injected, node, callId]) => {
if (node.nodeType === 1 /* Node.ELEMENT_NODE */)
injected.markTargetElements(new Set([node as Node as Element]), callId);
- }, metadata.id);
+ }, progress.metadata.id));
}
async hover(metadata: CallMetadata, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
- await this._markAsTargetElement(metadata);
+ await this._markAsTargetElement(progress);
const result = await this._hover(progress, options);
return assertDone(throwRetargetableDOMError(result));
}, options.timeout);
}
_hover(progress: Progress, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise<'error:notconnected' | 'done'> {
- return this._retryPointerAction(progress, 'hover', false /* waitForEnabled */, point => this._page.mouse.move(point.x, point.y), { ...options, waitAfter: 'disabled' });
+ return this._retryPointerAction(progress, 'hover', false /* waitForEnabled */, point => this._page.mouse._move(progress, point.x, point.y), { ...options, waitAfter: 'disabled' });
}
async click(metadata: CallMetadata, options: { noWaitAfter?: boolean } & types.MouseClickOptions & types.PointerActionWaitOptions): Promise {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
- await this._markAsTargetElement(metadata);
+ await this._markAsTargetElement(progress);
const result = await this._click(progress, { ...options, waitAfter: !options.noWaitAfter });
return assertDone(throwRetargetableDOMError(result));
}, options.timeout);
}
_click(progress: Progress, options: { waitAfter: boolean | 'disabled' } & types.MouseClickOptions & types.PointerActionWaitOptions): Promise<'error:notconnected' | 'done'> {
- return this._retryPointerAction(progress, 'click', true /* waitForEnabled */, point => this._page.mouse.click(point.x, point.y, options), options);
+ return this._retryPointerAction(progress, 'click', true /* waitForEnabled */, point => this._page.mouse._click(progress, point.x, point.y, options), options);
}
async dblclick(metadata: CallMetadata, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions): Promise {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
- await this._markAsTargetElement(metadata);
+ await this._markAsTargetElement(progress);
const result = await this._dblclick(progress, options);
return assertDone(throwRetargetableDOMError(result));
}, options.timeout);
}
_dblclick(progress: Progress, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions): Promise<'error:notconnected' | 'done'> {
- return this._retryPointerAction(progress, 'dblclick', true /* waitForEnabled */, point => this._page.mouse.dblclick(point.x, point.y, options), { ...options, waitAfter: 'disabled' });
+ return this._retryPointerAction(progress, 'dblclick', true /* waitForEnabled */, point => this._page.mouse._click(progress, point.x, point.y, { ...options, clickCount: 2 }), { ...options, waitAfter: 'disabled' });
}
async tap(metadata: CallMetadata, options: types.PointerActionWaitOptions): Promise {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
- await this._markAsTargetElement(metadata);
+ await this._markAsTargetElement(progress);
const result = await this._tap(progress, options);
return assertDone(throwRetargetableDOMError(result));
}, options.timeout);
}
_tap(progress: Progress, options: types.PointerActionWaitOptions): Promise<'error:notconnected' | 'done'> {
- return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, point => this._page.touchscreen.tap(point.x, point.y), { ...options, waitAfter: 'disabled' });
+ return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, point => this._page.touchscreen._tap(progress, point.x, point.y), { ...options, waitAfter: 'disabled' });
}
async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
- await this._markAsTargetElement(metadata);
+ await this._markAsTargetElement(progress);
const result = await this._selectOption(progress, elements, values, options);
return throwRetargetableDOMError(result);
}, options.timeout);
@@ -609,18 +597,18 @@ export class ElementHandle extends js.JSHandle {
async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise {
let resultingOptions: string[] = [];
const result = await this._retryAction(progress, 'select option', async () => {
- await this.instrumentation.onBeforeInputAction(this, progress.metadata);
+ await progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata));
if (!options.force)
progress.log(` waiting for element to be visible and enabled`);
const optionsToSelect = [...elements, ...values];
- const result = await this.evaluateInUtility(async ([injected, node, { optionsToSelect, force }]) => {
+ const result = await progress.race(this.evaluateInUtility(async ([injected, node, { optionsToSelect, force }]) => {
if (!force) {
const checkResult = await injected.checkElementStates(node, ['visible', 'enabled']);
if (checkResult)
return checkResult;
}
return injected.selectOptions(node, optionsToSelect);
- }, { optionsToSelect, force: options.force });
+ }, { optionsToSelect, force: options.force }));
if (Array.isArray(result)) {
progress.log(' selected specified option(s)');
resultingOptions = result;
@@ -636,7 +624,7 @@ export class ElementHandle extends js.JSHandle {
async fill(metadata: CallMetadata, value: string, options: types.CommonActionOptions): Promise {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
- await this._markAsTargetElement(metadata);
+ await this._markAsTargetElement(progress);
const result = await this._fill(progress, value, options);
assertDone(throwRetargetableDOMError(result));
}, options.timeout);
@@ -645,23 +633,22 @@ export class ElementHandle extends js.JSHandle