Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support generating static report for playwright #505

Merged
merged 18 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 => {
shadowusr marked this conversation as resolved.
Show resolved Hide resolved
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) {
shadowusr marked this conversation as resolved.
Show resolved Hide resolved
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
Loading