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 {
 | 
			
		||||
  display: flex !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -20,12 +20,22 @@ import ansi2html from 'ansi-to-html';
 | 
			
		||||
import { downArrow, rightArrow, TreeItem } from '../components/treeItem';
 | 
			
		||||
import { TabbedPane } from '../traceViewer/ui/tabbedPane';
 | 
			
		||||
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 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 = () => {
 | 
			
		||||
  const searchParams = new URLSearchParams(window.location.hash.slice(1));
 | 
			
		||||
 | 
			
		||||
  const [fetchError, setFetchError] = React.useState<string | undefined>();
 | 
			
		||||
  const [report, setReport] = React.useState<HTMLReport | undefined>();
 | 
			
		||||
  const [expandedFiles, setExpandedFiles] = React.useState<Map<string, boolean>>(new Map());
 | 
			
		||||
  const [filterText, setFilterText] = React.useState(searchParams.get('q') || '');
 | 
			
		||||
@ -34,12 +44,11 @@ export const Report: React.FC = () => {
 | 
			
		||||
    if (report)
 | 
			
		||||
      return;
 | 
			
		||||
    (async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        const report = await fetch('data/report.json', { cache: 'no-cache' }).then(r => r.json() as Promise<HTMLReport>);
 | 
			
		||||
        setReport(report);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        setFetchError(e.message);
 | 
			
		||||
      }
 | 
			
		||||
      const zipReader = new zipjs.ZipReader(new zipjs.Data64URIReader(window.playwrightReportBase64), { useWebWorkers: false }) as zip.ZipReader;
 | 
			
		||||
      window.entries = new Map<string, zip.Entry>();
 | 
			
		||||
      for (const entry of await zipReader.getEntries())
 | 
			
		||||
        window.entries.set(entry.filename, entry);
 | 
			
		||||
      setReport(await readJsonEntry('report.json') as HTMLReport);
 | 
			
		||||
      window.addEventListener('popstate', () => {
 | 
			
		||||
        const params = new URLSearchParams(window.location.hash.slice(1));
 | 
			
		||||
        setFilterText(params.get('q') || '');
 | 
			
		||||
@ -49,16 +58,8 @@ export const Report: React.FC = () => {
 | 
			
		||||
 | 
			
		||||
  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'>
 | 
			
		||||
    {!fetchError && <div className='flow-container'>
 | 
			
		||||
    {<div className='flow-container'>
 | 
			
		||||
      <Route params=''>
 | 
			
		||||
        <AllTestFilesSummaryView report={report} filter={filter} expandedFiles={expandedFiles} setExpandedFiles={setExpandedFiles} filterText={filterText} setFilterText={setFilterText}></AllTestFilesSummaryView>
 | 
			
		||||
      </Route>
 | 
			
		||||
@ -176,8 +177,7 @@ const TestCaseView: React.FC<{
 | 
			
		||||
      const fileId = testId.split('-')[0];
 | 
			
		||||
      if (!fileId)
 | 
			
		||||
        return;
 | 
			
		||||
      const result = await fetch(`data/${fileId}.json`, { cache: 'no-cache' });
 | 
			
		||||
      const file = await result.json() as TestFile;
 | 
			
		||||
      const file = await readJsonEntry(`${fileId}.json`) as TestFile;
 | 
			
		||||
      for (const t of file.tests) {
 | 
			
		||||
        if (t.testId === testId) {
 | 
			
		||||
          setTest(t);
 | 
			
		||||
@ -253,7 +253,7 @@ const TestResultView: React.FC<{
 | 
			
		||||
    {!!traces.length && <Chip header='Traces'>
 | 
			
		||||
      {traces.map((a, i) => <div key={`trace-${i}`}>
 | 
			
		||||
        <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>
 | 
			
		||||
      </div>)}
 | 
			
		||||
    </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 = {
 | 
			
		||||
  text: 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 './colors.css';
 | 
			
		||||
 | 
			
		||||
(async () => {
 | 
			
		||||
window.onload = () => {
 | 
			
		||||
  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 HtmlWebPackPlugin = require('html-webpack-plugin');
 | 
			
		||||
const CopyPlugin = require('copy-webpack-plugin');
 | 
			
		||||
const BundleJsPlugin = require('./bundleJsPlugin');
 | 
			
		||||
 | 
			
		||||
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  mode,
 | 
			
		||||
  entry: {
 | 
			
		||||
    zip: path.resolve(__dirname, '../../../../../node_modules/@zip.js/zip.js/dist/zip-no-worker-inflate.min.js'),
 | 
			
		||||
    app: path.join(__dirname, 'index.tsx'),
 | 
			
		||||
  },
 | 
			
		||||
  resolve: {
 | 
			
		||||
@ -38,17 +55,11 @@ module.exports = {
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    new CopyPlugin({
 | 
			
		||||
      patterns: [
 | 
			
		||||
        {
 | 
			
		||||
          from: path.resolve(__dirname, 'static'),
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }),
 | 
			
		||||
    new HtmlWebPackPlugin({
 | 
			
		||||
      title: 'Playwright Test Report',
 | 
			
		||||
      template: path.join(__dirname, 'index.html'),
 | 
			
		||||
      inject: true,
 | 
			
		||||
    })
 | 
			
		||||
    }),
 | 
			
		||||
    new BundleJsPlugin(),
 | 
			
		||||
  ]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -18,11 +18,13 @@ import colors from 'colors/safe';
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
import open from 'open';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import { Transform, TransformCallback } from 'stream';
 | 
			
		||||
import { FullConfig, Suite } from '../../types/testReporter';
 | 
			
		||||
import { HttpServer } from 'playwright-core/lib/utils/httpServer';
 | 
			
		||||
import { calculateSha1, removeFolders } from 'playwright-core/lib/utils/utils';
 | 
			
		||||
import RawReporter, { JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep, JsonAttachment } from './raw';
 | 
			
		||||
import assert from 'assert';
 | 
			
		||||
import yazl from 'yazl';
 | 
			
		||||
 | 
			
		||||
export type Stats = {
 | 
			
		||||
  total: number;
 | 
			
		||||
@ -127,8 +129,8 @@ class HtmlReporter {
 | 
			
		||||
    });
 | 
			
		||||
    const reportFolder = htmlReportFolder(this._outputFolder);
 | 
			
		||||
    await removeFolders([reportFolder]);
 | 
			
		||||
    const builder = new HtmlBuilder(reportFolder, this.config.rootDir);
 | 
			
		||||
    const { ok, singleTestId } = builder.build(reports);
 | 
			
		||||
    const builder = new HtmlBuilder(reportFolder);
 | 
			
		||||
    const { ok, singleTestId } = await builder.build(reports);
 | 
			
		||||
 | 
			
		||||
    if (process.env.PWTEST_SKIP_TEST_OUTPUT || process.env.CI)
 | 
			
		||||
      return;
 | 
			
		||||
@ -198,16 +200,16 @@ class HtmlBuilder {
 | 
			
		||||
  private _reportFolder: string;
 | 
			
		||||
  private _tests = new Map<string, JsonTestCase>();
 | 
			
		||||
  private _testPath = new Map<string, string[]>();
 | 
			
		||||
  private _dataFolder: string;
 | 
			
		||||
  private _dataZipFile: yazl.ZipFile;
 | 
			
		||||
  private _hasTraces = false;
 | 
			
		||||
 | 
			
		||||
  constructor(outputDir: string, rootDir: string) {
 | 
			
		||||
  constructor(outputDir: string) {
 | 
			
		||||
    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 } {
 | 
			
		||||
    fs.mkdirSync(this._dataFolder, { recursive: true });
 | 
			
		||||
  async build(rawReports: JsonReport[]): Promise<{ ok: boolean, singleTestId: string | undefined }> {
 | 
			
		||||
 | 
			
		||||
    const data = new Map<string, { testFile: TestFile, testFileSummary: TestFileSummary }>();
 | 
			
		||||
    for (const projectJson of rawReports) {
 | 
			
		||||
@ -259,7 +261,7 @@ class HtmlBuilder {
 | 
			
		||||
        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 = {
 | 
			
		||||
      files: [...data.values()].map(e => e.testFileSummary),
 | 
			
		||||
@ -272,15 +274,11 @@ class HtmlBuilder {
 | 
			
		||||
      return w2 - w1;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    fs.writeFileSync(path.join(this._dataFolder, 'report.json'), JSON.stringify(htmlReport, undefined, 2));
 | 
			
		||||
    this._addDataFile('report.json', htmlReport);
 | 
			
		||||
 | 
			
		||||
    // Copy app.
 | 
			
		||||
    const appFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'webpack', 'htmlReport');
 | 
			
		||||
    for (const file of fs.readdirSync(appFolder)) {
 | 
			
		||||
      if (file.endsWith('.map'))
 | 
			
		||||
        continue;
 | 
			
		||||
      fs.copyFileSync(path.join(appFolder, file), path.join(this._reportFolder, file));
 | 
			
		||||
    }
 | 
			
		||||
    fs.copyFileSync(path.join(appFolder, 'index.html'), path.join(this._reportFolder, 'index.html'));
 | 
			
		||||
 | 
			
		||||
    // Copy trace viewer.
 | 
			
		||||
    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;
 | 
			
		||||
    if (htmlReport.stats.total === 1) {
 | 
			
		||||
      const testFile: TestFile  = data.values().next().value.testFile;
 | 
			
		||||
      singleTestId = testFile.tests[0].testId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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[]) {
 | 
			
		||||
    const newPath = [...path, suite.title];
 | 
			
		||||
    suite.suites.map(s => this._processJsonSuite(s, fileId, projectName, newPath, out));
 | 
			
		||||
@ -358,6 +373,7 @@ class HtmlBuilder {
 | 
			
		||||
            const buffer = fs.readFileSync(a.path);
 | 
			
		||||
            const sha1 = calculateSha1(buffer) + path.extname(a.path);
 | 
			
		||||
            fileName = 'data/' + sha1;
 | 
			
		||||
            fs.mkdirSync(path.join(this._reportFolder, 'data'), { recursive: true });
 | 
			
		||||
            fs.writeFileSync(path.join(this._reportFolder, 'data', sha1), buffer);
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
          }
 | 
			
		||||
@ -419,4 +435,30 @@ const addStats = (stats: Stats, delta: Stats): 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;
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,6 @@
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
import { test as baseTest, expect } from './playwright-test-fixtures';
 | 
			
		||||
import { HttpServer } from 'playwright-core/lib/utils/httpServer';
 | 
			
		||||
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('should generate report', async ({ runInlineTest }, testInfo) => {
 | 
			
		||||
test('should generate report', async ({ runInlineTest, showReport, page }) => {
 | 
			
		||||
  await runInlineTest({
 | 
			
		||||
    'playwright.config.ts': `
 | 
			
		||||
      module.exports = { name: 'project-name' };
 | 
			
		||||
@ -45,7 +44,7 @@ test('should generate report', async ({ runInlineTest }, testInfo) => {
 | 
			
		||||
      test('fails', async ({}) => {
 | 
			
		||||
        expect(1).toBe(2);
 | 
			
		||||
      });
 | 
			
		||||
      test('skip', async ({}) => {
 | 
			
		||||
      test('skipped', async ({}) => {
 | 
			
		||||
        test.skip('Does not work')
 | 
			
		||||
      });
 | 
			
		||||
      test('flaky', async ({}, testInfo) => {
 | 
			
		||||
@ -53,87 +52,19 @@ test('should generate report', async ({ runInlineTest }, testInfo) => {
 | 
			
		||||
      });
 | 
			
		||||
    `,
 | 
			
		||||
  }, { 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>();
 | 
			
		||||
  for (const test of reportObject.files[0].tests) {
 | 
			
		||||
    fileNames.add(testInfo.outputPath('playwright-report', 'data', test.fileId + '.json'));
 | 
			
		||||
    delete test.testId;
 | 
			
		||||
    delete test.fileId;
 | 
			
		||||
    delete test.location.line;
 | 
			
		||||
    delete test.location.column;
 | 
			
		||||
    delete test.duration;
 | 
			
		||||
    delete test.path;
 | 
			
		||||
  }
 | 
			
		||||
  expect(reportObject).toEqual({
 | 
			
		||||
    files: [
 | 
			
		||||
      {
 | 
			
		||||
        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,
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  await showReport();
 | 
			
		||||
 | 
			
		||||
  await expect(page.locator('.subnav-item:has-text("All") .counter')).toHaveText('4');
 | 
			
		||||
  await expect(page.locator('.subnav-item:has-text("Passed") .counter')).toHaveText('1');
 | 
			
		||||
  await expect(page.locator('.subnav-item:has-text("Failed") .counter')).toHaveText('1');
 | 
			
		||||
  await expect(page.locator('.subnav-item:has-text("Flaky") .counter')).toHaveText('1');
 | 
			
		||||
  await expect(page.locator('.subnav-item:has-text("Skipped") .counter')).toHaveText('1');
 | 
			
		||||
 | 
			
		||||
  await expect(page.locator('.test-summary.outcome-unexpected >> text=fails')).toBeVisible();
 | 
			
		||||
  await expect(page.locator('.test-summary.outcome-flaky >> text=flaky')).toBeVisible();
 | 
			
		||||
  await expect(page.locator('.test-summary.outcome-expected >> text=passes')).toBeVisible();
 | 
			
		||||
  await expect(page.locator('.test-summary.outcome-skipped >> text=skipped')).toBeVisible();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should not throw when attachment is missing', async ({ runInlineTest }) => {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user