fix(html): put HTML report next to package.json by default (#13141)
Fixes #12970
This commit is contained in:
parent
de0a457856
commit
aa1daeba85
|
|
@ -17,7 +17,7 @@
|
||||||
import { installTransform, setCurrentlyLoadingTestFile } from './transform';
|
import { installTransform, setCurrentlyLoadingTestFile } from './transform';
|
||||||
import type { Config, FullProject, Project, ReporterDescription, PreserveOutput } from './types';
|
import type { Config, FullProject, Project, ReporterDescription, PreserveOutput } from './types';
|
||||||
import type { FullConfigInternal } from './types';
|
import type { FullConfigInternal } from './types';
|
||||||
import { mergeObjects, errorWithFile } from './util';
|
import { getPackageJsonPath, mergeObjects, errorWithFile } from './util';
|
||||||
import { setCurrentlyLoadingFileSuite } from './globals';
|
import { setCurrentlyLoadingFileSuite } from './globals';
|
||||||
import { Suite } from './test';
|
import { Suite } from './test';
|
||||||
import { SerializedLoaderData } from './ipc';
|
import { SerializedLoaderData } from './ipc';
|
||||||
|
|
@ -99,6 +99,7 @@ export class Loader {
|
||||||
const configUse = mergeObjects(this._defaultConfig.use, config.use);
|
const configUse = mergeObjects(this._defaultConfig.use, config.use);
|
||||||
config = mergeObjects(mergeObjects(this._defaultConfig, config), { use: configUse });
|
config = mergeObjects(mergeObjects(this._defaultConfig, config), { use: configUse });
|
||||||
|
|
||||||
|
(this._fullConfig as any).__configDir = configDir;
|
||||||
this._fullConfig.rootDir = config.testDir || this._configDir;
|
this._fullConfig.rootDir = config.testDir || this._configDir;
|
||||||
this._fullConfig.forbidOnly = takeFirst(this._configOverrides.forbidOnly, config.forbidOnly, baseFullConfig.forbidOnly);
|
this._fullConfig.forbidOnly = takeFirst(this._configOverrides.forbidOnly, config.forbidOnly, baseFullConfig.forbidOnly);
|
||||||
this._fullConfig.fullyParallel = takeFirst(this._configOverrides.fullyParallel, config.fullyParallel, baseFullConfig.fullyParallel);
|
this._fullConfig.fullyParallel = takeFirst(this._configOverrides.fullyParallel, config.fullyParallel, baseFullConfig.fullyParallel);
|
||||||
|
|
@ -498,30 +499,6 @@ export function fileIsModule(file: string): boolean {
|
||||||
return folderIsModule(folder);
|
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 {
|
export function folderIsModule(folder: string): boolean {
|
||||||
const packageJsonPath = getPackageJsonPath(folder);
|
const packageJsonPath = getPackageJsonPath(folder);
|
||||||
if (!packageJsonPath)
|
if (!packageJsonPath)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import RawReporter, { JsonAttachment, JsonReport, JsonSuite, JsonTestCase, JsonT
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import yazl from 'yazl';
|
import yazl from 'yazl';
|
||||||
import { stripAnsiEscapes } from './base';
|
import { stripAnsiEscapes } from './base';
|
||||||
|
import { getPackageJsonPath } from '../util';
|
||||||
|
|
||||||
export type Stats = {
|
export type Stats = {
|
||||||
total: number;
|
total: number;
|
||||||
|
|
@ -115,16 +116,19 @@ type TestEntry = {
|
||||||
|
|
||||||
const kMissingContentType = 'x-playwright/missing';
|
const kMissingContentType = 'x-playwright/missing';
|
||||||
|
|
||||||
|
type HtmlReportOpenOption = 'always' | 'never' | 'on-failure';
|
||||||
|
type HtmlReporterOptions = {
|
||||||
|
outputFolder?: string,
|
||||||
|
open?: HtmlReportOpenOption,
|
||||||
|
};
|
||||||
|
|
||||||
class HtmlReporter implements Reporter {
|
class HtmlReporter implements Reporter {
|
||||||
private config!: FullConfig;
|
private config!: FullConfig;
|
||||||
private suite!: Suite;
|
private suite!: Suite;
|
||||||
private _outputFolder: string | undefined;
|
private _options: HtmlReporterOptions;
|
||||||
private _open: 'always' | 'never' | 'on-failure';
|
|
||||||
|
|
||||||
constructor(options: { outputFolder?: string, open?: 'always' | 'never' | 'on-failure' } = {}) {
|
constructor(options: HtmlReporterOptions = {}) {
|
||||||
// TODO: resolve relative to config.
|
this._options = options;
|
||||||
this._outputFolder = options.outputFolder;
|
|
||||||
this._open = process.env.PW_TEST_HTML_REPORT_OPEN as any || options.open || 'on-failure';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
printsToStdio() {
|
printsToStdio() {
|
||||||
|
|
@ -136,49 +140,68 @@ class HtmlReporter implements Reporter {
|
||||||
this.suite = suite;
|
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() {
|
async onEnd() {
|
||||||
|
const { open, outputFolder } = this._resolveOptions();
|
||||||
const projectSuites = this.suite.suites;
|
const projectSuites = this.suite.suites;
|
||||||
const reports = projectSuites.map(suite => {
|
const reports = projectSuites.map(suite => {
|
||||||
const rawReporter = new RawReporter();
|
const rawReporter = new RawReporter();
|
||||||
const report = rawReporter.generateProjectReport(this.config, suite);
|
const report = rawReporter.generateProjectReport(this.config, suite);
|
||||||
return report;
|
return report;
|
||||||
});
|
});
|
||||||
const reportFolder = htmlReportFolder(this._outputFolder);
|
await removeFolders([outputFolder]);
|
||||||
await removeFolders([reportFolder]);
|
const builder = new HtmlBuilder(outputFolder);
|
||||||
const builder = new HtmlBuilder(reportFolder);
|
|
||||||
const { ok, singleTestId } = await builder.build(new RawReporter().generateAttachments(this.config), reports);
|
const { ok, singleTestId } = await builder.build(new RawReporter().generateAttachments(this.config), reports);
|
||||||
|
|
||||||
if (process.env.CI)
|
if (process.env.CI)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const shouldOpen = this._open === 'always' || (!ok && this._open === 'on-failure');
|
|
||||||
|
const shouldOpen = open === 'always' || (!ok && open === 'on-failure');
|
||||||
if (shouldOpen) {
|
if (shouldOpen) {
|
||||||
await showHTMLReport(reportFolder, singleTestId);
|
await showHTMLReport(outputFolder, singleTestId);
|
||||||
} else {
|
} 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('');
|
||||||
console.log('To open last HTML report run:');
|
console.log('To open last HTML report run:');
|
||||||
console.log(colors.cyan(`
|
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`])
|
if (process.env[`PLAYWRIGHT_HTML_REPORT`])
|
||||||
return path.resolve(process.cwd(), process.env[`PLAYWRIGHT_HTML_REPORT`]);
|
return path.resolve(process.cwd(), process.env[`PLAYWRIGHT_HTML_REPORT`]);
|
||||||
if (outputFolder)
|
return undefined;
|
||||||
return outputFolder;
|
|
||||||
return defaultReportFolder();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultReportFolder(): string {
|
function defaultReportFolder(searchForPackageJson: string): string {
|
||||||
return path.resolve(process.cwd(), 'playwright-report');
|
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) {
|
export async function showHTMLReport(reportFolder: string | undefined, testId?: string) {
|
||||||
const folder = reportFolder || htmlReportFolder();
|
const folder = reportFolder ?? standaloneDefaultFolder();
|
||||||
try {
|
try {
|
||||||
assert(fs.statSync(folder).isDirectory());
|
assert(fs.statSync(folder).isDirectory());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -224,7 +247,7 @@ class HtmlBuilder {
|
||||||
private _hasTraces = false;
|
private _hasTraces = false;
|
||||||
|
|
||||||
constructor(outputDir: string) {
|
constructor(outputDir: string) {
|
||||||
this._reportFolder = path.resolve(process.cwd(), outputDir);
|
this._reportFolder = outputDir;
|
||||||
fs.mkdirSync(this._reportFolder, { recursive: true });
|
fs.mkdirSync(this._reportFolder, { recursive: true });
|
||||||
this._dataZipFile = new yazl.ZipFile();
|
this._dataZipFile = new yazl.ZipFile();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import colors from 'colors/safe';
|
import colors from 'colors/safe';
|
||||||
|
|
@ -253,3 +254,27 @@ export function currentExpectTimeout(options: { timeout?: number }) {
|
||||||
return defaultExpectTimeout;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
import { test as baseTest, expect, createImage } from './playwright-test-fixtures';
|
import { test as baseTest, expect, createImage } from './playwright-test-fixtures';
|
||||||
import { HttpServer } from '../../packages/playwright-core/lib/utils/httpServer';
|
import { HttpServer } from '../../packages/playwright-core/lib/utils/httpServer';
|
||||||
import { startHtmlReportServer } from '../../packages/playwright-test/lib/reporters/html';
|
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();
|
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) => {
|
test('should not throw when attachment is missing', async ({ runInlineTest, page, showReport }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue