Skip to content

Commit 5dbebe6

Browse files
fix: ensure that chromium based browsers do not send out a lot of font requests when global styles change (#28217)
1 parent 8c4a106 commit 5dbebe6

File tree

17 files changed

+286
-119
lines changed

17 files changed

+286
-119
lines changed

cli/CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ _Released 11/7/2023 (PENDING)_
66
**Bugfixes:**
77

88
- Fixed an issue determining visibility when an element is hidden by an ancestor with a shared edge. Fixes [#27514](https://github.com/cypress-io/cypress/issues/27514).
9-
- Fixed an issue with 'other' targets (e.g. pdf documents embedded in an object tag) not fully loading. Fixes [#28228](https://github.com/cypress-io/cypress/issues/28228)
9+
- Fixed an issue where in chromium based browsers, global style updates can trigger flooding of font face requests in DevTools and Test Replay. This can affect performance due to the flooding of messages in CDP. Fixes [#28150](https://github.com/cypress-io/cypress/issues/28150) and [#28215](https://github.com/cypress-io/cypress/issues/28215).
10+
- Fixed an issue with 'other' targets (e.g. pdf documents embedded in an object tag) not fully loading. Fixes [#28228](https://github.com/cypress-io/cypress/issues/28228) and [#28162](https://github.com/cypress-io/cypress/issues/28162).
1011
- Fixed an issue where network requests made from tabs/windows other than the main Cypress tab would be delayed. Fixes [#28113](https://github.com/cypress-io/cypress/issues/28113).
1112
- Stopped processing CDP events at the end of a spec when Test Isolation is off and Test Replay is enabled. Addressed in [#28213](https://github.com/cypress-io/cypress/pull/28213).
1213

packages/driver/cypress/e2e/commands/actions/click.cy.js

-4
Original file line numberDiff line numberDiff line change
@@ -1678,16 +1678,12 @@ describe('src/cy/commands/actions/click', () => {
16781678
it('can scroll to and click elements in html with scroll-behavior: smooth', () => {
16791679
cy.get('html').invoke('css', 'scrollBehavior', 'smooth')
16801680
cy.get('#table tr:first').click()
1681-
// Validate that the scrollBehavior is still smooth even after the actionability fixes we do
1682-
cy.get('html').invoke('css', 'scrollBehavior').then((scrollBehavior) => expect(scrollBehavior).to.eq('smooth'))
16831681
})
16841682

16851683
// https://github.com/cypress-io/cypress/issues/3200
16861684
it('can scroll to and click elements in ancestor element with scroll-behavior: smooth', () => {
16871685
cy.get('#dom').invoke('css', 'scrollBehavior', 'smooth')
16881686
cy.get('#table tr:first').click()
1689-
// Validate that the scrollBehavior is still smooth even after the actionability fixes we do
1690-
cy.get('#dom').invoke('css', 'scrollBehavior').then((scrollBehavior) => expect(scrollBehavior).to.eq('smooth'))
16911687
})
16921688
})
16931689
})

packages/driver/cypress/e2e/dom/visibility.cy.ts

+1-24
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe('src/cypress/dom/visibility', () => {
5353
expect(fn()).to.be.true
5454
})
5555

56-
it('returns false if window and body < window height', () => {
56+
it('returns false window and body > window height', () => {
5757
cy.$$('body').html('<div>foo</div>')
5858

5959
const win = cy.state('window')
@@ -65,29 +65,6 @@ describe('src/cypress/dom/visibility', () => {
6565
expect(fn()).to.be.false
6666
})
6767

68-
it('returns true if document element and body > window height', function () {
69-
this.add('<div style="height: 1000px; width: 10px;" />')
70-
const documentElement = Cypress.dom.wrap(cy.state('document').documentElement)
71-
72-
const fn = () => {
73-
return dom.isScrollable(documentElement)
74-
}
75-
76-
expect(fn()).to.be.true
77-
})
78-
79-
it('returns false if document element and body < window height', () => {
80-
cy.$$('body').html('<div>foo</div>')
81-
82-
const documentElement = Cypress.dom.wrap(cy.state('document').documentElement)
83-
84-
const fn = () => {
85-
return dom.isScrollable(documentElement)
86-
}
87-
88-
expect(fn()).to.be.false
89-
})
90-
9168
it('returns false el is not scrollable', function () {
9269
const noScroll = this.add(`\
9370
<div style="height: 100px; overflow: auto;">

packages/driver/src/cy/actionability.ts

+11-33
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import $utils from './../cypress/utils'
88
import type { ElWindowPostion, ElViewportPostion, ElementPositioning } from '../dom/coordinates'
99
import $elements from '../dom/elements'
1010
import $errUtils from '../cypress/error_utils'
11-
import { callNativeMethod, getNativeProp } from '../dom/elements/nativeProps'
1211
const debug = debugFn('cypress:driver:actionability')
1312

1413
const delay = 50
@@ -461,46 +460,24 @@ const verify = function (cy, $el, config, options, callbacks: VerifyCallbacks) {
461460
// make scrolling occur instantly. we do this by adding a style tag
462461
// and then removing it after we finish scrolling
463462
// https://github.com/cypress-io/cypress/issues/3200
464-
const addScrollBehaviorFix = (element: JQuery<HTMLElement>) => {
465-
const affectedParents: Map<HTMLElement, string> = new Map()
463+
const addScrollBehaviorFix = () => {
464+
let style
466465

467466
try {
468-
let parent: JQuery<HTMLElement> | null = element
467+
const doc = $el.get(0).ownerDocument
469468

470-
do {
471-
if ($dom.isScrollable(parent)) {
472-
const parentElement = parent[0]
473-
const style = getNativeProp(parentElement, 'style')
474-
const styles = getComputedStyle(parentElement)
475-
476-
if (styles.scrollBehavior === 'smooth') {
477-
affectedParents.set(parentElement, callNativeMethod(style, 'getStyleProperty', 'scroll-behavior'))
478-
callNativeMethod(style, 'setStyleProperty', 'scroll-behavior', 'auto')
479-
}
480-
}
481-
482-
parent = $dom.getFirstScrollableParent(parent)
483-
} while (parent)
469+
style = doc.createElement('style')
470+
style.innerHTML = '* { scroll-behavior: inherit !important; }'
471+
// there's guaranteed to be a <script> tag, so that's the safest thing
472+
// to query for and add the style tag after
473+
doc.querySelector('script').after(style)
484474
} catch (err) {
485475
// the above shouldn't error, but out of an abundance of caution, we
486476
// ignore any errors since this fix isn't worth failing the test over
487477
}
488478

489479
return () => {
490-
for (const [parent, value] of affectedParents) {
491-
const style = getNativeProp(parent, 'style')
492-
493-
if (value === '') {
494-
if (callNativeMethod(style, 'getStyleProperty', 'length') === 1) {
495-
callNativeMethod(parent, 'removeAttribute', 'style')
496-
} else {
497-
callNativeMethod(style, 'removeProperty', 'scroll-behavior')
498-
}
499-
} else {
500-
callNativeMethod(style, 'setStyleProperty', 'scroll-behavior', value)
501-
}
502-
}
503-
affectedParents.clear()
480+
if (style) style.remove()
504481
}
505482
}
506483

@@ -523,7 +500,8 @@ const verify = function (cy, $el, config, options, callbacks: VerifyCallbacks) {
523500
if (options.scrollBehavior !== false) {
524501
// scroll the element into view
525502
const scrollBehavior = scrollBehaviorOptionsMap[options.scrollBehavior]
526-
const removeScrollBehaviorFix = addScrollBehaviorFix($el)
503+
504+
const removeScrollBehaviorFix = addScrollBehaviorFix()
527505

528506
debug('scrollIntoView:', $el[0])
529507
$el.get(0).scrollIntoView({ block: scrollBehavior })

packages/driver/src/dom/elements/complexElements.ts

-6
Original file line numberDiff line numberDiff line change
@@ -290,12 +290,6 @@ export const isScrollable = ($el) => {
290290
return false
291291
}
292292

293-
const documentElement = $document.getDocumentFromElement(el).documentElement
294-
295-
if (el === documentElement) {
296-
return checkDocumentElement($window.getWindowByElement(el), el)
297-
}
298-
299293
// if we're any other element, we do some css calculations
300294
// to see that the overflow is correct and the scroll
301295
// area is larger than the actual height or width

packages/driver/src/dom/elements/nativeProps.ts

-5
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@ const nativeGetters = {
207207
body: descriptor('Document', 'body').get,
208208
frameElement: Object.getOwnPropertyDescriptor(window, 'frameElement')!.get,
209209
maxLength: _getMaxLength,
210-
style: descriptor('HTMLElement', 'style').get,
211210
}
212211

213212
const nativeSetters = {
@@ -225,16 +224,12 @@ const nativeMethods = {
225224
execCommand: window.document.execCommand,
226225
getAttribute: window.Element.prototype.getAttribute,
227226
setAttribute: window.Element.prototype.setAttribute,
228-
removeAttribute: window.Element.prototype.removeAttribute,
229227
setSelectionRange: _nativeSetSelectionRange,
230228
modify: window.Selection.prototype.modify,
231229
focus: _nativeFocus,
232230
hasFocus: window.document.hasFocus,
233231
blur: _nativeBlur,
234232
select: _nativeSelect,
235-
getStyleProperty: window.CSSStyleDeclaration.prototype.getPropertyValue,
236-
setStyleProperty: window.CSSStyleDeclaration.prototype.setProperty,
237-
removeStyleProperty: window.CSSStyleDeclaration.prototype.removeProperty,
238233
}
239234

240235
export const getNativeProp = function<T, K extends keyof T> (obj: T, prop: K): T[K] {

packages/server/lib/browsers/chrome.ts

+3
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,9 @@ export = {
427427
args.push(`--remote-debugging-port=${port}`)
428428
args.push('--remote-debugging-address=127.0.0.1')
429429

430+
// control memory caching per execution context so that font flooding does not occur: https://github.com/cypress-io/cypress/issues/28215
431+
args.push('--enable-features=ScopeMemoryCachePerContext')
432+
430433
return args
431434
},
432435

packages/server/lib/cypress.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -164,19 +164,24 @@ module.exports = {
164164
options.headed = false
165165
}
166166

167+
const electronApp = require('./util/electron-app')
168+
167169
if (options.runProject && !options.headed) {
168170
debug('scaling electron app in headless mode')
169171
// scale the electron browser window
170172
// to force retina screens to not
171173
// upsample their images when offscreen
172174
// rendering
173-
require('./util/electron-app').scale()
175+
electronApp.scale()
174176
}
175177

178+
// control memory caching per execution context so that font flooding does not occur: https://github.com/cypress-io/cypress/issues/28215
179+
electronApp.setScopeMemoryCachePerContext()
180+
176181
// make sure we have the appData folder
177182
return Promise.all([
178183
require('./util/app_data').ensure(),
179-
require('./util/electron-app').setRemoteDebuggingPort(),
184+
electronApp.setRemoteDebuggingPort(),
180185
])
181186
.then(() => {
182187
// else determine the mode by

packages/server/lib/util/electron-app.js

+13
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ const setRemoteDebuggingPort = async () => {
4242
}
4343
}
4444

45+
const setScopeMemoryCachePerContext = () => {
46+
try {
47+
const { app } = require('electron')
48+
49+
app.commandLine.appendSwitch('enable-features', 'ScopeMemoryCachePerContext')
50+
} catch (err) {
51+
// Catch errors for when we're running outside of electron in development
52+
return
53+
}
54+
}
55+
4556
const isRunning = () => {
4657
// are we in the electron or the node process?
4758
return Boolean(process.env.ELECTRON_RUN_AS_NODE || process.versions && process.versions.electron)
@@ -60,6 +71,8 @@ const isRunningAsElectronProcess = ({ debug } = {}) => {
6071
module.exports = {
6172
scale,
6273

74+
setScopeMemoryCachePerContext,
75+
6376
getRemoteDebuggingPort,
6477

6578
setRemoteDebuggingPort,

system-tests/__snapshots__/protocol_spec.js

-44
Original file line numberDiff line numberDiff line change
@@ -6678,50 +6678,6 @@ exports['component events - experimentalSingleTabRunMode: true'] = `
66786678
"pageLoading": [],
66796679
"resetTest": [],
66806680
"responseEndedWithEmptyBody": [
6681-
{
6682-
"requestId": "Any.Number",
6683-
"isCached": true,
6684-
"timings": {
6685-
"cdpRequestWillBeSentTimestamp": "Any.Number",
6686-
"cdpRequestWillBeSentReceivedTimestamp": "Any.Number",
6687-
"proxyRequestReceivedTimestamp": "Any.Number",
6688-
"cdpLagDuration": "Any.Number",
6689-
"proxyRequestCorrelationDuration": "Any.Number"
6690-
}
6691-
},
6692-
{
6693-
"requestId": "Any.Number",
6694-
"isCached": true,
6695-
"timings": {
6696-
"cdpRequestWillBeSentTimestamp": "Any.Number",
6697-
"cdpRequestWillBeSentReceivedTimestamp": "Any.Number",
6698-
"proxyRequestReceivedTimestamp": "Any.Number",
6699-
"cdpLagDuration": "Any.Number",
6700-
"proxyRequestCorrelationDuration": "Any.Number"
6701-
}
6702-
},
6703-
{
6704-
"requestId": "Any.Number",
6705-
"isCached": true,
6706-
"timings": {
6707-
"cdpRequestWillBeSentTimestamp": "Any.Number",
6708-
"cdpRequestWillBeSentReceivedTimestamp": "Any.Number",
6709-
"proxyRequestReceivedTimestamp": "Any.Number",
6710-
"cdpLagDuration": "Any.Number",
6711-
"proxyRequestCorrelationDuration": "Any.Number"
6712-
}
6713-
},
6714-
{
6715-
"requestId": "Any.Number",
6716-
"isCached": true,
6717-
"timings": {
6718-
"cdpRequestWillBeSentTimestamp": "Any.Number",
6719-
"cdpRequestWillBeSentReceivedTimestamp": "Any.Number",
6720-
"proxyRequestReceivedTimestamp": "Any.Number",
6721-
"cdpLagDuration": "Any.Number",
6722-
"proxyRequestCorrelationDuration": "Any.Number"
6723-
}
6724-
},
67256681
{
67266682
"requestId": "Any.Number",
67276683
"isCached": true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import path from 'path'
2+
import fs from 'fs-extra'
3+
import type { AppCaptureProtocolInterface, ResponseEndedWithEmptyBodyOptions, ResponseStreamOptions, ResponseStreamTimedOutOptions } from '@packages/types'
4+
import type { Readable } from 'stream'
5+
6+
const getFilePath = (filename) => {
7+
return path.join(
8+
path.resolve(__dirname),
9+
'cypress',
10+
'system-tests-protocol-dbs',
11+
`${filename}.json`,
12+
)
13+
}
14+
15+
export class AppCaptureProtocol implements AppCaptureProtocolInterface {
16+
private filename: string
17+
private events = {
18+
numberOfFontRequests: 0,
19+
}
20+
private cdpClient: any
21+
22+
getDbMetadata (): { offset: number, size: number } {
23+
return {
24+
offset: 0,
25+
size: 0,
26+
}
27+
}
28+
29+
responseStreamReceived (options: ResponseStreamOptions): Readable {
30+
return options.responseStream
31+
}
32+
33+
connectToBrowser = async (cdpClient) => {
34+
if (cdpClient) {
35+
this.cdpClient = cdpClient
36+
}
37+
38+
this.cdpClient.on('Network.requestWillBeSent', (params) => {
39+
// For the font flooding test, we want to count the number of font requests.
40+
// There should only be 2 requests. One for each test in the spec.
41+
if (params.type === 'Font') {
42+
this.events.numberOfFontRequests += 1
43+
}
44+
})
45+
}
46+
47+
addRunnables = (runnables) => {
48+
return Promise.resolve()
49+
}
50+
51+
beforeSpec = ({ archivePath, db }) => {
52+
this.filename = getFilePath(path.basename(db.name))
53+
54+
if (!fs.existsSync(archivePath)) {
55+
// If a dummy file hasn't been created by the test, write a tar file so that it can be fake uploaded
56+
fs.writeFileSync(archivePath, '')
57+
}
58+
}
59+
60+
async afterSpec (): Promise<void> {
61+
try {
62+
fs.outputFileSync(this.filename, JSON.stringify(this.events, null, 2))
63+
} catch (e) {
64+
console.log('error writing protocol events', e)
65+
}
66+
}
67+
68+
beforeTest = (test) => {
69+
return Promise.resolve()
70+
}
71+
72+
commandLogAdded = (log) => {
73+
}
74+
75+
commandLogChanged = (log) => {
76+
}
77+
78+
viewportChanged = (input) => {
79+
}
80+
81+
urlChanged = (input) => {
82+
}
83+
84+
pageLoading = (input) => {
85+
}
86+
87+
preAfterTest = (test, options) => {
88+
return Promise.resolve()
89+
}
90+
91+
afterTest = (test) => {
92+
return Promise.resolve()
93+
}
94+
95+
responseEndedWithEmptyBody = (options: ResponseEndedWithEmptyBodyOptions) => {
96+
}
97+
98+
responseStreamTimedOut (options: ResponseStreamTimedOutOptions): void {
99+
}
100+
101+
resetTest (testId: string): void {
102+
}
103+
}

0 commit comments

Comments
 (0)