From 31c7cd6d5d3930913eb4cccae44f020336c2b2be Mon Sep 17 00:00:00 2001 From: Blackbaud-SteveBrush Date: Fri, 24 Apr 2026 10:04:14 -0400 Subject: [PATCH 1/2] ci: remove focus rings for visual tests Co-authored-by: Copilot --- libs/sdk/cypress-commands/src/index.ts | 1 + .../src/lib/blur-before-screenshot-command.ts | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 libs/sdk/cypress-commands/src/lib/blur-before-screenshot-command.ts diff --git a/libs/sdk/cypress-commands/src/index.ts b/libs/sdk/cypress-commands/src/index.ts index 6f25d08881..07b20e5a8a 100644 --- a/libs/sdk/cypress-commands/src/index.ts +++ b/libs/sdk/cypress-commands/src/index.ts @@ -1,3 +1,4 @@ +import './lib/blur-before-screenshot-command'; import './lib/choose-theme-command'; import './lib/ready-command'; import './lib/visual-test-command'; diff --git a/libs/sdk/cypress-commands/src/lib/blur-before-screenshot-command.ts b/libs/sdk/cypress-commands/src/lib/blur-before-screenshot-command.ts new file mode 100644 index 0000000000..f7519211a1 --- /dev/null +++ b/libs/sdk/cypress-commands/src/lib/blur-before-screenshot-command.ts @@ -0,0 +1,28 @@ +/** + * Blur the active element before every screenshot capture. This prevents + * focus rings from appearing in screenshots when components auto-focus + * elements via setTimeout or similar async patterns. + */ +Cypress.Commands.overwrite( + 'screenshot', + function (originalFn, subject, ...args) { + // Blur the active element and wait a tick for the browser to repaint + // before taking the screenshot. + cy.document({ log: false }) + .then({ timeout: 1000 }, (doc) => { + (doc.activeElement as HTMLElement)?.blur(); + }) + .wait(0, { log: false }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const callOriginal = (): any => originalFn(subject, ...args); + + if (subject) { + return cy.wrap(subject, { log: false }).then(() => { + return callOriginal(); + }); + } + + return callOriginal(); + }, +); From 16238cd9c1b53fc5a051e467f5ae1d15ceffd1ac Mon Sep 17 00:00:00 2001 From: Blackbaud-SteveBrush Date: Fri, 24 Apr 2026 10:42:06 -0400 Subject: [PATCH 2/2] different approach Co-authored-by: Copilot --- .../src/e2e/colorpicker.component.cy.ts | 43 +++-- .../src/e2e/inline-form.component.cy.ts | 44 +++-- .../src/e2e/lookup.component.cy.ts | 174 ++++++++++-------- .../src/lib/blur-before-screenshot-command.ts | 46 +++-- 4 files changed, 175 insertions(+), 132 deletions(-) diff --git a/apps/e2e/colorpicker-storybook-e2e/src/e2e/colorpicker.component.cy.ts b/apps/e2e/colorpicker-storybook-e2e/src/e2e/colorpicker.component.cy.ts index 47811f841e..d05a81a602 100644 --- a/apps/e2e/colorpicker-storybook-e2e/src/e2e/colorpicker.component.cy.ts +++ b/apps/e2e/colorpicker-storybook-e2e/src/e2e/colorpicker.component.cy.ts @@ -39,13 +39,16 @@ describe('colorpicker-storybook', () => { cy.get('app-colorpicker') .should('exist') .should('be.visible') + .skyBlur() .screenshot(`colorpickercomponent-colorpicker--colorpicker-${theme}`); - cy.get('app-colorpicker').percySnapshot( - `colorpickercomponent-colorpicker--colorpicker-${theme}`, - { - widths: E2eVariations.DISPLAY_WIDTHS, - }, - ); + cy.get('app-colorpicker') + .skyBlur() + .percySnapshot( + `colorpickercomponent-colorpicker--colorpicker-${theme}`, + { + widths: E2eVariations.DISPLAY_WIDTHS, + }, + ); }); colorpickerVariations.forEach((colorpicker) => { @@ -68,18 +71,22 @@ describe('colorpicker-storybook', () => { cy.wrap($el.position().left).should('be.gte', 12); }); - cy.window().screenshot( - `colorpickercomponent-colorpicker--${colorpicker.id}-menu-${theme}`, - { - disableTimersAndAnimations: true, - }, - ); - cy.window().percySnapshot( - `colorpickercomponent-colorpicker--${colorpicker.id}-menu-${theme}`, - { - widths: E2eVariations.DISPLAY_WIDTHS, - }, - ); + cy.window() + .skyBlur() + .screenshot( + `colorpickercomponent-colorpicker--${colorpicker.id}-menu-${theme}`, + { + disableTimersAndAnimations: true, + }, + ); + cy.window() + .skyBlur() + .percySnapshot( + `colorpickercomponent-colorpicker--${colorpicker.id}-menu-${theme}`, + { + widths: E2eVariations.DISPLAY_WIDTHS, + }, + ); }); }); }); diff --git a/apps/e2e/inline-form-storybook-e2e/src/e2e/inline-form.component.cy.ts b/apps/e2e/inline-form-storybook-e2e/src/e2e/inline-form.component.cy.ts index e7ff90276e..2960b87332 100644 --- a/apps/e2e/inline-form-storybook-e2e/src/e2e/inline-form.component.cy.ts +++ b/apps/e2e/inline-form-storybook-e2e/src/e2e/inline-form.component.cy.ts @@ -8,15 +8,19 @@ describe('inline-form-storybook', () => { `/iframe.html?globals=theme:${theme}&id=inlineformcomponent-inlineform--inline-form-custom-buttons`, ); - cy.skyReady('app-inline-form').screenshot( - `inlineformcomponent-inlineform--inline-form-closed-${theme}`, - ); - cy.get('app-inline-form').percySnapshot( - `inlineformcomponent-inlineform--inline-form-closed-${theme}`, - { - widths: E2eVariations.DISPLAY_WIDTHS, - }, - ); + cy.skyReady('app-inline-form') + .skyBlur() + .screenshot( + `inlineformcomponent-inlineform--inline-form-closed-${theme}`, + ); + cy.get('app-inline-form') + .skyBlur() + .percySnapshot( + `inlineformcomponent-inlineform--inline-form-closed-${theme}`, + { + widths: E2eVariations.DISPLAY_WIDTHS, + }, + ); }); [ 'custom', @@ -40,15 +44,19 @@ describe('inline-form-storybook', () => { .should('be.visible') .click(); - cy.get('app-inline-form').screenshot( - `inlineformcomponent-inlineform--inline-form-${buttonCombo}-buttons-${theme}`, - ); - cy.get('app-inline-form').percySnapshot( - `inlineformcomponent-inlineform--inline-form-${buttonCombo}-buttons-${theme}`, - { - widths: E2eVariations.DISPLAY_WIDTHS, - }, - ); + cy.get('app-inline-form') + .skyBlur() + .screenshot( + `inlineformcomponent-inlineform--inline-form-${buttonCombo}-buttons-${theme}`, + ); + cy.get('app-inline-form') + .skyBlur() + .percySnapshot( + `inlineformcomponent-inlineform--inline-form-${buttonCombo}-buttons-${theme}`, + { + widths: E2eVariations.DISPLAY_WIDTHS, + }, + ); }); }); }); diff --git a/apps/e2e/lookup-storybook-e2e/src/e2e/lookup.component.cy.ts b/apps/e2e/lookup-storybook-e2e/src/e2e/lookup.component.cy.ts index bead49a1fb..6edcd4833a 100644 --- a/apps/e2e/lookup-storybook-e2e/src/e2e/lookup.component.cy.ts +++ b/apps/e2e/lookup-storybook-e2e/src/e2e/lookup.component.cy.ts @@ -18,15 +18,17 @@ describe('lookup-storybook', () => { .viewport(1300, 900), ); it(`should render the component`, () => { - cy.skyReady('app-lookup').screenshot( - `lookupcomponent-lookup--lookup-${mode}-${theme}`, - ); - cy.get('app-lookup').percySnapshot( - `lookupcomponent-lookup--lookup-${mode}-${theme}`, - { - widths: E2eVariations.DISPLAY_WIDTHS, - }, - ); + cy.skyReady('app-lookup') + .skyBlur() + .screenshot(`lookupcomponent-lookup--lookup-${mode}-${theme}`); + cy.get('app-lookup') + .skyBlur() + .percySnapshot( + `lookupcomponent-lookup--lookup-${mode}-${theme}`, + { + widths: E2eVariations.DISPLAY_WIDTHS, + }, + ); }); }); }); @@ -52,15 +54,19 @@ describe('lookup-storybook', () => { .should('exist') .should('be.visible'); - cy.get('app-lookup').screenshot( - `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-with-filtering-${theme}`, - ); - cy.get('app-lookup').percySnapshot( - `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-with-filtering-${theme}`, - { - widths: E2eVariations.DISPLAY_WIDTHS, - }, - ); + cy.get('app-lookup') + .skyBlur() + .screenshot( + `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-with-filtering-${theme}`, + ); + cy.get('app-lookup') + .skyBlur() + .percySnapshot( + `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-with-filtering-${theme}`, + { + widths: E2eVariations.DISPLAY_WIDTHS, + }, + ); }); it('should render show more dropdown without filtering', () => { cy.skyReady('app-lookup'); @@ -75,15 +81,19 @@ describe('lookup-storybook', () => { .should('exist') .should('be.visible'); - cy.get('app-lookup').screenshot( - `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-no-filtering-${theme}`, - ); - cy.get('app-lookup').percySnapshot( - `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-no-filtering-${theme}`, - { - widths: E2eVariations.DISPLAY_WIDTHS, - }, - ); + cy.get('app-lookup') + .skyBlur() + .screenshot( + `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-no-filtering-${theme}`, + ); + cy.get('app-lookup') + .skyBlur() + .percySnapshot( + `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-no-filtering-${theme}`, + { + widths: E2eVariations.DISPLAY_WIDTHS, + }, + ); }); it('should render show more dropdown with no results', () => { cy.skyReady('app-lookup'); @@ -97,15 +107,19 @@ describe('lookup-storybook', () => { .should('exist') .should('be.visible'); - cy.get('app-lookup').screenshot( - `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-no-results-${theme}`, - ); - cy.get('app-lookup').percySnapshot( - `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-no-results-${theme}`, - { - widths: E2eVariations.DISPLAY_WIDTHS, - }, - ); + cy.get('app-lookup') + .skyBlur() + .screenshot( + `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-no-results-${theme}`, + ); + cy.get('app-lookup') + .skyBlur() + .percySnapshot( + `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-no-results-${theme}`, + { + widths: E2eVariations.DISPLAY_WIDTHS, + }, + ); }); it('should render show more dropdown add button', () => { cy.skyReady('app-lookup'); @@ -120,15 +134,19 @@ describe('lookup-storybook', () => { .should('exist') .should('be.visible'); - cy.get('app-lookup').screenshot( - `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-add-more-button-${theme}`, - ); - cy.get('app-lookup').percySnapshot( - `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-add-more-button-${theme}`, - { - widths: E2eVariations.DISPLAY_WIDTHS, - }, - ); + cy.get('app-lookup') + .skyBlur() + .screenshot( + `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-add-more-button-${theme}`, + ); + cy.get('app-lookup') + .skyBlur() + .percySnapshot( + `lookupcomponent-lookup--lookup-${mode}-show-more-dropdown-add-more-button-${theme}`, + { + widths: E2eVariations.DISPLAY_WIDTHS, + }, + ); }); describe('in show more modal', () => { it('should open and render modal', () => { @@ -147,15 +165,19 @@ describe('lookup-storybook', () => { cy.get('.sky-modal').should('exist').should('be.visible'); - cy.get('app-lookup').screenshot( - `lookupcomponent-lookup--lookup-${mode}-show-more-modal-${theme}`, - ); - cy.get('app-lookup').percySnapshot( - `lookupcomponent-lookup--lookup-${mode}-show-more-modal-${theme}`, - { - widths: E2eVariations.DISPLAY_WIDTHS, - }, - ); + cy.get('app-lookup') + .skyBlur() + .screenshot( + `lookupcomponent-lookup--lookup-${mode}-show-more-modal-${theme}`, + ); + cy.get('app-lookup') + .skyBlur() + .percySnapshot( + `lookupcomponent-lookup--lookup-${mode}-show-more-modal-${theme}`, + { + widths: E2eVariations.DISPLAY_WIDTHS, + }, + ); }); it('should open and render modal with add more', () => { cy.skyReady('app-lookup'); @@ -173,15 +195,19 @@ describe('lookup-storybook', () => { cy.get('.sky-modal').should('exist').should('be.visible'); - cy.get('app-lookup').screenshot( - `lookupcomponent-lookup--lookup-${mode}-show-more-modal-add-more-${theme}`, - ); - cy.get('app-lookup').percySnapshot( - `lookupcomponent-lookup--lookup-${mode}-show-more-modal-add-more-${theme}`, - { - widths: E2eVariations.DISPLAY_WIDTHS, - }, - ); + cy.get('app-lookup') + .skyBlur() + .screenshot( + `lookupcomponent-lookup--lookup-${mode}-show-more-modal-add-more-${theme}`, + ); + cy.get('app-lookup') + .skyBlur() + .percySnapshot( + `lookupcomponent-lookup--lookup-${mode}-show-more-modal-add-more-${theme}`, + { + widths: E2eVariations.DISPLAY_WIDTHS, + }, + ); }); it('should open and render modal with preselected values', () => { cy.skyReady('app-lookup'); @@ -199,15 +225,19 @@ describe('lookup-storybook', () => { cy.get('.sky-modal').should('exist').should('be.visible'); - cy.get('app-lookup').screenshot( - `lookupcomponent-lookup--lookup-${mode}-show-more-modal-preselected-values-${theme}`, - ); - cy.get('app-lookup').percySnapshot( - `lookupcomponent-lookup--lookup-${mode}-show-more-modal-preselected-values-${theme}`, - { - widths: E2eVariations.DISPLAY_WIDTHS, - }, - ); + cy.get('app-lookup') + .skyBlur() + .screenshot( + `lookupcomponent-lookup--lookup-${mode}-show-more-modal-preselected-values-${theme}`, + ); + cy.get('app-lookup') + .skyBlur() + .percySnapshot( + `lookupcomponent-lookup--lookup-${mode}-show-more-modal-preselected-values-${theme}`, + { + widths: E2eVariations.DISPLAY_WIDTHS, + }, + ); }); }); }); diff --git a/libs/sdk/cypress-commands/src/lib/blur-before-screenshot-command.ts b/libs/sdk/cypress-commands/src/lib/blur-before-screenshot-command.ts index f7519211a1..6316e02d04 100644 --- a/libs/sdk/cypress-commands/src/lib/blur-before-screenshot-command.ts +++ b/libs/sdk/cypress-commands/src/lib/blur-before-screenshot-command.ts @@ -1,28 +1,26 @@ +// eslint-disable-next-line @typescript-eslint/no-namespace +declare namespace Cypress { + interface Chainable { + /** + * Blur the active element to remove focus rings before taking a screenshot. + */ + skyBlur(): Chainable; + } +} + /** - * Blur the active element before every screenshot capture. This prevents - * focus rings from appearing in screenshots when components auto-focus - * elements via setTimeout or similar async patterns. + * Blur the active element to remove focus rings before taking a screenshot. + * This prevents focus rings from appearing in screenshots when components + * auto-focus elements via setTimeout or similar async patterns. */ -Cypress.Commands.overwrite( - 'screenshot', - function (originalFn, subject, ...args) { - // Blur the active element and wait a tick for the browser to repaint - // before taking the screenshot. - cy.document({ log: false }) - .then({ timeout: 1000 }, (doc) => { - (doc.activeElement as HTMLElement)?.blur(); - }) - .wait(0, { log: false }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const callOriginal = (): any => originalFn(subject, ...args); +Cypress.Commands.add('skyBlur', { prevSubject: 'optional' }, (subject) => { + cy.document({ log: false }).then({ timeout: 1000 }, (doc) => { + (doc.activeElement as HTMLElement)?.blur(); + }); - if (subject) { - return cy.wrap(subject, { log: false }).then(() => { - return callOriginal(); - }); - } + if (subject) { + return cy.wrap(subject, { log: false }); + } - return callOriginal(); - }, -); + return; +});