diff --git a/package-lock.json b/package-lock.json index f106b0f9fd..6cfdd18c7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "mime": "^2.4.6", "minimatch": "^3.0.3", "ms": "^2.1.2", + "open": "^8.2.1", "pirates": "^4.0.1", "pixelmatch": "^5.2.1", "pngjs": "^5.0.0", @@ -3829,6 +3830,14 @@ "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", "dev": true }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -6157,6 +6166,20 @@ "node": ">=0.10.0" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -7421,6 +7444,33 @@ "wrappy": "1" } }, + "node_modules/open": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.2.1.tgz", + "integrity": "sha512-rXILpcQlkF/QuFez2BJDf3GsqpjGKbkUUToAIGo9A0Q6ZkoSGogZJulrUdwRkrAsoQvoZsrjCYt8+zblOk7JQQ==", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -8894,7 +8944,6 @@ }, "node_modules/socksv5/node_modules/ipv6": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/ipv6/-/ipv6-3.1.1.tgz", "dev": true, "inBundle": true, "license": "MIT", @@ -8913,7 +8962,6 @@ }, "node_modules/socksv5/node_modules/ipv6/node_modules/sprintf": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/sprintf/-/sprintf-0.1.3.tgz", "dev": true, "inBundle": true, "engines": { @@ -13727,6 +13775,11 @@ "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", "dev": true }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -15651,6 +15704,11 @@ } } }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" + }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -16677,6 +16735,26 @@ "wrappy": "1" } }, + "open": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.2.1.tgz", + "integrity": "sha512-rXILpcQlkF/QuFez2BJDf3GsqpjGKbkUUToAIGo9A0Q6ZkoSGogZJulrUdwRkrAsoQvoZsrjCYt8+zblOk7JQQ==", + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "dependencies": { + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "requires": { + "is-docker": "^2.0.0" + } + } + } + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -17860,7 +17938,6 @@ "dependencies": { "ipv6": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/ipv6/-/ipv6-3.1.1.tgz", "bundled": true, "dev": true, "requires": { @@ -17871,7 +17948,6 @@ "dependencies": { "sprintf": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/sprintf/-/sprintf-0.1.3.tgz", "bundled": true, "dev": true } diff --git a/package.json b/package.json index 93a1715925..6d1a377bc2 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "mime": "^2.4.6", "minimatch": "^3.0.3", "ms": "^2.1.2", + "open": "^8.2.1", "pirates": "^4.0.1", "pixelmatch": "^5.2.1", "pngjs": "^5.0.0", diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 03108a4f49..025e785888 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -237,8 +237,10 @@ if (!process.env.PW_CLI_TARGET_LANG) { if (playwrightTestPackagePath) { require(playwrightTestPackagePath).addTestCommand(program); - if (process.env.PW_EXPERIMENTAL) + if (process.env.PW_EXPERIMENTAL) { require(playwrightTestPackagePath).addGenerateHtmlCommand(program); + require(playwrightTestPackagePath).addShowHtmlCommand(program); + } } else { const command = program.command('test').allowUnknownOption(true); command.description('Run tests with Playwright Test. Available in @playwright/test package.'); diff --git a/src/test/cli.ts b/src/test/cli.ts index 8f814c9d0a..cea0bde0b7 100644 --- a/src/test/cli.ts +++ b/src/test/cli.ts @@ -16,15 +16,17 @@ /* eslint-disable no-console */ -import * as commander from 'commander'; -import * as fs from 'fs'; -import * as path from 'path'; +import commander from 'commander'; +import fs from 'fs'; +import open from 'open'; +import path from 'path'; import type { Config } from './types'; import { Runner, builtInReporters, BuiltInReporter } from './runner'; import { stopProfiling, startProfiling } from './profiler'; import { FilePatternFilter } from './util'; import { Loader } from './loader'; import { HtmlBuilder } from './html/htmlBuilder'; +import { HttpServer } from '../utils/httpServer'; const defaultTimeout = 30000; const defaultReporter: BuiltInReporter = process.env.CI ? 'dot' : 'list'; @@ -84,23 +86,12 @@ export function addTestCommand(program: commander.CommanderStatic) { } export function addGenerateHtmlCommand(program: commander.CommanderStatic) { - const command = program.command('generate-html'); + const command = program.command('generate-report'); command.description('Generate HTML report'); command.option('-c, --config ', `Configuration file, or a test directory with optional "${tsConfig}"/"${jsConfig}"`); command.option('--output ', `Folder for output artifacts (default: "playwright-report")`, 'playwright-report'); command.action(async opts => { - const output = opts.output; - delete opts.output; - const loader = await createLoader(opts); - const outputFolders = new Set(loader.projects().map(p => p.config.outputDir)); - const reportFiles = new Set(); - for (const outputFolder of outputFolders) { - const reportFolder = path.join(outputFolder, 'report'); - const files = fs.readdirSync(reportFolder).filter(f => f.endsWith('.report')); - for (const file of files) - reportFiles.add(path.join(reportFolder, file)); - } - new HtmlBuilder([...reportFiles], output, loader.fullConfig().rootDir); + await generateHTMLReport(opts); }).on('--help', () => { console.log(''); console.log('Examples:'); @@ -109,6 +100,46 @@ export function addGenerateHtmlCommand(program: commander.CommanderStatic) { }); } +export function addShowHtmlCommand(program: commander.CommanderStatic) { + const command = program.command('show-report'); + command.description('Show HTML report for last run'); + command.option('-c, --config ', `Configuration file, or a test directory with optional "${tsConfig}"/"${jsConfig}"`); + command.option('--output ', `Folder for output artifacts (default: "playwright-report")`, 'playwright-report'); + command.action(async opts => { + const output = await generateHTMLReport(opts); + const server = new HttpServer(); + server.routePrefix('/', (request, response) => { + let relativePath = request.url!; + if (relativePath === '/') + relativePath = '/index.html'; + const absolutePath = path.join(output, ...relativePath.split('/')); + return server.serveFile(response, absolutePath); + }); + open(await server.start()); + }).on('--help', () => { + console.log(''); + console.log('Examples:'); + console.log(''); + console.log(' $ show-report'); + }); +} + +async function generateHTMLReport(opts: any): Promise { + const output = opts.output; + delete opts.output; + const loader = await createLoader(opts); + const outputFolders = new Set(loader.projects().map(p => p.config.outputDir)); + const reportFiles = new Set(); + for (const outputFolder of outputFolders) { + const reportFolder = path.join(outputFolder, 'report'); + const files = fs.readdirSync(reportFolder).filter(f => f.endsWith('.report')); + for (const file of files) + reportFiles.add(path.join(reportFolder, file)); + } + new HtmlBuilder([...reportFiles], output, loader.fullConfig().rootDir); + return output; +} + async function createLoader(opts: { [key: string]: any }): Promise { if (opts.browser) { const browserOpt = opts.browser.toLowerCase(); diff --git a/src/web/htmlReport2/htmlReport.tsx b/src/web/htmlReport2/htmlReport.tsx index 38faa0b815..74a4f43cc7 100644 --- a/src/web/htmlReport2/htmlReport.tsx +++ b/src/web/htmlReport2/htmlReport.tsx @@ -50,7 +50,7 @@ export const Report: React.FC = () => { return
- +
{ (['Failing', 'All'] as Filter[]).map(item => {