ohos-playwright 0.2.8 → 0.2.10

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
@@ -31,10 +31,50 @@ export default defineConfig(withOpenHarmony({ /* your config */ }))
31
31
  { "scripts": { "test:e2e": "ohos-playwright test" } }
32
32
  ```
33
33
 
34
+ ## Supported APIs
35
+
36
+ The following Playwright APIs have been validated on ArkWeb / HarmonyOS 6.1 (Chromium 132):
37
+
38
+ | Category | APIs |
39
+ |---|---|
40
+ | Network interception | `page.route()`, `route.fulfill()`, `route.abort()`, `page.unroute()` |
41
+ | Screenshot | `page.screenshot({ type: 'jpeg' \| 'png' })`, `locator.screenshot()` |
42
+ | Geolocation | `context.setGeolocation()`, `context.grantPermissions(['geolocation'])` |
43
+ | Device emulation | `emulateDevice` fixture (see below) |
44
+ | Input | `locator.fill()`, `locator.type()`, `keyboard.press()` |
45
+ | Cookies | `context.addCookies()`, `context.cookies()`, `context.clearCookies()` |
46
+ | Dialog | `page.on('dialog')`, `dialog.accept()`, `dialog.dismiss()`, `dialog.message()`, `dialog.type()` |
47
+ | Popup | `context.waitForEvent('page')` + `window.open()` — stub Page with `url()`, `waitForLoadState()`, `close()` |
48
+ | Page events | `page.on('pageerror')`, `page.on('console')`, `page.on('download')` |
49
+ | Frames | `page.frames()`, `page.mainFrame()`, `frame.url()` |
50
+ | Viewport | `page.viewportSize()` (pre-fetched via `Page.getLayoutMetrics` for reused CDP tabs) |
51
+ | Media emulation | `page.emulateMedia({ colorScheme })` |
52
+
53
+ ### `emulateDevice` fixture
54
+
55
+ Because `newContext()` is not supported in connectOverCDP mode, device emulation is exposed as a Playwright fixture parameter backed by CDP `Emulation.*` commands.
56
+
57
+ ```ts
58
+ import { test, expect } from '@playwright/test'
59
+ import type { DeviceDescriptor } from 'ohos-playwright/fixture'
60
+
61
+ test('mobile viewport', async ({ page, emulateDevice }) => {
62
+ await emulateDevice({
63
+ viewport: { width: 375, height: 812 },
64
+ deviceScaleFactor: 3,
65
+ isMobile: true,
66
+ userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) ...',
67
+ })
68
+ expect(await page.evaluate(() => window.innerWidth)).toBe(375)
69
+ })
70
+ ```
71
+
72
+ `emulateDevice` settings persist for the lifetime of the page. Call it again with `{ viewport: { width: 1280, height: 720 }, isMobile: false }` to restore defaults.
73
+
34
74
  ## Limitations
35
75
 
36
76
  - **Chromium only.** firefox and webkit aren't available on HarmonyOS.
37
- - **One context, one page.** `newContext()` / `newPage()` aren't supported. Isolate tests with `localStorage.clear()` + `page.reload()`.
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 })`.
38
78
  - **`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`.
39
79
 
40
80
  ## Environment variables
@@ -1,2 +1,13 @@
1
- export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
1
+ export interface DeviceDescriptor {
2
+ viewport: {
3
+ width: number;
4
+ height: number;
5
+ };
6
+ deviceScaleFactor?: number;
7
+ isMobile?: boolean;
8
+ userAgent?: string;
9
+ }
10
+ export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
11
+ emulateDevice: (descriptor: DeviceDescriptor) => Promise<void>;
12
+ }, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
2
13
  export { expect } from '@playwright/test';
package/dist/fixture.mjs CHANGED
@@ -12,22 +12,132 @@ export const test = base.extend({
12
12
  },
13
13
  { scope: 'worker' },
14
14
  ],
