Files
Claude-Code-Workflow/ccw/frontend/tests/e2e/a2ui-notifications.spec.ts
catlog22 345437415f Add end-to-end tests for workspace switching and backend tests for ask_question tool
- Implemented E2E tests for workspace switching functionality, covering scenarios such as switching workspaces, data isolation, language preference maintenance, and UI updates.
- Added tests to ensure workspace data is cleared on logout and handles unsaved changes during workspace switches.
- Created comprehensive backend tests for the ask_question tool, validating question creation, execution, answer handling, cancellation, and timeout scenarios.
- Included edge case tests to ensure robustness against duplicate questions and invalid answers.
2026-01-31 16:02:20 +08:00

617 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ========================================
// E2E Tests: A2UI Notification Rendering
// ========================================
// End-to-end tests for A2UI surface notification rendering
import { test, expect } from '@playwright/test';
test.describe('[A2UI Notifications] - E2E Rendering Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
});
test('A2UI-01: should render A2UI notification in notification panel', async ({ page }) => {
// Send A2UI surface via WebSocket message
await page.evaluate(() => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-surface',
surfaceId: 'test-notification-1',
title: 'Test Notification',
surface: {
surfaceId: 'test-notification-1',
components: [
{
id: 'title',
component: {
Text: {
text: { literalString: 'Notification Title' },
usageHint: 'h3',
},
},
},
{
id: 'message',
component: {
Text: {
text: { literalString: 'This is a test notification message' },
usageHint: 'p',
},
},
},
{
id: 'button',
component: {
Button: {
onClick: { actionId: 'action-1', parameters: {} },
content: { Text: { text: { literalString: 'Action' } } },
variant: 'primary',
},
},
},
],
initialState: { count: 0 },
},
},
});
window.dispatchEvent(event);
});
// Open notification panel
const notificationButton = page.locator('[data-testid="notification-panel-button"]').or(
page.getByRole('button', { name: /notifications/i })
).or(
page.locator('button').filter({ hasText: /notifications/i })
);
// Try to find and click notification button
const isVisible = await notificationButton.isVisible().catch(() => false);
if (isVisible) {
await notificationButton.click();
}
// Check if notification is visible
await expect(page.getByText('Notification Title')).toBeVisible();
await expect(page.getByText('This is a test notification message')).toBeVisible();
await expect(page.getByRole('button', { name: 'Action' })).toBeVisible();
});
test('A2UI-02: should render CLIOutput component with syntax highlighting', async ({ page }) => {
await page.evaluate(() => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-surface',
surfaceId: 'test-cli-output',
title: 'CLI Output',
surface: {
surfaceId: 'test-cli-output',
components: [
{
id: 'cli',
component: {
CLIOutput: {
output: {
literalString: '$ npm install\nInstalling dependencies...\nDone!\n'
},
language: 'bash',
streaming: false,
},
},
},
],
initialState: {},
},
},
});
window.dispatchEvent(event);
});
// Check for CLI output styling
await expect(page.locator('.a2ui-cli-output')).toBeVisible();
await expect(page.getByText(/\$ npm install/)).toBeVisible();
await expect(page.getByText(/Done!/)).toBeVisible();
});
test('A2UI-03: should render CLIOutput with streaming indicator', async ({ page }) => {
await page.evaluate(() => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-surface',
surfaceId: 'test-streaming',
title: 'Streaming Output',
surface: {
surfaceId: 'test-streaming',
components: [
{
id: 'cli',
component: {
CLIOutput: {
output: {
literalString: 'Processing...'
},
language: 'bash',
streaming: true,
},
},
},
],
initialState: {},
},
},
});
window.dispatchEvent(event);
});
// Check for streaming indicator
await expect(page.getByText(/Streaming/i)).toBeVisible();
});
test('A2UI-04: should render DateTimeInput component', async ({ page }) => {
await page.evaluate(() => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-surface',
surfaceId: 'test-datetime',
title: 'Date Time Input',
surface: {
surfaceId: 'test-datetime',
components: [
{
id: 'title',
component: {
Text: {
text: { literalString: 'Select appointment date' },
usageHint: 'h3',
},
},
},
{
id: 'datetime',
component: {
DateTimeInput: {
onChange: { actionId: 'datetime-change', parameters: {} },
placeholder: 'Select date and time',
includeTime: true,
},
},
},
],
initialState: {},
},
},
});
window.dispatchEvent(event);
});
// Check for datetime input
await expect(page.getByText('Select appointment date')).toBeVisible();
const datetimeInput = page.locator('input[type="datetime-local"]');
await expect(datetimeInput).toBeVisible();
});
test('A2UI-05: should render Card component with nested content', async ({ page }) => {
await page.evaluate(() => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-surface',
surfaceId: 'test-card',
title: 'Card Component',
surface: {
surfaceId: 'test-card',
components: [
{
id: 'card',
component: {
Card: {
title: { literalString: 'Card Title' },
description: { literalString: 'Card description text' },
content: [
{
id: 'text1',
component: { Text: { text: { literalString: 'First item' } } },
},
{
id: 'text2',
component: { Text: { text: { literalString: 'Second item' } } },
},
],
},
},
},
],
initialState: {},
},
},
});
window.dispatchEvent(event);
});
// Check for card elements
await expect(page.getByText('Card Title')).toBeVisible();
await expect(page.getByText('Card description text')).toBeVisible();
await expect(page.getByText('First item')).toBeVisible();
await expect(page.getByText('Second item')).toBeVisible();
});
test('A2UI-06: should render Progress component', async ({ page }) => {
await page.evaluate(() => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-surface',
surfaceId: 'test-progress',
title: 'Progress',
surface: {
surfaceId: 'test-progress',
components: [
{
id: 'progress',
component: {
Progress: {
value: { literalNumber: 75 },
max: 100,
},
},
},
],
initialState: {},
},
},
});
window.dispatchEvent(event);
});
// Check for progress element
const progress = page.locator('progress').or(page.locator('[role="progressbar"]'));
await expect(progress).toBeVisible();
});
test('A2UI-07: should render Dropdown component', async ({ page }) => {
await page.evaluate(() => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-surface',
surfaceId: 'test-dropdown',
title: 'Dropdown',
surface: {
surfaceId: 'test-dropdown',
components: [
{
id: 'dropdown',
component: {
Dropdown: {
options: [
{ label: { literalString: 'Option 1' }, value: 'opt1' },
{ label: { literalString: 'Option 2' }, value: 'opt2' },
{ label: { literalString: 'Option 3' }, value: 'opt3' },
],
onChange: { actionId: 'select', parameters: {} },
placeholder: 'Choose an option',
},
},
},
],
initialState: {},
},
},
});
window.dispatchEvent(event);
});
// Check for dropdown
const dropdown = page.getByRole('combobox');
await expect(dropdown).toBeVisible();
// Open dropdown
await dropdown.click();
// Check options
await expect(page.getByRole('option', { name: 'Option 1' })).toBeVisible();
await expect(page.getByRole('option', { name: 'Option 2' })).toBeVisible();
await expect(page.getByRole('option', { name: 'Option 3' })).toBeVisible();
});
test('A2UI-08: should render Checkbox component', async ({ page }) => {
await page.evaluate(() => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-surface',
surfaceId: 'test-checkbox',
title: 'Checkbox',
surface: {
surfaceId: 'test-checkbox',
components: [
{
id: 'checkbox',
component: {
Checkbox: {
checked: { literalBoolean: false },
onChange: { actionId: 'check', parameters: {} },
label: { literalString: 'Accept terms and conditions' },
},
},
},
],
initialState: {},
},
},
});
window.dispatchEvent(event);
});
// Check for checkbox
await expect(page.getByText('Accept terms and conditions')).toBeVisible();
const checkbox = page.getByRole('checkbox');
await expect(checkbox).toBeVisible();
});
test('A2UI-09: should handle A2UI action events', async ({ page }) => {
let actionReceived = false;
// Set up listener for A2UI action
await page.evaluate(() => {
(window as any).testActionReceived = false;
window.addEventListener('a2ui-action', (e: Event) => {
const customEvent = e as CustomEvent;
if (customEvent.detail?.actionId === 'test-action') {
(window as any).testActionReceived = true;
}
});
});
// Send A2UI surface with button
await page.evaluate(() => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-surface',
surfaceId: 'test-action',
title: 'Action Test',
surface: {
surfaceId: 'test-action',
components: [
{
id: 'btn',
component: {
Button: {
onClick: { actionId: 'test-action', parameters: { key: 'value' } },
content: { Text: { text: { literalString: 'Click Me' } } },
variant: 'primary',
},
},
},
],
initialState: {},
},
},
});
window.dispatchEvent(event);
});
// Click button
await page.getByRole('button', { name: 'Click Me' }).click();
// Wait and check if action was received
await page.waitForTimeout(500);
actionReceived = await page.evaluate(() => (window as any).testActionReceived || false);
expect(actionReceived).toBe(true);
});
test('A2UI-10: should update A2UI state dynamically', async ({ page }) => {
// Send initial surface
await page.evaluate(() => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-surface',
surfaceId: 'test-state-update',
title: 'State Test',
surface: {
surfaceId: 'test-state-update',
components: [
{
id: 'counter',
component: {
Text: {
text: { literalString: 'Count: 0' },
},
},
},
{
id: 'btn',
component: {
Button: {
onClick: { actionId: 'increment', parameters: {} },
content: { Text: { text: { literalString: 'Increment' } } },
},
},
},
],
initialState: { count: 0 },
},
},
});
window.dispatchEvent(event);
});
// Check initial state
await expect(page.getByText('Count: 0')).toBeVisible();
// Simulate state update via WebSocket
await page.evaluate(() => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-state-update',
surfaceId: 'test-state-update',
updates: { count: 5 },
},
});
window.dispatchEvent(event);
});
// Wait for update to be reflected
await page.waitForTimeout(500);
// Note: The actual update handling depends on implementation
});
test('A2UI-11: should render multiple A2UI notifications', async ({ page }) => {
// Send multiple surfaces
for (let i = 1; i <= 3; i++) {
await page.evaluate((index) => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-surface',
surfaceId: `test-multi-${index}`,
title: `Notification ${index}`,
surface: {
surfaceId: `test-multi-${index}`,
components: [
{
id: 'title',
component: {
Text: {
text: { literalString: `Message ${index}` },
},
},
},
],
initialState: {},
},
},
});
window.dispatchEvent(event);
}, i);
await page.waitForTimeout(100);
}
// Check that all notifications are rendered
await expect(page.getByText('Message 1')).toBeVisible();
await expect(page.getByText('Message 2')).toBeVisible();
await expect(page.getByText('Message 3')).toBeVisible();
});
test('A2UI-12: should handle dismissible A2UI notifications', async ({ page }) => {
await page.evaluate(() => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-surface',
surfaceId: 'test-dismissible',
title: 'Dismissible',
surface: {
surfaceId: 'test-dismissible',
components: [
{
id: 'content',
component: {
Text: {
text: { literalString: 'This can be dismissed' },
},
},
},
],
initialState: {},
},
},
});
window.dispatchEvent(event);
});
// Check that notification is visible
await expect(page.getByText('This can be dismissed')).toBeVisible();
// Find and click dismiss button
const dismissButton = page.locator('[aria-label="Close"]').or(
page.locator('button').filter({ hasText: '×' })
).or(
page.locator('button').filter({ hasText: /close|dismiss/i })
);
const isVisible = await dismissButton.isVisible().catch(() => false);
if (isVisible) {
await dismissButton.click();
// Notification should be dismissed
await page.waitForTimeout(500);
}
});
test('A2UI-13: should render TextArea component', async ({ page }) => {
await page.evaluate(() => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-surface',
surfaceId: 'test-textarea',
title: 'Text Area',
surface: {
surfaceId: 'test-textarea',
components: [
{
id: 'textarea',
component: {
TextArea: {
onChange: { actionId: 'text-change', parameters: {} },
placeholder: 'Enter multi-line text',
rows: 4,
},
},
},
],
initialState: {},
},
},
});
window.dispatchEvent(event);
});
// Check for textarea
const textarea = page.locator('textarea');
await expect(textarea).toBeVisible();
await expect(textarea).toHaveAttribute('placeholder', 'Enter multi-line text');
});
test('A2UI-14: should render TextField with different types', async ({ page }) => {
await page.evaluate(() => {
const event = new CustomEvent('ws-message', {
detail: {
type: 'a2ui-surface',
surfaceId: 'test-textfield',
title: 'Text Field',
surface: {
surfaceId: 'test-textfield',
components: [
{
id: 'email',
component: {
TextField: {
onChange: { actionId: 'email', parameters: {} },
placeholder: 'Email address',
type: 'email',
},
},
},
{
id: 'password',
component: {
TextField: {
onChange: { actionId: 'password', parameters: {} },
placeholder: 'Password',
type: 'password',
},
},
},
],
initialState: {},
},
},
});
window.dispatchEvent(event);
});
// Check for email field
await expect(page.getByPlaceholderText('Email address')).toBeVisible();
await expect(page.getByPlaceholderText('Email address')).toHaveAttribute('type', 'email');
// Check for password field
await expect(page.getByPlaceholderText('Password')).toBeVisible();
await expect(page.getByPlaceholderText('Password')).toHaveAttribute('type', 'password');
});
});