diff --git a/README.md b/README.md index f023e8698..23aa10e99 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GitHub 项目状态看板 -静态 GitHub Pages 看板,展示 Issue、PR、门禁、每日构建、版本验证、UT / ST / E2E、标签化测试及 Node.js / Python 覆盖率(可展开到文件)。总览优先列出需要处理的流水线问题;Issue 与 PR 可在表格和本地看板之间切换;CI、测试与 Coverage 可在 main 和长期开发分支之间切换查看。采集由看板仓 GitHub Actions 执行;Bot 负责接收 Webhook 和触发刷新。 +静态 GitHub Pages 看板,展示 Issue、PR、门禁、每日构建、版本验证、UT / ST / E2E、标签化测试及 Node.js / Python 覆盖率(可展开到文件)。总览优先列出需要处理的流水线问题;Issue 与 PR 可在表格和本地看板之间切换;CI、E2E 与 Coverage 可在 main 和长期开发分支之间切换查看。采集由看板仓 GitHub Actions 执行;Bot 负责接收 Webhook 和触发刷新。 | 用途 | 数据源 | 站点 | | --- | --- | --- | diff --git a/docs/branch-lines.md b/docs/branch-lines.md index 529700cf7..c94c48a84 100644 --- a/docs/branch-lines.md +++ b/docs/branch-lines.md @@ -2,7 +2,7 @@ ## 功能 -源仓当前以 `main` 为默认分支,另有 `legacy` 和 `releases/v0.3.0.beta`。CI、测试、Coverage 三页在页首提供“分支线”切换,每次只显示一条分支线的数据,各线互不混合: +源仓当前以 `main` 为默认分支,另有 `legacy` 和 `releases/v0.3.0.beta`。CI、E2E、Coverage 三页在页首提供“分支线”切换,每次只显示一条分支线的数据,各线互不混合: - 每个 run 归入它的工作所针对的分支:PR 运行归入 PR 的目标分支,push、手动、定时运行归入运行所在分支。 - 目标分支不是任何已配置分支线的 run(版本 tag、其他分支的 push、无法匹配 PR 的运行)归入默认分支线,因此每个 run 只计一次。Nightly 按运行所在分支归线,手动在 release 分支执行也归 release 线。 @@ -52,7 +52,7 @@ ## 边界 -- 分支线只影响 CI、测试、Coverage 三页的展示与归类,不改变采集范围、历史记录和 Bot 触发策略。 +- 分支线只影响 CI、E2E、Coverage 三页的展示与归类,不改变采集范围、历史记录和 Bot 触发策略。 - `legacy` 和 `releases/v0.3.0.beta` 的标签化目录和完整覆盖率基线需要来自各自分支的 push 或手动运行;目标为它们的 PR 可能修改规则,不作为基线依据。 - 已归档到默认分支、后来才配置分支线的历史 run 会在重建快照时按目标分支重新归类;标签化目录只从配置后的新运行读取。 diff --git a/docs/dashboard-pages.md b/docs/dashboard-pages.md index 0ecb4f4dc..033c4706d 100644 --- a/docs/dashboard-pages.md +++ b/docs/dashboard-pages.md @@ -1,6 +1,6 @@ # 页面与交互 -看板只展示已采集的稳定状态,不运行测试。页面依次为:总览、Issue、PR、CI、测试、Coverage、版本验证、运维、GitCode 同步(见 [GitCode 同步记录](gitcode-sync.md))。配置了[分支线](branch-lines.md)时,CI、测试、Coverage 页首可在 main、legacy 与 release 分支之间切换,每次只显示一条分支线。旧的“看板”“构建报告”“历史数据”页已移除;看板形式并入 Issue 与 PR 页,历史分片仍作为采集器的存储保留。 +看板只展示已采集的稳定状态,不运行测试。页面依次为:总览、Issue、PR、CI、E2E、Coverage、GitCode 同步(见 [GitCode 同步记录](gitcode-sync.md))。配置了[分支线](branch-lines.md)时,CI、E2E、Coverage 页首可在 main、legacy 与 release 分支之间切换,每次只显示一条分支线。旧的“看板”“构建报告”“历史数据”页已移除;看板形式并入 Issue 与 PR 页,历史分片仍作为采集器的存储保留。 页首显示快照时间。没有数据变化的采集不改快照时间,新的 UTC 日期会重建一次,Bot 至少每 8 小时采集一次;所以快照超过 36 小时未更新时,页面提示采集可能已停止。 @@ -55,7 +55,7 @@ CI、PR、Issue 数据不可读取时显示“状态未知”,不会显示为 ## 测试与 Coverage -测试页展示: +E2E 页展示: - 测试文件与最近一次 UT / E2E 用例数; - Real E2E 质量评分:顶部是最近两天的 Nightly、最近一次 `real-e2e-results` 的出处和四个汇总数字;下面与 E2E 执行记录同一种排版,按评分体系(DeepResearchBench、BiomniBench、Research Team、PUCT Compression)分类,类别可收起,每个用例一排 ✓/×,按时间从左到右表示每次记录的交付结果,悬停或聚焦显示日期、交付、评分状态、各项分数、耗时和 run,点击打开该 run。每个用例下方直接画出每项分数和运行耗时的历史曲线,并标出最新值;交付失败的运行在分数曲线上画 ×,用虚线沿用上次实测(无历史按 0),缺失的分数留空不补零。历史为该分支线每个用例最近 30 个运行日(每天取一次运行)。质量分数仅供观察,不改变 CI 结论;点击用例名打开该用例的旅程报告页 `real-e2e.html`,列出场景、前置条件、评分表和执行历史。 @@ -63,7 +63,7 @@ CI、PR、Issue 数据不可读取时显示“状态未知”,不会显示为 - [标签化测试](tagged-tests.md); - 按层 / 包的测试文件分布,以及按包的测试结构。 -除最近一次 Real E2E 评分和保留期内的 E2E 执行记录外,逐次运行的明细和覆盖率不在测试页展示。 +除最近一次 Real E2E 评分和保留期内的 E2E 执行记录外,逐次运行的明细和覆盖率不在 E2E 页展示。 Coverage 页按语言展示整仓汇总、每日趋势、目录树和最近 PR 结果。原来按覆盖率分组列出的“路径覆盖率”表已并入目录树:每个分组就是一个目录,树上同一目录的数值与该分组完全相同。 @@ -78,9 +78,15 @@ Coverage 页按语言展示整仓汇总、每日趋势、目录树和最近 PR 目录树的数据来自源仓覆盖率摘要中的 `sources` 列表,每项为仓库相对路径加该文件自己的 totals。当前值与整仓结果口径一致:以最新完整基线的文件为底,每个主干增量替换它所测分组下的文件;PR 结果不改变主干视图。其他分支线只用自己运行的摘要,见[分支线](branch-lines.md)。摘要没有 `sources` 时(上游合入逐文件数据之前的主干,以及尚未同步该改动的分支),目录树改用摘要自带的覆盖率分组构建,只展开到分组目录,并在标题中注明。 -## 版本验证与运维 +## 页面入口 -版本验证页只把提交 SHA 与版本一致的验证运行关联到版本,并给出 E2E 通过数;没有匹配运行时明确标出,发布成功不代表测试完成。运维页展示分支与保护、安全、社区健康度、提交活跃度和陈旧治理;贡献者直接链接到 GitHub 的贡献者页面(`/graphs/contributors`),看板不再采集贡献者数据。 +「测试」标签改为「E2E」,仍使用 `#tests`,已有报告的返回链接和收藏地址继续可用。版本验证与运维两个标签页及其页面已移除;总览保留已有的版本质量摘要,版本详情改为直达 GitHub,版本构建卡片跳到 CI 页。访问旧的 `#releases`、`#ops` 或未知标签会回到总览,避免空白页。采集数据与历史分片不因页面调整而删除。 + +以下为隔离样例的桌面和窄屏页面: + +![桌面 E2E 页面导航](images/e2e-navigation/e2e-tabs-1440.png) + +![390px E2E 页面导航](images/e2e-navigation/e2e-tabs-390.png) ## 验证 @@ -98,7 +104,7 @@ node .e2e/node_modules/playwright/cli.js test --config test/playwright.config.cj - 看板列独立滚动与刷新后保留位置; - 覆盖率目录树的展开、收起、过滤、按覆盖率排序,以及无逐文件数据时按分组展示; - 分支线切换及各线数据互不混合; -- 版本验证关联; +- E2E 导航、旧报告返回入口、已移除标签的回退与总览版本外链; - 窄屏布局。 `tests/test_coverage.py` 验证逐文件数据随基线与增量组合;`tests/test_work_trend.py` 验证每日趋势的计算。 diff --git a/docs/e2e-run-records.md b/docs/e2e-run-records.md index 034110aee..6044f084c 100644 --- a/docs/e2e-run-records.md +++ b/docs/e2e-run-records.md @@ -2,13 +2,13 @@ ## 功能 -测试页的“E2E 执行记录”直接按类别(现有分片)列出用例,每个用例展示保留期内各次执行的状态时间轴。类别、用例是列表主体,单次运行只出现在对应历史点的提示中。页面自动读取已经发布的 JSON,不需要先点开某次运行,也不新增采集或下载产物。 +E2E 页的“E2E 执行记录”直接按类别(现有分片)列出用例,每个用例展示保留期内各次执行的状态时间轴。类别、用例是列表主体,单次运行只出现在对应历史点的提示中。页面自动读取已经发布的 JSON,不需要先点开某次运行,也不新增采集或下载产物。 -计数和状态沿用[测试报告契约](test-reports.md)的 Playwright outcome 规则;测试页其他用例计数卡片和设置弹窗保持不变;Real E2E 质量评分使用同样的“类别 → 用例 → 状态轴”排版,另在用例下画分数曲线,见[页面与交互](dashboard-pages.md)。 +计数和状态沿用[测试报告契约](test-reports.md)的 Playwright outcome 规则;E2E 页其他用例计数卡片和设置弹窗保持不变;Real E2E 质量评分使用同样的“类别 → 用例 → 状态轴”排版,另在用例下画分数曲线,见[页面与交互](dashboard-pages.md)。 ## 使用方式 -- **类别与用例**:打开测试页就能看到分片及其用例,标题在桌面和 390px 窄屏完整换行。点击类别标题可收起或展开用例,默认展开;标题按钮也支持键盘 Enter/空格操作。可按最新一次结果筛选,或搜索分片、用例标题和文件名。 +- **类别与用例**:打开E2E 页就能看到分片及其用例,标题在桌面和 390px 窄屏完整换行。点击类别标题可收起或展开用例,默认展开;标题按钮也支持键盘 Enter/空格操作。可按最新一次结果筛选,或搜索分片、用例标题和文件名。 - **执行时间轴**:按创建时间由早到晚,从左到右排列;空间不足时从下一行继续。保留期内的所有历史点一次展示,时间轴没有滚动条,也不撑宽页面。颜色和符号分别表示失败、重试通过、跳过、通过,区块上方提供文字图例。鼠标悬停或键盘 Tab 聚焦到点时,显示该次状态、浏览器时区的完整日期、耗时、运行名称/ID 和测试项目;点本身不铺开这些详情。 - **HTML 记录**:有已托管附件的历史点可在新标签页直接打开这一次的 `e2e//<分片>/data/.html`。无附件或未托管的点不提供链接,不退回 `index.html`。 - **页面内容**:每个用例只保留标题、文件、执行次数、最新状态和状态轴;不再显示步骤、错误摘要、运行详情,也不新增用例弹窗。需要旅程内容时,打开对应历史点的 HTML 附件。 diff --git a/docs/images/e2e-navigation/e2e-tabs-1440.png b/docs/images/e2e-navigation/e2e-tabs-1440.png new file mode 100644 index 000000000..b6c46a375 Binary files /dev/null and b/docs/images/e2e-navigation/e2e-tabs-1440.png differ diff --git a/docs/images/e2e-navigation/e2e-tabs-390.png b/docs/images/e2e-navigation/e2e-tabs-390.png new file mode 100644 index 000000000..0e66eed01 Binary files /dev/null and b/docs/images/e2e-navigation/e2e-tabs-390.png differ diff --git a/docs/tagged-tests.md b/docs/tagged-tests.md index dd0a46671..6837c731c 100644 --- a/docs/tagged-tests.md +++ b/docs/tagged-tests.md @@ -2,7 +2,7 @@ ## 功能 -测试页的「标签化测试」展示源仓标签化测试框架的冻结目录: +E2E 页的「标签化测试」展示源仓标签化测试框架的冻结目录: - 每个标签维度有哪些值、各值有多少用例。 - PR / Daily / Release 三个组合分别选取哪些标签值,每个组合选中多少用例。 @@ -36,7 +36,7 @@ - 下载结果产物时一并读取这三份文件;原始 ZIP、日志和 sourceHash 不保存。 - 目录压缩为“标签签名 + `[用例 ID, 签名序号, 源文件序号]`”,与最近一次各组合运行的选择器、目标和执行结果一起写入 `.sync/tagged.json`。 - 组合规则只取默认分支或已配置[分支线](branch-lines.md)上的非 PR 运行,按分支分别保存;Release 另外接受版本 tag 运行,归入默认分支。PR 分支可能修改自己的策略,因此不作为组合定义。 -- 目录取各分支最新一次运行,同一运行的各层结果可以分批补齐。测试页按所选分支线显示;非默认分支线不显示没有运行过的组合。 +- 目录取各分支最新一次运行,同一运行的各层结果可以分批补齐。E2E 页按所选分支线显示;非默认分支线不显示没有运行过的组合。 - 标签词表 `test/support/tagged/schema.json` 按各分支目录修订读取一次,用于补齐省略的默认值、列出未使用的词表值;读取失败时保留上一份词表。 计算与源仓框架一致: diff --git a/docs/test-reports.md b/docs/test-reports.md index a7c450bf5..e269a485c 100644 --- a/docs/test-reports.md +++ b/docs/test-reports.md @@ -11,7 +11,7 @@ Actions 产物名使用 `ut-results`、`st-results`、`e2e-results`、`real-e2e- 5. `run.log`:兼容现有 CI 的 TAP / unittest / pytest 汇总,作为没有结构化报告时的后备来源。 6. Real E2E 评分:从 `benchmark-metrics.json`、`team-metrics.json`、`evolve-metrics.json` 分别读取 DeepResearchBench、BiomniBench、Research Team 和 PUCT Compression 的原生分数、交付状态与耗时。看板不统一不同评分器的量纲,也不设置质量通过门槛;prompt、原始响应、错误详情和凭据不会进入公开快照。 -Real E2E 的简化分数与运行耗时随每次 run / attempt 保存在看板历史记录中。测试页按分支线、用例 ID 汇总最近 30 次有记录的运行;同一 run 重跑只采用最新 attempt;每个北京日取一次运行,手动或触发式优先,但只在最新 attempt 有可读 `real-e2e-results` 评分的运行中挑选,仍在运行或产物不可读的运行不会挡掉当天已读到的结果;缺失指标或耗时留空,不当作 0。测试页在每个用例下直接画出这些点的全部曲线,不需要打开弹窗。历史只从已采集的产物累积,无法用最新结果推造过去的趋势。 +Real E2E 的简化分数与运行耗时随每次 run / attempt 保存在看板历史记录中。E2E 页按分支线、用例 ID 汇总最近 30 次有记录的运行;同一 run 重跑只采用最新 attempt;每个北京日取一次运行,手动或触发式优先,但只在最新 attempt 有可读 `real-e2e-results` 评分的运行中挑选,仍在运行或产物不可读的运行不会挡掉当天已读到的结果;缺失指标或耗时留空,不当作 0。E2E 页在每个用例下直接画出这些点的全部曲线,不需要打开弹窗。历史只从已采集的产物累积,无法用最新结果推造过去的趋势。 同一产物内只取一种报告格式,优先级为 summary、Playwright、标签化 summary、JUnit、log。分片产物必须互不重叠,避免同时上传同一层的合并报告和分片。用例数按报告中的测试实例(包含浏览器项目)计数,不是测试文件数;重试不重复计数。稳定通过率 = passed / tests;skipped 和 flaky 单列,不计稳定通过。零用例不显示 100%。 diff --git a/static/app.js b/static/app.js index dc4330d51..3776b459f 100644 --- a/static/app.js +++ b/static/app.js @@ -87,16 +87,6 @@ return `
${segments.filter((s) => s.value).map((s) => ``).join('')}
`; }; const ratioBar = (v, tone = '') => ``; - const sparkline = (values, labels = [], unit = '次提交') => { - if (!values.length) return ''; - const w = 400, h = 54, pad = 4, max = Math.max(...values, 1); - const pts = values.map((v, i) => [pad + (i * (w - 2 * pad)) / Math.max(values.length - 1, 1), h - pad - (v / max) * (h - 2 * pad)]); - const path = pts.map((p, i) => `${i ? 'L' : 'M'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' '); - const area = `${path} L${pts[pts.length - 1][0].toFixed(1)},${h - pad} L${pts[0][0].toFixed(1)},${h - pad} Z`; - const last = pts[pts.length - 1]; - const dots = pts.map((p, i) => ``).join(''); - return `${dots}`; - }; const dayDate = (day) => new Date(`${day}T00:00:00Z`); const dayString = (value) => { const d = value instanceof Date ? value : dayDate(value); @@ -273,7 +263,7 @@ const scope = { issues: 'issue', prs: 'pr' }[STATE.tab]; if (scope && STATE.views[STATE.tab] === 'board') { try { window.GSBBoard?.mount(scope); } catch (err) { console.error(err); } } }; - // Branch lines: CI, 测试 and Coverage show one long-lived branch at a time. PR runs + // Branch lines: CI, E2E and Coverage show one long-lived branch at a time. PR runs // belong to the branch they target; sections hold the default line. const currentLine = (snap) => { const lines = snap?.lines || []; return lines.find((l) => l.key === STATE.line) || lines[0] || { key: '', ref: snap?.repository?.default_branch, default: true }; }; const lineSection = (snap, name) => { const line = currentLine(snap); return line.default ? snap.sections[name] : snap.line_sections?.[line.key]?.[name]; }; @@ -438,9 +428,9 @@ else if (main?.outcome === 'failure') items.push(['critical', `主干最近一次 CI 失败(${esc(main.time)},${esc(main.sha)})${runLink(main)}`, '#ci']); if (daily?.outcome === 'failure') items.push(['critical', `每日构建最近一次失败(${shortDate(daily.created_at)} ${esc(daily.time)})${runLink(daily)}`, '#ci']); else if (lanes.daily && hoursSince(daily?.created_at) > 36) items.push(['warn', daily ? `每日构建已 ${Math.floor(hoursSince(daily.created_at))} 小时没有运行` : '近 30 天没有每日构建运行', '#ci']); - const latestRelease = releases[0], releaseRun = latestRelease && validation(latestRelease); - if (latestRelease && !releaseRun) items.push(['warn', `版本 ${esc(latestRelease.tag)} 没有找到提交一致的版本验证运行`, '#releases']); - else if (releaseRun && releaseRun.status === 'completed' && releaseRun.conclusion !== 'success') items.push(['critical', `版本 ${esc(latestRelease.tag)} 的验证运行${esc(CONCLUSION_NAME[releaseRun.conclusion] || releaseRun.conclusion)}${runLink(releaseRun)}`, '#releases']); + const releaseURL = `${snap.repo_url}/releases`, latestRelease = releases[0], releaseRun = latestRelease && validation(latestRelease); + if (latestRelease && !releaseRun) items.push(['warn', `版本 ${esc(latestRelease.tag)} 没有找到提交一致的版本验证运行`, latestRelease.url || releaseURL]); + else if (releaseRun && releaseRun.status === 'completed' && releaseRun.conclusion !== 'success') items.push(['critical', `版本 ${esc(latestRelease.tag)} 的验证运行${esc(CONCLUSION_NAME[releaseRun.conclusion] || releaseRun.conclusion)}${runLink(releaseRun)}`, releaseRun.url || latestRelease.url || releaseURL]); if (prs?.ci_states?.failure) items.push(['warn', `${prs.ci_states.failure} 个开放 PR 的检查失败:${prs.items.filter((p) => p.ci?.state === 'failure').map((p) => link(p.url, `#${p.number}`)).join(' ')}`, '#prs']); if (prs?.waiting_review_count) items.push(['warn', `${prs.waiting_review_count} 个 PR 超过 ${prs.review_sla_days} 天无人评审:${prs.items.filter((p) => p.waiting_review).map((p) => link(p.url, `#${p.number}`)).join(' ')}`, '#prs']); if (prs?.items?.some((p) => p.mergeable === 'CONFLICTING')) items.push(['warn', `存在冲突的 PR:${prs.items.filter((p) => p.mergeable === 'CONFLICTING').map((p) => link(p.url, `#${p.number}`)).join(' ')}`, '#prs']); @@ -452,9 +442,9 @@ if (tagged?.uncovered?.cases) hints.push(['info', `${n(tagged.uncovered.cases)} 个标签化用例不会被任何流水线组合选中`, '#tests']); if (iss?.no_response_count) hints.push(['info', `${iss.no_response_count} 个开放 Issue 还没有评论,${iss.unassigned_count} 个无人认领`, '#issues']); if (iss?.stale_count) hints.push(['info', `${iss.stale_count} 个 Issue 超过 ${iss.stale_days_threshold} 天没有更新`, '#issues']); - if (ops?.releases?.unreleased?.commits > 50) hints.push(['info', `自 ${esc(ops.releases.latest?.tag)} 以来已有 ${n(ops.releases.unreleased.commits)} 个提交未发布`, '#releases']); - if (ops?.branches?.protection && !ops.branches.protection.enabled) hints.push(['info', `默认分支 ${esc(ops.branches.default)} 未启用分支保护`, '#ops']); - const line = ([sev, text, href]) => `
  • ${text} 详情
  • `; + if (ops?.releases?.unreleased?.commits > 50) hints.push(['info', `自 ${esc(ops.releases.latest?.tag)} 以来已有 ${n(ops.releases.unreleased.commits)} 个提交未发布`, releaseURL]); + if (ops?.branches?.protection && !ops.branches.protection.enabled) hints.push(['info', `默认分支 ${esc(ops.branches.default)} 未启用分支保护`, `${snap.repo_url}/branches`]); + const line = ([sev, text, href]) => `
  • ${text} ${href.startsWith("https://") ? link(href, "详情") : `详情`}
  • `; const serious = items.filter(([sev]) => sev === 'critical').length; let html = `

    ${serious ? `${serious} 个流水线问题需要处理` : items.length ? `${items.length} 项需要关注` : '流水线与交付状态正常'}数据截至 ${date(snap.generated_at)}(${ago(snap.generated_at)})

    ${items.length ? `
      ${items.map(line).join('')}
    ` : '
    主干、每日构建和最新版本都没有失败;开放 PR 没有失败检查或超期评审。
    '} @@ -467,7 +457,7 @@ return { label, value: text, tone, href, sub: run ? `${shortDate(run.created_at)} ${esc(run.time)} · ${esc(run.workflow)}${run.pr ? ` · PR #${run.pr}` : ''}${run.duration_s != null ? ` · 耗时 ${dur(run.duration_s)}` : ''}
    近 ${L.window_days} 天成功率 ${pct(rate)}(${n(lane.summary.total)} 次)` : lane ? `近 ${L.window_days} 天没有运行` : '分层历史尚未生成' }; }; html += sectionHead('流水线', L ? `最近一次运行的结论;成功率 = 成功 ÷(成功 + 失败)${(snap.lines || []).length > 1 ? `;此处为 ${esc(snap.lines[0].ref)} 分支线,其他分支线在 CI 页切换` : ''}` : ''); - html += tiles([laneTile('pr', 'PR 门禁', '#ci'), laneTile('main', '主干', '#ci'), laneTile('daily', '每日构建', '#ci'), laneTile('release', '版本构建', '#releases')]); + html += tiles([laneTile('pr', 'PR 门禁', '#ci'), laneTile('main', '主干', '#ci'), laneTile('daily', '每日构建', '#ci'), laneTile('release', '版本构建', '#ci')]); if (L) html += `
    ${card('近 14 天', ciLanes(recentLanes(L, Math.min(14, L.days.length)), { notes: false }), { sub: `每格一次运行,颜色为结论;完整 ${L.window_days} 天与 job 明细` })}
    `; // Delivery and quality at a glance. @@ -481,7 +471,7 @@ { label: '开放 Issue', value: n(iss?.open_count), href: '#issues', sub: iss ? `7 天 +${n(iss.counts.opened_7d)} / −${n(iss.counts.closed_7d)} · 陈旧 ${n(iss.stale_count)}` : '—' }, { label: '主干门禁用例', value: layers.length ? `${n(passed)}/ ${n(planned)}` : '—', href: '#tests', tone: layers.length ? (passed === planned ? 'good' : 'bad') : '', sub: layers.length ? layers.map(([s, r]) => `${s.toUpperCase()} ${n(r.passed)}/${n(r.planned)}`).join(' · ') : '尚未读取到门禁计划' }, { label: '整仓行覆盖率', value: lines != null ? pct(lines) : '—', href: '#coverage', sub: cov?.current?.kind === 'authoritative' ? '主干完整结果' : cov?.current?.kind === 'incremental' ? '基线 + 主干增量' : cov?.source ? '部分结果' : '暂无覆盖率' }, - { label: '最新版本', value: rel ? esc(rel.tag) : '—', href: '#releases', sub: rel ? `${days(rel.age_days)}前${ops?.releases?.unreleased ? ` · 之后 ${n(ops.releases.unreleased.commits)} 个提交未发布` : ''}` : '尚无 release' }, + { label: '最新版本', value: rel ? esc(rel.tag) : '—', href: latestRelease?.url || releaseURL, sub: rel ? `${days(rel.age_days)}前${ops?.releases?.unreleased ? ` · 之后 ${n(ops.releases.unreleased.commits)} 个提交未发布` : ''}` : '尚无 release' }, ]); const releaseRows = releases.slice(0, 3).map((release) => { @@ -491,7 +481,7 @@ const merged = (prs?.recent_merged || []).slice(0, 5).map((p) => `
  • ${badge('合并', 'good')} ${link(p.url, `#${p.number} ${esc(p.title)}`)} ${ago(p.merged_at)}
  • `); const opened = (iss?.items || []).slice().sort((a, b) => (b.created_at || '').localeCompare(a.created_at || '')).slice(0, 5).map((i) => `
  • ${badge('新建', 'info')} ${link(i.url, `#${i.number} ${esc(i.title)}`)} ${ago(i.created_at)}
  • `); html += `
    - ${card('版本质量', releaseRows ? `
      ${releaseRows}
    ` : empty('仓库尚无 release'), { sub: '版本提交的验证运行结论;全部版本' })} + ${card('版本质量', releaseRows ? `
      ${releaseRows}
    ` : empty('仓库尚无 release'), { sub: `版本提交的验证运行结论;${link(releaseURL, '在 GitHub 查看全部版本 ↗')}` })} ${card('最近动态', merged.length || opened.length ? `
      ${[...merged, ...opened].join('')}
    ` : empty('最近没有合并或新建'), { sub: '最近合并的 PR 与新建的 Issue' })}
    `; const status = Object.entries(S).filter(([, s]) => s.status !== 'ok').map(([k, s]) => badge(`${k} ${s.status}`, s.status === 'partial' ? 'warn' : 'bad')); @@ -499,20 +489,6 @@ return html; } - function renderReleases(snap) { - const releases = snap.releases || [], runsById = new Map((snap.quality?.runs || []).map((r) => [r.id, r])); - const html = sectionHead('版本验证', '只关联提交与版本一致的验证运行;发布成功不等于测试完成'); - if (!releases.length) return html + `
    ${empty('仓库尚无 Release。发布版本后,将按标签对应的提交关联验证证据。')}
    `; - return html + `
    ${releases.map((release) => { - const runs = (release.validation_run_ids || []).map((id) => runsById.get(id)).filter(Boolean); - const evidence = runs.length ? runs.map((run) => { - const e2eCounts = sumCounts((run.tests || []).filter((t) => t.layer === 'e2e')); - return `
    ${conclusionBadge(run.status === 'completed' ? run.conclusion : run.status)} ${link(run.url, esc(run.name))} ${e2eCounts ? `E2E ${n(e2eCounts.passed)} / ${n(e2eCounts.tests)} 通过` : '暂无 E2E 用例结果'}
    `; - }).join('') : `${badge('尚无匹配的版本验证', 'warn')}
    等待此版本提交的验证运行。
    `; - return `

    ${link(release.url, esc(release.tag))}

    ${badge(release.prerelease ? '预发布' : '已发布', release.prerelease ? 'warn' : 'good')} ${date(release.published_at)} ${esc((release.sha || '提交未知').slice(0, 12))}
    ${evidence}
    `; - }).join('')}
    `; - } - // Issue / PR daily flow. Daily counts and the number still open differ in scale, so they get // two charts on one shared time axis rather than two y-axes. The series come precomputed in the // snapshot; the charts are drawn after the tab is in the page so they can use its real width. @@ -1172,7 +1148,7 @@ function renderTests(sec, line) { const d = sec?.data; - let html = sectionState(sec, '测试'); + let html = sectionState(sec, 'E2E'); if (!d) return html; const tree = d.tree; const ut = d.executed.find((e) => e.artifact.startsWith('ut')); @@ -1362,56 +1338,15 @@ return html; } - function renderOps(sec) { - const d = sec?.data; - let html = sectionState(sec, '运维'); - if (!d) return html; - const cards = []; - // Releases - const r = d.releases; - cards.push(card('Release / Tag', r ? kv([ - ['最新', r.latest ? `${link(r.latest.url, esc(r.latest.tag))} ${r.latest.prerelease ? badge('预发布', 'warn') : ''} ${ago(r.latest.published_at)} · ${esc(r.latest.author)}` : '无 release'], - ['未发布提交', r.unreleased ? `${link(r.unreleased.url, n(r.unreleased.commits))} 个提交,${n(r.unreleased.files_changed)} 个文件` : '—'], - ['发布节奏', r.cadence_days != null ? `平均 ${r.cadence_days} 天一次(${r.count} 个 release)` : `${r.count} 个 release,不足以计算节奏`], - ['资产下载', n(r.total_downloads)], - ['Tags', r.tags.map((t) => `${esc(t.name)}`).join(' ') || '—'], - ]) + (r.items.length > 1 ? `
    全部 release
      ${r.items.map((x) => `
    • ${link(x.url, esc(x.tag))} ${ago(x.published_at)} · 资产 ${x.assets.length}
    • `).join('')}
    ` : '') : empty('Release 数据不可用'))); - // Branches - const b = d.branches; - cards.push(card('分支与保护', b ? `${kv([ - ['默认分支', `${esc(b.default)}`], - ['分支保护', b.protection?.enabled ? badge('已启用', 'good') + (b.protection.readable === false ? ' 规则详情需 admin 权限' : ` 需 ${b.protection.required_reviews ?? 0} 个批准 · 必需检查 ${(b.protection.required_checks || []).map(esc).join(', ') || '无'}`) : badge('未启用', 'bad') + ' 分支列表 protected=false'], - ['Rulesets', b.rulesets ? (b.rulesets.length ? b.rulesets.map((x) => `${esc(x.name)} (${esc(x.enforcement)})`).join(',') : '无') : '不可读'], - ])}${table('ops-br', [ - { key: 'name', label: '分支', render: (x) => `${esc(x.name)}${x.is_default ? ' ' + badge('默认', 'info') : ''}${x.protected ? ' ' + badge('保护', 'good') : ''}` }, - { key: 'ahead', label: '领先', num: true, render: (x) => n(x.ahead) }, - { key: 'behind', label: '落后', num: true, render: (x) => (x.behind == null ? '—' : x.behind >= 100 ? `${n(x.behind)}` : n(x.behind)) }, - { key: 'idle_days', label: '最近提交', render: (x) => (x.is_default ? '—' : x.last_commit_at ? ago(x.last_commit_at) : '—') }, - ], b.items, { defaultSort: { key: 'behind', dir: 'desc' } })}` : empty('分支数据不可用'), { sub: `${b?.count ?? 0} 个分支` })); - cards.push(card('安全', `${link(STATE.snap.repo_url+'/security', '在 GitHub 查看安全告警 ↗')}

    私有告警由 GitHub 权限控制;此公开站点只列出已公开的安全公告。

    ${d.public_advisories?.length ? '
      '+d.public_advisories.map(a=>'
    • '+badge(a.severity||'未知')+' '+link(a.url,esc(a.summary))+'
    • ').join('')+'
    ' : empty(d.public_advisories ? '没有已公开的安全公告' : '公开安全公告暂不可读取')}`)); - // Community - const c = d.community; - cards.push(card('社区健康度', c ? `
    GitHub community profile
    ${pct(c.health_percentage)}
      ${Object.entries(c.files).filter(([k]) => k !== 'code_of_conduct_file').map(([k, v]) => `
    • ${badge(v ? '有' : '缺', v ? 'good' : 'bad')} ${esc(k)}
    • `).join('')}
    ` : empty('不可用'))); - // Contributors: GitHub's own page is complete and current. - cards.push(card('贡献者', link(`${STATE.snap.repo_url}/graphs/contributors`, '在 GitHub 查看贡献者 ↗') + '

    贡献者名单与各自的提交统计以 GitHub 页面为准。

    ')); - // Activity - const a = d.activity; - cards.push(card('提交活跃度', `${a ? `${sparkline(a.weeks.map((w) => w.total), a.weeks.map((w) => new Date(w.week * 1000).toLocaleDateString('zh-CN')))}${kv([['近 4 周', `${n(a.commits_4w)} 次提交`], ['近 52 周', `${n(a.commits_52w)} 次提交`], ['近 7 天(默认分支)', `${n(d.commits_7d)} 次`]])}` : '
    周统计暂不可读取
    '}
    最近提交(${d.recent_commits.length})
      ${d.recent_commits.slice(0, 15).map((x) => `
    • ${link(x.url, esc(x.sha))} ${esc(x.message)} ${esc(x.author)} · ${ago(x.date)}
    • `).join('')}
    `, { sub: '近 26 周,每周提交数' })); - cards.push(card('流量与克隆', link(STATE.snap.repo_url+'/graphs/traffic', '在 GitHub Insights 查看流量 ↗')+'

    访问量、独立访客和克隆统计由 GitHub 对有权限的成员提供,不写入公开快照。

    ')); - // Stale automation & hygiene - const iss = STATE.snap.sections.issues?.data, prs = STATE.snap.sections.prs?.data; - cards.push(card('陈旧治理', kv([ - ['stale 自动化', d.stale_automation.workflow ? `${badge('已配置', 'good')} ${esc(d.stale_automation.workflow)}` : `${badge('未配置', 'warn')} 没有 workflow 使用 actions/stale`], - ['陈旧 Issue', iss ? `${n(iss.stale_count)} 个 ≥${iss.stale_days_threshold} 天无更新,${n(iss.no_response_count)} 个零回复` : '—'], - ['闲置 PR', prs ? `${prs.items.filter((p) => (p.idle_days || 0) >= (STATE.snap.config.pr_idle_days || 14)).length} 个 ≥14 天无更新` : '—'], - ['陈旧分支', b ? (b.stale.length ? b.stale.map((x) => `${esc(x.name)}`).join(' ') : '无') : '—'], - ]))); - html += `
    ${cards.join('')}
    `; - return html; - } - - // ------------------------------------------------------------ shell + const activeTab = () => { + const requested = location.hash.slice(1); + const known = [...document.querySelectorAll('#tabs [data-tab]')].some(tab => tab.dataset.tab === requested); + if (known) return requested; + if (requested) history.replaceState(history.state, '', location.pathname + location.search + '#overview'); + return 'overview'; + }; + function renderShell() { const snap=STATE.snap, btn=$('#refresh-btn'), banner=$('#global-banner'); btn.disabled=!!STATE.status?.refreshing; @@ -1441,7 +1376,7 @@ const snap = STATE.snap; const render = (id, fn) => { const el = $(`#tab-${id}`); if (el) el.innerHTML = fn(); }; if (!snap) { - ['overview', 'issues', 'prs', 'ci', 'tests', 'coverage', 'releases', 'ops'].forEach((id) => render(id, () => `
    ${STATE.status?.refreshing ? '采集中…' : '暂无数据'}
    `)); + ['overview', 'issues', 'prs', 'ci', 'tests', 'coverage'].forEach((id) => render(id, () => `
    ${STATE.status?.refreshing ? '采集中…' : '暂无数据'}
    `)); return; } document.querySelectorAll('.tab').forEach((t) => t.classList.toggle('active', t.id === `tab-${STATE.tab}`)); @@ -1455,8 +1390,6 @@ safe('ci', () => lineBar(snap) + renderCI(lineSection(snap, 'ci'), line)); safe('tests', () => lineBar(snap) + renderTests(lineSection(snap, 'tests'), line)); safe('coverage', () => lineBar(snap) + renderCoverage(lineSection(snap, 'tests'), line)); - safe('releases', () => renderReleases(snap)); - safe('ops', () => renderOps(snap.sections.ops)); pinLanes(keep); try { if (window.GSBBoard) window.GSBBoard.onSnapshot(snap); } catch (err) { console.error(err); } mountBoard(); @@ -1629,7 +1562,7 @@ $('#refresh-btn').addEventListener('click', refresh); // Browsers throttle timers in background tabs; re-sync as soon as the tab is visible again. document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { clearTimeout(STATE.pollTimer); load(); } }); - window.addEventListener('hashchange', () => { STATE.tab = location.hash.slice(1) || 'overview'; renderShell(); mountBoard(); document.querySelectorAll('.tab').forEach((t) => t.classList.toggle('active', t.id === `tab-${STATE.tab}`)); pinLanes(); drawTrends(); loadE2EHistory(); }); + window.addEventListener('hashchange', () => { STATE.tab = activeTab(); renderShell(); mountBoard(); document.querySelectorAll('.tab').forEach((t) => t.classList.toggle('active', t.id === `tab-${STATE.tab}`)); pinLanes(); drawTrends(); loadE2EHistory(); }); // Tooltip layer: any element with data-tip. const tipEl = $('#tooltip'); // Beside an element (for keyboard focus); a trend chart anchors it to its crosshair instead. @@ -1696,6 +1629,6 @@ // Shared helpers for the board module (static/board.js). window.GSB = { esc, ago, days, date, n, badge, labelChips, link, codeify, conclusionBadge, tip, empty, kv, CONCLUSION_NAME, refresh, snapshot:()=>STATE.snap, ageBucketOf, AGE_BUCKETS, ageFilter:()=>STATE.ageFilter, setAgeFilter }; - STATE.tab = location.hash.slice(1) || 'overview'; + STATE.tab = activeTab(); load(); })(); diff --git a/static/e2e-report.html b/static/e2e-report.html index 00ed54612..1b0777c6e 100644 --- a/static/e2e-report.html +++ b/static/e2e-report.html @@ -14,7 +14,7 @@ - + - - + + + diff --git a/static/real-e2e.html b/static/real-e2e.html index 2a061f363..d942b662f 100644 --- a/static/real-e2e.html +++ b/static/real-e2e.html @@ -40,8 +40,8 @@ - +
    正在读取用例详情…
    - + diff --git a/static/real-e2e.js b/static/real-e2e.js index 7d7f6e6ab..75aa04277 100644 --- a/static/real-e2e.js +++ b/static/real-e2e.js @@ -74,7 +74,7 @@ function render(snapshot) { const line = (snapshot.lines || []).find((item) => item.key === wantedLine); if (!line || !wantedCase || wantedCase.length > 100) { - root.innerHTML = '

    未找到对应的分支线或用例。返回测试看板

    '; + root.innerHTML = '

    未找到对应的分支线或用例。返回 E2E 看板

    '; return; } const data = (line.default ? snapshot.sections?.tests : snapshot.line_sections?.[line.key]?.tests)?.data || {}; @@ -84,7 +84,7 @@ run_id: report.run_id, attempt: report.attempt, created_at: report.created_at, url: report.url }] : [])) .slice().sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at) || Number(a.run_id) - Number(b.run_id)); if (!history.length) { - root.innerHTML = `

    当前 ${escape(line.ref)} 分支线没有 ${escape(wantedCase)} 的可读取记录。返回测试看板

    `; + root.innerHTML = `

    当前 ${escape(line.ref)} 分支线没有 ${escape(wantedCase)} 的可读取记录。返回 E2E 看板

    `; return; } const latest = history.at(-1); @@ -138,13 +138,13 @@ } if (!wantedCase || !wantedLine) { - root.innerHTML = '

    缺少用例或分支线参数。返回测试看板

    '; + root.innerHTML = '

    缺少用例或分支线参数。返回 E2E 看板

    '; return; } fetch('./data/snapshot.json', { cache: 'no-store' }).then((response) => { if (!response.ok) throw new Error('snapshot unavailable'); return response.json(); }).then(render).catch(() => { - root.innerHTML = '

    暂时无法读取用例详情。请稍后重试,或返回测试看板。

    '; + root.innerHTML = '

    暂时无法读取用例详情。请稍后重试,或返回 E2E 看板。

    '; }); })(); diff --git a/static/style.css b/static/style.css index 449d3bfdc..add4c96f1 100644 --- a/static/style.css +++ b/static/style.css @@ -458,14 +458,12 @@ dialog.settings footer { display: flex; justify-content: flex-end; gap: 8px; mar .rules-grid select, .rules-grid input[type=number] { font: inherit; font-size: 13px; padding: 3px 6px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--ink); } dialog.settings textarea { width: 100%; font: inherit; font-size: 13px; padding: 5px 8px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface); color: var(--ink); box-sizing: border-box; } -/* Shared report pieces: release rows, pass/fail bars, filters. */ +/* Shared report pieces: pass/fail bars and filters. */ .small-note{font-size:12px;color:var(--muted)} -.release-row{display:flex;justify-content:space-between;padding:18px;gap:20px;border-bottom:1px solid var(--grid)} -.release-row:last-child{border-bottom:0}.release-row h3{margin:0 0 6px;font-size:15px}.release-evidence{margin:4px 0} .bar{display:flex;height:6px;overflow:hidden;border-radius:5px;margin:12px 0}.bar .passed{background:var(--good)}.bar .failed{background:var(--critical)}.bar .skipped{background:var(--axis)}.bar .flaky{background:var(--warning)} .legend{display:flex;gap:12px;flex-wrap:wrap;font-size:12px}.count-inline{font-size:12px;color:var(--muted)}.filters{display:flex;gap:15px;align-items:center;flex-wrap:wrap;margin:15px 0} .board-local-note{margin:0 0 10px;color:var(--ink-2);font-size:12px} -@media(max-width:700px){.topbar,.brand{min-width:0}.repo-link{overflow-wrap:anywhere}.release-row{flex-direction:column}.tabs{overflow-x:auto;flex-wrap:nowrap}} +@media(max-width:700px){.topbar,.brand{min-width:0}.repo-link{overflow-wrap:anywhere}.tabs{overflow-x:auto;flex-wrap:nowrap}} @media(max-width:700px){ .tabs a{flex:none;white-space:nowrap}.brand{flex-wrap:wrap}.brand-title{white-space:nowrap} diff --git a/test/journey-board.spec.cjs b/test/journey-board.spec.cjs index 3f959661b..561f65ef1 100644 --- a/test/journey-board.spec.cjs +++ b/test/journey-board.spec.cjs @@ -23,7 +23,7 @@ test('all existing pages render; only static requests and no promotional copy', await expect(page.locator('#global-banner')).toContainText('超过 36 小时未更新'); await expect(page.locator('body')).not.toContainText('把进展与风险放在同一页'); await page.screenshot({path:shot('overview-desktop'),fullPage:true}); - await expect(page.locator('.topbar #tabs a')).toHaveCount(9); + await expect(page.locator('.topbar #tabs a')).toHaveCount(7); await expect(page.locator('.topbar #tabs')).not.toContainText('构建报告'); await expect(page.locator('.topbar #tabs')).not.toContainText('历史数据'); await expect(page.locator('body > footer')).toHaveCount(0); @@ -32,7 +32,7 @@ test('all existing pages render; only static requests and no promotional copy', const tabsBox=await page.locator('#tabs').boundingBox(), timeBox=await page.locator('#meta').boundingBox(); expect(timeBox.x).toBeGreaterThan(tabsBox.x+tabsBox.width); expect(timeBox.y).toBeLessThan(tabsBox.y+tabsBox.height); - for (const id of ['issues','prs','ci','tests','coverage','releases','ops','sync']) { + for (const id of ['issues','prs','ci','tests','coverage','sync']) { await page.locator(`[data-tab="${id}"]`).click(); await expect(page.locator(`#tab-${id}`)).toBeVisible(); await expect(page.locator(`#tab-${id}`)).not.toContainText('渲染出错'); @@ -402,7 +402,7 @@ test('drawer edits and field import/export stay local',async({page})=>{ expect(writes).toEqual([]); }); -test('CI trends, failed job steps, test distribution, coverage and operations',async({page})=>{ +test('CI trends, failed job steps, test distribution and coverage',async({page})=>{ await page.goto('/github-status-board/#ci'); await expect(page.locator('#tab-ci')).toContainText('Workflow 健康'); await expect(page.locator('#tab-ci')).toContainText('Run browser journeys'); @@ -525,12 +525,7 @@ test('CI trends, failed job steps, test distribution, coverage and operations',a await expect(page.locator('[data-cov-sort="lines"]').first()).toHaveClass(/on/); await page.locator('[data-cov-sort="name"]').first().click(); await page.screenshot({path:shot('coverage-desktop'),fullPage:true}); - await page.locator('[data-tab="ops"]').click(); - // Contributors are read on GitHub's own page. - await expect(page.locator('#tab-ops a[href$="/graphs/contributors"]')).toHaveCount(1); - await expect(page.locator('#tab-ops')).not.toContainText('前 50% 提交由'); - await expect(page.locator('#tab-ops')).toContainText('分支与保护'); - await expect(page.locator('#tab-ops a[href$="/graphs/traffic"]')).toHaveCount(1); + }); test('tagged dimensions, profile combinations and never-covered cases', async ({ page }) => { @@ -671,7 +666,7 @@ test('Real E2E case names open branch-specific run details', async ({ page }) => await expect(page.getByText('isolated Swarm stack', { exact: true })).toBeVisible(); await expect(page.getByText('本次报告没有记录独立的用户步骤。')).toBeVisible(); await expect(page.locator('.report-meta').last()).toContainText('E2E_API_TOKEN(仅变量名)'); - await page.getByRole('link', { name: '← 返回测试看板' }).click(); + await page.getByRole('link', { name: '← 返回 E2E 看板' }).click(); await expect(page.locator('#tab-tests')).toBeVisible(); await expect(page.locator('#tab-tests .line-switch [data-line="main"]')).toHaveClass(/on/); const caseLink = page.getByRole('link', { name: '查看 DRB-59 的 Real E2E 详情' }); @@ -758,7 +753,7 @@ test('E2E run records show categories and case timelines without opening runs', expect(decodeURI(src)).toContain('/journey-reports/issue-77-wake-notice/'); expect((await page.request.get(src)).status()).toBe(200); } - await tab.getByRole('link', { name: '返回测试看板' }).click(); + await tab.getByRole('link', { name: '返回 E2E 看板' }).click(); await expect(tab.locator('#tab-tests .e2e-case').filter({ has: tab.locator('h4', { hasText: '后台执行完成后显示运行时提示' }) }).first()).toBeInViewport(); await tab.close(); await expect(records.locator('.e2e-case-details, .e2e-attempt, .e2e-error, .e2e-steps')).toHaveCount(0); @@ -1046,10 +1041,33 @@ test('CI, tests and coverage switch between main, legacy and release without mix await expect(page.locator('#tab-overview .ci-lane-row:not(.ci-axis)')).toHaveCount(7); }); -test('release evidence matches SHA; no evidence remains unknown',async({page})=>{ - await page.goto('/github-status-board/#releases'); - await expect(page.locator('.release-row').first()).toContainText('10 / 10 通过'); - await expect(page.locator('.release-row').last()).toContainText('尚无匹配的版本验证'); +test('E2E navigation replaces tests and removed tabs have no dangling links', async ({ page }) => { + await page.goto('/github-status-board/'); + const tabs = page.getByRole('navigation', { name: '看板页面' }); + await expect(tabs.getByRole('link')).toHaveText(['总览', 'Issue', 'PR', 'CI', 'E2E', 'Coverage', /^GitCode 同步\d*$/]); + await expect(page.locator('#tab-releases, #tab-ops')).toHaveCount(0); + await expect(page.locator('a[href="#releases"], a[href="#ops"]')).toHaveCount(0); + await expect(page.getByRole('link', { name: '在 GitHub 查看全部版本 ↗' })).toHaveAttribute('href', /github\.com\/.+\/releases$/); + for (const width of [1440, 390]) { + await page.setViewportSize({ width, height: 1000 }); + await tabs.getByRole('link', { name: 'E2E', exact: true }).click(); + await expect(page.locator('#tab-tests')).toBeVisible(); + await expect(tabs.getByRole('link', { name: 'E2E', exact: true })).toHaveAttribute('aria-current', 'page'); + await expect(page.locator('#tab-tests')).toContainText('Real E2E'); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBeTruthy(); + await page.screenshot({ path: shot(`e2e-tabs-${width}`) }); + } + // Existing report return links keep #tests; old removed tabs recover to the overview. + for (const hash of ['releases', 'ops', 'unknown']) { + await page.goto(`/github-status-board/#${hash}`); + await expect(page.locator('#tab-overview')).toBeVisible(); + await expect(page).toHaveURL(/#overview$/); + } + // Exercise hashchange too, without loading a fresh document. + await tabs.getByRole('link', { name: 'CI', exact: true }).click(); + await page.evaluate(() => { location.hash = 'ops'; }); + await expect(page.locator('#tab-overview')).toBeVisible(); + await expect(page).toHaveURL(/#overview$/); }); test('unavailable data is not zero or a passing build',async({page})=>{ @@ -1059,8 +1077,7 @@ test('unavailable data is not zero or a passing build',async({page})=>{ await expect(health).not.toContainText('正常'); await expect(page.locator('#tab-overview .tile').filter({hasText:'主干门禁用例'}).locator('.value')).toHaveText('—'); await expect(page.locator('#global-banner')).toContainText('结果未知'); - await page.locator('[data-tab="releases"]').click(); - await expect(page.locator('#tab-releases')).toContainText('仓库尚无 Release'); + await expect(page.locator('#tabs [data-tab="releases"]')).toHaveCount(0); }); test('Issue and PR tabs chart daily flow with an adjustable range and series switches', async ({ page }) => {