diff --git a/packages/html-reporter/src/headerView.css b/packages/html-reporter/src/headerView.css index ef29c442ca..0ab2c3d839 100644 --- a/packages/html-reporter/src/headerView.css +++ b/packages/html-reporter/src/headerView.css @@ -30,3 +30,7 @@ border-right: none; } } + +.header-view-status-line { + padding-right: '10px' +} diff --git a/packages/html-reporter/src/headerView.tsx b/packages/html-reporter/src/headerView.tsx index cd5a252d66..ef8519e66a 100644 --- a/packages/html-reporter/src/headerView.tsx +++ b/packages/html-reporter/src/headerView.tsx @@ -29,7 +29,8 @@ export const HeaderView: React.FC void, projectNames: string[], -}>> = ({ stats, filterText, setFilterText, projectNames }) => { + reportLoaderError?: string, +}>> = ({ stats, filterText, setFilterText, projectNames, reportLoaderError }) => { React.useEffect(() => { (async () => { window.addEventListener('popstate', () => { @@ -57,9 +58,10 @@ export const HeaderView: React.FC -
+ {reportLoaderError &&
{reportLoaderError}
} +
{projectNames.length === 1 && Project: {projectNames[0]}} - Total time: {msToString(stats.duration)} + Total time: {msToString(stats.duration)}
); }; diff --git a/packages/html-reporter/src/index.tsx b/packages/html-reporter/src/index.tsx index 1f018966e1..65a9fbecda 100644 --- a/packages/html-reporter/src/index.tsx +++ b/packages/html-reporter/src/index.tsx @@ -49,6 +49,7 @@ window.onload = () => { class ZipReport implements LoadedReport { private _entries = new Map(); private _json!: HTMLReport; + private _loaderError: string | undefined; async loadFromBase64(reportBase64: string) { const zipReader = new zipjs.ZipReader(new zipjs.Data64URIReader(reportBase64), { useWebWorkers: false }) as zip.ZipReader; @@ -62,9 +63,17 @@ class ZipReport implements LoadedReport { const paddedNumber = String(i + 1).padStart(paddedLen, '0'); const fileName = `report-${paddedNumber}-of-${shardTotal}.zip`; const zipReader = new zipjs.ZipReader(new zipjs.HttpReader(fileName), { useWebWorkers: false }) as zip.ZipReader; - readers.push(this._readReportAndTestEntries(zipReader)); + readers.push(this._readReportAndTestEntries(zipReader).catch(e => { + // eslint-disable-next-line no-console + console.warn(e); + return undefined; + })); } - this._json = mergeReports(await Promise.all(readers)); + const reportsOrErrors = await Promise.all(readers); + const reports = reportsOrErrors.filter(Boolean) as HTMLReport[]; + if (reports.length < readers.length) + this._loaderError = `Only ${reports.length} of ${shardTotal} report shards loaded`; + this._json = mergeReports(reports); } private async _readReportAndTestEntries(zipReader: zip.ZipReader): Promise { @@ -83,5 +92,9 @@ class ZipReport implements LoadedReport { await reportEntry!.getData!(writer); return JSON.parse(await writer.getData()); } + + loaderError(): string | undefined { + return this._loaderError; + } } diff --git a/packages/html-reporter/src/loadedReport.ts b/packages/html-reporter/src/loadedReport.ts index c1ff2d7530..44d278ef31 100644 --- a/packages/html-reporter/src/loadedReport.ts +++ b/packages/html-reporter/src/loadedReport.ts @@ -19,4 +19,5 @@ import type { HTMLReport } from './types'; export interface LoadedReport { json(): HTMLReport; entry(name: string): Promise; + loaderError(): string | undefined; } diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx index 49eba20eea..00cb770302 100644 --- a/packages/html-reporter/src/reportView.tsx +++ b/packages/html-reporter/src/reportView.tsx @@ -51,7 +51,7 @@ export const ReportView: React.FC<{ return
- {report?.json() && } + {report?.json() && } {report?.json().metadata && } diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 923551430b..733aeaff9d 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -1039,3 +1039,54 @@ test('should pad report numbers with zeros', async ({ runInlineTest, showReport, `report-003-of-100.zip` ])); }); + +test('should show report with missing shards', async ({ runInlineTest, showReport, page }, testInfo) => { + const totalShards = 15; + + const testFiles = {}; + for (let i = 0; i < totalShards; i++) { + testFiles[`a-${String(i).padStart(2, '0')}.spec.ts`] = ` + const { test } = pwt; + test('passes', async ({}) => { expect(2).toBe(2); }); + test('fails', async ({}) => { expect(1).toBe(2); }); + test('skipped', async ({}) => { test.skip('Does not work') }); + test('flaky', async ({}, testInfo) => { expect(testInfo.retry).toBe(1); }); + `; + } + + const allReports = testInfo.outputPath(`aggregated-report`); + await fs.promises.mkdir(allReports, { recursive: true }); + + // Run tests in 2 out of 15 shards. + for (const i of [10, 13]) { + const result = await runInlineTest(testFiles, + { 'reporter': 'dot,html', 'retries': 1, 'shard': `${i}/${totalShards}` }, + { PW_TEST_HTML_REPORT_OPEN: 'never' }, + { usesCustomReporters: true }); + + + expect(result.exitCode).toBe(1); + const files = await fs.promises.readdir(testInfo.outputPath(`playwright-report`)); + expect(new Set(files)).toEqual(new Set([ + 'index.html', + `report-${i}-of-${totalShards}.zip` + ])); + await Promise.all(files.map(name => fs.promises.rename(testInfo.outputPath(`playwright-report/${name}`), `${allReports}/${name}`))); + } + + // Show aggregated report + await showReport(allReports); + + await expect(page.getByText('Only 2 of 15 report shards loaded')).toBeVisible(); + + await expect(page.locator('.subnav-item:has-text("All") .counter')).toHaveText('8'); + await expect(page.locator('.subnav-item:has-text("Passed") .counter')).toHaveText('2'); + await expect(page.locator('.subnav-item:has-text("Failed") .counter')).toHaveText('2'); + await expect(page.locator('.subnav-item:has-text("Flaky") .counter')).toHaveText('2'); + await expect(page.locator('.subnav-item:has-text("Skipped") .counter')).toHaveText('2'); + + await expect(page.locator('.test-file-test-outcome-unexpected >> text=fails')).toHaveCount(2); + await expect(page.locator('.test-file-test-outcome-flaky >> text=flaky')).toHaveCount(2); + await expect(page.locator('.test-file-test-outcome-expected >> text=passes')).toHaveCount(2); + await expect(page.locator('.test-file-test-outcome-skipped >> text=skipped')).toHaveCount(2); +});