Skip to content

Commit

Permalink
feat: support generating static report for playwright (#505)
Browse files Browse the repository at this point in the history
* test: add e2e tests for pwt

* feat: add playwright test-adapter

* feat: add playwright integration

* fix: update server host in GUI e2e tests

* fix: further advancements in static pwt report generation

* chore: rename test-adapter unit test to hermione

* test: implement unit tests for pwt test-adapter and e2e pwt fixture

* fix: add error name by default

* fix: review issues fixes, use pwt delimiter instead of space

* fix: display common page screenshot in pwt report when possible

* fix: various review issue fixes, p-queue, pwt fixture config, pwt separator

* fix: bring back old attempt computing

* test: fixing tests

* fix: review fixes, test fixes, fixing expand retries

* chore: update package-lock

* fix: get rid of replaceAll

* ci: install pwt browsers

* test: fix e2e
  • Loading branch information
shadowusr authored Sep 29, 2023
1 parent d4f9c85 commit d5eb9ac
Show file tree
Hide file tree
Showing 77 changed files with 1,438 additions and 893 deletions.
3 changes: 3 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
cd test/func/docker/browser-utils &&
npm ci &&
npm run install-chromium
- run:
name: Install Chromium for Playwright
command: npx playwright install chromium

- run:
name: Download Selenium
Expand Down
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ hot
/test/func/packages/*/plugin.js
/hermione-report
tmp
**/playwright-report
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ sqlite.db
.nyc_output
tmp

**/playwright-report
hermione-report
test/func/**/report
test/func/**/report-backup
Expand Down
6 changes: 2 additions & 4 deletions lib/common-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,7 @@ export const isNoRefImageError = (error?: unknown): error is NoRefImageError =>
};

export const hasNoRefImageErrors = ({assertViewResults = []}: {assertViewResults?: AssertViewResult[]}): boolean => {
const noRefImageErrors = assertViewResults.filter((assertViewResult: AssertViewResult) => isNoRefImageError(assertViewResult));

return !isEmpty(noRefImageErrors);
return assertViewResults.some((assertViewResult: AssertViewResult) => isNoRefImageError(assertViewResult));
};

const hasFailedImages = (result: {imagesInfo?: ImageInfoFull[]}): boolean => {
Expand Down Expand Up @@ -130,7 +128,7 @@ export const hasDiff = (assertViewResults: AssertViewResult[]): boolean => {
return assertViewResults.some((result) => isImageDiffError(result as {name?: string}));
};

export const isBase64Image = (image: ImageData | ImageBase64 | undefined): image is ImageBase64 => {
export const isBase64Image = (image: ImageData | ImageBase64 | null | undefined): image is ImageBase64 => {
return Boolean((image as ImageBase64 | undefined)?.base64);
};

Expand Down
3 changes: 3 additions & 0 deletions lib/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ export * from './browser';
export * from './database';
export * from './defaults';
export * from './diff-modes';
export * from './group-tests';
export * from './paths';
export * from './tests';
export * from './plugin-events';
export * from './save-formats';
export * from './test-statuses';
export * from './tool-names';
export * from './view-modes';
1 change: 1 addition & 0 deletions lib/constants/tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const PWT_TITLE_DELIMITER = ' › ';
4 changes: 4 additions & 0 deletions lib/constants/tool-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum ToolName {
Hermione = 'hermione',
Playwright = 'playwright'
}
6 changes: 3 additions & 3 deletions lib/db-utils/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import NestedError from 'nested-error-stacks';
import {StaticTestsTreeBuilder} from '../tests-tree-builder/static';
import * as commonSqliteUtils from './common';
import {isUrl, fetchFile, normalizeUrls, logger} from '../common-utils';
import {DATABASE_URLS_JSON_NAME, DB_COLUMNS, LOCAL_DATABASE_NAME, TestStatus} from '../constants';
import {DATABASE_URLS_JSON_NAME, DB_COLUMNS, LOCAL_DATABASE_NAME, TestStatus, ToolName} from '../constants';
import {DbLoadResult, HandleDatabasesOptions} from './common';
import {DbUrlsJsonData, RawSuitesRow, ReporterConfig} from '../types';
import {Tree} from '../tests-tree-builder/base';
Expand Down Expand Up @@ -57,10 +57,10 @@ export async function mergeDatabases(srcDbPaths: string[], reportPath: string):
}
}

