ohos-playwright 0.3.5 → 0.5.0

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 CHANGED
@@ -64,12 +64,16 @@ The following Playwright APIs have been validated on ArkWeb / HarmonyOS 6.1 (Chr
64
64
  | Navigation wait | `page.waitForURL()` — string, glob, RegExp, and `history.pushState` client-side navigation |
65
65
  | Frames | `page.frames()`, `page.mainFrame()`, `frame.url()` |
66
66
  | Viewport | `page.viewportSize()` (pre-fetched via `Page.getLayoutMetrics` for reused CDP tabs), `page.setViewportSize()` (applies precisely) |
67
- | Media emulation | `page.emulateMedia({ colorScheme })` |
67
+ | Media emulation | `page.emulateMedia({ colorScheme, reducedMotion, forcedColors, media })` — all four options work; combine freely |
68
+ | Visual snapshot | `expect(page).toHaveScreenshot()`, `expect(locator).toHaveScreenshot()` — use `SNAPSHOT_ENGINE=<tag>` to separate baselines when comparing ArkWeb vs another engine (both produce `*-linux.png` filenames) |
69
+ | API request | `request` fixture (`page`-independent HTTP client), `playwright.request.newContext()` — both work |
70
+ | Locator handler | `page.addLocatorHandler()`, `page.removeLocatorHandler()` — auto-dismisses overlays before Playwright's action retry |
71
+ | HTTP credentials | `browser.newContext({ httpCredentials: { username, password } })` — auto-injects Basic Auth. `ctx.newPage()` requires `PW_CHROMIUM_ATTACH_TO_OTHER=1`; cookie/storage operations work without it. **Note:** do not combine with `page.route()` on the same page — both use `Fetch.enable` internally and credentials will not be injected when a route is active. |
68
72
  | Web workers | `page.workers()` — returns the list of active workers |
69
73
  | WebSocket | `page.routeWebSocket()` (requires Playwright ≥ 1.48) — intercepts WebSocket connections |
70
74
  | Accessibility (CDP) | `newCDPSession` + `Accessibility.getFullAXTree` — returns the full AX node tree |
71
75
  | 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 **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. |
76
+ | Hover events | `locator.hover()` — fires `mouseover` / `mouseenter` event listeners **and** activates the CSS `:hover` pseudo-class via `boundingBox()` + `page.mouse.move()` (real `Input.dispatchMouseEvent` path) |
73
77
  | Locale (partial) | `emulateLocale(tag)` fixture — rewrites `navigator.language` / `navigator.languages` via `addInitScript`; does not affect HTTP `Accept-Language` or browser UI locale |
74
78
  | 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
79
  | Service Workers | `navigator.serviceWorker.register()` — works on HTTPS pages; `navigator.serviceWorker` is `undefined` on non-secure origins (`data:`, `about:blank`) as in all browsers |
@@ -77,7 +81,7 @@ The following Playwright APIs have been validated on ArkWeb / HarmonyOS 6.1 (Chr
77
81
 
78
82
  ### `emulateDevice` fixture
79
83
 
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.
84
+ Because `ctx.newPage()` requires `PW_CHROMIUM_ATTACH_TO_OTHER=1` (see "Multi-context" in Limitations), device emulation is exposed as a Playwright fixture parameter backed by CDP `Emulation.*` commands.
81
85
 
82
86
  ```ts
83
87
  import { test, expect } from '@playwright/test'
@@ -121,7 +125,9 @@ Coordinates are CSS pixels relative to the viewport (same as `touchscreen.tap`).
121
125
  ## Limitations
122
126
 
123
127
  - **Chromium only.** firefox and webkit aren't available on HarmonyOS.
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 })`.
128
+ - **`browser.newContext()` works; `ctx.newPage()` requires opt-in.** `browser.newContext()` is available without any opt-in use it freely for `addCookies` / `storageState()` / `clearCookies()`. However, `ctx.newPage()` on ArkWeb throws Playwright's natural `Cannot read properties of undefined (reading '_page')` because ArkWeb's `Target.createTarget` returns `type: 'other'` (not `'page'`), causing Playwright's `crBrowser._onAttachedToTarget` to skip the new target. To opt in to full `ctx.newPage()` support, set `process.env.PW_CHROMIUM_ATTACH_TO_OTHER = '1'` **before** importing `@playwright/test` — Playwright's upstream escape hatch then treats `'other'` targets as pages. 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 })`.
129
+
130
+ > **v0.5.0 breaking change:** Previously `browser.newContext()` threw a friendly error when `PW_CHROMIUM_ATTACH_TO_OTHER` was unset. Now it succeeds for cookie/storage operations; `ctx.newPage()` will throw Playwright's natural `_page undefined` error instead.
125
131
 
126
132
  **Multi-worker mode.** To run tests in parallel across multiple workers, two things are required together:
127
133
 
@@ -132,15 +138,16 @@ Coordinates are CSS pixels relative to the viewport (same as `touchscreen.tap`).
132
138
  import { test, expect } from 'ohos-playwright/parallel'
133
139
  ```
