Skip to content

Commit 86dbc33

Browse files
committed
feat(demo): two modes — API (default) + in-browser ISC engine
The demo now runs against api.interscript.org by default (POST /v1/transliterate, debounced 300ms) with a segmented toggle to the in-browser engine: the web worker + iscStrategy over the compact .isc maps. Replaces MapExplorer's main-thread bundledStrategy JSON path. Also: externalize the never-loaded onnxruntime-web/-node and @litertjs/core backends so dev scan and rolldown build pass; harden edge-case E2E assertions to poll (network round-trips replaced the sync engine); catalogue total 287→289; the any_char_class JSON guard becomes an ISC↔JSON behavioural equivalence guard; maps-integration gets a 120s timeout (each test re-parses the bundled JSON IR).
1 parent 8d385fd commit 86dbc33

7 files changed

Lines changed: 247 additions & 121 deletions

File tree

astro.config.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,15 @@ export default defineConfig({
1515
integrations: [vue()],
1616
vite: {
1717
plugins: [tailwindcss()],
18+
// interscript-ts ships ml/session/*-web.js with undeclared
19+
// onnxruntime-web / @litertjs/core imports behind lazy dynamic
20+
// imports; the transliteration path never loads them, but the dev
21+
// scanner and rolldown still try to resolve them.
22+
optimizeDeps: { exclude: ["interscript-ts"] },
23+
build: {
24+
rolldownOptions: {
25+
external: ["onnxruntime-web", "@litertjs/core", "onnxruntime-node"],
26+
},
27+
},
1828
},
1929
})

e2e/demo-edge-cases.spec.ts

Lines changed: 36 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,167 +3,151 @@
33
*
44
* Tests boundary conditions, unicode edge cases, performance,
55
* and error recovery that the happy-path tests don't cover.
6+
*
7+
* The default demo mode calls api.interscript.org, so output
8+
* assertions must poll (debounce + network round-trip) rather than
9+
* sleep for a fixed interval.
610
*/
7-
import { test, expect } from "@playwright/test"
11+
import { test, expect, type Page } from "@playwright/test"
12+
13+
async function ready(page: Page) {
14+
await expect(page.locator(".rail-status")).toContainText("ready", { timeout: 20000 })
15+
}
816

