Skip to content

fix(e2e): restore 12 broken e2e tests + add CI e2e job - #81

Merged
ErickXavier merged 5 commits into
mainfrom
fix/80-e2e-restoration
Aug 5, 2026
Merged

fix(e2e): restore 12 broken e2e tests + add CI e2e job#81
ErickXavier merged 5 commits into
mainfrom
fix/80-e2e-restoration

Conversation

@ErickXavier

Copy link
Copy Markdown
Collaborator

Summary

Refs #80

Restores the full e2e suite to green (435/435 across chromium, firefox, webkit) by addressing all 14 pre-existing failures documented in the root-cause analysis:

@dev commits (already on branch):

  • Popover light-dismiss bug (1 real product bug): fixed synchronous positioning + inline visibility guard in src/popover/popover.js to prevent the 1-frame misposition that swallowed outside clicks
  • Table fixture migration (7 tests): migrated docs/examples/table/index.html from removed sibling <tr else> pattern to else="templateId" + <template>
  • DnD lifecycle fixture (2 tests): renamed event listeners in docs/examples/dnd/index.html from on:receive/on:remove to on:nojs:dnd-receive/on:nojs:dnd-remove to match the BREAKING rename in commit 1b109ce

@qa commits (this PR):

  • DnD ARIA assertions (3 tests x3 browsers = 9 failures): rewrote tests asserting deprecated aria-grabbed to assert the current a11y contract — aria-roledescription="draggable item" on draggable items, nojs-dragging class toggle on Space/Escape keyboard drag, and live-region announcements ("Grabbed ... Use arrow keys to move/reorder." / "Drag/Reorder cancelled.")
  • Popover ARIA assertion (1 test x3 browsers = 3 failures): updated aria-haspopup expectation from "true" to "dialog" to match popover.js implementation
  • CI e2e job: added a new e2e job to .github/workflows/ci.yml that clones + builds NoJS Core, builds Elements, installs Playwright chromium, and runs npx playwright test --project=chromium — closing the 7-week CI gap that let stale fixtures and tests rot undetected

Test results

Run Passed Failed Browsers
Full suite 435 0 chromium, firefox, webkit
Stability (repeat-each=2) 870 0 chromium, firefox, webkit
Unit tests 498 0 jsdom

CI design choice

The e2e job runs chromium-only in CI to keep runner time reasonable (the full 3-browser suite takes ~45s locally, chromium-only takes ~15s). Chromium covers the rendering engine used by >70% of users; firefox and webkit are validated locally before merge. This matches the existing workers: 1 CI config in playwright.config.ts which already trades speed for determinism.

Stale artifacts

test-results/ and playwright-report/ directories are already in .gitignore and are NOT tracked in git (verified via git ls-files). No cleanup needed.

… fixtures

Popover: position synchronously on open instead of deferring to rAF, and
keep the popover visibility:hidden via CSS until positioned. This prevents
the popover from appearing at viewport (0,0) for one frame, which broke
light-dismiss under load. Added _isPopoverOpen() helper to safely handle
environments that don't support :popover-open (jsdom).

Table fixture: migrate from removed sibling <tr else> pattern to
else="templateId" on the each element with <template> references, per
Core v1.15 breaking change.

DnD fixture: rename on:reorder/on:remove/on:receive to
on:nojs:dnd-reorder/on:nojs:dnd-remove/on:nojs:dnd-receive per the
post-1b109ce event rename.
The CSS-class approach (.nojs-popover { visibility: hidden } +
.nojs-popover--positioned) broke dropdowns: the dropdown directive sets
popover="auto" on its menu, which causes the NoJS framework to also run
the popover directive and add the nojs-popover class -- making the menu
permanently invisible since the dropdown open path never adds the
--positioned class.

Fix: remove the CSS visibility rule entirely and use inline
style.visibility on the specific popover element being opened. Set
visibility:hidden before togglePopover()/showPopover(), position
synchronously, then clear the inline style. This scopes the guard to
only elements explicitly opened through the popover code path, with
zero impact on dropdown or other components that share the popover
attribute.

Proof: dropdown.spec.ts passes 24/24 on main, fails 15 with the CSS
approach, passes 24/24 with this inline approach.
- dnd.spec.ts: replace deprecated aria-grabbed assertions with current
  a11y contract (aria-roledescription, nojs-dragging class, live-region
  announcements for grab and cancel)
- popover.spec.ts: update aria-haspopup expectation from "true" to
  "dialog" to match popover.js implementation
- ci.yml: add e2e job running Playwright chromium against built Core +
  Elements, closing the 7-week CI gap that let these tests rot

@ErickXavier ErickXavier left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

QA Review — PR #81

Popover Fix Correctness

Verified all three open paths for the visibility hidden/position/clear sequence:

1. Click handler (popover-trigger, line 220-239): Sets style.visibility = "hidden", calls togglePopover(), then either positions+clears (if open) or just clears (if the toggle closed it). Synchronous positioning, no rAF in the open path.

2. popoverApi.open (line 287-301): Sets style.visibility = "hidden", wraps showPopover() in try/catch — on exception clears visibility and returns false. On success, positions synchronously + clears. All branches clear.

