diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9abcf78e92..52d6b4ce9a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -102,14 +102,38 @@ jobs: - name: Setup pnpm, Node.js, and dependencies uses: ./.github/actions/setup + - name: Get Playwright version + id: playwright-version + run: echo "version=$(pnpm exec playwright-core --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" + + # Cache the browser to avoid relying on azure.archive.ubuntu.com, which + # can be flaky. Only the default branch is trusted to write the cache, + # so PR runs cannot poison it. + - name: Restore Chromium cache + id: playwright-chromium-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/ms-playwright + key: playwright-chromium-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + - name: Install Chromium - run: pnpm exec playwright-core install --with-deps chromium + run: pnpm exec playwright-core install chromium + timeout-minutes: 5 + + - name: Save Chromium cache + if: github.ref == 'refs/heads/main' && steps.playwright-chromium-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/ms-playwright + key: ${{ steps.playwright-chromium-cache.outputs.cache-primary-key }} # Builds the Storybook (buildCommand in pixel.jsonc) and snapshots it. - name: Snapshot run: pnpm exec pixel-storybook env: PIXEL_KEY: ${{ secrets.PIXEL_KEY }} + # On pull_request, github.sha is a synthetic merge commit, not the PR head. + PIXEL_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} # Auto-approve on mainline to avoid blocking CI after squash merges. PIXEL_AUTO_REVIEW: ${{ github.ref == 'refs/heads/main' }} diff --git a/.storybook/preview.ts b/.storybook/preview.ts index ad5833c39f..ef464fb77a 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,5 +1,6 @@ /// +import { isPixel } from "@coder/pixel-storybook/storyapi"; import codiconCssUrl from "@vscode/codicons/dist/codicon.css?url"; import { createElement } from "react"; @@ -29,6 +30,11 @@ if (typeof window !== "undefined") { }); } +// Lets us skip motion animation during Pixel captures. +if (typeof document !== "undefined" && isPixel()) { + document.documentElement.setAttribute("data-pixel", "true"); +} + // Inject codicon stylesheet immediately (before any components render) // Must be a element with id "vscode-codicon-stylesheet" for vscode-elements if ( diff --git a/packages/ui/src/components/Icon/Icon.css b/packages/ui/src/components/Icon/Icon.css index 42a3429401..9967622c11 100644 --- a/packages/ui/src/components/Icon/Icon.css +++ b/packages/ui/src/components/Icon/Icon.css @@ -13,3 +13,8 @@ animation: none; } } + +/* data-pixel is set in .storybook/preview.ts via isPixel(). */ +:root[data-pixel] .ui-icon--spin { + animation: none; +} diff --git a/packages/ui/src/components/Menu/Menu.css b/packages/ui/src/components/Menu/Menu.css index c4a10c0e0a..ec56a68507 100644 --- a/packages/ui/src/components/Menu/Menu.css +++ b/packages/ui/src/components/Menu/Menu.css @@ -140,6 +140,14 @@ } } +/* data-pixel is set in .storybook/preview.ts via isPixel(). */ +@media (prefers-reduced-motion: no-preference) { + :where(:root[data-pixel]) .ui-menu[data-state="open"], + :where(:root[data-pixel]) .ui-menu[data-state="closed"] { + animation: none; + } +} + @media (forced-colors: active) { .ui-menu__item[data-highlighted], .ui-menu__item[data-state="open"] { diff --git a/packages/ui/src/components/ProgressBar/ProgressBar.css b/packages/ui/src/components/ProgressBar/ProgressBar.css index dc31caec85..5dcdd4453b 100644 --- a/packages/ui/src/components/ProgressBar/ProgressBar.css +++ b/packages/ui/src/components/ProgressBar/ProgressBar.css @@ -52,3 +52,8 @@ background: Highlight; } } + +/* data-pixel is set in .storybook/preview.ts via isPixel(). */ +:root[data-pixel] .ui-progress-bar--indeterminate .ui-progress-bar__indicator { + animation: none; +} diff --git a/packages/ui/src/components/Spinner/Spinner.css b/packages/ui/src/components/Spinner/Spinner.css index 023c91a016..eb32eb4ba4 100644 --- a/packages/ui/src/components/Spinner/Spinner.css +++ b/packages/ui/src/components/Spinner/Spinner.css @@ -50,3 +50,8 @@ border-inline-end-color: Highlight; } } + +/* data-pixel is set in .storybook/preview.ts via isPixel(). */ +:root[data-pixel] .ui-spinner { + animation: none; +} diff --git a/packages/ui/src/vscode-parity.stories.tsx b/packages/ui/src/vscode-parity.stories.tsx index d37c2067f4..a9a7e49a93 100644 --- a/packages/ui/src/vscode-parity.stories.tsx +++ b/packages/ui/src/vscode-parity.stories.tsx @@ -24,7 +24,7 @@ import { ProgressBar } from "./components/ProgressBar/ProgressBar"; import { SearchInput } from "./components/SearchInput/SearchInput"; import { Spinner } from "./components/Spinner/Spinner"; import { StatusPill } from "./components/StatusPill/StatusPill"; -import { openMenu, PIXEL_ALL_THEMES } from "./storybook"; +import { PIXEL_ALL_THEMES } from "./storybook"; import type { Meta, StoryObj } from "@storybook/react-vite"; @@ -177,8 +177,7 @@ const Parity = (): React.JSX.Element => ( ); -/* The reference menu renders inline; ours is a real portalled DropdownMenu, - so the play function opens it under its trigger. */ +/* defaultOpen + an invisible trigger mirror the reference's `show`. */ const MenuParity = (): React.JSX.Element => (
( fontSize: "13px", }} > - + - + Start workspace @@ -228,7 +227,4 @@ export const SideBySide: Story = {}; export const Menu: Story = { render: () => , - play: async ({ canvasElement }) => { - await openMenu(canvasElement, "Menu"); - }, };