diff --git a/.github/workflows/visual.yml b/.github/workflows/visual.yml index 5968641..adaad2e 100644 --- a/.github/workflows/visual.yml +++ b/.github/workflows/visual.yml @@ -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 }} diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index 0ba8332..3b85359 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -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); } @@ -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) { @@ -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); diff --git a/package/src/launcher/ChatLauncher.story.tsx b/package/src/launcher/ChatLauncher.story.tsx index ffd0a6a..c5e7688 100644 --- a/package/src/launcher/ChatLauncher.story.tsx +++ b/package/src/launcher/ChatLauncher.story.tsx @@ -163,7 +163,7 @@ function ShadowLauncher() { return ( - +