fix: workaround for the electron animation bug (#31391)

* fix: workaround for the electron animation bug

* fix bug

* Update packages/server/test/unit/browsers/electron_spec.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* try something

* try something

* try something

* try something

* fix

* fix

* fix

* refactor and ensure still broken

* and the fix

* Update packages/server/test/unit/browsers/electron_spec.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/server/lib/browsers/electron.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update CHANGELOG.md

* refactor

* Update cli/CHANGELOG.md

* Update cli/CHANGELOG.md

Co-authored-by: Matt Schile <mschile@cypress.io>

* fix build

* Update packages/server/test/unit/browsers/electron_spec.js

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
Co-authored-by: Matt Schile <mschile@cypress.io>
This commit is contained in:
Ryan Manuel
2025-03-31 13:50:14 +00:00
committed by GitHub
parent b3cb7198b4
commit b6a64604ba
6 changed files with 102 additions and 1 deletions
+1
View File
@@ -7,6 +7,7 @@ _Released 4/8/2025 (PENDING)_
- Allows for `babel-loader` version 10 to be a peer dependency of `@cypress/webpack-preprocessor`. Fixed in [#31218](https://github.com/cypress-io/cypress/pull/31218).
- Fixed an issue where Firefox BiDi was prematurely removing prerequests on pending requests. Fixes [#31376](https://github.com/cypress-io/cypress/issues/31376).
- Fixed an [issue](https://github.com/electron/electron/issues/45398) with Electron causing slow animations and increased test times by starting a CDP screencast with a noop configuration. Fixes [#30980](https://github.com/cypress-io/cypress/issues/30980).
**Misc:**
+21 -1
View File
@@ -17,6 +17,7 @@ import type { CDPSocketServer } from '@packages/socket/lib/cdp-socket'
import memory from './memory'
import { BrowserCriClient } from './browser-cri-client'
import { getRemoteDebuggingPort } from '../util/electron-app'
import type { CriClient } from './cri-client'
// TODO: unmix these two types
type ElectronOpts = Windows.WindowOptions & BrowserLaunchOpts
@@ -131,6 +132,25 @@ async function recordVideo (cdpAutomation: CdpAutomation, videoApi: RunModeVideo
await cdpAutomation.startVideoRecording(writeVideoFrame, screencastOpts())
}
// Start video legitimately if we have a video api. Otherwise, if we're in run mode, start a dummy screencast to prevent:
// https://github.com/electron/electron/issues/45398
async function handleVideo (handleVideoOptions: { pageCriClient: CriClient, cdpAutomation: CdpAutomation, videoApi?: RunModeVideoApi, options: BrowserLaunchOpts }) {
const { pageCriClient, cdpAutomation, videoApi, options } = handleVideoOptions
if (videoApi) {
await recordVideo(cdpAutomation, videoApi)
} else if (options.isTextTerminal) {
// To prevent https://github.com/electron/electron/issues/45398, we start a dummy screen cast with a quality of 0
// and only capture every 2^32 - 1 frames without listening to any frames. This is effectively a no-op, but it
// prevents the issue from occurring.
await pageCriClient.send('Page.startScreencast', {
format: 'jpeg',
everyNthFrame: 2 ** 31 - 1,
quality: 0,
})
}
}
export = {
_defaultOptions (projectRoot: string | undefined, state: Preferences, options: BrowserLaunchOpts, automation: Automation): ElectronOpts {
const _this = this
@@ -317,7 +337,7 @@ export = {
pageCriClient.send('ServiceWorker.enable'),
this.connectProtocolToBrowser({ protocolManager }),
cdpSocketServer?.attachCDPClient(cdpAutomation),
videoApi && recordVideo(cdpAutomation, videoApi),
handleVideo({ pageCriClient, cdpAutomation, videoApi, options }),
this._handleDownloads(win, options.downloadsFolder, automation),
utils.initializeCDP(pageCriClient, automation),
// Ensure to clear browser state in between runs. This is handled differently in browsers when we launch new tabs, but we don't have that concept in electron
@@ -14,6 +14,7 @@ const { Automation } = require(`../../../lib/automation`)
const { BrowserCriClient } = require('../../../lib/browsers/browser-cri-client')
const electronApp = require('../../../lib/util/electron-app')
const utils = require('../../../lib/browsers/utils')
const { screencastOpts } = require('../../../lib/browsers/cdp_automation')
const ELECTRON_PID = 10001
@@ -476,6 +477,41 @@ describe('lib/browsers/electron', () => {
})
})
it('expects the video to be fully enabled if specified in the config', async function () {
const mockWriteVideoFrame = sinon.stub()
const mockVideoApi = {
useFfmpegVideoController: sinon.stub().resolves({
writeVideoFrame: mockWriteVideoFrame,
}),
}
await electron._launch(this.win, this.url, this.automation, this.options, mockVideoApi, undefined, { attachCDPClient: sinon.stub() })
expect(mockVideoApi.useFfmpegVideoController).to.be.called
expect(this.pageCriClient.on).to.be.calledWith('Page.screencastFrame', sinon.match.func)
expect(this.pageCriClient.send).to.be.calledWith('Page.startScreencast', screencastOpts())
})
it('starts the screencast but does not capture the frames if video is not enabled but the app is in run mode', async function () {
this.options.isTextTerminal = true
await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() })
expect(this.pageCriClient.on).not.to.be.calledWith('Page.screencastFrame', sinon.match.func)
expect(this.pageCriClient.send).to.be.calledWith('Page.startScreencast', {
format: 'jpeg',
everyNthFrame: 2 ** 31 - 1,
quality: 0,
})
})
it('does not start the screencast if video is not enabled and the app is not in run mode', async function () {
await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() })
expect(this.pageCriClient.on).not.to.be.calledWith('Page.screencastFrame', sinon.match.func)
expect(this.pageCriClient.send).not.to.be.calledWith('Page.startScreencast', sinon.match.any)
})
it('registers onRequest automation middleware and calls show when requesting to be focused', function () {
sinon.spy(this.automation, 'use')
@@ -0,0 +1,9 @@
describe('electron animation bug', () => {
it('loads in less than .3 seconds', { defaultCommandTimeout: 750 }, () => {
cy.visit('/electron_animation_bug.html')
cy.get('#app').should('exist')
cy.get('#remove').click()
cy.get('#app').should('not.exist')
})
})
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div id="app">
<div>hello</div>
<div>world</div>
</div>
<button id="remove">remove</button>
<script>
const remove = document.getElementById('remove')
remove.addEventListener('click', () => {
const app = document.getElementById('app')
app.animate([
{ transform: 'translate(5px, 10px)', opacity: 0 }
], {
duration: 500,
easing: 'cubic-bezier(0.4, 0, 1, 1)'
}).finished.then(() => {
app.remove()
})
})
</script>
</body>
</html>
@@ -0,0 +1,9 @@
import { default as systemTests } from '../lib/system-tests'
describe('e2e electron animation bug', () => {
systemTests.it('executes a test that demonstrates the electron animation bug and ensures that we have worked around it', {
browser: 'electron',
project: 'e2e',
spec: 'electron_animation_bug.cy.ts',
})
})