917
test.describe("demo edge cases", () => {
1018
test("handles empty input", async ({ page }) => {
1119
await page.goto("/demo")
12-
await expect(page.locator(".rail-status")).toContainText("ready", { timeout: 20000 })
20+
await ready(page)
1321

1422
const textarea = page.locator("textarea.pane-body")
1523
await textarea.fill("")
16-
await page.waitForTimeout(300)
1724

1825
const output = page.locator(".pane-result")
1926
const text = await output.textContent()
2027
expect(text).toBeTruthy()
28+
await expect(page.locator(".error-banner")).not.toBeVisible()
2129
})
2230

2331
test("handles single character", async ({ page }) => {
2432
await page.goto("/demo")
25-
await expect(page.locator(".rail-status")).toContainText("ready", { timeout: 20000 })
33+
await ready(page)
2634

2735
const textarea = page.locator("textarea.pane-body")
2836
await textarea.fill("А")
29-
await page.waitForTimeout(300)
3037

31-
const output = page.locator(".pane-result")
32-
const text = (await output.textContent())?.trim()
33-
expect(text?.length).toBeGreaterThan(0)
38+
await expect(page.locator(".pane-result")).toContainText("A", { timeout: 8000 })
3439
})
3540

3641
test("handles very long input (1000+ chars)", async ({ page }) => {
3742
await page.goto("/demo")
38-
await expect(page.locator(".rail-status")).toContainText("ready", { timeout: 20000 })
43+
await ready(page)
3944

4045
const longText = "Антон ".repeat(200) // ~1200 chars
4146
const textarea = page.locator("textarea.pane-body")
4247
await textarea.fill(longText)
43-
await page.waitForTimeout(500)
4448

4549
const output = page.locator(".pane-result")
50+
// "Anton Anton" can only come from the repeated long input, not the
51+
// page's default single-word sample.
52+
await expect(output).toContainText("Anton Anton", { timeout: 15000 })
4653
const text = (await output.textContent())?.trim()
4754
expect(text?.length).toBeGreaterThan(100)
48-
expect(text).toContain("Anton")
4955
})
5056

5157
test("handles rapid typing without errors", async ({ page }) => {
5258
await page.goto("/demo")
53-
await expect(page.locator(".rail-status")).toContainText("ready", { timeout: 20000 })
59+
await ready(page)
5460

5561
const textarea = page.locator("textarea.pane-body")
5662
// Type rapidly character by character
5763
for (const char of "АнтонМир") {
5864
await textarea.type(char, { delay: 10 })
5965
}
60-
await page.waitForTimeout(500)
6166

62-
const errorBanner = page.locator(".error-banner")
63-
await expect(errorBanner).not.toBeVisible()
67+
await expect(page.locator(".pane-result")).toContainText("Anton", { timeout: 8000 })
68+
await expect(page.locator(".error-banner")).not.toBeVisible()
6469
})
6570

6671
test("handles mixed scripts", async ({ page }) => {
6772
await page.goto("/demo")
68-
await expect(page.locator(".rail-status")).toContainText("ready", { timeout: 20000 })
73+
await ready(page)
6974

7075
const textarea = page.locator("textarea.pane-body")
7176
await textarea.fill("Hello Антон мир 123")
72-
await page.waitForTimeout(300)
7377

74-
const output = page.locator(".pane-result")
75-
const text = (await output.textContent())?.trim()
76-
expect(text?.length).toBeGreaterThan(0)
7778
// Latin chars should pass through
78-
expect(text).toContain("Hello")
79+
await expect(page.locator(".pane-result")).toContainText("Hello", { timeout: 8000 })
7980
})
8081

8182
test("handles special characters (newlines, tabs)", async ({ page }) => {
8283
await page.goto("/demo")
83-
await expect(page.locator(".rail-status")).toContainText("ready", { timeout: 20000 })
84+
await ready(page)
8485

8586
const textarea = page.locator("textarea.pane-body")
8687
await textarea.fill("Антон\nМир\tПривет")
87-
await page.waitForTimeout(300)
8888

89-
const output = page.locator(".pane-result")
90-
const text = (await output.textContent())?.trim()
91-
expect(text?.length).toBeGreaterThan(0)
89+
await expect(page.locator(".pane-result")).toContainText("Anton", { timeout: 8000 })
9290
})
9391

9492
test("handles numbers and punctuation", async ({ page }) => {
9593
await page.goto("/demo")
96-
await expect(page.locator(".rail-status")).toContainText("ready", { timeout: 20000 })
94+
await ready(page)
9795

9896
const textarea = page.locator("textarea.pane-body")
9997
await textarea.fill("Антон123!@#")
100-
await page.waitForTimeout(300)
10198

102-
const output = page.locator(".pane-result")
103-
const text = (await output.textContent())?.trim()
104-
expect(text).toContain("123")
99+
await expect(page.locator(".pane-result")).toContainText("123", { timeout: 8000 })
105100
})
106101

107102
test("handles unicode surrogate pairs (emoji)", async ({ page }) => {
108103
await page.goto("/demo")
109-
await expect(page.locator(".rail-status")).toContainText("ready", { timeout: 20000 })
104+
await ready(page)
110105

111106
const textarea = page.locator("textarea.pane-body")
112107
await textarea.fill("Антон 🎉 мир")
113-
await page.waitForTimeout(300)
114108

115-
const output = page.locator(".pane-result")
116-
const text = (await output.textContent())?.trim()
117109
// Emoji should pass through (not crash)
118-
expect(text).toBeTruthy()
110+
await expect(page.locator(".pane-result")).toContainText("Anton", { timeout: 8000 })
111+
await expect(page.locator(".error-banner")).not.toBeVisible()
119112
})
120113

121114
test("system switching preserves input", async ({ page }) => {
122115
await page.goto("/demo")
123-
await expect(page.locator(".rail-status")).toContainText("ready", { timeout: 20000 })
116+
await ready(page)
124117

125118
const textarea = page.locator("textarea.pane-body")
126119
const select = page.locator("select.field-input")
127120

128121
// Type in Ukrainian
129122
await textarea.fill("Антон")
130-
await page.waitForTimeout(300)
123+
await expect(page.locator(".pane-result")).toContainText("Anton", { timeout: 8000 })
131124

132125
// Switch to Russian
133126
await select.selectOption("odni-rus-Cyrl-Latn-2015")
134-
await page.waitForTimeout(300)
135127

136-
// Input should be preserved
128+
// Input should be preserved; output should change (different system)
137129
const inputValue = await textarea.inputValue()
138130
expect(inputValue).toBe("Антон")
139-
140-
// Output should change (different system)
141-
const output = page.locator(".pane-result")
142-
const text = (await output.textContent())?.trim()
143-
expect(text?.length).toBeGreaterThan(0)
131+
await expect(page.locator(".pane-result")).toContainText("Anton", { timeout: 8000 })
144132
})
145133

146134
test("handles input that produces no transformation", async ({ page }) => {
147135
await page.goto("/demo")
148-
await expect(page.locator(".rail-status")).toContainText("ready", { timeout: 20000 })
136+
await ready(page)
149137

150138
const textarea = page.locator("textarea.pane-body")
151139
// Pure Latin text — should pass through unchanged for Cyrillic→Latin map
152140
await textarea.fill("Hello World")
153-
await page.waitForTimeout(300)
154141

155-
const output = page.locator(".pane-result")
156-
const text = (await output.textContent())?.trim()
157-
expect(text).toBe("Hello World")
142+
await expect(page.locator(".pane-result")).toHaveText("Hello World", { timeout: 8000 })
158143
})
159144

160145
test("character counter updates correctly", async ({ page }) => {
161146
await page.goto("/demo")
162-
await expect(page.locator(".rail-status")).toContainText("ready", { timeout: 20000 })
147+
await ready(page)
163148

164149
const textarea = page.locator("textarea.pane-body")
165150
await textarea.fill("Hi")
166-
await page.waitForTimeout(300)
167151

168152
// Look for character count indicator
169153
const meta = page.locator(".pane-meta").first()

e2e/demo.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,39 @@ test.describe("demo page — transliteration explorer", () => {
143143
await expect(errorBanner).not.toBeVisible()
144144
})
145145
})
146+
147+
test.describe("demo modes — API vs in-browser", () => {
148+
test("API mode is the default", async ({ page }) => {
149+
await page.goto("/demo")
150+
await expect(page.getByTestId("mode-api")).toHaveAttribute("aria-checked", "true")
151+
await expect(page.getByTestId("mode-browser")).toHaveAttribute("aria-checked", "false")
152+
})
153+
154+
test("browser mode transliterates via the local ISC engine", async ({ page }) => {
155+
await page.goto("/demo")
156+
157+
await page.getByTestId("mode-browser").click()
158+
const status = page.locator(".rail-status")
159+
await expect(status).toContainText("ready", { timeout: 30000 })
160+
161+
await page.locator("textarea.pane-body").fill("Антон")
162+
await expect(page.locator(".pane-result")).toContainText("Anton", { timeout: 10000 })
163+
})
164+
165+
test("both modes agree on Ukrainian", async ({ page }) => {
166+
await page.goto("/demo")
167+
168+
// API mode first
169+
const status = page.locator(".rail-status")
170+
await expect(status).toContainText("ready", { timeout: 30000 })
171+
await page.locator("textarea.pane-body").fill("Київ")
172+
await expect(page.locator(".pane-result")).toContainText("Kyiv", { timeout: 10000 })
173+
const apiOutput = await page.locator(".pane-result").textContent()
174+
175+
// Switch to the in-browser engine — same answer
176+
await page.getByTestId("mode-browser").click()
177+
await expect(page.locator(".pane-result")).toContainText(apiOutput ?? "Kyiv", {
178+
timeout: 30000,
179+
})
180+
})
181+
})

0 commit comments

Comments
 (0)