|
| 1 | +import { test, expect } from "@playwright/test"; |
| 2 | + |
| 3 | +const PAGE = "/wiktionary_pron/macronizer.html"; |
| 4 | + |
| 5 | +async function macronize(page, text) { |
| 6 | + await page.fill("#text_to_macronize", text); |
| 7 | + await page.click("#macronize_btn"); |
| 8 | + await expect(page.locator("#resultText .ipa").first()).toBeVisible({ timeout: 120_000 }); |
| 9 | +} |
| 10 | + |
| 11 | +// Client-rect centre of one character of the first word span. Uses a Range over the |
| 12 | +// span's text node so the click lands inside the glyph, not on the span's padding. |
| 13 | +async function charCenter(page, charIndex) { |
| 14 | + return await page.evaluate((charIndex) => { |
| 15 | + const span = document.querySelector("#resultText .ipa"); |
| 16 | + const tn = span.firstChild; |
| 17 | + const range = document.createRange(); |
| 18 | + range.setStart(tn, charIndex); |
| 19 | + range.setEnd(tn, Math.min(charIndex + 1, tn.length)); |
| 20 | + const r = range.getBoundingClientRect(); |
| 21 | + return { x: (r.left + r.right) / 2, y: (r.top + r.bottom) / 2 }; |
| 22 | + }, charIndex); |
| 23 | +} |
| 24 | + |
| 25 | +test.describe("Phase 2 editing", () => { |
| 26 | + test("click a vowel toggles its macron; Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z round-trip", async ({ page }) => { |
| 27 | + test.setTimeout(360_000); |
| 28 | + await page.goto(PAGE); |
| 29 | + await expect(page.locator("#macronize_btn")).toBeEnabled({ timeout: 240_000 }); |
| 30 | + |
| 31 | + await macronize(page, "divisa"); |
| 32 | + const span = page.locator("#resultText .ipa").first(); |
| 33 | + await expect(span).toHaveAttribute("content", "dīvīsa"); |
| 34 | + |
| 35 | + // Toggle the ī at index 1 → "divīsa" (that single vowel flips). |
| 36 | + const pt = await charCenter(page, 1); |
| 37 | + await page.mouse.click(pt.x, pt.y); |
| 38 | + await expect(span).toHaveAttribute("content", "divīsa"); |
| 39 | + |
| 40 | + // Ctrl+Z undoes the toggle. |
| 41 | + await page.keyboard.press("Control+z"); |
| 42 | + await expect(span).toHaveAttribute("content", "dīvīsa"); |
| 43 | + |
| 44 | + // Ctrl+Y redoes. |
| 45 | + await page.keyboard.press("Control+y"); |
| 46 | + await expect(span).toHaveAttribute("content", "divīsa"); |
| 47 | + |
| 48 | + // Ctrl+Shift+Z is the other redo chord — the word stays toggled. |
| 49 | + await page.keyboard.press("Control+Shift+z"); |
| 50 | + await expect(span).toHaveAttribute("content", "divīsa"); |
| 51 | + |
| 52 | + // Undo again — the round-trip is fully reversible. |
| 53 | + await page.keyboard.press("Control+z"); |
| 54 | + await expect(span).toHaveAttribute("content", "dīvīsa"); |
| 55 | + }); |
| 56 | + |
| 57 | + test("clicking a consonant does not toggle; typing edits and Ctrl+Z reverts", async ({ page }) => { |
| 58 | + test.setTimeout(360_000); |
| 59 | + await page.goto(PAGE); |
| 60 | + await expect(page.locator("#macronize_btn")).toBeEnabled({ timeout: 240_000 }); |
| 61 | + |
| 62 | + await macronize(page, "divisa"); |
| 63 | + const span = page.locator("#resultText .ipa").first(); |
| 64 | + await expect(span).toHaveAttribute("content", "dīvīsa"); |
| 65 | + |
| 66 | + // Click the consonant v (index 2) — precise hit-testing must NOT toggle a neighbour. |
| 67 | + const pt = await charCenter(page, 2); |
| 68 | + await page.mouse.click(pt.x, pt.y); |
| 69 | + await expect(span).toHaveAttribute("content", "dīvīsa"); |
| 70 | + |
| 71 | + // The caret sits inside the word; typing edits it and the content attr follows. |
| 72 | + await page.keyboard.type("x"); |
| 73 | + const edited = await span.textContent(); |
| 74 | + expect(edited).toContain("x"); |
| 75 | + await expect(span).toHaveAttribute("content", edited); |
| 76 | + |
| 77 | + await page.keyboard.press("Control+z"); |
| 78 | + await expect(span).toHaveAttribute("content", "dīvīsa"); |
| 79 | + }); |
| 80 | + |
| 81 | + test("keyboard: Enter on a flagged word opens the readings popup", async ({ page }) => { |
| 82 | + test.setTimeout(360_000); |
| 83 | + await page.goto(PAGE); |
| 84 | + await expect(page.locator("#macronize_btn")).toBeEnabled({ timeout: 240_000 }); |
| 85 | + |
| 86 | + await macronize(page, "divisa"); |
| 87 | + const span = page.locator("#resultText .ipa").first(); |
| 88 | + await expect(span).toHaveAttribute("content", "dīvīsa"); |
| 89 | + |
| 90 | + // Flagged span is focusable; Enter opens the dialog. The word must NOT change |
| 91 | + // (no accidental toggle from the activation). |
| 92 | + await span.focus(); |
| 93 | + await page.keyboard.press("Enter"); |
| 94 | + await expect(page.locator(".word-popup .popup-cycle")).toBeVisible({ timeout: 5000 }); |
| 95 | + await expect(span).toHaveAttribute("content", "dīvīsa"); |
| 96 | + }); |
| 97 | + |
| 98 | + test("touch: tap a flagged word opens the sheet, tap a clean word does not", async ({ browser }) => { |
| 99 | + // hasTouch context so isTouch() is true and the sheet path runs. |
| 100 | + const context = await browser.newContext({ hasTouch: true, viewport: { width: 390, height: 844 } }); |
| 101 | + const page = await context.newPage(); |
| 102 | + test.setTimeout(360_000); |
| 103 | + await page.goto(PAGE); |
| 104 | + await expect(page.locator("#macronize_btn")).toBeEnabled({ timeout: 240_000 }); |
| 105 | + |
| 106 | + await macronize(page, "provinciarum divisa"); |
| 107 | + const clean = page.locator("#resultText .ipa").nth(0); // prōvinciārum — unambiguous |
| 108 | + const flagged = page.locator("#resultText .ipa").nth(1); // divisa — 2 readings |
| 109 | + await expect(clean).toHaveAttribute("content", "prōvinciārum"); |
| 110 | + await expect(clean).not.toHaveClass(/ambig|unknown/); // premise: provinciarum is clean |
| 111 | + await expect(flagged).toHaveAttribute("content", "dīvīsa"); |
| 112 | + await expect(flagged).toHaveClass(/ambig/); // premise: divisa is flagged |
| 113 | + |
| 114 | + // Tap the CLEAN word → no sheet, no toggle (caret lands for editing). |
| 115 | + await clean.tap(); |
| 116 | + await expect(page.locator(".word-popup")).not.toBeVisible({ timeout: 5000 }); |
| 117 | + await expect(clean).toHaveAttribute("content", "prōvinciārum"); |
| 118 | + |
| 119 | + // Tap the FLAGGED word → the sheet (readings-first) appears. |
| 120 | + await flagged.tap(); |
| 121 | + await expect(page.locator(".word-popup.sheet")).toBeVisible({ timeout: 5000 }); |
| 122 | + const readings = page.locator(".word-popup .popup-section", { hasText: /^Possible readings/ }); |
| 123 | + await expect(readings).toBeVisible({ timeout: 5000 }); |
| 124 | + |
| 125 | + // Tap a reading row → the word changes to that spelling and the sheet closes. |
| 126 | + const rows = page.locator(".word-popup table.readings tr"); |
| 127 | + const rowCount = await rows.count(); |
| 128 | + expect(rowCount).toBeGreaterThan(1); |
| 129 | + await rows.nth(1).tap(); |
| 130 | + await expect(flagged).not.toHaveAttribute("content", "dīvīsa", { timeout: 5000 }); |
| 131 | + await expect(page.locator(".word-popup.sheet")).not.toBeVisible({ timeout: 5000 }); |
| 132 | + |
| 133 | + await context.close(); |
| 134 | + }); |
| 135 | +}); |
0 commit comments