ohos-playwright 0.3.1 → 0.3.3
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 +57 -11
- package/dist/fixture.d.mts +6 -0
- package/dist/fixture.mjs +186 -12
- package/dist/loader.mjs +7 -1
- package/dist/register.mjs +13 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,24 +37,43 @@ The following Playwright APIs have been validated on ArkWeb / HarmonyOS 6.1 (Chr
|
|
|
37
37
|
|
|
38
38
|
| Category | APIs |
|
|
39
39
|
|---|---|
|
|
40
|
-
| Network interception | `page.route()`, `route.fulfill()`, `route.abort()`, `page.unroute()` |
|
|
40
|
+
| Network interception | `page.route()`, `context.route()`, `route.fulfill()`, `route.abort()`, `page.unroute()` — `page.route` takes priority over `context.route` |
|
|
41
41
|
| Network headers | `page.setExtraHTTPHeaders()` — custom headers reach the server |
|
|
42
42
|
| Network conditions | `context.setOffline(true / false)` — actually applies (`ERR_INTERNET_DISCONNECTED` ↔ reachable) |
|
|
43
|
+
| Network events | `page.on('request')`, `page.on('response')`, `page.on('requestfinished')`, `page.on('requestfailed')` |
|
|
44
|
+
| Network intercept wait | `page.waitForRequest()`, `page.waitForResponse()` — string URL, glob, and predicate all work |
|
|
43
45
|
| Screenshot | `page.screenshot({ type: 'jpeg' \| 'png' })`, `locator.screenshot()` |
|
|
44
46
|
| PDF | `page.pdf()` — Chromium-only API, implemented by ArkWeb (produces a valid `%PDF` document) |
|
|
45
47
|
| Tracing | `context.tracing.start()` / `stop({ path })` — works under `connectOverCDP`; the resulting zip contains trace + screenshots + source maps |
|
|
48
|
+
| Coverage | `page.coverage.startJSCoverage()` / `stopJSCoverage()`, `startCSSCoverage()` / `stopCSSCoverage()` — returns per-page entries with ranges |
|
|
46
49
|
| Geolocation | `context.setGeolocation()`, `context.grantPermissions(['geolocation'])` |
|
|
47
50
|
| Device emulation | `emulateDevice` fixture (see below — `isMobile: false` for a precise viewport) |
|
|
48
51
|
| Input | `locator.fill()`, `locator.type()`, `keyboard.press()`, `page.selectOption()` |
|
|
52
|
+
| Keyboard combos | `keyboard.press('Control+a')`, `'Shift+Tab'`, `'Control+z'`, `'Shift+ArrowRight'`, `'Alt+ArrowLeft'` — modifier combinations work |
|
|
53
|
+
| Checkbox | `locator.check()`, `locator.uncheck()`, `locator.setChecked()` — disabled elements correctly rejected |
|
|
49
54
|
| Drag & drop | `locator.dragTo()` — triggers a `drop` event |
|
|
55
|
+
| Scroll | `page.mouse.wheel()` — scrolls correctly; `page.evaluate(() => el.scrollTo(...))` also works |
|
|
50
56
|
| File upload | `page.setInputFiles()` — fires `change`, file content readable |
|
|
51
57
|
| Cookies | `context.addCookies()`, `context.cookies()`, `context.clearCookies()` |
|
|
52
58
|
| Dialog | `page.on('dialog')`, `dialog.accept()`, `dialog.dismiss()`, `dialog.message()`, `dialog.type()` |
|
|
53
59
|
| Popup | `context.waitForEvent('page')` + `window.open()` — stub Page with `url()`, `waitForLoadState()`, `close()` |
|
|
54
60
|
| Page events | `page.on('pageerror')`, `page.on('console')`, `page.on('download')` |
|
|
61
|
+
| Script / style injection | `page.addScriptTag({ content \| path \| type:'module' })`, `page.addStyleTag({ content })` |
|
|
62
|
+
| Init script | `page.addInitScript()` — function or string, persists across `goto()` navigations |
|
|
63
|
+
| Expose to page | `page.exposeFunction()`, `page.exposeBinding()` — persists across navigations; `handle` mode not supported |
|
|
64
|
+
| Navigation wait | `page.waitForURL()` — string, glob, RegExp, and `history.pushState` client-side navigation |
|
|
55
65
|
| Frames | `page.frames()`, `page.mainFrame()`, `frame.url()` |
|
|
56
66
|
| Viewport | `page.viewportSize()` (pre-fetched via `Page.getLayoutMetrics` for reused CDP tabs), `page.setViewportSize()` (applies precisely) |
|
|
57
67
|
| Media emulation | `page.emulateMedia({ colorScheme })` |
|
|
68
|
+
| Web workers | `page.workers()` — returns the list of active workers |
|
|
69
|
+
| WebSocket | `page.routeWebSocket()` (requires Playwright ≥ 1.48) — intercepts WebSocket connections |
|
|
70
|
+
| Accessibility (CDP) | `newCDPSession` + `Accessibility.getFullAXTree` — returns the full AX node tree |
|
|
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; CSS `:hover` pseudo-class is **not** activated (adapter uses JS dispatch, not a real pointer move) |
|
|
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 (partial) | `emulateDevice({ userAgent })` — overrides `navigator.userAgent` for page scripts; call before `page.goto()` for the override to take effect. HTTP `User-Agent` request headers are not changed. |
|
|
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
|
+
| Clipboard | `navigator.clipboard.writeText()` / `readText()` — works on HTTPS pages after `context.grantPermissions(['clipboard-read', 'clipboard-write'])`; unavailable on non-secure origins |
|
|
58
77
|
|
|
59
78
|
### `emulateDevice` fixture
|
|
60
79
|
|
|
@@ -78,7 +97,9 @@ test('precise viewport', async ({ page, emulateDevice }) => {
|
|
|
78
97
|
`emulateDevice` settings persist for the lifetime of the page. Call it again with `{ viewport: { width: 1280, height: 720 }, isMobile: false }` to restore defaults.
|
|
79
98
|
|
|
80
99
|
> **⚠️ `isMobile: true` does not produce a precise viewport on ArkWeb.**
|
|
81
|
-
> 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.
|
|
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
|
+
>
|
|
102
|
+
> **`userAgent` override is JS-layer only.** Call `await emulateDevice({ userAgent: '...' })` before `page.goto(url)` — `navigator.userAgent` on the destination page will reflect the override. The HTTP `User-Agent` request header is not changed (ArkWeb does not honour CDP UA overrides for outgoing headers).
|
|
82
103
|
|
|
83
104
|
### `tap` fixture
|
|
84
105
|
|
|
@@ -100,16 +121,13 @@ Coordinates are CSS pixels relative to the viewport (same as `touchscreen.tap`).
|
|
|
100
121
|
## Limitations
|
|
101
122
|
|
|
102
123
|
- **Chromium only.** firefox and webkit aren't available on HarmonyOS.
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
- **`
|
|
106
|
-
- **`
|
|
107
|
-
- **`mouse.wheel()` is a no-op** — the command succeeds but `scrollTop` stays 0. Scroll via `page.evaluate(() => el.scrollTo(...))`.
|
|
108
|
-
- **Service Workers unavailable** — `navigator.serviceWorker` is `undefined` on ArkWeb; PWA / SW-based tests are not possible.
|
|
109
|
-
- **`page.workers()` returns empty** for web workers — CDP does not auto-attach worker targets. The workers themselves run fine (messages reach the main page), only the listing is affected.
|
|
110
|
-
- **Clipboard is a false positive** — `navigator.clipboard.writeText/readText` do not throw but `readText` returns `undefined`. Don't assert on clipboard contents.
|
|
124
|
+
- **`newPage()` after `browser.newContext()` throws.** The raw `browser.newContext()` call succeeds (returns an empty context, 0 pages) but `ctx.newPage()` fails with `Cannot read properties of undefined (reading '_page')`. ArkWeb's CDP `Target.createBrowserContext` / `Target.createTarget` aren't usable in `connectOverCDP` mode. Tests share one context and one page — isolate with `localStorage.clear()` + `page.reload()`. For device emulation use the `emulateDevice` fixture instead of `browser.newContext({ ...device })`.
|
|
125
|
+
- **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
|
+
- **`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 on the same element) was previously documented; reaudit could not reproduce it. The `mouseMove` / `mouseDown` / `mouseUp` fixtures remain available as JS-dispatch fallbacks for unusual cases.
|
|
111
128
|
- **`emulateDevice({ isMobile: true })` does not apply the viewport** — see the note in the `emulateDevice` fixture section above.
|
|
112
|
-
- **`
|
|
129
|
+
- **`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
|
+
- **`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`.
|
|
113
131
|
|
|
114
132
|
## Environment variables
|
|
115
133
|
|
|
@@ -122,6 +140,34 @@ Coordinates are CSS pixels relative to the viewport (same as `touchscreen.tap`).
|
|
|
122
140
|
| `OHOS_PW_INFO_PATH` | `<tmpdir>/ohos-playwright-cdp.json` |
|
|
123
141
|
| `OHOS_PW_UI_HOST` | `0.0.0.0` — used when `--ui` is passed without `--ui-host` on an OHOS device |
|
|
124
142
|
| `OHOS_PW_UI_PORT` | `8765` — used when `--ui` is passed without `--ui-port` on an OHOS device |
|
|
143
|
+
| `OHOS_PW_CDP_URL` | unset — when set, overrides the hdc-derived CDP endpoint entirely (e.g. `http://192.168.1.10:9222`) — used for LAN Chrome A/B comparison runs |
|
|
144
|
+
|
|
145
|
+
### LAN Chrome A/B comparison
|
|
146
|
+
|
|
147
|
+
`OHOS_PW_CDP_URL` lets you point the probe runner at an arbitrary CDP endpoint — useful for confirming whether a behaviour is an ArkWeb-specific gap or a general Chromium/CDP constraint.
|
|
148
|
+
|
|
149
|
+
**Windows Chrome setup** (PowerShell / cmd):
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
chrome.exe --remote-debugging-port=9222 --remote-debugging-address=0.0.0.0 ^
|
|
153
|
+
--user-data-dir=C:\cdp-profile --remote-allow-origins=* about:blank
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
> Binding to the LAN IP directly (e.g. `--remote-debugging-address=192.168.1.10`) is more reliable than `0.0.0.0` with Chrome ≥ M111. SSH tunnel fallback: `ssh -L 9222:127.0.0.1:9222 <windows-host>` then use `http://127.0.0.1:9222`.
|
|
157
|
+
|
|
158
|
+
**Running the Chrome leg** — must be from a **non-OpenHarmony host** so the adapter loader does not apply ArkWeb-specific fixture overrides:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
OHOS_PW_CDP_URL=http://192.168.1.10:9222 npx playwright test probes/ab-baseline.spec.ts
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**ArkWeb leg** (from the HarmonyPC host, as usual):
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
./dist/cli.mjs test --config=probes/playwright.config.ts probes/ab-baseline.spec.ts
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Compare the `[PROBE ab-baseline]` log lines between the two runs to isolate ArkWeb-specific behaviour.
|
|
125
171
|
|
|
126
172
|
### `--ui` and `--debug` on an OHOS device
|
|
127
173
|
|
package/dist/fixture.d.mts
CHANGED
|
@@ -26,7 +26,13 @@ export interface StorageState {
|
|
|
26
26
|
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
|
|
27
27
|
emulateDevice: (descriptor: DeviceDescriptor) => Promise<void>;
|
|
28
28
|
tap: (x: number, y: number) => Promise<void>;
|
|
29
|
+
mouseMove: (x: number, y: number, opts?: {
|
|
30
|
+
steps?: number;
|
|
31
|
+
}) => Promise<void>;
|
|
32
|
+
mouseDown: (x: number, y: number) => Promise<void>;
|
|
33
|
+
mouseUp: (x: number, y: number) => Promise<void>;
|
|
29
34
|
saveStorageState: (origin?: string) => Promise<StorageState>;
|
|
30
35
|
loadStorageState: (state: StorageState) => Promise<void>;
|
|
36
|
+
emulateLocale: (locale: string) => Promise<void>;
|
|
31
37
|
}, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
32
38
|
export { expect } from '@playwright/test';
|
package/dist/fixture.mjs
CHANGED
|
@@ -2,6 +2,17 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { test as base, chromium } from '@playwright/test';
|
|
3
3
|
import { INFO_PATH } from "./info-path.mjs";
|
|
4
4
|
function readInfo() {
|
|
5
|
+
const cdpUrl = process.env.OHOS_PW_CDP_URL;
|
|
6
|
+
if (cdpUrl) {
|
|
7
|
+
return {
|
|
8
|
+
port: 0,
|
|
9
|
+
pid: 0,
|
|
10
|
+
socket: '',
|
|
11
|
+
endpoint: cdpUrl,
|
|
12
|
+
openedNewTab: false,
|
|
13
|
+
launchUrl: process.env.OHOS_PW_LAUNCH_URL ?? 'about:blank',
|
|
14
|
+
};
|
|
15
|
+
}
|
|
5
16
|
return JSON.parse(readFileSync(INFO_PATH, 'utf8'));
|
|
6
17
|
}
|
|
7
18
|
function readEndpoint() { return readInfo().endpoint; }
|
|
@@ -56,7 +67,9 @@ export const test = base.extend({
|
|
|
56
67
|
? ([...pages].reverse().find((p) => p.url() === (info.launchUrl ?? 'about:blank')) ?? pages[pages.length - 1])
|
|
57
68
|
: (pages.find((p) => p.url().startsWith('http://localhost')) ?? pages[0]);
|
|
58
69
|
const ctxEmit = context.emit.bind(context);
|
|
59
|
-
// Patch baseURL
|
|
70
|
+
// Patch baseURL — save and restore to prevent wrapper accumulation across
|
|
71
|
+
// tests that share the same page object.
|
|
72
|
+
const savedGoto = page['goto'];
|
|
60
73
|
const baseURL = testInfo.project.use.baseURL;
|
|
61
74
|
if (baseURL) {
|
|
62
75
|
const root = baseURL.replace(/\/+$/, '');
|
|
@@ -67,20 +80,122 @@ export const test = base.extend({
|
|
|
67
80
|
// viewport size and viewportSize() returns null. Pre-fetch via CDP.
|
|
68
81
|
const session = await context.newCDPSession(page);
|
|
69
82
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
107
|
}
|
|
81
108
|
finally {
|
|
82
109
|
await session.detach();
|
|
83
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
|
+
};
|
|
84
199
|
// ArkWeb's new tab from window.open() is invisible to CDP (Target.createTarget hangs,
|
|
85
200
|
// Target.targetCreated never fires). Intercept via an init script:
|
|
86
201
|
// - queue the URL for our poller
|
|
@@ -142,6 +257,7 @@ export const test = base.extend({
|
|
|
142
257
|
finally {
|
|
143
258
|
clearInterval(popupPoller);
|
|
144
259
|
page.evaluate = savedEvaluate;
|
|
260
|
+
page.locator = savedLocator;
|
|
145
261
|
// Reset the test tab to about:blank so it's clean for the next test.
|
|
146
262
|
// page.close() sends Target.closeTarget which terminates the ArkWeb
|
|
147
263
|
// DevTools socket — use goto instead to keep the connection alive.
|
|
@@ -151,6 +267,9 @@ export const test = base.extend({
|
|
|
151
267
|
}
|
|
152
268
|
catch { }
|
|
153
269
|
}
|
|
270
|
+
// Restore goto to prevent wrapper accumulation across tests.
|
|
271
|
+
;
|
|
272
|
+
page.goto = savedGoto;
|
|
154
273
|
}
|
|
155
274
|
},
|
|
156
275
|
emulateDevice: async ({ page }, use) => {
|
|
@@ -166,7 +285,9 @@ export const test = base.extend({
|
|
|
166
285
|
'default mobile layout viewport; the passed width/height will NOT apply. ' +
|
|
167
286
|
'Use isMobile: false for a precise viewport.');
|
|
168
287
|
}
|
|
169
|
-
// ArkWeb note: setUserAgentOverride
|
|
288
|
+
// ArkWeb note: setUserAgentOverride takes effect after the next page.goto() — the override
|
|
289
|
+
// applies to the destination page's navigator.userAgent but not the currently-loaded page.
|
|
290
|
+
// The HTTP User-Agent header is also not changed by ArkWeb's UA override.
|
|
170
291
|
await session.send('Emulation.setDeviceMetricsOverride', {
|
|
171
292
|
width: descriptor.viewport.width,
|
|
172
293
|
height: descriptor.viewport.height,
|
|
@@ -212,6 +333,45 @@ export const test = base.extend({
|
|
|
212
333
|
}
|
|
213
334
|
});
|
|
214
335
|
},
|
|
336
|
+
mouseMove: async ({ page }, use) => {
|
|
337
|
+
// ArkWeb CDP limitation: events dispatched via locator.evaluate() only reach
|
|
338
|
+
// page-script listeners if the listener body contains no closure references and
|
|
339
|
+
// the element has only a single addEventListener call. Any script complexity
|
|
340
|
+
// (outer variable declarations, multiple listeners) causes ArkWeb to route the
|
|
341
|
+
// callback into an isolated CDP execution context where closures are inaccessible,
|
|
342
|
+
// silently suppressing the event. For typical web applications this fixture
|
|
343
|
+
// will not deliver events. Prefer locator.click() / locator.fill() where possible.
|
|
344
|
+
await use(async (x, y, opts) => {
|
|
345
|
+
const steps = Math.max(1, opts?.steps ?? 1);
|
|
346
|
+
for (let i = 0; i < steps; i++) {
|
|
347
|
+
await page.locator(':root').evaluate((root, [x, y]) => {
|
|
348
|
+
const el = root.ownerDocument.elementFromPoint(x, y);
|
|
349
|
+
if (el)
|
|
350
|
+
el.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: x, clientY: y }));
|
|
351
|
+
}, [x, y]);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
mouseDown: async ({ page }, use) => {
|
|
356
|
+
await use(async (x, y) => {
|
|
357
|
+
await page.locator(':root').evaluate((root, [x, y]) => {
|
|
358
|
+
const el = root.ownerDocument.elementFromPoint(x, y);
|
|
359
|
+
if (el)
|
|
360
|
+
el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0, buttons: 1 }));
|
|
361
|
+
}, [x, y]);
|
|
362
|
+
});
|
|
363
|
+
},
|
|
364
|
+
mouseUp: async ({ page }, use) => {
|
|
365
|
+
await use(async (x, y) => {
|
|
366
|
+
await page.locator(':root').evaluate((root, [x, y]) => {
|
|
367
|
+
const el = root.ownerDocument.elementFromPoint(x, y);
|
|
368
|
+
if (el) {
|
|
369
|
+
el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 }));
|
|
370
|
+
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 }));
|
|
371
|
+
}
|
|
372
|
+
}, [x, y]);
|
|
373
|
+
});
|
|
374
|
+
},
|
|
215
375
|
saveStorageState: async ({ page, context }, use) => {
|
|
216
376
|
await use(async (origin) => {
|
|
217
377
|
const derivedOrigin = origin ?? new URL(page.url()).origin;
|
|
@@ -246,6 +406,20 @@ export const test = base.extend({
|
|
|
246
406
|
return { cookies, origins: [{ origin: derivedOrigin, localStorage }] };
|
|
247
407
|
});
|
|
248
408
|
},
|
|
409
|
+
emulateLocale: async ({ page }, use) => {
|
|
410
|
+
await use(async (locale) => {
|
|
411
|
+
await page.addInitScript((loc) => {
|
|
412
|
+
try {
|
|
413
|
+
Object.defineProperty(navigator, 'language', { get: () => loc, configurable: true });
|
|
414
|
+
}
|
|
415
|
+
catch { }
|
|
416
|
+
try {
|
|
417
|
+
Object.defineProperty(navigator, 'languages', { get: () => [loc], configurable: true });
|
|
418
|
+
}
|
|
419
|
+
catch { }
|
|
420
|
+
}, locale);
|
|
421
|
+
});
|
|
422
|
+
},
|
|
249
423
|
loadStorageState: async ({ page, context }, use) => {
|
|
250
424
|
await use(async (state) => {
|
|
251
425
|
if (state.cookies?.length)
|
package/dist/loader.mjs
CHANGED
|
@@ -3,12 +3,18 @@ import { pathToFileURL } from 'node:url';
|
|
|
3
3
|
const FIXTURE_URL = pathToFileURL(resolvePath(import.meta.dirname, 'fixture.mjs')).href;
|
|
4
4
|
const TARGET = '@playwright/test';
|
|
5
5
|
const TEST_FILE = /\.(spec|test)\.[mc]?[tj]sx?$/;
|
|
6
|
+
// Also intercept from fixture helper files (e.g. fixtures.ts, fixtures.mts) that
|
|
7
|
+
// live outside our own compiled dist/ — this covers the common pattern where spec
|
|
8
|
+
// files delegate to a shared fixtures.ts that itself imports @playwright/test.
|
|
9
|
+
const FIXTURE_HELPER = /\bfixtures?\.[mc]?[tj]sx?$/;
|
|
10
|
+
const OWN_DIST_URL = pathToFileURL(resolvePath(import.meta.dirname) + '/').href;
|
|
6
11
|
const PACKAGE_ROOT_URL = pathToFileURL(resolvePath(import.meta.dirname, '..') + '/').href;
|
|
7
12
|
const PROJECT_ANCHOR = pathToFileURL(resolvePath(process.cwd(), 'noop.mjs')).href;
|
|
8
13
|
export async function resolve(specifier, context, nextResolve) {
|
|
9
14
|
if (specifier === TARGET) {
|
|
10
15
|
const parent = context.parentURL ?? '';
|
|
11
|
-
|
|
16
|
+
const isFromOwnDist = parent.startsWith(OWN_DIST_URL);
|
|
17
|
+
if (TEST_FILE.test(parent) || (FIXTURE_HELPER.test(parent) && !isFromOwnDist)) {
|
|
12
18
|
const result = await nextResolve(FIXTURE_URL, context);
|
|
13
19
|
return { url: result.url };
|
|
14
20
|
}
|
package/dist/register.mjs
CHANGED
|
@@ -7,11 +7,19 @@ if (process.platform === 'openharmony') {
|
|
|
7
7
|
// downstream (notably withOpenHarmony in the user's config) should consult
|
|
8
8
|
// OHOS_PW_HOST instead of process.platform, which is about to read 'linux'.
|
|
9
9
|
process.env.OHOS_PW_HOST = '1';
|
|
10
|
-
// Playwright's hostPlatform
|
|
11
|
-
//
|
|
12
|
-
// break.
|
|
13
|
-
//
|
|
14
|
-
//
|
|
10
|
+
// Playwright's calculatePlatform() (hostPlatform.ts:40) only branches on
|
|
11
|
+
// linux/darwin/win32 — on OpenHarmony it falls through to "<unknown>" and
|
|
12
|
+
// registry/hostPlatform consumers break. PLAYWRIGHT_HOST_PLATFORM_OVERRIDE
|
|
13
|
+
// (hostPlatform.ts:41) is the upstream escape hatch; pin to ubuntu24.04-arm64
|
|
14
|
+
// to skip the distro probing that would also reach "<unknown>".
|
|
15
|
+
process.env.PLAYWRIGHT_HOST_PLATFORM_OVERRIDE ??= 'ubuntu24.04-arm64';
|
|
16
|
+
// Beyond calculatePlatform(), playwright-core has 20+ direct process.platform
|
|
17
|
+
// reads on hot paths (userAgent.ts:39 UA string, crPage.ts:940 headful
|
|
18
|
+
// insets, input.ts:182 modifier key, registry/index.ts:479 Unsupported
|
|
19
|
+
// platform, tracing.ts:120 trace metadata). The env override above cannot
|
|
20
|
+
// reach those — we still must advertise 'linux' for the duration of this
|
|
21
|
+
// process. Safe because we connect over CDP and never touch Playwright's
|
|
22
|
+
// bundled browser binaries. Clean fix requires upstream openharmony branch.
|
|
15
23
|
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
16
24
|
register('./loader.mjs', import.meta.url);
|
|
17
25
|
}
|