From 7d7ee9b05c9ef537d03b53283a11f63198af487c Mon Sep 17 00:00:00 2001 From: shadowusr Date: Tue, 19 Dec 2023 02:56:39 +0300 Subject: [PATCH] draft: save changes --- lib/cache.ts | 33 +++ lib/common-utils.ts | 4 +- lib/errors/index.ts | 12 +- lib/gui/tool-runner/index.ts | 22 +- lib/image-handler.ts | 20 +- lib/image-store.ts | 10 +- lib/images-info-saver.ts | 218 ++++++++++++++++++ ...ges-saver.ts => local-image-file-saver.ts} | 4 +- lib/plugin-api.ts | 12 +- lib/report-builder/static.ts | 4 +- lib/reporter-helpers.ts | 4 +- lib/server-utils.ts | 40 +++- .../section/body/page-screenshot.tsx | 4 +- lib/test-adapter/hermione.ts | 2 +- lib/test-adapter/index.ts | 7 +- lib/test-adapter/playwright.ts | 8 +- lib/test-adapter/reporter.ts | 10 +- lib/test-adapter/sqlite.ts | 6 +- lib/test-adapter/utils/index.ts | 15 +- lib/types.ts | 39 ++-- lib/workers/worker.ts | 6 +- package.json | 2 +- test/unit/lib/image-handler.ts | 4 +- 23 files changed, 390 insertions(+), 96 deletions(-) create mode 100644 lib/cache.ts create mode 100644 lib/images-info-saver.ts rename lib/{local-images-saver.ts => local-image-file-saver.ts} (67%) diff --git a/lib/cache.ts b/lib/cache.ts new file mode 100644 index 000000000..5a8fc4227 --- /dev/null +++ b/lib/cache.ts @@ -0,0 +1,33 @@ +export class Cache { + private _getKeyHash: (key: Key) => string; + private _cache: Map; + + constructor(hashFn: (key: Key) => string) { + this._getKeyHash = hashFn; + this._cache = new Map(); + } + + has(key: Key): boolean { + const keyHash = this._getKeyHash(key); + + return this._cache.has(keyHash); + } + + get(key: Key): Value | undefined { + const keyHash = this._getKeyHash(key); + + return this._cache.get(keyHash); + } + + set(key: Key, value: Value): this { + const keyHash = this._getKeyHash(key); + + if (value !== undefined) { + this._cache.set(keyHash, value); + } else { + this._cache.delete(keyHash); + } + + return this; + } +} diff --git a/lib/common-utils.ts b/lib/common-utils.ts index 1988b43e4..08a5e69fd 100644 --- a/lib/common-utils.ts +++ b/lib/common-utils.ts @@ -17,7 +17,7 @@ import { } from './constants'; import {CHECKED, INDETERMINATE, UNCHECKED} from './constants/checked-statuses'; -import {ImageBase64, ImageData, ImageInfoFail, ImageInfoFull, TestError} from './types'; +import {ImageBase64, ImageFileData, ImageInfoFail, ImageInfoFull, TestError} from './types'; import {ErrorName, ImageDiffError, NoRefImageError} from './errors'; import {ReporterTestResult} from './test-adapter'; @@ -164,7 +164,7 @@ export const determineStatus = (testResult: Pick { +export const isBase64Image = (image: ImageFileData | ImageBase64 | null | undefined): image is ImageBase64 => { return Boolean((image as ImageBase64 | undefined)?.base64); }; diff --git a/lib/errors/index.ts b/lib/errors/index.ts index 0e6b1d449..19c9203bf 100644 --- a/lib/errors/index.ts +++ b/lib/errors/index.ts @@ -1,5 +1,5 @@ import {CoordBounds} from 'looks-same'; -import {DiffOptions, ImageData} from '../types'; +import {DiffOptions, ImageFileData} from '../types'; import {ValueOf} from 'type-fest'; export const ErrorName = { @@ -17,11 +17,11 @@ export interface ImageDiffError { stack: string; stateName: string; diffOpts: DiffOptions; - currImg: ImageData; - refImg: ImageData; + currImg: ImageFileData; + refImg: ImageFileData; diffClusters: CoordBounds[]; diffBuffer?: ArrayBuffer; - diffImg?: ImageData; + diffImg?: ImageFileData; } export interface NoRefImageError { @@ -29,6 +29,6 @@ export interface NoRefImageError { stateName: string; message: string; stack?: string; - currImg: ImageData; - refImg?: ImageData; + currImg: ImageFileData; + refImg?: ImageFileData; } diff --git a/lib/gui/tool-runner/index.ts b/lib/gui/tool-runner/index.ts index 5eed1cd92..74f425509 100644 --- a/lib/gui/tool-runner/index.ts +++ b/lib/gui/tool-runner/index.ts @@ -26,14 +26,14 @@ import { } from '../../constants'; import {formatId, mkFullTitle, mergeDatabasesForReuse, filterByEqualDiffSizes} from './utils'; import {getTestsTreeFromDatabase} from '../../db-utils/server'; -import {formatTestResult} from '../../server-utils'; +import {formatTestResult, getExpectedCacheKey} from '../../server-utils'; import { AssertViewResult, HermioneTestResult, HtmlReporterApi, - ImageData, + ImageFileData, ImageInfoFail, - ReporterConfig + ReporterConfig, TestSpecByPath } from '../../types'; import {GuiCliOptions, GuiConfigs} from '../index'; import {Tree, TreeImage} from '../../tests-tree-builder/base'; @@ -45,13 +45,14 @@ import {ImagesInfoFormatter} from '../../image-handler'; import {SqliteClient} from '../../sqlite-client'; import PQueue from 'p-queue'; import os from 'os'; +import {Cache} from '../../cache'; type ToolRunnerArgs = [paths: string[], hermione: Hermione & HtmlReporterApi, configs: GuiConfigs]; export type ToolRunnerTree = GuiReportBuilderResult & Pick; interface HermioneTestExtended extends HermioneTest { - assertViewResults: {stateName: string, refImg: ImageData, currImg: ImageData}; + assertViewResults: {stateName: string, refImg: ImageFileData, currImg: ImageFileData}; attempt: number; imagesInfo: Pick[]; } @@ -85,6 +86,7 @@ export class ToolRunner { private _eventSource: EventSource; protected _reportBuilder: GuiReportBuilder | null; private _tests: Record; + private _expectedImagesCache: Cache<[TestSpecByPath, string | undefined], string>; static create(this: new (...args: ToolRunnerArgs) => T, ...args: ToolRunnerArgs): T { return new this(...args); @@ -105,6 +107,8 @@ export class ToolRunner { this._reportBuilder = null; this._tests = {}; + + this._expectedImagesCache = new Cache(getExpectedCacheKey); } get config(): HermioneConfig { @@ -241,10 +245,10 @@ export class ToolRunner { } if (previousExpectedPath && (updateResult as HermioneTest).fullTitle) { - reportBuilder.imageHandler.updateCacheExpectedPath({ - fullName: (updateResult as HermioneTest).fullTitle(), + this._expectedImagesCache.set([{ + testPath: [(updateResult as HermioneTest).fullTitle()], browserId: (updateResult as HermioneTest).browserId - }, stateName, previousExpectedPath); + }, stateName], previousExpectedPath); } })); })); @@ -345,7 +349,7 @@ export class ToolRunner { const imagesInfo = test.imagesInfo .filter(({stateName, actualImg}) => Boolean(stateName) && Boolean(actualImg)) .map((imageInfo) => { - const {stateName, actualImg} = imageInfo as {stateName: string, actualImg: ImageData}; + const {stateName, actualImg} = imageInfo as {stateName: string, actualImg: ImageFileData}; const path = this._hermione.config.browsers[browserId].getScreenshotPath(rawTest, stateName); const refImg = {path, size: actualImg.size}; @@ -370,7 +374,7 @@ export class ToolRunner { : res; } - protected _emitUpdateReference({refImg}: {refImg: ImageData}, state: string): void { + protected _emitUpdateReference({refImg}: {refImg: ImageFileData}, state: string): void { this._hermione.emit( this._hermione.events.UPDATE_REFERENCE, {refImg, state} diff --git a/lib/image-handler.ts b/lib/image-handler.ts index c53badaf9..4f5a880df 100644 --- a/lib/image-handler.ts +++ b/lib/image-handler.ts @@ -10,11 +10,11 @@ import * as utils from './server-utils'; import { AssertViewResult, ImageBase64, - ImageData, + ImageFileData, ImageInfo, ImageInfoError, ImageInfoFail, ImageInfoFull, - ImagesSaver, + ImageFileSaver, ImageInfoPageSuccess } from './types'; import {ERROR, FAIL, PluginEvents, SUCCESS, TestStatus, UNKNOWN_ATTEMPT, UPDATED} from './constants'; @@ -49,29 +49,29 @@ interface TestSpec { export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter { private _imageStore: ImageStore; - private _imagesSaver: ImagesSaver; + private _imagesSaver: ImageFileSaver; private _options: ImageHandlerOptions; - constructor(imageStore: ImageStore, imagesSaver: ImagesSaver, options: ImageHandlerOptions) { + constructor(imageStore: ImageStore, imagesSaver: ImageFileSaver, options: ImageHandlerOptions) { super(); this._imageStore = imageStore; this._imagesSaver = imagesSaver; this._options = options; } - static getCurrImg(assertViewResults: AssertViewResult[], stateName?: string): ImageData | undefined { + static getCurrImg(assertViewResults: AssertViewResult[], stateName?: string): ImageFileData | undefined { return _.get(_.find(assertViewResults, {stateName}), 'currImg'); } - static getDiffImg(assertViewResults: AssertViewResult[], stateName?: string): ImageData | undefined { + static getDiffImg(assertViewResults: AssertViewResult[], stateName?: string): ImageFileData | undefined { return _.get(_.find(assertViewResults, {stateName}), 'diffImg'); } - static getRefImg(assertViewResults: AssertViewResult[], stateName?: string): ImageData | undefined { + static getRefImg(assertViewResults: AssertViewResult[], stateName?: string): ImageFileData | undefined { return _.get(_.find(assertViewResults, {stateName}), 'refImg'); } - static getScreenshot(testResult: ReporterTestResultPlain): ImageBase64 | ImageData | null | undefined { + static getScreenshot(testResult: ReporterTestResultPlain): ImageBase64 | ImageFileData | null | undefined { return testResult.screenshot; } @@ -236,7 +236,7 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter { return result; } - setImagesSaver(newImagesSaver: ImagesSaver): void { + setImagesSaver(newImagesSaver: ImageFileSaver): void { this._imagesSaver = newImagesSaver; } @@ -333,7 +333,7 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter { private async _savePageScreenshot(testResult: ReporterTestResultPlain): Promise { const screenshot = ImageHandler.getScreenshot(testResult); - if (!(screenshot as ImageBase64)?.base64 && !(screenshot as ImageData)?.path) { + if (!(screenshot as ImageBase64)?.base64 && !(screenshot as ImageFileData)?.path) { logger.warn('Cannot save screenshot on reject'); return Promise.resolve(); diff --git a/lib/image-store.ts b/lib/image-store.ts index 5970f0ea3..44805afa8 100644 --- a/lib/image-store.ts +++ b/lib/image-store.ts @@ -1,10 +1,9 @@ import {DB_COLUMNS} from './constants'; import {SqliteClient} from './sqlite-client'; -import {ImageInfo, ImageInfoFull, LabeledSuitesRow} from './types'; -import {ReporterTestResultPlain} from './image-handler'; +import {ImageInfo, ImageInfoFull, LabeledSuitesRow, TestSpecByPath} from './types'; export interface ImageStore { - getLastImageInfoFromDb(testResult: ReporterTestResultPlain, stateName?: string): ImageInfo | undefined ; + getLastImageInfoFromDb(testResult: TestSpecByPath, stateName?: string): ImageInfo | undefined ; } export class SqliteImageStore implements ImageStore { @@ -14,7 +13,7 @@ export class SqliteImageStore implements ImageStore { this._dbClient = dbClient; } - getLastImageInfoFromDb(testResult: ReporterTestResultPlain, stateName?: string): ImageInfo | undefined { + getLastImageInfoFromDb(testResult: TestSpecByPath, stateName?: string): ImageInfo | undefined { const browserName = testResult.browserId; const suitePath = testResult.testPath; const suitePathString = JSON.stringify(suitePath); @@ -23,7 +22,8 @@ export class SqliteImageStore implements ImageStore { select: DB_COLUMNS.IMAGES_INFO, where: `${DB_COLUMNS.SUITE_PATH} = ? AND ${DB_COLUMNS.NAME} = ?`, orderBy: DB_COLUMNS.TIMESTAMP, - orderDescending: true + orderDescending: true, + noCache: true }, suitePathString, browserName); const imagesInfo: ImageInfoFull[] = imagesInfoResult && JSON.parse(imagesInfoResult[DB_COLUMNS.IMAGES_INFO as keyof Pick]) || []; diff --git a/lib/images-info-saver.ts b/lib/images-info-saver.ts new file mode 100644 index 000000000..f06b63d96 --- /dev/null +++ b/lib/images-info-saver.ts @@ -0,0 +1,218 @@ +import makeDebug from 'debug'; +import EventEmitter2 from 'eventemitter2'; +import fs from 'fs-extra'; +import sizeOf from 'image-size'; +import _ from 'lodash'; +import PQueue from 'p-queue'; + +import {RegisterWorkers} from './workers/create-workers'; +import {ReporterTestResult} from './test-adapter'; +import { + DiffOptions, ImageBufferData, + ImageFileData, + ImageFileSaver, + ImageInfoFail, + ImageInfoFull, + ImageSize, TestSpecByPath +} from './types'; +import {copyAndUpdate, removeBufferFromImagesInfo} from './test-adapter/utils'; +import {cacheDiffImages} from './image-cache'; +import {PluginEvents, TestStatus, UPDATED} from './constants'; +import {createHash, getCurrentPath, getDiffPath, getReferencePath, isImageBufferData, makeDirFor} from './server-utils'; +import {mkTestId} from './common-utils'; +import {ImageStore} from './image-store'; +import {Cache} from './cache'; + +const debug = makeDebug('html-reporter:images-info-saver'); + +interface ImagesInfoSaverOptions { + imageFileSaver: ImageFileSaver; + reportPath: string; + imageStore: ImageStore; + expectedPathsCache: Cache<[TestSpecByPath, string | undefined], string>; +} + +export class ImagesInfoSaver extends EventEmitter2 { + private _imageFileSaver: ImageFileSaver; + private _reportPath: string; + private _imageStore: ImageStore; + private _expectedPathsCache: Cache<[TestSpecByPath, string | undefined], string>; + + constructor(options: ImagesInfoSaverOptions) { + super(); + + this._imageFileSaver = options.imageFileSaver; + this._reportPath = options.reportPath; + this._imageStore = options.imageStore; + this._expectedPathsCache = options.expectedPathsCache; + } + + async save(testResult: ReporterTestResult, workers: RegisterWorkers<['saveDiffTo']>): Promise { + const testDebug = debug.extend(testResult.imageDir); + testDebug(`Saving images of ${testResult.id}`); + + const newImagesInfos: ImageInfoFull[] = []; + + await Promise.all(testResult.imagesInfo.map(async (imagesInfo, index) => { + const imageDebug = testDebug.extend(index.toString()); + imageDebug.enabled && imageDebug('Handling %j', removeBufferFromImagesInfo(imagesInfo)); + + const newImagesInfo = _.clone(imagesInfo); + const {stateName} = imagesInfo as ImageInfoFail; + const actions = new PQueue(); + + actions.add(async () => { + (newImagesInfo as {actualImg?: ImageFileData}).actualImg + = await this._saveActualImageIfNeeded(testResult, imagesInfo, stateName, {logger: imageDebug}); + }); + + actions.add(async () => { + (newImagesInfo as {diffImg?: ImageFileData}).diffImg = + await this._saveDiffImageIfNeeded(testResult, imagesInfo, stateName, {workers, logger: imageDebug}); + }); + + actions.add(async () => { + (newImagesInfo as {expectedImg?: ImageFileData}).expectedImg = + await this._saveExpectedImageIfNeeded(testResult, imagesInfo, stateName, {logger: imageDebug}); + }); + + await actions.onIdle(); + + newImagesInfos.push(newImagesInfo); + })); + + await this.emitAsync(PluginEvents.TEST_SCREENSHOTS_SAVED, { + testId: mkTestId(testResult.fullName, testResult.browserId), + attempt: testResult.attempt, + imagesInfo: newImagesInfos + }); + + return copyAndUpdate(testResult, {imagesInfo: newImagesInfos}); + } + + setImageFileSaver(imageFileSaver: ImageFileSaver): void { + this._imageFileSaver = imageFileSaver; + } + + private async _createDiffInFile(imagesInfo: ImageInfoFail, filePath: string, workers: RegisterWorkers<['saveDiffTo']>): Promise { + await makeDirFor(filePath); + + const actualPath = imagesInfo.actualImg.path; + const expectedPath = imagesInfo.expectedImg.path; + + const [currBuffer, refBuffer] = await Promise.all([ + fs.readFile(actualPath), + fs.readFile(expectedPath) + ]); + + const hash = createHash(currBuffer) + createHash(refBuffer); + + if (cacheDiffImages.has(hash)) { + const cachedDiffPath = cacheDiffImages.get(hash) as string; + + await fs.copy(cachedDiffPath, filePath); + } else { + await workers.saveDiffTo({ + ...imagesInfo.diffOptions, + reference: expectedPath, + current: actualPath + } satisfies DiffOptions, filePath); + + cacheDiffImages.set(hash, filePath); + } + + return {path: filePath, size: _.pick(sizeOf(filePath), ['height', 'width']) as ImageSize}; + } + + private _getReusedExpectedPath(testResult: TestSpecByPath, imagesInfo: ImageInfoFull): string | null { + if (imagesInfo.status === UPDATED) { + return null; + } + + const {stateName} = imagesInfo as ImageInfoFail; + + if (this._expectedPathsCache.has([testResult, stateName])) { + return this._expectedPathsCache.get([testResult, stateName]) as string; + } + + const lastImageInfo = this._imageStore.getLastImageInfoFromDb(testResult, stateName) as ImageInfoFail; + + if (lastImageInfo && lastImageInfo.expectedImg) { + this._expectedPathsCache.set([testResult, stateName], (lastImageInfo.expectedImg as ImageFileData).path); + return (lastImageInfo.expectedImg as ImageFileData).path; + } + + return null; + } + + private async _saveImage(imageData: ImageFileData | ImageBufferData, destPath: string): Promise { + const sourceFilePath = isImageBufferData(imageData) ? destPath : imageData.path; + if (isImageBufferData(imageData)) { + await fs.writeFile(destPath, imageData.buffer); + } + + const savedFilePath = await this._imageFileSaver.saveImg(sourceFilePath, { + destPath, + reportDir: this._reportPath + }); + + return savedFilePath || destPath; + } + + private async _saveActualImageIfNeeded(testResult: ReporterTestResult, imagesInfo: ImageInfoFull, stateName: string | undefined, {logger}: {logger: debug.Debugger}): Promise { + if (!(imagesInfo as ImageInfoFail).actualImg) { + return; + } + const {actualImg} = imagesInfo as ImageInfoFail; + const reportActualPath = getCurrentPath(testResult, stateName); + + const newActualPath = await this._saveImage(actualImg, reportActualPath); + logger.log(`Saved actual image from ${(actualImg as ImageFileData).path ?? ''} to ${newActualPath}`); + + return {path: newActualPath, size: actualImg.size}; + } + + private async _saveDiffImageIfNeeded( + testResult: ReporterTestResult, + imagesInfo: ImageInfoFull, + stateName: string | undefined, + {workers, logger}: {workers: RegisterWorkers<['saveDiffTo']>, logger: debug.Debugger} + ): Promise { + const shouldSaveDiff = imagesInfo.status === TestStatus.FAIL && imagesInfo.expectedImg; + if (!shouldSaveDiff) { + return; + } + let {diffImg} = imagesInfo; + const reportDiffPath = getDiffPath(testResult, stateName); + + if (!diffImg) { + diffImg = await this._createDiffInFile(imagesInfo, reportDiffPath, workers); + logger.log(`Created new diff in file ${reportDiffPath}`); + } + + const newDiffPath = await this._saveImage(diffImg, reportDiffPath); + logger.log(`Saved diff image from ${(diffImg as ImageFileData).path ?? ''} to ${newDiffPath}`); + + return {path: newDiffPath, size: diffImg.size}; + } + + private async _saveExpectedImageIfNeeded(testResult: ReporterTestResult, imagesInfo: ImageInfoFull, stateName: string | undefined, {logger}: {logger: debug.Debugger}): Promise { + if (!(imagesInfo as ImageInfoFail).expectedImg) { + return; + } + const {expectedImg} = imagesInfo as ImageInfoFail; + const reusedExpectedPath = this._getReusedExpectedPath(testResult, imagesInfo); + const reportDiffPath = reusedExpectedPath ?? getReferencePath(testResult, stateName); + + let newExpectedPath = reportDiffPath; + + if (!reusedExpectedPath) { + newExpectedPath = await this._saveImage(expectedImg, reportDiffPath); + logger.log(`Saved expected image from ${(expectedImg as ImageFileData).path ?? ''} to ${newExpectedPath}`); + } else { + logger.log(`Reused expected image from ${reusedExpectedPath}`); + } + + return {path: newExpectedPath, size: expectedImg.size}; + } +} diff --git a/lib/local-images-saver.ts b/lib/local-image-file-saver.ts similarity index 67% rename from lib/local-images-saver.ts rename to lib/local-image-file-saver.ts index dea4dc890..205ede6f3 100644 --- a/lib/local-images-saver.ts +++ b/lib/local-image-file-saver.ts @@ -1,7 +1,7 @@ import {copyFileAsync} from './server-utils'; -import type {ImagesSaver} from './types'; +import type {ImageFileSaver} from './types'; -export const LocalImagesSaver: ImagesSaver = { +export const LocalImageFileSaver: ImageFileSaver = { saveImg: async (srcCurrPath, {destPath, reportDir}) => { await copyFileAsync(srcCurrPath, destPath, {reportDir}); diff --git a/lib/plugin-api.ts b/lib/plugin-api.ts index e23f56544..5176d8d7d 100644 --- a/lib/plugin-api.ts +++ b/lib/plugin-api.ts @@ -1,15 +1,15 @@ import EventsEmitter2 from 'eventemitter2'; import {PluginEvents, ToolName} from './constants'; import {downloadDatabases, getTestsTreeFromDatabase, mergeDatabases} from './db-utils/server'; -import {LocalImagesSaver} from './local-images-saver'; +import {LocalImageFileSaver} from './local-image-file-saver'; import {version} from '../package.json'; -import {ImagesSaver, ReporterConfig, ReportsSaver} from './types'; +import {ImageFileSaver, ReporterConfig, ReportsSaver} from './types'; export interface HtmlReporterValues { toolName: ToolName; extraItems: Record; metaInfoExtenders: Record; - imagesSaver: ImagesSaver; + imagesSaver: ImageFileSaver; reportsSaver: ReportsSaver | null; } @@ -41,7 +41,7 @@ export class HtmlReporter extends EventsEmitter2 { toolName: toolName ?? ToolName.Hermione, extraItems: {}, metaInfoExtenders: {}, - imagesSaver: LocalImagesSaver, + imagesSaver: LocalImageFileSaver, reportsSaver: null }; this._version = version; @@ -75,12 +75,12 @@ export class HtmlReporter extends EventsEmitter2 { return this._values.metaInfoExtenders; } - set imagesSaver(imagesSaver: ImagesSaver) { + set imagesSaver(imagesSaver: ImageFileSaver) { this.emit(PluginEvents.IMAGES_SAVER_UPDATED, imagesSaver); this._values.imagesSaver = imagesSaver; } - get imagesSaver(): ImagesSaver { + get imagesSaver(): ImageFileSaver { return this._values.imagesSaver; } diff --git a/lib/report-builder/static.ts b/lib/report-builder/static.ts index 38843eceb..43b9a8293 100644 --- a/lib/report-builder/static.ts +++ b/lib/report-builder/static.ts @@ -110,9 +110,7 @@ export class StaticReportBuilder { const actions: Promise[] = []; - if (!_.isEmpty(testResult.assertViewResults)) { - actions.push(this._imageHandler.saveTestImages(testResult, this._ensureWorkers())); - } + actions.push(this._imageHandler.saveTestImages(testResult, this._ensureWorkers())); if (this._pluginConfig.saveErrorDetails && testResult.errorDetails) { actions.push(saveErrorDetails(testResult, this._pluginConfig.path)); diff --git a/lib/reporter-helpers.ts b/lib/reporter-helpers.ts index f5d4ec2a2..405c852bb 100644 --- a/lib/reporter-helpers.ts +++ b/lib/reporter-helpers.ts @@ -4,6 +4,7 @@ import {getShortMD5} from './common-utils'; import * as utils from './server-utils'; import {ImageHandler} from './image-handler'; import {ReporterTestResult} from './test-adapter'; +import {getImagesInfoByStateName} from './server-utils'; const mkReferenceHash = (testId: string, stateName: string): string => getShortMD5(`${testId}#${stateName}`); @@ -43,7 +44,8 @@ export const revertReferenceImage = async (removedResult: ReporterTestResult, ne }; export const removeReferenceImage = async (testResult: ReporterTestResult, stateName: string): Promise => { - const imagePath = ImageHandler.getRefImg(testResult.assertViewResults, stateName)?.path; + const imagesInfo = getImagesInfoByStateName(testResult, stateName); + const imagePath = imagesInfo?.refImg?.path; if (!imagePath) { return; diff --git a/lib/server-utils.ts b/lib/server-utils.ts index 714e670ae..26b32b27d 100644 --- a/lib/server-utils.ts +++ b/lib/server-utils.ts @@ -3,11 +3,18 @@ import url from 'url'; import chalk from 'chalk'; import _ from 'lodash'; import fs from 'fs-extra'; -import {logger} from './common-utils'; +import {getShortMD5, logger, mkTestId} from './common-utils'; import {UPDATED, RUNNING, IDLE, SKIPPED, IMAGES_PATH, TestStatus} from './constants'; import type {HtmlReporter} from './plugin-api'; import type {ReporterTestResult} from './test-adapter'; -import {CustomGuiItem, HermioneTestResult, ReporterConfig} from './types'; +import { + CustomGuiItem, + HermioneTestResult, + ImageBufferData, + ImageFileData, ImageInfoWithState, + ReporterConfig, + TestSpecByPath +} from './types'; import type Hermione from 'hermione'; import crypto from 'crypto'; import {ImagesInfoFormatter} from './image-handler'; @@ -17,35 +24,35 @@ import {Router} from 'express'; const DATA_FILE_NAME = 'data.js'; interface GetPathOptions { - stateName?: string; imageDir: string; attempt: number; browserId: string; } -export const getReferencePath = (options: GetPathOptions): string => createPath({kind: 'ref', ...options}); -export const getCurrentPath = (options: GetPathOptions): string => createPath({kind: 'current', ...options}); -export const getDiffPath = (options: GetPathOptions): string => createPath({kind: 'diff', ...options}); +export const getReferencePath = (options: GetPathOptions, stateName?: string): string => createPath({kind: 'ref', stateName, ...options}); +export const getCurrentPath = (options: GetPathOptions, stateName?: string): string => createPath({kind: 'current', stateName, ...options}); +export const getDiffPath = (options: GetPathOptions, stateName?: string): string => createPath({kind: 'diff', stateName, ...options}); export const getReferenceAbsolutePath = (testResult: ReporterTestResult, reportDir: string, stateName: string): string => { - const referenceImagePath = getReferencePath({attempt: testResult.attempt, imageDir: testResult.imageDir, browserId: testResult.browserId, stateName}); + const referenceImagePath = getReferencePath({attempt: testResult.attempt, imageDir: testResult.imageDir, browserId: testResult.browserId}, stateName); return path.resolve(reportDir, referenceImagePath); }; export const getCurrentAbsolutePath = (testResult: ReporterTestResult, reportDir: string, stateName: string): string => { - const currentImagePath = getCurrentPath({attempt: testResult.attempt, imageDir: testResult.imageDir, browserId: testResult.browserId, stateName}); + const currentImagePath = getCurrentPath({attempt: testResult.attempt, imageDir: testResult.imageDir, browserId: testResult.browserId}, stateName); return path.resolve(reportDir, currentImagePath); }; export const getDiffAbsolutePath = (testResult: ReporterTestResult, reportDir: string, stateName: string): string => { - const diffImagePath = getDiffPath({attempt: testResult.attempt, imageDir: testResult.imageDir, browserId: testResult.browserId, stateName}); + const diffImagePath = getDiffPath({attempt: testResult.attempt, imageDir: testResult.imageDir, browserId: testResult.browserId}, stateName); return path.resolve(reportDir, diffImagePath); }; interface CreatePathOptions extends GetPathOptions { + stateName?: string; kind: string; } @@ -323,3 +330,18 @@ export const saveErrorDetails = async (testResult: ReporterTestResult, reportPat await makeDirFor(detailsFilePath); await fs.writeFile(detailsFilePath, detailsData); }; + +export const isImageBufferData = (imageData: ImageBufferData | ImageFileData): imageData is ImageBufferData => { + return Boolean((imageData as ImageBufferData).buffer); +}; + +export const getExpectedCacheKey = ([testResult, stateName]: [TestSpecByPath, string | undefined]): string => { + const shortTestId = getShortMD5(mkTestId(testResult.testPath.join(' '), testResult.browserId)); + + return shortTestId + '#' + stateName; +}; + +export const getImagesInfoByStateName = (testResult: ReporterTestResult, stateName: string): ImageInfoWithState | undefined => { + return testResult.imagesInfo.find( + imagesInfo => (imagesInfo as ImageInfoWithState).stateName === stateName) as ImageInfoWithState | undefined; +}; diff --git a/lib/static/components/section/body/page-screenshot.tsx b/lib/static/components/section/body/page-screenshot.tsx index f2e87424b..774cf60de 100644 --- a/lib/static/components/section/body/page-screenshot.tsx +++ b/lib/static/components/section/body/page-screenshot.tsx @@ -1,10 +1,10 @@ import React, {Component} from 'react'; import Details from '../../details'; import ResizedScreenshot from '../../state/screenshot/resized'; -import {ImageData} from '../../../../types'; +import {ImageFileData} from '../../../../types'; interface PageScreenshotProps { - image: ImageData; + image: ImageFileData; } export class PageScreenshot extends Component { diff --git a/lib/test-adapter/hermione.ts b/lib/test-adapter/hermione.ts index d3a59181b..6e91e5a4c 100644 --- a/lib/test-adapter/hermione.ts +++ b/lib/test-adapter/hermione.ts @@ -76,7 +76,7 @@ export class HermioneTestAdapter implements ReporterTestResult { return this._testResult.browserId; } - get imagesInfo(): ImageInfoFull[] | undefined { + get imagesInfo(): ImageInfoFull[] { return this._imagesInfoFormatter.getImagesInfo(this); } diff --git a/lib/test-adapter/index.ts b/lib/test-adapter/index.ts index 4458c2a77..28c53cd7d 100644 --- a/lib/test-adapter/index.ts +++ b/lib/test-adapter/index.ts @@ -1,10 +1,9 @@ import {TestStatus} from '../constants'; -import {AssertViewResult, ErrorDetails, ImageBase64, ImageData, ImageInfoFull, TestError} from '../types'; +import {ErrorDetails, ImageBase64, ImageFileData, ImageInfoFull, TestError} from '../types'; export * from './hermione'; export interface ReporterTestResult { - readonly assertViewResults: AssertViewResult[]; readonly attempt: number; readonly browserId: string; readonly description: string | undefined; @@ -15,10 +14,10 @@ export interface ReporterTestResult { readonly history: string[]; readonly id: string; readonly imageDir: string; - readonly imagesInfo: ImageInfoFull[] | undefined; + readonly imagesInfo: ImageInfoFull[]; readonly meta: {browserVersion?: string} & Record; readonly multipleTabs: boolean; - readonly screenshot: ImageBase64 | ImageData | null | undefined; + readonly screenshot: ImageBase64 | ImageFileData | 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 index be38db2f6..a6b197104 100644 --- a/lib/test-adapter/playwright.ts +++ b/lib/test-adapter/playwright.ts @@ -10,7 +10,7 @@ import {getShortMD5, isImageDiffError, isNoRefImageError, mkTestId} from '../com 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 {AssertViewResult, ErrorDetails, ImageFileData, ImageInfoFull, ImageSize, TestError} from '../types'; import * as utils from '../server-utils'; import type {CoordBounds} from 'looks-same'; @@ -108,7 +108,7 @@ const extractImageError = (result: PlaywrightTestResult, {state, expectedAttachm } : null; }; -const getImageData = (attachment: PlaywrightAttachment | undefined): ImageData | null => { +const getImageData = (attachment: PlaywrightAttachment | undefined): ImageFileData | null => { if (!attachment) { return null; } @@ -237,7 +237,7 @@ export class PlaywrightTestAdapter implements ReporterTestResult { return getShortMD5(this.fullName); } - get imagesInfo(): ImageInfoFull[] | undefined { + get imagesInfo(): ImageInfoFull[] { return this._imagesInfoFormatter.getImagesInfo(this); } @@ -249,7 +249,7 @@ export class PlaywrightTestAdapter implements ReporterTestResult { return true; } - get screenshot(): ImageData | null { + get screenshot(): ImageFileData | null { const pageScreenshot = this._testResult.attachments.find(a => a.contentType === 'image/png' && a.name === 'screenshot'); return getImageData(pageScreenshot); diff --git a/lib/test-adapter/reporter.ts b/lib/test-adapter/reporter.ts index 22a36bb22..bc1023b1a 100644 --- a/lib/test-adapter/reporter.ts +++ b/lib/test-adapter/reporter.ts @@ -1,5 +1,5 @@ import {TestStatus} from '../constants'; -import {AssertViewResult, TestError, ErrorDetails, ImageInfoFull, ImageBase64, ImageData} from '../types'; +import {TestError, ErrorDetails, ImageInfoFull, ImageBase64, ImageFileData} from '../types'; import {ReporterTestResult} from './index'; import _ from 'lodash'; import {extractErrorDetails, getTestHash} from './utils'; @@ -18,10 +18,6 @@ export class ReporterTestAdapter implements ReporterTestResult { this._errorDetails = null; } - get assertViewResults(): AssertViewResult[] { - return this._testResult.assertViewResults; - } - get attempt(): number { return this._testResult.attempt; } @@ -68,7 +64,7 @@ export class ReporterTestAdapter implements ReporterTestResult { return getShortMD5(this.fullName); } - get imagesInfo(): ImageInfoFull[] | undefined { + get imagesInfo(): ImageInfoFull[] { return this._testResult.imagesInfo; } @@ -80,7 +76,7 @@ export class ReporterTestAdapter implements ReporterTestResult { return this._testResult.multipleTabs; } - get screenshot(): ImageBase64 | ImageData | null | undefined { + get screenshot(): ImageBase64 | ImageFileData | null | undefined { return this.error?.screenshot; } diff --git a/lib/test-adapter/sqlite.ts b/lib/test-adapter/sqlite.ts index 5093d5ccd..12591c91c 100644 --- a/lib/test-adapter/sqlite.ts +++ b/lib/test-adapter/sqlite.ts @@ -6,7 +6,7 @@ import { ErrorDetails, ImageInfoFull, ImageBase64, - ImageData, + ImageFileData, RawSuitesRow } from '../types'; import {ReporterTestResult} from './index'; @@ -100,7 +100,7 @@ export class SqliteTestAdapter implements ReporterTestResult { return getTestHash(this); } - get imagesInfo(): ImageInfoFull[] | undefined { + get imagesInfo(): ImageInfoFull[] { if (!_.has(this._parsedTestResult, 'imagesInfo')) { this._parsedTestResult.imagesInfo = tryParseJson(this._testResult[DB_COLUMN_INDEXES.imagesInfo]) as ImageInfoFull[]; } @@ -120,7 +120,7 @@ export class SqliteTestAdapter implements ReporterTestResult { return Boolean(this._testResult[DB_COLUMN_INDEXES.multipleTabs]); } - get screenshot(): ImageBase64 | ImageData | null | undefined { + get screenshot(): ImageBase64 | ImageFileData | null | undefined { return this.error?.screenshot; } diff --git a/lib/test-adapter/utils/index.ts b/lib/test-adapter/utils/index.ts index 09a1e1d79..139577f3c 100644 --- a/lib/test-adapter/utils/index.ts +++ b/lib/test-adapter/utils/index.ts @@ -1,17 +1,17 @@ import _ from 'lodash'; import {ReporterTestResult} from '../index'; import {TupleToUnion} from 'type-fest'; -import {ErrorDetails} from '../../types'; +import {ErrorDetails, ImageInfoFail, ImageInfoFull} from '../../types'; import {ERROR_DETAILS_PATH} from '../../constants'; import {ReporterTestAdapter} from '../reporter'; import {getDetailsFileName} from '../../common-utils'; +import {isImageBufferData} from '../../server-utils'; export const copyAndUpdate = ( original: ReporterTestResult, updates: Partial ): ReporterTestResult => { const keys = [ - 'assertViewResults', 'attempt', 'browserId', 'description', @@ -67,3 +67,14 @@ export const extractErrorDetails = (testResult: ReporterTestResult): ErrorDetail export const getTestHash = (testResult: ReporterTestResult): string => { return testResult.testPath.concat(testResult.browserId, testResult.attempt.toString()).join(' '); }; + +export const removeBufferFromImagesInfo = (imagesInfo: ImageInfoFull): ImageInfoFull => { + const {diffImg} = imagesInfo as ImageInfoFail; + const newImagesInfo = _.clone(imagesInfo); + + if (isImageBufferData(diffImg)) { + (newImagesInfo as ImageInfoFail).diffImg = {...diffImg, buffer: Buffer.from('')}; + } + + return newImagesInfo; +}; diff --git a/lib/types.ts b/lib/types.ts index d129f0ec9..14641ff12 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -15,7 +15,7 @@ export interface HermioneTestResult extends HermioneTestResultOriginal { timestamp?: number; } -export interface ImagesSaver { +export interface ImageFileSaver { saveImg: (localFilePath: string, options: {destPath: string; reportDir: string}) => string | Promise; } @@ -34,11 +34,16 @@ export interface ImageSize { height: number; } -export interface ImageData { +export interface ImageFileData { path: string; size: ImageSize; } +export interface ImageBufferData { + buffer: Buffer; + size: ImageSize; +} + export interface ImageBase64 { base64: string; size: ImageSize @@ -53,39 +58,40 @@ export interface DiffOptions extends LooksSameOptions { export interface ImageInfoFail { status: TestStatus.FAIL; stateName: string; - refImg?: ImageData; + refImg?: ImageFileData; diffClusters?: CoordBounds[]; - expectedImg: ImageData; - actualImg: ImageData; - diffImg: ImageData; + expectedImg: ImageFileData; + actualImg: ImageFileData; + diffImg: ImageFileData | ImageBufferData; + diffOptions: DiffOptions; } interface AssertViewSuccess { stateName: string; - refImg: ImageData; + refImg: ImageFileData; } export interface ImageInfoSuccess { status: TestStatus.SUCCESS | TestStatus.UPDATED; stateName: string; - refImg?: ImageData; + refImg?: ImageFileData; diffClusters?: CoordBounds[]; - expectedImg: ImageData; - actualImg?: ImageData; + expectedImg: ImageFileData; + actualImg?: ImageFileData; } export interface ImageInfoPageSuccess { status: TestStatus.SUCCESS; - actualImg: ImageData; + actualImg: ImageFileData; } export interface ImageInfoError { status: TestStatus.ERROR; error?: {message: string; stack: string;} stateName?: string; - refImg?: ImageData; + refImg?: ImageFileData; diffClusters?: CoordBounds[]; - actualImg: ImageData; + actualImg: ImageFileData; } export type ImageInfoWithState = ImageInfoFail | ImageInfoSuccess | ImageInfoError; @@ -106,7 +112,12 @@ export interface TestError { stack?: string; stateName?: string; details?: ErrorDetails - screenshot?: ImageBase64 | ImageData + screenshot?: ImageBase64 | ImageFileData +} + +export interface TestSpecByPath { + testPath: string[]; + browserId: string; } export interface HtmlReporterApi { diff --git a/lib/workers/worker.ts b/lib/workers/worker.ts index 5476c4bb4..3a27c1ff2 100644 --- a/lib/workers/worker.ts +++ b/lib/workers/worker.ts @@ -1,8 +1,8 @@ import looksSame from 'looks-same'; -import type {ImageDiffError} from '../errors'; +import {DiffOptions} from '../types'; -export function saveDiffTo(imageDiffError: ImageDiffError, diffPath: string): Promise { - const {diffColor: highlightColor, ...otherOpts} = imageDiffError.diffOpts; +export function saveDiffTo(diffOpts: DiffOptions, diffPath: string): Promise { + const {diffColor: highlightColor, ...otherOpts} = diffOpts; return looksSame.createDiff({diff: diffPath, highlightColor, ...otherOpts}); } diff --git a/package.json b/package.json index 97a059029..12f5c4cc4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "html-reporter", - "version": "9.15.0", + "version": "9.15.0-12-dec-night", "description": "Plugin for hermione which is intended to aggregate the results of tests running into html report", "files": [ "build" diff --git a/test/unit/lib/image-handler.ts b/test/unit/lib/image-handler.ts index 20f45687b..60982f85e 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, ImageInfoFail, ImageInfoFull, ImageInfoSuccess, ImagesSaver} from 'lib/types'; +import {AssertViewResult, ImageInfoFail, ImageInfoFull, ImageInfoSuccess, ImageFileSaver} from 'lib/types'; import {ErrorName, ImageDiffError} from 'lib/errors'; import {ImageStore} from 'lib/image-store'; import {FAIL, PluginEvents, SUCCESS, UPDATED} from 'lib/constants'; @@ -34,7 +34,7 @@ describe('image-handler', function() { const mkImageStore = (): SinonStubbedInstance => ({getLastImageInfoFromDb: sinon.stub()} as SinonStubbedInstance); - const mkImagesSaver = (): SinonStubbedInstance => ({saveImg: sinon.stub()} as SinonStubbedInstance); + const mkImagesSaver = (): SinonStubbedInstance => ({saveImg: sinon.stub()} as SinonStubbedInstance); const mkTestResult = (result: Partial): ReporterTestResult => _.defaults(result, { id: 'some-id',