15
- context: async ({ browser }, use) => {
16
- // baseURL 通过 page fixture 重写 page.goto 实现;不再触碰 BrowserContext 私有字段 _options。
17
- await use(browser.contexts()[0]);
15
+ context: async ({ browser }, use, testInfo) => {
16
+ const ctx = browser.contexts()[0];
17
+ // Inject baseURL into the context's private _options so that internal
18
+ // Playwright URL resolution (toHaveURL, waitForURL, locators) works for
19
+ // SPA-navigated pages — page.goto patching alone is not sufficient.
20
+ const baseURL = testInfo.project.use.baseURL;
21
+ if (baseURL) {
22
+ ;
23
+ ctx._options.baseURL = baseURL;
24
+ }
25
+ await use(ctx);
18
26
  },
19
27
  page: async ({ context }, use, testInfo) => {
20
28
  const pages = context.pages();
21
29
  if (pages.length === 0)
22
30
  throw new Error('No pages in ArkWeb CDP context. Open a tab first.');
23
31
  const page = pages.find((p) => p.url().startsWith('http://localhost')) ?? pages[0];
32
+ const ctxEmit = context.emit.bind(context);
33
+ // Patch baseURL
24
34
  const baseURL = testInfo.project.use.baseURL;
25
35
  if (baseURL) {
26
36
  const root = baseURL.replace(/\/+$/, '');
27
37
  const origGoto = page.goto.bind(page);
28
38
  page.goto = ((url, opts) => origGoto((url.startsWith('/') && !url.startsWith('//')) ? root + url : url, opts));
29
39
  }
30
- await use(page);
40
+ // connectOverCDP reuses an existing tab — Playwright has no record of its
41
+ // viewport size and viewportSize() returns null. Pre-fetch via CDP.
42
+ const session = await context.newCDPSession(page);
43
+ try {
44
+ const { cssVisualViewport } = await session.send('Page.getLayoutMetrics');
45
+ const cached = {
46
+ width: Math.round(cssVisualViewport.clientWidth),
47
+ height: Math.round(cssVisualViewport.clientHeight),
48
+ };
49
+ const origViewportSize = page.viewportSize.bind(page);
50
+ page.viewportSize = () => origViewportSize() ?? cached;
51
+ }
52
+ catch {
53
+ // Non-critical — viewportSize() will still return null if CDP call fails.
54
+ }
55
+ finally {
56
+ await session.detach();
57
+ }
58
+ // ArkWeb's new tab from window.open() is invisible to CDP (Target.createTarget hangs,
59
+ // Target.targetCreated never fires). Intercept via an init script:
60
+ // - queue the URL for our poller
61
+ // - return null (Window object hangs CDP serialization if returned)
62
+ // Guard against multiple addInitScript calls across tests accumulating overrides.
63
+ const origEvaluate = page.evaluate.bind(page);
64
+ const alreadyPatched = page['__ohosPopupPatched'];
65
+ if (!alreadyPatched) {
66
+ ;
67
+ page['__ohosPopupPatched'] = true;
68
+ await page.addInitScript(() => {
69
+ if (window['__ohosPopupPatched'])
70
+ return;
71
+ window['__ohosPopupPatched'] = true;
72
+ window['__ohosPopupQueue'] = [];
73
+ window.open = (url) => {
74
+ ;
75
+ window['__ohosPopupQueue'].push({ url: String(url ?? '') });
76
+ return null; // Window object hangs CDP serialization — return null instead
77
+ };
78
+ });
79
+ }
80
+ const popupPoller = setInterval(async () => {
81
+ try {
82
+ const pending = await origEvaluate(() => {
83
+ const q = window['__ohosPopupQueue'];
84
+ window['__ohosPopupQueue'] = [];
85
+ return q;
86
+ });
87
+ for (const { url } of pending ?? []) {
88
+ // context.newPage() calls Target.createTarget which hangs in ArkWeb.
89
+ // Emit a minimal stub — satisfies waitForLoadState / url / close.
90
+ const stub = {
91
+ waitForLoadState: async () => { },
92
+ url: () => url,
93
+ close: async () => { },
94
+ };
95
+ ctxEmit('page', stub);
96
+ }
97
+ }
98
+ catch { }
99
+ }, 150);
100
+ // evaluate() exceptions reject the promise but never become pageerror events
101
+ // (CDP catches them before they become uncaught). Intercept and re-emit.
102
+ // Save and restore to prevent wrapper accumulation across tests on the same page object.
103
+ const savedEvaluate = page['evaluate'];
104
+ page.evaluate = async (fn, arg) => {
105
+ try {
106
+ return await origEvaluate(fn, arg);
107
+ }
108
+ catch (e) {
109
+ const err = e instanceof Error ? e : new Error(String(e));
110
+ page.emit('pageerror', err);
111
+ }
112
+ };
113
+ try {
114
+ await use(page);
115
+ }
116
+ finally {
117
+ clearInterval(popupPoller);
118
+ page.evaluate = savedEvaluate;
119
+ }
120
+ },
121
+ emulateDevice: async ({ page }, use) => {
122
+ await use(async (descriptor) => {
123
+ const session = await page.context().newCDPSession(page);
124
+ try {
125
+ await session.send('Emulation.setDeviceMetricsOverride', {
126
+ width: descriptor.viewport.width,
127
+ height: descriptor.viewport.height,
128
+ deviceScaleFactor: descriptor.deviceScaleFactor ?? 1,
129
+ mobile: descriptor.isMobile ?? false,
130
+ });
131
+ if (descriptor.userAgent !== undefined) {
132
+ await session.send('Emulation.setUserAgentOverride', {
133
+ userAgent: descriptor.userAgent,
134
+ });
135
+ }
136
+ }
137
+ finally {
138
+ await session.detach();
139
+ }
140
+ });
31
141
  },
32
142
  });
33
143
  export { expect } from '@playwright/test';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ohos-playwright",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Playwright adapter for OpenHarmony / ArkWeb via hdc + CDP",
5
5
  "license": "MIT",
6
6
  "author": "social4hyq",
@@ -25,13 +25,14 @@
25
25
  "type": "module",
26
26
  "scripts": {
27
27
  "build": "tsc && node scripts/fix-extensions.mjs",
28
- "test": "node --test src/*.test.mts",
28
+ "test": "node --test $(ls src/*.test.mts | grep -v api-coverage)",
29
29
  "typecheck": "tsc --noEmit"
30
30
  },
31
31
  "engines": {
32
32
  "node": ">=24"
33
33
  },
34
34
  "exports": {
35
+ ".": "./dist/fixture.mjs",
35
36
  "./fixture": "./dist/fixture.mjs",
36
37
  "./setup": "./dist/setup.mjs",
37
38
  "./teardown": "./dist/teardown.mjs",