Adding playwright tests for home and about page. Adding github actions.

This commit is contained in:
Bradley Shellnut 2025-08-23 14:54:11 -07:00
parent 8a66b190b7
commit 2714d05917
12 changed files with 807 additions and 221 deletions

View file

@ -2,6 +2,10 @@ name: Run_Svelte_Check_on_PRs
on:
pull_request:
branches:
- master
- main
- development
workflow_dispatch:

View file

@ -0,0 +1,48 @@
name: Run_Svelte_Integration_on_PRs
on:
push:
branches:
- master
- main
- development
workflow_dispatch:
env:
WALLABAG_MAX_ARTICLES: ${{ secrets.WALLABAG_MAX_ARTICLES }}
WALLABAG_MAX_PAGES: ${{ secrets.WALLABAG_MAX_PAGES }}
WALLABAG_CLIENT_ID: ${{ secrets.WALLABAG_CLIENT_ID }}
WALLABAG_CLIENT_SECRET: ${{ secrets.WALLABAG_CLIENT_SECRET }}
WALLABAG_USERNAME: ${{ secrets.WALLABAG_USERNAME }}
WALLABAG_PASSWORD: ${{ secrets.WALLABAG_PASSWORD }}
WALLABAG_URL: ${{ secrets.WALLABAG_URL }}
BANDCAMP_USERNAME: ${{ secrets.BANDCAMP_USERNAME }}
PUBLIC_SITE_URL: ${{ secrets.PUBLIC_SITE_URL }}
PUBLIC_URL: ${{ secrets.PUBLIC_URL }}
PUBLIC_UMAMI_DO_NOT_TRACK: ${{ secrets.PUBLIC_UMAMI_DO_NOT_TRACK }}
PUBLIC_UMAMI_URL: ${{ secrets.PUBLIC_UMAMI_URL }}
PUBLIC_UMAMI_ID: ${{ secrets.PUBLIC_UMAMI_ID }}
PAGE_SIZE: ${{ secrets.PAGE_SIZE }}
USE_REDIS_CACHE: ${{ secrets.USE_REDIS_CACHE }}
REDIS_URI: ${{ secrets.REDIS_URI }}
jobs:
e2e-tests:
runs-on: ubuntu-latest # or macos-latest, windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: microsoft/playwright-github-action@v2
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'pnpm'
- name: Install dependencies and run tests
run: pnpm install && pnpm test:integration

58
.github/workflows/svelte_unit.yml vendored Normal file
View file

@ -0,0 +1,58 @@
name: Run_Svelte_Unit_on_PRs
on:
push:
branches:
- master
- main
- development
pull_request:
branches:
- master
- main
- development
workflow_dispatch:
env:
WALLABAG_MAX_ARTICLES: ${{ secrets.WALLABAG_MAX_ARTICLES }}
WALLABAG_MAX_PAGES: ${{ secrets.WALLABAG_MAX_PAGES }}
WALLABAG_CLIENT_ID: ${{ secrets.WALLABAG_CLIENT_ID }}
WALLABAG_CLIENT_SECRET: ${{ secrets.WALLABAG_CLIENT_SECRET }}
WALLABAG_USERNAME: ${{ secrets.WALLABAG_USERNAME }}
WALLABAG_PASSWORD: ${{ secrets.WALLABAG_PASSWORD }}
WALLABAG_URL: ${{ secrets.WALLABAG_URL }}
BANDCAMP_USERNAME: ${{ secrets.BANDCAMP_USERNAME }}
PUBLIC_SITE_URL: ${{ secrets.PUBLIC_SITE_URL }}
PUBLIC_URL: ${{ secrets.PUBLIC_URL }}
PUBLIC_UMAMI_DO_NOT_TRACK: ${{ secrets.PUBLIC_UMAMI_DO_NOT_TRACK }}
PUBLIC_UMAMI_URL: ${{ secrets.PUBLIC_UMAMI_URL }}
PUBLIC_UMAMI_ID: ${{ secrets.PUBLIC_UMAMI_ID }}
PAGE_SIZE: ${{ secrets.PAGE_SIZE }}
USE_REDIS_CACHE: ${{ secrets.USE_REDIS_CACHE }}
REDIS_URI: ${{ secrets.REDIS_URI }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: pnpm-setup
uses: pnpm/action-setup@v4
with:
version: 10
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Execute unit tests
run: pnpm test:unit

3
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"workbench.colorTheme": "Dark+ (default dark)",
}

View file

@ -18,7 +18,7 @@
"test:unit": "vitest"
},
"devDependencies": {
"@biomejs/biome": "^2.2.0",
"@biomejs/biome": "^2.2.2",
"@chromatic-com/storybook": "^4.1.1",
"@internationalized/date": "^3.8.2",
"@playwright/test": "^1.55.0",
@ -27,7 +27,7 @@
"@storybook/addon-svelte-csf": "^5.0.8",
"@storybook/sveltekit": "^9.1.3",
"@sveltejs/enhanced-img": "^0.5.1",
"@sveltejs/kit": "^2.36.1",
"@sveltejs/kit": "^2.36.2",
"@sveltejs/vite-plugin-svelte": "^5.1.1",
"@unpic/svelte": "^1.0.0",
"@zerodevx/svelte-img": "^2.1.2",
@ -37,7 +37,7 @@
"postcss-custom-media": "^11.0.6",
"postcss-import": "^16.1.1",
"postcss-load-config": "^6.0.1",
"postcss-preset-env": "^10.2.4",
"postcss-preset-env": "^10.3.0",
"satori": "^0.12.2",
"satori-html": "^0.3.2",
"storybook": "^9.1.3",
@ -57,10 +57,10 @@
"@resvg/resvg-js": "^2.6.2",
"@sveltejs/adapter-node": "^5.3.1",
"@vercel/og": "^0.6.8",
"bits-ui": "2.9.2",
"bits-ui": "2.9.4",
"flexsearch": "^0.8.205",
"ioredis": "^5.7.0",
"lucide-svelte": "^0.539.0",
"lucide-svelte": "^0.541.0",
"scrape-it": "^6.1.11",
"sharp": "^0.34.3",
"svelte-local-storage-store": "^0.6.4"

View file

@ -3,7 +3,9 @@ import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
command: 'pnpm run build && pnpm run preview',
port: 4173
port: 4173,
timeout: 180_000,
reuseExistingServer: true
},
testDir: 'tests',
testMatch: /(.+\.)?(test|spec)\.[jt]s/,

View file

