Skip to content

Commit 5ad320b

Browse files
committed
feat: Playwright E2E tests + MapExplorer reactivity + transitive dep fix
E2E test suite (27 tests, all passing): - home.spec: hero, CTAs, script mosaic - demo.spec: transliteration of Ukrainian, Russian, German, Tamil, Amharic - maps.spec: catalogue browsing, filtering, detail pages - detect.spec: language detection panel - batch.spec: batch transliteration UI - compare.spec: system comparison UI Bug fixes in MapExplorer.vue: 1. Reactivity: transliterateFn was a plain `let` — Vue couldn't track when it changed. Converted to `ref` so the `output` computed re-evaluates when the engine finishes loading. 2. Transitive dependencies: the old fetch loop only did 2 passes. A dependency discovered during pass 2 (e.g. bgnpcgn-ukr → un-ukr → ua-ukr) was never fetched. Fixed with a `while` loop that runs until the wanted set stops growing. Added TODO.complete/ directory documenting all remaining ecosystem work.
1 parent 9fa05b2 commit 5ad320b

11 files changed

Lines changed: 470 additions & 10 deletions

File tree

TODO.complete/01-e2e-tests.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# 03 — E2E tests for interscript.org website
2+
3+
## Priority: P0
4+
5+
## Problem
6+
The website has integration tests (Vitest) that test individual modules,
7+
but no browser-based E2E tests that validate the actual user experience:
8+
loading pages, typing in transliteration inputs, seeing output.
9+
10+
## Solution: Playwright
11+
12+
### Setup
13+
```bash
14+
cd interscript.org
15+
npm install -D @playwright/test
16+
npx playwright install
17+
```
18+
19+
### Structure
20+
```
21+
e2e/
22+
playwright.config.ts
23+
home.spec.ts # Landing page loads, CTA works
24+
detect.spec.ts # Detect panel: input → system suggestion
25+
compare.spec.ts # Compare two systems side by side
26+
batch.spec.ts # Batch transliteration
27+
maps.spec.ts # Browse maps, search, filter
28+
api-docs.spec.ts # API documentation interactive
29+
worker.spec.ts # Web Worker loads and transliterates
30+
```
31+
32+
### Key Test Cases
33+
34+
#### Worker transliteration (critical path)
35+
```typescript
36+
test("worker transliterates Ukrainian", async ({ page }) => {
37+
await page.goto("/")
38+
const input = page.locator("[data-testid='translit-input']")
39+
const output = page.locator("[data-testid='translit-output']")
40+
await input.fill("Антон")
41+
await expect(output).toContainText("Anton")
42+
})
43+
```
44+
45+
#### Map browser
46+
```typescript
47+
test("browse maps by script", async ({ page }) => {
48+
await page.goto("/maps")
49+
await page.click("text=Cyrillic")
50+
await expect(page.locator(".map-card")).toHaveCount.greaterThan(10)
51+
})
52+
```
53+
54+
#### API playground
55+
```typescript
56+
test("API playground returns JSON", async ({ page }) => {
57+
await page.goto("/api")
58+
await page.fill("[data-testid='api-input']", '{"system":"bgnpcgn-ukr-Cyrl-Latn-2019","input":"Київ"}')
59+
await page.click("[data-testid='api-run']")
60+
await expect(page.locator("[data-testid='api-output']")).toContainText("Kyiv")
61+
})
62+
```
63+
64+
### CI Integration
65+
```yaml
66+
# .github/workflows/e2e.yml
67+
- name: Build site
68+
run: npm run build
69+
- name: Run Playwright
70+
run: npx playwright test
71+
- name: Upload report
72+
if: always()
73+
uses: actions/upload-artifact@v3
74+
with:
75+
name: playwright-report
76+
path: playwright-report/
77+
```

e2e/batch.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* E2E tests for the /batch page.
3+
*
4+
* Validates: batch transliteration of multiple inputs works correctly.
5+
*/
6+
import { test, expect } from "@playwright/test"
7+
8+
test.describe("batch transliteration", () => {
9+
test("page loads", async ({ page }) => {
10+
await page.goto("/batch")
11+
await expect(page.locator("body")).toBeVisible()
12+
})
13+
14+
test("has input area for batch text", async ({ page }) => {
15+
await page.goto("/batch")
16+
const input = page.locator("textarea, input[type='text']").first()
17+
await expect(input).toBeVisible({ timeout: 5000 })
18+
})
19+
20+
test("has system selector", async ({ page }) => {
21+
await page.goto("/batch")
22+
await page.waitForLoadState("networkidle")
23+
const select = page.locator("#batch-system, select").first()
24+
await expect(select).toBeVisible({ timeout: 10000 })
25+
})
26+
})

