Skip to content

test(e2e): cover unified DashboardContainer (collapse, border, tabs, drag)#2205

Open
alex-fedotyev wants to merge 6 commits into
mainfrom
alex/e2e-dashboard-container
Open

test(e2e): cover unified DashboardContainer (collapse, border, tabs, drag)#2205
alex-fedotyev wants to merge 6 commits into
mainfrom
alex/e2e-dashboard-container

Conversation

@alex-fedotyev
Copy link
Copy Markdown
Contributor

@alex-fedotyev alex-fedotyev commented May 6, 2026

Summary

Follow-up E2E coverage for PR #2015 (the unified DashboardContainer
that replaced the legacy section/group concept). Drew explicitly asked
for this in his top-level review on #2015 ("Can you confirm we have
followup issues/tickets covering... 2. New E2E tests covering the new
functionality"). Unit coverage landed in #2015 itself; this PR adds
the missing browser-level coverage.

The container UX has four moving pieces, all exercised here:
collapsible (chevron + URL state), bordered (overflow-menu toggle +
inline border style), tabs (tab bar appearance, tab switching, URL
state), and drag-to-reorder via @dnd-kit. Each test step cites the
source line that defines the behavior so a reviewer can double-check
the assertion matches the implementation.

Test cases

  1. Group renders with default collapsible chevron and bordered style;
    chevron toggles aria-expanded.
  2. Toggling Hide Border / Show Border via the overflow menu flips the
    inline border style and the menu label.
  3. Adding a tab brings the tab bar (1-tab groups don't render it),
    switching tabs updates ?activeTabs and aria-selected.
  4. ?collapsed and ?expanded URL params survive reload and restore
    per-viewer state.
  5. ?activeTabs URL param survives reload and restores the active
    tab.
  6. Save-and-reload round-trip preserves the containers list and the
    second tab on group B.
  7. Drag-to-reorder: drag-onto-self is a no-op (the DnD guard at
    DashboardDndContext.tsx:67-70); dragging A onto C in [A, B, C]
    yields [B, C, A] (arrayMove semantics) and the new order
    persists across navigation.

What changed since the first push

Side fixes pulled in

  • DashboardPage.ts page-object selector was still pointing at the
    stale add-new-section-menu-item testid; PR refactor: Unify section/group into single Group with collapsible/bordered options #2015 renamed it to
    add-new-group-menu-item. No existing spec exercised addSection,
    so this hadn't surfaced. Renamed the locator and the helper from
    addSection -> addGroup.
  • DashboardContainer.tsx adds data-testid="group-add-tab-${id}"
    on the existing Add Tab menu item so the spec doesn't have to
    match Mantine menu text. One-line non-behavior change.

Tier

Predicted Tier 2 by the local classifier: 1 production file,
10 production lines (the testid addition), no API/router/model
touch, single-layer (packages/app/). Test files are excluded from
the tier calculation per the classifier rules.

Out of scope

  • Concurrent-mutation back-pressure on setDashboard (PATCH clobber).
    Tracked in Dashboard: setDashboard mutations clobber each other when fired back-to-back #2216.
  • Multi-user URL state isolation (covered by
    dashboardSections.test.tsx unit tests).
  • Alert-dot indicators (covered by DashboardContainer.test.tsx).
  • Legacy type: "section" migration (one-time data shape; covered
    by dashboardSections.test.tsx).
  • Tile drag-reorder (uses react-grid-layout, not the new
    @dnd-kit container DnD).

Test plan

  • yarn lint clean.
  • yarn tsc --noEmit clean.
  • npx playwright test --list tests/e2e/features/dashboard-container.spec.ts lists all 7 cases.
  • prose-lint clean against origin/main.
  • Local tier prediction: Tier 2.
  • CI: full E2E shard run on this branch.

Refs PR #2015. Follow-up race tracked in #2216.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 6, 2026

⚠️ No Changeset found

Latest commit: e2a7c8e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment May 19, 2026 7:48pm

Request Review

@github-actions github-actions Bot added the review/tier-2 Low risk — AI review + quick human skim label May 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🔵 Tier 2 — Low Risk

Small, isolated change with no API route or data model modifications.

Why this tier:

  • Standard feature/fix — introduces new logic or modifies core functionality

Review process: AI review + quick human skim (target: 5–15 min). Reviewer validates AI assessment and checks for domain-specific concerns.
SLA: Resolve within 4 business hours.

Stats
  • Production files changed: 1
  • Production lines changed: 11 (+ 601 in test files, excluded from tier calculation)
  • Branch: alex/e2e-dashboard-container
  • Author: alex-fedotyev

To override this classification, remove the review/tier-2 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

PR Review

✅ No critical issues found.

Notes (non-blocking):

  • ⚠️ DashboardPage.ts is now 1088 lines; AGENTS.md asks for files under 300. Pre-existing growth, but consider splitting into a dedicated DashboardContainerActions mixin in a follow-up.
  • ℹ️ getCollapsedParam/getExpandedParam rely on container ids never containing , (true for nanoid). Fine for now — worth a one-line comment if ids ever change format.
  • ℹ️ Production-code surface is intentionally minimal: a data-bordered attribute (DashboardContainer.tsx:244) and a data-testid on the Add Tab menu item (DashboardContainer.tsx:168). Both are pure additive testability hooks, no behavior change.
  • ℹ️ waitForDashboardPatch() correctly registered before mutation (avoids the post-hoc waitForResponse race). Good pattern to keep reusing.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

E2E Test Results

All tests passed • 186 passed • 3 skipped • 1216s

Status Count
✅ Passed 186
❌ Failed 0
⚠️ Flaky 3
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Compound Engineering Review

PR #2205test(e2e): cover unified DashboardContainer (collapse, border, tabs, drag)

Scope: 1 production file (testid + data-bordered attr only), 1 new E2E spec, 1 page-object refactor (addSectionaddGroup rename + ~250 lines of helpers). No security, performance, or migration surface — review focused on race conditions, type discipline, simplicity, and pattern consistency.

P1 — Should fix before merge

  • packages/app/tests/e2e/page-objects/DashboardPage.ts:298-306waitForDashboardPatch predicate matches any PATCH against /api/dashboards/. In the round-trip and drag-reorder tests, the listener is registered after 2–3 prior addGroupAndGetId calls that each emit fire-and-forget PATCHes. A still-in-flight prior PATCH can resolve the listener instead of the targeted one, then the spec navigates while the targeted PATCH is still in flight → the change being tested is dropped. Fix: register listeners before the prior addGroupAndGetId calls (or Promise.all count them), OR tighten the predicate on request body / dashboard id.
  • packages/app/tests/e2e/page-objects/DashboardPage.ts:432-468dragGroupTo recomputes target.boundingBox() on the inner [data-testid="group-container-${id}"] Box, but @dnd-kit's 250ms translate3d transform is applied to the parent SortableContainerWrapper (DashboardDndComponents.tsx:69-78). The recompute returns the mid-animation projected rect, so on fast machines the pointer can land on a neighbour. Fix: add a testid to the wrapper and sample from there, or wait for the dnd-kit transition to settle before sampling.
  • packages/app/tests/e2e/features/dashboard-container.spec.ts:124-132, 204-220secondTabId is captured via a synchronous second read of getActiveTabsParam()[id] immediately after an expect.poll. The poll observes the value but the capture races any subsequent nuqs flush. Currently safe by accident; one new mutation step turns this into a stale-id bug with a horrendous diff. Fix: capture the id inside the poll callback.
  • packages/app/src/components/DashboardContainer.tsx:38, 283// [prose-lint: allow] markers consume nothing — grep -rn "prose-lint" finds zero hits outside this file (no plugin, no Vale config, no CI step). They're inert pragmas masquerading as load-bearing suppressions. Fix: delete the markers and replace the em-dashes with commas (DashboardPage.ts:871,885 already does exactly this in the same PR), or relax the prose-lint rule for source files at the config level.
  • packages/app/tests/e2e/page-objects/DashboardPage.ts:343-380getActiveTabsParam cargo-cults the production parseAsJsonEncoded parser: double-tryParse, raw fallback, runtime non-array-object validation, per-key string filter. The test owns both ends of this contract; the second tryParse(raw) fallback (line 358) is unreachable because decodeURIComponent on valid nuqs output never throws. Fix: collapse to try { return JSON.parse(decodeURIComponent(raw)) } catch { return {} } (~30 LOC saved).
  • packages/app/src/components/DashboardContainer.tsx:244data-bordered={bordered ? 'true' : 'false'} always-rendered convention diverges from the established repo pattern (SQLInlineEditor.tsx:305, AutocompleteInput.tsx:144) which uses data-expanded={x ? 'true' : undefined} (presence/absence). Pick one convention. Also: this attr is purely test-only — consider data-test-bordered to signal intent and prevent accidental coupling.
  • packages/app/tests/e2e/features/dashboard-container.spec.ts (25 step labels) — All 25 new test.step('lowercase…', …) labels break the repo's Sentence-case convention (469 prior Sentence-case steps in features/, only 2 lowercase outliers, both in dashboard.spec.ts). Fix: capitalize the first letter of each step description.

P2 — Should fix

  • packages/app/tests/e2e/features/dashboard-container.spec.ts:171-177, 186-192 — Reload paths read getCollapsedParam() synchronously after waitForLoaded(), which only checks the page-shell testid. Currently safe because the param was written before reload, but if nuqs ever does a mount-time URL reconciliation pass this becomes a flake. Wrap reads in expect.poll for symmetry with the activeTabs reload assertion at line 232.
  • packages/app/tests/e2e/page-objects/DashboardPage.ts (lines 95-114, 498, 502, 518, 539, 558, 571) — Locator-style mix: 32 getByTestId vs 3 page.locator('[data-testid="…"]'). New code uses the modern style; pre-existing constructor lines are the inconsistent half. Worth a follow-up cleanup PR while the file is in mind.
  • packages/app/tests/e2e/page-objects/DashboardPage.ts:240-249getGroupOrder filters empty ids with a 3-line justifying comment, but [data-testid^="group-container-"] already guarantees the prefix and container.id is always non-empty. Defending an unreachable state. Drop the filter.

P3 — Nice to have

  • packages/app/tests/e2e/page-objects/DashboardPage.ts:421-422 — Double mouse.move(startX, startY) to identical coords does not "stabilise pointerdown coalescing" — same-coord moves dispatch no event. Magic incantation; remove one or move from a different start.
  • Comment noise — Multiple new helpers (getGroupChevron, getGroupBorderedToggle, getGroupTabs, toggleGroupBordered, addTabToGroup) have JSDoc that just restates the function name. Either drop or add the WHY.
  • getGroupBorderedAttr (DashboardPage.ts:287-289) — Returns Promise<string|null> from a non-async function while sibling helpers are async. Pick one convention across the new block.

Files reviewed: packages/app/src/components/DashboardContainer.tsx, packages/app/tests/e2e/features/dashboard-container.spec.ts, packages/app/tests/e2e/page-objects/DashboardPage.ts (with DashboardDndContext.tsx, DashboardDndComponents.tsx, queryParsers.ts, useDashboardContainers.tsx consulted for context).

alex-fedotyev pushed a commit that referenced this pull request May 6, 2026
…races

Compound-review feedback on #2205:

- Replace `waitForLoadState('networkidle')` (which hangs on dashboards
  that keep polling tile queries) with a backend-PATCH waitForResponse,
  registered before the mutation so a fast handler doesn't return before
  the listener is attached.
- Tag the round-trip and drag-persists tests `@full-stack` so the
  backend-bearing CI lane picks them up.
- Replace inline `url().match(/\/dashboards\/.../)` with
  `dashboardPage.getCurrentDashboardId()` (existing helper) so a missing
  id throws instead of silently passing `undefined` to `goto()`.
- `getActiveTabsParam` now validates `JSON.parse` output is a non-array
  object and only accepts string-typed values, instead of casting `any`
  to `Record<string, string>`.
- `openGroupMenu` waits for `pointer-events: none` to clear before
  clicking the overflow trigger; auto-wait checks visibility but not
  pointer-events.
- `dragGroupTo` recomputes the target `boundingBox()` after the drag
  activates (otherwise @dnd-kit's 250ms sortable-item transform leaves
  the box stale) and shrinks the activation nudge to 10px so the cursor
  stays inside the handle.
- Multi-param state reads (`?collapsed` + `?expanded`) are wrapped in a
  single `expect.poll` tuple so the slow side can't be missed by a
  one-shot read after the fast side resolved.
- New page-object helpers (`getGroupChevron`, `getGroupBorderedToggle`,
  `getGroupTabs`, `getGroupBorderedAttr`, `waitForLoaded`,
  `waitForDashboardPatch`) keep testid prefixes out of specs.
- `data-bordered="true|false"` attribute on the container shell so the
  bordered toggle test reads an attr instead of inspecting inline
  `style.border`.
- `getGroupOrder` filters empty entries to surface DOM regressions.
- `addGroupMenuItem` switched to `getByTestId(...)` for consistency with
  sibling locators.
- Reverted unrelated em-dash → comma comment edits in
  `DashboardContainer.tsx` to keep the PR scope clean (em-dash sweep
  belongs in its own PR).
@alex-fedotyev
Copy link
Copy Markdown
Contributor Author

P1 flake risks and P2 cleanups addressed in 7796c3a:

  • PATCH wait via waitForResponse(...PATCH), registered before the mutation
  • dashboardPage.getCurrentDashboardId() (existing helper) instead of inline regex
  • @full-stack tag on the two persistence tests
  • getActiveTabsParam validates JSON.parse shape and value type
  • openGroupMenu waits for pointer-events: none to clear
  • dragGroupTo recomputes target box after activation, activation nudge shrunk to 10px
  • ?collapsed/?expanded reads polled as a tuple
  • New page-object getters: getGroupChevron, getGroupBorderedToggle, getGroupTabs, getGroupBorderedAttr, waitForLoaded, waitForDashboardPatch
  • data-bordered attribute on the container shell, asserted via attr instead of inline style.border
  • Reverted unrelated em-dash to comma comment edits in DashboardContainer.tsx (em-dash sweep belongs in its own PR)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

Deep Review

✅ No critical issues found.

The diff is one production line (a data-testid on the existing Add Tab menu item) plus ~600 lines of E2E test code and page-object helpers. Production behavior is unchanged. Test scaffolding is well-documented, the polling patterns correctly handle nuqs's asynchronous URL flush, and the renamed add-new-group-menu-item testid is verified present in packages/app/src/DBDashboardPage.tsx:2626.

🟡 P2 -- recommended

  • packages/app/tests/e2e/features/dashboard-container.spec.ts:282 -- the round-trip test asserts group order and that group B retains two tabs, but never asserts group A still has its original single-tab shape, so an accidental cross-contamination of tab state across containers would not be caught.

    • Fix: Add a step that reads getGroupTabs(idA) after reload and asserts the count is 0, mirroring the assertion for idB.
  • packages/app/tests/e2e/page-objects/DashboardPage.ts:381 -- getActiveTabsParam reimplements the production parseAsJsonEncoded decoding (double-decode plus single-decode fallback); if the serializer changes in packages/app/src/utils/queryParsers.ts the E2E parser silently diverges and tests pass against stale assumptions.

    • Fix: Either narrow the helper to read only one encoding level and rely on a smoke assertion against a known fixture, or add a brief comment cross-linking to the production parser so future edits are kept in sync.
🔵 P3 nitpicks (3)
  • packages/app/tests/e2e/page-objects/DashboardPage.ts:457 -- dragGroupTo recomputes the target bounding box after activation but not the source handle box; on a layout with sticky headers or zoom the original startX/startY could drift before mouse.down.

    • Fix: Recompute the handle box immediately before mouse.move(startX, startY) (or await handle.hover() first) to defend against pre-mousedown shifts.
  • packages/app/tests/e2e/page-objects/DashboardPage.ts:287 -- waitForLoaded only checks data-testid="dashboard-page" visibility, which renders before remote containers hydrate; individual specs rely on subsequent polls to compensate.

    • Fix: Add a secondary wait inside waitForLoaded (for example, until at least one [data-testid^="group-container-"] is attached or a known loading indicator clears) so every caller benefits from the same readiness guarantee.
  • packages/app/tests/e2e/features/dashboard-container.spec.ts:127 -- the non-null assertion on getActiveTabsParam()[id]! reads safely because expect.poll just confirmed truthiness, but a future edit that drops the preceding poll would silently capture undefined as a tab id without a TypeScript error.

    • Fix: Replace the non-null assertion with an explicit if (!secondTabId) throw new Error(...) guard so the precondition is part of the runtime contract, not the type assertion.

Reviewers (8): correctness, testing, maintainability, project-standards, kieran-typescript, julik-frontend-races, ce-agent-native-reviewer, ce-learnings-researcher.

Testing gaps:

  • Group rename via the inline TextInput flow is not covered by any E2E case (out of scope per PR description; unit-tested in DashboardContainer.test.tsx).
  • Delete-group modal interactions (Ungroup Tiles vs Delete Group & Tiles) have no E2E coverage; unit coverage exists.
  • Collapsible toggle via the overflow menu (Disable Collapse / Enable Collapse) is not exercised; only the chevron path is.

alex-fedotyev and others added 4 commits May 8, 2026 00:20
…drag)

