Visual Regression Testing
The simple [Variants] format enables easy visual regression testing of all your components using Playwright and a GitHub action to post the results on your PR as seen here.
Note: This is not yet available for Compositions, and due to the slowness of browser rendering, a much faster option that relies on HTML diffing instead of pixel diffing is in the plans for Kitbook. Though the below solution will remain for those who really need the pixel diffing.
Install Playwright
bash
pnpm install -D @playwright/test
Create Snapshots Test File
This function will take your current version of Playwright and your ([Kitbook config]) to construct urls to each variant found in your variants.ts
files. It snapshots each url for all your viewports (and [languages] if applicable).
e2e/kitbook.spec.tsts
import { getVariants, runComponentTests } from 'kitbook/test'import { expect, test } from '@playwright/test'import kitbookConfig from '../kitbook.config'const variantModules = await getVariants()runComponentTests({ test, expect, kitbookConfig, variantModules })
Add Playwright config
If you don't already use Playwright the following config will get you started. Locally it uses your current dev server if already running, and if not it will start a dev server. On CI it will use the deployment preview url as the base url for the tests run in a GitHub action if you setup an action as described below in [GitHub Action].
playwright.config.tsts
import { defineConfig, devices } from '@playwright/test'export default defineConfig({testDir: 'e2e',snapshotDir: 'e2e/snapshots',snapshotPathTemplate: '{snapshotDir}/{arg}-{projectName}-{platform}{ext}',fullyParallel: true,reporter: 'html',retries: 0, // important to keep at 0 as we are expecting "failures" for changed snapshots and don't want to produce multiple change snapshots for each retryworkers: process.env.CI ? 1 : undefined,use: {baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:5173',},projects: [{name: 'chromium',use: { ...devices['Desktop Chrome'] },},],webServer: {command: 'pnpm run dev',port: 5173,reuseExistingServer: true,},})
Add to .gitignore
.gitignoretxt
**/e2e/snapshots/*playwright-reporttest-results
Add Test Commands
These script will run playwright against all test files with "kitbook" in the name which will just pick up our component tests. The first will update snapshots to create a base and the second will be used to compare against the base for changes.
package.jsontxt
"test:components:update": "playwright test kitbook --update-snapshots","test:components": "playwright test kitbook",
Now you can run these commands and see the test results.
The action below is assuming that you are just running your component snapshot tests, but with some simple adjustments you could also incorporate your other Playwright tests into the same test runs if
you're already using Playwright for e2e tests. However, do note that an easy way to keep them separate because of different settings or other conflicts is to create a custom playwright config file like playwright.components.config.ts
and add the config flag to your script commands above, e.g. --config playwright.components.config.ts
.
You may also enjoy using the Playwright UI runner locally by adding the --ui
flag.
Add Changed Snapshots as PR Comment
Next, setup a Google Cloud Storage bucket to store snapshots and add a GitHub action that will comment on your PR with changed snapshots on every push.
Google Cloud Storage Bucket
Create a new bucket using the standard storage class and uncheck Enforce public access prevention on this bucket
. Then in your bucket go to the Permissions
tab and add a new principal allUsers
with the Storage Object Viewer
role and save. This is described here.
Still in the Google Cloud Console, create a service account for your project with the Storage Folder Admin
role. Then from that service account, create a service account key, download it as JSON, remove line breaks, and store it as a secret in your GitHub repo using GCS_COMPONENT_CHECK_BUCKETS_CREDENTIALS
or something you consider more helpful.
GitHub Action
The following action will use your recently created storage bucket along with Vercel deployment urls to comment on your PR with changed snapshots. Note each of the places pointed to with hands 👇 where you need to update using your own details.
.github/workflows/component-tests.ymlyaml
name: Kitbook Visual Regression Tests# Set all of these 👇env:PLAYWRIGHT_BASE_URL: ${{ github.event.deployment_status.target_url }}UPDATE_SNAPSHOTS_SCRIPT: pnpm test:components:updateCOMPARE_SNAPSHOTS_SCRIPT: pnpm test:componentsGOOGLE_CLOUD_CREDENTIALS: ${{ secrets.GCS_COMPONENT_CHECK_BUCKETS_CREDENTIALS }} # 👈 saved as a secret in your repoBUCKET_NAME: my-snapshotsPROJECT_NAME: fooPROJECT_ROOT: . # just a period for a root level project; in a monorepo this would be: ./packages/fooKITBOOK_ROUTE: kitbook # set this to a slash (/) if you use the root routeon:deployment_statuspermissions:pull-requests: writejobs:update-base-snapshots:name: Keep Base Component Snapshots In Sync with Main Branchif: github.event.deployment_status.state == 'success' && github.event.deployment_status.environment == 'Production – kitbook' # 👈 set thisruns-on: ubuntu-latesttimeout-minutes: 15container:image: mcr.microsoft.com/playwright:v1.43.1-jammy # 👈 keep version in sync with installed playwright package https://playwright.dev/docs/ci#github-actions-via-containerssteps:- uses: actions/checkout@v3- name: Install pnpmuses: pnpm/action-setup@v2with:version: 8.6.0- uses: actions/setup-node@v3with:node-version: 18cache: pnpm- name: Install Dependenciesrun: pnpm install- name: Run Playwright Component tests to save snapshotsrun: eval $UPDATE_SNAPSHOTS_SCRIPTenv:CI: true# PLAYWRIGHT_BASE_URL is set above already, otherwise it would need set here- name: Authenticate to Google Cloudid: authuses: google-github-actions/auth@v1with:credentials_json: '${{ env.GOOGLE_CLOUD_CREDENTIALS }}'- name: Set up Google Cloud SDKuses: google-github-actions/setup-gcloud@v1 # automatically picks up authentication from `auth`- name: Wipe Base Cleanrun: gcloud storage rm gs://${BUCKET_NAME}/${PROJECT_NAME}/base/**continue-on-error: true # ignore errors emitted when nothing is found to delete- name: Upload Base Snapshotsrun: gcloud storage cp --recursive ${PROJECT_ROOT}/e2e/snapshots gs://${BUCKET_NAME}/${PROJECT_NAME}/base/snapshots # 👈 recommend you setup your playwright.config.ts to save snapshots into $PROJECT_ROOT/e2e/snapshots to make this work smoothly (gcloud will create a base/snapshots folder if you just copy from e2e/snapshots to base, so if you tried to copy from e2e/foo to base/snapshots, I suspect things will land in base/snapshots/foo which will break things further on if you don't adjust) see https://cloud.google.com/sdk/gcloud/reference/storage for help if you do modify the commandscompare-snapshots:name: Compare Components Against Base Snapshotsif: github.event.deployment_status.state == 'success' && github.event.deployment_status.environment == 'Preview – kitbook' # 👈 set thisruns-on: ubuntu-latesttimeout-minutes: 15container:image: mcr.microsoft.com/playwright:v1.43.1-jammy # 👈 keep version in sync with installed playwright package https://playwright.dev/docs/ci#github-actions-via-containerssteps:- uses: actions/checkout@v3- name: Install pnpmuses: pnpm/action-setup@v2with:version: 8.6.0- uses: actions/setup-node@v3with:node-version: 18cache: pnpm- name: Install Dependenciesrun: pnpm install- name: Authenticate to Google Cloudid: authuses: google-github-actions/auth@v1with:credentials_json: '${{ env.GOOGLE_CLOUD_CREDENTIALS }}'- name: Set up Google Cloud SDKuses: google-github-actions/setup-gcloud@v1- name: Download Base Snapshotsrun: gcloud storage cp --recursive gs://${BUCKET_NAME}/${PROJECT_NAME}/base/snapshots ${PROJECT_ROOT}/e2e # 👈 recommend you setup your playwright.config.ts to save snapshots into $PROJECT_ROOT/e2e/snapshots to make this work smoothly- name: Run Playwright Component tests to get snapshot comparison filesrun: eval $COMPARE_SNAPSHOTS_SCRIPTenv:CI: true# PLAYWRIGHT_BASE_URL is set above already, otherwise it would need set herecontinue-on-error: true # 👈 you expect an error when components change and this is ok, this does mean however that if you have a clear bill of health saying the no snapshots changed when they should have, you know there's a bug in your app keeping your tests from running, until there is a better process you will need to verify your logs- uses: jwalton/gh-find-current-pr@v1id: findPr- name: Remove Old Report and Test Resultsrun: gcloud storage rm gs://${BUCKET_NAME}/${PROJECT_NAME}/pr/${{ steps.findPr.outputs.pr }}/**continue-on-error: true # ignore error emitted when nothing found to delete for this PR- name: Upload Playwright Reportrun: gcloud storage cp --recursive ${PROJECT_ROOT}/playwright-report gs://${BUCKET_NAME}/${PROJECT_NAME}/pr/${{ steps.findPr.outputs.pr }}/playwright-reportcontinue-on-error: true # ignore error when no components have changed and there is no report- name: Upload Test Results (Snapshots)id: upload-snapshotsuses: google-github-actions/upload-cloud-storage@v1 # use this instead of gcloud cli to easily output uploaded filenameswith:path: '${{ env.PROJECT_ROOT }}/test-results'destination: '${{ env.BUCKET_NAME }}/${{ env.PROJECT_NAME }}/pr/${{ steps.findPr.outputs.pr }}'continue-on-error: true # ignore error when no components have changed and there is no report- name: Format Changed Component Snapshots for PRif: steps.findPr.outputs.number && steps.upload-snapshots.outputs.uploadedid: format_snapshot_links_for_pruses: jacob-8/kitbook/packages/format-snapshot-results-action@v1 # 👈 use @v1 if you intend to keep up with Kitbook's beta updates until 1.0, but pin to a version (like @v1.0.0-beta.9) if you don't want to keep up with any possible breaking changes between beta and 1.0with:upload-results: '${{ steps.upload-snapshots.outputs.uploaded }}'pr: '${{ steps.findPr.outputs.pr }}'bucket: '${{ env.BUCKET_NAME }}'project: '${{ env.PROJECT_NAME }}'deployment-url: '${{ env.PLAYWRIGHT_BASE_URL }}'kitbook-route: '${{ env.KITBOOK_ROUTE }}'- name: Add Changed Component Snapshots to PRif: steps.findPr.outputs.number && steps.format_snapshot_links_for_pr.outputs.commentuses: marocchino/sticky-pull-request-comment@v2with:number: ${{ steps.findPr.outputs.pr }}recreate: trueheader: ${{ env.PROJECT_NAME }}message: |${{ steps.format_snapshot_links_for_pr.outputs.comment }}- uses: actions/upload-artifact@v3if: always()with:name: playwright-reportpath: ${{ env.PROJECT_ROOT }}/playwright-report/retention-days: 30
Snapshots will look slightly different on each platform, mainly because of font rendering differences, so know that comparisons don't work well across different types of devices (e.g. your PC vs colleagues' Mac). So just use the ones created in a Linux CI environment for comparisons.
Great! You've set up a completely free visual regression testing system for your components!
Tips
Skip Files
If you need to pass a Svelte component into a variant as a prop, you'll need to skip taking screenshots for that variants file as Playwright will choke when trying to import the Svelte file. You can pass an array of skipFiles
(without extensions) like this:
e2e/kitbook.spec.tsts
import { expect, test } from '@playwright/test'import { getVariants, runComponentTests } from 'kitbook/test'import kitbookConfig from '../kitbook.config'const skipFiles = ['/routes/foo/+page','/lib/ui/Button',]const variantModules = await getVariants({ skipFiles })runComponentTests({ test, expect, kitbookConfig, variantModules })
If you know a way to update Playwright's file parsing to handle imported Svelte files, please submit a PR. Or at least a way to ignore the Svelte file. That's not the data we need to run the tests. We only need file locations, variant names, viewports and languages.
Turn off Vercel Comments
You may notice from time to time that the comment call-to-action button shows up in your snapshots. If you don't use Vercel comments, you can turn off comments.
Use $lib imports in variants
Sometimes its nice to use $lib
imports when assembling your mock data for variants. You will need to specify your path aliases directly in your tsconfig.json
and not the extended svelte config. Also add baseUrl
according to the Playwright docs:
tsconfig.jsonjson
{"extends": "./.svelte-kit/tsconfig.json","compilerOptions": {// ... other settings"paths": {"$lib": ["./src/lib"],"$lib/*": ["./src/lib/*"]},"baseUrl": "."}}
Compositions (not yet availabled)
Screenshotting compositions is not yet supported but it will be added. In the meantime, if you read the source code used for variants you could probably set it up yourself.
Bucketless option
If you don't care about repo bloat (maybe you just have a small, personal project) and don't want to be bothered to set up cloud storage, you can store your baseline snapshots in your repo itself and rely on GitHub's visual diff viewer instead of a PR comment.
Expand to see how you could do it...
Update your test to clear snapshots before running tests so that stale images get cleared out for deleted variants.
e2e/kitbook.spec.tsts
import { clearSnapshots, getVariants, runComponentTests } from 'kitbook/test'import { expect, test } from '@playwright/test'import kitbookConfig from '../kitbook.config'clearSnapshots()const variantModules = await getVariants()runComponentTests({ test, expect, kitbookConfig, variantModules })
Then use this workflow.
.github/workflows/component-tests.ymlyaml
name: Kitbook Visual Regression Tests# Set all of these 👇env:PLAYWRIGHT_BASE_URL: ${{ github.event.deployment_status.target_url }}UPDATE_SNAPSHOTS_SCRIPT: pnpm test:components:updateon:deployment_statuspermissions:contents: writepull-requests: writejobs:update-snapshots:name: Update Component Snapshotsif: github.event.deployment_status.state == 'success' && github.event.deployment_status.environment == 'Preview'# Can also use (github.event.deployment_status.environment == 'Production' || github.event.deployment_status.environment == 'Preview') if wanting to run again on the main branch but this is unneeded if you never push directly to the main branchruns-on: ubuntu-latesttimeout-minutes: 15container:image: mcr.microsoft.com/playwright:v1.43.1-jammy # 👈 keep version in sync with installed package, https://playwright.dev/docs/ci#github-actions-via-containerssteps:- name: Allow image commit and branch name extractionrun: git config --system --add safe.directory /__w/kitbook/kitbook # 👈 change kitbook to the name of your repo (repo/repo, not owner/repo)- name: Get Branchuses: actions/checkout@v3with:fetch-depth: 0ref: '${{ github.event.deployment_status.deployment.ref }}' # ref only needed if using the deployment_status trigger- name: Extract branch name # only needed if using the deployment_status triggerrun: echo "GITHUB_BRANCH=$(git show -s --pretty=%D HEAD | tr -s ',' '\n' | sed 's/^ //' | grep -e 'origin/' | head -1 | sed 's/origin///g')" >> $GITHUB_OUTPUTid: extract_branch- name: Install pnpmuses: pnpm/action-setup@v2with:version: 8.6.0- uses: actions/setup-node@v3with:node-version: 18cache: pnpm- name: Install Dependenciesrun: pnpm install- name: Run Playwright Component testsrun: eval $UPDATE_SNAPSHOTS_SCRIPTenv:CI: true# PLAYWRIGHT_BASE_URL is set above already, otherwise it would need set here- name: Commit Changed Component Snapshots If Changes Founduses: stefanzweifel/git-auto-commit-action@v4.16.0# if: github.event.deployment_status.environment != 'Production' # only needed if you push directly to the main branchwith:commit_message: '[Updated Component Snapshots]' # 👈 you may want to update your auto-deployment service to ignore commits with this messagebranch: ${{ steps.extract_branch.outputs.GITHUB_BRANCH }} # only needed if using the deployment_status trigger- uses: actions/upload-artifact@v3if: always()with:name: playwright-reportpath: playwright-report/retention-days: 30
Then you will want to keep your deployment from redeploying when a commit is just to update snapshots. If using Vercel, you can set "Ignored Build Step" to "Run my Bash script" with bash vercel-deploy.sh
, then add this file to your repo:
vercel-deploy.shbash
#!/bin/bash# Check if there are changes in the latest commitgit diff --quiet HEAD^ HEAD ./HAS_CHANGES=$?# Check if the commit message contains [Updated Component Snapshots]git log -1 --pretty=%B | grep '[Updated Component Snapshots]'HAS_IGNORE_MESSAGE=$?echo "has changes is $HAS_CHANGES"echo "has ignore is $HAS_IGNORE_MESSAGE"if [ $HAS_CHANGES -eq 0 ] || [ $HAS_IGNORE_MESSAGE -eq 0 ]; then# Ignore the buildecho "🛑 - Build cancelled"exit 0elseecho "✅ - Build can proceed"exit 1fi
This workflow will not be maintained as using cloud storage is the better option, but if you find it helpful and have some improvement to it, feel free to submit a PR.
Test Component Interactions
Learn how to add [Addtional Component Tests] beyond simple snapshots.