sprints work
This commit is contained in:
276
src/Web/StellaOps.Web/e2e/playbook-suggestions.e2e.spec.ts
Normal file
276
src/Web/StellaOps.Web/e2e/playbook-suggestions.e2e.spec.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* @file playbook-suggestions.e2e.spec.ts
|
||||
* @sprint SPRINT_20260107_006_005_FE (OM-FE-006)
|
||||
* @description E2E tests for OpsMemory playbook suggestions in decision drawer.
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Playbook Suggestions in Decision Drawer', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Mock the OpsMemory API
|
||||
await page.route('**/api/v1/opsmemory/suggestions*', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
suggestions: [
|
||||
{
|
||||
suggestedAction: 'accept_risk',
|
||||
confidence: 0.85,
|
||||
rationale: 'Similar situations resolved successfully with risk acceptance',
|
||||
evidenceCount: 5,
|
||||
matchingFactors: ['severity', 'reachability', 'componentType'],
|
||||
evidence: [
|
||||
{
|
||||
memoryId: 'mem-abc123',
|
||||
cveId: 'CVE-2023-44487',
|
||||
action: 'accept_risk',
|
||||
outcome: 'success',
|
||||
resolutionTime: 'PT4H',
|
||||
similarity: 0.92,
|
||||
},
|
||||
{
|
||||
memoryId: 'mem-def456',
|
||||
cveId: 'CVE-2023-12345',
|
||||
action: 'accept_risk',
|
||||
outcome: 'success',
|
||||
resolutionTime: 'PT2H',
|
||||
similarity: 0.87,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
suggestedAction: 'target_fix',
|
||||
confidence: 0.65,
|
||||
rationale: 'Some similar situations required fixes',
|
||||
evidenceCount: 2,
|
||||
matchingFactors: ['severity'],
|
||||
evidence: [
|
||||
{
|
||||
memoryId: 'mem-ghi789',
|
||||
cveId: 'CVE-2023-99999',
|
||||
action: 'target_fix',
|
||||
outcome: 'success',
|
||||
resolutionTime: 'P1DT4H',
|
||||
similarity: 0.70,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
situationHash: 'abc123def456',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Navigate to triage page (mock or real)
|
||||
await page.goto('/triage/findings/test-finding-123');
|
||||
});
|
||||
|
||||
test('playbook panel appears in decision drawer', async ({ page }) => {
|
||||
// Open the decision drawer
|
||||
await page.click('[data-testid="open-decision-drawer"]');
|
||||
|
||||
// Check playbook panel is visible
|
||||
const playbookPanel = page.locator('stellaops-playbook-suggestion');
|
||||
await expect(playbookPanel).toBeVisible();
|
||||
|
||||
// Check header text
|
||||
await expect(playbookPanel.locator('.playbook-panel__title')).toContainText(
|
||||
'Past Decisions'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows suggestions with confidence badges', async ({ page }) => {
|
||||
await page.click('[data-testid="open-decision-drawer"]');
|
||||
|
||||
// Wait for suggestions to load
|
||||
await page.waitForResponse('**/api/v1/opsmemory/suggestions*');
|
||||
|
||||
// Check first suggestion
|
||||
const firstSuggestion = page.locator('.playbook-suggestion').first();
|
||||
await expect(firstSuggestion).toBeVisible();
|
||||
|
||||
// Check action badge
|
||||
await expect(
|
||||
firstSuggestion.locator('.playbook-suggestion__action')
|
||||
).toContainText('Accept Risk');
|
||||
|
||||
// Check confidence
|
||||
await expect(
|
||||
firstSuggestion.locator('.playbook-suggestion__confidence')
|
||||
).toContainText('85%');
|
||||
});
|
||||
|
||||
test('clicking "Use This Approach" pre-fills decision form', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.click('[data-testid="open-decision-drawer"]');
|
||||
await page.waitForResponse('**/api/v1/opsmemory/suggestions*');
|
||||
|
||||
// Click "Use This Approach" on first suggestion
|
||||
await page.click('.playbook-btn--use');
|
||||
|
||||
// Verify form was pre-filled
|
||||
const statusRadio = page.locator('input[name="status"][value="not_affected"]');
|
||||
await expect(statusRadio).toBeChecked();
|
||||
|
||||
// Check reason notes contain suggestion context
|
||||
const reasonText = page.locator('.reason-text');
|
||||
await expect(reasonText).toContainText('similar past decisions');
|
||||
});
|
||||
|
||||
test('expanding evidence details shows past decisions', async ({ page }) => {
|
||||
await page.click('[data-testid="open-decision-drawer"]');
|
||||
await page.waitForResponse('**/api/v1/opsmemory/suggestions*');
|
||||
|
||||
// Click "Show Details" on first suggestion
|
||||
await page.click('.playbook-btn--expand');
|
||||
|
||||
// Check evidence cards are visible
|
||||
const evidenceCards = page.locator('stellaops-evidence-card');
|
||||
await expect(evidenceCards).toHaveCount(2);
|
||||
|
||||
// Check evidence content
|
||||
const firstCard = evidenceCards.first();
|
||||
await expect(firstCard.locator('.evidence-card__cve')).toContainText(
|
||||
'CVE-2023-44487'
|
||||
);
|
||||
await expect(firstCard.locator('.evidence-card__similarity')).toContainText(
|
||||
'92%'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows empty state when no suggestions', async ({ page }) => {
|
||||
// Override route to return empty
|
||||
await page.route('**/api/v1/opsmemory/suggestions*', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
suggestions: [],
|
||||
situationHash: 'empty123',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.click('[data-testid="open-decision-drawer"]');
|
||||
await page.waitForResponse('**/api/v1/opsmemory/suggestions*');
|
||||
|
||||
// Check empty state message
|
||||
await expect(page.locator('.playbook-empty')).toContainText(
|
||||
'No similar past decisions'
|
||||
);
|
||||
});
|
||||
|
||||
test('handles API errors gracefully', async ({ page }) => {
|
||||
// Override route to return error
|
||||
await page.route('**/api/v1/opsmemory/suggestions*', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ message: 'Internal server error' }),
|
||||
});
|
||||
});
|
||||
|
||||
await page.click('[data-testid="open-decision-drawer"]');
|
||||
|
||||
// Wait for error state
|
||||
await expect(page.locator('.playbook-error')).toBeVisible();
|
||||
|
||||
// Check retry button
|
||||
const retryBtn = page.locator('.playbook-error__retry');
|
||||
await expect(retryBtn).toBeVisible();
|
||||
});
|
||||
|
||||
test('retry button refetches suggestions', async ({ page }) => {
|
||||
let callCount = 0;
|
||||
|
||||
await page.route('**/api/v1/opsmemory/suggestions*', async (route) => {
|
||||
callCount++;
|
||||
if (callCount === 1) {
|
||||
await route.fulfill({ status: 500 });
|
||||
} else {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
suggestions: [
|
||||
{
|
||||
suggestedAction: 'accept_risk',
|
||||
confidence: 0.85,
|
||||
rationale: 'Test',
|
||||
evidenceCount: 1,
|
||||
matchingFactors: [],
|
||||
evidence: [],
|
||||
},
|
||||
],
|
||||
situationHash: 'retry123',
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await page.click('[data-testid="open-decision-drawer"]');
|
||||
|
||||
// Wait for error state
|
||||
await expect(page.locator('.playbook-error')).toBeVisible();
|
||||
|
||||
// Click retry
|
||||
await page.click('.playbook-error__retry');
|
||||
|
||||
// Should now show suggestions
|
||||
await expect(page.locator('.playbook-suggestion')).toBeVisible();
|
||||
});
|
||||
|
||||
test('keyboard navigation works', async ({ page }) => {
|
||||
await page.click('[data-testid="open-decision-drawer"]');
|
||||
await page.waitForResponse('**/api/v1/opsmemory/suggestions*');
|
||||
|
||||
// Focus on playbook panel header
|
||||
await page.focus('.playbook-panel__header');
|
||||
|
||||
// Press Enter to toggle (if collapsed)
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// Tab to first suggestion's "Use This Approach" button
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
// Press Enter to use suggestion
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// Verify form was pre-filled
|
||||
const statusRadio = page.locator('input[name="status"][value="not_affected"]');
|
||||
await expect(statusRadio).toBeChecked();
|
||||
});
|
||||
|
||||
test('panel can be collapsed and expanded', async ({ page }) => {
|
||||
await page.click('[data-testid="open-decision-drawer"]');
|
||||
await page.waitForResponse('**/api/v1/opsmemory/suggestions*');
|
||||
|
||||
// Panel content should be visible
|
||||
await expect(page.locator('.playbook-panel__content')).toBeVisible();
|
||||
|
||||
// Click header to collapse
|
||||
await page.click('.playbook-panel__header');
|
||||
|
||||
// Content should be hidden
|
||||
await expect(page.locator('.playbook-panel__content')).not.toBeVisible();
|
||||
|
||||
// Click again to expand
|
||||
await page.click('.playbook-panel__header');
|
||||
|
||||
// Content should be visible again
|
||||
await expect(page.locator('.playbook-panel__content')).toBeVisible();
|
||||
});
|
||||
|
||||
test('matching factors are displayed', async ({ page }) => {
|
||||
await page.click('[data-testid="open-decision-drawer"]');
|
||||
await page.waitForResponse('**/api/v1/opsmemory/suggestions*');
|
||||
|
||||
const factors = page.locator('.playbook-suggestion__factor');
|
||||
await expect(factors).toHaveCount(3);
|
||||
await expect(factors.first()).toContainText('severity');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user