test(e2e): retarget FAB and event-menu selectors at the current UI (#39) #61

Merged
laoc merged 4 commits from fix/issue-39-e2e-selectors into dev 2026-07-30 12:08:15 +00:00
Owner

Closes #39.

Six e2e files hard-coded the daisyUI speed-dial markup and all six broke together when GlobalFAB replaced it. Routed through three shared helpers in fixtures.jsFAB_TRIGGER, openCreateHub(), clickCreateAction() — so the next redesign touches one place instead of six.

Two of the four bullets needed a semantic change, not a selector swap

button[data-tip="Create Event"] was wrong twice over. data-tip now carries the action description, not its label — and it is absent entirely for actions with no description, which includes poll and learning-content. So that selector matched nothing and could not be repaired by renaming the value. Tiles are keyed on aria-label (the action's ariaLabel in create-actions.js):

old new
.fab [role="button"] button[aria-label="Open actions menu"]
button[data-tip="Create Event"] button[aria-label="Create new event"]
button[data-tip="Create Learning Content"] button[aria-label="Share learning content"]
.fab button[data-tip="Create poll"] button[aria-label="Create poll"]

Note the create-hub sheet carries the same aria-label as the trigger on a div[role="menu"], so the button qualifier is load-bearing.

edit button hidden for non-owner asserted the wrong element. It checked that the trigger was invisible. EventContextMenu renders for everyone, because it also carries Copy link / Copy event ID — only the author actions are conditional. Swapping the selector alone would have made this test fail on a correct build. The assertion moved to the Edit item, plus a positive control that the menu did open, so a count of 0 means "no Edit item" rather than "no menu".

The event menu was orphaned, not renamed

The detail page routes through CalendarEventDetailViewDetailHeaderEventContextMenu, trigger aria-label="Event menu". EventManagementActions ("Manage event") still exists — but only inside CalendarEventDetailsModal, which the detail page never mounts.

Item labels changed too: EventContextMenu uses m.common_edit() / m.common_delete()'Edit' and 'Delete', not the old 'Delete Event'. The confirmation dialog still reads 'Delete Event?' (event_management_delete_confirm_title), so that assertion stays. Items are now matched by label instead of .first(), which would silently click "Share to communities" for a non-owner.

layout-consistency could not be selector-swapped either

That test asserted the FAB sat in a sticky bottom-0 h-0 mt-auto wrapper inside <main>. GlobalFAB is mounted at the layout root (+layout.svelte:379, outside <main>) and positioned fixed, so main .fab and the wrapper-geometry assertions were both unreachable.

Rewritten around the property the regression was actually about — on a page too short to scroll, the button is still at the bottom of the screen — plus explicit checks that it is outside <main> and viewport-fixed.

Related finding, deliberately not changed here: that sticky mt-auto h-0 wrapper still exists in +layout.svelte:369, but its only remaining child is ScrollToTopButton, which is itself position: fixed. So the whole mt-auto/sticky mechanism now has no observable effect. Removing it is a source change outside this issue's scope — worth a separate decision.

Also corrected

Two comments documenting removed behaviour (poll-flow's viewport rationale — the 9-button column that clipped no longer exists, the hub is a scrollable max-h-[80vh] sheet; and MainContentArea's claim that GlobalFAB is anchored to <main>), and the COVERAGE.md row describing the old data-tip assertion.

Verification

At 2d94d16b: all 316 tests across 38 e2e files parse and register under playwright test --list (imports resolve, renamed test present), unit suite 5153/5153 across 468 files, pnpm lint clean.

The browser run against the Docker relay stack is not done — that is TestOER's pass, per the testing split. A parse check proves the selectors are syntactically reachable, not that they match live DOM.

🤖 Generated with Claude Code

Closes #39. Six e2e files hard-coded the daisyUI speed-dial markup and all six broke together when `GlobalFAB` replaced it. Routed through three shared helpers in `fixtures.js` — `FAB_TRIGGER`, `openCreateHub()`, `clickCreateAction()` — so the next redesign touches one place instead of six. ## Two of the four bullets needed a semantic change, not a selector swap **`button[data-tip="Create Event"]` was wrong twice over.** `data-tip` now carries the action *description*, not its label — and it is **absent entirely** for actions with no description, which includes poll and learning-content. So that selector matched nothing and could not be repaired by renaming the value. Tiles are keyed on `aria-label` (the action's `ariaLabel` in `create-actions.js`): | old | new | |---|---| | `.fab [role="button"]` | `button[aria-label="Open actions menu"]` | | `button[data-tip="Create Event"]` | `button[aria-label="Create new event"]` | | `button[data-tip="Create Learning Content"]` | `button[aria-label="Share learning content"]` | | `.fab button[data-tip="Create poll"]` | `button[aria-label="Create poll"]` | Note the create-hub sheet carries the *same* `aria-label` as the trigger on a `div[role="menu"]`, so the `button` qualifier is load-bearing. **`edit button hidden for non-owner` asserted the wrong element.** It checked that the *trigger* was invisible. `EventContextMenu` renders for everyone, because it also carries Copy link / Copy event ID — only the author actions are conditional. Swapping the selector alone would have made this test fail on a correct build. The assertion moved to the Edit item, plus a positive control that the menu did open, so a count of 0 means "no Edit item" rather than "no menu". ## The event menu was orphaned, not renamed The detail page routes through `CalendarEventDetailView` → `DetailHeader` → `EventContextMenu`, trigger `aria-label="Event menu"`. `EventManagementActions` ("Manage event") **still exists** — but only inside `CalendarEventDetailsModal`, which the detail page never mounts. Item labels changed too: `EventContextMenu` uses `m.common_edit()` / `m.common_delete()` — **'Edit'** and **'Delete'**, not the old **'Delete Event'**. The *confirmation* dialog still reads 'Delete Event?' (`event_management_delete_confirm_title`), so that assertion stays. Items are now matched by label instead of `.first()`, which would silently click "Share to communities" for a non-owner. ## layout-consistency could not be selector-swapped either That test asserted the FAB sat in a `sticky bottom-0 h-0 mt-auto` wrapper inside `<main>`. `GlobalFAB` is mounted at the layout root (`+layout.svelte:379`, **outside `<main>`**) and positioned `fixed`, so `main .fab` and the wrapper-geometry assertions were both unreachable. Rewritten around the property the regression was actually about — on a page too short to scroll, the button is still at the bottom of the screen — plus explicit checks that it is outside `<main>` and viewport-fixed. **Related finding, deliberately not changed here:** that sticky `mt-auto h-0` wrapper still exists in `+layout.svelte:369`, but its only remaining child is `ScrollToTopButton`, which is *itself* `position: fixed`. So the whole `mt-auto`/sticky mechanism now has no observable effect. Removing it is a source change outside this issue's scope — worth a separate decision. ## Also corrected Two comments documenting removed behaviour (poll-flow's viewport rationale — the 9-button column that clipped no longer exists, the hub is a scrollable `max-h-[80vh]` sheet; and `MainContentArea`'s claim that GlobalFAB is anchored to `<main>`), and the `COVERAGE.md` row describing the old `data-tip` assertion. ## Verification At `2d94d16b`: all **316 tests across 38 e2e files** parse and register under `playwright test --list` (imports resolve, renamed test present), unit suite **5153/5153** across 468 files, `pnpm lint` clean. **The browser run against the Docker relay stack is not done** — that is TestOER's pass, per the testing split. A parse check proves the selectors are syntactically reachable, not that they match live DOM. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Six e2e files hard-coded the daisyUI speed-dial markup and all six broke
together when GlobalFAB replaced it. Routed through three shared helpers
in fixtures.js — FAB_TRIGGER, openCreateHub(), clickCreateAction() — so
the next redesign touches one place instead of six.

Two of the issue's four bullets needed a semantic change rather than a
selector swap:

- `button[data-tip="Create Event"]` was wrong twice over. `data-tip` now
  carries the action *description*, not its label, and is absent entirely
  for actions with no description — so the poll and learning-content
  tiles have no `data-tip` at all. Tiles are keyed on `aria-label` (the
  action's `ariaLabel` in create-actions.js): 'Create new event',
  'Share learning content', 'Create poll'.

- "edit button hidden for non-owner" asserted the *trigger* was
  invisible. EventContextMenu renders for everyone, because it also
  carries Copy link / Copy event ID; only the author actions are
  conditional. Swapping the selector alone would have failed on a correct
  build. The assertion moved to the Edit item, with a positive control
  that the menu did open — so a count of 0 means "no Edit item", not
  "no menu".

The event menu itself: the detail page routes through
CalendarEventDetailView -> DetailHeader -> EventContextMenu, whose
trigger is `aria-label="Event menu"`. EventManagementActions
("Manage event") still exists but only inside CalendarEventDetailsModal,
which the detail page never mounts — so the old selector was not renamed,
it was orphaned. The item labels changed too: EventContextMenu uses
m.common_edit()/m.common_delete() ('Edit'/'Delete'), not the old
'Delete Event'. The confirmation dialog still reads 'Delete Event?'.
Items are now matched by label instead of `.first()`, which would
silently click "Share to communities" for a non-owner.

layout-consistency's FAB test could not be fixed by selector swap either:
it asserted the FAB sat in a `sticky bottom-0 h-0 mt-auto` wrapper inside
<main>. GlobalFAB is mounted at the layout root, outside <main>, and
positioned `fixed`, so both `main .fab` and the wrapper-geometry
assertions were unreachable. Rewritten around the property the regression
was actually about — on a page too short to scroll, the button is still at
the bottom of the screen — plus explicit checks that it is outside <main>
and viewport-fixed.

Also corrected two comments that documented removed behaviour (poll-flow's
viewport rationale, MainContentArea's claim that GlobalFAB is anchored to
<main>) and the COVERAGE.md row describing the old data-tip assertion.

Verified: all 316 tests across 38 e2e files parse and register with
`playwright test --list`, unit suite 5153/5153 (468 files), lint clean.
The browser run against the Docker relay stack is TestOER's pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both from TestOER's browser run of the previous commit — 13 of 20 failed,
all through one root cause.

`GlobalFAB`'s "Suggested" section maps route-suggested ids back onto the
*same* action objects the "Create" section lists (`suggestedActions` looks
each id up in `visibleActions`), so on `/calendar` the identical
'Create new event' button renders twice inside the sheet. The `button`
qualifier added last commit correctly separated the trigger from the
sheet, but the remaining ambiguity is inside the sheet and is
route-dependent — which is why `playwright test --list` could not see it.

`.first()` is safe here for a reason worth writing down, because the same
operator was deliberately *removed* from the event context menu in the
previous commit: there the matches were genuinely different actions and
taking the first silently clicked "Share to communities". Here both
matches carry the same aria-label and both call `runAction` on the same
object, so either click is equivalent. The comment says so at the call
site.

The description assertion is now scoped to the description card instead
of a page-wide getByText. The summary renders through MarkdownRenderer,
so the wrapper and the paragraph it produces both contain the text and
the bare matcher resolved to two elements. Anchoring on the card also
makes the assertion state what it means — the summary is in the
description card, not merely somewhere on the page.

Not fixed here: three update-flow tests fail on content rather than
locators. Out of scope for a selector issue, and filed separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Left failing rather than skipped, and now says so at the call site. A
`test.skip` would make the suite green while hiding a bug that nobody had
seen until #39 fixed the selectors in front of it, and the next person to
run these should not have to re-derive that they are not selector bugs.

Comments only — no assertion or locator changed. 316 tests still register.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The port was a constant while `reuseExistingServer` is true outside CI, so a
preview server another worktree left running on 14173 is *reused* rather than
replaced — the run then exercises that worktree's build, with no error and no
warning, and every result is attributed to the wrong tree.

That is not hypothetical: 14173 is currently held by a `vite preview` from
.worktrees/testoer-pr52, and two concurrent e2e runs in this repo had no way
to isolate from each other without hand-editing the config.

Default is unchanged (14173), so existing invocations behave identically.
Verified both: unset -> 14173, E2E_PORT=14999 -> 14999 in baseURL, webServer
port and ORIGIN; 316 tests still register.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
laoc merged commit 3a4b9e8c9e into dev 2026-07-30 12:08:15 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
edufeed/edufeed-app!61
No description provided.