LikeC4 E2E 测试实战:DrawIO 导出、静态站点导航与 CLI 覆盖的设计原则和落地方法
【免费下载链接】likec4Visualize, collaborate, and evolve the software architecture with always actual and live diagrams from your code项目地址: https://gitcode.com/GitHub_Trending/li/likec4
LikeC4 仓库的e2e/目录承载了整个项目的端到端测试层,E2E-COVERAGE.md 是其中一份聚焦“DrawIO 集成与导出”这条链路的覆盖度档案:它记录了三条核心场景(Playground 的 DrawIO 导出、静态站点视图导航、文档站冒烟测试)与 CLIexport drawio命令已实现的 E2E 场景,并逐条对照源码阐述了背后的 DRY/SOLID/KISS/YAGNI 设计决策。读完本文,你将掌握这套 E2E 测试矩阵的组织方式、每个 spec 的关键断言与共享选择器/超时体系,以及如何按文档中的命令在不同配置下运行这些测试。
E2E 测试矩阵的整体布局
从源码结构看,LikeC4 的 E2E 测试分为两套运行器,各自有明确的文件边界:
| 运行器 | 配置文件 | 测试位置 | 覆盖对象 |
|---|---|---|---|
| Playwright(主配置) | playwright.config.ts | tests/ 目录 | 静态站点(likec4 build+preview)的视图导航、快照对比等 |
| Playwright(playground 配置) | playwright.playground.config.ts | 仅tests/drawio-playground.spec.ts | Playground 应用的 DrawIO 上下文菜单与下载 |
| Vitest | vitest.config.ts | src/ 下的*.spec.ts与*.test-d.ts | likec4CLI 命令的行为验证(导出 DrawIO、模型、视图等) |
三个关键配置文件的取舍可以直接从源码确认:
- 主配置 playwright.config.ts 的
testDir指向tests/,并通过testIgnore显式排除了drawio-playground.spec.ts、docs-smoke.spec.ts和mcp-render-app.spec.ts,注释说明 DrawIO playground 与 MCP App 测试只走各自的专用配置,而文档站冒烟测试需要 Astro 文档服务,不适合与likec4 start的图预览服务混跑。该配置的webServer执行pnpm build-and-preview(端口 62001),即“先构建再预览”的静态站点路径,并启用了retries: isCI ? 1 : 0、forbidOnly: isCI、CI 下的github报告器等典型 CI 加固项。 - Playground 配置 playwright.playground.config.ts 通过
testMatch: '**/drawio-playground.spec.ts'只匹配单个 spec,baseURL指向http://localhost:5174,其webServer用pnpm --filter @likec4/playground dev -- --port 5174在本地拉起开发服务器,单测超时放宽到 60 秒。 - Vitest 配置 vitest.config.ts 收集
src/**.spec.ts和src/**.test-d.ts(类型测试文件),并开启typecheck,因此 CLI 行为测试与类型层断言在同一个运行器内完成。
这个布局对应了 E2E-COVERAGE.md 中“Homogeneity(同构性)”一节强调的约定:Playwright specs 统一使用test.describe+test()/test.beforeEach,Vitest CLI specs 统一使用test与zx的$执行器,选择器与 URL 在适用时与 bootstrap 生成的快照测试共享,避免同一概念在多处各写一份。
共享基建:选择器与超时的单一事实来源
helpers/selectors.ts 是 Playwright 侧共享选择器的集中定义,文件头注释明确其目的是“避免 drawio-playground 与 static-navigation 两个 spec 之间重复”:
export const CANVAS_SELECTOR = '.react-flow.initialized' export const EDITOR_SELECTOR = '.monaco-editor' export const MENU_SELECTOR = '[role="menu"], .mantine-Menu-dropdown' export function canvas(page: Page): Locator { return page.locator(CANVAS_SELECTOR).first() } export function editor(page: Page): Locator { return page.locator(EDITOR_SELECTOR).first() }两个设计点值得注意:
initialized状态类作为图表就绪信号:canvas()定位的是.react-flow.initialized,即 React Flow 画布完成初始化后才被视为“可见”。这与 bootstrap 生成的快照测试中page.waitForSelector('.react-flow.initialized')的等待逻辑一致(见 bootstrap.mjs),属于跨配置共享的等待约定。MENU_SELECTOR同时覆盖 ARIA 与 Mantine 两类菜单:[role="menu"], .mantine-Menu-dropdown的并集写法让画布右键菜单和 Monaco 编辑器右键菜单(两者渲染路径不同,但共用同一菜单选择器)都能被同一个 helper 命中——drawio-playground.spec.ts 中专门用注释解释了这一点。
helpers/timeouts.ts 则是所有超时的单一调优点,从源码看共 8 个常量:TIMEOUT_CANVAS = 15_000、TIMEOUT_DIAGRAM = 45_000、TIMEOUT_MENU = 5_000、TIMEOUT_DOWNLOAD = 15_000、TIMEOUT_DOWNLOAD_ALL = 20_000、TIMEOUT_EDITOR = 10_000、TIMEOUT_EDITOR_MENU = 3_000、TIMEOUT_PAGE = 10_000。把“下载全部视图”的超时(20s)设置得高于“下载单个视图”(15s),与 Export all 需要遍历多张视图的实际耗时相符;这也呼应了覆盖度文档中“no magic numbers(不要魔法数字)”的原则——每个等待时长都有名字的语义。
已实现场景一:Playground 中的 DrawIO 集成
tests/drawio-playground.spec.ts 是 E2E-COVERAGE.md “Implemented scenarios”清单中前五项的载体,覆盖度文档对它的要点与源码逐条对应:
场景与断言
- 工作区加载后图表可见:
beforeEach导航到教程工作区/w/tutorial/,等待 URL 匹配/w\/tutorial\//、尽力等待networkidle(失败则吞掉,不阻断),再用TIMEOUT_DIAGRAM等待画布选择器;测试体本身只断言expect(canvas(page)).toBeVisible()。 - 画布右键菜单展示 DrawIO 项:右键点击画布固定坐标
{ x: 200, y: 200 }(常量RIGHT_CLICK_CANVAS),随后断言菜单中同时出现DrawIO(精确匹配)、Export to DrawIO与Export all。 - Export to DrawIO 触发下载且内容有效:这是断言最重的一条——下载文件名必须匹配
/^.+\.drawio$/,随后显式断言download.path()非空(文档中所谓“explicit assertion of download path, fail-fast, no silent branch”,即不依赖可选链静默跳过),再读取文件内容并断言包含<mxfile且长度大于 200 字节,确保下载到的是真实的 DrawIO XML 而非空文件。 - Export all 触发
.drawio下载:复用同一下载触发 helper,仅验证文件名后缀,因为整工作区导出的完整内容校验由 CLI 侧场景负责(见下文),避免重复。 - Monaco 编辑器右键菜单也含 Export to DrawIO:先断言编辑器可见(
TIMEOUT_EDITOR),在固定坐标{ x: 100, y: 50 }(常量EDITOR_CLICK_POSITION)先左键聚焦、再右键,等待编辑器上下文菜单出现并断言其中含Export to DrawIO(TIMEOUT_EDITOR_MENU)。
关键 helper 的设计
openDrawioContextMenu(page)只负责“右键 + 返回菜单 Locator”;triggerDrawioDownload(page, menuItemLabel, downloadTimeout)则体现了一个典型的先订阅事件再触发操作的时序模式:
async function triggerDrawioDownload( page: Page, menuItemLabel: 'Export to DrawIO' | 'Export all', downloadTimeout: number, ): Promise<Download> { const downloadPromise = page.waitForEvent('download', { timeout: downloadTimeout }) const menu = await openDrawioContextMenu(page) await expect(menu).toBeVisible({ timeout: TIMEOUT_MENU }) await menu.getByText(menuItemLabel, { exact: false }).click() return downloadPromise }downloadPromise在点击菜单项之前就创建,消除了“先点击、后监听事件”可能丢事件的竞态;菜单项标签用联合类型'Export to DrawIO' | 'Export all'而非自由字符串,把 UI 上的合法菜单项约束到了类型层面。这正是覆盖度文档提到的“helpers with single responsibility”和“no duplication in download flows”:两个下载测试共用同一条触发路径。
已实现场景二:静态站点的视图间导航
tests/static-navigation.spec.ts 验证likec4静态站点在多个视图 URL 之间切换时图表能正确渲染。它有三个值得注意的细节:
- URL 形状与 bootstrap 共享:
viewUrl()生成/project/{project}/export/{viewId}/?padding=22形式的地址。这个形状并非随手写的——bootstrap.mjs 为每个已计算视图自动生成 Playwright 快照测试时使用的正是同一条 URL(?padding=22),静态导航测试因此与 31 张已入库的基准截图(见 tests/screenshots)跑在同一套路由约定上。 - 单一等待点:
gotoViewAndAssertDiagram内部只做一次expect(canvas(page)).toBeVisible({ timeout: TIMEOUT_CANVAS }),没有额外的waitForLoadState或waitForSelector——覆盖度文档称之为“single wait point for the diagram”,减少多重等待叠加导致的伪失败。 - 遍历固定视图集合:测试依次访问
index、cloud、amazon三个视图(来自 bootstrap 校验的e2e项目,见 bootstrap.mjs 中对工作区项目清单['e2e', 'export-config', 'export-disabled', 'issue-2282']的断言),在一个测试内完成“导航切换 + 图表更新”的完整语义。
从源码结构看,该 spec 依赖 bootstrap 先运行以生成e2e项目的工作区快照与路由数据,这与 E2E-COVERAGE.md “How to run”一节中“includesstatic-navigation.spec.tsafter bootstrap”的说明一致。
已实现场景三:文档站冒烟测试
tests/docs-smoke.spec.ts 是面向 Astro/Starlight 文档站的三个独立短测试,每个测试只验证“页面能加载 + 有一个标志性内容”:
- 首页:标题匹配
/LikeC4/i,且存在名为 LikeC4 的链接; /tooling/drawio/页:标题匹配Draw.io,页面出现 export/Draw.io 相关正文;/tooling/cli/页:出现CLI或command line字样。
三个页面 URL 以DOCS_HOME、DOCS_TOOLING_DRAWIO、DOCS_TOOLING_CLI常量集中在文件头部,等待统一使用TIMEOUT_PAGE(10s),测试之间无共享状态——这正是覆盖度文档中“short, independent tests”的具体形态:任何一个页面坏了都能独立定位,不会因为上一个断言失败而连坐。
覆盖度文档在运行说明中写的是pnpm test:docs(配合playwright.docs.config.ts、端口 4321)。需要注意,从当前仓库状态看,e2e/package.json 暴露的脚本为test、test:playground、test:mcp-app、typecheck、bootstrap等,尚未包含test:docs脚本,e2e/目录下也不存在playwright.docs.config.ts文件,而主配置通过testIgnore将docs-smoke.spec.ts排除在主运行之外。可以推断文档站冒烟测试目前处于“spec 已就绪、专用配置待接入”的状态,运行前应以仓库脚本的实际内容为准。
已实现场景四:CLIexport drawio的行为验证
Playwright 无法覆盖命令行行为,这部分由 Vitest 承载。src/likec4-cli-export-drawio.spec.ts 用zx的$执行器直接跑likec4CLI,覆盖度文档对它的三点评价都能在源码中验证:
1. 命名谓词isDrawioFile
function isDrawioFile(entry: { isFile: () => boolean; name: string }): boolean { return entry.isFile() && entry.name.endsWith('.drawio') }把“是不是我们关心的产物”抽成有名字的布尔函数,readdirSync(...).filter(isDrawioFile)的读法与业务意图一致。
2. 确定性文件顺序
两个导出测试都执行entries.filter(isDrawioFile).sort((a, b) => a.name.localeCompare(b.name))后再读取第一个文件——覆盖度文档中“deterministic file order (sort by name) before reading the first”指的就是这个细节:不排序时“第一个.drawio文件”取决于文件系统返回顺序,跨平台可能不同,断言就会变得不稳定。
3. 显式断言与失败路径
- 主测试在
likec4的项目根(e2e/src/likec4,项目 id 为e2e,来自 src/likec4/likec4.config.ts)下执行likec4 export drawio . -o <outDir> --project e2e,随后断言产物含<mxfile且长度大于 200; - 另有一条
--profile leanix --uncompressed测试,额外断言内容含bridgeManaged=true且匹配/likec4Id=/,验证 LeanIX 桥接风格在导出中生效; - 空工作区测试用
--project指向一个空目录,接受“退出码 1”或“退出码 0 但 stderr 含no LikeC4 sources found”两种失败形态,源码注释解释了这是 CI 中打包版 CLI 的已知偏差,并预留了“quirk 修复后收紧为只接受 exitCode === 1”的演进方向——E2E 断言如何与 CI 现实妥协,这里给出了一个具体范例。
从源码结构看,当前e2e/src/下还包含likec4-cli-bridge.spec.ts、likec4-model.spec.ts、likec4-views.spec.ts等兄弟 spec 及对应的*.test-d.ts类型测试文件,说明 Vitest 侧的 CLI 覆盖面已从单一 DrawIO 导出扩展到桥接、模型与视图多个领域;覆盖度文档中提及的likec4-cli-build.spec.ts在当前文件列表中已不存在,相关构建行为验证应当已并入上述 spec 体系。
设计原则如何落到代码
E2E-COVERAGE.md 开头一节把 DRY、SOLID、KISS、YAGNI 逐 spec 对号入座,结合源码可以归纳出四条可复用的模式:
- 常量上移、helper 单一职责(drawio-playground、static-navigation、docs-smoke):
TUTORIAL_PATH、RIGHT_CLICK_CANVAS、VIEW_IDS、DOCS_*等全部声明在文件顶部;canvas(page)、editor(page)、gotoViewAndAssertDiagram(page, viewId)这类小函数各自只做一件事,测试体读起来接近伪代码。 - 单一等待点(static-navigation):一个导航动作只配一个断言等待,避免“等待风暴”。
- 命名谓词 + 确定性排序(cli-export-drawio):把过滤条件与顺序假设显式化,消除对文件系统行为与字符串字面量的隐式依赖。
- 失败路径也要测(cli-export-drawio 的空工作区测试):CLI 的“必须失败”场景同样有 E2E 级验证,且对 CI 已知偏差留了注释化的收紧计划。
文档最后还提到一次“integrity(全局一致性扫描)”:ExportDrawioParams、isSourceFile、ROUNDTRIP_IGNORED_DIRS等导出侧类型、OnDrawioExportError、CollectViewModelsOptions等 Playground 侧类型、以及applyLoggerConfig与Page类型等,均在 CLI/Playground/E2E 各层被检查过一致性。这提示维护者:E2E 层改动选择器、URL 或导出文件名约定时,需要同步检查各包中共享的类型与常量,而不是只改测试。
如何运行这些测试
以下命令均来自 E2E-COVERAGE.md “How to run”一节,并与 e2e/package.json 的脚本定义核对一致:
# 前置:安装依赖(CI 场景下依赖以本地 tarball 形式引入,见 package.json 中的 file:../packages/*/package.tgz) pnpm install # Playwright(playground):DrawIO 菜单、下载等场景 cd e2e && pnpm test:playground # Playwright(主配置):静态站点导航 + bootstrap 生成的视图快照 cd e2e && pnpm bootstrap # 生成 e2e 项目数据与快照 spec cd e2e && pnpm test # Vitest:CLI 行为测试(含 export drawio),同时跑类型检查 cd e2e && pnpm typecheck运行前提与限制:
- bootstrap 先于主 Playwright 测试:
pnpm bootstrap(bootstrap.mjs)会执行likec4 codegen生成 React 组件与模型文件,校验工作区项目清单,并为e2e与issue-2282两个项目的每个视图生成tests/*-gen.spec.ts快照测试。主配置下的导航与快照测试都依赖这一步。 - 端口与服务器由 webServer 配置托管:主配置自动拉起
pnpm build-and-preview(likec4 build -o ./dist ./src && likec4 preview,端口 62001);playground 配置自动拉起 5174 端口的 dev server,且本地运行时reuseExistingServer: !isCI——本地已手动起服务可以直接复用。 - 截图基准已入库:tests/screenshots按
chromium-linux/chromium-darwin两个平台目录存放了 31 张基准 PNG,snapshotPathTemplate将其固定为{testDir}/__screenshots__/{projectName}-{platform}/{arg}{ext};更新基线用pnpm test:update-screenshots,且主配置将像素差异容忍度设为maxDiffPixelRatio: 0.01以吸收 CI 上字体/布局的微小抖动。 - 浏览器:当前配置只跑 Chromium(
Desktop Chrome HiDPI设备描述),本地首次运行前可执行pnpm install:chromium确保浏览器可用。
覆盖度现状小结
对照 E2E-COVERAGE.md 的清单,全部列出的场景均已勾选完成:Playground 的 DrawIO 菜单/可见性/单视图导出/全量导出/编辑器菜单五项,CLI 的export drawio产出<mxfile一项,以及“Pending (optional)”中的静态站点导航与文档站冒烟两项也都已实现。从当前仓库状态看,仍有两处与代码存在待对齐的落差:文档站冒烟对应的专用配置与test:docs脚本尚未在 e2e/package.json 中出现,文档提到的likec4-cli-build.spec.ts已不在 e2e/src 的 spec 列表中。这两点可以作为后续维护该测试矩阵时的优先核对项;除此之外,文档中列出的每条设计原则与运行命令,都能在上述源码文件里找到一一对应的实现证据。
【免费下载链接】likec4Visualize, collaborate, and evolve the software architecture with always actual and live diagrams from your code项目地址: https://gitcode.com/GitHub_Trending/li/likec4
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考