Skip to content
Merged
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
104 changes: 104 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
name: CI

on:
push:
branches: [master]
pull_request:

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Build and validate content
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc

- name: Enable Corepack
run: corepack enable

- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
node_modules
.yarn/install-state.gz
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-

- name: Install dependencies
run: yarn install --immutable

- name: Validate blog frontmatter
run: yarn validate:content

- name: Build site
run: yarn build

- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: docusaurus-build
path: build
retention-days: 1

e2e:
name: E2E and visual tests
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc

- name: Enable Corepack
run: corepack enable

- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
node_modules
.yarn/install-state.gz
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-

- name: Install dependencies
run: yarn install --immutable

- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: docusaurus-build
path: build

- name: Install Playwright browsers
run: yarn playwright install --with-deps chromium

- name: Run Playwright tests
run: yarn test:e2e

- name: Upload Playwright test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-test-results
path: |
playwright-report/
test-results/
if-no-files-found: ignore
retention-days: 7
57 changes: 57 additions & 0 deletions .github/workflows/update-snapshots.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Update Playwright snapshots

on:
workflow_dispatch:

permissions:
contents: write

jobs:
update-snapshots:
name: Regenerate visual baselines
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc

- name: Enable Corepack
run: corepack enable

- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
node_modules
.yarn/install-state.gz
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-

- name: Install dependencies
run: yarn install --immutable

- name: Build site
run: yarn build

- name: Install Playwright browsers
run: yarn playwright install --with-deps chromium

- name: Update snapshots
run: yarn test:e2e:update

- name: Commit and push snapshots
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add e2e/site.spec.ts-snapshots/
if git diff --staged --quiet; then
echo "No snapshot changes to commit."
exit 0
fi
git commit -m "chore(e2e): update Playwright visual snapshots"
git push
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
Binary file modified .yarn/install-state.gz
Binary file not shown.
59 changes: 59 additions & 0 deletions e2e/site.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { expect, test, type Page } from '@playwright/test';

async function prepareVisualSnapshot(page: Page) {
await page.addStyleTag({
content: `
*, *::before, *::after {
font-family: Arial, Helvetica, sans-serif !important;
}
`,
});
await page.evaluate(() => document.fonts.ready);
}

test.describe('site smoke tests', () => {
test.beforeEach(({ }, testInfo) => {
test.skip(
testInfo.project.name !== 'desktop',
'Smoke tests run on desktop only',
);
});

test('homepage lists blog posts', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/yakov\.dev/i);
await expect(
page.getByRole('navigation').getByRole('link', { name: /GitHub/i }),
).toBeVisible();
await expect(page.getByRole('article').first()).toBeVisible();
});

test('blog post page renders', async ({ page }) => {
await page.goto('/recursion-in-react-simplified');
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'Recursion in React simplified',
);
});
});

test.describe('visual regression', () => {
test('homepage screenshot', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('article').first()).toBeVisible();
await prepareVisualSnapshot(page);
await expect(page).toHaveScreenshot('homepage.png', {
animations: 'disabled',
fullPage: false,
});
});

test('blog post screenshot', async ({ page }) => {
await page.goto('/recursion-in-react-simplified');
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
await prepareVisualSnapshot(page);
await expect(page).toHaveScreenshot('blog-post.png', {
animations: 'disabled',
fullPage: false,
});
});
});
Binary file added e2e/site.spec.ts-snapshots/blog-post-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added e2e/site.spec.ts-snapshots/blog-post-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added e2e/site.spec.ts-snapshots/homepage-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added e2e/site.spec.ts-snapshots/homepage-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids"
"write-heading-ids": "docusaurus write-heading-ids",
"validate:content": "node scripts/validate-content.mjs",
"test:e2e": "playwright test",
"test:e2e:update": "playwright test --update-snapshots"
},
"dependencies": {
"@docusaurus/core": "3.10.1",
Expand All @@ -28,7 +31,9 @@
"devDependencies": {
"@docusaurus/module-type-aliases": "3.10.1",
"@docusaurus/tsconfig": "3.10.1",
"typescript": "^5.1.3"
"@playwright/test": "^1.52.0",
"typescript": "^5.1.3",
"yaml": "^2.8.0"
},
"browserslist": {
"production": [
Expand Down
42 changes: 42 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { defineConfig, devices } from '@playwright/test';

const port = 3000;
const baseURL = `http://127.0.0.1:${port}`;

export default defineConfig({
testDir: './e2e',
snapshotPathTemplate:
'{testDir}/{testFilePath}-snapshots/{arg}-{projectName}{ext}',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? 'github' : 'list',
use: {
baseURL,
trace: 'on-first-retry',
// Keep visual snapshots stable in CI (gtag is configured in docusaurus.config.js).
serviceWorkers: 'block',
},
expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.05,
},
},
projects: [
{
name: 'desktop',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'mobile',
use: { ...devices['Pixel 7'] },
},
],
webServer: {
command: `yarn docusaurus serve --port ${port} --host 127.0.0.1 --no-open`,
url: baseURL,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
Loading
Loading