Skip to content

[CHAIN] test(ui): add Vitest Browser test coverage for Attack Paths#10970

Merged
pfe-nazaries merged 39 commits intoPROWLER-1273/react-flow-migrationfrom
PROWLER-1405/test-coverage-vitest-browser
May 5, 2026
Merged

[CHAIN] test(ui): add Vitest Browser test coverage for Attack Paths#10970
pfe-nazaries merged 39 commits intoPROWLER-1273/react-flow-migrationfrom
PROWLER-1405/test-coverage-vitest-browser

Conversation

@pfe-nazaries
Copy link
Copy Markdown
Contributor

@pfe-nazaries pfe-nazaries commented May 4, 2026

🔗 Part of Chained PRs

Field Value
Feature Branch PROWLER-1273/react-flow-migration
Main PR #10686
Sub-task PROWLER-1405
Chain Position 5 of 5
Depends on #10800 (PR3 — Export + Minimap) — 🟢 Merged

Chain Overview

         ┌──────────────────────────┐
         │ PR0: Normalize data      │ ← #10701 🟢
         └────────────┬─────────────┘
                      │
         ┌────────────▼─────────────┐
         │ PR1: React Flow core     │ ← #10705 🟢
         │   rendering              │
         └────────────┬─────────────┘
                      │
         ┌────────────▼─────────────┐
         │ PR2: Interactions        │ ← #10756 🟢
         │   🎯 MVP                 │
         └────────────┬─────────────┘
                      │
         ┌────────────▼─────────────┐
         │ PR3: Export + Minimap    │ ← #10800 🟢
         │   ✅ Phase 1 complete    │
         └────────────┬─────────────┘
                      │
         ┌────────────▼─────────────┐
         │ 📍 PR4: Test coverage    │ ← THIS PR
         │   🛡️ Hardening complete  │
         └────────────┬─────────────┘
                      │
         ┌────────────▼─────────────┐
         │ #10686 Main PR           │
         │   → master               │
         └──────────────────────────┘

Context

Part of chained PRs for the React Flow migration. This is the Hardening complete milestone — it adds the Vitest Browser-mode test infrastructure and a page-level suite for <AttackPathsPage /> so the migrated graph is covered end-to-end with real DOM, real React Flow, and real Dagre layout.

jsdom cannot drive React Flow (no measurable DOM, no Canvas API for export). Vitest Browser with the Playwright provider runs the page in a real Chromium tab while keeping the unit-test ergonomics: vitest run, --related, watch mode, project filters, and CI integration.


Description

Changes:

Test infrastructure

  • New pnpm test:browser script and a browser Vitest project running in Playwright/Chromium via @vitest/browser-playwright
  • vitest.browser.setup.ts configures MSW + ResizeObserver/matchMedia polyfills used by React Flow inside the browser bundle
  • __tests__/msw/{worker,handlers} provides the MSW service worker for in-browser network mocking; mockServiceWorker.js is published to public/
  • __tests__/render-browser.tsx wraps vitest-browser-react with a TestProviders shell so future global providers slot in without touching call sites

Attack Paths coverage

  • attack-paths-page.browser.test.tsx — 23 tests grouped by user-perceived flow (loading the page, running a query, exploring the graph, exporting the graph, running a different query)
  • attack-paths-page.harness.ts — single-file harness exposing the page through semantic queries (graph.findingNodes, graph.executeQuery(), graph.clickFirstFindingNode(), …); tests never reach into the DOM directly
  • attack-paths-page.fixtures.ts — typed fixtures for typical, single-node, empty-graph, large (200-node), disconnected, findings-only, resources-only, edge-cases and empty-scans scenarios; each fixture wires its own MSW handlers

Source changes pulled in to make tests deterministic

  • Tier 1 expansion state lifted from local component state into the Zustand graph store, so it survives the data swap that happens when entering/exiting filtered view (and is reset on fresh data loads)
  • useGraphStore exported and reset in beforeEach, replacing the original fixture's manual unmount tracking — the regression that used to remount mid-test now exercises setGraphData's reset path by re-running the query, which is what production actually does

CI

  • .github/workflows/ui-tests.yml runs pnpm test:browser on PRs touching ui/, with Playwright's Chromium cached via the standard action

Steps to review

  1. cd ui && pnpm install && pnpm test:browser — 24 tests should pass in roughly under a minute
  2. Read attack-paths-page.harness.ts first — every test is intentionally a thin call-site over the harness, so the harness defines the contract
  3. Skim attack-paths-page.fixtures.ts to see the data shapes covered (typical, single-node, large, disconnected, edge-cases, …)
  4. Open attack-paths-page.browser.test.tsx — verify each describe block matches a spec section in the migration RFC
  5. Confirm useGraphStore reset in beforeEach cleanly isolates tests (no need to remount inside a test)
  6. Check vitest.config.ts and vitest.browser.setup.ts for the project split (unit vs browser) and that the unit project still excludes browser tests

