diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6a1a9d0f8b..6ec603beece 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,7 +82,7 @@ jobs: - name: Run tests defined in vitest uses: docker://mcr.microsoft.com/playwright:v1.55.0-jammy with: - args: npm test + args: npm test -- --project '!@primer/styled-react (vrt)' continue-on-error: ${{ matrix.react-version == 'react-19' }} type-check: diff --git a/.github/workflows/vrt-upload-snapshots.yml b/.github/workflows/vrt-upload-snapshots.yml new file mode 100644 index 00000000000..07ac9d09b30 --- /dev/null +++ b/.github/workflows/vrt-upload-snapshots.yml @@ -0,0 +1,50 @@ +name: Visual Regression Snapshots + +on: + workflow_dispatch: + push: + branches: + - 'main' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + snapshots-runner: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shard: [1, 2] + steps: + - name: Checkout repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + - name: Set up Node.js + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 + with: + node-version-file: '.nvmrc' + cache: 'npm' + - name: Set up turbo cache + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- + - name: Install dependencies + run: npm ci + - name: Build + run: npm run build + - name: Run tests defined in vitest + uses: docker://mcr.microsoft.com/playwright:v1.55.0-jammy + with: + args: npx vitest --project '@primer/styled-react (vrt)' --update --shard="${{ matrix.shard }}/${{ strategy.job-total }}" + - name: Upload images + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + with: + name: vrt-snapshots-${{ strategy.shard }} + path: '**/__screenshots__/**/*-linux.png' diff --git a/.gitignore b/.gitignore index ce59f6f054a..b72ff0bbe44 100644 --- a/.gitignore +++ b/.gitignore @@ -49,7 +49,8 @@ results.json *.module.css.d.ts # Vitest -**/__screenshots__/** +**/__screenshots__/**/*.png +**/.vitest-attachments/** # Turbo .turbo diff --git a/package-lock.json b/package-lock.json index 80e38040423..85a3b417c20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@vitest/browser": "^4.0.3", "@vitest/browser-playwright": "^4.0.3", "@vitest/eslint-plugin": "^1.3.24", + "@vitest/ui": "^4.0.3", "babel-plugin-react-compiler": "^1.0.0", "eslint": "^9.39.1", "eslint-import-resolver-typescript": "3.7.0", @@ -9876,6 +9877,65 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/ui": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.3.tgz", + "integrity": "sha512-HURRrgGVzz2GQ2Imurp55FA+majHXgCXMzcwtojUZeRsAXyHNgEvxGRJf4QQY4kJeVakiugusGYeUqBgZ/xylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.3", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.3" + } + }, + "node_modules/@vitest/ui/node_modules/@vitest/pretty-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.3.tgz", + "integrity": "sha512-N7gly/DRXzxa9w9sbDXwD9QNFYP2hw90LLLGDobPNwiWgyW95GMxsCt29/COIKKh3P7XJICR38PSDePenMBtsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui/node_modules/@vitest/utils": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.3.tgz", + "integrity": "sha512-qV6KJkq8W3piW6MDIbGOmn1xhvcW4DuA07alqaQ+vdx7YA49J85pnwnxigZVQFQw3tWnQNRKWwhz5wbP6iv/GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.3", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui/node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@vitest/utils": { "version": "3.2.4", "dev": true, @@ -14651,6 +14711,13 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -27527,6 +27594,7 @@ "@types/styled-components": "^5.1.26", "@vitejs/plugin-react": "^4.3.3", "babel-plugin-styled-components": "2.1.4", + "postcss-preset-primer": "^0.0.0", "publint": "^0.3.15", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/package.json b/package.json index 6836581153f..5f62c14807c 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@vitest/browser": "^4.0.3", "@vitest/browser-playwright": "^4.0.3", "@vitest/eslint-plugin": "^1.3.24", + "@vitest/ui": "^4.0.3", "babel-plugin-react-compiler": "^1.0.0", "eslint": "^9.39.1", "eslint-import-resolver-typescript": "3.7.0", diff --git a/packages/styled-react/config/vitest/visual/global.css b/packages/styled-react/config/vitest/visual/global.css new file mode 100644 index 00000000000..bfece00e01e --- /dev/null +++ b/packages/styled-react/config/vitest/visual/global.css @@ -0,0 +1,88 @@ +/* stylelint-disable selector-pseudo-class-no-unknown */ + +@import '@primer/primitives/dist/css/base/motion/motion.css'; +@import '@primer/primitives/dist/css/base/size/size.css'; +@import '@primer/primitives/dist/css/base/typography/typography.css'; +@import '@primer/primitives/dist/css/functional/size/border.css'; +@import '@primer/primitives/dist/css/functional/size/breakpoints.css'; +@import '@primer/primitives/dist/css/functional/size/size-coarse.css'; +@import '@primer/primitives/dist/css/functional/size/size-fine.css'; +@import '@primer/primitives/dist/css/functional/size/size.css'; +@import '@primer/primitives/dist/css/functional/size/viewport.css'; +@import '@primer/primitives/dist/css/functional/themes/dark-colorblind.css'; +@import '@primer/primitives/dist/css/functional/themes/dark-dimmed.css'; +@import '@primer/primitives/dist/css/functional/themes/dark-high-contrast.css'; +@import '@primer/primitives/dist/css/functional/themes/dark-tritanopia.css'; +@import '@primer/primitives/dist/css/functional/themes/dark.css'; +@import '@primer/primitives/dist/css/functional/themes/light-colorblind.css'; +@import '@primer/primitives/dist/css/functional/themes/light-high-contrast.css'; +@import '@primer/primitives/dist/css/functional/themes/light-tritanopia.css'; +@import '@primer/primitives/dist/css/functional/themes/light.css'; +@import '@primer/primitives/dist/css/functional/typography/typography.css'; + +* { + box-sizing: border-box; +} + +body { + color: var(--fgColor-default); + background-color: var(--bgColor-default); + font-family: var(--fontStack-system); + line-height: var(--text-body-lineHeight-large); + margin: 0; +} + +table { + /* stylelint-disable-next-line primer/borders */ + border-collapse: collapse; +} + +[data-color-mode='light'] input { + color-scheme: light; +} + +[data-color-mode='dark'] input { + color-scheme: dark; +} + +@media (prefers-color-scheme: light) { + [data-color-mode='auto'][data-light-theme*='light'] { + color-scheme: light; + } +} + +@media (prefers-color-scheme: dark) { + [data-color-mode='auto'][data-dark-theme*='dark'] { + color-scheme: dark; + } +} + +[role='button']:focus:not(:focus-visible):not(:global(.focus-visible)), +/* stylelint-disable-next-line selector-max-specificity */ +[role='tabpanel'][tabindex='0']:focus:not(:focus-visible):not(:global(.focus-visible)), +button:focus:not(:focus-visible):not(:global(.focus-visible)), +summary:focus:not(:focus-visible):not(:global(.focus-visible)), +a:focus:not(:focus-visible):not(:global(.focus-visible)) { + outline: none; + box-shadow: none; +} + +[tabindex='0']:focus:not(:focus-visible):not(:global(.focus-visible)), +details-dialog:focus:not(:focus-visible):not(:global(.focus-visible)) { + outline: none; +} + +/* https://vitest.dev/guide/browser/visual-regression-testing.html#disable-animations */ +*, +*::before, +*::after { + animation-duration: 0s !important; + animation-delay: 0s !important; + transition-duration: 0s !important; + transition-delay: 0s !important; +} + +[data-testid='screenshot'] { + display: inline-flex; + padding: 1rem; +} diff --git a/packages/styled-react/config/vitest/visual/setup.ts b/packages/styled-react/config/vitest/visual/setup.ts new file mode 100644 index 00000000000..7a36d76957b --- /dev/null +++ b/packages/styled-react/config/vitest/visual/setup.ts @@ -0,0 +1,14 @@ +import {beforeEach} from 'vitest' +import './global.css' +import {updateGlobalTheme} from '../../../src/test-helpers/themes' + +// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope +document.documentElement.setAttribute('data-color-mode', 'auto') +// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope +document.documentElement.setAttribute('data-light-theme', 'light') +// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope +document.documentElement.setAttribute('data-dark-theme', 'dark') + +beforeEach(() => { + updateGlobalTheme('light') +}) diff --git a/packages/styled-react/package.json b/packages/styled-react/package.json index 252f4986a42..94a12fab88d 100644 --- a/packages/styled-react/package.json +++ b/packages/styled-react/package.json @@ -50,6 +50,7 @@ "@types/styled-components": "^5.1.26", "@vitejs/plugin-react": "^4.3.3", "babel-plugin-styled-components": "2.1.4", + "postcss-preset-primer": "^0.0.0", "publint": "^0.3.15", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/packages/styled-react/src/components/__tests__/Button.visual.test.tsx b/packages/styled-react/src/components/__tests__/Button.visual.test.tsx new file mode 100644 index 00000000000..bc131926f9c --- /dev/null +++ b/packages/styled-react/src/components/__tests__/Button.visual.test.tsx @@ -0,0 +1,31 @@ +import {render} from '@testing-library/react' +import {describe, test, expect} from 'vitest' +import {page} from 'vitest/browser' +import {ButtonComponent as Button} from '../Button' +import {themes, updateGlobalTheme} from '../../test-helpers/themes' + +describe('Button', () => { + test.each(themes)('color with sx prop (%s) @vrt', async theme => { + updateGlobalTheme(theme) + + render( +