Dashboards Fase 2 — UX / Performance / A11y Smoke Test
Manual verification that the Fase 2 production-grade surface (2.A–2.F) holds end to end. Run in dev with at least one dashboard that has 10+ widgets and one widget bound to a table with >1 000 rows.
Keep Chrome DevTools open: Network, Console, React DevTools Profiler (extension), and the device emulation toolbar. Axe DevTools extension is recommended for a11y.
All nine cases must pass before closing Fase 2.
1 — Design tokens: dark/light swap clean (2.A)
Setup: open /dashboards. Toggle dark mode.
Expected:
- No flash of unthemed content — all surfaces swap within one frame.
- Accent blue (
--ws-accent-primary) consistent across: hero search focus ring, dashboard card hover border, save button, widget slot selected border, filter pill active, loading spinner. - Success green (
--ws-accent-success): "published" badge, Saved state, Publish button. - Error red (
--ws-accent-error): save error banner, query error placeholder, WidgetErrorBoundary fallback. - Chart widget with 5+ series: each series picks from
--ws-chart-1..15. In dark mode, series colours stay legible (no blown-out reds / too-dim blues). - Inspect any widget via DevTools →
styleattribute referencesvar(--ws-*), not#hex.
2 — Skeletons + empty/error primitives (2.A)
Setup: throttle network to "Slow 3G" in DevTools. Open a dashboard.
Expected:
- Widget bodies show chart-shaped skeletons (animated shimmer) while loading, not blank boxes.
- KPI widgets show KPI-shaped skeletons (big number + label + delta shimmer).
- Table widget shows header + rows skeleton.
- Open data sidebar on a dashboard with no sources attached →
EmptyStatewith icon + "No sources yet" + primary CTA "+ Add data source". - Click a dataset that fails to query → widget renders the
ErrorState(or Placeholder) with coherent error copy, not a crash.
3 — Re-render isolation (2.B)
Setup: open a dashboard with 15+ widgets. Open React DevTools Profiler. Click "Record" → single-click on one widget → stop recording.
Expected:
- Profiler shows ≤ 3 components with commit:
- The touched
WidgetSlot - Its parent (re-renders one level up are normal)
WidgetConfigPanelif open
- The touched
- The other 14+
WidgetSlots show as "Did not render" or skipped. - Hover a
WidgetSlotthat didn't render → tooltip: "This component was not affected by this update".
4 — Virtual scroll in TableViz (2.B)
Setup: table widget bound to a dataset with ≥ 1 000 rows (or mock a large dataset). Widget in view.
Expected:
- Inspect DOM inside the widget: only ~15–25
div[role="row"]elements present at any moment, not 1 000+. - Scroll rapidly through the widget → stays at 60 fps (DevTools Performance tab: no dropped frames, no yellow/red bars on main thread).
- Header (
role="row"withrole="columnheader"cells) stays pinned at top during scroll. - Row numbers in the
#column are correct at any scroll position (not capped at 200).
5 — Focus trap + ESC in modals (2.C)
Setup: click + New dashboard to open the create modal.
Expected:
- Focus lands on the first interactive element (name input) automatically.
- Tab cycles through: name → description → location → Cancel → Save → back to name.
- Shift+Tab cycles backward symmetrically.
- ESC closes the modal when
isSavingis false; focus returns to the+ New dashboardbutton. - Start a save that fails (offline) → ESC is ignored while saving, resumes working after the failure.
- Repeat for Data Source modal (from the builder) and Widget Picker modal — same behaviour.
- Axe DevTools → "Run full-page scan" on the open modal → zero serious/critical violations.
6 — Canvas keyboard navigation (2.C)
Setup: dashboard with 5+ widgets. Click one widget to select it.
Expected:
- Tab from outside the canvas → focus enters the selected widget
(visible
:focus-visibleoutline in primary colour). - ArrowRight / ArrowDown → selects and focuses the next widget; the
previous one loses its
aria-selected="true". - ArrowLeft / ArrowUp → reverse. Wraps around at the ends.
- Delete or Backspace on a selected widget → removes that widget; focus ends up somewhere reasonable (next sibling or none).
- Screen-reader announcement (VoiceOver / NVDA) reads the widget title + "widget" + "selected".
7 — Responsive: iPad + iPhone (2.D)
Setup: DevTools device toolbar → emulate iPhone 12 Pro, then iPad Pro.
Open /dashboards and open one dashboard.
iPhone 12 Pro (portrait):
- Listing: grid 1 column. Top nav tabs scroll horizontally without breaking.
- Dashboard card row collapses — no horizontal overflow.
- Create modal opens as a bottom sheet (aligned to bottom, full width, rounded top corners).
- Open builder → data sidebar, filter sidebar, config panel all hidden; AI Assist and Publish buttons hidden.
- Canvas visible with widgets. Touching a widget does not accidentally drag (
touchMode=truedisables RGL gestures).
iPad Pro (portrait):
- Listing: 2–3 cards per row (dashGrid auto-fill).
- Builder: data sidebar narrower (~200px), config panel narrower (~260px). Canvas runs 8-col grid.
- Touch tap on a widget selects it (no drag). Rotating to landscape → canvas returns to 12-col.
8 — Sidebar polish + frescura (2.E)
Setup: open a builder with 5+ attached sources (mix of dataset and object_type).
Expected:
- Data sidebar header is sticky: scroll the list down, header + search row stay pinned at top.
- Header count badge is pill-shaped with primary accent background.
- Search input appears (because ≥ 2 sources). Type "xyz" (no match) → sidebar shows
EmptyState"No matches — Nothing matches 'xyz'", no error. - Clear button (×) next to the search input clears the query.
- Each group header (
DATASETS,OBJECT TYPES) has its own small count badge next to the label. - Empty case (detach all sources) → sidebar shows
EmptyStatewith icon + "No sources yet" + primary CTA that opens the data browser. - On the Page tab, filter sidebar shows
EmptyStatewith message about "Coming in the next release". Add button is disabled with tooltip "Coming soon". - Press any primary CTA → subtle scale-down animation (0.98) on active, returns smoothly.
9 — Templates removed cleanly (2.F)
Setup: in DevTools console, seed a stale session:
sessionStorage.setItem('dashboards_tab_nav', JSON.stringify({ topTab: 'templates', selectedDashboardId: null, viewMode: 'grid' }));
Reload /dashboards.
Expected:
- Top nav shows only "All Dashboards" and "Shared with you" — no "Templates" tab.
- The stale
topTab: 'templates'session restores as'all'(no error in console, no blank state). - No template cards rendered anywhere. Searching the source for
TEMPLATES[returns zero results in the codebase (sanity check). -
grep -n 'templates' components/workspace/tabs/dashboards/DashboardTab.tsxreturns zero operational lines (only the header doc comment + the migration guard).
Closing — cross-cutting
Run once at the end:
-
npx tsc --noEmit→ clean. - Lighthouse → Accessibility score ≥ 95 on
/dashboards(listing and with one dashboard open). - Fase 1 smoke test (docs/dashboards-reliability-smoketest.md) re-run, all 6 cases still pass. Zero regressions on reliability is non-negotiable.
- Prometheus:
dashboard_widget_renders_total{status=...}still increments with widget renders (no telemetry break). - Dashboard with one
object_type-backed widget and one rawdatasetwidget — both emit metrics with the correctsource_kindlabel. The ontology differentiator channel stays alive.
If any item fails, stop and file before moving to Fase 3.