Checklist

Community Checklist
  • This feature/issue is listed in here or roadmap.prowler.com
  • Is it assigned to me, if not, request it via the issue/feature in here or Prowler Community Slack

UI

  • All issue/task requirements work as expected on the UI
  • Screenshots/Video of the functionality flow (if applicable) - Mobile (X < 640px)
  • Screenshots/Video of the functionality flow (if applicable) - Table (640px > X < 1024px)
  • Screenshots/Video of the functionality flow (if applicable) - Desktop (X > 1024px)
  • Ensure new entries are added to CHANGELOG.md, if applicable.

License

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Pablo F.G and others added 30 commits April 17, 2026 14:41
- Narrow GraphEdge.source/target from string | object to string
- Remove typeof guards in adapter, graph component, and utilities
- Remove unused panX, panY, zoomLevel fields from graph state store
- Delete orphaned NodeRelationships component (never integrated)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Aligns with design decision D4 — GraphCanvas now owns layoutWithDagre()
and node enrichment (selection, hasFindings), making AttackPathGraph a
thin wrapper around ReactFlowProvider. This prepares PR2 where
GraphCanvas needs setEdges() ownership for hover highlight.

Also removes unused AttackPathGraphRef deprecated alias and isFilteredView
prop from outer component.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rewrite attack-path-graph.tsx from D3 imperative SVG to React Flow declarative components
- Add pure layoutWithDagre() function using @dagrejs/dagre maintained fork
- Create custom node components: FindingNode (hexagon), ResourceNode (pill), InternetNode (globe)
- Implement outer/inner component split (ReactFlowProvider constraint)
- Disable export button temporarily (re-enabled in PR3 with html-to-image)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace require() with ESM import for @dagrejs/dagre
- Pre-compute resourcesWithFindings set to avoid O(n*e) per-node loop
- Extract isFindingNode helper to deduplicate label checks
- Remove no-op handleWheel handler and unused initialNodeId prop
- Replace dangerouslySetInnerHTML with style children
- Add aria-hidden to SVG defs and role/aria-label to graph container

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace nodes.find() inside edges loop with precomputed Map
- Fix misleading complexity comment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Tier 1 click: toggle finding visibility on resource nodes
- Add Tier 2 click: enter filtered subgraph view on finding nodes
- Add path highlight on hover with upstream/downstream edges
- Add node selection with visual indicator and detail panel sync
- Fix graph state not resetting on scan change
- Extract NodeDetailPanel to deduplicate fullscreen and main panels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace nodes.find() inside edges loop with pre-built Map
- Update graph-interactions spec to reflect D4 render-body derivation
- Update Ctrl+scroll spec to document zoomOnPinch native handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Post-rebase conflict resolution: extend the local NodeDetailPanel with
onViewFinding / viewFindingLoading props so the master-added finding
drawer flow remains wired through the extracted panel component.
…n' into PROWLER-1375/graph-interactions-filtered-view
CodeRabbit CLI and similar tools may create ui/.claude/settings.local.json
with machine-specific paths or local credentials; add .claude/ to the ui
gitignore so it cannot accidentally reach source control.

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

Delivers PROWLER-1376 (PR3 of the React Flow migration):

- exportGraphAsPNG() rewritten to use modern-screenshot (domToPng) against
  the React Flow container, with viewport math via getViewportForBounds()
  to fit all nodes regardless of the user's zoom/pan. Picked
  modern-screenshot over the better-known html-to-image (inactive since
  2025-04) — near-identical API, actively maintained, better modern-CSS
  support for the React Flow viewport.
- Export path now signals missing-container and empty-node cases via
  explicit Errors instead of silent early returns, so the caller shows
  a real failure toast instead of a misleading success.
- GraphHandle exposes getNodesBounds() so the export honors the React
  Flow instance's nodeLookup (correct bounds for sub-flows).
- Adds a fullscreen Dialog with its own AttackPathGraph instance and
  controls, plus the React Flow <MiniMap /> in the main view.
- Drops dagre/@types/dagre (feature moved to @dagrejs/dagre earlier in
  the migration); adds modern-screenshot 4.7.0.
