diff --git a/.circleci/config.yml b/.circleci/config.yml index 32a2d8e1f..d4029cf5c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/.eslintignore b/.eslintignore index 7633041e5..89b59a1d3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -11,3 +11,4 @@ hot /test/func/packages/*/plugin.js /hermione-report tmp +**/playwright-report diff --git a/.gitignore b/.gitignore index 6fb2d61f3..3362220f0 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ sqlite.db .nyc_output tmp +**/playwright-report hermione-report test/func/**/report test/func/**/report-backup diff --git a/lib/common-utils.ts b/lib/common-utils.ts index 76f2fef9a..9e8d7311a 100644 --- a/lib/common-utils.ts +++ b/lib/common-utils.ts @@ -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 => { @@ -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); }; diff --git a/lib/constants/index.ts b/lib/constants/index.ts index be8296aa2..ea288b471 100644 --- a/lib/constants/index.ts +++ b/lib/constants/index.ts @@ -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'; diff --git a/lib/constants/tests.ts b/lib/constants/tests.ts new file mode 100644 index 000000000..072614418 --- /dev/null +++ b/lib/constants/tests.ts @@ -0,0 +1 @@ +export const PWT_TITLE_DELIMITER = ' › '; diff --git a/lib/constants/tool-names.ts b/lib/constants/tool-names.ts new file mode 100644 index 000000000..9fddacf89 --- /dev/null +++ b/lib/constants/tool-names.ts @@ -0,0 +1,4 @@ +export enum ToolName { + Hermione = 'hermione', + Playwright = 'playwright' +} diff --git a/lib/db-utils/server.ts b/lib/db-utils/server.ts index a814d4845..fbdb54560 100644 --- a/lib/db-utils/server.ts +++ b/lib/db-utils/server.ts @@ -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'; @@ -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() diff --git a/lib/errors/index.ts b/lib/errors/index.ts index 6e51602aa..0e6b1d449 100644 --- a/lib/errors/index.ts +++ b/lib/errors/index.ts @@ -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' @@ -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; } diff --git a/lib/gui/tool-runner/index.js b/lib/gui/tool-runner/index.js index 4abd9d9e7..60865352a 100644 --- a/lib/gui/tool-runner/index.js +++ b/lib/gui/tool-runner/index.js @@ -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) { @@ -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}`)); diff --git a/lib/image-handler.ts b/lib/image-handler.ts index f146c1fd5..aa988d54a 100644 --- a/lib/image-handler.ts +++ b/lib/image-handler.ts @@ -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'; @@ -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} @@ -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 } }; } @@ -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; @@ -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; @@ -158,8 +188,8 @@ 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)) { @@ -167,11 +197,13 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter { } 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) { @@ -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, { @@ -289,18 +321,24 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter { cacheDiffImages.set(hash, destPath); } - private async _saveErrorScreenshot(testResult: ReporterTestResultPlain): Promise { + private async _savePageScreenshot(testResult: ReporterTestResultPlain): Promise { 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); } diff --git a/lib/image-store.ts b/lib/image-store.ts index 2117048f4..bdbb6db79 100644 --- a/lib/image-store.ts +++ b/lib/image-store.ts @@ -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]) || []; - return imagesInfo.find(info => info.stateName === stateName); + return imagesInfo.find(info => (info as {stateName?: string}).stateName === stateName); } } diff --git a/lib/plugin-adapter.ts b/lib/plugin-adapter.ts index 401979a62..4c08c3e08 100644 --- a/lib/plugin-adapter.ts +++ b/lib/plugin-adapter.ts @@ -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; @@ -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; } diff --git a/lib/plugin-api.ts b/lib/plugin-api.ts index e99fc3faf..d8db5556d 100644 --- a/lib/plugin-api.ts +++ b/lib/plugin-api.ts @@ -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; metaInfoExtenders: Record; imagesSaver: ImagesSaver; reportsSaver: ReportsSaver | null; } +interface ReporterOptions { + toolName: ToolName; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ParametersExceptFirst = 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(this: new (config: ReporterConfig) => T, config: ReporterConfig): T { - return new this(config); + static create( + this: new (config: ReporterConfig, options?: Partial) => T, + config: ReporterConfig, + options?: Partial + ): T { + return new this(config, options); } - constructor(config: ReporterConfig) { + constructor(config: ReporterConfig, {toolName}: Partial = {}) { super(); this._config = config; this._values = { + toolName: toolName ?? ToolName.Hermione, extraItems: {}, metaInfoExtenders: {}, imagesSaver: LocalImagesSaver, @@ -91,7 +104,7 @@ export class HtmlReporter extends EventsEmitter2 { return mergeDatabases(...args); } - getTestsTreeFromDatabase(...args: Parameters): ReturnType { - return getTestsTreeFromDatabase(...args); + getTestsTreeFromDatabase(...args: ParametersExceptFirst): ReturnType { + return getTestsTreeFromDatabase(this.values.toolName, ...args); } } diff --git a/lib/report-builder/gui.js b/lib/report-builder/gui.js index e074a5d04..456f452d1 100644 --- a/lib/report-builder/gui.js +++ b/lib/report-builder/gui.js @@ -6,10 +6,10 @@ const path = require('path'); const {StaticReportBuilder} = require('./static'); const GuiTestsTreeBuilder = require('../tests-tree-builder/gui'); const {IDLE, RUNNING, SKIPPED, FAIL, SUCCESS, UPDATED} = require('../constants/test-statuses'); -const {hasResultFails, hasNoRefImageErrors} = require('../static/modules/utils'); -const {isSkippedStatus, isUpdatedStatus} = require('../common-utils'); +const {isSkippedStatus, isUpdatedStatus, hasNoRefImageErrors, hasResultFails} = require('../common-utils'); const {getConfigForStaticFile, deleteFile} = require('../server-utils'); const {DB_COLUMNS} = require('../constants/database'); +const {ToolName} = require('../constants'); module.exports = class GuiReportBuilder extends StaticReportBuilder { static create(...args) { @@ -19,7 +19,7 @@ module.exports = class GuiReportBuilder extends StaticReportBuilder { constructor(...args) { super(...args); - this._testsTree = GuiTestsTreeBuilder.create(); + this._testsTree = GuiTestsTreeBuilder.create({toolName: ToolName.Hermione}); this._skips = []; this._apiValues = {}; } diff --git a/lib/report-builder/static.ts b/lib/report-builder/static.ts index e779494e1..dbb43d809 100644 --- a/lib/report-builder/static.ts +++ b/lib/report-builder/static.ts @@ -16,13 +16,12 @@ import { } from '../constants'; import {PreparedTestResult, SqliteAdapter} from '../sqlite-adapter'; import {ReporterTestResult} from '../test-adapter'; -import {hasNoRefImageErrors} from '../static/modules/utils'; import {hasImage, saveStaticFilesToReportDir, writeDatabaseUrlsFile} from '../server-utils'; import {ReporterConfig} from '../types'; import {HtmlReporter} from '../plugin-api'; import {ImageHandler} from '../image-handler'; import {SqliteImageStore} from '../image-store'; -import {getUrlWithBase, getError, getRelativeUrl, hasDiff} from '../common-utils'; +import {getUrlWithBase, getError, getRelativeUrl, hasDiff, hasNoRefImageErrors} from '../common-utils'; import {getTestFromDb} from '../db-utils/server'; const ignoredStatuses = [RUNNING, IDLE]; diff --git a/lib/static/components/prop-types.ts b/lib/static/components/prop-types.ts new file mode 100644 index 000000000..a06bf884f --- /dev/null +++ b/lib/static/components/prop-types.ts @@ -0,0 +1,9 @@ +import PropTypes from 'prop-types'; + +export const ImageData = PropTypes.shape({ + path: PropTypes.string.isRequired, + size: PropTypes.shape({ + height: PropTypes.number.isRequired, + width: PropTypes.number.isRequired + }).isRequired +}); diff --git a/lib/static/components/section/body/meta-info/content.jsx b/lib/static/components/section/body/meta-info/content.jsx index 8672386e6..d4f008b89 100644 --- a/lib/static/components/section/body/meta-info/content.jsx +++ b/lib/static/components/section/body/meta-info/content.jsx @@ -90,9 +90,11 @@ class MetaInfoContent extends Component { const extraMetaInfo = this.getExtraMetaInfo(); const formattedMetaInfo = { ...serializedMetaValues, - ...extraMetaInfo, - url: mkLinkToUrl(getUrlWithBase(result.suiteUrl, baseHost), result.metaInfo.url) + ...extraMetaInfo }; + if (result.suiteUrl) { + formattedMetaInfo.url = mkLinkToUrl(getUrlWithBase(result.suiteUrl, baseHost), result.metaInfo.url); + } return metaToElements(formattedMetaInfo, metaInfoBaseUrls); } diff --git a/lib/static/components/section/body/page-screenshot.tsx b/lib/static/components/section/body/page-screenshot.tsx new file mode 100644 index 000000000..f2e87424b --- /dev/null +++ b/lib/static/components/section/body/page-screenshot.tsx @@ -0,0 +1,18 @@ +import React, {Component} from 'react'; +import Details from '../../details'; +import ResizedScreenshot from '../../state/screenshot/resized'; +import {ImageData} from '../../../../types'; + +interface PageScreenshotProps { + image: ImageData; +} + +export class PageScreenshot extends Component { + render(): JSX.Element { + return
} + extendClassNames="details_type_image" + />; + } +} diff --git a/lib/static/components/section/body/result.jsx b/lib/static/components/section/body/result.jsx index 607f23222..94b850fa7 100644 --- a/lib/static/components/section/body/result.jsx +++ b/lib/static/components/section/body/result.jsx @@ -1,3 +1,4 @@ +import {pick} from 'lodash'; import React, {Component, Fragment} from 'react'; import {connect} from 'react-redux'; import PropTypes from 'prop-types'; @@ -7,6 +8,8 @@ import Description from './description'; import Tabs from './tabs'; import ExtensionPoint from '../../extension-point'; import {RESULT_META} from '../../../../constants/extension-points'; +import {PageScreenshot} from './page-screenshot'; +import * as projectPropTypes from '../../prop-types'; class Result extends Component { static propTypes = { @@ -17,25 +20,36 @@ class Result extends Component { status: PropTypes.string.isRequired, imageIds: PropTypes.array.isRequired, description: PropTypes.string - }).isRequired + }).isRequired, + pageScreenshot: PropTypes.shape({ + actualImg: projectPropTypes.ImageData.isRequired + }) }; render() { - const {result, resultId, testName} = this.props; + const {result, resultId, testName, pageScreenshot} = this.props; return ( - - + + - {result.description && } - + {result.description && } + + {pageScreenshot &&
} + {pageScreenshot && }
); } } export default connect( - ({tree}, {resultId}) => ({result: tree.results.byId[resultId]}) + ({tree}, {resultId}) => { + const result = tree.results.byId[resultId]; + const images = Object.values(pick(tree.images.byId, result.imageIds)); + const pageScreenshot = images.find(image => !image.stateName && image.actualImg); + + return {result, pageScreenshot}; + } )(Result); diff --git a/lib/static/components/section/body/tabs.jsx b/lib/static/components/section/body/tabs.jsx index c414748ca..2b5777502 100644 --- a/lib/static/components/section/body/tabs.jsx +++ b/lib/static/components/section/body/tabs.jsx @@ -1,17 +1,19 @@ import React, {Component} from 'react'; +import {connect} from 'react-redux'; import PropTypes from 'prop-types'; import {isEmpty} from 'lodash'; import State from '../../state'; -import {isSuccessStatus, isErrorStatus} from '../../../../common-utils'; +import {isSuccessStatus, isErrorStatus, isSkippedStatus} from '../../../../common-utils'; -export default class Tabs extends Component { +class Tabs extends Component { static propTypes = { result: PropTypes.shape({ id: PropTypes.string.isRequired, status: PropTypes.string.isRequired, imageIds: PropTypes.array.isRequired, multipleTabs: PropTypes.bool.isRequired, - screenshot: PropTypes.bool.isRequired + screenshot: PropTypes.bool.isRequired, + error: PropTypes.object }).isRequired }; @@ -38,9 +40,14 @@ export default class Tabs extends Component { const errorTabId = `${result.id}_error`; if (isEmpty(result.imageIds)) { - return isSuccessStatus(result.status) - ? null - : this._drawTab({key: errorTabId}); + if (isSuccessStatus(result.status)) { + return null; + } + if (isSkippedStatus(result.status) && isEmpty(result.error)) { + return null; + } + + return this._drawTab({key: errorTabId}); } const tabs = result.imageIds.map((imageId) => this._drawTab({key: imageId, imageId})); @@ -50,3 +57,12 @@ export default class Tabs extends Component { : tabs; } } + +export default connect( + ({tree}, {result}) => { + const filteredResult = {...result}; + filteredResult.imageIds = filteredResult.imageIds.filter(imageId => tree.images.byId[imageId].stateName); + + return {result: filteredResult}; + } +)(Tabs); diff --git a/lib/static/components/state/state-error.jsx b/lib/static/components/state/state-error.jsx index 226541695..0b7e88b01 100644 --- a/lib/static/components/state/state-error.jsx +++ b/lib/static/components/state/state-error.jsx @@ -39,24 +39,13 @@ class StateError extends Component { _drawImage() { const {image, error} = this.props; - if (!image.actualImg) { - return null; + if (image.actualImg && isNoRefImageError(error)) { + return ; } - return isNoRefImageError(error) - ? - :
} - extendClassNames="details_type_image" - onClick={this.onTogglePageScreenshot} - />; + return null; } - onTogglePageScreenshot = () => { - this.props.actions.togglePageScreenshot(); - }; - _errorToElements(error) { return map(error, (value, key) => { if (!value) { diff --git a/lib/static/modules/actions.js b/lib/static/modules/actions.js index 62d1f3336..60197eff6 100644 --- a/lib/static/modules/actions.js +++ b/lib/static/modules/actions.js @@ -11,6 +11,7 @@ import {fetchDataFromDatabases, mergeDatabases, connectToDatabase, getMainDataba import {setFilteredBrowsers} from './query-params'; import plugins from './plugins'; import performanceMarks from '../../constants/performance-marks'; +import {ToolName} from '../../constants'; export const createNotification = (id, status, message, props = {}) => { const notificationProps = { @@ -91,15 +92,16 @@ export const initStaticReport = () => { await plugins.loadAll(dataFromStaticFile.config); performance?.mark?.(performanceMarks.PLUGINS_LOADED); + const {toolName = ToolName.Hermione} = dataFromStaticFile.apiValues; + const testsTreeBuilder = StaticTestsTreeBuilder.create({toolName}); if (!db || isEmpty(fetchDbDetails)) { return dispatch({ type: actionNames.INIT_STATIC_REPORT, - payload: {...dataFromStaticFile, db, fetchDbDetails, tree: {suites: []}, stats: [], skips: [], browsers: []} + payload: {...dataFromStaticFile, db, fetchDbDetails, tree: testsTreeBuilder.build([]).tree, stats: {}, skips: [], browsers: []} }); } - const testsTreeBuilder = StaticTestsTreeBuilder.create(); const suitesRows = getSuitesTableRows(db); performance?.mark?.(performanceMarks.DB_EXTRACTED_ROWS); diff --git a/lib/static/modules/reducers/grouped-tests/by/result.js b/lib/static/modules/reducers/grouped-tests/by/result.js index bda822228..43efaacb4 100644 --- a/lib/static/modules/reducers/grouped-tests/by/result.js +++ b/lib/static/modules/reducers/grouped-tests/by/result.js @@ -1,8 +1,8 @@ import {get} from 'lodash'; import {handleActiveResults, addGroupItem, sortGroupValues} from '../helpers'; -import {isAssertViewError} from '../../../utils'; import {ensureDiffProperty} from '../../../utils/state'; import {ERROR_KEY, RESULT_KEYS} from '../../../../../constants/group-tests'; +import {isAssertViewError} from '../../../../../common-utils'; const imageComparisonErrorMessage = 'image comparison failed'; diff --git a/lib/static/modules/reducers/tree/nodes/suites.js b/lib/static/modules/reducers/tree/nodes/suites.js index 6f252d5cf..5a51430a3 100644 --- a/lib/static/modules/reducers/tree/nodes/suites.js +++ b/lib/static/modules/reducers/tree/nodes/suites.js @@ -116,11 +116,11 @@ export function calcSuitesOpenness({tree, expand, suiteIds = [], diff = tree}) { youngestSuites.forEach((suite) => { const shouldBeOpened = calcSuiteOpenness(suite, expand, tree); - changeSuiteState(tree, suite.id, {shouldBeOpened}); + changeSuiteState(tree, suite.id, {shouldBeOpened}, diff); }); const changeParentSuiteCb = (parentSuite) => { - changeSuiteState(tree, parentSuite.id, {shouldBeOpened: shouldSuiteBeOpened(parentSuite, tree)}, diff); + changeSuiteState(tree, parentSuite.id, {shouldBeOpened: shouldSuiteBeOpened(parentSuite, tree, diff)}, diff); }; calcParentSuitesState(youngestSuites, tree, changeParentSuiteCb); diff --git a/lib/static/modules/utils.js b/lib/static/modules/utils.js deleted file mode 100644 index df0b1a7cb..000000000 --- a/lib/static/modules/utils.js +++ /dev/null @@ -1,218 +0,0 @@ -'use strict'; - -const {get, isEmpty, find, isFunction, flatMap, isPlainObject, isUndefined} = require('lodash'); -const {isIdleStatus, isSuccessStatus, isUpdatedStatus, isFailStatus, isErrorStatus, isSkippedStatus} = require('../../common-utils'); -const {getCommonErrors} = require('../../constants/errors'); -const {ViewMode} = require('../../constants/view-modes'); -const {SECTIONS, RESULT_KEYS, KEY_DELIMITER} = require('../../constants/group-tests'); - -const AVAILABLE_GROUP_SECTIONS = Object.values(SECTIONS); -const {NO_REF_IMAGE_ERROR, ASSERT_VIEW_ERROR} = getCommonErrors(); - -function hasFailedImages(result) { - const {imagesInfo = []} = result; - - return imagesInfo.some(({error, status}) => !isAssertViewError(error) && (isErrorStatus(status) || isFailStatus(status))); -} - -function isNoRefImageError(error) { - const stack = get(error, 'stack', ''); - return stack.startsWith(NO_REF_IMAGE_ERROR); -} - -function isAssertViewError(error) { - const stack = get(error, 'stack', ''); - return stack.startsWith(ASSERT_VIEW_ERROR); -} - -function hasNoRefImageErrors({imagesInfo = []}) { - return Boolean(imagesInfo.filter(({error}) => isNoRefImageError(error)).length); -} - -function hasResultFails(testResult) { - return hasFailedImages(testResult) || isErrorStatus(testResult.status) || isFailStatus(testResult.status); -} - -function isSuiteIdle(suite) { - return isIdleStatus(suite.status); -} - -function isSuiteSuccessful(suite) { - return isSuccessStatus(suite.status); -} - -function isNodeFailed(node) { - return isFailStatus(node.status) || isErrorStatus(node.status); -} - -function isNodeSuccessful(node) { - return isSuccessStatus(node.status) || isUpdatedStatus(node.status); -} - -function isAcceptable({status, error}) { - return isErrorStatus(status) && isNoRefImageError(error) || isFailStatus(status) || isSkippedStatus(status); -} - -function isScreenRevertable({gui, image, isLastResult}) { - return gui && image.stateName && isLastResult && isUpdatedStatus(image.status); -} - -function dateToLocaleString(date) { - if (!date) { - return ''; - } - const lang = isEmpty(navigator.languages) ? navigator.language : navigator.languages[0]; - return new Date(date).toLocaleString(lang); -} - -function getHttpErrorMessage(error) { - const {message, response} = error; - - return response ? `(${response.status}) ${response.data}` : message; -} - -function isTestNameMatchFilters(testName, testNameFilter, strictMatchFilter) { - if (!testNameFilter) { - return true; - } - - return strictMatchFilter - ? testName === testNameFilter - : testName.includes(testNameFilter); -} - -function isBrowserMatchViewMode(browser, lastResult, viewMode, diff = browser) { - const {status} = lastResult; - - if (viewMode === ViewMode.ALL) { - return true; - } - - if (viewMode === ViewMode.PASSED && isSuccessStatus(status)) { - return true; - } - - if (viewMode === ViewMode.FAILED && (isFailStatus(status) || isErrorStatus(status))) { - return true; - } - - if (viewMode === ViewMode.RETRIED) { - return getUpdatedProperty(browser, diff, 'resultIds.length') > 1; - } - - return status === viewMode; -} - -function shouldShowBrowser(browser, filteredBrowsers, diff = browser) { - if (isEmpty(filteredBrowsers)) { - return true; - } - - const browserToFilterBy = find(filteredBrowsers, {id: getUpdatedProperty(browser, diff, 'name')}); - - if (!browserToFilterBy) { - return false; - } - - const browserVersionsToFilterBy = [].concat(browserToFilterBy.versions).filter(Boolean); - - if (isEmpty(browserVersionsToFilterBy)) { - return true; - } - - return browserVersionsToFilterBy.includes(getUpdatedProperty(browser, diff, 'version')); -} - -function iterateSuites(node, {suiteCb, browserCb, browserIdsCb}) { - let resultFromBrowsers = []; - let resultFromSuites = []; - - if (node.browserIds && [browserCb, browserIdsCb].some(isFunction)) { - resultFromBrowsers = browserIdsCb - ? browserIdsCb(node.browserIds, node) - : flatMap(node.browserIds, (browserId) => browserCb(browserId, node)); - } - - if (node.suiteIds && isFunction(suiteCb)) { - resultFromSuites = flatMap(node.suiteIds, (suiteId) => suiteCb(suiteId, node)); - } - - return [...resultFromBrowsers, ...resultFromSuites]; -} - -function parseKeyToGroupTestsBy(key) { - let [groupSection, ...groupKey] = key.split(KEY_DELIMITER); - groupKey = groupKey.join(KEY_DELIMITER); - - if (!AVAILABLE_GROUP_SECTIONS.includes(groupSection)) { - throw new Error(`Group section must be one of ${AVAILABLE_GROUP_SECTIONS.join(', ')}, but got ${groupSection}`); - } - - if (groupSection === SECTIONS.RESULT && !RESULT_KEYS.includes(groupKey)) { - throw new Error(`Group key must be one of ${RESULT_KEYS.join(', ')}, but got ${groupKey}`); - } - - return [groupSection, groupKey]; -} - -function preloadImage(url) { - new Image().src = url; -} - -function applyStateUpdate(state, diff) { - const result = {...state}; - - for (const key in diff) { - if (isPlainObject(diff[key]) && isPlainObject(state[key])) { - result[key] = applyStateUpdate(state[key], diff[key]); - } else if (diff[key] !== undefined) { - result[key] = diff[key]; - } else { - delete result[key]; - } - } - - return result; -} - -function ensureDiffProperty(diff, path) { - let state = diff; - - for (let i = 0; i < path.length; i++) { - const property = path[i]; - - state[property] = state[property] || {}; - - state = state[property]; - } -} - -function getUpdatedProperty(state, diff, path) { - const diffValue = get(diff, path); - - return isUndefined(diffValue) ? get(state, path) : diffValue; -} - -module.exports = { - isNoRefImageError, - isAssertViewError, - hasNoRefImageErrors, - hasResultFails, - isSuiteIdle, - isSuiteSuccessful, - isNodeFailed, - isNodeSuccessful, - isAcceptable, - isScreenRevertable, - dateToLocaleString, - getHttpErrorMessage, - isTestNameMatchFilters, - isBrowserMatchViewMode, - shouldShowBrowser, - iterateSuites, - parseKeyToGroupTestsBy, - preloadImage, - applyStateUpdate, - ensureDiffProperty, - getUpdatedProperty -}; diff --git a/lib/static/modules/utils/index.js b/lib/static/modules/utils/index.js index d9e45c44d..4f1d80d62 100644 --- a/lib/static/modules/utils/index.js +++ b/lib/static/modules/utils/index.js @@ -2,9 +2,8 @@ const {isEmpty, find, isFunction, flatMap} = require('lodash'); const {isIdleStatus, isSuccessStatus, isUpdatedStatus, isFailStatus, isErrorStatus, isSkippedStatus, isNoRefImageError} = require('../../../common-utils'); -const {ViewMode} = require('../../constants/view-modes'); -const {SECTIONS, RESULT_KEYS, KEY_DELIMITER} = require('../../constants/group-tests'); -const {getUpdatedProperty} = require('./state'); +const {ViewMode, SECTIONS, RESULT_KEYS, KEY_DELIMITER} = require('../../../constants'); +const {applyStateUpdate, ensureDiffProperty, getUpdatedProperty} = require('./state'); const AVAILABLE_GROUP_SECTIONS = Object.values(SECTIONS); @@ -51,9 +50,11 @@ function isTestNameMatchFilters(testName, testNameFilter, strictMatchFilter) { return true; } + const filterRegExpStr = testNameFilter.replace(/ /g, '(?: | › )'); + return strictMatchFilter - ? testName === testNameFilter - : testName.includes(testNameFilter); + ? new RegExp(`^${filterRegExpStr}$`).test(testName) + : new RegExp(filterRegExpStr).test(testName); } function isBrowserMatchViewMode(browser, lastResult, viewMode, diff = browser) { @@ -135,6 +136,9 @@ function preloadImage(url) { } module.exports = { + applyStateUpdate, + ensureDiffProperty, + getUpdatedProperty, isSuiteIdle, isSuiteSuccessful, isNodeFailed, diff --git a/lib/test-adapter/cache/playwright.ts b/lib/test-adapter/cache/playwright.ts new file mode 100644 index 000000000..d040676b2 --- /dev/null +++ b/lib/test-adapter/cache/playwright.ts @@ -0,0 +1 @@ +export const testsAttempts: Map = new Map(); diff --git a/lib/test-adapter/index.ts b/lib/test-adapter/index.ts index 23ec0aa85..9ea46ce21 100644 --- a/lib/test-adapter/index.ts +++ b/lib/test-adapter/index.ts @@ -1,5 +1,5 @@ import {TestStatus} from '../constants'; -import {AssertViewResult, ErrorDetails, ImageBase64, ImageInfoFull, TestError} from '../types'; +import {AssertViewResult, ErrorDetails, ImageBase64, ImageData, ImageInfoFull, TestError} from '../types'; export * from './hermione'; @@ -20,7 +20,7 @@ export interface ReporterTestResult { readonly isUpdated?: boolean; readonly meta: Record; readonly multipleTabs: boolean; - readonly screenshot: ImageBase64 | undefined; + readonly screenshot: ImageBase64 | ImageData | null | undefined; readonly sessionId: string; readonly skipReason?: string; readonly state: { name: string }; diff --git a/lib/test-adapter/playwright.ts b/lib/test-adapter/playwright.ts new file mode 100644 index 000000000..25401ea5b --- /dev/null +++ b/lib/test-adapter/playwright.ts @@ -0,0 +1,262 @@ +import path from 'path'; +import {TestCase as PlaywrightTestCase, TestResult as PlaywrightTestResult} from '@playwright/test/reporter'; +import sizeOf from 'image-size'; +import _ from 'lodash'; +import stripAnsi from 'strip-ansi'; + +import {ReporterTestResult} from './index'; +import {testsAttempts} from './cache/playwright'; +import {getShortMD5, isImageDiffError, isNoRefImageError, mkTestId} from '../common-utils'; +import {FAIL, PWT_TITLE_DELIMITER, TestStatus} from '../constants'; +import {ErrorName} from '../errors'; +import {ImagesInfoFormatter} from '../image-handler'; +import {AssertViewResult, ErrorDetails, ImageData, ImageInfoFull, ImageSize, TestError} from '../types'; +import * as utils from '../server-utils'; + +export type PlaywrightAttachment = PlaywrightTestResult['attachments'][number]; + +export enum PwtTestStatus { + PASSED = 'passed', + FAILED = 'failed', + TIMED_OUT = 'timedOut', + INTERRUPTED = 'interrupted', + SKIPPED = 'skipped', +} + +export enum ImageTitleEnding { + Expected = '-expected.png', + Actual = '-actual.png', + Diff = '-diff.png', + Previous = '-previous.png' +} + +const ANY_IMAGE_ENDING_REGEXP = new RegExp(Object.values(ImageTitleEnding).map(ending => `${ending}$`).join('|')); + +export const getStatus = (result: PlaywrightTestResult): TestStatus => { + if (result.status === PwtTestStatus.PASSED) { + return TestStatus.SUCCESS; + } + + if ( + [PwtTestStatus.FAILED, PwtTestStatus.TIMED_OUT, PwtTestStatus.INTERRUPTED].includes( + result.status as PwtTestStatus + ) + ) { + return TestStatus.FAIL; + } + + return TestStatus.SKIPPED; +}; + +const extractErrorMessage = (result: PlaywrightTestResult): string => { + if (_.isEmpty(result.errors)) { + return ''; + } + + if (result.errors.length === 1) { + return stripAnsi(result.errors[0].message || '').split('\n')[0]; + } + + return JSON.stringify(result.errors.map(err => stripAnsi(err.message || '').split('\n')[0])); +}; + +const extractErrorStack = (result: PlaywrightTestResult): string => { + if (_.isEmpty(result.errors)) { + return ''; + } + + if (result.errors.length === 1) { + return stripAnsi(result.errors[0].stack || ''); + } + + return JSON.stringify(result.errors.map(err => stripAnsi(err.stack || ''))); +}; + +const getImageData = (attachment: PlaywrightAttachment | undefined): ImageData | null => { + if (!attachment) { + return null; + } + + return { + path: attachment.path as string, + size: _.pick(sizeOf(attachment.path as string), ['height', 'width']) as ImageSize + }; +}; + +export interface PlaywrightTestAdapterOptions { + imagesInfoFormatter: ImagesInfoFormatter; +} + +export class PlaywrightTestAdapter implements ReporterTestResult { + private readonly _testCase: PlaywrightTestCase; + private readonly _testResult: PlaywrightTestResult; + private _attempt: number; + private _imagesInfoFormatter: ImagesInfoFormatter; + + constructor(testCase: PlaywrightTestCase, testResult: PlaywrightTestResult, {imagesInfoFormatter}: PlaywrightTestAdapterOptions) { + this._testCase = testCase; + this._testResult = testResult; + this._imagesInfoFormatter = imagesInfoFormatter; + + const testId = mkTestId(this.fullName, this.browserId); + if (utils.shouldUpdateAttempt(this.status)) { + testsAttempts.set(testId, _.isUndefined(testsAttempts.get(testId)) ? 0 : testsAttempts.get(testId) as number + 1); + } + + this._attempt = testsAttempts.get(testId) || 0; + } + + get assertViewResults(): AssertViewResult[] { + return Object.entries(this._attachmentsByState).map(([state, attachments]): AssertViewResult | null => { + const refImg = getImageData(attachments.find(a => a.name?.endsWith(ImageTitleEnding.Expected))); + const diffImg = getImageData(attachments.find(a => a.name?.endsWith(ImageTitleEnding.Diff))); + const currImg = getImageData(attachments.find(a => a.name?.endsWith(ImageTitleEnding.Actual))); + + if (this.error?.name === ErrorName.IMAGE_DIFF && refImg && diffImg && currImg) { + return { + name: ErrorName.IMAGE_DIFF, + stateName: state, + refImg, + diffImg, + currImg + }; + } else if (this.error?.name === ErrorName.NO_REF_IMAGE && currImg) { + return { + name: ErrorName.NO_REF_IMAGE, + message: this.error.message, + stack: this.error.stack, + stateName: state, + currImg + }; + } + + return null; + }).filter(Boolean) as AssertViewResult[]; + } + + get attempt(): number { + return this._attempt; + } + + get browserId(): string { + return this._testCase.parent.project()?.name as string; + } + + get description(): string | undefined { + return undefined; + } + + get error(): TestError | undefined { + const message = extractErrorMessage(this._testResult); + if (message) { + const result: TestError = {name: ErrorName.GENERAL_ERROR, message}; + + const stack = extractErrorStack(this._testResult); + if (stack) { + result.stack = stack; + } + + if (message.includes('snapshot doesn\'t exist') && message.includes('.png')) { + result.name = ErrorName.NO_REF_IMAGE; + } else if (message.includes('Screenshot comparison failed')) { + result.name = ErrorName.IMAGE_DIFF; + } + + return result; + } + return undefined; + } + + get errorDetails(): ErrorDetails | null { + return null; + } + + get file(): string { + return path.relative(process.cwd(), this._testCase.location.file); + } + + get fullName(): string { + return this.testPath.join(PWT_TITLE_DELIMITER); + } + + get history(): string[] { + return this._testResult.steps.map(step => `${step.title} <- ${step.duration}ms\n`); + } + + get id(): string { + return this.testPath.concat(this.browserId, this.attempt.toString()).join(' '); + } + + get imageDir(): string { + return getShortMD5(this.fullName); + } + + get imagesInfo(): ImageInfoFull[] | undefined { + return this._imagesInfoFormatter.getImagesInfo(this); + } + + get isUpdated(): boolean { + return false; + } + + get meta(): Record { + return Object.fromEntries(this._testCase.annotations.map(a => [a.type, a.description ?? ''])); + } + + get multipleTabs(): boolean { + return true; + } + + get screenshot(): ImageData | null { + const pageScreenshot = this._testResult.attachments.find(a => a.contentType === 'image/png' && a.name === 'screenshot'); + + return getImageData(pageScreenshot); + } + + get sessionId(): string { + // TODO: implement getting sessionId + return ''; + } + + get skipReason(): string { + return this._testCase.annotations.find(a => a.type === 'skip')?.description || ''; + } + + get state(): { name: string } { + return {name: this._testCase.title}; + } + + get status(): TestStatus { + const status = getStatus(this._testResult); + if (status === TestStatus.FAIL) { + if (isNoRefImageError(this.error) || isImageDiffError(this.error)) { + return FAIL; + } + return TestStatus.ERROR; + } + + return status; + } + + get testPath(): string[] { + // slicing because first entries are not actually test-name, but a file, etc. + return this._testCase.titlePath().slice(3); + } + + get timestamp(): number | undefined { + return this._testResult.startTime.getTime(); + } + + get url(): string { + // TODO: HERMIONE-1191 + return ''; + } + + private get _attachmentsByState(): Record { + // Filtering out only images. Page screenshots on reject are named "screenshot", we don't want them in state either. + const imageAttachments = this._testResult.attachments.filter( + a => a.contentType === 'image/png' && ANY_IMAGE_ENDING_REGEXP.test(a.name)); + + return _.groupBy(imageAttachments, a => a.name.replace(ANY_IMAGE_ENDING_REGEXP, '')); + } +} diff --git a/lib/tests-tree-builder/base.ts b/lib/tests-tree-builder/base.ts index e89b784d8..27ffb6335 100644 --- a/lib/tests-tree-builder/base.ts +++ b/lib/tests-tree-builder/base.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import {determineStatus} from '../common-utils'; -import {TestStatus, BrowserVersions} from '../constants'; +import {BrowserVersions, PWT_TITLE_DELIMITER, TestStatus, ToolName} from '../constants'; import {ReporterTestResult} from '../test-adapter'; import {ImageInfoFull, ParsedSuitesRow} from '../types'; @@ -73,14 +73,24 @@ interface ImagesPayload { parentId: string; } +export interface BaseTestsTreeBuilderOptions { + toolName: ToolName; +} + export class BaseTestsTreeBuilder { protected _tree: Tree; + protected _toolName: ToolName; - static create(this: new () => T): T { - return new this(); + static create( + this: new (options: BaseTestsTreeBuilderOptions) => T, + options: BaseTestsTreeBuilderOptions + ): T { + return new this(options); } - constructor() { + constructor({toolName}: BaseTestsTreeBuilderOptions) { + this._toolName = toolName; + this._tree = { suites: {byId: {}, allIds: [], allRootIds: []}, browsers: {byId: {}, allIds: []}, @@ -118,7 +128,8 @@ export class BaseTestsTreeBuilder { const browserId = this._buildId(suiteId, browserName); const testResultId = this._buildId(browserId, attempt.toString()); const imageIds = imagesInfo - .map((image: ImageInfoFull, i: number) => this._buildId(testResultId, image.stateName || `${image.status}_${i}`)); + .map((image: ImageInfoFull, i: number) => + this._buildId(testResultId, (image as {stateName?: string}).stateName || `${image.status}_${i}`)); this._addSuites(testPath, browserId); this._addBrowser({id: browserId, parentId: suiteId, name: browserName, version: browserVersion}, testResultId, attempt); @@ -129,7 +140,12 @@ export class BaseTestsTreeBuilder { } protected _buildId(parentId: string | string[] = [], name: string | string[] = []): string { - return ([] as string[]).concat(parentId, name).join(' '); + let delimiter = ' '; + if (this._toolName === ToolName.Playwright) { + delimiter = PWT_TITLE_DELIMITER; + } + + return ([] as string[]).concat(parentId, name).join(delimiter); } protected _addSuites(testPath: string[], browserId: string): void { diff --git a/lib/tests-tree-builder/static.ts b/lib/tests-tree-builder/static.ts index 24bb64e94..f8ccbf816 100644 --- a/lib/tests-tree-builder/static.ts +++ b/lib/tests-tree-builder/static.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import {BaseTestsTreeBuilder, Tree} from './base'; +import {BaseTestsTreeBuilder, BaseTestsTreeBuilderOptions, Tree} from './base'; import {BrowserVersions, DB_COLUMN_INDEXES, TestStatus} from '../constants'; import {Attempt, ParsedSuitesRow, RawSuitesRow} from '../types'; @@ -30,14 +30,17 @@ interface BrowserItem { versions: string[]; } +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface StaticTestsTreeBuilderOptions extends BaseTestsTreeBuilderOptions {} + export class StaticTestsTreeBuilder extends BaseTestsTreeBuilder { protected _stats: FinalStats; protected _skips: SkipItem[]; protected _failedBrowserIds: { [key: string]: boolean }; protected _passedBrowserIds: { [key: string]: boolean }; - constructor() { - super(); + constructor(options: StaticTestsTreeBuilderOptions) { + super(options); this._stats = { ...initStats(), diff --git a/lib/types.ts b/lib/types.ts index e5bcf0d69..706a9fefd 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -75,6 +75,11 @@ export interface ImageInfoSuccess { actualImg?: ImageData; } +export interface ImageInfoPageSuccess { + status: TestStatus.SUCCESS; + actualImg: ImageData; +} + export interface ImageInfoError { status: TestStatus.ERROR; error?: {message: string; stack: string;} @@ -84,12 +89,13 @@ export interface ImageInfoError { actualImg: ImageData; } -export type ImageInfoFull = ImageInfoFail | ImageInfoSuccess | ImageInfoError; +export type ImageInfoFull = ImageInfoFail | ImageInfoSuccess | ImageInfoError | ImageInfoPageSuccess; export type ImageInfo = | Omit | Omit - | Omit; + | Omit + | Omit; export type AssertViewResult = AssertViewSuccess | ImageDiffError | NoRefImageError; @@ -99,7 +105,7 @@ export interface TestError { stack?: string; stateName?: string; details?: ErrorDetails - screenshot?: ImageBase64 + screenshot?: ImageBase64 | ImageData } export interface LabeledSuitesRow { diff --git a/package-lock.json b/package-lock.json index 5dffaeee0..11d2a70e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "fs-extra": "^7.0.1", "gemini-configparser": "^1.0.0", "http-codes": "1.0.0", + "image-size": "^1.0.2", "inquirer": "^8.2.0", "json-stringify-safe": "^5.0.1", "lodash": "^4.17.4", @@ -39,6 +40,7 @@ "p-queue": "^5.0.0", "qs": "^6.9.1", "signal-exit": "^3.0.2", + "strip-ansi": "^6.0.1", "tmp": "^0.1.0", "worker-farm": "^1.7.0" }, @@ -3573,22 +3575,18 @@ } }, "node_modules/@playwright/test": { - "version": "1.37.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.37.1.tgz", - "integrity": "sha512-bq9zTli3vWJo8S3LwB91U0qDNQDpEXnw7knhxLM0nwDvexQAwx9tO8iKDZSqqneVq+URd/WIoz+BALMqUTgdSg==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", + "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", "dev": true, "dependencies": { - "@types/node": "*", - "playwright-core": "1.37.1" + "playwright": "1.38.1" }, "bin": { "playwright": "cli.js" }, "engines": { "node": ">=16" - }, - "optionalDependencies": { - "fsevents": "2.3.2" } }, "node_modules/@puppeteer/browsers": { @@ -3634,16 +3632,6 @@ "node": ">= 14" } }, - "node_modules/@puppeteer/browsers/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@puppeteer/browsers/node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -3976,19 +3964,6 @@ "node": ">=8" } }, - "node_modules/@puppeteer/browsers/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@puppeteer/browsers/node_modules/tar-fs": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", @@ -5867,27 +5842,6 @@ "node": ">=12.0.0" } }, - "node_modules/@wdio/logger/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wdio/logger/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@wdio/protocols": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.11.0.tgz", @@ -7280,6 +7234,18 @@ "node": ">=0.10.0" } }, + "node_modules/babel-code-frame/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/babel-code-frame/node_modules/supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -8637,6 +8603,18 @@ "node": ">=0.10.0" } }, + "node_modules/clap/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/clap/node_modules/supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -9286,15 +9264,6 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/concurrently/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/concurrently/node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -9347,18 +9316,6 @@ "node": ">=8" } }, - "node_modules/concurrently/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/concurrently/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -9936,6 +9893,18 @@ "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", "dev": true }, + "node_modules/conventional-changelog-lint/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/conventional-changelog-lint/node_modules/supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -10607,15 +10576,6 @@ "copyup": "copyfiles" } }, - "node_modules/copyfiles/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/copyfiles/node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -10668,18 +10628,6 @@ "node": ">=8" } }, - "node_modules/copyfiles/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/copyfiles/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -15641,15 +15589,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -15958,18 +15897,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -18888,6 +18815,27 @@ "node": ">=0.10.0" } }, + "node_modules/hermione/node_modules/chalk/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hermione/node_modules/chalk/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/hermione/node_modules/chalk/node_modules/supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -18941,27 +18889,6 @@ "node": ">=12" } }, - "node_modules/hermione/node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/hermione/node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/hermione/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -19830,27 +19757,6 @@ "node": ">=8" } }, - "node_modules/hermione/node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/hermione/node_modules/string-width/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/hermione/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -20033,15 +19939,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/hermione/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/hermione/node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -20057,18 +19954,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/hermione/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/hermione/node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -20854,16 +20739,17 @@ "dev": true }, "node_modules/image-size": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", - "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", - "dev": true, - "optional": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz", + "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==", + "dependencies": { + "queue": "6.0.2" + }, "bin": { "image-size": "bin/image-size.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=14.0.0" } }, "node_modules/immer": { @@ -21036,14 +20922,6 @@ "node": ">=8.0.0" } }, - "node_modules/inquirer/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/inquirer/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -21070,17 +20948,6 @@ "node": ">=8" } }, - "node_modules/inquirer/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/insert-module-globals": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.1.tgz", @@ -22696,6 +22563,19 @@ "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" } }, + "node_modules/less/node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/less/node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -23747,15 +23627,6 @@ "node": ">=6" } }, - "node_modules/mocha/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/mocha/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -23962,18 +23833,6 @@ "node": ">=8" } }, - "node_modules/mocha/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/mocha/node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -25299,25 +25158,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/os-browserify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.1.2.tgz", @@ -25883,10 +25723,28 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", + "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", + "dev": true, + "dependencies": { + "playwright-core": "1.38.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/playwright-core": { - "version": "1.37.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.37.1.tgz", - "integrity": "sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", + "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -25895,6 +25753,10 @@ "node": ">=16" } }, + "node_modules/playwright-fixture-report": { + "resolved": "test/func/fixtures/playwright", + "link": true + }, "node_modules/plugins-fixture-report": { "resolved": "test/func/fixtures/plugins", "link": true @@ -27029,6 +26891,18 @@ "node": ">=0.10.0" } }, + "node_modules/postcss/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/postcss/node_modules/supports-color": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", @@ -27410,6 +27284,14 @@ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -28292,6 +28174,18 @@ "strip-ansi": "^3.0.1" } }, + "node_modules/renderkid/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/repeat-element": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", @@ -30022,6 +29916,18 @@ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "dev": true }, + "node_modules/standard-version/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/standard-version/node_modules/supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -30327,15 +30233,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -30351,18 +30248,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string-width/node_modules/ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", @@ -30466,15 +30351,14 @@ } }, "node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { - "ansi-regex": "^2.0.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/strip-ansi-cjs": { @@ -30499,6 +30383,14 @@ "node": ">=8" } }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -34175,6 +34067,18 @@ "semver": "bin/semver.js" } }, + "node_modules/webpack-dev-server/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/webpack-dev-server/node_modules/supports-color": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", @@ -34684,15 +34588,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -34722,18 +34617,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", @@ -35102,6 +34985,10 @@ "test/func/fixtures/hermione-gui": { "version": "0.0.0" }, + "test/func/fixtures/playwright": { + "name": "playwright-fixture-report", + "version": "0.0.0" + }, "test/func/fixtures/plugins": { "name": "plugins-fixture-report", "version": "0.0.0" @@ -37686,14 +37573,12 @@ "optional": true }, "@playwright/test": { - "version": "1.37.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.37.1.tgz", - "integrity": "sha512-bq9zTli3vWJo8S3LwB91U0qDNQDpEXnw7knhxLM0nwDvexQAwx9tO8iKDZSqqneVq+URd/WIoz+BALMqUTgdSg==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", + "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", "dev": true, "requires": { - "@types/node": "*", - "fsevents": "2.3.2", - "playwright-core": "1.37.1" + "playwright": "1.38.1" } }, "@puppeteer/browsers": { @@ -37722,13 +37607,6 @@ "debug": "^4.3.4" } }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "optional": true - }, "ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -37985,16 +37863,6 @@ "strip-ansi": "^6.0.1" } }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, "tar-fs": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", @@ -39406,23 +39274,6 @@ "loglevel": "^1.6.0", "loglevel-plugin-prefix": "^0.8.4", "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } } }, "@wdio/protocols": { @@ -40542,6 +40393,15 @@ "supports-color": "^2.0.0" } }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -41639,6 +41499,15 @@ "supports-color": "^2.0.0" } }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -42168,12 +42037,6 @@ "yargs": "^17.3.1" }, "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -42214,15 +42077,6 @@ "strip-ansi": "^6.0.1" } }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, "supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -42711,6 +42565,15 @@ "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", "dev": true }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -43237,12 +43100,6 @@ "yargs": "^16.1.0" }, "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -43283,15 +43140,6 @@ "strip-ansi": "^6.0.1" } }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -47077,12 +46925,6 @@ "uri-js": "^4.2.2" } }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -47295,15 +47137,6 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -49587,6 +49420,21 @@ "supports-color": "^2.0.0" }, "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -49624,23 +49472,6 @@ "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } } }, "color-convert": { @@ -50247,23 +50078,6 @@ "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } } }, "tr46": { @@ -50406,12 +50220,6 @@ "strip-ansi": "^6.0.0" }, "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -50420,15 +50228,6 @@ "requires": { "color-convert": "^2.0.1" } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } } } }, @@ -51123,11 +50922,12 @@ "dev": true }, "image-size": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", - "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", - "dev": true, - "optional": true + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz", + "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==", + "requires": { + "queue": "6.0.2" + } }, "immer": { "version": "7.0.15", @@ -51266,11 +51066,6 @@ "through": "^2.3.6" }, "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -51290,14 +51085,6 @@ "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } } } }, @@ -52465,6 +52252,13 @@ "tslib": "^1.10.0" }, "dependencies": { + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true + }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -53350,12 +53144,6 @@ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -53505,15 +53293,6 @@ "strip-ansi": "^6.0.1" } }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -54582,21 +54361,6 @@ "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - } } }, "os-browserify": { @@ -55046,12 +54810,25 @@ } } }, + "playwright": { + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", + "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.38.1" + } + }, "playwright-core": { - "version": "1.37.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.37.1.tgz", - "integrity": "sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", + "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", "dev": true }, + "playwright-fixture-report": { + "version": "file:test/func/fixtures/playwright" + }, "plugins-fixture-report": { "version": "file:test/func/fixtures/plugins" }, @@ -55155,6 +54932,15 @@ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", "dev": true }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", @@ -56323,6 +56109,14 @@ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true }, + "queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "requires": { + "inherits": "~2.0.3" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -57051,6 +56845,17 @@ "htmlparser2": "^6.1.0", "lodash": "^4.17.21", "strip-ansi": "^3.0.1" + }, + "dependencies": { + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } } }, "repeat-element": { @@ -58469,6 +58274,15 @@ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "dev": true }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -58751,12 +58565,6 @@ "strip-ansi": "^6.0.1" }, "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -58768,15 +58576,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } } } }, @@ -58841,12 +58640,18 @@ } }, "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "requires": { - "ansi-regex": "^2.0.0" + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + } } }, "strip-ansi-cjs": { @@ -61888,6 +61693,15 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", @@ -62173,12 +61987,6 @@ "strip-ansi": "^6.0.0" }, "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -62201,15 +62009,6 @@ "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } } } }, diff --git a/package.json b/package.json index a70579e11..65e4cf17f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ ], "exports": { "./gemini": "./build/gemini.js", - "./hermione": "./build/hermione.js" + "./hermione": "./build/hermione.js", + "./playwright": "./build/playwright.js" }, "scripts": { "build": "rimraf build && npm run build:client && npm run build:server", @@ -75,6 +76,7 @@ "fs-extra": "^7.0.1", "gemini-configparser": "^1.0.0", "http-codes": "1.0.0", + "image-size": "^1.0.2", "inquirer": "^8.2.0", "json-stringify-safe": "^5.0.1", "lodash": "^4.17.4", @@ -85,6 +87,7 @@ "p-queue": "^5.0.0", "qs": "^6.9.1", "signal-exit": "^3.0.2", + "strip-ansi": "^6.0.1", "tmp": "^0.1.0", "worker-farm": "^1.7.0" }, diff --git a/playwright.ts b/playwright.ts new file mode 100644 index 000000000..bc222336d --- /dev/null +++ b/playwright.ts @@ -0,0 +1,69 @@ +import {promisify} from 'util'; +import _ from 'lodash'; +import type {Reporter, TestCase, TestResult as PwtTestResult} from '@playwright/test/reporter'; +import workerFarm, {Workers} from 'worker-farm'; + +import {StaticReportBuilder} from './lib/report-builder/static'; +import {HtmlReporter} from './lib/plugin-api'; +import {ReporterConfig} from './lib/types'; +import {parseConfig} from './lib/config'; +import {PluginEvents, TestStatus, ToolName} from './lib/constants'; +import {RegisterWorkers} from './lib/workers/create-workers'; +import {EventEmitter} from 'events'; +import {PlaywrightTestAdapter, getStatus} from './lib/test-adapter/playwright'; +import PQueue from 'p-queue'; + +export {ReporterConfig} from './lib/types'; + +class MyReporter implements Reporter { + protected _promiseQueue: PQueue = new PQueue(); + protected _staticReportBuilder: StaticReportBuilder; + protected _htmlReporter: HtmlReporter; + protected _config: ReporterConfig; + protected _workerFarm: Workers; + protected _workers: RegisterWorkers<['saveDiffTo']>; + + constructor(opts: Partial) { + this._config = parseConfig(_.omit(opts, ['configDir'])); + this._htmlReporter = HtmlReporter.create(this._config, {toolName: ToolName.Playwright}); + this._staticReportBuilder = StaticReportBuilder.create(this._htmlReporter, this._config); + this._workerFarm = workerFarm(require.resolve('./lib/workers/worker'), ['saveDiffTo']); + + const workers: RegisterWorkers<['saveDiffTo']> = new EventEmitter() as RegisterWorkers<['saveDiffTo']>; + workers.saveDiffTo = (imageDiffError: unknown, diffPath: unknown): Promise => + promisify(this._workerFarm.saveDiffTo)(imageDiffError, diffPath); + this._workers = workers; + + this._promiseQueue.add(() => this._staticReportBuilder.init() + .then(() => this._staticReportBuilder.saveStaticFiles()) + ); + } + + onTestEnd(test: TestCase, result: PwtTestResult): void { + const status = getStatus(result); + const formattedResult = new PlaywrightTestAdapter(test, result, {imagesInfoFormatter: this._staticReportBuilder.imageHandler}); + + if (status === TestStatus.FAIL) { + if (formattedResult.status === TestStatus.FAIL) { + this._staticReportBuilder.addFail(formattedResult); + } else { + this._staticReportBuilder.addError(formattedResult); + } + } else if (status === TestStatus.SUCCESS) { + this._staticReportBuilder.addSuccess(formattedResult); + } else if (status === TestStatus.SKIPPED) { + this._staticReportBuilder.addSkipped(formattedResult); + } + this._promiseQueue.add(() => this._staticReportBuilder.imageHandler.saveTestImages(formattedResult, this._workers)); + } + + async onEnd(): Promise { + await this._promiseQueue.onIdle(); + + await this._staticReportBuilder.finalize(); + + await this._htmlReporter.emitAsync(PluginEvents.REPORT_SAVED); + } +} + +export default MyReporter; diff --git a/test/func/fixtures/hermione/screens/7357338/chrome/header.png b/test/func/fixtures/hermione/screens/7357338/chrome/header.png new file mode 100644 index 000000000..5bc4221ca Binary files /dev/null and b/test/func/fixtures/hermione/screens/7357338/chrome/header.png differ diff --git a/test/func/fixtures/playwright/index.html b/test/func/fixtures/playwright/index.html new file mode 100644 index 000000000..412c348f3 --- /dev/null +++ b/test/func/fixtures/playwright/index.html @@ -0,0 +1,60 @@ + + + + + + + + +
+

Some header

+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +
+ +
+
+
Copyright ©
+ + + diff --git a/test/func/fixtures/playwright/package.json b/test/func/fixtures/playwright/package.json new file mode 100644 index 000000000..36624eedd --- /dev/null +++ b/test/func/fixtures/playwright/package.json @@ -0,0 +1,8 @@ +{ + "name": "playwright-fixture-report", + "version": "0.0.0", + "private": true, + "scripts": { + "generate": "npx playwright test" + } +} diff --git a/test/func/fixtures/playwright/playwright.config.ts b/test/func/fixtures/playwright/playwright.config.ts new file mode 100644 index 000000000..b0c5d6bcc --- /dev/null +++ b/test/func/fixtures/playwright/playwright.config.ts @@ -0,0 +1,47 @@ +import path from 'path'; +import {defineConfig, devices} from '@playwright/test'; + +const serverHost = process.env.SERVER_HOST ?? 'localhost'; +const serverPort = process.env.SERVER_PORT ?? 8085; + +export default defineConfig({ + testDir: './tests', + snapshotPathTemplate: '{testDir}/screens/{testName}/{projectName}/{arg}{ext}', + timeout: 30 * 1000, + expect: { + timeout: 5000 + }, + fullyParallel: true, + forbidOnly: true, + repeatEach: 2, + workers: 1, + reporter: [ + ['html-reporter-tester/playwright', { + path: path.resolve(__dirname, 'report'), + saveFormat: 'sqlite', + defaultView: 'all', + saveErrorDetails: true + }] + ], + + use: { + baseURL: `http://${serverHost}:${serverPort}/fixtures/playwright/index.html`, + screenshot: { + mode: 'on', + fullPage: true + } + }, + + projects: [ + { + name: 'chromium', + use: {...devices['Desktop Chrome']} + } + ], + + webServer: { + command: `npx http-server ${path.resolve(__dirname, '../..')} -c-1 -p ${serverPort}`, + port: Number(serverPort), + stdout: 'pipe' + } +}); diff --git a/test/func/fixtures/playwright/tests/failed-describe.spec.ts b/test/func/fixtures/playwright/tests/failed-describe.spec.ts new file mode 100644 index 000000000..d13d89a64 --- /dev/null +++ b/test/func/fixtures/playwright/tests/failed-describe.spec.ts @@ -0,0 +1,40 @@ +import fs from 'fs'; +import fsPromises from 'fs/promises'; +import path from 'path'; +import {test, expect} from '@playwright/test'; + +test.describe('failed describe', () => { + test('successfully passed test', async ({page, baseURL}) => { + await page.goto(baseURL as string); + }); + + test('test without screenshot', async ({page, baseURL}) => { + await page.goto(baseURL as string); + + const screenshotPath = path.resolve(__dirname, 'screens/failed-describe-test-without-screenshot/chromium/header.png'); + if (fs.existsSync(screenshotPath)) { + await fsPromises.rm(screenshotPath); + } + await expect(page.locator('header')).toHaveScreenshot('header.png'); + }); + + test('test with image comparison diff', async ({page, baseURL}) => { + await page.goto(baseURL as string); + + await expect(page.locator('header')).toHaveScreenshot('header.png'); + }); + + test('test with long error message', async () => { + throw new Error(`long_error_message ${'0123456789'.repeat(20)}\n message content`); + }); + + test('test skipped at the end', async ({page, baseURL}) => { + await page.goto(baseURL as string); + + test.skip(true, 'foo-bar'); + }); + + test.skip('test skipped', async ({page, baseURL}) => { + await page.goto(baseURL as string); + }); +}); diff --git a/test/func/fixtures/playwright/tests/screens/failed-describe-test-with-diff/chromium/header.png b/test/func/fixtures/playwright/tests/screens/failed-describe-test-with-diff/chromium/header.png new file mode 100644 index 000000000..5bc4221ca Binary files /dev/null and b/test/func/fixtures/playwright/tests/screens/failed-describe-test-with-diff/chromium/header.png differ diff --git a/test/func/fixtures/playwright/tests/screens/failed-describe-test-with-image-comparison-diff/chromium/header.png b/test/func/fixtures/playwright/tests/screens/failed-describe-test-with-image-comparison-diff/chromium/header.png new file mode 100644 index 000000000..e5d168297 Binary files /dev/null and b/test/func/fixtures/playwright/tests/screens/failed-describe-test-with-image-comparison-diff/chromium/header.png differ diff --git a/test/func/fixtures/playwright/tests/screens/failed-describe-test-without-screenshot/chromium/header.png b/test/func/fixtures/playwright/tests/screens/failed-describe-test-without-screenshot/chromium/header.png new file mode 100644 index 000000000..2baf53cb5 Binary files /dev/null and b/test/func/fixtures/playwright/tests/screens/failed-describe-test-without-screenshot/chromium/header.png differ diff --git a/test/func/fixtures/playwright/tests/screens/success-describe-test-with-screenshot/chromium/header.png b/test/func/fixtures/playwright/tests/screens/success-describe-test-with-screenshot/chromium/header.png new file mode 100644 index 000000000..2baf53cb5 Binary files /dev/null and b/test/func/fixtures/playwright/tests/screens/success-describe-test-with-screenshot/chromium/header.png differ diff --git a/test/func/fixtures/playwright/tests/success-describe.spec.ts b/test/func/fixtures/playwright/tests/success-describe.spec.ts new file mode 100644 index 000000000..5a38ae2a3 --- /dev/null +++ b/test/func/fixtures/playwright/tests/success-describe.spec.ts @@ -0,0 +1,13 @@ +import {test, expect} from '@playwright/test'; + +test.describe('success describe', () => { + test('successfully passed test', async ({page, baseURL}) => { + await page.goto(baseURL as string); + }); + + // test('test with screenshot', async ({page, baseURL}) => { + // await page.goto(baseURL as string); + // + // await expect(page.locator('header')).toHaveScreenshot('header.png'); + // }); +}); diff --git a/test/func/packages/html-reporter-tester/package.json b/test/func/packages/html-reporter-tester/package.json index d7ac76e40..d3030a81d 100644 --- a/test/func/packages/html-reporter-tester/package.json +++ b/test/func/packages/html-reporter-tester/package.json @@ -2,7 +2,10 @@ "name": "html-reporter-tester", "version": "1.0.0", "description": "", - "main": "index.js", + "exports": { + ".": "./index.js", + "./playwright": "./playwright.js" + }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/test/func/packages/html-reporter-tester/playwright.js b/test/func/packages/html-reporter-tester/playwright.js new file mode 100644 index 000000000..5c7eadc36 --- /dev/null +++ b/test/func/packages/html-reporter-tester/playwright.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('../../../../build/playwright'); diff --git a/test/func/tests/.hermione.conf.js b/test/func/tests/.hermione.conf.js index 07399e9bb..5d72a170a 100644 --- a/test/func/tests/.hermione.conf.js +++ b/test/func/tests/.hermione.conf.js @@ -19,20 +19,31 @@ if (!projectUnderTest) { throw 'Project under test was not specified'; } -const config = _.merge(getCommonConfig(__dirname), { +const commonConfig = getCommonConfig(__dirname); + +const config = _.merge(commonConfig, { baseUrl: `http://${serverHost}:${serverPort}/fixtures/${projectUnderTest}/report/index.html`, + browsers: { + // TODO: this is a hack to be able to have 2 sets of screenshots, for hermione-based report and pwt-based report + // currently, those have weird tiny diffs. Would be nice to figure out the cause and have common screenshots. + 'chrome-pwt': {...commonConfig.browsers.chrome} + }, + sets: { common: { files: 'common/**/*.hermione.js' }, 'common-gui': { + browsers: ['chrome'], files: 'common-gui/**/*.hermione.js' }, eye: { + browsers: ['chrome'], files: 'eye/**/*.hermione.js', }, plugins: { + browsers: ['chrome'], files: 'plugins/**/*.hermione.js' } }, diff --git a/test/func/tests/common/error-group.hermione.js b/test/func/tests/common/error-group.hermione.js index a3aee70e0..4049b4d15 100644 --- a/test/func/tests/common/error-group.hermione.js +++ b/test/func/tests/common/error-group.hermione.js @@ -8,9 +8,6 @@ describe('Error grouping', function() { const groupedTestsContainer = await browser.$('.grouped-tests'); - const errorGroups = await browser.$$('.grouped-tests > div'); - assert.equal(errorGroups.length, 3); - const longErrorMessageGroup = await groupedTestsContainer.$('span*=long_error_message').$('..'); await expect(longErrorMessageGroup).toBeDisplayed(); diff --git a/test/func/tests/common/test-results-appearance.hermione.js b/test/func/tests/common/test-results-appearance.hermione.js index 7b866b41e..303aed572 100644 --- a/test/func/tests/common/test-results-appearance.hermione.js +++ b/test/func/tests/common/test-results-appearance.hermione.js @@ -1,10 +1,4 @@ -const {getTestSectionByNameSelector, getImageSectionSelector, getTestStateByNameSelector, getElementWithTextSelector} = require('../utils'); - -const hideHeader = async (browser) => { - await browser.execute(() => { - document.querySelector('.sticky-header').style.visibility = 'hidden'; - }); -}; +const {getTestSectionByNameSelector, getImageSectionSelector, getTestStateByNameSelector, getElementWithTextSelector, hideHeader} = require('../utils'); describe('Test results appearance', () => { beforeEach(async ({browser}) => { @@ -72,6 +66,9 @@ describe('Test results appearance', () => { ); await hideHeader(browser); + await browser.execute(() =>{ + window.scrollTo(0, 10000); + }); await retrySelectorButton.assertView('retry-selector'); }); diff --git a/test/func/tests/common/tests-details.hermione.js b/test/func/tests/common/tests-details.hermione.js index 3ef759ca3..60832fa0a 100644 --- a/test/func/tests/common/tests-details.hermione.js +++ b/test/func/tests/common/tests-details.hermione.js @@ -1,6 +1,10 @@ -const {getTestSectionByNameSelector, getElementWithTextSelector} = require('../utils'); +const {getTestSectionByNameSelector, getElementWithTextSelector, hideHeader} = require('../utils'); describe('Test details', function() { + beforeEach(async ({browser}) => { + await hideHeader(browser); + }); + it('should show details', async ({browser}) => { await browser.$('div*=test with long error message').waitForDisplayed(); @@ -8,14 +12,13 @@ describe('Test details', function() { await erroredTestSection.$('.details__summary').click(); - const fileMetaInfo = await erroredTestSection.$('div*=failed-describe.hermione.js').$('..'); + const fileMetaInfo = await erroredTestSection.$('div*=failed-describe').$('..'); await expect(fileMetaInfo).toBeDisplayed(); await expect(await fileMetaInfo.$('span*=file')).toBeDisplayed(); }); - // TODO: figure out why this test is flaky between different runs, hermione vs playwright fixtures - it.skip('should prevent details summary overflow', async ({browser}) => { + it('should prevent details summary overflow', async ({browser}) => { const selector = getTestSectionByNameSelector('test with long error message') + `//summary[.${getElementWithTextSelector('span', 'message')}/..]`; diff --git a/test/func/tests/package.json b/test/func/tests/package.json index 40978ba56..0d4765bea 100644 --- a/test/func/tests/package.json +++ b/test/func/tests/package.json @@ -3,13 +3,15 @@ "version": "0.0.0", "private": true, "scripts": { - "gui:hermione-common": "PROJECT_UNDER_TEST=hermione npx hermione --set common gui", + "gui:hermione-common": "PROJECT_UNDER_TEST=hermione npx hermione --set common -b chrome gui", "gui:hermione-eye": "PROJECT_UNDER_TEST=hermione-eye npx hermione --no --set eye gui", "gui:hermione-gui": "PROJECT_UNDER_TEST=hermione-gui npx hermione --no --set common-gui gui", + "gui:playwright": "PROJECT_UNDER_TEST=playwright npx hermione --set common -b chrome-pwt gui", "gui:plugins": "PROJECT_UNDER_TEST=plugins SERVER_PORT=8084 npx hermione --set plugins gui", - "hermione:hermione-common": "PROJECT_UNDER_TEST=hermione SERVER_PORT=8061 npx hermione --set common", + "hermione:hermione-common": "PROJECT_UNDER_TEST=hermione SERVER_PORT=8061 npx hermione --set common -b chrome", "hermione:hermione-eye": "PROJECT_UNDER_TEST=hermione-eye SERVER_PORT=8062 npx hermione --set eye", "hermione:hermione-gui": "PROJECT_UNDER_TEST=hermione-gui SERVER_PORT=8063 npx hermione --no --set common-gui", + "hermione:playwright": "PROJECT_UNDER_TEST=playwright SERVER_PORT=8065 npx hermione --set common -b chrome-pwt", "hermione:plugins": "PROJECT_UNDER_TEST=plugins SERVER_PORT=8064 npx hermione --set plugins", "test": "run-s hermione:*" } diff --git a/test/func/tests/screens/1361a92/chrome-pwt/retry-selector.png b/test/func/tests/screens/1361a92/chrome-pwt/retry-selector.png new file mode 100644 index 000000000..dbf3ade14 Binary files /dev/null and b/test/func/tests/screens/1361a92/chrome-pwt/retry-selector.png differ diff --git a/test/func/tests/screens/42ea26d/chrome-pwt/retry-selector.png b/test/func/tests/screens/42ea26d/chrome-pwt/retry-selector.png new file mode 100644 index 000000000..a40bb6b78 Binary files /dev/null and b/test/func/tests/screens/42ea26d/chrome-pwt/retry-selector.png differ diff --git a/test/func/tests/screens/5c90021/chrome/basic plugins.png b/test/func/tests/screens/5c90021/chrome/basic plugins.png index 864b25792..7e9ab48f2 100644 Binary files a/test/func/tests/screens/5c90021/chrome/basic plugins.png and b/test/func/tests/screens/5c90021/chrome/basic plugins.png differ diff --git a/test/func/tests/screens/be4ff5b/chrome/basic plugins clicked.png b/test/func/tests/screens/be4ff5b/chrome/basic plugins clicked.png index 123812681..e60537dc7 100644 Binary files a/test/func/tests/screens/be4ff5b/chrome/basic plugins clicked.png and b/test/func/tests/screens/be4ff5b/chrome/basic plugins clicked.png differ diff --git a/test/func/tests/screens/c0db305/chrome-pwt/details summary.png b/test/func/tests/screens/c0db305/chrome-pwt/details summary.png new file mode 100644 index 000000000..3f884f8ef Binary files /dev/null and b/test/func/tests/screens/c0db305/chrome-pwt/details summary.png differ diff --git a/test/func/tests/screens/c0db305/chrome/details summary.png b/test/func/tests/screens/c0db305/chrome/details summary.png index 4add0266f..82a84485a 100644 Binary files a/test/func/tests/screens/c0db305/chrome/details summary.png and b/test/func/tests/screens/c0db305/chrome/details summary.png differ diff --git a/test/func/tests/screens/d8c5b8a/chrome/redux plugin clicked.png b/test/func/tests/screens/d8c5b8a/chrome/redux plugin clicked.png index 4d74d34ba..a2c802fe5 100644 Binary files a/test/func/tests/screens/d8c5b8a/chrome/redux plugin clicked.png and b/test/func/tests/screens/d8c5b8a/chrome/redux plugin clicked.png differ diff --git a/test/func/tests/screens/d90f7de/chrome-pwt/retry-selector.png b/test/func/tests/screens/d90f7de/chrome-pwt/retry-selector.png new file mode 100644 index 000000000..0ea2a4b54 Binary files /dev/null and b/test/func/tests/screens/d90f7de/chrome-pwt/retry-selector.png differ diff --git a/test/func/tests/screens/ff4deba/chrome-pwt/retry-selector.png b/test/func/tests/screens/ff4deba/chrome-pwt/retry-selector.png new file mode 100644 index 000000000..cf8509857 Binary files /dev/null and b/test/func/tests/screens/ff4deba/chrome-pwt/retry-selector.png differ diff --git a/test/func/tests/utils.js b/test/func/tests/utils.js index 08c6217e4..1f9489479 100644 --- a/test/func/tests/utils.js +++ b/test/func/tests/utils.js @@ -14,10 +14,17 @@ const getElementWithTextSelector = (tagName, text) => `//${tagName}[contains(tex /** Returns
element which has summary containing name */ const getSpoilerByNameSelector = (name) => `details[.//summary[contains(text(), "${name}")]]`; +const hideHeader = async (browser) => { + await browser.execute(() => { + document.querySelector('.sticky-header').style.visibility = 'hidden'; + }); +}; + module.exports = { getTestSectionByNameSelector, getTestStateByNameSelector, getImageSectionSelector, getElementWithTextSelector, - getSpoilerByNameSelector + getSpoilerByNameSelector, + hideHeader }; diff --git a/test/unit/lib/image-handler.ts b/test/unit/lib/image-handler.ts index 8c70ba398..20f45687b 100644 --- a/test/unit/lib/image-handler.ts +++ b/test/unit/lib/image-handler.ts @@ -8,7 +8,7 @@ import type * as originalUtils from 'lib/server-utils'; import {logger} from 'lib/common-utils'; import {ImageHandler as ImageHandlerOriginal} from 'lib/image-handler'; import {RegisterWorkers} from 'lib/workers/create-workers'; -import {AssertViewResult, ImageInfoFull, ImageInfoSuccess, ImagesSaver} from 'lib/types'; +import {AssertViewResult, ImageInfoFail, ImageInfoFull, ImageInfoSuccess, ImagesSaver} from 'lib/types'; import {ErrorName, ImageDiffError} from 'lib/errors'; import {ImageStore} from 'lib/image-store'; import {FAIL, PluginEvents, SUCCESS, UPDATED} from 'lib/constants'; @@ -169,7 +169,7 @@ describe('image-handler', function() { it('should warn about it', () => { const testResult = mkTestResult({ - error: {screenshot: {base64: null}} as any, + screenshot: {base64: null} as any, assertViewResults: [] }); const imageHandler = new ImageHandler(mkImageStore(), mkImagesSaver(), {reportPath: ''}); @@ -181,7 +181,7 @@ describe('image-handler', function() { it('should create directory for screenshot', () => { const testResult = mkTestResult({ - error: {screenshot: {base64: 'base64-data'}} as any, + screenshot: {base64: 'base64-data'} as any, assertViewResults: [] }); utils.getCurrentPath.returns('dest/path'); @@ -193,7 +193,7 @@ describe('image-handler', function() { it('should save screenshot from base64 format', async () => { const testResult = mkTestResult({ - error: {screenshot: {base64: 'base64-data'}} as any, + screenshot: {base64: 'base64-data'} as any, assertViewResults: [] }); utils.getCurrentPath.returns('dest/path'); @@ -272,7 +272,7 @@ describe('image-handler', function() { describe('getScreenshot', () => { it('should return error screenshot from test result', () => { - const testResult = mkTestResult({error: {screenshot: 'some-value'} as any}); + const testResult = mkTestResult({screenshot: 'some-value'} as any); assert.equal(ImageHandler.getScreenshot(testResult), 'some-value' as any); }); @@ -290,7 +290,7 @@ describe('image-handler', function() { }); const imageHandler = new ImageHandler(mkImageStore(), mkImagesSaver(), {reportPath: ''}); - const [{diffClusters}] = imageHandler.getImagesInfo(testResult); + const [{diffClusters}] = imageHandler.getImagesInfo(testResult) as ImageInfoFail[]; assert.deepEqual(diffClusters, [{left: 0, top: 0, right: 1, bottom: 1}]); }); diff --git a/test/unit/lib/plugin-api.js b/test/unit/lib/plugin-api.js index 6d303aa81..5863a8535 100644 --- a/test/unit/lib/plugin-api.js +++ b/test/unit/lib/plugin-api.js @@ -2,6 +2,7 @@ const {HtmlReporter} = require('lib/plugin-api'); const {PluginEvents} = require('lib/constants/plugin-events'); +const {ToolName} = require('lib/constants'); describe('plugin api', () => { it('should store extra items', () => { @@ -29,6 +30,7 @@ describe('plugin api', () => { pluginApi.reportsSaver = {some: 'reports_saver'}; assert.deepEqual(pluginApi.values, { + toolName: ToolName.Hermione, extraItems: {key1: 'value1'}, metaInfoExtenders: {key2: 'value2'}, imagesSaver: {some: 'images_saver'}, diff --git a/test/unit/lib/static/components/section/body/tabs.jsx b/test/unit/lib/static/components/section/body/tabs.jsx index 8ee7174ce..9f1ea971f 100644 --- a/test/unit/lib/static/components/section/body/tabs.jsx +++ b/test/unit/lib/static/components/section/body/tabs.jsx @@ -2,12 +2,13 @@ import React from 'react'; import proxyquire from 'proxyquire'; import {defaultsDeep} from 'lodash'; import {ERROR, SUCCESS} from 'lib/constants/test-statuses'; +import {mkConnectedComponent} from '../../utils'; describe('', () => { const sandbox = sinon.sandbox.create(); let Tabs, State; - const mkTabs = (props = {}) => { + const mkTabs = (props = {}, state) => { props = defaultsDeep(props, { result: { id: 'default-result-id', @@ -18,7 +19,20 @@ describe('', () => { } }); - return mount(); + const initialState = defaultsDeep(state, { + initialState: { + tree: { + images: { + byId: { + 'img-1': {stateName: 'some-state'}, + 'img-2': {stateName: 'some-state'} + } + } + } + } + }); + + return mkConnectedComponent(, initialState); }; beforeEach(() => { diff --git a/test/unit/lib/static/components/state/state-error.jsx b/test/unit/lib/static/components/state/state-error.jsx index d2eb0aadd..4f03a9a93 100644 --- a/test/unit/lib/static/components/state/state-error.jsx +++ b/test/unit/lib/static/components/state/state-error.jsx @@ -101,15 +101,4 @@ describe(' component', () => { assert.equal(component.find('.details__content .foo-bar').text(), ['some-hint']); }); }); - - describe('"togglePageScreenshot" action', () => { - it('should call on click in "Page screenshot"', () => { - const image = {actualImg: {}}; - - const component = mkStateErrorComponent({result: {error: {}}, image}); - component.find('.details__summary').last().simulate('click'); - - assert.calledOnceWith(actionsStub.togglePageScreenshot); - }); - }); }); diff --git a/test/unit/lib/static/modules/actions.js b/test/unit/lib/static/modules/actions.js index e9d96ef00..cc62d2f3d 100644 --- a/test/unit/lib/static/modules/actions.js +++ b/test/unit/lib/static/modules/actions.js @@ -6,6 +6,7 @@ import actionNames from 'lib/static/modules/action-names'; import {StaticTestsTreeBuilder} from 'lib/tests-tree-builder/static'; import {LOCAL_DATABASE_NAME} from 'lib/constants/database'; import {DiffModes} from 'lib/constants/diff-modes'; +import {ToolName} from 'lib/constants'; // eslint-disable-next-line globalThis.performance = globalThis.performance; // node v14 stub @@ -177,6 +178,9 @@ describe('lib/static/modules/actions', () => { mergeDatabasesStub = sandbox.stub().resolves(); global.window = { + data: { + apiValues: {toolName: ToolName.Hermione} + }, location: { href: 'http://localhost/random/path.html' } @@ -253,7 +257,9 @@ describe('lib/static/modules/actions', () => { it('should build tests tree', async () => { const db = {}; const suitesFromDb = ['rows-with-suites']; - const treeBuilderResult = {tree: {}, stats: {}, skips: {}, browsers: {}}; + // TODO: properly test this case. This PR only fixed default state, which is now correct, but + // this test never worked correctly + const treeBuilderResult = {tree: undefined, stats: {}, skips: [], browsers: []}; mergeDatabasesStub.resolves(db); getSuitesTableRows.withArgs(db).returns(suitesFromDb); @@ -272,7 +278,7 @@ describe('lib/static/modules/actions', () => { it('should init plugins with the config from data.js', async () => { const config = {pluginsEnabled: true, plugins: []}; - global.window.data = {config}; + global.window.data.config = config; await actions.initStaticReport()(dispatch); diff --git a/test/unit/lib/static/modules/reducers/grouped-tests/by/result.js b/test/unit/lib/static/modules/reducers/grouped-tests/by/result.js index 6a526307a..e9ce35071 100644 --- a/test/unit/lib/static/modules/reducers/grouped-tests/by/result.js +++ b/test/unit/lib/static/modules/reducers/grouped-tests/by/result.js @@ -15,7 +15,7 @@ describe('lib/static/modules/reducers/grouped-tests/by/result', () => { module = proxyquire('lib/static/modules/reducers/grouped-tests/by/result', { '../helpers': {handleActiveResults, addGroupItem, sortGroupValues}, - '../../../utils': {isAssertViewError} + '../../../../../common-utils': {isAssertViewError} }); }); diff --git a/test/unit/lib/static/modules/utils.js b/test/unit/lib/static/modules/utils.js index 8b635a536..a0a9c2237 100644 --- a/test/unit/lib/static/modules/utils.js +++ b/test/unit/lib/static/modules/utils.js @@ -4,9 +4,9 @@ const utils = require('lib/static/modules/utils'); const {IDLE, FAIL, ERROR, SKIPPED, SUCCESS} = require('lib/constants/test-statuses'); const {ViewMode} = require('lib/constants/view-modes'); const {SECTIONS, RESULT_KEYS, KEY_DELIMITER} = require('lib/constants/group-tests'); -const {NO_REF_IMAGE_ERROR} = require('lib/constants/errors').getCommonErrors(); const {mkBrowser, mkResult} = require('../../static/state-utils'); +const {ErrorName} = require('lib/errors'); describe('static/modules/utils', () => { describe('isSuiteIdle', () => { @@ -72,7 +72,7 @@ describe('static/modules/utils', () => { }); it('test with missing reference image', () => { - const error = {stack: NO_REF_IMAGE_ERROR}; + const error = {name: ErrorName.NO_REF_IMAGE}; assert.isTrue(utils.isAcceptable({status: ERROR, error})); }); diff --git a/test/unit/lib/test-adapter.ts b/test/unit/lib/test-adapter/hermione.ts similarity index 100% rename from test/unit/lib/test-adapter.ts rename to test/unit/lib/test-adapter/hermione.ts diff --git a/test/unit/lib/test-adapter/playwright.ts b/test/unit/lib/test-adapter/playwright.ts new file mode 100644 index 000000000..335fbb13f --- /dev/null +++ b/test/unit/lib/test-adapter/playwright.ts @@ -0,0 +1,277 @@ +import sinon from 'sinon'; +import _ from 'lodash'; +import proxyquire from 'proxyquire'; +import {TestCase, TestResult} from '@playwright/test/reporter'; +import {ImageTitleEnding, PlaywrightAttachment, PlaywrightTestAdapterOptions, PwtTestStatus} from 'lib/test-adapter/playwright'; +import {ErrorName, ImageDiffError, NoRefImageError} from 'lib/errors'; +import {TestStatus} from 'lib/constants'; + +describe('PlaywrightTestAdapter', () => { + let sandbox: sinon.SinonSandbox; + let PlaywrightTestAdapter: typeof import('lib/test-adapter/playwright').PlaywrightTestAdapter; + let imageSizeStub: sinon.SinonStub; + let playwrightCache: typeof import('lib/test-adapter/cache/playwright'); + + const createAttachment = (path: string): PlaywrightAttachment => ({ + contentType: 'image/png', + name: path, + path, + body: Buffer.from('dummy-data') + }); + + const mkTestCase = (overrides: Partial = {}): TestCase => _.defaults(overrides, { + parent: {project: sinon.stub().returns({name: 'some-browser'})}, + titlePath: sinon.stub().returns(['root', 'suite', 'subsuite', 'describe', 'test']), + annotations: [], + location: {file: 'test-file-path', column: 0, line: 0} + } as any); + const mkTestResult = (overrides: Partial = {}): TestResult => _.defaults(overrides, { + status: 'failed', + attachments: [ + createAttachment('state1' + ImageTitleEnding.Expected), + createAttachment('state1' + ImageTitleEnding.Diff), + createAttachment('state1' + ImageTitleEnding.Actual) + ], + errors: [{name: ErrorName.IMAGE_DIFF, message: 'Screenshot comparison failed', stack: ''}], + steps: [] + } as any); + + const mkAdapterOptions = (overrides: Partial = {}): PlaywrightTestAdapterOptions => _.defaults(overrides, { + imagesInfoFormatter: sinon.stub() + } as any); + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + playwrightCache = {testsAttempts: new Map()}; + + imageSizeStub = sinon.stub().returns({height: 100, width: 200}); + + PlaywrightTestAdapter = proxyquire('lib/test-adapter/playwright', { + 'image-size': imageSizeStub, + './cache/playwright': playwrightCache + }).PlaywrightTestAdapter; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('assertViewResults', () => { + it('should return an IMAGE_DIFF result when error is IMAGE_DIFF and all images are present', () => { + const testCaseStub = mkTestCase(); + const testResultStub = mkTestResult(); + const adapter = new PlaywrightTestAdapter(testCaseStub, testResultStub, mkAdapterOptions()); + + const results = adapter.assertViewResults as ImageDiffError[]; + + assert.lengthOf(results, 1); + assert.strictEqual(results[0].name, ErrorName.IMAGE_DIFF); + assert.strictEqual(results[0].stateName, 'state1'); + assert.strictEqual(results[0].refImg?.path, 'state1' + ImageTitleEnding.Expected); + assert.strictEqual(results[0].diffImg?.path, 'state1' + ImageTitleEnding.Diff); + assert.strictEqual(results[0].currImg?.path, 'state1' + ImageTitleEnding.Actual); + }); + + it('should return a NO_REF_IMAGE result when error is NO_REF_IMAGE and only actual image is present', () => { + const testCaseStub = mkTestCase(); + const testResultStub = mkTestResult({ + attachments: [createAttachment('state1' + ImageTitleEnding.Actual)], + errors: [{name: ErrorName.NO_REF_IMAGE, message: 'snapshot doesn\'t exist: some.png.', stack: 'error-stack'}] as any + }); + const adapter = new PlaywrightTestAdapter(testCaseStub, testResultStub, mkAdapterOptions()); + + const results = adapter.assertViewResults as NoRefImageError[]; + + assert.lengthOf(results, 1); + assert.strictEqual(results[0].name, ErrorName.NO_REF_IMAGE); + assert.strictEqual(results[0].stateName, 'state1'); + assert.strictEqual(results[0].currImg?.path, 'state1' + ImageTitleEnding.Actual); + }); + }); + + describe('attempt', () => { + it('should return suite attempt', () => { + // eslint-disable-next-line no-new + new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), mkAdapterOptions()); + const adapter2 = new PlaywrightTestAdapter(mkTestCase({titlePath: sinon.stub().returns(['another-title'])}), mkTestResult(), mkAdapterOptions()); + const adapter3 = new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), mkAdapterOptions()); + + assert.equal(adapter3.attempt, 1); + assert.equal(adapter2.attempt, 0); + }); + + it('should not increment attempt for skipped tests', () => { + const testResult = mkTestResult({status: 'skipped'}); + + // eslint-disable-next-line no-new + new PlaywrightTestAdapter(mkTestCase(), testResult, mkAdapterOptions()); + const adapter2 = new PlaywrightTestAdapter(mkTestCase(), testResult, mkAdapterOptions()); + + assert.equal(adapter2.attempt, 0); + }); + }); + + describe('browserId', () => { + it('should return browserId', () => { + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), mkAdapterOptions()); + + assert.equal(adapter.browserId, 'some-browser'); + }); + }); + + describe('error', () => { + it('should return undefined if there are no errors', () => { + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({errors: []}), mkAdapterOptions()); + + const {error} = adapter; + + assert.isUndefined(error); + }); + + it('should return an error with name NO_REF_IMAGE for snapshot missing errors', () => { + const errorMessage = 'A snapshot doesn\'t exist: image-name.png.'; + const errors = [{message: errorMessage}]; + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({errors}), mkAdapterOptions()); + + const {error} = adapter; + + assert.strictEqual(error?.name, ErrorName.NO_REF_IMAGE); + assert.strictEqual(error?.message, errorMessage); + }); + + it('should return an error with name IMAGE_DIFF for screenshot comparison failures', () => { + const errorMessage = 'Screenshot comparison failed'; + const errors = [{message: errorMessage}]; + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({errors}), mkAdapterOptions()); + + const {error} = adapter; + + assert.strictEqual(error?.name, ErrorName.IMAGE_DIFF); + assert.strictEqual(error?.message, errorMessage); + }); + + it('should include the error stack if present', () => { + const errorMessage = 'Some error occurred'; + const errorStack = 'Error: Some error occurred at some-file.ts:10:15'; + const errors = [{message: errorMessage, stack: errorStack}]; + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({errors}), mkAdapterOptions()); + + const {error} = adapter; + + assert.strictEqual(error?.stack, errorStack); + }); + + it('should convert multiple errors to a single JSON string', () => { + const errors = [ + {message: 'First error', stack: 'Error: First error at some-file.ts:5:10'}, + {message: 'Second error', stack: 'Error: Second error at another-file.ts:15:20'} + ]; + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({errors}), mkAdapterOptions()); + const expectedMessage = JSON.stringify(errors.map(err => err.message)); + const expectedStack = JSON.stringify(errors.map(err => err.stack)); + + const {error} = adapter; + + assert.strictEqual(error?.message, expectedMessage); + assert.strictEqual(error?.stack, expectedStack); + }); + }); + + describe('file', () => { + it('should return file path', () => { + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), mkAdapterOptions()); + + assert.strictEqual(adapter.file, 'test-file-path'); + }); + }); + + describe('fullName', () => { + it('should return fullName', () => { + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), mkAdapterOptions()); + + assert.strictEqual(adapter.fullName, 'describe › test'); + }); + }); + + describe('history', () => { + it('should return an array of formatted step titles and durations', () => { + const steps = [ + {title: 'Step1', duration: 100}, + {title: 'Step2', duration: 200} + ]; + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({steps} as any), mkAdapterOptions()); + const expectedHistory = ['Step1 <- 100ms\n', 'Step2 <- 200ms\n']; + + assert.deepEqual(adapter.history, expectedHistory); + }); + }); + + describe('id', () => { + it('should return id', () => { + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), mkAdapterOptions()); + + assert.strictEqual(adapter.id, 'describe test some-browser 0'); + }); + }); + + describe('imageDir', () => { + it('should return imageDir', () => { + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), mkAdapterOptions()); + + assert.strictEqual(adapter.imageDir, '4050de5'); + }); + }); + + describe('imagesInfo', () => { + it('should call formatter', () => { + const getImagesInfoStub = sinon.stub(); + const options = mkAdapterOptions({imagesInfoFormatter: {getImagesInfo: getImagesInfoStub}}); + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), options); + + adapter.imagesInfo; + + assert.calledOnceWith(getImagesInfoStub, adapter); + }); + }); + + describe('status', () => { + it('should return SUCCESS for PASSED PwtTestStatus', () => { + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.PASSED}), mkAdapterOptions()); + + assert.equal(adapter.status, TestStatus.SUCCESS); + }); + + it('should return FAIL for FAILED PwtTestStatus', () => { + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.FAILED}), mkAdapterOptions()); + + assert.equal(adapter.status, TestStatus.FAIL); + }); + + it('should return FAIL for TIMED_OUT PwtTestStatus', () => { + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.TIMED_OUT}), mkAdapterOptions()); + + assert.equal(adapter.status, TestStatus.FAIL); + }); + + it('should return FAIL for INTERRUPTED PwtTestStatus', () => { + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.INTERRUPTED}), mkAdapterOptions()); + + assert.equal(adapter.status, TestStatus.FAIL); + }); + + it('should return SKIPPED for any other PwtTestStatus', () => { + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.SKIPPED}), mkAdapterOptions()); + + assert.equal(adapter.status, TestStatus.SKIPPED); + }); + }); + + describe('testPath', () => { + it('should return testPath', () => { + const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), mkAdapterOptions()); + + assert.deepEqual(adapter.testPath, ['describe', 'test']); + }); + }); +}); diff --git a/test/unit/lib/tests-tree-builder/base.js b/test/unit/lib/tests-tree-builder/base.js index ac6fa30d3..35e6163cd 100644 --- a/test/unit/lib/tests-tree-builder/base.js +++ b/test/unit/lib/tests-tree-builder/base.js @@ -4,6 +4,7 @@ const _ = require('lodash'); const proxyquire = require('proxyquire'); const {FAIL, ERROR, SUCCESS} = require('lib/constants/test-statuses'); const {BrowserVersions} = require('lib/constants/browser'); +const {ToolName} = require('lib/constants'); describe('ResultsTreeBuilder', () => { const sandbox = sinon.sandbox.create(); @@ -27,7 +28,7 @@ describe('ResultsTreeBuilder', () => { '../common-utils': {determineStatus} }).BaseTestsTreeBuilder; - builder = ResultsTreeBuilder.create(); + builder = ResultsTreeBuilder.create({toolName: ToolName.Hermione}); }); afterEach(() => sandbox.restore()); diff --git a/test/unit/lib/tests-tree-builder/gui.js b/test/unit/lib/tests-tree-builder/gui.js index 17e2cb7e0..e4770239b 100644 --- a/test/unit/lib/tests-tree-builder/gui.js +++ b/test/unit/lib/tests-tree-builder/gui.js @@ -3,6 +3,7 @@ const _ = require('lodash'); const GuiResultsTreeBuilder = require('lib/tests-tree-builder/gui'); const {FAIL, SUCCESS, IDLE, UPDATED} = require('lib/constants/test-statuses'); +const {ToolName} = require('lib/constants'); describe('GuiResultsTreeBuilder', () => { let builder; @@ -19,8 +20,10 @@ describe('GuiResultsTreeBuilder', () => { }); }; + const mkGuiTreeBuilder = () => GuiResultsTreeBuilder.create({toolName: ToolName.Hermione}); + beforeEach(() => { - builder = GuiResultsTreeBuilder.create(); + builder = mkGuiTreeBuilder(); }); describe('"getLastResult" method', () => { @@ -72,7 +75,7 @@ describe('GuiResultsTreeBuilder', () => { describe('"reuseTestsTree" method', () => { describe('reuse browsers', () => { it('should not reuse browser result if browser ids are not matched', () => { - const srcBuilder = GuiResultsTreeBuilder.create(); + const srcBuilder = mkGuiTreeBuilder(); srcBuilder.addTestResult( mkTestResult_(), mkFormattedResult_({testPath: ['s1'], browserId: 'b1', attempt: 0}) @@ -90,7 +93,7 @@ describe('GuiResultsTreeBuilder', () => { }); it('should reuse browser result from the passed tree if browser ids matched', () => { - const srcBuilder = GuiResultsTreeBuilder.create(); + const srcBuilder = mkGuiTreeBuilder(); srcBuilder.addTestResult( mkTestResult_(), mkFormattedResult_({testPath: ['s1'], browserId: 'b1', attempt: 0}) @@ -109,7 +112,7 @@ describe('GuiResultsTreeBuilder', () => { describe('reuse test results', () => { it('should not reuse result if browser ids does not matched', () => { - const srcBuilder = GuiResultsTreeBuilder.create(); + const srcBuilder = mkGuiTreeBuilder(); srcBuilder.addTestResult( mkTestResult_({status: FAIL}), mkFormattedResult_({testPath: ['s1'], browserId: 'b1', attempt: 0}) @@ -127,7 +130,7 @@ describe('GuiResultsTreeBuilder', () => { }); it('should reuse all results from the passed tree if browser ids matched', () => { - const srcBuilder = GuiResultsTreeBuilder.create(); + const srcBuilder = mkGuiTreeBuilder(); srcBuilder.addTestResult( mkTestResult_({status: FAIL}), mkFormattedResult_({testPath: ['s1'], browserId: 'b1', attempt: 0}) @@ -149,7 +152,7 @@ describe('GuiResultsTreeBuilder', () => { }); it('should register reused result ids', () => { - const srcBuilder = GuiResultsTreeBuilder.create(); + const srcBuilder = mkGuiTreeBuilder(); srcBuilder.addTestResult( mkTestResult_(), mkFormattedResult_({testPath: ['s1'], browserId: 'b1', attempt: 1}) @@ -168,7 +171,7 @@ describe('GuiResultsTreeBuilder', () => { describe('reuse images', () => { it('should not reuse images if browser ids does not matched', () => { - const srcBuilder = GuiResultsTreeBuilder.create(); + const srcBuilder = mkGuiTreeBuilder(); srcBuilder.addTestResult( mkTestResult_({imagesInfo: [{stateName: 'img1'}]}), mkFormattedResult_({testPath: ['s1'], browserId: 'b1', attempt: 0}) @@ -186,7 +189,7 @@ describe('GuiResultsTreeBuilder', () => { }); it('should reuse all images from the passed tree if browser ids matched', () => { - const srcBuilder = GuiResultsTreeBuilder.create(); + const srcBuilder = mkGuiTreeBuilder(); srcBuilder.addTestResult( mkTestResult_({imagesInfo: [{stateName: 'img1'}, {stateName: 'img2'}]}), mkFormattedResult_({testPath: ['s1'], browserId: 'b1', attempt: 0}) @@ -204,7 +207,7 @@ describe('GuiResultsTreeBuilder', () => { }); it('should register reused images ids', () => { - const srcBuilder = GuiResultsTreeBuilder.create(); + const srcBuilder = mkGuiTreeBuilder(); srcBuilder.addTestResult( mkTestResult_({imagesInfo: [{stateName: 'img1'}, {stateName: 'img2'}]}), mkFormattedResult_({testPath: ['s1'], browserId: 'b1', attempt: 0}) @@ -223,7 +226,7 @@ describe('GuiResultsTreeBuilder', () => { describe('reuse suite status', () => { it('should not reuse suite status if browser ids does not matched', () => { - const srcBuilder = GuiResultsTreeBuilder.create(); + const srcBuilder = mkGuiTreeBuilder(); srcBuilder.addTestResult( mkTestResult_({status: FAIL}), mkFormattedResult_({testPath: ['s1'], browserId: 'b1', attempt: 0}) @@ -240,7 +243,7 @@ describe('GuiResultsTreeBuilder', () => { }); it('should reuse suite status from passed tree with if browser ids matched', () => { - const srcBuilder = GuiResultsTreeBuilder.create(); + const srcBuilder = mkGuiTreeBuilder(); srcBuilder.addTestResult( mkTestResult_({status: FAIL}), mkFormattedResult_({testPath: ['s1'], browserId: 'b1', attempt: 0}) diff --git a/test/unit/lib/tests-tree-builder/static.js b/test/unit/lib/tests-tree-builder/static.js index d80978ad3..2404e359e 100644 --- a/test/unit/lib/tests-tree-builder/static.js +++ b/test/unit/lib/tests-tree-builder/static.js @@ -4,6 +4,7 @@ const _ = require('lodash'); const {StaticTestsTreeBuilder} = require('lib/tests-tree-builder/static'); const {SUCCESS} = require('lib/constants/test-statuses'); const {BrowserVersions} = require('lib/constants/browser'); +const {ToolName} = require('lib/constants'); describe('StaticResultsTreeBuilder', () => { const sandbox = sinon.sandbox.create(); @@ -68,7 +69,7 @@ describe('StaticResultsTreeBuilder', () => { sandbox.stub(StaticTestsTreeBuilder.prototype, 'addTestResult'); sandbox.stub(StaticTestsTreeBuilder.prototype, 'sortTree'); - builder = StaticTestsTreeBuilder.create(); + builder = StaticTestsTreeBuilder.create({toolName: ToolName.Hermione}); }); afterEach(() => sandbox.restore()); diff --git a/tsconfig.json b/tsconfig.json index be93720e4..68083ee03 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.common.json", - "include": ["lib", "hermione.js", "gemini.js"], + "include": ["lib", "hermione.js", "gemini.js", "playwright.ts"], "exclude": ["lib/static",], "compilerOptions": { "outDir": "build", diff --git a/tsconfig.spec.json b/tsconfig.spec.json index aff5e05b5..375ce25db 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.common.json", - "include": ["lib", "test", "hermione.js", "gemini.js"], + "include": ["lib", "test", "hermione.js", "gemini.js", "playwright.ts"], + "exclude": ["test/func"], "compilerOptions": { "baseUrl": ".", "jsx": "react",