123456789export interface ProjectResult {10 name: string;11 totalTests: number;12 passed: number;13 skipped: number;14 flaky: number;15}1617export interface GateResult {18 projects: ProjectResult[];19}2021export const REQUIRED_BROWSER_PROJECTS = ["chrome-release", "firefox-release"] as const;2223export interface PlaywrightJsonReport {24 suites?: Array<{25 title?: string;26 specs?: Array<{ tests?: Array<{ status?: string; results?: Array<{ status?: string }> }> }>;27 suites?: PlaywrightJsonReport["suites"];28 }>;29}3031export function gateResultFromPlaywright(report: PlaywrightJsonReport): GateResult {32 const projects = new Map<string, ProjectResult>();33 const visit = (suites: PlaywrightJsonReport["suites"] = []) => {34 for (const suite of suites) {35 if (suite.title && REQUIRED_BROWSER_PROJECTS.includes(suite.title as (typeof REQUIRED_BROWSER_PROJECTS)[number])) {36 const project = projects.get(suite.title) ?? { name: suite.title, totalTests: 0, passed: 0, skipped: 0, flaky: 0 };37 for (const spec of suite.specs ?? []) for (const test of spec.tests ?? []) {38 project.totalTests++;39 const results = test.results ?? [];40 const status = results.at(-1)?.status ?? test.status;41 if (status === "skipped") project.skipped++;42 else if (status === "passed") project.passed++;43 if (results.length > 1 && results.some((result) => result.status === "passed")) project.flaky++;44 }45 projects.set(suite.title, project);46 }47 visit(suite.suites);48 }49 };50 visit(report.suites);51 return { projects: [...projects.values()] };52}5354export function validatePlaywrightJsonReport(report: PlaywrightJsonReport): string[] {55 return validateGateResult(gateResultFromPlaywright(report));56}5758export function formatGateErrors(errors: string[]): never {59 throw new Error(`Release browser gate validation failed:\n ${errors.join("\n ")}`);60}6162export function assertValidPlaywrightJsonReport(report: PlaywrightJsonReport): void {63 const errors = validatePlaywrightJsonReport(report);64 if (errors.length > 0) formatGateErrors(errors);65}66676869707172export function validateGateResult(73 result: GateResult,74 requiredProjects: readonly string[] = REQUIRED_BROWSER_PROJECTS,75): string[] {76 const errors: string[] = [];7778 const totalTests = result.projects.reduce((sum, p) => sum + p.totalTests, 0);79 if (totalTests === 0) {80 errors.push("zero tests discovered across all projects");81 }8283 for (const name of requiredProjects) {84 const project = result.projects.find((p) => p.name === name);85 if (!project) {86 errors.push(`required project "${name}" is missing`);87 continue;88 }89 if (project.totalTests === 0) {90 errors.push(`project "${name}" has zero tests`);91 }92 if (project.skipped > 0) {93 errors.push(`project "${name}" has ${project.skipped} skipped test(s)`);94 }95 if (project.flaky > 0) {96 errors.push(`project "${name}" has ${project.flaky} flaky retry/retr(y)ies`);97 }98 }99100 return errors;101}