fix(html): put HTML report next to package.json by default (#13141)

Fixes #12970
This commit is contained in:
Andrey Lushnikov 2022-03-29 15:19:31 -06:00 committed by GitHub
parent de0a457856
commit aa1daeba85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 97 additions and 46 deletions

View File

@ -17,7 +17,7 @@
import { installTransform, setCurrentlyLoadingTestFile } from './transform';
import type { Config, FullProject, Project, ReporterDescription, PreserveOutput } from './types';
import type { FullConfigInternal } from './types';
import { mergeObjects, errorWithFile } from './util';
import { getPackageJsonPath, mergeObjects, errorWithFile } from './util';
import { setCurrentlyLoadingFileSuite } from './globals';
import { Suite } from './test';
import { SerializedLoaderData } from './ipc';
@ -99,6 +99,7 @@ export class Loader {
const configUse = mergeObjects(this._defaultConfig.use, config.use);
config = mergeObjects(mergeObjects(this._defaultConfig, config), { use: configUse });
(this._fullConfig as any).__configDir = configDir;
this._fullConfig.rootDir = config.testDir || this._configDir;
this._fullConfig.forbidOnly = takeFirst(this._configOverrides.forbidOnly, config.forbidOnly, baseFullConfig.forbidOnly);
this._fullConfig.fullyParallel = takeFirst(this._configOverrides.fullyParallel, config.fullyParallel, baseFullConfig.fullyParallel);
@ -498,30 +499,6 @@ export function fileIsModule(file: string): boolean {
return folderIsModule(folder);
}
const folderToPackageJsonPath = new Map<string, string>();
function getPackageJsonPath(folderPath: string): string {
const cached = folderToPackageJsonPath.get(folderPath);
if (cached !== undefined)
return cached;
const packageJsonPath = path.join(folderPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
folderToPackageJsonPath.set(folderPath, packageJsonPath);
return packageJsonPath;
}
const parentFolder = path.dirname(folderPath);
if (folderPath === parentFolder) {
folderToPackageJsonPath.set(folderPath, '');
return '';
}
const result = getPackageJsonPath(parentFolder);
folderToPackageJsonPath.set(folderPath, result);
return result;
}
export function folderIsModule(folder: string): boolean {
const packageJsonPath = getPackageJsonPath(folder);
if (!packageJsonPath)

View File

@ -26,6 +26,7 @@ import RawReporter, { JsonAttachment, JsonReport, JsonSuite, JsonTestCase, JsonT
import assert from 'assert';
import yazl from 'yazl';
import { stripAnsiEscapes } from './base';
import { getPackageJsonPath } from '../util';
export type Stats = {
total: number;
@ -115,16 +116,19 @@ type TestEntry = {
const kMissingContentType = 'x-playwright/missing';
type HtmlReportOpenOption = 'always' | 'never' | 'on-failure';
type HtmlReporterOptions = {
outputFolder?: string,
open?: HtmlReportOpenOption,
};
class HtmlReporter implements Reporter {
private config!: FullConfig;
private suite!: Suite;
private _outputFolder: string | undefined;
private _open: 'always' | 'never' | 'on-failure';
private _options: HtmlReporterOptions;
constructor(options: { outputFolder?: string, open?: 'always' | 'never' | 'on-failure' } = {}) {
// TODO: resolve relative to config.
this._outputFolder = options.outputFolder;
this._open = process.env.PW_TEST_HTML_REPORT_OPEN as any || options.open || 'on-failure';
constructor(options: HtmlReporterOptions = {}) {
this._options = options;
}
printsToStdio() {
@ -136,49 +140,68 @@ class HtmlReporter implements Reporter {
this.suite = suite;
}
_resolveOptions(): { outputFolder: string, open: HtmlReportOpenOption } {
let { outputFolder } = this._options;
const configDir: string = (this.config as any).__configDir;
if (outputFolder)
outputFolder = path.resolve(configDir, outputFolder);
return {
outputFolder: reportFolderFromEnv() ?? outputFolder ?? defaultReportFolder(configDir),
open: process.env.PW_TEST_HTML_REPORT_OPEN as any || this._options.open || 'on-failure',
};
}
async onEnd() {
const { open, outputFolder } = this._resolveOptions();
const projectSuites = this.suite.suites;
const reports = projectSuites.map(suite => {
const rawReporter = new RawReporter();
const report = rawReporter.generateProjectReport(this.config, suite);
return report;
});
const reportFolder = htmlReportFolder(this._outputFolder);
await removeFolders([reportFolder]);
const builder = new HtmlBuilder(reportFolder);
await removeFolders([outputFolder]);
const builder = new HtmlBuilder(outputFolder);
const { ok, singleTestId } = await builder.build(new RawReporter().generateAttachments(this.config), reports);
if (process.env.CI)
return;
const shouldOpen = this._open === 'always' || (!ok && this._open === 'on-failure');
const shouldOpen = open === 'always' || (!ok && open === 'on-failure');
if (shouldOpen) {
await showHTMLReport(reportFolder, singleTestId);
await showHTMLReport(outputFolder, singleTestId);
} else {
const outputFolderPath = htmlReportFolder(this._outputFolder) === defaultReportFolder() ? '' : ' ' + path.relative(process.cwd(), htmlReportFolder(this._outputFolder));
const relativeReportPath = outputFolder === standaloneDefaultFolder() ? '' : ' ' + path.relative(process.cwd(), outputFolder);
console.log('');
console.log('To open last HTML report run:');
console.log(colors.cyan(`
npx playwright show-report${outputFolderPath}
npx playwright show-report${relativeReportPath}
`));
}
}
}
export function htmlReportFolder(outputFolder?: string): string {
function reportFolderFromEnv(): string | undefined {
if (process.env[`PLAYWRIGHT_HTML_REPORT`])
return path.resolve(process.cwd(), process.env[`PLAYWRIGHT_HTML_REPORT`]);
if (outputFolder)
return outputFolder;
return defaultReportFolder();
return undefined;
}
function defaultReportFolder(): string {
return path.resolve(process.cwd(), 'playwright-report');
function defaultReportFolder(searchForPackageJson: string): string {
let basePath = getPackageJsonPath(searchForPackageJson);
if (basePath)
basePath = path.dirname(basePath);
else
basePath = process.cwd();
return path.resolve(basePath, 'playwright-report');
}
function standaloneDefaultFolder(): string {
return reportFolderFromEnv() ?? defaultReportFolder(process.cwd());
}
export async function showHTMLReport(reportFolder: string | undefined, testId?: string) {
const folder = reportFolder || htmlReportFolder();
const folder = reportFolder ?? standaloneDefaultFolder();
try {
assert(fs.statSync(folder).isDirectory());
} catch (e) {
@ -224,7 +247,7 @@ class HtmlBuilder {
private _hasTraces = false;
constructor(outputDir: string) {
this._reportFolder = path.resolve(process.cwd(), outputDir);
this._reportFolder = outputDir;
fs.mkdirSync(this._reportFolder, { recursive: true });
this._dataZipFile = new yazl.ZipFile();
}

View File

@ -15,6 +15,7 @@
*/
import util from 'util';
import fs from 'fs';
import path from 'path';
import url from 'url';
import colors from 'colors/safe';
@ -253,3 +254,27 @@ export function currentExpectTimeout(options: { timeout?: number }) {
return defaultExpectTimeout;
}
const folderToPackageJsonPath = new Map<string, string>();
export function getPackageJsonPath(folderPath: string): string {
const cached = folderToPackageJsonPath.get(folderPath);
if (cached !== undefined)
return cached;
const packageJsonPath = path.join(folderPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
folderToPackageJsonPath.set(folderPath, packageJsonPath);
return packageJsonPath;
}
const parentFolder = path.dirname(folderPath);
if (folderPath === parentFolder) {
folderToPackageJsonPath.set(folderPath, '');
return '';
}
const result = getPackageJsonPath(parentFolder);
folderToPackageJsonPath.set(folderPath, result);
return result;
}

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import fs from 'fs';
import { test as baseTest, expect, createImage } from './playwright-test-fixtures';
import { HttpServer } from '../../packages/playwright-core/lib/utils/httpServer';
import { startHtmlReportServer } from '../../packages/playwright-test/lib/reporters/html';
@ -70,6 +71,31 @@ test('should generate report', async ({ runInlineTest, showReport, page }) => {
await expect(page.locator('.metadata-view')).not.toBeVisible();
});
test('should generate report wrt package.json', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'foo/package.json': `{ "name": "foo" }`,
'foo/bar/playwright.config.js': `
module.exports = { projects: [ {} ] };
`,
'foo/bar/baz/tests/a.spec.js': `
const { test } = pwt;
const fs = require('fs');
test('pass', ({}, testInfo) => {
});
`
}, { 'reporter': 'html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }, {
cwd: 'foo/bar/baz/tests',
usesCustomOutputDir: true
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
expect(fs.existsSync(testInfo.outputPath('playwright-report'))).toBe(false);
expect(fs.existsSync(testInfo.outputPath('foo', 'playwright-report'))).toBe(true);
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'playwright-report'))).toBe(false);
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'tests', 'playwright-report'))).toBe(false);
});
test('should not throw when attachment is missing', async ({ runInlineTest, page, showReport }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `