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
3 changes: 2 additions & 1 deletion .copier-answers.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Changes here will be overwritten by Copier
_commit: 7522e24
_commit: 195919b
_src_path: https://github.com/python-project-templates/base.git
add_extension: site-sveltekit
brand_name: Example
Expand All @@ -8,3 +8,4 @@ project_description: A fast, accessible SvelteKit website.
project_name: javascript-template-site-sveltekit
site_adapter: static
site_url: https://example.com

3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ pnpm test
pnpm build
```

Visual regression snapshots live beside `tests/visual/` and are skipped when `CI` is set. Record or
refresh them locally with `pnpm test:e2e:update`.

This site uses SvelteKit's `adapter-static` deployment adapter.
12 changes: 8 additions & 4 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: 'tests',
testMatch: ['**/*.spec.ts'],
testIgnore: process.env.CI ? ['**/visual/**'] : [],
snapshotPathTemplate: '{testDir}/{testFilePath}-snapshots/{arg}-{projectName}{ext}',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? 'github' : 'list',
expect: {
toHaveScreenshot: { maxDiffPixelRatio: 0.01, threshold: 0.2, animations: 'disabled', caret: 'hide' }
},
use: {
baseURL: 'http://127.0.0.1:4177',
trace: 'retain-on-failure',
Expand All @@ -18,9 +24,7 @@ export default defineConfig({
timeout: 120_000
},
projects: [
{
name: 'desktop',
use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 800 } }
}
{ name: 'desktop', use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 800 } } },
{ name: 'mobile', use: { ...devices['Desktop Chrome'], viewport: { width: 390, height: 844 } } }
]
});
28 changes: 25 additions & 3 deletions src/app.css
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
@import 'tailwindcss';

:root {
color: #171717;
background: #ffffff;
font-family: system-ui, sans-serif;
--paper: #ffffff;
--ink: #171717;
--muted: #5c5c5c;
--line: #e4e4e4;

color-scheme: light;
}

:root[data-theme='dark'] {
--paper: #0b0b0b;
--ink: #f2f2f2;
--muted: #a1a1a1;
--line: #262626;

color-scheme: dark;
}

@theme inline {
--color-paper: var(--paper);
--color-ink: var(--ink);
--color-muted: var(--muted);
--color-line: var(--line);
}

body {
margin: 0;
color: var(--ink);
background: var(--paper);
font-family: system-ui, sans-serif;
}

a {
Expand Down
20 changes: 20 additions & 0 deletions src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#ffffff" />
<script>
try {
const savedTheme = localStorage.getItem('theme');
const theme =
savedTheme === 'light' || savedTheme === 'dark'
? savedTheme
: matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';

document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
document
.querySelector('meta[name="theme-color"]')
?.setAttribute('content', theme === 'dark' ? '#0b0b0b' : '#ffffff');
} catch {
// Use the light theme when browser storage is unavailable.
}
</script>
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
Expand Down
18 changes: 18 additions & 0 deletions src/routes/+error.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script lang="ts">
import { page } from '$app/state';
</script>

<svelte:head>
<title>{page.status} — Example</title>
</svelte:head>

<main class="mx-auto grid min-h-[70vh] max-w-5xl place-items-center px-6 py-24 text-center">
<div>
<p class="text-sm uppercase tracking-widest text-muted">Error / {page.status}</p>
<h1 class="mt-6 text-5xl font-semibold tracking-tight sm:text-7xl">This path went quiet.</h1>
<p class="mx-auto mt-6 max-w-md leading-relaxed text-muted">
{page.error?.message ?? 'The page could not be found.'}
</p>
<a class="mt-9 inline-block text-sm font-semibold underline" href="/">Return home →</a>
</div>
</main>
32 changes: 32 additions & 0 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
<script lang="ts">
import { onMount } from 'svelte';
import '../app.css';

let { children } = $props();

let theme = $state<'light' | 'dark'>('light');

onMount(() => {
theme = document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light';
});

const toggleTheme = () => {
theme = theme === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
document
.querySelector('meta[name="theme-color"]')
?.setAttribute('content', theme === 'dark' ? '#0b0b0b' : '#ffffff');

try {
localStorage.setItem('theme', theme);
} catch {
// Keep the in-page theme when browser storage is unavailable.
}
};
</script>

<svelte:head>
Expand All @@ -13,4 +35,14 @@
<meta property="og:url" content="https://example.com" />
</svelte:head>

<button
class="fixed right-5 top-5 z-50 rounded-full border border-line bg-paper px-4 py-2 text-xs font-semibold text-muted"
type="button"
data-testid="theme-toggle"
aria-label="Switch to {theme === 'dark' ? 'light' : 'dark'} theme"
onclick={toggleTheme}
>
{theme === 'dark' ? 'Light' : 'Dark'}
</button>

{@render children()}
1 change: 1 addition & 0 deletions src/routes/+layout.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const prerender = true;
export const trailingSlash = 'always';
18 changes: 18 additions & 0 deletions src/routes/sitemap.xml/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { RequestHandler } from './$types';

export const prerender = true;

const pages = [''];

export const GET: RequestHandler = () => {
const urls = pages.map((page) => `<url><loc>https://example.com/${page}</loc></url>`).join('');

return new Response(
`<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}</urlset>`,
{
headers: {
'Content-Type': 'application/xml'
}
}
);
};
13 changes: 13 additions & 0 deletions static/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 17 additions & 6 deletions tests/a11y/audit.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';

test('home page has no automatically detectable accessibility violations', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle('Example');
const routes = ['/'];

const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
for (const route of routes) {
for (const theme of ['light', 'dark'] as const) {
test(`${route} has no serious or critical accessibility violations in ${theme} mode`, async ({ page }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.addInitScript((value) => localStorage.setItem('theme', value), theme);
await page.goto(route);

const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze();
const violations = results.violations.filter((violation) =>
['serious', 'critical'].includes(violation.impact ?? '')
);

expect(violations).toEqual([]);
});
}
}
15 changes: 15 additions & 0 deletions tests/behavior/theme.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { expect, test } from '@playwright/test';

test('theme selection persists across navigation', async ({ page }) => {
await page.goto('/');
await page.evaluate(() => localStorage.removeItem('theme'));
await page.reload();

const initial = await page.locator('html').getAttribute('data-theme');
await page.getByTestId('theme-toggle').click();
const expected = initial === 'dark' ? 'light' : 'dark';

await expect(page.locator('html')).toHaveAttribute('data-theme', expected);
await page.reload();
await expect(page.locator('html')).toHaveAttribute('data-theme', expected);
});
13 changes: 13 additions & 0 deletions tests/visual/pages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expect, test } from '@playwright/test';

const routes = [{ name: 'home', path: '/' }];

for (const route of routes) {
for (const theme of ['light', 'dark'] as const) {
test(`${route.name} in ${theme} mode`, async ({ page }) => {
await page.addInitScript((value) => localStorage.setItem('theme', value), theme);
await page.goto(route.path);
await expect(page).toHaveScreenshot(`${route.name}-${theme}.png`, { fullPage: true });
});
}
}
Loading