Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
318
src/Web/StellaOps.Web/e2e/binary-resolution.e2e.spec.ts
Normal file
318
src/Web/StellaOps.Web/e2e/binary-resolution.e2e.spec.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* Binary Resolution UI E2E Tests.
|
||||
* Sprint: SPRINT_1227_0003_0001 (Backport-Aware Resolution UI)
|
||||
* Task: T9 - E2E tests
|
||||
*
|
||||
* Tests the complete binary resolution flow:
|
||||
* 1. Viewing a vulnerability with binary resolution
|
||||
* 2. Opening the evidence drawer
|
||||
* 3. Viewing function diffs
|
||||
* 4. Copying attestation
|
||||
*/
|
||||
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
test.describe('Binary Resolution UI', () => {
|
||||
const vulnUrl = '/vulnerabilities/CVE-2024-0001';
|
||||
|
||||
test.describe('Resolution Chip Display', () => {
|
||||
test('displays resolution chip for vulnerability with binary resolution', async ({ page }) => {
|
||||
await page.goto(vulnUrl);
|
||||
|
||||
// Wait for resolution chip to appear
|
||||
const chip = page.locator('stella-resolution-chip');
|
||||
await expect(chip).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows Fixed (backport) label for fingerprint matches', async ({ page }) => {
|
||||
await page.goto(vulnUrl);
|
||||
|
||||
const chip = page.locator('.resolution-chip');
|
||||
await expect(chip).toContainText('backport');
|
||||
});
|
||||
|
||||
test('displays correct icon for backport detection', async ({ page }) => {
|
||||
await page.goto(vulnUrl);
|
||||
|
||||
const icon = page.locator('.resolution-chip__icon');
|
||||
await expect(icon).toContainText('🔍');
|
||||
});
|
||||
|
||||
test('shows info button when evidence is available', async ({ page }) => {
|
||||
await page.goto(vulnUrl);
|
||||
|
||||
const infoButton = page.locator('.resolution-chip__info-btn');
|
||||
await expect(infoButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows correct color for Fixed status', async ({ page }) => {
|
||||
await page.goto(vulnUrl);
|
||||
|
||||
const chip = page.locator('.resolution-chip');
|
||||
await expect(chip).toHaveClass(/resolution-chip--fixed/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Evidence Drawer Interaction', () => {
|
||||
test('opens evidence drawer when Show evidence button clicked', async ({ page }) => {
|
||||
await page.goto(vulnUrl);
|
||||
|
||||
// Click the show evidence button
|
||||
await page.locator('.evidence-toggle').click();
|
||||
|
||||
// Verify drawer is visible
|
||||
const drawer = page.locator('.evidence-drawer');
|
||||
await expect(drawer).toHaveClass(/evidence-drawer--open/);
|
||||
});
|
||||
|
||||
test('opens drawer when info button on chip is clicked', async ({ page }) => {
|
||||
await page.goto(vulnUrl);
|
||||
|
||||
// Click info button on chip
|
||||
await page.locator('.resolution-chip__info-btn').click();
|
||||
|
||||
// Verify drawer is open
|
||||
const drawer = page.locator('.evidence-drawer');
|
||||
await expect(drawer).toHaveClass(/evidence-drawer--open/);
|
||||
});
|
||||
|
||||
test('closes drawer when X button clicked', async ({ page }) => {
|
||||
await page.goto(vulnUrl);
|
||||
|
||||
// Open drawer
|
||||
await page.locator('.evidence-toggle').click();
|
||||
await expect(page.locator('.evidence-drawer')).toHaveClass(/evidence-drawer--open/);
|
||||
|
||||
// Close drawer
|
||||
await page.locator('.evidence-drawer__close').click();
|
||||
|
||||
// Verify drawer is closed
|
||||
await expect(page.locator('.evidence-drawer')).not.toHaveClass(/evidence-drawer--open/);
|
||||
});
|
||||
|
||||
test('closes drawer when backdrop clicked', async ({ page }) => {
|
||||
await page.goto(vulnUrl);
|
||||
|
||||
// Open drawer
|
||||
await page.locator('.evidence-toggle').click();
|
||||
|
||||
// Click backdrop
|
||||
await page.locator('.evidence-drawer__backdrop').click();
|
||||
|
||||
// Verify drawer is closed
|
||||
await expect(page.locator('.evidence-drawer')).not.toHaveClass(/evidence-drawer--open/);
|
||||
});
|
||||
|
||||
test('closes drawer when Escape key pressed', async ({ page }) => {
|
||||
await page.goto(vulnUrl);
|
||||
|
||||
// Open drawer
|
||||
await page.locator('.evidence-toggle').click();
|
||||
|
||||
// Press Escape
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// Verify drawer is closed
|
||||
await expect(page.locator('.evidence-drawer')).not.toHaveClass(/evidence-drawer--open/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Evidence Content Display', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(vulnUrl);
|
||||
await page.locator('.evidence-toggle').click();
|
||||
});
|
||||
|
||||
test('displays match method', async ({ page }) => {
|
||||
await expect(page.locator('.evidence-drawer')).toContainText('Match Method');
|
||||
await expect(page.locator('.evidence-drawer__value--highlight')).toContainText('Fingerprint');
|
||||
});
|
||||
|
||||
test('displays confidence gauge', async ({ page }) => {
|
||||
const gauge = page.locator('.confidence-gauge');
|
||||
await expect(gauge).toBeVisible();
|
||||
|
||||
const label = page.locator('.confidence-gauge__label');
|
||||
await expect(label).toContainText('%');
|
||||
});
|
||||
|
||||
test('displays advisory link', async ({ page }) => {
|
||||
const advisoryLink = page.locator('.evidence-drawer__link').first();
|
||||
await expect(advisoryLink).toBeVisible();
|
||||
});
|
||||
|
||||
test('opens advisory in new tab when clicked', async ({ page, context }) => {
|
||||
const advisoryLink = page.locator('.evidence-drawer__link').first();
|
||||
|
||||
// Listen for new page
|
||||
const [newPage] = await Promise.all([
|
||||
context.waitForEvent('page'),
|
||||
advisoryLink.click(),
|
||||
]);
|
||||
|
||||
expect(newPage.url()).toContain('debian.org/security');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Function Diff Interaction', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(vulnUrl);
|
||||
await page.locator('.evidence-toggle').click();
|
||||
});
|
||||
|
||||
test('displays changed functions list', async ({ page }) => {
|
||||
const functionList = page.locator('.function-list');
|
||||
await expect(functionList).toBeVisible();
|
||||
|
||||
const items = page.locator('.function-list__item');
|
||||
await expect(items.first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows View Diff button for modified functions', async ({ page }) => {
|
||||
const diffButton = page.locator('.function-list__diff-btn').first();
|
||||
await expect(diffButton).toBeVisible();
|
||||
await expect(diffButton).toHaveText('View Diff');
|
||||
});
|
||||
|
||||
test('function names are displayed', async ({ page }) => {
|
||||
const functionName = page.locator('.function-list__name').first();
|
||||
await expect(functionName).toBeVisible();
|
||||
});
|
||||
|
||||
test('change type badge is displayed', async ({ page }) => {
|
||||
const changeType = page.locator('.function-list__type').first();
|
||||
await expect(changeType).toBeVisible();
|
||||
await expect(changeType).toHaveText(/Modified|Added|Removed/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('DSSE Attestation Handling', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(vulnUrl);
|
||||
await page.locator('.evidence-toggle').click();
|
||||
});
|
||||
|
||||
test('displays attestation section when available', async ({ page }) => {
|
||||
await expect(page.locator('.evidence-drawer')).toContainText('Attestation');
|
||||
});
|
||||
|
||||
test('shows signer information', async ({ page }) => {
|
||||
await expect(page.locator('.evidence-drawer')).toContainText('Signer');
|
||||
});
|
||||
|
||||
test('shows Copy DSSE Envelope button', async ({ page }) => {
|
||||
const copyButton = page.locator('.evidence-drawer__copy-btn');
|
||||
await expect(copyButton).toBeVisible();
|
||||
await expect(copyButton).toContainText('Copy DSSE Envelope');
|
||||
});
|
||||
|
||||
test('shows Copied feedback after clicking copy button', async ({ page }) => {
|
||||
const copyButton = page.locator('.evidence-drawer__copy-btn');
|
||||
await copyButton.click();
|
||||
|
||||
await expect(copyButton).toContainText('Copied!');
|
||||
});
|
||||
|
||||
test('resets copy button text after 2 seconds', async ({ page }) => {
|
||||
const copyButton = page.locator('.evidence-drawer__copy-btn');
|
||||
await copyButton.click();
|
||||
|
||||
await expect(copyButton).toContainText('Copied!');
|
||||
|
||||
// Wait for reset
|
||||
await page.waitForTimeout(2500);
|
||||
await expect(copyButton).toContainText('Copy DSSE Envelope');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Responsive Behavior', () => {
|
||||
test('drawer takes full width on mobile', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.goto(vulnUrl);
|
||||
await page.locator('.evidence-toggle').click();
|
||||
|
||||
const drawer = page.locator('.evidence-drawer');
|
||||
const box = await drawer.boundingBox();
|
||||
|
||||
expect(box?.width).toBeGreaterThan(300);
|
||||
});
|
||||
|
||||
test('drawer has max-width on desktop', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
await page.goto(vulnUrl);
|
||||
await page.locator('.evidence-toggle').click();
|
||||
|
||||
const drawer = page.locator('.evidence-drawer');
|
||||
const box = await drawer.boundingBox();
|
||||
|
||||
expect(box?.width).toBeLessThanOrEqual(400);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Dark Mode Support', () => {
|
||||
test('applies dark theme styles when dark mode is enabled', async ({ page }) => {
|
||||
await page.goto(vulnUrl);
|
||||
|
||||
// Enable dark mode (assuming it's toggled via a class on body)
|
||||
await page.evaluate(() => {
|
||||
document.body.classList.add('dark-theme');
|
||||
});
|
||||
|
||||
await page.locator('.evidence-toggle').click();
|
||||
|
||||
// Drawer should have dark mode styles applied via :host-context
|
||||
const drawer = page.locator('.evidence-drawer');
|
||||
await expect(drawer).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Error Handling', () => {
|
||||
test('handles missing resolution gracefully', async ({ page }) => {
|
||||
// Mock API to return no resolution
|
||||
await page.route('**/api/v1/resolve/**', route => {
|
||||
route.fulfill({
|
||||
status: 404,
|
||||
body: JSON.stringify({ error: 'Not found' }),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(vulnUrl);
|
||||
|
||||
// Resolution chip should not be visible
|
||||
const chip = page.locator('stella-resolution-chip');
|
||||
await expect(chip).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('handles API errors gracefully', async ({ page }) => {
|
||||
// Mock API to return error
|
||||
await page.route('**/api/v1/resolve/**', route => {
|
||||
route.fulfill({
|
||||
status: 500,
|
||||
body: JSON.stringify({ error: 'Internal server error' }),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(vulnUrl);
|
||||
|
||||
// Page should still render without resolution
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Keyboard Navigation', () => {
|
||||
test('can navigate through drawer with Tab key', async ({ page }) => {
|
||||
await page.goto('/vulnerabilities/CVE-2024-0001');
|
||||
await page.locator('.evidence-toggle').click();
|
||||
|
||||
// Focus should be manageable via Tab
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
// Should be able to close with Enter on close button
|
||||
await page.locator('.evidence-drawer__close').focus();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(page.locator('.evidence-drawer')).not.toHaveClass(/evidence-drawer--open/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user