Skip to content

Commit

Permalink
feat: support playwright in gui mode
Browse files Browse the repository at this point in the history
  • Loading branch information
DudaGod committed Jul 11, 2024
1 parent 2a8e8d9 commit 882ad47
Show file tree
Hide file tree
Showing 21 changed files with 1,908 additions and 17 deletions.
115 changes: 115 additions & 0 deletions lib/adapters/config/playwright.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import path from 'node:path';
import crypto from 'node:crypto';
import _ from 'lodash';

import type {ConfigAdapter} from './index';
import type {FullConfig, FullProject} from '@playwright/test/reporter';
import type {TestAdapter} from '../test';

export type PwtProject = FullProject & {
snapshotPathTemplate?: string;
};

export type PwtConfig = FullConfig & {
testDir?: string;
snapshotDir?: string;
snapshotPathTemplate?: string;
projects?: PwtProject[]
}

export const DEFAULT_BROWSER_ID = 'chromium';
const DEFAULT_SNAPSHOT_PATH_TEMPLATE = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';

export class PlaywrightConfigAdapter implements ConfigAdapter {
private _config: PwtConfig;
private _browserIds: string[];

static create<T extends PlaywrightConfigAdapter>(this: new (config: PwtConfig) => T, config: PwtConfig): T {
return new this(config);
}

constructor(config: PwtConfig) {
this._config = config;
this._browserIds = _.isEmpty(this._config.projects) ? [DEFAULT_BROWSER_ID] : this._config.projects.map(prj => prj.name).filter(Boolean);
}

get original(): PwtConfig {
return this._config;
}

get tolerance(): number {
return 2.3;
}

get antialiasingTolerance(): number {
return 4;
}

get browserIds(): string[] {
return this._browserIds;
}

// used from pwt - /~https://github.com/microsoft/playwright/blob/v1.45.1/packages/playwright/src/worker/testInfo.ts#L452-L473
getScreenshotPath(test: TestAdapter, stateName: string): string {
const subPath = `${stateName}.png`;
const parsedSubPath = path.parse(subPath);
const parsedRelativeTestFilePath = path.parse(test.file);

const currProject = (this._config.projects || []).find(prj => prj.name === test.browserId) as PwtProject || {};
const projectNamePathSegment = sanitizeForFilePath(test.browserId);

const snapshotPathTemplate = currProject.snapshotPathTemplate || this._config.snapshotPathTemplate || DEFAULT_SNAPSHOT_PATH_TEMPLATE;

const testDir = path.resolve(currProject.testDir || this._config.testDir || '');
let snapshotDir = currProject.snapshotDir || this._config.snapshotDir;
snapshotDir = snapshotDir ? path.resolve(snapshotDir) : testDir;

const snapshotSuffix = process.platform;

const snapshotPath = snapshotPathTemplate
.replace(/\{(.)?testDir\}/g, '$1' + testDir)
.replace(/\{(.)?snapshotDir\}/g, '$1' + snapshotDir)
.replace(/\{(.)?snapshotSuffix\}/g, snapshotSuffix ? '$1' + snapshotSuffix : '')
.replace(/\{(.)?testFileDir\}/g, '$1' + parsedRelativeTestFilePath.dir)
.replace(/\{(.)?platform\}/g, '$1' + process.platform)
.replace(/\{(.)?projectName\}/g, projectNamePathSegment ? '$1' + projectNamePathSegment : '')
.replace(/\{(.)?testName\}/g, '$1' + fsSanitizedTestName(test.titlePath))
.replace(/\{(.)?testFileName\}/g, '$1' + parsedRelativeTestFilePath.base)
.replace(/\{(.)?testFilePath\}/g, '$1' + test.file)
.replace(/\{(.)?arg\}/g, '$1' + path.join(parsedSubPath.dir, parsedSubPath.name))
.replace(/\{(.)?ext\}/g, parsedSubPath.ext ? '$1' + parsedSubPath.ext : '');

return path.normalize(path.resolve(snapshotPath));
}
}

function sanitizeForFilePath(s: string): string {
// eslint-disable-next-line no-control-regex
return s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
}

function fsSanitizedTestName(titlePath: string[]): string {
const fullTitleWithoutSpec = titlePath.join(' ');

return sanitizeForFilePath(trimLongString(fullTitleWithoutSpec));
}

function trimLongString(s: string, length = 100): string {
if (s.length <= length) {
return s;
}

const hash = calculateSha1(s);
const middle = `-${hash.substring(0, 5)}-`;
const start = Math.floor((length - middle.length) / 2);
const end = length - middle.length - start;

return s.substring(0, start) + middle + s.slice(-end);
}

function calculateSha1(buffer: Buffer | string): string {
const hash = crypto.createHash('sha1');
hash.update(buffer);

return hash.digest('hex');
}
21 changes: 21 additions & 0 deletions lib/adapters/test-collection/playwright.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {PlaywrightTestAdapter, type PwtRawTest} from '../test/playwright';
import type {TestCollectionAdapter} from './';

export class PlaywrightTestCollectionAdapter implements TestCollectionAdapter {
private _testAdapters: PlaywrightTestAdapter[];

static create<T>(
this: new (tests: PwtRawTest[]) => T,
tests: PwtRawTest[]
): T {
return new this(tests);
}

constructor(tests: PwtRawTest[]) {
this._testAdapters = tests.map(test => PlaywrightTestAdapter.create(test));
}

get tests(): PlaywrightTestAdapter[] {
return this._testAdapters;
}
}
2 changes: 1 addition & 1 deletion lib/adapters/test-collection/testplane.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {TestplaneTestAdapter} from '../test/testplane';
import type {TestCollectionAdapter} from './index';
import type {TestCollectionAdapter} from './';
import type {TestCollection} from 'testplane';

export class TestplaneTestCollectionAdapter implements TestCollectionAdapter {
Expand Down
51 changes: 44 additions & 7 deletions lib/adapters/test-result/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,27 @@ import stripAnsi from 'strip-ansi';

import {ReporterTestResult} from './index';
import {getError, getShortMD5, isImageDiffError, isNoRefImageError} from '../../common-utils';
import {ERROR, FAIL, DEFAULT_TITLE_DELIMITER, SUCCESS, TestStatus} from '../../constants';
import {ERROR, FAIL, SUCCESS, UPDATED, TestStatus, DEFAULT_TITLE_DELIMITER} from '../../constants';
import {ErrorName} from '../../errors';
import {
DiffOptions,
ErrorDetails,
ImageFile,
ImageInfoDiff,
ImageInfoFull, ImageInfoNoRef, ImageInfoPageError, ImageInfoPageSuccess, ImageInfoSuccess,
ImageInfoFull, ImageInfoNoRef, ImageInfoPageError, ImageInfoPageSuccess, ImageInfoSuccess, ImageInfoUpdated,
ImageSize,
TestError
} from '../../types';
import type {CoordBounds} from 'looks-same';

export type PlaywrightAttachment = PlaywrightTestResult['attachments'][number];
export type PlaywrightAttachment = PlaywrightTestResult['attachments'][number] & {
relativePath?: string,
size?: {
width: number;
height: number;
},
isUpdated?: boolean
};

type ExtendedError<T> = TestError & {meta?: T & {type: string}};

Expand All @@ -32,7 +39,7 @@ export enum PwtTestStatus {
FAILED = 'failed',
TIMED_OUT = 'timedOut',
INTERRUPTED = 'interrupted',
SKIPPED = 'skipped',
SKIPPED = 'skipped'
}

export enum ImageTitleEnding {
Expand All @@ -42,13 +49,25 @@ export enum ImageTitleEnding {
Previous = '-previous.png'
}

export interface TestResultWithGuiStatus extends Omit<PlaywrightTestResult, 'status'> {
status: PlaywrightTestResult['status'] | TestStatus.RUNNING | TestStatus.UPDATED;
}

const ANY_IMAGE_ENDING_REGEXP = new RegExp(Object.values(ImageTitleEnding).map(ending => `${ending}$`).join('|'));

export const DEFAULT_DIFF_OPTIONS = {
diffColor: '#ff00ff'
} satisfies Partial<DiffOptions>;

export const getStatus = (result: PlaywrightTestResult): TestStatus => {
export const getStatus = (result: TestResultWithGuiStatus): TestStatus => {
if (result.status === TestStatus.RUNNING) {
return TestStatus.RUNNING;
}

if (result.status === TestStatus.UPDATED) {
return TestStatus.UPDATED;
}

if (result.status === PwtTestStatus.PASSED) {
return TestStatus.SUCCESS;
}
Expand Down Expand Up @@ -127,7 +146,8 @@ const getImageData = (attachment: PlaywrightAttachment | undefined): ImageFile |

return {
path: attachment.path as string,
size: _.pick(sizeOf(attachment.path as string), ['height', 'width']) as ImageSize
size: !attachment.size ? _.pick(sizeOf(attachment.path as string), ['height', 'width']) as ImageSize : attachment.size,
...(attachment.relativePath ? {relativePath: attachment.relativePath} : {})
};
};

Expand All @@ -136,6 +156,15 @@ export class PlaywrightTestResultAdapter implements ReporterTestResult {
private readonly _testResult: PlaywrightTestResult;
private _attempt: number;

static create<T extends PlaywrightTestResultAdapter>(
this: new (testCase: PlaywrightTestCase, testResult: PlaywrightTestResult, attempt: number) => T,
testCase: PlaywrightTestCase,
testResult: PlaywrightTestResult,
attempt: number
): T {
return new this(testCase, testResult, attempt);
}

constructor(testCase: PlaywrightTestCase, testResult: PlaywrightTestResult, attempt: number) {
this._testCase = testCase;
this._testResult = testResult;
Expand Down Expand Up @@ -165,7 +194,7 @@ export class PlaywrightTestResultAdapter implements ReporterTestResult {
result.stack = stack;
}

if (message.includes('snapshot doesn\'t exist') && message.includes('.png')) {
if (/snapshot .*doesn't exist/.test(message) && message.includes('.png')) {
result.name = ErrorName.NO_REF_IMAGE;
} else if (message.includes('Screenshot comparison failed')) {
result.name = ErrorName.IMAGE_DIFF;
Expand Down Expand Up @@ -230,6 +259,14 @@ export class PlaywrightTestResultAdapter implements ReporterTestResult {
error: _.pick(error, ['message', 'name', 'stack']),
actualImg
} satisfies ImageInfoNoRef;
} else if (expectedAttachment?.isUpdated && expectedImg && actualImg) {
return {
status: UPDATED,
stateName: state,
refImg: _.clone(expectedImg),
expectedImg,
actualImg
} satisfies ImageInfoUpdated;
} else if (!error && expectedImg) {
return {
status: SUCCESS,
Expand Down
2 changes: 2 additions & 0 deletions lib/adapters/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface TestAdapter {
readonly silentlySkipped: boolean;
readonly browserId: string;
readonly fullName: string;
readonly file: string;
readonly titlePath: string[];

createTestResult(opts: CreateTestResultOpts): ReporterTestResult;
}
Loading

0 comments on commit 882ad47

Please sign in to comment.