/**
* Choose Next Flow E2E Tests
*
* Tests the complete flow for choose-next requests including:
* - Decision workflow display with option cards
* - Option selection and highlighting
* - Abort and "request new ideas" actions
* - Markdown rendering in option descriptions
* - Responsive grid layout
* - Selection confirmation and submission
*/
import { test, expect } from '@playwright/test';
import { RequestHandlerPage } from '../support/page-objects/request-handler.page';
import { ChooseNextResponsePage } from '../support/page-objects/response-components.page';
import { ServerManager, TEST_PORTS, getAvailablePort } from '../support/test-utils/server-manager';
import { createChooseNextRequest } from '../support/test-utils/test-data';
import {
mockBrowserNotifications,
waitForSSEConnection,
waitForNetworkRequest,
logTestStep
} from '../support/test-utils/helpers';
test.describe('Choose Next Flow', () => {
let serverManager: ServerManager;
let requestHandler: RequestHandlerPage;
let chooseNext: ChooseNextResponsePage;
test.beforeEach(async ({ page }) => {
await mockBrowserNotifications(page);
const availablePort = await getAvailablePort(TEST_PORTS.CHOOSE_NEXT);
serverManager = new ServerManager({
port: availablePort,
debug: true
});
logTestStep('Starting MCP server', { port: availablePort });
await serverManager.start();
requestHandler = new RequestHandlerPage(page);
chooseNext = new ChooseNextResponsePage(page);
await requestHandler.goto(availablePort);
await requestHandler.waitForLoad();
await waitForSSEConnection(page);
await requestHandler.waitForConnection();
});
test.afterEach(async () => {
if (serverManager) {
await serverManager.stop();
}
});
test('should display choose next request correctly', async ({ page }) => {
logTestStep('Testing choose next display');
const testRequest = createChooseNextRequest();
await page.request.post(`${serverManager.getServerUrl()}/mcp/simulate-request`, {
data: testRequest
});
// Wait for request to appear in UI
await expect(requestHandler.currentRequest).toBeVisible({ timeout: 20000 });
// Verify request type is detected correctly
const requestType = await requestHandler.getCurrentRequestType();
expect(requestType).toBe('choose-next');
// Verify choose next component is visible
await expect(chooseNext.component).toBeVisible();
// Check option count
const optionCount = await chooseNext.getOptionCount();
expect(optionCount).toBeGreaterThan(0);
// Verify action buttons are present
await expect(chooseNext.abortButton).toBeVisible();
await expect(chooseNext.newIdeasButton).toBeVisible();
logTestStep('Choose next displayed correctly', { optionCount });
});
test('should handle option selection', async ({ page }) => {
logTestStep('Testing option selection');
const testRequest = createChooseNextRequest();
await page.request.post(`${serverManager.getServerUrl()}/mcp/simulate-request`, {
data: testRequest
});
await expect(chooseNext.component).toBeVisible();
// Select the first option
await chooseNext.selectOption(0);
// Verify selection is shown
expect(await chooseNext.hasSelectedOption()).toBe(true);
const selectedText = await chooseNext.getSelectedOptionText();
expect(selectedText.length).toBeGreaterThan(0);
logTestStep('Option selection verified', { selectedText: selectedText.substring(0, 50) });
});
test('should handle option card display', async ({ page }) => {
logTestStep('Testing option card display');
const testRequest = createChooseNextRequest();
await page.request.post(`${serverManager.getServerUrl()}/mcp/simulate-request`, {
data: testRequest
});
await expect(chooseNext.component).toBeVisible();
// Check that options are displayed as cards
const firstOption = chooseNext.options.nth(0);
await expect(firstOption).toBeVisible();
// Verify card contains title and description
const cardContent = await firstOption.textContent();
expect(cardContent).toContain('Authentication Service'); // From test data
// Check for icon/emoji presence
const hasIcon = await firstOption.locator('.icon, [class*="emoji"]').count() > 0;
logTestStep('Option card display verified', { hasIcon });
});
test('should handle abort action', async ({ page }) => {
logTestStep('Testing abort action');
const testRequest = createChooseNextRequest();
await page.request.post(`${serverManager.getServerUrl()}/mcp/simulate-request`, {
data: testRequest
});
await expect(chooseNext.component).toBeVisible();
// Click abort button
const responsePromise = waitForNetworkRequest(page, '/mcp/response');
await chooseNext.clickAbort();
await responsePromise;
// Request should be cleared after abort
await expect(requestHandler.currentRequest).toBeHidden();
await expect(requestHandler.waitingMessage).toBeVisible();
logTestStep('Abort action verified');
});
test('should handle request new ideas action', async ({ page }) => {
logTestStep('Testing request new ideas action');
const testRequest = createChooseNextRequest();
await page.request.post(`${serverManager.getServerUrl()}/mcp/simulate-request`, {
data: testRequest
});
await expect(chooseNext.component).toBeVisible();
// Click request new ideas button
const responsePromise = waitForNetworkRequest(page, '/mcp/response');
await chooseNext.clickRequestNewIdeas();
await responsePromise;
// Request should be cleared after action
await expect(requestHandler.currentRequest).toBeHidden();
await expect(requestHandler.waitingMessage).toBeVisible();
logTestStep('Request new ideas action verified');
});
test('should handle option selection and submission', async ({ page }) => {
logTestStep('Testing option selection and submission');
const testRequest = createChooseNextRequest();
await page.request.post(`${serverManager.getServerUrl()}/mcp/simulate-request`, {
data: testRequest
});
await expect(chooseNext.component).toBeVisible();
// Set up network request listener before clicking (component auto-submits after 500ms)
const responsePromise = page.waitForRequest(request =>
request.method() === 'POST' && request.url().includes('/mcp/response'),
{ timeout: 10000 }
);
// Select an option (this will trigger auto-submit after 500ms)
await chooseNext.selectOption(1); // Second option
// Verify selection
expect(await chooseNext.hasSelectedOption()).toBe(true);
// Wait for the auto-submit with extended timeout
await responsePromise;
logTestStep('Option selection and submission verified');
});
test('should handle markdown in option descriptions', async ({ page }) => {
logTestStep('Testing markdown in option descriptions');
// Create request with markdown in descriptions
const markdownRequest = createChooseNextRequest();
// Modify the context to include markdown
if (markdownRequest.context &&
typeof markdownRequest.context === 'object' &&
'challenge' in markdownRequest.context) {
const challenge = markdownRequest.context.challenge as any;
challenge.options[0].description = '**Important**: Implement *OAuth 2.0* and `JWT` token management first';
challenge.options[1].description = 'Create user tables with:\n\n- Primary keys\n- Foreign key relationships\n- Indexes for performance';
}
await page.request.post(`${serverManager.getServerUrl()}/mcp/simulate-request`, {
data: markdownRequest
});
await expect(chooseNext.component).toBeVisible();
// Check if markdown elements are rendered in options
const firstOption = chooseNext.options.nth(0);
// Look for markdown-rendered elements
const bold = firstOption.locator('strong, b');
const italic = firstOption.locator('em, i');
const code = firstOption.locator('code');
// At least some markdown elements should be present
const hasMarkdownElements = await Promise.all([
bold.count(),
italic.count(),
code.count()
]).then(counts => counts.some(count => count > 0));
if (hasMarkdownElements) {
logTestStep('Markdown rendering verified in options');
} else {
logTestStep('Markdown rendering not detected (plain text rendering)');
}
});
test('should handle responsive grid layout', async ({ page }) => {
logTestStep('Testing responsive grid layout');
const testRequest = createChooseNextRequest();
await page.request.post(`${serverManager.getServerUrl()}/mcp/simulate-request`, {
data: testRequest
});
await expect(chooseNext.component).toBeVisible();
// Test mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await expect(chooseNext.component).toBeVisible();
await expect(chooseNext.options.first()).toBeVisible();
// Options should stack vertically on mobile
const firstOptionBox = await chooseNext.options.nth(0).boundingBox();
const secondOptionBox = await chooseNext.options.nth(1).boundingBox();
if (firstOptionBox && secondOptionBox) {
// On mobile, second option should be below first (higher Y coordinate)
expect(secondOptionBox.y).toBeGreaterThan(firstOptionBox.y);
}
// Test desktop viewport
await page.setViewportSize({ width: 1200, height: 800 });
await expect(chooseNext.component).toBeVisible();
await expect(chooseNext.options.first()).toBeVisible();
logTestStep('Responsive grid layout verified');
});
test('should handle option hover and focus states', async ({ page }) => {
logTestStep('Testing option hover and focus states');
const testRequest = createChooseNextRequest();
await page.request.post(`${serverManager.getServerUrl()}/mcp/simulate-request`, {
data: testRequest
});
await expect(chooseNext.component).toBeVisible();
const firstOption = chooseNext.options.nth(0);
// Test hover state
await firstOption.hover();
// Check for hover styling (this depends on CSS implementation)
const hoverClass = await firstOption.getAttribute('class');
const hasHoverEffect = hoverClass?.includes('hover') ||
await firstOption.evaluate(el => {
const style = window.getComputedStyle(el);
return style.transform !== 'none' ||
style.boxShadow !== 'none' ||
style.backgroundColor !== 'rgba(0, 0, 0, 0)';
});
logTestStep('Option hover state verified', { hasHoverEffect });
// Test focus state
await firstOption.focus();
const focusedElement = await page.locator(':focus').count();
expect(focusedElement).toBeGreaterThan(0);
logTestStep('Option focus state verified');
});
test('should handle keyboard navigation', async ({ page }) => {
logTestStep('Testing keyboard navigation');
const testRequest = createChooseNextRequest();
await page.request.post(`${serverManager.getServerUrl()}/mcp/simulate-request`, {
data: testRequest
});
await expect(chooseNext.component).toBeVisible();
// Focus first option and use keyboard navigation
await chooseNext.options.nth(0).focus();
// Test Tab navigation
await page.keyboard.press('Tab');
// Test Enter to select
await page.keyboard.press('Enter');
// Check if selection occurred
const hasSelection = await chooseNext.hasSelectedOption();
logTestStep('Keyboard navigation verified', { hasSelection });
});
test('should handle large number of options', async ({ page }) => {
logTestStep('Testing large number of options');
// Create request with many options
const largeRequest = createChooseNextRequest();
if (largeRequest.context &&
typeof largeRequest.context === 'object' &&
'challenge' in largeRequest.context) {
const challenge = largeRequest.context.challenge as any;
// Add more options
for (let i = 5; i <= 12; i++) {
challenge.options.push({
id: `opt${i}`,
title: `Option ${i}`,
description: `Description for option ${i}`,
icon: '🔧'
});
}
}
await page.request.post(`${serverManager.getServerUrl()}/mcp/simulate-request`, {
data: largeRequest
});
await expect(chooseNext.component).toBeVisible();
// Verify all options are displayed
const optionCount = await chooseNext.getOptionCount();
expect(optionCount).toBeGreaterThanOrEqual(8);
// Should handle scrolling if needed
const lastOption = chooseNext.options.nth(optionCount - 1);
await lastOption.scrollIntoViewIfNeeded();
await expect(lastOption).toBeVisible();
logTestStep('Large number of options handled correctly', { optionCount });
});
test('should handle option selection visual feedback', async ({ page }) => {
logTestStep('Testing option selection visual feedback');
const testRequest = createChooseNextRequest();
await page.request.post(`${serverManager.getServerUrl()}/mcp/simulate-request`, {
data: testRequest
});
await expect(chooseNext.component).toBeVisible();
// Select first option
await chooseNext.selectOption(0);
// Wait a moment for the selection state to be applied, but before submission
await page.waitForTimeout(100);
// Check for visual selection feedback
const selectedOption = chooseNext.options.nth(0);
const selectionClass = await selectedOption.getAttribute('class');
const hasSelectionStyling = selectionClass?.includes('selected') ||
selectionClass?.includes('active') ||
selectionClass?.includes('chosen');
expect(hasSelectionStyling).toBe(true);
// Visual feedback confirmed - selection state is working
logTestStep('Option selection visual feedback verified', {
hasSelectionStyling
});
});
test('should handle error scenarios gracefully', async ({ page }) => {
logTestStep('Testing error scenario handling');
const testRequest = createChooseNextRequest();
await page.request.post(`${serverManager.getServerUrl()}/mcp/simulate-request`, {
data: testRequest
});
await expect(chooseNext.component).toBeVisible();
// Select an option
await chooseNext.selectOption(0);
// Simulate server error during submission
await page.route('**/mcp/response', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Server error' })
});
});
// Try to submit (depending on implementation)
try {
await chooseNext.selectOption(0); // Double-click might trigger submission
// If error handling is implemented, UI should remain functional
await expect(chooseNext.component).toBeVisible();
} catch (error) {
logTestStep('Error scenario handled', { error: (error as Error).message });
}
logTestStep('Error scenarios tested');
});
});