From dcf020dbdbe756a656a4532343aa5db966636f5f Mon Sep 17 00:00:00 2001 From: shadowusr Date: Wed, 28 Jan 2026 17:16:55 +0300 Subject: [PATCH 1/5] feat: implement screens-migrator command --- lib/cli/commands/index.ts | 3 +- lib/cli/commands/migrate-screens/cli-ui.ts | 268 ++++++++++++++++++ .../diff-causes/pixel-rounding-changes.ts | 85 ++++++ lib/cli/commands/migrate-screens/index.ts | 259 +++++++++++++++++ lib/cli/commands/migrate-screens/types.ts | 12 + lib/cli/commands/migrate-screens/utils.ts | 140 +++++++++ lib/common-utils.ts | 7 +- lib/db-utils/server.ts | 38 +-- lib/gui/tool-runner/index.ts | 51 +--- lib/gui/tool-runner/{utils.ts => utils/db.ts} | 90 +----- lib/gui/tool-runner/utils/index.ts | 11 + .../tool-runner/utils/similar-diff-search.ts | 69 +++++ .../utils/update-reference-images.ts | 23 ++ lib/report-builder/gui.ts | 24 +- lib/reporter-helpers.ts | 2 +- lib/tests-tree-builder/gui.ts | 11 +- 16 files changed, 942 insertions(+), 151 deletions(-) create mode 100644 lib/cli/commands/migrate-screens/cli-ui.ts create mode 100644 lib/cli/commands/migrate-screens/diff-causes/pixel-rounding-changes.ts create mode 100644 lib/cli/commands/migrate-screens/index.ts create mode 100644 lib/cli/commands/migrate-screens/types.ts create mode 100644 lib/cli/commands/migrate-screens/utils.ts rename lib/gui/tool-runner/{utils.ts => utils/db.ts} (53%) create mode 100644 lib/gui/tool-runner/utils/index.ts create mode 100644 lib/gui/tool-runner/utils/similar-diff-search.ts create mode 100644 lib/gui/tool-runner/utils/update-reference-images.ts diff --git a/lib/cli/commands/index.ts b/lib/cli/commands/index.ts index f6ec574a7..881ca3831 100644 --- a/lib/cli/commands/index.ts +++ b/lib/cli/commands/index.ts @@ -1,5 +1,6 @@ export const cliCommands = { GUI: 'gui', MERGE_REPORTS: 'merge-reports', - REMOVE_UNUSED_SCREENS: 'remove-unused-screens' + REMOVE_UNUSED_SCREENS: 'remove-unused-screens', + MIGRATE_SCREENS: 'migrate-screens' } as const; diff --git a/lib/cli/commands/migrate-screens/cli-ui.ts b/lib/cli/commands/migrate-screens/cli-ui.ts new file mode 100644 index 000000000..008acabba --- /dev/null +++ b/lib/cli/commands/migrate-screens/cli-ui.ts @@ -0,0 +1,268 @@ +import chalk from 'chalk'; +import readline from 'node:readline'; + +type StatusState = { + processed: number; + total: number; + currentId: string; + failed: number; + failedLogName?: string; +}; + +type SummaryData = { + processed: number; + autoAccepted: number; + elapsedMs: number; + downloadMs: number; + compareMs: number; + warningMessage?: string; +}; + +type ProgressApi = { + start: () => void; + updateStatus: (next: Partial) => void; + finish: (summary: SummaryData) => void; + fail: (message: string) => void; +}; + +const SPINNER_FRAMES = ['·', '‥', '…', '⁖', '⁘', '⁙', '⁙', '⁘', '⁖', '…', '‥', '·', ' ', ' ']; +const WORDS = [ + 'Pixelating', + 'Comparitating', + 'Validazzling', + 'Renderlizing', + 'Baselineing', + 'Quantumizing', + 'Crystallizing', + 'Diffing', + 'Thunking', + 'Testplaning' +]; + +const TOOL_TITLE = ' ∵ Testplane — screenshots migrator tool'; + +const TOOL_DESCRIPTION = ` ─────────────────────────────────────────────────────────────────────── + + This tool is aimed at easing migration from Testplane v8 to v9 and can mass-update reference screenshots. + For it to work, you need to run your tests and get html-report with failed visual checks first.`; + +const pickWord = (current: string): string => { + if (WORDS.length <= 1) { + return WORDS[0] ?? current; + } + let next = current; + while (next === current) { + next = WORDS[Math.floor(Math.random() * WORDS.length)]; + } + return next; +}; + +const formatSeconds = (ms: number): string => (ms / 1000).toFixed(1); + +const renderBar = (part: number, total: number, width: number): string => { + if (total <= 0) { + return '░'.repeat(width); + } + const filled = Math.max(0, Math.min(width, Math.round((part / total) * width))); + return '▓'.repeat(filled) + '░'.repeat(width - filled); +}; + +export const createCliUi = ( + total: number, + options: {onStderr?: (text: string) => void} = {} +): ProgressApi => { + const stdout = process.stdout; + const isInteractive = Boolean(stdout.isTTY); + const title = chalk.hex('#9a66ff')('\n' + TOOL_TITLE); + const subtitle = chalk.dim(TOOL_DESCRIPTION); + const spinnerColor = chalk.hex('#FFFFFF'); + const statusColor = chalk.dim; + const originalStderrWrite = process.stderr.write.bind(process.stderr); + + let frameIndex = 0; + let dotsIndex = 0; + let currentWord = pickWord(''); + let status: StatusState = {processed: 0, total, currentId: '—', failed: 0}; + let startAt = Date.now(); + let spinnerTimer: NodeJS.Timeout | null = null; + let dotsTimer: NodeJS.Timeout | null = null; + let elapsedTimer: NodeJS.Timeout | null = null; + let wordTimer: NodeJS.Timeout | null = null; + let lastLoggedAt = 0; + let lastLoggedProcessed = 0; + let interceptActive = false; + + const render = (): void => { + if (!isInteractive) { + return; + } + readline.moveCursor(stdout, 0, -4); + readline.clearLine(stdout, 0); + readline.cursorTo(stdout, 0); + const frame = SPINNER_FRAMES[frameIndex]; + const dots = '.'.repeat(dotsIndex); + stdout.write(` ${spinnerColor(frame)} ${currentWord} ${dots}\n`); + + readline.clearLine(stdout, 0); + readline.cursorTo(stdout, 0); + stdout.write(statusColor(` ╰ Processed ${status.processed} of ${status.total} test results. Now working on: ${status.currentId}`) + '\n'); + + readline.clearLine(stdout, 0); + readline.cursorTo(stdout, 0); + if (status.failed > 0) { + const suffix = status.failedLogName ? ` See ${status.failedLogName}` : ''; + stdout.write(chalk.yellow(` ╰ Failed ${status.failed} test results.${suffix}`) + '\n'); + } else { + stdout.write('\n'); + } + + readline.clearLine(stdout, 0); + readline.cursorTo(stdout, 0); + stdout.write(` ⏱ ${formatSeconds(Date.now() - startAt)}s elapsed\n`); + }; + + const start = (): void => { + if (!isInteractive) { + stdout.write(`∵ Testplane — screenshots migrator tool\n`); + stdout.write('This tool is aimed at easing migration from Testplane v8 to v9 and can mass-update reference screenshots. For it to work, you need to run your tests and get html-report with failed visual checks first.\n'); + return; + } + stdout.write(`${title}\n`); + stdout.write(`${subtitle}\n\n`); + stdout.write('\n\n\n\n'); + startAt = Date.now(); + spinnerTimer = setInterval(() => { + frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length; + render(); + }, 120); + dotsTimer = setInterval(() => { + dotsIndex = (dotsIndex + 1) % 4; + }, 360); + elapsedTimer = setInterval(render, 100); + wordTimer = setInterval(() => { + currentWord = pickWord(currentWord); + }, 5000); + render(); + }; + + const updateStatus = (next: Partial): void => { + status = {...status, ...next}; + if (!isInteractive) { + const now = Date.now(); + const shouldLog = status.processed === status.total || + status.processed !== lastLoggedProcessed && (status.processed % 10 === 0 || now - lastLoggedAt > 5000); + if (!shouldLog) { + return; + } + lastLoggedAt = now; + lastLoggedProcessed = status.processed; + const suffix = status.failed > 0 && status.failedLogName ? ` (failed ${status.failed}, log ${status.failedLogName})` : ''; + stdout.write(`Processed ${status.processed}/${status.total}. Now working on: ${status.currentId}${suffix}\n`); + } + }; + + const finish = (summary: SummaryData): void => { + if (!isInteractive) { + const elapsedSec = (summary.elapsedMs / 1000).toFixed(1); + const warningLine = summary.warningMessage ? `\n${summary.warningMessage}` : ''; + stdout.write(`\nTestplane complete\nItems ${summary.processed} processed, ${summary.autoAccepted} auto-accepted\nTotal ${elapsedSec}s${warningLine}\n`); + return; + } + if (spinnerTimer) { + clearInterval(spinnerTimer); + } + if (dotsTimer) { + clearInterval(dotsTimer); + } + if (elapsedTimer) { + clearInterval(elapsedTimer); + } + if (wordTimer) { + clearInterval(wordTimer); + } + if (interceptActive) { + process.stderr.write = originalStderrWrite; + interceptActive = false; + } + readline.moveCursor(stdout, 0, -4); + for (let i = 0; i < 4; i += 1) { + readline.clearLine(stdout, 0); + readline.cursorTo(stdout, 0); + if (i < 3) { + stdout.write('\n'); + } + } + readline.moveCursor(stdout, 0, -3); + + const elapsedSec = summary.elapsedMs / 1000; + const workTotal = summary.downloadMs + summary.compareMs; + const downloadSec = workTotal > 0 ? elapsedSec * (summary.downloadMs / workTotal) : 0; + const compareSec = Math.max(0, elapsedSec - downloadSec); + const barWidth = 10; + + stdout.write(` ✓ Migration finished\n`); + stdout.write(` ───────────────────────────────────────────────────────────────────────\n\n`); + stdout.write(` Items ${summary.processed} processed, ${summary.autoAccepted} auto-accepted\n`); + stdout.write(` Total ${elapsedSec.toFixed(1)}s\n`); + stdout.write(` ├─ Download ${downloadSec.toFixed(1)}s ${renderBar(downloadSec, elapsedSec, barWidth)}\n`); + stdout.write(` └─ Processing ${compareSec.toFixed(1)}s ${renderBar(compareSec, elapsedSec, barWidth)}\n\n`); + if (summary.warningMessage) { + stdout.write(` ${chalk.yellow(summary.warningMessage)}\n\n`); + } + stdout.write(` Launch HTML Reporter GUI to see results: ${chalk.blue('npx testplane gui')} or your project-specific CLI command\n\n`); + }; + + const fail = (message: string): void => { + if (!isInteractive) { + stdout.write(`\nMigration failed\n${message}\n`); + return; + } + if (spinnerTimer) { + clearInterval(spinnerTimer); + } + if (dotsTimer) { + clearInterval(dotsTimer); + } + if (elapsedTimer) { + clearInterval(elapsedTimer); + } + if (wordTimer) { + clearInterval(wordTimer); + } + if (interceptActive) { + process.stderr.write = originalStderrWrite; + interceptActive = false; + } + readline.moveCursor(stdout, 0, -4); + for (let i = 0; i < 4; i += 1) { + readline.clearLine(stdout, 0); + readline.cursorTo(stdout, 0); + if (i < 3) { + stdout.write('\n'); + } + } + readline.moveCursor(stdout, 0, -3); + const paddedMessage = message + .split('\n') + .map((line) => ` ${line}`) + .join('\n'); + + stdout.write(` ✗ Migration failed\n`); + stdout.write(` ───────────────────────────────────────────────────────────────────────\n\n`); + stdout.write(`${chalk.red(paddedMessage)}\n\n`); + }; + + if (options.onStderr && isInteractive) { + process.stderr.write = ((chunk: unknown, _encoding?: unknown, callback?: () => void) => { + const text = Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk); + options.onStderr?.(text); + if (callback) { + callback(); + } + return true; + }) as typeof process.stderr.write; + interceptActive = true; + } + + return {start, updateStatus, finish, fail}; +}; diff --git a/lib/cli/commands/migrate-screens/diff-causes/pixel-rounding-changes.ts b/lib/cli/commands/migrate-screens/diff-causes/pixel-rounding-changes.ts new file mode 100644 index 000000000..cd1cd9df5 --- /dev/null +++ b/lib/cli/commands/migrate-screens/diff-causes/pixel-rounding-changes.ts @@ -0,0 +1,85 @@ +import looksSame, {CoordBounds} from 'looks-same'; +import {ImageInfoDiff} from '../../../../types'; +import {LooksSameOptions, TimingStats} from '../types'; +import {downloadImageIfNeeded} from '../utils'; + +type CropAxisShift = {start: number; end: number}; + +const getAxisShifts = (delta: number): CropAxisShift[] => { + if (delta === 0) { + return [{start: 0, end: 0}]; + } + if (delta === 1) { + return [{start: 0, end: 1}, {start: 1, end: 0}]; + } + if (delta === 2) { + return [{start: 0, end: 2}, {start: 1, end: 1}, {start: 2, end: 0}]; + } + return []; +}; + +const createSource = (source: string, boundingBox?: CoordBounds): string | {source: string; boundingBox: CoordBounds} => { + return boundingBox ? {source, boundingBox} : source; +}; + +export const isDiffDueToPixelRoundingChanges = async ( + imageInfo: ImageInfoDiff, + {compareOpts, reportPath, stats}: {compareOpts: LooksSameOptions; reportPath: string; stats: TimingStats} +): Promise => { + const actualSize = imageInfo.actualImg.size; + const expectedSize = imageInfo.expectedImg.size; + + if (!actualSize.width || !actualSize.height || !expectedSize.width || !expectedSize.height) { + return false; + } + + const diffWidth = actualSize.width - expectedSize.width; + const diffHeight = actualSize.height - expectedSize.height; + + if ( + diffWidth === 0 && diffHeight === 0 || + Math.abs(diffWidth) > 2 || Math.abs(diffHeight) > 2 || + (diffWidth !== 0 && diffHeight !== 0 && Math.sign(diffWidth) !== Math.sign(diffHeight)) + ) { + return false; + } + + const cropActual = diffWidth >= 0 && diffHeight >= 0; + const targetWidth = cropActual ? actualSize.width : expectedSize.width; + const targetHeight = cropActual ? actualSize.height : expectedSize.height; + + const widthShifts = getAxisShifts(Math.abs(diffWidth)); + const heightShifts = getAxisShifts(Math.abs(diffHeight)); + + const actualPath = await downloadImageIfNeeded(reportPath, imageInfo.actualImg.path, stats); + const expectedPath = await downloadImageIfNeeded(reportPath, imageInfo.expectedImg.path, stats); + + if (!actualPath || !expectedPath) { + return false; + } + + for (const heightShift of heightShifts) { + for (const widthShift of widthShifts) { + const boundingBox = { + left: widthShift.start, + top: heightShift.start, + right: targetWidth - widthShift.end - 1, + bottom: targetHeight - heightShift.end - 1 + }; + + const actualSource = createSource(actualPath, cropActual ? boundingBox : undefined); + const expectedSource = createSource(expectedPath, !cropActual ? boundingBox : undefined); + + const compareStartedAt = Date.now(); + const comparison = await looksSame(actualSource, expectedSource, compareOpts); + stats.compareMs += Date.now() - compareStartedAt; + stats.comparisons += 1; + + if (comparison.equal) { + return true; + } + } + } + + return false; +}; diff --git a/lib/cli/commands/migrate-screens/index.ts b/lib/cli/commands/migrate-screens/index.ts new file mode 100644 index 000000000..db4c54f63 --- /dev/null +++ b/lib/cli/commands/migrate-screens/index.ts @@ -0,0 +1,259 @@ +import path from 'path'; + +import type {Command} from '@gemini-testing/commander'; +import os from 'node:os'; +import fs from 'fs-extra'; +import PQueue from 'p-queue'; +import { + DB_COLUMN_INDEXES, + DEFAULT_TITLE_DELIMITER, + LOCAL_DATABASE_NAME, + TestStatus, + ToolName, + UNKNOWN_ATTEMPT +} from '../../../constants'; +import type {ToolAdapter} from '../../../adapters/tool'; +import {cliCommands} from '../'; +import {mergeDatabasesForReuse, prepareLocalDatabase} from '../../../gui/tool-runner/utils'; +import {Cache} from '../../../cache'; +import {GuiReportBuilder} from '../../../report-builder/gui'; +import {SqliteClient} from '../../../sqlite-client'; +import {SqliteImageStore} from '../../../image-store'; +import {ImagesInfoSaver} from '../../../images-info-saver'; +import {getExpectedCacheKey} from '../../../server-utils'; +import {getTestRowsFromDatabase, getTestsTreeFromDatabase} from '../../../db-utils/server'; +import {SqliteTestResultAdapter} from '../../../adapters/test-result/sqlite'; +import * as commonSqliteUtils from '../../../db-utils/common'; +import {ImageInfoDiff, ImageInfoFull} from '../../../types'; +import type {ReporterTestResult} from '../../../adapters/test-result'; +import {TestplaneConfigAdapter} from '../../../adapters/config/testplane'; +import {isDiffDueToPixelRoundingChanges} from './diff-causes/pixel-rounding-changes'; +import {LooksSameOptions, TimingStats, RefPathMap} from './types'; +import {createCliUi} from './cli-ui'; +import {copyAndUpdate} from '../../../adapters/test-result/utils'; +import {downloadAndResolveImagePaths} from './utils'; + +const {MIGRATE_SCREENS: commandName} = cliCommands; + +const collect = (newValue: string, array: string[] = []): string[] => { + return array.concat(newValue); +}; + +const parseRefPathMaps = (values: string[]): RefPathMap[] => { + return values.map((value) => { + const [from, to] = value.split('='); + if (!from || to === undefined) { + throw new Error(`Invalid --ref-path-map value: "${value}". Expected "="`); + } + return {from, to}; + }); +}; + +export = (program: Command, toolAdapter: ToolAdapter): void => { + program + .command(`${commandName}`) + .description('Auto-accept screenshot diffs caused by the new visual checks algorithms and assertView command changes in Testplane v9.\n\n' + + 'Note: this command is only available for Testplane.\n' + + 'How this works:\n' + + '- You run all your tests with Testplane v9 and get HTML report with diffs\n' + + `- When you run the ${commandName} command, it opens a report in path, specified in the 'path' parameter of html-reporter plugin in your Testplane config\n` + + '- This command iterates over all the diffs in the report and if a diff is caused by the assertView command changes, it auto-accepts it\n' + + '- You can then view what has changed in HTML Reporter GUI mode' + ) + .option('--ref-path-map =', 'map invalid absolute ref paths to local paths (can be specified multiple times)', collect, []) + .action(async (options: {refPathMap?: string[]}) => { + await migrateScreens({ + toolAdapter, + refPathMaps: parseRefPathMaps(options.refPathMap ?? []) + }); + }); +}; + +interface MigrateScreensOptions { + toolAdapter: ToolAdapter; + refPathMaps: RefPathMap[]; +} + +const isAutoAcceptableDiff = async ( + imageInfo: ImageInfoDiff, + {compareOpts, reportPath, stats}: {compareOpts: LooksSameOptions; reportPath: string; stats: TimingStats} +): Promise => { + return isDiffDueToPixelRoundingChanges(imageInfo, {compareOpts, reportPath, stats}); +}; + +async function migrateScreens({toolAdapter, refPathMaps}: MigrateScreensOptions): Promise { + if (toolAdapter.toolName !== ToolName.Testplane) { + throw new Error(`CLI command "${commandName}" supports only "${ToolName.Testplane}" tool`); + } + + const reportPath = toolAdapter.reporterConfig.path; + const dbPath = path.resolve(reportPath, LOCAL_DATABASE_NAME); + const logTimestamp = Date.now(); + const logFileName = `testplane-migrate-screens-${logTimestamp}-err.log`; + let logStream: fs.WriteStream | null = null; + const ui = createCliUi(0, { + onStderr: (text) => { + if (!logStream) { + logStream = fs.createWriteStream(logFileName, {flags: 'a'}); + } + logStream.write(`[stderr] ${text}`); + } + }); + ui.start(); + + let reportBuilder: GuiReportBuilder; + let testRows: Awaited>; + try { + await mergeDatabasesForReuse(reportPath); + await prepareLocalDatabase(reportPath); + + const dbClient = await SqliteClient.create({htmlReporter: toolAdapter.htmlReporter, reportPath, reuse: true}); + const imageStore = new SqliteImageStore(dbClient); + + const imagesInfoSaver = new ImagesInfoSaver({ + imageFileSaver: toolAdapter.htmlReporter.imagesSaver, + expectedPathsCache: new Cache(getExpectedCacheKey), + imageStore, + reportPath + }); + + reportBuilder = GuiReportBuilder.create({ + htmlReporter: toolAdapter.htmlReporter, + reporterConfig: toolAdapter.reporterConfig, + dbClient, + imagesInfoSaver + }); + const testsTree = await getTestsTreeFromDatabase(dbPath, toolAdapter.reporterConfig.baseHost); + reportBuilder.reuseTestsTree(testsTree, {force: true}); + + testRows = await getTestRowsFromDatabase(dbPath); + } catch (err) { + const formatDbError = (err: unknown): string => err instanceof Error ? err.stack || err.message : String(err); + ui.fail( + `Failed to read report database at "${dbPath}".\n` + + 'Make sure the report directory exists and contains database files.\n' + + `Details: ${formatDbError(err)}` + ); + process.exitCode = 1; + return; + } + const sortedRows = testRows.sort(commonSqliteUtils.compareDatabaseRowsByTimestamp); + const testResultsMap = new Map(); + sortedRows.forEach(row => { + const fullName = (JSON.parse(row[DB_COLUMN_INDEXES.suitePath]) as string[]).join(DEFAULT_TITLE_DELIMITER); + const browserId = row[DB_COLUMN_INDEXES.name]; + + const attempt = reportBuilder.getLatestAttempt({fullName, browserId}); + + const result = new SqliteTestResultAdapter(row, attempt); + + testResultsMap.set(result.id, result); + }); + + const testResults = Array.from(testResultsMap.values()).filter(testResult => testResult.status === TestStatus.FAIL); + + let autoAccepted = 0; + let started = 0; + let completed = 0; + let errorCount = 0; + let isClosingLogStream = false; + const timing: TimingStats = { + startedAt: Date.now(), + downloadMs: 0, + compareMs: 0, + downloads: 0, + comparisons: 0 + }; + + const queue = new PQueue({concurrency: os.cpus().length}); + ui.updateStatus({processed: 0, total: testResults.length, currentId: '—', failed: errorCount, failedLogName: logFileName}); + ui.updateStatus({failed: errorCount, failedLogName: logFileName}); + + testResults.forEach((testResult) => { + queue.add(async () => { + const current = started + 1; + started = current; + + try { + ui.updateStatus({processed: completed, total: testResults.length, currentId: testResult.id, failed: errorCount, failedLogName: logFileName}); + + const browserConfig = (toolAdapter.config as TestplaneConfigAdapter).getBrowserConfig(testResult.browserId); + const compareOpts: LooksSameOptions = { + tolerance: browserConfig.tolerance, + antialiasingTolerance: browserConfig.antialiasingTolerance, + ...browserConfig.compareOpts, + ...browserConfig.buildDiffOpts, + stopOnFirstFail: true, + shouldCluster: false + }; + + const imagesInfo: ImageInfoFull[] = []; + for (const imageInfo of testResult.imagesInfo) { + if (imageInfo.status !== TestStatus.FAIL || !(imageInfo as ImageInfoDiff).diffImg) { + continue; + } + + const isAcceptable = await isAutoAcceptableDiff(imageInfo, {compareOpts, reportPath, stats: timing}); + if (!isAcceptable) { + continue; + } + + imagesInfo.push({...imageInfo, status: TestStatus.UPDATED} as ImageInfoFull); + } + + if (imagesInfo.length === 0) { + return; + } + + const normalizedImagesInfo = await downloadAndResolveImagePaths(imagesInfo, reportPath, timing, refPathMaps, process.cwd()); + autoAccepted += 1; + return; + const updatedResult = copyAndUpdate(testResult, {imagesInfo: normalizedImagesInfo, status: TestStatus.UPDATED, attempt: UNKNOWN_ATTEMPT, timestamp: Date.now()}); + await reportBuilder.updateReferenceImages(updatedResult, () => {}); + } catch (err) { + const error = err as Error; + const message = error.stack || error.message || String(err); + const entry = `[${testResult.id}] ${message}\n`; + errorCount += 1; + ui.updateStatus({processed: completed, total: testResults.length, currentId: testResult.id, failed: errorCount, failedLogName: logFileName}); + if (!logStream) { + logStream = fs.createWriteStream(logFileName, {flags: 'a'}); + } + logStream.write(entry); + } finally { + completed += 1; + ui.updateStatus({processed: completed, total: testResults.length, currentId: testResult.id, failed: errorCount, failedLogName: logFileName}); + } + }); + }); + + const closeLogStream = async (): Promise => { + if (!logStream || isClosingLogStream) { + return; + } + isClosingLogStream = true; + await new Promise((resolve) => { + logStream?.end(() => resolve()); + }); + }; + + await queue.onIdle(); + await closeLogStream(); + + let warningMessage: string | undefined; + if (errorCount > 0) { + warningMessage = `Failed to migrate ${errorCount} screenshots. See details in ${logFileName}`; + process.exitCode = 1; + } + + ui.finish({ + processed: completed, + autoAccepted, + elapsedMs: Date.now() - timing.startedAt, + downloadMs: timing.downloadMs, + compareMs: timing.compareMs, + warningMessage + }); + + await reportBuilder.finalize(); +} diff --git a/lib/cli/commands/migrate-screens/types.ts b/lib/cli/commands/migrate-screens/types.ts new file mode 100644 index 000000000..8674ac657 --- /dev/null +++ b/lib/cli/commands/migrate-screens/types.ts @@ -0,0 +1,12 @@ +import {LooksSameOptions as LooksSameOptionsBase} from 'looks-same'; + +export interface TimingStats { + startedAt: number; + downloadMs: number; + compareMs: number; + downloads: number; + comparisons: number; +} + +export type LooksSameOptions = LooksSameOptionsBase & {createDiffImage?: false}; +export type RefPathMap = {from: string; to: string}; diff --git a/lib/cli/commands/migrate-screens/utils.ts b/lib/cli/commands/migrate-screens/utils.ts new file mode 100644 index 000000000..8198ec2fc --- /dev/null +++ b/lib/cli/commands/migrate-screens/utils.ts @@ -0,0 +1,140 @@ +import path from 'node:path'; +import url from 'node:url'; +import fs from 'fs-extra'; + +import {isUrl} from '../../../common-utils'; +import {getShortMD5} from '../../../common-utils'; +import {getTempPath} from '../../../server-utils'; +import {fetchFile} from '../../../common-utils'; +import {makeDirFor} from '../../../server-utils'; +import {TimingStats, RefPathMap} from './types'; +import {ImageInfoFull, RefImageFile} from '../../../types'; + +const remoteImageCache = new Map(); + +export const downloadImageIfNeeded = async ( + reportPath: string, + imagePath: string, + stats: TimingStats +): Promise => { + if (isUrl(imagePath)) { + if (remoteImageCache.has(imagePath)) { + return remoteImageCache.get(imagePath) as string; + } + + const downloadStartedAt = Date.now(); + const extension = path.extname(url.parse(imagePath).pathname ?? '') || '.png'; + const downloadFileName = `${getShortMD5(imagePath)}${extension}`; + const tempPath = await getTempPath(path.join('remote-images', downloadFileName)); + + const {data} = await fetchFile(imagePath, {responseType: 'arraybuffer'}, true); + if (!data) { + return ''; + } + + await makeDirFor(tempPath); + await fs.writeFile(tempPath, Buffer.from(data)); + + remoteImageCache.set(imagePath, tempPath); + stats.downloadMs += Date.now() - downloadStartedAt; + stats.downloads += 1; + + return tempPath; + } + + return path.isAbsolute(imagePath) ? imagePath : path.resolve(reportPath, imagePath); +}; + +const normalizeRefPathMap = (maps: RefPathMap[], cwd: string): RefPathMap[] => { + return maps.map(({from, to}) => ({ + from, + to: to === '.' ? cwd : to + })); +}; + +const resolveRefPath = async ( + refPath: string, + maps: RefPathMap[], + cwd: string +): Promise => { + if (await fs.pathExists(refPath)) { + return refPath; + } + + for (const map of normalizeRefPathMap(maps, cwd)) { + if (refPath.startsWith(map.from)) { + const suffix = refPath.slice(map.from.length).replace(/^[\\/]+/, ''); + const mapped = path.resolve(map.to, suffix); + if (await fs.pathExists(mapped)) { + return mapped; + } + } + } + + const cwdName = path.basename(cwd); + if (cwdName) { + const normalized = refPath.replace(/\\/g, '/'); + const marker = `/${cwdName}/`; + if (normalized.includes(marker)) { + const suffix = normalized.split(marker)[1]; + const mapped = path.resolve(cwd, suffix); + if (await fs.pathExists(mapped)) { + return mapped; + } + } else if (normalized.endsWith(`/${cwdName}`)) { + if (await fs.pathExists(cwd)) { + return cwd; + } + } + } + + return null; +}; + +export const downloadAndResolveImagePaths = async ( + imagesInfo: ImageInfoFull[], + reportPath: string, + stats: TimingStats, + refPathMaps: RefPathMap[], + cwd: string +): Promise => { + return Promise.all(imagesInfo.map(async (imageInfo) => { + const updated = {...imageInfo} as ImageInfoFull; + + if ('actualImg' in updated && updated.actualImg && 'path' in updated.actualImg) { + const resolvedActual = await downloadImageIfNeeded(reportPath, updated.actualImg.path, stats); + if (resolvedActual) { + updated.actualImg = {...updated.actualImg, path: resolvedActual}; + } + } + + if ('expectedImg' in updated && updated.expectedImg && 'path' in updated.expectedImg) { + const resolvedExpected = await downloadImageIfNeeded(reportPath, updated.expectedImg.path, stats); + if (resolvedExpected) { + updated.expectedImg = {...updated.expectedImg, path: resolvedExpected}; + } + } + + if ('diffImg' in updated && updated.diffImg && 'path' in updated.diffImg) { + const resolvedDiff = await downloadImageIfNeeded(reportPath, updated.diffImg.path, stats); + if (resolvedDiff) { + updated.diffImg = {...updated.diffImg, path: resolvedDiff}; + } + } + + if ('refImg' in updated && updated.refImg && 'path' in updated.refImg) { + const resolvedRef = await resolveRefPath(updated.refImg.path, refPathMaps, cwd); + if (!resolvedRef) { + throw new Error( + `Failed to resolve reference image path from db:\n ${updated.refImg.path}\n` + + 'Use --ref-path-map =.\n' + + 'Example: --ref-path-map /path/to/your/project=.\n' + + 'This maps the stored path prefix to the current working directory.' + ); + } + updated.refImg = {...updated.refImg, path: resolvedRef} as RefImageFile; + } + + return updated; + })); +}; diff --git a/lib/common-utils.ts b/lib/common-utils.ts index 2782dda94..ba30dee6c 100644 --- a/lib/common-utils.ts +++ b/lib/common-utils.ts @@ -196,7 +196,7 @@ export const isUrl = (str: string): boolean => { return !!parsedUrl.host && !!parsedUrl.protocol; }; -export const fetchFile = async (url: string, options?: AxiosRequestConfig) : Promise<{data: T | null, status: number}> => { +export const fetchFile = async (url: string, options?: AxiosRequestConfig, silent = false) : Promise<{data: T | null, status: number}> => { const {default: axios} = await import('axios'); try { @@ -204,8 +204,9 @@ export const fetchFile = async (url: string, options?: AxiosRequest return {data, status}; } catch (e: any) { // eslint-disable-line @typescript-eslint/no-explicit-any - logger.warn(`Error while fetching ${url}`, e); - + if (!silent) { + logger.warn(`Error while fetching ${url}`, e); + } // 'unknown' for request blocked by CORS policy const status = e.response ? e.response.status : 'unknown'; diff --git a/lib/db-utils/server.ts b/lib/db-utils/server.ts index 350b9d2c6..baeec1792 100644 --- a/lib/db-utils/server.ts +++ b/lib/db-utils/server.ts @@ -89,30 +89,36 @@ export async function mergeDatabases(srcDbPaths: string[], reportPath: string): } } -export async function getTestsTreeFromDatabase(dbPath: string, baseHost: string): Promise { - try { - await fs.ensureFile(dbPath); +export async function getTestRowsFromDatabase(dbPath: string): Promise { + await fs.ensureFile(dbPath); - const db = await makeSqlDatabaseFromFile(dbPath); - - const testsTreeBuilder = StaticTestsTreeBuilder.create({baseHost}); + const db = await makeSqlDatabaseFromFile(dbPath); - const suitesQueryStatement = db.prepare(commonSqliteUtils.selectAllSuitesQuery()); - const suitesRows: RawSuitesRow[] = []; + const suitesQueryStatement = db.prepare(commonSqliteUtils.selectAllSuitesQuery()); + const suitesRows: RawSuitesRow[] = []; - while (suitesQueryStatement.step()) { - const row = suitesQueryStatement.get(); - if (Array.isArray(row)) { - suitesRows.push(row as RawSuitesRow); - } + while (suitesQueryStatement.step()) { + const row = suitesQueryStatement.get(); + if (Array.isArray(row)) { + suitesRows.push(row as RawSuitesRow); } - suitesQueryStatement.free(); + } + suitesQueryStatement.free(); + + db.close(); + + return suitesRows; +} + +export async function getTestsTreeFromDatabase(dbPath: string, baseHost: string): Promise { + try { + const suitesRows = await getTestRowsFromDatabase(dbPath); + + const testsTreeBuilder = StaticTestsTreeBuilder.create({baseHost}); const sortedRows = suitesRows.sort(commonSqliteUtils.compareDatabaseRowsByTimestamp); const {tree} = testsTreeBuilder.build(sortedRows); - db.close(); - return tree; } catch (err: any) { // eslint-disable-line @typescript-eslint/no-explicit-any throw new NestedError('Error while getting data from database', err); diff --git a/lib/gui/tool-runner/index.ts b/lib/gui/tool-runner/index.ts index f16ea02a4..d5197faec 100644 --- a/lib/gui/tool-runner/index.ts +++ b/lib/gui/tool-runner/index.ts @@ -16,8 +16,8 @@ import {Cache} from '../../cache'; import {ImagesInfoSaver} from '../../images-info-saver'; import {SqliteImageStore} from '../../image-store'; import * as reporterHelper from '../../reporter-helpers'; -import {logger, getShortMD5, isUpdatedStatus} from '../../common-utils'; -import {formatId, mkFullTitle, mergeDatabasesForReuse, filterByEqualDiffSizes, prepareLocalDatabase} from './utils'; +import {logger, getShortMD5} from '../../common-utils'; +import {formatId, mkFullTitle, mergeDatabasesForReuse, filterByEqualDiffSizes, prepareLocalDatabase, getAssertViewResults} from './utils'; import {getExpectedCacheKey, getTimeTravelModeEnumSafe} from '../../server-utils'; import {getTestsTreeFromDatabase} from '../../db-utils/server'; import { @@ -38,10 +38,8 @@ import type {ReporterTestResult} from '../../adapters/test-result'; import type {Tree, TreeImage} from '../../tests-tree-builder/base'; import type {TestSpec} from '../../adapters/tool/types'; import type { - AssertViewResult, - ImageFile, ImageInfoDiff, ImageInfoUpdated, ImageInfoWithState, - ReporterConfig, TestSpecByPath, RefImageFile + ReporterConfig, TestSpecByPath } from '../../types'; import type {TestAdapter} from '../../adapters/test/index'; import type {TestCollectionAdapter} from '../../adapters/test-collection'; @@ -196,26 +194,9 @@ export class ToolRunner { return Promise.all(tests.map(async (test): Promise => { const testAdapter = this._getTestAdapterById(test); - const assertViewResults = this._prepareAssertViewResults(test.imagesInfo, testAdapter); + const assertViewResults = getAssertViewResults(test.imagesInfo, testAdapter, this._toolAdapter.config.getScreenshotPath); const {sessionId, url} = test.metaInfo as {sessionId?: string; url?: string}; - const latestAttempt = reportBuilder.getLatestAttempt({ - fullName: testAdapter.fullName, - browserId: testAdapter.browserId - }); - - const latestResult = testAdapter.createTestResult({ - assertViewResults, - status: UPDATED, - attempt: latestAttempt, - error: test.error, - sessionId, - meta: {url}, - duration: 0 - }); - - const estimatedStatus = reportBuilder.getUpdatedReferenceTestStatus(latestResult); - const formattedResultWithoutAttempt = testAdapter.createTestResult({ assertViewResults, status: UPDATED, @@ -226,10 +207,7 @@ export class ToolRunner { duration: 0 }); - const formattedResult = reportBuilder.provideAttempt(formattedResultWithoutAttempt); - const formattedResultUpdated = await reporterHelper.updateReferenceImages(formattedResult, this._reportPath, this._handleReferenceUpdate.bind(this)); - - await reportBuilder.addTestResult(formattedResultUpdated, {status: estimatedStatus}); + const formattedResultUpdated = await reportBuilder.updateReferenceImages(formattedResultWithoutAttempt, this._handleReferenceUpdate.bind(this)); return reportBuilder.getTestBranch(formattedResultUpdated.id); })); @@ -241,7 +219,7 @@ export class ToolRunner { await Promise.all(tests.map(async (test) => { const testAdapter = this._getTestAdapterById(test); - const assertViewResults = this._prepareAssertViewResults(test.imagesInfo, testAdapter); + const assertViewResults = getAssertViewResults(test.imagesInfo, testAdapter, this._toolAdapter.config.getScreenshotPath); const {sessionId, url} = test.metaInfo as {sessionId?: string; url?: string}; const formattedResultWithoutAttempt = testAdapter.createTestResult({ @@ -381,23 +359,6 @@ export class ToolRunner { return this._testAdapters[testId]; } - protected _prepareAssertViewResults(imagesInfo: TestRefUpdateData['imagesInfo'], testAdapter: TestAdapter): AssertViewResult[] { - const assertViewResults: AssertViewResult[] = []; - - imagesInfo - .filter(({stateName, actualImg}) => Boolean(stateName) && Boolean(actualImg)) - .forEach((imageInfo) => { - const {stateName, actualImg} = imageInfo as {stateName: string, actualImg: ImageFile}; - const absoluteRefImgPath = this._toolAdapter.config.getScreenshotPath(testAdapter, stateName); - const relativeRefImgPath = absoluteRefImgPath && path.relative(process.cwd(), absoluteRefImgPath); - const refImg: RefImageFile = {path: absoluteRefImgPath, relativePath: relativeRefImgPath, size: actualImg.size}; - - assertViewResults.push({stateName, refImg, currImg: actualImg, isUpdated: isUpdatedStatus(imageInfo.status)}); - }); - - return assertViewResults; - } - protected _handleReferenceUpdate(testResult: ReporterTestResult, imageInfo: ImageInfoUpdated, state: string): void { this._expectedImagesCache.set([testResult, imageInfo.stateName], imageInfo.expectedImg.path); diff --git a/lib/gui/tool-runner/utils.ts b/lib/gui/tool-runner/utils/db.ts similarity index 53% rename from lib/gui/tool-runner/utils.ts rename to lib/gui/tool-runner/utils/db.ts index 6bfb9d58c..c81a68002 100644 --- a/lib/gui/tool-runner/utils.ts +++ b/lib/gui/tool-runner/utils/db.ts @@ -1,26 +1,16 @@ -import _ from 'lodash'; import path from 'path'; -import fs from 'fs-extra'; -import chalk from 'chalk'; -import type {Statement} from '@gemini-testing/sql.js'; -import type {CoordBounds} from 'looks-same'; - -import {logger} from '../../common-utils'; -import {DATABASE_URLS_JSON_NAME, DB_CURRENT_VERSION, LOCAL_DATABASE_NAME} from '../../constants'; -import {makeSqlDatabaseFromFile, mergeTables} from '../../db-utils/server'; -import {TestEqualDiffsData, TestRefUpdateData} from '../../tests-tree-builder/gui'; -import {ImageInfoDiff, ImageSize} from '../../types'; -import {backupAndReset, getDatabaseVersion, migrateDatabase} from '../../db-utils/migrations'; +import type {Statement} from '@gemini-testing/sql.js'; import makeDebug from 'debug'; +import fs from 'fs-extra'; +import chalk from 'chalk'; -const debug = makeDebug('html-reporter:gui:tool-runner:utils'); - -export const formatId = (hash: string, browserId: string): string => `${hash}/${browserId}`; +import {logger} from '../../../common-utils'; +import {DATABASE_URLS_JSON_NAME, DB_CURRENT_VERSION, LOCAL_DATABASE_NAME} from '../../../constants'; +import {makeSqlDatabaseFromFile, mergeTables} from '../../../db-utils/server'; +import {backupAndReset, getDatabaseVersion, migrateDatabase} from '../../../db-utils/migrations'; -export const mkFullTitle = ({suite, state}: Pick): string => { - return suite.path.length > 0 ? `${suite.path.join(' ')} ${state.name}` : state.name; -}; +const debug = makeDebug('html-reporter:gui:tool-runner:utils:db'); export const mergeDatabasesForReuse = async (reportPath: string): Promise => { const dbUrlsJsonPath = path.resolve(reportPath, DATABASE_URLS_JSON_NAME); @@ -99,67 +89,3 @@ export const prepareLocalDatabase = async (reportPath: string): Promise => db.close(); } }; - -export const filterByEqualDiffSizes = (imagesInfo: TestEqualDiffsData[], refDiffClusters?: CoordBounds[]): TestEqualDiffsData[] => { - if (!refDiffClusters || _.isEmpty(refDiffClusters)) { - return []; - } - - const refDiffSizes = refDiffClusters.map(getDiffClusterSizes); - - return _.filter(imagesInfo, (imageInfo) => { - const imageInfoFail = imageInfo as ImageInfoDiff; - - const imageDiffSizes = imageInfoFail.diffClusters?.map(getDiffClusterSizes) ?? []; - const equal = compareDiffSizes(imageDiffSizes, refDiffSizes); - - if (!equal) { - return false; - } - - if (!_.isEqual(imageDiffSizes, refDiffSizes)) { - imageInfoFail.diffClusters = reorderClustersByEqualSize(imageInfoFail.diffClusters ?? [], imageDiffSizes, refDiffSizes); - } - - return true; - }); -}; - -function getDiffClusterSizes(diffCluster: CoordBounds): ImageSize { - return { - width: diffCluster.right - diffCluster.left + 1, - height: diffCluster.bottom - diffCluster.top + 1 - }; -} - -function compareDiffSizes(diffSizes1: ImageSize[], diffSizes2: ImageSize[]): boolean { - if (diffSizes1.length !== diffSizes2.length) { - return false; - } - - return diffSizes1.every((diffSize) => { - const foundIndex = _.findIndex(diffSizes2, diffSize); - - if (foundIndex < 0) { - return false; - } - - diffSizes2 = diffSizes2.filter((_v, ind) => ind !== foundIndex); - - return true; - }); -} - -function reorderClustersByEqualSize(diffClusters1: CoordBounds[], diffSizes1: ImageSize[], diffSizes2: ImageSize[]): CoordBounds[] { - return diffClusters1.reduce((acc, cluster, i) => { - if (diffSizes1[i] !== diffSizes2[i]) { - const foundIndex = _.findIndex(diffSizes2, diffSizes1[i]); - diffSizes2 = diffSizes2.filter((_v, ind) => ind !== foundIndex); - acc[foundIndex] = cluster; - } else { - acc[i] = cluster; - } - - return acc; - }, [] as CoordBounds[]); -} diff --git a/lib/gui/tool-runner/utils/index.ts b/lib/gui/tool-runner/utils/index.ts new file mode 100644 index 000000000..beb20ea84 --- /dev/null +++ b/lib/gui/tool-runner/utils/index.ts @@ -0,0 +1,11 @@ +export * from './db'; +export * from './similar-diff-search'; +export * from './update-reference-images'; + +import {TestRefUpdateData} from '../../../tests-tree-builder/gui'; + +export const formatId = (hash: string, browserId: string): string => `${hash}/${browserId}`; + +export const mkFullTitle = ({suite, state}: Pick): string => { + return suite.path.length > 0 ? `${suite.path.join(' ')} ${state.name}` : state.name; +}; diff --git a/lib/gui/tool-runner/utils/similar-diff-search.ts b/lib/gui/tool-runner/utils/similar-diff-search.ts new file mode 100644 index 000000000..ddb37d2f6 --- /dev/null +++ b/lib/gui/tool-runner/utils/similar-diff-search.ts @@ -0,0 +1,69 @@ +import _ from 'lodash'; +import type {CoordBounds} from 'looks-same'; + +import {TestEqualDiffsData} from '../../../tests-tree-builder/gui'; +import {ImageInfoDiff, ImageSize} from '../../../types'; + +export const filterByEqualDiffSizes = (imagesInfo: TestEqualDiffsData[], refDiffClusters?: CoordBounds[]): TestEqualDiffsData[] => { + if (!refDiffClusters || _.isEmpty(refDiffClusters)) { + return []; + } + + const refDiffSizes = refDiffClusters.map(getDiffClusterSizes); + + return _.filter(imagesInfo, (imageInfo) => { + const imageInfoFail = imageInfo as ImageInfoDiff; + + const imageDiffSizes = imageInfoFail.diffClusters?.map(getDiffClusterSizes) ?? []; + const equal = compareDiffSizes(imageDiffSizes, refDiffSizes); + + if (!equal) { + return false; + } + + if (!_.isEqual(imageDiffSizes, refDiffSizes)) { + imageInfoFail.diffClusters = reorderClustersByEqualSize(imageInfoFail.diffClusters ?? [], imageDiffSizes, refDiffSizes); + } + + return true; + }); +}; + +function getDiffClusterSizes(diffCluster: CoordBounds): ImageSize { + return { + width: diffCluster.right - diffCluster.left + 1, + height: diffCluster.bottom - diffCluster.top + 1 + }; +} + +function compareDiffSizes(diffSizes1: ImageSize[], diffSizes2: ImageSize[]): boolean { + if (diffSizes1.length !== diffSizes2.length) { + return false; + } + + return diffSizes1.every((diffSize) => { + const foundIndex = _.findIndex(diffSizes2, diffSize); + + if (foundIndex < 0) { + return false; + } + + diffSizes2 = diffSizes2.filter((_v, ind) => ind !== foundIndex); + + return true; + }); +} + +function reorderClustersByEqualSize(diffClusters1: CoordBounds[], diffSizes1: ImageSize[], diffSizes2: ImageSize[]): CoordBounds[] { + return diffClusters1.reduce((acc, cluster, i) => { + if (diffSizes1[i] !== diffSizes2[i]) { + const foundIndex = _.findIndex(diffSizes2, diffSizes1[i]); + diffSizes2 = diffSizes2.filter((_v, ind) => ind !== foundIndex); + acc[foundIndex] = cluster; + } else { + acc[i] = cluster; + } + + return acc; + }, [] as CoordBounds[]); +} diff --git a/lib/gui/tool-runner/utils/update-reference-images.ts b/lib/gui/tool-runner/utils/update-reference-images.ts new file mode 100644 index 000000000..6fa549be9 --- /dev/null +++ b/lib/gui/tool-runner/utils/update-reference-images.ts @@ -0,0 +1,23 @@ +import path from 'path'; + +import {TestAdapter} from '../../../adapters/test'; +import {isUpdatedStatus} from '../../../common-utils'; +import {TestRefUpdateData} from '../../../tests-tree-builder/gui'; +import {AssertViewResult, ImageFile, RefImageFile} from '../../../types'; + +export const getAssertViewResults = (imagesInfo: TestRefUpdateData['imagesInfo'], testAdapter: TestAdapter, getScreenshotPath: (testAdapter: TestAdapter, stateName: string) => string): AssertViewResult[] =>{ + const assertViewResults: AssertViewResult[] = []; + + imagesInfo + .filter(({stateName, actualImg}) => Boolean(stateName) && Boolean(actualImg)) + .forEach((imageInfo) => { + const {stateName, actualImg} = imageInfo as {stateName: string, actualImg: ImageFile}; + const absoluteRefImgPath = getScreenshotPath(testAdapter, stateName); + const relativeRefImgPath = absoluteRefImgPath && path.relative(process.cwd(), absoluteRefImgPath); + const refImg: RefImageFile = {path: absoluteRefImgPath, relativePath: relativeRefImgPath, size: actualImg.size}; + + assertViewResults.push({stateName, refImg, currImg: actualImg, isUpdated: isUpdatedStatus(imageInfo.status)}); + }); + + return assertViewResults; +}; diff --git a/lib/report-builder/gui.ts b/lib/report-builder/gui.ts index 58c0feced..ab27aa1cb 100644 --- a/lib/report-builder/gui.ts +++ b/lib/report-builder/gui.ts @@ -10,6 +10,7 @@ import {determineStatus, isUpdatedStatus} from '../common-utils'; import {HtmlReporterValues} from '../plugin-api'; import {SkipItem} from '../tests-tree-builder/static'; import {copyAndUpdate} from '../adapters/test-result/utils'; +import * as reporterHelper from '../reporter-helpers'; interface UndoAcceptImageResult { updatedImage: TreeImage | undefined; @@ -46,8 +47,10 @@ export class GuiReportBuilder extends StaticReportBuilder { return this; } - reuseTestsTree(tree: Tree): void { - this._testsTree.reuseTestsTree(tree); + reuseTestsTree(tree: Tree, options: {force?: boolean} = {}): void { + const {force = false} = options; + + this._testsTree.reuseTestsTree(tree, {force}); // Fill test attempt manager with data from db for (const [, testResult] of Object.entries(tree.results.byId)) { @@ -88,6 +91,23 @@ export class GuiReportBuilder extends StaticReportBuilder { return this._testsTree.getImageDataToFindEqualDiffs(imageIds); } + /** Accepts all images that have status "updated" in imagesInfo and adds new test result with status "updated". + * To accept images one by one, imagesInfo should be filtered before calling this method to only contain images that should be updated. */ + async updateReferenceImages(testResultWithoutAttempt: ReporterTestResult, onReferenceUpdate: reporterHelper.OnReferenceUpdateCb): Promise { + const latestAttempt = this.getLatestAttempt({ + fullName: testResultWithoutAttempt.fullName, + browserId: testResultWithoutAttempt.browserId + }); + + const latestResult = copyAndUpdate(testResultWithoutAttempt, {attempt: latestAttempt}); + const estimatedStatus = this.getUpdatedReferenceTestStatus(latestResult); + + const formattedResult = this.provideAttempt(testResultWithoutAttempt); + const formattedResultUpdated = await reporterHelper.updateReferenceImages(formattedResult, this._reporterConfig.path, onReferenceUpdate); + + return this.addTestResult(formattedResultUpdated, {status: estimatedStatus}); + } + undoAcceptImage(testResultWithoutAttempt: ReporterTestResult, stateName: string): UndoAcceptImageResult | null { const attempt = this._testAttemptManager.getCurrentAttempt(testResultWithoutAttempt); const testResult = copyAndUpdate(testResultWithoutAttempt, {attempt}); diff --git a/lib/reporter-helpers.ts b/lib/reporter-helpers.ts index e265d2c82..9e042cb5e 100644 --- a/lib/reporter-helpers.ts +++ b/lib/reporter-helpers.ts @@ -10,7 +10,7 @@ import {UPDATED} from './constants'; const mkReferenceHash = (testId: string, stateName: string): string => getShortMD5(`${testId}#${stateName}`); -type OnReferenceUpdateCb = (testResult: ReporterTestResult, images: ImageInfoUpdated, state: string) => void; +export type OnReferenceUpdateCb = (testResult: ReporterTestResult, images: ImageInfoUpdated, state: string) => void; export const updateReferenceImages = async (testResult: ReporterTestResult, reportPath: string, onReferenceUpdateCb: OnReferenceUpdateCb): Promise => { const {default: tmp} = await import('tmp'); diff --git a/lib/tests-tree-builder/gui.ts b/lib/tests-tree-builder/gui.ts index b94f7f300..370b7158f 100644 --- a/lib/tests-tree-builder/gui.ts +++ b/lib/tests-tree-builder/gui.ts @@ -150,7 +150,16 @@ export class GuiTestsTreeBuilder extends BaseTestsTreeBuilder { }; } - reuseTestsTree(testsTree: Tree): void { + /** Loads tests from given tree, but only for those tests that already exist in the current tree. + * If force is true, the current tree is replaced with the given tree without any checks. */ + reuseTestsTree(testsTree: Tree, options: {force?: boolean} = {}): void { + const {force = false} = options; + + if (force) { + this._tree = testsTree; + return; + } + this._tree.browsers.allIds.forEach((browserId) => this._reuseBrowser(testsTree, browserId)); } From 15c993b1b84962724bea772ee6302dc867c6a467 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Wed, 28 Jan 2026 18:54:52 +0300 Subject: [PATCH 2/5] test: fix unit tests --- lib/gui/tool-runner/index.ts | 4 +- test/unit/lib/gui/tool-runner/index.js | 69 ++++++++------------------ test/unit/lib/gui/tool-runner/utils.ts | 8 +-- test/unit/lib/report-builder/gui.js | 39 +++++++++++++++ 4 files changed, 65 insertions(+), 55 deletions(-) diff --git a/lib/gui/tool-runner/index.ts b/lib/gui/tool-runner/index.ts index d5197faec..fa19f6627 100644 --- a/lib/gui/tool-runner/index.ts +++ b/lib/gui/tool-runner/index.ts @@ -194,7 +194,7 @@ export class ToolRunner { return Promise.all(tests.map(async (test): Promise => { const testAdapter = this._getTestAdapterById(test); - const assertViewResults = getAssertViewResults(test.imagesInfo, testAdapter, this._toolAdapter.config.getScreenshotPath); + const assertViewResults = getAssertViewResults(test.imagesInfo, testAdapter, this._toolAdapter.config.getScreenshotPath.bind(this._toolAdapter.config)); const {sessionId, url} = test.metaInfo as {sessionId?: string; url?: string}; const formattedResultWithoutAttempt = testAdapter.createTestResult({ @@ -219,7 +219,7 @@ export class ToolRunner { await Promise.all(tests.map(async (test) => { const testAdapter = this._getTestAdapterById(test); - const assertViewResults = getAssertViewResults(test.imagesInfo, testAdapter, this._toolAdapter.config.getScreenshotPath); + const assertViewResults = getAssertViewResults(test.imagesInfo, testAdapter, this._toolAdapter.config.getScreenshotPath.bind(this._toolAdapter.config)); const {sessionId, url} = test.metaInfo as {sessionId?: string; url?: string}; const formattedResultWithoutAttempt = testAdapter.createTestResult({ diff --git a/test/unit/lib/gui/tool-runner/index.js b/test/unit/lib/gui/tool-runner/index.js index bfd6ea121..ca6abf9f6 100644 --- a/test/unit/lib/gui/tool-runner/index.js +++ b/test/unit/lib/gui/tool-runner/index.js @@ -65,6 +65,7 @@ describe('lib/gui/tool-runner/index', () => { reportBuilder = sandbox.createStubInstance(GuiReportBuilder); reportBuilder.addTestResult.callsFake(_.identity); reportBuilder.provideAttempt.callsFake(_.identity); + reportBuilder.updateReferenceImages.callsFake(_.identity); looksSame = sandbox.stub().named('looksSame').resolves({equal: true}); @@ -252,10 +253,14 @@ describe('lib/gui/tool-runner/index', () => { await gui.updateReferenceImage(testRefUpdateData); - assert.calledOnceWith(toolAdapter.updateReference, { - refImg: {path: '/ref/path1', relativePath: '../path1', size: {height: 100, width: 200}}, - state: 'plain1' + assert.calledOnce(reportBuilder.updateReferenceImages); + const [testResult] = reportBuilder.updateReferenceImages.firstCall.args; + assert.deepEqual(testResult.imagesInfo[0].refImg, { + path: '/ref/path1', + relativePath: '../path1', + size: {height: 100, width: 200} }); + assert.equal(testResult.imagesInfo[0].stateName, 'plain1'); }); it('should update reference for each image', async () => { @@ -301,54 +306,20 @@ describe('lib/gui/tool-runner/index', () => { await gui.updateReferenceImage(tests); - assert.calledTwice(toolAdapter.updateReference); - assert.calledWith(toolAdapter.updateReference.firstCall, { - refImg: {path: '/ref/path1', relativePath: '../path1', size: {height: 100, width: 200}}, - state: 'plain1' + assert.calledOnce(reportBuilder.updateReferenceImages); + const [firstTestResult] = reportBuilder.updateReferenceImages.firstCall.args; + assert.deepEqual(firstTestResult.imagesInfo[0].refImg, { + path: '/ref/path1', + relativePath: '../path1', + size: {height: 100, width: 200} }); - assert.calledWith(toolAdapter.updateReference.secondCall, { - refImg: {path: '/ref/path2', relativePath: '../path2', size: {height: 200, width: 300}}, - state: 'plain2' + assert.equal(firstTestResult.imagesInfo[0].stateName, 'plain1'); + assert.deepEqual(firstTestResult.imagesInfo[1].refImg, { + path: '/ref/path2', + relativePath: '../path2', + size: {height: 200, width: 300} }); - }); - - it('should determine status based on the latest result', async () => { - const testRefUpdateData = [{ - id: 'some-id', - fullTitle: () => 'some-title', - clone: () => testRefUpdateData[0], - browserId: 'yabro', - suite: {path: ['suite1']}, - state: {}, - metaInfo: {}, - imagesInfo: [{ - status: UPDATED, - stateName: 'plain1', - actualImg: { - size: {height: 100, width: 200} - } - }] - }]; - - const getScreenshotPath = sandbox.stub().returns('/ref/path1'); - const config = mkConfigAdapter_(stubConfig({ - browsers: {yabro: {getScreenshotPath}} - })); - - const testCollection = {tests: [mkTestAdapter_(testRefUpdateData[0])]}; - const toolAdapter = stubToolAdapter({config, testCollection}); - - reportBuilder.getLatestAttempt.withArgs({fullName: 'some-title', browserId: 'yabro'}).returns(100500); - reportBuilder.getUpdatedReferenceTestStatus.withArgs(sinon.match({attempt: 100500})).returns(TestStatus.UPDATED); - - const gui = initGuiReporter({toolAdapter}); - await gui.initialize(); - - reportBuilder.addTestResult.reset(); - - await gui.updateReferenceImage(testRefUpdateData); - - assert.calledOnceWith(reportBuilder.addTestResult, sinon.match.any, {status: TestStatus.UPDATED}); + assert.equal(firstTestResult.imagesInfo[1].stateName, 'plain2'); }); }); diff --git a/test/unit/lib/gui/tool-runner/utils.ts b/test/unit/lib/gui/tool-runner/utils.ts index 99d920536..2c0405948 100644 --- a/test/unit/lib/gui/tool-runner/utils.ts +++ b/test/unit/lib/gui/tool-runner/utils.ts @@ -49,8 +49,8 @@ describe('lib/gui/tool-runner/utils', () => { backupAndResetStub = sinon.stub(); consoleWarnStub = sinon.stub(console, 'warn'); - utilsWithStubs = proxyquire('lib/gui/tool-runner/utils', { - '../../db-utils/migrations': { + utilsWithStubs = proxyquire('lib/gui/tool-runner/utils/db', { + '../../../db-utils/migrations': { getDatabaseVersion, migrateDatabase, backupAndReset: backupAndResetStub @@ -168,8 +168,8 @@ describe('lib/gui/tool-runner/utils', () => { db.close(); const dbStub = {close: sinon.stub()}; - const utilsWithErrorStub = proxyquire('lib/gui/tool-runner/utils', { - '../../db-utils/server': ({makeSqlDatabaseFromFile: () => Promise.resolve(dbStub)}) + const utilsWithErrorStub = proxyquire('lib/gui/tool-runner/utils/db', { + '../../../db-utils/server': ({makeSqlDatabaseFromFile: () => Promise.resolve(dbStub)}) }); await utilsWithErrorStub.prepareLocalDatabase(reportPath); diff --git a/test/unit/lib/report-builder/gui.js b/test/unit/lib/report-builder/gui.js index 2d9170f17..25f0797f0 100644 --- a/test/unit/lib/report-builder/gui.js +++ b/test/unit/lib/report-builder/gui.js @@ -13,6 +13,7 @@ const {LOCAL_DATABASE_NAME} = require('lib/constants/database'); const {ImagesInfoSaver} = require('lib/images-info-saver'); const sinon = require('sinon'); const {SKIPPED, SUCCESS, ERROR} = require('lib/constants'); +const {TestStatus} = require('../../../../build/lib/constants'); const TEST_REPORT_PATH = 'test'; const TEST_DB_PATH = `${TEST_REPORT_PATH}/${LOCAL_DATABASE_NAME}`; @@ -246,6 +247,44 @@ describe('GuiReportBuilder', () => { }); }); + describe('"updateReferenceImages"', () => { + it('should determine status based on the latest result', async () => { + const testRefUpdateData = { + id: 'some-id', + fullTitle: () => 'some-title', + clone: () => testRefUpdateData[0], + browserId: 'yabro', + suite: {path: ['suite1']}, + state: {}, + metaInfo: {}, + imagesInfo: [{ + status: UPDATED, + stateName: 'plain1', + refImg: { + path: 'ref-path1', + size: {height: 100, width: 200} + }, + expectedImg: { + path: 'expected-path1', + size: {height: 100, width: 200} + }, + actualImg: { + path: 'actual-path1', + size: {height: 100, width: 200} + } + }] + }; + + const reportBuilder = await mkGuiReportBuilder_(); + sinon.stub(reportBuilder, 'getUpdatedReferenceTestStatus').returns(TestStatus.UPDATED); + sinon.stub(reportBuilder, 'addTestResult'); + + await reportBuilder.updateReferenceImages(testRefUpdateData, sinon.stub()); + + assert.calledOnceWith(reportBuilder.addTestResult, sinon.match.any, {status: TestStatus.UPDATED}); + }); + }); + describe('"undoAcceptImages"', () => { let reportBuilder; From 16ce31ab4fe95ed8158493304a8d039802c2cbcc Mon Sep 17 00:00:00 2001 From: shadowusr Date: Wed, 28 Jan 2026 23:57:47 +0300 Subject: [PATCH 3/5] test: add fixture report for migrate-screens command --- .../migrate-screens/.testplane.conf.js | 7 +++++ .../fixtures/migrate-screens/package.json | 10 +++++++ .../fixtures/migrate-screens/report/data.js | 2 ++ .../migrate-screens/report/databaseUrls.json | 1 + .../chrome~current_1769631040949_0.png | Bin 0 -> 1746 bytes .../message/chrome~diff_1769631040949_0.png | Bin 0 -> 1594 bytes .../message/chrome~ref_1769631040949_0.png | Bin 0 -> 1767 bytes .../message/chrome~ref_1769631037254_0.png | Bin 0 -> 10970 bytes .../chrome~current_1769631043495_0.png | Bin 0 -> 5206 bytes .../message/chrome~diff_1769631043495_0.png | Bin 0 -> 5523 bytes .../message/chrome~ref_1769631043495_0.png | Bin 0 -> 5403 bytes .../chrome~current_1769631038126_0.png | Bin 0 -> 3754 bytes .../message/chrome~diff_1769631038126_0.png | Bin 0 -> 7390 bytes .../message/chrome~ref_1769631038126_0.png | Bin 0 -> 10970 bytes .../fixtures/migrate-screens/report/sqlite.db | Bin 0 -> 28672 bytes .../screens/22643bd/chrome/message.png | Bin 0 -> 10970 bytes .../screens/5e608e3/chrome/message.png | Bin 0 -> 10970 bytes .../screens/deaa096/chrome/message.png | Bin 0 -> 10970 bytes .../screens/e7de426/chrome/message.png | Bin 0 -> 10970 bytes .../migrate-screens/tests.testplane.js | 25 ++++++++++++++++++ test/func/utils/constants.js | 4 +++ 21 files changed, 49 insertions(+) create mode 100644 test/func/fixtures/migrate-screens/.testplane.conf.js create mode 100644 test/func/fixtures/migrate-screens/package.json create mode 100644 test/func/fixtures/migrate-screens/report/data.js create mode 100644 test/func/fixtures/migrate-screens/report/databaseUrls.json create mode 100644 test/func/fixtures/migrate-screens/report/images/22643bd/message/chrome~current_1769631040949_0.png create mode 100644 test/func/fixtures/migrate-screens/report/images/22643bd/message/chrome~diff_1769631040949_0.png create mode 100644 test/func/fixtures/migrate-screens/report/images/22643bd/message/chrome~ref_1769631040949_0.png create mode 100644 test/func/fixtures/migrate-screens/report/images/5e608e3/message/chrome~ref_1769631037254_0.png create mode 100644 test/func/fixtures/migrate-screens/report/images/deaa096/message/chrome~current_1769631043495_0.png create mode 100644 test/func/fixtures/migrate-screens/report/images/deaa096/message/chrome~diff_1769631043495_0.png create mode 100644 test/func/fixtures/migrate-screens/report/images/deaa096/message/chrome~ref_1769631043495_0.png create mode 100644 test/func/fixtures/migrate-screens/report/images/e7de426/message/chrome~current_1769631038126_0.png create mode 100644 test/func/fixtures/migrate-screens/report/images/e7de426/message/chrome~diff_1769631038126_0.png create mode 100644 test/func/fixtures/migrate-screens/report/images/e7de426/message/chrome~ref_1769631038126_0.png create mode 100644 test/func/fixtures/migrate-screens/report/sqlite.db create mode 100644 test/func/fixtures/migrate-screens/screens/22643bd/chrome/message.png create mode 100644 test/func/fixtures/migrate-screens/screens/5e608e3/chrome/message.png create mode 100644 test/func/fixtures/migrate-screens/screens/deaa096/chrome/message.png create mode 100644 test/func/fixtures/migrate-screens/screens/e7de426/chrome/message.png create mode 100644 test/func/fixtures/migrate-screens/tests.testplane.js diff --git a/test/func/fixtures/migrate-screens/.testplane.conf.js b/test/func/fixtures/migrate-screens/.testplane.conf.js new file mode 100644 index 000000000..1cee134eb --- /dev/null +++ b/test/func/fixtures/migrate-screens/.testplane.conf.js @@ -0,0 +1,7 @@ +'use strict'; + +const _ = require('lodash'); + +const {getFixturesConfig} = require('../fixtures.testplane.conf'); + +module.exports = _.merge(getFixturesConfig(__dirname, 'testplane'), {}); diff --git a/test/func/fixtures/migrate-screens/package.json b/test/func/fixtures/migrate-screens/package.json new file mode 100644 index 000000000..e39b014d3 --- /dev/null +++ b/test/func/fixtures/migrate-screens/package.json @@ -0,0 +1,10 @@ +{ + "name": "migrate-screens-fixture-report", + "version": "0.0.0", + "private": true, + "scripts": { + "clean": "rm -rf report", + "generate": "true # This fixture should not be generated. Report is commited to VCS.", + "gui": "npx testplane gui --hostname 0.0.0.0 --port $(../../utils/get-port.js migrate-screens gui)" + } +} diff --git a/test/func/fixtures/migrate-screens/report/data.js b/test/func/fixtures/migrate-screens/report/data.js new file mode 100644 index 000000000..be2aa1bef --- /dev/null +++ b/test/func/fixtures/migrate-screens/report/data.js @@ -0,0 +1,2 @@ +var data = {"skips":[],"config":{"defaultView":"all","diffMode":"3-up","baseHost":"","errorPatterns":[],"metaInfoBaseUrls":{},"customScripts":[],"yandexMetrika":{"enabled":true,"counterNumber":99267510},"pluginsEnabled":false,"plugins":[],"staticImageAccepter":{"enabled":false,"repositoryUrl":"","pullRequestUrl":"","serviceUrl":"","meta":{},"axiosRequestOptions":{}},"uiMode":null},"apiValues":{"toolName":"testplane","extraItems":{},"metaInfoExtenders":{},"imagesSaver":{"saveImg":"async (srcCurrPath, { destPath, reportDir })=>{\n await (0, server_utils_1.copyFileAsync)(srcCurrPath, destPath, {\n reportDir\n });\n return destPath;\n }"},"reportsSaver":null,"snapshotsSaver":null},"timestamp":1769633641645,"date":"Wed Jan 28 2026 23:54:01 GMT+0300 (Moscow Standard Time)"}; +try { module.exports = data; } catch(e) {} \ No newline at end of file diff --git a/test/func/fixtures/migrate-screens/report/databaseUrls.json b/test/func/fixtures/migrate-screens/report/databaseUrls.json new file mode 100644 index 000000000..41feca783 --- /dev/null +++ b/test/func/fixtures/migrate-screens/report/databaseUrls.json @@ -0,0 +1 @@ +{"dbUrls":["sqlite.db"],"jsonUrls":[]} diff --git a/test/func/fixtures/migrate-screens/report/images/22643bd/message/chrome~current_1769631040949_0.png b/test/func/fixtures/migrate-screens/report/images/22643bd/message/chrome~current_1769631040949_0.png new file mode 100644 index 0000000000000000000000000000000000000000..163283c5c022118102642532a2797ba967a420d7 GIT binary patch literal 1746 zcmV;@1}*uCP)Hb(7?&Z!oGmcj)ov(9X;hnS3>+W^-!=g-n+I{(k!T z^Cx}#_Km)N{YqcHe4)>uKT}UnkHQE6Jwl(*EA$IJL*HhLMXaz_FaahM7ozaz0lh>2 zLoE(T{q^gYB6ExY77RWdl|f1U@#BXyu)^pAybM-sk~%Ojz`H^mOkwN*F&8}9DiWy( z%*x$52f&l9L`ou6S*UXX7AqDjQdJCfPKYs9Cly(h*jT}NV9bhjQkC0vZh${4Qv3S) zq(%zP2k@wqYQ{=CJ3A>oJ)Hsr18L&Ki8OWUR9d-mCEdMyS88hy&z?Oae}8|vbLS2f z6co_Hg$t!N3g)3%s|9BrGtX|Dnwn_dym@r+;6XZj_AF!BvSmyz$P(Mz0yf6>?c1rP zrG;+Yx5mBl&YwRo$rS!Bz^CQ=_wSOx zEZ)C=Pvggr*GWBm_%K;47RIDWlPEMaly2UMjt+W zVA+KW7bqkogxuZT$=BDHcJJP8wUO4XTc?k$p|rG=Wi>T5ybj~q;89krSfPKufB!yv z7KDzOGiOpxPL9?14LN-d;9h+1qTO9^8#I6T@)T3PDhU(wVHRd zfa8q?hgVTB6hINUq4xH6+PZZs1qB7MTZIgyE?KgKT{V=napOi>uwa4Sck$xIthnpf zuhXSVmpDG}*s+6UMMXtaUS7^I`rNs5SFc{x`>tNSnu)^Lz>FC)hD<8L1I9~A zN}?M#Ztz?ncK`nUEPwv|Id8HpTeeVDRTW*kc8xMKGr9iBlP5giwr$%eDk@5!AN__y z>YhD&sIIO~s@HJ)_HEj`cdu0M2ryEMnIy>wC`;R|bLY;rN|ZpVrbv*F8-R7-GYInZ z^wc*1P*PIDiIX@-M@I+Eo;{n%(&{23Bk9zsQ~GB=KffW9igg$_7l2I#t3?Qb5*IC6 zB(1{;c#gQZIIB9dq&|4?K=)$R)zy+58_LbijoUnW^hlBq2JmSylO!2|oqO-zJ+`*& z>}*~H6a*Pab#--R_XRgPWy%!Zd62=~jT<+P6{!JjVL{s1aO3EMIGviBYBeuNF~)9@ zR4BqYHb~Y;g@uXZy?*_g`_$IfvV7OBT~=}t_DG#DVFKT4KtKSG@$utFsjm^ojvdq2 zY9todsDtdpi4#&kM*yG0CRIdCObo4Avxcuh>e8i4skOD0cN!EiYt}4|o4L8UG-u8n zcFRUQe*9P;3o)#*v5{{OZH<=cqBDo8Yr3%710b?45VR_lg^LV?(Hv9Ynk zCKX-)7Kn|FTmbhe_7x!#J5oXcCe`d4nRoBr+01}(WAX9vd~I~UKy!05FBC+<{lYSF zoDs*cg_M*Oj@h_wYinccee>pxF1JOhvA^hkk%SrL7cX99MZ#kD?b}BY5fRcFjIc+l z(fw}Pv`JsHBY=F(6$7|Fe*C!9Zm0mCrekVlWhEy?M~)m}Wfm6~)7rIbS(%6hXrm=c zu+sYadd_a)UXfT~L%@a3pFf|<%F5X7ojiGx5)%`-Z&p?oj|qbCclz|{bo%sZdinAt zTOE=tjH>~O5|jYBBT}_QY4hgI95xCI3%S3RC_R1pl-+)Eax&lNl`B`6M8tM&JbR>O zWMoi7LjzsDe3{40%gd8=XAhdij>JMWAn`SQq(Op%hXu%35l0a>p|pyM3cZbZqzMZP z<4jVM!!0KyBygsQF>vHT#tT_~em+0kKtCi&hYlU$dgG1T0_2WJ1#BH@y1Tn|IX2<) z<;!_~xcc?$*K;_~t<}2!(7`tOrmJxmf?NL4TmH zUcLQ)VD;+FS*%sTKBSlAD9T!$8#cerf}*~7=A5wm#WSraH|$(6x?QbEq#`9Y`{+<% zbO4@geM?^=6$K54`<_+d@4(d0E{sLR*s%#QhKglSQc;jqsW>|}03JsCQ$}M^KRO;g o{PrImi$(o=fIo-d_3r`x1Lzn>n6r3J0000007*qoM6N<$f@?HB-T(jq literal 0 HcmV?d00001 diff --git a/test/func/fixtures/migrate-screens/report/images/22643bd/message/chrome~diff_1769631040949_0.png b/test/func/fixtures/migrate-screens/report/images/22643bd/message/chrome~diff_1769631040949_0.png new file mode 100644 index 0000000000000000000000000000000000000000..a9ec4654a8b7a0c19e4b5eaacd594121a1760020 GIT binary patch literal 1594 zcmV-A2F3Y_P)R`000I7Nkl<7{^7{vX))VmC+s~yF$b^#IQT;Z}(y^ z+=qt;6RMLDX;eTUqQS%FVh`M#n;SDyUkO;zLBPf8Vh!%o(-X`4MtPhm$M{r^nhZ{w zizT=y;AU+#9>#MxI5=rGT$bRP5$hgcxw9aGRf`r}v&Om?SPocKgOza6g*`{=FlD;N zVPsvj60Vt!T?65{oE`k6MTC2Qe{V+ZE1dG^bp&u^iwIXw)E*xn)2C0LC_X-(e0_Z> zG&Gd*^73eDX~}K}`SIfi#m2_c;^HE8cXv}-S{fUW-jgx+muDL(dO2LomGoR2{{HO4xrg%a5zY1Wu@d{CRk%i0ywg{FNce3mi7SQ`&?XHsP^CA z->4zOdwqSa3$7NBlamt}Ty{so!-K7UetwjkoJ{lc^OByuy*(-~ zFK5SugoMbB9UUE|q@*PB_xGoWhzP2#t|rl;N=iy3*<8oa&=8aT_3Kyi^74|Ucx0H| z++1nf>gp=XG$<)iQBl<1-Y%2Bv9Uq<`S}zO5I|vJVFZ1Lhllj;-8+?D=s!Cgecj+KxszJDV19xR?YxA`}M4iS6$0(uWTpm_Vdk$l}8B8^+?Fxt@%S4B0;TzN)H< zZR_vvr{UpYR>S#m;o%tdWtCDp_Kh2V8MH!NI}o^W5AV(*?i{4Gm2G zuV23;XPcg$rm3kZs;jGG+jn+$n7)dN3YmUUwjMjy)YQ=O^0GzSFDxujZEdY?HV9S; zuAXK}96XH1BS#5OJYhJ2_&xp(3=EV-mn8%7?TAROJ3BjDc5F;cj0&!DoT7YXegE?1 zi`CrClGL+|U3-bEi+&2s_5p(^GYFDJdxy9fO~gD4VBg zOAd5!aNrPJ(dbCa_>6OX^X3hG{P>YhPfx8zT~}8ZI|gGF0po9Ma|A5x2cAyDReEPZ zK>-yP7pn}lxw)yCHhIcM+Qz!IwIvw?HSXKDZ_;r+Jw4KQ;o;#n;c}Vy&C|UlxMDMC zPXzMF)q*SP=I- zwbHU^cxBC1rn%3bKRX?{%JUJ&Y%4{>39vsJPppncEY|~`QsnBbxs#KVl#q}>ZEbCY zE_GmFfZo4si}!wp+cR-bOBS1iHQlx z0Fvi=&p778zvT#3_H{SW8nO zG2LoSh0f2Q+ew2W+vNARm;$yc47g;4bI-1Z4Yh4ep7R4w%y}i9QQ@O27 zAnTTIZ*QFzXiK3H-&?DPnVFfoyGkR*^crQzE@I$LzM~zpD^HH6E-jr!MMWmu|DCez z@*{2{=&if1gvxbc>Sd{p@e3s{F|OC7Q+4A2aLrqO#$V%o3Fw-){M;paU&3JSqSNBd zdNX?6KXR=;W&^#HZKI8tfWSdIQ3#7r{ip=)%l8{5l{T`U sR`000KANklw{Irr*EUX)+Ieo;q9 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 v`+c+dvnGCx;J+SiU;T^V|J(`dlSKG literal 0 HcmV?d00001 diff --git a/test/func/fixtures/migrate-screens/report/images/deaa096/message/chrome~current_1769631043495_0.png b/test/func/fixtures/migrate-screens/report/images/deaa096/message/chrome~current_1769631043495_0.png new file mode 100644 index 0000000000000000000000000000000000000000..96c583c8bc1e4a49d343411c449740040df62d69 GIT binary patch literal 5206 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 B{u=-Q literal 0 HcmV?d00001 diff --git a/test/func/fixtures/migrate-screens/report/images/deaa096/message/chrome~diff_1769631043495_0.png b/test/func/fixtures/migrate-screens/report/images/deaa096/message/chrome~diff_1769631043495_0.png new file mode 100644 index 0000000000000000000000000000000000000000..c700bc4f46ac98f1cb9f7e119555e20cab67f37d GIT binary patch literal 5523 zcmY*dbyQT{*B)wymd=p@5ea$e7-^91lp09^m2hZ~96^x*2Bnb(1!=~YQ0Wc@r5lEl z?ylh*#drPIcmKHSuC?zzXP^B%=j^keNPS&33Q`tQ002Otp{{HQ0N_J$-x|a~+*>FC zRto^oTWBaN8v9voX81pbsen62S+^(U>lBo!pXn>_HV_%m?W)!(Dy!@g-jG=fQmVq{ z@E~7+kRamBQt?7rmw|j8miPnp!OGRd_g^GjQE#`~{J%G3NIvuNotf zIJ?{%cr4kNnoON3mzS4kw?0yOE3Frqlfxk_B*d<)x}(MV22Vr**acu_Cr||fL7)$g zp+2FnLX&n55#3Q|(Xp{nu9MZ>$iw5~sD=i);3owC{rxEWHZYqv`=4y#Y;oiz9X}k7 zo}05pvf#sLn&{dt6#owDPOh-Q{>do%C$BmNb1Qm;std~gB`f-R)=)iQ!cX4Y?-p*u zJ32ZDJGuiOBsowA|8EK^V73Y@?;aW-L?RzOLDi%LSVS+rB>&?bX9tK+fyKQo#Tn)3 zB*Vpc)y)OQZxK8SC4YmkV+xgEW@bjC%v4ChuhZ-VdOV5m;@0@vh$r#}Nbr--0Fu`< zS3%dTO`vTRmj3on8WecR+2d%%VTs)EH+5mVrG^4so+SML&yPR9Z9xXnI&MfPLc#7& z;h}n+yauLFACeFcLduaZyWYY{_u<{YQ~D>CN~X|P{ql$=MZXBqd}2Eb_C2XbG1qGd z6ai4!(?R_%U;H3L^S{u#9t-F7zB`ny{Vg?E^%=3vJHVdB;$Me28-M{g5zbFUIa7OZNd;JPH4P12p`;hEPn;E>(v1&C3|8DfOKq=cRVqnFf zd!GQ}{!-?n0I=e#`<le9hVY1!ApEx6`X{L?wDt9tZ37Oj zcB`M3?7k)a2>?%n4?I>P-g2LyI+IR z%%XcVuYyRy^xeBAPDw`76cU$OS8;g0z%@q6D0%1g_LZ@ei@u^l+Cz$72U=)J?6hyC zp3LM7Vc!q=t8{L+KuUvErO3$6U6ezsE-Q|`m>vaR-`MCev6^boHlK@|Iakf$!TUxR z(7L@-@{xN$bv+4Kdo}4B@Xp1{$1P%eWo2cbE<-pp3l)73k zZxmR`advZK@SSpxfG)nd|DITSuZy#tGgju-Na@{KCQpkP#`1+Dp%tIArDW^2)8#C| zqseFT=*y$YDD69(nZakPFH-WR$!erslNx&8M3_sL&s}+M5(J6vs;PDb$$5$|1b#nj zTTc3E?e-&*5kXt^8WOH$h`&7Ul$9u2JkQ3kVf`7N1KnvNi0S`ekGiK?V*YPOAvoE2 zmq~JmZ(O$#dsaO5q#$F@@5wX0t7I3h$w=?Ci;~ue4Cm2r>b(=XcMB zCe%kd`{*|puv3Zr@Ha4+HlZp>mSNl*4j7DOd{9^VpOun>g<<%_X3gxDG3r~DU$k^! z;?rW!AF`u7ukKbgl#DJq3GhPf@_Sp(9Ok!AwUjrf+ULs~88xaoEDpS0;6QF08}rL- zV!nBkcB}wW+^1sBL+h}>;w-f=2sFz&Ni}G={16BV=}nzw|MlIO>S{<*M>B-}EIolA z?$T3OxVf+ujiak)C za4GJoKmC`KFN=XhxNmV7*wm6%;aR zy!7$?>2R^>w}7yW)*E{fkYFB^^cK{71P=secdI`f0nb7LHqic~{p)yHQ(l8Q1Kd0d zpS}su>ged)1H=60UV6;V1Z@AaBl*f3ZNpuCxzl$w&Kl<5wi zDdBSc@l2FAl@yRT9e2u2`fWV)o#r}jouh)DNb|!fPs2b%r_!L#d2oAujY{L z`r}{Ij8eMZdR<(IK{Sh)l&*^@K6lx7J^4#99_}KR<^npt z*(3@z3*SKypi9W2q68bD1$|IGM(9Z+4V0=xQ~ecW2*l>czCE-1!oqv1Uk#G+FC^ID zP~ntne(oHh&~{2eLE-Q6(5qi%&yN^9^cJ7nU)b>R!lp-{{k{itX*VtOtw(;-&8sMG zSl;R^J$h3ZcV2#UH~)lHigxRdW9U{k+F#sHCfh#~<{|@IVs!?n!F4ZXwq98WY>JO7 zgdFs%b=bdsyukhi<4;L77dW5+$%^>%Zd#j%p$c117 z?U`qBb<+j{BQOw)Z$gjXTrPYb?xd}_gMoqb)VeA*D_SXOvx1INrx+Qa1)0yE!r6k! zQwS+v`Quo5kUbYk1N~@Z9Pwh@>W2J9g#iX5V`R)%*SpBd1t0RJF0I>cIJe8H{k$A^ zK1i?B6C&=(NTz)ya2O_{KT`58a&ArOvit0FB-k3u0`BSIC^WxXw$kwl`7+Y@2tn|c zdid>~`e`v`ss{JOFwf1ZQ5J|&;%MmC4hn-gMgggp(%W^S+nJV@mIMd_1F9|cLyjU4 zB=9E_-X{|9qKATZxGOS;Ilie`@OgZwqFNCd*eJEAdBQ$cRvC@2#(3-I&6{ZaPDe{d zMA)40u&o+NI<$5{MqK%@jaF}Q?C`tjj|8I*Z^S;X!u>ksctI4in~Ir8P4>t8@#5x=v07;M{=_*Y%hNXG&BsiUp-aiPFJjd#ZB3 z38^=p0oc>! z7zZ0aM;w`Mi1LL|$74+c@!W zz`r&%&%QpfIU%@Ddtw=4qH2cuc-x*>QM*x%MocOwAxxdJ;q@z&rz-OuviwSSX$CeYK1b_*$ z3%8&=(tI!S9HFUYg@iqoPX_Vbn!oa*p_&+*bGh>k&PyU|+%_7Y$CU^2xah-u8`fmz zX!n>K$J}=vb2ld;m9{Uv-a#KkrZqGK$KvDexAJ6i8w>H`ki*?>y@Pt(#pUPEL@f-v zZ*$dK<(Be7&!z+TFdi==Pm0{gDbzXFQ#^5r4?g6=J~S=D5H?79^#6-Pbwb@hG}v=LkogbWbCv5@JG zQm%VKmj)lGQ~WCbCur77#u1oGh>HDGV8NX7G`i2@*Z0&J*6u<_t*LSvX#S|W&50pN zdf{W%8Bec*3V6TB7gF~eNP5`I`M}AfL$oiZQZJQ|QlW;zh>HtO%ACn}Xxnxx*iYV| zaB1g)M4g#}XUkT6H}Am@RSz#e`P^d97`3WPQywB&+rNQU!UC}|mTJpzAxlhDZ-zLB^ zz3y3N0)Iz_tR)jgwvjQ4lerAaYb?%H$h9xc{E*mwg0SA=_p? z#Ff;z&h!q@B6<1O>~A4qsaPsZ1#?i%Le^SpHp~krms4cZwXCh_Vqd3?54j&g-}_Dy z5eDi$u;R1C_n4BB%r}+&o+5l7$tyAAQun8yekq#0?CG+~O6AGOFJ6|IOP1k(B?i39 zZr=>e^2aqv0a%Jl6Q*q|B1)FDo!Y`cQgDe@*pq3OAyQ9~O_cy#nlptb%K1z^uP@7s}s{LN3 zPu_G_(~y03L4hQ$P(2E zy8tgY{<$93lYzYz#bHbLF_@RBOl8Pg=B=#f6cRzYS@DP#CWXGJbt!$@$)f!UnX;Fy zXJeP60}sA7R-jPj%kKv+9BRwk2q^`S4(I5NAT$}?Et+up2RcSJLHDAT=tiIeR+Ws` zQh+uPD3b4Pnngcb=l^e033uJ?es7k0;S{uyr-0MNHx6&Pe+vLjL$Fz%`zkg+)^Z zL#5iH{y)*88BnQnC#}~I1LyU<{uL*IWItsJt?nkjal^Ei;zJIpKLz&eKOIJYoLph@ zk*caH5B%fA{{)kw-+_f4N8OfiZx*0{p06&<0<~M zz`uG`Avq?I+#pLG5k|}Ial!(^H{%PH$s)hD;^eh#ofeID+Z=m(D7fs_8#%-y^To*P zp*0-<_aqHk4bk_Yw)EkP8O_bjNVbT(!!|NmH*Q`>_6E`82(V~N(1+<$qE>Av%}uHo zPHhI{e`gg>L_dc7Gr0!6jIslbRBLPN_g-5^S}+|<@I-K^5K&x#IbVOALLd=0XlfDcrkNQV3R$nM4fvvD3oSmVy~k^yO6Hx zT)Z4pAk!1iYs^x}$rU)+ZJFNTP2jQMTeOUFb5ga=B}@EACP=pU(&#h$G^$v^N)-^d_!04cMCXr-gs z6}@x;7;HwmVa8W&5ops@CrcSVm)GIZB4W%QJ|6`K6WUokVPN= zkT5$*dtlN9w(J{&mu)DkRQidi*~X@(1nzUi?%ksQ-$KHs6#^d16U?s$)zwj(9IM4c zRK+Y${una`4R+NbRyH=O=PdwF|=2Nk>d)`X?Ch&1&NHX#~6 zgob7pH&+J(nEO!eN61XF$k93IUwFoyJME6P(ZTfK7c7!`5($5-gi+{xn!%elfFLT3 z&>5S5ERxE2dB>pN2;hp_)boa}1y20G7CJXX^uXA%4T9)mNXbA=0u27SNZki0s z$~CkzfE&#P!&md@euv9W(g@g*-<6iU2v|D|9}v(WXm=K5L5REb>`IP z|CjDkG9Q;B#C?p`VHD?oW$>~+n5zph=YxtZMLFG^dhn|f7viEZ+MPCpxMKAVfr&wmIoqjQt z(d3qM(_Z4ydgXworR94WAqUcb!<0V`oQTqJ6+Y<8RgNr5PLXh)Ch%Q|QziMXr!8aY ze>h9(tMwi7x{LHLyrAt1lX>q73;Fm$Yinv!Th5O8^2#E3q3JTGyCXH}Wmm8Lit#Zs zla0Nh1CLgTL_CkSXe6W1qvs23dLOabj`u3!OnYfZPcSXc%J-yQ;?+=fYs_QhCts6( zxw#}oSgmKBY{u17td(!C^VdqGsmw}IG1wdZM*kR?;6_XxW; z^<`S)tg=?Qa9+fnG?RJUdZAm3Be5KcbL4nO(Mpg*8u=7Ic9NIz@NQuBVpheykrAZ5 zqnB-Heo?{0bA&Ijrp)x<7TlSyF5sFmJov^I7#p6WDN9TEr45m*T)fZd>1b?jaW!i) zpUUH@z5UM*8|RS;Wf4Jcmcl>q1$sp_zJGshH$2Dzz18}PS+uv#WnQ5)TvN7_givpd zr8wCZv;2|_>e4rKh)FJea8BHcW7`8 zU%HC8LLKqEZmhoKgp#v)PvJ(n5f4kD>~>6&Hh_29NMeEx9CP=~YgoMljYeBV3e=Rr zS3~F_xAcwO-$*XRDmn~K!Uklt^z`BZ;Q-YT_+1=!q^SJs6{~|OPYHb!@Auw^v*CTd zm?rBhEHIqLBOW%3H2C5K%u%~m~7ED8_-P!EBd$Hf(+{4f6tx|G7vjh;P+Rh&N2uk;7%anmtbEV4BbLZ*Z>G(u!b!{NK9Ug2Qk6i)e`FxFH>yWE8nJeYu6uWsB5FN}3CnuhbBYS4>6JZM zH*YyuOmlYfa3M8Y~)+O+=!wS}+6G%MR}y9c^}CH&S9DJ-gZNd?&@ z*l55si<3bz?%cM7>lqt65++hqnRi52!}s=)&b23k^~; zu9rMS1#isYJE(+ej1&Y7WB`sEduDvvs6jdmDphe`A%C@G*gfUka_XQBf~W4cEWFm} z#W%fG>D4=KAB)$bB&KcFKI>TU!|69X1Foifap|x1iVlcr%O1TuHQFy{awq^S;hl@) zBd;tT*%`RYrjKG5JPL?Kz$2u7*0l*fb={w^M(#Y$9=rM>iux{g(|==bRsQIOa!5}w zjjT<$XP1BiyK7@XLAPt*;U{#WVJ{(#v?=7y3ir;4_FZ+PZ#5aS z?eaABpAYX1EUee)X9-(j+w=KW^$fi>-oW7T{ZI7p13Abv_qgeBE>fJy4?h>+9qM9> zhe9pb*E*<8iZp~)40%@<)A%R36~GPn%cm_&h3`A)z(hL!<*KAl`Yp3s#sdjdH`zq3 zG#4t>gQ&>Eh#>xG`l@Ah7X=!ZvJr@sG7%(K1Q@s=_(h{V@57h9TV_w6@4YEL!H+1$ ziKP%a$BTvOO3kNpkwWV;jq;54seou4zN-7U7rl=GyoocyW7_68;SSJ9snd&Ku4pjl z+|z#f>wTFrQYDBS&-LZEG0{Kx%84Kb1}TngA*woQ^=$N`UykUr>dbGwl-sS1EDX1g zmzehAnltq2{vMvKIJzm(WIB|!;sZU@3~|h3ldD)i3B0ibWH;}z*E|@uWTt>5+|Fyf zgSI_-m<7dF`gzCXe@uPWvnWul;8^-b1+}RmHpL1$DD>W49s&|mH*v_fu`?-rB_3vv zybPWRz%^|M_x6S&jf@2@FNtsY7~gGUtZb657DjOT0nC|?Qj2gX zN`3*1J6@~BPvd0myQgCKV_dKQ@yZAMk&SX^SddJ}2l5OETD>IGi>5Sl!w>!gdq#a4 zoV&acCxX^bK5K<2TGVHBd8OxTn_8j_Td$p+aGOSt*2*$)e-Gm((VflT{m$VVZ_DY( z(1c}pT38G}+k+B~=B;>9cyMQDVxYR)^FdnqB2YSoP~)vlu%h5Xgw7*Q`24mslMe^& zadAnBAf4JpVyxZQ^uTQjP?!%bbHaSPCMR4`kW@Z0lN`4sB1W%)dc`FCIZ<(%9QV3( z?tEh!1)T1Y3P7<67Gp&QKw=2z%rJ~z8+A+it4q=vJvWr@XzH%gms?Q*xHwf-Tjh`) zp(Vc~bW$R7cRNjAUmyOzYa~2F4h78;2<7yU{pG_!08U zx3;67eG)Sul%eYYiaib?Z?XMcs16lWCzD%&-D_<;qS!dr#U(sSCZ<4=Tutx9WK(wM zYMy7K^5eHk%h*w=50)lFhrGOiaI;*$%U~w?jGqAkpKtE37M4ziSFcTKb3@5YDc|at zxViGfIVV7%uo$vLw5&FzdV39ZbtaB8(P|@@kjH@$Opsnj?Ii6 zq47Iso%Sx?M*i@&g+S;MBd0tdeBdqR{ues%P^IUH-~#iJZ^m-Q_W5>Ln|cNo|Co_c z8r1IU_FA|t1&cd7!3=y?R7;d%3=HDzv0hzBE^H8QRuJiJZyv#pb|A>d*r|CY|*4#A%w_vHzss`Y<>Lc(zZ9w1LlSi&(vQ6=IoWp{Y%+;{1Z**gtZ~khhM>g!5A}Ya)K4 zjEg7)iOnr0=s_Y97=E=2Guq`9mRacn<)EShCSs>lV_Uh?v9eEGTs~={Is?HWoXTz6 z&m!*d*a)s@*b0+|w;5_x4T%m&)2}F-bw;rYge4!|1$Lfso40Q)Wd%5u-d)ZPUiSoJ z8h45YN}7-D_=><`0n1;0c+E#S`VNgHtZ0j+GIrINE9QuDy_YcySQc?IvontPjC6H`RJ;psz6^GDjBQa3g@m z4V{~+83G!a8SU*ZSntZYp5nRix(>5KR?R#9pF;YwZ7I7tapu8;X5YJUso=1>_JOmU zTLxywfjQ(-pJvjJ)Jd=Rqf-+es4MivJzG-#1o!Ala(6;FeuENi~hnpatfr z+_fv#8Yu3xfOsI6%Oe1ojL-3u3VI=(h-UG0isu@c16v-L4O&&pCO8 zs%nz){U61Q4`iFie)f21fXG*%()LUcTgNR z@QhH-OawLHX|P_MHZ;$pyvUP+N%M+onM&t1KP!yxHSGy;N$G7nCvK&*M3BK@s+3FM zUd(Jju^|uJFYd29;F;6UQZkP?x5WF^7bpM)PhwX(OiB# zZ}52r8KMy!6GK9-BG;hFDE?)wi|2vhDY;rSd~B>nEjlXAE$Vrfk;E1*zCqI7o+bHv zW%Td-4UDnr|={@0&M1}FKJ0K7hRH|YW4#G+H0rN0SztO+%>qGqr`(hK{# z8rfjj?8kG{?wOSued91#-gZQm4pDSkO%}5mDSpTRpM`zP);G2MPbm;a;YVXi#(-(I zaE7$AOu5=KJPBNK&1CtF@?4zYyTXR2%9BVWY};&i@xh}93G*r4?P}SY2`Xk z@-O%RwrpEYfb~R$Wsbhu|8tNkD>tGMxs`#+}lN&7Jw=A_ujv%$Uob(u^k1E^S-(M{*khT%~ryh|f zW0z8|*-%^pcXtd?y=S@iN0ZVg&=6^#e0DA_h~0%8QeFSY*Qt+FZv0C+}uLW-v6g0!QUjoqobn{A#?H+pjz9`o7k^!{wOeI6~blQ z5ghqdMA69lhK4t3@uXCOe>`S?5AD;P*`_dH1UtB0%*jhOQjO@GRb>BrpNUCMFI!-c zsf$|G@&QW8Sw=?Yk)^x4`@~CO2+u6N?DfC>)F{1s;R-y&OpI{y0u?;5b9Ddi3~>_7 Tdy@Gc0sc}~hA5RNmxtgv!Zbc1H%P>BLn@1p$?RpqfB11SkKl8^|7?mwMTO0YBf%Lm02v7Wl3eBq>EQ% zvep7xF5%0re)?3w8Xy;1^PYn}Ss_6be_5eAkfM2SWoFT)wWu`WV?>(Ucx21UqyDFQ zQT-2xsmRiflc#xQ{=yOx8u2%9=PzDNPNW?*PGsg9GT1B7<6)kRj0~^OEdv7s*Xa!X zzV|;qjsZdf0!Da~Z~5Jkq@U3wle=KkT=R3p;o%3Fcye3n83&Qg)k4LK_)wjceOKBq z_AInAreeh`3D5)qm5*D}C=G1Ik#E+#N+)BCO>gQJ9ax%4X@m~*URYC!55k-HZ|RwA zO(mFm{>^8oN=`1~$R3snar9 zpAyPP1NJyDk@AvW&xczt+_o8^*%{odCGqQKsw27t4ivIs2pqp&!rnqp&0MwC=L#u6 zHkWYK>}G5w9yo(Do`uuPHu#KWM~^9d5N7vz(#zYzPsP>aCRwQ4`$}gx+(WFXMMz$2 zWFN0%D3POhptF6lG(z;OT%+ePd7sth7GKHfp5_SL$g4b@=fSmO7P#mqiHn)2R>Ib0 z`;A*1Gm!#{>`bY&cI|GeQ0CcyZm0NB_9p_X{SHFuDHzPxmn>rj4t#cQcu2e0pDL5_ z0zP{;$(|p6RZ1kdz|xKBLAFB_Les$VXz9R3y<<}I)3PISsqdo*KczyAlCZiqtkCLQ zM8E4#XN^wFLUSO|oT{K&arx^Rk{=hmG+5x?_gb27xZA!dW11+08Y^K-KgMM#|HV*0N59(`#CMDMDjkcY%i9qj~6fnyL(amwc ztZ#sWemRS95=iMuH*&H=RTv@^zGht|WCM%-F!S5^-fa;#R`g02AFUgnrUD?yH8C+U zcS7TeTZvs0RNWW>Qjd*|ovpq7Nf5??7YMtoI)1F#orTXJUJrvc{X`AMaLmADBvo+; zN4O^t2>du(bUo;ssBYw>x2)v0H*9rWu`Y_bf2rP~J#xx!y(M(({qowH+6Lypp&HfN z=D^1UT`p^Oz^15Q=mKXTWm-LOZ{<;4*pBY1edpu`$)}d;^ zWM|5w2)Va9!BUkjs`3? z3VM;4+PRsa>tk$65E&uICbic#c&5J9aY8Id+JKG&SB9;+6p7nwYW*djJmap zo{@i+OBZ`2qCrFR7yG8_JTPEYP7J{@YVXnJA7XPLZYh9}Xy{#h1qZZ)f8TLYM`>gS z?~C$xb9(-a+(`{P*_(*<-Mon?v~RLkq`mD-2v@SEy*(k+SCJ|Erwwe0(Ls|Yi3@yO zWsg)DQHi}(Li&xZt%SDyX>a|QAViT~^laP7J^{yfRV#q83Mn8E>^ampUTG&F^1ANs zd30A=yhF8#O5TewR{|U!qsFbFkxv3%=K-twt<(B7U*I-pl(#&J; z;+t+NDG+K(fc&co34;Kg++a;iyH@bpwQ{Q}Fg{z6Bcyo~aWI^OEh&y%-8 zF_XD>CF_*JUw_&27ajU&nkN|@2V0x0>Gijb9E545C2h>prkGw|ESsSBP+{q3Rmb1! zwfP(0*AWgPX$Ok~d9#0Mi|t>yAPQUT7xEKY(t?5)+HPrbFJ*4DQ$e8v+SyUteX_IR zkSXW9RJ7w|O;Y>r2LXl56x~sA&dRJg-O=?1j%n~)7KBS+x|P8POJ>N5ewAa#x+eTM z6xP`QKxAslE}c*hSadG5Meht~NCBoe(RwGl7XMtEGW(BY-rB7V8s>6kOKNKZAn+k^ z*aq(jAk`y!tb$8;8Q`qB`t0autUi1=%3EI7`k&Z$EdFCfAipHV4%n zyqy5j#`z=FU2?=Q*rfZe4_2C)dzv*dI7(#lQ!5!G0y|inQdPe~tF5*G_O=1jwU0Tu z=^WvMgEuP}UgeAic1lJIcJBP4Qe*eNAeg2aDP5G+)t&4ep&TnkQmwDn(0&W-xUG~w zJt}Tu;jsO1KAEJx7kqq`RY~?5&+}1Yl%iya_>$Vesq)etR4)uP-_V-YbrClGJv%LK zQw9F2RLH#|_iK!niV~bR?utv5wge}tJ15DWfC;8AVv^_j*2~1Pmodf^gqENtx=uqB zrf*38HF!P$trZF)RR1pF4S6mGFPOfI=2r0Jxqt?bO&RY%v1IN-yA_5`MV*?tFn_=S z2^WiNpV?E4Fxjo24zpnbJp7*R!@%4tsJ>t!*STqHQSUn4E6mD?A)q9wRTA znpFavD8#Y_LFQ%undS-lk2LMnR{~~~!wu?e@!VZb4LWdR0?CHW@(6xmOrr z#%5kDHM8Qf?!nsX8`N<>7(oqn6&~qb`zoWVUjpEQ=+(|_x9UP_bIz%^AfxH8Q$8!t z!{~hXE6fof&vDj&lK>hNK{OYo&h+RQ`6R}zMaQ+<0Sv4H zppMSc_%34X)Nrwplrtl4i{B>&w5@yGfD9BS_MhK>X;re#jQ=}Xupg60mZ?ijfcwd$ z@87<;cKWddT&E3q!g&q@ zr{DuwNmZ65ql5g+ujA{jORsMmT|f)HVB67#A>@kI(}7c~Rs+4UAsR9yubgXKo^KTZ zh+2RbROo&3+oRz-66ZpA2o9#8VICNzB!cQ%2*XO}PdnVZQK0phNfypnBz6vv#qd3w zn{9sxrZu;a!hv@B%KTzkQenr2wW!U55ceqD?hBdOlL_P|R8Q8b#(*2)*+ab%ojS!P z)%inS-V|#yiZYpN*MN{A*csF&IWm11Eh0YhPkzr~hhKtR#aG>~eoUVNw6($TJlaNl zm&AUn9sWHwQO!I%#&}4cLOHCA`>*r8nDnz(-aQHD(@%sipyM7p zSUqNvx%^rM7#_z~95;$K4mScI7`%!;-M+S{zVn}WCkRtE$8~uG2uUV=yRQ!&P+YK9 zDcXp;?m`_?Nv_6+33psPH55y|ujp zBr2q@q=s&;zik3*o38zJQ;WaN4AJ>)W#F#D2zoYH;K${f$@nqRQ2tUJ_wAV@NR;Fu zw^K6?9NEE8Md7Pbv}-hgL+rBds|V#=8w)6&`Z_C?aV)I;qatd2Y&)C@1--_Tfiwfq z_^z+~UO)*yBenSbY%i17fl!`D4e^zJ=-Jx-j`*ni8xY?h=76nm^%))=-n0ef4GV}T z*OzIK_`#^%Gz$Y)nFmwSK8DVfJP1*>{q8Vlr<#_q~6N z71Pyny1N5kj;eg}SJVYMo9H@`R9NZI8qNsnQHhkhu9imEswPTai)FadUs!uf`Eclz zd#?Y}@-k7_a@P!cgd^9Vn4ryE!WMW^`tAo0>_>z=O=yk?|IR4sd6R1gMW0&;3Cxs} zYKoIkg(|1J)xWE>PtS!kx=AeCS2aRFO9LtB@UE=Ip{-8$$;sCO#qS!lm*2=(Hvowv zHv{`O8|43PCDC%|6~px^&;7AQ5N7S6HL06u(w>)ynAo0s%XUNrO-O><;Yeg?$Y@p# zUveARf}beisny;<@Z*u{h%OC*(8X_nhrfkTL&NT13$bxMV3UMIbMdVV-hVhDB~n^yY8Z!h6BIe$dCuX`r*U5IRCtY`B~I7L8G`y)Q$jMH6<8v> zzH{SAU6y!<(kH-v+RZaGO@E6?NF4BY3k$2nqq-afed{|vBVwb%r~-6!nbW@{l{a^# ztMp$-4uQ|KXwG=D?_x;%D&^i8L8>9v?O*<6hvF;E&)^o2y^YB&sA?Y7&H{2&<&?J$ zrw18~T*8L~5M}qThNjB*73q1fmpdv;QRIrk`OUqGeA#D=V|~#Cx{l>CNu%2t93r7uacqaE2F-TMiwhE-la)NY(Ez1W~jGbD)#lQa#JLYG;|yJ97T^* f6FD<{f0Xi++1$^^Jr<*RPzIx0W(KGmPS5`jr$SyM literal 0 HcmV?d00001 diff --git a/test/func/fixtures/migrate-screens/report/images/e7de426/message/chrome~diff_1769631038126_0.png b/test/func/fixtures/migrate-screens/report/images/e7de426/message/chrome~diff_1769631038126_0.png new file mode 100644 index 0000000000000000000000000000000000000000..1449cc64957e541fe454d2a039007c120c88e73f GIT binary patch literal 7390 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 Jg%|A}{SWMG6p;V` literal 0 HcmV?d00001 diff --git a/test/func/fixtures/migrate-screens/report/images/e7de426/message/chrome~ref_1769631038126_0.png b/test/func/fixtures/migrate-screens/report/images/e7de426/message/chrome~ref_1769631038126_0.png new file mode 100644 index 0000000000000000000000000000000000000000..8555b15cbf0a59a941239b47088832682e76027a GIT binary patch literal 10970 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 v`+c+dvnGCx;J+SiU;T^V|J(`dlSKG literal 0 HcmV?d00001 diff --git a/test/func/fixtures/migrate-screens/report/sqlite.db b/test/func/fixtures/migrate-screens/report/sqlite.db new file mode 100644 index 0000000000000000000000000000000000000000..000695183026f15b06f97757505c8889f705a5ab GIT binary patch literal 28672 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 v`+c+dvnGCx;J+SiU;T^V|J(`dlSKG literal 0 HcmV?d00001 diff --git a/test/func/fixtures/migrate-screens/screens/5e608e3/chrome/message.png b/test/func/fixtures/migrate-screens/screens/5e608e3/chrome/message.png new file mode 100644 index 0000000000000000000000000000000000000000..8555b15cbf0a59a941239b47088832682e76027a GIT binary patch literal 10970 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 v`+c+dvnGCx;J+SiU;T^V|J(`dlSKG literal 0 HcmV?d00001 diff --git a/test/func/fixtures/migrate-screens/screens/deaa096/chrome/message.png b/test/func/fixtures/migrate-screens/screens/deaa096/chrome/message.png new file mode 100644 index 0000000000000000000000000000000000000000..8555b15cbf0a59a941239b47088832682e76027a GIT binary patch literal 10970 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 v`+c+dvnGCx;J+SiU;T^V|J(`dlSKG literal 0 HcmV?d00001 diff --git a/test/func/fixtures/migrate-screens/screens/e7de426/chrome/message.png b/test/func/fixtures/migrate-screens/screens/e7de426/chrome/message.png new file mode 100644 index 0000000000000000000000000000000000000000..8555b15cbf0a59a941239b47088832682e76027a GIT binary patch literal 10970 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 v`+c+dvnGCx;J+SiU;T^V|J(`dlSKG literal 0 HcmV?d00001 diff --git a/test/func/fixtures/migrate-screens/tests.testplane.js b/test/func/fixtures/migrate-screens/tests.testplane.js new file mode 100644 index 000000000..3dfa6dd49 --- /dev/null +++ b/test/func/fixtures/migrate-screens/tests.testplane.js @@ -0,0 +1,25 @@ +describe('visual checks', function() { + it('successful assertView', async ({browser}) => { + await browser.url('https://example.com'); + + await browser.assertView('message', 'div'); + }); + + it('failed assertView due to design changes and should not be migrated', async ({browser}) => { + await browser.url('https://ya.ru'); + + await browser.assertView('message', '.search3__input-wrapper'); + }); + + it('failed assertView 1 due to fractional pixel difference in testplane v9 and should be migrated', async ({browser}) => { + await browser.url('https://ya.ru'); + + await browser.assertView('message', '.search3__input-wrapper'); + }); + + it('failed assertView 2 due to fractional pixel difference in testplane v9 and should be migrated', async ({browser}) => { + await browser.url('https://ya.ru'); + + await browser.assertView('message', '.search3__input-wrapper'); + }); +}); diff --git a/test/func/utils/constants.js b/test/func/utils/constants.js index 2c346036f..b1838ae0b 100644 --- a/test/func/utils/constants.js +++ b/test/func/utils/constants.js @@ -29,6 +29,10 @@ module.exports = { 'db-migrations': { server: 8086, gui: 8076 + }, + 'migrate-screens': { + server: 8087, + gui: 8077 } } }; From 615087c3aa8da2dc9fad5c6f52e04608dcfb3f3f Mon Sep 17 00:00:00 2001 From: shadowusr Date: Thu, 29 Jan 2026 00:49:38 +0300 Subject: [PATCH 4/5] test: implement e2e test on migrate-screens tool --- lib/cli/commands/migrate-screens/cli-ui.ts | 2 +- lib/cli/commands/migrate-screens/index.ts | 1 - test/func/tests/.testplane.conf.js | 3 + .../tests/migrate-screens/index.testplane.js | 157 ++++++++++++++++++ test/func/tests/package.json | 2 + 5 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 test/func/tests/migrate-screens/index.testplane.js diff --git a/lib/cli/commands/migrate-screens/cli-ui.ts b/lib/cli/commands/migrate-screens/cli-ui.ts index 008acabba..5f3f8c369 100644 --- a/lib/cli/commands/migrate-screens/cli-ui.ts +++ b/lib/cli/commands/migrate-screens/cli-ui.ts @@ -165,7 +165,7 @@ export const createCliUi = ( if (!isInteractive) { const elapsedSec = (summary.elapsedMs / 1000).toFixed(1); const warningLine = summary.warningMessage ? `\n${summary.warningMessage}` : ''; - stdout.write(`\nTestplane complete\nItems ${summary.processed} processed, ${summary.autoAccepted} auto-accepted\nTotal ${elapsedSec}s${warningLine}\n`); + stdout.write(`\nMigration finished\nItems: ${summary.processed} processed, ${summary.autoAccepted} auto-accepted\nTotal time: ${elapsedSec}s${warningLine}\n`); return; } if (spinnerTimer) { diff --git a/lib/cli/commands/migrate-screens/index.ts b/lib/cli/commands/migrate-screens/index.ts index db4c54f63..f4e625c8f 100644 --- a/lib/cli/commands/migrate-screens/index.ts +++ b/lib/cli/commands/migrate-screens/index.ts @@ -207,7 +207,6 @@ async function migrateScreens({toolAdapter, refPathMaps}: MigrateScreensOptions) const normalizedImagesInfo = await downloadAndResolveImagePaths(imagesInfo, reportPath, timing, refPathMaps, process.cwd()); autoAccepted += 1; - return; const updatedResult = copyAndUpdate(testResult, {imagesInfo: normalizedImagesInfo, status: TestStatus.UPDATED, attempt: UNKNOWN_ATTEMPT, timestamp: Date.now()}); await reportBuilder.updateReferenceImages(updatedResult, () => {}); } catch (err) { diff --git a/test/func/tests/.testplane.conf.js b/test/func/tests/.testplane.conf.js index 160f6533b..0b0aebb6e 100644 --- a/test/func/tests/.testplane.conf.js +++ b/test/func/tests/.testplane.conf.js @@ -49,6 +49,9 @@ const config = _.merge(commonConfig, { }, 'db-migrations': { files: 'db-migrations/**/*.testplane.js' + }, + 'migrate-screens': { + files: 'migrate-screens/**/*.testplane.js' } }, diff --git a/test/func/tests/migrate-screens/index.testplane.js b/test/func/tests/migrate-screens/index.testplane.js new file mode 100644 index 000000000..836e7c604 --- /dev/null +++ b/test/func/tests/migrate-screens/index.testplane.js @@ -0,0 +1,157 @@ +const childProcess = require('child_process'); +const {existsSync} = require('fs'); +const fs = require('fs/promises'); +const path = require('path'); +const {promisify} = require('util'); + +const treeKill = promisify(require('tree-kill')); + +const {PORTS} = require('../../utils/constants'); +const {runGui} = require('../utils'); + +const serverHost = process.env.SERVER_HOST ?? 'host.docker.internal'; + +const projectName = process.env.PROJECT_UNDER_TEST; +const projectDir = path.resolve(__dirname, '../../fixtures', projectName); +const guiUrl = `http://${serverHost}:${PORTS[projectName].gui}`; + +const reportDir = path.join(projectDir, 'report'); +const reportBackupDir = path.join(projectDir, 'report-backup'); +// const screensDir = path.join(projectDir, 'screens'); + +const runMigrateScreens = async (cwd) => { + return new Promise((resolve, reject) => { + let output = ''; + const child = childProcess.spawn('npx', ['testplane', 'migrate-screens'], {cwd}); + + child.stdout.on('data', (data) => { + output += data.toString(); + }); + + child.stderr.on('data', (data) => { + output += data.toString(); + }); + + child.on('close', (code) => { + if (code !== 0) { + reject(new Error(`migrate-screens exited with code ${code}: ${output}`)); + } else { + resolve(output); + } + }); + + child.on('error', (err) => { + reject(err); + }); + }); +}; + +describe('migrate-screens CLI tool', function() { + let guiProcess; + + beforeEach(async () => { + if (existsSync(reportBackupDir)) { + await fs.rm(reportDir, {recursive: true, force: true, maxRetries: 3}); + await fs.cp(reportBackupDir, reportDir, {recursive: true, force: true}); + } else { + await fs.cp(reportDir, reportBackupDir, {recursive: true}); + } + }); + + afterEach(async () => { + if (guiProcess) { + await treeKill(guiProcess.pid); + } + + childProcess.execSync('git restore .', {cwd: projectDir}); + childProcess.execSync('git clean -dfx .', {cwd: projectDir}); + }); + + /* What this test does: + - Opens GUI before migration and verifies report state + - Runs migrate-screens CLI tool + - Opens GUI after migration and verifies that correct tests were migrated and other tests were not migrated + */ + it('should migrate screens and update references', async ({browser}) => { + const testsToVerify = [ + 'failed assertView 1 due to fractional pixel difference in testplane v9 and should be migrated', + 'failed assertView 2 due to fractional pixel difference in testplane v9 and should be migrated' + ]; + + guiProcess = await runGui(projectDir); + + await browser.url(guiUrl + '/new-ui'); + + await browser.waitUntil(async () => { + const title = await browser.getTitle(); + return title.includes('GUI report'); + }, {timeout: 10000}); + + for (const testName of testsToVerify) { + const chromeItem = await browser.$(`[data-list-item*="${testName}"][data-list-item*="chrome"]`); + await chromeItem.click(); + + await browser.$('[data-qa="suite-title-counter"]').waitForDisplayed(); + + const attempts = await browser.$$('[data-qa="retry-switcher"]'); + expect(attempts.length).toBe(1); + } + + await treeKill(guiProcess.pid); + guiProcess = null; + + const output = await runMigrateScreens(projectDir); + + expect(output).toContain('Migration finished'); + expect(output).toContain('2 auto-accepted'); + + guiProcess = await runGui(projectDir); + + await browser.url(guiUrl + '/new-ui'); + + await browser.waitUntil(async () => { + const title = await browser.getTitle(); + return title.includes('GUI report'); + }, {timeout: 10000}); + + for (const testName of testsToVerify) { + const chromeItem = await browser.$(`[data-list-item*="${testName}"][data-list-item*="chrome"]`); + await chromeItem.click(); + + await browser.$('[data-qa="suite-title-counter"]').waitForDisplayed(); + + const attempts = await browser.$$('[data-qa="retry-switcher"]'); + expect(attempts.length).toBe(2); + + const statusElement = await browser.$('[data-qa="suite-status-bar-status"]'); + await expect(statusElement).toHaveText('Success'); + + const assertViewStep = await browser.$('//li[contains(@class, "g-list-item-view") and .//span[text()="assertView"]]'); + await assertViewStep.click(); + + const assertViewStatus = await browser.$('[data-qa="assert-view-status"]'); + await assertViewStatus.waitForDisplayed(); + await expect(assertViewStatus).toHaveText('Reference updated'); + } + + const notMigratedTest = await browser.$('[data-list-item*="failed assertView due to design changes and should not be migrated"][data-list-item*="chrome"]'); + await notMigratedTest.click(); + await browser.$('[data-qa="suite-title-counter"]').waitForDisplayed(); + + const notMigratedAttempts = await browser.$$('[data-qa="retry-switcher"]'); + expect(notMigratedAttempts.length).toBe(1); + + const notMigratedStatus = await browser.$('[data-qa="suite-status-bar-status"]'); + await expect(notMigratedStatus).toHaveText('Fail'); + + const successfulTest = await browser.$('[data-list-item*="successful assertView"][data-list-item*="chrome"]'); + await successfulTest.click(); + await browser.$('[data-qa="suite-title-counter"]').waitForDisplayed(); + + const successfulAttempts = await browser.$$('[data-qa="retry-switcher"]'); + expect(successfulAttempts.length).toBe(1); + + const successfulStatus = await browser.$('[data-qa="suite-status-bar-status"]'); + await expect(successfulStatus).toHaveText('Success'); + }); +}); diff --git a/test/func/tests/package.json b/test/func/tests/package.json index 53fc3e482..5fdb6a852 100644 --- a/test/func/tests/package.json +++ b/test/func/tests/package.json @@ -11,6 +11,7 @@ "gui:testplane-tinder": "TOOL=testplane PROJECT_UNDER_TEST=testplane-tinder SERVER_PORT=8076 npx testplane --set common-tinder gui", "gui:analytics": "TOOL=testplane PROJECT_UNDER_TEST=analytics SERVER_PORT=8085 npx testplane --set analytics gui", "gui:db-migrations": "TOOL=testplane PROJECT_UNDER_TEST=db-migrations SERVER_PORT=8086 npx testplane --set db-migrations gui", + "gui:migrate-screens": "TOOL=testplane PROJECT_UNDER_TEST=migrate-screens SERVER_PORT=8087 npx testplane --set migrate-screens gui", "testplane:testplane-common": "TOOL=testplane PROJECT_UNDER_TEST=testplane SERVER_PORT=8061 npx testplane --set common", "testplane:testplane-eye": "TOOL=testplane PROJECT_UNDER_TEST=testplane-eye SERVER_PORT=8062 npx testplane --set eye", "testplane:testplane-gui": "TOOL=testplane PROJECT_UNDER_TEST=testplane-gui SERVER_PORT=8063 npx testplane --no --set common-gui", @@ -19,6 +20,7 @@ "testplane:testplane-tinder": "TOOL=testplane PROJECT_UNDER_TEST=testplane-tinder SERVER_PORT=8086 npx testplane --set common-tinder", "testplane:analytics": "TOOL=testplane PROJECT_UNDER_TEST=analytics SERVER_PORT=8085 npx testplane --set analytics", "testplane:db-migrations": "TOOL=testplane PROJECT_UNDER_TEST=db-migrations SERVER_PORT=8086 npx testplane --set db-migrations", + "testplane:migrate-screens": "TOOL=testplane PROJECT_UNDER_TEST=migrate-screens SERVER_PORT=8087 npx testplane --set migrate-screens", "test": "run-s testplane:*" }, "devDependencies": { From cb89e6443b53c6ba6e19f7b0af825dd980b0856e Mon Sep 17 00:00:00 2001 From: shadowusr Date: Thu, 29 Jan 2026 01:06:42 +0300 Subject: [PATCH 5/5] fix: fix cli ui results alignment --- lib/cli/commands/migrate-screens/cli-ui.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cli/commands/migrate-screens/cli-ui.ts b/lib/cli/commands/migrate-screens/cli-ui.ts index 5f3f8c369..7d314984e 100644 --- a/lib/cli/commands/migrate-screens/cli-ui.ts +++ b/lib/cli/commands/migrate-screens/cli-ui.ts @@ -204,8 +204,8 @@ export const createCliUi = ( stdout.write(` ───────────────────────────────────────────────────────────────────────\n\n`); stdout.write(` Items ${summary.processed} processed, ${summary.autoAccepted} auto-accepted\n`); stdout.write(` Total ${elapsedSec.toFixed(1)}s\n`); - stdout.write(` ├─ Download ${downloadSec.toFixed(1)}s ${renderBar(downloadSec, elapsedSec, barWidth)}\n`); - stdout.write(` └─ Processing ${compareSec.toFixed(1)}s ${renderBar(compareSec, elapsedSec, barWidth)}\n\n`); + stdout.write(` ├─ Download ${downloadSec.toFixed(1)}s\t${renderBar(downloadSec, elapsedSec, barWidth)}\n`); + stdout.write(` └─ Processing ${compareSec.toFixed(1)}s\t${renderBar(compareSec, elapsedSec, barWidth)}\n\n`); if (summary.warningMessage) { stdout.write(` ${chalk.yellow(summary.warningMessage)}\n\n`); }