3. popoverApi.toggle (line 311-326): Same pattern as click handler — sets hidden, calls togglePopover(), branches on _isPopoverOpen, clears in both branches.

Safety net: The toggle event handler (line 163) clears el.style.visibility = "" on close, providing a secondary cleanup path.

should — inconsistent exception handling: popoverApi.open wraps showPopover() in try/catch (line 291) but the click handler and popoverApi.toggle call togglePopover() without try/catch. If togglePopover() throws after _supportsPopover() passes (theoretically possible per spec for InvalidStateError), style.visibility = "hidden" would stick and the popover would be permanently invisible. Low probability since _supportsPopover() already validates the method exists, but the inconsistency is easy to fix — wrap the two bare togglePopover() calls the same way showPopover() is wrapped in popoverApi.open. No urgency, but worth a follow-up.

Dropdown interaction: Confirmed safe. The dropdown directive (dropdown.js:164) sets popover="auto" on its menu and calls showPopover()/hidePopover() directly — it never goes through the popover-trigger click handler or the programmatic API. The inline style.visibility is only set in those explicit popover code paths, so dropdown menus are never affected. The commit message documents the CSS-class approach failure (15 of 24 dropdown tests broke) and the inline approach passing 24/24.

Test Assertion Accuracy

Live-region regexes vs source strings:

Test regex Source string (element.js) Match?
/Grabbed .+\. Use arrow keys to (?:move|reorder)\./ Grabbed ${label}. Use arrow keys to move. (line 345/430) Yes
same Grabbed ${label}. Use arrow keys to reorder. (line 787/831) Yes
/(?:Drag|Reorder) cancelled\./ Drag cancelled. (line 440) Yes
same Reorder cancelled. (line 841) Yes

Regexes are specific enough — they won't false-positive on unrelated text. The (?:move|reorder) alternation correctly covers both drag-item and drag-list contexts.

Regression detection: If the a11y contract regresses (class not added, attribute removed, announcement text changed, live-region element removed), the tests will fail. Verified each assertion path.

Popover ARIA: aria-haspopup assertion changed from "true" to "dialog" — matches popover.js line 211 which sets "dialog".

Fixtures

Table: else="noUsersTpl" + <template id="noUsersTpl"> matches Core v1.15 else="templateId" semantics.

DnD events: on:nojs:dnd-receive, on:nojs:dnd-remove, on:nojs:dnd-reorder match the CustomEvent names dispatched in element.js (lines 1042, 1033, 1014).

CI YAML

  • Triggers: push: [main] + pull_request: [main] — runs on PRs targeting main. Correct.
  • Core clone: git clone --depth 1 from main (unpinned). This matches the existing build-and-test job pattern. Unpinned main means Core breakage can red-light Elements CI — acceptable since they're tightly coupled.
  • Chromium-only: PR body documents the rationale and states 3-browser validation is local-only. Acceptable trade-off.
  • No Playwright browser caching: Each run downloads chromium. Works but adds ~10s. Not a blocker.

CI Check Status

The e2e job failed (12s runtime). Root cause: npm ci on NoJS Core failed with Missing: conventional-commits-parser@6.4.0 from lock file. This is a lockfile sync issue in the Core repo (likely from the merged commitlint bump in PR #299), not caused by this PR. The build-and-test job passed (28s). A CI re-run after Core's lockfile is fixed should pass.

Hygiene

  • dist/ committed: All three formats (cjs, esm, iife) contain the visibility hidden pattern and _isPopoverOpen helper. Consistent with source.
  • No attribution trailers: Verified via --json commits — clean.
  • Commit messages: Use branch-name prefix (fix/80-e2e-restoration:) rather than standard conventional format. The PR title itself (fix(e2e): ...) is conventional. Nit — since the project uses merge commits (not squash), individual commit format is visible in history.

Not Verified

Did not rerun popover.spec.ts or dropdown.spec.ts in a scratch clone. The code-level analysis of the inline-style approach is thorough and the approach is sound — the dropdown code path never sets inline visibility.

Verdict

Approve. The popover fix is correct across all three open paths, the inline style approach is properly scoped to avoid dropdown interference, test assertions match the shipped a11y contract, fixtures use the current Core semantics, and dist/ is consistent. The CI e2e failure is an upstream Core lockfile issue — re-run after that is fixed.

One follow-up item: wrap the two bare togglePopover() calls in try/catch for exception safety parity with popoverApi.open.

togglePopover()/showPopover() can throw, and _positionPopover() can
throw after a successful open (e.g. getBoundingClientRect on a detached
anchor). Either would leave style.visibility='hidden' stuck on an open
popover. Wrap the hide-open-position-clear sequence in try/finally in
all three open paths (click handler, popoverApi.open, popoverApi.toggle)
so the inline visibility is cleared unconditionally.
Core's package-lock.json was regenerated under npm 11 (node 26) during the
v1.20.1 release. npm 10 (node 20) rejects it with missing dependency errors
and EBADENGINE warnings from @commitlint/cli@21 (requires node >=22.12).
@ErickXavier
ErickXavier merged commit b193055 into main Aug 5, 2026
3 of 4 checks passed
@ErickXavier
ErickXavier deleted the fix/80-e2e-restoration branch August 5, 2026 02:07
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.

1 participant