ohos-playwright 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -7
- package/dist/config.mjs +14 -2
- package/dist/fixture.d.mts +5 -0
- package/dist/fixture.mjs +226 -217
- package/dist/loader.d.mts +2 -15
- package/dist/loader.mjs +2 -3
- package/dist/parallel.d.mts +17 -0
- package/dist/parallel.mjs +48 -0
- package/dist/register.mjs +17 -2
- package/dist/setup.d.mts +0 -1
- package/dist/setup.mjs +24 -25
- package/dist/teardown.mjs +6 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -69,15 +69,15 @@ The following Playwright APIs have been validated on ArkWeb / HarmonyOS 6.1 (Chr
|
|
|
69
69
|
| WebSocket | `page.routeWebSocket()` (requires Playwright ≥ 1.48) — intercepts WebSocket connections |
|
|
70
70
|
| Accessibility (CDP) | `newCDPSession` + `Accessibility.getFullAXTree` — returns the full AX node tree |
|
|
71
71
|
| Navigation history | `page.goBack()`, `page.goForward()` — implemented via `history.back/forward()` + CDP polling; returns when the history index changes |
|
|
72
|
-
| Hover events | `locator.hover()` — fires `mouseover` / `mouseenter` event listeners
|
|
72
|
+
| Hover events | `locator.hover()` — fires `mouseover` / `mouseenter` event listeners **and** activates the CSS `:hover` pseudo-class via the real `Input.dispatchMouseEvent` path. Falls back to JS-only dispatch (DOM events without `:hover`) if Playwright's `boundingBox()` hangs for more than 5 s. |
|
|
73
73
|
| Locale (partial) | `emulateLocale(tag)` fixture — rewrites `navigator.language` / `navigator.languages` via `addInitScript`; does not affect HTTP `Accept-Language` or browser UI locale |
|
|
74
|
-
| User-Agent
|
|
74
|
+
| User-Agent | `emulateDevice({ userAgent })` — overrides both `navigator.userAgent` and the outgoing HTTP `User-Agent` header; call before `page.goto()` for the override to take effect. (`context.setExtraHTTPHeaders({ 'User-Agent': ... })` does **not** override UA — ArkWeb preserves the browser default there.) |
|
|
75
75
|
| Service Workers | `navigator.serviceWorker.register()` — works on HTTPS pages; `navigator.serviceWorker` is `undefined` on non-secure origins (`data:`, `about:blank`) as in all browsers |
|
|
76
76
|
| Clipboard | `navigator.clipboard.writeText()` / `readText()` — works on HTTPS pages after `context.grantPermissions(['clipboard-read', 'clipboard-write'])`; unavailable on non-secure origins |
|
|
77
77
|
|
|
78
78
|
### `emulateDevice` fixture
|
|
79
79
|
|
|
80
|
-
Because `newContext()` is not
|
|
80
|
+
Because `newContext()` is not available in default single-context mode (see "Multi-context / multi-page" in Limitations), device emulation is exposed as a Playwright fixture parameter backed by CDP `Emulation.*` commands.
|
|
81
81
|
|
|
82
82
|
```ts
|
|
83
83
|
import { test, expect } from '@playwright/test'
|
|
@@ -99,7 +99,7 @@ test('precise viewport', async ({ page, emulateDevice }) => {
|
|
|
99
99
|
> **⚠️ `isMobile: true` does not produce a precise viewport on ArkWeb.**
|
|
100
100
|
> When `Emulation.setDeviceMetricsOverride` is called with `mobile: true`, ArkWeb enables its mobile layout-viewport compatibility path and renders at the 980px default mobile layout viewport — the passed `width`/`height` are effectively ignored (`window.innerWidth` reads 980 regardless). `deviceScaleFactor` has no effect on this. Use `isMobile: false` when you need an exact pixel viewport.
|
|
101
101
|
>
|
|
102
|
-
> **`userAgent` override
|
|
102
|
+
> **`userAgent` override applies to both JS and HTTP layers.** Call `await emulateDevice({ userAgent: '...' })` before `page.goto(url)` — `navigator.userAgent` on the destination page will reflect the override, and the outgoing HTTP `User-Agent` header is rewritten as well. Use `emulateDevice` rather than `context.setExtraHTTPHeaders({ 'User-Agent': ... })` — the latter does not override UA on ArkWeb.
|
|
103
103
|
|
|
104
104
|
### `tap` fixture
|
|
105
105
|
|
|
@@ -121,10 +121,25 @@ Coordinates are CSS pixels relative to the viewport (same as `touchscreen.tap`).
|
|
|
121
121
|
## Limitations
|
|
122
122
|
|
|
123
123
|
- **Chromium only.** firefox and webkit aren't available on HarmonyOS.
|
|
124
|
-
-
|
|
124
|
+
- **Multi-context / multi-page is opt-in.** By default the adapter intercepts `browser.newContext()` with a friendly error. The root cause: ArkWeb's `Target.createTarget` returns a target with `type: 'other'`, not `'page'`, so Playwright's `crBrowser._onAttachedToTarget` skips it and `ctx.newPage()` throws `Cannot read properties of undefined (reading '_page')`. To opt in, set `process.env.PW_CHROMIUM_ATTACH_TO_OTHER = '1'` **before** importing `@playwright/test` — Playwright's upstream escape hatch then treats `'other'` targets as pages and `browser.newContext()` / `ctx.newPage()` work normally. Trade-off: ArkWeb's internal `'other'` targets (shared workers, etc.) also get treated as pages, which can perturb tests that use `touchscreen.tap()` or `context.recordHar()`. For single-context tests (the common case) leave the env unset and isolate with `localStorage.clear()` + `page.reload()`. For device emulation use the `emulateDevice` fixture instead of `browser.newContext({ ...device })`.
|
|
125
|
+
|
|
126
|
+
**Multi-worker mode.** To run tests in parallel across multiple workers, two things are required together:
|
|
127
|
+
|
|
128
|
+
1. Set `PW_CHROMIUM_ATTACH_TO_OTHER=1` (enables `newContext()` as above).
|
|
129
|
+
2. Import `test` from `ohos-playwright/parallel` instead of `ohos-playwright`:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { test, expect } from 'ohos-playwright/parallel'
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The `ohos-playwright/parallel` fixture opens one ArkWeb context per **test** via `browser.newContext()` (test-scoped) and one page via `ctx.newPage()`. Both are closed after each test. Cookies and localStorage are fully isolated between tests. All ArkWeb workarounds (goBack/goForward, popup interception, hover, evaluate→pageerror) apply identically.
|
|
136
|
+
|
|
137
|
+
Trade-offs to be aware of:
|
|
138
|
+
- **`newContext()` serialises internally in ArkWeb.** Concurrent `newContext()` calls on the same CDP connection are safe (~100 ms for 3 at once). The apparent ~3.4 s bottleneck was from concurrent `connectOverCDP` calls (connection handshakes serialise), not context creation itself. More than ~4 workers may not yield additional throughput.
|
|
139
|
+
- **The default `ohos-playwright` fixture is not parallel-safe.** Its `context` fixture always returns `browser.contexts()[0]` — all workers would race on the same ArkWeb tab. If you set `workers > 1` with `PW_CHROMIUM_ATTACH_TO_OTHER=1` but keep the default `test` import, `withOpenHarmony()` will warn at startup.
|
|
125
140
|
- **HTTP `User-Agent` header can be changed via CDP, but not via `setExtraHTTPHeaders`.** `Emulation.setUserAgentOverride` (sent by `emulateDevice({ userAgent })`) rewrites both `navigator.userAgent` and the outgoing HTTP UA header — call it before `page.goto()` so it applies to the destination page. `context.setExtraHTTPHeaders({ 'User-Agent': '...' })` does **not** override UA on ArkWeb (the header is preserved as browser default).
|
|
126
141
|
- **`locator.hover()` activates CSS `:hover` on typical pages.** The fixture goes through the real `Input.dispatchMouseEvent` path via `page.mouse.move`. On pages where Playwright's `boundingBox()` hangs (e.g. some MutationObserver configurations), it falls back to a JS `mouseover` dispatch after a 5 s timeout — DOM listeners still fire but `:hover` will not activate in that fallback path.
|
|
127
|
-
- **`page.mouse.move()` / `page.mouse.down()` / `page.mouse.up()` work normally.** DOM listeners receive `mousemove` / `mousedown` / `mouseup` / `click` in ArkWeb just as they do on stock Chromium. A narrow edge case (data: URL with embedded newlines AND a shared function reference registered for multiple event types
|
|
142
|
+
- **`page.mouse.move()` / `page.mouse.down()` / `page.mouse.up()` work normally.** DOM listeners receive `mousemove` / `mousedown` / `mouseup` / `click` in ArkWeb just as they do on stock Chromium. A previously documented narrow edge case (data: URL with embedded newlines AND a shared function reference registered for multiple event types) could not be reproduced in the v0.3.3 reaudit. The `mouseMove` / `mouseDown` / `mouseUp` fixtures remain in the API surface for backward compatibility but are no longer needed for normal use.
|
|
128
143
|
- **`emulateDevice({ isMobile: true })` does not apply the viewport** — see the note in the `emulateDevice` fixture section above.
|
|
129
144
|
- **`exposeBinding({ handle: true })` is not supported.** Playwright 1.60's public `exposeBinding` signature is `(name, callback)` — the third `{ handle }` argument is silently ignored. The callback receives a serialized form of the argument (DOM nodes arrive as the string `"ref: <Node>"`), not a JSHandle. Use `exposeFunction` or a plain `exposeBinding` callback that reads element properties directly and returns a by-value object.
|
|
130
145
|
- **`process.platform` reads `'linux'`** during the run — Playwright's `calculatePlatform()` only branches on linux/darwin/win32 (falls through to `<unknown>` on openharmony), and 20+ other sites in `playwright-core` read `process.platform` directly (UA string assembly, headful window insets, modifier keys, registry). The adapter patches `process.platform` to `'linux'` and pins `PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-arm64` for `calculatePlatform()`. For real platform checks use `process.env.OHOS_PW_HOST`.
|
|
@@ -190,7 +205,7 @@ Playwright's bundled Chromium cannot exec inside the OHOS app sandbox, which bre
|
|
|
190
205
|
|
|
191
206
|
**`defineConfig(...)` has no type hints** — set `moduleResolution` to `bundler` or `nodenext` in tsconfig.
|
|
192
207
|
|
|
193
|
-
**`未发现设备` / no device found** — on the device, enable Developer Options → Wireless Debugging
|
|
208
|
+
**`未发现设备` / no device found** — on the device, enable Developer Options → Wireless Debugging and make sure it's on the same Wi-Fi as the host. In CI, run `hdc tconn <ip:port>` manually before tests.
|
|
194
209
|
|
|
195
210
|
**`Failed to launch` the browser** — bundle not installed or wrong bundle name. List installed browsers with:
|
|
196
211
|
|
package/dist/config.mjs
CHANGED
|
@@ -12,10 +12,22 @@ export function withOpenHarmony(config) {
|
|
|
12
12
|
// overwritten platform to 'linux' by the time this function runs.
|
|
13
13
|
if (!process.env.OHOS_PW_HOST)
|
|
14
14
|
return config;
|
|
15
|
+
const multiContextOk = process.env.PW_CHROMIUM_ATTACH_TO_OTHER === '1';
|
|
16
|
+
const configuredWorkers = typeof config.workers === 'number' ? config.workers : 0;
|
|
17
|
+
if (multiContextOk && configuredWorkers > 1) {
|
|
18
|
+
console.warn('[ohos-playwright] workers > 1 with PW_CHROMIUM_ATTACH_TO_OTHER=1: ' +
|
|
19
|
+
'make sure you import { test } from \'ohos-playwright/parallel\' — ' +
|
|
20
|
+
'the default fixture shares contexts()[0] across workers and will race. ' +
|
|
21
|
+
'See README "Multi-worker mode".');
|
|
22
|
+
}
|
|
15
23
|
return {
|
|
16
24
|
...config,
|
|
17
|
-
//
|
|
18
|
-
workers
|
|
25
|
+
// Without PW_CHROMIUM_ATTACH_TO_OTHER, all workers connect to the same
|
|
26
|
+
// endpoint and share contexts()[0].pages()[0] — parallel workers would
|
|
27
|
+
// race on the same page. Force workers:1. With the opt-in env, each
|
|
28
|
+
// worker can browser.newContext() + ctx.newPage() independently via
|
|
29
|
+
// ohos-playwright/parallel, so respect the user's workers setting.
|
|
30
|
+
workers: multiContextOk ? config.workers : 1,
|
|
19
31
|
globalSetup: 'ohos-playwright/setup',
|
|
20
32
|
globalTeardown: 'ohos-playwright/teardown',
|
|
21
33
|
// ArkWeb only speaks Chromium CDP; drop firefox/webkit projects.
|
package/dist/fixture.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from '@playwright/test';
|
|
1
2
|
export interface DeviceDescriptor {
|
|
2
3
|
viewport: {
|
|
3
4
|
width: number;
|
|
@@ -23,6 +24,10 @@ export interface StorageState {
|
|
|
23
24
|
}[];
|
|
24
25
|
}[];
|
|
25
26
|
}
|
|
27
|
+
export type PageCleanup = (opts?: {
|
|
28
|
+
navigateTo?: string;
|
|
29
|
+
}) => Promise<void>;
|
|
30
|
+
export declare function installPageWrappers(page: Page, context: BrowserContext, baseURL: string | undefined): Promise<PageCleanup>;
|
|
26
31
|
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
|
|
27
32
|
emulateDevice: (descriptor: DeviceDescriptor) => Promise<void>;
|
|
28
33
|
tap: (x: number, y: number) => Promise<void>;
|
package/dist/fixture.mjs
CHANGED
|
@@ -16,28 +16,235 @@ function readInfo() {
|
|
|
16
16
|
return JSON.parse(readFileSync(INFO_PATH, 'utf8'));
|
|
17
17
|
}
|
|
18
18
|
function readEndpoint() { return readInfo().endpoint; }
|
|
19
|
+
export async function installPageWrappers(page, context, baseURL) {
|
|
20
|
+
const ctxEmit = context.emit.bind(context);
|
|
21
|
+
// Patch baseURL — save and restore to prevent wrapper accumulation across
|
|
22
|
+
// tests that share the same page object.
|
|
23
|
+
const savedGoto = page['goto'];
|
|
24
|
+
if (baseURL) {
|
|
25
|
+
const root = baseURL.replace(/\/+$/, '');
|
|
26
|
+
const origGoto = page.goto.bind(page);
|
|
27
|
+
page.goto = ((url, opts) => origGoto((url.startsWith('/') && !url.startsWith('//')) ? root + url : url, opts));
|
|
28
|
+
}
|
|
29
|
+
// connectOverCDP reuses an existing tab — Playwright has no record of its
|
|
30
|
+
// viewport size and viewportSize() returns null. Pre-fetch via CDP.
|
|
31
|
+
const session = await context.newCDPSession(page);
|
|
32
|
+
try {
|
|
33
|
+
try {
|
|
34
|
+
const { cssVisualViewport } = await session.send('Page.getLayoutMetrics');
|
|
35
|
+
const cached = {
|
|
36
|
+
width: Math.round(cssVisualViewport.clientWidth),
|
|
37
|
+
height: Math.round(cssVisualViewport.clientHeight),
|
|
38
|
+
};
|
|
39
|
+
const origViewportSize = page.viewportSize.bind(page);
|
|
40
|
+
page.viewportSize = () => origViewportSize() ?? cached;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Non-critical — viewportSize() will still return null if CDP call fails.
|
|
44
|
+
}
|
|
45
|
+
// Bring the tab to foreground so Input.dispatchMouseEvent reaches DOM listeners.
|
|
46
|
+
// page.mouse.move/down/up events are silently dropped when the tab is not active.
|
|
47
|
+
try {
|
|
48
|
+
const targets = await session.send('Target.getTargets');
|
|
49
|
+
const pageTarget = targets.targetInfos.find((t) => t.type === 'page' && t.url === page.url());
|
|
50
|
+
if (pageTarget) {
|
|
51
|
+
await session.send('Target.activateTarget', { targetId: pageTarget.targetId });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Non-fatal: some ArkWeb versions may not support Target.activateTarget.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await session.detach();
|
|
60
|
+
}
|
|
61
|
+
// Override goBack: Page.navigateToHistoryEntry hangs in ArkWeb (never resolves).
|
|
62
|
+
// ArkWeb also does not emit Page.frameNavigated for history navigation, so waitForURL
|
|
63
|
+
// never fires. Poll Page.getNavigationHistory.currentIndex instead.
|
|
64
|
+
;
|
|
65
|
+
page.goBack = async (options) => {
|
|
66
|
+
const timeout = options?.timeout ?? 30000;
|
|
67
|
+
const s = await page.context().newCDPSession(page);
|
|
68
|
+
try {
|
|
69
|
+
const nav = await s.send('Page.getNavigationHistory');
|
|
70
|
+
const prevIndex = nav.currentIndex;
|
|
71
|
+
if (prevIndex <= 0)
|
|
72
|
+
return null;
|
|
73
|
+
await page.evaluate(() => history.back());
|
|
74
|
+
const deadline = Date.now() + timeout;
|
|
75
|
+
while (Date.now() < deadline) {
|
|
76
|
+
const nav2 = await s.send('Page.getNavigationHistory');
|
|
77
|
+
if (nav2.currentIndex < prevIndex)
|
|
78
|
+
break;
|
|
79
|
+
await new Promise(r => setTimeout(r, 80));
|
|
80
|
+
}
|
|
81
|
+
if (Date.now() >= deadline)
|
|
82
|
+
throw new Error(`page.goBack: Timeout ${timeout}ms exceeded`);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await s.detach();
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
page.goForward = async (options) => {
|
|
90
|
+
const timeout = options?.timeout ?? 30000;
|
|
91
|
+
const s = await page.context().newCDPSession(page);
|
|
92
|
+
try {
|
|
93
|
+
const nav = await s.send('Page.getNavigationHistory');
|
|
94
|
+
const prevIndex = nav.currentIndex;
|
|
95
|
+
if (prevIndex >= nav.entries.length - 1)
|
|
96
|
+
return null;
|
|
97
|
+
await page.evaluate(() => history.forward());
|
|
98
|
+
const deadline = Date.now() + timeout;
|
|
99
|
+
while (Date.now() < deadline) {
|
|
100
|
+
const nav2 = await s.send('Page.getNavigationHistory');
|
|
101
|
+
if (nav2.currentIndex > prevIndex)
|
|
102
|
+
break;
|
|
103
|
+
await new Promise(r => setTimeout(r, 80));
|
|
104
|
+
}
|
|
105
|
+
if (Date.now() >= deadline)
|
|
106
|
+
throw new Error(`page.goForward: Timeout ${timeout}ms exceeded`);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await s.detach();
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
};
|
|
113
|
+
// Override locator().hover() to bypass Playwright's internal visibility check
|
|
114
|
+
// (which can hang on ArkWeb), but still go through the real Input.dispatchMouseEvent
|
|
115
|
+
// path so the pointer position is set and CSS :hover activates. The earlier
|
|
116
|
+
// JS-dispatch workaround was disproved by the 2026-06-27 reaudit — ab-hover-css
|
|
117
|
+
// shows the native path delivers both DOM events and :hover activation.
|
|
118
|
+
const savedLocator = page['locator'];
|
|
119
|
+
const origLocator = page.locator.bind(page);
|
|
120
|
+
page.locator = (...args) => {
|
|
121
|
+
const loc = origLocator(...args);
|
|
122
|
+
loc.hover = async (_options) => {
|
|
123
|
+
// Try the real Input.dispatchMouseEvent path first (activates :hover).
|
|
124
|
+
// Some pages (e.g. ones with MutationObservers that re-enter layout) can
|
|
125
|
+
// make Playwright's evaluate/boundingBox hang on ArkWeb; fall back to a
|
|
126
|
+
// JS-only dispatch with a tight timeout so hover() at least returns and
|
|
127
|
+
// DOM listeners fire.
|
|
128
|
+
const viaRealMouse = await Promise.race([
|
|
129
|
+
(async () => {
|
|
130
|
+
const box = await loc.boundingBox();
|
|
131
|
+
if (!box)
|
|
132
|
+
return false;
|
|
133
|
+
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
134
|
+
return true;
|
|
135
|
+
})(),
|
|
136
|
+
new Promise(r => setTimeout(() => r(false), 5000)),
|
|
137
|
+
]);
|
|
138
|
+
if (!viaRealMouse) {
|
|
139
|
+
await Promise.race([
|
|
140
|
+
loc.evaluate((el) => {
|
|
141
|
+
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true }));
|
|
142
|
+
}),
|
|
143
|
+
new Promise(r => setTimeout(r, 2000)),
|
|
144
|
+
]).catch(() => { });
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
return loc;
|
|
148
|
+
};
|
|
149
|
+
// ArkWeb's new tab from window.open() is invisible to CDP (Target.createTarget hangs,
|
|
150
|
+
// Target.targetCreated never fires). Intercept via an init script:
|
|
151
|
+
// - queue the URL for our poller
|
|
152
|
+
// - return null (Window object hangs CDP serialization if returned)
|
|
153
|
+
// Guard against multiple addInitScript calls across tests accumulating overrides.
|
|
154
|
+
const origEvaluate = page.evaluate.bind(page);
|
|
155
|
+
const alreadyPatched = page['__ohosPopupPatched'];
|
|
156
|
+
if (!alreadyPatched) {
|
|
157
|
+
;
|
|
158
|
+
page['__ohosPopupPatched'] = true;
|
|
159
|
+
await page.addInitScript(() => {
|
|
160
|
+
if (window['__ohosPopupPatched'])
|
|
161
|
+
return;
|
|
162
|
+
window['__ohosPopupPatched'] = true;
|
|
163
|
+
window['__ohosPopupQueue'] = [];
|
|
164
|
+
window.open = (url) => {
|
|
165
|
+
;
|
|
166
|
+
window['__ohosPopupQueue'].push({ url: String(url ?? '') });
|
|
167
|
+
return null; // Window object hangs CDP serialization — return null instead
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const popupPoller = setInterval(async () => {
|
|
172
|
+
try {
|
|
173
|
+
const pending = await origEvaluate(() => {
|
|
174
|
+
const q = window['__ohosPopupQueue'];
|
|
175
|
+
window['__ohosPopupQueue'] = [];
|
|
176
|
+
return q;
|
|
177
|
+
});
|
|
178
|
+
for (const { url } of pending ?? []) {
|
|
179
|
+
// context.newPage() calls Target.createTarget which hangs in ArkWeb.
|
|
180
|
+
// Emit a minimal stub — satisfies waitForLoadState / url / close.
|
|
181
|
+
const stub = {
|
|
182
|
+
waitForLoadState: async () => { },
|
|
183
|
+
url: () => url,
|
|
184
|
+
close: async () => { },
|
|
185
|
+
};
|
|
186
|
+
ctxEmit('page', stub);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch { }
|
|
190
|
+
}, 150);
|
|
191
|
+
// evaluate() exceptions reject the promise but never become pageerror events
|
|
192
|
+
// (CDP catches them before they become uncaught). Intercept and re-emit.
|
|
193
|
+
// Save and restore to prevent wrapper accumulation across tests on the same page object.
|
|
194
|
+
const savedEvaluate = page['evaluate'];
|
|
195
|
+
page.evaluate = async (fn, arg) => {
|
|
196
|
+
try {
|
|
197
|
+
return await origEvaluate(fn, arg);
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
201
|
+
page.emit('pageerror', err);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
return async (opts) => {
|
|
205
|
+
clearInterval(popupPoller);
|
|
206
|
+
page.evaluate = savedEvaluate;
|
|
207
|
+
page.locator = savedLocator;
|
|
208
|
+
if (opts?.navigateTo) {
|
|
209
|
+
// Reset the shared tab before restoring goto — keeps the connection alive.
|
|
210
|
+
// page.close() would terminate the ArkWeb DevTools socket.
|
|
211
|
+
try {
|
|
212
|
+
await page.goto(opts.navigateTo);
|
|
213
|
+
}
|
|
214
|
+
catch { }
|
|
215
|
+
}
|
|
216
|
+
;
|
|
217
|
+
page.goto = savedGoto;
|
|
218
|
+
};
|
|
219
|
+
}
|
|
19
220
|
export const test = base.extend({
|
|
20
221
|
browser: [
|
|
21
222
|
async ({}, use) => {
|
|
22
223
|
const browser = await chromium.connectOverCDP(readEndpoint());
|
|
23
|
-
// ArkWeb
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
browser.newContext
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
224
|
+
// ArkWeb's Target.createTarget returns type='other', so Playwright's
|
|
225
|
+
// crBrowser._onAttachedToTarget skips it and ctx.newPage() throws
|
|
226
|
+
// "Cannot read properties of undefined (reading '_page')". The upstream
|
|
227
|
+
// PW_CHROMIUM_ATTACH_TO_OTHER=1 escape hatch fixes newContext/newPage
|
|
228
|
+
// but also makes Playwright treat internal "other" targets as pages
|
|
229
|
+
// (perturbs touchscreen / recordHar). When the user has not opted in,
|
|
230
|
+
// intercept browser.newContext() with a friendly, actionable error.
|
|
231
|
+
if (!process.env.PW_CHROMIUM_ATTACH_TO_OTHER) {
|
|
232
|
+
const origNewContext = browser.newContext.bind(browser);
|
|
233
|
+
browser.newContext = (() => {
|
|
234
|
+
throw new Error('browser.newContext() is not supported on ArkWeb unless you opt in via ' +
|
|
235
|
+
'process.env.PW_CHROMIUM_ATTACH_TO_OTHER=\'1\' before importing @playwright/test. ' +
|
|
236
|
+
'See ohos-playwright README "Limitations" → "Multi-context" for trade-offs.');
|
|
237
|
+
});
|
|
238
|
+
try {
|
|
239
|
+
await use(browser);
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
;
|
|
243
|
+
browser.newContext = origNewContext;
|
|
244
|
+
}
|
|
36
245
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
;
|
|
40
|
-
browser.newContext = origNewContext;
|
|
246
|
+
else {
|
|
247
|
+
await use(browser);
|
|
41
248
|
}
|
|
42
249
|
},
|
|
43
250
|
{ scope: 'worker' },
|
|
@@ -66,210 +273,12 @@ export const test = base.extend({
|
|
|
66
273
|
const page = info.openedNewTab
|
|
67
274
|
? ([...pages].reverse().find((p) => p.url() === (info.launchUrl ?? 'about:blank')) ?? pages[pages.length - 1])
|
|
68
275
|
: (pages.find((p) => p.url().startsWith('http://localhost')) ?? pages[0]);
|
|
69
|
-
const
|
|
70
|
-
// Patch baseURL — save and restore to prevent wrapper accumulation across
|
|
71
|
-
// tests that share the same page object.
|
|
72
|
-
const savedGoto = page['goto'];
|
|
73
|
-
const baseURL = testInfo.project.use.baseURL;
|
|
74
|
-
if (baseURL) {
|
|
75
|
-
const root = baseURL.replace(/\/+$/, '');
|
|
76
|
-
const origGoto = page.goto.bind(page);
|
|
77
|
-
page.goto = ((url, opts) => origGoto((url.startsWith('/') && !url.startsWith('//')) ? root + url : url, opts));
|
|
78
|
-
}
|
|
79
|
-
// connectOverCDP reuses an existing tab — Playwright has no record of its
|
|
80
|
-
// viewport size and viewportSize() returns null. Pre-fetch via CDP.
|
|
81
|
-
const session = await context.newCDPSession(page);
|
|
82
|
-
try {
|
|
83
|
-
try {
|
|
84
|
-
const { cssVisualViewport } = await session.send('Page.getLayoutMetrics');
|
|
85
|
-
const cached = {
|
|
86
|
-
width: Math.round(cssVisualViewport.clientWidth),
|
|
87
|
-
height: Math.round(cssVisualViewport.clientHeight),
|
|
88
|
-
};
|
|
89
|
-
const origViewportSize = page.viewportSize.bind(page);
|
|
90
|
-
page.viewportSize = () => origViewportSize() ?? cached;
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
// Non-critical — viewportSize() will still return null if CDP call fails.
|
|
94
|
-
}
|
|
95
|
-
// Bring the tab to foreground so Input.dispatchMouseEvent reaches DOM listeners.
|
|
96
|
-
// page.mouse.move/down/up events are silently dropped when the tab is not active.
|
|
97
|
-
try {
|
|
98
|
-
const targets = await session.send('Target.getTargets');
|
|
99
|
-
const pageTarget = targets.targetInfos.find((t) => t.type === 'page' && t.url === page.url());
|
|
100
|
-
if (pageTarget) {
|
|
101
|
-
await session.send('Target.activateTarget', { targetId: pageTarget.targetId });
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
// Non-fatal: some ArkWeb versions may not support Target.activateTarget.
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
finally {
|
|
109
|
-
await session.detach();
|
|
110
|
-
}
|
|
111
|
-
// Override goBack: Page.navigateToHistoryEntry hangs in ArkWeb (never resolves).
|
|
112
|
-
// ArkWeb also does not emit Page.frameNavigated for history navigation, so waitForURL
|
|
113
|
-
// never fires. Poll Page.getNavigationHistory.currentIndex instead.
|
|
114
|
-
;
|
|
115
|
-
page.goBack = async (options) => {
|
|
116
|
-
const timeout = options?.timeout ?? 30000;
|
|
117
|
-
const s = await page.context().newCDPSession(page);
|
|
118
|
-
try {
|
|
119
|
-
const nav = await s.send('Page.getNavigationHistory');
|
|
120
|
-
const prevIndex = nav.currentIndex;
|
|
121
|
-
if (prevIndex <= 0)
|
|
122
|
-
return null;
|
|
123
|
-
await page.evaluate(() => history.back());
|
|
124
|
-
const deadline = Date.now() + timeout;
|
|
125
|
-
while (Date.now() < deadline) {
|
|
126
|
-
const nav2 = await s.send('Page.getNavigationHistory');
|
|
127
|
-
if (nav2.currentIndex < prevIndex)
|
|
128
|
-
break;
|
|
129
|
-
await new Promise(r => setTimeout(r, 80));
|
|
130
|
-
}
|
|
131
|
-
if (Date.now() >= deadline)
|
|
132
|
-
throw new Error(`page.goBack: Timeout ${timeout}ms exceeded`);
|
|
133
|
-
}
|
|
134
|
-
finally {
|
|
135
|
-
await s.detach();
|
|
136
|
-
}
|
|
137
|
-
return null;
|
|
138
|
-
};
|
|
139
|
-
page.goForward = async (options) => {
|
|
140
|
-
const timeout = options?.timeout ?? 30000;
|
|
141
|
-
const s = await page.context().newCDPSession(page);
|
|
142
|
-
try {
|
|
143
|
-
const nav = await s.send('Page.getNavigationHistory');
|
|
144
|
-
const prevIndex = nav.currentIndex;
|
|
145
|
-
if (prevIndex >= nav.entries.length - 1)
|
|
146
|
-
return null;
|
|
147
|
-
await page.evaluate(() => history.forward());
|
|
148
|
-
const deadline = Date.now() + timeout;
|
|
149
|
-
while (Date.now() < deadline) {
|
|
150
|
-
const nav2 = await s.send('Page.getNavigationHistory');
|
|
151
|
-
if (nav2.currentIndex > prevIndex)
|
|
152
|
-
break;
|
|
153
|
-
await new Promise(r => setTimeout(r, 80));
|
|
154
|
-
}
|
|
155
|
-
if (Date.now() >= deadline)
|
|
156
|
-
throw new Error(`page.goForward: Timeout ${timeout}ms exceeded`);
|
|
157
|
-
}
|
|
158
|
-
finally {
|
|
159
|
-
await s.detach();
|
|
160
|
-
}
|
|
161
|
-
return null;
|
|
162
|
-
};
|
|
163
|
-
// Override locator().hover() to bypass Playwright's internal visibility check
|
|
164
|
-
// (which can hang on ArkWeb), but still go through the real Input.dispatchMouseEvent
|
|
165
|
-
// path so the pointer position is set and CSS :hover activates. The earlier
|
|
166
|
-
// JS-dispatch workaround was disproved by the 2026-06-27 reaudit — ab-hover-css
|
|
167
|
-
// shows the native path delivers both DOM events and :hover activation.
|
|
168
|
-
const savedLocator = page['locator'];
|
|
169
|
-
const origLocator = page.locator.bind(page);
|
|
170
|
-
page.locator = (...args) => {
|
|
171
|
-
const loc = origLocator(...args);
|
|
172
|
-
loc.hover = async (_options) => {
|
|
173
|
-
// Try the real Input.dispatchMouseEvent path first (activates :hover).
|
|
174
|
-
// Some pages (e.g. ones with MutationObservers that re-enter layout) can
|
|
175
|
-
// make Playwright's evaluate/boundingBox hang on ArkWeb; fall back to a
|
|
176
|
-
// JS-only dispatch with a tight timeout so hover() at least returns and
|
|
177
|
-
// DOM listeners fire.
|
|
178
|
-
const viaRealMouse = await Promise.race([
|
|
179
|
-
(async () => {
|
|
180
|
-
const box = await loc.boundingBox();
|
|
181
|
-
if (!box)
|
|
182
|
-
return false;
|
|
183
|
-
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
184
|
-
return true;
|
|
185
|
-
})(),
|
|
186
|
-
new Promise(r => setTimeout(() => r(false), 5000)),
|
|
187
|
-
]);
|
|
188
|
-
if (!viaRealMouse) {
|
|
189
|
-
await Promise.race([
|
|
190
|
-
loc.evaluate((el) => {
|
|
191
|
-
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true }));
|
|
192
|
-
}),
|
|
193
|
-
new Promise(r => setTimeout(r, 2000)),
|
|
194
|
-
]).catch(() => { });
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
return loc;
|
|
198
|
-
};
|
|
199
|
-
// ArkWeb's new tab from window.open() is invisible to CDP (Target.createTarget hangs,
|
|
200
|
-
// Target.targetCreated never fires). Intercept via an init script:
|
|
201
|
-
// - queue the URL for our poller
|
|
202
|
-
// - return null (Window object hangs CDP serialization if returned)
|
|
203
|
-
// Guard against multiple addInitScript calls across tests accumulating overrides.
|
|
204
|
-
const origEvaluate = page.evaluate.bind(page);
|
|
205
|
-
const alreadyPatched = page['__ohosPopupPatched'];
|
|
206
|
-
if (!alreadyPatched) {
|
|
207
|
-
;
|
|
208
|
-
page['__ohosPopupPatched'] = true;
|
|
209
|
-
await page.addInitScript(() => {
|
|
210
|
-
if (window['__ohosPopupPatched'])
|
|
211
|
-
return;
|
|
212
|
-
window['__ohosPopupPatched'] = true;
|
|
213
|
-
window['__ohosPopupQueue'] = [];
|
|
214
|
-
window.open = (url) => {
|
|
215
|
-
;
|
|
216
|
-
window['__ohosPopupQueue'].push({ url: String(url ?? '') });
|
|
217
|
-
return null; // Window object hangs CDP serialization — return null instead
|
|
218
|
-
};
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
const popupPoller = setInterval(async () => {
|
|
222
|
-
try {
|
|
223
|
-
const pending = await origEvaluate(() => {
|
|
224
|
-
const q = window['__ohosPopupQueue'];
|
|
225
|
-
window['__ohosPopupQueue'] = [];
|
|
226
|
-
return q;
|
|
227
|
-
});
|
|
228
|
-
for (const { url } of pending ?? []) {
|
|
229
|
-
// context.newPage() calls Target.createTarget which hangs in ArkWeb.
|
|
230
|
-
// Emit a minimal stub — satisfies waitForLoadState / url / close.
|
|
231
|
-
const stub = {
|
|
232
|
-
waitForLoadState: async () => { },
|
|
233
|
-
url: () => url,
|
|
234
|
-
close: async () => { },
|
|
235
|
-
};
|
|
236
|
-
ctxEmit('page', stub);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
catch { }
|
|
240
|
-
}, 150);
|
|
241
|
-
// evaluate() exceptions reject the promise but never become pageerror events
|
|
242
|
-
// (CDP catches them before they become uncaught). Intercept and re-emit.
|
|
243
|
-
// Save and restore to prevent wrapper accumulation across tests on the same page object.
|
|
244
|
-
const savedEvaluate = page['evaluate'];
|
|
245
|
-
page.evaluate = async (fn, arg) => {
|
|
246
|
-
try {
|
|
247
|
-
return await origEvaluate(fn, arg);
|
|
248
|
-
}
|
|
249
|
-
catch (e) {
|
|
250
|
-
const err = e instanceof Error ? e : new Error(String(e));
|
|
251
|
-
page.emit('pageerror', err);
|
|
252
|
-
}
|
|
253
|
-
};
|
|
276
|
+
const cleanup = await installPageWrappers(page, context, testInfo.project.use.baseURL);
|
|
254
277
|
try {
|
|
255
278
|
await use(page);
|
|
256
279
|
}
|
|
257
280
|
finally {
|
|
258
|
-
|
|
259
|
-
page.evaluate = savedEvaluate;
|
|
260
|
-
page.locator = savedLocator;
|
|
261
|
-
// Reset the test tab to about:blank so it's clean for the next test.
|
|
262
|
-
// page.close() sends Target.closeTarget which terminates the ArkWeb
|
|
263
|
-
// DevTools socket — use goto instead to keep the connection alive.
|
|
264
|
-
if (info.openedNewTab) {
|
|
265
|
-
try {
|
|
266
|
-
await page.goto('about:blank');
|
|
267
|
-
}
|
|
268
|
-
catch { }
|
|
269
|
-
}
|
|
270
|
-
// Restore goto to prevent wrapper accumulation across tests.
|
|
271
|
-
;
|
|
272
|
-
page.goto = savedGoto;
|
|
281
|
+
await cleanup({ navigateTo: info.openedNewTab ? 'about:blank' : undefined });
|
|
273
282
|
}
|
|
274
283
|
},
|
|
275
284
|
emulateDevice: async ({ page }, use) => {
|
package/dist/loader.d.mts
CHANGED
|
@@ -1,15 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
[key: string]: unknown;
|
|
4
|
-
}
|
|
5
|
-
interface NextResolve {
|
|
6
|
-
(specifier: string, context: ResolveContext): {
|
|
7
|
-
url: string;
|
|
8
|
-
} | Promise<{
|
|
9
|
-
url: string;
|
|
10
|
-
}>;
|
|
11
|
-
}
|
|
12
|
-
export declare function resolve(specifier: string, context: ResolveContext, nextResolve: NextResolve): Promise<{
|
|
13
|
-
url: string;
|
|
14
|
-
}>;
|
|
15
|
-
export {};
|
|
1
|
+
import type { ResolveHookContext, ResolveFnOutput } from 'node:module';
|
|
2
|
+
export declare function resolve(specifier: string, context: ResolveHookContext, nextResolve: (specifier: string, context?: Partial<ResolveHookContext>) => ResolveFnOutput): ResolveFnOutput;
|
package/dist/loader.mjs
CHANGED
|
@@ -10,13 +10,12 @@ const FIXTURE_HELPER = /\bfixtures?\.[mc]?[tj]sx?$/;
|
|
|
10
10
|
const OWN_DIST_URL = pathToFileURL(resolvePath(import.meta.dirname) + '/').href;
|
|
11
11
|
const PACKAGE_ROOT_URL = pathToFileURL(resolvePath(import.meta.dirname, '..') + '/').href;
|
|
12
12
|
const PROJECT_ANCHOR = pathToFileURL(resolvePath(process.cwd(), 'noop.mjs')).href;
|
|
13
|
-
export
|
|
13
|
+
export function resolve(specifier, context, nextResolve) {
|
|
14
14
|
if (specifier === TARGET) {
|
|
15
15
|
const parent = context.parentURL ?? '';
|
|
16
16
|
const isFromOwnDist = parent.startsWith(OWN_DIST_URL);
|
|
17
17
|
if (TEST_FILE.test(parent) || (FIXTURE_HELPER.test(parent) && !isFromOwnDist)) {
|
|
18
|
-
|
|
19
|
-
return { url: result.url };
|
|
18
|
+
return nextResolve(FIXTURE_URL, context);
|
|
20
19
|
}
|
|
21
20
|
if (parent.startsWith(PACKAGE_ROOT_URL)) {
|
|
22
21
|
return nextResolve(specifier, { ...context, parentURL: PROJECT_ANCHOR });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from '@playwright/test';
|
|
2
|
+
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
|
|
3
|
+
emulateDevice: (descriptor: import("./fixture.mts").DeviceDescriptor) => Promise<void>;
|
|
4
|
+
tap: (x: number, y: number) => Promise<void>;
|
|
5
|
+
mouseMove: (x: number, y: number, opts?: {
|
|
6
|
+
steps?: number;
|
|
7
|
+
}) => Promise<void>;
|
|
8
|
+
mouseDown: (x: number, y: number) => Promise<void>;
|
|
9
|
+
mouseUp: (x: number, y: number) => Promise<void>;
|
|
10
|
+
saveStorageState: (origin?: string) => Promise<import("./fixture.mts").StorageState>;
|
|
11
|
+
loadStorageState: (state: import("./fixture.mts").StorageState) => Promise<void>;
|
|
12
|
+
emulateLocale: (locale: string) => Promise<void>;
|
|
13
|
+
} & {
|
|
14
|
+
context: BrowserContext;
|
|
15
|
+
page: Page;
|
|
16
|
+
}, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
17
|
+
export { expect } from '@playwright/test';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { test as base, installPageWrappers } from "./fixture.mjs";
|
|
2
|
+
// Parallel-safe test fixture for ohos-playwright.
|
|
3
|
+
//
|
|
4
|
+
// Requirements:
|
|
5
|
+
// - PW_CHROMIUM_ATTACH_TO_OTHER=1 must be set before importing @playwright/test
|
|
6
|
+
// - Use withOpenHarmony() in your playwright.config.ts as usual
|
|
7
|
+
//
|
|
8
|
+
// Differences from the default 'ohos-playwright' fixture:
|
|
9
|
+
// - Each test opens its own ArkWeb context via browser.newContext() (test-scoped).
|
|
10
|
+
// Cookies and localStorage are fully isolated between tests.
|
|
11
|
+
// - Each test gets ctx.newPage() and both page and context are closed after the
|
|
12
|
+
// test. All ArkWeb workarounds (goBack, goForward, popup, hover,
|
|
13
|
+
// evaluate→pageerror) are applied identically to the default fixture.
|
|
14
|
+
// - newContext() in ArkWeb costs ~100 ms; concurrent creation is safe.
|
|
15
|
+
//
|
|
16
|
+
// Usage:
|
|
17
|
+
// import { test, expect } from 'ohos-playwright/parallel'
|
|
18
|
+
export const test = base.extend({
|
|
19
|
+
context: async ({ browser }, use, testInfo) => {
|
|
20
|
+
if (!process.env.PW_CHROMIUM_ATTACH_TO_OTHER) {
|
|
21
|
+
throw new Error('[ohos-playwright/parallel] PW_CHROMIUM_ATTACH_TO_OTHER=\'1\' must be set before ' +
|
|
22
|
+
'importing @playwright/test when using this fixture. ' +
|
|
23
|
+
'See ohos-playwright README "Multi-worker mode".');
|
|
24
|
+
}
|
|
25
|
+
const baseURL = testInfo.project.use.baseURL;
|
|
26
|
+
const ctx = await browser.newContext(baseURL ? { baseURL } : {});
|
|
27
|
+
try {
|
|
28
|
+
await use(ctx);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
await ctx.close().catch(() => { });
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
page: async ({ context }, use) => {
|
|
35
|
+
const page = await context.newPage();
|
|
36
|
+
// baseURL is set on the context via newContext({ baseURL }), so Playwright
|
|
37
|
+
// resolves relative URLs natively — pass undefined to skip the goto patch.
|
|
38
|
+
const cleanup = await installPageWrappers(page, context, undefined);
|
|
39
|
+
try {
|
|
40
|
+
await use(page);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await cleanup();
|
|
44
|
+
await page.close().catch(() => { });
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
export { expect } from '@playwright/test';
|
package/dist/register.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { registerHooks } from 'node:module';
|
|
2
|
+
import { resolve } from './loader.mjs';
|
|
2
3
|
// Adapter only activates on OpenHarmony — elsewhere this file is a no-op so
|
|
3
4
|
// the same ohos-playwright entry point and the same playwright.config.ts can
|
|
4
5
|
// run on Windows / Linux / macOS with stock Playwright.
|
|
@@ -21,5 +22,19 @@ if (process.platform === 'openharmony') {
|
|
|
21
22
|
// process. Safe because we connect over CDP and never touch Playwright's
|
|
22
23
|
// bundled browser binaries. Clean fix requires upstream openharmony branch.
|
|
23
24
|
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
24
|
-
|
|
25
|
+
// ArkWeb's Target.createTarget returns a target with type='other', not
|
|
26
|
+
// 'page'. Playwright's crBrowser._onAttachedToTarget (crBrowser.ts:191)
|
|
27
|
+
// only registers 'page' targets into _crPages — 'other' targets get
|
|
28
|
+
// detached and ctx.newPage() throws "Cannot read properties of undefined
|
|
29
|
+
// (reading '_page')".
|
|
30
|
+
//
|
|
31
|
+
// PW_CHROMIUM_ATTACH_TO_OTHER (crBrowser.ts:181) is playwright's upstream
|
|
32
|
+
// escape hatch: treat type='other' as page so newContext/newPage work.
|
|
33
|
+
// It's opt-in (not set here by default) because it also makes Playwright
|
|
34
|
+
// treat ArkWeb's internal "other" targets (shared workers, etc.) as pages,
|
|
35
|
+
// which perturbs page-list assumptions in tests that use touchscreen /
|
|
36
|
+
// recordHar. Users who need multi-context set it explicitly:
|
|
37
|
+
// process.env.PW_CHROMIUM_ATTACH_TO_OTHER = '1'
|
|
38
|
+
// before importing @playwright/test.
|
|
39
|
+
registerHooks({ resolve });
|
|
25
40
|
}
|
package/dist/setup.d.mts
CHANGED
|
@@ -7,7 +7,6 @@ export declare function retry<T>(fn: () => T | Promise<T>, { max, interval, labe
|
|
|
7
7
|
export declare function findBrowserPid(): number | null;
|
|
8
8
|
export declare function countCdpPages(listJson: string): number;
|
|
9
9
|
export declare function hasDeviceConnected(): boolean;
|
|
10
|
-
export declare function discoverDevices(): string[];
|
|
11
10
|
export declare function tryLocalDevice(): boolean;
|
|
12
11
|
export declare function ensureHdcKey(): boolean;
|
|
13
12
|
export declare function ensureDeviceConnected(): Promise<void>;
|
package/dist/setup.mjs
CHANGED
|
@@ -107,13 +107,34 @@ function pickFreePort() {
|
|
|
107
107
|
srv.on('error', rej);
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
|
+
function parseFportRules(lsOutput) {
|
|
111
|
+
const rules = [];
|
|
112
|
+
for (const line of lsOutput.split('\n')) {
|
|
113
|
+
const m = line.match(/tcp:(\d+)\s+(localabstract:\S+)/);
|
|
114
|
+
if (m)
|
|
115
|
+
rules.push({ port: m[1], socket: m[2] });
|
|
116
|
+
}
|
|
117
|
+
return rules;
|
|
118
|
+
}
|
|
110
119
|
function setupForward(port, socketName) {
|
|
111
|
-
|
|
120
|
+
// Remove all existing rules for this socket (any port) before creating a new
|
|
121
|
+
// one — prevents rule accumulation from crashed runs. hdc fport rm requires
|
|
122
|
+
// separate arguments; a single "tcp:PORT localabstract:SOCKET" string is
|
|
123
|
+
// silently ignored by hdc.
|
|
112
124
|
try {
|
|
113
|
-
hdc(['fport', '
|
|
125
|
+
const ls = hdc(['fport', 'ls']);
|
|
126
|
+
const target = `localabstract:${socketName}`;
|
|
127
|
+
for (const { port: p, socket: s } of parseFportRules(ls)) {
|
|
128
|
+
if (s === target) {
|
|
129
|
+
try {
|
|
130
|
+
hdc(['fport', 'rm', `tcp:${p}`, s]);
|
|
131
|
+
}
|
|
132
|
+
catch { }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
114
135
|
}
|
|
115
136
|
catch { }
|
|
116
|
-
hdc(['fport',
|
|
137
|
+
hdc(['fport', `tcp:${port}`, `localabstract:${socketName}`]);
|
|
117
138
|
}
|
|
118
139
|
function cdpGet(port, path) {
|
|
119
140
|
return new Promise((res) => {
|
|
@@ -142,18 +163,6 @@ export function hasDeviceConnected() {
|
|
|
142
163
|
const t = listTargets();
|
|
143
164
|
return t.length > 0 && t !== '[Empty]';
|
|
144
165
|
}
|
|
145
|
-
// discoverDevices timeout reduced from 6s to 3s — LAN broadcast on local
|
|
146
|
-
// network should respond within 1-2s; longer wait is unlikely to help.
|
|
147
|
-
export function discoverDevices() {
|
|
148
|
-
let out = '';
|
|
149
|
-
try {
|
|
150
|
-
out = hdc(['discover'], { timeout: 3000 });
|
|
151
|
-
}
|
|
152
|
-
catch (e) {
|
|
153
|
-
out = e.stdout?.toString() ?? '';
|
|
154
|
-
}
|
|
155
|
-
return out.split('\n').map(s => s.trim()).filter(s => IP_PORT_RE.test(s));
|
|
156
|
-
}
|
|
157
166
|
// 已连接时 hdc tconn 返回 "Target is connected, repeat operation",也视作成功。
|
|
158
167
|
function tconn(addr) {
|
|
159
168
|
try {
|
|
@@ -175,7 +184,6 @@ const CONNECT_HELP = [
|
|
|
175
184
|
' 1) 进入「设置 → 关于本机」连点版本号开启「开发者选项」',
|
|
176
185
|
' 2) 进入「开发者选项」启用「无线调试」',
|
|
177
186
|
' 3) 确认设备与本机在同一 Wi-Fi 下',
|
|
178
|
-
' 4) 防火墙放行本机 UDP:8710 入站(hdc discover 广播用)',
|
|
179
187
|
'也可手动跑 `hdc tconn <ip:port>` 后重新启动测试。',
|
|
180
188
|
'若不希望自动连接,设 OHOS_PW_AUTO_CONNECT=0 跳过。',
|
|
181
189
|
].join('\n');
|
|
@@ -264,15 +272,6 @@ export async function ensureDeviceConnected() {
|
|
|
264
272
|
return;
|
|
265
273
|
if (tryLocalDevice())
|
|
266
274
|
return;
|
|
267
|
-
console.log('[ohos-playwright] no local device, broadcasting (hdc discover)...');
|
|
268
|
-
const found = discoverDevices();
|
|
269
|
-
for (const addr of found) {
|
|
270
|
-
console.log(`[ohos-playwright] hdc tconn ${addr}`);
|
|
271
|
-
if (tconn(addr) && hasDeviceConnected())
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
if (found.length > 0)
|
|
275
|
-
console.warn('[ohos-playwright] discovered devices but none connected');
|
|
276
275
|
if (!process.stdin.isTTY)
|
|
277
276
|
throw new Error(CONNECT_HELP);
|
|
278
277
|
console.log(CONNECT_HELP);
|
package/dist/teardown.mjs
CHANGED
|
@@ -10,10 +10,13 @@ export default async function globalTeardown() {
|
|
|
10
10
|
catch {
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
|
-
const
|
|
13
|
+
const tcpArg = `tcp:${info.port}`;
|
|
14
|
+
const sockArg = `localabstract:${info.socket}`;
|
|
14
15
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
// hdc fport rm requires separate arguments — a single combined string is
|
|
17
|
+
// silently ignored.
|
|
18
|
+
execFileSync(HDC, ['fport', 'rm', tcpArg, sockArg], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
19
|
+
console.log(`[ohos-playwright] removed fport ${tcpArg} ${sockArg}`);
|
|
17
20
|
}
|
|
18
21
|
catch (e) {
|
|
19
22
|
const msg = e instanceof Error ? e.message?.split('\n')[0] : String(e);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ohos-playwright",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Playwright adapter for OpenHarmony / ArkWeb via hdc + CDP",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "social4hyq",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"./teardown": "./dist/teardown.mjs",
|
|
39
39
|
"./register": "./dist/register.mjs",
|
|
40
40
|
"./loader": "./dist/loader.mjs",
|
|
41
|
-
"./config": "./dist/config.mjs"
|
|
41
|
+
"./config": "./dist/config.mjs",
|
|
42
|
+
"./parallel": "./dist/parallel.mjs"
|
|
42
43
|
},
|
|
43
44
|
"bin": {
|
|
44
45
|
"ohos-playwright": "./dist/cli.mjs"
|