// ----------------------------------------------------------------------------- // binary-diff-panel.spec.ts // Sprint: SPRINT_20260117_018_FE_ux_components // Task: UXC-008 - Integration tests with Playwright // Description: Playwright e2e tests for Binary-Diff Panel component // ----------------------------------------------------------------------------- import { expect, test } from '@playwright/test'; import { policyAuthorSession } from '../../src/app/testing'; const mockConfig = { authority: { issuer: 'https://authority.local', clientId: 'stellaops-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 findings:read binary:read', 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, }; test.beforeEach(async ({ page }) => { await page.addInitScript((session) => { try { window.sessionStorage.clear(); } catch { // ignore storage errors in restricted contexts } (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()); }); test.describe('Binary-Diff Panel Component', () => { test('renders header with base and candidate info', async ({ page }) => { await page.goto('/binary/diff'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); // Verify header shows base and candidate await expect(page.getByText('Base')).toBeVisible(); await expect(page.getByText('Candidate')).toBeVisible(); // Verify diff stats await expect(page.locator('.diff-stats')).toBeVisible(); }); test('scope selector switches between file, section, function', async ({ page }) => { await page.goto('/binary/diff'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); // Find scope selector buttons const fileBtn = page.getByRole('button', { name: /File/i }); const sectionBtn = page.getByRole('button', { name: /Section/i }); const functionBtn = page.getByRole('button', { name: /Function/i }); await expect(fileBtn).toBeVisible(); await expect(sectionBtn).toBeVisible(); await expect(functionBtn).toBeVisible(); // Click section scope await sectionBtn.click(); await expect(sectionBtn).toHaveClass(/active/); // Click function scope await functionBtn.click(); await expect(functionBtn).toHaveClass(/active/); // Click file scope await fileBtn.click(); await expect(fileBtn).toHaveClass(/active/); }); test('scope selection updates diff view', async ({ page }) => { await page.goto('/binary/diff'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); // Select an entry in the tree const treeItem = page.locator('.tree-item').first(); await treeItem.click(); // Verify selection state await expect(treeItem).toHaveClass(/selected/); // Verify diff view updates (footer shows hashes) await expect(page.locator('.diff-footer')).toBeVisible(); }); test('show only changed toggle filters unchanged entries', async ({ page }) => { await page.goto('/binary/diff'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); // Find the toggle const toggle = page.getByLabel(/Show only changed/i); await expect(toggle).toBeVisible(); // Count items before toggle const itemsBefore = await page.locator('.tree-item').count(); // Enable toggle await toggle.check(); await expect(toggle).toBeChecked(); // Items may be filtered (or same count if all changed) const itemsAfter = await page.locator('.tree-item').count(); expect(itemsAfter).toBeLessThanOrEqual(itemsBefore); }); test('opcodes/decompiled toggle changes view mode', async ({ page }) => { await page.goto('/binary/diff'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); // Find the toggle const toggle = page.getByLabel(/Opcodes|Decompiled/i); await expect(toggle).toBeVisible(); // Toggle and verify label changes const initialText = await toggle.locator('..').textContent(); await toggle.click(); const newText = await toggle.locator('..').textContent(); // Text should change between Opcodes and Decompiled expect(initialText).not.toEqual(newText); }); test('export signed diff button is functional', async ({ page }) => { await page.goto('/binary/diff'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); // Find export button const exportBtn = page.getByRole('button', { name: /Export Signed Diff/i }); await expect(exportBtn).toBeVisible(); // Click and verify action await exportBtn.click(); // Should trigger download or modal (implementation dependent) // At minimum, button should be clickable without error }); test('tree navigation supports keyboard', async ({ page }) => { await page.goto('/binary/diff'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); // Focus first tree item const firstItem = page.locator('.tree-item').first(); await firstItem.focus(); // Press Enter to select await page.keyboard.press('Enter'); await expect(firstItem).toHaveClass(/selected/); }); test('diff view shows side-by-side comparison', async ({ page }) => { await page.goto('/binary/diff'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); // Verify side-by-side columns await expect(page.locator('.diff-header-row')).toBeVisible(); await expect(page.locator('.line-base').first()).toBeVisible(); await expect(page.locator('.line-candidate').first()).toBeVisible(); }); test('change indicators show correct colors', async ({ page }) => { await page.goto('/binary/diff'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); // Check for change type classes on tree items const addedItem = page.locator('.tree-item.change-added'); const removedItem = page.locator('.tree-item.change-removed'); const modifiedItem = page.locator('.tree-item.change-modified'); // At least one type should exist in a real diff const hasChanges = (await addedItem.count()) > 0 || (await removedItem.count()) > 0 || (await modifiedItem.count()) > 0; expect(hasChanges).toBeTruthy(); }); test('hash display in footer shows base and candidate hashes', async ({ page }) => { await page.goto('/binary/diff'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); // Select an entry await page.locator('.tree-item').first().click(); // Verify footer hash display await expect(page.getByText('Base Hash:')).toBeVisible(); await expect(page.getByText('Candidate Hash:')).toBeVisible(); }); });