Skip to content

test(e2e): add Playwright E2E tests#22

Open
miyaji255 wants to merge 32 commits into
mainfrom
feat/e2e-playwright-tests
Open

test(e2e): add Playwright E2E tests#22
miyaji255 wants to merge 32 commits into
mainfrom
feat/e2e-playwright-tests

Conversation

@miyaji255
Copy link
Copy Markdown
Collaborator

Summary

Adds Playwright Test as a new E2E layer that exercises the running app via the Vite dev server (pnpm dev, port 6123). Tests live in e2e/ and reuse the existing demo/pdfpw-demo.pdf + pdfpw-demo.pdfpc fixtures. The existing Vitest browser-mode unit tests remain untouched.

Implements docs/superpowers/specs/2026-05-04-e2e-tests-design.md per docs/superpowers/plans/2026-05-04-e2e-tests-playwright.md.

E2E scenarios (8 specs, 20 tests)

  1. home-upload — file input → presenter navigation, slide counter renders
  2. slide-navigation — Next/Prev buttons + ArrowRight/Left + Home/End + g <n> Enter + Backspace history
  3. presenter-modes — Freeze / Blackout / Overview toggles with aria-pressed assertions
  4. timer — Start/Reset (icon buttons get accessible labels in the same commit)
  5. pdfpc-note — first-slide note text + "no notes" placeholder
  6. locale-switch/en/ja via LocaleSwitcher + pdfpw:locale localStorage
  7. recent-files — uploaded file appears in the library + delete
  8. presenter ↔ presentation sync — multi-window via BrowserContext.waitForEvent('page'), asserts Broadcast Channel propagates Next-slide and Blackout to the presentation window

