From 737375784d834ae90791721d3b9ef999a796280b Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Thu, 13 Nov 2025 16:46:33 -0500 Subject: [PATCH 01/19] perf: reduce calls to getComputedStyles for element dimension calculations (used for snapshots) --- packages/app/src/runner/dimensions.ts | 38 ++++++++++++++++----------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/app/src/runner/dimensions.ts b/packages/app/src/runner/dimensions.ts index 3bfba4a8955..2f18f03f738 100644 --- a/packages/app/src/runner/dimensions.ts +++ b/packages/app/src/runner/dimensions.ts @@ -1,18 +1,22 @@ export const getElementDimensions = (el: HTMLElement) => { const { offsetHeight, offsetWidth } = el - const paddingTop = getStylePropertyNumber(el, 'padding-top') - const paddingRight = getStylePropertyNumber(el, 'padding-right') - const paddingBottom = getStylePropertyNumber(el, 'padding-bottom') - const paddingLeft = getStylePropertyNumber(el, 'padding-left') - const borderTop = getStylePropertyNumber(el, 'border-top-width') - const borderRight = getStylePropertyNumber(el, 'border-right-width') - const borderBottom = getStylePropertyNumber(el, 'border-bottom-width') - const borderLeft = getStylePropertyNumber(el, 'border-left-width') - const marginTop = getStylePropertyNumber(el, 'margin-top') - const marginRight = getStylePropertyNumber(el, 'margin-right') - const marginBottom = getStylePropertyNumber(el, 'margin-bottom') - const marginLeft = getStylePropertyNumber(el, 'margin-left') + // Call getComputedStyle once and cache the result to avoid + // multiple layout/reflow operations. + const computedStyle: CSSStyleDeclaration = getComputedStyle(el, null) + + const paddingTop = getStylePropertyNumberFromStyle(computedStyle, 'padding-top') + const paddingRight = getStylePropertyNumberFromStyle(computedStyle, 'padding-right') + const paddingBottom = getStylePropertyNumberFromStyle(computedStyle, 'padding-bottom') + const paddingLeft = getStylePropertyNumberFromStyle(computedStyle, 'padding-left') + const borderTop = getStylePropertyNumberFromStyle(computedStyle, 'border-top-width') + const borderRight = getStylePropertyNumberFromStyle(computedStyle, 'border-right-width') + const borderBottom = getStylePropertyNumberFromStyle(computedStyle, 'border-bottom-width') + const borderLeft = getStylePropertyNumberFromStyle(computedStyle, 'border-left-width') + const marginTop = getStylePropertyNumberFromStyle(computedStyle, 'margin-top') + const marginRight = getStylePropertyNumberFromStyle(computedStyle, 'margin-right') + const marginBottom = getStylePropertyNumberFromStyle(computedStyle, 'margin-bottom') + const marginLeft = getStylePropertyNumberFromStyle(computedStyle, 'margin-left') // NOTE: offsetWidth/height always give us content + padding + border, so subtract them // to get the true "clientHeight" and "clientWidth". @@ -68,8 +72,10 @@ export const getElementDimensions = (el: HTMLElement) => { export const setOffset = (el: HTMLElement, offset: { top: number, left: number }) => { const curOffset = getOffset(el) - const curTop = parseFloat(getComputedStyle(el, null).top) - const curLeft = parseFloat(getComputedStyle(el, null).left) + // Cache getComputedStyle result to avoid multiple layout operations + const computedStyle: CSSStyleDeclaration = getComputedStyle(el, null) + const curTop = parseFloat(computedStyle.top) + const curLeft = parseFloat(computedStyle.left) el.style.top = `${offset.top - curOffset.top + curTop}px` el.style.left = `${offset.left - curOffset.left + curLeft}px` @@ -87,8 +93,8 @@ export const getOffset = (el: HTMLElement) => { } } -const getStylePropertyNumber = (el: HTMLElement, property: string) => { - const value = parseFloat(getComputedStyle(el, null).getPropertyValue(property)) +const getStylePropertyNumberFromStyle = (computedStyle: CSSStyleDeclaration, property: string): number => { + const value = parseFloat(computedStyle.getPropertyValue(property)) if (isNaN(value)) { throw new Error('Element attr did not return a valid number') From 1cae27f45b534b748e3be08da1b2658028960e07 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Thu, 13 Nov 2025 17:00:43 -0500 Subject: [PATCH 02/19] consolidate the 2 calls to getComputedStyles further --- packages/app/src/runner/aut-iframe.ts | 19 +++++++++++-------- packages/app/src/runner/dimensions.ts | 7 +++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/app/src/runner/aut-iframe.ts b/packages/app/src/runner/aut-iframe.ts index af8f24b3d9a..387202622b1 100644 --- a/packages/app/src/runner/aut-iframe.ts +++ b/packages/app/src/runner/aut-iframe.ts @@ -307,17 +307,17 @@ export class AutIframe { // bail if our el no longer exists in the parent body if (!this.$.contains(body, element)) return - // switch to using offsetWidth + offsetHeight - // because we want to highlight our element even - // if it only has margin and zero content height / width - const dimensions = this._getOffsetSize($_el.get(0)) + // Get all dimensions and computed styles in one call to avoid multiple getComputedStyle calls + const dimensions = getElementDimensions($_el.get(0)) // dont show anything if our element displaces nothing - if (dimensions.width === 0 || dimensions.height === 0 || $_el.css('display') === 'none') { + // Use offsetWidth/offsetHeight to check because we want to highlight our element even + // if it only has margin and zero content height / width + if (dimensions.offsetWidth === 0 || dimensions.offsetHeight === 0 || dimensions.display === 'none') { return } - this._addElementBoxModelLayers($_el, $body).setAttribute('data-highlight-el', `true`) + this._addElementBoxModelLayers($_el, $body, dimensions).setAttribute('data-highlight-el', `true`) }) if (coords) { @@ -622,13 +622,16 @@ export class AutIframe { } } - private _addElementBoxModelLayers ($el, $body) { + private _addElementBoxModelLayers ($el, $body, dimensions?: ReturnType) { $body = $body || $('body') const el = $el.get(0) const body = $body.get(0) - const dimensions = getElementDimensions(el) + // Use existing dimensions if provided to avoid redundant getComputedStyle calls + if (!dimensions) { + dimensions = getElementDimensions(el) + } const container = document.createElement('div') diff --git a/packages/app/src/runner/dimensions.ts b/packages/app/src/runner/dimensions.ts index 2f18f03f738..c3dcb98406c 100644 --- a/packages/app/src/runner/dimensions.ts +++ b/packages/app/src/runner/dimensions.ts @@ -43,6 +43,10 @@ export const getElementDimensions = (el: HTMLElement) => { // offset disregards margin but takes into account border + padding offset: getOffset(el), + // Include original offsetWidth/offsetHeight for direct access (equivalent to widthWithBorder/heightWithBorder) + offsetWidth, + offsetHeight, + paddingTop, paddingRight, paddingBottom, @@ -65,6 +69,9 @@ export const getElementDimensions = (el: HTMLElement) => { widthWithPadding, widthWithBorder, widthWithMargin, + + // Include display property from computed style to avoid additional getComputedStyle calls + display: computedStyle.display, } } From 4802b79176653669bdfdfd5f4f03317c36657c1d Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Thu, 13 Nov 2025 17:05:04 -0500 Subject: [PATCH 03/19] reduce call of this._contents --- packages/app/src/runner/aut-iframe.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app/src/runner/aut-iframe.ts b/packages/app/src/runner/aut-iframe.ts index 387202622b1..b576e0bf279 100644 --- a/packages/app/src/runner/aut-iframe.ts +++ b/packages/app/src/runner/aut-iframe.ts @@ -328,7 +328,9 @@ export class AutIframe { } removeHighlights = () => { - this._contents() && this._contents()?.find('.__cypress-highlight').remove() + const $contents = this._contents() + + $contents?.find('.__cypress-highlight').remove() } toggleSelectorPlayground = (isEnabled) => { From 9b1773933d178bfa58f7ce34a12a32aefdf80742 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Thu, 13 Nov 2025 17:08:39 -0500 Subject: [PATCH 04/19] few other reduced _contents calls --- packages/app/src/runner/aut-iframe.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/app/src/runner/aut-iframe.ts b/packages/app/src/runner/aut-iframe.ts index b576e0bf279..47dea37e64b 100644 --- a/packages/app/src/runner/aut-iframe.ts +++ b/packages/app/src/runner/aut-iframe.ts @@ -167,18 +167,24 @@ export class AutIframe { const Cypress = this.eventManager.getCypress() const { headStyles = undefined, bodyStyles = undefined } = Cypress ? Cypress.cy.getStyles(snapshot) : {} const { body, htmlAttrs } = snapshot - const contents = this._contents() - const $html = contents?.find('html') as any as JQuery + const $contents = this._contents() + + if (!$contents) return + + // Cache DOM queries to avoid redundant _contents() calls + const $html = $contents.find('html') as any as JQuery + const $head = $contents.find('head') as any as JQuery + const $body = $contents.find('body') as unknown as JQuery if ($html) { this._replaceHtmlAttrs($html, htmlAttrs) } - this._replaceHeadStyles(headStyles) + // Pass $head to avoid _replaceHeadStyles calling _contents() again + this._replaceHeadStyles(headStyles, $head) // remove the old body and replace with restored one - - this._body()?.remove() + $body?.remove() this._insertBodyStyles(body.get(), bodyStyles) $html?.append(body.get()) @@ -209,8 +215,12 @@ export class AutIframe { }) } - _replaceHeadStyles (styles: Record = {}) { - const $head = this._contents()?.find('head') + _replaceHeadStyles (styles: Record = {}, $head?: JQuery) { + // Use provided $head if available, otherwise query for it + if (!$head) { + $head = this._contents()?.find('head') as any as JQuery + } + const existingStyles = $head?.find('link[rel="stylesheet"],style') _.each(styles, (style, index) => { From 72a4112712409b62e4b8d4828140b72b0b3c4c98 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 14 Nov 2025 09:26:35 -0500 Subject: [PATCH 05/19] fix tscheck error --- packages/app/src/runner/aut-iframe.ts | 29 +++++++++++++-------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/app/src/runner/aut-iframe.ts b/packages/app/src/runner/aut-iframe.ts index 47dea37e64b..367d359aff2 100644 --- a/packages/app/src/runner/aut-iframe.ts +++ b/packages/app/src/runner/aut-iframe.ts @@ -641,9 +641,8 @@ export class AutIframe { const body = $body.get(0) // Use existing dimensions if provided to avoid redundant getComputedStyle calls - if (!dimensions) { - dimensions = getElementDimensions(el) - } + // Assign to non-optional variable so TypeScript knows it's defined + const elementDimensions: ReturnType = dimensions || getElementDimensions(el) const container = document.createElement('div') @@ -669,19 +668,19 @@ export class AutIframe { // rearrange the contents offset so // its inside of our border + padding obj = { - width: dimensions.width, - height: dimensions.height, - top: dimensions.offset.top + dimensions.borderTop + dimensions.paddingTop, - left: dimensions.offset.left + dimensions.borderLeft + dimensions.paddingLeft, + width: elementDimensions.width, + height: elementDimensions.height, + top: elementDimensions.offset.top + elementDimensions.borderTop + elementDimensions.paddingTop, + left: elementDimensions.offset.left + elementDimensions.borderLeft + elementDimensions.paddingLeft, } break default: obj = { - width: this._getDimensionsFor(dimensions, attr, 'width'), - height: this._getDimensionsFor(dimensions, attr, 'height'), - top: dimensions.offset.top, - left: dimensions.offset.left, + width: this._getDimensionsFor(elementDimensions, attr, 'width'), + height: this._getDimensionsFor(elementDimensions, attr, 'height'), + top: elementDimensions.offset.top, + left: elementDimensions.offset.left, } } @@ -689,13 +688,13 @@ export class AutIframe { // subtract what the actual marginTop + marginLeft // values are, since offset disregards margin completely if (attr === 'Margin') { - obj.top -= dimensions.marginTop - obj.left -= dimensions.marginLeft + obj.top -= elementDimensions.marginTop + obj.left -= elementDimensions.marginLeft } if (attr === 'Padding') { - obj.top += dimensions.borderTop - obj.left += dimensions.borderLeft + obj.top += elementDimensions.borderTop + obj.left += elementDimensions.borderLeft } // bail if the dimensions of this layer match the previous one From b12787fd54f6178bada6fa74ac72b53cc482ae4e Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 14 Nov 2025 09:55:28 -0500 Subject: [PATCH 06/19] Update the other dimensions file in the driver (used for blackout) --- packages/driver/src/dom/dimensions.ts | 50 ++++++++++++++++----------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/packages/driver/src/dom/dimensions.ts b/packages/driver/src/dom/dimensions.ts index 76928fbba6f..586b7b185e7 100644 --- a/packages/driver/src/dom/dimensions.ts +++ b/packages/driver/src/dom/dimensions.ts @@ -33,22 +33,27 @@ const getElementDimensions = ($el: JQuery) => { const { offsetHeight, offsetWidth } = el + // Call getComputedStyle once and cache the result to avoid + // multiple layout/reflow operations. + const computedStyle: CSSStyleDeclaration = getComputedStyle(el, null) + const box: Box = { // offset disregards margin but takes into account border + padding offset: $el.offset(), // dont use jquery here for width/height because it uses getBoundingClientRect() which returns scaled values. - paddingTop: getPadding($el, 'top'), - paddingRight: getPadding($el, 'right'), - paddingBottom: getPadding($el, 'bottom'), - paddingLeft: getPadding($el, 'left'), - borderTop: getBorder($el, 'top'), - borderRight: getBorder($el, 'right'), - borderBottom: getBorder($el, 'bottom'), - borderLeft: getBorder($el, 'left'), - marginTop: getMargin($el, 'top'), - marginRight: getMargin($el, 'right'), - marginBottom: getMargin($el, 'bottom'), - marginLeft: getMargin($el, 'left'), + // Use cached computedStyle instead of calling $el.css() multiple times + paddingTop: getPaddingFromStyle(computedStyle, 'top'), + paddingRight: getPaddingFromStyle(computedStyle, 'right'), + paddingBottom: getPaddingFromStyle(computedStyle, 'bottom'), + paddingLeft: getPaddingFromStyle(computedStyle, 'left'), + borderTop: getBorderFromStyle(computedStyle, 'top'), + borderRight: getBorderFromStyle(computedStyle, 'right'), + borderBottom: getBorderFromStyle(computedStyle, 'bottom'), + borderLeft: getBorderFromStyle(computedStyle, 'left'), + marginTop: getMarginFromStyle(computedStyle, 'top'), + marginRight: getMarginFromStyle(computedStyle, 'right'), + marginBottom: getMarginFromStyle(computedStyle, 'bottom'), + marginLeft: getMarginFromStyle(computedStyle, 'left'), } // NOTE: offsetWidth/height always give us content + padding + border, so subtract them @@ -81,11 +86,13 @@ const getElementDimensions = ($el: JQuery) => { } type dir = 'top' | 'right' | 'bottom' | 'left' -type attr = `padding-${dir}` | `border-${dir}-width` | `margin-${dir}` -const getNumAttrValue = ($el: JQuery, attr: attr) => { +// Helper to extract numeric value from computed style property +// Replicates the behavior of $el.css(attr).replace(/[^0-9\.-]+/, '') +const getStylePropertyNumber = (computedStyle: CSSStyleDeclaration, property: string): number => { + const value = computedStyle.getPropertyValue(property) // nuke anything thats not a number or a negative symbol - const num = _.toNumber($el.css(attr).replace(/[^0-9\.-]+/, '')) + const num = _.toNumber(value.replace(/[^0-9\.-]+/, '')) if (!_.isFinite(num)) { throw new Error('Element attr did not return a valid number') @@ -94,16 +101,17 @@ const getNumAttrValue = ($el: JQuery, attr: attr) => { return num } -const getPadding = ($el: JQuery, dir: dir) => { - return getNumAttrValue($el, `padding-${dir}`) +// Optimized versions that read from cached computedStyle +const getPaddingFromStyle = (computedStyle: CSSStyleDeclaration, dir: dir): number => { + return getStylePropertyNumber(computedStyle, `padding-${dir}`) } -const getBorder = ($el: JQuery, dir: dir) => { - return getNumAttrValue($el, `border-${dir}-width`) +const getBorderFromStyle = (computedStyle: CSSStyleDeclaration, dir: dir): number => { + return getStylePropertyNumber(computedStyle, `border-${dir}-width`) } -const getMargin = ($el: JQuery, dir: dir) => { - return getNumAttrValue($el, `margin-${dir}`) +const getMarginFromStyle = (computedStyle: CSSStyleDeclaration, dir: dir): number => { + return getStylePropertyNumber(computedStyle, `margin-${dir}`) } export default { From da0dd952ef334fcf6b1b420ea1be27eb1ac27f87 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 14 Nov 2025 09:55:44 -0500 Subject: [PATCH 07/19] optimize blackout a bit --- packages/driver/src/dom/blackout.ts | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/packages/driver/src/dom/blackout.ts b/packages/driver/src/dom/blackout.ts index 4fa75bb90c6..26691fe03f6 100644 --- a/packages/driver/src/dom/blackout.ts +++ b/packages/driver/src/dom/blackout.ts @@ -1,16 +1,6 @@ import $ from 'jquery' import $dimensions from './dimensions' -const resetStyles = ` - border: none !important; - margin: 0 !important; - padding: 0 !important; -` - -const styles = (styleString) => { - return styleString.replace(/\s*\n\s*/g, '') -} - function addBlackoutForElement ($body: JQuery, $el: JQuery) { const dimensions = $dimensions.getElementDimensions($el) const width = dimensions.widthWithBorder @@ -18,18 +8,13 @@ function addBlackoutForElement ($body: JQuery, $el: JQuery`).appendTo($body) + const style = `border: none !important; margin: 0 !important; padding: 0 !important; position: absolute; top: ${top}px; left: ${left}px; width: ${width}px; height: ${height}px; background-color: black; z-index: 2147483647;` + + const div = document.createElement('div') + + div.className = '__cypress-blackout' + div.style.cssText = style + $body.append(div) } function addBlackouts ($body: JQuery, $container: JQuery, selector: string) { From 0eff22bc5abf442b49c1349930f44da2ed300183 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 14 Nov 2025 09:55:52 -0500 Subject: [PATCH 08/19] remove old todo --- packages/app/src/runner/iframe-model.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app/src/runner/iframe-model.ts b/packages/app/src/runner/iframe-model.ts index 7eabb3c44cc..99888b12e65 100644 --- a/packages/app/src/runner/iframe-model.ts +++ b/packages/app/src/runner/iframe-model.ts @@ -153,7 +153,6 @@ export class IframeModel { this._showSnapshotVue(snapshots[0], snapshotProps) } - /// todo(lachlan): UNIFY-1318 - figure out shape of these two args _showSnapshotVue = (snapshot: any, snapshotProps: AutSnapshot) => { const snapshotStore = useSnapshotStore() From 1dc83bf41dfafe1fabcc44e0b746ccbe75b67adf Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 14 Nov 2025 09:56:11 -0500 Subject: [PATCH 09/19] make further optimizations to performance --- packages/app/src/runner/aut-iframe.ts | 53 ++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/packages/app/src/runner/aut-iframe.ts b/packages/app/src/runner/aut-iframe.ts index 367d359aff2..376db6e752e 100644 --- a/packages/app/src/runner/aut-iframe.ts +++ b/packages/app/src/runner/aut-iframe.ts @@ -311,6 +311,11 @@ export class AutIframe { } } + // Collect all containers first, then append in a single batch to minimize reflows + const containers: HTMLElement[] = [] + const elementsToProcess: Array<{ $el: any, dimensions: ReturnType }> = [] + + // collect all valid elements and their dimensions $el.each((__, element) => { const $_el = this.$(element) @@ -327,9 +332,29 @@ export class AutIframe { return } - this._addElementBoxModelLayers($_el, $body, dimensions).setAttribute('data-highlight-el', `true`) + elementsToProcess.push({ $el: $_el, dimensions }) }) + // create all containers (off-DOM) + elementsToProcess.forEach(({ $el: $_el, dimensions }) => { + const container = this._addElementBoxModelLayers($_el, $body, dimensions) + + container.setAttribute('data-highlight-el', `true`) + containers.push(container) + }) + + // batch append all containers at once to minimize reflows + if (containers.length > 0) { + // Use DocumentFragment for even better performance when appending many elements + const fragment = document.createDocumentFragment() + + containers.forEach((container) => { + fragment.appendChild(container) + }) + + body.appendChild(fragment) + } + if (coords) { requestAnimationFrame(() => { this._addHitBoxLayer(coords, $body.get(0)).setAttribute('data-highlight-hitbox', 'true') @@ -340,7 +365,18 @@ export class AutIframe { removeHighlights = () => { const $contents = this._contents() - $contents?.find('.__cypress-highlight').remove() + if (!$contents) return + + const contentsElement = $contents[0] as Document | Element + + if (!contentsElement || typeof contentsElement.querySelectorAll !== 'function') { + return + } + + const highlights = contentsElement.querySelectorAll('.__cypress-highlight') + + // Batch remove using native DOM API + highlights.forEach((el) => el.remove()) } toggleSelectorPlayground = (isEnabled) => { @@ -638,7 +674,6 @@ export class AutIframe { $body = $body || $('body') const el = $el.get(0) - const body = $body.get(0) // Use existing dimensions if provided to avoid redundant getComputedStyle calls // Assign to non-optional variable so TypeScript knows it's defined @@ -704,14 +739,14 @@ export class AutIframe { this._createLayer($el.get(0), attr, color, container, obj) }) - body.appendChild(container) - + // Set offsets before appending to DOM to avoid layout thrashing + // This way all layout calculations happen off-DOM, then we append once for (let i = 0; i < container.children.length; i++) { - const el = container.children[i] as HTMLElement - const top = parseFloat(el.getAttribute('data-top')!) - const left = parseFloat(el.getAttribute('data-left')!) + const childEl = container.children[i] as HTMLElement + const top = parseFloat(childEl.getAttribute('data-top')!) + const left = parseFloat(childEl.getAttribute('data-left')!) - setOffset(el, { top, left }) + setOffset(childEl, { top, left }) } return container From 4f8c8a2f12cb296321b969067a2b6dddfb24a3f0 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 14 Nov 2025 09:56:21 -0500 Subject: [PATCH 10/19] write a dimensions test --- packages/app/src/runner/dimensions.cy.tsx | 58 +++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 packages/app/src/runner/dimensions.cy.tsx diff --git a/packages/app/src/runner/dimensions.cy.tsx b/packages/app/src/runner/dimensions.cy.tsx new file mode 100644 index 00000000000..d98b9955574 --- /dev/null +++ b/packages/app/src/runner/dimensions.cy.tsx @@ -0,0 +1,58 @@ +import { getElementDimensions } from './dimensions' + +describe('dimensions utilities', () => { + describe('getElementDimensions', () => { + let mockGetComputedStyle: typeof getComputedStyle + let getComputedStyleCallCount: number + + beforeEach(() => { + getComputedStyleCallCount = 0 + + mockGetComputedStyle = window.getComputedStyle + window.getComputedStyle = (element: Element, pseudoElement?: string | null) => { + getComputedStyleCallCount++ + + return mockGetComputedStyle.call(window, element, pseudoElement) + } + }) + + afterEach(() => { + window.getComputedStyle = mockGetComputedStyle + }) + + it('should call getComputedStyle only once per element', () => { + const el = document.createElement('div') + + el.style.padding = '10px' + el.style.margin = '20px' + el.style.border = '5px solid black' + el.style.width = '100px' + el.style.height = '50px' + el.style.display = 'block' + + document.body.appendChild(el) + + const dimensions = getElementDimensions(el) + + // Verify getComputedStyle was called exactly once + // to ensure this remains performant + expect(getComputedStyleCallCount).to.equal(1) + + expect(dimensions.paddingTop).to.equal(10) + expect(dimensions.paddingRight).to.equal(10) + expect(dimensions.paddingBottom).to.equal(10) + expect(dimensions.paddingLeft).to.equal(10) + expect(dimensions.marginTop).to.equal(20) + expect(dimensions.marginRight).to.equal(20) + expect(dimensions.marginBottom).to.equal(20) + expect(dimensions.marginLeft).to.equal(20) + expect(dimensions.borderTop).to.equal(5) + expect(dimensions.borderRight).to.equal(5) + expect(dimensions.borderBottom).to.equal(5) + expect(dimensions.borderLeft).to.equal(5) + expect(dimensions.display).to.equal('block') + + document.body.removeChild(el) + }) + }) +}) From 1d0d6485584c2d9cbffc9faa4999a44805abcde9 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 14 Nov 2025 11:44:06 -0500 Subject: [PATCH 11/19] few more updates + tests --- packages/app/src/runner/aut-iframe.cy.tsx | 137 ++++++++++++++++++++++ packages/app/src/runner/aut-iframe.ts | 63 +++++----- packages/app/src/runner/dimensions.ts | 14 ++- 3 files changed, 184 insertions(+), 30 deletions(-) create mode 100644 packages/app/src/runner/aut-iframe.cy.tsx diff --git a/packages/app/src/runner/aut-iframe.cy.tsx b/packages/app/src/runner/aut-iframe.cy.tsx new file mode 100644 index 00000000000..ec202cf6f37 --- /dev/null +++ b/packages/app/src/runner/aut-iframe.cy.tsx @@ -0,0 +1,137 @@ +import { AutIframe } from './aut-iframe' +import { createEventManager } from '../../cypress/component/support/ctSupport' +import { getElementDimensions } from './dimensions' + +describe('AutIframe._addElementBoxModelLayers', () => { + let autIframe: AutIframe + let mockGetComputedStyle: typeof getComputedStyle + let getComputedStyleCallCount: number + let mockJQuery: any + + beforeEach(() => { + getComputedStyleCallCount = 0 + + mockGetComputedStyle = window.getComputedStyle + window.getComputedStyle = (element: Element, pseudoElement?: string | null) => { + getComputedStyleCallCount++ + + return mockGetComputedStyle.call(window, element, pseudoElement) + } + + mockJQuery = (selector: any) => { + if (typeof selector === 'string') { + return { + get: (index?: number) => { + if (selector === 'body') { + return index === 0 ? document.body : [document.body] + } + + return null + }, + } + } + + if (selector && selector.nodeType) { + return { + get: (index?: number) => { + return index === 0 ? selector : [selector] + }, + } + } + + return { + get: () => null, + } + } + + const eventManager = createEventManager() + + autIframe = new AutIframe('Test Project', eventManager, mockJQuery) + }) + + afterEach(() => { + window.getComputedStyle = mockGetComputedStyle + }) + + it('should not call getComputedStyle when dimensions are provided', () => { + const testElement = document.createElement('div') + + testElement.style.width = '100px' + testElement.style.height = '50px' + testElement.style.padding = '10px' + testElement.style.border = '5px solid black' + testElement.style.margin = '15px' + testElement.style.position = 'absolute' + testElement.style.top = '20px' + testElement.style.left = '30px' + testElement.style.display = 'block' + testElement.style.transform = 'translateX(10px)' + testElement.style.zIndex = '100' + document.body.appendChild(testElement) + + // Get dimensions first (this will call getComputedStyle once) + const dimensions = getElementDimensions(testElement) + + // Verify dimensions include transform and zIndex + expect(dimensions.transform).to.exist + expect(dimensions.zIndex).to.exist + + // Reset the counter since getElementDimensions also calls getComputedStyle + getComputedStyleCallCount = 0 + + const $el = mockJQuery(testElement) + const $body = mockJQuery('body') + + // When dimensions are provided, _addElementBoxModelLayers should NOT call getComputedStyle + const container = (autIframe as any)._addElementBoxModelLayers($el, $body, dimensions) + + // Verify getComputedStyle was NOT called in _addElementBoxModelLayers + // (it should use transform and zIndex from the provided dimensions) + expect(getComputedStyleCallCount).to.equal(0, 'getComputedStyle should not be called when dimensions are provided') + + expect(container).to.exist + expect(container.classList.contains('__cypress-highlight')).to.be.true + expect(container.children.length).to.be.greaterThan(0, 'Should create at least one layer') + + const layers = Array.from(container.children) as HTMLElement[] + + layers.forEach((layer) => { + expect(layer.style.position).to.equal('absolute') + expect(layer.style.top).to.match(/^\d+px$/, 'Top should be set as pixels') + expect(layer.style.left).to.match(/^\d+px$/, 'Left should be set as pixels') + expect(layer.getAttribute('data-layer')).to.exist + // Verify transform and zIndex were applied from dimensions + expect(layer.style.transform).to.equal('translateX(10px)') + expect(layer.style.zIndex).to.equal('100') + }) + + document.body.removeChild(testElement) + }) + + it('should call getComputedStyle only once when dimensions are not provided', () => { + const testElement = document.createElement('div') + + testElement.style.width = '100px' + testElement.style.height = '50px' + testElement.style.display = 'block' + document.body.appendChild(testElement) + + getComputedStyleCallCount = 0 + + const $el = mockJQuery(testElement) + const $body = mockJQuery('body') + + // Call without providing dimensions (will call getElementDimensions internally) + const container = (autIframe as any)._addElementBoxModelLayers($el, $body) + + // getElementDimensions will call getComputedStyle once and return transform/zIndex, + // so _addElementBoxModelLayers won't need to call it again + // We expect only 1 call total (from getElementDimensions) + expect(getComputedStyleCallCount).to.equal(1, 'Should call getComputedStyle only once in getElementDimensions') + + expect(container).to.exist + expect(container.children.length).to.be.greaterThan(0) + + document.body.removeChild(testElement) + }) +}) diff --git a/packages/app/src/runner/aut-iframe.ts b/packages/app/src/runner/aut-iframe.ts index 376db6e752e..c23e63050e8 100644 --- a/packages/app/src/runner/aut-iframe.ts +++ b/packages/app/src/runner/aut-iframe.ts @@ -6,7 +6,7 @@ import _ from 'lodash' import type { DebouncedFunc } from 'lodash' import { useStudioStore } from '../store/studio-store' import { getElementDimensions, setOffset } from './dimensions' -import { getOrCreateHelperDom, getSelectorHighlightStyles, getZIndex, INT32_MAX } from './dom' +import { getOrCreateHelperDom, getSelectorHighlightStyles, INT32_MAX } from './dom' import highlightMounter from './selector-playground/highlight-mounter' import Highlight from './selector-playground/Highlight.ce.vue' @@ -320,7 +320,9 @@ export class AutIframe { const $_el = this.$(element) // bail if our el no longer exists in the parent body - if (!this.$.contains(body, element)) return + if (!this.$.contains(body, element)) { + return + } // Get all dimensions and computed styles in one call to avoid multiple getComputedStyle calls const dimensions = getElementDimensions($_el.get(0)) @@ -353,6 +355,17 @@ export class AutIframe { }) body.appendChild(fragment) + + // Now that containers are in DOM, set offsets using setOffset (which uses getBoundingClientRect) + containers.forEach((container) => { + for (let i = 0; i < container.children.length; i++) { + const childEl = container.children[i] as HTMLElement + const top = parseFloat(childEl.getAttribute('data-top')!) + const left = parseFloat(childEl.getAttribute('data-left')!) + + setOffset(childEl, { top, left }) + } + }) } if (coords) { @@ -663,29 +676,25 @@ export class AutIframe { return box } - private _getOffsetSize (el: HTMLElement) { - return { - width: el.offsetWidth, - height: el.offsetHeight, - } - } - private _addElementBoxModelLayers ($el, $body, dimensions?: ReturnType) { $body = $body || $('body') const el = $el.get(0) // Use existing dimensions if provided to avoid redundant getComputedStyle calls - // Assign to non-optional variable so TypeScript knows it's defined const elementDimensions: ReturnType = dimensions || getElementDimensions(el) + // Ensure transform and zIndex are valid (should always be set by getElementDimensions) + const transform = elementDimensions.transform || 'none' + const zIndex = elementDimensions.zIndex ?? 2147483647 + const container = document.createElement('div') container.classList.add('__cypress-highlight') - container.style.opacity = `0.7` + container.style.opacity = '0.7' container.style.position = 'absolute' - container.style.zIndex = `${INT32_MAX}` + container.style.zIndex = INT32_MAX.toString() const layers = { Content: '#9FC4E7', @@ -734,36 +743,32 @@ export class AutIframe { // bail if the dimensions of this layer match the previous one // so we dont create unnecessary layers - if (this._dimensionsMatchPreviousLayer(obj, container)) return + if (this._dimensionsMatchPreviousLayer(obj, container)) { + return + } - this._createLayer($el.get(0), attr, color, container, obj) + this._createLayer(attr, color, container, obj, transform, zIndex) }) - // Set offsets before appending to DOM to avoid layout thrashing - // This way all layout calculations happen off-DOM, then we append once - for (let i = 0; i < container.children.length; i++) { - const childEl = container.children[i] as HTMLElement - const top = parseFloat(childEl.getAttribute('data-top')!) - const left = parseFloat(childEl.getAttribute('data-left')!) - - setOffset(childEl, { top, left }) - } - + // Note: setOffset will be called after container is appended to DOM + // The offsets are stored in data-top/data-left attributes for now return container } - private _createLayer (el, attr, color, container, dimensions) { + private _createLayer (attr, color, container, dimensions, transform: string, zIndex: number) { const div = document.createElement('div') - div.style.transform = getComputedStyle(el, null).transform + // Set transform directly (original code always set it, even if 'none') + div.style.transform = transform + div.style.width = `${dimensions.width}px` div.style.height = `${dimensions.height}px` div.style.position = 'absolute' - div.style.zIndex = `${getZIndex(el)}` + div.style.zIndex = `${zIndex}` div.style.backgroundColor = color - div.setAttribute('data-top', dimensions.top) - div.setAttribute('data-left', dimensions.left) + div.setAttribute('data-top', dimensions.top.toString()) + div.setAttribute('data-left', dimensions.left.toString()) div.setAttribute('data-layer', attr) container.prepend(div) diff --git a/packages/app/src/runner/dimensions.ts b/packages/app/src/runner/dimensions.ts index c3dcb98406c..4ff01567bc8 100644 --- a/packages/app/src/runner/dimensions.ts +++ b/packages/app/src/runner/dimensions.ts @@ -39,6 +39,16 @@ export const getElementDimensions = (el: HTMLElement) => { const widthWithBorder = widthWithPadding + borderLeft + borderRight const widthWithMargin = widthWithBorder + marginLeft + marginRight + // Extract transform and z-index from computed style to avoid additional getComputedStyle calls + // Use .transform property directly to match original behavior (getComputedStyle(el, null).transform) + // Ensure it's always a string (fallback to 'none' if undefined/null) + const transform = computedStyle.transform || 'none' + const zIndexValue = computedStyle.getPropertyValue('z-index') + // Use INT32_MAX for auto/0 z-index values (matching getZIndex behavior) + const INT32_MAX = 2147483647 + const parsedZIndex = parseFloat(zIndexValue) + const zIndex = /^(auto|0)$/.test(zIndexValue) || isNaN(parsedZIndex) ? INT32_MAX : parsedZIndex + return { // offset disregards margin but takes into account border + padding offset: getOffset(el), @@ -70,8 +80,10 @@ export const getElementDimensions = (el: HTMLElement) => { widthWithBorder, widthWithMargin, - // Include display property from computed style to avoid additional getComputedStyle calls + // Include display, transform, and zIndex from computed style to avoid additional getComputedStyle calls display: computedStyle.display, + transform, + zIndex, } } From 8ada8ad11f72fdfb1a2c0c81aed7c55769fba638 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 14 Nov 2025 12:50:46 -0500 Subject: [PATCH 12/19] IMplement batching of offset and canceling of highlights --- packages/app/src/runner/aut-iframe.ts | 57 +++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/packages/app/src/runner/aut-iframe.ts b/packages/app/src/runner/aut-iframe.ts index c23e63050e8..78619143d3e 100644 --- a/packages/app/src/runner/aut-iframe.ts +++ b/packages/app/src/runner/aut-iframe.ts @@ -20,6 +20,7 @@ export class AutIframe { debouncedToggleSelectorPlayground: DebouncedFunc<(isEnabled: any) => void> $iframe?: JQuery _highlightedEl?: Element + private _highlightingCancelled: boolean = false constructor ( private projectName: string, @@ -286,7 +287,11 @@ export class AutIframe { } highlightEl = ({ body }, { $el, coords, highlightAttr, scrollBy }) => { + // Cancel any ongoing highlighting operation + this._highlightingCancelled = true this.removeHighlights() + // Reset cancellation flag for this new highlighting operation + this._highlightingCancelled = false if (body) { $el = body.get().find(`[${highlightAttr}]`) @@ -357,15 +362,53 @@ export class AutIframe { body.appendChild(fragment) // Now that containers are in DOM, set offsets using setOffset (which uses getBoundingClientRect) - containers.forEach((container) => { - for (let i = 0; i < container.children.length; i++) { - const childEl = container.children[i] as HTMLElement - const top = parseFloat(childEl.getAttribute('data-top')!) - const left = parseFloat(childEl.getAttribute('data-left')!) + // Batch setOffset calls to avoid layout thrashing - process in chunks using requestAnimationFrame + const OFFSET_BATCH_SIZE = 100 + let offsetIndex = 0 + + const processOffsetBatch = () => { + // Check if highlighting was cancelled (e.g., user switched to different snapshot) + if (this._highlightingCancelled) { + return + } + + const endIndex = Math.min(offsetIndex + OFFSET_BATCH_SIZE, containers.length) + + for (let j = offsetIndex; j < endIndex; j++) { + const container = containers[j] + + // Check if container is still in the DOM (might have been removed by removeHighlights) + if (!container.isConnected) { + continue + } + + for (let i = 0; i < container.children.length; i++) { + const childEl = container.children[i] as HTMLElement + + // Check if element is still in the DOM before positioning + if (!childEl.isConnected) { + continue + } + + const top = parseFloat(childEl.getAttribute('data-top')!) + const left = parseFloat(childEl.getAttribute('data-left')!) - setOffset(childEl, { top, left }) + setOffset(childEl, { top, left }) + } } - }) + + offsetIndex = endIndex + + if (offsetIndex < containers.length) { + // Process next batch on next frame to avoid blocking + requestAnimationFrame(processOffsetBatch) + } else { + // All highlights have been positioned + } + } + + // Start processing offsets in batches + processOffsetBatch() } if (coords) { From cd0735faa097bf86de8e70053bd7c9ccdbf5fde0 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 14 Nov 2025 12:56:03 -0500 Subject: [PATCH 13/19] update test --- packages/app/src/runner/aut-iframe.cy.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/app/src/runner/aut-iframe.cy.tsx b/packages/app/src/runner/aut-iframe.cy.tsx index ec202cf6f37..d32fa7ca8da 100644 --- a/packages/app/src/runner/aut-iframe.cy.tsx +++ b/packages/app/src/runner/aut-iframe.cy.tsx @@ -97,8 +97,11 @@ describe('AutIframe._addElementBoxModelLayers', () => { layers.forEach((layer) => { expect(layer.style.position).to.equal('absolute') - expect(layer.style.top).to.match(/^\d+px$/, 'Top should be set as pixels') - expect(layer.style.left).to.match(/^\d+px$/, 'Left should be set as pixels') + // Verify positions are stored in data attributes (not style properties) + expect(layer.getAttribute('data-top')).to.exist + expect(layer.getAttribute('data-left')).to.exist + expect(parseFloat(layer.getAttribute('data-top')!)).to.be.a('number') + expect(parseFloat(layer.getAttribute('data-left')!)).to.be.a('number') expect(layer.getAttribute('data-layer')).to.exist // Verify transform and zIndex were applied from dimensions expect(layer.style.transform).to.equal('translateX(10px)') From 5037eab9444173dc03cfba7d0d31814846171446 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 14 Nov 2025 13:15:01 -0500 Subject: [PATCH 14/19] changelog entry --- cli/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 6e9465c8124..c1e97aefcba 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,6 +3,10 @@ _Released 11/18/2025 (PENDING)_ +**Performance:** + +- Improved performance when viewing command snapshots in the Command Log. Element highlighting is now significantly faster, especially when highlighting multiple elements or complex pages. This is achieved by reducing redundant style calculations and batching DOM operations to minimize browser reflows. Addressed in [#32951](https://github.com/cypress-io/cypress/pull/32951). + **Bugfixes:** - 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). From 6690c4bef8f549d4340e38dc6ba915a9ff54ac35 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 14 Nov 2025 13:23:39 -0500 Subject: [PATCH 15/19] fix true/false race condition of highlighting cancellation --- packages/app/src/runner/aut-iframe.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/app/src/runner/aut-iframe.ts b/packages/app/src/runner/aut-iframe.ts index 78619143d3e..f57fb279037 100644 --- a/packages/app/src/runner/aut-iframe.ts +++ b/packages/app/src/runner/aut-iframe.ts @@ -20,7 +20,7 @@ export class AutIframe { debouncedToggleSelectorPlayground: DebouncedFunc<(isEnabled: any) => void> $iframe?: JQuery _highlightedEl?: Element - private _highlightingCancelled: boolean = false + private _currentHighlightingId: number = 0 constructor ( private projectName: string, @@ -287,11 +287,13 @@ export class AutIframe { } highlightEl = ({ body }, { $el, coords, highlightAttr, scrollBy }) => { - // Cancel any ongoing highlighting operation - this._highlightingCancelled = true + // Cancel any ongoing highlighting operation by incrementing the operation ID + // This ensures any async work from previous calls will see a different ID and stop processing + this._currentHighlightingId++ this.removeHighlights() - // Reset cancellation flag for this new highlighting operation - this._highlightingCancelled = false + + // Capture the current operation ID for this highlighting operation + const highlightingId = this._currentHighlightingId if (body) { $el = body.get().find(`[${highlightAttr}]`) @@ -367,8 +369,9 @@ export class AutIframe { let offsetIndex = 0 const processOffsetBatch = () => { - // Check if highlighting was cancelled (e.g., user switched to different snapshot) - if (this._highlightingCancelled) { + // Check if this highlighting operation was cancelled (e.g., user switched to different snapshot) + // by comparing the captured operation ID with the current one + if (this._currentHighlightingId !== highlightingId) { return } @@ -413,6 +416,11 @@ export class AutIframe { if (coords) { requestAnimationFrame(() => { + // Check if this highlighting operation was cancelled before adding hitbox + if (this._currentHighlightingId !== highlightingId) { + return + } + this._addHitBoxLayer(coords, $body.get(0)).setAttribute('data-highlight-hitbox', 'true') }) } From c761e8f6fd9f523a64f8db0277abd07ed4d61665 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Tue, 18 Nov 2025 13:24:00 -0500 Subject: [PATCH 16/19] add body gaurd --- packages/app/src/runner/aut-iframe.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/app/src/runner/aut-iframe.ts b/packages/app/src/runner/aut-iframe.ts index f57fb279037..f53a0bd084a 100644 --- a/packages/app/src/runner/aut-iframe.ts +++ b/packages/app/src/runner/aut-iframe.ts @@ -421,7 +421,13 @@ export class AutIframe { return } - this._addHitBoxLayer(coords, $body.get(0)).setAttribute('data-highlight-hitbox', 'true') + const bodyElement = $body.get(0) + + if (!bodyElement) { + return + } + + this._addHitBoxLayer(coords, bodyElement).setAttribute('data-highlight-hitbox', 'true') }) } } From 4f166bea89e07ba91fc61f394486c61faf23978c Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Wed, 19 Nov 2025 13:00:07 -0500 Subject: [PATCH 17/19] fix tests --- packages/app/src/runner/aut-iframe.cy.tsx | 14 ++++++++++---- packages/app/src/runner/aut-iframe.ts | 2 +- packages/app/src/runner/dimensions.ts | 12 ++++++++++-- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/app/src/runner/aut-iframe.cy.tsx b/packages/app/src/runner/aut-iframe.cy.tsx index d32fa7ca8da..782362c9f59 100644 --- a/packages/app/src/runner/aut-iframe.cy.tsx +++ b/packages/app/src/runner/aut-iframe.cy.tsx @@ -31,7 +31,7 @@ describe('AutIframe._addElementBoxModelLayers', () => { } } - if (selector && selector.nodeType) { + if (selector && (selector.nodeType || selector instanceof HTMLElement || selector instanceof Element)) { return { get: (index?: number) => { return index === 0 ? selector : [selector] @@ -89,7 +89,9 @@ describe('AutIframe._addElementBoxModelLayers', () => { // (it should use transform and zIndex from the provided dimensions) expect(getComputedStyleCallCount).to.equal(0, 'getComputedStyle should not be called when dimensions are provided') - expect(container).to.exist + expect(container).to.not.be.undefined + expect(container).to.not.be.null + expect(container).to.be.instanceof(HTMLElement) expect(container.classList.contains('__cypress-highlight')).to.be.true expect(container.children.length).to.be.greaterThan(0, 'Should create at least one layer') @@ -104,7 +106,9 @@ describe('AutIframe._addElementBoxModelLayers', () => { expect(parseFloat(layer.getAttribute('data-left')!)).to.be.a('number') expect(layer.getAttribute('data-layer')).to.exist // Verify transform and zIndex were applied from dimensions - expect(layer.style.transform).to.equal('translateX(10px)') + // Note: getComputedStyle returns computed transform as a matrix, not the original CSS value + // So we check that transform is set (not 'none') and matches the computed value from dimensions + expect(layer.style.transform).to.equal(dimensions.transform) expect(layer.style.zIndex).to.equal('100') }) @@ -132,7 +136,9 @@ describe('AutIframe._addElementBoxModelLayers', () => { // We expect only 1 call total (from getElementDimensions) expect(getComputedStyleCallCount).to.equal(1, 'Should call getComputedStyle only once in getElementDimensions') - expect(container).to.exist + expect(container).to.not.be.undefined + expect(container).to.not.be.null + expect(container).to.be.instanceof(HTMLElement) expect(container.children.length).to.be.greaterThan(0) document.body.removeChild(testElement) diff --git a/packages/app/src/runner/aut-iframe.ts b/packages/app/src/runner/aut-iframe.ts index f53a0bd084a..3ee5b5333cb 100644 --- a/packages/app/src/runner/aut-iframe.ts +++ b/packages/app/src/runner/aut-iframe.ts @@ -734,7 +734,7 @@ export class AutIframe { } private _addElementBoxModelLayers ($el, $body, dimensions?: ReturnType) { - $body = $body || $('body') + $body = $body || this.$('body') const el = $el.get(0) diff --git a/packages/app/src/runner/dimensions.ts b/packages/app/src/runner/dimensions.ts index 4ff01567bc8..adbc2977c43 100644 --- a/packages/app/src/runner/dimensions.ts +++ b/packages/app/src/runner/dimensions.ts @@ -106,9 +106,17 @@ export const getOffset = (el: HTMLElement) => { const rect = el.getBoundingClientRect() const win = el.ownerDocument.defaultView + // Handle test environments where defaultView might be null + if (!win) { + return { + top: rect.top, + left: rect.left, + } + } + return { - top: rect.top + win!.scrollY, - left: rect.left + win!.scrollX, + top: rect.top + win.scrollY, + left: rect.left + win.scrollX, } } From 5bc6f8d35bac1ca8a1f96410d4c4ee6290e29d89 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Wed, 19 Nov 2025 14:47:06 -0500 Subject: [PATCH 18/19] fix changelog --- cli/CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index f4fceeca99d..3118b107081 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,4 +1,13 @@ + +## 15.7.1 + +_Released 12/2/2025 (PENDING)_ + +**Performance:** + +- Improved performance when viewing command snapshots in the Command Log. Element highlighting is now significantly faster, especially when highlighting multiple elements or complex pages. This is achieved by reducing redundant style calculations and batching DOM operations to minimize browser reflows. Addressed in [#32951](https://github.com/cypress-io/cypress/pull/32951). + ## 15.7.0 _Released 11/19/2025_ From 267486dca33ccff34388858a29a15599cce8c955 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Wed, 19 Nov 2025 14:49:58 -0500 Subject: [PATCH 19/19] remove extra line --- cli/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 3118b107081..6139ae9116b 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,5 +1,4 @@ - ## 15.7.1 _Released 12/2/2025 (PENDING)_