From a18014e1e334203f2993bfcef2ba8c22e00c3d75 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 19 Sep 2026 15:53:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A4=E6=8D=AE:=20=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E9=A1=B5=E8=AE=BE=E5=A4=87=E5=88=A4=E6=8D=AE=EF=BC=88=E5=88=B0?= =?UTF-8?q?=E6=9C=9F=E9=9D=99=E6=80=81=E5=88=A4=E6=8D=AE=E7=9A=84=E7=AC=AC?= =?UTF-8?q?=E4=B8=80=E6=9D=A1=E5=8D=87=E7=BA=A7=EF=BC=89+=20=E4=BF=AE?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=88=A4=E6=8D=AE=E5=9F=BA=E5=BB=BA=E7=9A=84?= =?UTF-8?q?=E4=B8=89=E4=B8=AA=E7=9C=9F=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `run-all.mjs` 的 `STATIC_ONLY` 里登记着 6 条"只能静态验"的判据,前提是 「本工作区能装、能点设备」。设备现在可用 ⇒ 它们**到期**了(欠账当场变红)。 这一批升级第一条:`harmony-admin`(用户管理页)。 ## 一、新设备判据:管理页真的能打开、列表真的渲染 上面 27 条静态判据判的都是逻辑与接线形态。它们全绿时, "管理页能不能打开、列表能不能渲染"**一句都没验过** —— 而它最容易坏: 路由注册对了但入口没接上、接口回来了但列表没渲染。 判据自己搭现场:拉起应用 → 进「我的」→ 滚到底 → 点「管理」→ 断言真的在管理页(出现「新建用户」)且**渲染出用户行**。 ## 二、途中撞出三个**判据自身**的 bug(都已修,都写了教训) 1. **`tapText` 从来没成功点过任何东西**(`lib/harmony-device.mjs`) `findByText` 返回的是**数组**,我当单个节点用了 ⇒ `node.attributes` 恒 `undefined` ⇒ 恒返回 `false`。症状极隐蔽:调用方以为"没找到那个文案", 实际是帮手自己坏了。修:从数组里挑,且**优先挑可点的那个** (同名文案常常一个可点、一个不可点)。 2. **判据之间互相干扰**(新增 `backToMain`) 每个设备判据都是写操作,会把前台留在它操作完的那一页。`harmony-admin` 把人留在管理页 —— 那是 `pushUrl` 出去的独立 `@Entry` 页,**没有侧栏** ⇒ 后面的 `cross-client-gesture` / `harmony-appearance` 找不到侧栏、 双双 skip(skip 原因写的是"宽屏侧栏找不到",看起来像功能没了)。 `launchOurApp` 解决不了(`aa start` 只切前台,不弹栈)⇒ 新增 `backToMain`。 3. **"找不到元素"要先分清是功能缺失还是判据没摆好现场** 本判据连栽四种形态,每一种都伪装成"功能缺失": · 锚点文案错(入口是「管理」,我按「用户管理」找 —— 后者只是说明的一部分) · 元素在滚动下方(dumpLayout 只报可见节点) · 文字节点不可点(`.onClick` 在包住它的容器上,ArkUI 的常态) · **滚动步长跨过了它**(诊断打印现场才发现:停在了「系统通知」那一带, 而「管理」只占 ~0.04 屏,一次 0.2 屏的滑动必然越过) ⇒ 最后改成"**先滚到底、再小步回扫**"(只管往前找在不均匀列表上必漏), 并加了 `AGENTMAIL_ADMIN_DEBUG=1` 的诊断入口把现场打出来。 ## 三、判据 `run-all.mjs` → `files=32 ran=32 checks=505 pass=505 fail=0 skip=0 red=0 broken=0 unreported=0`(连跑两次稳定)。 `harmony-admin` 27 → 28 条。`hvigorw assembleHap` 成功;前端重建。 **设备实测**:管理页从「我的」页打开,显示「管理 / 用户管理 3 / 新建用户」 与三行用户(jianf 管理员 / gui-lab 用户 / test 用户)。 **仍到期未升级**:`harmony-appearance`(已加 2 条设备判据,但登记里其余部分仍静态)、 `harmony-logic`、`cross-client-theme`、`appearance-defaults`、`harmony-imageprep` —— 按"每条缺什么设备侧验证"逐条来,不为了消数字而凑。 --- .../test/cross-client-gesture.test.mjs | 11 +- client/electron/test/harmony-admin.test.mjs | 199 ++++++++++++++++++ .../electron/test/harmony-appearance.test.mjs | 2 + client/electron/test/lib/harmony-device.mjs | 89 +++++++- client/electron/test/run-all.mjs | 2 +- 5 files changed, 297 insertions(+), 6 deletions(-) diff --git a/client/electron/test/cross-client-gesture.test.mjs b/client/electron/test/cross-client-gesture.test.mjs index 89ffa2c..fb6b853 100644 --- a/client/electron/test/cross-client-gesture.test.mjs +++ b/client/electron/test/cross-client-gesture.test.mjs @@ -41,7 +41,7 @@ import { code, prose } from './lib/read.mjs'; */ import { findHdc, hasTarget, dumpLayout, walk, swipe, tapText, - launchOurApp, boundsCenter, tap, + launchOurApp, boundsCenter, tap, backToMain, } from './lib/harmony-device.mjs'; import { test } from 'node:test'; import assert from 'node:assert/strict'; @@ -278,7 +278,14 @@ test('★ 设备:在网格列上左滑,日历标题真的翻到下一段', a * 这才是设备判据该有的样子(前置条件自己满足,而不是等人摆好)。 */ assert.ok(await launchOurApp(hdc), '要能拉起我们的应用并等到它到前台'); - await new Promise((r) => setTimeout(r, 1500)); + /* + * ★★ 必须先回**主界面**:前面跑过的设备判据可能把前台留在 + * push 出去的独立页(管理页/详情页/写信页)—— 那些页面**没有侧栏**, + * 于是这里找不到侧栏项、判据会 skip 成"导航轨只找到 1 项"。 + * 那是**判据间干扰**,不是功能缺失(详见 `backToMain` 的注释)。 + */ + assert.ok(await backToMain(hdc), '要能回到主界面(侧栏/底栏可见)'); + await new Promise((r) => setTimeout(r, 1200)); /* 切到日历窗格:宽屏点侧栏第 2 项 / 窄屏点底栏「日历」 */ const rootA = dumpLayout(hdc); diff --git a/client/electron/test/harmony-admin.test.mjs b/client/electron/test/harmony-admin.test.mjs index 1727a5f..82a3dde 100644 --- a/client/electron/test/harmony-admin.test.mjs +++ b/client/electron/test/harmony-admin.test.mjs @@ -526,3 +526,202 @@ test('★ 「我的」页的数据都来自服务端真实字段(不是写死/ assert.ok(key.includes('key_token'), 'UserKey 要有 key_token —— 密钥全文只在创建响应里出现一次,没有它就没法展示'); }); + +/* ═══════════════ 设备侧:管理页真的能打开、能操作 ═══════════════ */ + +test('★ 设备:管理页能从「我的」页打开,且列表真的渲染出用户', async (t) => { + /* + * 这条补的是 `run-all.mjs` 的 `STATIC_ONLY` 登记里说的那个缺口: + * 「用户管理页:`.ets` 页面要 hvigorw 才能编译、要设备才能点(本机两者都没有)」 + * + * 上面 27 条判的都是**逻辑与接线形态**。它们全绿时,"管理页能不能打开、 + * 列表能不能渲染"这件事**一句都没验过** —— 而它偏偏最容易坏: + * 路由注册对了但入口没接上、接口回来了但列表没渲染、非管理员也能看见入口。 + * + * ★★ 写这条判据时连撞三个坑,每个都是**判据自己的**错(记下来,它们是同类): + * ① 锚点文案写错:入口标题是「管理」,我却按「用户管理」找 + * (后者只是它下面那行说明的一部分)⇒ **永远跳过**。 + * 而那个跳过看起来像"非管理员、行为正确"—— 最危险的那种静默。 + * ② 元素在**滚动下方**:dumpLayout 只报可见节点 ⇒ 不滚到底根本搜不到。 + * ③ 文字节点不可点:`Text('管理')` 的 `clickable=false`, + * 点击挂在包住它的容器上(ArkUI 的常态)。 + * + * ⇒ 结论:设备判据里"找不到元素"必须先分清是**功能缺失**还是**判据没摆好现场**。 + * 前两个坑都是后者,而它们伪装成前者。 + */ + const D = await import('./lib/harmony-device.mjs'); + const hdc = D.findHdc(); + if (!hdc || !D.hasTarget(hdc)) { + return t.skip('设备不在 —— 本条的设备半边本次不跑(上面静态层仍把住逻辑与接线)'); + } + assert.ok(await D.launchOurApp(hdc), '要能拉起应用并等到它到前台'); + await new Promise((r) => setTimeout(r, 2000)); + + const dims = () => { + let w = 0; + let h = 0; + for (const n of D.walk(D.dumpLayout(hdc))) { + const m = /\[\d+,\d+\]\[(\d+),(\d+)\]/.exec(n.attributes?.bounds || ''); + if (m) { + w = Math.max(w, Number(m[1])); + h = Math.max(h, Number(m[2])); + } + } + return { w, h }; + }; + const hasText = (needle) => [...D.walk(D.dumpLayout(hdc))] + .some((n) => (n.attributes?.text || '').includes(needle)); + + /* 已经在管理页了?那直接用(判据不该强求"必须从某处点进来") */ + if (!hasText('新建用户')) { + const { w: scrW, h: scrH } = dims(); + const wide = scrW / scrH > 1.2; + + /* + * ① 进「我的」 + * + * ★★ 「在「我的」页」**不能用单个文案判**(第一版用「用户名」⇒ 假红): + * 本页有 8 个分段、可滚动,而**判据里的滚动会把「用户名」滚出屏幕** + * —— 同一个判据第二次跑到这里时(或前面判据把它留在这页且已滚动), + * `hasText('用户名')` 为假,于是它以为"不在「我的」页"、又去点一次导航, + * 结果页面被滚到别处、后面全乱。 + * + * 改用**分段标记**判定:这些文案分布在本页的不同段落, + * 滚动到任何位置都至少能看见一个。 + */ + const inMePane = () => ['用户名', '多账号', '外观', '客户端连接密钥', '修改密码'] + .some((marker) => hasText(marker)); + if (!inMePane()) { + if (wide) { + const avatar = [...D.walk(D.dumpLayout(hdc))].find((n) => { + const a = n.attributes || {}; + if (a.clickable !== 'true') return false; + const m = /\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(a.bounds || ''); + if (!m) return false; + const [, , y1, x2, y2] = m.map(Number); + return x2 <= scrW * 0.08 && y1 > scrH * 0.5 && (y2 - y1) > 80; + }); + if (!avatar) return t.skip('宽屏侧栏头像找不到 —— 无法进「我的」'); + const c = D.boundsCenter(avatar.attributes.bounds); + assert.ok(D.tap(hdc, c.cx, c.cy), '要能点到侧栏头像'); + } else { + assert.ok(D.tapText(hdc, '我的'), '要能点到「我的」'); + } + let landed = false; + for (let i = 0; i < 20; i++) { + await new Promise((r) => setTimeout(r, 500)); + if (inMePane()) { landed = true; break; } + } + assert.ok(landed, '要能进「我的」页'); + } + + /* + * ② 找「管理」入口(在 8 个分段之后,屏幕外)。 + * + * ★★ 滚动必须**小步 + 每步检查**(我前两版都栽在这里): + * 大步滑(`0.8h → 0.25h`)会**跳过**那个入口 —— 实测:滚 1~2 次时它在屏外, + * 第 3 次才出现(`y=1994`,屏高 2232,贴着底)。而"滑到底再看" + * 会因为惯性/回弹停在别处,于是"找不到"——看起来像功能缺失。 + * + * 现在每步只滑 ~1/4 屏,滚动后立刻查一次。 + */ + /* + * ★★ 第四版(诊断逼出来的真相):小步滚也会**越过**它。 + * 实测现场:滚到「系统通知」那一带(`03:50` 附近)时,「管理」**已经在上面**了 + * —— 它在「客户端连接密钥」之后、「系统通知」之前,而我的步长一次跨过了整个 + * 区间(每次 ~0.2 屏,而它只占 ~0.04 屏)。 + * + * ⇒ 正确策略是**先滚到底,再从下往上找**(到底之后它一定在屏幕内或其上方, + * 再按小步回滚就必然扫过它)。"只管往前找"在不均匀列表上必然漏。 + */ + const scrollDown = async (steps) => { + for (let i = 0; i < steps; i++) { + D.swipe(hdc, Math.round(scrW * 0.5), Math.round(scrH * 0.7), + Math.round(scrW * 0.5), Math.round(scrH * 0.3), 2500); + await new Promise((r) => setTimeout(r, 600)); + } + }; + /* ① 一口气滚到底(多滚几次确保到底) */ + await scrollDown(8); + /* ② 若「管理」已经在屏上就好了;否则小步**回滚**(内容往下走)扫回来 */ + let found = hasText('管理'); + for (let i = 0; i < 8 && !found; i++) { + D.swipe(hdc, Math.round(scrW * 0.5), Math.round(scrH * 0.35), + Math.round(scrW * 0.5), Math.round(scrH * 0.6), 1500); + await new Promise((r) => setTimeout(r, 650)); + found = hasText('管理'); + } + if (!found) { + const seen = [...D.walk(D.dumpLayout(hdc))] + .map((n) => (n.attributes?.text || '').trim()) + .filter(Boolean).slice(-20).join(' | '); + return t.skip(`滚到底后仍找不到「管理」入口(非管理员时这是正确行为)—— 末尾读到:${seen}`); + } + + /* + * ③ 点它。文字节点不可点(`.onClick` 在包住它的容器上),所以: + * 先让 `tapText` 试(它会挑同名的**可点**节点),失败再退回点文字中心 + * (命中测试会冒泡到祖先的 onClick)。 + * + * ★ 兜底那一步要**重新 dump 并当场找**,不能复用上面 `found` 那次的快照 —— + * 实测(套件里红、单独跑绿):`found` 为真说明"那一瞬间有", + * 而下一步再 dump 时可能正在重绘/滚动的惯性里 ⇒ 快照里没有 ⇒ 假红。 + * 设备上的两次 dump 之间窗口状态是可以变的,别假设它们一致。 + */ + /* + * ★★ 第三版:**直接按文字节点的矩形中心点**,不走 `tapText`。 + * + * 为什么 `tapText` 在这里不可靠(实测:单独跑绿、套件里红): + * 它先找"同名且**可点**"的节点;这个入口的 `.onClick` 挂在 + * **外面那层 `Column`** 上(`SettingsPage.ets:1144`),而那层的 + * bounds 比文字大一圈、且**没有文字** ⇒ `findByText` 找不到它, + * `tapText` 退回点文字中心;但文字节点的中心在**卡片内部**, + * 命中测试是否冒泡到那层 Column 取决于层级细节 —— 不稳定。 + * + * 而实测(手动点 `(1592, 2017)`,即文字所在行的中心)**每次都进管理页**。 + * ⇒ 用文字节点的**矩形中心**直接点,这条路径最接近"人点那一行"。 + */ + /* 诊断入口:`AGENTMAIL_ADMIN_DEBUG=1` 时把现场打出来(排查设备判据专用) */ + const dbg = process.env.AGENTMAIL_ADMIN_DEBUG + ? (m) => console.log(`[admin] ${m}`) : () => {}; + const snap = D.dumpLayout(hdc); + const node = [...D.walk(snap)] + .find((n) => (n.attributes?.text || '').trim() === '管理'); + if (!node) { + const seen = [...D.walk(snap)] + .map((n) => (n.attributes?.text || '').trim()).filter(Boolean).slice(-18).join(' | '); + dbg(`找不到「管理」节点;末尾文本:${seen}`); + } + assert.ok(node, '滚到之后「管理」文字节点应在 dump 里'); + const c = D.boundsCenter(node.attributes.bounds); + assert.ok(c, '要能解析出「管理」的坐标'); + /* + * 点之前**再确认一次前台是我们的应用**:套件里前面跑过的判据可能把前台 + * 切走(那是"判据间干扰"的另一种形态)。切走了就不是"点不到入口", + * 而是"点在了别人的界面上"—— 后者必须显式失败,不能算通过。 + */ + assert.equal(D.foregroundBundle(hdc), D.ourBundle(), + '点之前前台必须还是我们的应用(前面判据若切走了前台,这里点的是别人的界面)'); + const ok = D.tap(hdc, c.cx, c.cy); + dbg(`点 (${c.cx},${c.cy}) ⇒ ${ok}`); + assert.ok(ok, `要能点到「管理」入口(坐标 ${c.cx},${c.cy})`); + } + + /* ④ 等页面真的切过去 */ + let onAdmin = false; + for (let i = 0; i < 20; i++) { + await new Promise((r) => setTimeout(r, 500)); + if (hasText('新建用户')) { onAdmin = true; break; } + } + assert.ok(onAdmin, + '★ 点「管理」之后要真的在**管理页**(标志:出现「新建用户」按钮)—— ' + + '只断言"点到了入口"证明不了页面切过去了(路由没注册时点击静默无效)'); + + /* ⑤ 列表要真的渲染出用户行(抓"接口回来了但列表没接上") */ + const root = D.dumpLayout(hdc); + const texts = [...D.walk(root)].map((n) => (n.attributes?.text || '').trim()).filter(Boolean); + const userRows = texts.filter((x) => /用户管理|启用|管理员|用户$/.test(x)); + assert.ok(userRows.length > 0, + '★ 管理页要真的渲染出**用户行**(带角色/状态的那种)—— ' + + `只有标题而没有行,说明数据没渲染。实际读到:${texts.slice(0, 25).join(' | ')}`); +}); diff --git a/client/electron/test/harmony-appearance.test.mjs b/client/electron/test/harmony-appearance.test.mjs index 245da92..76f16fb 100644 --- a/client/electron/test/harmony-appearance.test.mjs +++ b/client/electron/test/harmony-appearance.test.mjs @@ -1226,6 +1226,8 @@ test('★ 设备:窗格内容不得超出屏幕(「我的」页滑杆数值 return t.skip('设备不在 —— 本条的设备半边本次不跑'); } assert.ok(await D.launchOurApp(hdc), '要能拉起应用'); + /* 回主界面(前面判据可能把前台留在 push 出去的页上 —— 那些页没有侧栏) */ + assert.ok(await D.backToMain(hdc), '要能回到主界面'); /* * 逐个窗格检查。宽屏下「我的」用侧栏头像进,窄屏用底栏 —— 这里只跑宽屏那份 diff --git a/client/electron/test/lib/harmony-device.mjs b/client/electron/test/lib/harmony-device.mjs index 056d5d6..10e8915 100644 --- a/client/electron/test/lib/harmony-device.mjs +++ b/client/electron/test/lib/harmony-device.mjs @@ -367,9 +367,92 @@ export async function launchOurApp(hdc, { settle = 400, tries = 40 } = {}) { export function tapText(hdc, text) { if (!hdc) return false; const root = dumpLayout(hdc); - const node = findByText(root, text); - if (node === null) return false; - const c = boundsCenter(node.attributes?.bounds); + /* + * ★★ 2026-09-19 修(真 bug,而且是我自己写的帮手): + * `findByText` 返回的是**数组**(同名节点可能多个),而我当单个节点用了 + * ⇒ `node.attributes` 恒为 `undefined` ⇒ 这个帮手**从来没成功点过任何东西**。 + * + * 它造成的症状极其隐蔽:`tapText(...)` 返回 `false`,调用方以为 + * "没找到那个文案"——而实际是**帮手自己坏了**。用它的判据(appearance/admin) + * 都会表现为"找不到入口"或"点不到",看起来像功能缺失。 + * + * 修两处:① 从数组里挑;② **优先挑可点的那个** —— 同名文案常常一个可点 + * (入口按钮)、一个不可点(说明文字),随便挑会点到说明文字上(点了没反应)。 + */ + const nodes = findByText(root, text); + if (nodes.length === 0) return false; + const target = nodes.find((n) => n.attributes?.clickable === 'true') || nodes[0]; + /* + * ★★ 2026-09-19 补第三层:文字节点本身常常**不可点**,点击挂在它**祖先**上。 + * + * 实测(「我的」页的管理入口):`Text('管理')` 的 `clickable=false`, + * 而 `.onClick` 写在包住它的那层 `Row` 上 —— 这是 ArkUI 的常态 + * (文字只是内容,可点的是容器)。直接点文字节点的中心**也能触到** + * (命中测试会冒泡到祖先),但那样判据就依赖"冒泡行为"而不是"我点到了那个入口"。 + * + * 所以这里显式用**祖先的可点容器**的边界:找不到可点祖先时才退回文字节点自己。 + * 当前实现从目标节点的 bounds 出发做一次"就近扩展": + * 若目标不可点,就用**同文本的可点兄弟/祖先链**(dump 树里 clickable 的那一层 + * 一般把文字整个包住,所以用它的 bounds 最稳)。 + */ + const clickableAncestor = (() => { + if (target.attributes?.clickable === 'true') return target; + /* 往上找:dump 树的节点带 children,父链要从根重新走一遍才能拿到 */ + let found = null; + const visit = (n) => { + if (found) return; + const kids = n.children || []; + for (const k of kids) visit(k); + if (found) return; + const self = n.attributes || {}; + if (self.clickable === 'true' + && String(self.bounds || '') === String(target.attributes?.bounds || '') + && kids.length > 0) { + found = n; + } + }; + visit(root); + return found || target; + })(); + const c = boundsCenter(clickableAncestor.attributes?.bounds); if (c === null) return false; return tap(hdc, c.cx, c.cy); } + + +/** + * 确保回到**主界面**(`MainPage`),而不是某个 push 出去的页 + * (管理页 / 详情页 / 写信页 —— 它们没有侧栏与底栏)。 + * + * ★★ 为什么需要它(2026-09-19 实测撞出来的**判据间干扰**): + * 每个设备判据都是"写操作",会把前台留在它操作完的那一页上。 + * 于是**前一个判据留下的位置**决定了后一个判据能不能跑: + * `harmony-admin` 把人留在管理页(那是 `pushUrl` 出去的独立 @Entry 页, + * **没有侧栏**)⇒ 后面的 `cross-client-gesture` / `harmony-appearance` + * 找不到宽屏侧栏 ⇒ 双双 skip。而它们的 skip 原因写的是 + * "宽屏侧栏找不到",看起来像功能没了。 + * + * `launchOurApp` 解决不了这个:`aa start` 只把应用切到前台, + * 不会把已经 push 的页面弹栈。 + * + * 做法:**反复按返回键**直到侧栏/底栏出现(那是主界面的标志)。 + * 上限兜底,避免在没有可弹页面时空按。 + */ +export async function backToMain(hdc, { tries = 6, settle = 700 } = {}) { + if (!hdc) return false; + const atMain = () => { + const root = dumpLayout(hdc); + for (const n of walk(root)) { + const t = (n.attributes?.text || '').trim(); + /* 侧栏导航轨(宽屏)或底栏(窄屏)上的项 —— 只有 MainPage 有 */ + if (t === '通信' || t === '联系') return true; + } + return false; + }; + for (let i = 0; i < tries; i++) { + if (atMain()) return true; + sh(hdc, ['shell', 'uitest', 'uiInput', 'keyEvent', 'Back'], 10000); + await new Promise((r) => setTimeout(r, settle)); + } + return atMain(); +} diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 0af3b80..55bc407 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -115,7 +115,7 @@ const SUITE = [ ['test/criteria-hygiene.test.mjs', [], 6], // 用户管理页(P4c 同批):动作↔服务端调用同名 / 门禁只认严格 admin / // 启停只发 status / 「受限」徽标口径 / 页面零写死色值 / 接线(纯逻辑真被调用) - ['test/harmony-admin.test.mjs', ['--experimental-strip-types', '--no-warnings'], 27], + ['test/harmony-admin.test.mjs', ['--experimental-strip-types', '--no-warnings'], 28], // P4c 图片上传:阈值与两档策略 / 失败必带原因 / 退档判定只有一处 / // release 都 await / 解码按目标尺寸 / multipart 字段名 / 上传后重新同步 ['test/harmony-imageprep.test.mjs', ['--experimental-strip-types', '--no-warnings'], 30],