- Bumps the openspec submodule to reflect the modern-screenshot choice
  and the new export-failure Scenario in graph-export.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Satisfies Radix Dialog a11y requirement (missing description /
aria-describedby warning).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Aligns with design decision D4 — GraphCanvas now owns layoutWithDagre()
and node enrichment (selection, hasFindings), making AttackPathGraph a
thin wrapper around ReactFlowProvider. This prepares PR2 where
GraphCanvas needs setEdges() ownership for hover highlight.

Also removes unused AttackPathGraphRef deprecated alias and isFilteredView
prop from outer component.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rewrite attack-path-graph.tsx from D3 imperative SVG to React Flow declarative components
- Add pure layoutWithDagre() function using @dagrejs/dagre maintained fork
- Create custom node components: FindingNode (hexagon), ResourceNode (pill), InternetNode (globe)
- Implement outer/inner component split (ReactFlowProvider constraint)
- Disable export button temporarily (re-enabled in PR3 with html-to-image)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace require() with ESM import for @dagrejs/dagre
- Pre-compute resourcesWithFindings set to avoid O(n*e) per-node loop
- Extract isFindingNode helper to deduplicate label checks
- Remove no-op handleWheel handler and unused initialNodeId prop
- Replace dangerouslySetInnerHTML with style children
- Add aria-hidden to SVG defs and role/aria-label to graph container

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace nodes.find() inside edges loop with precomputed Map
- Fix misleading complexity comment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address Alan's review comment on #10705: deduplicate the pieces that all
three React Flow node components share without forcing a generic node
renderer. FindingNode, ResourceNode and InternetNode stay separate; only
the boilerplate is extracted.

- HiddenHandles: invisible target/source handles used identically by all
  three nodes.
- truncateLabel: shared label truncation helper used by FindingNode and
  ResourceNode (24/22 chars).
- resolveNodeColors: layered fill/border resolution covering selection
  highlight and finding-alert state. Per-node strokeWidth is intentionally
  kept local since each node has its own visual weight.
Address Alan's CHANGES_REQUESTED on #10705: add automated coverage so
the new React Flow rendering contract is guarded against regressions.

- _lib/layout.test.ts: deterministic checks on layoutWithDagre — empty
  input, node typing/dimensions by labels, run-to-run determinism,
  top-left position offset, container relationship inversion (RUNS_IN
  & friends with originalSource/originalTarget preserved on the rfEdge),
  finding-edge animation/className, and stable rfEdge IDs.
- graph-controls.test.tsx: GraphControls Export button is disabled and
  surfaces "Export available soon" without an onExport callback, and
  enabled + invokes the callback when one is provided.

Mounting the full AttackPathGraph with React Flow in jsdom is fragile
(ResizeObserver, dimensions, edge measurement); that coverage is left
for the next chained PR which introduces Vitest Browser Mode.
Test additions are an internal contract; user-facing changelogs only use
the standard Added/Changed/Fixed/Removed/Security sections.
- Skip browser-replay artifacts under .expect/ to prevent the
  prettier plugin from crashing the lint pipeline.
- Clear expanded resources and hovered node when graph data changes,
  preventing stale interaction state across scans and query runs.
- Forward View Finding handler to the fullscreen detail panel so
  related-finding actions are no longer no-ops.
…375/graph-interactions-filtered-view

Bring chained PR #10705 up to date in #10756 so both target the same
feature branch with consistent React Flow refactor.

Conflict resolutions:
- nodes/{finding,internet,resource}-node.tsx: take 1374 versions (use
  HiddenHandles primitive + resolveNodeColors / truncateLabel helpers)
- _lib/graph-utils.ts: keep 1375 perf optimization (nodeLabelMap O(1))
- _components/graph/attack-path-graph.tsx: keep 1375 (already includes
  React Flow refactor + interactions, filtered view, hover highlight)
- attack-paths-page.tsx: keep 1375 (already extracts NodeDetailPanel)

Inbound from 1374: shared HiddenHandles, resolveNodeColors,
truncateLabel, layoutWithDagre tests, graph-controls tests.
Use resolvedTheme from next-themes to drive MiniMap bgColor, maskColor and
maskStrokeColor, and reuse getNodeColor/getNodeBorderColor so minimap nodes
match the main graph palette in both light and dark modes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverts the workaround in b3e08a6. The .expect/ directory was
local tooling output and is no longer kept under the worktree, so
the ignore rule in eslint.config.mjs is dead weight.
…inimap' into PROWLER-1405/test-coverage-vitest-browser
Move the expanded-resources Set from local GraphCanvas state to the
zustand graph store and let the parent control it. Local state was
reset on every data swap, which wiped expansion when entering or
exiting filtered view; lifting it preserves the user's expansion
across those transitions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pablo F.G and others added 4 commits May 4, 2026 11:00
Wire up Vitest's browser provider (Playwright + Chromium) alongside
the existing jsdom unit project, with MSW driving HTTP mocks via
service worker. The two projects extend a shared root config and run
under separate `pnpm test:unit` / `pnpm test:browser` scripts; the
existing `pnpm test` is repointed to a single `vitest run` so it
picks up both. CI installs and caches the Playwright browser before
running the new browser job.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cover the Attack Paths query-builder page end-to-end in the new
browser mode: graph rendering and normalization, Tier 1 expansion,
filtered-view enter/exit, hover/selection, large-graph performance,
and edge cases (self-loops, disconnected components, unicode labels,
remount). Selectors and flows live in `GraphHarness` so tests stay
declarative.

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

