mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(html): bake report zip into the html report, allow opening from fs (#9939)
This commit is contained in:
parent
4e52b64619
commit
9ac8829583
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) Microsoft Corporation.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
|
||||||
|
class BundleJsPlugin {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(compiler) {
|
||||||
|
compiler.hooks.compilation.tap('bundle-js-plugin', compilation => {
|
||||||
|
HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync('bundle-js-plugin', (htmlPluginData, callback) => {
|
||||||
|
callback(null, this.processTags(compilation, htmlPluginData));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
processTags(compilation, pluginData) {
|
||||||
|
const headTags = pluginData.headTags.map(tag => this.processTag(compilation, tag));
|
||||||
|
const bodyTags = pluginData.bodyTags.map(tag => this.processTag(compilation, tag));
|
||||||
|
return { ...pluginData, headTags, bodyTags };
|
||||||
|
}
|
||||||
|
|
||||||
|
processTag(compilation, tag) {
|
||||||
|
if (tag.tagName !== 'script' || !tag.attributes.src)
|
||||||
|
return tag;
|
||||||
|
|
||||||
|
const asset = getAssetByName(compilation.assets, tag.attributes.src);
|
||||||
|
const innerHTML = asset.source().replace(/(<)(\/script>)/g, '\\x3C$2');
|
||||||
|
return {
|
||||||
|
tagName: 'script',
|
||||||
|
attributes: {
|
||||||
|
type: 'text/javascript'
|
||||||
|
},
|
||||||
|
closeTag: true,
|
||||||
|
innerHTML,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAssetByName (assets, assetName) {
|
||||||
|
for (var key in assets) {
|
||||||
|
if (assets.hasOwnProperty(key)) {
|
||||||
|
var processedKey = path.posix.relative('', key);
|
||||||
|
if (processedKey === assetName) {
|
||||||
|
return assets[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BundleJsPlugin;
|
||||||
@ -449,24 +449,6 @@ a.no-decorations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.needs-server-message {
|
|
||||||
max-width: 500px;
|
|
||||||
margin: auto;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bash-snippet {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-family: monospace;
|
|
||||||
background: var(--color-fg-default);
|
|
||||||
color: var(--color-canvas-default);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-flex {
|
.d-flex {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,12 +20,22 @@ import ansi2html from 'ansi-to-html';
|
|||||||
import { downArrow, rightArrow, TreeItem } from '../components/treeItem';
|
import { downArrow, rightArrow, TreeItem } from '../components/treeItem';
|
||||||
import { TabbedPane } from '../traceViewer/ui/tabbedPane';
|
import { TabbedPane } from '../traceViewer/ui/tabbedPane';
|
||||||
import { msToString } from '../uiUtils';
|
import { msToString } from '../uiUtils';
|
||||||
|
import { traceImage } from './images';
|
||||||
import type { TestCase, TestResult, TestStep, TestFile, Stats, TestAttachment, HTMLReport, TestFileSummary, TestCaseSummary } from '@playwright/test/src/reporters/html';
|
import type { TestCase, TestResult, TestStep, TestFile, Stats, TestAttachment, HTMLReport, TestFileSummary, TestCaseSummary } from '@playwright/test/src/reporters/html';
|
||||||
|
import type zip from '@zip.js/zip.js';
|
||||||
|
|
||||||
|
const zipjs = (self as any).zip;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
playwrightReportBase64?: string;
|
||||||
|
entries: Map<string, zip.Entry>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const Report: React.FC = () => {
|
export const Report: React.FC = () => {
|
||||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||||
|
|
||||||
const [fetchError, setFetchError] = React.useState<string | undefined>();
|
|
||||||
const [report, setReport] = React.useState<HTMLReport | undefined>();
|
const [report, setReport] = React.useState<HTMLReport | undefined>();
|
||||||
const [expandedFiles, setExpandedFiles] = React.useState<Map<string, boolean>>(new Map());
|
const [expandedFiles, setExpandedFiles] = React.useState<Map<string, boolean>>(new Map());
|
||||||
const [filterText, setFilterText] = React.useState(searchParams.get('q') || '');
|
const [filterText, setFilterText] = React.useState(searchParams.get('q') || '');
|
||||||
@ -34,12 +44,11 @@ export const Report: React.FC = () => {
|
|||||||
if (report)
|
if (report)
|
||||||
return;
|
return;
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
const zipReader = new zipjs.ZipReader(new zipjs.Data64URIReader(window.playwrightReportBase64), { useWebWorkers: false }) as zip.ZipReader;
|
||||||
const report = await fetch('data/report.json', { cache: 'no-cache' }).then(r => r.json() as Promise<HTMLReport>);
|
window.entries = new Map<string, zip.Entry>();
|
||||||
setReport(report);
|
for (const entry of await zipReader.getEntries())
|
||||||
} catch (e) {
|
window.entries.set(entry.filename, entry);
|
||||||
setFetchError(e.message);
|
setReport(await readJsonEntry('report.json') as HTMLReport);
|
||||||
}
|
|
||||||
window.addEventListener('popstate', () => {
|
window.addEventListener('popstate', () => {
|
||||||
const params = new URLSearchParams(window.location.hash.slice(1));
|
const params = new URLSearchParams(window.location.hash.slice(1));
|
||||||
setFilterText(params.get('q') || '');
|
setFilterText(params.get('q') || '');
|
||||||
@ -49,16 +58,8 @@ export const Report: React.FC = () => {
|
|||||||
|
|
||||||
const filter = React.useMemo(() => Filter.parse(filterText), [filterText]);
|
const filter = React.useMemo(() => Filter.parse(filterText), [filterText]);
|
||||||
|
|
||||||
if (window.location.protocol === 'file:') {
|
|
||||||
return <div className='needs-server-message'>
|
|
||||||
Playwright report needs to be served as a web page. Consider the following options to view it locally:
|
|
||||||
<div className='bash-snippet'>npx node-static playwright-report</div>
|
|
||||||
<div className='bash-snippet'>cd playwright-report && python -m SimpleHTTPServer</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className='vbox columns'>
|
return <div className='vbox columns'>
|
||||||
{!fetchError && <div className='flow-container'>
|
{<div className='flow-container'>
|
||||||
<Route params=''>
|
<Route params=''>
|
||||||
<AllTestFilesSummaryView report={report} filter={filter} expandedFiles={expandedFiles} setExpandedFiles={setExpandedFiles} filterText={filterText} setFilterText={setFilterText}></AllTestFilesSummaryView>
|
<AllTestFilesSummaryView report={report} filter={filter} expandedFiles={expandedFiles} setExpandedFiles={setExpandedFiles} filterText={filterText} setFilterText={setFilterText}></AllTestFilesSummaryView>
|
||||||
</Route>
|
</Route>
|
||||||
@ -176,8 +177,7 @@ const TestCaseView: React.FC<{
|
|||||||
const fileId = testId.split('-')[0];
|
const fileId = testId.split('-')[0];
|
||||||
if (!fileId)
|
if (!fileId)
|
||||||
return;
|
return;
|
||||||
const result = await fetch(`data/${fileId}.json`, { cache: 'no-cache' });
|
const file = await readJsonEntry(`${fileId}.json`) as TestFile;
|
||||||
const file = await result.json() as TestFile;
|
|
||||||
for (const t of file.tests) {
|
for (const t of file.tests) {
|
||||||
if (t.testId === testId) {
|
if (t.testId === testId) {
|
||||||
setTest(t);
|
setTest(t);
|
||||||
@ -253,7 +253,7 @@ const TestResultView: React.FC<{
|
|||||||
{!!traces.length && <Chip header='Traces'>
|
{!!traces.length && <Chip header='Traces'>
|
||||||
{traces.map((a, i) => <div key={`trace-${i}`}>
|
{traces.map((a, i) => <div key={`trace-${i}`}>
|
||||||
<a href={`trace/index.html?trace=${new URL(a.path!, window.location.href)}`}>
|
<a href={`trace/index.html?trace=${new URL(a.path!, window.location.href)}`}>
|
||||||
<img src='trace.png' style={{ width: 192, height: 117, marginLeft: 20 }} />
|
<img src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
|
||||||
</a>
|
</a>
|
||||||
</div>)}
|
</div>)}
|
||||||
</Chip>}
|
</Chip>}
|
||||||
@ -598,6 +598,13 @@ class Filter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readJsonEntry(entryName: string): Promise<any> {
|
||||||
|
const reportEntry = window.entries.get(entryName);
|
||||||
|
const writer = new zipjs.TextWriter() as zip.TextWriter;
|
||||||
|
await reportEntry!.getData!(writer);
|
||||||
|
return JSON.parse(await writer.getData());
|
||||||
|
}
|
||||||
|
|
||||||
type SearchValues = {
|
type SearchValues = {
|
||||||
text: string;
|
text: string;
|
||||||
project: string;
|
project: string;
|
||||||
|
|||||||
17
packages/playwright-core/src/web/htmlReport/images.ts
Normal file
17
packages/playwright-core/src/web/htmlReport/images.ts
Normal file
File diff suppressed because one or more lines are too long
@ -19,6 +19,6 @@ import * as ReactDOM from 'react-dom';
|
|||||||
import { Report } from './htmlReport';
|
import { Report } from './htmlReport';
|
||||||
import './colors.css';
|
import './colors.css';
|
||||||
|
|
||||||
(async () => {
|
window.onload = () => {
|
||||||
ReactDOM.render(<Report />, document.querySelector('#root'));
|
ReactDOM.render(<Report />, document.querySelector('#root'));
|
||||||
})();
|
};
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB |
@ -1,12 +1,29 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) Microsoft Corporation.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const HtmlWebPackPlugin = require('html-webpack-plugin');
|
const HtmlWebPackPlugin = require('html-webpack-plugin');
|
||||||
const CopyPlugin = require('copy-webpack-plugin');
|
const BundleJsPlugin = require('./bundleJsPlugin');
|
||||||
|
|
||||||
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode,
|
mode,
|
||||||
entry: {
|
entry: {
|
||||||
|
zip: path.resolve(__dirname, '../../../../../node_modules/@zip.js/zip.js/dist/zip-no-worker-inflate.min.js'),
|
||||||
app: path.join(__dirname, 'index.tsx'),
|
app: path.join(__dirname, 'index.tsx'),
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
@ -38,17 +55,11 @@ module.exports = {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new CopyPlugin({
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
from: path.resolve(__dirname, 'static'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
new HtmlWebPackPlugin({
|
new HtmlWebPackPlugin({
|
||||||
title: 'Playwright Test Report',
|
title: 'Playwright Test Report',
|
||||||
template: path.join(__dirname, 'index.html'),
|
template: path.join(__dirname, 'index.html'),
|
||||||
inject: true,
|
inject: true,
|
||||||
})
|
}),
|
||||||
|
new BundleJsPlugin(),
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@ -18,11 +18,13 @@ import colors from 'colors/safe';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import open from 'open';
|
import open from 'open';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { Transform, TransformCallback } from 'stream';
|
||||||
import { FullConfig, Suite } from '../../types/testReporter';
|
import { FullConfig, Suite } from '../../types/testReporter';
|
||||||
import { HttpServer } from 'playwright-core/lib/utils/httpServer';
|
import { HttpServer } from 'playwright-core/lib/utils/httpServer';
|
||||||
import { calculateSha1, removeFolders } from 'playwright-core/lib/utils/utils';
|
import { calculateSha1, removeFolders } from 'playwright-core/lib/utils/utils';
|
||||||
import RawReporter, { JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep, JsonAttachment } from './raw';
|
import RawReporter, { JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep, JsonAttachment } from './raw';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
import yazl from 'yazl';
|
||||||
|
|
||||||
export type Stats = {
|
export type Stats = {
|
||||||
total: number;
|
total: number;
|
||||||
@ -127,8 +129,8 @@ class HtmlReporter {
|
|||||||
});
|
});
|
||||||
const reportFolder = htmlReportFolder(this._outputFolder);
|
const reportFolder = htmlReportFolder(this._outputFolder);
|
||||||
await removeFolders([reportFolder]);
|
await removeFolders([reportFolder]);
|
||||||
const builder = new HtmlBuilder(reportFolder, this.config.rootDir);
|
const builder = new HtmlBuilder(reportFolder);
|
||||||
const { ok, singleTestId } = builder.build(reports);
|
const { ok, singleTestId } = await builder.build(reports);
|
||||||
|
|
||||||
if (process.env.PWTEST_SKIP_TEST_OUTPUT || process.env.CI)
|
if (process.env.PWTEST_SKIP_TEST_OUTPUT || process.env.CI)
|
||||||
return;
|
return;
|
||||||
@ -198,16 +200,16 @@ class HtmlBuilder {
|
|||||||
private _reportFolder: string;
|
private _reportFolder: string;
|
||||||
private _tests = new Map<string, JsonTestCase>();
|
private _tests = new Map<string, JsonTestCase>();
|
||||||
private _testPath = new Map<string, string[]>();
|
private _testPath = new Map<string, string[]>();
|
||||||
private _dataFolder: string;
|
private _dataZipFile: yazl.ZipFile;
|
||||||
private _hasTraces = false;
|
private _hasTraces = false;
|
||||||
|
|
||||||
constructor(outputDir: string, rootDir: string) {
|
constructor(outputDir: string) {
|
||||||
this._reportFolder = path.resolve(process.cwd(), outputDir);
|
this._reportFolder = path.resolve(process.cwd(), outputDir);
|
||||||
this._dataFolder = path.join(this._reportFolder, 'data');
|
fs.mkdirSync(this._reportFolder, { recursive: true });
|
||||||
|
this._dataZipFile = new yazl.ZipFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
build(rawReports: JsonReport[]): { ok: boolean, singleTestId: string | undefined } {
|
async build(rawReports: JsonReport[]): Promise<{ ok: boolean, singleTestId: string | undefined }> {
|
||||||
fs.mkdirSync(this._dataFolder, { recursive: true });
|
|
||||||
|
|
||||||
const data = new Map<string, { testFile: TestFile, testFileSummary: TestFileSummary }>();
|
const data = new Map<string, { testFile: TestFile, testFileSummary: TestFileSummary }>();
|
||||||
for (const projectJson of rawReports) {
|
for (const projectJson of rawReports) {
|
||||||
@ -259,7 +261,7 @@ class HtmlBuilder {
|
|||||||
return t1.location.line - t2.location.line;
|
return t1.location.line - t2.location.line;
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.writeFileSync(path.join(this._dataFolder, fileId + '.json'), JSON.stringify(testFile, undefined, 2));
|
this._addDataFile(fileId + '.json', testFile);
|
||||||
}
|
}
|
||||||
const htmlReport: HTMLReport = {
|
const htmlReport: HTMLReport = {
|
||||||
files: [...data.values()].map(e => e.testFileSummary),
|
files: [...data.values()].map(e => e.testFileSummary),
|
||||||
@ -272,15 +274,11 @@ class HtmlBuilder {
|
|||||||
return w2 - w1;
|
return w2 - w1;
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.writeFileSync(path.join(this._dataFolder, 'report.json'), JSON.stringify(htmlReport, undefined, 2));
|
this._addDataFile('report.json', htmlReport);
|
||||||
|
|
||||||
// Copy app.
|
// Copy app.
|
||||||
const appFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'webpack', 'htmlReport');
|
const appFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'webpack', 'htmlReport');
|
||||||
for (const file of fs.readdirSync(appFolder)) {
|
fs.copyFileSync(path.join(appFolder, 'index.html'), path.join(this._reportFolder, 'index.html'));
|
||||||
if (file.endsWith('.map'))
|
|
||||||
continue;
|
|
||||||
fs.copyFileSync(path.join(appFolder, file), path.join(this._reportFolder, file));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy trace viewer.
|
// Copy trace viewer.
|
||||||
if (this._hasTraces) {
|
if (this._hasTraces) {
|
||||||
@ -294,14 +292,31 @@ class HtmlBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inline report data.
|
||||||
|
const indexFile = path.join(this._reportFolder, 'index.html');
|
||||||
|
fs.appendFileSync(indexFile, '<script>\nwindow.playwrightReportBase64 = "data:application/zip;base64,');
|
||||||
|
await new Promise(f => {
|
||||||
|
this._dataZipFile!.end(undefined, () => {
|
||||||
|
this._dataZipFile!.outputStream
|
||||||
|
.pipe(new Base64Encoder())
|
||||||
|
.pipe(fs.createWriteStream(indexFile, { flags: 'a' })).on('close', f);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
fs.appendFileSync(indexFile, '";</script>');
|
||||||
|
|
||||||
let singleTestId: string | undefined;
|
let singleTestId: string | undefined;
|
||||||
if (htmlReport.stats.total === 1) {
|
if (htmlReport.stats.total === 1) {
|
||||||
const testFile: TestFile = data.values().next().value.testFile;
|
const testFile: TestFile = data.values().next().value.testFile;
|
||||||
singleTestId = testFile.tests[0].testId;
|
singleTestId = testFile.tests[0].testId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok, singleTestId };
|
return { ok, singleTestId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _addDataFile(fileName: string, data: any) {
|
||||||
|
this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName);
|
||||||
|
}
|
||||||
|
|
||||||
private _processJsonSuite(suite: JsonSuite, fileId: string, projectName: string, path: string[], out: TestEntry[]) {
|
private _processJsonSuite(suite: JsonSuite, fileId: string, projectName: string, path: string[], out: TestEntry[]) {
|
||||||
const newPath = [...path, suite.title];
|
const newPath = [...path, suite.title];
|
||||||
suite.suites.map(s => this._processJsonSuite(s, fileId, projectName, newPath, out));
|
suite.suites.map(s => this._processJsonSuite(s, fileId, projectName, newPath, out));
|
||||||
@ -358,6 +373,7 @@ class HtmlBuilder {
|
|||||||
const buffer = fs.readFileSync(a.path);
|
const buffer = fs.readFileSync(a.path);
|
||||||
const sha1 = calculateSha1(buffer) + path.extname(a.path);
|
const sha1 = calculateSha1(buffer) + path.extname(a.path);
|
||||||
fileName = 'data/' + sha1;
|
fileName = 'data/' + sha1;
|
||||||
|
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) {
|
||||||
}
|
}
|
||||||
@ -419,4 +435,30 @@ const addStats = (stats: Stats, delta: Stats): Stats => {
|
|||||||
return stats;
|
return stats;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class Base64Encoder extends Transform {
|
||||||
|
private _remainder: Buffer | undefined;
|
||||||
|
|
||||||
|
override _transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback): void {
|
||||||
|
if (this._remainder) {
|
||||||
|
chunk = Buffer.concat([this._remainder, chunk]);
|
||||||
|
this._remainder = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remaining = chunk.length % 3;
|
||||||
|
if (remaining) {
|
||||||
|
this._remainder = chunk.slice(chunk.length - remaining);
|
||||||
|
chunk = chunk.slice(0, chunk.length - remaining);
|
||||||
|
}
|
||||||
|
chunk = chunk.toString('base64');
|
||||||
|
this.push(Buffer.from(chunk));
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
override _flush(callback: TransformCallback): void {
|
||||||
|
if (this._remainder)
|
||||||
|
this.push(Buffer.from(this._remainder.toString('base64')));
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default HtmlReporter;
|
export default HtmlReporter;
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
|
||||||
import { test as baseTest, expect } from './playwright-test-fixtures';
|
import { test as baseTest, expect } from './playwright-test-fixtures';
|
||||||
import { HttpServer } from 'playwright-core/lib/utils/httpServer';
|
import { HttpServer } from 'playwright-core/lib/utils/httpServer';
|
||||||
import { startHtmlReportServer } from '../../packages/playwright-test/lib/reporters/html';
|
import { startHtmlReportServer } from '../../packages/playwright-test/lib/reporters/html';
|
||||||
@ -34,7 +33,7 @@ const test = baseTest.extend<{ showReport: () => Promise<void> }>({
|
|||||||
|
|
||||||
test.use({ channel: 'chrome' });
|
test.use({ channel: 'chrome' });
|
||||||
|
|
||||||
test('should generate report', async ({ runInlineTest }, testInfo) => {
|
test('should generate report', async ({ runInlineTest, showReport, page }) => {
|
||||||
await runInlineTest({
|
await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
module.exports = { name: 'project-name' };
|
module.exports = { name: 'project-name' };
|
||||||
@ -45,7 +44,7 @@ test('should generate report', async ({ runInlineTest }, testInfo) => {
|
|||||||
test('fails', async ({}) => {
|
test('fails', async ({}) => {
|
||||||
expect(1).toBe(2);
|
expect(1).toBe(2);
|
||||||
});
|
});
|
||||||
test('skip', async ({}) => {
|
test('skipped', async ({}) => {
|
||||||
test.skip('Does not work')
|
test.skip('Does not work')
|
||||||
});
|
});
|
||||||
test('flaky', async ({}, testInfo) => {
|
test('flaky', async ({}, testInfo) => {
|
||||||
@ -53,87 +52,19 @@ test('should generate report', async ({ runInlineTest }, testInfo) => {
|
|||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
}, { reporter: 'dot,html', retries: 1 });
|
}, { reporter: 'dot,html', retries: 1 });
|
||||||
const report = testInfo.outputPath('playwright-report', 'data', 'report.json');
|
|
||||||
const reportObject = JSON.parse(fs.readFileSync(report, 'utf-8'));
|
|
||||||
delete reportObject.testIdToFileId;
|
|
||||||
delete reportObject.files[0].fileId;
|
|
||||||
delete reportObject.files[0].stats.duration;
|
|
||||||
delete reportObject.stats.duration;
|
|
||||||
|
|
||||||
const fileNames = new Set<string>();
|
await showReport();
|
||||||
for (const test of reportObject.files[0].tests) {
|
|
||||||
fileNames.add(testInfo.outputPath('playwright-report', 'data', test.fileId + '.json'));
|
await expect(page.locator('.subnav-item:has-text("All") .counter')).toHaveText('4');
|
||||||
delete test.testId;
|
await expect(page.locator('.subnav-item:has-text("Passed") .counter')).toHaveText('1');
|
||||||
delete test.fileId;
|
await expect(page.locator('.subnav-item:has-text("Failed") .counter')).toHaveText('1');
|
||||||
delete test.location.line;
|
await expect(page.locator('.subnav-item:has-text("Flaky") .counter')).toHaveText('1');
|
||||||
delete test.location.column;
|
await expect(page.locator('.subnav-item:has-text("Skipped") .counter')).toHaveText('1');
|
||||||
delete test.duration;
|
|
||||||
delete test.path;
|
await expect(page.locator('.test-summary.outcome-unexpected >> text=fails')).toBeVisible();
|
||||||
}
|
await expect(page.locator('.test-summary.outcome-flaky >> text=flaky')).toBeVisible();
|
||||||
expect(reportObject).toEqual({
|
await expect(page.locator('.test-summary.outcome-expected >> text=passes')).toBeVisible();
|
||||||
files: [
|
await expect(page.locator('.test-summary.outcome-skipped >> text=skipped')).toBeVisible();
|
||||||
{
|
|
||||||
fileName: 'a.test.js',
|
|
||||||
tests: [
|
|
||||||
{
|
|
||||||
title: 'fails',
|
|
||||||
projectName: 'project-name',
|
|
||||||
location: {
|
|
||||||
file: 'a.test.js'
|
|
||||||
},
|
|
||||||
outcome: 'unexpected',
|
|
||||||
ok: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'flaky',
|
|
||||||
projectName: 'project-name',
|
|
||||||
location: {
|
|
||||||
file: 'a.test.js'
|
|
||||||
},
|
|
||||||
outcome: 'flaky',
|
|
||||||
ok: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'passes',
|
|
||||||
projectName: 'project-name',
|
|
||||||
location: {
|
|
||||||
file: 'a.test.js'
|
|
||||||
},
|
|
||||||
outcome: 'expected',
|
|
||||||
ok: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'skip',
|
|
||||||
projectName: 'project-name',
|
|
||||||
location: {
|
|
||||||
file: 'a.test.js'
|
|
||||||
},
|
|
||||||
outcome: 'skipped',
|
|
||||||
ok: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
stats: {
|
|
||||||
total: 4,
|
|
||||||
expected: 1,
|
|
||||||
unexpected: 1,
|
|
||||||
flaky: 1,
|
|
||||||
skipped: 1,
|
|
||||||
ok: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
projectNames: [
|
|
||||||
'project-name'
|
|
||||||
],
|
|
||||||
stats: {
|
|
||||||
expected: 1,
|
|
||||||
flaky: 1,
|
|
||||||
ok: false,
|
|
||||||
skipped: 1,
|
|
||||||
total: 4,
|
|
||||||
unexpected: 1,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not throw when attachment is missing', async ({ runInlineTest }) => {
|
test('should not throw when attachment is missing', async ({ runInlineTest }) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user