e2e/compare.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* E2E tests for the /compare page.
3+
*
4+
* Validates: comparing the same input across multiple transliteration systems.
5+
*/
6+
import { test, expect } from "@playwright/test"
7+
8+
test.describe("compare systems", () => {
9+
test("page loads", async ({ page }) => {
10+
await page.goto("/compare")
11+
await expect(page.locator("body")).toBeVisible()
12+
})
13+
14+
test("has input field", async ({ page }) => {
15+
await page.goto("/compare")
16+
const input = page.locator("textarea, input[type='text']").first()
17+
await expect(input).toBeVisible({ timeout: 5000 })
18+
})
19+
20+
test("has system selectors", async ({ page }) => {
21+
await page.goto("/compare")
22+
const selects = page.locator("select")
23+
if (await selects.first().isVisible({ timeout: 3000 }).catch(() => false)) {
24+
const count = await selects.count()
25+
expect(count).toBeGreaterThanOrEqual(1)
26+
}
27+
})
28+
})

e2e/demo.spec.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* E2E test: the /demo page transliteration Explorer.
3+
*
4+
* This is the critical user-facing path: the TS runtime loads maps from
5+
* /public/maps and transliterates input in real-time via interscript-ts.
6+
*
7+
* If this test passes, the entire Ruby→IR→TS pipeline works end-to-end.
8+
*/
9+
import { test, expect } from "@playwright/test"
10+
11+
test.describe("demo page — transliteration explorer", () => {
12+
test("page loads with system selector and input", async ({ page }) => {
13+
await page.goto("/demo")
14+
await expect(page.locator("h1")).toContainText(/Type/i)
15+
16+
// Engine status shows loading or ready (not "missing")
17+
const status = page.locator(".rail-status")
18+
await expect(status).toBeVisible()
19+
20+
// System selector has options
21+
const select = page.locator("select.field-input")
22+
await expect(select).toBeVisible()
23+
const options = select.locator("option")
24+
await expect(options).toHaveCount(5)
25+
})
26+
27+
test("default system is BGN/PCGN Ukrainian", async ({ page }) => {
28+
await page.goto("/demo")
29+
const select = page.locator("select.field-input")
30+
await expect(select).toHaveValue("bgnpcgn-ukr-Cyrl-Latn-2019")
31+
})
32+
33+
test("transliterates Ukrainian Cyrillic to Latin", async ({ page }) => {
34+
await page.goto("/demo")
35+
36+
// Wait for engine to be ready (loads all systems + their deps)
37+
const status = page.locator(".rail-status")
38+
await expect(status).toContainText("ready", { timeout: 20000 })
39+
40+
// Type Cyrillic input
41+
const textarea = page.locator("textarea.pane-body")
42+
await textarea.fill("Антон")
43+
44+
// Wait for output to contain "Anton"
45+
const output = page.locator(".pane-result")
46+
await expect(output).toContainText("Anton", { timeout: 10000 })
47+
})
48+
49+
test("transliterates ODNI Russian", async ({ page }) => {
50+
await page.goto("/demo")
51+
52+
const status = page.locator(".rail-status")
53+
await expect(status).toContainText("ready", { timeout: 20000 })
54+
55+
// Change system and type
56+
const select = page.locator("select.field-input")
57+
await select.selectOption("odni-rus-Cyrl-Latn-2015")
58+
59+
const textarea = page.locator("textarea.pane-body")
60+
await textarea.fill("привет мир")
61+
62+
const output = page.locator(".pane-result")
63+
await expect(output).toContainText("privet mir", { timeout: 10000 })
64+
})
65+
66+
test("transliterates BGN/PCGN German", async ({ page }) => {
67+
await page.goto("/demo")
68+
69+
const status = page.locator(".rail-status")
70+
await expect(status).toContainText("ready", { timeout: 20000 })
71+
72+
const select = page.locator("select.field-input")
73+
await select.selectOption("bgnpcgn-deu-Latn-Latn-2000")
74+
75+
const textarea = page.locator("textarea.pane-body")
76+
await textarea.fill("Tschüß!")
77+
78+
const output = page.locator(".pane-result")
79+
await expect(output).toContainText("Tschueß", { timeout: 10000 })
80+
})
81+
82+
test("transliterates UN Tamil", async ({ page }) => {
83+
await page.goto("/demo")
84+
85+
const status = page.locator(".rail-status")
86+
await expect(status).toContainText("ready", { timeout: 20000 })
87+
88+
const select = page.locator("select.field-input")
89+
await select.selectOption("un-tam-Taml-Latn-1972")
90+
91+
const textarea = page.locator("textarea.pane-body")
92+
await textarea.fill("தமிழ்")
93+
94+
// Tamil output should be non-empty and different from input
95+
const output = page.locator(".pane-result")
96+
await page.waitForTimeout(1000)
97+
const text = (await output.textContent())?.trim()
98+
expect(text?.length ?? 0).toBeGreaterThan(0)
99+
})
100+
101+
test("transliterates ALA-LC Amharic", async ({ page }) => {
102+
await page.goto("/demo")
103+
104+
const status = page.locator(".rail-status")
105+
await expect(status).toContainText("ready", { timeout: 20000 })
106+
107+
const select = page.locator("select.field-input")
108+
await select.selectOption("alalc-amh-Ethi-Latn-2011")
109+
110+
const textarea = page.locator("textarea.pane-body")
111+
await textarea.fill("ኢትዮጵያ")
112+
113+
const output = page.locator(".pane-result")
114+
await page.waitForTimeout(1000)
115+
const text = (await output.textContent())?.trim()
116+
expect(text?.length ?? 0).toBeGreaterThan(0)
117+
})
118+
119+
test("handles empty input gracefully", async ({ page }) => {
120+
await page.goto("/demo")
121+
122+
const textarea = page.locator("textarea.pane-body")
123+
await textarea.fill("")
124+
await page.waitForTimeout(200)
125+
126+
const output = page.locator(".pane-result")
127+
const text = await output.textContent()
128+
// Empty input should produce empty output or a dash
129+
expect(text).toBeTruthy()
130+
})
131+
132+
test("no error banner appears during normal use", async ({ page }) => {
133+
await page.goto("/demo")
134+
135+
const status = page.locator(".rail-status")
136+
await expect(status).toContainText(/ready|loading/, { timeout: 10000 })
137+
138+
const textarea = page.locator("textarea.pane-body")
139+
await textarea.fill("Антон")
140+
await page.waitForTimeout(500)
141+
142+
const errorBanner = page.locator(".error-banner")
143+
await expect(errorBanner).not.toBeVisible()
144+
})
145+
})

