/** * Evidence Panel Micro-Interactions E2E Tests * SPRINT_0341_0001_0001 - T8: Playwright tests for EvidencePanel micro-interactions * * Tests the "Verify locally" commands, copy affordances, and ProofSpine interactions. */ import { test, expect, Page } from '@playwright/test'; // Test fixtures for deterministic evidence data const MOCK_EVIDENCE = { digest: 'sha256:abc123def456', artifactPurl: 'pkg:oci/myimage@sha256:abc123def456', sbomDigest: 'sha256:sbom789012', rekorLogIndex: 12345678, rekorLogId: 'test-rekor-log-id', bundleDigest: 'sha256:bundle456789' }; test.describe('Evidence Panel - Verify Locally Commands', () => { test.beforeEach(async ({ page }) => { // Navigate to evidence panel with mock data await page.goto('/evidence?digest=' + MOCK_EVIDENCE.digest); // Wait for evidence panel to load await page.waitForSelector('.evidence-panel'); // Navigate to Linkset tab await page.click('[role="tab"]:has-text("Linkset")'); await page.waitForSelector('.linkset-panel'); }); test('should display verify locally section when linkset has verification data', async ({ page }) => { // Arrange - Wait for verify section const verifySection = page.locator('.linkset-panel__verify'); // Assert await expect(verifySection).toBeVisible(); await expect(verifySection.locator('h4')).toHaveText('Verify Locally'); await expect(verifySection.locator('.linkset-panel__verify-description')).toContainText( 'independently verify the evidence chain' ); }); test('should display artifact signature verification command', async ({ page }) => { // Arrange const verifyCommands = page.locator('.verify-command'); // Find the artifact signature command const signatureCommand = verifyCommands.filter({ hasText: 'Verify Artifact Signature' }); // Assert await expect(signatureCommand).toBeVisible(); await expect(signatureCommand.locator('.verify-command__description')).toContainText('Cosign'); // The command should contain the artifact reference const codeBlock = signatureCommand.locator('.verify-command__code code'); await expect(codeBlock).toContainText('cosign verify'); }); test('should display SBOM attestation verification command', async ({ page }) => { // Arrange const verifyCommands = page.locator('.verify-command'); // Find the SBOM attestation command const sbomCommand = verifyCommands.filter({ hasText: 'Verify SBOM Attestation' }); // Assert await expect(sbomCommand).toBeVisible(); await expect(sbomCommand.locator('.verify-command__description')).toContainText('attestation'); // The command should contain the predicate type const codeBlock = sbomCommand.locator('.verify-command__code code'); await expect(codeBlock).toContainText('--type spdxjson'); }); test('should display Rekor transparency verification command when available', async ({ page }) => { // Arrange const verifyCommands = page.locator('.verify-command'); // Find the Rekor command const rekorCommand = verifyCommands.filter({ hasText: 'Verify Transparency Log' }); // Assert await expect(rekorCommand).toBeVisible(); await expect(rekorCommand.locator('.verify-command__description')).toContainText('Rekor'); // The command should contain rekor-cli const codeBlock = rekorCommand.locator('.verify-command__code code'); await expect(codeBlock).toContainText('rekor-cli get'); }); test('should display policy decision verification command', async ({ page }) => { // Arrange const verifyCommands = page.locator('.verify-command'); // Find the policy command const policyCommand = verifyCommands.filter({ hasText: 'Verify Policy Decision' }); // Assert await expect(policyCommand).toBeVisible(); // The command should contain stella policy const codeBlock = policyCommand.locator('.verify-command__code code'); await expect(codeBlock).toContainText('stella policy verify'); }); }); test.describe('Evidence Panel - Copy Interactions', () => { test.beforeEach(async ({ page }) => { await page.goto('/evidence?digest=' + MOCK_EVIDENCE.digest); await page.waitForSelector('.evidence-panel'); await page.click('[role="tab"]:has-text("Linkset")'); await page.waitForSelector('.linkset-panel__verify'); }); test('should copy verification command on copy button click', async ({ page }) => { // Arrange const copyButton = page.locator('.verify-command__copy').first(); // Act await copyButton.click(); // Assert - button should show "Copied!" state await expect(copyButton).toHaveText('Copied!'); await expect(copyButton).toHaveClass(/copied/); }); test('should reset copy button state after delay', async ({ page }) => { // Arrange const copyButton = page.locator('.verify-command__copy').first(); // Act await copyButton.click(); await expect(copyButton).toHaveText('Copied!'); // Wait for reset (typically 2-3 seconds) await page.waitForTimeout(3500); // Assert - button should reset to "Copy" await expect(copyButton).toHaveText('Copy'); await expect(copyButton).not.toHaveClass(/copied/); }); test('should copy correct command text to clipboard', async ({ page, context }) => { // Grant clipboard permissions await context.grantPermissions(['clipboard-read', 'clipboard-write']); // Arrange const firstCommand = page.locator('.verify-command').first(); const expectedCommand = await firstCommand.locator('.verify-command__code code').textContent(); // Act await firstCommand.locator('.verify-command__copy').click(); // Assert - check clipboard content const clipboardText = await page.evaluate(() => navigator.clipboard.readText()); expect(clipboardText).toBe(expectedCommand?.trim()); }); test('should be keyboard accessible', async ({ page }) => { // Arrange const copyButton = page.locator('.verify-command__copy').first(); // Act - focus and press Enter await copyButton.focus(); await page.keyboard.press('Enter'); // Assert await expect(copyButton).toHaveText('Copied!'); }); test('should have proper aria-label for copy button', async ({ page }) => { // Arrange const copyButton = page.locator('.verify-command__copy').first(); // Assert - initial state await expect(copyButton).toHaveAttribute('aria-label', 'Copy command'); // Act await copyButton.click(); // Assert - copied state await expect(copyButton).toHaveAttribute('aria-label', 'Copied!'); }); }); test.describe('Evidence Panel - ProofSpine Component', () => { test.beforeEach(async ({ page }) => { await page.goto('/evidence?digest=' + MOCK_EVIDENCE.digest); await page.waitForSelector('.evidence-panel'); await page.click('[role="tab"]:has-text("Linkset")'); }); test('should display bundle hash in ProofSpine', async ({ page }) => { // The ProofSpine should show the evidence bundle digest const proofSpine = page.locator('.linkset-panel__provenance'); await expect(proofSpine).toBeVisible(); // Check for bundle hash display const bundleHash = proofSpine.locator('code').filter({ hasText: /sha256:/ }); await expect(bundleHash.first()).toBeVisible(); }); test('should truncate long hashes with copy on click', async ({ page }) => { // Find truncated hash const truncatedHash = page.locator('.linkset-panel__provenance code').first(); // Verify it shows truncated form const text = await truncatedHash.textContent(); expect(text?.length).toBeLessThan(64); // SHA256 is 64 chars }); }); test.describe('Evidence Panel - Tab Navigation', () => { test.beforeEach(async ({ page }) => { await page.goto('/evidence?digest=' + MOCK_EVIDENCE.digest); await page.waitForSelector('.evidence-panel'); }); test('should support keyboard navigation between tabs', async ({ page }) => { // Focus first tab const firstTab = page.locator('[role="tab"]').first(); await firstTab.focus(); // Press right arrow to move to next tab await page.keyboard.press('ArrowRight'); // Verify focus moved const focusedElement = page.locator(':focus'); await expect(focusedElement).toHaveAttribute('role', 'tab'); }); test('should announce tab content changes to screen readers', async ({ page }) => { // The tabpanel should have proper aria attributes const tabpanel = page.locator('[role="tabpanel"]'); await expect(tabpanel).toHaveAttribute('aria-label'); }); }); test.describe('Evidence Panel - Responsive Behavior', () => { test('should stack verify commands on mobile viewport', async ({ page }) => { // Set mobile viewport await page.setViewportSize({ width: 375, height: 667 }); await page.goto('/evidence?digest=' + MOCK_EVIDENCE.digest); await page.waitForSelector('.evidence-panel'); await page.click('[role="tab"]:has-text("Linkset")'); // Verify commands container should be flex column const commandsContainer = page.locator('.verify-commands'); const display = await commandsContainer.evaluate((el) => window.getComputedStyle(el).flexDirection ); expect(display).toBe('column'); }); test('should wrap long command text on small screens', async ({ page }) => { // Set small viewport await page.setViewportSize({ width: 375, height: 667 }); await page.goto('/evidence?digest=' + MOCK_EVIDENCE.digest); await page.click('[role="tab"]:has-text("Linkset")'); // Command code should wrap const codeBlock = page.locator('.verify-command__code').first(); const whiteSpace = await codeBlock.evaluate((el) => window.getComputedStyle(el).whiteSpace ); expect(whiteSpace).toBe('pre-wrap'); }); }); test.describe('Evidence Panel - Error States', () => { test('should not show verify section when no verification data available', async ({ page }) => { // Navigate to evidence without Rekor/signature data await page.goto('/evidence?digest=sha256:nosig123'); await page.waitForSelector('.evidence-panel'); await page.click('[role="tab"]:has-text("Linkset")'); // Verify section should be hidden or empty const verifySection = page.locator('.linkset-panel__verify'); // Either hidden or shows no commands const verifyCommands = page.locator('.verify-command'); const count = await verifyCommands.count(); if (count === 0) { await expect(verifySection).not.toBeVisible(); } }); test('should handle clipboard API failure gracefully', async ({ page, context }) => { // Deny clipboard permissions await context.clearPermissions(); await page.goto('/evidence?digest=' + MOCK_EVIDENCE.digest); await page.click('[role="tab"]:has-text("Linkset")'); // Click copy - should not crash const copyButton = page.locator('.verify-command__copy').first(); await copyButton.click(); // Should show error state or fallback // Implementation may vary - check it doesn't throw await expect(page.locator('.evidence-panel')).toBeVisible(); }); });