From c733aff51645dfc04cfe4ec6a3627941d402dbb0 Mon Sep 17 00:00:00 2001 From: cmd-ob Date: Mon, 3 Aug 2026 13:22:21 +0100 Subject: [PATCH] fix: normalize Slack blob links in Playwright test health report Map Playwright testDir-relative and CI absolute paths to repo-relative GitHub blob URLs so Mobile Appium health-report spec links stop 404ing (MMQA-2013). Co-authored-by: Cursor --- .../playwright-test-health-report/action.yml | 8 ++ .../create-playwright-test-health-report.mjs | 2 + .../lib/normalize-repo-file-path.mjs | 49 ++++++++++ .../lib/report-health.test.mjs | 92 +++++++++++++++++++ .../lib/slack-test-health-blocks.mjs | 8 +- 5 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 .github/actions/playwright-test-health-report/lib/normalize-repo-file-path.mjs diff --git a/.github/actions/playwright-test-health-report/action.yml b/.github/actions/playwright-test-health-report/action.yml index 10306330..6318f1f8 100644 --- a/.github/actions/playwright-test-health-report/action.yml +++ b/.github/actions/playwright-test-health-report/action.yml @@ -54,6 +54,13 @@ inputs: description: Slack header title override required: false default: Playwright Test Health Report + test-source-prefix: + description: >- + Optional path prefix prepended to Playwright testDir-relative paths when + building GitHub blob links (e.g. tests/smoke-appium). Absolute CI paths + are stripped to repo-relative regardless of this value. + required: false + default: '' github-tools-repository: required: false default: ${{ github.action_repository }} @@ -89,6 +96,7 @@ runs: RESULTS_FILE_PATTERN: ${{ inputs.results-file-pattern }} TOP_N: ${{ inputs.top-n }} REPORT_TITLE: ${{ inputs.report-title }} + TEST_SOURCE_PREFIX: ${{ inputs.test-source-prefix }} GITHUB_TOKEN: ${{ inputs.github-token }} SLACK_WEBHOOK: ${{ inputs.slack-webhook }} run: node .github/actions/playwright-test-health-report/create-playwright-test-health-report.mjs diff --git a/.github/actions/playwright-test-health-report/create-playwright-test-health-report.mjs b/.github/actions/playwright-test-health-report/create-playwright-test-health-report.mjs index b3932ba2..6e244734 100644 --- a/.github/actions/playwright-test-health-report/create-playwright-test-health-report.mjs +++ b/.github/actions/playwright-test-health-report/create-playwright-test-health-report.mjs @@ -32,6 +32,7 @@ const env = { RESULTS_FILE_PATTERN: process.env.RESULTS_FILE_PATTERN || 'playwright-report', TOP_N: parsePositiveInt(process.env.TOP_N, 15), REPORT_TITLE: process.env.REPORT_TITLE || 'Playwright Test Health Report', + TEST_SOURCE_PREFIX: process.env.TEST_SOURCE_PREFIX?.trim() || '', SLACK_WEBHOOK: process.env.SLACK_WEBHOOK || '', GITHUB_TOKEN: githubToken, }; @@ -158,6 +159,7 @@ async function sendSlackReport(summary, dateDisplay, metadata) { testFailureRunCount: metadata.testFailureRunCount, otherFailedRunCount: metadata.otherFailedRunCount, lookbackDays: env.LOOKBACK_DAYS, + testSourcePrefix: env.TEST_SOURCE_PREFIX, }); await sendSlackBatched(env.SLACK_WEBHOOK, blocks); console.log('✅ Report sent to Slack successfully'); diff --git a/.github/actions/playwright-test-health-report/lib/normalize-repo-file-path.mjs b/.github/actions/playwright-test-health-report/lib/normalize-repo-file-path.mjs new file mode 100644 index 00000000..12ea4c69 --- /dev/null +++ b/.github/actions/playwright-test-health-report/lib/normalize-repo-file-path.mjs @@ -0,0 +1,49 @@ +/** + * Normalize a Playwright test path into a GitHub blob-friendly repo-relative path. + * + * Handles: + * - CI absolute checkout paths (e.g. /Users/runner/work/.../tests/...) + * - Optional testDir prefix (e.g. accounts/foo.spec.ts → tests/smoke-appium/accounts/foo.spec.ts) + * - Idempotent when the path already starts with the prefix or `tests/` + * + * @param {string} filePath + * @param {{ prefix?: string }} [options] + * @returns {string} + */ +export function normalizeRepoFilePath(filePath, { prefix } = {}) { + if (!filePath) { + return filePath; + } + + let normalized = String(filePath).replace(/\\/g, '/'); + + const isAbsolute = normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized); + if (isAbsolute) { + const testsMatch = normalized.match(/(?:^|\/)(tests\/.+)$/); + if (testsMatch) { + normalized = testsMatch[1]; + } else { + const runnerMatch = normalized.match(/\/work\/[^/]+\/[^/]+\/(.+)$/); + if (runnerMatch) { + normalized = runnerMatch[1]; + } else { + normalized = normalized.replace(/^\/+/, '').replace(/^[A-Za-z]:\//, ''); + } + } + } + + normalized = normalized.replace(/^\/+/, ''); + + const cleanPrefix = prefix?.trim().replace(/\\/g, '/').replace(/^\/+|\/+$/g, ''); + if (cleanPrefix) { + const alreadyPrefixed = + normalized === cleanPrefix || + normalized.startsWith(`${cleanPrefix}/`) || + normalized.startsWith('tests/'); + if (!alreadyPrefixed) { + normalized = `${cleanPrefix}/${normalized}`; + } + } + + return normalized; +} diff --git a/.github/actions/playwright-test-health-report/lib/report-health.test.mjs b/.github/actions/playwright-test-health-report/lib/report-health.test.mjs index a382a568..a335a57e 100644 --- a/.github/actions/playwright-test-health-report/lib/report-health.test.mjs +++ b/.github/actions/playwright-test-health-report/lib/report-health.test.mjs @@ -5,6 +5,8 @@ import { formatWatchHistory, partitionSummary, } from './classify-report-buckets.mjs'; +import { normalizeRepoFilePath } from './normalize-repo-file-path.mjs'; +import { createSlackBlocks } from './slack-test-health-blocks.mjs'; import { summarizeTestHealth } from './summarize-test-health.mjs'; describe('summarizeTestHealth', () => { @@ -98,3 +100,93 @@ describe('formatWatchHistory', () => { assert.match(text, /flaky 3\/8 runs/); }); }); + +describe('normalizeRepoFilePath', () => { + it('prefixes testDir-relative paths', () => { + assert.equal( + normalizeRepoFilePath('accounts/account-syncing-settings-toggle.spec.ts', { + prefix: 'tests/smoke-appium', + }), + 'tests/smoke-appium/accounts/account-syncing-settings-toggle.spec.ts', + ); + }); + + it('strips CI absolute checkout paths to repo-relative', () => { + assert.equal( + normalizeRepoFilePath( + '/Users/runner/work/metamask-mobile/metamask-mobile/tests/framework/config/global.setup.ts', + ), + 'tests/framework/config/global.setup.ts', + ); + }); + + it('leaves paths already under tests/ unchanged when prefix is set', () => { + assert.equal( + normalizeRepoFilePath('tests/smoke-appium/accounts/foo.spec.ts', { + prefix: 'tests/smoke-appium', + }), + 'tests/smoke-appium/accounts/foo.spec.ts', + ); + }); + + it('does not double-apply an identical prefix', () => { + assert.equal( + normalizeRepoFilePath('tests/smoke-appium/accounts/foo.spec.ts', { + prefix: 'tests/smoke-appium', + }), + 'tests/smoke-appium/accounts/foo.spec.ts', + ); + }); + + it('returns relative paths unchanged when no prefix is set', () => { + assert.equal(normalizeRepoFilePath('accounts/foo.spec.ts'), 'accounts/foo.spec.ts'); + }); +}); + +describe('createSlackBlocks path links', () => { + it('builds blob URLs with normalized repo-relative paths', () => { + const blocks = createSlackBlocks( + [ + { + name: 'toggles sync', + path: 'accounts/account-syncing-settings-toggle.spec.ts', + projectName: 'ios', + latestClassification: 'broken', + historicalBrokenCount: 1, + historicalFlakyCount: 0, + historicalInfraCount: 0, + brokenCount: 1, + flakyCount: 0, + infraCount: 0, + totalRuns: 1, + lastBrokenError: 'timeout', + lastBrokenRunUrl: 'https://github.com/MetaMask/metamask-mobile/actions/runs/1', + }, + ], + '2026-06-24', + { + owner: 'MetaMask', + repository: 'metamask-mobile', + branch: 'main', + reportTitle: 'Playwright Test Health Report', + topN: 15, + workflowsScanned: ['ci.yml'], + workflowCount: 1, + testFailureRunCount: 1, + otherFailedRunCount: 0, + lookbackDays: 1, + testSourcePrefix: 'tests/smoke-appium', + }, + ); + + const link = blocks + .flatMap(block => block.elements || []) + .flatMap(element => element.elements || []) + .find(element => element.type === 'link' && element.text === 'toggles sync'); + + assert.equal( + link?.url, + 'https://github.com/MetaMask/metamask-mobile/blob/main/tests/smoke-appium/accounts/account-syncing-settings-toggle.spec.ts', + ); + }); +}); diff --git a/.github/actions/playwright-test-health-report/lib/slack-test-health-blocks.mjs b/.github/actions/playwright-test-health-report/lib/slack-test-health-blocks.mjs index de3dbca0..3e6c0bc1 100644 --- a/.github/actions/playwright-test-health-report/lib/slack-test-health-blocks.mjs +++ b/.github/actions/playwright-test-health-report/lib/slack-test-health-blocks.mjs @@ -5,6 +5,7 @@ import { formatWatchHistory, partitionSummary, } from './classify-report-buckets.mjs'; +import { normalizeRepoFilePath } from './normalize-repo-file-path.mjs'; export function normalizeErrorForSlack(message, maxLength = 120) { if (!message) { @@ -67,8 +68,9 @@ function pushSectionHeader(blocks, emoji, title) { }); } -function pushTestLine(blocks, { index, owner, repository, branch, test, statusText, runKind }) { - const fileUrl = `https://github.com/${owner}/${repository}/blob/${branch}/${test.path}`; +function pushTestLine(blocks, { index, owner, repository, branch, test, statusText, runKind, testSourcePrefix }) { + const repoPath = normalizeRepoFilePath(test.path, { prefix: testSourcePrefix }); + const fileUrl = `https://github.com/${owner}/${repository}/blob/${branch}/${repoPath}`; const runUrl = buildRunUrl(owner, repository, test, runKind); blocks.push({ @@ -112,6 +114,7 @@ export function createSlackBlocks(summary, dateDisplay, options) { testFailureRunCount, otherFailedRunCount, lookbackDays = 1, + testSourcePrefix, } = options; const { brokenItems, flakyItems, watchItems, infraItems } = partitionSummary(summary); @@ -229,6 +232,7 @@ export function createSlackBlocks(summary, dateDisplay, options) { test, statusText: section.statusText(test), runKind: section.runKind, + testSourcePrefix, }); const error = section.error(test);