Export `useGraphStore` and reset it in a `beforeEach`, replacing the
`mountWith` fixture's manual unmount tracking. The remount-based regression
test ("remount reinitializes the store") is rewritten as "re-running a query
clears the previous filtered view", which exercises the same `setGraphData`
reset path without depending on component lifecycle.

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

@Alan-TheGentleman Alan-TheGentleman left a comment

Choose a reason for hiding this comment

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

The export happy-path test deferred from #10800 was not added. The harness exposes exportAsPNG() (attack-paths-page.harness.ts:352) and graph-controls.test.tsx covers the button callback at the component level, but no test invokes the harness method or mocks domToPng(). The only case under describe("graph-export", ...) is "export button is enabled and clickable when a graph is rendered" (L253-263), which just asserts the button is present and not disabled.

Please add a browser-mode test that drives harness.exportAsPNG() against a mocked domToPng() and verifies the bounds passed to getViewportForBounds() match the rendered graph, so the helper does not silently regress.

Pablo F.G and others added 2 commits May 5, 2026 09:44
Tests were grouped by internal spec taxonomy (graph-data-normalization,
graph-layout, graph-rendering, ...). Reorganize them around what a user
actually does on the page: loading the page, running a query, exploring
the graph, exporting the graph, running a different query.

Also add __screenshots__/ to ui/.gitignore — Vitest Browser writes those
artifacts on test failure and they should not be tracked.

Bumps openspec submodule to keep PR4 task list aligned with the new
describe block layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n' into PROWLER-1405/test-coverage-vitest-browser

# Conflicts:
#	openspec
#	ui/CHANGELOG.md
#	ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/attack-path-graph.tsx
#	ui/pnpm-lock.yaml
Copy link
Copy Markdown
Contributor

@Alan-TheGentleman Alan-TheGentleman left a comment

Choose a reason for hiding this comment

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

Confirmed in 8710412a. The new "clicking export downloads a PNG sized to the configured export canvas" test (L324-371) drives harness.exportAsPNG() end-to-end, intercepts the download anchor, and decodes the PNG IHDR chunk to assert width=1920 height=1080. Stronger than the mocked path I asked for: it covers the viewport element passed to domToPng, the configured export size, and the bounds-driven viewport transform in one shot. Approving.

Pablo F.G and others added 3 commits May 5, 2026 13:30
Tests were still reaching into the page through raw selectors, classList
filters, transform parsing, and a hand-rolled PNG-download interceptor.
Move all of it into GraphHarness so test bodies stay declarative and the
harness stays the single source of truth for "how to talk to the page".

Harness additions:
- emptyStateMessage(), containsText(): page-level text surface
- nodePositions: parsed translate() per node, for layout assertions
- findingEdges, resourceEdges, highlightedEdges: filtered edge groups
- toolbar.isExportButtonEnabled: typed predicate
- hoverFirstResourceNode, dblClickFirstResourceNode,
  rapidlyClickFirstFindingNode, clickEmptyCanvas: action methods that
  used to be ad-hoc in tests
- captureExportPNG(): triggers an export, intercepts the anchor-click,
  and parses width/height from the PNG IHDR chunk; replaces ~30 lines
  of test-side plumbing with a single typed call

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The harness drives <AttackPathsPage />, not graphs in general; pair it
with the page name so future page-level harnesses follow the same
convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up the openspec docs rename in
prowler-openspec-opensource#2 to match the source rename in this PR.
@pfe-nazaries pfe-nazaries merged commit 8acbddd into PROWLER-1273/react-flow-migration May 5, 2026
1 check passed
@pfe-nazaries pfe-nazaries deleted the PROWLER-1405/test-coverage-vitest-browser branch May 5, 2026 11:38
Alan-TheGentleman pushed a commit that referenced this pull request May 8, 2026
…10970)

Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com>
Co-authored-by: Claude Opus 4.6 (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