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
60 changes: 60 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Test

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: yarn

- run: yarn install --frozen-lockfile

- run: npx playwright install --with-deps chromium

- run: yarn test

- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

update-snapshots:
if: failure()
needs: test
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref || github.ref_name }}

- uses: actions/setup-node@v4
with:
node-version: 22
cache: yarn

- run: yarn install --frozen-lockfile

- run: npx playwright install --with-deps chromium

- run: yarn test:update-snapshots

- name: Commit updated snapshots
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add e2e/
git diff --staged --quiet || (git commit -m "Update Playwright snapshots" && git push)
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ yarn-error.log

.env.local

.idea
.idea
.claude

/test-results/
/playwright-report/
37 changes: 37 additions & 0 deletions e2e/home.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {test, expect} from '@playwright/test'

test.describe('Home page', () => {
test.beforeEach(async ({page}) => {
await page.goto('/')
})

test('renders the welcome heading', async ({page}) => {
await expect(page.getByRole('heading', {name: /Hi! I'm Timo/})).toBeVisible()
})

test('renders the tagline', async ({page}) => {
await expect(page.getByText(/I live in NYC/)).toBeVisible()
})

test('renders the profile image', async ({page}) => {
const img = page.getByAltText("Timo's face")
await expect(img).toBeVisible()
})

test('has navigation with site title', async ({page}) => {
await expect(page.getByRole('link', {name: 'Timo M. Staudinger'})).toBeVisible()
})

test('has link to Uses page', async ({page}) => {
await expect(page.getByRole('link', {name: 'Uses'})).toBeVisible()
})

test('renders social links', async ({page}) => {
await expect(page.locator('a[href*="github.com"]')).toBeVisible()
await expect(page.locator('a[href*="linkedin.com"]')).toBeVisible()
})

test('visual regression', async ({page}) => {
await expect(page).toHaveScreenshot('home.png', {maxDiffPixelRatio: 0.01})
})
})
Binary file added e2e/home.spec.ts-snapshots/home-chromium.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/home.spec.ts-snapshots/home-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions e2e/ice-auth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {test, expect} from '@playwright/test'

test.describe('ICE auth page', () => {
test.beforeEach(async ({page}) => {
await page.goto('/ice/auth')
})

test('renders the auth form', async ({page}) => {
await expect(page.getByRole('heading', {name: /In Case of Emergency/})).toBeVisible()
await expect(page.getByPlaceholder('Password')).toBeVisible()
await expect(page.getByRole('button', {name: 'Continue'})).toBeVisible()
})

test('password field is numeric input mode', async ({page}) => {
const input = page.getByPlaceholder('Password')
await expect(input).toHaveAttribute('type', 'password')
await expect(input).toHaveAttribute('inputmode', 'numeric')
})

test('rejects invalid PIN', async ({page}) => {
await page.getByPlaceholder('Password').fill('0000')
await page.getByRole('button', {name: 'Continue'}).click()
await page.waitForURL(/\/ice\/auth/)
})

test('visual regression', async ({page}) => {
await expect(page).toHaveScreenshot('ice-auth.png', {maxDiffPixelRatio: 0.01})
})
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions e2e/navigation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {test, expect} from '@playwright/test'

test.describe('Navigation', () => {
test('navigate from home to uses', async ({page}) => {
await page.goto('/')
await page.getByRole('link', {name: 'Uses'}).click()
await expect(page).toHaveURL('/uses')
await expect(page.getByRole('heading', {name: 'Uses'})).toBeVisible()
})

test('navigate from uses back to home via title', async ({page}) => {
await page.goto('/uses')
await page.getByRole('link', {name: 'Timo M. Staudinger'}).click()
await expect(page).toHaveURL('/')
await expect(page.getByRole('heading', {name: /Hi! I'm Timo/})).toBeVisible()
})

test('404 page for unknown routes', async ({page}) => {
const response = await page.goto('/nonexistent-page')
expect(response?.status()).toBe(404)
})
})
37 changes: 37 additions & 0 deletions e2e/uses.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {test, expect} from '@playwright/test'

test.describe('Uses page', () => {
test.beforeEach(async ({page}) => {
await page.goto('/uses')
})

test('renders the page header', async ({page}) => {
await expect(page.getByRole('heading', {name: 'Uses'})).toBeVisible()
await expect(page.getByText(/Software and Hardware/)).toBeVisible()
})

test('renders editor section', async ({page}) => {
await expect(page.getByRole('heading', {name: 'Editor'})).toBeVisible()
await expect(page.getByRole('link', {name: 'Visual Studio Code'})).toBeVisible()
})

test('renders hardware section', async ({page}) => {
await expect(page.getByRole('heading', {name: 'Hardware'})).toBeVisible()
})

test('renders desktop apps section', async ({page}) => {
await expect(page.getByRole('heading', {name: 'Desktop Apps'})).toBeVisible()
})

test('external links open correctly', async ({page}) => {
const vscodeLink = page.getByRole('link', {name: 'Visual Studio Code'})
await expect(vscodeLink).toHaveAttribute('href', 'https://code.visualstudio.com/')
})

test('visual regression', async ({page}) => {
await expect(page).toHaveScreenshot('uses.png', {
maxDiffPixelRatio: 0.01,
fullPage: true,
})
})
})
Binary file added e2e/uses.spec.ts-snapshots/uses-chromium.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/uses.spec.ts-snapshots/uses-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
"start": "next start",
"test": "playwright test",
"test:update-snapshots": "playwright test --update-snapshots"
},
"dependencies": {
"@vercel/analytics": "^2.0.1",
Expand All @@ -27,6 +29,7 @@
},
"devDependencies": {
"@next/eslint-plugin-next": "^16.2.1",
"@playwright/test": "^1.58.2",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^25.5.0",
"@types/react": "^19.2.9",
Expand Down
31 changes: 31 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {defineConfig, devices} from '@playwright/test'

export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? 'github' : 'html',
snapshotPathTemplate: '{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}-{projectName}{ext}',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: {...devices['Desktop Chrome']},
},
{
name: 'mobile',
use: {...devices['Pixel 7']},
},
],
webServer: {
command: 'yarn build && yarn start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
})
26 changes: 26 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,13 @@
resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e"
integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==

"@playwright/test@^1.58.2":
version "1.58.2"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.58.2.tgz#b0ad585d2e950d690ef52424967a42f40c6d2cbd"
integrity sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==
dependencies:
playwright "1.58.2"

"@rtsao/scc@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
Expand Down Expand Up @@ -1678,6 +1685,11 @@ for-each@^0.3.3, for-each@^0.3.5:
dependencies:
is-callable "^1.2.7"

fsevents@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==

function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
Expand Down Expand Up @@ -2790,6 +2802,20 @@ picomatch@^4.0.3:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==

playwright-core@1.58.2:
version "1.58.2"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.58.2.tgz#ac5f5b4b10d29bcf934415f0b8d133b34b0dcb13"
integrity sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==

playwright@1.58.2:
version "1.58.2"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.58.2.tgz#afe547164539b0bcfcb79957394a7a3fa8683cfd"
integrity sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==
dependencies:
playwright-core "1.58.2"
optionalDependencies:
fsevents "2.3.2"

possible-typed-array-names@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae"
Expand Down
Loading