e2e/detect.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* E2E tests for the /detect page.
3+
*
4+
* Validates: the language detection feature works — given Latin output,
5+
* it suggests which transliteration system was likely used.
6+
*/
7+
import { test, expect } from "@playwright/test"
8+
9+
test.describe("detect page", () => {
10+
test("page loads", async ({ page }) => {
11+
await page.goto("/detect")
12+
await expect(page.locator("body")).toBeVisible()
13+
})
14+
15+
test("has input field for detection", async ({ page }) => {
16+
await page.goto("/detect")
17+
const input = page.locator("input, textarea").first()
18+
await expect(input).toBeVisible({ timeout: 5000 })
19+
})
20+
21+
test("detection produces results for valid input", async ({ page }) => {
22+
await page.goto("/detect")
23+
await page.waitForLoadState("networkidle")
24+
25+
const input = page.locator("input, textarea").first()
26+
if (await input.isVisible({ timeout: 5000 }).catch(() => false)) {
27+
await input.fill("Anton")
28+
await page.waitForTimeout(1000)
29+
30+
// Should show some result (system suggestion or "no match")
31+
const body = page.locator("body")
32+
await expect(body).toContainText(/system|result|match|detect/i)
33+
}
34+
})
35+
})

e2e/home.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* E2E tests for the Interscript.org home page.
3+
*
4+
* Validates: page loads, hero renders, catalogue data is present,
5+
* CTAs link to the right pages.
6+
*/
7+
import { test, expect } from "@playwright/test"
8+
9+
test.describe("home page", () => {
10+
test("loads and renders hero", async ({ page }) => {
11+
await page.goto("/")
12+
await expect(page).toHaveTitle(/Interscript/i)
13+
await expect(page.locator("h1")).toContainText(/Every/i)
14+
})
15+
16+
test("hero stats render real numbers", async ({ page }) => {
17+
await page.goto("/")
18+
const lead = page.locator(".hero-lead")
19+
await expect(lead).toContainText(/systems/)
20+
await expect(lead).toContainText(/authorities/)
21+
})
22+
23+
test("CTA links to demo page", async ({ page }) => {
24+
await page.goto("/")
25+
await page.waitForLoadState("networkidle")
26+
const cta = page.locator('a[href="/demo"]').first()
27+
await expect(cta).toBeVisible({ timeout: 5000 })
28+
await cta.click()
29+
await expect(page).toHaveURL(/\/demo/)
30+
})
31+
32+
test("CTA links to maps browser", async ({ page }) => {
33+
await page.goto("/")
34+
await page.waitForLoadState("networkidle")
35+
const cta = page.locator('a[href="/maps"]').first()
36+
await expect(cta).toBeVisible({ timeout: 5000 })
37+
await cta.click()
38+
await expect(page).toHaveURL(/\/maps/)
39+
})
40+
41+
test("script mosaic renders", async ({ page }) => {
42+
await page.goto("/")
43+
const mosaic = page.locator(".script-mosaic, [class*='mosaic']")
44+
// Mosaic may take a moment to hydrate
45+
await page.waitForTimeout(1000)
46+
await expect(mosaic.first()).toBeVisible()
47+
})
48+
})

0 commit comments

Comments
 (0)