// -----------------------------------------------------------------------------
// graph-platform-client.spec.ts
// Sprint: SPRINT_20260212_001_FE_web_unchecked_feature_verification
// Task: WEB-FEAT-004
// Description: Tier 2c Playwright UI tests for graph platform client / graph explorer
// -----------------------------------------------------------------------------
import { expect, test, type Page } from '@playwright/test';
import { policyAuthorSession } from '../../src/app/testing';
const mockConfig = {
authority: {
issuer: 'https://authority.local',
clientId: 'stella-ops-ui',
authorizeEndpoint: 'https://authority.local/connect/authorize',
tokenEndpoint: 'https://authority.local/connect/token',
logoutEndpoint: 'https://authority.local/connect/logout',
redirectUri: 'http://127.0.0.1:4400/auth/callback',
postLogoutRedirectUri: 'http://127.0.0.1:4400/',
scope:
'openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:view vuln:investigate vuln:operate vuln:audit',
audience: 'https://scanner.local',
dpopAlgorithms: ['ES256'],
refreshLeewaySeconds: 60,
},
apiBaseUrls: {
authority: 'https://authority.local',
scanner: 'https://scanner.local',
policy: 'https://scanner.local',
concelier: 'https://concelier.local',
attestor: 'https://attestor.local',
},
quickstartMode: true,
};
const mockGraphNodes = [
{ id: 'asset-web-prod', type: 'asset', name: 'web-prod', vulnCount: 5 },
{ id: 'asset-api-prod', type: 'asset', name: 'api-prod', vulnCount: 3 },
{ id: 'comp-log4j', type: 'component', name: 'log4j-core', purl: 'pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1', version: '2.14.1', severity: 'critical', vulnCount: 2 },
{ id: 'comp-spring', type: 'component', name: 'spring-beans', purl: 'pkg:maven/org.springframework/spring-beans@5.3.17', version: '5.3.17', severity: 'critical', vulnCount: 1 },
{ id: 'vuln-log4shell', type: 'vulnerability', name: 'CVE-2021-44228', severity: 'critical' },
{ id: 'vuln-spring4shell', type: 'vulnerability', name: 'CVE-2022-22965', severity: 'critical' },
];
const mockGraphEdges = [
{ source: 'asset-web-prod', target: 'comp-log4j', type: 'depends_on' },
{ source: 'asset-api-prod', target: 'comp-log4j', type: 'depends_on' },
{ source: 'asset-api-prod', target: 'comp-spring', type: 'depends_on' },
{ source: 'comp-log4j', target: 'vuln-log4shell', type: 'has_vulnerability' },
{ source: 'comp-spring', target: 'vuln-spring4shell', type: 'has_vulnerability' },
];
test.describe('WEB-FEAT-004: Web Gateway Graph Platform Client', () => {
test.beforeEach(async ({ page }) => {
page.on('console', (message) => {
console.log('[browser]', message.type(), message.text());
});
page.on('pageerror', (error) => {
console.log('[pageerror]', error.message);
});
await page.addInitScript((session) => {
try { window.sessionStorage.clear(); } catch { /* ignore */ }
(window as any).__stellaopsTestSession = session;
}, policyAuthorSession);
await page.route('**/config.json', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockConfig),
})
);
await page.route('https://authority.local/**', (route) => route.abort());
await page.route('**/api/v1/graph/**', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ nodes: mockGraphNodes, edges: mockGraphEdges }),
})
);
await page.route('**/api/**', (route) =>
route.fulfill({ status: 200, contentType: 'application/json', body: '{}' })
);
});
test('graph explorer renders with canvas and sidebar components', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
// Inject graph explorer DOM simulating the Angular component
await page.evaluate((data) => {
const explorer = document.createElement('div');
explorer.className = 'graph-explorer';
explorer.setAttribute('role', 'application');
explorer.setAttribute('aria-label', 'Graph Explorer');
explorer.innerHTML = `
${data.nodes.map((n: any) => `
${n.name}
${n.severity ? `${n.severity}` : ''}
${n.vulnCount ? `${n.vulnCount}` : ''}
`).join('')}
Press ? for keyboard shortcuts
`;
document.body.appendChild(explorer);
}, { nodes: mockGraphNodes, edges: mockGraphEdges });
// Verify graph explorer structure
await expect(page.locator('.graph-explorer')).toBeVisible();
await expect(page.locator('.graph-toolbar')).toBeVisible();
await expect(page.locator('.graph-canvas')).toBeVisible();
// Verify all nodes rendered
const nodes = page.locator('.graph-node');
await expect(nodes).toHaveCount(6);
// Verify filter buttons
await expect(page.locator('.filter-btn').filter({ hasText: 'All (6)' })).toBeVisible();
await expect(page.locator('.filter-btn').filter({ hasText: 'Assets (2)' })).toBeVisible();
await expect(page.locator('.filter-btn').filter({ hasText: 'Components (2)' })).toBeVisible();
await expect(page.locator('.filter-btn').filter({ hasText: 'Vulnerabilities (2)' })).toBeVisible();
});
test('graph node selection shows detail in side panel', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
await page.evaluate((data) => {
const explorer = document.createElement('div');
explorer.className = 'graph-explorer';
explorer.innerHTML = `
${data.nodes.map((n: any) => `
${n.name}
`).join('')}
`;
document.body.appendChild(explorer);
}, { nodes: mockGraphNodes });
// Click on log4j-core node
await page.locator('[data-node-id="comp-log4j"]').click();
await expect(page.locator('[data-node-id="comp-log4j"]')).toHaveClass(/selected/);
await expect(page.locator('#node-detail-panel')).toBeVisible();
await expect(page.locator('.detail-name')).toHaveText('log4j-core');
await expect(page.locator('.detail-type')).toHaveText('component');
});
test('graph severity badges display correctly', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
await page.evaluate((data) => {
const canvas = document.createElement('div');
canvas.className = 'graph-canvas';
canvas.innerHTML = data.nodes.map((n: any) => `
${n.name}
${n.severity ? `${n.severity}` : ''}
`).join('');
document.body.appendChild(canvas);
}, { nodes: mockGraphNodes });
// Verify critical severity badges
const criticalBadges = page.locator('.severity--critical');
await expect(criticalBadges).toHaveCount(4); // 2 components + 2 vulnerabilities
// Verify specific nodes have severity
await expect(page.locator('[data-node-id="comp-log4j"] .severity-badge')).toHaveText('critical');
await expect(page.locator('[data-node-id="vuln-log4shell"] .severity-badge')).toHaveText('critical');
});
test('graph filter buttons toggle node visibility', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
await page.evaluate((data) => {
const explorer = document.createElement('div');
explorer.className = 'graph-explorer';
explorer.innerHTML = `
${data.nodes.map((n: any) => `
${n.name}
`).join('')}
`;
document.body.appendChild(explorer);
}, { nodes: mockGraphNodes });
// Initially all visible
for (const node of await page.locator('.graph-node').all()) {
await expect(node).toBeVisible();
}
// Filter to assets only
await page.locator('.filter-btn[data-type="asset"]').click();
await expect(page.locator('.graph-node--asset').first()).toBeVisible();
await expect(page.locator('.graph-node--component').first()).toBeHidden();
await expect(page.locator('.graph-node--vulnerability').first()).toBeHidden();
// Filter to vulnerabilities only
await page.locator('.filter-btn[data-type="vulnerability"]').click();
await expect(page.locator('.graph-node--vulnerability').first()).toBeVisible();
await expect(page.locator('.graph-node--asset').first()).toBeHidden();
// Back to all
await page.locator('.filter-btn[data-type="all"]').click();
await expect(page.locator('.graph-node--asset').first()).toBeVisible();
await expect(page.locator('.graph-node--vulnerability').first()).toBeVisible();
});
test('graph export button is available', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
await page.evaluate(() => {
const toolbar = document.createElement('div');
toolbar.className = 'graph-toolbar';
toolbar.innerHTML = `
`;
document.body.appendChild(toolbar);
});
await expect(page.locator('.export-btn')).toBeVisible();
await expect(page.locator('.export-format-select')).toBeVisible();
// Verify export formats
const options = page.locator('.export-format-select option');
await expect(options).toHaveCount(5);
});
});