feat: add Header with language switcher and theme toggle
Also disables Vitest file parallelism since concurrent astro builds across test files raced on the shared dist/ output.
This commit is contained in:
@@ -468,6 +468,8 @@ describe('BaseLayout SEO tags and theming', () => {
|
||||
|
||||
Note: from this task on, tests call `npm run build` themselves via `beforeAll`, so `npm run test` alone is sufficient going forward. Astro inlines small stylesheets into a `<style>` tag during build rather than emitting an external `<link>` — the test accepts either form.
|
||||
|
||||
Once a second build-triggering test file exists (Task 4 onward), Vitest's default parallel file execution races multiple concurrent `astro build` runs against the same `dist/` directory. Add `vitest.config.ts` at the repo root with `test: { fileParallelism: false }` to force test files to run sequentially.
|
||||
|
||||
- [ ] **Step 2: Run the test and confirm it fails**
|
||||
|
||||
Run: `npm run test -- tests/seo-theme.test.ts`
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
import LanguageSwitcher from './LanguageSwitcher.astro';
|
||||
import ThemeToggle from './ThemeToggle.astro';
|
||||
import { getDictionary } from '../i18n';
|
||||
import type { Locale } from '../i18n/locale';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const t = getDictionary(locale);
|
||||
---
|
||||
<header class="site-header">
|
||||
<a class="brand" href={`/${locale}/`}>{t.nav.brand}</a>
|
||||
<div class="header-actions">
|
||||
<LanguageSwitcher locale={locale} />
|
||||
<ThemeToggle locale={locale} />
|
||||
</div>
|
||||
</header>
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
import { otherLocale, swapLocalePath } from '../i18n/locale';
|
||||
import type { Locale } from '../i18n/locale';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const target = otherLocale(locale);
|
||||
const href = swapLocalePath(Astro.url.pathname, locale, target);
|
||||
const label = target === 'en' ? 'English' : 'Français';
|
||||
---
|
||||
<a class="language-switcher" href={href} hreflang={target}>{label}</a>
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
import { getDictionary } from '../i18n';
|
||||
import type { Locale } from '../i18n/locale';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const t = getDictionary(locale);
|
||||
---
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
data-label-to-dark={t.theme.toggleToDark}
|
||||
data-label-to-light={t.theme.toggleToLight}
|
||||
aria-label={t.theme.toggleToDark}
|
||||
>
|
||||
🌙
|
||||
</button>
|
||||
<script is:inline>
|
||||
(function () {
|
||||
document.querySelectorAll('.theme-toggle').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var html = document.documentElement;
|
||||
var current = html.getAttribute('data-theme');
|
||||
var next = current === 'dark' ? 'light' : 'dark';
|
||||
html.setAttribute('data-theme', next);
|
||||
localStorage.setItem('ombrora-theme', next);
|
||||
btn.setAttribute(
|
||||
'aria-label',
|
||||
next === 'dark' ? btn.dataset.labelToDark : btn.dataset.labelToLight
|
||||
);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import Header from '../../components/Header.astro';
|
||||
|
||||
const locale = 'en';
|
||||
---
|
||||
<BaseLayout locale={locale}>
|
||||
<Header locale={locale} />
|
||||
<h1>Ombrora</h1>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import Header from '../../components/Header.astro';
|
||||
|
||||
const locale = 'fr';
|
||||
---
|
||||
<BaseLayout locale={locale}>
|
||||
<Header locale={locale} />
|
||||
<h1>Ombrora</h1>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
describe('Header, language switcher, theme toggle', () => {
|
||||
beforeAll(() => {
|
||||
execSync('npm run build', { stdio: 'inherit' });
|
||||
});
|
||||
|
||||
it('renders the brand link on both locales', () => {
|
||||
const en = readFileSync('dist/en/index.html', 'utf-8');
|
||||
const fr = readFileSync('dist/fr/index.html', 'utf-8');
|
||||
expect(en).toContain('class="brand" href="/en/"');
|
||||
expect(fr).toContain('class="brand" href="/fr/"');
|
||||
});
|
||||
|
||||
it('language switcher on /en/ links to /fr/ and vice versa', () => {
|
||||
const en = readFileSync('dist/en/index.html', 'utf-8');
|
||||
const fr = readFileSync('dist/fr/index.html', 'utf-8');
|
||||
expect(en).toContain('class="language-switcher" href="/fr/"');
|
||||
expect(fr).toContain('class="language-switcher" href="/en/"');
|
||||
});
|
||||
|
||||
it('theme toggle button carries localized aria-labels', () => {
|
||||
const en = readFileSync('dist/en/index.html', 'utf-8');
|
||||
const fr = readFileSync('dist/fr/index.html', 'utf-8');
|
||||
expect(en).toContain('aria-label="Switch to dark mode"');
|
||||
expect(fr).toContain('aria-label="Passer en mode sombre"');
|
||||
});
|
||||
|
||||
it('theme toggle click handler flips data-theme and persists to localStorage', () => {
|
||||
const en = readFileSync('dist/en/index.html', 'utf-8');
|
||||
expect(en).toContain("localStorage.setItem('ombrora-theme', next)");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
fileParallelism: false,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user