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 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. Note that `userAgent` is also not applied (`Emulation.setUserAgentOverride` is acked but ignored by ArkWeb); the browser UA cannot be changed via CDP.
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
- - **One context, one page.** `newContext()` / `newPage()` aren't supported (both throw an explicit error). Isolate tests with `localStorage.clear()` + `page.reload()`. For device emulation use the `emulateDevice` fixture instead of `browser.newContext({ ...device })`.
104
- - **`locator.hover()` hangs** on ArkWeb (CDP `Input.dispatchMouseEvent` mouseMoved blocks until the Playwright timeout). Use `:focus`-driven styles or a direct `click()` instead of hover-driven assertions.
105
- - **`page.goBack()` / `page.goForward()` hang** (CDP history navigation never resolves). Re-navigate with `page.goto()` instead.
106
- - **`Emulation.setUserAgentOverride` is ignored** the command is acked but `navigator.userAgent` is unchanged. The browser UA cannot be changed via CDP.
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
- - **`process.platform` reads `'linux'`** during the run we patch it because Playwright's hostPlatform detection only branches on linux/darwin/win32 and falls through to `<unknown>` on openharmony. For real platform checks use `process.env.OHOS_PW_HOST`.
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
 
@@ -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
- const { cssVisualViewport } = await session.send('Page.getLayoutMetrics');
71
- const cached = {
72
- width: Math.round(cssVisualViewport.clientWidth),
73
- height: Math.round(cssVisualViewport.clientHeight),
74
- };
75
- const origViewportSize = page.viewportSize.bind(page);
76
- page.viewportSize = () => origViewportSize() ?? cached;
77
- }
78
- catch {
79
- // Non-critical — viewportSize() will still return null if CDP call fails.
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 is acked but ignored UA cannot be changed via CDP.
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
- if (TEST_FILE.test(parent)) {
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 detection only branches on linux/darwin/win32.
11
- // On OpenHarmony it falls through to "<unknown>" and various code paths
12
- // break. We connect over CDP and never touch Playwright's bundled browser
13
- // binaries, so it's safe to advertise linux for the duration of this
14
- // process.
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ohos-playwright",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Playwright adapter for OpenHarmony / ArkWeb via hdc + CDP",
5
5
  "license": "MIT",
6
6
  "author": "social4hyq",