// ----------------------------------------------------------------------------- // 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 = `
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); }); });