Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 2 additions & 1 deletion .buildkite/pull-requests.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"trigger_comment_regex": "^(?:(?:buildkite\\W+)?(?:build|test)\\W+(?:this|it))",
"always_trigger_comment_regex": "^(?:(?:buildkite\\W+)?(?:build|test)\\W+(?:this|it))",
"set_commit_status": true,
"skip_ci_on_only_changed": ["^.github/", "^wiki/"]
"skip_ci_on_only_changed": ["^.github/", "^wiki/"],
"cancel_intermediate_builds": true
}
]
}
10 changes: 8 additions & 2 deletions .buildkite/scripts/deploy_docs/step_notify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,15 @@ if [[ -n "${BUILDKITE_PULL_REQUEST:-}" ]] && [[ "${BUILDKITE_PULL_REQUEST}" != "
vrt_pr_comment="\n* :no_entry_sign: Visual regression tests skipped (remove the \`skip-vrt\` label to re-enable)"
else
annotation_style="error"
# `vrt_comment_url` is only set when `step_vrt.sh` actually found visual differences
vrt_comment_url="$(buildkite-agent meta-data get vrt_comment_url --default "" 2>/dev/null)"
vrt_annotation="- :x: Visual regression tests failed ([view diff table](${vrt_comment_url:-${BUILDKITE_BUILD_URL}}))"
vrt_pr_comment="\n* :x: Visual regression tests failed ([view diff table](${BUILDKITE_BUILD_URL}}))"
if [[ -n "${vrt_comment_url}" ]]; then
vrt_annotation="- :x: Visual regression tests failed ([view diff table](${vrt_comment_url}))"
vrt_pr_comment="\n* :x: Visual regression tests failed ([view diff table](${vrt_comment_url}))"
else
vrt_annotation="- :x: Visual regression tests failed ([see build](${BUILDKITE_BUILD_URL}))"
vrt_pr_comment="\n* :x: Visual regression tests failed ([see build](${BUILDKITE_BUILD_URL}))"
fi
fi
fi

Expand Down
39 changes: 22 additions & 17 deletions .buildkite/scripts/deploy_docs/step_vrt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,27 +66,32 @@ yarn workspace @elastic/eui test-visual-regression -- --url "${STORYBOOK_URL}" 2
|| VRT_PASSED=false

############################################################
# On pass: commit any new baselines #
# Commit any new baselines (first run) #
############################################################

if [[ "${VRT_PASSED}" == "true" ]]; then
buildkite-agent meta-data set vrt_passed "true"
# `test-runner.ts` writes a baseline directly to disk the first time it
# encounters a story without one. Commit those net-new baselines now,
# *regardless* of whether VRT overall passed or failed.
new_files="$(git ls-files --others --exclude-standard -- "${REF_DIR}")"
if [[ -n "${new_files}" ]]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thinking, this is much nicer 🎉

echo "+++ Committing new VRT baseline screenshots (first run)"

github_user_vault="secret/ci/elastic-eui/github_machine_user"
git config --local user.name "$(retry 5 vault read -field=name "${github_user_vault}")"
git config --local user.email "$(retry 5 vault read -field=email "${github_user_vault}")"
gh auth setup-git
git add -- ${new_files}
git commit -m "chore(eui): add VRT baseline screenshots" --no-verify
git_push_to_pr_branch
echo "New VRT baseline screenshots committed and pushed"
fi

if [[ -n "$(git status --porcelain -- "${REF_DIR}")" ]]; then
echo "+++ Committing new VRT baseline screenshots (first run)"

github_user_vault="secret/ci/elastic-eui/github_machine_user"
git config --local user.name "$(retry 5 vault read -field=name "${github_user_vault}")"
git config --local user.email "$(retry 5 vault read -field=email "${github_user_vault}")"
gh auth setup-git
git add "${REF_DIR}"
git commit -m "chore(eui): add VRT baseline screenshots" --no-verify
git_push_to_pr_branch
echo "New VRT baseline screenshots committed and pushed"
else
echo "Visual regression tests passed with no new VRT baseline screenshots"
fi
############################################################
# On pass: nothing more to do #
############################################################

if [[ "${VRT_PASSED}" == "true" ]]; then
buildkite-agent meta-data set vrt_passed "true"
exit 0
fi

Expand Down
40 changes: 31 additions & 9 deletions packages/eui/.storybook/test-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,28 @@ import type { TestRunnerConfig } from '@storybook/test-runner';
import { getStoryContext, waitForPageReady } from '@storybook/test-runner';
import { toMatchImageSnapshot } from 'jest-image-snapshot';

import { VRT_SELECTORS } from './vrt';
import {
VRT_SELECTORS,
VARIANTS,
VRT_VARIANT_ATTRIBUTE,
isVariantName,
isVariantSkipped,
type VrtSkip,
} from './vrt';

/**
* `{ animations: 'disabled' }` pauses CSS animations before taking a screenshot,
* preventing stability timeouts on infinite looping animations (spinners etc.).
*/
const SCREENSHOT_OPTIONS = { animations: 'disabled' } as const;

const MOBILE_VIEWPORT_WIDTH = 390;
/**
* The active variant for this run, determined by the `VRT_VARIANT` env var.
* Falls back to desktop when run directly (e.g. `yarn test-storybook`).
*/
const activeVariant = isVariantName(process.env.VRT_VARIANT)
? VARIANTS[process.env.VRT_VARIANT]
: VARIANTS.desktop;

/**
* Ensures all `<img>` elements are fully loaded before taking a screenshot.
Expand Down Expand Up @@ -50,29 +63,38 @@ const config: TestRunnerConfig = {
jest.retryTimes(2, { logErrorsBeforeRetry: true });
},
async preVisit(page) {
// Set the viewport before the story renders (and before its `play` runs) so
// both layout and interactions happen at the active variant's dimensions.
await page.setViewportSize(activeVariant.viewport);
// Expose the active variant to `playDecorator` so it can honor `vrt.skip`.
await page.evaluate(
({ attribute, name }) => {
document.documentElement.setAttribute(attribute, name);
},
{ attribute: VRT_VARIANT_ATTRIBUTE, name: activeVariant.name }
);
// Emulate `prefers-reduced-motion` so EUI components that respect it
// render in their reduced/static state before the screenshot is taken
await page.emulateMedia({ reducedMotion: 'reduce' });
},
async postVisit(page, context) {
const storyContext = await getStoryContext(page, context);

if (storyContext.parameters?.vrt?.skip) return;
const skip: VrtSkip | undefined = storyContext.parameters?.vrt?.skip;
if (isVariantSkipped(skip, activeVariant.name)) return;

const selector =
storyContext.parameters?.vrt?.selector ?? VRT_SELECTORS.default;

await waitForPageReady(page);
await waitForImagesToLoad(page);

const selector =
storyContext.parameters?.vrt?.selector ?? VRT_SELECTORS.default;
const viewport = page.viewportSize();
const project =
viewport?.width === MOBILE_VIEWPORT_WIDTH ? 'mobile' : 'desktop';
const image =
selector === 'page'
? await page.screenshot(SCREENSHOT_OPTIONS)
: await page.locator(selector).first().screenshot(SCREENSHOT_OPTIONS);

const snapshotId = `${context.id}-${project}`;
const snapshotId = `${context.id}-${activeVariant.name}`;
const snapshotPath = path.join(
__dirname,
'..',
Expand Down
53 changes: 51 additions & 2 deletions packages/eui/.storybook/vrt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,48 @@
import type { PlayFunction } from '@storybook/csf';
import type { ReactRenderer } from '@storybook/react';

/**
* Viewport variants every story is screenshotted under. The test-runner is
* invoked once per variant (see `scripts/test-visual-regression.js`) using the
* `VRT_VARIANT` env var.
*
* Keys are the variant names, used both as the baseline suffix
* (e.g. `${context.id}-desktop.png`) and in `parameters.vrt.skip`.
*/
export const VARIANTS = {
desktop: { name: 'desktop', viewport: { width: 1440, height: 900 } },
mobile: { name: 'mobile', viewport: { width: 390, height: 844 } },
} as const;

export type VariantName = keyof typeof VARIANTS;

/**
* `parameters.vrt.skip` opts a story out of VRT:
* - `true` skips every variant,
* - an array (e.g. `['mobile']`) skips only the listed variants.
*/
export type VrtSkip = boolean | VariantName[];

export const isVariantSkipped = (
skip: VrtSkip | undefined,
variant: VariantName
): boolean => skip === true || (Array.isArray(skip) && skip.includes(variant));

export const isVariantName = (
value: string | null | undefined
): value is VariantName => value != null && value in VARIANTS;

/**
* Attribute set on `<html>` by the test-runner in `preVisit` so browser-side
* code (play functions) can read the active variant.
*/
export const VRT_VARIANT_ATTRIBUTE = 'data-vrt-variant';

const getActiveVariant = (): VariantName | undefined => {
const value = document.documentElement.getAttribute(VRT_VARIANT_ATTRIBUTE);
return isVariantName(value) ? value : undefined;
};

export const VRT_SELECTORS = {
/**
* Default story selector
Expand Down Expand Up @@ -46,8 +88,10 @@ export const playDecorator = (
vrtOnly: boolean = true
): PlayFunction<ReactRenderer, any> | undefined => {
return async (context) => {
// Respect `vrt.skip` - if the story opts out of VRT, skip the play function too.
if (context.parameters?.vrt?.skip) return;
const skip: VrtSkip | undefined = context.parameters?.vrt?.skip;

// Fully opted out of VRT - skip the play body everywhere (dev included).
if (skip === true) return;

// `navigator.webdriver` is true when Playwright (or any WebDriver-controlled browser)
// is driving the page - works regardless of whether Storybook was started by the
Expand All @@ -56,6 +100,11 @@ export const playDecorator = (

if (vrtOnly && !isVrtRunning) return;

// Opted out of the active variant - skip the play body so it doesn't run
// (and potentially fail) at a viewport the story isn't built for.
const activeVariant = getActiveVariant();
if (activeVariant && isVariantSkipped(skip, activeVariant)) return;

// using `ownerDocument.body` over `parentElement` to ensure element is always available
// related: https://github.com/storybookjs/storybook/issues/16971#issue-1076103727
const body = context.canvasElement.ownerDocument.body;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Diff not rendered.
Diff not rendered.
Loading