Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ dist/
.vscode/

*.local
test-results/
playwright-report/
22 changes: 15 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,25 @@ This document provides a detailed guide for agents interacting with this codebas

### Testing Commands

(Note: No testing framework is currently integrated. Update this section if added.)
This project uses **Vitest** for unit/logic testing and **Playwright** for E2E/smoke testing.

To simulate the test command:
- **Run all tests**:
```bash
pnpm test
```

```bash
pnpm test
```
- **Run unit tests only**:
```bash
pnpm test:unit
```

Currently, this outputs `Error: no test specified`.
- **Run E2E tests only**:
```bash
pnpm test:e2e
```

For individual test setups, the framework used (e.g. Jest, Vitest) will define the process.
Vitest tests should be located in `__tests__` directories relative to the code they test.
Playwright tests are located in the `e2e/` directory.

---

Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,27 @@ pnpm build
pnpm preview
```

## Testing

This project uses **Vitest** for unit/logic testing and **Playwright** for End-to-End (E2E) smoke testing.

### Run all tests
```bash
pnpm test
```

### Run unit tests only
Used for verifying translations, utility functions, and business logic.
```bash
pnpm test:unit
```

### Run E2E tests only
Used for checking links, SEO metadata, and user flows across all supported browsers.
```bash
pnpm test:e2e
```

## SEO & New Pages

To maintain good SEO and consistency as the project grows, follow these guidelines when adding new pages:
Expand Down
33 changes: 33 additions & 0 deletions e2e/accessibility.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'

const languages = ['es', 'en', 'ca']
const pages = [
'', // Home
'code-of-conduct',
'location',
'sponsors'
]

test.describe('Accessibility Audit', () => {
for (const lang of languages) {
for (const pagePath of pages) {
const url = `/${lang}/${pagePath}`

test(`page "${url}" should not have accessibility violations`, async ({ page }) => {
await page.goto(url)

// Wait for page to be ready
await page.waitForLoadState('networkidle')

// Run scan
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze()

// If it fails, Playwright report will detailed the issues
expect(accessibilityScanResults.violations).toEqual([])
})
}
}
})
56 changes: 56 additions & 0 deletions e2e/language-picker.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { test, expect } from '@playwright/test'

const languages = ['es', 'en', 'ca']

test.describe('Language Selection', () => {
test('Desktop: should allow switching languages', async ({ page, isMobile }) => {
test.skip(isMobile, 'This test is for desktop only')

await page.goto('/es/')

// Switch to English
const enLink = page.locator('.desktop-menu + div .lang-picker a').filter({
hasText: /^en$/i
})
await expect(enLink).toBeVisible()
await enLink.click()

// Match /en or /en/
await expect(page).toHaveURL(/\/en\/?$/)

// Switch to Catalan
const caLink = page.locator('.desktop-menu + div .lang-picker a').filter({
hasText: /^ca$/i
})
await expect(caLink).toBeVisible()
await caLink.click()
await expect(page).toHaveURL(/\/ca\/?$/)
})

test('Mobile: should allow switching languages via mobile menu', async ({ page, isMobile }) => {
test.skip(!isMobile, 'This test is for mobile only')

await page.goto('/es/')

// Open mobile menu
await page.locator('.responsive-toggle').click()

// Switch to English
const enLink = page.locator('.mobile-menu .lang-picker-mobile a').filter({
hasText: /^en$/i
})
await expect(enLink).toBeVisible()
await enLink.click()

await expect(page).toHaveURL(/\/en\/?$/)

// Re-open menu to switch to Catalan
await page.locator('.responsive-toggle').click()
const caLink = page.locator('.mobile-menu .lang-picker-mobile a').filter({
hasText: /^ca$/i
})
await expect(caLink).toBeVisible()
await caLink.click()
await expect(page).toHaveURL(/\/ca\/?$/)
})
})
36 changes: 36 additions & 0 deletions e2e/mobile-nav.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { test, expect } from '@playwright/test'

test.describe('Mobile Navigation', () => {
test.use({ viewport: { width: 375, height: 667 } }) // Mobile viewport

test('should toggle mobile menu when clicking the burger button', async ({ page }) => {
await page.goto('/es/')

const toggle = page.locator('.responsive-toggle')
const mobileMenu = page.locator('.mobile-menu')

// Initially hidden
await expect(mobileMenu).not.toHaveClass(/show/)

// Toggle ON
await toggle.click()
await expect(mobileMenu).toHaveClass(/show/)

// Toggle OFF
await toggle.click()
await expect(mobileMenu).not.toHaveClass(/show/)
})

test('should close mobile menu on Escape key', async ({ page }) => {
await page.goto('/es/')

const toggle = page.locator('.responsive-toggle')
const mobileMenu = page.locator('.mobile-menu')

await toggle.click()
await expect(mobileMenu).toHaveClass(/show/)

await page.keyboard.press('Escape')
await expect(mobileMenu).not.toHaveClass(/show/)
})
})
36 changes: 36 additions & 0 deletions e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { test, expect } from '@playwright/test'

const languages = ['es', 'en', 'ca']

for (const lang of languages) {
test.describe(`Language: ${lang}`, () => {
test('homepage should load and have SEO metadata', async ({ page }) => {
await page.goto(`/${lang}/`)

// Check Title
await expect(page).toHaveTitle(/PyConES 2026/i)

// Check for Meta Description (standard for SEO)
const description = await page.getAttribute('meta[name="description"]', 'content')
expect(description).toBeTruthy()
expect(description?.length).toBeGreaterThan(50)

// Check for viewport meta (responsive)
const viewport = await page.getAttribute('meta[name="viewport"]', 'content')
expect(viewport).toContain('width=device-width')
})

test('should not have broken internal links', async ({ page }) => {
await page.goto(`/${lang}/`)

const links = await page.getByRole('link').all()
for (const link of links) {
const href = await link.getAttribute('href')
if (href && href.startsWith('/') && !href.startsWith('//')) {
const response = await page.request.get(href)
expect(response.status()).toBe(200)
}
}
})
})
}
28 changes: 28 additions & 0 deletions e2e/visual.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { test, expect } from '@playwright/test'

test.describe('Visual Regression', () => {
test('should match homepage snapshot (Desktop)', async ({ page, isMobile }) => {
test.skip(isMobile, 'Only run on Desktop for visual consistency')

await page.goto('/es/')

// Wait for dynamic content and animations to settle
await page.waitForLoadState('networkidle')
await page.waitForTimeout(1000)

await expect(page).toHaveScreenshot('homepage-es-desktop.png', {
fullPage: true,
maxDiffPixelRatio: 0.1,
})
})

test('should match navigation snapshot', async ({ page }) => {
await page.goto('/es/')

// Use the navigation bar ID, as the <header> tag might have 0 height due to fixed positioning
const nav = page.locator('#main-navigation')

await expect(nav).toBeVisible()
await expect(nav).toHaveScreenshot('navigation-es.png')
})
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
"name": "2026.es.pycon.org",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "pnpm test:unit && pnpm test:e2e",
"test:unit": "vitest run",
"test:e2e": "playwright test",
"test:visual": "playwright test visual --update-snapshots",
"test:a11y": "playwright test accessibility",
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
Expand All @@ -25,9 +30,12 @@
},
"devDependencies": {
"@astrojs/check": "^0.9.6",
"@axe-core/playwright": "^4.11.1",
"@playwright/test": "^1.58.2",
"prettier": "^3.7.4",
"prettier-plugin-astro": "^0.14.1",
"typescript": "^5.9.3"
"typescript": "^5.9.3",
"vitest": "^4.1.0"
},
"prettier": {
"tabWidth": 2,
Expand Down
29 changes: 29 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:4321',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'mobile',
use: { ...devices['iPhone 13'] },
},
],
webServer: {
command: 'pnpm dev',
url: 'http://localhost:4321',
reuseExistingServer: !process.env.CI,
},
})
Loading
Loading