ohos-playwright 0.1.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/LICENSE +21 -0
- package/README.md +122 -0
- package/SKILL.md +155 -0
- package/package.json +54 -0
- package/src/cli.mjs +26 -0
- package/src/config.d.mts +3 -0
- package/src/config.mjs +23 -0
- package/src/fixture.mjs +49 -0
- package/src/info-path.mjs +7 -0
- package/src/loader.mjs +23 -0
- package/src/register.mjs +20 -0
- package/src/setup.mjs +203 -0
- package/src/teardown.mjs +24 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 social4hyq
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# ohos-playwright
|
|
2
|
+
|
|
3
|
+
让 Playwright 在 OpenHarmony 上跑 e2e —— 接管设备上的系统浏览器(ArkWeb,Chromium 内核)。同一份 `playwright.config.ts` 和 spec 在 Windows / Linux / macOS 上仍按原版 Playwright 跑,**不需要写两份**。
|
|
4
|
+
|
|
5
|
+
## 它解决什么问题
|
|
6
|
+
|
|
7
|
+
OpenHarmony 上没有官方的 Playwright:没有适配的浏览器二进制可下载,Playwright 自己的平台检测在 OpenHarmony 上直接崩。
|
|
8
|
+
|
|
9
|
+
ohos-playwright 换了个思路 —— **不让 Playwright 自己启动浏览器**,而是通过 `hdc` 把设备上正在运行的系统浏览器的调试通道转发回本机,让 Playwright "接管"它。spec 文件一行不动,`playwright.config.ts` 只多一层包装;CI / 本地 / 鸿蒙开发机共用同一份配置。
|
|
10
|
+
|
|
11
|
+
## 快速开始
|
|
12
|
+
|
|
13
|
+
### 1. 安装
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pnpm add -D ohos-playwright @playwright/test
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
> `@playwright/test` 是 peer dependency,版本 `>=1.59.0`。
|
|
20
|
+
|
|
21
|
+
### 2. 包一层 config
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// playwright.config.ts
|
|
25
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
26
|
+
import { withOpenHarmony } from 'ohos-playwright/config'
|
|
27
|
+
|
|
28
|
+
export default defineConfig(withOpenHarmony({
|
|
29
|
+
testDir: './e2e',
|
|
30
|
+
use: { baseURL: 'http://localhost:5173' },
|
|
31
|
+
projects: [
|
|
32
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
33
|
+
// firefox / webkit 在 OpenHarmony 上会被自动裁掉,留着也不影响其他主机
|
|
34
|
+
],
|
|
35
|
+
// ...其他正常的 Playwright 配置
|
|
36
|
+
}))
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
非 OpenHarmony 主机上 `withOpenHarmony` 原样返回 config;OpenHarmony 上它会注入 setup/teardown、把 workers 锁 1、过滤掉 firefox/webkit。
|
|
40
|
+
|
|
41
|
+
### 3. 改 npm script
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{ "scripts": { "test:e2e": "ohos-playwright test" } }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`ohos-playwright` 这个 bin 在非 OpenHarmony 上等价于 `playwright`,所有参数透传。
|
|
48
|
+
|
|
49
|
+
### 4. spec 文件不用动
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { test, expect } from '@playwright/test'
|
|
53
|
+
|
|
54
|
+
test('something', async ({ page }) => {
|
|
55
|
+
await page.goto('/')
|
|
56
|
+
await expect(page).toHaveTitle(/.../)
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 5. 运行
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
pnpm test:e2e
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
第一次跑会自动找设备、拉起系统浏览器、转发调试端口、让 Playwright 接管。
|
|
67
|
+
|
|
68
|
+
## 工作原理
|
|
69
|
+
|
|
70
|
+
**非 OpenHarmony 主机**:什么都不做,等价于 stock Playwright,所以配置可以跨平台共用。
|
|
71
|
+
|
|
72
|
+
**OpenHarmony 主机**:
|
|
73
|
+
|
|
74
|
+
- **复用而非启动**:Playwright 不再自己起浏览器,而是接管设备上正在跑的系统浏览器,通过 `hdc` 把它的调试通道转发到本机。
|
|
75
|
+
- **Spec 无感**:你写的还是 `@playwright/test`,底层透明替换为走 CDP 复用设备上现有 page 的实现。
|
|
76
|
+
- **配置自动收紧**:`workers` 锁 1、firefox / webkit project 自动裁掉、globalSetup / globalTeardown 自动注入 —— 都由 `withOpenHarmony` 在 OpenHarmony 上完成,其他主机不受影响。
|
|
77
|
+
- **设备没连会自救**:测试启动时如果设备没连,先 LAN 广播找设备并自动连上,找不到再弹提示。
|
|
78
|
+
|
|
79
|
+
## 环境变量
|
|
80
|
+
|
|
81
|
+
| 变量 | 默认 | 何时需要设 |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| `OHOS_PW_HDC` | `/data/service/hnp/bin/hdc` | `hdc` 不在默认路径 |
|
|
84
|
+
| `OHOS_PW_BUNDLE` | `com.huawei.hmos.browser` | 接管别的浏览器 bundle |
|
|
85
|
+
| `OHOS_PW_LAUNCH_URL` | `http://localhost:5173` | 浏览器**首次启动**时导航到的 URL(dev server 端口不是 5173 必设) |
|
|
86
|
+
| `OHOS_PW_INFO_PATH` | `os.tmpdir()/ohos-playwright-cdp.json` | 想固定 CDP info 文件位置(CI 多 job 隔离等) |
|
|
87
|
+
| `OHOS_PW_AUTO_CONNECT` | (未设) | 设 `0` 跳过自动 discover / tconn 流程 |
|
|
88
|
+
| `OHOS_PW_HOST` | 自动 | 内部标志位,**不要手动设** |
|
|
89
|
+
|
|
90
|
+
## 约束
|
|
91
|
+
|
|
92
|
+
设备和 ArkWeb 本身带来的硬约束,不是 ohos-playwright 能解决的:
|
|
93
|
+
|
|
94
|
+
- **`workers: 1`**:设备上只有一个浏览器实例,多 worker 会互相抢同一份 localStorage / URL。
|
|
95
|
+
- **不能并发 project**:firefox / webkit 在 ArkWeb 上跑不了,会被自动裁掉。
|
|
96
|
+
- **不能 `newContext` / `newPage`**:全程复用一个 context、一个 page。要做用例隔离,用 `localStorage.clear()` + `page.reload()` 代替独立 context。
|
|
97
|
+
- **测试结束不关浏览器**:浏览器进程归 OS 管,不归测试管。
|
|
98
|
+
- **`process.platform` 在测试进程里被改成 `'linux'`**:如果你在 spec / fixture 里读 `process.platform` 做平台分支,会拿到 `'linux'`。判断真实主机身份请用 `process.env.OHOS_PW_HOST`。
|
|
99
|
+
- **设备必须开发者模式 + 无线调试或 USB 调试**:自动连接依赖 `hdc discover`(UDP:8710),CI 上推荐预先 `hdc tconn`。
|
|
100
|
+
|
|
101
|
+
## 排错
|
|
102
|
+
|
|
103
|
+
| 报错 | 怎么处理 |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `Cannot find module 'ohos-playwright/config'` | 没装或没 install,按"快速开始"重做 |
|
|
106
|
+
| `defineConfig({...})` 没有类型提示 | `tsconfig` 的 `moduleResolution` 改成 `bundler` 或 `nodenext` |
|
|
107
|
+
| `未发现设备...`(中文) | 设备上开「开发者选项 → 无线调试」;本机防火墙放行 UDP:8710;CI 上预先手动 `hdc tconn <ip:port>` |
|
|
108
|
+
| `Failed to launch com.huawei.hmos.browser` | 设备上没装该浏览器,或 bundle name 不对,设 `OHOS_PW_BUNDLE` |
|
|
109
|
+
| `DevTools socket not found for pid` | 该浏览器没暴露 CDP(不是所有 OpenHarmony 浏览器都启用了 devtools) |
|
|
110
|
+
| `CDP probe failed` | `hdc fport` 转发没起来,`hdc fport ls` 看现有 ruler、`hdc fport rm` 清残留 |
|
|
111
|
+
| `await page.goto('/foo')` 不拼 baseURL | 检查 `use.baseURL` 是否设了非空值 |
|
|
112
|
+
|
|
113
|
+
## 兼容性
|
|
114
|
+
|
|
115
|
+
- **Playwright**:`>=1.59.0`(测试覆盖 1.60)
|
|
116
|
+
- **Node**:`>=24`(OpenHarmony 上只支持 Node 24+)
|
|
117
|
+
- **OpenHarmony**:`hdc` 可用 + 系统浏览器暴露 CDP。已验证 `com.huawei.hmos.browser`(华为浏览器,Chromium 132)
|
|
118
|
+
- **其他主机**:Windows / Linux / macOS 上自动降级为 stock Playwright,无副作用
|
|
119
|
+
|
|
120
|
+
## 协议
|
|
121
|
+
|
|
122
|
+
MIT
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ohos-playwright-migrate
|
|
3
|
+
description: Use when the user asks to make a project's Playwright e2e tests run on OpenHarmony / ArkWeb, or to "add ohos-playwright", "适配 OpenHarmony", "用 ohos-playwright 跑 e2e",or when you see a Playwright config in a repo that's expected to support OpenHarmony but still uses stock `playwright test`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ohos-playwright 迁移 SKILL
|
|
7
|
+
|
|
8
|
+
把一个使用 stock Playwright 的工程改造成可以在 OpenHarmony 上接管华为浏览器(ArkWeb)跑 e2e,同时**保持在 Windows / Linux / macOS 上的原行为不变**。整个适配在非 OpenHarmony 主机上自动降级为 no-op。
|
|
9
|
+
|
|
10
|
+
## 适用场景(什么时候应该启动这个 SKILL)
|
|
11
|
+
|
|
12
|
+
满足任一即可:
|
|
13
|
+
|
|
14
|
+
- 用户明说"接入 ohos-playwright"、"适配 OpenHarmony e2e"、"让 Playwright 在鸿蒙上跑"。
|
|
15
|
+
- 工程根目录有 `playwright.config.ts` / `playwright.config.js`,并且工程目标平台包含 OpenHarmony。
|
|
16
|
+
- 工程在 OpenHarmony 上跑 `playwright test` 直接报 `<unknown>` 平台 / 找不到浏览器二进制。
|
|
17
|
+
|
|
18
|
+
不要启动这个 SKILL 的场景:
|
|
19
|
+
|
|
20
|
+
- 工程不用 Playwright(比如 Cypress / WebdriverIO)。
|
|
21
|
+
- 用户只想在 Linux/macOS/Windows 上跑 Playwright,并不关心鸿蒙。
|
|
22
|
+
|
|
23
|
+
## 前置检查(动手前必须确认)
|
|
24
|
+
|
|
25
|
+
1. **包管理器**:检查 `pnpm-lock.yaml` / `package-lock.json` / `yarn.lock` 确定是 pnpm / npm / yarn 中的哪一个。后续 `pnpm add` / `npm install` / `yarn add` 命令对应替换。
|
|
26
|
+
2. **是否 workspace**:根目录有 `pnpm-workspace.yaml` 或 `package.json` 里有 `workspaces` 字段 → 是 monorepo。如果 `ohos-playwright` 已作为 workspace 包存在,依赖应该写 `"ohos-playwright": "workspace:*"` 而不是从 registry 装。
|
|
27
|
+
3. **现有 Playwright 版本**:`@playwright/test` 必须 ≥ 1.59.0(`ohos-playwright` 的 peerDependency 下限)。低于的话先升级。
|
|
28
|
+
4. **Node 版本**:`ohos-playwright` 的 `engines.node` 是 `>=24`(OpenHarmony 上只支持 Node 24+)。低于的话先升级,否则 install 时会有 EBADENGINE 警告,运行时也可能崩。
|
|
29
|
+
5. **现有 config 形态**:读 `playwright.config.ts`,记录是 `defineConfig({...})` 还是 `defineConfig({...} satisfies PlaywrightTestConfig)`,是否已有 `globalSetup` / `globalTeardown` / `workers` 等字段。
|
|
30
|
+
|
|
31
|
+
## 改造步骤
|
|
32
|
+
|
|
33
|
+
按顺序做,每一步做完都跑验证再继续。
|
|
34
|
+
|
|
35
|
+
### 步骤 1:安装 `ohos-playwright`
|
|
36
|
+
|
|
37
|
+
- **workspace 内部**:在工程根 `package.json` 的 `devDependencies` 加 `"ohos-playwright": "workspace:*"`。
|
|
38
|
+
- **独立工程**:`pnpm add -D ohos-playwright`(或对应包管理器命令)。
|
|
39
|
+
|
|
40
|
+
然后跑一次安装让 bin 链接生效:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
pnpm install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
验证:`pnpm exec ohos-playwright --version` 能输出 Playwright 版本号。
|
|
47
|
+
|
|
48
|
+
### 步骤 2:包装 `playwright.config.ts`
|
|
49
|
+
|
|
50
|
+
最小改动:加两行,包一层。
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
54
|
+
import { withOpenHarmony } from 'ohos-playwright/config'
|
|
55
|
+
|
|
56
|
+
export default defineConfig(withOpenHarmony({
|
|
57
|
+
// ...原有所有配置一字不动
|
|
58
|
+
}))
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**`withOpenHarmony` 的行为**:
|
|
62
|
+
|
|
63
|
+
- 非 OpenHarmony 主机:原样返回 config,等于没包。
|
|
64
|
+
- OpenHarmony 主机:
|
|
65
|
+
- `workers` 强制为 1(ArkWeb 单实例,多 worker 会抢同一份状态)。
|
|
66
|
+
- `globalSetup` / `globalTeardown` 注入为 `ohos-playwright/setup` / `ohos-playwright/teardown`。
|
|
67
|
+
- `projects` 过滤为只剩 `name === 'chromium'`(firefox / webkit 跑不了 ArkWeb)。
|
|
68
|
+
|
|
69
|
+
**注意事项**:
|
|
70
|
+
|
|
71
|
+
- 不要自己再写 `workers: 1` 或 `globalSetup` —— `withOpenHarmony` 在 OpenHarmony 上会覆盖。但在其他主机上你的写法保留,所以原本 `workers: process.env.CI ? 1 : undefined` 这类按主机逻辑可以保留。
|
|
72
|
+
- 如果用户原本就有 `globalSetup`,迁移后会被 `withOpenHarmony` 覆盖。**不要默默覆盖** —— 把这个冲突告诉用户,让他决定:原 globalSetup 是否纯为 e2e 服务,能否放弃;如果不能,需要在 spec 启动前先手动调用其逻辑,或者改造 ohos-playwright/setup 的封装方式。
|
|
73
|
+
- `projects` 数组里 chromium project 必须存在且 `name: 'chromium'`,否则 OpenHarmony 上没东西可跑。
|
|
74
|
+
|
|
75
|
+
### 步骤 3:改 npm script
|
|
76
|
+
|
|
77
|
+
`package.json` 里把 `test:e2e` 从 `playwright test` 换成 `ohos-playwright test`。其他参数(`--workers`、`--project`、`--grep` 等)原样保留 —— `ohos-playwright` 透传给 Playwright CLI。
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"scripts": {
|
|
82
|
+
"test:e2e": "ohos-playwright test"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
不要再加 `--workers=1` / `--project=chromium`,这些由 `withOpenHarmony` 在 OpenHarmony 上自动处理;在非 OH 主机上你也不希望被强制锁住。
|
|
88
|
+
|
|
89
|
+
### 步骤 4:验证迁移
|
|
90
|
+
|
|
91
|
+
按以下顺序验证:
|
|
92
|
+
|
|
93
|
+
1. **类型检查**:跑工程现有的 type-check 命令(如 `pnpm typecheck` / `tsc --noEmit`)。`defineConfig(withOpenHarmony({...}))` 里的对象字面量应该有完整的 `PlaywrightTestConfig` 上下文类型;故意写一个错字段名应该报 `TS2353`。
|
|
94
|
+
2. **列出测试**:`pnpm test:e2e --list`。
|
|
95
|
+
- 在 OpenHarmony 上:只应该出现 `[chromium]` 开头的用例。
|
|
96
|
+
- 在其他主机上:原本配置里的所有 project 都应该列出。
|
|
97
|
+
3. **跑一遍全量**:`pnpm test:e2e`。
|
|
98
|
+
- OpenHarmony 上日志应该出现 `[ohos-playwright] browser pid=...` / `CDP ready: Chrome/...`。
|
|
99
|
+
- 浏览器进程在跑完后**不应该**被关闭(归 OS 管理)。
|
|
100
|
+
|
|
101
|
+
## 用户自定义场景
|
|
102
|
+
|
|
103
|
+
下面这些用户经常会问,预先准备好答案:
|
|
104
|
+
|
|
105
|
+
### 设备上的 `hdc` 不在默认路径
|
|
106
|
+
|
|
107
|
+
设置环境变量 `OHOS_PW_HDC`,例如 `OHOS_PW_HDC=/path/to/hdc pnpm test:e2e`。默认值 `/data/service/hnp/bin/hdc`。
|
|
108
|
+
|
|
109
|
+
### 想接管别的浏览器 bundle
|
|
110
|
+
|
|
111
|
+
设 `OHOS_PW_BUNDLE`,默认 `com.huawei.hmos.browser`。
|
|
112
|
+
|
|
113
|
+
### dev server 端口不是 5173
|
|
114
|
+
|
|
115
|
+
`withOpenHarmony` 会保留 `webServer` 配置和 `use.baseURL`,所以工程里改 dev server 端口正常生效。但浏览器**首次启动**时的导航 URL 由 `OHOS_PW_LAUNCH_URL` 控制,默认 `http://localhost:5173`,如果你的 dev server 不是这个端口,需要设这个环境变量。
|
|
116
|
+
|
|
117
|
+
### CDP info 文件位置
|
|
118
|
+
|
|
119
|
+
默认在 `os.tmpdir()/ohos-playwright-cdp.json`,setup 写、fixture 和 teardown 读。需要确定性位置就设 `OHOS_PW_INFO_PATH`。
|
|
120
|
+
|
|
121
|
+
### spec 文件里用 `newContext` / `newPage`
|
|
122
|
+
|
|
123
|
+
ArkWeb CDP **不实现** `Target.createBrowserContext`。fixture 复用 `browser.contexts()[0]` 和它已有的 page,所以 `page.context().newPage()` 会报错。改造 spec 使用 `localStorage.clear()` + `page.reload()` 实现隔离。
|
|
124
|
+
|
|
125
|
+
### `hdc list targets` 返回 `[Empty]`(设备没连)
|
|
126
|
+
|
|
127
|
+
setup 会自动救场:先 `hdc discover` 在 LAN 上 UDP 广播找听 TCP 的 daemon,找到就 `hdc tconn <ip:port>` 连上去。整套要求用户**在设备上**:① 进入「关于本机」连点版本号开启「开发者选项」;② 在「开发者选项」里启用「无线调试」;③ 设备与本机同 Wi-Fi;④ 本机防火墙放行 UDP:8710 入站。
|
|
128
|
+
|
|
129
|
+
如果广播找不到设备:
|
|
130
|
+
|
|
131
|
+
- 当前是 TTY → setup 用 readline 提示用户粘贴 `ip:port`,回车继续。
|
|
132
|
+
- 非 TTY(CI / daemonized)→ 直接 `throw` 带步骤的中文提示。CI 上的正解是先在 host 上手动跑过 `hdc tconn <ip:port>`,让 hdc 把连接信息持久化。
|
|
133
|
+
|
|
134
|
+
要完全跳过这套自动连接:设 `OHOS_PW_AUTO_CONNECT=0`,setup 会把"没设备"的报错让位给后续的 `hdc shell` 自己抛。
|
|
135
|
+
|
|
136
|
+
## 排错
|
|
137
|
+
|
|
138
|
+
| 症状 | 原因 | 处理 |
|
|
139
|
+
| --- | --- | --- |
|
|
140
|
+
| `Cannot find module 'ohos-playwright/config'` | 没装或没 `pnpm install` | 按步骤 1 重做 |
|
|
141
|
+
| `defineConfig({...})` 里失去类型提示 | TS 没找到 `config.d.mts` | 确认 `moduleResolution` 是 `bundler` / `nodenext`;如果用的是 `node16` 老式解析,可能要在 `package.json` 的 exports 加 types 路径(包内已ship .d.mts,正常解析模式都能找到)|
|
|
142
|
+
| `Failed to launch com.huawei.hmos.browser` | 设备上没装该浏览器,或 bundle name 不对 | 用 `OHOS_PW_BUNDLE` 改成实际的 bundle name |
|
|
143
|
+
| `DevTools socket not found for pid` | 浏览器没开 devtools,或 `/proc/net/unix` 看不到 `webview_devtools_remote_<pid>` | 一般等几秒后 setup 会重试;持续失败说明 ArkWeb 版本不支持 CDP |
|
|
144
|
+
| `CDP probe failed` | `hdc fport` 转发没生效 | 手动跑 `hdc fport ls` 看 ruler 是否存在;`hdc fport rm` 清掉残留后重试 |
|
|
145
|
+
| `未发现设备。请在设备上:...` | 设备没连且 LAN 广播没找到 | 按提示在设备开「无线调试」;本机放行 UDP:8710;非 TTY 场景预先 `hdc tconn` |
|
|
146
|
+
| `hdc tconn ... failed` | 网络不通 / 端口不对 / 设备拒绝 | 确认 ip:port 准确、双方同网段;设备屏幕上的端口可能每次启用无线调试都变 |
|
|
147
|
+
| 用例里 `await page.goto('/foo')` 不拼 baseURL | fixture 包装漏了 | 检查 `playwright.config.ts` 的 `use.baseURL` 是否非空;fixture 只在 baseURL 存在时才包 `page.goto` |
|
|
148
|
+
| OpenHarmony 上跑了一遍后 firefox/webkit 测试莫名跑不了 | 你没在 OpenHarmony 上,但 `process.env.OHOS_PW_HOST` 被遗留 | 这个 env var 由 `register.mjs` 在 OpenHarmony 上自动设;如果在其他主机看到它,说明误手动设置了 |
|
|
149
|
+
|
|
150
|
+
## 不应该做的事
|
|
151
|
+
|
|
152
|
+
- **不要**为了"让它跨平台"自己写 `process.platform === 'openharmony'` 的分支 —— `withOpenHarmony` + `register.mjs` 已经处理;register 在 OpenHarmony 上会把 `process.platform` 改成 `'linux'`,所以你的检测也会失效。要查"是不是 OpenHarmony 主机"用 `process.env.OHOS_PW_HOST`。
|
|
153
|
+
- **不要**给 `playwright-core` 打 patch 来改 hostPlatform 检测。本包用进程级 `Object.defineProperty(process, 'platform', ...)` 代替,跨版本鲁棒。
|
|
154
|
+
- **不要**手动管理 `hdc fport` 端口 —— setup 自己挑空闲端口、自己拆。
|
|
155
|
+
- **不要**在测试结束后 `await browser.close()`。浏览器进程不归测试管。
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ohos-playwright",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Playwright adapter for OpenHarmony / ArkWeb via hdc + CDP",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "social4hyq",
|
|
7
|
+
"homepage": "https://github.com/social4hyq/ohos-playwright#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/social4hyq/ohos-playwright.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/social4hyq/ohos-playwright/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"playwright",
|
|
17
|
+
"openharmony",
|
|
18
|
+
"ohos",
|
|
19
|
+
"harmonyos",
|
|
20
|
+
"arkweb",
|
|
21
|
+
"e2e",
|
|
22
|
+
"hdc",
|
|
23
|
+
"cdp"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=24"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
"./fixture": "./src/fixture.mjs",
|
|
31
|
+
"./setup": "./src/setup.mjs",
|
|
32
|
+
"./teardown": "./src/teardown.mjs",
|
|
33
|
+
"./register": "./src/register.mjs",
|
|
34
|
+
"./loader": "./src/loader.mjs",
|
|
35
|
+
"./config": {
|
|
36
|
+
"types": "./src/config.d.mts",
|
|
37
|
+
"default": "./src/config.mjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"bin": {
|
|
41
|
+
"ohos-playwright": "src/cli.mjs"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"src",
|
|
45
|
+
"SKILL.md"
|
|
46
|
+
],
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@playwright/test": ">=1.59.0"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public",
|
|
52
|
+
"provenance": true
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const register = resolve(__dirname, 'register.mjs')
|
|
10
|
+
|
|
11
|
+
// @playwright/test's exports map blocks direct subpath resolution to cli.js,
|
|
12
|
+
// so resolve the main entry and walk up to the package root, then append it.
|
|
13
|
+
const req = createRequire(import.meta.url)
|
|
14
|
+
let pkgRoot = dirname(req.resolve('@playwright/test'))
|
|
15
|
+
while (!existsSync(resolve(pkgRoot, 'package.json'))) pkgRoot = dirname(pkgRoot)
|
|
16
|
+
const playwrightCli = resolve(pkgRoot, 'cli.js')
|
|
17
|
+
|
|
18
|
+
const child = spawn(
|
|
19
|
+
process.execPath,
|
|
20
|
+
['--import', register, playwrightCli, ...process.argv.slice(2)],
|
|
21
|
+
{ stdio: 'inherit' },
|
|
22
|
+
)
|
|
23
|
+
child.on('exit', (code, signal) => {
|
|
24
|
+
if (signal) process.kill(process.pid, signal)
|
|
25
|
+
else process.exit(code ?? 1)
|
|
26
|
+
})
|
package/src/config.d.mts
ADDED
package/src/config.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
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) return config
|
|
14
|
+
return {
|
|
15
|
+
...config,
|
|
16
|
+
// Single ArkWeb instance via CDP — workers must be 1.
|
|
17
|
+
workers: 1,
|
|
18
|
+
globalSetup: 'ohos-playwright/setup',
|
|
19
|
+
globalTeardown: 'ohos-playwright/teardown',
|
|
20
|
+
// ArkWeb only speaks Chromium CDP; drop firefox/webkit projects.
|
|
21
|
+
projects: config.projects?.filter((p) => p.name === 'chromium') ?? config.projects,
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/fixture.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { test as base, chromium } from '@playwright/test'
|
|
3
|
+
import { INFO_PATH } from './info-path.mjs'
|
|
4
|
+
|
|
5
|
+
function readEndpoint() {
|
|
6
|
+
return JSON.parse(readFileSync(INFO_PATH, 'utf8')).endpoint
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const test = base.extend({
|
|
10
|
+
browser: [
|
|
11
|
+
async ({}, use) => {
|
|
12
|
+
const browser = await chromium.connectOverCDP(readEndpoint())
|
|
13
|
+
await use(browser)
|
|
14
|
+
// Do not close — the underlying browser is managed by the OS, not by us.
|
|
15
|
+
},
|
|
16
|
+
{ scope: 'worker' },
|
|
17
|
+
],
|
|
18
|
+
|
|
19
|
+
context: async ({ browser }, use, testInfo) => {
|
|
20
|
+
// ArkWeb CDP doesn't implement Target.createBrowserContext, so newContext()
|
|
21
|
+
// would fail. Reuse the default context and force-feed baseURL into the
|
|
22
|
+
// private _options so internal URL resolution (toHaveURL, locators) works.
|
|
23
|
+
const ctx = browser.contexts()[0]
|
|
24
|
+
const baseURL = testInfo.project.use.baseURL
|
|
25
|
+
if (baseURL) ctx._options.baseURL = baseURL
|
|
26
|
+
await use(ctx)
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
page: async ({ context }, use, testInfo) => {
|
|
30
|
+
const pages = context.pages()
|
|
31
|
+
if (pages.length === 0) {
|
|
32
|
+
throw new Error('No pages in ArkWeb CDP context. Open a tab in the browser first.')
|
|
33
|
+
}
|
|
34
|
+
const page = pages.find((p) => p.url().startsWith('http://localhost')) ?? pages[0]
|
|
35
|
+
|
|
36
|
+
// page.goto resolves URLs against the frame, not _options, so it needs its
|
|
37
|
+
// own baseURL wrapper to make `/foo`-style relative paths work.
|
|
38
|
+
const baseURL = testInfo.project.use.baseURL
|
|
39
|
+
if (baseURL) {
|
|
40
|
+
const root = baseURL.replace(/\/$/, '')
|
|
41
|
+
const origGoto = page.goto.bind(page)
|
|
42
|
+
page.goto = (url, opts) => origGoto(url.startsWith('/') ? root + url : url, opts)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await use(page)
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export { expect } from '@playwright/test'
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { tmpdir } from 'node:os'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
// Setup writes the CDP endpoint info here; fixture and teardown read from it.
|
|
5
|
+
// Override with OHOS_PW_INFO_PATH if you need a deterministic location.
|
|
6
|
+
export const INFO_PATH =
|
|
7
|
+
process.env.OHOS_PW_INFO_PATH ?? resolve(tmpdir(), 'ohos-playwright-cdp.json')
|
package/src/loader.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { resolve as resolvePath, dirname } from 'node:path'
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
3
|
+
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
5
|
+
const FIXTURE_URL = pathToFileURL(resolvePath(__dirname, 'fixture.mjs')).href
|
|
6
|
+
|
|
7
|
+
const TARGET = '@playwright/test'
|
|
8
|
+
|
|
9
|
+
// Match Playwright's default testMatch: *.spec.* or *.test.* with common
|
|
10
|
+
// JS/TS extensions. Only files matching this pattern get '@playwright/test'
|
|
11
|
+
// rewritten — user config (playwright.config.ts), helpers, and node_modules
|
|
12
|
+
// stay on stock Playwright.
|
|
13
|
+
const TEST_FILE = /\.(spec|test)\.[mc]?[tj]sx?$/
|
|
14
|
+
|
|
15
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
16
|
+
if (specifier === TARGET) {
|
|
17
|
+
const parent = context.parentURL ?? ''
|
|
18
|
+
if (TEST_FILE.test(parent)) {
|
|
19
|
+
return nextResolve(FIXTURE_URL, context)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return nextResolve(specifier, context)
|
|
23
|
+
}
|
package/src/register.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { register } from 'node:module'
|
|
2
|
+
|
|
3
|
+
// Adapter only activates on OpenHarmony — elsewhere this file is a no-op so
|
|
4
|
+
// the same ohos-playwright entry point and the same playwright.config.ts can
|
|
5
|
+
// run on Windows / Linux / macOS with stock Playwright.
|
|
6
|
+
if (process.platform === 'openharmony') {
|
|
7
|
+
// Mark the run as OpenHarmony before we lie about the platform — any code
|
|
8
|
+
// downstream (notably withOpenHarmony in the user's config) should consult
|
|
9
|
+
// OHOS_PW_HOST instead of process.platform, which is about to read 'linux'.
|
|
10
|
+
process.env.OHOS_PW_HOST = '1'
|
|
11
|
+
|
|
12
|
+
// Playwright's hostPlatform detection only branches on linux/darwin/win32.
|
|
13
|
+
// On OpenHarmony it falls through to "<unknown>" and various code paths
|
|
14
|
+
// break. We connect over CDP and never touch Playwright's bundled browser
|
|
15
|
+
// binaries, so it's safe to advertise linux for the duration of this
|
|
16
|
+
// process.
|
|
17
|
+
Object.defineProperty(process, 'platform', { value: 'linux' })
|
|
18
|
+
|
|
19
|
+
register('./loader.mjs', import.meta.url)
|
|
20
|
+
}
|
package/src/setup.mjs
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { execSync } 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
|
+
|
|
9
|
+
const HDC = process.env.OHOS_PW_HDC ?? '/data/service/hnp/bin/hdc'
|
|
10
|
+
const BUNDLE = process.env.OHOS_PW_BUNDLE ?? 'com.huawei.hmos.browser'
|
|
11
|
+
const LAUNCH_URL = process.env.OHOS_PW_LAUNCH_URL ?? 'http://localhost:5173'
|
|
12
|
+
|
|
13
|
+
function sh(cmd) {
|
|
14
|
+
return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function shellOnDevice(cmd) {
|
|
18
|
+
return sh(`${HDC} shell "${cmd.replace(/"/g, '\\"')}"`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function findBrowserPid() {
|
|
22
|
+
const ps = shellOnDevice('ps -ef')
|
|
23
|
+
for (const line of ps.split('\n')) {
|
|
24
|
+
const parts = line.trim().split(/\s+/)
|
|
25
|
+
const cmd = parts[parts.length - 1]
|
|
26
|
+
if (cmd === BUNDLE) return parseInt(parts[1], 10)
|
|
27
|
+
}
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function launchBrowser() {
|
|
32
|
+
shellOnDevice(`aa start -b ${BUNDLE} -m entry -a MainAbility -U ${LAUNCH_URL}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function findDevToolsSocket(pid) {
|
|
36
|
+
const unix = shellOnDevice('cat /proc/net/unix')
|
|
37
|
+
const name = `webview_devtools_remote_${pid}`
|
|
38
|
+
return unix.includes(`@${name}`) ? name : null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pickFreePort() {
|
|
42
|
+
return new Promise((res, rej) => {
|
|
43
|
+
const srv = createServer()
|
|
44
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
45
|
+
const port = srv.address().port
|
|
46
|
+
srv.close((err) => (err ? rej(err) : res(port)))
|
|
47
|
+
})
|
|
48
|
+
srv.on('error', rej)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setupForward(port, socketName) {
|
|
53
|
+
sh(`${HDC} fport tcp:${port} localabstract:${socketName}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function probeCdp(port) {
|
|
57
|
+
return new Promise((res) => {
|
|
58
|
+
const req = http.get(`http://127.0.0.1:${port}/json/version`, (r) => {
|
|
59
|
+
let body = ''
|
|
60
|
+
r.on('data', (c) => (body += c))
|
|
61
|
+
r.on('end', () => res({ ok: r.statusCode === 200, body }))
|
|
62
|
+
})
|
|
63
|
+
req.on('error', (e) => res({ ok: false, err: e.code }))
|
|
64
|
+
req.setTimeout(3000, () => {
|
|
65
|
+
req.destroy()
|
|
66
|
+
res({ ok: false, err: 'TIMEOUT' })
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
72
|
+
|
|
73
|
+
const IP_PORT_RE = /^(\d{1,3}\.){3}\d{1,3}:\d+$/
|
|
74
|
+
|
|
75
|
+
function listTargets() {
|
|
76
|
+
// hdc emits literal '[Empty]' when no devices are connected.
|
|
77
|
+
return sh(`${HDC} list targets`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function discoverDevices() {
|
|
81
|
+
// `hdc discover` broadcasts on UDP:8710 and blocks for a few seconds. Lines
|
|
82
|
+
// beginning with [Info] are noise; real entries are bare ip:port. Exit code
|
|
83
|
+
// is always 0 — parse stdout, don't trust status.
|
|
84
|
+
let out = ''
|
|
85
|
+
try {
|
|
86
|
+
out = execSync(`${HDC} discover`, {
|
|
87
|
+
encoding: 'utf8',
|
|
88
|
+
timeout: 6000,
|
|
89
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
90
|
+
})
|
|
91
|
+
} catch (e) {
|
|
92
|
+
out = e.stdout?.toString() ?? ''
|
|
93
|
+
}
|
|
94
|
+
return out
|
|
95
|
+
.split('\n')
|
|
96
|
+
.map((s) => s.trim())
|
|
97
|
+
.filter((s) => IP_PORT_RE.test(s))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function tconn(addr) {
|
|
101
|
+
try {
|
|
102
|
+
const out = execSync(`${HDC} tconn ${addr}`, {
|
|
103
|
+
encoding: 'utf8',
|
|
104
|
+
timeout: 10000,
|
|
105
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
106
|
+
})
|
|
107
|
+
return out.includes('Connect OK')
|
|
108
|
+
} catch {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function promptAddress() {
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
116
|
+
rl.question('[ohos-playwright] paste device ip:port (Enter to abort): ', (ans) => {
|
|
117
|
+
rl.close()
|
|
118
|
+
resolve(ans.trim())
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const CONNECT_HELP = [
|
|
124
|
+
'[ohos-playwright] 未发现设备。请在设备上:',
|
|
125
|
+
' 1) 进入「设置 → 关于本机」连点版本号开启「开发者选项」',
|
|
126
|
+
' 2) 进入「开发者选项」启用「无线调试」',
|
|
127
|
+
' 3) 确认设备与本机在同一 Wi-Fi 下',
|
|
128
|
+
' 4) 防火墙放行本机 UDP:8710 入站(hdc discover 广播用)',
|
|
129
|
+
'也可手动跑 `hdc tconn <ip:port>` 后重新启动测试。',
|
|
130
|
+
'若不希望自动连接,设 OHOS_PW_AUTO_CONNECT=0 跳过。',
|
|
131
|
+
].join('\n')
|
|
132
|
+
|
|
133
|
+
async function ensureDeviceConnected() {
|
|
134
|
+
if (process.env.OHOS_PW_AUTO_CONNECT === '0') return
|
|
135
|
+
|
|
136
|
+
if (listTargets() !== '[Empty]') return
|
|
137
|
+
|
|
138
|
+
console.log('[ohos-playwright] no device connected, broadcasting (hdc discover)...')
|
|
139
|
+
const found = discoverDevices()
|
|
140
|
+
|
|
141
|
+
if (found.length > 0) {
|
|
142
|
+
console.log(`[ohos-playwright] discovered: ${found.join(', ')}`)
|
|
143
|
+
for (const addr of found) {
|
|
144
|
+
console.log(`[ohos-playwright] hdc tconn ${addr}`)
|
|
145
|
+
if (tconn(addr) && listTargets() !== '[Empty]') return
|
|
146
|
+
}
|
|
147
|
+
console.warn('[ohos-playwright] discovered devices but none connected successfully')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!process.stdin.isTTY) throw new Error(CONNECT_HELP)
|
|
151
|
+
|
|
152
|
+
console.log(CONNECT_HELP)
|
|
153
|
+
const addr = await promptAddress()
|
|
154
|
+
if (!addr) throw new Error('[ohos-playwright] no device address provided; aborting.')
|
|
155
|
+
if (!IP_PORT_RE.test(addr)) {
|
|
156
|
+
throw new Error(`[ohos-playwright] "${addr}" is not a valid ip:port.`)
|
|
157
|
+
}
|
|
158
|
+
if (!tconn(addr)) throw new Error(`[ohos-playwright] hdc tconn ${addr} failed.`)
|
|
159
|
+
if (listTargets() === '[Empty]') {
|
|
160
|
+
throw new Error('[ohos-playwright] tconn reported OK but list targets is still empty.')
|
|
161
|
+
}
|
|
162
|
+
console.log(`[ohos-playwright] connected: ${addr}`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export default async function globalSetup() {
|
|
166
|
+
await ensureDeviceConnected()
|
|
167
|
+
console.log(`[ohos-playwright] locating ${BUNDLE}...`)
|
|
168
|
+
let pid = findBrowserPid()
|
|
169
|
+
if (!pid) {
|
|
170
|
+
console.log('[ohos-playwright] browser not running, launching...')
|
|
171
|
+
launchBrowser()
|
|
172
|
+
for (let i = 0; i < 20 && !pid; i++) {
|
|
173
|
+
await sleep(500)
|
|
174
|
+
pid = findBrowserPid()
|
|
175
|
+
}
|
|
176
|
+
if (!pid) throw new Error(`Failed to launch ${BUNDLE}`)
|
|
177
|
+
}
|
|
178
|
+
console.log(`[ohos-playwright] browser pid=${pid}`)
|
|
179
|
+
|
|
180
|
+
let socket = findDevToolsSocket(pid)
|
|
181
|
+
for (let i = 0; i < 10 && !socket; i++) {
|
|
182
|
+
await sleep(500)
|
|
183
|
+
socket = findDevToolsSocket(pid)
|
|
184
|
+
}
|
|
185
|
+
if (!socket) throw new Error(`DevTools socket not found for pid ${pid}`)
|
|
186
|
+
console.log(`[ohos-playwright] socket=${socket}`)
|
|
187
|
+
|
|
188
|
+
const port = await pickFreePort()
|
|
189
|
+
setupForward(port, socket)
|
|
190
|
+
console.log(`[ohos-playwright] hdc fport tcp:${port} -> localabstract:${socket}`)
|
|
191
|
+
|
|
192
|
+
const probe = await probeCdp(port)
|
|
193
|
+
if (!probe.ok) throw new Error(`CDP probe failed: ${probe.err || probe.body}`)
|
|
194
|
+
const info = JSON.parse(probe.body)
|
|
195
|
+
console.log(`[ohos-playwright] CDP ready: ${info.Browser}`)
|
|
196
|
+
|
|
197
|
+
mkdirSync(dirname(INFO_PATH), { recursive: true })
|
|
198
|
+
writeFileSync(
|
|
199
|
+
INFO_PATH,
|
|
200
|
+
JSON.stringify({ port, pid, socket, endpoint: `http://127.0.0.1:${port}` }, null, 2),
|
|
201
|
+
)
|
|
202
|
+
console.log(`[ohos-playwright] wrote ${INFO_PATH}`)
|
|
203
|
+
}
|
package/src/teardown.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { readFileSync, unlinkSync } from 'node:fs'
|
|
3
|
+
import { INFO_PATH } from './info-path.mjs'
|
|
4
|
+
|
|
5
|
+
const HDC = process.env.OHOS_PW_HDC ?? '/data/service/hnp/bin/hdc'
|
|
6
|
+
|
|
7
|
+
export default async function globalTeardown() {
|
|
8
|
+
let info
|
|
9
|
+
try {
|
|
10
|
+
info = JSON.parse(readFileSync(INFO_PATH, 'utf8'))
|
|
11
|
+
} catch {
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
const ruler = `tcp:${info.port} localabstract:${info.socket}`
|
|
15
|
+
try {
|
|
16
|
+
execSync(`${HDC} fport rm "${ruler}"`, { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
17
|
+
console.log(`[ohos-playwright] removed fport ${ruler}`)
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.warn(`[ohos-playwright] fport rm failed (non-fatal): ${e.message?.split('\n')[0]}`)
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
unlinkSync(INFO_PATH)
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|