首页
/ Impeccable Framework Fixtures 深度指南:跨框架 Live 模式测试矩阵的设计与实战

Impeccable Framework Fixtures 深度指南:跨框架 Live 模式测试矩阵的设计与实战

2026-09-09 09:33:31作者:昌雅子Ethen

Impeccable 的 live 模式需要在 Vite、Next.js、Astro、SvelteKit、Nuxt、TanStack 等不同框架约定下完成脚本注入、元素包裹(wrap)、变更接受(accept)与 CSP 探测,这是一份非常容易因框架差异而失真的能力。本文以仓库中 tests/framework-fixtures/README.md 为骨架,结合 tests/framework-fixtures.test.mjstests/lib/engine-bin.mjstests/live-e2e.test.mjs 及全部 34 个 fixture 实例,完整讲解 fixture 的目录布局、fixture.json 全量 schema、运行时 E2E 七步流程、appDir 根解析与各场景探针的配置方法。读完本文,你将能够读懂、扩展并运行 Impeccable 的跨框架测试矩阵,为新的框架形态编写自己的 fixture。

一、Fixture 是什么:一套可复现的"框架形状"

Framework fixture 是代表不同框架工程约定的微型项目树。它不追求还原真实项目全貌,而是精准复刻那些会影响 live 模式工作的关键结构——index.html 壳、app/layout.tsxsrc/app.htmlsvelte.config.js 里的 CSP 指令、monorepo 的共享配置包等。

测试运行时,harness 会把每个 fixture 拷贝进一个临时 git 仓库,然后驱动引擎二进制(engine binary)的 live-injectlive-wraplive-acceptdetect-csp 动词,覆盖两条测试路径:

  • 静态 sweep:仅做注入、包裹、CSP 探测三项检查,由 tests/framework-fixtures.test.mjs 执行,命令为 node --test tests/framework-fixtures.test.mjs
  • 运行时 E2E:真正安装依赖、启动框架 dev server、用 Playwright 驱动浏览器验证 live 握手,由 tests/live-e2e.test.mjs 执行,命令为 bun run test:live-e2e。只有 fixture.json 中带 runtime 块的 fixture 才进入此路径。

引擎二进制的定位逻辑见 tests/lib/engine-bin.mjs:依次检查环境变量 IMPECCABLE_BINskill/scripts/bin/<os>-<arch>/impeccable[.exe](由 bun run fetch:engine 写入)、target/release/impeccable[.exe](由 cargo build --release -p impeccable 写入)。找不到二进制时返回 null,测试套件会整体 skip,而不是在无引擎的机器上报错——这是所有 sweep 默认"跳过而不是失败"的机制来源。

二、目录布局与 fixture.json 全量 Schema

2.1 标准三段式布局

<fixture>/
  files/              project tree the test copies into tmp
  gitignore.txt       becomes .gitignore in tmp (so we can commit the real files here)
  fixture.json        config + expected results the test consumes
  • files/:要被拷贝进临时仓库的项目树;
  • gitignore.txt:在 tmp 中变成 .gitignore,让仓库本体可以真实提交 dist/node_modules/ 等通常被忽略的内容(例如 multipage-with-generator/dist/ 就靠它 gitignore);
  • fixture.json:测试消费的配置与期望结果,下文逐字段展开。

2.2 顶层字段:静态验证部分

字段 类型 说明
name string 人类可读的标签,测试会断言其非空
config object 写入 .impeccable/live/config.json 的注入配置(见 2.3)
sourceFiles string[] is-generated 应判为"源文件"(false)的路径列表
generatedFiles string[] is-generated 应判为"生成文件"(true)的路径列表
wrapCases object[] 包裹断言列表(见 2.4)
csp object CSP 探测期望(见 2.5),可省略
runtime object 运行时 E2E 配置(见第三节),可省略

2.3 config:live 注入配置

对应引擎二进制读取的 .impeccable/live/config.json,以 vite-react/fixture.json 为例:

{
  "config": {
    "files": ["index.html"],
    "insertBefore": "</body>",
    "commentSyntax": "html"
  }
}
  • files:需要注入 live 脚本的文件列表;
  • insertBefore:注入锚点,如 </body> 或 TanStack Start 的 <Scripts
  • commentSyntax:注入标记的注释语法,html(HTML 注释)或 jsx(JSX 注释,Next.js 场景)。

不同框架的注入目标差异体现在各 fixture 中:nextjs-app/app/layout.tsx 作为 JSX 注入目标,astro/src/layouts/Layout.astro 作为 HTML 注释目标,sveltekit/ 注入 src/app.html 壳,tanstack-start/ 则因为没有静态 index.html,把注入目标指向 src/routes/__root.tsx 并在 insertBefore 使用 <Scripts

2.4 wrapCases:包裹落点断言

wrapCases 定义 live-wrap 的调用参数与期望落点:

"wrapCases": [
  {
    "name": "wraps hero title in source JSX",
    "args": { "classes": "hero-title", "tag": "h1" },
    "expectedFile": "src/App.jsx"
  }
]

字段说明:

  • name:描述;
  • args.classes / args.tag / args.elementId:对应 live-wrap--classes--tag--element-id 参数;
  • expectedFile:包裹应落在哪个文件(相对 fixture 根);
  • expectedSourceFile:可选,断言来源文件(Svelte 组件预览场景下与 expectedFile 分离,后者指向 manifest);
  • expectedPreviewMode:可选,如 svelte-component
  • expectsError:可选错误码,如 element_not_in_sourceelement_not_found

tests/framework-fixtures.test.mjslive-wrap 用例中,harness 会为每个 case 追加 --id wraptest<N> --count 3 等旗标,并解析引擎 JSON 输出中的 filesourceFilepreviewMode 字段逐一断言。multipage-with-generator/ 正是靠 wrapCases 里的 expectsError 来验证 is-generated 守卫element_not_in_source 回退路径:dist/ 是生成目录,live-wrap 不会把修改写进生成文件。

2.5 csp:CSP 形状与修补目标

"csp": {
  "shape": "shared-helper | inline-headers | middleware | meta-tag | null",
  "signals": ["diagnostic hints — paths where CSP was detected"],
  "patchTarget": "which file the agent should modify",
  "expectedAfter": "filename of the reference post-patch output inside this fixture"
}
  • shape:CSP 形状枚举。null 表示没有 CSP(大多数非 CSP fixture 不写 csp 块即等价于期望 null);
  • signals:CSP 被检测到的诊断提示路径,格式为 文件:符号/键名
  • patchTarget:agent 应修改的文件;
  • expectedAfter:参考修补后输出的文件名,fixture.json 同级存放(不在 files/ 内),仅作人工/agent 审查参考,测试不会自动应用补丁。

仓库内的真实形状分布:

  • append-arrays(数组合并)nextjs-turborepo/fixture.json 共享 createBaseNextConfig 助手(signals 指向 packages/shared/src/next-config/index.ts:buildCSPConfig 等);sveltekit-csp/fixture.jsonsvelte.config.js:kit.csp.directives
  • append-string(字符串拼接)nextjs-inline-csp/fixture.jsonnext.config.js 字面量 CSP;nuxt-csp/fixture.jsonnuxt.config.ts routeRules 字面量 header;
  • middleware(Next.js 中间件):README 明确指出该名字同时覆盖 Next.js middleware.* 与 Next.js 16 的 proxy.* 约定。nextjs-proxy-csp/ 即使用 proxy.ts,且 tests/framework-fixtures.test.mjs 末尾专门有一个用例验证 proxy.ts 的放置规则:app 根与 src/ 根下的 proxy.ts 被识别为 middleware 形状,而 lib/network/proxy.tsapps/web/lib/proxy.ts 这类同名助手不被误判

nextjs-turborepo/expected-after-patch.ts 为例,可以看到 append-arrays 修补后应有的形态:通过 createBaseNextConfig 传入 additionalScriptSrc / additionalConnectSrc,并用 NODE_ENV === "development" 守卫追加 http://localhost:8400 这一开发期允许项,非开发环境为空数组。

三、运行时 E2E:runtime 块与七步握手流程

3.1 完整 Schema 与默认值

runtime 块可选。没有它的 fixture 只跑静态检查;带它的 fixture 额外跑 tests/live-e2e.test.mjs 中的 E2E 套件,完整字段如下(README 原文照录):