Other changes

  • thumbnail.test.ts: removed vi.mock("pdfjs-dist", ...) and vi.mock("pdfjs-dist/build/pdf.worker.min.mjs?url", ...). Loads the real demo/pdfpw-demo.pdf via ?url import + fetch, asserts the JPEG data URL is non-trivial. Canvas-getContext-null branch still uses a vi.spyOn.
  • a11y: added aria-label ("Previous slide" / "Next slide") to NextPrevFooter icon buttons and ("Start or pause timer" / "Reset timer") to Timer icon buttons. Driven by E2E selector needs but valid accessibility improvements in their own right.
  • CI: new e2e job in .github/workflows/pr-check.yaml parallel to test and build. Runs pnpm e2e on Chromium. Uploads the Playwright HTML report on failure.
  • Vitest: vite.config.ts test.exclude now contains e2e/** so Vitest never picks up Playwright specs.
  • .gitignore: ignores test-results/, playwright-report/, playwright/.cache/.

Verification

  • pnpm tsc -b — clean
  • pnpm build — success
  • pnpm test — 92 passed (existing Vitest unit suite intact, including the rewritten thumbnail.test.ts)
  • pnpm e2e20 passed (23.9s) on Chromium

Notes for reviewers

  • slide-navigation.spec.ts: the g <n> Enter shortcut treats <n> as a PDF page number, not a user-slide index. The spec pins this current behavior; whether it should accept user-slide indices instead is a UX decision and is out of scope here.
  • WSL2-specific quirk: locally pnpm test reports a Firefox launch teardown timeout in vitest browser mode, but all 92 tests still report as passing. This is a pre-existing environmental issue and does not affect CI (Linux runner with Playwright FF available).

🤖 Generated with Claude Code

miyaji255 and others added 20 commits May 4, 2026 00:24
Plan to introduce Playwright Test alongside existing Vitest browser mode,
covering home → presenter flow, slide navigation, mode toggles, timer,
multi-window broadcast sync, pdfpc note, and locale switch. Also removes
PDF.js mocks from thumbnail.test.ts in favor of a real fixture PDF.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18-task plan for the spec at docs/superpowers/specs/2026-05-04-e2e-tests-design.md.
Covers Playwright scaffolding, 8 E2E scenarios (home upload, slide nav,
mode toggles, timer, pdfpc note, locale, recent files, presenter-presentation
broadcast sync), thumbnail.test.ts mock removal, and CI job addition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the first end-to-end Playwright scenario that uploads the demo
PDF + pdfpc files via the home page file input, follows the redirect
into the presenter route, and asserts the slide counter and stage
text render. Cleans up the auto-opened presentation window for test
isolation.

Also fix resetAppState to wait for the "/" -> "/{locale}" client-side
redirect to settle before evaluating IndexedDB / localStorage clears.
Without this wait, the navigation can destroy the execution context
mid-evaluate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73c1dbcd20

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread playwright.config.ts Outdated
miyaji255 and others added 2 commits May 4, 2026 09:08
Wait for the presentation canvas (positive signal that broadcast init
succeeded and SlideStage mounted) instead of "Connecting" disappearing,
which falsely resolves when the presentation enters an error fallback.

Also keep the auto-hiding menu visible during assertions via a background
mouse-nudge loop, and emit Playwright HTML reports so CI artifact upload
captures something on failure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The popup that hosts the presentation page snapshots the opener's
sessionStorage at creation time. Previously the pair-id was only set
during presenter render — which is gated on PDF.js + pdfpc Suspense —
so on slow environments the popup opened with empty sessionStorage and
fell back to lobby pairing. Lobby pairing has a 5s deadline, but the
presenter's broadcast hook can take longer than that to mount when the
PDF takes time to parse, surfacing TIMEOUT_PAIRING_PRESENTATION.

Calling ensurePresenterPairId(pdf.name) immediately before window.open
seeds the storage synchronously, so the popup inherits a usable pair-id
and the BroadcastChannel pairs without going through the lobby.

Caught by the new presenter-presentation-sync E2E spec running on the
GitHub Actions Linux runner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Playwright Test E2E layer (separate from the existing Vitest browser-mode suite) to exercise the running app via the Vite dev server, plus a few small app tweaks to make E2E selectors stable and multi-window pairing more reliable.

Changes:

  • Introduces Playwright E2E test harness (playwright.config.ts, e2e/ tests/helpers/fixtures) and adds pnpm e2e scripts + CI job.
  • Adds accessibility labels for presenter icon buttons (Next/Prev, Timer) to enable robust role-based selectors.
  • Adjusts test setup: Vitest excludes e2e/**, and thumbnail.test.ts now validates against a real PDF fixture instead of mocking pdfjs-dist.

Reviewed changes

Copilot reviewed 24 out of 26 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
vite.config.ts Excludes e2e/** from Vitest test discovery.
src/routes/$locale/(main)/index.tsx Seeds broadcast pairing id into sessionStorage before opening the presentation window.
src/routes/$locale/(main)/-presenter/Timer.tsx Adds localized aria-labels for timer icon buttons.
src/routes/$locale/(main)/-presenter/NextPrevFooter.tsx Adds localized aria-labels for prev/next slide icon buttons.
src/lib/thumbnail.test.ts Rewrites thumbnail tests to use a real demo PDF fixture via ?url + fetch.
pnpm-lock.yaml Locks @playwright/test addition.
playwright.config.ts Playwright Test config: dev-server webServer, chromium project, CI reporter/retries.
package.json Adds e2e / e2e:ui scripts and @playwright/test devDependency.
messages/ja.json Adds new message keys for presenter aria-label strings (JA).
messages/en.json Adds new message keys for presenter aria-label strings (EN).
e2e/tests/timer.spec.ts E2E spec for timer start/reset behavior.
e2e/tests/slide-navigation.spec.ts E2E spec for button + keyboard slide navigation flows.
e2e/tests/recent-files.spec.ts E2E spec for recent files library persistence + deletion.
e2e/tests/presenter-presentation-sync.spec.ts E2E spec for presenter ↔ presentation multi-window broadcast sync.
e2e/tests/presenter-modes.spec.ts E2E spec for freeze/blackout/overview mode toggles.
e2e/tests/pdfpc-note.spec.ts E2E spec for pdfpc note rendering + empty placeholder.
e2e/tests/locale-switch.spec.ts E2E spec for locale switching + persistence.
e2e/tests/home-upload.spec.ts E2E spec for upload → presenter navigation + basic render checks.
e2e/helpers/reset-state.ts Helper to clear IndexedDB/localStorage and reload between tests.
e2e/helpers/presentation-window.ts Helper for capturing the popup presentation page.
e2e/helpers/open-pdf.ts Helper to upload the demo PDF(+pdfpc) and wait for presenter readiness.
e2e/fixtures/pdfs.ts Resolves absolute paths to demo PDF/pdfpc fixtures for Playwright.
docs/superpowers/specs/2026-05-04-e2e-tests-design.md Design spec documenting E2E approach and scenarios.
docs/superpowers/plans/2026-05-04-e2e-tests-playwright.md Implementation plan for Playwright E2E integration.
.gitignore Ignores Playwright output directories (test-results/, playwright-report/, cache).
.github/workflows/pr-check.yaml Adds a CI e2e job running pnpm e2e and uploading report on failure.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread e2e/helpers/reset-state.ts
Comment thread e2e/tests/slide-navigation.spec.ts
Comment thread e2e/helpers/open-pdf.ts Outdated
Comment thread e2e/helpers/presentation-window.ts Outdated
Comment thread vite.config.ts
miyaji255 and others added 4 commits May 4, 2026 09:44
Adds 6 tests covering the laser pointer and pen drawing tools, which
broadcast state via sendTool. Verifies:

- laser activated on presenter syncs the dot to presentation
- laser activated on presentation syncs the dot to presenter
- pen stroke drawn on presenter appears on presentation
- pen stroke drawn on presentation appears on presenter
- erase (`e`) clears strokes on both windows
- Escape removes the overlay portal on both windows

Configured to run serially within the file (test.describe.configure
mode: serial) — these tests share heavy multi-window interactions and
race the dev server's module compilation when run in parallel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-talk

Adds `e2e/helpers/test-fixtures.ts` with a worker-scoped Playwright fixture
that copies the demo PDF and pdfpc to a unique-named pair (e.g.
`pdfpw-demo-w0.pdf`) per worker. The app keys its BroadcastChannel name and
sessionStorage pair-id on `File.name`, so unique names prevent parallel
test workers from cross-pairing or stomping on each other's tool state.

All upload-spawning specs now consume `uniqueFixtures` instead of the
static `fixtures` import. `locale-switch.spec.ts` is unchanged because it
doesn't upload.

Also:
- pointer-tools.spec.ts gets `retries: 2` (in addition to `mode: "serial"`)
  to absorb a residual flake where useToolShortcut's window keydown
  listener occasionally registers after the spec presses its first key.
- pressShortcut switched to `body.press` (single dispatch) — combining it
  with `window.dispatchEvent` was double-firing the toggle and observably
  flipping toolMode laser → none in one synthetic press.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- resetAppState: clear sessionStorage and seed pdfpw:locale="en". Forces
  English UI regardless of navigator.language so role/aria-label selectors
  stay deterministic, and prevents the broadcast pair-id from leaking
  between tests in the same context.
- Drop e2e/helpers/open-pdf.ts and presentation-window.ts (YAGNI — neither
  was consumed by any spec).
- vite.config.ts: extend Vitest's default exclude list instead of replacing
  it, so cypress/.idea/.git/.cache/config-file globs stay excluded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Existing specs only verify pairing implicitly (sync + pointer-tools wait
for the presentation canvas mid-test). This spec asserts pairing as the
primary subject so the regression we just fixed — popup losing the
pair-id when sessionStorage is seeded after window.open — surfaces
directly here:

- presenter sessionStorage carries a UUID pair-id keyed on the file name
- the popup's sessionStorage was inherited at creation and matches
- the presentation canvas mounts (init-response received)
- none of the error fallbacks (Connecting / Pairing failed / config /
  PDF load) are visible

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
miyaji255 and others added 6 commits May 5, 2026 11:54
The existing laser tests only verified the dot becomes visible after
pressing `l`; they did not check whether the dot actually follows the
cursor. This test moves the mouse to three distinct quadrants of the
slide canvas (25/25, 75/75, 10/90) and asserts the dot's
`style.left` / `style.top` percent values land within ±2% of the
expected position on BOTH the presenter and the presentation page —
catching coordinate-swap, axis-aliasing, or broadcast-truncation bugs
that the visibility-only assertion would miss.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a grid-of-9 visibility test for the laser dot. Verifies at each
point that:

1. The visible inner dot passes Element.checkVisibility() — covers
   display:none / visibility:hidden / opacity:0 regressions.
2. At the dot's pixel center the topmost element is part of the overlay
   portal — catches the "laser ends up behind the slide" stacking
   regression that checkVisibility alone cannot detect.

Chromium's elementsFromPoint() filters out pointer-events:none
elements, so the helper temporarily flips the overlay's pointer-events
inline style to "auto" during the probe, then restores it. The probe
runs once per grid point on both presenter and presentation pages.

Also stops using parseFloat and uses Number(...) consistently in the
percent-parsing helpers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
readLaserPercent already produces the numeric left/top from the wrapper's
style. Threading those values through evaluate's argument removes the
duplicate parse (and the .replace("%","") that came with it) inside the
browser-side closure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous on-top helper relied on Element.checkVisibility() with
default options, which only catches display:none and skips opacity:0 /
visibility:hidden / 0-area regressions. Playwright's
expect(locator).toBeVisible() is strictly stronger: it requires
display ≠ none, visibility ≠ hidden, opacity > 0 anywhere up the chain,
AND a non-zero bounding box.

Changes:

- Probe the *inner core* (the 12×12 red dot, last child of the wrapper)
  instead of the 0×0 wrapper. Targeting the wrapper made the bbox-area
  check meaningless because the wrapper is sized 0 by design.
- Use Playwright's toBeVisible() for the visibility step.
- Keep the existing position match + elementsFromPoint occlusion check
  as separate poll steps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-verification surfaced a hole: Playwright's expect(locator).toBeVisible()
does NOT walk ancestors to check opacity. Injecting opacity:0 on the
overlay portal made the laser invisible to a user but our check still
reported it as visible.

Changes:

- Add a supplementary `Element.checkVisibility({opacityProperty: true,
  visibilityProperty: true, contentVisibilityAuto: true})` step. Default
  options on this DOM API don't catch ancestor opacity either; the
  options enable the ancestor walk we actually need.

- Add three self-verifying tests that prove the check fails when the
  laser is forcibly hidden via:
  1. `display: none` injection
  2. `opacity: 0` ancestor injection
  3. A z-index 9999 div stacked in front

  These are guards against future regressions of the visibility helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`useContainerRect` ran its useEffect once at mount with `[ref]` deps.
When `pdfAreaRef.current` was null at that moment (the target div lives
inside <PdfPage>'s inner Suspense and PdfPageCanvas is suspended on
`use(getPage(...))`), the effect early-returned and never recovered:
`[ref]` is stable, so the effect is never re-fired. `rect` stayed null
forever, `PointerOverlay` returned null forever, and the laser dot was
permanently invisible on whichever side hit the race — most reliably
the presentation page, which has more PdfPages preloaded and runs the
race more often.

Fix: when `ref.current` is null at first run, set up a
`MutationObserver` on `document.body` and re-try once the target node
attaches. After it attaches, install the original ResizeObserver +
window listeners and disconnect the MutationObserver.

Surfaced by user-reported "presentation laser doesn't display" repro
(diagnostic: `body > div.fixed.pointer-events-none.z-50` was absent
in presentation's DOM after activation, confirming the portal never
rendered). The new `regression: presentation PointerOverlay portal
mounts even when PdfPage Suspense is slow` E2E test in
pointer-tools.spec.ts covers the contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants