ohos-playwright 0.1.1 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ohos-playwright",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Playwright adapter for OpenHarmony / ArkWeb via hdc + CDP",
5
5
  "license": "MIT",
6
6
  "author": "social4hyq",
@@ -24,33 +24,35 @@
24
24
  ],
25
25
  "type": "module",
26
26
  "scripts": {
27
- "test": "node --test src/loader.test.mjs"
27
+ "test": "node --test src/*.test.mts",
28
+ "typecheck": "tsc --noEmit"
28
29
  },
29
30
  "engines": {
30
31
  "node": ">=24"
31
32
  },
32
33
  "exports": {
33
- "./fixture": "./src/fixture.mjs",
34
- "./setup": "./src/setup.mjs",
35
- "./teardown": "./src/teardown.mjs",
36
- "./register": "./src/register.mjs",
37
- "./loader": "./src/loader.mjs",
38
- "./config": {
39
- "types": "./src/config.d.mts",
40
- "default": "./src/config.mjs"
41
- }
34
+ "./fixture": "./src/fixture.mts",
35
+ "./setup": "./src/setup.mts",
36
+ "./teardown": "./src/teardown.mts",
37
+ "./register": "./src/register.mts",
38
+ "./loader": "./src/loader.mts",
39
+ "./config": "./src/config.mts"
42
40
  },
43
41
  "bin": {
44
- "ohos-playwright": "src/cli.mjs"
42
+ "ohos-playwright": "./src/cli.mts"
45
43
  },
46
44
  "files": [
47
45
  "src",
48
- "!src/*.test.mjs",
49
- "SKILL.md"
46
+ "!src/*.test.mts"
50
47
  ],
51
48
  "peerDependencies": {
52
49
  "@playwright/test": ">=1.59.0"
53
50
  },
51
+ "devDependencies": {
52
+ "@playwright/test": ">=1.59.0",
53
+ "@types/node": "^25.9.2",
54
+ "typescript": "^6.0.3"
55
+ },
54
56
  "publishConfig": {
55
57
  "access": "public",
56
58
  "provenance": true
@@ -13,7 +13,7 @@ const req = createRequire(resolve(process.cwd(), 'noop.mjs'))
13
13
 
14
14
  // @playwright/test's exports map blocks direct subpath resolution to cli.js,
15
15
  // so resolve the main entry and walk up to the package root, then append it.
16
- let pwEntry
16
+ let pwEntry: string
17
17
  try {
18
18
  pwEntry = req.resolve('@playwright/test')
19
19
  } catch {
@@ -26,10 +26,21 @@ try {
26
26
  process.exit(1)
27
27
  }
28
28
  let pkgRoot = dirname(pwEntry)
29
- while (!existsSync(resolve(pkgRoot, 'package.json'))) pkgRoot = dirname(pkgRoot)
29
+ let levels = 0
30
+ while (!existsSync(resolve(pkgRoot, 'package.json')) && levels++ < 50) {
31
+ pkgRoot = dirname(pkgRoot)
32
+ }
33
+ if (!existsSync(resolve(pkgRoot, 'package.json'))) {
34
+ console.error(
35
+ `[ohos-playwright] Cannot find @playwright/test package.json (walked up from ${dirname(pwEntry)}). ` +
36
+ 'Please verify @playwright/test is correctly installed.',
37
+ )
38
+ process.exit(1)
39
+ }
30
40
  const playwrightCli = resolve(pkgRoot, 'cli.js')
31
41
 
32
- const register = resolve(import.meta.dirname, 'register.mjs')
42
+ // Node 24 has native TypeScript support; register.mts is resolved directly.
43
+ const register = resolve(import.meta.dirname!, 'register.mts')
33
44
 
34
45
  const child = spawn(
35
46
  process.execPath,
@@ -1,3 +1,5 @@
1
+ import type { PlaywrightTestConfig } from '@playwright/test'
2
+
1
3
  // Wrap a base Playwright config so it transparently turns into an ArkWeb/CDP
2
4
  // run on OpenHarmony and stays stock everywhere else. Usage:
3
5
  //
@@ -7,7 +9,7 @@
7
9
  // export default defineConfig(withOpenHarmony({ ...baseConfig }))
8
10
  //
9
11
  // On non-OpenHarmony hosts the input config is returned unchanged.
10
- export function withOpenHarmony(config) {
12
+ export function withOpenHarmony(config: PlaywrightTestConfig): PlaywrightTestConfig {
11
13
  // Consult OHOS_PW_HOST, not process.platform — register.mjs has already
12
14
  // overwritten platform to 'linux' by the time this function runs.
13
15
  if (!process.env.OHOS_PW_HOST) return config
@@ -0,0 +1,59 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { test as base, chromium } from '@playwright/test'
3
+ import type { Browser, BrowserContext, Page } from '@playwright/test'
4
+ import { INFO_PATH, type CdpInfo } from './info-path.mts'
5
+
6
+ function readEndpoint(): string {
7
+ return (JSON.parse(readFileSync(INFO_PATH, 'utf8')) as CdpInfo).endpoint
8
+ }
9
+
10
+ export const test = base.extend({
11
+ browser: [
12
+ async ({}, use: (b: Browser) => Promise<void>) => {
13
+ const browser = await chromium.connectOverCDP(readEndpoint())
14
+ await use(browser)
15
+ },
16
+ { scope: 'worker' as const },
17
+ ],
18
+
19
+ context: async (
20
+ { browser }: { browser: Browser },
21
+ use: (c: BrowserContext) => Promise<void>,
22
+ testInfo: { project: { use: { baseURL?: string } } },
23
+ ) => {
24
+ const ctx = browser.contexts()[0]
25
+ const baseURL = testInfo.project.use.baseURL
26
+ if (baseURL) {
27
+ try {
28
+ const opts = (ctx as unknown as Record<string, unknown>)._options as Record<string, unknown> | undefined
29
+ if (opts && typeof opts === 'object') opts.baseURL = baseURL
30
+ } catch (e: unknown) {
31
+ console.warn(`[ohos-playwright] Failed to inject baseURL: ${e instanceof Error ? e.message : e}`)
32
+ }
33
+ }
34
+ await use(ctx)
35
+ },
36
+
37
+ page: async (
38
+ { context }: { context: BrowserContext },
39
+ use: (p: Page) => Promise<void>,
40
+ testInfo: { project: { use: { baseURL?: string } } },
41
+ ) => {
42
+ const pages = context.pages()
43
+ if (pages.length === 0) throw new Error('No pages in ArkWeb CDP context. Open a tab first.')
44
+ const page = pages.find((p) => p.url().startsWith('http://localhost')) ?? pages[0]
45
+
46
+ const baseURL = testInfo.project.use.baseURL
47
+ if (baseURL) {
48
+ const root = baseURL.replace(/\/+$/, '')
49
+ const origGoto = page.goto.bind(page)
50
+ page.goto = ((url: string, opts?: Record<string, unknown>) =>
51
+ origGoto((url.startsWith('/') && !url.startsWith('//')) ? root + url : url, opts)
52
+ ) as typeof page.goto
53
+ }
54
+
55
+ await use(page)
56
+ },
57
+ })
58
+
59
+ export { expect } from '@playwright/test'
@@ -3,5 +3,12 @@ import { resolve } from 'node:path'
3
3
 
4
4
  // Setup writes the CDP endpoint info here; fixture and teardown read from it.
5
5
  // Override with OHOS_PW_INFO_PATH if you need a deterministic location.
6
- export const INFO_PATH =
6
+ export const INFO_PATH: string =
7
7
  process.env.OHOS_PW_INFO_PATH ?? resolve(tmpdir(), 'ohos-playwright-cdp.json')
8
+
9
+ export interface CdpInfo {
10
+ port: number
11
+ pid: number
12
+ socket: string
13
+ endpoint: string
14
+ }
package/src/loader.mts ADDED
@@ -0,0 +1,31 @@
1
+ import { resolve as resolvePath } from 'node:path'
2
+ import { pathToFileURL } from 'node:url'
3
+
4
+ const FIXTURE_URL = pathToFileURL(resolvePath(import.meta.dirname!, 'fixture.mts')).href
5
+ const TARGET = '@playwright/test'
6
+ const TEST_FILE = /\.(spec|test)\.[mc]?[tj]sx?$/
7
+ const PACKAGE_ROOT_URL = pathToFileURL(resolvePath(import.meta.dirname!, '..') + '/').href
8
+ const PROJECT_ANCHOR = pathToFileURL(resolvePath(process.cwd(), 'noop.mjs')).href
9
+
10
+ interface ResolveContext { parentURL?: string; [key: string]: unknown }
11
+ interface NextResolve {
12
+ (specifier: string, context: ResolveContext): { url: string } | Promise<{ url: string }>
13
+ }
14
+
15
+ export async function resolve(
16
+ specifier: string,
17
+ context: ResolveContext,
18
+ nextResolve: NextResolve,
19
+ ): Promise<{ url: string }> {
20
+ if (specifier === TARGET) {
21
+ const parent = context.parentURL ?? ''
22
+ if (TEST_FILE.test(parent)) {
23
+ const result = await nextResolve(FIXTURE_URL, context)
24
+ return { url: result.url }
25
+ }
26
+ if (parent.startsWith(PACKAGE_ROOT_URL)) {
27
+ return nextResolve(specifier, { ...context, parentURL: PROJECT_ANCHOR })
28
+ }
29
+ }
30
+ return nextResolve(specifier, context)
31
+ }
@@ -3,7 +3,7 @@ import { register } from 'node:module'
3
3
  // Adapter only activates on OpenHarmony — elsewhere this file is a no-op so
4
4
  // the same ohos-playwright entry point and the same playwright.config.ts can
5
5
  // run on Windows / Linux / macOS with stock Playwright.
6
- if (process.platform === 'openharmony') {
6
+ if ((process.platform as string) === 'openharmony') {
7
7
  // Mark the run as OpenHarmony before we lie about the platform — any code
8
8
  // downstream (notably withOpenHarmony in the user's config) should consult
9
9
  // OHOS_PW_HOST instead of process.platform, which is about to read 'linux'.
@@ -16,5 +16,5 @@ if (process.platform === 'openharmony') {
16
16
  // process.
17
17
  Object.defineProperty(process, 'platform', { value: 'linux' })
18
18
 
19
- register('./loader.mjs', import.meta.url)
19
+ register('./loader.mts', import.meta.url)
20
20
  }
package/src/setup.mts ADDED
@@ -0,0 +1,224 @@
1
+ import { execFileSync, type ExecFileSyncOptions } 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.mts'
8
+
9
+ const HDC: string = process.env.OHOS_PW_HDC ?? '/data/service/hnp/bin/hdc'
10
+ const BUNDLE: string = process.env.OHOS_PW_BUNDLE ?? 'com.huawei.hmos.browser'
11
+ const LAUNCH_URL: string = process.env.OHOS_PW_LAUNCH_URL ?? 'http://localhost:5173'
12
+
13
+ const HDC_OPTS: ExecFileSyncOptions = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] } as const
14
+
15
+ function hdc(args: string[], opts?: Partial<ExecFileSyncOptions>): string {
16
+ return String(execFileSync(HDC, args, { ...HDC_OPTS, ...opts })).trim()
17
+ }
18
+
19
+ function shellOnDevice(cmd: string): string { return hdc(['shell', cmd]) }
20
+
21
+ const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms))
22
+
23
+ interface RetryOptions { max?: number; interval?: number; label?: string }
24
+
25
+ // Exponential backoff: 100ms → 200ms → 400ms … capped at `interval` (default 1000ms).
26
+ export async function retry<T>(
27
+ fn: () => T | Promise<T>,
28
+ { max = 10, interval = 1000, label = '' }: RetryOptions = {},
29
+ ): Promise<T> {
30
+ for (let i = 0; i < max; i++) {
31
+ try { const r = await fn(); if (r) return r } catch {}
32
+ if (i < max - 1) {
33
+ const delay = Math.min(100 * Math.pow(2, i), interval)
34
+ await sleep(delay)
35
+ }
36
+ }
37
+ throw new Error(label ? `${label}: exhausted retries (${max} attempts)` : `retry exhausted after ${max} attempts`)
38
+ }
39
+
40
+ // Batch ps + /proc/net/unix into a single hdc shell call — avoids two
41
+ // subprocess spawns and transfers.
42
+ function fetchDeviceState(): { ps: string; unix: string } {
43
+ const raw = shellOnDevice('ps -o pid,args; echo "---SOCKET---"; cat /proc/net/unix')
44
+ const [ps, unix] = raw.split('---SOCKET---')
45
+ return { ps: ps || '', unix: unix || '' }
46
+ }
47
+
48
+ export function findBrowserPid(): number | null {
49
+ const { ps } = fetchDeviceState()
50
+ for (const line of ps.split('\n')) {
51
+ const t = line.trim()
52
+ if (!t) continue
53
+ const s = t.indexOf(' ')
54
+ if (s === -1) continue
55
+ if (t.slice(s + 1).includes(BUNDLE)) {
56
+ const pid = parseInt(t.slice(0, s), 10)
57
+ if (!Number.isNaN(pid)) return pid
58
+ }
59
+ }
60
+ return null
61
+ }
62
+
63
+ function launchBrowser(): void {
64
+ shellOnDevice(`aa start -b ${BUNDLE} -m entry -a MainAbility -U ${LAUNCH_URL}`)
65
+ }
66
+
67
+ function findDevToolsSocket(pid: number, cachedUnix?: string): string | null {
68
+ const unix = cachedUnix ?? shellOnDevice('cat /proc/net/unix')
69
+ const name = `webview_devtools_remote_${pid}`
70
+ return unix.includes(`@${name}`) ? name : null
71
+ }
72
+
73
+ function pickFreePort(): Promise<number> {
74
+ return new Promise((res, rej) => {
75
+ const srv = createServer()
76
+ srv.listen(0, '127.0.0.1', () => {
77
+ const a = srv.address()
78
+ if (a && typeof a === 'object') srv.close((e) => e ? rej(e) : res(a.port))
79
+ else srv.close(() => rej(new Error('Failed to get port')))
80
+ })
81
+ srv.on('error', rej)
82
+ })
83
+ }
84
+
85
+ function setupForward(port: number, socketName: string): void {
86
+ const ruler = `tcp:${port} localabstract:${socketName}`
87
+ try { hdc(['fport', 'rm', ruler]) } catch {}
88
+ hdc(['fport', 'tcp:' + port, 'localabstract:' + socketName])
89
+ }
90
+
91
+ interface CdpProbeResult { ok: boolean; err?: string; body?: string }
92
+
93
+ function probeCdp(port: number): Promise<CdpProbeResult> {
94
+ return new Promise((res) => {
95
+ const req = http.get(`http://127.0.0.1:${port}/json/version`, (r) => {
96
+ let b = ''
97
+ r.on('data', (c: string) => (b += c))
98
+ r.on('end', () => res({ ok: r.statusCode === 200, body: b }))
99
+ })
100
+ req.on('error', (e: NodeJS.ErrnoException) => res({ ok: false, err: e.code }))
101
+ req.setTimeout(2000, () => { req.destroy(); res({ ok: false, err: 'TIMEOUT' }) })
102
+ })
103
+ }
104
+
105
+ const IP_PORT_RE = /^(\d{1,3}\.){3}\d{1,3}:\d+$/
106
+
107
+ function listTargets(): string { return hdc(['list', 'targets']) }
108
+
109
+ export function hasDeviceConnected(): boolean {
110
+ const t = listTargets()
111
+ return t.length > 0 && t !== '[Empty]'
112
+ }
113
+
114
+ // discoverDevices timeout reduced from 6s to 3s — LAN broadcast on local
115
+ // network should respond within 1-2s; longer wait is unlikely to help.
116
+ export function discoverDevices(): string[] {
117
+ let out = ''
118
+ try { out = hdc(['discover'], { timeout: 3000 }) } catch (e: unknown) {
119
+ out = (e as { stdout?: Buffer | string }).stdout?.toString() ?? ''
120
+ }
121
+ return out.split('\n').map(s => s.trim()).filter(s => IP_PORT_RE.test(s))
122
+ }
123
+
124
+ function tconn(addr: string): boolean {
125
+ try { return hdc(['tconn', addr], { timeout: 10000 }).includes('Connect OK') } catch { return false }
126
+ }
127
+
128
+ function promptAddress(): Promise<string> {
129
+ return new Promise((res) => {
130
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
131
+ rl.question('[ohos-playwright] paste device ip:port (Enter to abort): ', (a) => { rl.close(); res(a.trim()) })
132
+ })
133
+ }
134
+
135
+ const CONNECT_HELP = [
136
+ '[ohos-playwright] 未发现设备。请在设备上:',
137
+ ' 1) 进入「设置 → 关于本机」连点版本号开启「开发者选项」',
138
+ ' 2) 进入「开发者选项」启用「无线调试」',
139
+ ' 3) 确认设备与本机在同一 Wi-Fi 下',
140
+ ' 4) 防火墙放行本机 UDP:8710 入站(hdc discover 广播用)',
141
+ '也可手动跑 `hdc tconn <ip:port>` 后重新启动测试。',
142
+ '若不希望自动连接,设 OHOS_PW_AUTO_CONNECT=0 跳过。',
143
+ ].join('\n')
144
+
145
+ export function tryLocalDevice(): boolean {
146
+ try {
147
+ const raw = String(execFileSync('param', ['get', 'persist.hdc.port'], { ...HDC_OPTS, timeout: 3000 })).trim()
148
+ const port = parseInt(raw, 10)
149
+ if (!port || port < 1 || port > 65535) return false
150
+ console.log(`[ohos-playwright] local device port from param: ${port}`)
151
+ const addr = `127.0.0.1:${port}`
152
+ if (tconn(addr) && hasDeviceConnected()) { console.log(`[ohos-playwright] connected: ${addr}`); return true }
153
+ console.warn(`[ohos-playwright] tconn ${addr} failed`)
154
+ } catch {}
155
+ return false
156
+ }
157
+
158
+ export async function ensureDeviceConnected(): Promise<void> {
159
+ if (process.env.OHOS_PW_AUTO_CONNECT === '0') return
160
+ if (hasDeviceConnected()) return
161
+ if (tryLocalDevice()) return
162
+
163
+ console.log('[ohos-playwright] no local device, broadcasting (hdc discover)...')
164
+ const found = discoverDevices()
165
+ for (const addr of found) {
166
+ console.log(`[ohos-playwright] hdc tconn ${addr}`)
167
+ if (tconn(addr) && hasDeviceConnected()) return
168
+ }
169
+ if (found.length > 0) console.warn('[ohos-playwright] discovered devices but none connected')
170
+
171
+ if (!process.stdin.isTTY) throw new Error(CONNECT_HELP)
172
+ console.log(CONNECT_HELP)
173
+ const addr = await promptAddress()
174
+ if (!addr) throw new Error('[ohos-playwright] no device address provided; aborting.')
175
+ if (!IP_PORT_RE.test(addr)) throw new Error(`[ohos-playwright] "${addr}" is not a valid ip:port.`)
176
+ if (!tconn(addr)) throw new Error(`[ohos-playwright] hdc tconn ${addr} failed.`)
177
+ if (!hasDeviceConnected()) throw new Error('[ohos-playwright] tconn reported OK but list targets still empty.')
178
+ console.log(`[ohos-playwright] connected: ${addr}`)
179
+ }
180
+
181
+ export default async function globalSetup(): Promise<void> {
182
+ await ensureDeviceConnected()
183
+ console.log(`[ohos-playwright] locating ${BUNDLE}...`)
184
+
185
+ // Batch ps + /proc/net/unix into a single hdc call.
186
+ let deviceState = fetchDeviceState()
187
+ let pid = findBrowserPid()
188
+
189
+ if (!pid) {
190
+ console.log('[ohos-playwright] browser not running, launching...')
191
+ launchBrowser()
192
+ // Backoff: 100, 200, 400, 800, 1000, 1000, … (max 20 attempts ≈ 10s total)
193
+ pid = await retry(findBrowserPid, { max: 20, interval: 1000, label: `Failed to launch ${BUNDLE}` }) as number
194
+ }
195
+ console.log(`[ohos-playwright] browser pid=${pid}`)
196
+
197
+ // Reuse the /proc/net/unix snapshot if it's fresh (same shell call as ps above).
198
+ // If we waited for browser launch, re-fetch since the socket may be new.
199
+ let unixCache: string | undefined = pid ? undefined : deviceState.unix
200
+ if (!unixCache) {
201
+ // Re-fetch device state to get fresh /proc/net/unix after browser launch
202
+ deviceState = fetchDeviceState()
203
+ unixCache = deviceState.unix
204
+ }
205
+
206
+ const socket = await retry(
207
+ () => findDevToolsSocket(pid, unixCache),
208
+ { max: 10, interval: 500, label: `DevTools socket not found for pid ${pid}` },
209
+ ) as string
210
+ console.log(`[ohos-playwright] socket=${socket}`)
211
+
212
+ const port = await pickFreePort()
213
+ setupForward(port, socket)
214
+ console.log(`[ohos-playwright] hdc fport tcp:${port} -> localabstract:${socket}`)
215
+
216
+ const probe = await probeCdp(port)
217
+ if (!probe.ok) throw new Error(`CDP probe failed: ${probe.err || probe.body}`)
218
+ const info = JSON.parse(probe.body!)
219
+ console.log(`[ohos-playwright] CDP ready: ${info.Browser}`)
220
+
221
+ mkdirSync(dirname(INFO_PATH), { recursive: true })
222
+ writeFileSync(INFO_PATH, JSON.stringify({ port, pid, socket, endpoint: `http://127.0.0.1:${port}` }, null, 2))
223
+ console.log(`[ohos-playwright] wrote ${INFO_PATH}`)
224
+ }
@@ -0,0 +1,30 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { readFileSync, unlinkSync } from 'node:fs'
3
+ import { INFO_PATH, type CdpInfo } from './info-path.mts'
4
+
5
+ const HDC: string = process.env.OHOS_PW_HDC ?? '/data/service/hnp/bin/hdc'
6
+
7
+ export default async function globalTeardown(): Promise<void> {
8
+ let info: CdpInfo
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
+ execFileSync(HDC, ['fport', 'rm', ruler], { stdio: ['ignore', 'pipe', 'pipe'] })
17
+ console.log(`[ohos-playwright] removed fport ${ruler}`)
18
+ } catch (e: unknown) {
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
+ } catch (e: unknown) {
25
+ const err = e as NodeJS.ErrnoException
26
+ if (err.code !== 'ENOENT') {
27
+ console.warn(`[ohos-playwright] Failed to remove ${INFO_PATH}: ${err.message}`)
28
+ }
29
+ }
30
+ }
package/SKILL.md DELETED
@@ -1,155 +0,0 @@
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/src/config.d.mts DELETED
@@ -1,3 +0,0 @@
1
- import type { PlaywrightTestConfig } from '@playwright/test'
2
-
3
- export function withOpenHarmony(config: PlaywrightTestConfig): PlaywrightTestConfig
package/src/fixture.mjs DELETED
@@ -1,49 +0,0 @@
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'
package/src/loader.mjs DELETED
@@ -1,46 +0,0 @@
1
- import { resolve as resolvePath } from 'node:path'
2
- import { pathToFileURL } from 'node:url'
3
-
4
- const FIXTURE_URL = pathToFileURL(resolvePath(import.meta.dirname, 'fixture.mjs')).href
5
-
6
- const TARGET = '@playwright/test'
7
-
8
- // Match Playwright's default testMatch: *.spec.* or *.test.* with common
9
- // JS/TS extensions. Only files matching this pattern get '@playwright/test'
10
- // rewritten — user config (playwright.config.ts), helpers, and node_modules
11
- // stay on stock Playwright.
12
- const TEST_FILE = /\.(spec|test)\.[mc]?[tj]sx?$/
13
-
14
- // Compute the package root URL from the loader's own location. Used below to
15
- // detect when the importing module lives inside ohos-playwright, regardless of
16
- // what the package directory is named or how it was installed (file: symlink,
17
- // pnpm store hash, etc.). Trailing '/' is critical to avoid matching sibling
18
- // directories whose names happen to share the same prefix.
19
- const PACKAGE_ROOT_URL = pathToFileURL(resolvePath(import.meta.dirname, '..') + '/').href
20
-
21
- // Anchor URL in the consumer project root for ESM resolution fallback.
22
- // When ohos-playwright is a file: symlink, modules inside it can't resolve
23
- // peer dependencies from the consumer's node_modules. Overriding parentURL
24
- // to a file inside the project restores normal resolution.
25
- // noop.mjs doesn't need to exist on disk.
26
- // PROJECT_ANCHOR is computed once at module-load time. By the time this
27
- // module is --import'ed (via register.mjs), process.cwd() is guaranteed to be
28
- // the directory where the user invoked ohos-playwright test — Node sets cwd
29
- // before running --import hooks. Do NOT defer this to the resolve hook; the
30
- // string is needed for synchronous prefix matching in the hot path.
31
- const PROJECT_ANCHOR = pathToFileURL(resolvePath(process.cwd(), 'noop.mjs')).href
32
-
33
- export async function resolve(specifier, context, nextResolve) {
34
- if (specifier === TARGET) {
35
- const parent = context.parentURL ?? ''
36
- if (TEST_FILE.test(parent)) {
37
- // Spec files -> use custom ArkWeb fixture
38
- return nextResolve(FIXTURE_URL, context)
39
- }
40
- if (parent.startsWith(PACKAGE_ROOT_URL)) {
41
- // Internal modules (fixture.mjs, etc.) -> resolve from consumer project
42
- return nextResolve(specifier, { ...context, parentURL: PROJECT_ANCHOR })
43
- }
44
- }
45
- return nextResolve(specifier, context)
46
- }
package/src/setup.mjs DELETED
@@ -1,203 +0,0 @@
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 DELETED
@@ -1,24 +0,0 @@
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
- }