134
140
 
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.
141
+ 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 propagation) apply identically.
136
142
 
137
143
  Trade-offs to be aware of:
138
144
  - **`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
145
  - **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.
140
146
  - **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).
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.
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.
147
+ - **`locator.hover()` activates CSS `:hover`.** The override calls `boundingBox()` + `page.mouse.move()` (real `Input.dispatchMouseEvent` path). `page.mouse.move()` / `page.mouse.down()` / `page.mouse.up()` deliver DOM events on ArkWeb identically to stock Chromium.
143
148
  - **`emulateDevice({ isMobile: true })` does not apply the viewport** — see the note in the `emulateDevice` fixture section above.
149
+ - **`context.recordVideo` is not supported.** ArkWeb does not implement `Page.startScreencast` (the CDP command Playwright uses for video recording). When running against a remote Chromium via `OHOS_PW_CDP_URL`, Playwright's ffmpeg binary also cannot execute on HarmonyOS without signing. Use `page.screenshot()` for visual verification instead.
150
+ - **`launchOptions.proxy` is not applicable** in `connectOverCDP` mode — there is no launch phase where proxy settings can be injected. Use `page.route()` to intercept and rewrite requests at the Playwright layer.
144
151
  - **`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.
145
152
  - **`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`.
146
153
 
@@ -219,7 +226,7 @@ Then set `OHOS_PW_BUNDLE` (e.g. `OHOS_PW_BUNDLE=com.quark.ohosbrowser`).
219
226
 
220
227
  **`CDP probe failed`** — leftover `hdc` forward rule from a prior crashed run. `hdc fport ls` to inspect, `hdc fport rm tcp:<port> localabstract:<socket>` to clear.
221
228
 
222
- **`page.goto('/foo')` doesn't prepend baseURL** — Playwright's standard behavior is `/foo` → `http://localhost:5173/foo`. If it's not working, check `use.baseURL` in `playwright.config.ts`.
229
+ **`page.goto('/foo')` doesn't prepend baseURL** — Playwright's standard behavior is `/foo` → `http://localhost:5173/foo`. The adapter injects `baseURL` directly into the context's internal options so Playwright's own URL resolution applies. If it's not working, verify `use.baseURL` is set in `playwright.config.ts` and that `withOpenHarmony()` wraps the config.
223
230
 
224
231
  ## License
225
232
 
@@ -31,11 +31,6 @@ export declare function installPageWrappers(page: Page, context: BrowserContext,
31
31
  export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
32
32
  emulateDevice: (descriptor: DeviceDescriptor) => Promise<void>;
33
33
  tap: (x: number, y: number) => Promise<void>;
34
- mouseMove: (x: number, y: number, opts?: {
35
- steps?: number;
36
- }) => Promise<void>;
37
- mouseDown: (x: number, y: number) => Promise<void>;
38
- mouseUp: (x: number, y: number) => Promise<void>;
39
34
  saveStorageState: (origin?: string) => Promise<StorageState>;
40
35
  loadStorageState: (state: StorageState) => Promise<void>;
41
36
  emulateLocale: (locale: string) => Promise<void>;
package/dist/fixture.mjs CHANGED
@@ -18,14 +18,6 @@ function readInfo() {
18
18
  function readEndpoint() { return readInfo().endpoint; }
19
19
  export async function installPageWrappers(page, context, baseURL) {
20
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
21
  // connectOverCDP reuses an existing tab — Playwright has no record of its
30
22
  // viewport size and viewportSize() returns null. Pre-fetch via CDP.
31
23
  const session = await context.newCDPSession(page);
@@ -42,18 +34,6 @@ export async function installPageWrappers(page, context, baseURL) {
42
34
  catch {
43
35
  // Non-critical — viewportSize() will still return null if CDP call fails.
44
36
  }
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
37
  }
58
38
  finally {
59
39
  await session.detach();
@@ -110,39 +90,19 @@ export async function installPageWrappers(page, context, baseURL) {
110
90
  }
111
91
  return null;
112
92
  };
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.
93
+ // Override locator().hover() to use page.mouse.move() directly so CSS :hover
94
+ // activates. Playwright's built-in hover() goes through a visibility check path
95
+ // that can hang on ArkWeb; bypassing it is safe because mouse.move() has been
96
+ // verified to deliver both DOM events and :hover activation (ab-hover-css probe).
118
97
  const savedLocator = page['locator'];
119
98
  const origLocator = page.locator.bind(page);
120
99
  page.locator = (...args) => {
121
100
  const loc = origLocator(...args);
122
101
  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
- }
102
+ const box = await loc.boundingBox();
103
+ if (!box)
104
+ return;
105
+ await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
146
106
  };
147
107
  return loc;
148
108
  };
@@ -206,46 +166,24 @@ export async function installPageWrappers(page, context, baseURL) {
206
166
  page.evaluate = savedEvaluate;
207
167
  page.locator = savedLocator;
208
168
  if (opts?.navigateTo) {
209
- // Reset the shared tab before restoring gotokeeps the connection alive.
210
- // page.close() would terminate the ArkWeb DevTools socket.
169
+ // Reset the shared tab to a neutral state page.close() would terminate
170
+ // the ArkWeb DevTools socket so we navigate instead.
211
171
  try {
212
172
  await page.goto(opts.navigateTo);
213
173
  }
214
174
  catch { }
215
175
  }
216
- ;
217
- page.goto = savedGoto;
218
176
  };
219
177
  }