"runtime": {
  "styling": "plain-css | tailwind-v4 | styled-components | ...",
  "appDir": "website",
  "install": ["npm", "install"],
  "devCommand": ["npm", "run", "dev"],
  "scheme": "http",
  "ignoreHTTPSErrors": false,
  "readyPattern": "Local:\\s+https?://[^:]+:(\\d+)",
  "readyTimeoutMs": 120000,
  "pickSelector": "h1.hero-title",
  "pickPosition": { "x": 10, "y": 10 },
  "variantSequence": [3, 1, 2],
  "acceptedSourcePattern": "<ul[^>]*class=\"[^\"]*\\bexpense-list\\b",
  "assertSourceContains": ["{#each expenses as expense, i}"],
  "stateProbe": {
    "textSelector": "[data-testid='open-count']",
    "expectedText": "3 offen",
    "windowProperty": "__impeccableStatefulMounts",
    "expectedWindowValue": 1,
    "expectWindowUnchanged": true
  },
  "paramsScenario": {
    "variant": 2,
    "rangeLabel": "Lead",
    "rangeValue": 1.8,
    "stepsLabel": "Density",
    "stepsOptionLabel": "Snug",
    "expectSourceContains": ["line-height: 1.8"],
    "expectSourceMissing": ["letter-spacing: 0.14em"]
  },
  "componentFailureScenarios": { "variant": 2, "storageLoss": false },
  "mode": "insert",
  "insert": {
    "anchorSelector": "section#features",
    "position": "after",
    "prompt": "Add a testimonial strip below features",
    "expectSelector": ".inserted-strip",
    "assertAnchorContains": "feature-grid"
  },
  "preActions": [
    { "type": "click", "selector": "[data-testid='open-modal']" },
    { "type": "goto",  "path": "/about" }
  ],
  "reloadProbe": {
    "preActions": [{ "type": "click", "selector": "[data-testid='open-modal']" }],
    "expectSelector": "h1.hero-title"
  },
  "steer": {
    "message": "steer-e2e mark hero",
    "expectSelector": "h1.hero-title[data-impeccable-steer=\"e2e\"]"
  },
  "probe": {
    "expectLiveInit": true,
    "expectConsoleClean": true
  }
}

3.2 运行时 E2E 七步流程

runtime 块的 fixture 会执行 tests/live-e2e.test.mjs 中的完整 E2E 套件:

  1. staging:把 fixture 拷入临时仓库;
  2. 安装真实依赖:执行 runtime.install
  3. 启动 live 基础设施:运行 impeccable live-server --background,再对它执行 impeccable live-inject --port
  4. 启动 dev server:spawn runtime.devCommand,用 runtime.readyPattern 从 stdout 抓取端口(第一个捕获组必须是端口号);
  5. Playwright 打开页面:在 runtime.readyTimeoutMs 内断言 window.__IMPECCABLE_LIVE_INIT__ === true(浏览器端握手 oracle);
  6. Steer smoke(除非 runtime.steer === false):在全局 Steer 栏提交消息,等 fake agent 回复 steer_done,断言栏解锁且 data-impeccable-steer 标记同时落入源码与 DOM;随后继续 pick → Go → 循环变体 → accept 主流程;
  7. 收尾:Playwright 关闭、dev server 收到 SIGTERM、live-server stop、删除 tmp。

3.3 Steer 冒烟的可选字段

"steer": {
  "message": "steer-e2e mark hero",
  "sourceFile": "src/routes/About.jsx",
  "expectSelector": "h1.hero-title[data-impeccable-steer=\"e2e\"]",
  "expectSourceContains": "data-impeccable-steer=\"e2e\"",
  "preActions": [{ "type": "click", "selector": "[data-testid='nav-about']" }]
}
  • 省略 preActions 时,steer smoke 继承 runtime.preActions 以先揭示隐藏的 hero 再做 DOM 检查;
  • 先断言源码,再通过"reload + 重试"覆盖 HMR 延迟;
  • "steer": false 跳过整个冒烟(如 vite8-sveltekit-stateful/fixture.json 就是 "steer": false,把资源留给更重的场景探针);设 "expectDom": false 则只做源码验证。

3.4 runtime.appDir:非根目录应用与根解析

appDir 默认 .,当被服务的应用不是仓库根时使用(例如根目录是 CLI 包、站点在 website/ 下的 monorepo-nested-vite/fixture.json)。设置后 harness 的行为:

  • 照常在 tmp 根做 git init 与 staging;
  • .impeccable/live/config.json 写到 <tmp>/<appDir>/ 下,fixture.json 里所有 fixture 相对路径(steer.sourceFile、manual-edit 的 expectedSourceFile 等)都相对该 app 目录解析;
  • runtime.installruntime.devCommand 以 app 目录为 cwd 执行;
  • 从 tmp 根调用 impeccable live(而非直接调 live-server / live-inject),从而真正锻炼引擎的根解析能力,而不是假设它正确。

解析后的 boot payload 暴露为 session.liveBoottests/live-e2e.test.mjs(L1463-L1478 附近)会断言 roots.appRoot/${appDir} 结尾、roots.appRootappRoot 相同、roots.contextRoot 等于 tmp 根、projectRoot 等于 appRoot,并检查持久化的 roots.json 与 repo 根指针;同时断言 hasProduct / hasDesign 为 true——证明 PRODUCT.mdDESIGN.md 从嵌套应用被正确读取。

