
authentik WebUI 测试体系完全指南unit / browser / lit / blueprints 四种测试类型的选择、编写与运行【免费下载链接】authentikThe authentication glue you need.项目地址: https://gitcode.com/GitHub_Trending/au/authentik导读本文以 web/test/AGENTS.md 为骨架系统讲解 authentik 前端WebUI自动化测试体系的组织方式test/unit纯 Node 单元测试、test/browser驱动真实 UI 的浏览器端到端测试、test/litLit 组件渲染辅助与test/blueprints测试种子蓝图四种形态如何划分边界、如何取舍、如何编写与运行。结合仓库中 web/test/unit/AGENTS.md、web/test/browser/AGENTS.md 以及web/e2e/、web/test/下的真实源码与测试用例你将掌握为 authentik WebUI 编写高质量自动化测试的完整规范并能直接用文中命令跑通本地测试链路。一、测试目录路由器四种测试形态一览authentik WebUI 是采用 Lit Web Components 与 PatternFly 4 设计系统的 TypeScript 单体仓库包含 Flow/if/flow/、User/if/user/、Admin/if/admin/三个独立应用详见 web/AGENTS.md。与之匹配的自动化测试体系并不只有一层而是四种各有其用途、各自有独立规范文档的形态。web/test/AGENTS.md正是这张目录路由表——在编写或修改任何测试之前必须先阅读对应目录的规范文档。目录存放内容运行器 / 环境约定文档test/unit/纯 Node 测试函数、类、模块无 DOM 依赖VitestNode 环境web/test/unit/AGENTS.mdtest/browser/端到端测试在 Chromium 中驱动 admin 与 user UI 访问运行中的 authentik 实例Vitest browser providerPlaywright#e2efixturesweb/test/browser/AGENTS.mdtest/lit/共享的 Lit 渲染辅助renderLit、LitViteContext用于组件级浏览器测试不直接存放测试——test/blueprints/YAML 蓝图如test-admin-user.yaml为浏览器测试提供认证所需的数据种子——从当前仓库目录结构可以确认这套体系已经完整落地web/test/unit/ 下已有lexer.test.ts、flow-graph.test.ts、flow-messages.test.ts、authenticator-validate-challenge-selection.test.ts、event-search.test.ts、labels.test.ts、unescape-locale-entities.test.ts以及maps/子目录下的bands.test.ts、wedges.test.ts、hexworld-style.test.ts等纯逻辑测试web/test/browser/ 下则是一组按编号与功能域组织的端到端套件100-session.test.ts、200-modals.test.ts、300-users.test.ts、400-groups.test.ts、500-roles.test.ts、600-providers.test.ts、700-applications.test.ts、800-rac.test.ts、900-invitations.test.ts以及会话生命周期、passkey、路由基路径等专项套件web/test/lit/ 提供rendering.js与setup.js两个渲染辅助文件web/test/blueprints/test-admin-user.yaml 是唯一的种子蓝图定义测试管理员账号。web/test/CLAUDE.md、web/test/unit/CLAUDE.md、web/test/browser/CLAUDE.md仅以一行AGENTS.md引用各自的规范文档进一步印证了规范入口唯一的设计意图。二、如何选择正确的测试形态自上而下的决策清单web/test/AGENTS.md给出了一条必须从上到下逐条匹配、命中即止的决策路径这是整个测试体系最重要的心智模型纯函数、无 DOM、无网络→test/unit/。便宜、快速、适合高分支覆盖率。具体约定见 web/test/unit/AGENTS.md。用户真实点击的功能流程向导、对话框、导航、列表表格、登录 →test/browser/。驱动真实 UI禁止用goauthentik/api客户端写单元测试来假装覆盖 UI 流程。约定见 web/test/browser/AGENTS.md。针对某个具体 Bug 的回归找到test/browser/中它所属的功能套件在现有文件中追加一个test(...)用例。不要新建以 Bug 命名的文件。如果 Bug 出在纯函数中则往匹配的test/unit/文件里追加it(...)。Lit 组件在隔离环境中的行为组件生命周期、slots、事件、响应式更新无整应用上下文 → 在源码旁放置Component.browser.test.ts——Vitest 配置会匹配**/*.browser.test.ts而 web/test/lit/setup.js 会向page暴露renderLit(...)用于挂载组件。由于目前还没有消费者新增第一个用例前需要与团队确认。如果你发现自己想做一件无法干净落入上述任何一个桶的事——例如写一个导入了 Lit 组件的单元测试或者写一个通过 REST API 播种数据的浏览器测试——那正是选错桶的强烈信号请回头重读目标桶的规范文档。从仓库现状看第 4 条已经出现了一个先行实践web/test/component/ak-map.browser.test.ts 以ak-map.browser.test.ts的命名方式紧挨源码组件所在目录存放印证了colocate 组件级浏览器测试的落地形态。三、跨目录通用规则所有测试都必须遵守无论落在哪个桶以下规则适用于test/下的一切代码禁止自定义 API 客户端绝不要在测试文件内构建基于fetch的管理端客户端。单元测试不需要它浏览器测试必须驱动 UI如果确实存在数据播种缺口应扩展 fixture 或 blueprint 而不是写客户端。禁止硬编码凭据fixture 中已有的除外浏览器测试通过session.login()认证使用 web/test/blueprints/test-admin-user.yaml 中的 bootstrap 管理员。不得在测试中读取process.env.AK_TEST_BOOTSTRAP_TOKEN。实体命名确定性浏览器测试创建数据时必须用IDGenerator.randomID(...)保证唯一性详见浏览器规范单元测试永远不需要它。一个文件对应一个功能 / 符号抵制创建以 Bug、工单号或日期命名的一次性文件。测试名称必须是完整句子如returns null once the input is exhausted、Create application with existing provider。不能是works更不能是#22383。其中确定性命名在源码中有清晰的落点randomName与IDGenerator由 web/e2e/utils/generators.ts 提供。其实现用种子字符串做 32 位简单哈希hash * 31 charCode再对形容词、颜色字典长度取模从而把同一seed稳定映射到同一组短语再经capitalCase生成形如 Amber Quiet Falcon 的可读名称。四、运行方式一条命令到单文件过滤npm test # 同时运行两个项目unit browser npx vitest run test/unit # 只跑单元测试 npx vitest run test/browser # 只跑浏览器测试 npx vitest run path/to/single.test.ts # 只跑单个文件 npm run test:e2e # Playwright e2e CLI 路径同一份 test/browser 源码浏览器测试要求有一个可运行的 authentik 实例地址由AK_TEST_RUNNER_PAGE_URL指定默认http://localhost:9000。如果实例未就绪web/test/browser/prerequisites.setup.ts 中的健康检查会立即报错退出。单元测试的快速迭代则另有更细的过滤手段见 web/test/unit/AGENTS.mdnpx vitest run test/unit/lexer.test.ts # 单文件 npx vitest test/unit/lexer.test.ts -t tokenization # 按名称过滤npm test脚本同时驱动 unit 与 browser 两个 Vitest 项目纯逻辑改动建议直接跑单文件以获得最快反馈。npm run test:e2e走 web/playwright.config.js 配置的 Playwright CLI 路径对 Chromium 配置了首次重试时记录 trace与深色配色方案。五、单元测试test/unit纯逻辑的快速验证层5.1 定位与适用场景单元测试是纯 Node、无浏览器的测试针对独立函数、纯逻辑和无 DOM 依赖的模块运行在 Vitest 的 Node 环境下——没有 Playwright、没有 Lit 渲染、没有运行中的 authentik 实例。适合被测对象是普通函数或类无 DOM、无网络、无组件生命周期想全面而快速地覆盖分支、边界、错误路径与不变式给定输入行为确定——没有定时器、没有外部服务、没有customElements.define。一旦答案涉及渲染 Lit 组件、点击、等待网络或对 DOM 断言就不属于这里应推给 colocate 的 Lit 组件测试或test/browser/。5.2 文件布局与导入文件位于test/unit/*.test.ts一个文件对应一个被测模块/特性以符号或模块命名如lexer.test.tsVitest 配置同时匹配全工作区的**/*.unit.test.ts因此紧密耦合的测试也可以作为foo.unit.test.ts放在源码旁边比塞进平行的test/unit/目录更清晰必须从vitest导入describe/it/expect/vi严禁从#e2e导入test/expect那是浏览器测试专用的会拖入 Playwright通过包别名#flow/…、#elements/…、#common/…访问源码绝不使用指向src/的相对路径。这与 web/AGENTS.md 中导入优先使用package.json#imports定义的别名的整体约定一致确保测试从任何位置包括测试目录都能正确解析导入vi用于 spy、mock 与定时器优先使用真实实现只在真正成问题的模块边界网络、时间、随机性处打桩。5.3 测试结构与断言惯例import { describe, expect, it, vi } from vitest; import { shouldResetSelectedChallenge } from #flow/stages/authenticator_validate/challenge-selection; describe(shouldResetSelectedChallenge, () { it(returns true when the previously selected challenge is no longer allowed, () { const selected makeDeviceChallenge(DeviceClassesEnum.Email, email-1); const allowed [ makeDeviceChallenge(DeviceClassesEnum.Totp, totp-1), makeDeviceChallenge(DeviceClassesEnum.Webauthn, webauthn-1), ]; expect(shouldResetSelectedChallenge(selected, allowed)).toBe(true); }); it(returns false when the previously selected challenge is still allowed, () { ... }); it(returns false when there was no selected challenge, () { ... }); });要点顶层describe(symbolName)可按方法或行为再嵌套参考 web/test/unit/lexer.test.ts 中的describe(tokenization)等写法it(returns X when Y)——以动词开头的完整句子同时写明结果与前置条件采用 Arrange / act / assert 三阶段阶段之间用空行分隔重复的测试数据形状用内联工厂如makeDeviceChallenge(...)放在文件顶部直到两个文件需要同一形状才抽到共享 helper每个it只测一个概念一旦名字里出现 and 就该拆分单元测试中expect()不加断言消息——测试名与匹配器已足以表达意图Vitest 输出足够清晰。匹配器选型toBe用于原始值与引用同一性toEqual用于结构相等toThrow(/regex/)用于错误路径匹配消息的稳定片段而非整句.mock.calls[i]?.[j]精确断言 spy 参数。mocking 上优先用vi.fn()内联构造测试替身而非模块级vi.mock(...)只有被测代码真正读时钟时才用vi.useFakeTimers()确实需要vi.mock(module)时提升到文件顶部并在理由不明显处加一行注释说明为什么。5.4 单元测试的禁区不从playwright/test或#e2e导入浏览器测试专属不调用customElements.define、不导入 Lit 组件Node 环境无 DOM组件覆盖应交给test/browser/或 colocate 的.browser.test.ts不访问网络或文件系统——纯函数测试如果单元需要 IO说明测错了层级不用try/catch静默吞错——错误路径用expect(() …).toThrow(...)缺失的 throw 必须让测试失败不对快照断言除非输出是稳定且有意为之的产物例如 token 流——快照在替代对契约的思考时腐烂得很快。六、浏览器端到端测试test/browser驱动真实 UI6.1 核心哲学驱动 UI而非 API浏览器测试是运行在 Vitest browser runnerChromium下的 Playwright 测试针对运行中的 authentik 实例端到端地演练 admin 与 user UI。其哲学有三条决定了整个目录的写作风格驱动 UI而不是 API功能的测试必须走用户路径——点 New Provider、填表单、点 Create、验证出现。不通过 REST API 播种实体再点一个按钮验证单一副作用。如果 UI 流程坏了测试必须一起坏如果走 API 捷径向导、模态框、导航和表单绑定中的回归就会漏网。覆盖功能而不是 Bug测试文件以功能命名providers.test.ts、applications.test.ts不以 Bug 命名。针对特定缺陷的回归测试属于该功能既有套件中的一个额外test(...)用例而不是带临时 API 管道的独立文件。无自定义 HTTP 客户端、无显式清理如果在测试里写出makeAPIClient停下——要么驱动 UI 创建前置状态要么扩展 fixture 使其可复用。实体名用IDGenerator.randomID(...)播种每次运行产生唯一 slug历史运行遗留的实体不会冲突允许在开发环境自然累积不要写try/finally清理块它会掩盖断言失败并吞掉真实故障。6.2 导入方式与 fixture 体系测试从#e2e别名导入从不直接导入playwright/testimport { expect, test } from #e2e; import { randomName } from #e2e/utils/generators; import { IDGenerator } from goauthentik/core/id; import { series } from goauthentik/core/promises;#e2e的入口 web/e2e/index.ts 重导出 Playwright 的expect并导出一个经 fixtures 扩展的test——它基于base.extendE2EFixturesTestScope, E2EWorkerScope(...)注册了navigator、session、form、pointer、passkey五类 fixture每个测试运行时都会按需构造。从测试回调中解构你需要的 fixture全部按测试逐一构造Fixture用途sessionlogin({ to, username?, password?, rememberMe? })、toLoginPage()、checkAuthenticated()。默认使用test-admingoauthentik.io/test-runner即 blueprint 中播种的 bootstrap 管理员navigatornavigate(to)与waitForPathname(to)——优先于page.goto保证 URL 等待的一致性formfill(label, value, ctx?)、search(query, ctx?)、selectSearchValue(label, pattern, ctx?)、setInputCheck(label, bool, ctx?)、setRadio(group, name, ctx?)、setFormGroup(pattern, open, ctx?)。理解ak-switch-input、ak-form-group与 search-select 下拉pointerclick(name, role?, ctx?)——按可访问名称的高层点击默认按钮/链接page原始 PlaywrightPage用于 fixture 覆盖不到的地方Shadow DOM 自动穿透baseURL实例 URL来自AK_TEST_RUNNER_PAGE_URL默认http://localhost:9000大多数步骤应经由form与pointer完成只有不存在对应 fixture 方法时才使用page.locator(...)。SessionFixtureweb/e2e/fixtures/SessionFixture.ts给出了驱动 UI 登录的源码级细节它定位ak-stage-identification、按getByLabel(Username)找用户名框、getByLabel(Password, { exact: true })精确匹配密码框避免与Show password开关的 substring 冲突、getByRole(checkbox, { name: Remember me on this device })处理记住我登录流程路径固定为/if/flow/default-authentication-flow/并对认证失败用getByRole(alert, …)捕获。整套选择器策略正是规范中ARIA role 优先、精确匹配防歧义的实践范本。6.3 测试结构与test.step分段test.describe(Feature name, () { const names new Mapstring, string(); test.beforeEach(Seed names, async ({ page: _page }, { testId }) { const seed IDGenerator.randomID(6); names.set(testId, ${randomName(seed)} (${seed})); }); test(Do the thing, async ({ session, navigator, form, pointer, page }, testInfo) { const name names.get(testInfo.testId)!; const { fill, search, selectSearchValue } form; const { click } pointer; await test.step(Authenticate, async () { await session.login({ to: /if/admin/core/providers }); }); const dialog page.getByRole(dialog, { name: New Provider Wizard }); await test.step(Open wizard, async () { await expect(dialog, Wizard is initially closed).toBeHidden(); await click(New Provider); await expect(dialog, Wizard opens).toBeVisible(); }); await test.step(Fill form, async () { await series( [click, OAuth2/OpenID, option], [fill, Provider Name, name], [ selectSearchValue, Authorization Flow, /default-provider-authorization-explicit-consent/, ], [click, Create], ); }); await test.step(Verify created, async () { await expect(await search(name), Provider is visible).toBeVisible(); }); }); });规范中强调的约定包括每个功能一个test.describe测试用祈使式命名每个有意义的阶段一个test.step(...)——它们会出现在 trace 与 HTML 报告中让失败自动定位名称用模块级Map按testId缓存在beforeEach中填充series([fn, ...args], ...)以自上而下可读的用户动作脚本形式编排有序表单填写序列对话框定位器只捕获一次再作为ctx?参数传入其内部的fill/click/selectSearchValue以限定作用域每个expect都要带第二个参数作为消息以被断言的属性来措辞Wizard opens、Provider is visible而不是复述匹配器第一个参数必须是解构模式即使不引用任何 fixture 也要写async ({ page: _page }, { testId }) {…}——裸标识符会触发 Playwright 运行时的First argument must use the object destructuring pattern它靠解析参数模式决定注入哪些 fixture而空解构async ({}, { testId }) {…}又会触发 ESLint 的no-empty-pattern解构并重命名是唯一同时满足两者的形式。真实的 web/test/browser/700-applications.test.ts 完整复刻了这一骨架test.describe(Applications) 模块级providerNamesMap beforeEach中用IDGenerator.randomID(6)播种、randomName(seed)生成可读名称随后按test.step依次创建 OAuth2 provider验证 provider 创建导航到 applications创建 application全程只通过对话框、按钮、表单与 search 交互。6.4 定位器优先级与断言定位器按以下顺序选用ARIA role 查询page.getByRole(button, { name: Create })、page.getByRole(dialog, { name: /Launch Endpoint/i })、page.getByLabel(Username)——能抗住样式/标记变更并表达意图Web 组件标签page.locator(ak-stage-identification)、page.locator(ak-form-group, { hasText: /Advanced/ })——稳定的元素契约data-test-idpage.getByTestId(...)Playwright 配置了testIdAttribute: data-test-id仅当 role/label 无法区分时才新增测试 idCSS 选择器最后手段。Shadow DOM 是透明穿透的——不要写.shadowRoot遍历。断言时始终传消息只有默认 5 秒确实不够时才显式{ timeout: ... }通常是对话框挂载或导航这类异步 UI 转换后的首个断言不要加page.waitForTimeout等待你真正关心的定位器条件。6.5 反模式清单测试文件内的自定义 API 客户端无makeAPIClient无fetch(${baseURL}/api/v3/...)做 setup从测试中读取process.env.AK_TEST_BOOTSTRAP_TOKEN——测试应通过session.login()以真实用户身份认证针对单一 Bug 的一次性回归文件——改为在相关功能套件中追加test(...)用例try/finally清理块——名称已随机化让实体自然累积无等待的page.goto——用navigator.navigate(to)或session.login({ to })存在 role/label 时仍对 CSS 选择器断言跳过test.step——冗长扁平的测试难以调试每个阶段都要包裹。6.6 新增覆盖的路径扩展既有套件时跟随周边模式——同样的 fixture 解构、同样的MaptestId, name风格、同样的对话框即上下文惯用法。引入新套件时以applications.test.ts或providers.test.ts为规范范例建模。如果需要尚不存在的 helper新的表单输入形态、新的通用导航应扩展 web/e2e/fixtures/ 中的 fixture而不是在测试里复制逻辑。七、Lit 组件渲染辅助test/lit组件级浏览器测试的基础设施test/lit/不直接存放测试而是提供两个共享渲染辅助web/test/lit/rendering.js 导出LitViteContext含render与cleanupweb/test/lit/setup.js 在 Vitest 的 browser 环境下把渲染能力挂到页面对象上import { LitViteContext } from ./rendering.js; import { beforeEach } from vitest; import { page } from vitest/browser; page.extend({ // ts-expect-error Extension is not properly typed. renderLit: LitViteContext.render, [Symbol.for(vitest:component-cleanup)]: LitViteContext.cleanup, }); beforeEach(() LitViteContext.cleanup());也就是说任何 colocate 的*.browser.test.ts都能通过page.renderLit(...)挂载组件且每个用例前自动清理上一次渲染的 DOM。这正好支撑了决策清单第 4 条的落地——目前仓库中已出现一个实践样本 web/test/component/ak-map.browser.test.ts但尚无更多消费者因此新增第一个组件级用例前需要与团队确认方向。八、测试种子蓝图test/blueprints浏览器测试的认证基石web/test/blueprints/test-admin-user.yaml 是一个 authentik 蓝图blueprint格式的 YAML 文件为浏览器测试播种 bootstrap 管理员version: 1 entries: - attrs: email: test-admingoauthentik.io is_active: true name: authentik Default Admin password: test-runner path: users type: internal groups: - !Find [authentik_core.group, [name, authentik Admins]] conditions: [] identifiers: username: akadmin model: authentik_core.user state: present它定义了用户名akadmin、邮箱test-admingoauthentik.io、密码test-runner并把该用户通过!Find表达式挂到authentik Admins组——这正是SessionFixture中GOOD_USERNAME/GOOD_PASSWORDweb/e2e/fixtures/SessionFixture.ts与session.login()默认凭据的数据来源。当浏览器测试遇到需要先有某种前置数据的缺口时正确做法是扩展这样的蓝图或 fixture而不是在测试里硬编码凭据或调用 API。九、文件与功能速查按 web/test/AGENTS.md 的 Where things live 一节各功能部件的最终落点汇总如下Playwright fixturessession、navigator、form、pointer、passkey与#e2e入口 web/e2e/index.ts 及 web/e2e/fixtures/SessionFixture.ts、NavigatorFixture.ts、FormFixture.ts、PointerFixture.ts、PasskeyFixture.ts、PageFixture.tsLit 渲染辅助 web/test/lit/setup.js 与 web/test/lit/rendering.js种子蓝图测试管理员等 web/test/blueprints/test-admin-user.yaml生成器IDGenerator、randomName web/e2e/utils/generators.ts 与goauthentik/core/id字典文件位于web/e2e/utils/dictionaries/下的adjectives.txt、colors.txt浏览器测试健康检查负责在实例未就绪时响亮地失败 web/test/browser/prerequisites.setup.ts。十、总结一张路由表驱动整个测试体系authentik WebUI 的测试体系本质上是一个**按形态路由**的体系web/test/AGENTS.md是入口路由表unit与browser两份规范文档分别是纯逻辑层与 UI 行为层的宪法lit与blueprints是支撑组件级测试与认证前置的基础设施e2e/则承载所有可复用的 fixture 与生成器。判断一个测试该写在哪里的口诀可以浓缩为有 DOM 交互去browser纯逻辑去unit组件隔离行为 colocate 成.browser.test.ts数据缺口补 blueprint能力缺口扩 fixture永远不要在自己的测试文件里发明 API 客户端。遵循这张路由表与两条规范文档你写出的测试将天然具备确定性命名、可自定位的test.step分段、消息完备的断言以及驱动 UI 而非 API的回归保障能力。【免费下载链接】authentikThe authentication glue you need.项目地址: https://gitcode.com/GitHub_Trending/au/authentik创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考