Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ _Released 11/18/2025 (PENDING)_

- Fixed an issue where [`cy.wrap()`](https://docs.cypress.io/api/commands/wrap) would cause infinite recursion and freeze the Cypress App when called with objects containing circular references. Fixes [#24715](https://github.com/cypress-io/cypress/issues/24715). Addressed in [#32917](https://github.com/cypress-io/cypress/pull/32917).
- 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:**

Expand Down
22 changes: 16 additions & 6 deletions packages/server/lib/screenshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
62 changes: 56 additions & 6 deletions packages/server/test/unit/screenshots_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,31 @@ 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 () {
this.timeout(60000)

const { setCtx, makeDataContext, clearCtx } = require('../../lib/makeDataContext')

// Clear and set up DataContext
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')
Expand Down Expand Up @@ -69,26 +85,34 @@ 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')
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.jimpImage.getPixelColor = this.getPixelColor
this.getPixelColor.withArgs(lastPixelIndex, 0).returns('white')
this.getPixelColor.withArgs(0, lastPixelIndex).returns('white')
this.getPixelColor.withArgs(lastPixelIndex, lastPixelIndex).returns('black')

sinon.stub(Jimp, 'read').resolves(this.jimpImage)
const intToRGBA = sinon.stub(Jimp, 'intToRGBA')
Expand Down Expand Up @@ -157,6 +181,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)
Expand Down