From 51eff5634667de2c9a8f42ac6804b15dd7fa00dd Mon Sep 17 00:00:00 2001 From: Shubham Oulkar Date: Tue, 21 Apr 2026 11:54:45 +0530 Subject: [PATCH 01/14] Implement E2E testing suite for redesign --- .github/workflows/playwright.yml | 84 ++++++++++++++++++++++++++++++++ .gitignore | 2 + package-lock.json | 64 ++++++++++++++++++++++++ package.json | 4 +- playwright.config.ts | 34 +++++++++++++ test-results/.last-run.json | 4 ++ tests/e2e/homepage.spec.ts | 68 ++++++++++++++++++++++++++ 7 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 playwright.config.ts create mode 100644 test-results/.last-run.json create mode 100644 tests/e2e/homepage.spec.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000000..59db7ec42d --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,84 @@ +name: Playwright Tests + +on: + pull_request: + branches: + - redesign + - gh-pages + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + playwright: + if: github.actor != 'dependabot[bot]' + name: Playwright Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24.13' + cache: 'npm' + + - name: Wait for Netlify preview + id: wait-for-preview + run: | + # Calculate the Netlify preview URL based on the PR number + PREVIEW_URL="https://deploy-preview-${{ github.event.pull_request.number }}--expressjscom-preview.netlify.app" + echo "PREVIEW_URL=$PREVIEW_URL" >> "$GITHUB_ENV" + + MAX_RETRIES=30 + DELAY=20 + echo "Checking Netlify preview: $PREVIEW_URL" + + for i in $(seq 1 $MAX_RETRIES); do + # Check if the URL returns a 200 OK + if curl -s -I "$PREVIEW_URL" | grep -q "HTTP/.* 200"; then + echo "✅ Preview is live at $PREVIEW_URL" + exit 0 + fi + echo "⏳ Waiting for Netlify to deploy... ($i/$MAX_RETRIES)" + sleep $DELAY + done + + echo "❌ Preview not live after $((MAX_RETRIES*DELAY)) seconds." + exit 1 + + - name: Install dependencies + run: npm ci + + - name: Get Playwright version + id: playwright-version + run: echo "version=$(npx playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + + - name: Install Playwright Browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright install --with-deps + + - name: Run Playwright tests + run: npm run test:e2e + env: + PLAYWRIGHT_BASE_URL: ${{ env.PREVIEW_URL }} + + - name: Upload Playwright test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 016b59ea14..0ad86b98b3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ pnpm-debug.log* # jetbrains setting folder .idea/ + +playwright-report/ diff --git a/package-lock.json b/package-lock.json index e1495feae9..d5c7b48b19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@csstools/postcss-global-data": "^4.0.0", "@eslint/js": "^9.39.2", "@iconify-json/fluent": "^1.2.41", + "@playwright/test": "^1.59.1", "@types/node": "^25.3.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -2250,6 +2251,22 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -9455,6 +9472,53 @@ "pathe": "^2.0.3" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index 3134af99d1..8fad85df05 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "check": "npm run format:check && npm run lint", "fix": "npm run format && npm run lint:fix", "prepare": "husky", - "lint-staged": "lint-staged" + "lint-staged": "lint-staged", + "test:e2e": "playwright test" }, "dependencies": { "@astrojs/mdx": "^5.0.3", @@ -62,6 +63,7 @@ "@csstools/postcss-global-data": "^4.0.0", "@eslint/js": "^9.39.2", "@iconify-json/fluent": "^1.2.41", + "@playwright/test": "^1.59.1", "@types/node": "^25.3.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000000..82db311633 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test'; + +const isCI = !!process.env.CI; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: isCI, + retries: isCI ? 2 : 0, + workers: isCI ? 1 : undefined, + reporter: isCI ? [['html'], ['github']] : [['html']], + + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:4321', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], +}); diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000000..544c11fbc3 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} diff --git a/tests/e2e/homepage.spec.ts b/tests/e2e/homepage.spec.ts new file mode 100644 index 0000000000..6402c56b6a --- /dev/null +++ b/tests/e2e/homepage.spec.ts @@ -0,0 +1,68 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Homepage Hero Section', () => { + test.beforeEach(async ({ page }) => { + // Navigate to the English homepage before each test + await page.goto('/en'); + }); + + test('should display the core brand elements', async ({ page }) => { + // 1. Verify the Tagline (H1) exists + const tagline = page.getByRole('heading', { level: 1 }); + await expect(tagline).toBeVisible(); + + // 2. Verify the Standard Logo is visible and the Kawaii logo is hidden + const standardLogo = page.locator('.hero__logo-default').first(); + const kawaiiLogo = page.locator('.hero__logo-kawaii'); + + await expect(standardLogo).toBeVisible(); + await expect(kawaiiLogo).toBeHidden(); // Hidden by default as we discovered! + + // 3. Verify Version text is present + // It's in BodyMd next to the logo + const versionText = page.locator('.hero__logo + p, .hero__logo + div'); + await expect(versionText).toContainText(/\d+\.\d+\.\d+/); + }); + + test('should display the installation command', async ({ page }) => { + // Verify the first code block contains "npm install" + const installCode = page.locator('.hero__code').first(); + await expect(installCode).toBeVisible(); + await expect(installCode).toContainText(/npm install/i); + }); + + test('should have a working "Get Started" call to action', async ({ page }) => { + const getStartedBtn = page.getByRole('link', { name: /Get Started/i }); + + // Verify it points to the correct 5.x installation page + await expect(getStartedBtn).toHaveAttribute('href', /\/en\/5x\/starter\/installing/); + + // Test navigation + await getStartedBtn.click(); + await expect(page).toHaveURL(/\/en\/5x\/starter\/installing/); + }); + + test('should display the example code block', async ({ page }) => { + // Verify the secondary code block (example code) is visible on larger screens + const exampleCode = page.locator('.hero__example-code'); + await expect(exampleCode).toBeVisible(); + }); + + test('should toggle the Kawaii logo when the data attribute is set', async ({ page }) => { + const standardLogo = page.locator('.hero__logo-default').first(); + const kawaiiLogo = page.locator('.hero__logo-kawaii'); + + // Initially standard is visible, kawaii is hidden + await expect(standardLogo).toBeVisible(); + await expect(kawaiiLogo).toBeHidden(); + + // Trigger Kawaii mode via script + await page.evaluate(() => { + document.documentElement.setAttribute('data-kawaii', 'true'); + }); + + // Now kawaii should be visible, standard should be hidden + await expect(kawaiiLogo).toBeVisible(); + await expect(standardLogo).toBeHidden(); + }); +}); From 252aeb8720527707b76a1dbf3701a201b7f034fa Mon Sep 17 00:00:00 2001 From: Shubham Oulkar Date: Tue, 21 Apr 2026 12:08:11 +0530 Subject: [PATCH 02/14] fix: add linux-x64 dependencies for sharp --- .github/workflows/playwright.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 59db7ec42d..b529c39dc5 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -35,11 +35,11 @@ jobs: # Calculate the Netlify preview URL based on the PR number PREVIEW_URL="https://deploy-preview-${{ github.event.pull_request.number }}--expressjscom-preview.netlify.app" echo "PREVIEW_URL=$PREVIEW_URL" >> "$GITHUB_ENV" - + MAX_RETRIES=30 DELAY=20 echo "Checking Netlify preview: $PREVIEW_URL" - + for i in $(seq 1 $MAX_RETRIES); do # Check if the URL returns a 200 OK if curl -s -I "$PREVIEW_URL" | grep -q "HTTP/.* 200"; then From 7b66bb009809d4cfde03536dac8b47152278aa1c Mon Sep 17 00:00:00 2001 From: Shubham Oulkar Date: Tue, 21 Apr 2026 12:27:08 +0530 Subject: [PATCH 03/14] add test for latest version from NPM --- .github/workflows/playwright.yml | 2 +- tests/e2e/homepage.spec.ts | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index b529c39dc5..03f0a4fe94 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -36,7 +36,7 @@ jobs: PREVIEW_URL="https://deploy-preview-${{ github.event.pull_request.number }}--expressjscom-preview.netlify.app" echo "PREVIEW_URL=$PREVIEW_URL" >> "$GITHUB_ENV" - MAX_RETRIES=30 + MAX_RETRIES=10 DELAY=20 echo "Checking Netlify preview: $PREVIEW_URL" diff --git a/tests/e2e/homepage.spec.ts b/tests/e2e/homepage.spec.ts index 6402c56b6a..2418019ae6 100644 --- a/tests/e2e/homepage.spec.ts +++ b/tests/e2e/homepage.spec.ts @@ -1,6 +1,14 @@ import { test, expect } from '@playwright/test'; test.describe('Homepage Hero Section', () => { + 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 }) => { // Navigate to the English homepage before each test await page.goto('/en'); @@ -18,10 +26,9 @@ test.describe('Homepage Hero Section', () => { await expect(standardLogo).toBeVisible(); await expect(kawaiiLogo).toBeHidden(); // Hidden by default as we discovered! - // 3. Verify Version text is present - // It's in BodyMd next to the logo - const versionText = page.locator('.hero__logo + p, .hero__logo + div'); - await expect(versionText).toContainText(/\d+\.\d+\.\d+/); + // 3. Verify the EXACT latest version from NPM is rendered + const versionText = page.locator('.hero').getByText(latestVersion); + await expect(versionText).toBeVisible(); }); test('should display the installation command', async ({ page }) => { From 233bb0190505baf0f0764e70a727b689c0af37ba Mon Sep 17 00:00:00 2001 From: Shubham Oulkar Date: Tue, 21 Apr 2026 12:37:15 +0530 Subject: [PATCH 04/14] ci: improve deps caching --- .github/workflows/playwright.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 03f0a4fe94..78e32d2a3c 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -65,6 +65,8 @@ jobs: with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + restore-keys: | + playwright-${{ runner.os }}- - name: Install Playwright Browsers if: steps.playwright-cache.outputs.cache-hit != 'true' From df3624ee73f690f4710d68882055f2e8d53ada80 Mon Sep 17 00:00:00 2001 From: Shubham Oulkar Date: Tue, 21 Apr 2026 17:23:25 +0530 Subject: [PATCH 05/14] add test for home page --- .gitignore | 1 + .../patterns/Features/Features.astro | 2 +- src/components/patterns/Hero/Hero.astro | 23 ++++--- src/components/primitives/Card/Card.astro | 2 +- src/i18n/ui/en.json | 1 + test-results/.last-run.json | 4 -- tests/e2e/homepage.spec.ts | 61 +++++++++++-------- 7 files changed, 57 insertions(+), 37 deletions(-) delete mode 100644 test-results/.last-run.json diff --git a/.gitignore b/.gitignore index 0ad86b98b3..c21e0d8d7a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ pnpm-debug.log* .idea/ playwright-report/ +test-results/ \ No newline at end of file diff --git a/src/components/patterns/Features/Features.astro b/src/components/patterns/Features/Features.astro index c63ec6a192..f63cac2a4c 100644 --- a/src/components/patterns/Features/Features.astro +++ b/src/components/patterns/Features/Features.astro @@ -26,7 +26,7 @@ interface Props extends HTMLAttributes<'section'> { const { title, class: className, ...rest } = Astro.props; --- -
+
diff --git a/src/components/patterns/Hero/Hero.astro b/src/components/patterns/Hero/Hero.astro index cd7e30a3d0..0a2374a68f 100644 --- a/src/components/patterns/Hero/Hero.astro +++ b/src/components/patterns/Hero/Hero.astro @@ -33,18 +33,27 @@ app.listen(port, () => {
- + {EXPRESS_VERSION} @@ -53,8 +62,8 @@ app.listen(port, () => {

{t('hero.tagline')}

-
- +
+
-
+
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/en.json b/src/i18n/ui/en.json index e7694734b5..8fe0b48a12 100644 --- a/src/i18n/ui/en.json +++ b/src/i18n/ui/en.json @@ -188,6 +188,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/test-results/.last-run.json b/test-results/.last-run.json deleted file mode 100644 index 544c11fbc3..0000000000 --- a/test-results/.last-run.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": "failed", - "failedTests": [] -} diff --git a/tests/e2e/homepage.spec.ts b/tests/e2e/homepage.spec.ts index 2418019ae6..cac65d04f9 100644 --- a/tests/e2e/homepage.spec.ts +++ b/tests/e2e/homepage.spec.ts @@ -1,6 +1,7 @@ import { test, expect } from '@playwright/test'; +import enStrings from '../../src/i18n/ui/en.json'; -test.describe('Homepage Hero Section', () => { +test.describe('Homepage Content', () => { let latestVersion: string; test.beforeAll(async () => { @@ -10,65 +11,77 @@ test.describe('Homepage Hero Section', () => { }); test.beforeEach(async ({ page }) => { - // Navigate to the English homepage before each test await page.goto('/en'); }); - test('should display the core brand elements', async ({ page }) => { - // 1. Verify the Tagline (H1) exists + test('should display the core brand elements and features', async ({ page }) => { + // 1. Verify Tagline const tagline = page.getByRole('heading', { level: 1 }); await expect(tagline).toBeVisible(); + await expect(tagline).toHaveText(enStrings.hero.tagline); - // 2. Verify the Standard Logo is visible and the Kawaii logo is hidden - const standardLogo = page.locator('.hero__logo-default').first(); - const kawaiiLogo = page.locator('.hero__logo-kawaii'); + // 2. Verify Logos + const standardLogo = page.getByTestId('logo-standard').first(); + const kawaiiLogo = page.getByAltText(enStrings.hero.kawaiiLogoAlt); await expect(standardLogo).toBeVisible(); - await expect(kawaiiLogo).toBeHidden(); // Hidden by default as we discovered! + await expect(kawaiiLogo).toBeHidden(); - // 3. Verify the EXACT latest version from NPM is rendered - const versionText = page.locator('.hero').getByText(latestVersion); + // 3. Verify Version + const versionText = page.getByTestId('hero-brand').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({ hasText: feature.title }); + await expect(card).toBeVisible(); + await expect(card.locator('p')).toHaveText(feature.body); + } }); test('should display the installation command', async ({ page }) => { - // Verify the first code block contains "npm install" - const installCode = page.locator('.hero__code').first(); + const installCode = page.getByTestId('install-command'); await expect(installCode).toBeVisible(); - await expect(installCode).toContainText(/npm install/i); + await expect(installCode).toContainText(enStrings.hero.tagline); // Verify it's in the hero area + 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: /Get Started/i }); - - // Verify it points to the correct 5.x installation page + const getStartedBtn = page.getByRole('link', { name: enStrings.hero.getStarted }); await expect(getStartedBtn).toHaveAttribute('href', /\/en\/5x\/starter\/installing/); - // Test navigation await getStartedBtn.click(); await expect(page).toHaveURL(/\/en\/5x\/starter\/installing/); }); test('should display the example code block', async ({ page }) => { - // Verify the secondary code block (example code) is visible on larger screens - const exampleCode = page.locator('.hero__example-code'); + const exampleCode = page.getByTestId('example-code'); await expect(exampleCode).toBeVisible(); }); test('should toggle the Kawaii logo when the data attribute is set', async ({ page }) => { - const standardLogo = page.locator('.hero__logo-default').first(); - const kawaiiLogo = page.locator('.hero__logo-kawaii'); + const standardLogo = page.getByTestId('logo-standard').first(); + const kawaiiLogo = page.getByAltText(enStrings.hero.kawaiiLogoAlt); - // Initially standard is visible, kawaii is hidden await expect(standardLogo).toBeVisible(); await expect(kawaiiLogo).toBeHidden(); - // Trigger Kawaii mode via script await page.evaluate(() => { document.documentElement.setAttribute('data-kawaii', 'true'); }); - // Now kawaii should be visible, standard should be hidden await expect(kawaiiLogo).toBeVisible(); await expect(standardLogo).toBeHidden(); }); From f815d85ab0cd8783b5f59183b6c0c983d98300a3 Mon Sep 17 00:00:00 2001 From: Shubham Oulkar Date: Tue, 21 Apr 2026 17:24:19 +0530 Subject: [PATCH 06/14] add docs for writing tests --- CONTRIBUTING.md | 66 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 423fc37ca9..66ccf2f188 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,25 +71,65 @@ Tooling required: ### Available Scripts -| Command | Description | -| ----------------- | ----------------------------------------- | -| `npm run dev` | Start development server with hot reload | -| `npm run build` | Build production site to `./dist` | -| `npm run preview` | Preview production build locally | -| `npm run lint` | Run ESLint to check for issues | -| `npm run check` | Run type checking and format verification | +| Command | Description | +| ------------------ | ----------------------------------------- | +| `npm run dev` | Start development server with hot reload | +| `npm run build` | Build production site to `./dist` | +| `npm run preview` | Preview production build locally | +| `npm run lint` | Run ESLint to check for issues | +| `npm run check` | Run type checking and format verification | +| `npm run test:e2e` | Run Playwright E2E tests | ## Submitting a Pull Request -1. Create a new branch from `main` -2. Make your changes -3. Run `npm run check` to verify code style and types -4. Commit with a clear message -5. Push to your fork -6. Open a PR against `main` +1. Create a new branch from `main`. +2. Make your changes. +3. Run `npm run check` to verify code style and types. +4. Run `npm run test:e2e` to ensure your changes don't break existing functionality. +5. Commit with a clear message. +6. Push to your fork. +7. Open a PR against `main`. > Ensure all checks pass and your branch is up to date with `main` before opening a PR. +## Testing + +We use **Playwright** for End-to-End (E2E) testing. All PRs are automatically tested against a Netlify Preview deployment before they can be merged. + +### Prerequisites + +Before running E2E tests for the first time, you need to install the browser binaries: + +```bash +npx playwright install --with-deps +``` + +### Running Tests Locally + +You can run the full test suite against your local development server: + +1. In one terminal, start the site: `npm run dev` +2. In another terminal, run the tests: `npm run test:e2e` + +### Writing Stable Tests + +When adding new tests or modifying components, please follow these stability guidelines: + +1. **Avoid CSS Classes**: Do not use CSS classes (e.g., `.hero__content`) for locators, as they are fragile and change during refactoring. +2. **Use data-testid**: Add `data-testid` attributes to components for stable targeting (e.g., `
`). +3. **User-Visible Locators**: Prefer semantic locators like `getByRole`, `getByText`, or `getByAltText` over IDs when possible. + +Example: + +```typescript +// Good: Stable and accessible +const logo = page.getByAltText('Express.js logo'); +const section = page.getByTestId('features-section'); + +// Bad: Fragile +const logo = page.locator('.hero__logo'); +``` + ## Further Documentation For more detailed documentation about the project, see the [`docs/`](docs/) folder: From 3604643cf716d886b98badfa4072288b6f43ffac Mon Sep 17 00:00:00 2001 From: Shubham Oulkar Date: Tue, 21 Apr 2026 17:31:40 +0530 Subject: [PATCH 07/14] fix runtime json file type --- tests/e2e/homepage.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/homepage.spec.ts b/tests/e2e/homepage.spec.ts index cac65d04f9..8bc7d42cfd 100644 --- a/tests/e2e/homepage.spec.ts +++ b/tests/e2e/homepage.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import enStrings from '../../src/i18n/ui/en.json'; +import enStrings from '../../src/i18n/ui/en.json' with { type: 'json' }; test.describe('Homepage Content', () => { let latestVersion: string; From d74ac96a13b3a7807c9eaf138126b04ecb964252 Mon Sep 17 00:00:00 2001 From: Shubham Oulkar Date: Tue, 21 Apr 2026 17:49:40 +0530 Subject: [PATCH 08/14] test footer --- src/components/patterns/Footer/Footer.astro | 3 +- tests/e2e/homepage.spec.ts | 81 ++++++++++++++++++--- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/components/patterns/Footer/Footer.astro b/src/components/patterns/Footer/Footer.astro index 2d25630dfa..10ca098a4e 100644 --- a/src/components/patterns/Footer/Footer.astro +++ b/src/components/patterns/Footer/Footer.astro @@ -9,7 +9,7 @@ import { Icon } from 'astro-icon/components'; import { BodyMd, Button, Col, Container, Grid } from '@/components/primitives'; --- -
+
- diff --git a/tests/e2e/homepage.spec.ts b/tests/e2e/homepage.spec.ts index 31d016861d..439cd23695 100644 --- a/tests/e2e/homepage.spec.ts +++ b/tests/e2e/homepage.spec.ts @@ -81,45 +81,72 @@ test.describe('Homepage Content', () => { await expect(exampleCode).toBeVisible(); }); - test('should toggle the Kawaii logo when clicking the footer button', async ({ page }) => { + test('should toggle Kawaii mode and persist it', async ({ page }) => { + const root = page.locator('html'); + const toggleBtn = page.locator('#kawaiiToggle'); const heroBrand = page.getByTestId('hero-brand'); + const standardLogo = heroBrand.getByTestId('logo-standard').first(); const kawaiiLogo = heroBrand.getByAltText(enStrings.hero.kawaiiLogoAlt); - // Initially standard is visible + // Ensure page is ready + await expect(toggleBtn).toBeVisible(); + await expect(toggleBtn).toBeEnabled(); + + // Initial state + await expect(root).not.toHaveAttribute('data-kawaii'); await expect(standardLogo).toBeVisible(); - // Click the toggle in the footer - const toggleBtn = page.getByTestId('kawaii-toggle'); - await toggleBtn.scrollIntoViewIfNeeded(); + await expect(kawaiiLogo).toBeHidden(); + + // Click toggle await toggleBtn.click({ force: true }); - // 1. Verify the attribute is actually set on HTML (confirms script ran) - await expect(page.locator('html')).toHaveAttribute('data-kawaii', ''); + // Wait until DOM actually updates + await expect + .poll(async () => { + return await page.evaluate(() => document.documentElement.hasAttribute('data-kawaii')); + }) + .toBe(true); + + // Assert accessibility state + await expect(toggleBtn).toHaveAttribute('aria-pressed', 'true'); - // 2. Now kawaii should be visible + // Assert UI change await expect(kawaiiLogo).toBeVisible(); + await expect(standardLogo).toBeHidden(); - // 3. Verify persistence in localStorage - const savedKawaii = await page.evaluate(() => localStorage.getItem('kawaii')); - expect(savedKawaii).toBe('true'); + // Assert persistence + await expect + .poll(async () => { + return await page.evaluate(() => localStorage.getItem('kawaii')); + }) + .toBe('true'); - // 4. All standard logos in the hero should be hidden - const allStandardLogos = heroBrand.getByTestId('logo-standard'); - const count = await allStandardLogos.count(); - for (let i = 0; i < count; i++) { - await expect(allStandardLogos.nth(i)).toBeHidden(); - } + // Reload → should persist + await page.reload(); + + await expect(root).toHaveAttribute('data-kawaii', ''); + await expect(kawaiiLogo).toBeVisible(); - // Click again to revert + // Toggle OFF await toggleBtn.click({ force: true }); - await expect(page.locator('html')).not.toHaveAttribute('data-kawaii'); - // Verify localStorage was cleared - const revertedKawaii = await page.evaluate(() => localStorage.getItem('kawaii')); - expect(revertedKawaii).toBeNull(); + await expect + .poll(async () => { + return await page.evaluate(() => document.documentElement.hasAttribute('data-kawaii')); + }) + .toBe(false); + await expect(root).not.toHaveAttribute('data-kawaii'); + await expect(toggleBtn).toHaveAttribute('aria-pressed', 'false'); await expect(standardLogo).toBeVisible(); - await expect(kawaiiLogo).toBeHidden(); + + // Storage cleared + await expect + .poll(async () => { + return await page.evaluate(() => localStorage.getItem('kawaii')); + }) + .toBe(null); }); test('should toggle between light and dark themes', async ({ page }) => { From 9b000ede16484b5bcf4f55f3f56153f1781aa8c9 Mon Sep 17 00:00:00 2001 From: Shubham Oulkar Date: Tue, 21 Apr 2026 19:02:12 +0530 Subject: [PATCH 12/14] test --- src/components/patterns/Footer/Footer.astro | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/patterns/Footer/Footer.astro b/src/components/patterns/Footer/Footer.astro index 5a9029b186..e871216a5f 100644 --- a/src/components/patterns/Footer/Footer.astro +++ b/src/components/patterns/Footer/Footer.astro @@ -163,7 +163,7 @@ import { BodyMd, Button, Col, Container, Grid } from '@/components/primitives';
From 8c4233965d4ecdec4ec2f6bf6b36d00b5d0f3f55 Mon Sep 17 00:00:00 2001 From: Shubham Oulkar Date: Tue, 21 Apr 2026 19:39:58 +0530 Subject: [PATCH 13/14] add localization test --- src/components/patterns/Footer/Footer.astro | 6 +- .../LanguageSelect/LanguageSelect.astro | 7 +- .../patterns/LanguageSelect/types.ts | 7 +- src/i18n/ui/de.json | 3 + src/i18n/ui/en.json | 3 + src/i18n/ui/es.json | 3 + src/i18n/ui/fr.json | 3 + src/i18n/ui/it.json | 3 + src/i18n/ui/ja.json | 3 + src/i18n/ui/ko.json | 3 + src/i18n/ui/pt-br.json | 3 + src/i18n/ui/zh-cn.json | 3 + src/i18n/ui/zh-tw.json | 5 +- tests/e2e/homepage.spec.ts | 116 ++++++++---------- 14 files changed, 89 insertions(+), 79 deletions(-) diff --git a/src/components/patterns/Footer/Footer.astro b/src/components/patterns/Footer/Footer.astro index e871216a5f..62b8fa30c1 100644 --- a/src/components/patterns/Footer/Footer.astro +++ b/src/components/patterns/Footer/Footer.astro @@ -187,9 +187,5 @@ import { BodyMd, Button, Col, Container, Grid } from '@/components/primitives'; }); }; - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initKawaii); - } else { - initKawaii(); - } + initKawaii(); 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/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 8fe0b48a12..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", 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..1cbd6b505e 100644 --- a/src/i18n/ui/zh-tw.json +++ b/src/i18n/ui/zh-tw.json @@ -18,7 +18,10 @@ "switchToDark": "切换到深色模式" }, "version": { - "selectLabel": "选择 API 版本" + "selectLabel": "選擇 API 版本" + }, + "language": { + "selectLabel": "選擇語言" }, "nav": { "mainMenu": "主菜单", diff --git a/tests/e2e/homepage.spec.ts b/tests/e2e/homepage.spec.ts index 439cd23695..013bd044ba 100644 --- a/tests/e2e/homepage.spec.ts +++ b/tests/e2e/homepage.spec.ts @@ -1,5 +1,14 @@ 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' }; /** * Homepage E2E Tests @@ -21,6 +30,7 @@ test.describe('Homepage Content', () => { await page.goto('/en'); }); + // testing hero component test('should display the core brand elements and features', async ({ page }) => { const heroBrand = page.getByTestId('hero-brand'); @@ -81,74 +91,7 @@ test.describe('Homepage Content', () => { await expect(exampleCode).toBeVisible(); }); - test('should toggle Kawaii mode and persist it', async ({ page }) => { - const root = page.locator('html'); - const toggleBtn = page.locator('#kawaiiToggle'); - const heroBrand = page.getByTestId('hero-brand'); - - const standardLogo = heroBrand.getByTestId('logo-standard').first(); - const kawaiiLogo = heroBrand.getByAltText(enStrings.hero.kawaiiLogoAlt); - - // Ensure page is ready - await expect(toggleBtn).toBeVisible(); - await expect(toggleBtn).toBeEnabled(); - - // Initial state - await expect(root).not.toHaveAttribute('data-kawaii'); - await expect(standardLogo).toBeVisible(); - await expect(kawaiiLogo).toBeHidden(); - - // Click toggle - await toggleBtn.click({ force: true }); - - // Wait until DOM actually updates - await expect - .poll(async () => { - return await page.evaluate(() => document.documentElement.hasAttribute('data-kawaii')); - }) - .toBe(true); - - // Assert accessibility state - await expect(toggleBtn).toHaveAttribute('aria-pressed', 'true'); - - // Assert UI change - await expect(kawaiiLogo).toBeVisible(); - await expect(standardLogo).toBeHidden(); - - // Assert persistence - await expect - .poll(async () => { - return await page.evaluate(() => localStorage.getItem('kawaii')); - }) - .toBe('true'); - - // Reload → should persist - await page.reload(); - - await expect(root).toHaveAttribute('data-kawaii', ''); - await expect(kawaiiLogo).toBeVisible(); - - // Toggle OFF - await toggleBtn.click({ force: true }); - - await expect - .poll(async () => { - return await page.evaluate(() => document.documentElement.hasAttribute('data-kawaii')); - }) - .toBe(false); - - await expect(root).not.toHaveAttribute('data-kawaii'); - await expect(toggleBtn).toHaveAttribute('aria-pressed', 'false'); - await expect(standardLogo).toBeVisible(); - - // Storage cleared - await expect - .poll(async () => { - return await page.evaluate(() => localStorage.getItem('kawaii')); - }) - .toBe(null); - }); - + // 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'); @@ -166,6 +109,43 @@ test.describe('Homepage Content', () => { await expect(root).toHaveAttribute('data-theme', initialTheme); }); + // Testing languages selector + const languagesToTest = [ + { code: 'es', label: 'Español', tagline: esStrings.hero.tagline }, + { code: 'fr', label: 'Français', tagline: frStrings.hero.tagline }, + { code: 'de', label: 'Deutsch', tagline: deStrings.hero.tagline }, + { code: 'it', label: 'Italiano', tagline: itStrings.hero.tagline }, + { code: 'ja', label: '日本語', tagline: jaStrings.hero.tagline }, + { code: 'ko', label: '한국어', tagline: koStrings.hero.tagline }, + { code: 'pt-br', label: 'Português', tagline: ptBrStrings.hero.tagline }, + { code: 'zh-cn', label: '简体中文', tagline: zhCnStrings.hero.tagline }, + { code: 'zh-tw', label: '繁體中文', tagline: zhTwStrings.hero.tagline }, + ]; + + 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); + }); + } + + // testing footer component test('should display copyright and foundation links', async ({ page }) => { const footer = page.getByTestId('footer'); await expect(footer).toBeVisible(); From 2ad5008a00e88908934290338e36d556564c358f Mon Sep 17 00:00:00 2001 From: Shubham Oulkar Date: Wed, 22 Apr 2026 07:05:59 +0530 Subject: [PATCH 14/14] test multi-locale theme label verification --- src/i18n/ui/zh-tw.json | 6 +-- tests/e2e/homepage.spec.ts | 76 ++++++++++++++++++++++++++++++++------ 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/i18n/ui/zh-tw.json b/src/i18n/ui/zh-tw.json index 1cbd6b505e..4d6d065cb3 100644 --- a/src/i18n/ui/zh-tw.json +++ b/src/i18n/ui/zh-tw.json @@ -13,9 +13,9 @@ "ariaLabel": "开始输入以搜索" }, "theme": { - "toggle": "切换主题", - "switchToLight": "切换到浅色模式", - "switchToDark": "切换到深色模式" + "toggle": "切換主題", + "switchToLight": "切換到淺色模式", + "switchToDark": "切換到深色模式" }, "version": { "selectLabel": "選擇 API 版本" diff --git a/tests/e2e/homepage.spec.ts b/tests/e2e/homepage.spec.ts index 013bd044ba..0b31eed779 100644 --- a/tests/e2e/homepage.spec.ts +++ b/tests/e2e/homepage.spec.ts @@ -10,6 +10,64 @@ 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. @@ -96,6 +154,8 @@ test.describe('Homepage Content', () => { 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'; @@ -110,18 +170,6 @@ test.describe('Homepage Content', () => { }); // Testing languages selector - const languagesToTest = [ - { code: 'es', label: 'Español', tagline: esStrings.hero.tagline }, - { code: 'fr', label: 'Français', tagline: frStrings.hero.tagline }, - { code: 'de', label: 'Deutsch', tagline: deStrings.hero.tagline }, - { code: 'it', label: 'Italiano', tagline: itStrings.hero.tagline }, - { code: 'ja', label: '日本語', tagline: jaStrings.hero.tagline }, - { code: 'ko', label: '한국어', tagline: koStrings.hero.tagline }, - { code: 'pt-br', label: 'Português', tagline: ptBrStrings.hero.tagline }, - { code: 'zh-cn', label: '简体中文', tagline: zhCnStrings.hero.tagline }, - { code: 'zh-tw', label: '繁體中文', tagline: zhTwStrings.hero.tagline }, - ]; - for (const lang of languagesToTest) { test(`should switch to ${lang.label} successfully`, async ({ page }) => { // 1. Find language switcher button (using translated aria-label) @@ -142,6 +190,10 @@ test.describe('Homepage Content', () => { // 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); }); }