feat(merge): generate html report with links to remote resources (#22968)

This commit is contained in:
Yury Semikhatsky 2023-05-12 09:26:04 -07:00 committed by GitHub
parent f96309776c
commit 3ccec7eae5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 107 additions and 13 deletions

View file

@ -88,6 +88,8 @@ Examples:
$ npx playwright show-report playwright-report`); $ npx playwright show-report playwright-report`);
} }
const kAttachmentModes: string[] = ['local', 'missing'];
function addMergeReportsCommand(program: Command) { function addMergeReportsCommand(program: Command) {
const command = program.command('merge-reports [dir]', { hidden: true }); const command = program.command('merge-reports [dir]', { hidden: true });
command.description('merge multiple blob reports (for sharded tests) into a single report'); command.description('merge multiple blob reports (for sharded tests) into a single report');
@ -101,6 +103,7 @@ function addMergeReportsCommand(program: Command) {
}); });
command.option('-c, --config <file>', `Configuration file. Can be used to specify additional configuration for the output report.`); command.option('-c, --config <file>', `Configuration file. Can be used to specify additional configuration for the output report.`);
command.option('--reporter <reporter>', `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${defaultReporter}")`); command.option('--reporter <reporter>', `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${defaultReporter}")`);
command.option('--attachments <mode>', `Whether the attachments are available locally. Supported values are ${kAttachmentModes.map(name => `"${name}"`).join(', ')} (default: "local")`);
command.addHelpText('afterAll', ` command.addHelpText('afterAll', `
Arguments [dir]: Arguments [dir]:
Directory containing blob reports. Directory containing blob reports.
@ -197,7 +200,12 @@ async function mergeReports(reportDir: string | undefined, opts: { [key: string]
reporterDescriptions = config.config.reporter; reporterDescriptions = config.config.reporter;
if (!reporterDescriptions) if (!reporterDescriptions)
reporterDescriptions = [[defaultReporter]]; reporterDescriptions = [[defaultReporter]];
await createMergedReport(config, dir, reporterDescriptions!); if (opts.attachments) {
if (!kAttachmentModes.includes(opts.attachments))
throw new Error(`Invalid --attachments value "${opts.attachments}", must be one of ${kAttachmentModes.map(name => `"${name}"`).join(', ')}.`);
}
const resolveAttachmentPaths = opts.attachments !== 'missing';
await createMergedReport(config, dir, reporterDescriptions!, resolveAttachmentPaths);
} }
function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides { function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides {

View file

@ -37,8 +37,6 @@ type TestEntry = {
testCaseSummary: TestCaseSummary testCaseSummary: TestCaseSummary
}; };
const kMissingContentType = 'x-playwright/missing';
type HtmlReportOpenOption = 'always' | 'never' | 'on-failure'; type HtmlReportOpenOption = 'always' | 'never' | 'on-failure';
type HtmlReporterOptions = { type HtmlReporterOptions = {
configDir: string, configDir: string,
@ -397,11 +395,6 @@ class HtmlBuilder {
fs.mkdirSync(path.join(this._reportFolder, 'data'), { recursive: true }); fs.mkdirSync(path.join(this._reportFolder, 'data'), { recursive: true });
fs.writeFileSync(path.join(this._reportFolder, 'data', sha1), buffer); fs.writeFileSync(path.join(this._reportFolder, 'data', sha1), buffer);
} catch (e) { } catch (e) {
return {
name: `Missing attachment "${a.name}"`,
contentType: kMissingContentType,
body: `Attachment file ${fileName} is missing`,
};
} }
return { return {
name: a.name, name: a.name,

View file

@ -23,10 +23,11 @@ import { TeleReporterReceiver, type JsonEvent, type JsonProject, type JsonSuite,
import { createReporters } from '../runner/reporters'; import { createReporters } from '../runner/reporters';
import { Multiplexer } from './multiplexer'; import { Multiplexer } from './multiplexer';
export async function createMergedReport(config: FullConfigInternal, dir: string, reporterDescriptions: ReporterDescription[]) { export async function createMergedReport(config: FullConfigInternal, dir: string, reporterDescriptions: ReporterDescription[], resolvePaths: boolean) {
const shardFiles = await sortedShardFiles(dir); const shardFiles = await sortedShardFiles(dir);
const events = await mergeEvents(dir, shardFiles); const events = await mergeEvents(dir, shardFiles);
patchAttachmentPaths(events, dir); if (resolvePaths)
patchAttachmentPaths(events, dir);
const reporters = await createReporters(config, 'merge', reporterDescriptions); const reporters = await createReporters(config, 'merge', reporterDescriptions);
const receiver = new TeleReporterReceiver(path.sep, new Multiplexer(reporters)); const receiver = new TeleReporterReceiver(path.sep, new Multiplexer(reporters));

View file

@ -15,6 +15,8 @@
*/ */
import * as fs from 'fs'; import * as fs from 'fs';
import path from 'path';
import url from 'url';
import type { HttpServer } from '../../packages/playwright-core/src/utils'; import type { HttpServer } from '../../packages/playwright-core/src/utils';
import { startHtmlReportServer } from '../../packages/playwright-test/lib/reporters/html'; import { startHtmlReportServer } from '../../packages/playwright-test/lib/reporters/html';
import { type CliRunResult, type RunOptions, stripAnsi } from './playwright-test-fixtures'; import { type CliRunResult, type RunOptions, stripAnsi } from './playwright-test-fixtures';
@ -405,14 +407,106 @@ test('preserve attachments', async ({ runInlineTest, mergeReports, showReport, p
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
await showReport(); await showReport();
// Check file attachment.
await page.getByText('first').click(); await page.getByText('first').click();
await expect(page.getByText('file-attachment')).toBeVisible(); await expect(page.getByText('file-attachment')).toBeVisible();
// Check file attachment content.
const popupPromise = page.waitForEvent('popup');
await page.getByText('file-attachment').click();
const popup = await popupPromise;
await expect(popup.locator('body')).toHaveText('hello!');
await popup.close();
await page.goBack(); await page.goBack();
await page.getByText('failing 1').click(); await page.getByText('failing 1').click();
await expect(page.getByText('\'text-attachment\', { body: \'hi!\'')).toBeVisible(); await expect(page.getByText('\'text-attachment\', { body: \'hi!\'')).toBeVisible();
}); });
test('generate html with attachment urls', async ({ runInlineTest, mergeReports, page, server }) => {
test.slow();
const reportDir = test.info().outputPath('blob-report');
const files = {
'playwright.config.ts': `
module.exports = {
retries: 1,
use: {
trace: 'on'
},
reporter: [['blob', { outputDir: '${reportDir.replace(/\\/g, '/')}' }]]
};
`,
'a.test.js': `
import { test, expect } from '@playwright/test';
import fs from 'fs';
test('first', async ({}) => {
const attachmentPath = test.info().outputPath('foo.txt');
fs.writeFileSync(attachmentPath, 'hello!');
await test.info().attach('file-attachment', { path: attachmentPath });
console.log('console info');
console.error('console error');
});
test('failing 1', async ({}) => {
await test.info().attach('text-attachment', { body: 'hi!' });
expect(1).toBe(2);
});
test.skip('skipped 1', async ({}) => {});
`,
'b.test.js': `
import { test, expect } from '@playwright/test';
test('math 2', async ({}) => { });
test('failing 2', async ({}) => {
expect(1).toBe(2);
});
test.skip('skipped 2', async ({}) => {});
`
};
await runInlineTest(files, { shard: `1/2` });
const reportFiles = await fs.promises.readdir(reportDir);
reportFiles.sort();
expect(reportFiles).toEqual([expect.stringMatching(/report-1-of-2.*.jsonl/), 'resources']);
const { exitCode } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html', '--attachments', 'missing'] });
expect(exitCode).toBe(0);
const htmlReportDir = test.info().outputPath('playwright-report');
for (const entry of await fs.promises.readdir(htmlReportDir))
await (fs.promises as any).cp(path.join(htmlReportDir, entry), path.join(reportDir, entry), { recursive: true });
const oldSeveFile = server.serveFile;
server.serveFile = async (req, res) => {
const pathName = url.parse(req.url!).pathname!;
const filePath = path.join(reportDir, pathName.substring(1));
return oldSeveFile.call(server, req, res, filePath);
};
// Check file attachment.
await page.goto(`${server.PREFIX}/index.html`);
await page.getByText('first').click();
await expect(page.getByText('file-attachment')).toBeVisible();
// Check file attachment content.
const popupPromise = page.waitForEvent('popup');
await page.getByText('file-attachment').click();
const popup = await popupPromise;
await expect(popup.locator('body')).toHaveText('hello!');
await popup.close();
await page.goBack();
// Check inline attachment.
await page.getByText('failing 1').click();
await expect(page.getByText('\'text-attachment\', { body: \'hi!\'')).toBeVisible();
await page.goBack();
// Check that trace loads.
await page.locator('div').filter({ hasText: /^a\.test\.js:13$/ }).getByRole('link', { name: 'View trace' }).click();
await expect(page).toHaveTitle('Playwright Trace Viewer');
await expect(page.getByTestId('action-list').locator('div').filter({ hasText: /^expect\.toBe$/ })).toBeVisible();
});
test('multiple output reports', async ({ runInlineTest, mergeReports, showReport, page }) => { test('multiple output reports', async ({ runInlineTest, mergeReports, showReport, page }) => {
test.slow(); test.slow();
const reportDir = test.info().outputPath('blob-report'); const reportDir = test.info().outputPath('blob-report');

View file

@ -98,9 +98,7 @@ test('should not throw when attachment is missing', async ({ runInlineTest, page
await showReport(); await showReport();
await page.click('text=passes'); await page.click('text=passes');
await page.locator('text=Missing attachment "screenshot"').click(); await expect(page.getByRole('link', { name: 'screenshot' })).toBeVisible();
const screenshotFile = testInfo.outputPath('test-results', 'a-passes', 'screenshot.png');
await expect(page.locator('.attachment-body')).toHaveText(`Attachment file ${screenshotFile} is missing`);
}); });
test('should include image diff', async ({ runInlineTest, page, showReport }) => { test('should include image diff', async ({ runInlineTest, page, showReport }) => {