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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/visual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ name: Visual regression
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
# Comparing two Storybook builds costs ~25 minutes, so it runs only where pixels can move.
# A release pull request touches the version and the changelog, and used to hold the release
# for as long as this job took, once per merge into main.
paths:
- 'package/src/**'
- '.storybook/**'
- 'package/package.json'
- 'yarn.lock'
- '.github/workflows/visual.yml'

concurrency:
group: ${{ github.workflow }}-${{ github.event.number }}
Expand Down
26 changes: 26 additions & 0 deletions .storybook/test-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ async function settle(page: Page) {
await waitForPageReady(page);
await page.evaluate(() => document.fonts.ready);
await page.evaluate(() => window.scrollTo(0, 0));
// A style tag stops at the shadow boundary, so an animation inside a widget kept running and the
// two sides of the comparison caught it in different frames.
await page.evaluate((css) => {
const freeze = (root: Document | ShadowRoot) => {
const sheet = new CSSStyleSheet();
sheet.replaceSync(css);
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
};
const walk = (root: Document | ShadowRoot) => {
freeze(root);
root.querySelectorAll('*').forEach((element) => {
if (element.shadowRoot) {
walk(element.shadowRoot);
}
});
};
walk(document);
}, FREEZE_CSS);
await page.waitForTimeout(300);
}

Expand All @@ -45,6 +63,9 @@ const config: TestRunnerConfig = {
async preVisit(page) {
await page.setViewportSize({ width: 1280, height: 800 });
await page.clock.setFixedTime(FIXED_NOW);
// Loaders, voice levels and pulse rings move from JavaScript, which no stylesheet can stop; the
// kit holds them still under reduced motion, so the screenshots ask for exactly that.
await page.emulateMedia({ reducedMotion: 'reduce' });
},

async postVisit(page, context) {
Expand All @@ -69,6 +90,11 @@ const config: TestRunnerConfig = {
}

await page.addStyleTag({ content: FREEZE_CSS });
// A widget that mounts itself into a shadow root reports when it is there; without the wait the
// baseline and the comparison could be taken on either side of that moment.
if (await page.locator('[data-shadow-ready]').count()) {
await page.locator('[data-shadow-ready="true"]').first().waitFor({ state: 'attached' });
}
for (const theme of THEMES) {
await setTheme(page, theme);
await settle(page);
Expand Down
2 changes: 1 addition & 1 deletion package/src/launcher/ChatLauncher.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ function ShadowLauncher() {
return (
<HostPage>
<style>{HOSTILE_CSS}</style>
<Box className="hostile-page" px={32}>
<Box className="hostile-page" px={32} data-shadow-ready={mounted || undefined}>
<Group gap={12}>
<button type="button">Host button</button>
<textarea aria-label="Host field" defaultValue="Host textarea" />
Expand Down
Loading