From 5b862a20055bc1452cbb301069f21382aa66189c Mon Sep 17 00:00:00 2001 From: Weiller Carvalho Date: Mon, 10 Nov 2025 21:49:11 -0300 Subject: [PATCH 1/3] fix:correct helper pixel detection --- packages/server/lib/screenshots.ts | 22 ++++++++---- packages/server/test/unit/screenshots_spec.js | 36 +++++++++++++++++-- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/packages/server/lib/screenshots.ts b/packages/server/lib/screenshots.ts index ff624162eb1..325e4f8ad6a 100644 --- a/packages/server/lib/screenshots.ts +++ b/packages/server/lib/screenshots.ts @@ -103,12 +103,22 @@ const intToRGBA = function (int: number): RGBAWithName { // when taking a 'runner' capture, we ensure the pixels ARE there const hasHelperPixels = function (image, pixelRatio) { - const topLeft = intToRGBA(image.getPixelColor(0, 0)) - const topLeftRight = intToRGBA(image.getPixelColor(1 * pixelRatio, 0)) - const topLeftDown = intToRGBA(image.getPixelColor(0, 1 * pixelRatio)) - const bottomLeft = intToRGBA(image.getPixelColor(0, image.bitmap.height)) - const topRight = intToRGBA(image.getPixelColor(image.bitmap.width, 0)) - const bottomRight = intToRGBA(image.getPixelColor(image.bitmap.width, image.bitmap.height)) + const maxX = image.bitmap.width - 1 + const maxY = image.bitmap.height - 1 + + const getClampedPixel = (x: number, y: number) => { + const clampedX = Math.max(0, Math.min(Math.round(x), maxX)) + const clampedY = Math.max(0, Math.min(Math.round(y), maxY)) + + return intToRGBA(image.getPixelColor(clampedX, clampedY)) + } + + const topLeft = getClampedPixel(0, 0) + const topLeftRight = getClampedPixel(1 * pixelRatio, 0) + const topLeftDown = getClampedPixel(0, 1 * pixelRatio) + const bottomLeft = getClampedPixel(0, maxY) + const topRight = getClampedPixel(maxX, 0) + const bottomRight = getClampedPixel(maxX, maxY) topLeft.isNotWhite = !isWhite(topLeft) topLeftRight.isWhite = isWhite(topLeftRight) diff --git a/packages/server/test/unit/screenshots_spec.js b/packages/server/test/unit/screenshots_spec.js index d6c74b25564..721263a6f3b 100644 --- a/packages/server/test/unit/screenshots_spec.js +++ b/packages/server/test/unit/screenshots_spec.js @@ -19,6 +19,8 @@ let ctx describe('lib/screenshots', () => { before(async function () { + this.timeout(60000) + const { setCtx, makeDataContext, clearCtx } = require('../../lib/makeDataContext') // Clear and set up DataContext @@ -82,12 +84,14 @@ describe('lib/screenshots', () => { context('.capture', () => { beforeEach(function () { this.getPixelColor = sinon.stub() + const lastPixelIndex = this.jimpImage.bitmap.width - 1 + this.getPixelColor.withArgs(0, 0).returns('grey') this.getPixelColor.withArgs(1, 0).returns('white') this.getPixelColor.withArgs(0, 1).returns('white') - this.getPixelColor.withArgs(40, 0).returns('white') - this.getPixelColor.withArgs(0, 40).returns('white') - this.getPixelColor.withArgs(40, 40).returns('black') + this.getPixelColor.withArgs(lastPixelIndex, 0).returns('white') + this.getPixelColor.withArgs(0, lastPixelIndex).returns('white') + this.getPixelColor.withArgs(lastPixelIndex, lastPixelIndex).returns('black') this.jimpImage.getPixelColor = this.getPixelColor sinon.stub(Jimp, 'read').resolves(this.jimpImage) @@ -157,6 +161,32 @@ describe('lib/screenshots', () => { }) }) + it('does not read helper pixels outside the bitmap bounds', function () { + this.getPixelColor.restore() + + const maxX = this.jimpImage.bitmap.width - 1 + const maxY = this.jimpImage.bitmap.height - 1 + const pixelColors = new Map([ + ['0,0', 'white'], + ['1,0', 'white'], + ['0,1', 'white'], + [`${maxX},0`, 'white'], + [`0,${maxY}`, 'white'], + [`${maxX},${maxY}`, 'black'], + ]) + + this.getPixelColor = sinon.stub().callsFake((x, y) => { + expect(x).to.be.within(0, maxX) + expect(y).to.be.within(0, maxY) + + return pixelColors.get(`${x},${y}`) || 'white' + }) + + this.jimpImage.getPixelColor = this.getPixelColor + + return screenshots.capture(this.appData, this.automate) + }) + describe('runner hidden', () => { beforeEach(function () { this.currentTest.timeout(5000) From d4e63ee651ee47554c411a8ee56837554849fa60 Mon Sep 17 00:00:00 2001 From: Weiller Carvalho Date: Tue, 11 Nov 2025 22:23:30 -0300 Subject: [PATCH 2/3] fix: fixed screenshots helper pixel stubs --- packages/server/test/unit/screenshots_spec.js | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/server/test/unit/screenshots_spec.js b/packages/server/test/unit/screenshots_spec.js index 721263a6f3b..7ba5b84f460 100644 --- a/packages/server/test/unit/screenshots_spec.js +++ b/packages/server/test/unit/screenshots_spec.js @@ -16,6 +16,7 @@ const image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+ const iso8601Regex = /^\d{4}\-\d{2}\-\d{2}T\d{2}\:\d{2}\:\d{2}\.?\d*Z?$/ let ctx +let originalGetBrowsers describe('lib/screenshots', () => { before(async function () { @@ -27,6 +28,19 @@ describe('lib/screenshots', () => { await clearCtx() setCtx(makeDataContext({})) ctx = require('../../lib/makeDataContext').getCtx() + // Provide a deterministic browser list so BrowserDataSource can resolve in CI environments + const availableBrowsers = [{ + name: 'chrome', + family: 'chromium', + channel: 'stable', + displayName: 'Chrome', + path: '/path/to/chrome', + version: '123.0.0', + majorVersion: '123', + }] + + originalGetBrowsers = ctx._apis.browserApi.getBrowsers + ctx._apis.browserApi.getBrowsers = async () => availableBrowsers Fixtures.scaffold() this.todosPath = Fixtures.projectPath('todos') @@ -71,19 +85,26 @@ describe('lib/screenshots', () => { clone: () => { return this.jimpImage }, + getPixelColor () { + throw new Error('getPixelColor should be stubbed') + }, } Jimp.prototype.composite = sinon.stub() // Jimp.prototype.getBuffer = sinon.stub().resolves(@buffer) }) - after(() => { + after(function () { + if (originalGetBrowsers) { + ctx._apis.browserApi.getBrowsers = originalGetBrowsers + } + return Fixtures.remove() }) context('.capture', () => { beforeEach(function () { - this.getPixelColor = sinon.stub() + this.getPixelColor = sinon.stub(this.jimpImage, 'getPixelColor') const lastPixelIndex = this.jimpImage.bitmap.width - 1 this.getPixelColor.withArgs(0, 0).returns('grey') @@ -92,7 +113,6 @@ describe('lib/screenshots', () => { this.getPixelColor.withArgs(lastPixelIndex, 0).returns('white') this.getPixelColor.withArgs(0, lastPixelIndex).returns('white') this.getPixelColor.withArgs(lastPixelIndex, lastPixelIndex).returns('black') - this.jimpImage.getPixelColor = this.getPixelColor sinon.stub(Jimp, 'read').resolves(this.jimpImage) const intToRGBA = sinon.stub(Jimp, 'intToRGBA') From 60b29ca9dc354ec361b0031900cc0468a0e35957 Mon Sep 17 00:00:00 2001 From: Weiller Carvalho Date: Wed, 12 Nov 2025 01:12:10 -0300 Subject: [PATCH 3/3] fix: fixed screenshots helper pixel stubs + adding changelog --- cli/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index a7418bedce8..3d996f231b9 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -6,6 +6,7 @@ _Released 11/18/2025 (PENDING)_ **Bugfixes:** - Fixed an issue where top changes on test retries could cause attempt numbers to show up more than one time in the reporter and cause attempts to be lost in Test Replay. Addressed in [#32888](https://github.com/cypress-io/cypress/pull/32888). +- Fixed a regression where screenshot helper pixels were read outside the bitmap bounds, which also caused the associated unit test to throw `this.getPixelColor.restore is not a function`. Addresses [#32927](https://github.com/cypress-io/cypress/issues/32927). **Misc:**