From 4ac44edacc1e6e8fe1c5ca8eb8049ddeed31b78e Mon Sep 17 00:00:00 2001 From: Roman Kuznetsov Date: Wed, 22 Apr 2026 19:54:20 +0300 Subject: [PATCH] feat(selectivity): consider png reference deps --- src/browser/cdp/selectivity/hash-reader.ts | 11 +- src/browser/cdp/selectivity/hash-writer.ts | 7 +- src/browser/cdp/selectivity/index.ts | 10 +- .../selectivity/merge-dumps/merge-tests.ts | 1 + .../selectivity/test-dependencies-reader.ts | 2 +- .../selectivity/test-dependencies-writer.ts | 2 +- .../cdp/selectivity/testplane-selectivity.ts | 29 ++- src/browser/cdp/selectivity/types.ts | 2 + src/browser/cdp/selectivity/utils.ts | 61 +++++- src/browser/commands/assert-view/index.js | 9 +- .../browser/cdp/selectivity/hash-reader.ts | 68 +++++++ .../browser/cdp/selectivity/hash-writer.ts | 51 ++++- test/src/browser/cdp/selectivity/index.ts | 10 +- .../selectivity/merge-dumps/merge-tests.ts | 20 +- .../selectivity/test-dependencies-reader.ts | 24 ++- .../selectivity/test-dependencies-writer.ts | 4 + test/src/browser/cdp/selectivity/utils.ts | 177 ++++++++++++++---- 17 files changed, 407 insertions(+), 81 deletions(-) diff --git a/src/browser/cdp/selectivity/hash-reader.ts b/src/browser/cdp/selectivity/hash-reader.ts index c61f57261..93ddb2239 100644 --- a/src/browser/cdp/selectivity/hash-reader.ts +++ b/src/browser/cdp/selectivity/hash-reader.ts @@ -35,19 +35,24 @@ export class HashReader { /** @returns changed deps or null, if nothing changed */ async getTestChangedDeps(testDeps: NormalizedDependencies): Promise { - const depFileTypes: Array = ["css", "js", "modules"] as const; + const depFileTypes: Array = ["css", "js", "modules", "png"] as const; const fileContents = await this._getHashFileContents(); let result: NormalizedDependencies | null = null; const checkForDepFileType = async (depFileType: keyof NormalizedDependencies): Promise => { + // Old selectivity dependency files did not have "png" property + if (!testDeps[depFileType]) { + return; + } + for (const filePath of testDeps[depFileType]) { const isChanged = this._fileStateCache.get(filePath); if (isChanged === false) { continue; } else if (isChanged === true) { - result ||= { css: [], js: [], modules: [] }; + result ||= { css: [], js: [], modules: [], png: [] }; result[depFileType].push(filePath); continue; } @@ -65,7 +70,7 @@ export class HashReader { } if (cachedFileHash !== calculatedFileHash) { - result ||= { css: [], js: [], modules: [] }; + result ||= { css: [], js: [], modules: [], png: [] }; result[depFileType].push(filePath); this._fileStateCache.set(filePath, true); } else { diff --git a/src/browser/cdp/selectivity/hash-writer.ts b/src/browser/cdp/selectivity/hash-writer.ts index 88548f6d8..35cbee96a 100644 --- a/src/browser/cdp/selectivity/hash-writer.ts +++ b/src/browser/cdp/selectivity/hash-writer.ts @@ -55,9 +55,10 @@ export class HashWriter { } addTestDependencyHashes(dependencies: NormalizedDependencies): void { - dependencies.css.forEach(dependency => this._addFileDependency(dependency)); - dependencies.js.forEach(dependency => this._addFileDependency(dependency)); - dependencies.modules.forEach(dependency => this._addModuleDependency(dependency)); + dependencies.css?.forEach(dependency => this._addFileDependency(dependency)); + dependencies.js?.forEach(dependency => this._addFileDependency(dependency)); + dependencies.png?.forEach(dependency => this._addFileDependency(dependency)); + dependencies.modules?.forEach(dependency => this._addModuleDependency(dependency)); } async save(readExisting?: boolean): Promise { diff --git a/src/browser/cdp/selectivity/index.ts b/src/browser/cdp/selectivity/index.ts index e34670b16..a3d277cfe 100644 --- a/src/browser/cdp/selectivity/index.ts +++ b/src/browser/cdp/selectivity/index.ts @@ -8,7 +8,7 @@ import type { Test, TestDepsContext, TestDepsData } from "../../../types"; import { getSelectivityTestsPath, mergeSourceDependencies, transformSourceDependencies } from "./utils"; import { getHashWriter } from "./hash-writer"; import { Compression } from "./types"; -import { getCollectedTestplaneDependencies } from "./testplane-selectivity"; +import { getCollectedTestplaneJsDependencies, getCollectedTestplanePngDependencies } from "./testplane-selectivity"; import { getHashReader } from "./hash-reader"; import type { Config } from "../../../config"; import { MasterEvents } from "../../../events"; @@ -257,10 +257,12 @@ export const startSelectivity = async (browser: ExistingBrowser): Promise { const testDeps = await readTestDependencies(this._selectivityTestsPath, test, this._compression); - let result: NormalizedDependencies = { css: [], js: [], modules: [] }; + let result: NormalizedDependencies = { css: [], js: [], modules: [], png: [] }; for (const browserId of Object.keys(testDeps)) { const depTypes = Object.keys(testDeps[browserId]); diff --git a/src/browser/cdp/selectivity/test-dependencies-writer.ts b/src/browser/cdp/selectivity/test-dependencies-writer.ts index 51a2b5547..9a2ae0960 100644 --- a/src/browser/cdp/selectivity/test-dependencies-writer.ts +++ b/src/browser/cdp/selectivity/test-dependencies-writer.ts @@ -7,7 +7,7 @@ import type { NormalizedDependencies, SelectivityCompressionType } from "./types import { writeJsonWithCompression } from "./json-utils"; const areDepsSame = (browserDepsA?: NormalizedDependencies, browserDepsB?: NormalizedDependencies): boolean => { - const props: Array = ["js", "css", "modules"] as const; + const props: Array = ["js", "css", "modules", "png"] as const; if (!browserDepsA || !browserDepsB) { return false; diff --git a/src/browser/cdp/selectivity/testplane-selectivity.ts b/src/browser/cdp/selectivity/testplane-selectivity.ts index c5ed20b39..c3f380373 100644 --- a/src/browser/cdp/selectivity/testplane-selectivity.ts +++ b/src/browser/cdp/selectivity/testplane-selectivity.ts @@ -6,7 +6,10 @@ import { debugSelectivity } from "./debug"; // eslint-disable-next-line @typescript-eslint/no-explicit-any const TypedModule = UntypedModule as unknown as { _resolveFilename: (...args: any) => string | void }; -const testDependenciesStorage = new AsyncLocalStorage<{ jsTestplaneDeps?: Set }>(); +const testDependenciesStorage = new AsyncLocalStorage<{ + jsTestplaneDeps?: Set; + pngTestplaneDeps?: Set; +}>(); const testFileDependenciesRamCache = new Map(); const testFileLocks: Record> = {}; @@ -49,24 +52,42 @@ export const enableCollectingTestplaneDependencies = (): void => { }; }; -export const getCollectedTestplaneDependencies = (): Set | null => { +export const getCollectedTestplaneJsDependencies = (): Set | null => { const store = testDependenciesStorage.getStore(); return store && store.jsTestplaneDeps ? store.jsTestplaneDeps : null; }; +export const getCollectedTestplanePngDependencies = (): Set | null => { + const store = testDependenciesStorage.getStore(); + + return store && store.pngTestplaneDeps ? store.pngTestplaneDeps : null; +}; + export const runWithTestplaneDependenciesCollecting = (fn: () => Promise): Promise => { enableCollectingTestplaneDependencies(); - const store: { jsTestplaneDeps?: Set } = { jsTestplaneDeps: new Set() }; + const store: { + jsTestplaneDeps?: Set; + pngTestplaneDeps?: Set; + } = { jsTestplaneDeps: new Set(), pngTestplaneDeps: new Set() }; return testDependenciesStorage.run(store, fn).finally(() => { // After "fn" completion, "store" is reachable in CDP ping interval callback, so it never GC-removed - // Thats why we do it manually. Removing "jsTestplaneDeps" is enough, and set remains unchanged, if used + // Thats why we do it manually. It is enough, and set remains unchanged, if used delete store.jsTestplaneDeps; + delete store.pngTestplaneDeps; }); }; +export const addTestplaneSelectivityPngDependency = (pngPath: string): void => { + const store = testDependenciesStorage.getStore(); + + if (store && store.pngTestplaneDeps) { + store.pngTestplaneDeps.add(pngPath); + } +}; + export const readTestFileWithTestplaneDependenciesCollecting = async ( file: string, fn: () => Promise, diff --git a/src/browser/cdp/selectivity/types.ts b/src/browser/cdp/selectivity/types.ts index 6d347dd73..6c25c82c7 100644 --- a/src/browser/cdp/selectivity/types.ts +++ b/src/browser/cdp/selectivity/types.ts @@ -5,6 +5,8 @@ export interface NormalizedDependencies { js: string[]; /** Module names from node_modules (e.g. "react", "@remix-run/router") */ modules: string[]; + /** Reference paths */ + png: string[]; } export const Compression = { diff --git a/src/browser/cdp/selectivity/utils.ts b/src/browser/cdp/selectivity/utils.ts index 8f77b6e19..99ee2491a 100644 --- a/src/browser/cdp/selectivity/utils.ts +++ b/src/browser/cdp/selectivity/utils.ts @@ -213,19 +213,24 @@ const warnUnsupportedProtocol = memoize((protocol: string, dependency: string): }); /** - * @param cssDependencies set of css dependenciy URI's - * @param jsDependencies set of js dependenciy URI's + * @param dependencies.css set of css dependency URI's + * @param dependencies.js set of js dependency URI's + * @param dependencies.png set of png dependency URI's * @returns sorted uniq arrays of relative paths */ export const transformSourceDependencies = ( - cssDependencies: Set | null, - jsDependencies: Set | null, + { + css: cssDependencies, + js: jsDependencies, + png: pngDependencies, + }: { css: Set | null; js: Set | null; png: Set | null }, mapDependencyPathFn?: null | ((relativePath: string) => string | void), ): NormalizedDependencies => { const nodeModulesLabel = "node_modules/"; const cssSet: Set = new Set(); const jsSet: Set = new Set(); const modulesSet: Set = new Set(); + const pngSet: Set = new Set(); const classifyDependency = (dependency: string, typedResultSet: Set): void => { dependency = decodeURIComponent(softFileURLToPath(dependency)); @@ -291,12 +296,19 @@ export const transformSourceDependencies = ( } } + if (pngDependencies) { + for (const pngDependency of pngDependencies.values()) { + classifyDependency(pngDependency, pngSet); + } + } + const cmpStr = (a: string, b: string): number => a.localeCompare(b); return { css: Array.from(cssSet).sort(cmpStr), js: Array.from(jsSet).sort(cmpStr), modules: Array.from(modulesSet).sort(cmpStr), + png: Array.from(pngSet).sort(cmpStr), }; }; @@ -305,12 +317,26 @@ export const mergeSourceDependencies = ( a: NormalizedDependencies, b: NormalizedDependencies, ): NormalizedDependencies => { - const result: NormalizedDependencies = { css: [], js: [], modules: [] }; + const result: NormalizedDependencies = { css: [], js: [], modules: [], png: [] }; for (const depType of Object.keys(result) as Array) { let aInd = 0, bInd = 0; + if (!a[depType]) { + if (!b[depType]) { + continue; + } + + result[depType] = b[depType]; + + continue; + } else if (!b[depType]) { + result[depType] = a[depType]; + + continue; + } + while (aInd < a[depType].length || bInd < b[depType].length) { let compareResult; @@ -392,13 +418,30 @@ export const getTestDependenciesPath = (selectivityTestsPath: string, test: Test path.join(selectivityTestsPath, `${getTestSelectivityDumpId(test)}.json`); /** @returns `Promise>>` */ -export const readTestDependencies = ( +export const readTestDependencies = async ( selectivityTestsPath: string, test: Test, compression: SelectivityCompressionType, -): Promise => - readJsonWithCompression(getTestDependenciesPath(selectivityTestsPath, test), compression, { +): Promise => { + const result = (await readJsonWithCompression(getTestDependenciesPath(selectivityTestsPath, test), compression, { defaultValue: {}, - }).catch(() => ({})); + }).catch(() => ({}))) as Record< + string, + Record & Pick> + >; + + for (const browserId in result) { + for (const depType in result[browserId]) { + const currentDeps = result[browserId][depType]; + + currentDeps.css ||= []; + currentDeps.js ||= []; + currentDeps.modules ||= []; + currentDeps.png ||= []; + } + } + + return result; +}; export const isCachedOnFs = (value: unknown): value is CachedOnFs => value === true; diff --git a/src/browser/commands/assert-view/index.js b/src/browser/commands/assert-view/index.js index 34fd62bc4..c1620e6f0 100644 --- a/src/browser/commands/assert-view/index.js +++ b/src/browser/commands/assert-view/index.js @@ -10,6 +10,7 @@ const { getCaptureProcessors } = require("./capture-processors"); const RuntimeConfig = require("../../../config/runtime-config"); const AssertViewResults = require("./assert-view-results"); const { BaseStateError } = require("./errors/base-state-error"); +const { addTestplaneSelectivityPngDependency } = require("../../cdp/selectivity/testplane-selectivity"); const getIgnoreDiffPixelCountRatio = value => { const percent = _.isString(value) && value.endsWith("%") ? parseFloat(value.slice(0, -1)) : false; @@ -75,7 +76,7 @@ module.exports.default = browser => { disableAnimation: opts.disableAnimation, }); - const { tempOpts } = RuntimeConfig.getInstance(); + const { tempOpts, updateRefs: isUpdatingRefs } = RuntimeConfig.getInstance(); temp.attach(tempOpts); const screenshoterOpts = _.pick(opts, [ @@ -99,9 +100,15 @@ module.exports.default = browser => { if (!fs.existsSync(refImg.path)) { await currImgInst.save(currImg.path); + if (isUpdatingRefs) { + addTestplaneSelectivityPngDependency(refImg.path); + } + return handleNoRefImage(currImg, refImg, state, { emitter }).catch(e => handleCaptureProcessorError(e)); } + addTestplaneSelectivityPngDependency(refImg.path); + const { canHaveCaret, pixelRatio } = page; const imageCompareOpts = { tolerance: opts.tolerance, diff --git a/test/src/browser/cdp/selectivity/hash-reader.ts b/test/src/browser/cdp/selectivity/hash-reader.ts index 5162f4c09..89bd09d40 100644 --- a/test/src/browser/cdp/selectivity/hash-reader.ts +++ b/test/src/browser/cdp/selectivity/hash-reader.ts @@ -146,6 +146,7 @@ describe("CDP/Selectivity/HashReader", () => { css: ["src/styles.css"], js: ["src/app.js"], modules: ["node_modules/react"], + png: [], }; const hashFileContents = { files: { @@ -178,6 +179,7 @@ describe("CDP/Selectivity/HashReader", () => { css: ["src/styles.css", "src/theme.css"], js: ["src/app.js"], modules: ["node_modules/react"], + png: [], }; const hashFileContents = { files: { @@ -208,6 +210,7 @@ describe("CDP/Selectivity/HashReader", () => { css: ["src/styles.css"], js: ["src/app.js"], modules: [], + png: [], }); }); @@ -217,6 +220,7 @@ describe("CDP/Selectivity/HashReader", () => { css: ["src/styles.css"], js: [], modules: ["node_modules/react", "node_modules/lodash"], + png: [], }; const hashFileContents = { files: { @@ -244,6 +248,7 @@ describe("CDP/Selectivity/HashReader", () => { css: [], js: [], modules: ["node_modules/react"], + png: [], }); }); @@ -253,6 +258,7 @@ describe("CDP/Selectivity/HashReader", () => { css: ["src/new-file.css"], js: ["src/new-app.js"], modules: ["node_modules/new-lib"], + png: [], }; const hashFileContents = { files: {}, @@ -275,6 +281,7 @@ describe("CDP/Selectivity/HashReader", () => { css: ["src/new-file.css"], js: ["src/new-app.js"], modules: ["node_modules/new-lib"], + png: [], }); }); @@ -284,6 +291,7 @@ describe("CDP/Selectivity/HashReader", () => { css: [], js: [], modules: [], + png: [], }; const hashFileContents = { files: {}, @@ -305,6 +313,7 @@ describe("CDP/Selectivity/HashReader", () => { css: ["src/changed.css", "src/unchanged.css"], js: ["src/unchanged.js"], modules: ["node_modules/changed-lib", "node_modules/unchanged-lib"], + png: [], }; const hashFileContents = { files: { @@ -338,6 +347,7 @@ describe("CDP/Selectivity/HashReader", () => { css: ["src/changed.css"], js: [], modules: ["node_modules/changed-lib"], + png: [], }); }); @@ -347,6 +357,7 @@ describe("CDP/Selectivity/HashReader", () => { css: [], js: [], modules: ["node_modules/react"], + png: [], }; const hashFileContents = { files: {}, @@ -362,6 +373,63 @@ describe("CDP/Selectivity/HashReader", () => { assert.calledWith(pathStub.join, "node_modules/react", "package.json"); assert.calledWith(hashProviderMock.calculateForFile, "node_modules/react/package.json"); }); + + it("should return changed png dependencies", async () => { + const reader = new HashReader("/test/selectivity", "none"); + const testDeps = { + css: [], + js: [], + modules: [], + png: ["screenshots/ref.png"], + }; + const hashFileContents = { + files: { + "screenshots/ref.png": "old-png-hash", + }, + modules: {}, + patterns: {}, + }; + + readHashFileContentsStub.resolves(hashFileContents); + hashProviderMock.calculateForFile.withArgs("screenshots/ref.png").resolves("new-png-hash"); + + const result = await reader.getTestChangedDeps(testDeps); + + assert.deepEqual(result, { + css: [], + js: [], + modules: [], + png: ["screenshots/ref.png"], + }); + }); + + it("should skip missing dep type for old format without png property", async () => { + const reader = new HashReader("/test/selectivity", "none"); + const testDeps = { + css: ["src/styles.css"], + js: ["src/app.js"], + modules: [], + } as any; + const hashFileContents = { + files: { + "src/styles.css": "css-hash", + "src/app.js": "js-hash", + }, + modules: {}, + patterns: {}, + }; + + readHashFileContentsStub.resolves(hashFileContents); + hashProviderMock.calculateForFile + .withArgs("src/styles.css") + .resolves("css-hash") + .withArgs("src/app.js") + .resolves("js-hash"); + + const result = await reader.getTestChangedDeps(testDeps); + + assert.isNull(result); + }); }); describe("getHashReader", () => { diff --git a/test/src/browser/cdp/selectivity/hash-writer.ts b/test/src/browser/cdp/selectivity/hash-writer.ts index 1ca61c0d1..75480fdf7 100644 --- a/test/src/browser/cdp/selectivity/hash-writer.ts +++ b/test/src/browser/cdp/selectivity/hash-writer.ts @@ -47,6 +47,7 @@ describe("CDP/Selectivity/HashWriter", () => { css: ["src/styles.css", "src/theme.css"], js: ["src/app.js", "src/utils.js"], modules: ["node_modules/react", "node_modules/lodash"], + png: [], }; fileHashProviderMock.calculateForFile.returns(Promise.resolve("hash123")); @@ -61,12 +62,30 @@ describe("CDP/Selectivity/HashWriter", () => { assert.calledWith(fileHashProviderMock.calculateForFile, "node_modules/lodash/package.json"); }); + it("should add png dependencies as file dependencies", () => { + const writer = new HashWriter("/test/selectivity", "none"); + const dependencies = { + css: [], + js: [], + modules: [], + png: ["screenshots/ref1.png", "screenshots/ref2.png"], + }; + + fileHashProviderMock.calculateForFile.returns(Promise.resolve("hash123")); + + writer.addTestDependencyHashes(dependencies); + + assert.calledWith(fileHashProviderMock.calculateForFile, "screenshots/ref1.png"); + assert.calledWith(fileHashProviderMock.calculateForFile, "screenshots/ref2.png"); + }); + it("should not add duplicate dependencies", () => { const writer = new HashWriter("/test/selectivity", "none"); const dependencies = { css: ["src/styles.css"], js: ["src/app.js"], modules: ["node_modules/react"], + png: [], }; fileHashProviderMock.calculateForFile.returns(Promise.resolve("hash123")); @@ -85,6 +104,7 @@ describe("CDP/Selectivity/HashWriter", () => { css: [], js: [], modules: [], + png: [], }; fileHashProviderMock.calculateForFile.returns(Promise.resolve("hash123")); @@ -109,7 +129,7 @@ describe("CDP/Selectivity/HashWriter", () => { const writer = new HashWriter("/test/selectivity", "none"); readHashFileContentsStub.resolves(defaultValue); - writer.addTestDependencyHashes({ css: [], js: [], modules: [] }); + writer.addTestDependencyHashes({ css: [], js: [], modules: [], png: [] }); await writer.save(); @@ -123,6 +143,7 @@ describe("CDP/Selectivity/HashWriter", () => { css: ["src/styles.css"], js: ["src/app.js"], modules: ["node_modules/react"], + png: [], }; readHashFileContentsStub.resolves(defaultValue); @@ -156,6 +177,7 @@ describe("CDP/Selectivity/HashWriter", () => { css: ["src/styles.css"], js: [], modules: [], + png: [], }; const error = new Error("File not found"); @@ -167,12 +189,38 @@ describe("CDP/Selectivity/HashWriter", () => { await assert.isRejected(writer.save(), "File not found"); }); + it("should save png dependency hashes to files section", async () => { + const defaultValue = { files: {}, modules: {}, patterns: {} }; + const writer = new HashWriter("/test/selectivity", "none"); + const dependencies = { + css: [], + js: [], + modules: [], + png: ["screenshots/ref.png"], + }; + + readHashFileContentsStub.resolves(defaultValue); + fileHashProviderMock.calculateForFile.withArgs("screenshots/ref.png").resolves("png-hash"); + + writer.addTestDependencyHashes(dependencies); + await writer.save(); + + assert.calledWith(writeJsonWithCompression, "/test/selectivity/hashes.json", { + files: { + "screenshots/ref.png": "png-hash", + }, + modules: {}, + patterns: {}, + }); + }); + it("should not readHashFileContents when readExisting is false", async () => { const writer = new HashWriter("/test/selectivity", "none"); const dependencies = { css: ["src/styles.css"], js: [], modules: [], + png: [], }; fileHashProviderMock.calculateForFile.withArgs("src/styles.css").resolves("css-hash"); @@ -194,6 +242,7 @@ describe("CDP/Selectivity/HashWriter", () => { css: ["src/styles.css"], js: ["src/app.js"], modules: ["node_modules/react"], + png: [], }; readHashFileContentsStub.resolves(existingContents); diff --git a/test/src/browser/cdp/selectivity/index.ts b/test/src/browser/cdp/selectivity/index.ts index e5e23ba22..be29b78d4 100644 --- a/test/src/browser/cdp/selectivity/index.ts +++ b/test/src/browser/cdp/selectivity/index.ts @@ -352,7 +352,11 @@ describe("CDP/Selectivity", () => { assert.calledWith(cssSelectivityMock.stop, false); assert.calledWith(jsSelectivityMock.stop, false); - assert.calledWith(transformSourceDependenciesStub, new Set(["src/styles.css"]), new Set(["src/app.js"])); + assert.calledWith(transformSourceDependenciesStub, { + css: new Set(["src/styles.css"]), + js: new Set(["src/app.js"]), + png: null, + }); assert.calledWith(getTestDependenciesWriterStub, "/test/dependencies"); assert.calledWith(testDependenciesWriterMock.saveFor, mockTest, { css: ["src/styles.css"], @@ -403,7 +407,7 @@ describe("CDP/Selectivity", () => { await stopFn(mockTest, false); - assert.calledWith(transformSourceDependenciesStub, new Set(["src/styles.css"]), []); + assert.calledWith(transformSourceDependenciesStub, { css: new Set(["src/styles.css"]), js: [], png: null }); assert.calledOnce(testDependenciesWriterMock.saveFor); }); @@ -412,7 +416,7 @@ describe("CDP/Selectivity", () => { await stopFn(mockTest, false); - assert.calledWith(transformSourceDependenciesStub, null, new Set(["src/app.js"])); + assert.calledWith(transformSourceDependenciesStub, { css: null, js: new Set(["src/app.js"]), png: null }); assert.calledOnce(testDependenciesWriterMock.saveFor); }); }); diff --git a/test/src/browser/cdp/selectivity/merge-dumps/merge-tests.ts b/test/src/browser/cdp/selectivity/merge-dumps/merge-tests.ts index 7618b8467..a754ed4e5 100644 --- a/test/src/browser/cdp/selectivity/merge-dumps/merge-tests.ts +++ b/test/src/browser/cdp/selectivity/merge-dumps/merge-tests.ts @@ -100,10 +100,10 @@ describe("CDP/Selectivity/MergeDumps/MergeTests", () => { .resolves(["test1.json.gz"]); const content1: TestDependenciesFileContents = { - chrome: { browser: { css: ["a.css"], js: ["a.js"], modules: ["react"] } }, + chrome: { browser: { css: ["a.css"], js: ["a.js"], modules: ["react"], png: [] } }, }; const content2: TestDependenciesFileContents = { - firefox: { browser: { css: ["b.css"], js: ["b.js"], modules: ["vue"] } }, + firefox: { browser: { css: ["b.css"], js: ["b.js"], modules: ["vue"], png: [] } }, }; readJsonWithCompressionStub.onFirstCall().resolves(content1).onSecondCall().resolves(content2); @@ -123,10 +123,10 @@ describe("CDP/Selectivity/MergeDumps/MergeTests", () => { .resolves(["test1.json"]); const content1: TestDependenciesFileContents = { - chrome: { browser: { css: ["a.css"], js: [], modules: [] } }, + chrome: { browser: { css: ["a.css"], js: [], modules: [], png: [] } }, }; const content2: TestDependenciesFileContents = { - firefox: { browser: { css: ["b.css"], js: [], modules: [] } }, + firefox: { browser: { css: ["b.css"], js: [], modules: [], png: [] } }, }; readJsonWithCompressionStub.onFirstCall().resolves(content1).onSecondCall().resolves(content2); @@ -146,10 +146,10 @@ describe("CDP/Selectivity/MergeDumps/MergeTests", () => { .resolves(["test1.json"]); const content1: TestDependenciesFileContents = { - chrome: { browser: { css: ["a.css"], js: ["a.js"], modules: [] } }, + chrome: { browser: { css: ["a.css"], js: ["a.js"], modules: [], png: [] } }, }; const content2: TestDependenciesFileContents = { - chrome: { testplane: { css: ["b.css"], js: ["b.js"], modules: [] } }, + chrome: { testplane: { css: ["b.css"], js: ["b.js"], modules: [], png: [] } }, }; readJsonWithCompressionStub.onFirstCall().resolves(content1).onSecondCall().resolves(content2); @@ -170,10 +170,10 @@ describe("CDP/Selectivity/MergeDumps/MergeTests", () => { .resolves(["test1.json"]); const content1: TestDependenciesFileContents = { - chrome: { browser: { css: ["common.css", "a.css"], js: ["common.js"], modules: ["react"] } }, + chrome: { browser: { css: ["common.css", "a.css"], js: ["common.js"], modules: ["react"], png: [] } }, }; const content2: TestDependenciesFileContents = { - chrome: { browser: { css: ["common.css", "b.css"], js: ["common.js"], modules: ["react"] } }, + chrome: { browser: { css: ["common.css", "b.css"], js: ["common.js"], modules: ["react"], png: [] } }, }; readJsonWithCompressionStub.onFirstCall().resolves(content1).onSecondCall().resolves(content2); @@ -194,10 +194,10 @@ describe("CDP/Selectivity/MergeDumps/MergeTests", () => { .resolves(["test1.json"]); const content1: TestDependenciesFileContents = { - chrome: { browser: { css: ["z.css"], js: ["z.js"], modules: ["vue"] } }, + chrome: { browser: { css: ["z.css"], js: ["z.js"], modules: ["vue"], png: [] } }, }; const content2: TestDependenciesFileContents = { - chrome: { browser: { css: ["a.css"], js: ["a.js"], modules: ["axios"] } }, + chrome: { browser: { css: ["a.css"], js: ["a.js"], modules: ["axios"], png: [] } }, }; readJsonWithCompressionStub.onFirstCall().resolves(content1).onSecondCall().resolves(content2); diff --git a/test/src/browser/cdp/selectivity/test-dependencies-reader.ts b/test/src/browser/cdp/selectivity/test-dependencies-reader.ts index afedf6e33..f9880d5ae 100644 --- a/test/src/browser/cdp/selectivity/test-dependencies-reader.ts +++ b/test/src/browser/cdp/selectivity/test-dependencies-reader.ts @@ -50,7 +50,7 @@ describe("CDP/Selectivity/TestDependenciesReader", () => { const result = await reader.getFor(mockTest); - assert.deepEqual(result, { css: [], js: [], modules: [] }); + assert.deepEqual(result, { css: [], js: [], modules: [], png: [] }); assert.calledWith(readTestDependenciesStub, "/test/selectivity/tests", mockTest, "none"); assert.notCalled(mergeSourceDependenciesStub); }); @@ -70,7 +70,11 @@ describe("CDP/Selectivity/TestDependenciesReader", () => { const result = await reader.getFor(mockTest); assert.equal(result, expectedResult); - assert.calledWith(mergeSourceDependenciesStub, { css: [], js: [], modules: [] }, testDeps.chrome.browser); + assert.calledWith( + mergeSourceDependenciesStub, + { css: [], js: [], modules: [], png: [] }, + testDeps.chrome.browser, + ); }); it("should merge dependencies for single browser with multiple dependency types", async () => { @@ -106,7 +110,7 @@ describe("CDP/Selectivity/TestDependenciesReader", () => { ); assert.calledWith( mergeSourceDependenciesStub.secondCall, - { css: [], js: [], modules: [] }, + { css: [], js: [], modules: [], png: [] }, mergedBrowserDeps, ); }); @@ -137,7 +141,7 @@ describe("CDP/Selectivity/TestDependenciesReader", () => { assert.calledTwice(mergeSourceDependenciesStub); assert.calledWith( mergeSourceDependenciesStub.firstCall, - { css: [], js: [], modules: [] }, + { css: [], js: [], modules: [], png: [] }, testDeps.chrome.browser, ); assert.calledWith(mergeSourceDependenciesStub.secondCall, chromeDeps, testDeps.firefox.browser); @@ -202,7 +206,11 @@ describe("CDP/Selectivity/TestDependenciesReader", () => { assert.equal(result, firefoxDeps); assert.calledOnce(mergeSourceDependenciesStub); - assert.calledWith(mergeSourceDependenciesStub, { css: [], js: [], modules: [] }, testDeps.firefox.browser); + assert.calledWith( + mergeSourceDependenciesStub, + { css: [], js: [], modules: [], png: [] }, + testDeps.firefox.browser, + ); }); it("should handle different compression types", async () => { @@ -236,7 +244,11 @@ describe("CDP/Selectivity/TestDependenciesReader", () => { const result = await reader.getFor(mockTest); assert.equal(result, expectedResult); - assert.calledWith(mergeSourceDependenciesStub, { css: [], js: [], modules: [] }, testDeps.chrome.browser); + assert.calledWith( + mergeSourceDependenciesStub, + { css: [], js: [], modules: [], png: [] }, + testDeps.chrome.browser, + ); }); it("should handle mixed empty and non-empty browsers", async () => { diff --git a/test/src/browser/cdp/selectivity/test-dependencies-writer.ts b/test/src/browser/cdp/selectivity/test-dependencies-writer.ts index 6b58f7570..810ab26a5 100644 --- a/test/src/browser/cdp/selectivity/test-dependencies-writer.ts +++ b/test/src/browser/cdp/selectivity/test-dependencies-writer.ts @@ -50,11 +50,13 @@ describe("CDP/Selectivity/TestDependenciesWriter", () => { css: ["src/styles.css"], js: ["src/app.js"], modules: ["node_modules/react"], + png: [], }; const mockEmptyDependencies = { css: [], js: [], modules: [], + png: [], }; it("should create directory on first save", async () => { @@ -151,11 +153,13 @@ describe("CDP/Selectivity/TestDependenciesWriter", () => { css: ["a.css", "b.css"], js: ["a.js", "b.js"], modules: ["react", "lodash"], + png: [], }; const deps2 = { css: ["a.css", "b.css"], js: ["a.js", "b.js"], modules: ["react", "lodash"], + png: [], }; const writer = new TestDependenciesWriter("/test/selectivity", "none"); diff --git a/test/src/browser/cdp/selectivity/utils.ts b/test/src/browser/cdp/selectivity/utils.ts index d3a1827b3..4a764205d 100644 --- a/test/src/browser/cdp/selectivity/utils.ts +++ b/test/src/browser/cdp/selectivity/utils.ts @@ -1,6 +1,7 @@ import sinon, { SinonStub, type SinonStubbedInstance } from "sinon"; import proxyquire from "proxyquire"; import type { CDPRuntime } from "src/browser/cdp/domains/runtime"; +import { NormalizedDependencies, TestDependenciesFileContents } from "src/browser/cdp/selectivity/types"; describe("CDP/Selectivity/Utils", () => { const sandbox = sinon.createSandbox(); @@ -489,7 +490,7 @@ describe("CDP/Selectivity/Utils", () => { fsStub.existsSync.returns(true); - const result = utils.transformSourceDependencies(cssDeps, jsDeps); + const result = utils.transformSourceDependencies({ css: cssDeps, js: jsDeps, png: null }); assert.deepEqual(result.css, ["src/styles.css"]); assert.deepEqual(result.js, ["src/app.js"]); @@ -502,7 +503,7 @@ describe("CDP/Selectivity/Utils", () => { fsStub.existsSync.returns(true); - const result = utils.transformSourceDependencies(cssDeps, jsDeps); + const result = utils.transformSourceDependencies({ css: cssDeps, js: jsDeps, png: null }); assert.deepEqual(result.modules, ["node_modules/@scope/package"]); }); @@ -514,7 +515,7 @@ describe("CDP/Selectivity/Utils", () => { fsStub.existsSync.returns(false); assert.throws(() => { - utils.transformSourceDependencies(cssDeps, jsDeps); + utils.transformSourceDependencies({ css: cssDeps, js: jsDeps, png: null }); }, /Selectivity: Couldn't find/); }); @@ -526,7 +527,7 @@ describe("CDP/Selectivity/Utils", () => { pathStub.posix.relative.returns("src/file with spaces.css"); fsStub.existsSync.returns(true); - const result = utils.transformSourceDependencies(cssDeps, jsDeps); + const result = utils.transformSourceDependencies({ css: cssDeps, js: jsDeps, png: null }); assert.calledWith(softFileURLToPathStub, "src/file%20with%20spaces.css"); assert.deepEqual(result.css, ["src/file with spaces.css"]); @@ -548,43 +549,55 @@ describe("CDP/Selectivity/Utils", () => { fsStub.existsSync.returns(true); - const result = utils.transformSourceDependencies(cssDeps, jsDeps, mapFn); + const result = utils.transformSourceDependencies({ css: cssDeps, js: jsDeps, png: null }, mapFn); assert.deepEqual(result.js, ["../bar"]); }); + + it("should classify png dependencies", () => { + const pngDeps = new Set(["screenshots/ref1.png", "screenshots/ref2.png"]); + + fsStub.existsSync.returns(true); + + const result = utils.transformSourceDependencies({ css: null, js: null, png: pngDeps }); + + assert.deepEqual(result.png, ["screenshots/ref1.png", "screenshots/ref2.png"]); + assert.deepEqual(result.css, []); + assert.deepEqual(result.js, []); + }); }); describe("mergeSourceDependencies", () => { it("should merge two empty dependency objects", () => { - const a = { css: [], js: [], modules: [] }; - const b = { css: [], js: [], modules: [] }; + const a = { css: [], js: [], modules: [], png: [] }; + const b = { css: [], js: [], modules: [], png: [] }; const result = utils.mergeSourceDependencies(a, b); - assert.deepEqual(result, { css: [], js: [], modules: [] }); + assert.deepEqual(result, { css: [], js: [], modules: [], png: [] }); }); it("should merge when first object is empty", () => { - const a = { css: [], js: [], modules: [] }; - const b = { css: ["style.css"], js: ["app.js"], modules: ["react"] }; + const a = { css: [], js: [], modules: [], png: [] }; + const b = { css: ["style.css"], js: ["app.js"], modules: ["react"], png: [] }; const result = utils.mergeSourceDependencies(a, b); - assert.deepEqual(result, { css: ["style.css"], js: ["app.js"], modules: ["react"] }); + assert.deepEqual(result, { css: ["style.css"], js: ["app.js"], modules: ["react"], png: [] }); }); it("should merge when second object is empty", () => { - const a = { css: ["style.css"], js: ["app.js"], modules: ["react"] }; - const b = { css: [], js: [], modules: [] }; + const a = { css: ["style.css"], js: ["app.js"], modules: ["react"], png: [] }; + const b = { css: [], js: [], modules: [], png: [] }; const result = utils.mergeSourceDependencies(a, b); - assert.deepEqual(result, { css: ["style.css"], js: ["app.js"], modules: ["react"] }); + assert.deepEqual(result, { css: ["style.css"], js: ["app.js"], modules: ["react"], png: [] }); }); it("should merge sorted arrays maintaining order", () => { - const a = { css: ["a.css", "c.css"], js: ["a.js", "c.js"], modules: ["lodash", "react"] }; - const b = { css: ["b.css", "d.css"], js: ["b.js", "d.js"], modules: ["axios", "vue"] }; + const a = { css: ["a.css", "c.css"], js: ["a.js", "c.js"], modules: ["lodash", "react"], png: [] }; + const b = { css: ["b.css", "d.css"], js: ["b.js", "d.js"], modules: ["axios", "vue"], png: [] }; const result = utils.mergeSourceDependencies(a, b); @@ -592,6 +605,7 @@ describe("CDP/Selectivity/Utils", () => { css: ["a.css", "b.css", "c.css", "d.css"], js: ["a.js", "b.js", "c.js", "d.js"], modules: ["axios", "lodash", "react", "vue"], + png: [], }); }); @@ -600,11 +614,13 @@ describe("CDP/Selectivity/Utils", () => { css: ["common.css", "unique-a.css"], js: ["common.js", "unique-a.js"], modules: ["react", "unique-a"], + png: [], }; const b = { css: ["common.css", "unique-b.css"], js: ["common.js", "unique-b.js"], modules: ["react", "unique-b"], + png: [], }; const result = utils.mergeSourceDependencies(a, b); @@ -613,12 +629,13 @@ describe("CDP/Selectivity/Utils", () => { css: ["common.css", "unique-a.css", "unique-b.css"], js: ["common.js", "unique-a.js", "unique-b.js"], modules: ["react", "unique-a", "unique-b"], + png: [], }); }); it("should handle arrays with consecutive duplicates", () => { - const a = { css: ["a.css", "a.css", "b.css"], js: ["a.js", "a.js"], modules: ["react", "react"] }; - const b = { css: ["a.css", "c.css", "c.css"], js: ["b.js", "b.js"], modules: ["vue", "vue"] }; + const a = { css: ["a.css", "a.css", "b.css"], js: ["a.js", "a.js"], modules: ["react", "react"], png: [] }; + const b = { css: ["a.css", "c.css", "c.css"], js: ["b.js", "b.js"], modules: ["vue", "vue"], png: [] }; const result = utils.mergeSourceDependencies(a, b); @@ -626,12 +643,13 @@ describe("CDP/Selectivity/Utils", () => { css: ["a.css", "b.css", "c.css"], js: ["a.js", "b.js"], modules: ["react", "vue"], + png: [], }); }); it("should handle mixed case sorting correctly", () => { - const a = { css: ["A.css", "c.css"], js: ["A.js", "c.js"], modules: ["React", "lodash"] }; - const b = { css: ["B.css", "a.css"], js: ["B.js", "a.js"], modules: ["axios", "Vue"] }; + const a = { css: ["A.css", "c.css"], js: ["A.js", "c.js"], modules: ["React", "lodash"], png: [] }; + const b = { css: ["B.css", "a.css"], js: ["B.js", "a.js"], modules: ["axios", "Vue"], png: [] }; const result = utils.mergeSourceDependencies(a, b); @@ -639,12 +657,13 @@ describe("CDP/Selectivity/Utils", () => { css: ["A.css", "B.css", "a.css", "c.css"], js: ["A.js", "B.js", "a.js", "c.js"], modules: ["axios", "React", "lodash", "Vue"], + png: [], }); }); it("should handle arrays of different lengths", () => { - const a = { css: ["a.css"], js: ["a.js", "b.js", "c.js", "d.js"], modules: ["react"] }; - const b = { css: ["b.css", "c.css", "d.css"], js: ["e.js"], modules: ["axios", "lodash", "vue"] }; + const a = { css: ["a.css"], js: ["a.js", "b.js", "c.js", "d.js"], modules: ["react"], png: [] }; + const b = { css: ["b.css", "c.css", "d.css"], js: ["e.js"], modules: ["axios", "lodash", "vue"], png: [] }; const result = utils.mergeSourceDependencies(a, b); @@ -652,16 +671,17 @@ describe("CDP/Selectivity/Utils", () => { css: ["a.css", "b.css", "c.css", "d.css"], js: ["a.js", "b.js", "c.js", "d.js", "e.js"], modules: ["axios", "lodash", "react", "vue"], + png: [], }); }); it("should handle identical arrays", () => { - const a = { css: ["style.css"], js: ["app.js"], modules: ["react"] }; - const b = { css: ["style.css"], js: ["app.js"], modules: ["react"] }; + const a = { css: ["style.css"], js: ["app.js"], modules: ["react"], png: [] }; + const b = { css: ["style.css"], js: ["app.js"], modules: ["react"], png: [] }; const result = utils.mergeSourceDependencies(a, b); - assert.deepEqual(result, { css: ["style.css"], js: ["app.js"], modules: ["react"] }); + assert.deepEqual(result, { css: ["style.css"], js: ["app.js"], modules: ["react"], png: [] }); }); it("should handle complex real-world scenario", () => { @@ -669,11 +689,13 @@ describe("CDP/Selectivity/Utils", () => { css: ["src/components/button.css", "src/styles/main.css"], js: ["src/components/button.js", "src/utils/helpers.js"], modules: ["@babel/core", "react", "webpack"], + png: [], }; const b = { css: ["src/components/modal.css", "src/styles/main.css"], js: ["src/components/modal.js", "src/utils/helpers.js"], modules: ["lodash", "react", "vue"], + png: [], }; const result = utils.mergeSourceDependencies(a, b); @@ -682,12 +704,22 @@ describe("CDP/Selectivity/Utils", () => { css: ["src/components/button.css", "src/components/modal.css", "src/styles/main.css"], js: ["src/components/button.js", "src/components/modal.js", "src/utils/helpers.js"], modules: ["@babel/core", "lodash", "react", "vue", "webpack"], + png: [], }); }); + it("should merge non-empty png arrays", () => { + const a = { css: [], js: [], modules: [], png: ["a.png", "c.png"] }; + const b = { css: [], js: [], modules: [], png: ["b.png", "d.png"] }; + + const result = utils.mergeSourceDependencies(a, b); + + assert.deepEqual(result.png, ["a.png", "b.png", "c.png", "d.png"]); + }); + it("should preserve original objects without mutation", () => { - const a = { css: ["a.css"], js: ["a.js"], modules: ["react"] }; - const b = { css: ["b.css"], js: ["b.js"], modules: ["vue"] }; + const a = { css: ["a.css"], js: ["a.js"], modules: ["react"], png: [] }; + const b = { css: ["b.css"], js: ["b.js"], modules: ["vue"], png: [] }; const originalA = JSON.parse(JSON.stringify(a)); const originalB = JSON.parse(JSON.stringify(b)); @@ -861,10 +893,14 @@ describe("CDP/Selectivity/Utils", () => { clone: sandbox.stub(), assign: sandbox.stub(), } as any; - const mockDependencies = { + const mockDependencies: TestDependenciesFileContents = { chrome: { - css: { css: ["src/styles.css"], js: [], modules: [] }, - js: { css: [], js: ["src/app.js"], modules: ["react"] }, + browser: { + css: ["src/styles.css"], + js: ["src/app.js"], + modules: ["react"], + png: [], + }, }, }; readJsonWithCompressionStub.resolves(mockDependencies); @@ -906,7 +942,12 @@ describe("CDP/Selectivity/Utils", () => { } as any; const mockDependencies = { firefox: { - css: { css: ["src/theme.css"], js: [], modules: [] }, + browser: { + css: ["src/theme.css"], + js: [], + modules: [], + png: [], + }, }, }; readJsonWithCompressionStub.resolves(mockDependencies); @@ -931,12 +972,32 @@ describe("CDP/Selectivity/Utils", () => { } as any; const mockDependencies = { chrome: { - css: { css: ["src/styles.css", "src/components.css"], js: [], modules: ["styled-components"] }, - js: { css: [], js: ["src/app.js", "src/utils.js"], modules: ["react", "lodash"] }, + browser: { + css: ["src/styles.css", "src/components.css"], + js: [], + modules: ["styled-components"], + png: [], + }, + testplane: { + css: [], + js: ["src/app.js", "src/utils.js"], + modules: ["react", "lodash"], + png: [], + }, }, firefox: { - css: { css: ["src/styles.css"], js: [], modules: [] }, - js: { css: [], js: ["src/app.js"], modules: ["react"] }, + browser: { + css: ["src/styles.css"], + js: [], + modules: [], + png: [], + }, + testplane: { + css: [], + js: ["src/app.js"], + modules: ["react"], + png: [], + }, }, }; readJsonWithCompressionStub.resolves(mockDependencies); @@ -948,5 +1009,51 @@ describe("CDP/Selectivity/Utils", () => { defaultValue: {}, }); }); + + it("should restore missing dependency properties", async () => { + const mockTest = { + id: "old-format-test", + title: "Old format test", + file: "test.js", + location: { line: 1, column: 1 }, + fn: sandbox.stub(), + clone: sandbox.stub(), + assign: sandbox.stub(), + } as any; + const oldFormatDependencies = { + chrome: { + browser: { + css: ["src/styles.css"], + modules: ["react"], + }, + testplane: { + js: ["src/utils.js"], + }, + }, + }; + readJsonWithCompressionStub.resolves(oldFormatDependencies); + + const result = await utils.readTestDependencies("/test/selectivity/tests", mockTest, "none"); + const completeExpectedBrowserDependencies: NormalizedDependencies = { + css: ["src/styles.css"], + js: [], + modules: ["react"], + png: [], + }; + + const completeExpectedTestplaneDependencies: NormalizedDependencies = { + css: [], + js: ["src/utils.js"], + modules: [], + png: [], + }; + + assert.deepEqual(result, { + chrome: { + browser: completeExpectedBrowserDependencies, + testplane: completeExpectedTestplaneDependencies, + }, + }); + }); }); });