export function getTestsTreeFromDatabase(dbPath: string): Tree {
export function getTestsTreeFromDatabase(toolName: ToolName, dbPath: string): Tree {
try {
const db = new Database(dbPath, {readonly: true, fileMustExist: true});
const testsTreeBuilder = StaticTestsTreeBuilder.create();
const testsTreeBuilder = StaticTestsTreeBuilder.create({toolName});

const suitesRows = (db.prepare(commonSqliteUtils.selectAllSuitesQuery())
.raw()
Expand Down
6 changes: 4 additions & 2 deletions lib/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {DiffOptions, ImageData} from '../types';
import {ValueOf} from 'type-fest';

export const ErrorName = {
GENERAL_ERROR: 'Error',
IMAGE_DIFF: 'ImageDiffError',
NO_REF_IMAGE: 'NoRefImageError',
ASSERT_VIEW: 'AssertViewError'
Expand All @@ -20,13 +21,14 @@ export interface ImageDiffError {
refImg: ImageData;
diffClusters: CoordBounds[];
diffBuffer?: ArrayBuffer;
diffImg?: ImageData;
}

export interface NoRefImageError {
name: ErrorNames['NO_REF_IMAGE'];
stateName: string;
message: string;
stack: string;
stack?: string;
currImg: ImageData;
refImg: ImageData;
refImg?: ImageData;
}
3 changes: 2 additions & 1 deletion lib/gui/tool-runner/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const {getShortMD5} = require('../../common-utils');
const {formatId, mkFullTitle, mergeDatabasesForReuse, filterByEqualDiffSizes} = require('./utils');
const {getTestsTreeFromDatabase} = require('../../db-utils/server');
const {formatTestResult} = require('../../server-utils');
const {ToolName} = require('../../constants');

module.exports = class ToolRunner {
static create(paths, hermione, configs) {
Expand Down Expand Up @@ -280,7 +281,7 @@ module.exports = class ToolRunner {
const dbPath = path.resolve(this._reportPath, LOCAL_DATABASE_NAME);

if (await fs.pathExists(dbPath)) {
return getTestsTreeFromDatabase(dbPath);
return getTestsTreeFromDatabase(ToolName.Hermione, dbPath);
}

logger.warn(chalk.yellow(`Nothing to reuse in ${this._reportPath}: can not load data from ${DATABASE_URLS_JSON_NAME}`));
Expand Down
88 changes: 63 additions & 25 deletions lib/image-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,18 @@ import {
ImageInfoFail,
ImageInfoFull,
ImagesSaver,
ImageSize
ImageInfoPageSuccess
} from './types';
import {ERROR, FAIL, PluginEvents, SUCCESS, TestStatus, UPDATED} from './constants';
import {getError, getShortMD5, isImageDiffError, isNoRefImageError, logger, mkTestId} from './common-utils';
import {
getError,
getShortMD5,
isBase64Image,
isImageDiffError,
isNoRefImageError,
logger,
mkTestId
} from './common-utils';
import {ImageDiffError} from './errors';
import {cacheExpectedPaths, cacheAllImages, cacheDiffImages} from './image-cache';
import {ReporterTestResult} from './test-adapter';
Expand Down Expand Up @@ -50,23 +58,38 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter {
return _.get(_.find(assertViewResults, {stateName}), 'currImg');
}

static getDiffImg(assertViewResults: AssertViewResult[], stateName?: string): ImageData | undefined {
return _.get(_.find(assertViewResults, {stateName}), 'diffImg');
}

static getRefImg(assertViewResults: AssertViewResult[], stateName?: string): ImageData | undefined {
return _.get(_.find(assertViewResults, {stateName}), 'refImg');
}

static getScreenshot(testResult: ReporterTestResultPlain): ImageBase64 | undefined {
return testResult.error?.screenshot;
static getScreenshot(testResult: ReporterTestResultPlain): ImageBase64 | ImageData | null | undefined {
return testResult.screenshot;
}

getImagesFor(testResult: ReporterTestResultPlain, assertViewStatus: TestStatus, stateName?: string): ImageInfo | undefined {
const refImg = ImageHandler.getRefImg(testResult.assertViewResults, stateName);
const currImg = ImageHandler.getCurrImg(testResult.assertViewResults, stateName);
const errImg = ImageHandler.getScreenshot(testResult);

const pageImg = ImageHandler.getScreenshot(testResult);

const {path: refPath} = this._getExpectedPath(testResult, stateName);
const currPath = utils.getCurrentPath({attempt: testResult.attempt, browserId: testResult.browserId, imageDir: testResult.imageDir, stateName});
const diffPath = utils.getDiffPath({attempt: testResult.attempt, browserId: testResult.browserId, imageDir: testResult.imageDir, stateName});

// Handling whole page common screenshots
if (!stateName && pageImg) {
return {
actualImg: {
path: this._getImgFromStorage(currPath),
size: pageImg.size
}
};
}

if ((assertViewStatus === SUCCESS || assertViewStatus === UPDATED) && refImg) {
const result: ImageInfo = {
expectedImg: {path: this._getImgFromStorage(refPath), size: refImg.size}
Expand Down Expand Up @@ -98,11 +121,11 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter {
};
}

if (assertViewStatus === ERROR) {
if (assertViewStatus === ERROR && currImg) {
return {
actualImg: {
path: testResult.state?.name ? this._getImgFromStorage(currPath) : '',
size: (currImg?.size || errImg?.size) as ImageSize
path: this._getImgFromStorage(currPath),
size: currImg.size
}
};
}
Expand All @@ -112,7 +135,7 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter {

getImagesInfo(testResult: ReporterTestResultPlain): ImageInfoFull[] {
const imagesInfo: ImageInfoFull[] = testResult.assertViewResults?.map((assertResult): ImageInfoFull => {
let status: TestStatus, error: {message: string; stack: string;} | undefined;
let status: TestStatus, error: {message: string; stack?: string;} | undefined;

if (testResult.isUpdated === true) {
status = UPDATED;
Expand All @@ -134,14 +157,21 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter {
) as ImageInfoFull;
}) ?? [];

// common screenshot on test fail
// Common page screenshot
if (ImageHandler.getScreenshot(testResult)) {
const errorImage = _.extend(
{status: ERROR, error: getError(testResult.error)},
this.getImagesFor(testResult, ERROR)
) as ImageInfoError;
const error = getError(testResult.error);

imagesInfo.push(errorImage);
if (!_.isEmpty(error)) {
imagesInfo.push(_.extend(
{status: ERROR, error},
this.getImagesFor(testResult, ERROR)
) as ImageInfoError);
} else {
imagesInfo.push(_.extend(
{status: SUCCESS},
this.getImagesFor(testResult, SUCCESS)
) as ImageInfoPageSuccess);
}
}

return imagesInfo;
Expand All @@ -158,20 +188,22 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter {
const destCurrPath = utils.getCurrentPath({attempt: testResult.attempt, browserId: testResult.browserId, imageDir: testResult.imageDir, stateName});
const srcCurrPath = ImageHandler.getCurrImg(testResult.assertViewResults, stateName)?.path;

const dstCurrPath = utils.getDiffPath({attempt: testResult.attempt, browserId: testResult.browserId, imageDir: testResult.imageDir, stateName});
const srcDiffPath = path.resolve(tmp.tmpdir, dstCurrPath);
const destDiffPath = utils.getDiffPath({attempt: testResult.attempt, browserId: testResult.browserId, imageDir: testResult.imageDir, stateName});
const srcDiffPath = ImageHandler.getDiffImg(assertViewResults, stateName)?.path ?? path.resolve(tmp.tmpdir, destDiffPath);
const actions: unknown[] = [];

if (!(assertResult instanceof Error)) {
actions.push(this._saveImg(srcRefPath, destRefPath));
}

if (isImageDiffError(assertResult)) {
await this._saveDiffInWorker(assertResult, srcDiffPath, worker);
if (!assertResult.diffImg) {
await this._saveDiffInWorker(assertResult, srcDiffPath, worker);
}

actions.push(
this._saveImg(srcCurrPath, destCurrPath),
this._saveImg(srcDiffPath, dstCurrPath)
this._saveImg(srcDiffPath, destDiffPath)
);

if (!reusedReference) {
Expand All @@ -187,7 +219,7 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter {
}));

if (ImageHandler.getScreenshot(testResult)) {
await this._saveErrorScreenshot(testResult);
await this._savePageScreenshot(testResult);
}

await this.emitAsync(PluginEvents.TEST_SCREENSHOTS_SAVED, {
Expand Down Expand Up @@ -289,18 +321,24 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter {
cacheDiffImages.set(hash, destPath);
}

private async _saveErrorScreenshot(testResult: ReporterTestResultPlain): Promise<void> {
private async _savePageScreenshot(testResult: ReporterTestResultPlain): Promise<void> {
const screenshot = ImageHandler.getScreenshot(testResult);
if (!screenshot?.base64) {
if (!(screenshot as ImageBase64)?.base64 && !(screenshot as ImageData)?.path) {
logger.warn('Cannot save screenshot on reject');

return Promise.resolve();
}

const currPath = utils.getCurrentPath({attempt: testResult.attempt, browserId: testResult.browserId, imageDir: testResult.imageDir});
const localPath = path.resolve(tmp.tmpdir, currPath);
await utils.makeDirFor(localPath);
await fs.writeFile(localPath, new Buffer(screenshot.base64, 'base64'), 'base64');
let localPath: string;

if (isBase64Image(screenshot)) {
localPath = path.resolve(tmp.tmpdir, currPath);
await utils.makeDirFor(localPath);
await fs.writeFile(localPath, new Buffer(screenshot.base64, 'base64'), 'base64');
} else {
localPath = screenshot?.path as string;
}

await this._saveImg(localPath, currPath);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/image-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ export class SqliteImageStore implements ImageStore {
}, suitePathString, browserName);

const imagesInfo: ImageInfoFull[] = imagesInfoResult && JSON.parse(imagesInfoResult[DB_COLUMNS.IMAGES_INFO as keyof Pick<LabeledSuitesRow, 'imagesInfo'>]) || [];
return imagesInfo.find(info => info.stateName === stateName);
return imagesInfo.find(info => (info as {stateName?: string}).stateName === stateName);
}
}
3 changes: 2 additions & 1 deletion lib/plugin-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as utils from './server-utils';
import {cliCommands} from './cli-commands';
import {HtmlReporter} from './plugin-api';
import {HtmlReporterApi, ReporterConfig, ReporterOptions} from './types';
import {ToolName} from './constants';

type PrepareFn = (hermione: Hermione & HtmlReporterApi, reportBuilder: StaticReportBuilder, config: ReporterConfig) => Promise<void>;

Expand All @@ -33,7 +34,7 @@ export class PluginAdapter {
}

addApi(): this {
this._hermione.htmlReporter = HtmlReporter.create(this._config);
this._hermione.htmlReporter = HtmlReporter.create(this._config, {toolName: ToolName.Hermione});
return this;
}

Expand Down
27 changes: 20 additions & 7 deletions lib/plugin-api.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,44 @@
import EventsEmitter2 from 'eventemitter2';
import {PluginEvents} from './constants';
import {downloadDatabases, mergeDatabases, getTestsTreeFromDatabase} from './db-utils/server';
import {PluginEvents, ToolName} from './constants';
import {downloadDatabases, getTestsTreeFromDatabase, mergeDatabases} from './db-utils/server';
import {LocalImagesSaver} from './local-images-saver';
import {version} from '../package.json';
import {ImagesSaver, ReporterConfig, ReportsSaver} from './types';

interface HtmlReporterValues {
toolName: ToolName;
extraItems: Record<string, string>;
metaInfoExtenders: Record<string, string>;
imagesSaver: ImagesSaver;
reportsSaver: ReportsSaver | null;
}

interface ReporterOptions {
toolName: ToolName;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ParametersExceptFirst<F> = F extends (arg0: any, ...rest: infer R) => any ? R : never;

export class HtmlReporter extends EventsEmitter2 {
protected _config: ReporterConfig;
protected _values: HtmlReporterValues;
protected _version: string;

static create<T extends HtmlReporter>(this: new (config: ReporterConfig) => T, config: ReporterConfig): T {
return new this(config);
static create<T extends HtmlReporter>(
this: new (config: ReporterConfig, options?: Partial<ReporterOptions>) => T,
config: ReporterConfig,
options?: Partial<ReporterOptions>
): T {
return new this(config, options);
}

constructor(config: ReporterConfig) {
constructor(config: ReporterConfig, {toolName}: Partial<ReporterOptions> = {}) {
super();

this._config = config;
this._values = {
toolName: toolName ?? ToolName.Hermione,
extraItems: {},
metaInfoExtenders: {},
imagesSaver: LocalImagesSaver,
Expand Down Expand Up @@ -91,7 +104,7 @@ export class HtmlReporter extends EventsEmitter2 {
return mergeDatabases(...args);
}

getTestsTreeFromDatabase(...args: Parameters<typeof getTestsTreeFromDatabase>): ReturnType<typeof getTestsTreeFromDatabase> {
return getTestsTreeFromDatabase(...args);
getTestsTreeFromDatabase(...args: ParametersExceptFirst<typeof getTestsTreeFromDatabase>): ReturnType<typeof getTestsTreeFromDatabase> {
return getTestsTreeFromDatabase(this.values.toolName, ...args);
}
}
Loading

0 comments on commit d5eb9ac

Please sign in to comment.