文件最后提交记录最后更新时间
16 天前
1 天前
1 天前
16 天前
8 天前
4 天前
15 天前
2 天前
11 天前
16 天前
16 天前
16 天前
16 天前
README

flexui-engine Test Lab(引擎测试台)

独立的 flexui-engine 测试 demo 工程。用于系统性验证引擎 ArkTS API 能力,测试用例与 demo skill 完全隔离,AI 可低成本新增用例,用户可在 UI 上动态修改参数验证。

一、目录结构

demos/engine-test-demo/
├── engine-tests/                  # ★ 用例资产(AI 新增用例的唯一入口)
│   ├── manifest.json              #   用例清单(注册用例)
│   ├── lib/test-harness.js        #   JS 断言框架(globalThis.__testHarness)
│   ├── skills/echo-skill/         #   测试专用技能(executeSkillApi 链路测试)
│   └── cases/<category>/<case>.js #   用例脚本(按引擎能力分类)
├── pc-server/                     # ★ PC 侧布局校验服务(uitest dumpLayout 解析)
│   └── server.js                  #   Node HTTP 服务:/api/health + /api/dump
├── entry/src/main/ets/
│   ├── pages/EngineTest.ets       # 测试台 UI 页面
│   ├── pages/CardPreviewPage.ets  # 独立全屏卡片页(layout 用例渲染载体)
│   └── test/                      # 测试基础设施(Runner / Module / Types)
│       ├── EngineTestRunner.ets   #   执行器:读清单→执行→汇总→hilog [TEST]
│       ├── EngineTestModule.ets   #   JS→ETS 断言上报原生模块 + AscfAIModule 回调代理
│       ├── LayoutVerifier.ets     #   PC 布局数据拉取 + layoutExpect 断言匹配器
│       └── TestResultTypes.ets    #   结果类型定义
└── scripts/build.sh               # 构建/回归命令

二、快速开始

# 1. 构建 HAP(自动同步 engine-tests → rawfile)
./build.sh build

# 2. 安装 + 真机运行
./build.sh install          # hdc install HAP

# 3. 命令行回归(AI/CI 场景)——启动测试台自动跑指定级别用例,轮询 hilog [TEST] 输出 PASS/FAIL 汇总
./build.sh test-engine          # 默认只跑基础用例(basic)
./build.sh test-engine feature  # 只跑特性用例
./build.sh test-engine bug      # 只跑缺陷回归用例
./build.sh test-engine all      # 全部用例(发版前回归)

# 4. 含布局校验的全量回归——后台起 PC 布局服务(uitest dumpLayout),layout 用例正常断言
./build.sh test-layout

# 5. 手工启动(UI 场景)——桌面点开 Engine Test Lab,或
./build.sh launch           # aa start(无 ascfTest 参数,进 UI 模式)

# 6. PC 布局服务单独调试
./build.sh fport            # 建立 hdc fport(设备 8710 → 主机 8710)
./build.sh pc-server        # 前台启动 PC 布局服务(curl http://127.0.0.1:8710/api/health 验证)

Windows 下用 Git Bash 执行:"D:\Program Files\Git\bin\bash.exe" -lc './build.sh <cmd>'

三、用例形态

形态 type 驱动方 覆盖能力 示例
JS 用例 js runScriptFromUri 在引擎 JS VM 执行 has.*、callNative、Timer Policy、异常、来源归因 cases/03-native-module/callnative-async.js
ETS 用例 ets Runner 在 ArkTS 侧直接调引擎 API createEngine、EnvAPI、executeSkillApi、回调链路、卡片特化场景 handler envapi-set / skill-api-echo / card-dedupe
卡片用例 card 页面 FlexView 驱动渲染 + 生命周期断言 + 可选 JS 断言 卡片渲染全链路(load/上屏/就绪/异常)、卡片上下文能力(自断言卡片) card-minimal-render / card-assert-js
布局校验用例 layout 独立卡片页(CardPreviewPage)全屏渲染 + PC 侧 uitest dumpLayout 布局断言 渲染效果精准感知:大小/类型/文本/颜色(经 PC 服务回传后应用内断言) card-layout-minimal / card-layout-complex

卡片自动化测试机制(card 用例不再 SKIP)

card 用例由测试台页面(EngineTest.ets)注册的 CardCaseDriver 驱动,Runner 的 runCardCase 编排:

Runner.runCardCase
  ├─ 1. 注入 lib/test-harness.js(waitCardReport / jsAssert 用例需要同 scope 的 __testHarness)
  ├─ 2. waitCardReport=true(自断言卡片):注册 reportCase 完成回调
  ├─ 3. CardCaseDriver.runCard(info) → 页面 FlexView 重建加载(cardVersion 强制重建)
  │      └─ 收集生命周期信号:onLoadCompleted(0/-204/错误码) / onFirstViewAdded /
  │         onViewReady(rootId) / onJsException,按用例期望结算(expectFail / requireRender)
  ├─ 4. jsAssert 脚本(可选):卡片就绪后同 scope 执行(与卡片共享 globalThis,
  │      可读 __GLOBAL__.appRegister / __ascfRegistry__ 等运行时状态)
  └─ 5. waitCardReport:等待卡片自身 JS 经 __testHarness 上报 reportCase

三种断言层次(可按用例组合):

层次 机制 断言什么 示例
ETS 生命周期 页面 FlexView client 回调 加载码、首帧上屏、viewReady rootId>0、无 JS 异常 card-minimal-render(真实渲染)
同 scope JS 脚本 jsAssert 后置脚本 appRegister 注册、实例注册表、卡片运行时可观测状态 待扩展
卡片自身 JS 自断言卡片engine-tests/cards/,run() 内直接写断言) params 交付、调用来源归因(origin='event' + rootId>0)定时器默认拒绝/特批放行屏幕事件(Page.onResize / pageLifetimes.resize) card-assert-js / card-assert-timer-allow / card-screen-resize