PR #2015 unified the legacy section/group concept into a single
DashboardContainer with collapsible / bordered / tabs / drag-to-reorder
plus URL-state persistence. Unit tests landed with #2015; this PR adds
the missing E2E coverage that Drew called out in his top-level review.

What this covers:

- Default collapsible chevron + bordered style on a new group.
- Toggling Hide Border / Show Border via the overflow menu.
- Adding a tab so the tab bar appears, switching tabs updates
  ?activeTabs and aria-selected.
- ?collapsed and ?expanded URL params survive reload.
- ?activeTabs URL param survives reload and restores the active tab.
- Save-and-reload round-trip preserves containers, bordered=false, and
  the second tab on group B.
- Drag-to-reorder: drag-onto-self is a no-op (DnD guard); dragging A
  onto C in [A, B, C] yields [B, C, A] (arrayMove semantics) and the
  new order persists across navigation.

Side fixes:

- DashboardPage.ts page-object selector updated from the stale
  add-new-section-menu-item to add-new-group-menu-item (renamed by PR
  #2015 in DBDashboardPage.tsx); helper renamed addSection -> addGroup.
- DashboardContainer.tsx adds data-testid="group-add-tab-\${id}" on
  the existing Add Tab menu item so the spec does not have to rely on
  Mantine menu text.

No API or schema change. Test-only plus one data-testid line.

Tracker: Nerve task 2026-05-04-hyperdxiohyperdx-e2e-tests-for-unifie

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The three failing dashboard-container tests in CI shard 1 came down to
two distinct races:

1. Tests #3 (line 112) and #5 (line 194) read getActiveTabsParam()[id]
   synchronously after Add Tab and after tab switches. nuqs flushes URL
   state asynchronously, so the read fires before the param is written.
   Wrap each sync read in expect.poll, mirroring the fix in PR #2209.

2. Test #6 (line 231) paired toggleGroupBordered(idA) with
   addTabToGroup(idB) back-to-back. Both setDashboard calls produce()
   from the same pre-mutation snapshot of the React Query cache, so
   the second PATCH overwrites the first; the toggle is silently
   dropped. The save-and-reload assertion then sees the wrong state
   (or, when goto fires before the PATCH lands, no state at all).

   Narrow the round-trip to a single mutation per step (addTabToGroup
   on group B), wait for networkidle before navigating away, capture
   the dashboard id from the URL while we are still on the page, and
   use expect.poll for the post-reload getGroupOrder assertion. The
   bordered toggle stays covered by the dedicated in-page test at
   line 78. The underlying back-pressure race is tracked separately
   in #2216.
The dashboard activeTabs URL state goes through parseAsJsonEncoded
(packages/app/src/utils/queryParsers.ts), which intentionally
double-encodes the JSON to survive Microsoft Teams' '+' -> '%2B'
re-encoding. The serializer writes encodeURIComponent(JSON.stringify(...))
and nuqs's URL machinery then encodes the '%XX' sequences a second
time. URLSearchParams.get(...) decodes one level, so the helper has
to decode the second level itself before JSON.parse.

The previous helper read the param, called JSON.parse on a still-
URL-encoded string ('%7B%22..."), threw, and returned {} silently.
Tests #112 and #197 polled getActiveTabsParam()[id] for 5s, got
undefined every time, and timed out on toBeTruthy.

Fix: decodeURIComponent before JSON.parse. Keep a fallback to plain
JSON.parse for compatibility with the old single-encoded format,
mirroring the parser's own backward-compat path.

The implementation (handleTabChange / setUrlActiveTabs in DBDashboardPage)
was correct all along; only the test-side reader needed updating.
…races

Compound-review feedback on #2205:

- Replace `waitForLoadState('networkidle')` (which hangs on dashboards
  that keep polling tile queries) with a backend-PATCH waitForResponse,
  registered before the mutation so a fast handler doesn't return before
  the listener is attached.
- Tag the round-trip and drag-persists tests `@full-stack` so the
  backend-bearing CI lane picks them up.
- Replace inline `url().match(/\/dashboards\/.../)` with
  `dashboardPage.getCurrentDashboardId()` (existing helper) so a missing
  id throws instead of silently passing `undefined` to `goto()`.
- `getActiveTabsParam` now validates `JSON.parse` output is a non-array
  object and only accepts string-typed values, instead of casting `any`
  to `Record<string, string>`.
- `openGroupMenu` waits for `pointer-events: none` to clear before
  clicking the overflow trigger; auto-wait checks visibility but not
  pointer-events.
- `dragGroupTo` recomputes the target `boundingBox()` after the drag
  activates (otherwise @dnd-kit's 250ms sortable-item transform leaves
  the box stale) and shrinks the activation nudge to 10px so the cursor
  stays inside the handle.
- Multi-param state reads (`?collapsed` + `?expanded`) are wrapped in a
  single `expect.poll` tuple so the slow side can't be missed by a
  one-shot read after the fast side resolved.
- New page-object helpers (`getGroupChevron`, `getGroupBorderedToggle`,
  `getGroupTabs`, `getGroupBorderedAttr`, `waitForLoaded`,
  `waitForDashboardPatch`) keep testid prefixes out of specs.
- `data-bordered="true|false"` attribute on the container shell so the
  bordered toggle test reads an attr instead of inspecting inline
  `style.border`.
- `getGroupOrder` filters empty entries to surface DOM regressions.
- `addGroupMenuItem` switched to `getByTestId(...)` for consistency with
  sibling locators.
- Reverted unrelated em-dash → comma comment edits in
  `DashboardContainer.tsx` to keep the PR scope clean (em-dash sweep
  belongs in its own PR).
@alex-fedotyev alex-fedotyev requested a review from a team May 19, 2026 02:13
@alex-fedotyev alex-fedotyev requested review from bot-hyperdx and removed request for a team May 19, 2026 02:13
@alex-fedotyev alex-fedotyev requested review from a team and karl-power and removed request for a team and bot-hyperdx May 19, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-2 Low risk — AI review + quick human skim

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants