fix: ensure that chromium based browsers do not send out a lot of font requests when global styles change (#28217)

This commit is contained in:
Ryan Manuel
2023-11-03 14:39:56 -05:00
committed by GitHub
parent 8c4a1067cb
commit 5dbebe6e5e
17 changed files with 286 additions and 119 deletions

View File

@@ -6,7 +6,8 @@ _Released 11/7/2023 (PENDING)_
**Bugfixes:**
- 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).
- 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)
- 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).
- 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).
- 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).
- 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).

View File

@@ -1678,16 +1678,12 @@ describe('src/cy/commands/actions/click', () => {
it('can scroll to and click elements in html with scroll-behavior: smooth', () => {
cy.get('html').invoke('css', 'scrollBehavior', 'smooth')
cy.get('#table tr:first').click()
// Validate that the scrollBehavior is still smooth even after the actionability fixes we do
cy.get('html').invoke('css', 'scrollBehavior').then((scrollBehavior) => expect(scrollBehavior).to.eq('smooth'))
})
// https://github.com/cypress-io/cypress/issues/3200
it('can scroll to and click elements in ancestor element with scroll-behavior: smooth', () => {
cy.get('#dom').invoke('css', 'scrollBehavior', 'smooth')
cy.get('#table tr:first').click()
// Validate that the scrollBehavior is still smooth even after the actionability fixes we do
cy.get('#dom').invoke('css', 'scrollBehavior').then((scrollBehavior) => expect(scrollBehavior).to.eq('smooth'))
})
})
})

View File

@@ -53,7 +53,7 @@ describe('src/cypress/dom/visibility', () => {
expect(fn()).to.be.true
})
it('returns false if window and body < window height', () => {
it('returns false window and body > window height', () => {
cy.$$('body').html('<div>foo</div>')
const win = cy.state('window')
@@ -65,29 +65,6 @@ describe('src/cypress/dom/visibility', () => {
expect(fn()).to.be.false
})
it('returns true if document element and body > window height', function () {
this.add('<div style="height: 1000px; width: 10px;" />')
const documentElement = Cypress.dom.wrap(cy.state('document').documentElement)
const fn = () => {
return dom.isScrollable(documentElement)
}
expect(fn()).to.be.true
})
it('returns false if document element and body < window height', () => {
cy.$$('body').html('<div>foo</div>')
const documentElement = Cypress.dom.wrap(cy.state('document').documentElement)
const fn = () => {
return dom.isScrollable(documentElement)
}
expect(fn()).to.be.false
})
it('returns false el is not scrollable', function () {
const noScroll = this.add(`\
<div style="height: 100px; overflow: auto;">

View File

@@ -8,7 +8,6 @@ import $utils from './../cypress/utils'
import type { ElWindowPostion, ElViewportPostion, ElementPositioning } from '../dom/coordinates'
import $elements from '../dom/elements'
import $errUtils from '../cypress/error_utils'
import { callNativeMethod, getNativeProp } from '../dom/elements/nativeProps'
const debug = debugFn('cypress:driver:actionability')
const delay = 50
@@ -461,46 +460,24 @@ const verify = function (cy, $el, config, options, callbacks: VerifyCallbacks) {
// make scrolling occur instantly. we do this by adding a style tag
// and then removing it after we finish scrolling
// https://github.com/cypress-io/cypress/issues/3200
const addScrollBehaviorFix = (element: JQuery<HTMLElement>) => {
const affectedParents: Map<HTMLElement, string> = new Map()
const addScrollBehaviorFix = () => {
let style
try {
let parent: JQuery<HTMLElement> | null = element
const doc = $el.get(0).ownerDocument
do {
if ($dom.isScrollable(parent)) {
const parentElement = parent[0]
const style = getNativeProp(parentElement, 'style')
const styles = getComputedStyle(parentElement)
if (styles.scrollBehavior === 'smooth') {
affectedParents.set(parentElement, callNativeMethod(style, 'getStyleProperty', 'scroll-behavior'))
callNativeMethod(style, 'setStyleProperty', 'scroll-behavior', 'auto')
}
}
parent = $dom.getFirstScrollableParent(parent)
} while (parent)
style = doc.createElement('style')
style.innerHTML = '* { scroll-behavior: inherit !important; }'
// there's guaranteed to be a <script> tag, so that's the safest thing
// to query for and add the style tag after
doc.querySelector('script').after(style)
} catch (err) {
// the above shouldn't error, but out of an abundance of caution, we
// ignore any errors since this fix isn't worth failing the test over
}
return () => {
for (const [parent, value] of affectedParents) {
const style = getNativeProp(parent, 'style')
if (value === '') {
if (callNativeMethod(style, 'getStyleProperty', 'length') === 1) {
callNativeMethod(parent, 'removeAttribute', 'style')
} else {
callNativeMethod(style, 'removeProperty', 'scroll-behavior')
}
} else {
callNativeMethod(style, 'setStyleProperty', 'scroll-behavior', value)
}
}
affectedParents.clear()
if (style) style.remove()
}
}
@@ -523,7 +500,8 @@ const verify = function (cy, $el, config, options, callbacks: VerifyCallbacks) {
if (options.scrollBehavior !== false) {
// scroll the element into view
const scrollBehavior = scrollBehaviorOptionsMap[options.scrollBehavior]
const removeScrollBehaviorFix = addScrollBehaviorFix($el)
const removeScrollBehaviorFix = addScrollBehaviorFix()
debug('scrollIntoView:', $el[0])
$el.get(0).scrollIntoView({ block: scrollBehavior })

View File

@@ -290,12 +290,6 @@ export const isScrollable = ($el) => {
return false
}
const documentElement = $document.getDocumentFromElement(el).documentElement
if (el === documentElement) {
return checkDocumentElement($window.getWindowByElement(el), el)
}
// if we're any other element, we do some css calculations
// to see that the overflow is correct and the scroll
// area is larger than the actual height or width

View File

@@ -207,7 +207,6 @@ const nativeGetters = {
body: descriptor('Document', 'body').get,
frameElement: Object.getOwnPropertyDescriptor(window, 'frameElement')!.get,
maxLength: _getMaxLength,
style: descriptor('HTMLElement', 'style').get,
}
const nativeSetters = {
@@ -225,16 +224,12 @@ const nativeMethods = {
execCommand: window.document.execCommand,
getAttribute: window.Element.prototype.getAttribute,
setAttribute: window.Element.prototype.setAttribute,
removeAttribute: window.Element.prototype.removeAttribute,
setSelectionRange: _nativeSetSelectionRange,
modify: window.Selection.prototype.modify,
focus: _nativeFocus,
hasFocus: window.document.hasFocus,
blur: _nativeBlur,
select: _nativeSelect,
getStyleProperty: window.CSSStyleDeclaration.prototype.getPropertyValue,
setStyleProperty: window.CSSStyleDeclaration.prototype.setProperty,
removeStyleProperty: window.CSSStyleDeclaration.prototype.removeProperty,
}
export const getNativeProp = function<T, K extends keyof T> (obj: T, prop: K): T[K] {

View File

@@ -427,6 +427,9 @@ export = {
args.push(`--remote-debugging-port=${port}`)
args.push('--remote-debugging-address=127.0.0.1')
// control memory caching per execution context so that font flooding does not occur: https://github.com/cypress-io/cypress/issues/28215
args.push('--enable-features=ScopeMemoryCachePerContext')
return args
},

View File

@@ -164,19 +164,24 @@ module.exports = {
options.headed = false
}
const electronApp = require('./util/electron-app')
if (options.runProject && !options.headed) {
debug('scaling electron app in headless mode')
// scale the electron browser window
// to force retina screens to not
// upsample their images when offscreen
// rendering
require('./util/electron-app').scale()
electronApp.scale()
}
// control memory caching per execution context so that font flooding does not occur: https://github.com/cypress-io/cypress/issues/28215
electronApp.setScopeMemoryCachePerContext()
// make sure we have the appData folder
return Promise.all([
require('./util/app_data').ensure(),
require('./util/electron-app').setRemoteDebuggingPort(),
electronApp.setRemoteDebuggingPort(),
])
.then(() => {
// else determine the mode by

View File

@@ -42,6 +42,17 @@ const setRemoteDebuggingPort = async () => {
}
}
const setScopeMemoryCachePerContext = () => {
try {
const { app } = require('electron')
app.commandLine.appendSwitch('enable-features', 'ScopeMemoryCachePerContext')
} catch (err) {
// Catch errors for when we're running outside of electron in development
return
}
}
const isRunning = () => {
// are we in the electron or the node process?
return Boolean(process.env.ELECTRON_RUN_AS_NODE || process.versions && process.versions.electron)
@@ -60,6 +71,8 @@ const isRunningAsElectronProcess = ({ debug } = {}) => {
module.exports = {
scale,
setScopeMemoryCachePerContext,
getRemoteDebuggingPort,
setRemoteDebuggingPort,

View File

@@ -6678,50 +6678,6 @@ exports['component events - experimentalSingleTabRunMode: true'] = `
"pageLoading": [],
"resetTest": [],
"responseEndedWithEmptyBody": [
{
"requestId": "Any.Number",
"isCached": true,
"timings": {
"cdpRequestWillBeSentTimestamp": "Any.Number",
"cdpRequestWillBeSentReceivedTimestamp": "Any.Number",
"proxyRequestReceivedTimestamp": "Any.Number",
"cdpLagDuration": "Any.Number",
"proxyRequestCorrelationDuration": "Any.Number"
}
},
{
"requestId": "Any.Number",
"isCached": true,
"timings": {
"cdpRequestWillBeSentTimestamp": "Any.Number",
"cdpRequestWillBeSentReceivedTimestamp": "Any.Number",
"proxyRequestReceivedTimestamp": "Any.Number",
"cdpLagDuration": "Any.Number",
"proxyRequestCorrelationDuration": "Any.Number"
}
},
{
"requestId": "Any.Number",
"isCached": true,
"timings": {
"cdpRequestWillBeSentTimestamp": "Any.Number",
"cdpRequestWillBeSentReceivedTimestamp": "Any.Number",
"proxyRequestReceivedTimestamp": "Any.Number",
"cdpLagDuration": "Any.Number",
"proxyRequestCorrelationDuration": "Any.Number"
}
},
{
"requestId": "Any.Number",
"isCached": true,
"timings": {
"cdpRequestWillBeSentTimestamp": "Any.Number",
"cdpRequestWillBeSentReceivedTimestamp": "Any.Number",
"proxyRequestReceivedTimestamp": "Any.Number",
"cdpLagDuration": "Any.Number",
"proxyRequestCorrelationDuration": "Any.Number"
}
},
{
"requestId": "Any.Number",
"isCached": true,

View File

@@ -0,0 +1,103 @@
import path from 'path'
import fs from 'fs-extra'
import type { AppCaptureProtocolInterface, ResponseEndedWithEmptyBodyOptions, ResponseStreamOptions, ResponseStreamTimedOutOptions } from '@packages/types'
import type { Readable } from 'stream'
const getFilePath = (filename) => {
return path.join(
path.resolve(__dirname),
'cypress',
'system-tests-protocol-dbs',
`${filename}.json`,
)
}
export class AppCaptureProtocol implements AppCaptureProtocolInterface {
private filename: string
private events = {
numberOfFontRequests: 0,
}
private cdpClient: any
getDbMetadata (): { offset: number, size: number } {
return {
offset: 0,
size: 0,
}
}
responseStreamReceived (options: ResponseStreamOptions): Readable {
return options.responseStream
}
connectToBrowser = async (cdpClient) => {
if (cdpClient) {
this.cdpClient = cdpClient
}
this.cdpClient.on('Network.requestWillBeSent', (params) => {
// For the font flooding test, we want to count the number of font requests.
// There should only be 2 requests. One for each test in the spec.
if (params.type === 'Font') {
this.events.numberOfFontRequests += 1
}
})
}
addRunnables = (runnables) => {
return Promise.resolve()
}
beforeSpec = ({ archivePath, db }) => {
this.filename = getFilePath(path.basename(db.name))
if (!fs.existsSync(archivePath)) {
// If a dummy file hasn't been created by the test, write a tar file so that it can be fake uploaded
fs.writeFileSync(archivePath, '')
}
}
async afterSpec (): Promise<void> {
try {
fs.outputFileSync(this.filename, JSON.stringify(this.events, null, 2))
} catch (e) {
console.log('error writing protocol events', e)
}
}
beforeTest = (test) => {
return Promise.resolve()
}
commandLogAdded = (log) => {
}
commandLogChanged = (log) => {
}
viewportChanged = (input) => {
}
urlChanged = (input) => {
}
pageLoading = (input) => {
}
preAfterTest = (test, options) => {
return Promise.resolve()
}
afterTest = (test) => {
return Promise.resolve()
}
responseEndedWithEmptyBody = (options: ResponseEndedWithEmptyBodyOptions) => {
}
responseStreamTimedOut (options: ResponseStreamTimedOutOptions): void {
}
resetTest (testId: string): void {
}
}

View File

@@ -48,3 +48,5 @@ export const PROTOCOL_STUB_BEFORESPEC_ERROR = stub('protocolStubWithBeforeSpecEr
export const PROTOCOL_STUB_NONFATAL_ERROR = stub('protocolStubWithNonFatalError.ts')
export const PROTOCOL_STUB_BEFORETEST_ERROR = stub('protocolStubWithBeforeTestError.ts')
export const PROTOCOL_STUB_FONT_FLOODING = stub('protocolStubFontFlooding.ts')

View File

@@ -0,0 +1,11 @@
describe('font flooding', () => {
it('will not occur', () => {
cy.visit('cypress/fixtures/font-flooding.html')
cy.get('#btn').click().should('have.text', 'Clicked')
})
it('will not occur', () => {
cy.visit('cypress/fixtures/font-flooding.html')
cy.get('#btn').click().should('have.text', 'Clicked')
})
})

View File

@@ -0,0 +1,63 @@
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZJhiJ-Ek-_EeAmM.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZthiJ-Ek-_EeAmM.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZNhiJ-Ek-_EeAmM.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZxhiJ-Ek-_EeAmM.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZBhiJ-Ek-_EeAmM.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZFhiJ-Ek-_EeAmM.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiJ-Ek-_EeA.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

View File

@@ -0,0 +1,18 @@
<html>
<link rel="stylesheet" type="text/css" href="./font-flooding.css">
<style>
button {
font-size: large;
font-family: "Inter";
}
</style>
<body>
<h1>hello</h1>
<button id="btn">Click Me</button>
<script>
window.btn.addEventListener('click', () => {
window.btn.innerText = 'Clicked'
})
</script>
</body>
</html>

View File

@@ -0,0 +1,51 @@
const fs = require('fs-extra')
const path = require('path')
const systemTests = require('../lib/system-tests').default
const Fixtures = require('../lib/fixtures')
const {
createRoutes,
setupStubbedServer,
enableCaptureProtocol,
} = require('../lib/serverStub')
const { PROTOCOL_STUB_FONT_FLOODING } = require('../lib/protocol-stubs/protocolStubResponse')
const getFilePath = (filename) => {
return path.join(
Fixtures.projectPath('protocol'),
'cypress',
'system-tests-protocol-dbs',
`${filename}.json`,
)
}
const BROWSERS = ['chrome', 'electron']
describe('capture-protocol', () => {
setupStubbedServer(createRoutes())
enableCaptureProtocol(PROTOCOL_STUB_FONT_FLOODING)
describe('font flooding', () => {
BROWSERS.forEach((browser) => {
it(`verifies the number of font requests is correct - ${browser}`, function () {
return systemTests.exec(this, {
key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
project: 'protocol',
spec: 'font-flooding.cy.js',
record: true,
expectedExitCode: 0,
port: 2121,
browser,
config: {
hosts: {
'*foobar.com': '127.0.0.1',
},
},
}).then(() => {
const protocolEvents = fs.readFileSync(getFilePath('e9e81b5e-cc58-4026-b2ff-8ae3161435a6.db'), 'utf8')
expect(JSON.parse(protocolEvents).numberOfFontRequests).to.equal(2)
})
})
})
})
})

View File

@@ -43,6 +43,7 @@ describe('capture-protocol', () => {
record: true,
expectedExitCode: 0,
port: 2121,
spec: 'protocol.cy.js,test-isolation.cy.js',
config: {
hosts: {
'*foobar.com': '127.0.0.1',