diff --git a/packages/eui/.storybook/test.ts b/packages/eui/.storybook/test.ts index fbabf0542cd2..920d1ff84334 100644 --- a/packages/eui/.storybook/test.ts +++ b/packages/eui/.storybook/test.ts @@ -37,10 +37,32 @@ const customWithin = (canvasElement: HTMLElement) => { await fireEvent.click(canvas.getByTestSubject(testSubject)); }, - waitForEuiPopoverVisible: async () => + waitForEuiPopoverVisible: async (anchorSelector?: string) => { await waitFor(() => expect(canvasElement.querySelector('[data-popover-open]')).toBeVisible() - ), + ); + + const { defaultView, fonts } = canvasElement.ownerDocument; + await fonts.ready; + defaultView?.dispatchEvent(new defaultView.Event('resize')); + + if (!anchorSelector) return; + + await waitFor(() => { + const panel = canvasElement.ownerDocument.querySelector( + '[data-popover-panel]' + ); + const anchor = canvasElement.querySelector(anchorSelector); + + if (!panel || !anchor) { + throw new Error('Popover did not render'); + } + + expect(panel.getBoundingClientRect().left).toBe( + anchor.getBoundingClientRect().left + ); + }); + }, waitForEuiPopoverHidden: async () => await waitFor(() => expect( diff --git a/packages/eui/.vrt/reference/display-euibetabadge--playground-desktop.png b/packages/eui/.vrt/reference/display-euibetabadge--playground-desktop.png index e6b816e6de7e..0f41a04aba3a 100644 Binary files a/packages/eui/.vrt/reference/display-euibetabadge--playground-desktop.png and b/packages/eui/.vrt/reference/display-euibetabadge--playground-desktop.png differ diff --git a/packages/eui/.vrt/reference/display-euibetabadge--playground-mobile.png b/packages/eui/.vrt/reference/display-euibetabadge--playground-mobile.png index e6b816e6de7e..0f41a04aba3a 100644 Binary files a/packages/eui/.vrt/reference/display-euibetabadge--playground-mobile.png and b/packages/eui/.vrt/reference/display-euibetabadge--playground-mobile.png differ diff --git a/packages/eui/.vrt/reference/display-euicomment-euicommentlist--high-contrast-desktop.png b/packages/eui/.vrt/reference/display-euicomment-euicommentlist--high-contrast-desktop.png index cce6e0e5e499..5e98f222007a 100644 Binary files a/packages/eui/.vrt/reference/display-euicomment-euicommentlist--high-contrast-desktop.png and b/packages/eui/.vrt/reference/display-euicomment-euicommentlist--high-contrast-desktop.png differ diff --git a/packages/eui/.vrt/reference/display-euicomment-euicommentlist--high-contrast-mobile.png b/packages/eui/.vrt/reference/display-euicomment-euicommentlist--high-contrast-mobile.png index 85adabd64eef..183868f23d3a 100644 Binary files a/packages/eui/.vrt/reference/display-euicomment-euicommentlist--high-contrast-mobile.png and b/packages/eui/.vrt/reference/display-euicomment-euicommentlist--high-contrast-mobile.png differ diff --git a/packages/eui/.vrt/reference/editors-syntax-euicodeblock--virtualized-code-block-scrolling-desktop.png b/packages/eui/.vrt/reference/editors-syntax-euicodeblock--virtualized-code-block-scrolling-desktop.png new file mode 100644 index 000000000000..149d80c71124 Binary files /dev/null and b/packages/eui/.vrt/reference/editors-syntax-euicodeblock--virtualized-code-block-scrolling-desktop.png differ diff --git a/packages/eui/.vrt/reference/editors-syntax-euicodeblock--virtualized-code-block-scrolling-mobile.png b/packages/eui/.vrt/reference/editors-syntax-euicodeblock--virtualized-code-block-scrolling-mobile.png new file mode 100644 index 000000000000..ffd775c832c5 Binary files /dev/null and b/packages/eui/.vrt/reference/editors-syntax-euicodeblock--virtualized-code-block-scrolling-mobile.png differ diff --git a/packages/eui/.vrt/reference/forms-euiexpression--high-contrast-desktop.png b/packages/eui/.vrt/reference/forms-euiexpression--high-contrast-desktop.png index c84b3afd838d..6e7194b071b0 100644 Binary files a/packages/eui/.vrt/reference/forms-euiexpression--high-contrast-desktop.png and b/packages/eui/.vrt/reference/forms-euiexpression--high-contrast-desktop.png differ diff --git a/packages/eui/.vrt/reference/forms-euiexpression--high-contrast-mobile.png b/packages/eui/.vrt/reference/forms-euiexpression--high-contrast-mobile.png index c84b3afd838d..8668d7b6f6a3 100644 Binary files a/packages/eui/.vrt/reference/forms-euiexpression--high-contrast-mobile.png and b/packages/eui/.vrt/reference/forms-euiexpression--high-contrast-mobile.png differ diff --git a/packages/eui/.vrt/reference/forms-euiexpression--kitchen-sink-desktop.png b/packages/eui/.vrt/reference/forms-euiexpression--kitchen-sink-desktop.png index ebedd8b8d79f..240d5f32b08e 100644 Binary files a/packages/eui/.vrt/reference/forms-euiexpression--kitchen-sink-desktop.png and b/packages/eui/.vrt/reference/forms-euiexpression--kitchen-sink-desktop.png differ diff --git a/packages/eui/.vrt/reference/forms-euiexpression--kitchen-sink-mobile.png b/packages/eui/.vrt/reference/forms-euiexpression--kitchen-sink-mobile.png index ebedd8b8d79f..4bd48d490b0b 100644 Binary files a/packages/eui/.vrt/reference/forms-euiexpression--kitchen-sink-mobile.png and b/packages/eui/.vrt/reference/forms-euiexpression--kitchen-sink-mobile.png differ diff --git a/packages/eui/.vrt/reference/layout-euiflyout-euiflyout--manual-return-focus-desktop.png b/packages/eui/.vrt/reference/layout-euiflyout-euiflyout--manual-return-focus-desktop.png deleted file mode 100644 index 8a6e857c7bcf..000000000000 Binary files a/packages/eui/.vrt/reference/layout-euiflyout-euiflyout--manual-return-focus-desktop.png and /dev/null differ diff --git a/packages/eui/.vrt/reference/layout-euiflyout-euiflyout--manual-return-focus-mobile.png b/packages/eui/.vrt/reference/layout-euiflyout-euiflyout--manual-return-focus-mobile.png deleted file mode 100644 index 59912bd6625c..000000000000 Binary files a/packages/eui/.vrt/reference/layout-euiflyout-euiflyout--manual-return-focus-mobile.png and /dev/null differ diff --git a/packages/eui/.vrt/reference/layout-euiheader-euiheaderalert--popover-example-desktop.png b/packages/eui/.vrt/reference/layout-euiheader-euiheaderalert--popover-example-desktop.png index a21b81fc076d..c7721de9a270 100644 Binary files a/packages/eui/.vrt/reference/layout-euiheader-euiheaderalert--popover-example-desktop.png and b/packages/eui/.vrt/reference/layout-euiheader-euiheaderalert--popover-example-desktop.png differ diff --git a/packages/eui/.vrt/reference/layout-euiheader-euiheaderalert--popover-example-mobile.png b/packages/eui/.vrt/reference/layout-euiheader-euiheaderalert--popover-example-mobile.png index 4f24eb9df75b..1492ffa119b6 100644 Binary files a/packages/eui/.vrt/reference/layout-euiheader-euiheaderalert--popover-example-mobile.png and b/packages/eui/.vrt/reference/layout-euiheader-euiheaderalert--popover-example-mobile.png differ diff --git a/packages/eui/.vrt/reference/layout-euiheader-euiheaderlinks--playground-mobile.png b/packages/eui/.vrt/reference/layout-euiheader-euiheaderlinks--playground-mobile.png index d403170dbb30..062a52bc5c01 100644 Binary files a/packages/eui/.vrt/reference/layout-euiheader-euiheaderlinks--playground-mobile.png and b/packages/eui/.vrt/reference/layout-euiheader-euiheaderlinks--playground-mobile.png differ diff --git a/packages/eui/.vrt/reference/layout-euipanel--overlapping-panels-desktop.png b/packages/eui/.vrt/reference/layout-euipanel--overlapping-panels-desktop.png index 8492035e0592..4c8a9d4032de 100644 Binary files a/packages/eui/.vrt/reference/layout-euipanel--overlapping-panels-desktop.png and b/packages/eui/.vrt/reference/layout-euipanel--overlapping-panels-desktop.png differ diff --git a/packages/eui/.vrt/reference/layout-euipanel--overlapping-panels-mobile.png b/packages/eui/.vrt/reference/layout-euipanel--overlapping-panels-mobile.png index b29457a1b786..19e77066c413 100644 Binary files a/packages/eui/.vrt/reference/layout-euipanel--overlapping-panels-mobile.png and b/packages/eui/.vrt/reference/layout-euipanel--overlapping-panels-mobile.png differ diff --git a/packages/eui/.vrt/reference/layout-euipopover-euipopover--playground-desktop.png b/packages/eui/.vrt/reference/layout-euipopover-euipopover--playground-desktop.png new file mode 100644 index 000000000000..86a1916198bd Binary files /dev/null and b/packages/eui/.vrt/reference/layout-euipopover-euipopover--playground-desktop.png differ diff --git a/packages/eui/.vrt/reference/layout-euipopover-euipopover--playground-mobile.png b/packages/eui/.vrt/reference/layout-euipopover-euipopover--playground-mobile.png new file mode 100644 index 000000000000..9202747ba1c1 Binary files /dev/null and b/packages/eui/.vrt/reference/layout-euipopover-euipopover--playground-mobile.png differ diff --git a/packages/eui/.vrt/reference/layout-euispacer--playground-desktop.png b/packages/eui/.vrt/reference/layout-euispacer--playground-desktop.png index d8f826ee3684..04226dce496f 100644 Binary files a/packages/eui/.vrt/reference/layout-euispacer--playground-desktop.png and b/packages/eui/.vrt/reference/layout-euispacer--playground-desktop.png differ diff --git a/packages/eui/.vrt/reference/layout-euispacer--playground-mobile.png b/packages/eui/.vrt/reference/layout-euispacer--playground-mobile.png index 7bf4bcdac522..8fc29af4f233 100644 Binary files a/packages/eui/.vrt/reference/layout-euispacer--playground-mobile.png and b/packages/eui/.vrt/reference/layout-euispacer--playground-mobile.png differ diff --git a/packages/eui/.vrt/reference/layout-euisplitpanel--high-contrast-desktop.png b/packages/eui/.vrt/reference/layout-euisplitpanel--high-contrast-desktop.png index 1de39bbf2550..2b93ea5f12b6 100644 Binary files a/packages/eui/.vrt/reference/layout-euisplitpanel--high-contrast-desktop.png and b/packages/eui/.vrt/reference/layout-euisplitpanel--high-contrast-desktop.png differ diff --git a/packages/eui/.vrt/reference/layout-euisplitpanel--high-contrast-mobile.png b/packages/eui/.vrt/reference/layout-euisplitpanel--high-contrast-mobile.png index 04c3e03a307f..c0ebcd8319bf 100644 Binary files a/packages/eui/.vrt/reference/layout-euisplitpanel--high-contrast-mobile.png and b/packages/eui/.vrt/reference/layout-euisplitpanel--high-contrast-mobile.png differ diff --git a/packages/eui/.vrt/reference/navigation-euicollapsiblenav-euicollapsiblenav--playground-desktop.png b/packages/eui/.vrt/reference/navigation-euicollapsiblenav-euicollapsiblenav--playground-desktop.png index 6726abfe7588..1b074511b861 100644 Binary files a/packages/eui/.vrt/reference/navigation-euicollapsiblenav-euicollapsiblenav--playground-desktop.png and b/packages/eui/.vrt/reference/navigation-euicollapsiblenav-euicollapsiblenav--playground-desktop.png differ diff --git a/packages/eui/.vrt/reference/navigation-euicollapsiblenav-euicollapsiblenav--playground-mobile.png b/packages/eui/.vrt/reference/navigation-euicollapsiblenav-euicollapsiblenav--playground-mobile.png index 6726abfe7588..bd0b628d51f2 100644 Binary files a/packages/eui/.vrt/reference/navigation-euicollapsiblenav-euicollapsiblenav--playground-mobile.png and b/packages/eui/.vrt/reference/navigation-euicollapsiblenav-euicollapsiblenav--playground-mobile.png differ diff --git a/packages/eui/.vrt/reference/navigation-euilink--external-desktop.png b/packages/eui/.vrt/reference/navigation-euilink--external-desktop.png new file mode 100644 index 000000000000..74352df29609 Binary files /dev/null and b/packages/eui/.vrt/reference/navigation-euilink--external-desktop.png differ diff --git a/packages/eui/.vrt/reference/navigation-euilink--external-mobile.png b/packages/eui/.vrt/reference/navigation-euilink--external-mobile.png new file mode 100644 index 000000000000..74352df29609 Binary files /dev/null and b/packages/eui/.vrt/reference/navigation-euilink--external-mobile.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--dark-mode-desktop.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--dark-mode-desktop.png index e98b2fe23863..1921a57c3c01 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--dark-mode-desktop.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--dark-mode-desktop.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--dark-mode-mobile.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--dark-mode-mobile.png index 0c33d1f6905b..cb9e26162762 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--dark-mode-mobile.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--dark-mode-mobile.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-nested-table-without-background-desktop.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-nested-table-without-background-desktop.png index 052b04388245..dda605331ef6 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-nested-table-without-background-desktop.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-nested-table-without-background-desktop.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-nested-table-without-background-mobile.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-nested-table-without-background-mobile.png index 0167cad9fc39..49fc757bc715 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-nested-table-without-background-mobile.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-nested-table-without-background-mobile.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-row-desktop.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-row-desktop.png index f8461dcc0326..db9e76f27935 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-row-desktop.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-row-desktop.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-row-mobile.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-row-mobile.png index fb6e23870622..246fd9561fab 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-row-mobile.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--expanded-row-mobile.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--high-contrast-mobile-desktop.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--high-contrast-mobile-desktop.png index 3c2ccaa5c2c8..d090b3c75aa4 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--high-contrast-mobile-desktop.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--high-contrast-mobile-desktop.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--high-contrast-mobile-mobile.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--high-contrast-mobile-mobile.png index d67e9f3dedc7..54643be541bf 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--high-contrast-mobile-mobile.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--high-contrast-mobile-mobile.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--large-expanded-row-desktop.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--large-expanded-row-desktop.png index 7f7cb5606883..5655cf6626a8 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--large-expanded-row-desktop.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--large-expanded-row-desktop.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--large-expanded-row-mobile.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--large-expanded-row-mobile.png index 51372ec88a21..38ff58cd328d 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--large-expanded-row-mobile.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--large-expanded-row-mobile.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--marked-row-desktop.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--marked-row-desktop.png index 2b53eadc7e1c..f77d3a9fa6c9 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--marked-row-desktop.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--marked-row-desktop.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--marked-row-mobile.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--marked-row-mobile.png index eba7285227be..571726da00ca 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--marked-row-mobile.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--marked-row-mobile.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--panelled-desktop.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--panelled-desktop.png index 993e73e3f4b8..98f89503ecae 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--panelled-desktop.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--panelled-desktop.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--panelled-mobile.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--panelled-mobile.png index 64b46f38a727..26d507f342ca 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--panelled-mobile.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--panelled-mobile.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--playground-desktop.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--playground-desktop.png index 2b53eadc7e1c..f77d3a9fa6c9 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--playground-desktop.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--playground-desktop.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--playground-mobile.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--playground-mobile.png index eba7285227be..571726da00ca 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--playground-mobile.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--playground-mobile.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--scrollable-desktop.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--scrollable-desktop.png index 6c3b760b826f..e5614375c46c 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--scrollable-desktop.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--scrollable-desktop.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euibasictable--scrollable-mobile.png b/packages/eui/.vrt/reference/tabular-content-euibasictable--scrollable-mobile.png index d72f1f0accda..f7728b3a8679 100644 Binary files a/packages/eui/.vrt/reference/tabular-content-euibasictable--scrollable-mobile.png and b/packages/eui/.vrt/reference/tabular-content-euibasictable--scrollable-mobile.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euidatagrid-gridstyle-prop--compact-desktop.png b/packages/eui/.vrt/reference/tabular-content-euidatagrid-gridstyle-prop--compact-desktop.png new file mode 100644 index 000000000000..50fad171c6f1 Binary files /dev/null and b/packages/eui/.vrt/reference/tabular-content-euidatagrid-gridstyle-prop--compact-desktop.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euidatagrid-gridstyle-prop--compact-mobile.png b/packages/eui/.vrt/reference/tabular-content-euidatagrid-gridstyle-prop--compact-mobile.png new file mode 100644 index 000000000000..e7f5ed638806 Binary files /dev/null and b/packages/eui/.vrt/reference/tabular-content-euidatagrid-gridstyle-prop--compact-mobile.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euidatagrid-toolbarvisibility-prop--column-selector-desktop.png b/packages/eui/.vrt/reference/tabular-content-euidatagrid-toolbarvisibility-prop--column-selector-desktop.png new file mode 100644 index 000000000000..305b3b23c6ad Binary files /dev/null and b/packages/eui/.vrt/reference/tabular-content-euidatagrid-toolbarvisibility-prop--column-selector-desktop.png differ diff --git a/packages/eui/.vrt/reference/tabular-content-euidatagrid-toolbarvisibility-prop--column-selector-mobile.png b/packages/eui/.vrt/reference/tabular-content-euidatagrid-toolbarvisibility-prop--column-selector-mobile.png new file mode 100644 index 000000000000..e7353b362a9e Binary files /dev/null and b/packages/eui/.vrt/reference/tabular-content-euidatagrid-toolbarvisibility-prop--column-selector-mobile.png differ diff --git a/packages/eui/.vrt/reference/utilities-euitexttruncate--resize-observer-desktop.png b/packages/eui/.vrt/reference/utilities-euitexttruncate--resize-observer-desktop.png index e30833e755e3..eedaef2189b7 100644 Binary files a/packages/eui/.vrt/reference/utilities-euitexttruncate--resize-observer-desktop.png and b/packages/eui/.vrt/reference/utilities-euitexttruncate--resize-observer-desktop.png differ diff --git a/packages/eui/.vrt/reference/utilities-euitexttruncate--resize-observer-mobile.png b/packages/eui/.vrt/reference/utilities-euitexttruncate--resize-observer-mobile.png index 085bc75e6a9b..22d51089657e 100644 Binary files a/packages/eui/.vrt/reference/utilities-euitexttruncate--resize-observer-mobile.png and b/packages/eui/.vrt/reference/utilities-euitexttruncate--resize-observer-mobile.png differ diff --git a/packages/eui/.vrt/reference/utilities-euitexttruncate--start-end-anchor-for-search-desktop.png b/packages/eui/.vrt/reference/utilities-euitexttruncate--start-end-anchor-for-search-desktop.png index fffed5a6e7e2..64ed5426c4d4 100644 Binary files a/packages/eui/.vrt/reference/utilities-euitexttruncate--start-end-anchor-for-search-desktop.png and b/packages/eui/.vrt/reference/utilities-euitexttruncate--start-end-anchor-for-search-desktop.png differ diff --git a/packages/eui/.vrt/reference/utilities-euitexttruncate--start-end-anchor-for-search-mobile.png b/packages/eui/.vrt/reference/utilities-euitexttruncate--start-end-anchor-for-search-mobile.png index e3bd8edd0b5d..4922ee9be2ab 100644 Binary files a/packages/eui/.vrt/reference/utilities-euitexttruncate--start-end-anchor-for-search-mobile.png and b/packages/eui/.vrt/reference/utilities-euitexttruncate--start-end-anchor-for-search-mobile.png differ diff --git a/packages/eui/package.json b/packages/eui/package.json index 21dd2abf7c0c..f36a60d3b228 100644 --- a/packages/eui/package.json +++ b/packages/eui/package.json @@ -44,6 +44,7 @@ "release-rc": "node ./scripts/release.js --type=prerelease", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", + "serve-storybook": "http-server storybook-static --port 6006 --silent", "test-storybook": "test-storybook", "pre-push": "yarn test-staged", "prerelease": "node ./scripts/prerelease.mjs" @@ -193,6 +194,7 @@ "expose-gc": "^1.0.0", "findup": "^0.1.5", "glob": "^11.0.3", + "http-server": "^14.1.1", "inquirer": "^9.1.4", "jest": "^29.7.0", "jest-cli": "^29.7.0", diff --git a/packages/eui/scripts/test-visual-regression.js b/packages/eui/scripts/test-visual-regression.js index 603a57bd4808..cec12de7c7fd 100644 --- a/packages/eui/scripts/test-visual-regression.js +++ b/packages/eui/scripts/test-visual-regression.js @@ -1,5 +1,6 @@ -const { execSync } = require('child_process'); +const { execSync, spawn, spawnSync } = require('child_process'); const fs = require('fs'); +const net = require('net'); const path = require('path'); const yargs = require('yargs/yargs'); const { hideBin } = require('yargs/helpers'); @@ -13,6 +14,9 @@ function findRepoRoot(start) { return start; } +const PACKAGE_ROOT = path.resolve(__dirname, '..'); +const REPO_ROOT = findRepoRoot(PACKAGE_ROOT); + /** * The test-runner is invoked once per variant. * These names must match the keys of the `VARIANTS` map in `.storybook/vrt.ts`. @@ -35,12 +39,116 @@ const { argv } = yargs(hideBin(process.argv)) type: 'boolean', description: 'Run tests inside a Linux Docker container (matching CI) to generate Linux-compatible screenshots. Defaults to true locally, false in CI.', + }) + .option('static', { + type: 'boolean', + description: + 'Serve a static Storybook build (like CI) instead of the dev server, avoiding the networkidle stalls that make `waitForPageReady` time out locally under emulation. Defaults on locally; use `--no-static` for the dev server. Ignored when `--url` is set.', + }) + .option('build', { + type: 'boolean', + description: + 'Rebuild the static Storybook before running (default in `--static` mode, so runs reflect story changes). Use `--no-build` to reuse an existing `storybook-static` directory.', }); const isUpdate = argv._.includes('update'); const extraArgs = argv._.filter((arg) => arg !== 'update'); const isCI = Boolean(process.env.CI || process.env.BUILDKITE); const useDocker = argv.docker !== undefined ? argv.docker : !isCI; +const useStatic = + !argv.url && (argv.static !== undefined ? argv.static : !isCI); +const forceBuild = argv.build !== undefined ? argv.build : true; +const STATIC_DIR = 'storybook-static'; +const STATIC_PORT = 6006; + +const staticBuildExists = () => + fs.existsSync(path.join(PACKAGE_ROOT, STATIC_DIR)); + +const isPortOpen = (port) => + new Promise((resolve) => { + const socket = net.createConnection({ host: '127.0.0.1', port }); + const finish = (isOpen) => { + socket.destroy(); + resolve(isOpen); + }; + + socket.once('connect', () => finish(true)); + socket.once('error', () => finish(false)); + }); + +const waitForPort = async (port, timeout = 30000) => { + const deadline = Date.now() + timeout; + + while (Date.now() < deadline) { + if (await isPortOpen(port)) return; + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + throw new Error(`Timed out waiting for port ${port}`); +}; + +const stopStaticServer = (server) => { + if (!server?.pid) return; + + if (process.platform === 'win32') { + spawnSync( + 'taskkill', + ['/pid', server.pid.toString(), '/t', '/f'], + { stdio: 'ignore' } + ); + return; + } + + try { + process.kill(-server.pid); + } catch (error) { + if (error.code !== 'ESRCH') throw error; + } +}; + +const startStaticServer = async () => { + if (await isPortOpen(STATIC_PORT)) { + throw new Error(`Port ${STATIC_PORT} is already in use`); + } + + // `detached` lets cleanup terminate both yarn and http-server. + const server = spawn('yarn', ['serve-storybook'], { + stdio: 'inherit', + detached: true, + cwd: PACKAGE_ROOT, + }); + + const serverFailed = new Promise((_, reject) => { + server.once('error', reject); + server.once('exit', (code) => { + reject(new Error(`Static Storybook server exited with code ${code}`)); + }); + }); + + try { + await Promise.race([waitForPort(STATIC_PORT), serverFailed]); + return server; + } catch (error) { + stopStaticServer(server); + throw error; + } +}; + +// Run every variant even when an earlier variant fails. +const runVariants = (runVariant) => { + let failed = false; + + for (const variant of VARIANTS) { + console.log(`\n--- Variant: ${variant}`); + try { + runVariant(variant); + } catch { + failed = true; + } + } + + return failed; +}; if (useDocker) { let playwrightVersion; @@ -52,12 +160,15 @@ if (useDocker) { const dockerImage = `mcr.microsoft.com/playwright:v${playwrightVersion}-jammy`; - // On macOS, Docker containers can't reach the host through localhost; use `host.docker.internal` instead. - // If no URL is provided, inject the default port with the correct hostname so the container can reach the - // Storybook dev server running on the host. let url = argv.url; - if (process.platform === 'darwin') { + // Static mode serves inside the container over localhost, so this only + // applies to the dev server: Docker Desktop containers can't reach the host + // through localhost, so rewrite to `host.docker.internal`. + if ( + !useStatic && + (process.platform === 'darwin' || process.platform === 'win32') + ) { url = (url || 'http://localhost:6006') .replace(/\blocalhost\b/g, 'host.docker.internal') .replace(/127\.0\.0\.1/g, 'host.docker.internal'); @@ -65,16 +176,15 @@ if (useDocker) { const testStorybookArgs = [ isUpdate && '--updateSnapshot', - url && `--url ${url}`, + !useStatic && url && `--url ${url}`, ...extraArgs, ].filter(Boolean); // Mount the full monorepo root so yarn can resolve `workspace:*` dependencies // across sibling packages (e.g. `@elastic/eui-theme-common`). - const repoRoot = findRepoRoot(__dirname); - const workspaceDir = path.relative(repoRoot, process.cwd()); + const workspaceDir = path.relative(REPO_ROOT, PACKAGE_ROOT); const nodeVersion = fs - .readFileSync(path.join(repoRoot, '.nvmrc'), 'utf8') + .readFileSync(path.join(REPO_ROOT, '.nvmrc'), 'utf8') .trim(); const argsSuffix = testStorybookArgs.length @@ -96,63 +206,97 @@ if (useDocker) { const networkFlag = process.platform === 'linux' ? '--network=host ' : ''; const dockerRun = (innerCmd) => - `docker run --rm -i --platform linux/amd64 ${networkFlag}-v "${repoRoot}:/work" -w /work ${dockerImage} bash -c ${JSON.stringify( + `docker run --rm -i --platform linux/amd64 ${networkFlag}-v "${REPO_ROOT}:/work" -w /work ${dockerImage} bash -c ${JSON.stringify( innerCmd )}`; console.log(`Running visual regression tests in Docker (${dockerImage})`); - // Run each variant in its OWN container. Sharing a single container let the - // first variant leak Chromium processes/memory that starve the next one under - // amd64 emulation, tripping the per-test timeout on the second variant. - // `--testTimeout` adds headroom for the slower emulated environment. All - // variants run even if an earlier one fails, so we still generate every - // baseline/diff; a non-zero exit is propagated if any variant reported diffs. + // Build in the container, not on the host: the container's `yarn` swaps + // mounted `node_modules` binaries to Linux, breaking host builds. + if (useStatic && (forceBuild || !staticBuildExists())) { + console.log('Building static Storybook inside the container...'); + execSync(dockerRun(`set -e; ${setup}; yarn build-storybook`), { + stdio: 'inherit', + }); + } else if (useStatic) { + console.log(`Reusing existing ${STATIC_DIR}/ (--no-build)`); + } - let failed = false; + // Serve the build over localhost and wait for it to bind before testing; + // flat files keep `networkidle` settling instantly (unlike the dev server). + const staticServe = useStatic + ? `yarn serve-storybook & server_pid=$!; ` + + `for attempt in $(seq 1 60); do ` + + `if ! kill -0 "$server_pid" 2>/dev/null; then echo "Static Storybook server exited before startup"; wait "$server_pid"; exit 1; fi; ` + + `if (echo > /dev/tcp/127.0.0.1/${STATIC_PORT}) 2>/dev/null; then break; fi; ` + + `sleep 0.5; done; ` + + `if ! (echo > /dev/tcp/127.0.0.1/${STATIC_PORT}) 2>/dev/null; then echo "Timed out waiting for static Storybook on port ${STATIC_PORT}"; kill "$server_pid"; exit 1; fi; ` + : ''; - for (const variant of VARIANTS) { - console.log(`\n--- Variant: ${variant}`); - const innerCmd = `set -e; ${setup}; VRT_VARIANT=${variant} yarn test-storybook --testTimeout=30000${argsSuffix}`; + // `--maxWorkers`/`--testTimeout` add headroom for the slower emulated env. + const failed = runVariants((variant) => { + const innerCmd = `set -e; ${setup}; ${staticServe}VRT_VARIANT=${variant} yarn test-storybook --maxWorkers=2 --testTimeout=60000${argsSuffix}`; + execSync(dockerRun(innerCmd), { stdio: 'inherit' }); + }); - try { - execSync(dockerRun(innerCmd), { stdio: 'inherit' }); - } catch { - failed = true; - } - } process.exit(failed ? 1 : 0); } -// Safe-guard to ensure the browser is installed before running the tests -execSync('yarn playwright install chromium', { stdio: 'inherit' }); +const runNativeTests = async () => { + // Safe-guard to ensure the browser is installed before running the tests + execSync('yarn playwright install chromium', { + stdio: 'inherit', + cwd: PACKAGE_ROOT, + }); -console.log('Running visual regression tests'); + console.log('Running visual regression tests'); -const baseCmd = [ - 'yarn test-storybook', - isUpdate && '--updateSnapshot', - argv.url && `--url ${argv.url}`, - ...extraArgs, -] - .filter(Boolean) - .join(' '); + let staticServer; -// Run each variant in its own test-runner process (clean slate per variant). -// Run all variants even if one fails, then exit non-zero if any reported diffs. -let failed = false; + if (useStatic) { + if (forceBuild || !staticBuildExists()) { + console.log('Building static Storybook (yarn build-storybook)...'); + execSync('yarn build-storybook', { + stdio: 'inherit', + cwd: PACKAGE_ROOT, + }); + } else { + console.log(`Reusing existing ${STATIC_DIR}/ (--no-build)`); + } -for (const variant of VARIANTS) { - console.log(`\n--- Variant: ${variant}`); + staticServer = await startStaticServer(); - try { + process.on('exit', () => stopStaticServer(staticServer)); + process.on('SIGINT', () => { + stopStaticServer(staticServer); + process.exit(1); + }); + } + + const baseCmd = [ + 'yarn test-storybook', + !isCI && '--maxWorkers=2', + !isCI && '--testTimeout=60000', + isUpdate && '--updateSnapshot', + !useStatic && argv.url && `--url ${argv.url}`, + ...extraArgs, + ] + .filter(Boolean) + .join(' '); + + const failed = runVariants((variant) => { execSync(baseCmd, { stdio: 'inherit', env: { ...process.env, VRT_VARIANT: variant }, + cwd: PACKAGE_ROOT, }); - } catch { - failed = true; - } -} + }); + + process.exit(failed ? 1 : 0); +}; -process.exit(failed ? 1 : 0); +runNativeTests().catch((error) => { + console.error(error.message); + process.exit(1); +}); diff --git a/packages/eui/src/components/badge/beta_badge/beta_badge.stories.tsx b/packages/eui/src/components/badge/beta_badge/beta_badge.stories.tsx index 4711b61bffee..ade236373743 100644 --- a/packages/eui/src/components/badge/beta_badge/beta_badge.stories.tsx +++ b/packages/eui/src/components/badge/beta_badge/beta_badge.stories.tsx @@ -35,9 +35,12 @@ type Story = StoryObj; export const Playground: Story = { args: { label: 'Beta', - // 'middle' avoids the badge being cropped in VRT when there is no surrounding text to anchor baseline alignment - alignment: 'middle', }, + render: (args: EuiBetaBadgeProps) => ( +
+ +
+ ), }; export const TitleAlignment: Story = { diff --git a/packages/eui/src/components/basic_table/basic_table.stories.tsx b/packages/eui/src/components/basic_table/basic_table.stories.tsx index 247c21f360ee..a0477af6b0d5 100644 --- a/packages/eui/src/components/basic_table/basic_table.stories.tsx +++ b/packages/eui/src/components/basic_table/basic_table.stories.tsx @@ -77,7 +77,7 @@ type User = { const users: User[] = []; -for (let i = 0; i < 75; i++) { +for (let i = 0; i < 15; i++) { users.push({ id: i + 1, firstName: faker.person.firstName(), diff --git a/packages/eui/src/components/code/code_block.stories.tsx b/packages/eui/src/components/code/code_block.stories.tsx index 4b40d37df1eb..9bb961a1027d 100644 --- a/packages/eui/src/components/code/code_block.stories.tsx +++ b/packages/eui/src/components/code/code_block.stories.tsx @@ -158,9 +158,6 @@ rendered correctly and not cut-off after scrolling */ export const VirtualizedCodeBlockScrolling: Story = { tags: ['vrt-only'], parameters: { - vrt: { - skip: true, - }, codeSnippet: { skip: true, }, @@ -168,10 +165,14 @@ export const VirtualizedCodeBlockScrolling: Story = { play: playDecorator(async ({ canvasElement }) => { // Wait for the virtualized code block to render the content. await waitFor( - () => + () => { expect( canvasElement.querySelector('.euiCodeBlock__pre')?.textContent - ).toContain('Post title'), + ).toContain('Post title'); + expect( + canvasElement.querySelector('[data-complete="true"]') + ).not.toBeNull(); + }, { timeout: 5000 } ); }), @@ -180,8 +181,9 @@ export const VirtualizedCodeBlockScrolling: Story = { render: function Render() { const [response, setResponse] = useState('{}'); const [isLoading, setIsLoading] = useState(false); + const [hasSecondLoadCompleted, setHasSecondLoadCompleted] = useState(false); - const handleSubmit = async () => { + const handleSubmit = (onComplete?: () => void) => { setIsLoading(true); const data = Array.from({ length: 100 }, (_, i) => ({ userId: Math.floor(i / 10) + 1, @@ -196,6 +198,7 @@ export const VirtualizedCodeBlockScrolling: Story = { setTimeout(() => { setResponse(JSON.stringify(data, null, 2)); setIsLoading(false); + onComplete?.(); }, 75); }; @@ -204,6 +207,8 @@ export const VirtualizedCodeBlockScrolling: Story = { }, []); useEffect(() => { + if (response === '{}') return; + // Scroll the code block after response updates to test if virtualization // Calculates the scroll height and position correctly const pre = document.querySelector('.euiCodeBlock__pre'); @@ -214,7 +219,7 @@ export const VirtualizedCodeBlockScrolling: Story = { // Trigger a second load after scroll to simulate potential issues setTimeout(() => { - handleSubmit(); + handleSubmit(() => setHasSecondLoadCompleted(true)); }, 10); } }, [response]); @@ -223,6 +228,7 @@ export const VirtualizedCodeBlockScrolling: Story = { css` block-size: calc( 100vh - ${mathWithUnits(euiTheme.size.base, (x) => x * 2)} @@ -234,7 +240,7 @@ export const VirtualizedCodeBlockScrolling: Story = { type="submit" iconType="sortRight" isLoading={isLoading} - onClick={handleSubmit} + onClick={() => handleSubmit()} > Submit diff --git a/packages/eui/src/components/collapsible_nav/collapsible_nav.stories.tsx b/packages/eui/src/components/collapsible_nav/collapsible_nav.stories.tsx index 10351246e5b6..0f71fcf41b93 100644 --- a/packages/eui/src/components/collapsible_nav/collapsible_nav.stories.tsx +++ b/packages/eui/src/components/collapsible_nav/collapsible_nav.stories.tsx @@ -89,6 +89,9 @@ const StatefulCollapsibleNav = (props: Partial) => { }; export const Playground: Story = { + parameters: { + vrt: { selector: VRT_SELECTORS.portal }, + }, render: ({ ...args }) => , args: { children: 'Collapsible nav content', diff --git a/packages/eui/src/components/comment_list/comment_list.stories.tsx b/packages/eui/src/components/comment_list/comment_list.stories.tsx index 27c56022d1f7..dfa5ec92e1bf 100644 --- a/packages/eui/src/components/comment_list/comment_list.stories.tsx +++ b/packages/eui/src/components/comment_list/comment_list.stories.tsx @@ -180,7 +180,10 @@ export const HighContrast: Story = { { username: 'you', timelineAvatar: ( - + ), children: ( { + await within(canvasElement).waitForEuiPopoverVisible(); + }), parameters: { vrt: { selector: VRT_SELECTORS.portal, @@ -166,6 +170,9 @@ export const InPopover: Story = { }; export const ScrollableList: Story = { + play: playDecorator(async ({ canvasElement }) => { + await within(canvasElement).waitForEuiPopoverVisible(); + }), parameters: { vrt: { selector: VRT_SELECTORS.portal, @@ -204,6 +211,9 @@ export const ScrollableList: Story = { }; export const InScrollablePopover: Story = { + play: playDecorator(async ({ canvasElement }) => { + await within(canvasElement).waitForEuiPopoverVisible(); + }), parameters: { vrt: { selector: VRT_SELECTORS.portal, diff --git a/packages/eui/src/components/datagrid/controls/data_grid_toolbar.stories.tsx b/packages/eui/src/components/datagrid/controls/data_grid_toolbar.stories.tsx index e169b654f60e..76ac22603812 100644 --- a/packages/eui/src/components/datagrid/controls/data_grid_toolbar.stories.tsx +++ b/packages/eui/src/components/datagrid/controls/data_grid_toolbar.stories.tsx @@ -108,6 +108,12 @@ export const ToolbarVisibilityOptions: StoryObj ), + play: playDecorator(async ({ canvasElement }) => { + const canvas = within(canvasElement); + await waitFor(() => + expect(canvas.getAllByRole('gridcell').length).toBeGreaterThan(0) + ); + }), }; export const AdditionalControlsOptions: StoryObj = @@ -266,7 +272,7 @@ const vrtProps = { export const ColumnSelector: Story = { tags: ['vrt-only'], parameters: { - vrt: { skip: true }, + vrt: { selector: VRT_SELECTORS.portal }, }, render: () => , // Column sorting is hidden on mobile otherwise play: async ({ canvasElement, step }: PlayFunctionContext) => { diff --git a/packages/eui/src/components/datagrid/data_grid_styles.stories.tsx b/packages/eui/src/components/datagrid/data_grid_styles.stories.tsx index ae9b028ef03d..686370df06c3 100644 --- a/packages/eui/src/components/datagrid/data_grid_styles.stories.tsx +++ b/packages/eui/src/components/datagrid/data_grid_styles.stories.tsx @@ -9,10 +9,11 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { css } from '@emotion/css'; +import { expect, waitFor } from '@storybook/test'; + import { enableFunctionToggleControls } from '../../../.storybook/utils'; -import { VRT_SELECTORS, playDecorator } from '../../../.storybook/vrt'; -import { waitFor, expect } from '@storybook/test'; import { within } from '../../../.storybook/test'; +import { VRT_SELECTORS, playDecorator } from '../../../.storybook/vrt'; import { StatefulDataGrid, @@ -128,15 +129,12 @@ export const Minimal: Story = { export const Compact: Story = { tags: ['vrt-only'], - parameters: { - vrt: { skip: true }, - }, args: { fontSize: 's', cellPadding: 's', }, render: (gridStyle) => ( - + ), play: playDecorator(async ({ canvasElement }) => { const canvas = within(canvasElement); diff --git a/packages/eui/src/components/expression/expression.stories.tsx b/packages/eui/src/components/expression/expression.stories.tsx index ed5ad9b7d43a..226e0286cd99 100644 --- a/packages/eui/src/components/expression/expression.stories.tsx +++ b/packages/eui/src/components/expression/expression.stories.tsx @@ -39,7 +39,7 @@ export const Playground: Story = { export const KitchenSink: Story = { tags: ['vrt-only'], render: () => ( - <> +
{/* Inline */} {}} /> - +
), }; diff --git a/packages/eui/src/components/flyout/flyout.stories.tsx b/packages/eui/src/components/flyout/flyout.stories.tsx index 7a09d45f32b8..733bcaef3231 100644 --- a/packages/eui/src/components/flyout/flyout.stories.tsx +++ b/packages/eui/src/components/flyout/flyout.stories.tsx @@ -172,6 +172,8 @@ export const ManualReturnFocus: Story = { controls: { include: ['focusTrapProps'], }, + // This story is visually identical to the default; it's purely behavioral. + vrt: { skip: true }, }, args: { children: ( diff --git a/packages/eui/src/components/header/header.stories.tsx b/packages/eui/src/components/header/header.stories.tsx index 97eba9f9e096..1149446258dc 100644 --- a/packages/eui/src/components/header/header.stories.tsx +++ b/packages/eui/src/components/header/header.stories.tsx @@ -106,8 +106,13 @@ export const DarkThemeWithSitewideSearch: Story = { parameters: { layout: 'fullscreen', controls: { include: ['theme'] }, - vrt: { selector: VRT_SELECTORS.portal }, // Required to capture the open popover + vrt: { selector: VRT_SELECTORS.portal }, }, + play: playDecorator(async ({ bodyElement }) => { + await waitFor(() => + expect(bodyElement.querySelector('[data-popover-open]')).toBeVisible() + ); + }), args: { theme: 'dark', position: 'fixed', diff --git a/packages/eui/src/components/header/header_alert/header_alert.stories.tsx b/packages/eui/src/components/header/header_alert/header_alert.stories.tsx index ebd8b5f549da..dec46c66af06 100644 --- a/packages/eui/src/components/header/header_alert/header_alert.stories.tsx +++ b/packages/eui/src/components/header/header_alert/header_alert.stories.tsx @@ -28,6 +28,8 @@ import { useEuiTheme, } from '../../../index'; +import { within } from '../../../../.storybook/test'; +import { VRT_SELECTORS, playDecorator } from '../../../../.storybook/vrt'; import { EuiHeader } from '../header'; import { EuiHeaderSection, @@ -131,7 +133,7 @@ export const FlyoutExample: Story = { const Popover = (props: any) => { const { euiTheme } = useEuiTheme(); - const [isPopoverVisible, setIsPopoverVisible] = useState(false); + const [isPopoverVisible, setIsPopoverVisible] = useState(true); return ( @@ -186,6 +188,11 @@ export const PopoverExample: Story = { codeSnippet: { resolveChildren: true, }, + vrt: { selector: VRT_SELECTORS.portal }, }, render: (args) => , + play: playDecorator(async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.waitForEuiPopoverVisible(); + }), }; diff --git a/packages/eui/src/components/link/link.stories.tsx b/packages/eui/src/components/link/link.stories.tsx index e8ea73fa8d83..af362a55a24c 100644 --- a/packages/eui/src/components/link/link.stories.tsx +++ b/packages/eui/src/components/link/link.stories.tsx @@ -34,3 +34,13 @@ export const Playground: Story = { href: 'http://www.elastic.co/', }, }; + +export const External: Story = { + tags: ['vrt-only'], + args: { + children: 'Elastic website', + href: 'http://www.elastic.co/', + external: true, + target: '_blank', + }, +}; diff --git a/packages/eui/src/components/panel/panel.stories.tsx b/packages/eui/src/components/panel/panel.stories.tsx index cae0f5a56376..b40e1de93cd7 100644 --- a/packages/eui/src/components/panel/panel.stories.tsx +++ b/packages/eui/src/components/panel/panel.stories.tsx @@ -65,21 +65,23 @@ export const OverlappingPanels: Story = { }, render: function Render(args: EuiPanelProps) { return ( - <> +
({ + position: 'relative', top: `-${euiTheme.size.m}`, })} /> ({ + position: 'relative', top: `-${euiTheme.size.l}`, })} /> - +
); }, }; diff --git a/packages/eui/src/components/panel/split_panel/split_panel_outer.stories.tsx b/packages/eui/src/components/panel/split_panel/split_panel_outer.stories.tsx index 23cd8b219548..7ab41e7029be 100644 --- a/packages/eui/src/components/panel/split_panel/split_panel_outer.stories.tsx +++ b/packages/eui/src/components/panel/split_panel/split_panel_outer.stories.tsx @@ -81,7 +81,7 @@ export const HighContrast: Story = { }, }, render: (_args) => ( - <> +
@@ -124,6 +124,6 @@ export const HighContrast: Story = { - +
), }; diff --git a/packages/eui/src/components/popover/popover.stories.tsx b/packages/eui/src/components/popover/popover.stories.tsx index dec097370347..fe94066e8066 100644 --- a/packages/eui/src/components/popover/popover.stories.tsx +++ b/packages/eui/src/components/popover/popover.stories.tsx @@ -56,9 +56,6 @@ export default meta; type Story = StoryObj; export const Playground: Story = { - parameters: { - vrt: { skip: true }, - }, args: { children: 'This is a popover', button: 'popover trigger', @@ -67,7 +64,7 @@ export const Playground: Story = { render: (args) => , play: playDecorator(async ({ canvasElement }) => { const canvas = within(canvasElement); - await canvas.waitForEuiPopoverVisible(); + await canvas.waitForEuiPopoverVisible('.euiPopover'); }), }; enableFunctionToggleControls(Playground, ['onPositionChange']); diff --git a/packages/eui/src/components/popover/popover_footer.stories.tsx b/packages/eui/src/components/popover/popover_footer.stories.tsx index aed9fe080d9d..42ebd693f934 100644 --- a/packages/eui/src/components/popover/popover_footer.stories.tsx +++ b/packages/eui/src/components/popover/popover_footer.stories.tsx @@ -9,7 +9,8 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { VRT_SELECTORS } from '../../../.storybook/vrt'; +import { VRT_SELECTORS, playDecorator } from '../../../.storybook/vrt'; +import { within } from '../../../.storybook/test'; import { PADDING_SIZES } from '../../global_styling'; import { EuiButton } from '../button'; import { EuiPopover } from './popover'; @@ -43,4 +44,7 @@ export const Playground: Story = { args: { children: 'Popover footer', }, + play: playDecorator(async ({ canvasElement }) => { + await within(canvasElement).waitForEuiPopoverVisible(); + }), }; diff --git a/packages/eui/src/components/popover/popover_title.stories.tsx b/packages/eui/src/components/popover/popover_title.stories.tsx index 772fdd122008..6ec28e9c66d1 100644 --- a/packages/eui/src/components/popover/popover_title.stories.tsx +++ b/packages/eui/src/components/popover/popover_title.stories.tsx @@ -9,7 +9,8 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { VRT_SELECTORS } from '../../../.storybook/vrt'; +import { VRT_SELECTORS, playDecorator } from '../../../.storybook/vrt'; +import { within } from '../../../.storybook/test'; import { PADDING_SIZES } from '../../global_styling'; import { EuiButton } from '../button'; import { EuiPopover } from './popover'; @@ -43,4 +44,7 @@ export const Playground: Story = { args: { children: 'Popover title', }, + play: playDecorator(async ({ canvasElement }) => { + await within(canvasElement).waitForEuiPopoverVisible(); + }), }; diff --git a/packages/eui/src/components/popover/wrapping_popover.stories.tsx b/packages/eui/src/components/popover/wrapping_popover.stories.tsx index a49d427c77ef..626466b68798 100644 --- a/packages/eui/src/components/popover/wrapping_popover.stories.tsx +++ b/packages/eui/src/components/popover/wrapping_popover.stories.tsx @@ -9,7 +9,6 @@ import React, { useEffect, useState } from 'react'; import { css } from '@emotion/react'; import type { Meta, StoryObj } from '@storybook/react'; -import { waitFor, expect } from '@storybook/test'; import { disableStorybookControls, @@ -18,6 +17,7 @@ import { moveStorybookControlsToCategory, } from '../../../.storybook/utils'; import { VRT_SELECTORS, playDecorator } from '../../../.storybook/vrt'; +import { within } from '../../../.storybook/test'; import { EuiButton } from '../button'; import { EuiFlexGroup } from '../flex'; import { EuiPopoverProps } from './popover'; @@ -79,9 +79,8 @@ export const Playground: Story = { }, render: (args) => , play: playDecorator(async ({ bodyElement }) => { - await waitFor(() => - expect(bodyElement.querySelector('[data-popover-open]')).toBeVisible() - ); + const body = within(bodyElement); + await body.waitForEuiPopoverVisible('#popover-anchor'); }), }; // hiding isOpen as we need to rely on the connected state toggle diff --git a/packages/eui/src/components/spacer/spacer.stories.tsx b/packages/eui/src/components/spacer/spacer.stories.tsx index e8a18cffff99..919bcf4fdbe1 100644 --- a/packages/eui/src/components/spacer/spacer.stories.tsx +++ b/packages/eui/src/components/spacer/spacer.stories.tsx @@ -22,7 +22,7 @@ const meta: Meta = { }, decorators: [ (Story, { args }) => ( - <> +

Observe the space created between this and the next text block.

@@ -32,7 +32,7 @@ const meta: Meta = { Observe the space created between this and the previous text block.

- +
), ], args: { diff --git a/packages/eui/src/components/text_truncate/text_truncate.stories.tsx b/packages/eui/src/components/text_truncate/text_truncate.stories.tsx index 28cd979ee227..5b81e258d5e8 100644 --- a/packages/eui/src/components/text_truncate/text_truncate.stories.tsx +++ b/packages/eui/src/components/text_truncate/text_truncate.stories.tsx @@ -49,7 +49,7 @@ export const ResizeObserver: Story = { controls: { include: ['onResize'] }, }, render: (props) => ( - <> +
Drag the corner of the text to resize, and look in the console log to see the reported width @@ -66,7 +66,7 @@ export const ResizeObserver: Story = { >
- + ), }; enableFunctionToggleControls(ResizeObserver, ['onResize']); @@ -87,7 +87,7 @@ export const StartEndAnchorForSearch: Story = { highlightStartPosition + Math.floor(highlight.length / 2); return ( - <> +
Type into the below textarea to highlight, e.g. "consec"
- + ); }, args: { diff --git a/wiki/contributing-to-eui/testing/visual-regression-testing.md b/wiki/contributing-to-eui/testing/visual-regression-testing.md index c7e486ffaa6f..73efa83fdc14 100644 --- a/wiki/contributing-to-eui/testing/visual-regression-testing.md +++ b/wiki/contributing-to-eui/testing/visual-regression-testing.md @@ -4,28 +4,39 @@ EUI uses [Playwright Test Runner](https://playwright.dev/) with [`jest-image-sna Visual regression tests run automatically on every pull request against the deployed Storybook preview. When differences are found, a diff table is posted as a PR comment and a Buildkite block step appears for human approval before baselines are updated. +> [!IMPORTANT] +> VRT runs in **CI** which owns the baselines and auto-commits them to your PR. Run it locally only when you really need to verify that many stories render as expected. + ## Running VRT locally Make sure you have [Docker](https://docs.docker.com/get-docker/) installed and running. It's used to take screenshots in a Linux environment matching CI. -Start a local Storybook server: +Run the visual regression tests: ```shell -yarn storybook --no-open +yarn workspace @elastic/eui test-visual-regression ``` -Run the visual regression tests: +Locally this builds and serves a static Storybook (like CI) and rebuilds it each run, so no dev server is needed and every run reflects your current stories. To test against a specific URL instead (e.g. a deployed PR preview): ```shell -yarn workspace @elastic/eui test-visual-regression +yarn workspace @elastic/eui test-visual-regression -- --url https://eui.elastic.co/pr_1234/storybook ``` -To test against a specific URL (e.g. a deployed PR preview): +### Static build vs. dev server + +The static build is served from flat files (no HMR) which keeps `networkidle` settling instantly. The dev server's HMR can otherwise stall it under emulation and make stories time out (see [Troubleshooting](#every-story-fails-with-pagewaitforloadstate-timeout-30000ms-exceeded)). The build happens inside the container (Linux, matching CI) and is reused across variants. ```shell -yarn workspace @elastic/eui test-visual-regression -- --url https://eui.elastic.co/pr_1234/storybook +# Reuse the existing build for fast iteration (skip the rebuild) +yarn workspace @elastic/eui test-visual-regression -- --no-build + +# Use the dev server instead (requires `yarn storybook --no-open` running) +yarn workspace @elastic/eui test-visual-regression -- --no-static ``` +`--static`/`--build` default on locally and are ignored when `--url` is set. + ## Updating baseline screenshots ```shell @@ -171,6 +182,127 @@ By default the play function is skipped when not running under Playwright (i.e. play: playDecorator(async (context) => { ... }, false) ``` +## Authoring stable stories + +VRT compares screenshots pixel-for-pixel, so anything non-deterministic (network requests, animations, randomness, timing or capturing the wrong element) produces false diffs or flaky failures. The test-runner already neutralizes several sources globally: + +- CSS animations are paused before the screenshot (`animations: 'disabled'`) and `prefers-reduced-motion: reduce` is emulated. +- The runner waits for the page to be ready and for all `` elements to finish loading before capturing. +- Failed screenshots are retried automatically. + +Some common failures and how to fix them: + +### Only the first element is captured (fragment roots) + +The default selector `#story-wrapper > *` screenshots the **first child element** of the wrapper. A `render` function or decorator that returns a fragment with multiple siblings therefore captures only the first one and clips the rest: + +```tsx +// ❌ Only the first is captured +render: () => ( + <> + + + +), + +// ✅ Wrap in a single element so the whole set is captured +render: () => ( +
+ + +
+), +``` + +This also applies to decorators that add sibling content (e.g. wrapping the story in explanatory text) - wrap them in an element, not a fragment. + +### Portalled content isn't captured + +Popovers, tooltips, modals, flyouts and dropdowns render outside the story wrapper, so the default selector misses them. Use the portal selector to take a full-page screenshot (see [Using non-default selectors](#using-non-default-selectors)): + +```tsx +parameters: { vrt: { selector: VRT_SELECTORS.portal } }, +``` + +### Overlays captured before they finish opening + +An overlay opened on mount (through `isOpen` or initial state) may not be rendered/positioned when the screenshot fires, producing a blank or half-positioned capture that flakes. Add a `play` that waits for it to be visible: + +```tsx +import { within } from '../../../.storybook/test'; +import { playDecorator } from '../../../.storybook/vrt'; + +export const Open: Story = { + parameters: { vrt: { selector: VRT_SELECTORS.portal } }, + play: playDecorator(async ({ canvasElement }) => { + await within(canvasElement).waitForEuiPopoverVisible(); + }), +}; +``` + +For portalled panels you can also assert against `bodyElement`: + +```tsx +play: playDecorator(async ({ bodyElement }) => { + await waitFor(() => + expect(bodyElement.querySelector('[data-popover-open]')).toBeVisible() + ); +}), +``` + +### Remote assets (images, avatars, fonts) + +Never reference remote URLs (`placehold.co`, `images.unsplash.com`, `picsum.photos`, `gravatar` etc.). Network fetches are slow and unreliable in CI, causing instability. Inline the asset as a `data:` URI instead: + +```tsx +const svg = ``; + + +``` + +If a story genuinely needs a real raster image (e.g. the `EuiImage` showcase), either bundle a committed local asset or `vrt.skip` the story. + +### Randomized or time-based data + +`faker` without a seed, `Math.random()`, `Date.now()` and `new Date()` render different output on every run. Pin them: + +```tsx +import { faker } from '@faker-js/faker'; + +faker.seed(123); // consistent data across runs +``` + +Use fixed dates/values for anything rendered (e.g. `date="January 1st 1970"`). + +### Oversized stories + +Stories that render hundreds of rows/items produce very tall screenshots that are slow to capture and can time out (especially under the `mobile` variant where responsive layouts stack every cell vertically). Keep datasets small. Note that `EuiBasicTable`/`EuiInMemoryTable` don't paginate `items` themselves, so passing a large array renders every row. + +### Interaction- or behavior-only stories + +If a story exists to demonstrate behavior (focus return, callbacks, keyboard handling) and is visually identical to another captured story, it adds no VRT value and only risks flake - [skip it](#skipping-stories) with `vrt.skip` and a comment. + +### Text-only components + +A bare text node can have zero height under `#story-wrapper > *`. Use `VRT_SELECTORS.textOnly` so the wrapper itself is captured (see [Using non-default selectors](#using-non-default-selectors)). + +## Troubleshooting + +### Every story fails with `page.waitForLoadState: Timeout 30000ms exceeded` + +`waitForPageReady` waits for `networkidle` on Playwright's own 30s timeout (separate from Jest's `--testTimeout`). The **dev server's** HMR keep the network busy. Under emulation it never goes idle in time, so *every* story times out and suites take 10-20 minutes. + +**Fix:** don't pass `--no-static` locally. The default [static build](#static-build-vs-dev-server) has no HMR, so `networkidle` settles instantly. + +### VRT is slow or times out locally on Apple Silicon + +Local runs use a `linux/amd64` container to match CI. On Apple Silicon (arm64) that image is **emulated** and much slower than native, making large stories hang or time out: + +- **Enable Rosetta (recommended).** In Docker Desktop, turn on _Settings → General → "Use Rosetta for x86_64/amd64 emulation on Apple Silicon"_, and raise CPU/memory under _Settings → Resources_. Far faster than the default QEMU emulation while still `linux/amd64`, so screenshots stay byte-identical to CI. +- **Run natively for quick checks only.** Dropping `--platform linux/amd64` runs the native arm64 image (no emulation, much faster). Renders are usually identical but architecture can introduce 1-2px anti-aliasing differences, so **don't commit baselines this way** - use it only to check stories run and let CI produce baselines. + +The Docker path in [`scripts/test-visual-regression.js`](https://github.com/elastic/eui/tree/main/packages/eui/scripts/test-visual-regression.js) also caps `--maxWorkers` and raises `--testTimeout` for headroom. + ## CI pipeline architecture VRT runs automatically on every pull request as part of the `eui-deploy-docs` Buildkite pipeline. diff --git a/yarn.lock b/yarn.lock index b24b35267ba2..533cd76bae1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7384,6 +7384,7 @@ __metadata: expose-gc: "npm:^1.0.0" findup: "npm:^0.1.5" glob: "npm:^11.0.3" + http-server: "npm:^14.1.1" inquirer: "npm:^9.1.4" jest: "npm:^29.7.0" jest-cli: "npm:^29.7.0"