diff --git a/packages/playwright-test/src/loader.ts b/packages/playwright-test/src/loader.ts index 4d73224cfd..7b03bcd7fb 100644 --- a/packages/playwright-test/src/loader.ts +++ b/packages/playwright-test/src/loader.ts @@ -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(); - -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) diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts index 353473e4b7..3e9cae1067 100644 --- a/packages/playwright-test/src/reporters/html.ts +++ b/packages/playwright-test/src/reporters/html.ts @@ -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(); } diff --git a/packages/playwright-test/src/util.ts b/packages/playwright-test/src/util.ts index 71bcd27283..61d37b2e7e 100644 --- a/packages/playwright-test/src/util.ts +++ b/packages/playwright-test/src/util.ts @@ -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(); + +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; +} + diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index e7cc5e312e..d73453b5c7 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -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': `