ohos-playwright 0.2.10 → 0.2.11

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
@@ -38,16 +38,22 @@ The following Playwright APIs have been validated on ArkWeb / HarmonyOS 6.1 (Chr
38
38
  | Category | APIs |
39
39
  |---|---|
40
40
  | Network interception | `page.route()`, `route.fulfill()`, `route.abort()`, `page.unroute()` |
41
+ | Network headers | `page.setExtraHTTPHeaders()` — custom headers reach the server |
42
+ | Network conditions | `context.setOffline(true / false)` — actually applies (`ERR_INTERNET_DISCONNECTED` ↔ reachable) |
41
43
  | Screenshot | `page.screenshot({ type: 'jpeg' \| 'png' })`, `locator.screenshot()` |
44
+ | PDF | `page.pdf()` — Chromium-only API, implemented by ArkWeb (produces a valid `%PDF` document) |
45
+ | Tracing | `context.tracing.start()` / `stop({ path })` — works under `connectOverCDP`; the resulting zip contains trace + screenshots + source maps |
42
46
  | Geolocation | `context.setGeolocation()`, `context.grantPermissions(['geolocation'])` |
43
- | Device emulation | `emulateDevice` fixture (see below) |
44
- | Input | `locator.fill()`, `locator.type()`, `keyboard.press()` |
47
+ | Device emulation | `emulateDevice` fixture (see below — `isMobile: false` for a precise viewport) |
48
+ | Input | `locator.fill()`, `locator.type()`, `keyboard.press()`, `page.selectOption()` |
49
+ | Drag & drop | `locator.dragTo()` — triggers a `drop` event |
50
+ | File upload | `page.setInputFiles()` — fires `change`, file content readable |
45
51
  | Cookies | `context.addCookies()`, `context.cookies()`, `context.clearCookies()` |
46
52
  | Dialog | `page.on('dialog')`, `dialog.accept()`, `dialog.dismiss()`, `dialog.message()`, `dialog.type()` |
47
53
  | Popup | `context.waitForEvent('page')` + `window.open()` — stub Page with `url()`, `waitForLoadState()`, `close()` |
48
54
  | Page events | `page.on('pageerror')`, `page.on('console')`, `page.on('download')` |
49
55
  | Frames | `page.frames()`, `page.mainFrame()`, `frame.url()` |
50
- | Viewport | `page.viewportSize()` (pre-fetched via `Page.getLayoutMetrics` for reused CDP tabs) |
56
+ | Viewport | `page.viewportSize()` (pre-fetched via `Page.getLayoutMetrics` for reused CDP tabs), `page.setViewportSize()` (applies precisely) |
51
57
  | Media emulation | `page.emulateMedia({ colorScheme })` |
52
58
 
53
59
  ### `emulateDevice` fixture
@@ -58,12 +64,12 @@ Because `newContext()` is not supported in connectOverCDP mode, device emulation
58
64
  import { test, expect } from '@playwright/test'
59
65
  import type { DeviceDescriptor } from 'ohos-playwright/fixture'
60
66
 
61
- test('mobile viewport', async ({ page, emulateDevice }) => {
67
+ test('precise viewport', async ({ page, emulateDevice }) => {
68
+ // isMobile: false applies the viewport precisely (window.innerWidth === 375).
62
69
  await emulateDevice({
63
70
  viewport: { width: 375, height: 812 },
64
71
  deviceScaleFactor: 3,
65
- isMobile: true,
66
- userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) ...',
72
+ isMobile: false,
67
73
  })
68
74
  expect(await page.evaluate(() => window.innerWidth)).toBe(375)
69
75
  })
@@ -71,10 +77,38 @@ test('mobile viewport', async ({ page, emulateDevice }) => {
71
77
 
72
78
  `emulateDevice` settings persist for the lifetime of the page. Call it again with `{ viewport: { width: 1280, height: 720 }, isMobile: false }` to restore defaults.
73
79
 
80
+ > **⚠️ `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.
82
+
83
+ ### `tap` fixture
84
+
85
+ ArkWeb fully implements touch input via CDP `Input.dispatchTouchEvent`, but Playwright's `page.touchscreen.tap()` refuses to run unless the context was created with `hasTouch: true` — impossible in single-context reuse mode. The `tap` fixture exposes a CDP-backed tap that works regardless:
86
+
87
+ ```ts
88
+ import { test, expect } from '@playwright/test'
89
+
90
+ test('tap a button', async ({ page, tap }) => {
91
+ await page.goto('http://localhost:5173/')
92
+ const box = await page.locator('#submit').boundingBox()
93
+ await tap(box.x + box.width / 2, box.y + box.height / 2)
94
+ await expect(page.locator('#result')).toHaveText('Done')
95
+ })
96
+ ```
97
+
98
+ Coordinates are CSS pixels relative to the viewport (same as `touchscreen.tap`). Each call issues a `touchStart` + `touchEnd` pair, which also synthesises a `click` event on the targeted element — so it works for both touch handlers and click handlers.
99
+
74
100
  ## Limitations
75
101
 
76
102
  - **Chromium only.** firefox and webkit aren't available on HarmonyOS.
77
- - **One context, one page.** `newContext()` / `newPage()` aren't supported. Isolate tests with `localStorage.clear()` + `page.reload()`. For device emulation use the `emulateDevice` fixture instead of `browser.newContext({ ...device })`.
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.
111
+ - **`emulateDevice({ isMobile: true })` does not apply the viewport** — see the note in the `emulateDevice` fixture section above.
78
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`.
79
113
 
80
114
  ## Environment variables
@@ -7,7 +7,26 @@ export interface DeviceDescriptor {
7
7
  isMobile?: boolean;
8
8
  userAgent?: string;
9
9
  }
10
+ export interface StorageState {
11
+ cookies: {
12
+ name: string;
13
+ value: string;
14
+ domain?: string;
15
+ path?: string;
16
+ url?: string;
17
+ }[];
18
+ origins: {
19
+ origin: string;
20
+ localStorage: {
21
+ name: string;
22
+ value: string;
23
+ }[];
24
+ }[];
25
+ }
10
26
  export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
11
27
  emulateDevice: (descriptor: DeviceDescriptor) => Promise<void>;
28
+ tap: (x: number, y: number) => Promise<void>;
29
+ saveStorageState: (origin?: string) => Promise<StorageState>;
30
+ loadStorageState: (state: StorageState) => Promise<void>;
12
31
  }, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
13
32
  export { expect } from '@playwright/test';
package/dist/fixture.mjs CHANGED
@@ -8,7 +8,25 @@ export const test = base.extend({
8
8
  browser: [
9
9
  async ({}, use) => {
10
10
  const browser = await chromium.connectOverCDP(readEndpoint());
11
- await use(browser);
11
+ // ArkWeb CDP does not implement Target.createBrowserContext — connectOverCDP
12
+ // reuses the single existing context. browser.newContext() returns without
13
+ // throwing but produces an empty shell (0 pages) that silently fails every
14
+ // subsequent operation. Intercept and throw an explicit, actionable error
15
+ // so users aren't misled by the false-positive success.
16
+ const origNewContext = browser.newContext.bind(browser);
17
+ browser.newContext = (() => {
18
+ throw new Error('browser.newContext() is not supported in ArkWeb CDP mode (single context only). ' +
19
+ 'Tests share one context and one page — isolate with localStorage.clear() + page.reload(). ' +
20
+ 'See ohos-playwright README "Limitations" section.');
21
+ });
22
+ try {
23
+ await use(browser);
24
+ }
25
+ finally {
26
+ // Restore in case the browser object is reused across workers.
27
+ ;
28
+ browser.newContext = origNewContext;
29
+ }
12
30
  },
13
31
  { scope: 'worker' },
14
32
  ],
@@ -122,6 +140,16 @@ export const test = base.extend({
122
140
  await use(async (descriptor) => {
123
141
  const session = await page.context().newCDPSession(page);
124
142
  try {
143
+ // ArkWeb note: with mobile:true, Emulation.setDeviceMetricsOverride enables
144
+ // the mobile layout-viewport path and the passed width/height are ignored
145
+ // (window.innerWidth reads 980 regardless). Use isMobile:false for a
146
+ // precise viewport. See README "emulateDevice fixture" section.
147
+ if (descriptor.isMobile) {
148
+ console.warn('[ohos-playwright] emulateDevice({ isMobile: true }): ArkWeb renders at the 980px ' +
149
+ 'default mobile layout viewport; the passed width/height will NOT apply. ' +
150
+ 'Use isMobile: false for a precise viewport.');
151
+ }
152
+ // ArkWeb note: setUserAgentOverride is acked but ignored — UA cannot be changed via CDP.
125
153
  await session.send('Emulation.setDeviceMetricsOverride', {
126
154
  width: descriptor.viewport.width,
127
155
  height: descriptor.viewport.height,
@@ -139,5 +167,84 @@ export const test = base.extend({
139
167
  }
140
168
  });
141
169
  },
170
+ tap: async ({ page }, use) => {
171
+ await use(async (x, y) => {
172
+ const session = await page.context().newCDPSession(page);
173
+ try {
174
+ // ArkWeb fully implements Input.dispatchTouchEvent (verified: touchstart
175
+ // touches=1 | touchend received). Playwright's touchscreen.tap() requires
176
+ // hasTouch at context creation, which is impossible in single-context reuse
177
+ // mode. This wrapper issues a press+release pair directly.
178
+ // TouchPoint.state isn't in Playwright's TS types but is part of the CDP
179
+ // spec and accepted by ArkWeb; cast to any to bypass the narrowed type.
180
+ await session.send('Input.dispatchTouchEvent', {
181
+ type: 'touchStart',
182
+ touchPoints: [{ x, y, id: 0, state: 'pressed' }],
183
+ modifiers: 0,
184
+ timeStamp: 0,
185
+ });
186
+ await session.send('Input.dispatchTouchEvent', {
187
+ type: 'touchEnd',
188
+ touchPoints: [{ x, y, id: 0, state: 'released' }],
189
+ modifiers: 0,
190
+ timeStamp: 0,
191
+ });
192
+ }
193
+ finally {
194
+ await session.detach();
195
+ }
196
+ });
197
+ },
198
+ saveStorageState: async ({ page, context }, use) => {
199
+ await use(async (origin) => {
200
+ const derivedOrigin = origin ?? new URL(page.url()).origin;
201
+ // Cookies come from the context (works in reuse mode). Filter to the
202
+ // target origin so we don't serialise unrelated domains that happen to
203
+ // share the single ArkWeb context. Match by hostname (cookies store
204
+ // domain without port; e.g. cookie.domain='127.0.0.1' for host 127.0.0.1:port).
205
+ const hostname = new URL(derivedOrigin).hostname;
206
+ const allCookies = await context.cookies();
207
+ const cookies = allCookies
208
+ .filter((c) => {
209
+ if (!origin)
210
+ return true;
211
+ const d = c.domain ?? '';
212
+ // cookie domain may have leading '.' (host-only=false) — strip it.
213
+ const bare = d.startsWith('.') ? d.slice(1) : d;
214
+ return bare === hostname || hostname.endsWith(bare);
215
+ })
216
+ .map((c) => ({ name: c.name, value: c.value, domain: c.domain, path: c.path }));
217
+ // localStorage must be read in a same-origin document; navigate if needed.
218
+ const cur = page.url();
219
+ if (!cur.startsWith(derivedOrigin))
220
+ await page.goto(derivedOrigin + '/');
221
+ const localStorage = await page.evaluate(() => {
222
+ const items = [];
223
+ for (let i = 0; i < window.localStorage.length; i++) {
224
+ const name = window.localStorage.key(i);
225
+ items.push({ name, value: window.localStorage.getItem(name) });
226
+ }
227
+ return items;
228
+ });
229
+ return { cookies, origins: [{ origin: derivedOrigin, localStorage }] };
230
+ });
231
+ },
232
+ loadStorageState: async ({ page, context }, use) => {
233
+ await use(async (state) => {
234
+ if (state.cookies?.length)
235
+ await context.addCookies(state.cookies);
236
+ for (const o of state.origins ?? []) {
237
+ if (!o.localStorage?.length)
238
+ continue;
239
+ // localStorage writes must run in a same-origin document.
240
+ if (!page.url().startsWith(o.origin))
241
+ await page.goto(o.origin + '/');
242
+ await page.evaluate((items) => {
243
+ for (const { name, value } of items)
244
+ window.localStorage.setItem(name, value);
245
+ }, o.localStorage);
246
+ }
247
+ });
248
+ },
142
249
  });
143
250
  export { expect } from '@playwright/test';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ohos-playwright",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "Playwright adapter for OpenHarmony / ArkWeb via hdc + CDP",
5
5
  "license": "MIT",
6
6
  "author": "social4hyq",