diff --git a/src/components/patterns/LanguageSelect/LanguageSelect.astro b/src/components/patterns/LanguageSelect/LanguageSelect.astro
index 0e1a26ae5e..b767a8b205 100644
--- a/src/components/patterns/LanguageSelect/LanguageSelect.astro
+++ b/src/components/patterns/LanguageSelect/LanguageSelect.astro
@@ -17,10 +17,13 @@
import { Select } from '@/components/primitives';
import type { LanguageSelectProps } from './types';
+import { getLangFromUrl, useTranslations } from '@/i18n/utils';
type Props = LanguageSelectProps;
-const { languages, currentLanguage = 'en', class: className, id, ...rest } = Astro.props;
+const { languages, currentLanguage: propLanguage, class: className, id, ...rest } = Astro.props;
+const currentLanguage = propLanguage || getLangFromUrl(Astro.url);
+const t = useTranslations(currentLanguage);
const options = languages.map((lang) => ({
value: lang.code,
@@ -36,7 +39,7 @@ const options = languages.map((lang) => ({
hideLabel={true}
variant="icon"
dropdownAlign="right"
- ariaLabel="Select language"
+ ariaLabel={t('language.selectLabel')}
class={className}
data-language-select
{...id && { id }}
diff --git a/src/components/patterns/LanguageSelect/types.ts b/src/components/patterns/LanguageSelect/types.ts
index 089bf60785..272f960e88 100644
--- a/src/components/patterns/LanguageSelect/types.ts
+++ b/src/components/patterns/LanguageSelect/types.ts
@@ -3,14 +3,15 @@
*/
import type { HTMLAttributes } from 'astro/types';
+import type { LanguageCode } from '@/i18n/locales';
export interface LanguageConfig {
- code: string;
+ code: LanguageCode;
label: string;
}
export type LanguageSelectProps = HTMLAttributes<'div'> & {
languages: LanguageConfig[];
- currentLanguage?: string;
- onLanguageChange?: (code: string) => void;
+ currentLanguage?: LanguageCode;
+ onLanguageChange?: (code: LanguageCode) => void;
};
diff --git a/src/components/primitives/Card/Card.astro b/src/components/primitives/Card/Card.astro
index 29c2953c3d..8e5061c243 100644
--- a/src/components/primitives/Card/Card.astro
+++ b/src/components/primitives/Card/Card.astro
@@ -23,7 +23,7 @@ interface Props extends HTMLAttributes<'div'> {
const { title, body, class: className, ...rest } = Astro.props;
---
-
+
{title}
{body}
diff --git a/src/i18n/ui/de.json b/src/i18n/ui/de.json
index 9ab191778e..287ad4fce2 100644
--- a/src/i18n/ui/de.json
+++ b/src/i18n/ui/de.json
@@ -20,6 +20,9 @@
"version": {
"selectLabel": "API-Version auswählen"
},
+ "language": {
+ "selectLabel": "Sprache auswählen"
+ },
"nav": {
"mainMenu": "Hauptmenü",
"toggleMenu": "Menü umschalten",
diff --git a/src/i18n/ui/en.json b/src/i18n/ui/en.json
index e7694734b5..0347fe4ad1 100644
--- a/src/i18n/ui/en.json
+++ b/src/i18n/ui/en.json
@@ -20,6 +20,9 @@
"version": {
"selectLabel": "Select API version"
},
+ "language": {
+ "selectLabel": "Select language"
+ },
"nav": {
"mainMenu": "Main menu",
"toggleMenu": "Toggle menu",
@@ -188,6 +191,7 @@
"hero": {
"tagline": "Fast, unopinionated, minimalist web framework for Node.js",
"getStarted": "Get Started",
+ "kawaiiLogoAlt": "Express.js kawaii logo",
"videoPause": "Pause background video",
"videoPlay": "Play background video"
}
diff --git a/src/i18n/ui/es.json b/src/i18n/ui/es.json
index dc888a36b8..ace5893d13 100644
--- a/src/i18n/ui/es.json
+++ b/src/i18n/ui/es.json
@@ -20,6 +20,9 @@
"version": {
"selectLabel": "Seleccionar versión de API"
},
+ "language": {
+ "selectLabel": "Seleccionar idioma"
+ },
"nav": {
"mainMenu": "Menú principal",
"toggleMenu": "Alternar menú",
diff --git a/src/i18n/ui/fr.json b/src/i18n/ui/fr.json
index b751760997..280ab48600 100644
--- a/src/i18n/ui/fr.json
+++ b/src/i18n/ui/fr.json
@@ -20,6 +20,9 @@
"version": {
"selectLabel": "Sélectionner la version de l’API"
},
+ "language": {
+ "selectLabel": "Sélectionner la langue"
+ },
"nav": {
"mainMenu": "Menu principal",
"toggleMenu": "Afficher/masquer le menu",
diff --git a/src/i18n/ui/it.json b/src/i18n/ui/it.json
index 21538bcd38..a591f6a9ca 100644
--- a/src/i18n/ui/it.json
+++ b/src/i18n/ui/it.json
@@ -20,6 +20,9 @@
"version": {
"selectLabel": "Seleziona la versione API"
},
+ "language": {
+ "selectLabel": "Seleziona lingua"
+ },
"nav": {
"mainMenu": "Menu principale",
"toggleMenu": "Mostra/Nascondi menu",
diff --git a/src/i18n/ui/ja.json b/src/i18n/ui/ja.json
index 682acb6767..21fae5d187 100644
--- a/src/i18n/ui/ja.json
+++ b/src/i18n/ui/ja.json
@@ -20,6 +20,9 @@
"version": {
"selectLabel": "APIバージョンを選択"
},
+ "language": {
+ "selectLabel": "言語を選択"
+ },
"nav": {
"mainMenu": "メインメニュー",
"toggleMenu": "メニューを切替",
diff --git a/src/i18n/ui/ko.json b/src/i18n/ui/ko.json
index 51f5cbd03a..894c11837f 100644
--- a/src/i18n/ui/ko.json
+++ b/src/i18n/ui/ko.json
@@ -20,6 +20,9 @@
"version": {
"selectLabel": "API 버전 선택"
},
+ "language": {
+ "selectLabel": "언어 선택"
+ },
"nav": {
"mainMenu": "메인 메뉴",
"toggleMenu": "메뉴 전환",
diff --git a/src/i18n/ui/pt-br.json b/src/i18n/ui/pt-br.json
index c1842e12bd..1781abdb15 100644
--- a/src/i18n/ui/pt-br.json
+++ b/src/i18n/ui/pt-br.json
@@ -20,6 +20,9 @@
"version": {
"selectLabel": "Selecionar versão da API"
},
+ "language": {
+ "selectLabel": "Selecionar idioma"
+ },
"nav": {
"mainMenu": "Menu principal",
"toggleMenu": "Alternar menu",
diff --git a/src/i18n/ui/zh-cn.json b/src/i18n/ui/zh-cn.json
index ea0ed68a5d..98b2590d7b 100644
--- a/src/i18n/ui/zh-cn.json
+++ b/src/i18n/ui/zh-cn.json
@@ -20,6 +20,9 @@
"version": {
"selectLabel": "选择 API 版本"
},
+ "language": {
+ "selectLabel": "选择语言"
+ },
"nav": {
"mainMenu": "主菜单",
"toggleMenu": "切换菜单",
diff --git a/src/i18n/ui/zh-tw.json b/src/i18n/ui/zh-tw.json
index ea0ed68a5d..4d6d065cb3 100644
--- a/src/i18n/ui/zh-tw.json
+++ b/src/i18n/ui/zh-tw.json
@@ -13,12 +13,15 @@
"ariaLabel": "开始输入以搜索"
},
"theme": {
- "toggle": "切换主题",
- "switchToLight": "切换到浅色模式",
- "switchToDark": "切换到深色模式"
+ "toggle": "切換主題",
+ "switchToLight": "切換到淺色模式",
+ "switchToDark": "切換到深色模式"
},
"version": {
- "selectLabel": "选择 API 版本"
+ "selectLabel": "選擇 API 版本"
+ },
+ "language": {
+ "selectLabel": "選擇語言"
},
"nav": {
"mainMenu": "主菜单",
diff --git a/tests/e2e/homepage.spec.ts b/tests/e2e/homepage.spec.ts
new file mode 100644
index 0000000000..0b31eed779
--- /dev/null
+++ b/tests/e2e/homepage.spec.ts
@@ -0,0 +1,245 @@
+import { test, expect } from '@playwright/test';
+import enStrings from '../../src/i18n/ui/en.json' with { type: 'json' };
+import esStrings from '../../src/i18n/ui/es.json' with { type: 'json' };
+import frStrings from '../../src/i18n/ui/fr.json' with { type: 'json' };
+import deStrings from '../../src/i18n/ui/de.json' with { type: 'json' };
+import itStrings from '../../src/i18n/ui/it.json' with { type: 'json' };
+import jaStrings from '../../src/i18n/ui/ja.json' with { type: 'json' };
+import koStrings from '../../src/i18n/ui/ko.json' with { type: 'json' };
+import ptBrStrings from '../../src/i18n/ui/pt-br.json' with { type: 'json' };
+import zhCnStrings from '../../src/i18n/ui/zh-cn.json' with { type: 'json' };
+import zhTwStrings from '../../src/i18n/ui/zh-tw.json' with { type: 'json' };
+
+// Add property here that will be used to test translations
+const languagesToTest = [
+ {
+ code: 'es',
+ label: 'Español',
+ tagline: esStrings.hero.tagline,
+ themeLabel: esStrings.theme.toggle,
+ },
+ {
+ code: 'fr',
+ label: 'Français',
+ tagline: frStrings.hero.tagline,
+ themeLabel: frStrings.theme.toggle,
+ },
+ {
+ code: 'de',
+ label: 'Deutsch',
+ tagline: deStrings.hero.tagline,
+ themeLabel: deStrings.theme.toggle,
+ },
+ {
+ code: 'it',
+ label: 'Italiano',
+ tagline: itStrings.hero.tagline,
+ themeLabel: itStrings.theme.toggle,
+ },
+ {
+ code: 'ja',
+ label: '日本語',
+ tagline: jaStrings.hero.tagline,
+ themeLabel: jaStrings.theme.toggle,
+ },
+ {
+ code: 'ko',
+ label: '한국어',
+ tagline: koStrings.hero.tagline,
+ themeLabel: koStrings.theme.toggle,
+ },
+ {
+ code: 'pt-br',
+ label: 'Português',
+ tagline: ptBrStrings.hero.tagline,
+ themeLabel: ptBrStrings.theme.toggle,
+ },
+ {
+ code: 'zh-cn',
+ label: '简体中文',
+ tagline: zhCnStrings.hero.tagline,
+ themeLabel: zhCnStrings.theme.toggle,
+ },
+ {
+ code: 'zh-tw',
+ label: '繁體中文',
+ tagline: zhTwStrings.hero.tagline,
+ themeLabel: zhTwStrings.theme.toggle,
+ },
+];
+
+/**
+ * Homepage E2E Tests
+ * Includes: Hero, Features, Theme Toggle, Kawaii Toggle, Footer and Language Selector tests.
+ *
+ * TODO: Add tests for Sidebar and Orama Search component in the future.
+ */
+
+test.describe('Homepage Content', () => {
+ let latestVersion: string;
+
+ test.beforeAll(async () => {
+ const response = await fetch('https://registry.npmjs.org/express/latest');
+ const data = await response.json();
+ latestVersion = data.version;
+ });
+
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/en');
+ });
+
+ // testing hero component
+ test('should display the core brand elements and features', async ({ page }) => {
+ const heroBrand = page.getByTestId('hero-brand');
+
+ // 1. Verify Tagline
+ const tagline = page.getByRole('heading', { level: 1 });
+ await expect(tagline).toBeVisible();
+ await expect(tagline).toHaveText(enStrings.hero.tagline);
+
+ // 2. Verify Logos
+ const standardLogo = heroBrand.getByTestId('logo-standard').first();
+ const kawaiiLogo = heroBrand.getByAltText(enStrings.hero.kawaiiLogoAlt);
+
+ await expect(standardLogo).toBeVisible();
+ await expect(kawaiiLogo).toBeHidden();
+
+ // 3. Verify Version
+ const versionText = heroBrand.getByText(latestVersion);
+ await expect(versionText).toBeVisible();
+
+ // 4. Verify Features Section
+ const featureSection = page.getByTestId('features-section');
+ const featuresTitle = featureSection.getByRole('heading', { level: 2 });
+ await expect(featuresTitle).toHaveText(enStrings.features.title);
+
+ // 5. Verify all four feature cards
+ const features = [
+ enStrings.features.webapplication,
+ enStrings.features.api,
+ enStrings.features.performance,
+ enStrings.features.middleware,
+ ];
+
+ for (const feature of features) {
+ const card = featureSection.getByTestId('card').filter({
+ has: page.getByRole('heading', { name: feature.title, exact: true }),
+ });
+ await expect(card).toBeVisible();
+ await expect(card.locator('p')).toHaveText(feature.body);
+ }
+ });
+
+ test('should display the installation command', async ({ page }) => {
+ const installCode = page.getByTestId('install-command');
+ await expect(installCode).toBeVisible();
+ await expect(installCode).toContainText(/npm install express --save/i);
+ });
+
+ test('should have a working "Get Started" call to action', async ({ page }) => {
+ const getStartedBtn = page.getByRole('link', { name: enStrings.hero.getStarted });
+ await expect(getStartedBtn).toHaveAttribute('href', /\/en\/5x\/starter\/installing/);
+
+ await getStartedBtn.click();
+ await expect(page).toHaveURL(/\/en\/5x\/starter\/installing/);
+ });
+
+ test('should display the example code block', async ({ page }) => {
+ const exampleCode = page.getByTestId('example-code');
+ await expect(exampleCode).toBeVisible();
+ });
+
+ // testing theme toggle component
+ test('should toggle between light and dark themes', async ({ page }) => {
+ const root = page.locator('html');
+ const toggleBtn = page.locator('#theme-toggle-button');
+
+ await expect(toggleBtn).toHaveAttribute('aria-label', enStrings.theme.toggle);
+
+ const initialTheme = (await root.getAttribute('data-theme')) || 'light';
+ const targetTheme = initialTheme === 'light' ? 'dark' : 'light';
+
+ await toggleBtn.click();
+ await expect(root).toHaveAttribute('data-theme', targetTheme);
+
+ const savedTheme = await page.evaluate(() => localStorage.getItem('theme'));
+ expect(savedTheme).toBe(targetTheme);
+
+ await toggleBtn.click();
+ await expect(root).toHaveAttribute('data-theme', initialTheme);
+ });
+
+ // Testing languages selector
+ for (const lang of languagesToTest) {
+ test(`should switch to ${lang.label} successfully`, async ({ page }) => {
+ // 1. Find language switcher button (using translated aria-label)
+ const langSwitcher = page.getByRole('button', { name: enStrings.language.selectLabel });
+ await expect(langSwitcher).toBeVisible();
+
+ // 2. Click to open dropdown
+ await langSwitcher.click();
+
+ // 3. Select the language option
+ const option = page.getByRole('option', { name: lang.label });
+ await expect(option).toBeVisible();
+ await option.click();
+
+ // 4. Verify URL change (handling potential trailing slashes or other path parts)
+ await expect(page).toHaveURL(new RegExp(`/${lang.code}`));
+
+ // 5. Verify tagline is correctly translated
+ const tagline = page.getByRole('heading', { level: 1 });
+ await expect(tagline).toHaveText(lang.tagline);
+
+ // 6. Verify theme toggle aria-label is correctly translated
+ const themeToggle = page.locator('#theme-toggle-button');
+ await expect(themeToggle).toHaveAttribute('aria-label', lang.themeLabel);
+ });
+ }
+
+ // testing footer component
+ test('should display copyright and foundation links', async ({ page }) => {
+ const footer = page.getByTestId('footer');
+ await expect(footer).toBeVisible();
+
+ // Verify copyright text
+ await expect(footer).toContainText(/Copyright/);
+ await expect(footer).toContainText(/OpenJS Foundation/);
+
+ // Verify key legal and foundation links
+ const foundationLinks = [
+ { name: /The OpenJS Foundation/i, href: 'https://openjsf.org' },
+ { name: /Privacy Policy/i, href: /privacy-policy/ },
+ { name: /Code of Conduct/i, href: /code-of-conduct/ },
+ { name: /Security Policy/i, href: /security\/policy/ },
+ { name: /Trademark Policy/i, href: /trademark-policy/ },
+ { name: /Terms of Use/i, href: /terms-of-use/ },
+ ];
+
+ for (const link of foundationLinks) {
+ const locator = footer.getByRole('link', { name: link.name }).first();
+ await expect(locator).toBeVisible();
+ await expect(locator).toHaveAttribute('href', link.href);
+ }
+ });
+
+ test('should display social media links', async ({ page }) => {
+ const footer = page.getByTestId('footer');
+
+ const githubLink = footer.getByRole('link', { name: /GitHub/i });
+ const youtubeLink = footer.getByRole('link', { name: /Youtube/i });
+ const xLink = footer.getByRole('link', { name: /X account/i });
+ const slackLink = footer.getByRole('link', { name: /slack/i });
+ const openCollectiveLink = footer.getByRole('link', { name: /Open Collective/i });
+ const blueskyLink = footer.getByRole('link', { name: /bluesky/i });
+ const rssLink = footer.getByRole('link', { name: /RSS Feed/i });
+
+ await expect(githubLink).toBeVisible();
+ await expect(youtubeLink).toBeVisible();
+ await expect(xLink).toBeVisible();
+ await expect(slackLink).toBeVisible();
+ await expect(openCollectiveLink).toBeVisible();
+ await expect(blueskyLink).toBeVisible();
+ await expect(rssLink).toBeVisible();
+ });
+});