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"] as const;2223export interface PlaywrightJsonReport {24 suites?: Array<{25 title?: string;26 specs?: Array<{27 tests?: Array<{28 projectName?: string;29 status?: string;30 results?: Array<{ status?: string }>;31 }>;32 }>;33 suites?: PlaywrightJsonReport["suites"];34 }>;35}3637export function gateResultFromPlaywright(report: PlaywrightJsonReport): GateResult {38 const projects = new Map<string, ProjectResult>();39 const visit = (suites: PlaywrightJsonReport["suites"] = []) => {40 for (const suite of suites) {41 for (const spec of suite.specs ?? []) {42 for (const test of spec.tests ?? []) {43 const projectName = test.projectName;44 if (!projectName || !REQUIRED_BROWSER_PROJECTS.includes(projectName as (typeof REQUIRED_BROWSER_PROJECTS)[number])) {45 continue;46 }47 const project = projects.get(projectName) ?? {48 name: projectName,49 totalTests: 0,50 passed: 0,51 skipped: 0,52 flaky: 0,53 };54 project.totalTests++;55 const results = test.results ?? [];56 const status = results.at(-1)?.status ?? test.status;57 if (status === "skipped") project.skipped++;58 else if (status === "passed") project.passed++;59 if (results.length > 1 && results.some((result) => result.status === "passed")) project.flaky++;60 projects.set(projectName, project);61 }62 }63 visit(suite.suites);64 }65 };66 visit(report.suites);67 return { projects: [...projects.values()] };68}6970export function validatePlaywrightJsonReport(report: PlaywrightJsonReport): string[] {71 return validateGateResult(gateResultFromPlaywright(report));72}7374export function formatGateErrors(errors: string[]): never {75 throw new Error(`Release browser gate validation failed:\n ${errors.join("\n ")}`);76}7778export function assertValidPlaywrightJsonReport(report: PlaywrightJsonReport): void {79 const errors = validatePlaywrightJsonReport(report);80 if (errors.length > 0) formatGateErrors(errors);81}82838485868788export function validateGateResult(89 result: GateResult,90 requiredProjects: readonly string[] = REQUIRED_BROWSER_PROJECTS,91): string[] {92 const errors: string[] = [];9394 const totalTests = result.projects.reduce((sum, p) => sum + p.totalTests, 0);95 if (totalTests === 0) {96 errors.push("zero tests discovered across all projects");97 }9899 for (const name of requiredProjects) {100 const project = result.projects.find((p) => p.name === name);101 if (!project) {102 errors.push(`required project "${name}" is missing`);103 continue;104 }105 if (project.totalTests === 0) {106 errors.push(`project "${name}" has zero tests`);107 }108 if (project.skipped > 0) {109 errors.push(`project "${name}" has ${project.skipped} skipped test(s)`);110 }111 if (project.flaky > 0) {112 errors.push(`project "${name}" has ${project.flaky} flaky retry/retr(y)ies`);113 }114 if (project.passed !== project.totalTests) {115 errors.push(`project "${name}" has ${project.totalTests - project.passed} test(s) that did not pass`);116 }117 }118119 return errors;120}