diff --git a/ccw/frontend/src/components/a2ui/AskQuestionDialog.tsx b/ccw/frontend/src/components/a2ui/AskQuestionDialog.tsx index 113dc52c..1d515755 100644 --- a/ccw/frontend/src/components/a2ui/AskQuestionDialog.tsx +++ b/ccw/frontend/src/components/a2ui/AskQuestionDialog.tsx @@ -12,13 +12,13 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from '@/components/ui/dialog'; +} from '@/components/ui/Dialog'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Textarea } from '@/components/ui/Textarea'; -import { Checkbox } from '@/components/ui/checkbox'; +import { Checkbox } from '@/components/ui/Checkbox'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; -import { Label } from '@/components/ui/label'; +import { Label } from '@/components/ui/Label'; import { AlertCircle } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useNotificationStore } from '@/stores'; diff --git a/ccw/frontend/src/components/a2ui/index.ts b/ccw/frontend/src/components/a2ui/index.ts index eccfb3b8..60f8e92d 100644 --- a/ccw/frontend/src/components/a2ui/index.ts +++ b/ccw/frontend/src/components/a2ui/index.ts @@ -4,4 +4,3 @@ // Export all A2UI-related components export { AskQuestionDialog } from './AskQuestionDialog'; -export { default as AskQuestionDialog } from './AskQuestionDialog'; diff --git a/ccw/frontend/src/components/shared/LogBlock/LogBlockList.tsx b/ccw/frontend/src/components/shared/LogBlock/LogBlockList.tsx index 95e4f09d..e7346ee3 100644 --- a/ccw/frontend/src/components/shared/LogBlock/LogBlockList.tsx +++ b/ccw/frontend/src/components/shared/LogBlock/LogBlockList.tsx @@ -29,7 +29,7 @@ export function LogBlockList({ executionId, className }: LogBlockListProps) { // This avoids duplicate logic and leverages store-side caching const blocks = useCliStreamStore( (state) => executionId ? state.getBlocks(executionId) : [], - (a, b) => a === b // Shallow comparison - arrays are cached in store + (a: LogBlockData[], b: LogBlockData[]) => a === b // Shallow comparison - arrays are cached in store ); // Get execution status for empty state display diff --git a/ccw/frontend/tests/e2e/a2ui-notifications.spec.ts b/ccw/frontend/tests/e2e/a2ui-notifications.spec.ts index b6aa5f53..681859ee 100644 --- a/ccw/frontend/tests/e2e/a2ui-notifications.spec.ts +++ b/ccw/frontend/tests/e2e/a2ui-notifications.spec.ts @@ -606,11 +606,11 @@ test.describe('[A2UI Notifications] - E2E Rendering Tests', () => { }); // Check for email field - await expect(page.getByPlaceholderText('Email address')).toBeVisible(); - await expect(page.getByPlaceholderText('Email address')).toHaveAttribute('type', 'email'); + await expect(page.getByPlaceholder('Email address')).toBeVisible(); + await expect(page.getByPlaceholder('Email address')).toHaveAttribute('type', 'email'); // Check for password field - await expect(page.getByPlaceholderText('Password')).toBeVisible(); - await expect(page.getByPlaceholderText('Password')).toHaveAttribute('type', 'password'); + await expect(page.getByPlaceholder('Password')).toBeVisible(); + await expect(page.getByPlaceholder('Password')).toHaveAttribute('type', 'password'); }); }); diff --git a/ccw/frontend/tests/e2e/ask-question.spec.ts b/ccw/frontend/tests/e2e/ask-question.spec.ts index 5362ada9..6b60bae1 100644 --- a/ccw/frontend/tests/e2e/ask-question.spec.ts +++ b/ccw/frontend/tests/e2e/ask-question.spec.ts @@ -254,7 +254,7 @@ test.describe('[ask_question] - E2E Workflow Tests', () => { await expect(page.getByText('Please enter your name')).toBeVisible(); // Type in text field - const inputField = page.getByPlaceholderText('Enter your name'); + const inputField = page.getByPlaceholder('Enter your name'); await inputField.fill('John Doe'); // Submit diff --git a/ccw/frontend/tests/e2e/cli-config.spec.ts b/ccw/frontend/tests/e2e/cli-config.spec.ts new file mode 100644 index 00000000..b3444ecb --- /dev/null +++ b/ccw/frontend/tests/e2e/cli-config.spec.ts @@ -0,0 +1,431 @@ +// ======================================== +// E2E Tests: CLI Configuration Management +// ======================================== +// End-to-end tests for CLI endpoints and tools configuration + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[CLI Config] - CLI Configuration Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display CLI endpoints list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI config page + await page.goto('/settings/cli/config', { waitUntil: 'networkidle' as const }); + + // Look for endpoints list container + const endpointsList = page.getByTestId('cli-endpoints-list').or( + page.locator('.cli-endpoints-list') + ); + + const isVisible = await endpointsList.isVisible().catch(() => false); + + if (isVisible) { + // Verify endpoint items exist + const endpointItems = page.getByTestId(/endpoint-item|cli-endpoint/).or( + page.locator('.endpoint-item') + ); + + const itemCount = await endpointItems.count(); + expect(itemCount).toBeGreaterThanOrEqual(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should update CLI endpoint configuration', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI config page + await page.goto('/settings/cli/config', { waitUntil: 'networkidle' as const }); + + // Look for existing endpoint + const endpointItems = page.getByTestId(/endpoint-item|cli-endpoint/).or( + page.locator('.endpoint-item') + ); + + const itemCount = await endpointItems.count(); + + if (itemCount > 0) { + const firstEndpoint = endpointItems.first(); + + // Look for edit button + const editButton = firstEndpoint.getByRole('button', { name: /edit|configure|settings/i }).or( + firstEndpoint.getByTestId('edit-endpoint-button') + ); + + const hasEditButton = await editButton.isVisible().catch(() => false); + + if (hasEditButton) { + await editButton.click(); + + // Look for config dialog/form + const dialog = page.getByRole('dialog').filter({ hasText: /configure|edit|settings/i }); + const hasDialog = await dialog.isVisible().catch(() => false); + + if (hasDialog) { + // Modify configuration + const enabledSwitch = dialog.getByRole('switch').first(); + const hasSwitch = await enabledSwitch.isVisible().catch(() => false); + + if (hasSwitch) { + await enabledSwitch.click(); + } + + const saveButton = page.getByRole('button', { name: /save|update/i }); + await saveButton.click(); + + // Verify success message + + const successMessage = page.getByText(/saved|updated|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should create new CLI endpoint', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI config page + await page.goto('/settings/cli/config', { waitUntil: 'networkidle' as const }); + + // Look for create endpoint button + const createButton = page.getByRole('button', { name: /create|new|add endpoint/i }).or( + page.getByTestId('create-endpoint-button') + ); + + const hasCreateButton = await createButton.isVisible().catch(() => false); + + if (hasCreateButton) { + await createButton.click(); + + // Look for create endpoint dialog/form + const dialog = page.getByRole('dialog').filter({ hasText: /create|new endpoint|add endpoint/i }); + const form = page.getByTestId('create-endpoint-form'); + + const hasDialog = await dialog.isVisible().catch(() => false); + const hasForm = await form.isVisible().catch(() => false); + + if (hasDialog || hasForm) { + // Fill in endpoint details + const nameInput = page.getByRole('textbox', { name: /name|id/i }).or( + page.getByLabel(/name|id/i) + ); + + const hasNameInput = await nameInput.isVisible().catch(() => false); + + if (hasNameInput) { + await nameInput.fill('e2e-test-endpoint'); + + // Select type if available + const typeSelect = page.getByRole('combobox', { name: /type/i }); + const hasTypeSelect = await typeSelect.isVisible().catch(() => false); + + if (hasTypeSelect) { + const typeOptions = await typeSelect.locator('option').count(); + if (typeOptions > 0) { + await typeSelect.selectOption({ index: 0 }); + } + } + + const submitButton = page.getByRole('button', { name: /create|save|submit/i }); + await submitButton.click(); + + // Verify endpoint was created + + const successMessage = page.getByText(/created|success/i).or( + page.getByTestId('success-message') + ); + + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should delete CLI endpoint', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI config page + await page.goto('/settings/cli/config', { waitUntil: 'networkidle' as const }); + + // Look for existing endpoint + const endpointItems = page.getByTestId(/endpoint-item|cli-endpoint/).or( + page.locator('.endpoint-item') + ); + + const itemCount = await endpointItems.count(); + + if (itemCount > 0) { + const firstEndpoint = endpointItems.first(); + + // Look for delete button + const deleteButton = firstEndpoint.getByRole('button', { name: /delete|remove/i }).or( + firstEndpoint.getByTestId('delete-button') + ); + + const hasDeleteButton = await deleteButton.isVisible().catch(() => false); + + if (hasDeleteButton) { + await deleteButton.click(); + + // Confirm delete if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /delete|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /delete|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify success message + + const successMessage = page.getByText(/deleted|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should toggle CLI endpoint enabled status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI config page + await page.goto('/settings/cli/config', { waitUntil: 'networkidle' as const }); + + // Look for endpoint items + const endpointItems = page.getByTestId(/endpoint-item|cli-endpoint/).or( + page.locator('.endpoint-item') + ); + + const itemCount = await endpointItems.count(); + + if (itemCount > 0) { + const firstEndpoint = endpointItems.first(); + + // Look for toggle switch + const toggleSwitch = firstEndpoint.getByRole('switch').or( + firstEndpoint.getByTestId('endpoint-toggle') + ).or( + firstEndpoint.getByRole('button', { name: /enable|disable|toggle/i }) + ); + + const hasToggle = await toggleSwitch.isVisible().catch(() => false); + + if (hasToggle) { + // Get initial state + const initialState = await toggleSwitch.getAttribute('aria-checked'); + const initialChecked = initialState === 'true'; + + // Toggle the endpoint + await toggleSwitch.click(); + + // Wait for update + + // Verify state changed + const newState = await toggleSwitch.getAttribute('aria-checked'); + const newChecked = newState === 'true'; + + expect(newChecked).toBe(!initialChecked); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should display CLI tools configuration', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI config page + await page.goto('/settings/cli/config', { waitUntil: 'networkidle' as const }); + + // Look for tools configuration section + const toolsSection = page.getByTestId('cli-tools-config').or( + page.getByText(/tools configuration|cli tools/i) + ); + + const isVisible = await toolsSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify tool items are displayed + const toolItems = page.getByTestId(/tool-item|cli-tool/).or( + toolsSection.locator('.tool-item') + ); + + const toolCount = await toolItems.count(); + expect(toolCount).toBeGreaterThan(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should update CLI tools configuration', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI config page + await page.goto('/settings/cli/config', { waitUntil: 'networkidle' as const }); + + // Look for tools configuration section + const toolsSection = page.getByTestId('cli-tools-config').or( + page.getByText(/tools configuration|cli tools/i) + ); + + const isVisible = await toolsSection.isVisible().catch(() => false); + + if (isVisible) { + // Look for edit config button + const editButton = toolsSection.getByRole('button', { name: /edit|configure/i }).or( + page.getByTestId('edit-tools-config-button') + ); + + const hasEditButton = await editButton.isVisible().catch(() => false); + + if (hasEditButton) { + await editButton.click(); + + // Look for config dialog + const dialog = page.getByRole('dialog').filter({ hasText: /configure|settings/i }); + const hasDialog = await dialog.isVisible().catch(() => false); + + if (hasDialog) { + // Modify configuration + const primaryModelInput = page.getByRole('textbox', { name: /primary model/i }); + const hasModelInput = await primaryModelInput.isVisible().catch(() => false); + + if (hasModelInput) { + await primaryModelInput.clear(); + await primaryModelInput.fill('gemini-2.5-flash'); + } + + const saveButton = page.getByRole('button', { name: /save|update/i }); + await saveButton.click(); + + // Verify success + + const successMessage = page.getByText(/saved|updated|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should display endpoint type', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI config page + await page.goto('/settings/cli/config', { waitUntil: 'networkidle' as const }); + + // Look for endpoint items + const endpointItems = page.getByTestId(/endpoint-item|cli-endpoint/).or( + page.locator('.endpoint-item') + ); + + const itemCount = await endpointItems.count(); + + if (itemCount > 0) { + const firstEndpoint = endpointItems.first(); + + // Look for type badge + const typeBadge = firstEndpoint.getByTestId('endpoint-type').or( + firstEndpoint.locator('*').filter({ hasText: /litellm|custom|wrapper|api/i }) + ); + + const hasType = await typeBadge.isVisible().catch(() => false); + + if (hasType) { + const text = await typeBadge.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should handle config API errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/cli/**', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to CLI config page + await page.reload({ waitUntil: 'networkidle' as const }); + + // Look for error indicator + const errorIndicator = page.getByText(/error|failed|unable to load/i).or( + page.getByTestId('error-state') + ); + + const hasError = await errorIndicator.isVisible().catch(() => false); + + // Restore routing + await page.unroute('**/api/cli/**'); + + // Error should be displayed or handled gracefully + expect(hasError).toBe(true); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/cli'], allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should validate endpoint configuration', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI config page + await page.goto('/settings/cli/config', { waitUntil: 'networkidle' as const }); + + // Look for create endpoint button + const createButton = page.getByRole('button', { name: /create|new|add/i }); + const hasCreateButton = await createButton.isVisible().catch(() => false); + + if (hasCreateButton) { + await createButton.click(); + + // Try to submit without required fields + const submitButton = page.getByRole('button', { name: /create|save|submit/i }); + const hasSubmit = await submitButton.isVisible().catch(() => false); + + if (hasSubmit) { + await submitButton.click(); + + // Look for validation error + + const errorMessage = page.getByText(/required|invalid|missing/i); + const hasError = await errorMessage.isVisible().catch(() => false); + expect(hasError).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/cli-history.spec.ts b/ccw/frontend/tests/e2e/cli-history.spec.ts new file mode 100644 index 00000000..6d5de3fd --- /dev/null +++ b/ccw/frontend/tests/e2e/cli-history.spec.ts @@ -0,0 +1,420 @@ +// ======================================== +// E2E Tests: CLI History Management +// ======================================== +// End-to-end tests for CLI execution history, detail view, and delete operations + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[CLI History] - CLI Execution History Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display CLI execution history', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI history page + await page.goto('/settings/cli/history', { waitUntil: 'networkidle' as const }); + + // Look for history list container + const historyList = page.getByTestId('cli-history-list').or( + page.locator('.cli-history-list') + ); + + const isVisible = await historyList.isVisible().catch(() => false); + + if (isVisible) { + // Verify history items exist or empty state is shown + const historyItems = page.getByTestId(/history-item|execution-item/).or( + page.locator('.history-item') + ); + + const itemCount = await historyItems.count(); + + if (itemCount === 0) { + const emptyState = page.getByTestId('empty-state').or( + page.getByText(/no history|no executions/i) + ); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should display CLI execution detail', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI history page + await page.goto('/settings/cli/history', { waitUntil: 'networkidle' as const }); + + // Look for history items + const historyItems = page.getByTestId(/history-item|execution-item/).or( + page.locator('.history-item') + ); + + const itemCount = await historyItems.count(); + + if (itemCount > 0) { + const firstItem = historyItems.first(); + + // Click on item to view detail + await firstItem.click(); + + // Verify detail view loads + await page.waitForURL(/\/history\//); + + const detailContainer = page.getByTestId('execution-detail').or( + page.locator('.execution-detail') + ); + + const hasDetail = await detailContainer.isVisible().catch(() => false); + expect(hasDetail).toBe(true); + + // Verify conversation turns are displayed + const conversationTurns = page.getByTestId(/turn|conversation/).or( + page.locator('.conversation-turn') + ); + + const turnCount = await conversationTurns.count(); + expect(turnCount).toBeGreaterThan(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should delete single execution', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI history page + await page.goto('/settings/cli/history', { waitUntil: 'networkidle' as const }); + + // Look for history items + const historyItems = page.getByTestId(/history-item|execution-item/).or( + page.locator('.history-item') + ); + + const initialCount = await historyItems.count(); + + if (initialCount > 0) { + const firstItem = historyItems.first(); + + // Look for delete button + const deleteButton = firstItem.getByRole('button', { name: /delete|remove/i }).or( + firstItem.getByTestId('delete-button') + ); + + const hasDeleteButton = await deleteButton.isVisible().catch(() => false); + + if (hasDeleteButton) { + await deleteButton.click(); + + // Confirm delete if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /delete|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /delete|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify success message + + const successMessage = page.getByText(/deleted|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should delete executions by tool', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI history page + await page.goto('/settings/cli/history', { waitUntil: 'networkidle' as const }); + + // Look for bulk delete by tool option + const bulkDeleteButton = page.getByRole('button', { name: /delete by tool|bulk delete/i }).or( + page.getByTestId('bulk-delete-button') + ); + + const hasBulkDelete = await bulkDeleteButton.isVisible().catch(() => false); + + if (hasBulkDelete) { + await bulkDeleteButton.click(); + + // Look for tool selection dialog + const dialog = page.getByRole('dialog').filter({ hasText: /select tool|choose tool/i }); + const hasDialog = await dialog.isVisible().catch(() => false); + + if (hasDialog) { + // Select a tool + const toolSelect = page.getByRole('combobox', { name: /tool/i }); + const toolOptions = await toolSelect.locator('option').count(); + + if (toolOptions > 0) { + await toolSelect.selectOption({ index: 0 }); + + const confirmButton = page.getByRole('button', { name: /delete|confirm/i }); + await confirmButton.click(); + + // Verify success message + + const successMessage = page.getByText(/deleted|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should delete all history', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI history page + await page.goto('/settings/cli/history', { waitUntil: 'networkidle' as const }); + + // Look for delete all button + const deleteAllButton = page.getByRole('button', { name: /delete all|clear history/i }).or( + page.getByTestId('delete-all-button') + ); + + const hasDeleteAll = await deleteAllButton.isVisible().catch(() => false); + + if (hasDeleteAll) { + await deleteAllButton.click(); + + // Confirm delete all if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /delete all|confirm|clear/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /delete|confirm|yes|clear/i }); + await confirmButton.click(); + } + + // Verify success message + + const successMessage = page.getByText(/deleted|cleared|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + + // Verify empty state is shown + const emptyState = page.getByTestId('empty-state').or( + page.getByText(/no history|no executions/i) + ); + + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should display execution metadata', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI history page + await page.goto('/settings/cli/history', { waitUntil: 'networkidle' as const }); + + // Look for history items + const historyItems = page.getByTestId(/history-item|execution-item/).or( + page.locator('.history-item') + ); + + const itemCount = await historyItems.count(); + + if (itemCount > 0) { + const firstItem = historyItems.first(); + + // Look for metadata indicators (tool, status, duration) + const toolBadge = firstItem.getByTestId('execution-tool').or( + firstItem.locator('*').filter({ hasText: /gemini|qwen|codex/i }) + ); + + const statusBadge = firstItem.getByTestId('execution-status').or( + firstItem.locator('*').filter({ hasText: /success|error|timeout/i }) + ); + + const durationBadge = firstItem.getByTestId('execution-duration').or( + firstItem.locator('*').filter({ hasText: /\d+ms|\d+s/i }) + ); + + const hasMetadata = await Promise.all([ + toolBadge.isVisible().catch(() => false), + statusBadge.isVisible().catch(() => false), + durationBadge.isVisible().catch(() => false), + ]); + + // At least some metadata should be visible + expect(hasMetadata.some(Boolean)).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should filter history by tool', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI history page + await page.goto('/settings/cli/history', { waitUntil: 'networkidle' as const }); + + // Look for tool filter + const toolFilter = page.getByRole('combobox', { name: /tool|filter by/i }).or( + page.getByTestId('tool-filter') + ); + + const hasToolFilter = await toolFilter.isVisible().catch(() => false); + + if (hasToolFilter) { + // Check if there are tool options + const toolOptions = await toolFilter.locator('option').count(); + + if (toolOptions > 1) { + await toolFilter.selectOption({ index: 1 }); + + // Wait for filtered results + + const historyItems = page.getByTestId(/history-item|execution-item/).or( + page.locator('.history-item') + ); + + const historyCount = await historyItems.count(); + expect(historyCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should search history', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI history page + await page.goto('/settings/cli/history', { waitUntil: 'networkidle' as const }); + + // Look for search input + const searchInput = page.getByRole('textbox', { name: /search|find/i }).or( + page.getByTestId('history-search') + ); + + const hasSearch = await searchInput.isVisible().catch(() => false); + + if (hasSearch) { + await searchInput.fill('test'); + + // Wait for search results + + // Search should either show results or no results message + const noResults = page.getByText(/no results|not found/i); + const hasNoResults = await noResults.isVisible().catch(() => false); + + const historyItems = page.getByTestId(/history-item|execution-item/).or( + page.locator('.history-item') + ); + + const historyCount = await historyItems.count(); + + // Either no results message or filtered history + expect(hasNoResults || historyCount >= 0).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should display conversation turns in detail', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI history page + await page.goto('/settings/cli/history', { waitUntil: 'networkidle' as const }); + + // Look for history items + const historyItems = page.getByTestId(/history-item|execution-item/).or( + page.locator('.history-item') + ); + + const itemCount = await historyItems.count(); + + if (itemCount > 0) { + const firstItem = historyItems.first(); + await firstItem.click(); + + // Wait for detail view + await page.waitForURL(/\/history\//); + + // Look for conversation turns + const conversationTurns = page.getByTestId(/turn|conversation/).or( + page.locator('.conversation-turn') + ); + + const turnCount = await conversationTurns.count(); + + if (turnCount > 0) { + // Verify each turn has prompt and output + const firstTurn = conversationTurns.first(); + + const promptSection = firstTurn.getByTestId('turn-prompt').or( + firstTurn.locator('.turn-prompt') + ); + + const outputSection = firstTurn.getByTestId('turn-output').or( + firstTurn.locator('.turn-output') + ); + + const hasPrompt = await promptSection.isVisible().catch(() => false); + const hasOutput = await outputSection.isVisible().catch(() => false); + + expect(hasPrompt || hasOutput).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should handle history API errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/cli/history**', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to CLI history page + await page.goto('/settings/cli/history', { waitUntil: 'networkidle' as const }); + + // Look for error indicator + const errorIndicator = page.getByText(/error|failed|unable to load/i).or( + page.getByTestId('error-state') + ); + + const hasError = await errorIndicator.isVisible().catch(() => false); + + // Restore routing + await page.unroute('**/api/cli/history**'); + + // Error should be displayed or handled gracefully + expect(hasError).toBe(true); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/cli/history'], allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/cli-installations.spec.ts b/ccw/frontend/tests/e2e/cli-installations.spec.ts new file mode 100644 index 00000000..4e77385d --- /dev/null +++ b/ccw/frontend/tests/e2e/cli-installations.spec.ts @@ -0,0 +1,362 @@ +// ======================================== +// E2E Tests: CLI Installations Management +// ======================================== +// End-to-end tests for CLI tool installation, uninstall, and upgrade operations + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[CLI Installations] - CLI Tools Installation Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display CLI installations list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI installations page + await page.goto('/settings/cli/installations', { waitUntil: 'networkidle' as const }); + + // Look for installations list container + const installationsList = page.getByTestId('cli-installations-list').or( + page.locator('.cli-installations-list') + ); + + const isVisible = await installationsList.isVisible().catch(() => false); + + if (isVisible) { + // Verify installation items exist + const installationItems = page.getByTestId(/installation-item|tool-item/).or( + page.locator('.installation-item') + ); + + const itemCount = await installationItems.count(); + expect(itemCount).toBeGreaterThan(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should install CLI tool', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI installations page + await page.goto('/settings/cli/installations', { waitUntil: 'networkidle' as const }); + + // Look for a tool that is not installed + const notInstalledTools = page.locator('.installation-item').filter({ hasText: /not installed|install/i }); + + const count = await notInstalledTools.count(); + + if (count > 0) { + const firstTool = notInstalledTools.first(); + + // Look for install button + const installButton = firstTool.getByRole('button', { name: /install/i }).or( + firstTool.getByTestId('install-button') + ); + + const hasInstallButton = await installButton.isVisible().catch(() => false); + + if (hasInstallButton) { + await installButton.click(); + + // Wait for installation to start + + // Look for progress indicator + const progressIndicator = page.getByTestId('installation-progress').or( + page.getByText(/installing|progress/i) + ); + + // Installation may take time, just verify it started + const hasProgress = await progressIndicator.isVisible().catch(() => false); + + if (hasProgress) { + expect(progressIndicator).toBeVisible(); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should uninstall CLI tool', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI installations page + await page.goto('/settings/cli/installations', { waitUntil: 'networkidle' as const }); + + // Look for installed tools + const installedTools = page.locator('.installation-item').filter({ hasText: /installed|active/i }); + + const count = await installedTools.count(); + + if (count > 0) { + const firstTool = installedTools.first(); + + // Look for uninstall button + const uninstallButton = firstTool.getByRole('button', { name: /uninstall|remove/i }).or( + firstTool.getByTestId('uninstall-button') + ); + + const hasUninstallButton = await uninstallButton.isVisible().catch(() => false); + + if (hasUninstallButton) { + await uninstallButton.click(); + + // Confirm uninstall if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /uninstall|confirm|remove/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /uninstall|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify uninstallation started + + const successMessage = page.getByText(/uninstalled|removed|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should upgrade CLI tool', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI installations page + await page.goto('/settings/cli/installations', { waitUntil: 'networkidle' as const }); + + // Look for tools with available updates + const updatableTools = page.locator('.installation-item').filter({ hasText: /update|upgrade|new version/i }); + + const count = await updatableTools.count(); + + if (count > 0) { + const firstTool = updatableTools.first(); + + // Look for upgrade button + const upgradeButton = firstTool.getByRole('button', { name: /upgrade|update/i }).or( + firstTool.getByTestId('upgrade-button') + ); + + const hasUpgradeButton = await upgradeButton.isVisible().catch(() => false); + + if (hasUpgradeButton) { + await upgradeButton.click(); + + // Wait for upgrade to start + + // Look for progress indicator + const progressIndicator = page.getByText(/upgrading|progress/i); + const hasProgress = await progressIndicator.isVisible().catch(() => false); + + if (hasProgress) { + expect(progressIndicator).toBeVisible(); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should check CLI tool status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI installations page + await page.goto('/settings/cli/installations', { waitUntil: 'networkidle' as const }); + + // Look for check status/refresh button + const checkButton = page.getByRole('button', { name: /check status|refresh|check updates/i }).or( + page.getByTestId('check-status-button') + ); + + const hasCheckButton = await checkButton.isVisible().catch(() => false); + + if (hasCheckButton) { + await checkButton.click(); + + // Wait for status check to complete + + // Verify status indicators are updated + const statusIndicators = page.getByTestId(/tool-status|installation-status/).or( + page.locator('*').filter({ hasText: /active|inactive|installed|not installed/i }) + ); + + const hasIndicators = await statusIndicators.isVisible().catch(() => false); + expect(hasIndicators).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should display tool version', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI installations page + await page.goto('/settings/cli/installations', { waitUntil: 'networkidle' as const }); + + // Look for installation items + const installationItems = page.getByTestId(/installation-item|tool-item/).or( + page.locator('.installation-item') + ); + + const itemCount = await installationItems.count(); + + if (itemCount > 0) { + const firstItem = installationItems.first(); + + // Look for version badge + const versionBadge = firstItem.getByTestId('tool-version').or( + firstItem.locator('*').filter({ hasText: /v?\d+\.\d+/i }) + ); + + const hasVersion = await versionBadge.isVisible().catch(() => false); + + if (hasVersion) { + const text = await versionBadge.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should display tool installation status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI installations page + await page.goto('/settings/cli/installations', { waitUntil: 'networkidle' as const }); + + // Look for installation items + const installationItems = page.getByTestId(/installation-item|tool-item/).or( + page.locator('.installation-item') + ); + + const itemCount = await installationItems.count(); + + if (itemCount > 0) { + const firstItem = installationItems.first(); + + // Look for status indicator + const statusBadge = firstItem.getByTestId('tool-status').or( + firstItem.locator('*').filter({ hasText: /active|inactive|installed|not installed/i }) + ); + + const hasStatus = await statusBadge.isVisible().catch(() => false); + expect(hasStatus).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should display tool path', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI installations page + await page.goto('/settings/cli/installations', { waitUntil: 'networkidle' as const }); + + // Look for installation items + const installationItems = page.getByTestId(/installation-item|tool-item/).or( + page.locator('.installation-item') + ); + + const itemCount = await installationItems.count(); + + if (itemCount > 0) { + const firstItem = installationItems.first(); + + // Look for path display + const pathDisplay = firstItem.getByTestId('tool-path').or( + firstItem.locator('*').filter({ hasText: /\/|\\/ }) + ); + + const hasPath = await pathDisplay.isVisible().catch(() => false); + + if (hasPath) { + const text = await pathDisplay.textContent(); + expect(text).toBeTruthy(); + expect(text?.length).toBeGreaterThan(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should handle installation errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/cli/installments/**', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to CLI installations page + await page.goto('/settings/cli/installations', { waitUntil: 'networkidle' as const }); + + // Try to install a tool + const notInstalledTools = page.locator('.installation-item').filter({ hasText: /not installed/i }); + + const count = await notInstalledTools.count(); + + if (count > 0) { + const firstTool = notInstalledTools.first(); + const installButton = firstTool.getByRole('button', { name: /install/i }); + + const hasInstallButton = await installButton.isVisible().catch(() => false); + + if (hasInstallButton) { + await installButton.click(); + + // Look for error message + + const errorMessage = page.getByText(/error|failed|unable/i); + const hasError = await errorMessage.isVisible().catch(() => false); + expect(hasError).toBe(true); + } + } + + // Restore routing + await page.unroute('**/api/cli/installments/**'); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/cli/installments'], allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should display last checked timestamp', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to CLI installations page + await page.goto('/settings/cli/installations', { waitUntil: 'networkidle' as const }); + + // Look for last checked indicator + const lastChecked = page.getByTestId('last-checked').or( + page.getByText(/last checked|last updated/i) + ); + + const hasLastChecked = await lastChecked.isVisible().catch(() => false); + + if (hasLastChecked) { + const text = await lastChecked.textContent(); + expect(text).toBeTruthy(); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/commands.spec.ts b/ccw/frontend/tests/e2e/commands.spec.ts new file mode 100644 index 00000000..d8431c74 --- /dev/null +++ b/ccw/frontend/tests/e2e/commands.spec.ts @@ -0,0 +1,344 @@ +// ======================================== +// E2E Tests: Commands Management +// ======================================== +// End-to-end tests for commands list and info display + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[Commands] - Commands Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display commands list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to commands page + await page.goto('/commands', { waitUntil: 'networkidle' as const }); + + // Look for commands list container + const commandsList = page.getByTestId('commands-list').or( + page.locator('.commands-list') + ); + + const isVisible = await commandsList.isVisible().catch(() => false); + + if (isVisible) { + // Verify command items exist + const commandItems = page.getByTestId(/command-item|command-card/).or( + page.locator('.command-item') + ); + + const itemCount = await commandItems.count(); + expect(itemCount).toBeGreaterThan(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should display command name', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to commands page + await page.goto('/commands', { waitUntil: 'networkidle' as const }); + + // Look for command items + const commandItems = page.getByTestId(/command-item|command-card/).or( + page.locator('.command-item') + ); + + const itemCount = await commandItems.count(); + + if (itemCount > 0) { + // Check each command has a name + for (let i = 0; i < Math.min(itemCount, 5); i++) { + const command = commandItems.nth(i); + + const nameElement = command.getByTestId('command-name').or( + command.locator('.command-name') + ); + + const hasName = await nameElement.isVisible().catch(() => false); + expect(hasName).toBe(true); + + const name = await nameElement.textContent(); + expect(name).toBeTruthy(); + expect(name?.length).toBeGreaterThan(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should display command description', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to commands page + await page.goto('/commands', { waitUntil: 'networkidle' as const }); + + // Look for command items + const commandItems = page.getByTestId(/command-item|command-card/).or( + page.locator('.command-item') + ); + + const itemCount = await commandItems.count(); + + if (itemCount > 0) { + const firstCommand = commandItems.first(); + + // Look for description + const description = firstCommand.getByTestId('command-description').or( + firstCommand.locator('.command-description') + ); + + const hasDescription = await description.isVisible().catch(() => false); + + if (hasDescription) { + const text = await description.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should display command usage', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to commands page + await page.goto('/commands', { waitUntil: 'networkidle' as const }); + + // Look for command items + const commandItems = page.getByTestId(/command-item|command-card/).or( + page.locator('.command-item') + ); + + const itemCount = await commandItems.count(); + + if (itemCount > 0) { + const firstCommand = commandItems.first(); + + // Look for usage info + const usage = firstCommand.getByTestId('command-usage').or( + firstCommand.locator('*').filter({ hasText: /usage|how to use/i }) + ); + + const hasUsage = await usage.isVisible().catch(() => false); + + if (hasUsage) { + const text = await usage.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should display command examples', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to commands page + await page.goto('/commands', { waitUntil: 'networkidle' as const }); + + // Look for command items + const commandItems = page.getByTestId(/command-item|command-card/).or( + page.locator('.command-item') + ); + + const itemCount = await commandItems.count(); + + if (itemCount > 0) { + const firstCommand = commandItems.first(); + + // Look for examples section + const examples = firstCommand.getByTestId('command-examples').or( + firstCommand.locator('*').filter({ hasText: /example/i }) + ); + + const hasExamples = await examples.isVisible().catch(() => false); + + if (hasExamples) { + const text = await examples.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should display command category', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to commands page + await page.goto('/commands', { waitUntil: 'networkidle' as const }); + + // Look for command items + const commandItems = page.getByTestId(/command-item|command-card/).or( + page.locator('.command-item') + ); + + const itemCount = await commandItems.count(); + + if (itemCount > 0) { + const firstCommand = commandItems.first(); + + // Look for category badge + const categoryBadge = firstCommand.getByTestId('command-category').or( + firstCommand.locator('.command-category') + ); + + const hasCategory = await categoryBadge.isVisible().catch(() => false); + + if (hasCategory) { + const text = await categoryBadge.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should filter commands by category', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to commands page + await page.goto('/commands', { waitUntil: 'networkidle' as const }); + + // Look for category filter + const categoryFilter = page.getByRole('combobox', { name: /category|filter/i }).or( + page.getByTestId('category-filter') + ); + + const hasCategoryFilter = await categoryFilter.isVisible().catch(() => false); + + if (hasCategoryFilter) { + // Check if there are category options + const categoryOptions = await categoryFilter.locator('option').count(); + + if (categoryOptions > 1) { + await categoryFilter.selectOption({ index: 1 }); + + // Wait for filtered results + + const commandItems = page.getByTestId(/command-item|command-card/).or( + page.locator('.command-item') + ); + + const commandCount = await commandItems.count(); + expect(commandCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should search commands', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to commands page + await page.goto('/commands', { waitUntil: 'networkidle' as const }); + + // Look for search input + const searchInput = page.getByRole('textbox', { name: /search|find/i }).or( + page.getByTestId('command-search') + ); + + const hasSearch = await searchInput.isVisible().catch(() => false); + + if (hasSearch) { + await searchInput.fill('test'); + + // Wait for search results + + // Search should either show results or no results message + const noResults = page.getByText(/no results|not found/i); + const hasNoResults = await noResults.isVisible().catch(() => false); + + const commandItems = page.getByTestId(/command-item|command-card/).or( + page.locator('.command-item') + ); + + const commandCount = await commandItems.count(); + + // Either no results message or filtered commands + expect(hasNoResults || commandCount >= 0).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should display command source type', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to commands page + await page.goto('/commands', { waitUntil: 'networkidle' as const }); + + // Look for command items + const commandItems = page.getByTestId(/command-item|command-card/).or( + page.locator('.command-item') + ); + + const itemCount = await commandItems.count(); + + if (itemCount > 0) { + const firstCommand = commandItems.first(); + + // Look for source badge + const sourceBadge = firstCommand.getByTestId('command-source').or( + firstCommand.locator('*').filter({ hasText: /builtin|custom/i }) + ); + + const hasSource = await sourceBadge.isVisible().catch(() => false); + + if (hasSource) { + const text = await sourceBadge.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should display command aliases', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to commands page + await page.goto('/commands', { waitUntil: 'networkidle' as const }); + + // Look for command items + const commandItems = page.getByTestId(/command-item|command-card/).or( + page.locator('.command-item') + ); + + const itemCount = await commandItems.count(); + + if (itemCount > 0) { + const firstCommand = commandItems.first(); + + // Look for aliases display + const aliases = firstCommand.getByTestId('command-aliases').or( + firstCommand.locator('*').filter({ hasText: /alias|also known as/i }) + ); + + const hasAliases = await aliases.isVisible().catch(() => false); + + if (hasAliases) { + const text = await aliases.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/dashboard.spec.ts b/ccw/frontend/tests/e2e/dashboard.spec.ts new file mode 100644 index 00000000..2502e030 --- /dev/null +++ b/ccw/frontend/tests/e2e/dashboard.spec.ts @@ -0,0 +1,322 @@ +// ======================================== +// E2E Tests: Dashboard +// ======================================== +// End-to-end tests for dashboard functionality with i18n support + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring, switchLanguageAndVerify, verifyI18nState, verifyPersistenceAfterReload } from './helpers/i18n-helpers'; + +test.describe('[Dashboard] - Core Functionality Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display dashboard stats', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Look for dashboard stats container + const statsContainer = page.getByTestId('dashboard-stats').or( + page.locator('[data-testid="stats"]') + ).or( + page.locator('.stats') + ); + + const isVisible = await statsContainer.isVisible().catch(() => false); + + if (isVisible) { + // Verify stat cards are present + const statCards = page.getByTestId(/stat-|stat-card/).or( + page.locator('.stat-card') + ); + + const cardCount = await statCards.count(); + expect(cardCount).toBeGreaterThan(0); + + // Verify each card has a value + for (let i = 0; i < Math.min(cardCount, 5); i++) { + const card = statCards.nth(i); + await expect(card).toBeVisible(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should display active sessions list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Look for sessions container + const sessionsContainer = page.getByTestId('sessions-list').or( + page.getByTestId('active-sessions') + ).or( + page.locator('.sessions-list') + ); + + const isVisible = await sessionsContainer.isVisible().catch(() => false); + + if (isVisible) { + // Verify session items are present or empty state is shown + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount === 0) { + // Check for empty state + const emptyState = page.getByText(/no sessions|empty|no data/i).or( + page.getByTestId('empty-state') + ); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } else { + expect(itemCount).toBeGreaterThan(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should support i18n (English/Chinese)', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Get language switcher + const languageSwitcher = page.getByRole('combobox', { name: /select language|language/i }).first(); + + const hasLanguageSwitcher = await languageSwitcher.isVisible().catch(() => false); + + if (hasLanguageSwitcher) { + // Switch to Chinese + await switchLanguageAndVerify(page, 'zh', languageSwitcher); + await verifyI18nState(page, 'zh'); + + // Verify dashboard content is in Chinese + const pageContent = await page.content(); + const hasChineseText = /[\u4e00-\u9fa5]/.test(pageContent); + expect(hasChineseText).toBe(true); + + // Switch back to English + await switchLanguageAndVerify(page, 'en', languageSwitcher); + await verifyI18nState(page, 'en'); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should handle empty state gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Look for empty state indicators + const emptyStateIndicators = [ + page.getByText(/no sessions/i), + page.getByText(/no data/i), + page.getByText(/get started/i), + page.getByTestId('empty-state'), + page.locator('.empty-state'), + ]; + + let hasEmptyState = false; + for (const indicator of emptyStateIndicators) { + if (await indicator.isVisible().catch(() => false)) { + hasEmptyState = true; + break; + } + } + + // If empty state is present, verify it has helpful content + if (hasEmptyState) { + // Look for call-to-action buttons + const ctaButton = page.getByRole('button', { name: /create|new|add|start/i }).first(); + const hasCTA = await ctaButton.isVisible().catch(() => false); + + // Empty state should guide users to take action + expect(hasCTA).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should persist language preference after reload', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Get language switcher + const languageSwitcher = page.getByRole('combobox', { name: /select language|language/i }).first(); + + const hasLanguageSwitcher = await languageSwitcher.isVisible().catch(() => false); + + if (hasLanguageSwitcher) { + // Switch to Chinese + await switchLanguageAndVerify(page, 'zh', languageSwitcher); + + // Verify persistence after reload + await verifyPersistenceAfterReload(page, 'zh'); + + // Verify language is still Chinese + const lang = await page.evaluate(() => document.documentElement.lang); + expect(lang).toBe('zh'); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should display archived sessions section', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Look for archived sessions section + const archivedSection = page.getByTestId('archived-sessions').or( + page.getByText(/archived/i) + ); + + const hasArchivedSection = await archivedSection.isVisible().catch(() => false); + + if (hasArchivedSection) { + // Verify archived sessions are visually distinct from active sessions + const activeSessions = page.getByTestId('active-sessions').or( + page.getByText(/active sessions/i) + ); + + const hasActiveSection = await activeSessions.isVisible().catch(() => false); + expect(hasActiveSection).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should update stats when workspace changes', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Look for workspace switcher + const workspaceSwitcher = page.getByTestId('workspace-switcher').or( + page.getByRole('combobox', { name: /workspace/i }) + ); + + const hasWorkspaceSwitcher = await workspaceSwitcher.isVisible().catch(() => false); + + if (hasWorkspaceSwitcher) { + // Get initial stats + const initialStats = await page.evaluate(() => { + const stats = document.querySelector('[data-testid*="stat"]'); + return stats?.textContent || ''; + }); + + // Try to switch workspace + await workspaceSwitcher.click(); + + const options = page.getByRole('option'); + const optionsCount = await options.count(); + + if (optionsCount > 0) { + const firstOption = options.first(); + await firstOption.click(); + + // Wait for data refresh + await page.waitForLoadState('networkidle'); + + // Verify stats container is still visible + const statsContainer = page.getByTestId('dashboard-stats').or( + page.locator('.stats') + ); + + const isStillVisible = await statsContainer.isVisible().catch(() => false); + expect(isStillVisible).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should handle API errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/data', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Reload page to trigger API call + await page.reload({ waitUntil: 'networkidle' as const }); + + // Look for error indicator or fallback content + const errorIndicator = page.getByText(/error|failed|unable to load/i).or( + page.getByTestId('error-state') + ); + + const hasError = await errorIndicator.isVisible().catch(() => false); + + // Either error is shown or page has fallback content + const pageContent = await page.content(); + const hasContent = pageContent.length > 1000; + + expect(hasError || hasContent).toBe(true); + + // Restore normal routing + await page.unroute('**/api/data'); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/data'], allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should navigate to session detail on click', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Look for session items + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + + // Click on session + await firstSession.click(); + + // Verify navigation to session detail + await page.waitForURL(/\/session|\/sessions\//); + + const currentUrl = page.url(); + expect(currentUrl).toMatch(/\/session|\/sessions\//); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should display today activity metric', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Look for today activity stat + const todayActivity = page.getByTestId('stat-today-activity').or( + page.getByTestId('today-activity') + ).or( + page.locator('*').filter({ hasText: /today|activity/i }) + ); + + const hasTodayActivity = await todayActivity.isVisible().catch(() => false); + + if (hasTodayActivity) { + const text = await todayActivity.textContent(); + expect(text).toBeTruthy(); + expect(text?.length).toBeGreaterThan(0); + + // Verify it contains a number + const hasNumber = /\d+/.test(text || ''); + expect(hasNumber).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/discovery.spec.ts b/ccw/frontend/tests/e2e/discovery.spec.ts new file mode 100644 index 00000000..a8b56154 --- /dev/null +++ b/ccw/frontend/tests/e2e/discovery.spec.ts @@ -0,0 +1,429 @@ +// ======================================== +// E2E Tests: Discovery Management +// ======================================== +// End-to-end tests for discovery sessions, details, and findings + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[Discovery] - Discovery Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display discovery sessions list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to discovery page + await page.goto('/discovery', { waitUntil: 'networkidle' as const }); + + // Look for discovery sessions list container + const sessionsList = page.getByTestId('discovery-sessions-list').or( + page.locator('.discovery-sessions-list') + ); + + const isVisible = await sessionsList.isVisible().catch(() => false); + + if (isVisible) { + // Verify session items exist or empty state is shown + const sessionItems = page.getByTestId(/discovery-item|discovery-session/).or( + page.locator('.discovery-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount === 0) { + const emptyState = page.getByTestId('empty-state').or( + page.getByText(/no discoveries|empty/i) + ); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should display discovery details', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to discovery page + await page.goto('/discovery', { waitUntil: 'networkidle' as const }); + + // Look for discovery session items + const sessionItems = page.getByTestId(/discovery-item|discovery-session/).or( + page.locator('.discovery-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + + // Click to view details + await firstSession.click(); + + // Verify detail view loads + await page.waitForURL(/\/discovery\//); + + const detailContainer = page.getByTestId('discovery-detail').or( + page.locator('.discovery-detail') + ); + + const hasDetail = await detailContainer.isVisible().catch(() => false); + expect(hasDetail).toBe(true); + + // Verify session info is displayed + const sessionInfo = page.getByTestId('discovery-info').or( + page.locator('.discovery-info') + ); + + const hasInfo = await sessionInfo.isVisible().catch(() => false); + expect(hasInfo).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should display discovery findings', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to discovery page + await page.goto('/discovery', { waitUntil: 'networkidle' as const }); + + // Look for discovery session items + const sessionItems = page.getByTestId(/discovery-item|discovery-session/).or( + page.locator('.discovery-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + + // Look for findings count + const findingsCount = firstSession.getByTestId('findings-count').or( + firstSession.locator('*').filter({ hasText: /\d+\s*findings/i }) + ); + + const hasFindingsCount = await findingsCount.isVisible().catch(() => false); + + if (hasFindingsCount) { + const text = await findingsCount.textContent(); + expect(text).toBeTruthy(); + } + + // Click to view findings + await firstSession.click(); + + await page.waitForURL(/\/discovery\//); + + // Look for findings list + const findingsList = page.getByTestId('findings-list').or( + page.locator('.findings-list') + ); + + const hasFindings = await findingsList.isVisible().catch(() => false); + + if (hasFindings) { + const findingItems = page.getByTestId(/finding-item|finding-card/).or( + page.locator('.finding-item') + ); + + const findingCount = await findingItems.count(); + expect(findingCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should display session status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to discovery page + await page.goto('/discovery', { waitUntil: 'networkidle' as const }); + + // Look for discovery session items + const sessionItems = page.getByTestId(/discovery-item|discovery-session/).or( + page.locator('.discovery-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + // Check each session has status indicator + for (let i = 0; i < Math.min(itemCount, 3); i++) { + const session = sessionItems.nth(i); + + // Look for status badge + const statusBadge = session.getByTestId('session-status').or( + session.locator('*').filter({ hasText: /running|completed|failed|pending/i }) + ); + + const hasStatus = await statusBadge.isVisible().catch(() => false); + expect(hasStatus).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should display session progress', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to discovery page + await page.goto('/discovery', { waitUntil: 'networkidle' as const }); + + // Look for discovery session items + const sessionItems = page.getByTestId(/discovery-item|discovery-session/).or( + page.locator('.discovery-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + + // Look for progress bar or percentage + const progressBar = firstSession.getByTestId('session-progress').or( + firstSession.locator('*').filter({ hasText: /\d+%/i }) + ); + + const hasProgress = await progressBar.isVisible().catch(() => false); + + if (hasProgress) { + const text = await progressBar.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should filter findings by severity', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to discovery page + await page.goto('/discovery', { waitUntil: 'networkidle' as const }); + + // Look for discovery session items + const sessionItems = page.getByTestId(/discovery-item|discovery-session/).or( + page.locator('.discovery-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/discovery\//); + + // Look for severity filter + const severityFilter = page.getByRole('combobox', { name: /severity|filter/i }).or( + page.getByTestId('severity-filter') + ); + + const hasFilter = await severityFilter.isVisible().catch(() => false); + + if (hasFilter) { + const filterOptions = await severityFilter.locator('option').count(); + + if (filterOptions > 1) { + await severityFilter.selectOption({ index: 1 }); + + // Wait for filtered results + + const findingItems = page.getByTestId(/finding-item|finding-card/).or( + page.locator('.finding-item') + ); + + const findingCount = await findingItems.count(); + expect(findingCount).toBeGreaterThanOrEqual(0); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should display finding severity', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to discovery page + await page.goto('/discovery', { waitUntil: 'networkidle' as const }); + + // Look for discovery session items + const sessionItems = page.getByTestId(/discovery-item|discovery-session/).or( + page.locator('.discovery-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/discovery\//); + + // Look for finding items + const findingItems = page.getByTestId(/finding-item|finding-card/).or( + page.locator('.finding-item') + ); + + const findingCount = await findingItems.count(); + + if (findingCount > 0) { + const firstFinding = findingItems.first(); + + // Look for severity badge + const severityBadge = firstFinding.getByTestId('finding-severity').or( + firstFinding.locator('*').filter({ hasText: /critical|high|medium|low/i }) + ); + + const hasSeverity = await severityBadge.isVisible().catch(() => false); + expect(hasSeverity).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should display finding details', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to discovery page + await page.goto('/discovery', { waitUntil: 'networkidle' as const }); + + // Look for discovery session items + const sessionItems = page.getByTestId(/discovery-item|discovery-session/).or( + page.locator('.discovery-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/discovery\//); + + // Look for finding items + const findingItems = page.getByTestId(/finding-item|finding-card/).or( + page.locator('.finding-item') + ); + + const findingCount = await findingItems.count(); + + if (findingCount > 0) { + const firstFinding = findingItems.first(); + + // Look for finding title + const title = firstFinding.getByTestId('finding-title').or( + firstFinding.locator('.finding-title') + ); + + const hasTitle = await title.isVisible().catch(() => false); + expect(hasTitle).toBe(true); + + // Look for finding description + const description = firstFinding.getByTestId('finding-description').or( + firstFinding.locator('.finding-description') + ); + + const hasDescription = await description.isVisible().catch(() => false); + + if (hasDescription) { + const text = await description.textContent(); + expect(text).toBeTruthy(); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should handle discovery API errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/discoveries/**', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to discovery page + await page.goto('/discovery', { waitUntil: 'networkidle' as const }); + + // Look for error indicator + const errorIndicator = page.getByText(/error|failed|unable to load/i).or( + page.getByTestId('error-state') + ); + + const hasError = await errorIndicator.isVisible().catch(() => false); + + // Restore routing + await page.unroute('**/api/discoveries/**'); + + // Error should be displayed or handled gracefully + expect(hasError).toBe(true); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/discoveries'], allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should export findings report', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to discovery page + await page.goto('/discovery', { waitUntil: 'networkidle' as const }); + + // Look for discovery session items + const sessionItems = page.getByTestId(/discovery-item|discovery-session/).or( + page.locator('.discovery-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + + // Look for export button + const exportButton = firstSession.getByRole('button', { name: /export|download|report/i }).or( + firstSession.getByTestId('export-button') + ); + + const hasExportButton = await exportButton.isVisible().catch(() => false); + + if (hasExportButton) { + // Click export and verify download starts + const downloadPromise = page.waitForEvent('download'); + + await exportButton.click(); + + const download = await downloadPromise.catch(() => null); + + // Either download started or there was feedback + const successMessage = page.getByText(/exporting|downloading|success/i); + const hasMessage = await successMessage.isVisible().catch(() => false); + + expect(download !== null || hasMessage).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/file-explorer.spec.ts b/ccw/frontend/tests/e2e/file-explorer.spec.ts new file mode 100644 index 00000000..3bd1f6dd --- /dev/null +++ b/ccw/frontend/tests/e2e/file-explorer.spec.ts @@ -0,0 +1,340 @@ +// ======================================== +// E2E Tests: File Explorer Management +// ======================================== +// End-to-end tests for file tree, content, search, and roots operations + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[File Explorer] - File Explorer Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display file tree', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to file explorer page + await page.goto('/explorer', { waitUntil: 'networkidle' as const }); + + // Look for file tree container + const fileTree = page.getByTestId('file-tree').or( + page.locator('.file-tree') + ); + + const isVisible = await fileTree.isVisible().catch(() => false); + + if (isVisible) { + // Verify tree nodes exist + const treeNodes = page.getByTestId(/tree-node|file-node|folder-node/).or( + page.locator('.tree-node') + ); + + const nodeCount = await treeNodes.count(); + expect(nodeCount).toBeGreaterThan(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should expand and collapse folders', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to file explorer page + await page.goto('/explorer', { waitUntil: 'networkidle' as const }); + + // Look for folder nodes + const folderNodes = page.getByTestId(/folder-node|directory-node/).or( + page.locator('.folder-node') + ); + + const nodeCount = await folderNodes.count(); + + if (nodeCount > 0) { + const firstFolder = folderNodes.first(); + + // Click to expand + await firstFolder.click(); + + // Wait for children to load + + // Verify children are visible + const childNodes = firstFolder.locator('.tree-node'); + const childCount = await childNodes.count(); + + // Click again to collapse + await firstFolder.click(); + + // Children should be hidden + const visibleChildCount = await firstFolder.locator('.tree-node:visible').count(); + + expect(childCount).toBeGreaterThan(visibleChildCount); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should display file content', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to file explorer page + await page.goto('/explorer', { waitUntil: 'networkidle' as const }); + + // Look for file nodes + const fileNodes = page.getByTestId(/file-node|tree-node/).filter({ hasText: /\.(ts|tsx|js|jsx|json|md)$/i }).or( + page.locator('.file-node').filter({ hasText: /\.(ts|tsx|js|jsx|json|md)$/i }) + ); + + const nodeCount = await fileNodes.count(); + + if (nodeCount > 0) { + const firstFile = fileNodes.first(); + + // Click to view content + await firstFile.click(); + + // Look for content viewer + const contentViewer = page.getByTestId('file-content').or( + page.locator('.file-content') + ); + + const hasContent = await contentViewer.isVisible().catch(() => false); + + if (hasContent) { + const content = await contentViewer.textContent(); + expect(content).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should search files', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to file explorer page + await page.goto('/explorer', { waitUntil: 'networkidle' as const }); + + // Look for search input + const searchInput = page.getByRole('textbox', { name: /search|find/i }).or( + page.getByTestId('file-search') + ); + + const hasSearch = await searchInput.isVisible().catch(() => false); + + if (hasSearch) { + await searchInput.fill('test'); + + // Wait for search results + + // Search should either show results or no results message + const noResults = page.getByText(/no results|not found/i); + const hasNoResults = await noResults.isVisible().catch(() => false); + + const searchResults = page.getByTestId(/search-result|file-match/).or( + page.locator('.search-result') + ); + + const resultCount = await searchResults.count(); + + // Either no results message or search results + expect(hasNoResults || resultCount >= 0).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should display available roots', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to file explorer page + await page.goto('/explorer', { waitUntil: 'networkidle' as const }); + + // Look for roots section + const rootsSection = page.getByTestId('available-roots').or( + page.getByText(/roots|drives/i) + ); + + const isVisible = await rootsSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify root items are displayed + const rootItems = page.getByTestId(/root-item|drive-item/).or( + rootsSection.locator('.root-item') + ); + + const rootCount = await rootItems.count(); + expect(rootCount).toBeGreaterThan(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should switch between roots', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to file explorer page + await page.goto('/explorer', { waitUntil: 'networkidle' as const }); + + // Look for root selector + const rootSelector = page.getByRole('combobox', { name: /root|drive|location/i }).or( + page.getByTestId('root-selector') + ); + + const hasSelector = await rootSelector.isVisible().catch(() => false); + + if (hasSelector) { + // Get initial root + const initialRoot = await rootSelector.textContent(); + + // Select different root + const rootOptions = await rootSelector.locator('option').count(); + + if (rootOptions > 1) { + await rootSelector.selectOption({ index: 1 }); + + // Wait for tree to refresh + + // Verify file tree is still visible + const fileTree = page.getByTestId('file-tree').or( + page.locator('.file-tree') + ); + + const isStillVisible = await fileTree.isVisible().catch(() => false); + expect(isStillVisible).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should display file metadata', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to file explorer page + await page.goto('/explorer', { waitUntil: 'networkidle' as const }); + + // Look for file nodes + const fileNodes = page.getByTestId(/file-node|tree-node/).or( + page.locator('.file-node') + ); + + const nodeCount = await fileNodes.count(); + + if (nodeCount > 0) { + const firstNode = fileNodes.first(); + + // Look for metadata display + const metadata = firstNode.getByTestId('file-metadata').or( + firstNode.locator('*').filter({ hasText: /\d+KB|\d+MB|\d+ bytes/i }) + ); + + const hasMetadata = await metadata.isVisible().catch(() => false); + + if (hasMetadata) { + const text = await metadata.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should handle file tree API errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/explorer/**', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to file explorer page + await page.goto('/explorer', { waitUntil: 'networkidle' as const }); + + // Look for error indicator + const errorIndicator = page.getByText(/error|failed|unable to load/i).or( + page.getByTestId('error-state') + ); + + const hasError = await errorIndicator.isVisible().catch(() => false); + + // Restore routing + await page.unroute('**/api/explorer/**'); + + // Error should be displayed or handled gracefully + expect(hasError).toBe(true); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/explorer'], allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should display binary file warning', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to file explorer page + await page.goto('/explorer', { waitUntil: 'networkidle' as const }); + + // Look for binary file nodes (images, executables) + const binaryFileNodes = page.getByTestId(/file-node/).filter({ + hasText: /\.(png|jpg|jpeg|gif|exe|dll|so|dylib)$/i + }); + + const nodeCount = await binaryFileNodes.count(); + + if (nodeCount > 0) { + const firstFile = binaryFileNodes.first(); + + // Click to view content + await firstFile.click(); + + // Look for binary file warning + const binaryWarning = page.getByText(/binary|cannot display|preview not available/i); + const hasWarning = await binaryWarning.isVisible().catch(() => false); + + if (hasWarning) { + expect(binaryWarning).toBeVisible(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should display file statistics', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to file explorer page + await page.goto('/explorer', { waitUntil: 'networkidle' as const }); + + // Look for statistics section + const statsSection = page.getByTestId('file-stats').or( + page.getByText(/files|directories|total size/i) + ); + + const isVisible = await statsSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify stats are displayed + const statItems = page.getByTestId(/stat-|files-count|directories-count/).or( + statsSection.locator('.stat-item') + ); + + const statCount = await statItems.count(); + expect(statCount).toBeGreaterThan(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/hooks.spec.ts b/ccw/frontend/tests/e2e/hooks.spec.ts new file mode 100644 index 00000000..40bd563e --- /dev/null +++ b/ccw/frontend/tests/e2e/hooks.spec.ts @@ -0,0 +1,456 @@ +// ======================================== +// E2E Tests: Hooks Management +// ======================================== +// End-to-end tests for hooks CRUD, toggle, and template operations + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[Hooks] - Hooks Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display hooks list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to hooks page + await page.goto('/settings/hooks', { waitUntil: 'networkidle' as const }); + + // Look for hooks list container + const hooksList = page.getByTestId('hooks-list').or( + page.locator('.hooks-list') + ); + + const isVisible = await hooksList.isVisible().catch(() => false); + + if (isVisible) { + // Verify hook items exist or empty state is shown + const hookItems = page.getByTestId(/hook-item|hook-card/).or( + page.locator('.hook-item') + ); + + const itemCount = await hookItems.count(); + + if (itemCount === 0) { + const emptyState = page.getByTestId('empty-state').or( + page.getByText(/no hooks|empty/i) + ); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should create new hook', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to hooks page + await page.goto('/settings/hooks', { waitUntil: 'networkidle' as const }); + + // Look for create hook button + const createButton = page.getByRole('button', { name: /create|new|add hook/i }).or( + page.getByTestId('create-hook-button') + ); + + const hasCreateButton = await createButton.isVisible().catch(() => false); + + if (hasCreateButton) { + await createButton.click(); + + // Look for create hook dialog/form + const dialog = page.getByRole('dialog').filter({ hasText: /create hook|new hook/i }); + const form = page.getByTestId('create-hook-form'); + + const hasDialog = await dialog.isVisible().catch(() => false); + const hasForm = await form.isVisible().catch(() => false); + + if (hasDialog || hasForm) { + // Fill in hook details + const nameInput = page.getByRole('textbox', { name: /name/i }).or( + page.getByLabel(/name/i) + ); + + const hasNameInput = await nameInput.isVisible().catch(() => false); + + if (hasNameInput) { + await nameInput.fill('e2e-test-hook'); + + // Select trigger + const triggerSelect = page.getByRole('combobox', { name: /trigger/i }); + const hasTriggerSelect = await triggerSelect.isVisible().catch(() => false); + + if (hasTriggerSelect) { + const triggerOptions = await triggerSelect.locator('option').count(); + if (triggerOptions > 0) { + await triggerSelect.selectOption({ index: 0 }); + } + } + + // Enter command + const commandInput = page.getByRole('textbox', { name: /command|script/i }); + const hasCommandInput = await commandInput.isVisible().catch(() => false); + + if (hasCommandInput) { + await commandInput.fill('echo "test"'); + } + + const submitButton = page.getByRole('button', { name: /create|save|submit/i }); + await submitButton.click(); + + // Verify hook was created + + const successMessage = page.getByText(/created|success/i).or( + page.getByTestId('success-message') + ); + + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should update hook', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to hooks page + await page.goto('/settings/hooks', { waitUntil: 'networkidle' as const }); + + // Look for existing hook + const hookItems = page.getByTestId(/hook-item|hook-card/).or( + page.locator('.hook-item') + ); + + const itemCount = await hookItems.count(); + + if (itemCount > 0) { + const firstHook = hookItems.first(); + + // Look for edit button + const editButton = firstHook.getByRole('button', { name: /edit|modify|configure/i }).or( + firstHook.getByTestId('edit-hook-button') + ); + + const hasEditButton = await editButton.isVisible().catch(() => false); + + if (hasEditButton) { + await editButton.click(); + + // Update hook command + const commandInput = page.getByRole('textbox', { name: /command|script/i }); + const hasCommandInput = await commandInput.isVisible().catch(() => false); + + if (hasCommandInput) { + await commandInput.clear(); + await commandInput.fill('echo "updated"'); + } + + // Save changes + const saveButton = page.getByRole('button', { name: /save|update|submit/i }); + await saveButton.click(); + + // Verify success message + + const successMessage = page.getByText(/updated|saved|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should delete hook', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to hooks page + await page.goto('/settings/hooks', { waitUntil: 'networkidle' as const }); + + // Look for existing hook + const hookItems = page.getByTestId(/hook-item|hook-card/).or( + page.locator('.hook-item') + ); + + const itemCount = await hookItems.count(); + + if (itemCount > 0) { + const firstHook = hookItems.first(); + + // Look for delete button + const deleteButton = firstHook.getByRole('button', { name: /delete|remove/i }).or( + firstHook.getByTestId('delete-button') + ); + + const hasDeleteButton = await deleteButton.isVisible().catch(() => false); + + if (hasDeleteButton) { + await deleteButton.click(); + + // Confirm delete if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /delete|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /delete|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify success message + + const successMessage = page.getByText(/deleted|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should toggle hook enabled status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to hooks page + await page.goto('/settings/hooks', { waitUntil: 'networkidle' as const }); + + // Look for hook items + const hookItems = page.getByTestId(/hook-item|hook-card/).or( + page.locator('.hook-item') + ); + + const itemCount = await hookItems.count(); + + if (itemCount > 0) { + const firstHook = hookItems.first(); + + // Look for toggle switch + const toggleSwitch = firstHook.getByRole('switch').or( + firstHook.getByTestId('hook-toggle') + ).or( + firstHook.getByRole('button', { name: /enable|disable|toggle/i }) + ); + + const hasToggle = await toggleSwitch.isVisible().catch(() => false); + + if (hasToggle) { + // Get initial state + const initialState = await toggleSwitch.getAttribute('aria-checked'); + const initialChecked = initialState === 'true'; + + // Toggle the hook + await toggleSwitch.click(); + + // Wait for update + + // Verify state changed + const newState = await toggleSwitch.getAttribute('aria-checked'); + const newChecked = newState === 'true'; + + expect(newChecked).toBe(!initialChecked); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should display hook trigger', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to hooks page + await page.goto('/settings/hooks', { waitUntil: 'networkidle' as const }); + + // Look for hook items + const hookItems = page.getByTestId(/hook-item|hook-card/).or( + page.locator('.hook-item') + ); + + const itemCount = await hookItems.count(); + + if (itemCount > 0) { + const firstHook = hookItems.first(); + + // Look for trigger badge + const triggerBadge = firstHook.getByTestId('hook-trigger').or( + firstHook.locator('*').filter({ hasText: /pre-commit|post-commit|pre-push|on-save/i }) + ); + + const hasTrigger = await triggerBadge.isVisible().catch(() => false); + + if (hasTrigger) { + const text = await triggerBadge.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should install hook from template', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to hooks page + await page.goto('/settings/hooks', { waitUntil: 'networkidle' as const }); + + // Look for template installation button + const templateButton = page.getByRole('button', { name: /template|install template/i }).or( + page.getByTestId('install-template-button') + ); + + const hasTemplateButton = await templateButton.isVisible().catch(() => false); + + if (hasTemplateButton) { + await templateButton.click(); + + // Look for template selection dialog + const dialog = page.getByRole('dialog').filter({ hasText: /template|choose/i }); + const hasDialog = await dialog.isVisible().catch(() => false); + + if (hasDialog) { + // Select a template + const templateOption = dialog.getByRole('button').first(); + const hasOption = await templateOption.isVisible().catch(() => false); + + if (hasOption) { + await templateOption.click(); + + // Confirm installation + const confirmButton = page.getByRole('button', { name: /install|add/i }); + await confirmButton.click(); + + // Verify success + + const successMessage = page.getByText(/installed|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should display hook matcher pattern', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to hooks page + await page.goto('/settings/hooks', { waitUntil: 'networkidle' as const }); + + // Look for hook items + const hookItems = page.getByTestId(/hook-item|hook-card/).or( + page.locator('.hook-item') + ); + + const itemCount = await hookItems.count(); + + if (itemCount > 0) { + const firstHook = hookItems.first(); + + // Look for matcher pattern display + const matcherDisplay = firstHook.getByTestId('hook-matcher').or( + firstHook.locator('*').filter({ hasText: /\*\..+|\.ts$|\.js$/i }) + ); + + const hasMatcher = await matcherDisplay.isVisible().catch(() => false); + + if (hasMatcher) { + const text = await matcherDisplay.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should filter hooks by trigger type', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to hooks page + await page.goto('/settings/hooks', { waitUntil: 'networkidle' as const }); + + // Look for trigger filter + const triggerFilter = page.getByRole('combobox', { name: /trigger|filter/i }).or( + page.getByTestId('trigger-filter') + ); + + const hasTriggerFilter = await triggerFilter.isVisible().catch(() => false); + + if (hasTriggerFilter) { + // Check if there are filter options + const filterOptions = await triggerFilter.locator('option').count(); + + if (filterOptions > 1) { + await triggerFilter.selectOption({ index: 1 }); + + // Wait for filtered results + + const hookItems = page.getByTestId(/hook-item|hook-card/).or( + page.locator('.hook-item') + ); + + const hookCount = await hookItems.count(); + expect(hookCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should handle hooks API errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/hooks/**', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to hooks page + await page.goto('/settings/hooks', { waitUntil: 'networkidle' as const }); + + // Try to create a hook + const createButton = page.getByRole('button', { name: /create|new|add/i }); + const hasCreateButton = await createButton.isVisible().catch(() => false); + + if (hasCreateButton) { + await createButton.click(); + + const nameInput = page.getByRole('textbox', { name: /name/i }); + const hasNameInput = await nameInput.isVisible().catch(() => false); + + if (hasNameInput) { + await nameInput.fill('test-hook'); + + const submitButton = page.getByRole('button', { name: /create|save/i }); + await submitButton.click(); + + // Look for error message + + const errorMessage = page.getByText(/error|failed|unable/i); + const hasError = await errorMessage.isVisible().catch(() => false); + expect(hasError).toBe(true); + } + } + + // Restore routing + await page.unroute('**/api/hooks/**'); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/hooks'], allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/index-management.spec.ts b/ccw/frontend/tests/e2e/index-management.spec.ts new file mode 100644 index 00000000..33374cda --- /dev/null +++ b/ccw/frontend/tests/e2e/index-management.spec.ts @@ -0,0 +1,326 @@ +// ======================================== +// E2E Tests: Index Management +// ======================================== +// End-to-end tests for index status and rebuild operations + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[Index Management] - Index Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display index status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to index management page + await page.goto('/settings/index', { waitUntil: 'networkidle' as const }); + + // Look for index status container + const statusContainer = page.getByTestId('index-status').or( + page.locator('.index-status') + ); + + const isVisible = await statusContainer.isVisible().catch(() => false); + + if (isVisible) { + // Verify status information is displayed + const statusInfo = page.getByTestId('status-info').or( + statusContainer.locator('*').filter({ hasText: /indexed|files|last updated/i }) + ); + + const hasStatusInfo = await statusInfo.isVisible().catch(() => false); + expect(hasStatusInfo).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should display indexed file count', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to index management page + await page.goto('/settings/index', { waitUntil: 'networkidle' as const }); + + // Look for file count display + const fileCount = page.getByTestId('indexed-files-count').or( + page.getByText(/\d+\s*files?/i) + ); + + const hasFileCount = await fileCount.isVisible().catch(() => false); + + if (hasFileCount) { + const text = await fileCount.textContent(); + expect(text).toBeTruthy(); + // Verify it contains a number + const hasNumber = /\d+/.test(text || ''); + expect(hasNumber).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should display last index time', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to index management page + await page.goto('/settings/index', { waitUntil: 'networkidle' as const }); + + // Look for last indexed time + const lastIndexed = page.getByTestId('last-indexed').or( + page.getByText(/last indexed|last updated/i) + ); + + const hasLastIndexed = await lastIndexed.isVisible().catch(() => false); + + if (hasLastIndexed) { + const text = await lastIndexed.textContent(); + expect(text).toBeTruthy(); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should rebuild index', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to index management page + await page.goto('/settings/index', { waitUntil: 'networkidle' as const }); + + // Look for rebuild button + const rebuildButton = page.getByRole('button', { name: /rebuild|re-index/i }).or( + page.getByTestId('rebuild-button') + ); + + const hasRebuildButton = await rebuildButton.isVisible().catch(() => false); + + if (hasRebuildButton) { + await rebuildButton.click(); + + // Confirm rebuild if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /rebuild|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /rebuild|confirm|yes/i }); + await confirmButton.click(); + } + + // Look for progress indicator + + const progressIndicator = page.getByTestId('index-progress').or( + page.getByText(/indexing|rebuilding|progress/i) + ); + + const hasProgress = await progressIndicator.isVisible().catch(() => false); + + if (hasProgress) { + expect(progressIndicator).toBeVisible(); + } + + // Wait for rebuild to complete (or timeout) + + // Look for success message + const successMessage = page.getByText(/rebuilt|completed|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + + // Success message may or may not be present depending on timing + if (hasSuccess) { + expect(successMessage).toBeVisible(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should display index size', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to index management page + await page.goto('/settings/index', { waitUntil: 'networkidle' as const }); + + // Look for index size display + const indexSize = page.getByTestId('index-size').or( + page.getByText(/\d+KB|\d+MB|\d+GB/i) + ); + + const hasIndexSize = await indexSize.isVisible().catch(() => false); + + if (hasIndexSize) { + const text = await indexSize.textContent(); + expect(text).toBeTruthy(); + // Verify it contains a size unit + const hasSizeUnit = /KB|MB|GB/.test(text || ''); + expect(hasSizeUnit).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should cancel index rebuild', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to index management page + await page.goto('/settings/index', { waitUntil: 'networkidle' as const }); + + // Look for rebuild button + const rebuildButton = page.getByRole('button', { name: /rebuild|re-index/i }).or( + page.getByTestId('rebuild-button') + ); + + const hasRebuildButton = await rebuildButton.isVisible().catch(() => false); + + if (hasRebuildButton) { + await rebuildButton.click(); + + // Look for cancel button (if rebuild is in progress) + + const cancelButton = page.getByRole('button', { name: /cancel/i }).or( + page.getByTestId('cancel-button') + ); + + const hasCancelButton = await cancelButton.isVisible().catch(() => false); + + if (hasCancelButton) { + await cancelButton.click(); + + // Verify cancellation message + + const cancelMessage = page.getByText(/cancelled|stopped/i); + const hasCancelMessage = await cancelMessage.isVisible().catch(() => false); + + if (hasCancelMessage) { + expect(cancelMessage).toBeVisible(); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should display index health status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to index management page + await page.goto('/settings/index', { waitUntil: 'networkidle' as const }); + + // Look for health indicator + const healthIndicator = page.getByTestId('index-health').or( + page.getByText(/healthy|status|ok/i) + ); + + const hasHealth = await healthIndicator.isVisible().catch(() => false); + + if (hasHealth) { + const text = await healthIndicator.textContent(); + expect(text).toBeTruthy(); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should handle index API errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/index/**', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to index management page + await page.goto('/settings/index', { waitUntil: 'networkidle' as const }); + + // Look for error indicator + const errorIndicator = page.getByText(/error|failed|unable to load/i).or( + page.getByTestId('error-state') + ); + + const hasError = await errorIndicator.isVisible().catch(() => false); + + // Restore routing + await page.unroute('**/api/index/**'); + + // Error should be displayed or handled gracefully + expect(hasError).toBe(true); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/index'], allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should show index rebuild progress', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to index management page + await page.goto('/settings/index', { waitUntil: 'networkidle' as const }); + + // Look for rebuild button + const rebuildButton = page.getByRole('button', { name: /rebuild/i }); + const hasRebuildButton = await rebuildButton.isVisible().catch(() => false); + + if (hasRebuildButton) { + await rebuildButton.click(); + + // Look for progress bar + + const progressBar = page.getByTestId('rebuild-progress').or( + page.getByRole('progressbar') + ); + + const hasProgressBar = await progressBar.isVisible().catch(() => false); + + if (hasProgressBar) { + expect(progressBar).toBeVisible(); + + // Verify progress value is present + const progressValue = await progressBar.getAttribute('aria-valuenow'); + const hasProgressValue = progressValue !== null; + + if (hasProgressValue) { + const progress = parseInt(progressValue || '0', 10); + expect(progress).toBeGreaterThanOrEqual(0); + expect(progress).toBeLessThanOrEqual(100); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should display index configuration', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to index management page + await page.goto('/settings/index', { waitUntil: 'networkidle' as const }); + + // Look for configuration section + const configSection = page.getByTestId('index-config').or( + page.getByText(/configuration|settings|options/i) + ); + + const isVisible = await configSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify config options are displayed + const configOptions = configSection.locator('*').filter({ hasText: /exclude|include|depth/i }); + const configCount = await configOptions.count(); + + expect(configCount).toBeGreaterThan(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/issues-queue.spec.ts b/ccw/frontend/tests/e2e/issues-queue.spec.ts new file mode 100644 index 00000000..3b2385ef --- /dev/null +++ b/ccw/frontend/tests/e2e/issues-queue.spec.ts @@ -0,0 +1,423 @@ +// ======================================== +// E2E Tests: Issues and Queue Management +// ======================================== +// End-to-end tests for issues CRUD and queue operations + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[Issues & Queue] - Issue Tracking Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display issues list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to issues page + await page.goto('/issues', { waitUntil: 'networkidle' as const }); + + // Look for issues list container + const issuesList = page.getByTestId('issues-list').or( + page.locator('.issues-list') + ); + + const isVisible = await issuesList.isVisible().catch(() => false); + + if (isVisible) { + // Verify issue items exist or empty state is shown + const issueItems = page.getByTestId(/issue-item|issue-card/).or( + page.locator('.issue-item') + ); + + const itemCount = await issueItems.count(); + + if (itemCount === 0) { + const emptyState = page.getByTestId('empty-state').or( + page.getByText(/no issues/i) + ); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should create new issue', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to issues page + await page.goto('/issues', { waitUntil: 'networkidle' as const }); + + // Look for create issue button + const createButton = page.getByRole('button', { name: /create|new|add issue/i }).or( + page.getByTestId('create-issue-button') + ); + + const hasCreateButton = await createButton.isVisible().catch(() => false); + + if (hasCreateButton) { + await createButton.click(); + + // Look for create issue dialog/form + const dialog = page.getByRole('dialog').filter({ hasText: /create issue|new issue/i }); + const form = page.getByTestId('create-issue-form'); + + const hasDialog = await dialog.isVisible().catch(() => false); + const hasForm = await form.isVisible().catch(() => false); + + if (hasDialog || hasForm) { + // Fill in issue details + const titleInput = page.getByRole('textbox', { name: /title|subject/i }).or( + page.getByLabel(/title|subject/i) + ); + + const hasTitleInput = await titleInput.isVisible().catch(() => false); + + if (hasTitleInput) { + await titleInput.fill('E2E Test Issue'); + + // Set priority if available + const prioritySelect = page.getByRole('combobox', { name: /priority/i }); + const hasPrioritySelect = await prioritySelect.isVisible().catch(() => false); + + if (hasPrioritySelect) { + await prioritySelect.selectOption('medium'); + } + + const submitButton = page.getByRole('button', { name: /create|save|submit/i }); + await submitButton.click(); + + // Verify issue was created + + const successMessage = page.getByText(/created|success/i).or( + page.getByTestId('success-message') + ); + + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should update issue status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to issues page + await page.goto('/issues', { waitUntil: 'networkidle' as const }); + + // Look for existing issue + const issueItems = page.getByTestId(/issue-item|issue-card/).or( + page.locator('.issue-item') + ); + + const itemCount = await issueItems.count(); + + if (itemCount > 0) { + const firstIssue = issueItems.first(); + + // Look for status change button/dropdown + const statusButton = firstIssue.getByRole('button', { name: /status|in.progress|open|close/i }).or( + firstIssue.getByTestId('status-button') + ); + + const hasStatusButton = await statusButton.isVisible().catch(() => false); + + if (hasStatusButton) { + await statusButton.click(); + + // Select new status + const statusOption = page.getByRole('option', { name: /in.progress|working/i }).or( + page.getByRole('menuitem', { name: /in.progress|working/i }) + ); + + const hasOption = await statusOption.isVisible().catch(() => false); + + if (hasOption) { + await statusOption.click(); + + // Verify status updated + + const updatedStatus = firstIssue.getByText(/in.progress|working/i); + const hasUpdated = await updatedStatus.isVisible().catch(() => false); + expect(hasUpdated).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should delete issue', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to issues page + await page.goto('/issues', { waitUntil: 'networkidle' as const }); + + // Look for existing issue + const issueItems = page.getByTestId(/issue-item|issue-card/).or( + page.locator('.issue-item') + ); + + const itemCount = await issueItems.count(); + + if (itemCount > 0) { + const firstIssue = issueItems.first(); + + // Look for delete button + const deleteButton = firstIssue.getByRole('button', { name: /delete|remove/i }).or( + firstIssue.getByTestId('delete-button') + ); + + const hasDeleteButton = await deleteButton.isVisible().catch(() => false); + + if (hasDeleteButton) { + await deleteButton.click(); + + // Confirm delete if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /delete|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /delete|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify success message + + const successMessage = page.getByText(/deleted|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should display issue queue', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to issues/queue page + await page.goto('/queue', { waitUntil: 'networkidle' as const }); + + // Look for queue container + const queueContainer = page.getByTestId('issue-queue').or( + page.locator('.queue-container') + ); + + const isVisible = await queueContainer.isVisible().catch(() => false); + + if (isVisible) { + // Verify queue items or empty state + const queueItems = page.getByTestId(/queue-item|task-item/).or( + page.locator('.queue-item') + ); + + const itemCount = await queueItems.count(); + expect(itemCount).toBeGreaterThanOrEqual(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should activate queue', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to queue page + await page.goto('/queue', { waitUntil: 'networkidle' as const }); + + // Look for activate button + const activateButton = page.getByRole('button', { name: /activate|start queue/i }).or( + page.getByTestId('activate-queue-button') + ); + + const hasActivateButton = await activateButton.isVisible().catch(() => false); + + if (hasActivateButton) { + await activateButton.click(); + + // Verify queue activation + + const activeIndicator = page.getByText(/active|running/i).or( + page.getByTestId('queue-active') + ); + + const hasActive = await activeIndicator.isVisible().catch(() => false); + expect(hasActive).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should deactivate queue', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to queue page + await page.goto('/queue', { waitUntil: 'networkidle' as const }); + + // Look for deactivate button (may only be visible when queue is active) + const deactivateButton = page.getByRole('button', { name: /deactivate|stop|pause/i }).or( + page.getByTestId('deactivate-queue-button') + ); + + const hasDeactivateButton = await deactivateButton.isVisible().catch(() => false); + + if (hasDeactivateButton) { + await deactivateButton.click(); + + // Verify queue deactivation + + const inactiveIndicator = page.getByText(/inactive|stopped|paused/i).or( + page.getByTestId('queue-inactive') + ); + + const hasInactive = await inactiveIndicator.isVisible().catch(() => false); + expect(hasInactive).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should delete queue', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to queue page + await page.goto('/queue', { waitUntil: 'networkidle' as const }); + + // Look for delete queue button + const deleteButton = page.getByRole('button', { name: /delete|remove queue/i }).or( + page.getByTestId('delete-queue-button') + ); + + const hasDeleteButton = await deleteButton.isVisible().catch(() => false); + + if (hasDeleteButton) { + await deleteButton.click(); + + // Confirm delete if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /delete|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /delete|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify success message + + const successMessage = page.getByText(/deleted|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should merge queues', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to queue page + await page.goto('/queue', { waitUntil: 'networkidle' as const }); + + // Look for merge button + const mergeButton = page.getByRole('button', { name: /merge|combine/i }).or( + page.getByTestId('merge-queue-button') + ); + + const hasMergeButton = await mergeButton.isVisible().catch(() => false); + + if (hasMergeButton) { + await mergeButton.click(); + + // Look for merge dialog + const dialog = page.getByRole('dialog').filter({ hasText: /merge|combine/i }); + const hasDialog = await dialog.isVisible().catch(() => false); + + if (hasDialog) { + // Select source and target queues + const sourceSelect = page.getByRole('combobox', { name: /source|from/i }); + const targetSelect = page.getByRole('combobox', { name: /target|to/i }); + + const hasSourceSelect = await sourceSelect.isVisible().catch(() => false); + const hasTargetSelect = await targetSelect.isVisible().catch(() => false); + + if (hasSourceSelect && hasTargetSelect) { + // Select options (if available) + const sourceOptions = await sourceSelect.locator('option').count(); + const targetOptions = await targetSelect.locator('option').count(); + + if (sourceOptions > 1 && targetOptions > 1) { + await sourceSelect.selectOption({ index: 1 }); + await targetSelect.selectOption({ index: 2 }); + } + + const confirmButton = page.getByRole('button', { name: /merge|combine/i }); + await confirmButton.click(); + + // Verify success message + + const successMessage = page.getByText(/merged|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should verify cache invalidation after mutations', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to issues page + await page.goto('/issues', { waitUntil: 'networkidle' as const }); + + // Get initial issue count + const issueItems = page.getByTestId(/issue-item|issue-card/).or( + page.locator('.issue-item') + ); + + const initialCount = await issueItems.count(); + + // Look for create button + const createButton = page.getByRole('button', { name: /create|new|add/i }); + const hasCreateButton = await createButton.isVisible().catch(() => false); + + if (hasCreateButton) { + await createButton.click(); + + // Quick fill and submit + const titleInput = page.getByRole('textbox', { name: /title|subject/i }); + const hasTitleInput = await titleInput.isVisible().catch(() => false); + + if (hasTitleInput) { + await titleInput.fill('Cache Test Issue'); + + const submitButton = page.getByRole('button', { name: /create|save/i }); + await submitButton.click(); + + // Wait for cache update and list refresh + + // Verify list is updated (cache invalidated) + const newCount = await issueItems.count(); + expect(newCount).toBe(initialCount + 1); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/lite-tasks.spec.ts b/ccw/frontend/tests/e2e/lite-tasks.spec.ts new file mode 100644 index 00000000..4283da96 --- /dev/null +++ b/ccw/frontend/tests/e2e/lite-tasks.spec.ts @@ -0,0 +1,365 @@ +// ======================================== +// E2E Tests: Lite Tasks Management +// ======================================== +// End-to-end tests for lite tasks list and detail view + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[Lite Tasks] - Lite Tasks Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display lite tasks list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to lite tasks page + await page.goto('/lite-tasks', { waitUntil: 'networkidle' as const }); + + // Look for lite tasks list container + const tasksList = page.getByTestId('lite-tasks-list').or( + page.locator('.lite-tasks-list') + ); + + const isVisible = await tasksList.isVisible().catch(() => false); + + if (isVisible) { + // Verify task items exist or empty state is shown + const taskItems = page.getByTestId(/lite-task-item|lite-task-card/).or( + page.locator('.lite-task-item') + ); + + const itemCount = await taskItems.count(); + + if (itemCount === 0) { + const emptyState = page.getByTestId('empty-state').or( + page.getByText(/no tasks|empty/i) + ); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should display lite task detail', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to lite tasks page + await page.goto('/lite-tasks', { waitUntil: 'networkidle' as const }); + + // Look for task items + const taskItems = page.getByTestId(/lite-task-item|lite-task-card/).or( + page.locator('.lite-task-item') + ); + + const itemCount = await taskItems.count(); + + if (itemCount > 0) { + const firstTask = taskItems.first(); + + // Click to view detail + await firstTask.click(); + + // Verify detail view loads + await page.waitForURL(/\/lite-tasks\//); + + const detailContainer = page.getByTestId('lite-task-detail').or( + page.locator('.lite-task-detail') + ); + + const hasDetail = await detailContainer.isVisible().catch(() => false); + expect(hasDetail).toBe(true); + + // Verify task info is displayed + const taskInfo = page.getByTestId('task-info').or( + page.locator('.task-info') + ); + + const hasInfo = await taskInfo.isVisible().catch(() => false); + expect(hasInfo).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should display task title', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to lite tasks page + await page.goto('/lite-tasks', { waitUntil: 'networkidle' as const }); + + // Look for task items + const taskItems = page.getByTestId(/lite-task-item|lite-task-card/).or( + page.locator('.lite-task-item') + ); + + const itemCount = await taskItems.count(); + + if (itemCount > 0) { + // Check each task has a title + for (let i = 0; i < Math.min(itemCount, 3); i++) { + const task = taskItems.nth(i); + + const titleElement = task.getByTestId('task-title').or( + task.locator('.task-title') + ); + + const hasTitle = await titleElement.isVisible().catch(() => false); + expect(hasTitle).toBe(true); + + const title = await titleElement.textContent(); + expect(title).toBeTruthy(); + expect(title?.length).toBeGreaterThan(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should display task status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to lite tasks page + await page.goto('/lite-tasks', { waitUntil: 'networkidle' as const }); + + // Look for task items + const taskItems = page.getByTestId(/lite-task-item|lite-task-card/).or( + page.locator('.lite-task-item') + ); + + const itemCount = await taskItems.count(); + + if (itemCount > 0) { + // Check each task has a status indicator + for (let i = 0; i < Math.min(itemCount, 3); i++) { + const task = taskItems.nth(i); + + const statusBadge = task.getByTestId('task-status').or( + task.locator('*').filter({ hasText: /pending|in.progress|completed|blocked|failed/i }) + ); + + const hasStatus = await statusBadge.isVisible().catch(() => false); + expect(hasStatus).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should display task type', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to lite tasks page + await page.goto('/lite-tasks', { waitUntil: 'networkidle' as const }); + + // Look for task items + const taskItems = page.getByTestId(/lite-task-item|lite-task-card/).or( + page.locator('.lite-task-item') + ); + + const itemCount = await taskItems.count(); + + if (itemCount > 0) { + const firstTask = taskItems.first(); + + // Look for type badge + const typeBadge = firstTask.getByTestId('task-type').or( + firstTask.locator('*').filter({ hasText: /lite.plan|lite.fix|multi.cli/i }) + ); + + const hasType = await typeBadge.isVisible().catch(() => false); + + if (hasType) { + const text = await typeBadge.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should filter tasks by type', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to lite tasks page + await page.goto('/lite-tasks', { waitUntil: 'networkidle' as const }); + + // Look for type filter + const typeFilter = page.getByRole('combobox', { name: /type|filter/i }).or( + page.getByTestId('type-filter') + ); + + const hasTypeFilter = await typeFilter.isVisible().catch(() => false); + + if (hasTypeFilter) { + // Check if there are type options + const typeOptions = await typeFilter.locator('option').count(); + + if (typeOptions > 1) { + await typeFilter.selectOption({ index: 1 }); + + // Wait for filtered results + + const taskItems = page.getByTestId(/lite-task-item|lite-task-card/).or( + page.locator('.lite-task-item') + ); + + const taskCount = await taskItems.count(); + expect(taskCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should filter tasks by status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to lite tasks page + await page.goto('/lite-tasks', { waitUntil: 'networkidle' as const }); + + // Look for status filter + const statusFilter = page.getByRole('combobox', { name: /status|filter/i }).or( + page.getByTestId('status-filter') + ); + + const hasStatusFilter = await statusFilter.isVisible().catch(() => false); + + if (hasStatusFilter) { + // Check if there are status options + const statusOptions = await statusFilter.locator('option').count(); + + if (statusOptions > 1) { + await statusFilter.selectOption({ index: 1 }); + + // Wait for filtered results + + const taskItems = page.getByTestId(/lite-task-item|lite-task-card/).or( + page.locator('.lite-task-item') + ); + + const taskCount = await taskItems.count(); + expect(taskCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should search lite tasks', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to lite tasks page + await page.goto('/lite-tasks', { waitUntil: 'networkidle' as const }); + + // Look for search input + const searchInput = page.getByRole('textbox', { name: /search|find/i }).or( + page.getByTestId('task-search') + ); + + const hasSearch = await searchInput.isVisible().catch(() => false); + + if (hasSearch) { + await searchInput.fill('test'); + + // Wait for search results + + // Search should either show results or no results message + const noResults = page.getByText(/no results|not found/i); + const hasNoResults = await noResults.isVisible().catch(() => false); + + const taskItems = page.getByTestId(/lite-task-item|lite-task-card/).or( + page.locator('.lite-task-item') + ); + + const taskCount = await taskItems.count(); + + // Either no results message or filtered tasks + expect(hasNoResults || taskCount >= 0).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should display task creation date', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to lite tasks page + await page.goto('/lite-tasks', { waitUntil: 'networkidle' as const }); + + // Look for task items + const taskItems = page.getByTestId(/lite-task-item|lite-task-card/).or( + page.locator('.lite-task-item') + ); + + const itemCount = await taskItems.count(); + + if (itemCount > 0) { + const firstTask = taskItems.first(); + + // Look for creation date + const dateDisplay = firstTask.getByTestId('task-created-at').or( + firstTask.locator('*').filter({ hasText: /\d{4}-\d{2}-\d{2}|created/i }) + ); + + const hasDate = await dateDisplay.isVisible().catch(() => false); + + if (hasDate) { + const text = await dateDisplay.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should display task metadata in detail view', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to lite tasks page + await page.goto('/lite-tasks', { waitUntil: 'networkidle' as const }); + + // Look for task items + const taskItems = page.getByTestId(/lite-task-item|lite-task-card/).or( + page.locator('.lite-task-item') + ); + + const itemCount = await taskItems.count(); + + if (itemCount > 0) { + const firstTask = taskItems.first(); + await firstTask.click(); + + // Wait for detail view + await page.waitForURL(/\/lite-tasks\//); + + // Look for metadata section + const metadataSection = page.getByTestId('task-metadata').or( + page.locator('.task-metadata') + ); + + const hasMetadata = await metadataSection.isVisible().catch(() => false); + + if (hasMetadata) { + // Verify metadata is displayed + const text = await metadataSection.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/loops.spec.ts b/ccw/frontend/tests/e2e/loops.spec.ts new file mode 100644 index 00000000..67c37c9f --- /dev/null +++ b/ccw/frontend/tests/e2e/loops.spec.ts @@ -0,0 +1,423 @@ +// ======================================== +// E2E Tests: Loops Management +// ======================================== +// End-to-end tests for loop CRUD operations and controls + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[Loops] - Loop Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display loops list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to loops page + await page.goto('/loops', { waitUntil: 'networkidle' as const }); + + // Look for loops list container + const loopsList = page.getByTestId('loops-list').or( + page.locator('.loops-list') + ); + + const isVisible = await loopsList.isVisible().catch(() => false); + + if (isVisible) { + // Verify loop items exist or empty state is shown + const loopItems = page.getByTestId(/loop-item|loop-card/).or( + page.locator('.loop-item') + ); + + const itemCount = await loopItems.count(); + + if (itemCount === 0) { + const emptyState = page.getByTestId('empty-state').or( + page.getByText(/no loops/i) + ); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should create new loop', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to loops page + await page.goto('/loops', { waitUntil: 'networkidle' as const }); + + // Look for create loop button + const createButton = page.getByRole('button', { name: /create|new|add loop/i }).or( + page.getByTestId('create-loop-button') + ); + + const hasCreateButton = await createButton.isVisible().catch(() => false); + + if (hasCreateButton) { + await createButton.click(); + + // Look for create loop dialog/form + const dialog = page.getByRole('dialog').filter({ hasText: /create loop|new loop/i }); + const form = page.getByTestId('create-loop-form'); + + const hasDialog = await dialog.isVisible().catch(() => false); + const hasForm = await form.isVisible().catch(() => false); + + if (hasDialog || hasForm) { + // Fill in loop details + const promptInput = page.getByRole('textbox', { name: /prompt|description/i }).or( + page.getByLabel(/prompt|description/i) + ); + + const hasPromptInput = await promptInput.isVisible().catch(() => false); + + if (hasPromptInput) { + await promptInput.fill('E2E Test Loop prompt'); + + // Select tool if available + const toolSelect = page.getByRole('combobox', { name: /tool/i }); + const hasToolSelect = await toolSelect.isVisible().catch(() => false); + + if (hasToolSelect) { + const toolOptions = await toolSelect.locator('option').count(); + if (toolOptions > 0) { + await toolSelect.selectOption({ index: 0 }); + } + } + + const submitButton = page.getByRole('button', { name: /create|save|submit|start/i }); + await submitButton.click(); + + // Verify loop was created + + const successMessage = page.getByText(/created|started|success/i).or( + page.getByTestId('success-message') + ); + + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should pause running loop', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to loops page + await page.goto('/loops', { waitUntil: 'networkidle' as const }); + + // Look for running loop + const runningLoops = page.getByTestId(/loop-item|loop-card/).filter({ hasText: /running|active/i }).or( + page.locator('.loop-item').filter({ hasText: /running|active/i }) + ); + + const count = await runningLoops.count(); + + if (count > 0) { + const firstLoop = runningLoops.first(); + + // Look for pause button + const pauseButton = firstLoop.getByRole('button', { name: /pause/i }).or( + firstLoop.getByTestId('pause-button') + ); + + const hasPauseButton = await pauseButton.isVisible().catch(() => false); + + if (hasPauseButton) { + await pauseButton.click(); + + // Verify loop is paused + + const pausedIndicator = firstLoop.getByText(/paused/i); + const hasPaused = await pausedIndicator.isVisible().catch(() => false); + expect(hasPaused).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should resume paused loop', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to loops page + await page.goto('/loops', { waitUntil: 'networkidle' as const }); + + // Look for paused loop + const pausedLoops = page.getByTestId(/loop-item|loop-card/).filter({ hasText: /paused/i }).or( + page.locator('.loop-item').filter({ hasText: /paused/i }) + ); + + const count = await pausedLoops.count(); + + if (count > 0) { + const firstLoop = pausedLoops.first(); + + // Look for resume button + const resumeButton = firstLoop.getByRole('button', { name: /resume|continue/i }).or( + firstLoop.getByTestId('resume-button') + ); + + const hasResumeButton = await resumeButton.isVisible().catch(() => false); + + if (hasResumeButton) { + await resumeButton.click(); + + // Verify loop is resumed + + const runningIndicator = firstLoop.getByText(/running|active/i); + const hasRunning = await runningIndicator.isVisible().catch(() => false); + expect(hasRunning).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should stop loop', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to loops page + await page.goto('/loops', { waitUntil: 'networkidle' as const }); + + // Look for active/paused loop + const activeLoops = page.locator('.loop-item').filter({ hasText: /running|paused|active/i }); + + const count = await activeLoops.count(); + + if (count > 0) { + const firstLoop = activeLoops.first(); + + // Look for stop button + const stopButton = firstLoop.getByRole('button', { name: /stop/i }).or( + firstLoop.getByTestId('stop-button') + ); + + const hasStopButton = await stopButton.isVisible().catch(() => false); + + if (hasStopButton) { + await stopButton.click(); + + // Confirm stop if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /stop|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /stop|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify loop is stopped + + const stoppedIndicator = firstLoop.getByText(/stopped|completed/i); + const hasStopped = await stoppedIndicator.isVisible().catch(() => false); + expect(hasStopped).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should delete loop', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to loops page + await page.goto('/loops', { waitUntil: 'networkidle' as const }); + + // Look for existing loop + const loopItems = page.getByTestId(/loop-item|loop-card/).or( + page.locator('.loop-item') + ); + + const itemCount = await loopItems.count(); + + if (itemCount > 0) { + const firstLoop = loopItems.first(); + + // Look for delete button + const deleteButton = firstLoop.getByRole('button', { name: /delete|remove/i }).or( + firstLoop.getByTestId('delete-button') + ); + + const hasDeleteButton = await deleteButton.isVisible().catch(() => false); + + if (hasDeleteButton) { + await deleteButton.click(); + + // Confirm delete if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /delete|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /delete|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify success message + + const successMessage = page.getByText(/deleted|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should display loop status correctly', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to loops page + await page.goto('/loops', { waitUntil: 'networkidle' as const }); + + // Look for loop items + const loopItems = page.getByTestId(/loop-item|loop-card/).or( + page.locator('.loop-item') + ); + + const itemCount = await loopItems.count(); + + if (itemCount > 0) { + // Check each loop has a status indicator + for (let i = 0; i < Math.min(itemCount, 3); i++) { + const loop = loopItems.nth(i); + + // Look for status indicator + const statusIndicator = loop.getByTestId('loop-status').or( + loop.locator('*').filter({ hasText: /running|paused|stopped|completed|created/i }) + ); + + const hasStatus = await statusIndicator.isVisible().catch(() => false); + expect(hasStatus).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should display loop progress', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to loops page + await page.goto('/loops', { waitUntil: 'networkidle' as const }); + + // Look for loop items with progress + const loopItems = page.getByTestId(/loop-item|loop-card/).or( + page.locator('.loop-item') + ); + + const itemCount = await loopItems.count(); + + if (itemCount > 0) { + const firstLoop = loopItems.first(); + + // Look for progress bar or step indicator + const progressBar = firstLoop.getByTestId('loop-progress').or( + firstLoop.locator('*').filter({ hasText: /\d+\/\d+|step/i }) + ); + + const hasProgress = await progressBar.isVisible().catch(() => false); + + // Progress is optional but if present should be visible + if (hasProgress) { + expect(progressBar).toBeVisible(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should handle loop creation errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/loops', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to loops page + await page.goto('/loops', { waitUntil: 'networkidle' as const }); + + // Look for create button + const createButton = page.getByRole('button', { name: /create|new|add/i }); + const hasCreateButton = await createButton.isVisible().catch(() => false); + + if (hasCreateButton) { + await createButton.click(); + + // Try to create loop + const promptInput = page.getByRole('textbox', { name: /prompt|description/i }); + const hasPromptInput = await promptInput.isVisible().catch(() => false); + + if (hasPromptInput) { + await promptInput.fill('Test prompt'); + + const submitButton = page.getByRole('button', { name: /create|save|submit/i }); + await submitButton.click(); + + // Look for error message + + const errorMessage = page.getByText(/error|failed|unable/i); + const hasError = await errorMessage.isVisible().catch(() => false); + expect(hasError).toBe(true); + } + } + + // Restore routing + await page.unroute('**/api/loops'); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/loops'], allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should support batch operations on loops', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to loops page + await page.goto('/loops', { waitUntil: 'networkidle' as const }); + + // Look for batch operation controls + const selectAllCheckbox = page.getByRole('checkbox', { name: /select all/i }).or( + page.getByTestId('select-all-loops') + ); + + const hasSelectAll = await selectAllCheckbox.isVisible().catch(() => false); + + if (hasSelectAll) { + await selectAllCheckbox.check(); + + // Look for batch action buttons + const batchStopButton = page.getByRole('button', { name: /stop selected|stop all/i }).or( + page.getByTestId('batch-stop-button') + ); + + const hasBatchStop = await batchStopButton.isVisible().catch(() => false); + + if (hasBatchStop) { + expect(batchStopButton).toBeVisible(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/mcp.spec.ts b/ccw/frontend/tests/e2e/mcp.spec.ts new file mode 100644 index 00000000..d6af0dbe --- /dev/null +++ b/ccw/frontend/tests/e2e/mcp.spec.ts @@ -0,0 +1,587 @@ +// ======================================== +// E2E Tests: MCP (Model Context Protocol) Management +// ======================================== +// End-to-end tests for MCP servers, Codex MCP, and CCW MCP configuration + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[MCP] - MCP Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display MCP servers list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to MCP settings page + await page.goto('/settings/mcp', { waitUntil: 'networkidle' as const }); + + // Look for MCP servers list container + const serversList = page.getByTestId('mcp-servers-list').or( + page.locator('.mcp-servers-list') + ); + + const isVisible = await serversList.isVisible().catch(() => false); + + if (isVisible) { + // Verify server items exist + const serverItems = page.getByTestId(/server-item|mcp-server/).or( + page.locator('.server-item') + ); + + const itemCount = await serverItems.count(); + expect(itemCount).toBeGreaterThanOrEqual(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should create new MCP server', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to MCP settings page + await page.goto('/settings/mcp', { waitUntil: 'networkidle' as const }); + + // Look for create server button + const createButton = page.getByRole('button', { name: /create|new|add server/i }).or( + page.getByTestId('create-mcp-server-button') + ); + + const hasCreateButton = await createButton.isVisible().catch(() => false); + + if (hasCreateButton) { + await createButton.click(); + + // Look for create server dialog/form + const dialog = page.getByRole('dialog').filter({ hasText: /create server|add server/i }); + const form = page.getByTestId('create-mcp-server-form'); + + const hasDialog = await dialog.isVisible().catch(() => false); + const hasForm = await form.isVisible().catch(() => false); + + if (hasDialog || hasForm) { + // Fill in server details + const nameInput = page.getByRole('textbox', { name: /name/i }).or( + page.getByLabel(/name/i) + ); + + const hasNameInput = await nameInput.isVisible().catch(() => false); + + if (hasNameInput) { + await nameInput.fill('e2e-test-server'); + + const commandInput = page.getByRole('textbox', { name: /command/i }); + const hasCommandInput = await commandInput.isVisible().catch(() => false); + + if (hasCommandInput) { + await commandInput.fill('npx'); + } + + const submitButton = page.getByRole('button', { name: /create|save|submit/i }); + await submitButton.click(); + + // Verify server was created + + const successMessage = page.getByText(/created|success/i).or( + page.getByTestId('success-message') + ); + + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should update MCP server', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to MCP settings page + await page.goto('/settings/mcp', { waitUntil: 'networkidle' as const }); + + // Look for existing server + const serverItems = page.getByTestId(/server-item|mcp-server/).or( + page.locator('.server-item') + ); + + const itemCount = await serverItems.count(); + + if (itemCount > 0) { + const firstServer = serverItems.first(); + + // Look for edit button + const editButton = firstServer.getByRole('button', { name: /edit|modify|configure/i }).or( + firstServer.getByTestId('edit-server-button') + ); + + const hasEditButton = await editButton.isVisible().catch(() => false); + + if (hasEditButton) { + await editButton.click(); + + // Update server configuration + const argsInput = page.getByRole('textbox', { name: /args|arguments/i }); + const hasArgsInput = await argsInput.isVisible().catch(() => false); + + if (hasArgsInput) { + await argsInput.fill('--version'); + } + + // Save changes + const saveButton = page.getByRole('button', { name: /save|update|submit/i }); + await saveButton.click(); + + // Verify success message + + const successMessage = page.getByText(/updated|saved|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should delete MCP server', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to MCP settings page + await page.goto('/settings/mcp', { waitUntil: 'networkidle' as const }); + + // Look for existing server + const serverItems = page.getByTestId(/server-item|mcp-server/).or( + page.locator('.server-item') + ); + + const itemCount = await serverItems.count(); + + if (itemCount > 0) { + const firstServer = serverItems.first(); + + // Look for delete button + const deleteButton = firstServer.getByRole('button', { name: /delete|remove/i }).or( + firstServer.getByTestId('delete-button') + ); + + const hasDeleteButton = await deleteButton.isVisible().catch(() => false); + + if (hasDeleteButton) { + await deleteButton.click(); + + // Confirm delete if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /delete|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /delete|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify success message + + const successMessage = page.getByText(/deleted|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should toggle MCP server enabled status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to MCP settings page + await page.goto('/settings/mcp', { waitUntil: 'networkidle' as const }); + + // Look for server items + const serverItems = page.getByTestId(/server-item|mcp-server/).or( + page.locator('.server-item') + ); + + const itemCount = await serverItems.count(); + + if (itemCount > 0) { + const firstServer = serverItems.first(); + + // Look for toggle switch + const toggleSwitch = firstServer.getByRole('switch').or( + firstServer.getByTestId('server-toggle') + ).or( + firstServer.getByRole('button', { name: /enable|disable|toggle/i }) + ); + + const hasToggle = await toggleSwitch.isVisible().catch(() => false); + + if (hasToggle) { + // Get initial state + const initialState = await toggleSwitch.getAttribute('aria-checked'); + const initialChecked = initialState === 'true'; + + // Toggle the server + await toggleSwitch.click(); + + // Wait for update + + // Verify state changed + const newState = await toggleSwitch.getAttribute('aria-checked'); + const newChecked = newState === 'true'; + + expect(newChecked).toBe(!initialChecked); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should display Codex MCP servers', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to MCP settings page + await page.goto('/settings/mcp', { waitUntil: 'networkidle' as const }); + + // Look for Codex MCP section + const codexSection = page.getByTestId('codex-mcp-section').or( + page.getByText(/codex/i) + ); + + const isVisible = await codexSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify Codex servers are displayed + const codexServers = page.getByTestId(/codex-server/).or( + codexSection.locator('.server-item') + ); + + const serverCount = await codexServers.count(); + expect(serverCount).toBeGreaterThanOrEqual(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should add Codex MCP server', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to MCP settings page + await page.goto('/settings/mcp', { waitUntil: 'networkidle' as const }); + + // Look for Codex MCP section + const codexSection = page.getByTestId('codex-mcp-section').or( + page.getByText(/codex/i) + ); + + const isVisible = await codexSection.isVisible().catch(() => false); + + if (isVisible) { + // Look for add Codex server button + const addButton = codexSection.getByRole('button', { name: /add|create|new/i }).or( + page.getByTestId('add-codex-server-button') + ); + + const hasAddButton = await addButton.isVisible().catch(() => false); + + if (hasAddButton) { + await addButton.click(); + + // Look for add server dialog/form + const dialog = page.getByRole('dialog').filter({ hasText: /add codex|create codex/i }); + const hasDialog = await dialog.isVisible().catch(() => false); + + if (hasDialog) { + // Fill in server details + const nameInput = page.getByRole('textbox', { name: /name/i }); + const hasNameInput = await nameInput.isVisible().catch(() => false); + + if (hasNameInput) { + await nameInput.fill('e2e-codex-server'); + + const submitButton = page.getByRole('button', { name: /add|create|save/i }); + await submitButton.click(); + + // Verify server was added + + const successMessage = page.getByText(/added|created|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should display CCW MCP configuration', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to MCP settings page + await page.goto('/settings/mcp', { waitUntil: 'networkidle' as const }); + + // Look for CCW MCP section + const ccwSection = page.getByTestId('ccw-mcp-section').or( + page.getByText(/ccw|core memory/i) + ); + + const isVisible = await ccwSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify CCW MCP config is displayed + const configIndicator = page.getByTestId('ccw-config').or( + ccwSection.locator('*').filter({ hasText: /installed|enabled|configured/i }) + ); + + const hasConfig = await configIndicator.isVisible().catch(() => false); + expect(hasConfig).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should update CCW MCP configuration', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to MCP settings page + await page.goto('/settings/mcp', { waitUntil: 'networkidle' as const }); + + // Look for CCW MCP section + const ccwSection = page.getByTestId('ccw-mcp-section').or( + page.getByText(/ccw|core memory/i) + ); + + const isVisible = await ccwSection.isVisible().catch(() => false); + + if (isVisible) { + // Look for configure button + const configButton = ccwSection.getByRole('button', { name: /configure|settings|edit/i }).or( + page.getByTestId('ccw-config-button') + ); + + const hasConfigButton = await configButton.isVisible().catch(() => false); + + if (hasConfigButton) { + await configButton.click(); + + // Look for config dialog + const dialog = page.getByRole('dialog').filter({ hasText: /configure|settings/i }); + const hasDialog = await dialog.isVisible().catch(() => false); + + if (hasDialog) { + // Modify configuration + const enabledToolsCheckbox = page.getByRole('checkbox', { name: /enabled tools|tools/i }); + const hasCheckbox = await enabledToolsCheckbox.isVisible().catch(() => false); + + if (hasCheckbox) { + await enabledToolsCheckbox.check(); + } + + const saveButton = page.getByRole('button', { name: /save|update/i }); + await saveButton.click(); + + // Verify success + + const successMessage = page.getByText(/saved|updated|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should install CCW MCP', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to MCP settings page + await page.goto('/settings/mcp', { waitUntil: 'networkidle' as const }); + + // Look for CCW MCP section + const ccwSection = page.getByTestId('ccw-mcp-section').or( + page.getByText(/ccw|core memory/i) + ); + + const isVisible = await ccwSection.isVisible().catch(() => false); + + if (isVisible) { + // Look for install button (only if not installed) + const installButton = ccwSection.getByRole('button', { name: /install/i }).or( + page.getByTestId('install-ccw-mcp-button') + ); + + const hasInstallButton = await installButton.isVisible().catch(() => false); + + if (hasInstallButton) { + await installButton.click(); + + // Confirm installation if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /install|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /install|confirm|yes/i }); + await confirmButton.click(); + } + + // Wait for installation to complete + + // Verify installation message + const successMessage = page.getByText(/installed|success|completed/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.11 - should uninstall CCW MCP', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to MCP settings page + await page.goto('/settings/mcp', { waitUntil: 'networkidle' as const }); + + // Look for CCW MCP section + const ccwSection = page.getByTestId('ccw-mcp-section').or( + page.getByText(/ccw|core memory/i) + ); + + const isVisible = await ccwSection.isVisible().catch(() => false); + + if (isVisible) { + // Look for uninstall button (only if installed) + const uninstallButton = ccwSection.getByRole('button', { name: /uninstall|remove/i }).or( + page.getByTestId('uninstall-ccw-mcp-button') + ); + + const hasUninstallButton = await uninstallButton.isVisible().catch(() => false); + + if (hasUninstallButton) { + await uninstallButton.click(); + + // Confirm uninstallation if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /uninstall|confirm|remove/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /uninstall|confirm|yes/i }); + await confirmButton.click(); + } + + // Wait for uninstallation to complete + + // Verify uninstallation message + const successMessage = page.getByText(/uninstalled|removed|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.12 - should display server scope (project/global)', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to MCP settings page + await page.goto('/settings/mcp', { waitUntil: 'networkidle' as const }); + + // Look for server items + const serverItems = page.getByTestId(/server-item|mcp-server/).or( + page.locator('.server-item') + ); + + const itemCount = await serverItems.count(); + + if (itemCount > 0) { + const firstServer = serverItems.first(); + + // Look for scope badge + const scopeBadge = firstServer.getByTestId('server-scope').or( + firstServer.locator('*').filter({ hasText: /project|global/i }) + ); + + const hasScope = await scopeBadge.isVisible().catch(() => false); + + if (hasScope) { + const text = await scopeBadge.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.13 - should separate project and global servers', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to MCP settings page + await page.goto('/settings/mcp', { waitUntil: 'networkidle' as const }); + + // Look for project servers section + const projectSection = page.getByTestId('project-servers').or( + page.getByText(/project servers/i) + ); + + // Look for global servers section + const globalSection = page.getByTestId('global-servers').or( + page.getByText(/global servers/i) + ); + + const hasProject = await projectSection.isVisible().catch(() => false); + const hasGlobal = await globalSection.isVisible().catch(() => false); + + // At least one section should be visible + expect(hasProject || hasGlobal).toBe(true); + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.14 - should handle MCP API errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/mcp/**', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to MCP settings page + await page.reload({ waitUntil: 'networkidle' as const }); + + // Look for error indicator + const errorIndicator = page.getByText(/error|failed|unable to load/i).or( + page.getByTestId('error-state') + ); + + const hasError = await errorIndicator.isVisible().catch(() => false); + + // Restore routing + await page.unroute('**/api/mcp/**'); + + // Error should be displayed or handled gracefully + expect(hasError).toBe(true); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/mcp'], allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/memory.spec.ts b/ccw/frontend/tests/e2e/memory.spec.ts new file mode 100644 index 00000000..96bd5e67 --- /dev/null +++ b/ccw/frontend/tests/e2e/memory.spec.ts @@ -0,0 +1,406 @@ +// ======================================== +// E2E Tests: Memory Management +// ======================================== +// End-to-end tests for memory CRUD, prompts, and insights + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[Memory] - Memory Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display memories list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to memory page + await page.goto('/memory', { waitUntil: 'networkidle' as const }); + + // Look for memories list container + const memoriesList = page.getByTestId('memories-list').or( + page.locator('.memories-list') + ); + + const isVisible = await memoriesList.isVisible().catch(() => false); + + if (isVisible) { + // Verify memory items exist or empty state is shown + const memoryItems = page.getByTestId(/memory-item|memory-card/).or( + page.locator('.memory-item') + ); + + const itemCount = await memoryItems.count(); + + if (itemCount === 0) { + const emptyState = page.getByTestId('empty-state').or( + page.getByText(/no memories|empty/i) + ); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should create new memory', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to memory page + await page.goto('/memory', { waitUntil: 'networkidle' as const }); + + // Look for create memory button + const createButton = page.getByRole('button', { name: /create|new|add memory/i }).or( + page.getByTestId('create-memory-button') + ); + + const hasCreateButton = await createButton.isVisible().catch(() => false); + + if (hasCreateButton) { + await createButton.click(); + + // Look for create memory dialog/form + const dialog = page.getByRole('dialog').filter({ hasText: /create memory|new memory/i }); + const form = page.getByTestId('create-memory-form'); + + const hasDialog = await dialog.isVisible().catch(() => false); + const hasForm = await form.isVisible().catch(() => false); + + if (hasDialog || hasForm) { + // Fill in memory details + const contentInput = page.getByRole('textbox', { name: /content|description|message/i }).or( + page.getByLabel(/content|description|message/i) + ); + + const hasContentInput = await contentInput.isVisible().catch(() => false); + + if (hasContentInput) { + await contentInput.fill('E2E Test Memory content'); + + // Add tags if available + const tagsInput = page.getByRole('textbox', { name: /tags/i }); + const hasTagsInput = await tagsInput.isVisible().catch(() => false); + + if (hasTagsInput) { + await tagsInput.fill('test,e2e'); + } + + const submitButton = page.getByRole('button', { name: /create|save|submit/i }); + await submitButton.click(); + + // Verify memory was created + + const successMessage = page.getByText(/created|success/i).or( + page.getByTestId('success-message') + ); + + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should update memory', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to memory page + await page.goto('/memory', { waitUntil: 'networkidle' as const }); + + // Look for existing memory + const memoryItems = page.getByTestId(/memory-item|memory-card/).or( + page.locator('.memory-item') + ); + + const itemCount = await memoryItems.count(); + + if (itemCount > 0) { + const firstMemory = memoryItems.first(); + + // Look for edit button + const editButton = firstMemory.getByRole('button', { name: /edit|modify/i }).or( + firstMemory.getByTestId('edit-memory-button') + ); + + const hasEditButton = await editButton.isVisible().catch(() => false); + + if (hasEditButton) { + await editButton.click(); + + // Update memory content + const contentInput = page.getByRole('textbox', { name: /content|description|message/i }); + await contentInput.clear(); + await contentInput.fill('Updated E2E Test Memory content'); + + // Save changes + const saveButton = page.getByRole('button', { name: /save|update|submit/i }); + await saveButton.click(); + + // Verify success message + + const successMessage = page.getByText(/updated|saved|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should delete memory', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to memory page + await page.goto('/memory', { waitUntil: 'networkidle' as const }); + + // Look for existing memory + const memoryItems = page.getByTestId(/memory-item|memory-card/).or( + page.locator('.memory-item') + ); + + const itemCount = await memoryItems.count(); + + if (itemCount > 0) { + const firstMemory = memoryItems.first(); + + // Look for delete button + const deleteButton = firstMemory.getByRole('button', { name: /delete|remove/i }).or( + firstMemory.getByTestId('delete-button') + ); + + const hasDeleteButton = await deleteButton.isVisible().catch(() => false); + + if (hasDeleteButton) { + await deleteButton.click(); + + // Confirm delete if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /delete|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /delete|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify success message + + const successMessage = page.getByText(/deleted|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should display prompt history', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to memory/prompts page + await page.goto('/memory/prompts', { waitUntil: 'networkidle' as const }); + + // Look for prompts list container + const promptsList = page.getByTestId('prompts-list').or( + page.locator('.prompts-list') + ); + + const isVisible = await promptsList.isVisible().catch(() => false); + + if (isVisible) { + // Verify prompt items exist or empty state is shown + const promptItems = page.getByTestId(/prompt-item|prompt-card/).or( + page.locator('.prompt-item') + ); + + const itemCount = await promptItems.count(); + expect(itemCount).toBeGreaterThanOrEqual(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should display prompt insights', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to memory/insights page + await page.goto('/memory/insights', { waitUntil: 'networkidle' as const }); + + // Look for insights container + const insightsContainer = page.getByTestId('insights-container').or( + page.locator('.insights-container') + ); + + const isVisible = await insightsContainer.isVisible().catch(() => false); + + if (isVisible) { + // Verify insights exist or empty state is shown + const insightItems = page.getByTestId(/insight-item|insight-card/).or( + page.locator('.insight-item') + ); + + const itemCount = await insightItems.count(); + + if (itemCount === 0) { + const emptyState = page.getByText(/no insights|analyze prompts/i); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should delete prompt from history', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to memory/prompts page + await page.goto('/memory/prompts', { waitUntil: 'networkidle' as const }); + + // Look for existing prompt + const promptItems = page.getByTestId(/prompt-item|prompt-card/).or( + page.locator('.prompt-item') + ); + + const itemCount = await promptItems.count(); + + if (itemCount > 0) { + const firstPrompt = promptItems.first(); + + // Look for delete button + const deleteButton = firstPrompt.getByRole('button', { name: /delete|remove/i }).or( + firstPrompt.getByTestId('delete-button') + ); + + const hasDeleteButton = await deleteButton.isVisible().catch(() => false); + + if (hasDeleteButton) { + await deleteButton.click(); + + // Confirm delete if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /delete|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /delete|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify success message + + const successMessage = page.getByText(/deleted|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should search memories', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to memory page + await page.goto('/memory', { waitUntil: 'networkidle' as const }); + + // Look for search input + const searchInput = page.getByRole('textbox', { name: /search|find/i }).or( + page.getByTestId('memory-search') + ); + + const hasSearch = await searchInput.isVisible().catch(() => false); + + if (hasSearch) { + await searchInput.fill('test'); + + // Wait for search results + + // Search should either show results or no results message + const noResults = page.getByText(/no results|not found/i); + const hasNoResults = await noResults.isVisible().catch(() => false); + + const memoryItems = page.getByTestId(/memory-item|memory-card/).or( + page.locator('.memory-item') + ); + + const memoryCount = await memoryItems.count(); + + // Either no results message or filtered memories + expect(hasNoResults || memoryCount >= 0).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should filter memories by tags', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to memory page + await page.goto('/memory', { waitUntil: 'networkidle' as const }); + + // Look for tag filter + const tagFilter = page.getByRole('combobox', { name: /tags?|filter/i }).or( + page.getByTestId('tag-filter') + ); + + const hasTagFilter = await tagFilter.isVisible().catch(() => false); + + if (hasTagFilter) { + // Check if there are tag options + const tagOptions = await tagFilter.locator('option').count(); + + if (tagOptions > 1) { + await tagFilter.selectOption({ index: 1 }); + + // Wait for filtered results + + const memoryItems = page.getByTestId(/memory-item|memory-card/).or( + page.locator('.memory-item') + ); + + const memoryCount = await memoryItems.count(); + expect(memoryCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should display memory statistics', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to memory page + await page.goto('/memory', { waitUntil: 'networkidle' as const }); + + // Look for statistics section + const statsSection = page.getByTestId('memory-stats').or( + page.locator('.memory-stats') + ); + + const isVisible = await statsSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify stat items are present + const statItems = page.getByTestId(/stat-/).or( + page.locator('.stat-item') + ); + + const statCount = await statItems.count(); + expect(statCount).toBeGreaterThan(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/project-overview.spec.ts b/ccw/frontend/tests/e2e/project-overview.spec.ts new file mode 100644 index 00000000..f7758a46 --- /dev/null +++ b/ccw/frontend/tests/e2e/project-overview.spec.ts @@ -0,0 +1,293 @@ +// ======================================== +// E2E Tests: Project Overview +// ======================================== +// End-to-end tests for project overview display and navigation + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring, switchLanguageAndVerify } from './helpers/i18n-helpers'; + +test.describe('[Project Overview] - Project Overview Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display project overview', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to project overview page + await page.goto('/project', { waitUntil: 'networkidle' as const }); + + // Look for project overview container + const overviewContainer = page.getByTestId('project-overview').or( + page.locator('.project-overview') + ); + + const isVisible = await overviewContainer.isVisible().catch(() => false); + + if (isVisible) { + // Verify project name is displayed + const projectName = page.getByTestId('project-name').or( + page.locator('h1').filter({ hasText: /.+/ }) + ); + + const hasName = await projectName.isVisible().catch(() => false); + expect(hasName).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should display technology stack', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to project overview page + await page.goto('/project', { waitUntil: 'networkidle' as const }); + + // Look for technology stack section + const techStackSection = page.getByTestId('tech-stack').or( + page.getByText(/technology stack|tech stack|languages/i) + ); + + const isVisible = await techStackSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify tech stack items are displayed + const techItems = page.getByTestId(/tech-item|language-item/).or( + techStackSection.locator('.tech-item') + ); + + const techCount = await techItems.count(); + expect(techCount).toBeGreaterThan(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should display architecture information', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to project overview page + await page.goto('/project', { waitUntil: 'networkidle' as const }); + + // Look for architecture section + const archSection = page.getByTestId('architecture').or( + page.getByText(/architecture|design/i) + ); + + const isVisible = await archSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify architecture info is displayed + const archInfo = archSection.locator('*').filter({ hasText: /layers|patterns|style/i }); + + const hasInfo = await archInfo.isVisible().catch(() => false); + expect(hasInfo).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should display key components', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to project overview page + await page.goto('/project', { waitUntil: 'networkidle' as const }); + + // Look for key components section + const componentsSection = page.getByTestId('key-components').or( + page.getByText(/components|modules/i) + ); + + const isVisible = await componentsSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify component items are displayed + const componentItems = page.getByTestId(/component-item|key-component/).or( + componentsSection.locator('.component-item') + ); + + const componentCount = await componentItems.count(); + expect(componentCount).toBeGreaterThan(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should display development index', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to project overview page + await page.goto('/project', { waitUntil: 'networkidle' as const }); + + // Look for development index section + const devIndexSection = page.getByTestId('development-index').or( + page.getByText(/development index|features|enhancements/i) + ); + + const isVisible = await devIndexSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify index items are displayed or empty state + const indexItems = page.getByTestId(/index-item|feature-item/).or( + devIndexSection.locator('.index-item') + ); + + const indexCount = await indexItems.count(); + + if (indexCount === 0) { + const emptyState = page.getByText(/no entries|empty/i); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should support i18n in project overview', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to project overview page + await page.goto('/project', { waitUntil: 'networkidle' as const }); + + // Get language switcher + const languageSwitcher = page.getByRole('combobox', { name: /select language|language/i }).first(); + + const hasLanguageSwitcher = await languageSwitcher.isVisible().catch(() => false); + + if (hasLanguageSwitcher) { + // Switch to Chinese + await switchLanguageAndVerify(page, 'zh', languageSwitcher); + + // Verify project overview content is in Chinese + const pageContent = await page.content(); + const hasChineseText = /[\u4e00-\u9fa5]/.test(pageContent); + expect(hasChineseText).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should display project guidelines', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to project overview page + await page.goto('/project', { waitUntil: 'networkidle' as const }); + + // Look for guidelines section + const guidelinesSection = page.getByTestId('project-guidelines').or( + page.getByText(/guidelines|conventions|rules/i) + ); + + const isVisible = await guidelinesSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify guideline items are displayed or empty state + const guidelineItems = page.getByTestId(/guideline-item|convention-item/).or( + guidelinesSection.locator('.guideline-item') + ); + + const guidelineCount = await guidelineItems.count(); + + if (guidelineCount === 0) { + const emptyState = page.getByText(/no guidelines|empty/i); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should display project initialization date', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to project overview page + await page.goto('/project', { waitUntil: 'networkidle' as const }); + + // Look for initialization date + const initDate = page.getByTestId('initialization-date').or( + page.getByText(/initialized|created|since/i) + ); + + const hasInitDate = await initDate.isVisible().catch(() => false); + + if (hasInitDate) { + const text = await initDate.textContent(); + expect(text).toBeTruthy(); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should handle project overview API errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/ccw**', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to project overview page + await page.goto('/project', { waitUntil: 'networkidle' as const }); + + // Look for error indicator + const errorIndicator = page.getByText(/error|failed|unable to load/i).or( + page.getByTestId('error-state') + ); + + const hasError = await errorIndicator.isVisible().catch(() => false); + + // Restore routing + await page.unroute('**/api/ccw**'); + + // Error should be displayed or handled gracefully + expect(hasError).toBe(true); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/ccw'], allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should refresh project data', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to project overview page + await page.goto('/project', { waitUntil: 'networkidle' as const }); + + // Get initial content + const initialContent = await page.content(); + + // Look for refresh button + const refreshButton = page.getByRole('button', { name: /refresh|reload/i }).or( + page.getByTestId('refresh-button') + ); + + const hasRefreshButton = await refreshButton.isVisible().catch(() => false); + + if (hasRefreshButton) { + await refreshButton.click(); + + // Wait for data refresh + await page.waitForLoadState('networkidle'); + + // Verify content is still displayed + const newContent = await page.content(); + expect(newContent.length).toBeGreaterThan(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/prompt-memory.spec.ts b/ccw/frontend/tests/e2e/prompt-memory.spec.ts new file mode 100644 index 00000000..db0f9748 --- /dev/null +++ b/ccw/frontend/tests/e2e/prompt-memory.spec.ts @@ -0,0 +1,386 @@ +// ======================================== +// E2E Tests: Prompt Memory Management +// ======================================== +// End-to-end tests for prompt history, insights, and delete operations + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[Prompt Memory] - Prompt Memory Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display prompt history', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to prompt memory page + await page.goto('/memory/prompts', { waitUntil: 'networkidle' as const }); + + // Look for prompts list container + const promptsList = page.getByTestId('prompts-list').or( + page.locator('.prompts-list') + ); + + const isVisible = await promptsList.isVisible().catch(() => false); + + if (isVisible) { + // Verify prompt items exist or empty state is shown + const promptItems = page.getByTestId(/prompt-item|prompt-card/).or( + page.locator('.prompt-item') + ); + + const itemCount = await promptItems.count(); + + if (itemCount === 0) { + const emptyState = page.getByTestId('empty-state').or( + page.getByText(/no prompts|empty/i) + ); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should display prompt insights', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to prompt insights page + await page.goto('/memory/insights', { waitUntil: 'networkidle' as const }); + + // Look for insights container + const insightsContainer = page.getByTestId('insights-container').or( + page.locator('.insights-container') + ); + + const isVisible = await insightsContainer.isVisible().catch(() => false); + + if (isVisible) { + // Verify insights exist or empty/analyze state is shown + const insightItems = page.getByTestId(/insight-item|insight-card/).or( + page.locator('.insight-item') + ); + + const itemCount = await insightItems.count(); + + if (itemCount === 0) { + // Empty state or analyze prompt button + const analyzeButton = page.getByRole('button', { name: /analyze|generate insights/i }); + const hasAnalyzeButton = await analyzeButton.isVisible().catch(() => false); + + const emptyState = page.getByText(/no insights|analyze prompts/i); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + + expect(hasAnalyzeButton || hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should delete prompt from history', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to prompt history page + await page.goto('/memory/prompts', { waitUntil: 'networkidle' as const }); + + // Look for existing prompt + const promptItems = page.getByTestId(/prompt-item|prompt-card/).or( + page.locator('.prompt-item') + ); + + const itemCount = await promptItems.count(); + + if (itemCount > 0) { + const firstPrompt = promptItems.first(); + + // Look for delete button + const deleteButton = firstPrompt.getByRole('button', { name: /delete|remove/i }).or( + firstPrompt.getByTestId('delete-button') + ); + + const hasDeleteButton = await deleteButton.isVisible().catch(() => false); + + if (hasDeleteButton) { + await deleteButton.click(); + + // Confirm delete if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /delete|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /delete|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify success message + + const successMessage = page.getByText(/deleted|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should analyze prompts for insights', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to prompt insights page + await page.goto('/memory/insights', { waitUntil: 'networkidle' as const }); + + // Look for analyze button + const analyzeButton = page.getByRole('button', { name: /analyze|generate insights/i }).or( + page.getByTestId('analyze-button') + ); + + const hasAnalyzeButton = await analyzeButton.isVisible().catch(() => false); + + if (hasAnalyzeButton) { + await analyzeButton.click(); + + // Look for progress indicator + + const progressIndicator = page.getByTestId('analysis-progress').or( + page.getByText(/analyzing|generating|progress/i) + ); + + const hasProgress = await progressIndicator.isVisible().catch(() => false); + + if (hasProgress) { + expect(progressIndicator).toBeVisible(); + } + + // Wait for analysis to complete + + // Look for insights after analysis + const insightItems = page.getByTestId(/insight-item|insight-card/).or( + page.locator('.insight-item') + ); + + const insightCount = await insightItems.count(); + + // Either insights were generated or there's a message + expect(insightCount).toBeGreaterThanOrEqual(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should display prompt patterns', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to prompt insights page + await page.goto('/memory/insights', { waitUntil: 'networkidle' as const }); + + // Look for patterns section + const patternsSection = page.getByTestId('patterns-section').or( + page.getByText(/patterns|recurring/i) + ); + + const isVisible = await patternsSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify pattern items are displayed + const patternItems = page.getByTestId(/pattern-item|pattern-card/).or( + patternsSection.locator('.pattern-item') + ); + + const patternCount = await patternItems.count(); + + if (patternCount === 0) { + // Empty state or analyze button + const analyzeButton = page.getByRole('button', { name: /analyze/i }); + const hasAnalyzeButton = await analyzeButton.isVisible().catch(() => false); + + const emptyState = page.getByText(/no patterns/i); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + + expect(hasAnalyzeButton || hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should display prompt suggestions', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to prompt insights page + await page.goto('/memory/insights', { waitUntil: 'networkidle' as const }); + + // Look for suggestions section + const suggestionsSection = page.getByTestId('suggestions-section').or( + page.getByText(/suggestions|recommendations/i) + ); + + const isVisible = await suggestionsSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify suggestion items are displayed + const suggestionItems = page.getByTestId(/suggestion-item|suggestion-card/).or( + suggestionsSection.locator('.suggestion-item') + ); + + const suggestionCount = await suggestionItems.count(); + + if (suggestionCount === 0) { + // Empty state or analyze button + const analyzeButton = page.getByRole('button', { name: /analyze/i }); + const hasAnalyzeButton = await analyzeButton.isVisible().catch(() => false); + + const emptyState = page.getByText(/no suggestions/i); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + + expect(hasAnalyzeButton || hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should display prompt timestamp', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to prompt history page + await page.goto('/memory/prompts', { waitUntil: 'networkidle' as const }); + + // Look for prompt items + const promptItems = page.getByTestId(/prompt-item|prompt-card/).or( + page.locator('.prompt-item') + ); + + const itemCount = await promptItems.count(); + + if (itemCount > 0) { + const firstPrompt = promptItems.first(); + + // Look for timestamp display + const timestamp = firstPrompt.getByTestId('prompt-timestamp').or( + firstPrompt.locator('*').filter({ hasText: /\d{4}-\d{2}-\d{2}|\d+\/\d+\/\d+/i }) + ); + + const hasTimestamp = await timestamp.isVisible().catch(() => false); + + if (hasTimestamp) { + const text = await timestamp.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should filter prompts by date range', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to prompt history page + await page.goto('/memory/prompts', { waitUntil: 'networkidle' as const }); + + // Look for date filter + const dateFilter = page.getByRole('combobox', { name: /date|period|range/i }).or( + page.getByTestId('date-filter') + ); + + const hasDateFilter = await dateFilter.isVisible().catch(() => false); + + if (hasDateFilter) { + // Check if there are filter options + const filterOptions = await dateFilter.locator('option').count(); + + if (filterOptions > 1) { + await dateFilter.selectOption({ index: 1 }); + + // Wait for filtered results + + const promptItems = page.getByTestId(/prompt-item|prompt-card/).or( + page.locator('.prompt-item') + ); + + const promptCount = await promptItems.count(); + expect(promptCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should search prompts', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to prompt history page + await page.goto('/memory/prompts', { waitUntil: 'networkidle' as const }); + + // Look for search input + const searchInput = page.getByRole('textbox', { name: /search|find/i }).or( + page.getByTestId('prompt-search') + ); + + const hasSearch = await searchInput.isVisible().catch(() => false); + + if (hasSearch) { + await searchInput.fill('test'); + + // Wait for search results + + // Search should either show results or no results message + const noResults = page.getByText(/no results|not found/i) + const hasNoResults = await noResults.isVisible().catch(() => false); + + const promptItems = page.getByTestId(/prompt-item|prompt-card/).or( + page.locator('.prompt-item') + ); + + const promptCount = await promptItems.count(); + + // Either no results message or filtered prompts + expect(hasNoResults || promptCount >= 0).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should handle prompt memory API errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/memory/prompts/**', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to prompt history page + await page.goto('/memory/prompts', { waitUntil: 'networkidle' as const }); + + // Look for error indicator + const errorIndicator = page.getByText(/error|failed|unable to load/i).or( + page.getByTestId('error-state') + ); + + const hasError = await errorIndicator.isVisible().catch(() => false); + + // Restore routing + await page.unroute('**/api/memory/prompts/**'); + + // Error should be displayed or handled gracefully + expect(hasError).toBe(true); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/memory/prompts'], allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/review.spec.ts b/ccw/frontend/tests/e2e/review.spec.ts new file mode 100644 index 00000000..03369c69 --- /dev/null +++ b/ccw/frontend/tests/e2e/review.spec.ts @@ -0,0 +1,431 @@ +// ======================================== +// E2E Tests: Review Sessions Management +// ======================================== +// End-to-end tests for review sessions list and detail view + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[Review] - Review Sessions Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display review sessions list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to review sessions page + await page.goto('/review', { waitUntil: 'networkidle' as const }); + + // Look for review sessions list container + const sessionsList = page.getByTestId('review-sessions-list').or( + page.locator('.review-sessions-list') + ); + + const isVisible = await sessionsList.isVisible().catch(() => false); + + if (isVisible) { + // Verify session items exist or empty state is shown + const sessionItems = page.getByTestId(/review-item|review-session/).or( + page.locator('.review-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount === 0) { + const emptyState = page.getByTestId('empty-state').or( + page.getByText(/no reviews|empty/i) + ); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should display review session detail', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to review sessions page + await page.goto('/review', { waitUntil: 'networkidle' as const }); + + // Look for review session items + const sessionItems = page.getByTestId(/review-item|review-session/).or( + page.locator('.review-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + + // Click to view detail + await firstSession.click(); + + // Verify detail view loads + await page.waitForURL(/\/review\//); + + const detailContainer = page.getByTestId('review-detail').or( + page.locator('.review-detail') + ); + + const hasDetail = await detailContainer.isVisible().catch(() => false); + expect(hasDetail).toBe(true); + + // Verify session info is displayed + const sessionInfo = page.getByTestId('review-info').or( + page.locator('.review-info') + ); + + const hasInfo = await sessionInfo.isVisible().catch(() => false); + expect(hasInfo).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should display review session title', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to review sessions page + await page.goto('/review', { waitUntil: 'networkidle' as const }); + + // Look for review session items + const sessionItems = page.getByTestId(/review-item|review-session/).or( + page.locator('.review-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + // Check each session has a title + for (let i = 0; i < Math.min(itemCount, 3); i++) { + const session = sessionItems.nth(i); + + const titleElement = session.getByTestId('review-title').or( + session.locator('.review-title') + ); + + const hasTitle = await titleElement.isVisible().catch(() => false); + expect(hasTitle).toBe(true); + + const title = await titleElement.textContent(); + expect(title).toBeTruthy(); + expect(title?.length).toBeGreaterThan(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should display review findings', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to review sessions page + await page.goto('/review', { waitUntil: 'networkidle' as const }); + + // Look for review session items + const sessionItems = page.getByTestId(/review-item|review-session/).or( + page.locator('.review-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + + // Look for findings count + const findingsCount = firstSession.getByTestId('findings-count').or( + firstSession.locator('*').filter({ hasText: /\d+\s*findings/i }) + ); + + const hasFindingsCount = await findingsCount.isVisible().catch(() => false); + + if (hasFindingsCount) { + const text = await findingsCount.textContent(); + expect(text).toBeTruthy(); + } + + // Click to view findings + await firstSession.click(); + + await page.waitForURL(/\/review\//); + + // Look for findings list + const findingsList = page.getByTestId('findings-list').or( + page.locator('.findings-list') + ); + + const hasFindings = await findingsList.isVisible().catch(() => false); + + if (hasFindings) { + const findingItems = page.getByTestId(/finding-item|finding-card/).or( + page.locator('.finding-item') + ); + + const findingCount = await findingItems.count(); + expect(findingCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should display review dimensions', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to review sessions page + await page.goto('/review', { waitUntil: 'networkidle' as const }); + + // Look for review session items + const sessionItems = page.getByTestId(/review-item|review-session/).or( + page.locator('.review-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/review\//); + + // Look for dimensions list + const dimensionsList = page.getByTestId('review-dimensions').or( + page.locator('.review-dimensions') + ); + + const hasDimensions = await dimensionsList.isVisible().catch(() => false); + + if (hasDimensions) { + const dimensionItems = page.getByTestId(/dimension-item|dimension-card/).or( + dimensionsList.locator('.dimension-item') + ); + + const dimensionCount = await dimensionItems.count(); + expect(dimensionCount).toBeGreaterThan(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should display finding severity', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to review sessions page + await page.goto('/review', { waitUntil: 'networkidle' as const }); + + // Look for review session items + const sessionItems = page.getByTestId(/review-item|review-session/).or( + page.locator('.review-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/review\//); + + // Look for finding items + const findingItems = page.getByTestId(/finding-item|finding-card/).or( + page.locator('.finding-item') + ); + + const findingCount = await findingItems.count(); + + if (findingCount > 0) { + const firstFinding = findingItems.first(); + + // Look for severity badge + const severityBadge = firstFinding.getByTestId('finding-severity').or( + firstFinding.locator('*').filter({ hasText: /critical|high|medium|low/i }) + ); + + const hasSeverity = await severityBadge.isVisible().catch(() => false); + expect(hasSeverity).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should filter findings by severity', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to review sessions page + await page.goto('/review', { waitUntil: 'networkidle' as const }); + + // Look for review session items + const sessionItems = page.getByTestId(/review-item|review-session/).or( + page.locator('.review-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/review\//); + + // Look for severity filter + const severityFilter = page.getByRole('combobox', { name: /severity|filter/i }).or( + page.getByTestId('severity-filter') + ); + + const hasFilter = await severityFilter.isVisible().catch(() => false); + + if (hasFilter) { + const filterOptions = await severityFilter.locator('option').count(); + + if (filterOptions > 1) { + await severityFilter.selectOption({ index: 1 }); + + // Wait for filtered results + + const findingItems = page.getByTestId(/finding-item|finding-card/).or( + page.locator('.finding-item') + ); + + const findingCount = await findingItems.count(); + expect(findingCount).toBeGreaterThanOrEqual(0); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should display finding recommendations', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to review sessions page + await page.goto('/review', { waitUntil: 'networkidle' as const }); + + // Look for review session items + const sessionItems = page.getByTestId(/review-item|review-session/).or( + page.locator('.review-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/review\//); + + // Look for finding items + const findingItems = page.getByTestId(/finding-item|finding-card/).or( + page.locator('.finding-item') + ); + + const findingCount = await findingItems.count(); + + if (findingCount > 0) { + const firstFinding = findingItems.first(); + + // Look for recommendations section + const recommendations = firstFinding.getByTestId('finding-recommendations').or( + firstFinding.locator('*').filter({ hasText: /recommend|fix|suggestion/i }) + ); + + const hasRecommendations = await recommendations.isVisible().catch(() => false); + + if (hasRecommendations) { + const text = await recommendations.textContent(); + expect(text).toBeTruthy(); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should export review report', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to review sessions page + await page.goto('/review', { waitUntil: 'networkidle' as const }); + + // Look for review session items + const sessionItems = page.getByTestId(/review-item|review-session/).or( + page.locator('.review-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + + // Look for export button + const exportButton = firstSession.getByRole('button', { name: /export|download|report/i }).or( + firstSession.getByTestId('export-button') + ); + + const hasExportButton = await exportButton.isVisible().catch(() => false); + + if (hasExportButton) { + // Click export and verify download starts + const downloadPromise = page.waitForEvent('download'); + + await exportButton.click(); + + const download = await downloadPromise.catch(() => null); + + // Either download started or there was feedback + const successMessage = page.getByText(/exporting|downloading|success/i); + const hasMessage = await successMessage.isVisible().catch(() => false); + + expect(download !== null || hasMessage).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should handle review API errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/**/review**', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to review sessions page + await page.goto('/review', { waitUntil: 'networkidle' as const }); + + // Look for error indicator + const errorIndicator = page.getByText(/error|failed|unable to load/i).or( + page.getByTestId('error-state') + ); + + const hasError = await errorIndicator.isVisible().catch(() => false); + + // Restore routing + await page.unroute('**/api/**/review**'); + + // Error should be displayed or handled gracefully + expect(hasError).toBe(true); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api'], allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/rules.spec.ts b/ccw/frontend/tests/e2e/rules.spec.ts new file mode 100644 index 00000000..ddc1fae8 --- /dev/null +++ b/ccw/frontend/tests/e2e/rules.spec.ts @@ -0,0 +1,439 @@ +// ======================================== +// E2E Tests: Rules Management +// ======================================== +// End-to-end tests for rules CRUD and toggle operations + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[Rules] - Rules Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display rules list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to rules page + await page.goto('/settings/rules', { waitUntil: 'networkidle' as const }); + + // Look for rules list container + const rulesList = page.getByTestId('rules-list').or( + page.locator('.rules-list') + ); + + const isVisible = await rulesList.isVisible().catch(() => false); + + if (isVisible) { + // Verify rule items exist or empty state is shown + const ruleItems = page.getByTestId(/rule-item|rule-card/).or( + page.locator('.rule-item') + ); + + const itemCount = await ruleItems.count(); + + if (itemCount === 0) { + const emptyState = page.getByTestId('empty-state').or( + page.getByText(/no rules|empty/i) + ); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should create new rule', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to rules page + await page.goto('/settings/rules', { waitUntil: 'networkidle' as const }); + + // Look for create rule button + const createButton = page.getByRole('button', { name: /create|new|add rule/i }).or( + page.getByTestId('create-rule-button') + ); + + const hasCreateButton = await createButton.isVisible().catch(() => false); + + if (hasCreateButton) { + await createButton.click(); + + // Look for create rule dialog/form + const dialog = page.getByRole('dialog').filter({ hasText: /create rule|new rule/i }); + const form = page.getByTestId('create-rule-form'); + + const hasDialog = await dialog.isVisible().catch(() => false); + const hasForm = await form.isVisible().catch(() => false); + + if (hasDialog || hasForm) { + // Fill in rule details + const ruleInput = page.getByRole('textbox', { name: /rule|pattern|name/i }).or( + page.getByLabel(/rule|pattern|name/i) + ); + + const hasRuleInput = await ruleInput.isVisible().catch(() => false); + + if (hasRuleInput) { + await ruleInput.fill('e2e-test-rule'); + + // Select scope if available + const scopeSelect = page.getByRole('combobox', { name: /scope/i }); + const hasScopeSelect = await scopeSelect.isVisible().catch(() => false); + + if (hasScopeSelect) { + const scopeOptions = await scopeSelect.locator('option').count(); + if (scopeOptions > 0) { + await scopeSelect.selectOption({ index: 0 }); + } + } + + const submitButton = page.getByRole('button', { name: /create|save|submit/i }); + await submitButton.click(); + + // Verify rule was created + + const successMessage = page.getByText(/created|success/i).or( + page.getByTestId('success-message') + ); + + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should update rule', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to rules page + await page.goto('/settings/rules', { waitUntil: 'networkidle' as const }); + + // Look for existing rule + const ruleItems = page.getByTestId(/rule-item|rule-card/).or( + page.locator('.rule-item') + ); + + const itemCount = await ruleItems.count(); + + if (itemCount > 0) { + const firstRule = ruleItems.first(); + + // Look for edit button + const editButton = firstRule.getByRole('button', { name: /edit|modify|configure/i }).or( + firstRule.getByTestId('edit-rule-button') + ); + + const hasEditButton = await editButton.isVisible().catch(() => false); + + if (hasEditButton) { + await editButton.click(); + + // Update rule details + const ruleInput = page.getByRole('textbox', { name: /rule|description/i }); + const hasRuleInput = await ruleInput.isVisible().catch(() => false); + + if (hasRuleInput) { + await ruleInput.clear(); + await ruleInput.fill('updated e2e-test-rule'); + } + + // Save changes + const saveButton = page.getByRole('button', { name: /save|update|submit/i }); + await saveButton.click(); + + // Verify success message + + const successMessage = page.getByText(/updated|saved|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should delete rule', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to rules page + await page.goto('/settings/rules', { waitUntil: 'networkidle' as const }); + + // Look for existing rule + const ruleItems = page.getByTestId(/rule-item|rule-card/).or( + page.locator('.rule-item') + ); + + const itemCount = await ruleItems.count(); + + if (itemCount > 0) { + const firstRule = ruleItems.first(); + + // Look for delete button + const deleteButton = firstRule.getByRole('button', { name: /delete|remove/i }).or( + firstRule.getByTestId('delete-button') + ); + + const hasDeleteButton = await deleteButton.isVisible().catch(() => false); + + if (hasDeleteButton) { + await deleteButton.click(); + + // Confirm delete if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /delete|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /delete|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify success message + + const successMessage = page.getByText(/deleted|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should toggle rule enabled status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to rules page + await page.goto('/settings/rules', { waitUntil: 'networkidle' as const }); + + // Look for rule items + const ruleItems = page.getByTestId(/rule-item|rule-card/).or( + page.locator('.rule-item') + ); + + const itemCount = await ruleItems.count(); + + if (itemCount > 0) { + const firstRule = ruleItems.first(); + + // Look for toggle switch + const toggleSwitch = firstRule.getByRole('switch').or( + firstRule.getByTestId('rule-toggle') + ).or( + firstRule.getByRole('button', { name: /enable|disable|toggle/i }) + ); + + const hasToggle = await toggleSwitch.isVisible().catch(() => false); + + if (hasToggle) { + // Get initial state + const initialState = await toggleSwitch.getAttribute('aria-checked'); + const initialChecked = initialState === 'true'; + + // Toggle the rule + await toggleSwitch.click(); + + // Wait for update + + // Verify state changed + const newState = await toggleSwitch.getAttribute('aria-checked'); + const newChecked = newState === 'true'; + + expect(newChecked).toBe(!initialChecked); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should display rule scope', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to rules page + await page.goto('/settings/rules', { waitUntil: 'networkidle' as const }); + + // Look for rule items + const ruleItems = page.getByTestId(/rule-item|rule-card/).or( + page.locator('.rule-item') + ); + + const itemCount = await ruleItems.count(); + + if (itemCount > 0) { + const firstRule = ruleItems.first(); + + // Look for scope badge + const scopeBadge = firstRule.getByTestId('rule-scope').or( + firstRule.locator('*').filter({ hasText: /project|workspace|global/i }) + ); + + const hasScope = await scopeBadge.isVisible().catch(() => false); + + if (hasScope) { + const text = await scopeBadge.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should filter rules by scope', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to rules page + await page.goto('/settings/rules', { waitUntil: 'networkidle' as const }); + + // Look for scope filter + const scopeFilter = page.getByRole('combobox', { name: /scope|filter/i }).or( + page.getByTestId('scope-filter') + ); + + const hasScopeFilter = await scopeFilter.isVisible().catch(() => false); + + if (hasScopeFilter) { + // Check if there are scope options + const scopeOptions = await scopeFilter.locator('option').count(); + + if (scopeOptions > 1) { + await scopeFilter.selectOption({ index: 1 }); + + // Wait for filtered results + + const ruleItems = page.getByTestId(/rule-item|rule-card/).or( + page.locator('.rule-item') + ); + + const ruleCount = await ruleItems.count(); + expect(ruleCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should search rules', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to rules page + await page.goto('/settings/rules', { waitUntil: 'networkidle' as const }); + + // Look for search input + const searchInput = page.getByRole('textbox', { name: /search|find/i }).or( + page.getByTestId('rule-search') + ); + + const hasSearch = await searchInput.isVisible().catch(() => false); + + if (hasSearch) { + await searchInput.fill('test'); + + // Wait for search results + + // Search should either show results or no results message + const noResults = page.getByText(/no results|not found/i); + const hasNoResults = await noResults.isVisible().catch(() => false); + + const ruleItems = page.getByTestId(/rule-item|rule-card/).or( + page.locator('.rule-item') + ); + + const ruleCount = await ruleItems.count(); + + // Either no results message or filtered rules + expect(hasNoResults || ruleCount >= 0).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should display rule enforcement status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to rules page + await page.goto('/settings/rules', { waitUntil: 'networkidle' as const }); + + // Look for rule items + const ruleItems = page.getByTestId(/rule-item|rule-card/).or( + page.locator('.rule-item') + ); + + const itemCount = await ruleItems.count(); + + if (itemCount > 0) { + const firstRule = ruleItems.first(); + + // Look for enforced by indicator + const enforcedByBadge = firstRule.getByTestId('rule-enforced-by').or( + firstRule.locator('*').filter({ hasText: /enforced by|lint|hook/i }) + ); + + const hasEnforcedBy = await enforcedByBadge.isVisible().catch(() => false); + + if (hasEnforcedBy) { + const text = await enforcedByBadge.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should handle rules API errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/rules/**', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to rules page + await page.goto('/settings/rules', { waitUntil: 'networkidle' as const }); + + // Try to create a rule + const createButton = page.getByRole('button', { name: /create|new|add/i }); + const hasCreateButton = await createButton.isVisible().catch(() => false); + + if (hasCreateButton) { + await createButton.click(); + + const ruleInput = page.getByRole('textbox', { name: /rule|name/i }); + const hasRuleInput = await ruleInput.isVisible().catch(() => false); + + if (hasRuleInput) { + await ruleInput.fill('test-rule'); + + const submitButton = page.getByRole('button', { name: /create|save/i }); + await submitButton.click(); + + // Look for error message + + const errorMessage = page.getByText(/error|failed|unable/i); + const hasError = await errorMessage.isVisible().catch(() => false); + expect(hasError).toBe(true); + } + } + + // Restore routing + await page.unroute('**/api/rules/**'); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/rules'], allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/session-detail.spec.ts b/ccw/frontend/tests/e2e/session-detail.spec.ts new file mode 100644 index 00000000..cc024793 --- /dev/null +++ b/ccw/frontend/tests/e2e/session-detail.spec.ts @@ -0,0 +1,400 @@ +// ======================================== +// E2E Tests: Session Detail +// ======================================== +// End-to-end tests for session detail view and related operations + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[Session Detail] - Session Detail Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display session detail', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page first + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Look for session with detail + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + // Verify detail view loads + await page.waitForURL(/\/sessions\//); + + const detailContainer = page.getByTestId('session-detail').or( + page.locator('.session-detail') + ); + + const hasDetail = await detailContainer.isVisible().catch(() => false); + expect(hasDetail).toBe(true); + + // Verify session title is displayed + const titleElement = page.getByTestId('session-title').or( + page.locator('h1, h2').filter({ hasText: /.+/ }) + ); + + const hasTitle = await titleElement.isVisible().catch(() => false); + expect(hasTitle).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should display session context', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for context section + const contextSection = page.getByTestId('session-context').or( + page.getByText(/context|requirements/i) + ); + + const hasContext = await contextSection.isVisible().catch(() => false); + + if (hasContext) { + // Verify context items are displayed + const contextItems = page.getByTestId(/context-item|requirement/).or( + contextSection.locator('.context-item') + ); + + const contextCount = await contextItems.count(); + expect(contextCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should display session summary', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for summary section + const summarySection = page.getByTestId('session-summary').or( + page.getByText(/summary|overview/i) + ); + + const hasSummary = await summarySection.isVisible().catch(() => false); + + if (hasSummary) { + const summaryContent = await summarySection.textContent(); + // Verify summary has some content or empty state message + expect(summaryContent?.length).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should display implementation plan', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for implementation plan section + const implPlanSection = page.getByTestId('implementation-plan').or( + page.getByText(/implementation|plan/i) + ); + + const hasImplPlan = await implPlanSection.isVisible().catch(() => false); + + if (hasImplPlan) { + // Verify plan items are displayed + const planItems = page.getByTestId(/plan-item|step-item/).or( + implPlanSection.locator('.plan-item') + ); + + const planCount = await planItems.count(); + expect(planCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should display session status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for status badge + const statusBadge = page.getByTestId('session-status').or( + page.locator('*').filter({ hasText: /in.progress|completed|planning|paused|archived/i }) + ); + + const hasStatus = await statusBadge.isVisible().catch(() => false); + expect(hasStatus).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should display session tasks', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for tasks section + const tasksSection = page.getByTestId('session-tasks').or( + page.getByText(/tasks/i) + ); + + const hasTasks = await tasksSection.isVisible().catch(() => false); + + if (hasTasks) { + const taskItems = page.getByTestId(/task-item|task-card/).or( + page.locator('.task-item') + ); + + const taskCount = await taskItems.count(); + expect(taskCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should navigate back to sessions list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for back button + const backButton = page.getByRole('button', { name: /back|return|sessions/i }).or( + page.getByTestId('back-button') + ); + + const hasBackButton = await backButton.isVisible().catch(() => false); + + if (hasBackButton) { + await backButton.click(); + + // Verify navigation back to list + await page.waitForURL(/\/sessions$/); + + const currentUrl = page.url(); + expect(currentUrl).toMatch(/\/sessions$/); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should display session metadata', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for metadata section + const metadataSection = page.getByTestId('session-metadata').or( + page.getByText(/metadata|created|updated/i) + ); + + const hasMetadata = await metadataSection.isVisible().catch(() => false); + + if (hasMetadata) { + // Verify dates are displayed + const datePattern = /\d{4}-\d{2}-\d{2}|created|updated/i; + const hasDates = await metadataSection.locator('*').filter({ hasText: datePattern }).isVisible(); + + expect(hasDates).toBe(true); + } + } + + monitoring.assertClean({ ignoreAPIPatterns: ['/api'], allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should handle session detail API errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure + await page.route('**/api/session-detail**', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + // Look for error indicator + + const errorIndicator = page.getByText(/error|failed|unable to load/i).or( + page.getByTestId('error-state') + ); + + const hasError = await errorIndicator.isVisible().catch(() => false); + + // Restore routing + await page.unroute('**/api/session-detail**'); + + // Error should be displayed or handled gracefully + expect(hasError).toBe(true); + } + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/session-detail'], allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should display session summaries list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for summaries section + const summariesSection = page.getByTestId('session-summaries').or( + page.getByText(/summaries/i) + ); + + const hasSummaries = await summariesSection.isVisible().catch(() => false); + + if (hasSummaries) { + const summaryItems = page.getByTestId(/summary-item/).or( + summariesSection.locator('.summary-item') + ); + + const summaryCount = await summaryItems.count(); + expect(summaryCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/sessions-crud.spec.ts b/ccw/frontend/tests/e2e/sessions-crud.spec.ts new file mode 100644 index 00000000..50c5b8f1 --- /dev/null +++ b/ccw/frontend/tests/e2e/sessions-crud.spec.ts @@ -0,0 +1,449 @@ +// ======================================== +// E2E Tests: Sessions CRUD Operations +// ======================================== +// End-to-end tests for session create, read, update, archive, and delete operations + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring, switchLanguageAndVerify } from './helpers/i18n-helpers'; + +test.describe('[Sessions CRUD] - Session Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display sessions list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Look for sessions list container + const sessionsList = page.getByTestId('sessions-list').or( + page.locator('.sessions-list') + ); + + const isVisible = await sessionsList.isVisible().catch(() => false); + + if (isVisible) { + // Verify session items exist or empty state is shown + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount === 0) { + const emptyState = page.getByTestId('empty-state').or( + page.getByText(/no sessions/i) + ); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should create new session', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Look for create session button + const createButton = page.getByRole('button', { name: /create|new|add session/i }).or( + page.getByTestId('create-session-button') + ); + + const hasCreateButton = await createButton.isVisible().catch(() => false); + + if (hasCreateButton) { + await createButton.click(); + + // Look for create session dialog/form + const dialog = page.getByRole('dialog').filter({ hasText: /create session|new session/i }); + const form = page.getByTestId('create-session-form'); + + const hasDialog = await dialog.isVisible().catch(() => false); + const hasForm = await form.isVisible().catch(() => false); + + if (hasDialog || hasForm) { + // Fill in session details + const titleInput = page.getByRole('textbox', { name: /title|name/i }).or( + page.getByLabel(/title|name/i) + ); + + const hasTitleInput = await titleInput.isVisible().catch(() => false); + + if (hasTitleInput) { + await titleInput.fill('E2E Test Session'); + + const submitButton = page.getByRole('button', { name: /create|save|submit/i }); + await submitButton.click(); + + // Verify session was created + + const successMessage = page.getByText(/created|success/i).or( + page.getByTestId('success-message') + ); + + // Either success message or the session appears in list + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should read session details', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Look for existing session + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + + // Click on session to view details + await firstSession.click(); + + // Verify session detail page loads + await page.waitForURL(/\/sessions\//); + + const detailContainer = page.getByTestId('session-detail').or( + page.locator('.session-detail') + ); + + const hasDetail = await detailContainer.isVisible().catch(() => false); + expect(hasDetail).toBe(true); + + // Verify session title is displayed + const titleElement = page.getByTestId('session-title').or( + page.locator('h1, h2').filter({ hasText: /.+/ }) + ); + + const hasTitle = await titleElement.isVisible().catch(() => false); + expect(hasTitle).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should update session title and description', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Look for existing session + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + + // Click on session to view details + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for edit button + const editButton = page.getByRole('button', { name: /edit|modify/i }).or( + page.getByTestId('edit-session-button') + ); + + const hasEditButton = await editButton.isVisible().catch(() => false); + + if (hasEditButton) { + await editButton.click(); + + // Update title + const titleInput = page.getByRole('textbox', { name: /title|name/i }); + await titleInput.clear(); + await titleInput.fill('Updated E2E Test Session'); + + // Update description + const descInput = page.getByRole('textbox', { name: /description/i }).or( + page.getByLabel(/description/i) + ); + + const hasDescInput = await descInput.isVisible().catch(() => false); + + if (hasDescInput) { + await descInput.fill('Updated description for E2E testing'); + } + + // Save changes + const saveButton = page.getByRole('button', { name: /save|update|submit/i }); + await saveButton.click(); + + // Verify success message + + const successMessage = page.getByText(/updated|saved|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should archive session', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Look for existing session + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + + // Look for archive button + const archiveButton = firstSession.getByRole('button', { name: /archive/i }).or( + firstSession.getByTestId('archive-button') + ); + + const hasArchiveButton = await archiveButton.isVisible().catch(() => false); + + if (hasArchiveButton) { + await archiveButton.click(); + + // Confirm archive if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /archive|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /archive|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify success message + + const successMessage = page.getByText(/archived|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should delete session', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Look for existing session + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + + // Look for delete button + const deleteButton = firstSession.getByRole('button', { name: /delete|remove/i }).or( + firstSession.getByTestId('delete-button') + ); + + const hasDeleteButton = await deleteButton.isVisible().catch(() => false); + + if (hasDeleteButton) { + await deleteButton.click(); + + // Confirm delete if dialog appears + const confirmDialog = page.getByRole('dialog').filter({ hasText: /delete|confirm/i }); + const hasDialog = await confirmDialog.isVisible().catch(() => false); + + if (hasDialog) { + const confirmButton = page.getByRole('button', { name: /delete|confirm|yes/i }); + await confirmButton.click(); + } + + // Verify success message + + const successMessage = page.getByText(/deleted|success/i); + const hasSuccess = await successMessage.isVisible().catch(() => false); + expect(hasSuccess).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should update list after mutation', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Get initial session count + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const initialCount = await sessionItems.count(); + + // Look for create button + const createButton = page.getByRole('button', { name: /create|new|add/i }); + const hasCreateButton = await createButton.isVisible().catch(() => false); + + if (hasCreateButton) { + await createButton.click(); + + // Quick fill and submit + const titleInput = page.getByRole('textbox', { name: /title|name/i }); + const hasTitleInput = await titleInput.isVisible().catch(() => false); + + if (hasTitleInput) { + await titleInput.fill('List Update Test Session'); + + const submitButton = page.getByRole('button', { name: /create|save/i }); + await submitButton.click(); + + // Wait for list update + + // Verify list is updated + const newCount = await sessionItems.count(); + expect(newCount).toBe(initialCount + 1); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should handle API errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure for sessions + await page.route('**/api/sessions', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to sessions page + await page.reload({ waitUntil: 'networkidle' as const }); + + // Look for error indicator + const errorIndicator = page.getByText(/error|failed|unable to load/i).or( + page.getByTestId('error-state') + ); + + const hasError = await errorIndicator.isVisible().catch(() => false); + + // Restore routing + await page.unroute('**/api/sessions'); + + // Error should be displayed or handled gracefully + expect(hasError).toBe(true); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/sessions'], allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should support i18n in session operations', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Get language switcher + const languageSwitcher = page.getByRole('combobox', { name: /select language|language/i }).first(); + + const hasLanguageSwitcher = await languageSwitcher.isVisible().catch(() => false); + + if (hasLanguageSwitcher) { + // Switch to Chinese + await switchLanguageAndVerify(page, 'zh', languageSwitcher); + + // Verify session-related UI is in Chinese + const sessionsHeading = page.getByRole('heading', { name: /session/i }).or( + page.locator('h1, h2').filter({ hasText: /session/i }) + ); + + const hasHeading = await sessionsHeading.isVisible().catch(() => false); + if (hasHeading) { + const pageContent = await page.content(); + const hasChineseText = /[\u4e00-\u9fa5]/.test(pageContent); + expect(hasChineseText).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should display session tasks', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Look for existing session + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + + // Click on session to view details + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for tasks section + const tasksSection = page.getByTestId('session-tasks').or( + page.getByText(/tasks/i) + ); + + const hasTasksSection = await tasksSection.isVisible().catch(() => false); + + if (hasTasksSection) { + const taskItems = page.getByTestId(/task-item|task-card/).or( + page.locator('.task-item') + ); + + const taskCount = await taskItems.count(); + // Tasks can be empty, just verify the section exists + expect(taskCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/skills.spec.ts b/ccw/frontend/tests/e2e/skills.spec.ts new file mode 100644 index 00000000..2ede306d --- /dev/null +++ b/ccw/frontend/tests/e2e/skills.spec.ts @@ -0,0 +1,364 @@ +// ======================================== +// E2E Tests: Skills Management +// ======================================== +// End-to-end tests for skills list and toggle operations + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring, switchLanguageAndVerify } from './helpers/i18n-helpers'; + +test.describe('[Skills] - Skills Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display skills list', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to skills page + await page.goto('/skills', { waitUntil: 'networkidle' as const }); + + // Look for skills list container + const skillsList = page.getByTestId('skills-list').or( + page.locator('.skills-list') + ); + + const isVisible = await skillsList.isVisible().catch(() => false); + + if (isVisible) { + // Verify skill items exist + const skillItems = page.getByTestId(/skill-item|skill-card/).or( + page.locator('.skill-item') + ); + + const itemCount = await skillItems.count(); + expect(itemCount).toBeGreaterThan(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should toggle skill enabled status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to skills page + await page.goto('/skills', { waitUntil: 'networkidle' as const }); + + // Look for skill items + const skillItems = page.getByTestId(/skill-item|skill-card/).or( + page.locator('.skill-item') + ); + + const itemCount = await skillItems.count(); + + if (itemCount > 0) { + const firstSkill = skillItems.first(); + + // Look for toggle switch/button + const toggleSwitch = firstSkill.getByRole('switch').or( + firstSkill.getByTestId('skill-toggle') + ).or( + firstSkill.getByRole('button', { name: /enable|disable|toggle/i }) + ); + + const hasToggle = await toggleSwitch.isVisible().catch(() => false); + + if (hasToggle) { + // Get initial state + const initialState = await toggleSwitch.getAttribute('aria-checked'); + const initialChecked = initialState === 'true'; + + // Toggle the skill + await toggleSwitch.click(); + + // Wait for update + + // Verify state changed + const newState = await toggleSwitch.getAttribute('aria-checked'); + const newChecked = newState === 'true'; + + expect(newChecked).toBe(!initialChecked); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should display skill description', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to skills page + await page.goto('/skills', { waitUntil: 'networkidle' as const }); + + // Look for skill items + const skillItems = page.getByTestId(/skill-item|skill-card/).or( + page.locator('.skill-item') + ); + + const itemCount = await skillItems.count(); + + if (itemCount > 0) { + const firstSkill = skillItems.first(); + + // Look for skill description + const description = firstSkill.getByTestId('skill-description').or( + firstSkill.locator('.skill-description') + ); + + const hasDescription = await description.isVisible().catch(() => false); + + if (hasDescription) { + const text = await description.textContent(); + expect(text).toBeTruthy(); + expect(text?.length).toBeGreaterThan(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should display skill triggers', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to skills page + await page.goto('/skills', { waitUntil: 'networkidle' as const }); + + // Look for skill items + const skillItems = page.getByTestId(/skill-item|skill-card/).or( + page.locator('.skill-item') + ); + + const itemCount = await skillItems.count(); + + if (itemCount > 0) { + const firstSkill = skillItems.first(); + + // Look for triggers section + const triggers = firstSkill.getByTestId('skill-triggers').or( + firstSkill.locator('.skill-triggers') + ); + + const hasTriggers = await triggers.isVisible().catch(() => false); + + if (hasTriggers) { + const text = await triggers.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should filter skills by category', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to skills page + await page.goto('/skills', { waitUntil: 'networkidle' as const }); + + // Look for category filter + const categoryFilter = page.getByRole('combobox', { name: /category|filter/i }).or( + page.getByTestId('category-filter') + ); + + const hasCategoryFilter = await categoryFilter.isVisible().catch(() => false); + + if (hasCategoryFilter) { + // Check if there are category options + const categoryOptions = await categoryFilter.locator('option').count(); + + if (categoryOptions > 1) { + await categoryFilter.selectOption({ index: 1 }); + + // Wait for filtered results + + const skillItems = page.getByTestId(/skill-item|skill-card/).or( + page.locator('.skill-item') + ); + + const skillCount = await skillItems.count(); + expect(skillCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should search skills', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to skills page + await page.goto('/skills', { waitUntil: 'networkidle' as const }); + + // Look for search input + const searchInput = page.getByRole('textbox', { name: /search|find/i }).or( + page.getByTestId('skill-search') + ); + + const hasSearch = await searchInput.isVisible().catch(() => false); + + if (hasSearch) { + await searchInput.fill('test'); + + // Wait for search results + + // Search should either show results or no results message + const noResults = page.getByText(/no results|not found/i); + const hasNoResults = await noResults.isVisible().catch(() => false); + + const skillItems = page.getByTestId(/skill-item|skill-card/).or( + page.locator('.skill-item') + ); + + const skillCount = await skillItems.count(); + + // Either no results message or filtered skills + expect(hasNoResults || skillCount >= 0).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should display skill source type', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to skills page + await page.goto('/skills', { waitUntil: 'networkidle' as const }); + + // Look for skill items + const skillItems = page.getByTestId(/skill-item|skill-card/).or( + page.locator('.skill-item') + ); + + const itemCount = await skillItems.count(); + + if (itemCount > 0) { + const firstSkill = skillItems.first(); + + // Look for source badge + const sourceBadge = firstSkill.getByTestId('skill-source').or( + firstSkill.locator('*').filter({ hasText: /builtin|custom|community/i }) + ); + + const hasSource = await sourceBadge.isVisible().catch(() => false); + + if (hasSource) { + const text = await sourceBadge.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should support i18n in skills page', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to skills page + await page.goto('/skills', { waitUntil: 'networkidle' as const }); + + // Get language switcher + const languageSwitcher = page.getByRole('combobox', { name: /select language|language/i }).first(); + + const hasLanguageSwitcher = await languageSwitcher.isVisible().catch(() => false); + + if (hasLanguageSwitcher) { + // Switch to Chinese + await switchLanguageAndVerify(page, 'zh', languageSwitcher); + + // Verify skills-related UI is in Chinese + const pageContent = await page.content(); + const hasChineseText = /[\u4e00-\u9fa5]/.test(pageContent); + expect(hasChineseText).toBe(true); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should display skill version', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to skills page + await page.goto('/skills', { waitUntil: 'networkidle' as const }); + + // Look for skill items + const skillItems = page.getByTestId(/skill-item|skill-card/).or( + page.locator('.skill-item') + ); + + const itemCount = await skillItems.count(); + + if (itemCount > 0) { + const firstSkill = skillItems.first(); + + // Look for version badge + const versionBadge = firstSkill.getByTestId('skill-version').or( + firstSkill.locator('*').filter({ hasText: /v\d+\./i }) + ); + + const hasVersion = await versionBadge.isVisible().catch(() => false); + + if (hasVersion) { + const text = await versionBadge.textContent(); + expect(text).toBeTruthy(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should handle toggle errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure for skill toggle + await page.route('**/api/skills/**', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to skills page + await page.goto('/skills', { waitUntil: 'networkidle' as const }); + + // Try to toggle a skill + const skillItems = page.getByTestId(/skill-item|skill-card/).or( + page.locator('.skill-item') + ); + + const itemCount = await skillItems.count(); + + if (itemCount > 0) { + const firstSkill = skillItems.first(); + const toggleSwitch = firstSkill.getByRole('switch').or( + firstSkill.getByTestId('skill-toggle') + ); + + const hasToggle = await toggleSwitch.isVisible().catch(() => false); + + if (hasToggle) { + await toggleSwitch.click(); + + // Look for error message + + const errorMessage = page.getByText(/error|failed|unable/i); + const hasError = await errorMessage.isVisible().catch(() => false); + expect(hasError).toBe(true); + } + } + + // Restore routing + await page.unroute('**/api/skills/**'); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/skills'], allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/tasks.spec.ts b/ccw/frontend/tests/e2e/tasks.spec.ts new file mode 100644 index 00000000..c2e24e3a --- /dev/null +++ b/ccw/frontend/tests/e2e/tasks.spec.ts @@ -0,0 +1,494 @@ +// ======================================== +// E2E Tests: Tasks Management +// ======================================== +// End-to-end tests for task fetching and updates + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring } from './helpers/i18n-helpers'; + +test.describe('[Tasks] - Task Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display tasks for session', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page first + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Look for session with tasks + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + // Wait for navigation to session detail + await page.waitForURL(/\/sessions\//); + + // Look for tasks section + const tasksSection = page.getByTestId('session-tasks').or( + page.locator('.tasks-section') + ); + + const hasTasksSection = await tasksSection.isVisible().catch(() => false); + + if (hasTasksSection) { + const taskItems = page.getByTestId(/task-item|task-card/).or( + page.locator('.task-item') + ); + + const taskCount = await taskItems.count(); + expect(taskCount).toBeGreaterThanOrEqual(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should update task status', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Look for session with tasks + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for task items + const taskItems = page.getByTestId(/task-item|task-card/).or( + page.locator('.task-item') + ); + + const taskCount = await taskItems.count(); + + if (taskCount > 0) { + const firstTask = taskItems.first(); + + // Look for status change button/dropdown + const statusButton = firstTask.getByRole('button', { name: /status|change/i }).or( + firstTask.getByTestId('task-status-button') + ); + + const hasStatusButton = await statusButton.isVisible().catch(() => false); + + if (hasStatusButton) { + await statusButton.click(); + + // Select new status + const statusOption = page.getByRole('option', { name: /completed|done/i }).or( + page.getByRole('menuitem', { name: /completed|done/i }) + ); + + const hasOption = await statusOption.isVisible().catch(() => false); + + if (hasOption) { + await statusOption.click(); + + // Verify status updated + + const completedIndicator = firstTask.getByText(/completed|done/i); + const hasCompleted = await completedIndicator.isVisible().catch(() => false); + expect(hasCompleted).toBe(true); + } + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should display task details', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Look for session with tasks + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for task items + const taskItems = page.getByTestId(/task-item|task-card/).or( + page.locator('.task-item') + ); + + const taskCount = await taskItems.count(); + + if (taskCount > 0) { + const firstTask = taskItems.first(); + + // Verify task has title + const taskTitle = firstTask.getByTestId('task-title').or( + firstTask.locator('.task-title') + ); + + const hasTitle = await taskTitle.isVisible().catch(() => false); + expect(hasTitle).toBe(true); + + // Verify task has status indicator + const taskStatus = firstTask.getByTestId('task-status').or( + firstTask.locator('.task-status') + ); + + const hasStatus = await taskStatus.isVisible().catch(() => false); + expect(hasStatus).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should handle task update errors gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Mock API failure for task updates + await page.route('**/api/sessions/*/tasks/*', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'Internal Server Error' }), + }); + }); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Try to update a task + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + const taskItems = page.getByTestId(/task-item|task-card/).or( + page.locator('.task-item') + ); + + const taskCount = await taskItems.count(); + + if (taskCount > 0) { + const firstTask = taskItems.first(); + const statusButton = firstTask.getByRole('button', { name: /status|change/i }); + + const hasStatusButton = await statusButton.isVisible().catch(() => false); + + if (hasStatusButton) { + await statusButton.click(); + + const statusOption = page.getByRole('option', { name: /completed|done/i }); + const hasOption = await statusOption.isVisible().catch(() => false); + + if (hasOption) { + await statusOption.click(); + + // Look for error message + + const errorMessage = page.getByText(/error|failed|unable/i); + const hasError = await errorMessage.isVisible().catch(() => false); + expect(hasError).toBe(true); + } + } + } + } + + // Restore routing + await page.unroute('**/api/sessions/*/tasks/*'); + + monitoring.assertClean({ ignoreAPIPatterns: ['/api/sessions'], allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should refresh tasks after session reload', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Get initial task count + const taskItems = page.getByTestId(/task-item|task-card/).or( + page.locator('.task-item') + ); + + const initialCount = await taskItems.count(); + + // Reload page + await page.reload({ waitUntil: 'networkidle' as const }); + + // Verify tasks are still displayed + const newTaskItems = page.getByTestId(/task-item|task-card/).or( + page.locator('.task-item') + ); + + const newCount = await newTaskItems.count(); + expect(newCount).toBe(initialCount); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should support task filtering', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for filter controls + const filterSelect = page.getByRole('combobox', { name: /filter|show/i }).or( + page.getByTestId('task-filter') + ); + + const hasFilter = await filterSelect.isVisible().catch(() => false); + + if (hasFilter) { + // Select a filter option + const filterOptions = await filterSelect.locator('option').count(); + + if (filterOptions > 1) { + await filterSelect.selectOption({ index: 1 }); + + // Verify filtered results + + const taskItems = page.getByTestId(/task-item|task-card/).or( + page.locator('.task-item') + ); + + const taskCount = await taskItems.count(); + expect(taskCount).toBeGreaterThanOrEqual(0); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should display empty state for tasks', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + // Look for session that might have no tasks + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for empty state + const emptyState = page.getByTestId('tasks-empty-state').or( + page.getByText(/no tasks|no task/i) + ); + + const hasEmptyState = await emptyState.isVisible().catch(() => false); + + const taskItems = page.getByTestId(/task-item|task-card/).or( + page.locator('.task-item') + ); + + const taskCount = await taskItems.count(); + + // If no tasks, should show empty state + if (taskCount === 0) { + expect(hasEmptyState).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should support task search', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for search input + const searchInput = page.getByRole('textbox', { name: /search|find/i }).or( + page.getByTestId('task-search') + ); + + const hasSearch = await searchInput.isVisible().catch(() => false); + + if (hasSearch) { + await searchInput.fill('test'); + + // Wait for search results + + // Search should either show results or no results message + const noResults = page.getByText(/no results|not found/i); + const hasNoResults = await noResults.isVisible().catch(() => false); + + const taskItems = page.getByTestId(/task-item|task-card/).or( + page.locator('.task-item') + ); + + const taskCount = await taskItems.count(); + + // Either no results message or filtered tasks + expect(hasNoResults || taskCount >= 0).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should display task progress indicator', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for progress indicator + const progressBar = page.getByTestId('tasks-progress').or( + page.locator('*').filter({ hasText: /\d+\/\d+|progress/i }) + ); + + const hasProgress = await progressBar.isVisible().catch(() => false); + + // Progress is optional but if present should be visible + if (hasProgress) { + expect(progressBar).toBeVisible(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should support batch task updates', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Navigate to sessions page + await page.goto('/sessions', { waitUntil: 'networkidle' as const }); + + const sessionItems = page.getByTestId(/session-item|session-card/).or( + page.locator('.session-item') + ); + + const itemCount = await sessionItems.count(); + + if (itemCount > 0) { + const firstSession = sessionItems.first(); + await firstSession.click(); + + await page.waitForURL(/\/sessions\//); + + // Look for select all checkbox + const selectAllCheckbox = page.getByRole('checkbox', { name: /select all/i }).or( + page.getByTestId('select-all-tasks') + ); + + const hasSelectAll = await selectAllCheckbox.isVisible().catch(() => false); + + if (hasSelectAll) { + await selectAllCheckbox.check(); + + // Look for batch action buttons + const batchCompleteButton = page.getByRole('button', { name: /complete all|mark complete/i }).or( + page.getByTestId('batch-complete-button') + ); + + const hasBatchButton = await batchCompleteButton.isVisible().catch(() => false); + + if (hasBatchButton) { + expect(batchCompleteButton).toBeVisible(); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +}); diff --git a/ccw/frontend/tests/e2e/workspace-switching.spec.ts b/ccw/frontend/tests/e2e/workspace-switching.spec.ts index a4a0869c..8527ce0b 100644 --- a/ccw/frontend/tests/e2e/workspace-switching.spec.ts +++ b/ccw/frontend/tests/e2e/workspace-switching.spec.ts @@ -148,7 +148,7 @@ test.describe('[Workspace Switching] - E2E Data Isolation Tests', () => { test('WS-05: should clear workspace data on logout', async ({ page }) => { // Set some workspace-specific data await page.evaluate(() => { - localStorage.setItem('workspace-1-data', JSON.stringify { user: 'alice' })); + localStorage.setItem('workspace-1-data', JSON.stringify({ user: 'alice' })); localStorage.setItem('ccw-current-workspace', 'workspace-1'); }); diff --git a/ccw/frontend/tests/e2e/workspace.spec.ts b/ccw/frontend/tests/e2e/workspace.spec.ts new file mode 100644 index 00000000..f66e1caf --- /dev/null +++ b/ccw/frontend/tests/e2e/workspace.spec.ts @@ -0,0 +1,358 @@ +// ======================================== +// E2E Tests: Workspace Management +// ======================================== +// End-to-end tests for workspace switching, recent paths, and data refresh + +import { test, expect } from '@playwright/test'; +import { setupEnhancedMonitoring, verifyI18nState } from './helpers/i18n-helpers'; + +test.describe('[Workspace] - Workspace Management Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' as const }); + }); + + test('L3.1 - should display recent paths', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Look for recent paths section + const recentPathsSection = page.getByTestId('recent-paths').or( + page.getByText(/recent|history/i) + ); + + const isVisible = await recentPathsSection.isVisible().catch(() => false); + + if (isVisible) { + // Verify recent path items exist + const pathItems = page.getByTestId(/recent-path|path-item/).or( + page.locator('.recent-path-item') + ); + + const itemCount = await pathItems.count(); + + if (itemCount === 0) { + // Empty state is acceptable + const emptyState = page.getByText(/no recent|empty/i); + const hasEmptyState = await emptyState.isVisible().catch(() => false); + expect(hasEmptyState).toBe(true); + } else { + expect(itemCount).toBeGreaterThan(0); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.2 - should remove recent path', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Look for recent paths + const pathItems = page.getByTestId(/recent-path|path-item/).or( + page.locator('.recent-path-item') + ); + + const itemCount = await pathItems.count(); + + if (itemCount > 0) { + const firstPath = pathItems.first(); + + // Look for remove button + const removeButton = firstPath.getByRole('button', { name: /remove|delete|x/i }).or( + firstPath.getByTestId('remove-path-button') + ); + + const hasRemoveButton = await removeButton.isVisible().catch(() => false); + + if (hasRemoveButton) { + const initialCount = await pathItems.count(); + + await removeButton.click(); + + // Verify path is removed + + const newCount = await pathItems.count(); + expect(newCount).toBe(initialCount - 1); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.3 - should switch workspace', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Look for workspace switcher + const workspaceSwitcher = page.getByTestId('workspace-switcher').or( + page.getByRole('combobox', { name: /workspace/i }) + ); + + const isVisible = await workspaceSwitcher.isVisible().catch(() => false); + + if (isVisible) { + const initialWorkspace = await workspaceSwitcher.textContent(); + + await workspaceSwitcher.click(); + + // Look for workspace options + const options = page.getByRole('option'); + const optionsCount = await options.count(); + + if (optionsCount > 0) { + const firstOption = options.first(); + const optionText = await firstOption.textContent(); + + if (optionText !== initialWorkspace) { + await firstOption.click(); + + // Verify workspace changed + await page.waitForLoadState('networkidle'); + + const newWorkspace = await workspaceSwitcher.textContent(); + expect(newWorkspace).not.toBe(initialWorkspace); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.4 - should refresh data after workspace switch', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Get initial stats + const initialStats = await page.evaluate(() => { + const stats = document.querySelector('[data-testid*="stat"], .stat'); + return stats?.textContent || ''; + }); + + // Look for workspace switcher + const workspaceSwitcher = page.getByTestId('workspace-switcher').or( + page.getByRole('combobox', { name: /workspace/i }) + ); + + const isVisible = await workspaceSwitcher.isVisible().catch(() => false); + + if (isVisible) { + await workspaceSwitcher.click(); + + const options = page.getByRole('option'); + const optionsCount = await options.count(); + + if (optionsCount > 0) { + const firstOption = options.first(); + await firstOption.click(); + + // Wait for data refresh + await page.waitForLoadState('networkidle'); + + // Verify data is refreshed (stats container is still visible) + const statsContainer = page.getByTestId('dashboard-stats').or( + page.locator('.stats') + ); + + const isStillVisible = await statsContainer.isVisible().catch(() => false); + expect(isStillVisible).toBe(true); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.5 - should maintain i18n preference after workspace switch', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Set language to Chinese + const languageSwitcher = page.getByRole('combobox', { name: /select language|language/i }).first(); + const hasLanguageSwitcher = await languageSwitcher.isVisible().catch(() => false); + + if (hasLanguageSwitcher) { + await languageSwitcher.click(); + const chineseOption = page.getByText('中文'); + await chineseOption.click(); + + const initialLang = await page.evaluate(() => document.documentElement.lang); + + // Switch workspace + const workspaceSwitcher = page.getByTestId('workspace-switcher').or( + page.getByRole('combobox', { name: /workspace/i }) + ); + + const hasWorkspaceSwitcher = await workspaceSwitcher.isVisible().catch(() => false); + + if (hasWorkspaceSwitcher) { + await workspaceSwitcher.click(); + + const options = page.getByRole('option'); + const optionsCount = await options.count(); + + if (optionsCount > 0) { + await options.first().click(); + await page.waitForLoadState('networkidle'); + + // Verify language is maintained + const currentLang = await page.evaluate(() => document.documentElement.lang); + expect(currentLang).toBe(initialLang); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.6 - should handle workspace switch with unsaved changes', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Simulate unsaved changes + await page.evaluate(() => { + sessionStorage.setItem('unsaved-changes', JSON.stringify({ + form: { field1: 'value1' }, + timestamp: Date.now(), + })); + }); + + // Try to switch workspace + const workspaceSwitcher = page.getByTestId('workspace-switcher').or( + page.getByRole('combobox', { name: /workspace/i }) + ); + + const isVisible = await workspaceSwitcher.isVisible().catch(() => false); + + if (isVisible) { + await workspaceSwitcher.click(); + + // Check for unsaved changes warning + const warningDialog = page.getByRole('dialog').filter({ hasText: /unsaved|changes|save/i }); + + const hasWarning = await warningDialog.isVisible().catch(() => false); + + if (hasWarning) { + expect(warningDialog).toBeVisible(); + + // Test cancel button (stay on current workspace) + const cancelButton = page.getByRole('button', { name: /cancel|stay/i }); + const hasCancel = await cancelButton.isVisible().catch(() => false); + + if (hasCancel) { + await cancelButton.click(); + } + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.7 - should persist workspace selection on reload', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Look for workspace switcher + const workspaceSwitcher = page.getByTestId('workspace-switcher').or( + page.getByRole('combobox', { name: /workspace/i }) + ); + + const isVisible = await workspaceSwitcher.isVisible().catch(() => false); + + if (isVisible) { + const initialWorkspace = await workspaceSwitcher.textContent(); + + // Reload page + await page.reload({ waitUntil: 'networkidle' as const }); + + // Verify workspace is restored + const restoredWorkspace = await workspaceSwitcher.textContent(); + expect(restoredWorkspace).toBe(initialWorkspace); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.8 - should handle invalid workspace gracefully', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Try to navigate to invalid workspace + await page.goto('/?workspace=invalid-workspace-that-does-not-exist', { waitUntil: 'networkidle' as const }); + + // Page should still be functional + const isPageFunctional = await page.evaluate(() => { + return document.body !== null && document.visibilityState === 'visible'; + }); + + expect(isPageFunctional).toBe(true); + + // Check for error indicator or fallback + const errorIndicator = page.getByText(/error|not found|invalid/i); + const hasError = await errorIndicator.isVisible().catch(() => false); + + // Error indicator is acceptable, but page should still load + const pageContent = await page.content(); + const hasContent = pageContent.length > 1000; + + expect(hasError || hasContent).toBe(true); + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.9 - should update UI elements on workspace switch', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Get initial header state + const initialHeader = await page.locator('header').textContent(); + + // Look for workspace switcher + const workspaceSwitcher = page.getByTestId('workspace-switcher').or( + page.getByRole('combobox', { name: /workspace/i }) + ); + + const isVisible = await workspaceSwitcher.isVisible().catch(() => false); + + if (isVisible) { + await workspaceSwitcher.click(); + + const options = page.getByRole('option'); + const optionsCount = await options.count(); + + if (optionsCount > 0) { + await options.first().click(); + + // Wait for UI update + await page.waitForLoadState('networkidle'); + + // Check that header is updated (if workspace name is displayed) + const newHeader = await page.locator('header').textContent(); + expect(newHeader).toBeDefined(); + } + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); + + test('L3.10 - should display current workspace in header', async ({ page }) => { + const monitoring = setupEnhancedMonitoring(page); + + // Get header element + const header = page.locator('header'); + + // Check for workspace indicator + const workspaceIndicator = header.locator('[data-testid="current-workspace"]').or( + header.locator('*').filter({ hasText: /workspace/i }) + ); + + const isVisible = await workspaceIndicator.isVisible().catch(() => false); + + if (isVisible) { + const text = await workspaceIndicator.textContent(); + expect(text).toBeTruthy(); + expect(text?.length).toBeGreaterThan(0); + } + + monitoring.assertClean({ allowWarnings: true }); + monitoring.stop(); + }); +});