@ -13,13 +13,13 @@ importers:
version: 2.6.2
'@sveltejs/adapter-node':
specifier: ^5.3.1
version: 5.3.1(@sveltejs/kit@2.36.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))
version: 5.3.1(@sveltejs/kit@2.36.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))
'@vercel/og':
specifier: ^0.6.8
version: 0.6.8
bits-ui:
specifier: 2.9.2
version: 2.9.2(@internationalized/date@3.8.2)(svelte@5.38.2)
specifier: 2.9.4
version: 2.9.4(@internationalized/date@3.8.2)(svelte@5.38.2)
flexsearch:
specifier: ^0.8.205
version: 0.8.205
@ -27,8 +27,8 @@ importers:
specifier: ^5.7.0
version: 5.7.0
lucide-svelte:
specifier: ^0.539.0
version: 0.539.0(svelte@5.38.2)
specifier: ^0.541.0
version: 0.541.0(svelte@5.38.2)
scrape-it:
specifier: ^6.1.11
version: 6.1.11
@ -40,8 +40,8 @@ importers:
version: 0.6.4(svelte@5.38.2)
devDependencies:
'@biomejs/biome':
specifier: ^2.2.0
version: 2.2.0
specifier: ^2.2.2
version: 2.2.2
'@chromatic-com/storybook':
specifier: ^4.1.1
version: 4.1.1(storybook@9.1.3(@testing-library/dom@10.4.1)(vite@6.3.5(yaml@2.7.0)))
@ -67,8 +67,8 @@ importers:
specifier: ^0.5.1
version: 0.5.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))(rollup@4.34.8)(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0))
'@sveltejs/kit':
specifier: ^2.36.1
version: 2.36.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0))
specifier: ^2.36.2
version: 2.36.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0))
'@sveltejs/vite-plugin-svelte':
specifier: ^5.1.1
version: 5.1.1(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0))
@ -97,8 +97,8 @@ importers:
specifier: ^6.0.1
version: 6.0.1(postcss@8.5.6)(yaml@2.7.0)
postcss-preset-env:
specifier: ^10.2.4
version: 10.2.4(postcss@8.5.6)
specifier: ^10.3.0
version: 10.3.0(postcss@8.5.6)
satori:
specifier: ^0.12.2
version: 0.12.2
@ -113,7 +113,7 @@ importers:
version: 5.38.2
svelte-check:
specifier: ^4.3.1
version: 4.3.1(picomatch@4.0.3)(svelte@5.38.2)(typescript@5.9.2)
version: 4.3.1(picomatch@4.0.2)(svelte@5.38.2)(typescript@5.9.2)
svelte-meta-tags:
specifier: ^4.4.0
version: 4.4.0(svelte@5.38.2)
@ -163,55 +163,55 @@ packages:
resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
engines: {node: '>=6.9.0'}
'@biomejs/biome@2.2.0':
resolution: {integrity: sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMN1yAAvHtzIw==}
'@biomejs/biome@2.2.2':
resolution: {integrity: sha512-j1omAiQWCkhuLgwpMKisNKnsM6W8Xtt1l0WZmqY/dFj8QPNkIoTvk4tSsi40FaAAkBE1PU0AFG2RWFBWenAn+w==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@2.2.0':
resolution: {integrity: sha512-zKbwUUh+9uFmWfS8IFxmVD6XwqFcENjZvEyfOxHs1epjdH3wyyMQG80FGDsmauPwS2r5kXdEM0v/+dTIA9FXAg==}
'@biomejs/cli-darwin-arm64@2.2.2':
resolution: {integrity: sha512-6ePfbCeCPryWu0CXlzsWNZgVz/kBEvHiPyNpmViSt6A2eoDf4kXs3YnwQPzGjy8oBgQulrHcLnJL0nkCh80mlQ==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@2.2.0':
resolution: {integrity: sha512-+OmT4dsX2eTfhD5crUOPw3RPhaR+SKVspvGVmSdZ9y9O/AgL8pla6T4hOn1q+VAFBHuHhsdxDRJgFCSC7RaMOw==}
'@biomejs/cli-darwin-x64@2.2.2':
resolution: {integrity: sha512-Tn4JmVO+rXsbRslml7FvKaNrlgUeJot++FkvYIhl1OkslVCofAtS35MPlBMhXgKWF9RNr9cwHanrPTUUXcYGag==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@2.2.0':
resolution: {integrity: sha512-egKpOa+4FL9YO+SMUMLUvf543cprjevNc3CAgDNFLcjknuNMcZ0GLJYa3EGTCR2xIkIUJDVneBV3O9OcIlCEZQ==}
'@biomejs/cli-linux-arm64-musl@2.2.2':
resolution: {integrity: sha512-/MhYg+Bd6renn6i1ylGFL5snYUn/Ct7zoGVKhxnro3bwekiZYE8Kl39BSb0MeuqM+72sThkQv4TnNubU9njQRw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-arm64@2.2.0':
resolution: {integrity: sha512-6eoRdF2yW5FnW9Lpeivh7Mayhq0KDdaDMYOJnH9aT02KuSIX5V1HmWJCQQPwIQbhDh68Zrcpl8inRlTEan0SXw==}
'@biomejs/cli-linux-arm64@2.2.2':
resolution: {integrity: sha512-JfrK3gdmWWTh2J5tq/rcWCOsImVyzUnOS2fkjhiYKCQ+v8PqM+du5cfB7G1kXas+7KQeKSWALv18iQqdtIMvzw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-x64-musl@2.2.0':
resolution: {integrity: sha512-I5J85yWwUWpgJyC1CcytNSGusu2p9HjDnOPAFG4Y515hwRD0jpR9sT9/T1cKHtuCvEQ/sBvx+6zhz9l9wEJGAg==}
'@biomejs/cli-linux-x64-musl@2.2.2':
resolution: {integrity: sha512-ZCLXcZvjZKSiRY/cFANKg+z6Fhsf9MHOzj+NrDQcM+LbqYRT97LyCLWy2AS+W2vP+i89RyRM+kbGpUzbRTYWig==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-linux-x64@2.2.0':
resolution: {integrity: sha512-5UmQx/OZAfJfi25zAnAGHUMuOd+LOsliIt119x2soA2gLggQYrVPA+2kMUxR6Mw5M1deUF/AWWP2qpxgH7Nyfw==}
'@biomejs/cli-linux-x64@2.2.2':
resolution: {integrity: sha512-Ogb+77edO5LEP/xbNicACOWVLt8mgC+E1wmpUakr+O4nKwLt9vXe74YNuT3T1dUBxC/SnrVmlzZFC7kQJEfquQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-win32-arm64@2.2.0':
resolution: {integrity: sha512-n9a1/f2CwIDmNMNkFs+JI0ZjFnMO0jdOyGNtihgUNFnlmd84yIYY2KMTBmMV58ZlVHjgmY5Y6E1hVTnSRieggA==}
'@biomejs/cli-win32-arm64@2.2.2':
resolution: {integrity: sha512-wBe2wItayw1zvtXysmHJQoQqXlTzHSpQRyPpJKiNIR21HzH/CrZRDFic1C1jDdp+zAPtqhNExa0owKMbNwW9cQ==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@2.2.0':
resolution: {integrity: sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww==}
'@biomejs/cli-win32-x64@2.2.2':
resolution: {integrity: sha512-DAuHhHekGfiGb6lCcsT4UyxQmVwQiBCBUMwVra/dcOSs9q8OhfaZgey51MlekT3p8UwRqtXQfFuEJBhJNdLZwg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
@ -229,8 +229,8 @@ packages:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/color-helpers@5.0.2':
resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==}
'@csstools/color-helpers@5.1.0':
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
engines: {node: '>=18'}
'@csstools/css-calc@2.1.4':
@ -240,8 +240,8 @@ packages:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-color-parser@3.0.10':
resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==}
'@csstools/css-color-parser@3.1.0':
resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.5
@ -264,32 +264,44 @@ packages:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/postcss-alpha-function@1.0.0':
resolution: {integrity: sha512-r2L8KNg5Wriq5n8IUQcjzy2Rh37J5YjzP9iOyHZL5fxdWYHB08vqykHQa4wAzN/tXwDuCHnhQDGCtxfS76xn7g==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-cascade-layers@5.0.2':
resolution: {integrity: sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-color-function@4.0.10':
resolution: {integrity: sha512-4dY0NBu7NVIpzxZRgh/Q/0GPSz/jLSw0i/u3LTUor0BkQcz/fNhN10mSWBDsL0p9nDb0Ky1PD6/dcGbhACuFTQ==}
'@csstools/postcss-color-function-display-p3-linear@1.0.0':
resolution: {integrity: sha512-7q+OuUqfowRrP84m/Jl0wv3pfCQyUTCW5MxDIux+/yty5IkUUHOTigCjrC0Fjy3OT0ncGLudHbfLWmP7E1arNA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-color-mix-function@3.0.10':
resolution: {integrity: sha512-P0lIbQW9I4ShE7uBgZRib/lMTf9XMjJkFl/d6w4EMNHu2qvQ6zljJGEcBkw/NsBtq/6q3WrmgxSS8kHtPMkK4Q==}
'@csstools/postcss-color-function@4.0.11':
resolution: {integrity: sha512-AtH22zLHTLm64HLdpv5EedT/zmYTm1MtdQbQhRZXxEB6iYtS6SrS1jLX3TcmUWMFzpumK/OVylCm3HcLms4slw==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-color-mix-variadic-function-arguments@1.0.0':
resolution: {integrity: sha512-Z5WhouTyD74dPFPrVE7KydgNS9VvnjB8qcdes9ARpCOItb4jTnm7cHp4FhxCRUoyhabD0WVv43wbkJ4p8hLAlQ==}
'@csstools/postcss-color-mix-function@3.0.11':
resolution: {integrity: sha512-cQpXBelpTx0YhScZM5Ve0jDCA4RzwFc7oNafzZOGgCHt/GQVYiU8Vevz9QJcwy/W0Pyi/BneY+KMjz23lI9r+Q==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-content-alt-text@2.0.6':
resolution: {integrity: sha512-eRjLbOjblXq+byyaedQRSrAejKGNAFued+LcbzT+LCL78fabxHkxYjBbxkroONxHHYu2qxhFK2dBStTLPG3jpQ==}
'@csstools/postcss-color-mix-variadic-function-arguments@1.0.1':
resolution: {integrity: sha512-c7hyBtbF+jlHIcUGVdWY06bHICgguV9ypfcELU3eU3W/9fiz2dxM8PqxQk2ndXYTzLnwPvNNqu1yCmQ++N6Dcg==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-content-alt-text@2.0.7':
resolution: {integrity: sha512-cq/zWaEkpcg3RttJ5+GdNwk26NwxY5KgqgtNL777Fdd28AVGHxuBvqmK4Jq4oKhW1NX4M2LbgYAVVN0NZ+/XYQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
@ -306,26 +318,26 @@ packages:
peerDependencies:
postcss: ^8.4
'@csstools/postcss-gamut-mapping@2.0.10':
resolution: {integrity: sha512-QDGqhJlvFnDlaPAfCYPsnwVA6ze+8hhrwevYWlnUeSjkkZfBpcCO42SaUD8jiLlq7niouyLgvup5lh+f1qessg==}
'@csstools/postcss-gamut-mapping@2.0.11':
resolution: {integrity: sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-gradients-interpolation-method@5.0.10':
resolution: {integrity: sha512-HHPauB2k7Oits02tKFUeVFEU2ox/H3OQVrP3fSOKDxvloOikSal+3dzlyTZmYsb9FlY9p5EUpBtz0//XBmy+aw==}
'@csstools/postcss-gradients-interpolation-method@5.0.11':
resolution: {integrity: sha512-8M3mcNTL3cGIJXDnvrJ2oWEcKi3zyw7NeYheFKePUlBmLYm1gkw9Rr/BA7lFONrOPeQA3yeMPldrrws6lqHrug==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-hwb-function@4.0.10':
resolution: {integrity: sha512-nOKKfp14SWcdEQ++S9/4TgRKchooLZL0TUFdun3nI4KPwCjETmhjta1QT4ICQcGVWQTvrsgMM/aLB5We+kMHhQ==}
'@csstools/postcss-hwb-function@4.0.11':
resolution: {integrity: sha512-9meZbsVWTZkWsSBazQips3cHUOT29a/UAwFz0AMEXukvpIGGDR9+GMl3nIckWO5sPImsadu4F5Zy+zjt8QgCdA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-ic-unit@4.0.2':
resolution: {integrity: sha512-lrK2jjyZwh7DbxaNnIUjkeDmU8Y6KyzRBk91ZkI5h8nb1ykEfZrtIVArdIjX4DHMIBGpdHrgP0n4qXDr7OHaKA==}
'@csstools/postcss-ic-unit@4.0.3':
resolution: {integrity: sha512-RtYYm2qUIu9vAaHB0cC8rQGlOCQAUgEc2tMr7ewlGXYipBQKjoWmyVArqsk7SEr8N3tErq6P6UOJT3amaVof5Q==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
@ -342,8 +354,8 @@ packages:
peerDependencies:
postcss: ^8.4
'@csstools/postcss-light-dark-function@2.0.9':
resolution: {integrity: sha512-1tCZH5bla0EAkFAI2r0H33CDnIBeLUaJh1p+hvvsylJ4svsv2wOmJjJn+OXwUZLXef37GYbRIVKX+X+g6m+3CQ==}
'@csstools/postcss-light-dark-function@2.0.10':
resolution: {integrity: sha512-g7Lwb294lSoNnyrwcqoooh9fTAp47rRNo+ILg7SLRSMU3K9ePIwRt566sNx+pehiCelv4E1ICaU1EwLQuyF2qw==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
@ -402,14 +414,14 @@ packages:
peerDependencies:
postcss: ^8.4
'@csstools/postcss-oklab-function@4.0.10':
resolution: {integrity: sha512-ZzZUTDd0fgNdhv8UUjGCtObPD8LYxMH+MJsW9xlZaWTV8Ppr4PtxlHYNMmF4vVWGl0T6f8tyWAKjoI6vePSgAg==}
'@csstools/postcss-oklab-function@4.0.11':
resolution: {integrity: sha512-9f03ZGxZ2VmSCrM4SDXlAYP+Xpu4VFzemfQUQFL9OYxAbpvDy0FjDipZ0i8So1pgs8VIbQI0bNjFWgfdpGw8ig==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-progressive-custom-properties@4.1.0':
resolution: {integrity: sha512-YrkI9dx8U4R8Sz2EJaoeD9fI7s7kmeEBfmO+UURNeL6lQI7VxF6sBE+rSqdCBn4onwqmxFdBU3lTwyYb/lCmxA==}
'@csstools/postcss-progressive-custom-properties@4.2.0':
resolution: {integrity: sha512-fWCXRasX17N1NCPTCuwC3FJDV+Wc031f16cFuuMEfIsYJ1q5ABCa59W0C6VeMGqjNv6ldf37vvwXXAeaZjD9PA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
@ -420,8 +432,8 @@ packages:
peerDependencies:
postcss: ^8.4
'@csstools/postcss-relative-color-syntax@3.0.10':
resolution: {integrity: sha512-8+0kQbQGg9yYG8hv0dtEpOMLwB9M+P7PhacgIzVzJpixxV4Eq9AUQtQw8adMmAJU1RBBmIlpmtmm3XTRd/T00g==}
'@csstools/postcss-relative-color-syntax@3.0.11':
resolution: {integrity: sha512-oQ5fZvkcBrWR+k6arHXk0F8FlkmD4IxM+rcGDLWrF2f31tWyEM3lSraeWAV0f7BGH6LIrqmyU3+Qo/1acfoJng==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
@ -444,8 +456,8 @@ packages:
peerDependencies:
postcss: ^8.4
'@csstools/postcss-text-decoration-shorthand@4.0.2':
resolution: {integrity: sha512-8XvCRrFNseBSAGxeaVTaNijAu+FzUvjwFXtcrynmazGb/9WUdsPCpBX+mHEHShVRq47Gy4peYAoxYs8ltUnmzA==}
'@csstools/postcss-text-decoration-shorthand@4.0.3':
resolution: {integrity: sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
@ -639,8 +651,8 @@ packages:
'@floating-ui/core@1.7.3':
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
'@floating-ui/dom@1.7.3':
resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==}
'@floating-ui/dom@1.7.4':
resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
@ -1332,8 +1344,8 @@ packages:
svelte: ^5.0.0
vite: '>= 5.0.0'
'@sveltejs/kit@2.36.1':
resolution: {integrity: sha512-dldNCtSIpaGxQMEfHaUxSPH/k3uU28pTZwtKzfkn8fqpOjWufKlMBeIL7FJ/s93dOrhEq41zaQYkXh+XTgEgVw==}
'@sveltejs/kit@2.36.2':
resolution: {integrity: sha512-WlBGY060nHe4UE5QrDAJAbls5hOsG6mljtrDGkM8jJCDQ4JEcAEH04XrTVmQ0Ex1CU8nzoZto0EE75aiLA3G8Q==}
engines: {node: '>=18.13'}
hasBin: true
peerDependencies:
@ -1522,8 +1534,8 @@ packages:
resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}
engines: {node: '>=12.0.0'}
bits-ui@2.9.2:
resolution: {integrity: sha512-GGbyr4oVKtHin//Q0AhlygkasmfWt328VjsnmB3sP+h8Sh+Eyghm+1AQ8o+xQMDCYbdL35JZ9UZGTZYTMar4Uw==}
bits-ui@2.9.4:
resolution: {integrity: sha512-Cqn685P6DDuEyBZT/CWMyS5+8JAnYbctvoEVPcmiut+HUpG3SozVgjoDaUib5VG4ZYUKEi1FPwHxiXo9c6J0PA==}
engines: {node: '>=20'}
peerDependencies:
'@internationalized/date': ^3.8.1
@ -1675,8 +1687,8 @@ packages:
css.escape@1.5.1:
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
cssdb@8.3.1:
resolution: {integrity: sha512-XnDRQMXucLueX92yDe0LPKupXetWoFOgawr4O4X41l5TltgK2NVbJJVDnnOywDYfW1sTJ28AcXGKOqdRKwCcmQ==}
cssdb@8.4.0:
resolution: {integrity: sha512-lyATYGyvXwQ8h55WeQeEHXhI+47rl52pXSYkFK/ZrCbAJSgVIaPFjYc3RM8TpRHKk7W3wsAZImmLps+P5VyN9g==}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
@ -2060,8 +2072,8 @@ packages:
lower-case@2.0.2:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
lucide-svelte@0.539.0:
resolution: {integrity: sha512-p4k3GOje/9Si1eIkg1W1OQUhozeja5Ka5shjVpfyP5X2ye+B7sfyMnX3d5D2et+MYJwUFGrMna5MIYgq6bLfqw==}
lucide-svelte@0.541.0:
resolution: {integrity: sha512-Jk+LiOYDl62R/0nWkG1s5XL2k6LHmPq3wUfiJ6qtBhb8jGefB4PU10x5HJrAihwaKqVc2vH5wjKMELGjHJenEQ==}
peerDependencies:
svelte: ^3 || ^4 || ^5.0.0-next.42
@ -2198,8 +2210,8 @@ packages:
peerDependencies:
postcss: ^8.4.6
postcss-color-functional-notation@7.0.10:
resolution: {integrity: sha512-k9qX+aXHBiLTRrWoCJuUFI6F1iF6QJQUXNVWJVSbqZgj57jDhBlOvD8gNUGl35tgqDivbGLhZeW3Ongz4feuKA==}
postcss-color-functional-notation@7.0.11:
resolution: {integrity: sha512-zfqoUSaHMko/k2PA9xnaydVTHqYv5vphq5Q2AHcG/dCdv/OkHYWcVWfVTBKZ526uzT8L7NghuvSw3C9PxlKnLg==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
@ -2240,8 +2252,8 @@ packages:
peerDependencies:
postcss: ^8.4
postcss-double-position-gradients@6.0.2:
resolution: {integrity: sha512-7qTqnL7nfLRyJK/AHSVrrXOuvDDzettC+wGoienURV8v2svNbu6zJC52ruZtHaO6mfcagFmuTGFdzRsJKB3k5Q==}
postcss-double-position-gradients@6.0.3:
resolution: {integrity: sha512-Dl0Z9sdbMwrPslgOaGBZRGo3TASmmgTcqcUODr82MTYyJk6devXZM6MlQjpQKMJqlLJ6oL1w78U7IXFdPA5+ug==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
@ -2281,8 +2293,8 @@ packages:
peerDependencies:
postcss: ^8.0.0
postcss-lab-function@7.0.10:
resolution: {integrity: sha512-tqs6TCEv9tC1Riq6fOzHuHcZyhg4k3gIAMB8GGY/zA1ssGdm6puHMVE7t75aOSoFg7UD2wyrFFhbldiCMyyFTQ==}
postcss-lab-function@7.0.11:
resolution: {integrity: sha512-BEA4jId8uQe1gyjZZ6Bunb6ZsH2izks+v25AxQJDBtigXCjTLmCPWECwQpLTtcxH589MVxhs/9TAmRC6lUEmXQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
@ -2340,8 +2352,8 @@ packages:
peerDependencies:
postcss: ^8.4
postcss-preset-env@10.2.4:
resolution: {integrity: sha512-q+lXgqmTMdB0Ty+EQ31SuodhdfZetUlwCA/F0zRcd/XdxjzI+Rl2JhZNz5US2n/7t9ePsvuhCnEN4Bmu86zXlA==}
postcss-preset-env@10.3.0:
resolution: {integrity: sha512-khE99iwEbWLzXBVFNsS0QdnfYXDpqH/pxoHFcaCaVlh+e29swc3UyiLSSJ89dTK8e+Si3wNKYDGs6jEMmbE8TQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
@ -2876,39 +2888,39 @@ snapshots:
'@babel/runtime@7.28.3': {}
'@biomejs/biome@2.2.0':
'@biomejs/biome@2.2.2':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.2.0
'@biomejs/cli-darwin-x64': 2.2.0
'@biomejs/cli-linux-arm64': 2.2.0
'@biomejs/cli-linux-arm64-musl': 2.2.0
'@biomejs/cli-linux-x64': 2.2.0
'@biomejs/cli-linux-x64-musl': 2.2.0
'@biomejs/cli-win32-arm64': 2.2.0
'@biomejs/cli-win32-x64': 2.2.0
'@biomejs/cli-darwin-arm64': 2.2.2
'@biomejs/cli-darwin-x64': 2.2.2
'@biomejs/cli-linux-arm64': 2.2.2
'@biomejs/cli-linux-arm64-musl': 2.2.2
'@biomejs/cli-linux-x64': 2.2.2
'@biomejs/cli-linux-x64-musl': 2.2.2
'@biomejs/cli-win32-arm64': 2.2.2
'@biomejs/cli-win32-x64': 2.2.2
'@biomejs/cli-darwin-arm64@2.2.0':
'@biomejs/cli-darwin-arm64@2.2.2':
optional: true
'@biomejs/cli-darwin-x64@2.2.0':
'@biomejs/cli-darwin-x64@2.2.2':
optional: true
'@biomejs/cli-linux-arm64-musl@2.2.0':
'@biomejs/cli-linux-arm64-musl@2.2.2':
optional: true
'@biomejs/cli-linux-arm64@2.2.0':
'@biomejs/cli-linux-arm64@2.2.2':
optional: true
'@biomejs/cli-linux-x64-musl@2.2.0':
'@biomejs/cli-linux-x64-musl@2.2.2':
optional: true
'@biomejs/cli-linux-x64@2.2.0':
'@biomejs/cli-linux-x64@2.2.2':
optional: true
'@biomejs/cli-win32-arm64@2.2.0':
'@biomejs/cli-win32-arm64@2.2.2':
optional: true
'@biomejs/cli-win32-x64@2.2.0':
'@biomejs/cli-win32-x64@2.2.2':
optional: true
'@chromatic-com/storybook@4.1.1(storybook@9.1.3(@testing-library/dom@10.4.1)(vite@6.3.5(yaml@2.7.0)))':
@ -2928,16 +2940,16 @@ snapshots:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/color-helpers@5.0.2': {}
'@csstools/color-helpers@5.1.0': {}
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
'@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/color-helpers': 5.0.2
'@csstools/color-helpers': 5.1.0
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
@ -2953,44 +2965,62 @@ snapshots:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-alpha-function@1.0.0(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.6)':
dependencies:
'@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0)
postcss: 8.5.6
postcss-selector-parser: 7.1.0
'@csstools/postcss-color-function@4.0.10(postcss@8.5.6)':
'@csstools/postcss-color-function-display-p3-linear@1.0.0(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-color-mix-function@3.0.10(postcss@8.5.6)':
'@csstools/postcss-color-function@4.0.11(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-color-mix-variadic-function-arguments@1.0.0(postcss@8.5.6)':
'@csstools/postcss-color-mix-function@3.0.11(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-content-alt-text@2.0.6(postcss@8.5.6)':
'@csstools/postcss-color-mix-variadic-function-arguments@1.0.1(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-content-alt-text@2.0.7(postcss@8.5.6)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
@ -3007,34 +3037,34 @@ snapshots:
postcss: 8.5.6
postcss-value-parser: 4.2.0
'@csstools/postcss-gamut-mapping@2.0.10(postcss@8.5.6)':
'@csstools/postcss-gamut-mapping@2.0.11(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
postcss: 8.5.6
'@csstools/postcss-gradients-interpolation-method@5.0.10(postcss@8.5.6)':
'@csstools/postcss-gradients-interpolation-method@5.0.11(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-hwb-function@4.0.10(postcss@8.5.6)':
'@csstools/postcss-hwb-function@4.0.11(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-ic-unit@4.0.2(postcss@8.5.6)':
'@csstools/postcss-ic-unit@4.0.3(postcss@8.5.6)':
dependencies:
'@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
postcss-value-parser: 4.2.0
@ -3049,11 +3079,11 @@ snapshots:
postcss: 8.5.6
postcss-selector-parser: 7.1.0
'@csstools/postcss-light-dark-function@2.0.9(postcss@8.5.6)':
'@csstools/postcss-light-dark-function@2.0.10(postcss@8.5.6)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
@ -3106,16 +3136,16 @@ snapshots:
postcss: 8.5.6
postcss-value-parser: 4.2.0
'@csstools/postcss-oklab-function@4.0.10(postcss@8.5.6)':
'@csstools/postcss-oklab-function@4.0.11(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-progressive-custom-properties@4.1.0(postcss@8.5.6)':
'@csstools/postcss-progressive-custom-properties@4.2.0(postcss@8.5.6)':
dependencies:
postcss: 8.5.6
postcss-value-parser: 4.2.0
@ -3127,12 +3157,12 @@ snapshots:
'@csstools/css-tokenizer': 3.0.4
postcss: 8.5.6
'@csstools/postcss-relative-color-syntax@3.0.10(postcss@8.5.6)':
'@csstools/postcss-relative-color-syntax@3.0.11(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
@ -3155,9 +3185,9 @@ snapshots:
'@csstools/css-tokenizer': 3.0.4
postcss: 8.5.6
'@csstools/postcss-text-decoration-shorthand@4.0.2(postcss@8.5.6)':
'@csstools/postcss-text-decoration-shorthand@4.0.3(postcss@8.5.6)':
dependencies:
'@csstools/color-helpers': 5.0.2
'@csstools/color-helpers': 5.1.0
postcss: 8.5.6
postcss-value-parser: 4.2.0
@ -3273,7 +3303,7 @@ snapshots:
dependencies:
'@floating-ui/utils': 0.2.10
'@floating-ui/dom@1.7.3':
'@floating-ui/dom@1.7.4':
dependencies:
'@floating-ui/core': 1.7.3
'@floating-ui/utils': 0.2.10
@ -3809,12 +3839,12 @@ snapshots:
dependencies:
acorn: 8.15.0
'@sveltejs/adapter-node@5.3.1(@sveltejs/kit@2.36.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))':
'@sveltejs/adapter-node@5.3.1(@sveltejs/kit@2.36.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))':
dependencies:
'@rollup/plugin-commonjs': 28.0.2(rollup@4.34.8)
'@rollup/plugin-json': 6.1.0(rollup@4.34.8)
'@rollup/plugin-node-resolve': 16.0.0(rollup@4.34.8)
'@sveltejs/kit': 2.36.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0))
'@sveltejs/kit': 2.36.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0))
rollup: 4.34.8
'@sveltejs/enhanced-img@0.5.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))(rollup@4.34.8)(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0))':
@ -3830,7 +3860,7 @@ snapshots:
transitivePeerDependencies:
- rollup
'@sveltejs/kit@2.36.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0))':
'@sveltejs/kit@2.36.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.2)(vite@6.3.5(yaml@2.7.0))':
dependencies:
'@standard-schema/spec': 1.0.0
'@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0)
@ -4049,10 +4079,10 @@ snapshots:
dependencies:
open: 8.4.2
bits-ui@2.9.2(@internationalized/date@3.8.2)(svelte@5.38.2):
bits-ui@2.9.4(@internationalized/date@3.8.2)(svelte@5.38.2):
dependencies:
'@floating-ui/core': 1.7.3
'@floating-ui/dom': 1.7.3
'@floating-ui/dom': 1.7.4
'@internationalized/date': 3.8.2
esm-env: 1.2.2
runed: 0.29.2(svelte@5.38.2)
@ -4216,7 +4246,7 @@ snapshots:
css.escape@1.5.1: {}
cssdb@8.3.1: {}
cssdb@8.4.0: {}
cssesc@3.0.0: {}
@ -4396,10 +4426,6 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
fdir@6.4.4(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
fdir@6.4.6(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
@ -4564,7 +4590,7 @@ snapshots:
dependencies:
tslib: 2.8.1
lucide-svelte@0.539.0(svelte@5.38.2):
lucide-svelte@0.541.0(svelte@5.38.2):
dependencies:
svelte: 5.38.2
@ -4682,12 +4708,12 @@ snapshots:
postcss: 8.5.6
postcss-value-parser: 4.2.0
postcss-color-functional-notation@7.0.10(postcss@8.5.6):
postcss-color-functional-notation@7.0.11(postcss@8.5.6):
dependencies:
'@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
@ -4733,9 +4759,9 @@ snapshots:
postcss: 8.5.6
postcss-selector-parser: 7.1.0
postcss-double-position-gradients@6.0.2(postcss@8.5.6):
postcss-double-position-gradients@6.0.3(postcss@8.5.6):
dependencies:
'@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
postcss-value-parser: 4.2.0
@ -4771,12 +4797,12 @@ snapshots:
read-cache: 1.0.0
resolve: 1.22.10
postcss-lab-function@7.0.10(postcss@8.5.6):
postcss-lab-function@7.0.11(postcss@8.5.6):
dependencies:
'@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
@ -4817,22 +4843,24 @@ snapshots:
postcss: 8.5.6
postcss-value-parser: 4.2.0
postcss-preset-env@10.2.4(postcss@8.5.6):
postcss-preset-env@10.3.0(postcss@8.5.6):
dependencies:
'@csstools/postcss-alpha-function': 1.0.0(postcss@8.5.6)
'@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.6)
'@csstools/postcss-color-function': 4.0.10(postcss@8.5.6)
'@csstools/postcss-color-mix-function': 3.0.10(postcss@8.5.6)
'@csstools/postcss-color-mix-variadic-function-arguments': 1.0.0(postcss@8.5.6)
'@csstools/postcss-content-alt-text': 2.0.6(postcss@8.5.6)
'@csstools/postcss-color-function': 4.0.11(postcss@8.5.6)
'@csstools/postcss-color-function-display-p3-linear': 1.0.0(postcss@8.5.6)
'@csstools/postcss-color-mix-function': 3.0.11(postcss@8.5.6)
'@csstools/postcss-color-mix-variadic-function-arguments': 1.0.1(postcss@8.5.6)
'@csstools/postcss-content-alt-text': 2.0.7(postcss@8.5.6)
'@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.6)
'@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.6)
'@csstools/postcss-gamut-mapping': 2.0.10(postcss@8.5.6)
'@csstools/postcss-gradients-interpolation-method': 5.0.10(postcss@8.5.6)
'@csstools/postcss-hwb-function': 4.0.10(postcss@8.5.6)
'@csstools/postcss-ic-unit': 4.0.2(postcss@8.5.6)
'@csstools/postcss-gamut-mapping': 2.0.11(postcss@8.5.6)
'@csstools/postcss-gradients-interpolation-method': 5.0.11(postcss@8.5.6)
'@csstools/postcss-hwb-function': 4.0.11(postcss@8.5.6)
'@csstools/postcss-ic-unit': 4.0.3(postcss@8.5.6)
'@csstools/postcss-initial': 2.0.1(postcss@8.5.6)
'@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.6)
'@csstools/postcss-light-dark-function': 2.0.9(postcss@8.5.6)
'@csstools/postcss-light-dark-function': 2.0.10(postcss@8.5.6)
'@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.6)
'@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.6)
'@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.6)
@ -4842,14 +4870,14 @@ snapshots:
'@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.6)
'@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.6)
'@csstools/postcss-normalize-display-values': 4.0.0(postcss@8.5.6)
'@csstools/postcss-oklab-function': 4.0.10(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6)
'@csstools/postcss-oklab-function': 4.0.11(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.0(postcss@8.5.6)
'@csstools/postcss-random-function': 2.0.1(postcss@8.5.6)
'@csstools/postcss-relative-color-syntax': 3.0.10(postcss@8.5.6)
'@csstools/postcss-relative-color-syntax': 3.0.11(postcss@8.5.6)
'@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.6)
'@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.6)
'@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.6)
'@csstools/postcss-text-decoration-shorthand': 4.0.2(postcss@8.5.6)
'@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.6)
'@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.6)
'@csstools/postcss-unset-value': 4.0.0(postcss@8.5.6)
autoprefixer: 10.4.21(postcss@8.5.6)
@ -4857,24 +4885,24 @@ snapshots:
css-blank-pseudo: 7.0.1(postcss@8.5.6)
css-has-pseudo: 7.0.2(postcss@8.5.6)
css-prefers-color-scheme: 10.0.0(postcss@8.5.6)
cssdb: 8.3.1
cssdb: 8.4.0
postcss: 8.5.6
postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.6)
postcss-clamp: 4.1.0(postcss@8.5.6)
postcss-color-functional-notation: 7.0.10(postcss@8.5.6)
postcss-color-functional-notation: 7.0.11(postcss@8.5.6)
postcss-color-hex-alpha: 10.0.0(postcss@8.5.6)
postcss-color-rebeccapurple: 10.0.0(postcss@8.5.6)
postcss-custom-media: 11.0.6(postcss@8.5.6)
postcss-custom-properties: 14.0.6(postcss@8.5.6)
postcss-custom-selectors: 8.0.5(postcss@8.5.6)
postcss-dir-pseudo-class: 9.0.1(postcss@8.5.6)
postcss-double-position-gradients: 6.0.2(postcss@8.5.6)
postcss-double-position-gradients: 6.0.3(postcss@8.5.6)
postcss-focus-visible: 10.0.1(postcss@8.5.6)
postcss-focus-within: 9.0.1(postcss@8.5.6)
postcss-font-variant: 5.0.0(postcss@8.5.6)
postcss-gap-properties: 6.0.0(postcss@8.5.6)
postcss-image-set-function: 7.0.0(postcss@8.5.6)
postcss-lab-function: 7.0.10(postcss@8.5.6)
postcss-lab-function: 7.0.11(postcss@8.5.6)
postcss-logical: 8.1.0(postcss@8.5.6)
postcss-nesting: 13.0.2(postcss@8.5.6)
postcss-opacity-percentage: 3.0.0(postcss@8.5.6)
@ -5198,11 +5226,11 @@ snapshots:
svelte: 5.38.2
zimmerframe: 1.1.2
svelte-check@4.3.1(picomatch@4.0.3)(svelte@5.38.2)(typescript@5.9.2):
svelte-check@4.3.1(picomatch@4.0.2)(svelte@5.38.2)(typescript@5.9.2):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
chokidar: 4.0.3
fdir: 6.4.4(picomatch@4.0.3)
fdir: 6.4.4(picomatch@4.0.2)
picocolors: 1.1.1
sade: 1.8.1
svelte: 5.38.2

View file

@ -1 +0,0 @@
/* Write your global styles here, in PostCSS syntax */

View file

@ -1,34 +1,28 @@
<script lang="ts">
import { ExternalLink } from "lucide-svelte";
import type {
ExternalLinkType,
LinkIconType,
} from "$lib/types/externalLinkTypes";
import { ExternalLink } from 'lucide-svelte';
import type { ExternalLinkType, LinkIconType } from '$lib/types/externalLinkTypes';
const { iconData, linkData, textData }: ExternalLinkType = $props();
const { iconData, linkData, textData }: ExternalLinkType = $props();
let textLocationClass = "";
if (textData?.location === "top") {
textLocationClass = "text-top";
} else if (textData?.location === "bottom") {
textLocationClass = "text-bottom";
} else if (textData?.location === "left") {
textLocationClass = "text-left";
} else if (textData?.location === "right") {
textLocationClass = "text-right";
} else {
textLocationClass = "text-left";
}
let textLocationClass = '';
if (textData?.location === 'top') {
textLocationClass = 'text-top';
} else if (textData?.location === 'bottom') {
textLocationClass = 'text-bottom';
} else if (textData?.location === 'left') {
textLocationClass = 'text-left';
} else if (textData?.location === 'right') {
textLocationClass = 'text-right';
} else {
textLocationClass = 'text-left';
}
const linkDecoration =
linkData?.textDecoration && linkData?.textDecoration === "none"
? `text-decoration-${linkData?.textDecoration}`
: "text-decoration-underline";
const linkClass =
`${linkData?.clazz || ""} ${textLocationClass} ${linkDecoration}`.trim();
const linkDecoration =
linkData?.textDecoration && linkData?.textDecoration === 'none' ? `text-decoration-${linkData?.textDecoration}` : 'text-decoration-underline';
const linkClass = `${linkData?.clazz || ''} ${textLocationClass} ${linkDecoration}`.trim();
// Default icon config to satisfy typings when no iconData is provided
const defaultIconData: LinkIconType = { type: "icon", icon: ExternalLink };
// Default icon config to satisfy typings when no iconData is provided
const defaultIconData: LinkIconType = { type: 'icon', icon: ExternalLink };
</script>
{#snippet externalLink({ iconData, linkData, textData }: ExternalLinkType)}

View file

@ -1,12 +1,12 @@
<script lang="ts">
import { page } from "$app/state";
import ContactHub from "$lib/components/ContactHub.svelte";
import { page } from '$app/state';
import ContactHub from '$lib/components/ContactHub.svelte';
const userNames = {
github: "BradNut",
linkedIn: "bradley-shellnut",
email: "website[at]bradleyshellnut.com",
};
const userNames = {
github: 'BradNut',
linkedIn: 'bradley-shellnut',
email: 'website[at]bradleyshellnut.com',
};
</script>
<footer>

231
tests/about.test.ts Normal file
View file

@ -0,0 +1,231 @@
import { expect, test } from '@playwright/test';
test.describe('About page', () => {
test('has expected main heading', async ({ page }) => {
await page.goto('/about');
await expect(page.getByRole('heading', { level: 1, name: 'About' })).toBeVisible();
});
test('header/footer links hover: color becomes shellYellow', async ({ page }) => {
await page.goto('/about');
const shellYellow = await page.evaluate(() => {
const probe = document.createElement('div');
probe.style.color = 'var(--shellYellow)';
document.body.appendChild(probe);
const color = getComputedStyle(probe).color;
probe.remove();
return color;
});
const areas = [
'header[aria-label="header navigation"]',
'footer nav[aria-label="footer navigation"]',
];
for (const area of areas) {
const nav = page.locator(area);
await expect(nav).toBeVisible();
const link = nav.getByRole('link', { name: 'Portfolio', exact: true });
await expect(link).toBeVisible();
const before = await link.evaluate((el) => {
const cs = getComputedStyle(el as Element) as CSSStyleDeclaration;
return { color: cs.color };
});
await link.hover();
const after = await link.evaluate((el) => {
const cs = getComputedStyle(el as Element) as CSSStyleDeclaration;
return { color: cs.color };
});
expect(after.color).toBe(shellYellow);
// Sanity: it should change from the default color
expect(after.color).not.toBe(before.color);
}
});
test('current page (About) link is active in header and footer', async ({ page }) => {
await page.goto('/about');
const areas = [
'header[aria-label="header navigation"]',
'footer nav[aria-label="footer navigation"]',
];
for (const area of areas) {
const nav = page.locator(area);
const aboutLink = nav.getByRole('link', { name: 'About', exact: true });
await expect(aboutLink).toBeVisible();
const isActive = await aboutLink.evaluate((el) => (el as Element).classList.contains('active'));
expect(isActive).toBeTruthy();
}
});
test('tech list hover changes color to shellYellow', async ({ page }) => {
await page.goto('/about');
const techList = page.locator('.tech-list');
await expect(techList).toBeVisible();
// Resolve the actual computed rgb color value for --shellYellow in the browser context
const shellYellow = await page.evaluate(() => {
const probe = document.createElement('div');
probe.style.color = 'var(--shellYellow)';
document.body.appendChild(probe);
const color = getComputedStyle(probe).color;
probe.remove();
return color;
});
const names = ['Svelte', 'Hono', 'TypeScript', 'Drizzle ORM', 'React', 'Next.js', 'Docker'];
for (const name of names) {
const link = techList.locator(`a[title="${name}"]`).first();
await expect(link).toBeVisible();
const before = await link.evaluate((el) => getComputedStyle(el as Element).color);
await link.hover();
const after = await link.evaluate((el) => getComputedStyle(el as Element).color);
expect(before).not.toBe(shellYellow);
expect(after).toBe(shellYellow);
}
});
test('tech list has accessible links for key technologies', async ({ page }) => {
await page.goto('/about');
const techList = page.locator('.tech-list');
await expect(techList).toBeVisible();
const names = ['Svelte', 'Hono', 'TypeScript', 'Drizzle ORM', 'React', 'Next.js', 'Docker'];
for (const name of names) {
const link = techList.locator(`a[title="${name}"]`).first();
await expect(link).toBeVisible();
await expect(link).toHaveAccessibleName(new RegExp(name, 'i'));
}
});
test('tablet viewport (~800px): extracurricular wraps to multiple rows', async ({ page }) => {
await page.setViewportSize({ width: 800, height: 1000 });
await page.goto('/about');
const container = page.locator('.extracurricular');
await expect(container).toBeVisible();
const cards = container.locator('.card');
const count = await cards.count();
expect(count).toBeGreaterThanOrEqual(3);
const [c0, c1, c2] = await Promise.all([
cards.nth(0).boundingBox(),
cards.nth(1).boundingBox(),
cards.nth(2).boundingBox(),
]);
expect(c0 && c1 && c2).toBeTruthy();
if (c0 && c1 && c2) {
// first two side-by-side on same row, third wrapped below
expect(Math.abs(c0.y - c1.y)).toBeLessThan(10);
expect(c2.y).toBeGreaterThan(c0.y + 10);
}
});
test('mobile viewport (375px): extracurricular cards stack vertically', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 900 });
await page.goto('/about');
const container = page.locator('.extracurricular');
const cards = container.locator('.card');
const count = await cards.count();
expect(count).toBeGreaterThanOrEqual(2);
const [a, b] = await Promise.all([
cards.nth(0).boundingBox(),
cards.nth(1).boundingBox(),
]);
expect(a && b).toBeTruthy();
if (a && b) {
expect(b.y).toBeGreaterThan(a.y + 10);
expect(Math.abs(b.x - a.x)).toBeLessThan(40);
}
});
// Mirror header link presence from home tests
test('header navigation shows expected links', async ({ page }) => {
await page.goto('/about');
const headerNav = page.locator('header[aria-label="header navigation"]');
await expect(headerNav).toBeVisible();
await expect(headerNav.getByRole('link', { name: 'Home', exact: true })).toBeVisible();
await expect(headerNav.getByRole('link', { name: 'About', exact: true })).toBeVisible();
await expect(headerNav.getByRole('link', { name: 'Portfolio', exact: true })).toBeVisible();
await expect(headerNav.getByRole('link', { name: 'Uses', exact: true })).toBeVisible();
});
// Mirror header navigation flow from home tests (starting on /about)
test('header navigation links go to correct routes (from /about)', async ({ page }) => {
await page.goto('/about');
const headerNav = page.locator('header[aria-label="header navigation"]');
await headerNav.getByRole('link', { name: 'Portfolio', exact: true }).click();
await expect(page).toHaveURL(/\/portfolio\/?$/);
await headerNav.getByRole('link', { name: 'Uses', exact: true }).click();
await expect(page).toHaveURL(/\/uses\/?$/);
await headerNav.getByRole('link', { name: 'Home', exact: true }).click();
await expect(page).toHaveURL(/\/?$/);
await headerNav.getByRole('link', { name: 'About', exact: true }).click();
await expect(page).toHaveURL(/\/about\/?$/);
});
// Mirror footer link presence from home tests
test('footer shows expected links', async ({ page }) => {
await page.goto('/about');
const footerNav = page.getByRole('navigation', { name: 'footer navigation' });
await expect(footerNav).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Home', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'About', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Portfolio', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Uses', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Privacy', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Favorite Articles', exact: true })).toBeVisible();
});
// Mirror footer navigation flow from home tests (starting on /about)
test('footer navigation links go to correct routes (from /about)', async ({ page }) => {
await page.goto('/about');
const footerNav = page.getByRole('navigation', { name: 'footer navigation' });
await footerNav.getByRole('link', { name: 'Privacy', exact: true }).click();
await expect(page).toHaveURL(/\/privacy\/?$/);
// Favorite Articles may route to /articles or /articles/1
await footerNav.getByRole('link', { name: 'Favorite Articles', exact: true }).click();
await expect(page).toHaveURL(/\/articles(\/\d+)?\/?$/);
await footerNav.getByRole('link', { name: 'About', exact: true }).click();
await expect(page).toHaveURL(/\/about\/?$/);
await footerNav.getByRole('link', { name: 'Home', exact: true }).click();
await expect(page).toHaveURL(/\/?$/);
});
// Mobile viewport: ensure cat section has no horizontal overflow and second image fits viewport
test('mobile: cat section no horizontal overflow; second cat image fully visible', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 800 });
await page.goto('/about');
const catSection = page.locator('.cat-pics');
await catSection.scrollIntoViewIfNeeded();
// The cat section itself should not horizontally overflow its own box
const sectionOverflowX = await catSection.evaluate((el) => el.scrollWidth - el.clientWidth);
expect(sectionOverflowX).toBeLessThanOrEqual(2);
// Second image inside .cat-pics is fully within the cat section horizontally
const img = page.locator('.cat-pics figure:nth-of-type(2) img');
await expect(img).toBeVisible();
const [imgBox, sectionBox] = await Promise.all([
img.boundingBox(),
catSection.boundingBox(),
]);
expect(imgBox && sectionBox).toBeTruthy();
if (imgBox && sectionBox) {
expect(imgBox.x).toBeGreaterThanOrEqual(sectionBox.x - 1);
expect(imgBox.x + imgBox.width).toBeLessThanOrEqual(sectionBox.x + sectionBox.width + 1);
}
});
});

View file

@ -1,6 +1,225 @@
import { expect, test } from '@playwright/test';
test('index page has expected h1', async ({ page }) => {
test.describe('Home page', () => {
test('has expected main heading', async ({ page }) => {
await page.goto('/');
expect(await page.textContent('h1')).toBe("Hello! I'm Bradley Shellnut.");
await expect(page.locator('h1')).toHaveText("Hello! I'm Bradley Shellnut.");
});
test('header/footer links hover: color becomes shellYellow', async ({ page }) => {
await page.goto('/');
const shellYellow = await page.evaluate(() => {
const probe = document.createElement('div');
probe.style.color = 'var(--shellYellow)';
document.body.appendChild(probe);
const color = getComputedStyle(probe).color;
probe.remove();
return color;
});
const areas = [
'header[aria-label="header navigation"]',
'footer nav[aria-label="footer navigation"]',
];
for (const area of areas) {
const nav = page.locator(area);
await expect(nav).toBeVisible();
const link = nav.getByRole('link', { name: 'Portfolio', exact: true });
await expect(link).toBeVisible();
const before = await link.evaluate((el) => {
const cs = getComputedStyle(el as Element) as CSSStyleDeclaration;
return { color: cs.color };
});
await link.hover();
const after = await link.evaluate((el) => {
const cs = getComputedStyle(el as Element) as CSSStyleDeclaration;
return { color: cs.color };
});
expect(after.color).toBe(shellYellow);
expect(after.color).not.toBe(before.color);
}
});
test('current page (Home) link is active in header and footer', async ({ page }) => {
await page.goto('/');
const areas = [
'header[aria-label="header navigation"]',
'footer nav[aria-label="footer navigation"]',
];
for (const area of areas) {
const nav = page.locator(area);
const link = nav.getByRole('link', { name: 'Home', exact: true });
await expect(link).toBeVisible();
const isActive = await link.evaluate((el) => (el as Element).classList.contains('active'));
expect(isActive).toBeTruthy();
}
});
test('header navigation links go to correct routes', async ({ page }) => {
await page.goto('/');
const headerNav = page.locator('header[aria-label="header navigation"]');
// About
await headerNav.getByRole('link', { name: 'About', exact: true }).click();
await expect(page).toHaveURL(/\/about\/?$/);
// Portfolio
await headerNav.getByRole('link', { name: 'Portfolio', exact: true }).click();
await expect(page).toHaveURL(/\/portfolio\/?$/);
// Uses
await headerNav.getByRole('link', { name: 'Uses', exact: true }).click();
await expect(page).toHaveURL(/\/uses\/?$/);
// Home
await headerNav.getByRole('link', { name: 'Home', exact: true }).click();
await expect(page).toHaveURL(/\/?$/);
});
test('header navigation shows expected links', async ({ page }) => {
await page.goto('/');
const headerNavContainer = page.locator('header[aria-label="header navigation"]');
await expect(headerNavContainer).toBeVisible();
await expect(headerNavContainer.getByRole('link', { name: 'Home', exact: true })).toBeVisible();
await expect(headerNavContainer.getByRole('link', { name: 'About', exact: true })).toBeVisible();
await expect(headerNavContainer.getByRole('link', { name: 'Portfolio', exact: true })).toBeVisible();
await expect(headerNavContainer.getByRole('link', { name: 'Uses', exact: true })).toBeVisible();
});
test('shows key sections', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { level: 2, name: 'Currently listening to:' })).toBeVisible();
await expect(page.getByRole('heading', { level: 2, name: 'Favorite Articles' })).toBeVisible();
});
test('renders Bandcamp albums (max 6)', async ({ page }) => {
await page.goto('/');
const albumImages = page.locator('.albumsStyles .album-artwork');
const count = await albumImages.count();
expect(count).toBeGreaterThan(0);
expect(count).toBeLessThanOrEqual(6);
});
test('renders at least one favorite article card', async ({ page }) => {
await page.goto('/');
const cards = page.locator('section.articles article.card');
await expect(cards.first()).toBeVisible();
});
test('"more articles" link points to /articles and navigates', async ({ page }) => {
await page.goto('/');
const more = page.locator('a.moreArticles');
await expect(more).toHaveAttribute('href', '/articles');
await expect(more).toContainText('more articles');
await more.click();
await expect(page).toHaveURL(/\/articles(\/\d+)?\/?$/);
});
test('has social/contact links', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('link', { name: 'Contact through LinkedIn', exact: true })).toBeVisible();
await expect(page.getByRole('link', { name: 'Contact through Github', exact: true })).toBeVisible();
});
test('footer shows expected links', async ({ page }) => {
await page.goto('/');
const footerNav = page.getByRole('navigation', { name: 'footer navigation' });
await expect(footerNav).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Home', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'About', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Portfolio', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Uses', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Privacy', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Favorite Articles', exact: true })).toBeVisible();
});
test('small viewport: Bandcamp grid 2x3 above Articles', async ({ page }) => {
await page.setViewportSize({ width: 800, height: 1000 }); // <1000px and >575px
await page.goto('/');
const albumsGrid = page.locator('.albumsStyles');
const articlesSection = page.locator('section.articles');
await expect(albumsGrid).toBeVisible();
await expect(articlesSection).toBeVisible();
// Order: Bandcamp above Articles
const [albumsTop, articlesTop] = await Promise.all([
albumsGrid.boundingBox().then((b) => b?.y ?? Number.POSITIVE_INFINITY),
articlesSection.boundingBox().then((b) => b?.y ?? Number.NEGATIVE_INFINITY),
]);
expect(albumsTop).toBeLessThan(articlesTop);
// Layout: assert first two items share the same row, third wraps to next row
const albumItems = page.locator('.albumsStyles .album-artwork');
const n = await albumItems.count();
expect(n).toBeGreaterThanOrEqual(3);
const [b0, b1, b2] = await Promise.all([
albumItems.nth(0).boundingBox(),
albumItems.nth(1).boundingBox(),
albumItems.nth(2).boundingBox(),
]);
expect(b0 && b1 && b2).toBeTruthy();
if (b0 && b1 && b2) {
expect(Math.abs(b0.y - b1.y)).toBeLessThan(6); // same row
expect(b2.y).toBeGreaterThan(b0.y + 10); // next row
}
});
test('mobile viewport: Bandcamp vertical scroll, Articles stacked', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 800 }); // <=575px rules apply
await page.goto('/');
const albumsGrid = page.locator('.albumsStyles');
const articlesSection = page.locator('section.articles');
await expect(albumsGrid).toBeVisible();
await expect(articlesSection).toBeVisible();
// Order: Bandcamp above Articles
const [albumsTop, articlesTop] = await Promise.all([
albumsGrid.boundingBox().then((b) => b?.y ?? Number.POSITIVE_INFINITY),
articlesSection.boundingBox().then((b) => b?.y ?? Number.NEGATIVE_INFINITY),
]);
expect(albumsTop).toBeLessThan(articlesTop);
// Layout: single column and scrollable vertically
const scrollInfo = await albumsGrid.evaluate((el) => ({
overflowY: getComputedStyle(el as HTMLElement).overflowY,
scrollHeight: (el as HTMLElement).scrollHeight,
clientHeight: (el as HTMLElement).clientHeight,
}));
expect(scrollInfo.clientHeight).toBeLessThan(scrollInfo.scrollHeight);
expect(['auto', 'scroll']).toContain(scrollInfo.overflowY);
// Albums are a vertical list (y increasing); first two must be on different rows
const albumItems = page.locator('.albumsStyles .album-artwork');
const m = await albumItems.count();
expect(m).toBeGreaterThanOrEqual(2);
const [a0, a1] = await Promise.all([
albumItems.nth(0).boundingBox(),
albumItems.nth(1).boundingBox(),
]);
expect(a0 && a1).toBeTruthy();
if (a0 && a1) {
expect(a1.y).toBeGreaterThan(a0.y + 10);
expect(Math.abs(a1.x - a0.x)).toBeLessThan(6);
}
// Articles are a vertical list (same x, increasing y)
const boxes = await page.locator('section.articles article.card').evaluateAll((els) =>
(els as HTMLElement[]).slice(0, Math.min(4, els.length)).map((el) => el.getBoundingClientRect())
);
expect(boxes.length).toBeGreaterThan(0);
const x0 = boxes[0].left;
for (let i = 1; i < boxes.length; i++) {
expect(Math.abs(boxes[i].left - x0)).toBeLessThan(6);
expect(boxes[i].top).toBeGreaterThan(boxes[i - 1].top);
}
});
});