自断言卡片是覆盖「卡片上下文能力」的关键——卡片 bundle(engine-tests/cards/*/index.pack.js)与测试 harness 同 scope, run()(loadInstance 入口)内直接 __testHarness.expect(...) 并同步上报,能验证只有卡片上下文才存在的语义:

  • params 交付(loadCard paramsJson → run(payload).params);
  • 宏任务归因(getCallOrigin():卡片实例化入口 origin='event' + rootId=卡片根,与 __instanceId__ 一致);
  • Timer Policy 卡片级默认拒绝(setTimeout 同步抛 permission denied)/ allowTimer 特批放行;
  • 屏幕事件:卡片经 EngineTestModule.pushCardSize(ETS,走生产 updateDimension → Dimensions.set 链路) 推送容器尺寸变化 → 页面根 Page.onResize / pageLifetimes.resize 收到 res.size.windowWidth/windowHeightcard-screen-resize,需 allowTimer 轮询等待广播落地)。

特殊卡片场景(file:// scheme、REPEAT_LOAD 去重、未知 scheme)由 type=ets + card-* handler 实现, handler 内做前置准备(写沙箱文件 / 多轮驱动)后复用同一个 CardCaseDriver。

布局校验用例(type=layout)——渲染效果精准感知

生命周期断言只能回答「卡片加载成功没」,无法感知渲染出来的效果(大小/类型/文本/颜色)。 layout 用例打通「设备应用 ↔ PC」通道:

应用(CardPreviewPage 独立全屏渲染卡片)
  └─ http 127.0.0.1:8710(hdc fport 转发)→ PC pc-server/server.js
       ├─ hdc shell uitest dumpLayout -a   # 抓当前窗口控件树 JSON(含 bounds/text/type/颜色)
       ├─ hdc file recv <json> <local>     # 拉回主机
       └─ 解析压缩 → { ok, nodes[] }        # 每节点 {type,text,x,y,w,h,color,bg,depth}
  ← 应用按 manifest.layoutExpect 断言(匹配器 LayoutVerifier)

流程:Runner 推入 pages/CardPreviewPage(校验模式 = 无头部、全屏纯卡片,避免测试台 其它布局污染 dump)→ 卡片生命周期结算(与 card 用例同语义)→ 等 600ms 布局稳定 → health 探测 → dump → 逐条 check 断言 → reportAssert/reportCase 上报 → 返回测试台。

约定(重要):应用连不上 PC 服务 → 跳过布局校验(用例仍按生命周期结算,不判失败, 断言标记 SKIP);连接成功但 dump 失败(hdc/uitest 异常)→ 判失败并上报具体错误,便于 AI 排查。

layoutExpect 断言规格(manifest 字段,JSON 字符串):

{
  "tolerance": 4,                       // 尺寸/坐标容差 px(默认 4)
  "checks": [
    { "name": "标题存在", "type": "Text", "text": "Engine Test" },      // 过滤 + 至少 1 个
    { "name": "标题尺寸", "type": "Text", "text": "Engine Test", "w": 240, "h": 40 },
    { "name": "红色块存在", "bg": "#ff0000", "count": 1 },               // 颜色过滤 + 精确数量
    { "name": "无图片", "type": "Image", "count": 0 }                    // 断言不存在
  ]
}
  • 过滤字段:type(子串、忽略大小写)、text(子串)、bg / color(归一化相等,#AARRGGBB/0x 兼容);
  • 期望字段(对首个匹配节点,±tolerance):w / h / x / y / minW / maxW / minH / maxH
  • count:精确匹配数(缺省 = 至少 1 个;0 = 断言不存在)。
  • flexui 卡片在 dump 中的形态(真机实测)
    • 非滚动卡片:flexui 元素 = type="Custom" 节点(key=FlexUIId<N>),尺寸/位置/背景色可断言; 文本与字体颜色本机型 dump 不携带(text 空、无 fontColor)→ 文本语义断言走 JS 自断言通道;
    • scroll-view 卡片:单个 JsView 叶子节点(子树不展开)→ 只能断言容器级(存在/尺寸);
    • 颜色断言优先用 bg(backgroundColor 真机可用),color(fontColor)本机型恒为空。

PC 服务pc-server/server.js,Node 无第三方依赖):

  • GET /api/health(连通性探测)、POST /api/dump(执行 dumpLayout + recv + 解析);
  • 端口默认 8710(hdc fport tcp:8710 tcp:8710 设备侧→主机侧,类似 adb reverse);
  • ./build.sh pc-server 一键起服务;./build.sh test-layout 后台起服务跑全量回归。

手工调试:测试台选中 card/layout 用例 → 「全屏预览」→ 页面内「验证布局」可即时查看 解析出的节点列表(类型/文本/尺寸/颜色)与逐条断言结果,方便迭代 layoutExpect。 已知边界:本机型 scroll-view 卡片的 flexui 子树不展开(显示为单个 JsView 白盒), 非滚动卡片只暴露 Custom 节点(尺寸/背景色可断言,文本不可见)——按实际 dump 调整断言。

命令行回归(./build.sh test-engine)与 UI 模式共用同一驱动:ascfTest=true 启动时页面自动 runAll, card 用例照常执行并输出 [TEST] PASS/FAIL,无需人工操作。

四、用例分级与执行(basic / feature / bug)

每个用例在 manifest 中按性质打标 level,测试台 UI 分 3 个 tab(基础/特性/缺陷)展示与执行:

level 含义 何时编写 执行
basic 基础用例:稳定能力冒烟(渲染/桥/API 主链路) 新能力面落地 默认执行test-engine
feature 特性用例:新特性覆盖 新增特性必须补 test-engine feature
bug 缺陷回归用例:关键修复覆盖 关键设计/缺陷修复必须补 test-engine bug;发版前 test-engine all
  • UI:顶部 3 个 tab 切换用例列表(各 tab 显示该类用例数量);「运行本类」执行当前 tab,「运行所有」执行全部级别;
  • 命令行:./build.sh test-engine [basic|feature|bug|all](缺省 basic),经启动参数 ascfLevel 透传;
  • 发版前回归跑全部./build.sh test-engine all(含 feature + bug 全量);
  • 强制约定见根 CLAUDE.md「flexui-engine 测试用例规范」:新特性/关键修复必须随提交携带对应 level 用例并通过真机回归。

五、AI 新增一个测试用例(3 步)

① 新增用例脚本(JS 用例示例):

// engine-tests/cases/<category>/<case-id>.js
// 用 globalThis.__testHarness 写断言(由测试台注入)
__testHarness.run('<case-id>', [
  {
    name: '断言描述',
    fn: async function () {
      const res = await __testHarness.callModuleSync('EngineTestModule', 'ping', { tag: 'x' })
      __testHarness.expect(JSON.parse(res).errMsg).toEqual('ping:ok')
    },
  },
])

② manifest.json 注册(追加一项):

{
  "id": "<case-id>",
  "name": "用例显示名",
  "category": "03-native-module",
  "capability": "场景二:JS 调用原生能力",
  "type": "js",
  "level": "feature",        // ★ 按性质打标:basic(默认)/ feature(新特性)/ bug(关键修复回归)
  "script": "cases/<category>/<case-id>.js",
  "timeoutMs": 15000
}

③ 跑回归

./build.sh test-engine            # 基础用例(默认),结果在 hilog:[TEST] PASS/FAIL case=<id>
./build.sh test-engine feature    # 只跑本用例所属级别
./build.sh test-engine all        # 发版前全量

ETS 用例(type=ets)需要同时在 EngineTestRunner.etsrunEtsCase() 中新增 handler 分支;卡片用例(type=card)分两种:

普通卡片渲染用例(真实渲染验证,无需写 JS):

{
  "id": "card-my-render",
  "name": "我的卡片渲染",
  "category": "02-card-render",
  "capability": "场景一:...",
  "type": "card",
  "bundleUrl": "ascf-ai-demo/.../index.pack.js",   // 卡片 bundle URL(asset 相对路径)
  "componentName": "skill_component",               // JS 侧注册的组件名
  "paramsJson": "{\"title\":\"x\"}",
  "timeoutMs": 20000
  // 可选:expectFail=true(负向:期望加载失败)、requireRender=false(自断言卡片不渲染原生树)
}

自断言卡片用例(验证卡片上下文能力,卡片 JS 内写断言):

  1. engine-tests/cards/<case>/index.pack.js 写卡片 bundle——顶层 __GLOBAL__.appRegister[componentName] = { run }run() 内用 globalThis.__testHarness 写断言(可验证 params 交付 / origin 归因 / 定时器策略等卡片独有语义), 参考 cards/assert-card/index.pack.js
  2. manifest 注册(waitCardReport: true 表示等待卡片 JS 上报 reportCase,requireRender: false 表示不要求首帧上屏)。 特殊准备(写沙箱文件、多轮加载、自定义 scheme)用 type=ets + card-* handler 实现,参考 card-file / card-dedupe

五、能力覆盖矩阵(对齐 docs/learn/flexui-engine-arkts-api.md)

API 文档场景 用例 形态 说明
场景一:卡片渲染与交互 card-minimal-render / card-complex-render(生命周期自动化断言) card ✅ 自动化:onLoadCompleted(0/-204) + onFirstViewAdded + onViewReady(rootId>0)
场景一:渲染效果精准感知 card-layout-minimal / card-layout-complex(PC uitest dumpLayout + 应用内断言) layout ✅ 自动化:大小/类型/文本/颜色断言(PC 服务不可达时跳过)
场景一:卡片上下文能力 card-assert-js(自断言:params 交付 / origin 归因 / 定时器默认拒绝) card ✅ 自动化:卡片 run() 内断言
场景一:-204 去重语义 card-dedupe(同 URL 二次加载) ets ✅ 自动化:-204 + 实例化仍成功
场景一 1.2.1:URL 加载规范 card-file(file:// scheme) ets ✅ 自动化:沙箱文件 → file:// loadCard
场景一 1.2.1:未注册 scheme card-unknown-scheme(负向) card ✅ 自动化:加载失败不崩溃
场景一 1.2.2:系统配置变更自动重建 config-change-recreate(首帧就绪 → 模拟 colorMode 变更 → FlexView 按 recreateOnConfigChange 自动 reloadCard,rootIdA→rootIdB 换新实例) ets ✅ 自动化
场景二:JS 调用原生能力 callnative-async / callnative-sync js ✅ PASS
场景二:真实异步 promise 回包 callnative-async-roundtrip(callId → callBack → .then) ets ✅ 自动化
场景二:promise resolve 数据完整回传 promise-resolve-rich(异步路径:call() 返回 null,resolveRich 富对象 → .then 全字段断言)+ promise-resolve-rich-sync(同步路径:call() 返回 promise,callNativeSync 直接返回完整对象) ets + js ✅ 自动化
场景二:promise reject 数据完整回传 promise-reject-completeness(异步路径:call() 返回 null)+ promise-reject-completeness-sync(同步路径:call() 返回 promise)——均含普通对象 errMsg/errorCode/嵌套/中文/数组 + 集成 error 对象还原为真实 Error 实例含附加字段 ets + js ✅ 自动化
场景二:error 回传 stack 边界策略 promise-reject-stack-policy(reject 集成 error 对象 → error 还原为 Error 实例:native 层 stack 不注入(toJsonSafeValue 序列化排除 + restoreValue 对 stack/topstack key 跳过,不含 .ets 路径/序列化层/native 构造点);JS 层 stack 自然保留(new Error 还原生成的栈不再主动删除,stack 非空且含 JS 侧帧)) ets ✅ 自动化
场景二:同步失败错误透传 callnative-error-sync(嵌套 JSON/中文/转义字符) js ✅ 自动化
场景二 2.3:调用来源归因 callorigin-runscript(rootId=0)+ card-assert-js(卡片 event/rootId>0) js + card ✅ 自动化
场景二:宿主 API 层 has-sync-smoke(has_apis.js 同步链路 + storage 往返) js ✅ PASS
场景三:EnvAPI envapi-set(ETS 冒烟)+ envapi-roundtrip(JS has.env 活对象两阶段) ets ✅ 自动化
场景三:onCardEvent 事件分发 oncard-event-broadcast(ETS 广播)+ UI「发事件」 ets ✅ PASS
场景三方式三:回调链路 invoke-callback-roundtrip(success/fail/complete 序列断言) ets ✅ 自动化
场景六:Skill API 执行 skill-api-echo(echo=42)+ skill-api-error(未注册/抛错/isError 路径) ets ✅ 自动化
场景七:异常上报 js-exception-report(可捕获)+ js-uncaught-exception(引擎级 exceptionHandler)+ card-init-throw(J-03 入口异常 -205) js + ets + card ✅ 自动化
场景八:DFX 监控 dfx-startup(onStartupReport,含 10s 兜底) ets ✅ PASS
场景九:Timer Policy timer-policy-runscript(runScript 放行)+ card-assert-js(卡片默认拒绝)+ card-assert-timer-allow(allowTimer 特批) js + card ✅ 自动化
引擎初始化 engine-ready ets ✅ PASS

真机回归结果(2026-08-26,27 用例)runAllTests: done total=27 pass=27 fail=0 skip=0 error=0 timeout=0

历史回归重点全部有自动化用例:宏任务归因(rootId 清零/卡片打标)、同步错误信息完整透传、 Timer Policy 卡片级管控(默认拒绝 / allowTimer 特批)、J-03 入口异常上报、REPEAT_LOAD 去重、 EnvAPI 活对象广播、invokeAsyncCallback 一次性回调语义、URL 加载规范(file:// / mem:// / 未知 scheme)。

TODO:场景四自定义组件渲染、场景五全页面引擎、executeSkillApi 成功路径更多 API 断言。

六、测试台 UI 用法

打开 App(Engine Test Lab):

  1. 用例列表:按能力分类展示,点选进入详情
  2. 运行全部 / 运行此用例:执行并实时更新状态(PASS/FAIL/TIMEOUT/ERROR)
  3. 参数编辑:选中用例后编辑 paramsJson(JSON 文本)
  4. 卡片预览(card 用例):底部 FlexView 实时渲染(如 apidemos minimal 卡片,深色 flex 布局);「重载卡片」用新参数重建
  5. 发事件(onCardEvent 链路):输入事件类型 + 数据 → 经 __ascfCallbacks.onCardEvent 广播到 JS 的 instanceEventBus → 测试台预注册的通配监听器(engine-tests/lib/event-listener.js)接收 → 日志面板实时显示「收到事件 data=...」,验证 ETS→C++→JS→监听器全链路
  6. 临时 JS 控制台:输入任意 JS 文本执行(写入沙箱 files/engine-tests-console/ 经 file:// 加载,无需重装 HAP)——适合快速验证引擎 API 或调试
  7. 运行日志:面板滚动输出,含 [TEST] 标记(与命令行回归同源)

注意FlexViewRef.sendEvent(命令式)与 onCardEvent(广播)是两条不同链路——sendEvent 走 EventDispatcher.receiveNativeEvent(需卡片组件主动监听逻辑事件,现有 demo 卡片均未实现);onCardEvent 走 instanceEventBus(卡片 viewCtx.on(type, cb) 监听,测试台预注册通配监听器回报日志)。测试台「发事件」采用 onCardEvent 链路以提供可见反馈。

七、hilog 标记约定(命令行回归解析依据)

  • [TEST] RUNNING suite total=N
  • [TEST] PASS|FAIL case=<id> name=<name> msg=<...>
  • [TEST] runAllTests: done total=N pass=N fail=N ...
  • 脚本 scripts/build.sh test-engine 轮询 hilog -x 抓取并汇总。

八、签名与 bundle

  • 复用 com.enjoy.now.hmos bundleName 与 ascf-agent 签名(开发期覆盖安装可接受;如需与 ascf-agent 共存,改 AppScope/app.json5 的 bundleName 并重新签名)
  • 引擎 HAR 依赖:根 oh-package.json5 override @flexui/engine../../flexui-engine/output/flexui_engine.har(构建前需存在)