ohos-playwright 0.1.1 → 0.2.1
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 +83 -39
- package/dist/cli.d.mts +2 -0
- package/dist/cli.mjs +44 -0
- package/dist/config.d.mts +2 -0
- package/dist/config.mjs +24 -0
- package/dist/fixture.d.mts +2 -0
- package/dist/fixture.mjs +44 -0
- package/dist/info-path.d.mts +7 -0
- package/dist/info-path.mjs +5 -0
- package/dist/loader.d.mts +15 -0
- package/dist/loader.mjs +20 -0
- package/dist/register.d.mts +1 -0
- package/dist/register.mjs +17 -0
- package/dist/setup.d.mts +13 -0
- package/dist/setup.mjs +219 -0
- package/dist/teardown.d.mts +1 -0
- package/dist/teardown.mjs +31 -0
- package/package.json +18 -15
- package/SKILL.md +0 -155
- package/src/cli.mjs +0 -42
- package/src/config.d.mts +0 -3
- package/src/config.mjs +0 -23
- package/src/fixture.mjs +0 -49
- package/src/info-path.mjs +0 -7
- package/src/loader.mjs +0 -46
- package/src/register.mjs +0 -20
- package/src/setup.mjs +0 -203
- package/src/teardown.mjs +0 -24
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# ohos-playwright
|
|
2
2
|
|
|
3
|
-
让 Playwright 在 OpenHarmony 上跑 e2e
|
|
3
|
+
让 Playwright 在 OpenHarmony / ArkWeb 上跑 e2e,同一份 `playwright.config.ts` 和 spec 在 Windows / Linux / macOS 上仍按原版 Playwright 跑,**不需要写两份**。
|
|
4
4
|
|
|
5
5
|
## 它解决什么问题
|
|
6
6
|
|
|
7
|
-
OpenHarmony 上没有官方的 Playwright
|
|
7
|
+
OpenHarmony 上没有官方的 Playwright:没有适配的浏览器二进制,Playwright 的平台检测在 `openharmony` 上直接崩。
|
|
8
8
|
|
|
9
|
-
ohos-playwright
|
|
9
|
+
ohos-playwright **不让 Playwright 自己启动浏览器**,而是通过 `hdc` 把设备上正在运行的系统浏览器的 CDP 调试通道转发回本机,让 Playwright "接管"它。spec 一行不动,config 只多一层包装。
|
|
10
10
|
|
|
11
11
|
## 快速开始
|
|
12
12
|
|
|
@@ -16,7 +16,7 @@ ohos-playwright 换了个思路 —— **不让 Playwright 自己启动浏览器
|
|
|
16
16
|
pnpm add -D ohos-playwright @playwright/test
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
> `@playwright/test` 是 peer dependency,版本 `>=1.59.0`。
|
|
19
|
+
> `@playwright/test` 是 peer dependency,版本 `>=1.59.0`。Node ≥ 24。
|
|
20
20
|
|
|
21
21
|
### 2. 包一层 config
|
|
22
22
|
|
|
@@ -30,23 +30,22 @@ export default defineConfig(withOpenHarmony({
|
|
|
30
30
|
use: { baseURL: 'http://localhost:5173' },
|
|
31
31
|
projects: [
|
|
32
32
|
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
33
|
-
// firefox / webkit 在 OpenHarmony
|
|
33
|
+
// firefox / webkit 在 OpenHarmony 上会被自动裁掉
|
|
34
34
|
],
|
|
35
|
-
// ...其他正常的 Playwright 配置
|
|
36
35
|
}))
|
|
37
36
|
```
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
`withOpenHarmony` 在非 OH 主机上原样返回 config;在 OH 上注入 setup/teardown、锁 workers=1、过滤非 chromium project。
|
|
40
39
|
|
|
41
|
-
### 3.
|
|
40
|
+
### 3. npm script
|
|
42
41
|
|
|
43
42
|
```json
|
|
44
43
|
{ "scripts": { "test:e2e": "ohos-playwright test" } }
|
|
45
44
|
```
|
|
46
45
|
|
|
47
|
-
`ohos-playwright`
|
|
46
|
+
`ohos-playwright` 在非 OH 上等价于 `playwright`,所有参数透传。
|
|
48
47
|
|
|
49
|
-
### 4. spec
|
|
48
|
+
### 4. spec 不用动
|
|
50
49
|
|
|
51
50
|
```ts
|
|
52
51
|
import { test, expect } from '@playwright/test'
|
|
@@ -63,56 +62,101 @@ test('something', async ({ page }) => {
|
|
|
63
62
|
pnpm test:e2e
|
|
64
63
|
```
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
首次跑会自动找设备、拉起浏览器、转发 CDP 端口。如果本机就是 OpenHarmony 设备,会通过 `param get persist.hdc.port` 自动发现端口并连接,无需手动配置。
|
|
67
66
|
|
|
68
67
|
## 工作原理
|
|
69
68
|
|
|
70
|
-
**非 OpenHarmony 主机**:什么都不做,等价于 stock Playwright
|
|
69
|
+
**非 OpenHarmony 主机**:什么都不做,等价于 stock Playwright。
|
|
71
70
|
|
|
72
71
|
**OpenHarmony 主机**:
|
|
73
72
|
|
|
74
|
-
- **复用而非启动**:Playwright
|
|
75
|
-
- **Spec
|
|
76
|
-
- **配置自动收紧**:`workers` 锁 1、firefox
|
|
77
|
-
-
|
|
73
|
+
- **复用而非启动**:Playwright 不再自己起浏览器,接管设备上正在跑的系统浏览器,通过 `hdc` 把 CDP 通道转发到本机。
|
|
74
|
+
- **Spec 无感**:底层透明替换 `@playwright/test` 导入,走 CDP 复用设备上现有 page。
|
|
75
|
+
- **配置自动收紧**:`workers` 锁 1、firefox/webkit 裁掉、globalSetup/Teardown 自动注入。
|
|
76
|
+
- **智能设备发现**(v0.2.0):
|
|
77
|
+
1. 已有连接 → 直接复用
|
|
78
|
+
2. 本机就是 OH 设备 → 读 `persist.hdc.port` 秒连
|
|
79
|
+
3. LAN 广播 `hdc discover` → 找独立设备
|
|
80
|
+
4. 找不到 → TTY 提示手动输入 / CI 直接报错
|
|
81
|
+
|
|
82
|
+
## 设备连接
|
|
83
|
+
|
|
84
|
+
### 本机即设备(一台 OH 机器跑全部)
|
|
85
|
+
|
|
86
|
+
无需额外配置,`ensureDeviceConnected()` 会自动通过 `param get persist.hdc.port` 连上本机 hdc 守护进程。
|
|
87
|
+
|
|
88
|
+
### 固定无线调试端口(一劳永逸)
|
|
89
|
+
|
|
90
|
+
无线调试每次开关端口都会变。设固定端口后不再需要看屏幕:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
# 先连上(USB 或看屏幕临时端口)
|
|
94
|
+
hdc tconn 127.0.0.1:<临时端口>
|
|
95
|
+
|
|
96
|
+
# 设固定端口(会重启设备)
|
|
97
|
+
hdc tmode port 5555
|
|
98
|
+
|
|
99
|
+
# 之后永远用固定端口
|
|
100
|
+
hdc tconn 127.0.0.1:5555
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
设好后 `param get persist.hdc.port` 也返回固定值,ohos-playwright 自动识别。
|
|
104
|
+
|
|
105
|
+
### 跳过自动连接
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
OHOS_PW_AUTO_CONNECT=0 pnpm test:e2e
|
|
109
|
+
```
|
|
78
110
|
|
|
79
111
|
## 环境变量
|
|
80
112
|
|
|
81
|
-
| 变量 |
|
|
82
|
-
|
|
83
|
-
| `OHOS_PW_HDC` | `/data/service/hnp/bin/hdc` |
|
|
84
|
-
| `OHOS_PW_BUNDLE` | `com.huawei.hmos.browser` |
|
|
85
|
-
| `OHOS_PW_LAUNCH_URL` | `http://localhost:5173` |
|
|
86
|
-
| `OHOS_PW_INFO_PATH` | `os.tmpdir()/ohos-playwright-cdp.json` |
|
|
87
|
-
| `OHOS_PW_AUTO_CONNECT` | (未设) |
|
|
113
|
+
| 变量 | 默认值 | 说明 |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `OHOS_PW_HDC` | `/data/service/hnp/bin/hdc` | hdc 二进制路径 |
|
|
116
|
+
| `OHOS_PW_BUNDLE` | `com.huawei.hmos.browser` | 接管的目标浏览器 bundle |
|
|
117
|
+
| `OHOS_PW_LAUNCH_URL` | `http://localhost:5173` | 浏览器首次启动时的导航 URL |
|
|
118
|
+
| `OHOS_PW_INFO_PATH` | `os.tmpdir()/ohos-playwright-cdp.json` | CDP info 文件位置 |
|
|
119
|
+
| `OHOS_PW_AUTO_CONNECT` | (未设) | 设为 `0` 跳过自动 discover/tconn 流程 |
|
|
88
120
|
| `OHOS_PW_HOST` | 自动 | 内部标志位,**不要手动设** |
|
|
89
121
|
|
|
90
122
|
## 约束
|
|
91
123
|
|
|
92
|
-
设备和 ArkWeb
|
|
124
|
+
设备和 ArkWeb 本身的硬约束:
|
|
93
125
|
|
|
94
|
-
- **`workers: 1
|
|
95
|
-
- **不能并发 project**:firefox
|
|
96
|
-
- **不能 `newContext` / `newPage
|
|
97
|
-
- **测试结束不关浏览器**:浏览器进程归 OS
|
|
98
|
-
- **`process.platform`
|
|
99
|
-
-
|
|
126
|
+
- **`workers: 1`**:设备上只有一个浏览器实例。
|
|
127
|
+
- **不能并发 project**:firefox/webkit 在 ArkWeb 上跑不了,自动裁掉。
|
|
128
|
+
- **不能 `newContext` / `newPage`**:全进程复用一个 context、一个 page。用例隔离用 `localStorage.clear()` + `page.reload()`。
|
|
129
|
+
- **测试结束不关浏览器**:浏览器进程归 OS 管。
|
|
130
|
+
- **`process.platform` 被改成 `'linux'`**:判断真实主机用 `process.env.OHOS_PW_HOST`。
|
|
131
|
+
- **设备需要开发者模式 + 无线调试或 USB 调试**。
|
|
100
132
|
|
|
101
133
|
## 排错
|
|
102
134
|
|
|
103
|
-
| 报错 |
|
|
104
|
-
|
|
105
|
-
| `Cannot find module 'ohos-playwright/config'` | 没装或没 install
|
|
135
|
+
| 报错 | 处理 |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `Cannot find module 'ohos-playwright/config'` | 没装或没 install |
|
|
106
138
|
| `defineConfig({...})` 没有类型提示 | `tsconfig` 的 `moduleResolution` 改成 `bundler` 或 `nodenext` |
|
|
107
|
-
|
|
|
108
|
-
| `Failed to launch com.huawei.hmos.browser` |
|
|
109
|
-
| `DevTools socket not found for pid` |
|
|
110
|
-
| `CDP probe failed` | `hdc fport
|
|
111
|
-
| `await page.goto('/foo')` 不拼 baseURL | 检查 `use.baseURL`
|
|
139
|
+
| `未发现设备...` | 设备上开「开发者选项 → 无线调试」;本机防火墙放行 UDP:8710;CI 预先 `hdc tconn` |
|
|
140
|
+
| `Failed to launch com.huawei.hmos.browser` | 设备上没装该浏览器,或设 `OHOS_PW_BUNDLE` |
|
|
141
|
+
| `DevTools socket not found for pid` | 浏览器没暴露 CDP,或等几秒重试 |
|
|
142
|
+
| `CDP probe failed` | `hdc fport ls` 查现 ruler,`hdc fport rm` 清残留 |
|
|
143
|
+
| `await page.goto('/foo')` 不拼 baseURL | 检查 `use.baseURL` 是否非空 |
|
|
144
|
+
|
|
145
|
+
## TypeScript
|
|
146
|
+
|
|
147
|
+
项目源码使用 TypeScript(`.mts`),Node 24 原生支持类型剥离,无需编译步骤。
|
|
148
|
+
|
|
149
|
+
支持类型检查:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
npm run typecheck # tsc --noEmit
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`withOpenHarmony` 从 `ohos-playwright/config` 导入时有完整的 `PlaywrightTestConfig` 类型提示(直接从源码推断,无需 `.d.ts` 文件)。
|
|
112
156
|
|
|
113
157
|
## 兼容性
|
|
114
158
|
|
|
115
|
-
- **Playwright**:`>=1.59.0
|
|
159
|
+
- **Playwright**:`>=1.59.0`
|
|
116
160
|
- **Node**:`>=24`(OpenHarmony 上只支持 Node 24+)
|
|
117
161
|
- **OpenHarmony**:`hdc` 可用 + 系统浏览器暴露 CDP。已验证 `com.huawei.hmos.browser`(华为浏览器,Chromium 132)
|
|
118
162
|
- **其他主机**:Windows / Linux / macOS 上自动降级为 stock Playwright,无副作用
|
package/dist/cli.d.mts
ADDED
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { dirname, resolve } from 'node:path';
|
|
6
|
+
// Resolve @playwright/test from the consumer's project root (process.cwd()),
|
|
7
|
+
// not from this package's own directory (which has no node_modules/ for its
|
|
8
|
+
// peer dependencies when installed via a file: symlink).
|
|
9
|
+
// noop.mjs doesn't need to exist on disk -- createRequire uses it only to
|
|
10
|
+
// anchor the module resolution base path.
|
|
11
|
+
const req = createRequire(resolve(process.cwd(), 'noop.mjs'));
|
|
12
|
+
// @playwright/test's exports map blocks direct subpath resolution to cli.js,
|
|
13
|
+
// so resolve the main entry and walk up to the package root, then append it.
|
|
14
|
+
let pwEntry;
|
|
15
|
+
try {
|
|
16
|
+
pwEntry = req.resolve('@playwright/test');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
console.error(`[ohos-playwright] Cannot find @playwright/test from ${process.cwd()}.\n` +
|
|
20
|
+
'Make sure it is installed in the current project or an ancestor:\n' +
|
|
21
|
+
' npm install -D @playwright/test\n' +
|
|
22
|
+
' pnpm add -D @playwright/test');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
let pkgRoot = dirname(pwEntry);
|
|
26
|
+
let levels = 0;
|
|
27
|
+
while (!existsSync(resolve(pkgRoot, 'package.json')) && levels++ < 50) {
|
|
28
|
+
pkgRoot = dirname(pkgRoot);
|
|
29
|
+
}
|
|
30
|
+
if (!existsSync(resolve(pkgRoot, 'package.json'))) {
|
|
31
|
+
console.error(`[ohos-playwright] Cannot find @playwright/test package.json (walked up from ${dirname(pwEntry)}). ` +
|
|
32
|
+
'Please verify @playwright/test is correctly installed.');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const playwrightCli = resolve(pkgRoot, 'cli.js');
|
|
36
|
+
// Node 24 has native TypeScript support; register.mts is resolved directly.
|
|
37
|
+
const register = resolve(import.meta.dirname, 'register.mjs');
|
|
38
|
+
const child = spawn(process.execPath, ['--import', register, playwrightCli, ...process.argv.slice(2)], { stdio: 'inherit' });
|
|
39
|
+
child.on('exit', (code, signal) => {
|
|
40
|
+
if (signal)
|
|
41
|
+
process.kill(process.pid, signal);
|
|
42
|
+
else
|
|
43
|
+
process.exit(code ?? 1);
|
|
44
|
+
});
|
package/dist/config.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Wrap a base Playwright config so it transparently turns into an ArkWeb/CDP
|
|
2
|
+
// run on OpenHarmony and stays stock everywhere else. Usage:
|
|
3
|
+
//
|
|
4
|
+
// import { defineConfig, devices } from '@playwright/test'
|
|
5
|
+
// import { withOpenHarmony } from 'ohos-playwright/config'
|
|
6
|
+
//
|
|
7
|
+
// export default defineConfig(withOpenHarmony({ ...baseConfig }))
|
|
8
|
+
//
|
|
9
|
+
// On non-OpenHarmony hosts the input config is returned unchanged.
|
|
10
|
+
export function withOpenHarmony(config) {
|
|
11
|
+
// Consult OHOS_PW_HOST, not process.platform — register.mjs has already
|
|
12
|
+
// overwritten platform to 'linux' by the time this function runs.
|
|
13
|
+
if (!process.env.OHOS_PW_HOST)
|
|
14
|
+
return config;
|
|
15
|
+
return {
|
|
16
|
+
...config,
|
|
17
|
+
// Single ArkWeb instance via CDP — workers must be 1.
|
|
18
|
+
workers: 1,
|
|
19
|
+
globalSetup: 'ohos-playwright/setup',
|
|
20
|
+
globalTeardown: 'ohos-playwright/teardown',
|
|
21
|
+
// ArkWeb only speaks Chromium CDP; drop firefox/webkit projects.
|
|
22
|
+
projects: config.projects?.filter((p) => p.name === 'chromium') ?? config.projects,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
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>;
|
|
2
|
+
export { expect } from '@playwright/test';
|
package/dist/fixture.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { test as base, chromium } from '@playwright/test';
|
|
3
|
+
import { INFO_PATH } from "./info-path.mjs";
|
|
4
|
+
function readEndpoint() {
|
|
5
|
+
return JSON.parse(readFileSync(INFO_PATH, 'utf8')).endpoint;
|
|
6
|
+
}
|
|
7
|
+
export const test = base.extend({
|
|
8
|
+
browser: [
|
|
9
|
+
async ({}, use) => {
|
|
10
|
+
const browser = await chromium.connectOverCDP(readEndpoint());
|
|
11
|
+
await use(browser);
|
|
12
|
+
},
|
|
13
|
+
{ scope: 'worker' },
|
|
14
|
+
],
|
|
15
|
+
context: async ({ browser }, use, testInfo) => {
|
|
16
|
+
const ctx = browser.contexts()[0];
|
|
17
|
+
const baseURL = testInfo.project.use.baseURL;
|
|
18
|
+
if (baseURL) {
|
|
19
|
+
try {
|
|
20
|
+
const opts = ctx._options;
|
|
21
|
+
if (opts && typeof opts === 'object')
|
|
22
|
+
opts.baseURL = baseURL;
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
console.warn(`[ohos-playwright] Failed to inject baseURL: ${e instanceof Error ? e.message : e}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
await use(ctx);
|
|
29
|
+
},
|
|
30
|
+
page: async ({ context }, use, testInfo) => {
|
|
31
|
+
const pages = context.pages();
|
|
32
|
+
if (pages.length === 0)
|
|
33
|
+
throw new Error('No pages in ArkWeb CDP context. Open a tab first.');
|
|
34
|
+
const page = pages.find((p) => p.url().startsWith('http://localhost')) ?? pages[0];
|
|
35
|
+
const baseURL = testInfo.project.use.baseURL;
|
|
36
|
+
if (baseURL) {
|
|
37
|
+
const root = baseURL.replace(/\/+$/, '');
|
|
38
|
+
const origGoto = page.goto.bind(page);
|
|
39
|
+
page.goto = ((url, opts) => origGoto((url.startsWith('/') && !url.startsWith('//')) ? root + url : url, opts));
|
|
40
|
+
}
|
|
41
|
+
await use(page);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
export { expect } from '@playwright/test';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { tmpdir } from 'node:os';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
// Setup writes the CDP endpoint info here; fixture and teardown read from it.
|
|
4
|
+
// Override with OHOS_PW_INFO_PATH if you need a deterministic location.
|
|
5
|
+
export const INFO_PATH = process.env.OHOS_PW_INFO_PATH ?? resolve(tmpdir(), 'ohos-playwright-cdp.json');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface ResolveContext {
|
|
2
|
+
parentURL?: string;
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
}
|
|
5
|
+
interface NextResolve {
|
|
6
|
+
(specifier: string, context: ResolveContext): {
|
|
7
|
+
url: string;
|
|
8
|
+
} | Promise<{
|
|
9
|
+
url: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export declare function resolve(specifier: string, context: ResolveContext, nextResolve: NextResolve): Promise<{
|
|
13
|
+
url: string;
|
|
14
|
+
}>;
|
|
15
|
+
export {};
|
package/dist/loader.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { resolve as resolvePath } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
const FIXTURE_URL = pathToFileURL(resolvePath(import.meta.dirname, 'fixture.mjs')).href;
|
|
4
|
+
const TARGET = '@playwright/test';
|
|
5
|
+
const TEST_FILE = /\.(spec|test)\.[mc]?[tj]sx?$/;
|
|
6
|
+
const PACKAGE_ROOT_URL = pathToFileURL(resolvePath(import.meta.dirname, '..') + '/').href;
|
|
7
|
+
const PROJECT_ANCHOR = pathToFileURL(resolvePath(process.cwd(), 'noop.mjs')).href;
|
|
8
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
9
|
+
if (specifier === TARGET) {
|
|
10
|
+
const parent = context.parentURL ?? '';
|
|
11
|
+
if (TEST_FILE.test(parent)) {
|
|
12
|
+
const result = await nextResolve(FIXTURE_URL, context);
|
|
13
|
+
return { url: result.url };
|
|
14
|
+
}
|
|
15
|
+
if (parent.startsWith(PACKAGE_ROOT_URL)) {
|
|
16
|
+
return nextResolve(specifier, { ...context, parentURL: PROJECT_ANCHOR });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return nextResolve(specifier, context);
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { register } from 'node:module';
|
|
2
|
+
// Adapter only activates on OpenHarmony — elsewhere this file is a no-op so
|
|
3
|
+
// the same ohos-playwright entry point and the same playwright.config.ts can
|
|
4
|
+
// run on Windows / Linux / macOS with stock Playwright.
|
|
5
|
+
if (process.platform === 'openharmony') {
|
|
6
|
+
// Mark the run as OpenHarmony before we lie about the platform — any code
|
|
7
|
+
// downstream (notably withOpenHarmony in the user's config) should consult
|
|
8
|
+
// OHOS_PW_HOST instead of process.platform, which is about to read 'linux'.
|
|
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.
|
|
15
|
+
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
16
|
+
register('./loader.mjs', import.meta.url);
|
|
17
|
+
}
|
package/dist/setup.d.mts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface RetryOptions {
|
|
2
|
+
max?: number;
|
|
3
|
+
interval?: number;
|
|
4
|
+
label?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function retry<T>(fn: () => T | Promise<T>, { max, interval, label }?: RetryOptions): Promise<T>;
|
|
7
|
+
export declare function findBrowserPid(): number | null;
|
|
8
|
+
export declare function hasDeviceConnected(): boolean;
|
|
9
|
+
export declare function discoverDevices(): string[];
|
|
10
|
+
export declare function tryLocalDevice(): boolean;
|
|
11
|
+
export declare function ensureDeviceConnected(): Promise<void>;
|
|
12
|
+
export default function globalSetup(): Promise<void>;
|
|
13
|
+
export {};
|
package/dist/setup.mjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { createServer } from 'node:net';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
import http from 'node:http';
|
|
7
|
+
import { INFO_PATH } from "./info-path.mjs";
|
|
8
|
+
const HDC = process.env.OHOS_PW_HDC ?? '/data/service/hnp/bin/hdc';
|
|
9
|
+
const BUNDLE = process.env.OHOS_PW_BUNDLE ?? 'com.huawei.hmos.browser';
|
|
10
|
+
const LAUNCH_URL = process.env.OHOS_PW_LAUNCH_URL ?? 'http://localhost:5173';
|
|
11
|
+
const HDC_OPTS = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] };
|
|
12
|
+
function hdc(args, opts) {
|
|
13
|
+
return String(execFileSync(HDC, args, { ...HDC_OPTS, ...opts })).trim();
|
|
14
|
+
}
|
|
15
|
+
function shellOnDevice(cmd) { return hdc(['shell', cmd]); }
|
|
16
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
17
|
+
// Exponential backoff: 100ms → 200ms → 400ms … capped at `interval` (default 1000ms).
|
|
18
|
+
export async function retry(fn, { max = 10, interval = 1000, label = '' } = {}) {
|
|
19
|
+
for (let i = 0; i < max; i++) {
|
|
20
|
+
try {
|
|
21
|
+
const r = await fn();
|
|
22
|
+
if (r)
|
|
23
|
+
return r;
|
|
24
|
+
}
|
|
25
|
+
catch { }
|
|
26
|
+
if (i < max - 1) {
|
|
27
|
+
const delay = Math.min(100 * Math.pow(2, i), interval);
|
|
28
|
+
await sleep(delay);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw new Error(label ? `${label}: exhausted retries (${max} attempts)` : `retry exhausted after ${max} attempts`);
|
|
32
|
+
}
|
|
33
|
+
// Batch ps + /proc/net/unix into a single hdc shell call — avoids two
|
|
34
|
+
// subprocess spawns and transfers.
|
|
35
|
+
function fetchDeviceState() {
|
|
36
|
+
const raw = shellOnDevice('ps -o pid,args; echo "---SOCKET---"; cat /proc/net/unix');
|
|
37
|
+
const [ps, unix] = raw.split('---SOCKET---');
|
|
38
|
+
return { ps: ps || '', unix: unix || '' };
|
|
39
|
+
}
|
|
40
|
+
export function findBrowserPid() {
|
|
41
|
+
const { ps } = fetchDeviceState();
|
|
42
|
+
for (const line of ps.split('\n')) {
|
|
43
|
+
const t = line.trim();
|
|
44
|
+
if (!t)
|
|
45
|
+
continue;
|
|
46
|
+
const s = t.indexOf(' ');
|
|
47
|
+
if (s === -1)
|
|
48
|
+
continue;
|
|
49
|
+
if (t.slice(s + 1).includes(BUNDLE)) {
|
|
50
|
+
const pid = parseInt(t.slice(0, s), 10);
|
|
51
|
+
if (!Number.isNaN(pid))
|
|
52
|
+
return pid;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function launchBrowser() {
|
|
58
|
+
shellOnDevice(`aa start -b ${BUNDLE} -m entry -a MainAbility -U ${LAUNCH_URL}`);
|
|
59
|
+
}
|
|
60
|
+
function findDevToolsSocket(pid, cachedUnix) {
|
|
61
|
+
const unix = cachedUnix ?? shellOnDevice('cat /proc/net/unix');
|
|
62
|
+
const name = `webview_devtools_remote_${pid}`;
|
|
63
|
+
return unix.includes(`@${name}`) ? name : null;
|
|
64
|
+
}
|
|
65
|
+
function pickFreePort() {
|
|
66
|
+
return new Promise((res, rej) => {
|
|
67
|
+
const srv = createServer();
|
|
68
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
69
|
+
const a = srv.address();
|
|
70
|
+
if (a && typeof a === 'object')
|
|
71
|
+
srv.close((e) => e ? rej(e) : res(a.port));
|
|
72
|
+
else
|
|
73
|
+
srv.close(() => rej(new Error('Failed to get port')));
|
|
74
|
+
});
|
|
75
|
+
srv.on('error', rej);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function setupForward(port, socketName) {
|
|
79
|
+
const ruler = `tcp:${port} localabstract:${socketName}`;
|
|
80
|
+
try {
|
|
81
|
+
hdc(['fport', 'rm', ruler]);
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
84
|
+
hdc(['fport', 'tcp:' + port, 'localabstract:' + socketName]);
|
|
85
|
+
}
|
|
86
|
+
function probeCdp(port) {
|
|
87
|
+
return new Promise((res) => {
|
|
88
|
+
const req = http.get(`http://127.0.0.1:${port}/json/version`, (r) => {
|
|
89
|
+
let b = '';
|
|
90
|
+
r.on('data', (c) => (b += c));
|
|
91
|
+
r.on('end', () => res({ ok: r.statusCode === 200, body: b }));
|
|
92
|
+
});
|
|
93
|
+
req.on('error', (e) => res({ ok: false, err: e.code }));
|
|
94
|
+
req.setTimeout(2000, () => { req.destroy(); res({ ok: false, err: 'TIMEOUT' }); });
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const IP_PORT_RE = /^(\d{1,3}\.){3}\d{1,3}:\d+$/;
|
|
98
|
+
function listTargets() { return hdc(['list', 'targets']); }
|
|
99
|
+
export function hasDeviceConnected() {
|
|
100
|
+
const t = listTargets();
|
|
101
|
+
return t.length > 0 && t !== '[Empty]';
|
|
102
|
+
}
|
|
103
|
+
// discoverDevices timeout reduced from 6s to 3s — LAN broadcast on local
|
|
104
|
+
// network should respond within 1-2s; longer wait is unlikely to help.
|
|
105
|
+
export function discoverDevices() {
|
|
106
|
+
let out = '';
|
|
107
|
+
try {
|
|
108
|
+
out = hdc(['discover'], { timeout: 3000 });
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
out = e.stdout?.toString() ?? '';
|
|
112
|
+
}
|
|
113
|
+
return out.split('\n').map(s => s.trim()).filter(s => IP_PORT_RE.test(s));
|
|
114
|
+
}
|
|
115
|
+
function tconn(addr) {
|
|
116
|
+
try {
|
|
117
|
+
return hdc(['tconn', addr], { timeout: 10000 }).includes('Connect OK');
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function promptAddress() {
|
|
124
|
+
return new Promise((res) => {
|
|
125
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
126
|
+
rl.question('[ohos-playwright] paste device ip:port (Enter to abort): ', (a) => { rl.close(); res(a.trim()); });
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const CONNECT_HELP = [
|
|
130
|
+
'[ohos-playwright] 未发现设备。请在设备上:',
|
|
131
|
+
' 1) 进入「设置 → 关于本机」连点版本号开启「开发者选项」',
|
|
132
|
+
' 2) 进入「开发者选项」启用「无线调试」',
|
|
133
|
+
' 3) 确认设备与本机在同一 Wi-Fi 下',
|
|
134
|
+
' 4) 防火墙放行本机 UDP:8710 入站(hdc discover 广播用)',
|
|
135
|
+
'也可手动跑 `hdc tconn <ip:port>` 后重新启动测试。',
|
|
136
|
+
'若不希望自动连接,设 OHOS_PW_AUTO_CONNECT=0 跳过。',
|
|
137
|
+
].join('\n');
|
|
138
|
+
export function tryLocalDevice() {
|
|
139
|
+
try {
|
|
140
|
+
const raw = String(execFileSync('param', ['get', 'persist.hdc.port'], { ...HDC_OPTS, timeout: 3000 })).trim();
|
|
141
|
+
const port = parseInt(raw, 10);
|
|
142
|
+
if (!port || port < 1 || port > 65535)
|
|
143
|
+
return false;
|
|
144
|
+
console.log(`[ohos-playwright] local device port from param: ${port}`);
|
|
145
|
+
const addr = `127.0.0.1:${port}`;
|
|
146
|
+
if (tconn(addr) && hasDeviceConnected()) {
|
|
147
|
+
console.log(`[ohos-playwright] connected: ${addr}`);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
console.warn(`[ohos-playwright] tconn ${addr} failed`);
|
|
151
|
+
}
|
|
152
|
+
catch { }
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
export async function ensureDeviceConnected() {
|
|
156
|
+
if (process.env.OHOS_PW_AUTO_CONNECT === '0')
|
|
157
|
+
return;
|
|
158
|
+
if (hasDeviceConnected())
|
|
159
|
+
return;
|
|
160
|
+
if (tryLocalDevice())
|
|
161
|
+
return;
|
|
162
|
+
console.log('[ohos-playwright] no local device, broadcasting (hdc discover)...');
|
|
163
|
+
const found = discoverDevices();
|
|
164
|
+
for (const addr of found) {
|
|
165
|
+
console.log(`[ohos-playwright] hdc tconn ${addr}`);
|
|
166
|
+
if (tconn(addr) && hasDeviceConnected())
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (found.length > 0)
|
|
170
|
+
console.warn('[ohos-playwright] discovered devices but none connected');
|
|
171
|
+
if (!process.stdin.isTTY)
|
|
172
|
+
throw new Error(CONNECT_HELP);
|
|
173
|
+
console.log(CONNECT_HELP);
|
|
174
|
+
const addr = await promptAddress();
|
|
175
|
+
if (!addr)
|
|
176
|
+
throw new Error('[ohos-playwright] no device address provided; aborting.');
|
|
177
|
+
if (!IP_PORT_RE.test(addr))
|
|
178
|
+
throw new Error(`[ohos-playwright] "${addr}" is not a valid ip:port.`);
|
|
179
|
+
if (!tconn(addr))
|
|
180
|
+
throw new Error(`[ohos-playwright] hdc tconn ${addr} failed.`);
|
|
181
|
+
if (!hasDeviceConnected())
|
|
182
|
+
throw new Error('[ohos-playwright] tconn reported OK but list targets still empty.');
|
|
183
|
+
console.log(`[ohos-playwright] connected: ${addr}`);
|
|
184
|
+
}
|
|
185
|
+
export default async function globalSetup() {
|
|
186
|
+
await ensureDeviceConnected();
|
|
187
|
+
console.log(`[ohos-playwright] locating ${BUNDLE}...`);
|
|
188
|
+
// Batch ps + /proc/net/unix into a single hdc call.
|
|
189
|
+
let deviceState = fetchDeviceState();
|
|
190
|
+
let pid = findBrowserPid();
|
|
191
|
+
if (!pid) {
|
|
192
|
+
console.log('[ohos-playwright] browser not running, launching...');
|
|
193
|
+
launchBrowser();
|
|
194
|
+
// Backoff: 100, 200, 400, 800, 1000, 1000, … (max 20 attempts ≈ 10s total)
|
|
195
|
+
pid = await retry(findBrowserPid, { max: 20, interval: 1000, label: `Failed to launch ${BUNDLE}` });
|
|
196
|
+
}
|
|
197
|
+
console.log(`[ohos-playwright] browser pid=${pid}`);
|
|
198
|
+
// Reuse the /proc/net/unix snapshot if it's fresh (same shell call as ps above).
|
|
199
|
+
// If we waited for browser launch, re-fetch since the socket may be new.
|
|
200
|
+
let unixCache = pid ? undefined : deviceState.unix;
|
|
201
|
+
if (!unixCache) {
|
|
202
|
+
// Re-fetch device state to get fresh /proc/net/unix after browser launch
|
|
203
|
+
deviceState = fetchDeviceState();
|
|
204
|
+
unixCache = deviceState.unix;
|
|
205
|
+
}
|
|
206
|
+
const socket = await retry(() => findDevToolsSocket(pid, unixCache), { max: 10, interval: 500, label: `DevTools socket not found for pid ${pid}` });
|
|
207
|
+
console.log(`[ohos-playwright] socket=${socket}`);
|
|
208
|
+
const port = await pickFreePort();
|
|
209
|
+
setupForward(port, socket);
|
|
210
|
+
console.log(`[ohos-playwright] hdc fport tcp:${port} -> localabstract:${socket}`);
|
|
211
|
+
const probe = await probeCdp(port);
|
|
212
|
+
if (!probe.ok)
|
|
213
|
+
throw new Error(`CDP probe failed: ${probe.err || probe.body}`);
|
|
214
|
+
const info = JSON.parse(probe.body);
|
|
215
|
+
console.log(`[ohos-playwright] CDP ready: ${info.Browser}`);
|
|
216
|
+
mkdirSync(dirname(INFO_PATH), { recursive: true });
|
|
217
|
+
writeFileSync(INFO_PATH, JSON.stringify({ port, pid, socket, endpoint: `http://127.0.0.1:${port}` }, null, 2));
|
|
218
|
+
console.log(`[ohos-playwright] wrote ${INFO_PATH}`);
|
|
219
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function globalTeardown(): Promise<void>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { INFO_PATH } from "./info-path.mjs";
|
|
4
|
+
const HDC = process.env.OHOS_PW_HDC ?? '/data/service/hnp/bin/hdc';
|
|
5
|
+
export default async function globalTeardown() {
|
|
6
|
+
let info;
|
|
7
|
+
try {
|
|
8
|
+
info = JSON.parse(readFileSync(INFO_PATH, 'utf8'));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const ruler = `tcp:${info.port} localabstract:${info.socket}`;
|
|
14
|
+
try {
|
|
15
|
+
execFileSync(HDC, ['fport', 'rm', ruler], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
16
|
+
console.log(`[ohos-playwright] removed fport ${ruler}`);
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
const msg = e instanceof Error ? e.message?.split('\n')[0] : String(e);
|
|
20
|
+
console.warn(`[ohos-playwright] fport rm failed (non-fatal): ${msg}`);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
unlinkSync(INFO_PATH);
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
const err = e;
|
|
27
|
+
if (err.code !== 'ENOENT') {
|
|
28
|
+
console.warn(`[ohos-playwright] Failed to remove ${INFO_PATH}: ${err.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|