session 对象同时携带两条路径:session.tmp 是仓库根(用于 git 与产物捕获),session.appRoot 是应用;没有 appDir 的 fixture 两者相同。

3.5 Picking、Cycling 与渲染证明

  • pickSelector + pickPosition:picker 解析光标下的元素,因此"中心被子元素覆盖的容器永远无法被选中"。需要 pickPosition(相对元素左上角的像素 {x, y})瞄准容器真正拥有的点(如它自己的 padding);
  • variantSequence:默认 [2],是按顺序循环的顺序,最后一个条目是最终接受的变体。每个落到的变体都会在 fake-agent 模式下得到一次计算出的 font-weight 断言——因为 fake agent 以不同字重渲染每个变体(FAKE_VARIANT_FONT_WEIGHTS,见 tests/live-e2e/agent.mjs:300 / 900 / 600)。这把"变体 N 可见"从 bar 标签声明变成了渲染事实,所以 [3, 1, 2] 这样的序列可以证明三个变体真的都被渲染过(vite8-sveltekit-stateful/fixture.json 正是此序列);
  • acceptedSourcePattern:覆盖默认的接受后源码检查(默认是 <h1 class="hero-title">);assertSourceContains 列出必须在整个 wrap → accept → carbonize 周期后存活的字符串。在 Svelte 组件预览上,这是证明控制流未被拍平的关键手段:把 {#each} 头和逐项表达式都列进去(如 {#each expenses as expense, i}{expense.name}{expense.amount}href={expense.doc})即可验证循环结构被保留;
  • stateProbe:断言页面状态不丢失。textSelector/expectedText 检查渲染状态;windowProperty 配合 expectedWindowValueexpectWindowUnchanged 检查一个"挂载时自增"的计数器,因此静默重挂载页面的脚手架会直接失败。它在 preActions 之后、变体落定之后、(非组件预览下)accept 之后各运行一次。

四、Extra Scenarios:参数旋钮、故障注入与存储丢失

除核心周期外,fixture 通过声明配置来启用场景,每个场景还受 IMPECCABLE_E2E_SCENARIOS 门控:

场景名 启用字段 证明的内容
params runtime.paramsScenario 在真实 Tune popover 中拨动 rangesteps 旋钮并接受,断言所选值以字面量烘焙进源码,未选分支被丢弃,且不残留 data-p-* / var(--p-*)
mount-failure runtime.componentFailureScenarios 破坏已发布的 r<N>/v<variant> 修订文件并步入该变体,断言持久化挂载错误卡片出现、会话存活(bar + localStorage 完好)、variant_mount_failed 事件落入 session journal;然后恢复、Retry、重新到达该变体
republish runtime.componentFailureScenarios 重新编写每个变体并再次回复 done,浏览器必须挂载新内容——这正是服务器 revision-dir bump 要保证的
storage-loss runtime.componentFailureScenarios(除非 storageLoss: false 清空 localStorage、reload,断言对比结果仅凭服务器持久化 session 记录即可恢复

componentFailureScenarios.variant 选择要破坏/观察哪个变体;storageLoss: false 用于"选中元素只在 preActions 之后才存在"的 fixture——reload 会丢弃该状态,重建又会与预览挂载竞态(vite8-sveltekit-stateful/fixture.json 即如此设置)。

这些场景断言确定性内容,因此 IMPECCABLE_E2E_AGENT=llm 下会整体跳过。

五、常用 E2E 过滤与环境变量

运行时 E2E 常用过滤(直接作用于测试进程):

  • IMPECCABLE_E2E_ONLY=<fixture>[,<fixture>]:把运行范围限定到指定 fixture 名;
  • IMPECCABLE_E2E_SCENARIOS=core:只跑主"点击 → Go → 循环 → accept"路径;省略或设为 all 则包含 manual edit、annotation、exit、params 与组件故障注入探针。合法值:coremanualannotationsexitmissed-doneparamsmount-failurerepublishstorage-loss
  • IMPECCABLE_E2E_TEST_TIMEOUT_MSIMPECCABLE_E2E_INSTALL_TIMEOUT_MSIMPECCABLE_E2E_DEV_READY_TIMEOUT_MS:收紧 CI 冒烟超时,无需改动 fixture 元数据;
  • 二进制相关:IMPECCABLE_BIN 可直接指定引擎二进制路径;IMPECCABLE_E2E_AGENT=llm 切换到 LLM agent 模式。

六、静态 sweep 的一个关键陷阱:glob 双根解析

tests/framework-fixtures.test.mjs 会把同一 fixture 平铺(flat) staging,并把 live 配置写到 tmp 根。这意味着 config.files 必须同时能从仓库根与应用根解析:

  • glob(如 "**/index.html")同时满足两种解析,monorepo-nested-vite/fixture.json 正是靠它工作;
  • 字面量(如 "index.html")只在应用根有效,会让静态 sweep 报 file_not_found

七、当前 Fixture 全览

Fixture Shape
vite-react/ 受跟踪的 index.html 壳 + src/App.jsx,注入壳
nextjs-app/ app/layout.tsx 作为 JSX 注入目标(commentSyntax jsx
astro/ src/layouts/Layout.astro 注入目标,HTML 注释
sveltekit/ src/app.html 壳 + src/routes/+page.svelte
vite8-sveltekit-stateful/ Svelte 5 路由:$state{#if} 分支、{#each} 列表;选列表容器,组件预览脚手架须把循环作为单个 collection prop 携带并从 live DOM 水合条目;同时承载 params、故障注入与状态保持探针
nuxt-vite7/ Nuxt 4 app/ 结构 + Vue 3 SFC,live 通过生成的 dev-only client plugin 加载
tanstack-router-vite/ Vite + TanStack Router(基于代码的 SPA),受跟踪 index.html 壳注入(基线 Vite 路径,无 adapter)
tanstack-start/ Vite + TanStack Start(SSR),无静态 index.html,live 修补 __root.tsx 文档以挂载生成式 dev-only React 组件加载 bundle
multipage-with-generator/ src/ 受跟踪、dist/ gitignore,锻炼 is-generated 守卫与 element_not_in_source 回退
nextjs-turborepo/ monorepo + 共享 CSP 助手(createBaseNextConfig),CSP 形状 append-arrays
nextjs-inline-csp/ next.config.js 字面量 CSP 字符串,形状 append-string
sveltekit-csp/ svelte.config.jskit.csp.directives,形状 append-arrays
nuxt-csp/ nuxt.config.ts routeRules 字面量 CSP header,形状 append-string
monorepo-nested-vite/ 仓库根是 CLI 包(无 dev 配置、无 workspaces),被服务的 Vite + React 应用在 website/,锻炼 runtime.appDir 与 live 根解析

除上表外,仓库还维护了针对 Vite 8 生态的系列 fixture(vite8-react-*vite8-httpsvite8-sveltekit 等),覆盖 Tailwind v3/v4、Unocss、vanilla-extract、CSS Modules、styled-components、emotion、Radix Dialog、base-path、https、CSP meta 等工程形态,与表内 fixture 共用同一套 schema 与流程。

八、如何新增一个 Fixture

按 README 给出的流程,添加新 fixture 的步骤是:

  1. 克隆一个现有目录作为起点(选择一个最接近目标框架形态的 fixture,例如要新增 Next.js App Router 场景可克隆 nextjs-app/);
  2. 替换 files/ 中的文件,让它成为你目标框架的最小可运行工程;
  3. 更新 fixture.json:填写 nameconfig(files / insertBefore / commentSyntax)、sourceFiles / generatedFileswrapCases(含 expectsError 回退用例)、csp(含 signalspatchTargetexpectedAfter,如适用);
  4. 如果带 runtime,还需把 fixture 名字加入 .github/workflows/ci.yml 的 live-e2e 矩阵:live-e2e-full 组列表,以及(希望每个 PR 都跑时)一个 live-e2e-smoke 组。保持两组规模大致相当——它们并行运行,而 job 超时上限为 15 分钟。

新增后可用如下命令验证(前提是已通过 bun run fetch:enginecargo build --release -p impeccable 准备好引擎二进制):

# 静态 sweep:inject / wrap / csp-detect
node --test tests/framework-fixtures.test.mjs

# 仅跑目标 fixture 的运行时 E2E
IMPECCABLE_E2E_ONLY=my-fixture bun run test:live-e2e

九、总结:这套矩阵在验证什么

Fixtures 矩阵的核心价值在于把"live 模式在不同框架下都能工作"这一宏愿拆解成可断言、可复现的最小单元:注入是否落在框架要求的正确文件与语法上(live-inject)、包裹是否路由到真正的源文件而非生成文件(live-wrap + is-generated 守卫)、CSP 是否被按形状识别并可给出修补目标(detect-csp + expectedAfter)、以及完整浏览器周期下握手、变体渲染、状态保持、故障恢复是否真实成立(runtime E2E 与各场景探针)。理解这套 fixture 的 schema 与流程,等于同时理解了 Impeccable live 模式在各主流框架下的全部适配点。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
927
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.94 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
603
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
396
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
527