Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4d57f3b
chore: add prowler-openspec-opensource as git submodule
Apr 14, 2026
6e77abe
chore: update openspec submodule with react-flow-migration proposal
Apr 14, 2026
9922b15
chore: update openspec with expect-cli validation tasks
Apr 14, 2026
5c981f5
chore: update openspec with restructured expect-cli tests
Apr 14, 2026
b4601ab
chore(openspec): consolidate submodule to include all chain task and …
Apr 17, 2026
10a62a6
Merge remote-tracking branch 'origin/master' into PROWLER-1273/react-…
Apr 17, 2026
a9427c8
chore(openspec): bump submodule to include PR4 test coverage proposal
Apr 17, 2026
ba84b23
[CHAIN] refactor(ui): normalize graph edge types and remove dead code…
pfe-nazaries Apr 22, 2026
ff2bf5b
[CHAIN] refactor(ui): replace D3 graph rendering with React Flow (#10…
pfe-nazaries May 5, 2026
1d54244
[CHAIN] feat(ui): add graph interactions and filtered view (#10756)
pfe-nazaries May 5, 2026
a4fc230
[CHAIN] feat(ui): add graph export, minimap and fullscreen polish (#1…
pfe-nazaries May 5, 2026
3d4f5e6
Merge remote-tracking branch 'origin/master' into PROWLER-1273/react-…
May 5, 2026
8acbddd
[CHAIN] test(ui): add Vitest Browser test coverage for Attack Paths (…
pfe-nazaries May 5, 2026
48882b5
Merge remote-tracking branch 'origin/master' into PROWLER-1273/react-…
May 5, 2026
74e5118
Merge remote-tracking branch 'origin/PROWLER-1273/react-flow-migratio…
May 5, 2026
c183d5e
fix: format
May 5, 2026
4d5a77a
chore(openspec): stop tracking openspec as submodule
May 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ jobs:
fonts.gstatic.com:443
api.github.com:443
release-assets.githubusercontent.com:443
cdn.playwright.dev:443
objects.githubusercontent.com:443

- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -152,6 +154,24 @@ jobs:
echo "Only test files changed - running ALL unit tests"
pnpm run test:run

- name: Cache Playwright browsers
if: steps.check-changes.outputs.any_changed == 'true'
id: playwright-cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-chromium-${{ hashFiles('ui/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-playwright-chromium-

- name: Install Playwright Chromium browser
if: steps.check-changes.outputs.any_changed == 'true' && steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install chromium

- name: Run browser tests
if: steps.check-changes.outputs.any_changed == 'true'
run: pnpm run test:browser

- name: Build application
if: steps.check-changes.outputs.any_changed == 'true'
run: pnpm run build
4 changes: 4 additions & 0 deletions ui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

# testing
/coverage
__screenshots__/

# next.js
/.next/
Expand All @@ -28,6 +29,9 @@ yarn-error.log*
.env*.local
.env

# Claude Code local settings
.claude/

# vercel
.vercel

Expand Down
12 changes: 12 additions & 0 deletions ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable changes to the **Prowler UI** are documented in this file.

## Unreleased

### πŸš€ Added

- Browser test mode using Vitest with the Playwright provider, with initial coverage of the Attack Paths page and a new `pnpm test:browser` script wired into CI

### πŸ”„ Changed

- Attack Paths graph: extract shared primitives across `FindingNode`, `ResourceNode`, and `InternetNode` (hidden handles, label truncation, fill/border resolution) without forcing a generic node renderer [(#10705)](https://github.com/prowler-cloud/prowler/pull/10705)

---

## [1.25.2] (Prowler v5.25.2)

### πŸ”„ Changed
Expand Down
106 changes: 106 additions & 0 deletions ui/__tests__/msw/handlers/attack-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { http, HttpResponse } from "msw";

import type { PageFixture } from "@/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.fixtures";
import type {
AttackPathQueriesResponse,
AttackPathQuery,
AttackPathQueryResult,
AttackPathScan,
AttackPathScansResponse,
QueryResultAttributes,
} from "@/types/attack-paths";

const API = process.env.NEXT_PUBLIC_API_BASE_URL!;

type JsonApiErrorBody = {
errors: Array<{ detail: string; status: string }>;
};

const toScansApiResponse = (
scans: AttackPathScan[],
): AttackPathScansResponse => ({
data: scans,
links: {
first: `${API}/attack-paths-scans?page=1`,
last: `${API}/attack-paths-scans?page=1`,
next: null,
prev: null,
},
});

const toQueriesApiResponse = (
queries: AttackPathQuery[],
): AttackPathQueriesResponse => ({
data: queries,
});

const toQueryResultApiResponse = (
attrs: QueryResultAttributes,
queryId: string,
): AttackPathQueryResult => ({
data: {
type: "attack-paths-query-run-requests",
id: queryId,
attributes: attrs,
},
});

const toErrorBody = (detail: string, status: number): JsonApiErrorBody => ({
errors: [{ detail, status: String(status) }],
});

export const handlersForFixture = (fx: PageFixture) => [
http.get(`${API}/attack-paths-scans`, () =>
HttpResponse.json<AttackPathScansResponse>(toScansApiResponse(fx.scans)),
),

http.get<{ scanId: string }>(
`${API}/attack-paths-scans/:scanId/queries`,
() =>
HttpResponse.json<AttackPathQueriesResponse>(
toQueriesApiResponse(fx.queries),
),
),

http.post<{ scanId: string }>(
`${API}/attack-paths-scans/:scanId/queries/run`,
() => {
if (fx.queryError) {
return HttpResponse.json<JsonApiErrorBody>(
toErrorBody(fx.queryError.error, fx.queryError.status),
{ status: fx.queryError.status },
);
}
if (!fx.queryResult) {
return HttpResponse.json<JsonApiErrorBody>(
toErrorBody("No data found", 404),
{ status: 404 },
);
}
return HttpResponse.json<AttackPathQueryResult>(
toQueryResultApiResponse(fx.queryResult, fx.queryId),
);
},
),

http.post<{ scanId: string }>(
`${API}/attack-paths-scans/:scanId/queries/custom`,
() => {
if (fx.queryError) {
return HttpResponse.json<JsonApiErrorBody>(
toErrorBody(fx.queryError.error, fx.queryError.status),
{ status: fx.queryError.status },
);
}
if (!fx.queryResult) {
return HttpResponse.json<JsonApiErrorBody>(
toErrorBody("No data found", 404),
{ status: 404 },
);
}
return HttpResponse.json<AttackPathQueryResult>(
toQueryResultApiResponse(fx.queryResult, fx.queryId),
);
},
),
];
13 changes: 13 additions & 0 deletions ui/__tests__/msw/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { HttpHandler } from "msw";

/**
* Static handlers shared by every browser test β€” registered as defaults on
* the worker. Use this list for endpoints whose response doesn't change
* across tests (e.g. `/users/me`, `/tenants/current`, health checks).
*
* Per-domain dynamic handlers that depend on fixture data live in their own
* files alongside this index (e.g. `./attack-paths.ts`) and are imported
* directly by the tests that need them, then wired via
* `worker.use(...handlersForFixture(fx))`.
*/
export const handlers: HttpHandler[] = [];
5 changes: 5 additions & 0 deletions ui/__tests__/msw/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { setupWorker } from "msw/browser";

import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);
25 changes: 25 additions & 0 deletions ui/__tests__/render-browser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { ComponentType, PropsWithChildren, ReactElement } from "react";
import { render as vitestRender } from "vitest-browser-react";

const TestProviders = ({ children }: PropsWithChildren) => <>{children}</>;

type RenderOptions = Parameters<typeof vitestRender>[1];

export function render(ui: ReactElement, options?: RenderOptions) {
const userWrapper = options?.wrapper as
| ComponentType<PropsWithChildren>
| undefined;

const Wrapper = userWrapper
? ({ children }: PropsWithChildren) => {
const Inner = userWrapper;
return (
<TestProviders>
<Inner>{children}</Inner>
</TestProviders>
);
}
: TestProviders;

return vitestRender(ui, { ...options, wrapper: Wrapper });
}
29 changes: 9 additions & 20 deletions ui/actions/attack-paths/query-result.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,27 +131,16 @@ export function adaptQueryResultToGraphData(
// Populate findings and resources based on HAS_FINDING edges
edges.forEach((edge) => {
if (edge.type === "HAS_FINDING") {
const sourceId =
typeof edge.source === "string"
? edge.source
: (edge.source as { id?: string })?.id;
const targetId =
typeof edge.target === "string"
? edge.target
: (edge.target as { id?: string })?.id;

if (sourceId && targetId) {
// Add finding to source node (resource -> finding)
const sourceNode = normalizedNodes.find((n) => n.id === sourceId);
if (sourceNode) {
sourceNode.findings.push(targetId);
}
// Add finding to source node (resource -> finding)
const sourceNode = normalizedNodes.find((n) => n.id === edge.source);
if (sourceNode) {
sourceNode.findings.push(edge.target);
}

// Add resource to target node (finding <- resource)
const targetNode = normalizedNodes.find((n) => n.id === targetId);
if (targetNode) {
targetNode.resources.push(sourceId);
}
// Add resource to target node (finding <- resource)
const targetNode = normalizedNodes.find((n) => n.id === edge.target);
if (targetNode) {
targetNode.resources.push(edge.source);
}
}
});
Expand Down
Loading
Loading