220
178
  export const test = base.extend({
221
179
  browser: [
222
180
  async ({}, use) => {
223
181
  const browser = await chromium.connectOverCDP(readEndpoint());
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
- }
245
- }
246
- else {
247
- await use(browser);
248
- }
182
+ // browser.newContext() + addCookies / storageState() work in connectOverCDP mode.
183
+ // ctx.newPage() requires PW_CHROMIUM_ATTACH_TO_OTHER=1 on ArkWeb (Target.createTarget
184
+ // returns type='other' so Playwright skips the new target); without it Playwright
185
+ // throws its natural "_page undefined" error.
186
+ await use(browser);
249
187
  },
250
188
  { scope: 'worker' },
251
189
  ],
@@ -342,45 +280,6 @@ export const test = base.extend({
342
280
  }
343
281
  });
344
282
  },
345
- mouseMove: async ({ page }, use) => {
346
- // ArkWeb CDP limitation: events dispatched via locator.evaluate() only reach
347
- // page-script listeners if the listener body contains no closure references and
348
- // the element has only a single addEventListener call. Any script complexity
349
- // (outer variable declarations, multiple listeners) causes ArkWeb to route the
350
- // callback into an isolated CDP execution context where closures are inaccessible,
351
- // silently suppressing the event. For typical web applications this fixture
352
- // will not deliver events. Prefer locator.click() / locator.fill() where possible.
353
- await use(async (x, y, opts) => {
354
- const steps = Math.max(1, opts?.steps ?? 1);
355
- for (let i = 0; i < steps; i++) {
356
- await page.locator(':root').evaluate((root, [x, y]) => {
357
- const el = root.ownerDocument.elementFromPoint(x, y);
358
- if (el)
359
- el.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: x, clientY: y }));
360
- }, [x, y]);
361
- }
362
- });
363
- },
364
- mouseDown: 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('mousedown', { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0, buttons: 1 }));
370
- }, [x, y]);
371
- });
372
- },
373
- mouseUp: async ({ page }, use) => {
374
- await use(async (x, y) => {
375
- await page.locator(':root').evaluate((root, [x, y]) => {
376
- const el = root.ownerDocument.elementFromPoint(x, y);
377
- if (el) {
378
- el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 }));
379
- el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 }));
380
- }
381
- }, [x, y]);
382
- });
383
- },
384
283
  saveStorageState: async ({ page, context }, use) => {
385
284
  await use(async (origin) => {
386
285
  const derivedOrigin = origin ?? new URL(page.url()).origin;
@@ -2,11 +2,6 @@ import type { BrowserContext, Page } from '@playwright/test';
2
2
  export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
3
3
  emulateDevice: (descriptor: import("./fixture.mts").DeviceDescriptor) => Promise<void>;
4
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
5
  saveStorageState: (origin?: string) => Promise<import("./fixture.mts").StorageState>;
11
6
  loadStorageState: (state: import("./fixture.mts").StorageState) => Promise<void>;
12
7
  emulateLocale: (locale: string) => Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ohos-playwright",
3
- "version": "0.3.5",
3
+ "version": "0.5.0",
4
4
  "description": "Playwright adapter for OpenHarmony / ArkWeb via hdc + CDP",
5
5
  "license": "MIT",
6
6
  "author": "social4hyq",