Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/actions/playwright-test-health-report/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 }}
Expand DownExpand Up@@ -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
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
};
Expand DownExpand Up@@ -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');
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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;
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -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', () => {
Expand DownExpand Up@@ -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',
);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) {
Expand DownExpand Up@@ -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({
Expand DownExpand Up@@ -112,6 +114,7 @@ export function createSlackBlocks(summary, dateDisplay, options) {
testFailureRunCount,
otherFailedRunCount,
lookbackDays = 1,
testSourcePrefix,
} = options;

const { brokenItems, flakyItems, watchItems, infraItems } = partitionSummary(summary);
Expand DownExpand Up@@ -229,6 +232,7 @@ export function createSlackBlocks(summary, dateDisplay, options) {
test,
statusText: section.statusText(test),
runKind: section.runKind,
testSourcePrefix,
});

const error = section.error(test);
Expand Down