diff --git a/frontend/e2e/tests/settings-model.spec.ts b/frontend/e2e/tests/settings-model.spec.ts index 87455024f..c29e44f5f 100644 --- a/frontend/e2e/tests/settings-model.spec.ts +++ b/frontend/e2e/tests/settings-model.spec.ts @@ -4,14 +4,18 @@ test.describe('Settings - Model Management', () => { test.beforeEach(async ({ page }) => { await page.goto('/settings?tab=models') await page.waitForLoadState('domcontentloaded') + // Wait for the model management title to be visible + await expect( + page.locator('h2:has-text("Model Management"), h2:has-text("模型管理")').first() + ).toBeVisible({ timeout: 20000 }) }) test('should access model management page', async ({ page }) => { - // Verify we're on settings page (models is the default tab) + // Verify we're on settings page (models tab) await expect(page).toHaveURL(/\/settings/) - // Wait for model management title to load - await expect(page.locator('h2:has-text("Model")')).toBeVisible({ timeout: 20000 }) + // The title is already verified in beforeEach, just verify the content area + await page.waitForSelector('[class*="overflow-y-auto"]', { state: 'visible', timeout: 10000 }) }) test('should display model list or empty state', async ({ page }) => { @@ -41,9 +45,15 @@ test.describe('Settings - Model Management', () => { await createButton.first().click() - // Model edit is a full page form - check for the model ID input - const modelIdInput = page.locator('input#modelIdName, input[placeholder*="model"]') - await expect(modelIdInput.first()).toBeVisible({ timeout: 5000 }) + // Wait for dialog to open (animation + rendering time) + await page.waitForTimeout(1500) + + // Wait for dialog content to be visible + await page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 15000 }) + + // Model edit is a dialog form - check for the model ID input + const modelIdInput = page.locator('[data-testid="model-id-name-input"]') + await expect(modelIdInput).toBeVisible({ timeout: 15000 }) }) test('should create new model', async ({ page, testPrefix }) => { @@ -56,33 +66,47 @@ test.describe('Settings - Model Management', () => { await expect(createButton.first()).toBeVisible({ timeout: 20000 }) await createButton.first().click() - // Model edit is a full page form, wait for model ID input - const nameInput = page.locator('input#modelIdName, input[placeholder*="model"]').first() - await expect(nameInput).toBeVisible({ timeout: 5000 }) + // Wait for dialog to open + await page.waitForTimeout(1500) + await page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 15000 }) + + // Model edit is a dialog form, wait for model ID input + const nameInput = page.locator('[data-testid="model-id-name-input"]') + await expect(nameInput).toBeVisible({ timeout: 15000 }) await nameInput.fill(modelName) // Fill API key (required field) const apiKeyInput = page.locator('input#api_key, input[type="password"]').first() - if (await apiKeyInput.isVisible({ timeout: 2000 }).catch(() => false)) { + if (await apiKeyInput.isVisible({ timeout: 3000 }).catch(() => false)) { await apiKeyInput.fill('test-api-key-for-e2e') } + // Fill model ID - click on the Model ID dropdown and select a model + // The dropdown button has text like "Select Model ID" or similar + const modelIdDropdown = page + .locator('button:has-text("Select Model"), button:has-text("选择模型")') + .first() + if (await modelIdDropdown.isVisible({ timeout: 3000 }).catch(() => false)) { + await modelIdDropdown.click() + await page.waitForTimeout(500) + // Select the first available model option (skip "Custom..." which is usually last) + const modelOption = page.locator('[role="option"]').first() + if (await modelOption.isVisible({ timeout: 2000 }).catch(() => false)) { + await modelOption.click() + } + } + // Submit form const submitButton = page.locator('button:has-text("Save"), button:has-text("保存")').first() - if (await submitButton.isVisible({ timeout: 3000 }).catch(() => false)) { + if (await submitButton.isVisible({ timeout: 5000 }).catch(() => false)) { await submitButton.click() - // Wait for navigation back to list or validation error - await page.waitForURL(/\/settings/, { timeout: 10000 }).catch(() => { - // May stay on form with validation errors - }) + // Wait for dialog to close or validation error + await page.waitForTimeout(2000) } }) test('should show test connection button for user models', async ({ page }) => { - // Wait for page to load - await expect(page.locator('h2:has-text("Model")')).toBeVisible({ timeout: 20000 }) - // Test connection button only appears for user models (not public) // Check if there are any user model cards with test button const testButton = page.locator('button[title*="Test"], button:has-text("Test")').first() @@ -97,9 +121,6 @@ test.describe('Settings - Model Management', () => { }) test('should show delete button for user models', async ({ page }) => { - // Wait for page to load - await expect(page.locator('h2:has-text("Model")')).toBeVisible({ timeout: 20000 }) - // Delete button only appears for user models (not public) const deleteButton = page.locator('button[title*="Delete"], button:has-text("Delete")').first() diff --git a/frontend/e2e/tests/tasks/chat-image-browser-e2e.spec.ts b/frontend/e2e/tests/tasks/chat-image-browser-e2e.spec.ts index 506554c37..77c15d1eb 100644 --- a/frontend/e2e/tests/tasks/chat-image-browser-e2e.spec.ts +++ b/frontend/e2e/tests/tasks/chat-image-browser-e2e.spec.ts @@ -225,6 +225,11 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => { */ async function dismissOnboardingTour(page: Page): Promise { try { + // First, try to forcefully remove all driver.js elements from DOM + await page.evaluate(() => { + document.querySelectorAll('.driver-overlay, .driver-popover, .driver-popover-tip, [class*="driver-"]').forEach(el => el.remove()) + }) + // Check for driver.js overlay (onboarding tour) const driverOverlay = page.locator('.driver-overlay, .driver-popover') if (await driverOverlay.isVisible({ timeout: 1000 }).catch(() => false)) { @@ -249,11 +254,15 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => { console.log('Pressed Escape to dismiss overlay') } - // Verify overlay is gone + // Verify overlay is gone, if not forcefully remove it if (await driverOverlay.isVisible({ timeout: 500 }).catch(() => false)) { - console.warn('Overlay still visible, trying to click outside') - // Click outside the overlay to dismiss it - await page.mouse.click(10, 10) + console.warn('Overlay still visible, forcefully removing from DOM') + await page.evaluate(() => { + document.querySelectorAll('.driver-overlay, .driver-popover, .driver-popover-tip, [class*="driver-"]').forEach(el => { + ;(el as HTMLElement).style.display = 'none' + el.remove() + }) + }) await page.waitForTimeout(500) } } @@ -334,6 +343,8 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => { .first() if (await teamSelectorButton.isVisible({ timeout: 2000 }).catch(() => false)) { console.log('Found TeamSelectorButton, clicking...') + // Dismiss tour before clicking to avoid driver-overlay blocking + await dismissOnboardingTour(page) await teamSelectorButton.click() await page.waitForTimeout(500) @@ -351,6 +362,8 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => { const teamSelector = page.locator('[data-tour="team-selector"]') if (await teamSelector.isVisible({ timeout: 3000 }).catch(() => false)) { console.log('Found team selector with data-tour attribute') + // Dismiss tour before clicking to avoid driver-overlay blocking + await dismissOnboardingTour(page) await teamSelector.click() await page.waitForTimeout(1000) @@ -367,6 +380,8 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => { // Strategy 4: Direct click on team card if visible anywhere on page const teamCard = page.locator(`text="${TEST_TEAM_NAME}"`).first() if (await teamCard.isVisible({ timeout: 3000 }).catch(() => false)) { + // Dismiss tour before clicking to avoid driver-overlay blocking + await dismissOnboardingTour(page) await teamCard.click() await page.waitForTimeout(1000) console.log(`Selected team from direct card: ${TEST_TEAM_NAME}`) @@ -390,46 +405,91 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => { */ async function selectTestModel(page: Page): Promise { try { - // Look for model selector button - it shows "Please select a model" or "请选择模型" when required - const modelSelectorButton = page - .locator( - 'button:has-text("Please select a model"), button:has-text("请选择模型"), button[role="combobox"]:has(svg.lucide-brain)' - ) - .first() + // Wait for page to fully load + await page.waitForTimeout(2000) - if (await modelSelectorButton.isVisible({ timeout: 3000 }).catch(() => false)) { - const buttonText = await modelSelectorButton.textContent() + // First try to find by data-testid (most reliable) + const modelSelectorByTestId = page.locator('[data-testid="model-selector"]').first() + if (await modelSelectorByTestId.isVisible({ timeout: 5000 }).catch(() => false)) { + const buttonText = await modelSelectorByTestId.textContent() console.log('Model selector button text:', buttonText) - // Check if model selection is required - if (buttonText?.includes('Please select') || buttonText?.includes('请选择模型')) { + // Check if model selection is required (shows "请选择模型" or "选择模型" or isModelRequired error state) + const needsSelection = buttonText?.includes('选择模型') || + buttonText?.includes('Please select') || + buttonText?.includes('Model') || + buttonText?.includes('required') || + buttonText?.includes('必须') || + await modelSelectorByTestId.locator('..').locator('..').locator('[class*="border-error"]').isVisible({ timeout: 1000 }).catch(() => false) + + if (needsSelection) { console.log('Model selection required, clicking selector...') // Dismiss tour before clicking await dismissOnboardingTour(page) - await modelSelectorButton.click({ force: true }) - await page.waitForTimeout(500) + await modelSelectorByTestId.click({ force: true }) + // Wait longer for dropdown to fully load and render options + await page.waitForTimeout(2000) // Look for our test model in the dropdown - const modelOption = page.locator(`[role="option"]:has-text("${TEST_MODEL_NAME}")`).first() - if (await modelOption.isVisible({ timeout: 3000 }).catch(() => false)) { + const modelOption = page.locator(`[role="option"]:has-text("${TEST_MODEL_NAME}"), [data-testid*="model-option"]`).first() + if (await modelOption.isVisible({ timeout: 5000 }).catch(() => false)) { console.log('Found test model, selecting...') await modelOption.click() - await page.waitForTimeout(500) + await page.waitForTimeout(1000) return true } - // Try to select any available model - const anyModelOption = page.locator('[role="option"]').first() - if (await anyModelOption.isVisible({ timeout: 2000 }).catch(() => false)) { + // Try to select any available model (exclude the search input if present) + const anyModelOption = page.locator('[role="option"]').filter({ hasNot: page.locator('input') }).first() + if (await anyModelOption.isVisible({ timeout: 5000 }).catch(() => false)) { console.log('Selecting first available model...') await anyModelOption.click() - await page.waitForTimeout(500) + await page.waitForTimeout(1000) return true } console.warn('No model options available in dropdown') // Press Escape to close dropdown await page.keyboard.press('Escape') + await page.waitForTimeout(500) + return false + } + + // Model already selected + console.log('Model already selected:', buttonText) + return true + } + + // Fallback: Look for model selector by text + const modelSelectorButton = page + .locator( + 'button:has-text("选择模型"), button:has-text("Please select"), button[role="combobox"]:has(svg.lucide-brain)' + ) + .first() + + if (await modelSelectorButton.isVisible({ timeout: 5000 }).catch(() => false)) { + const buttonText = await modelSelectorButton.textContent() + console.log('Model selector button text (fallback):', buttonText) + + // Check if model selection is required + if (buttonText?.includes('选择模型') || buttonText?.includes('Please select')) { + console.log('Model selection required (fallback), clicking selector...') + await dismissOnboardingTour(page) + await modelSelectorButton.click({ force: true }) + await page.waitForTimeout(1000) + + // Look for any model option + const anyModelOption = page.locator('[role="option"]').first() + if (await anyModelOption.isVisible({ timeout: 3000 }).catch(() => false)) { + console.log('Selecting first available model...') + await anyModelOption.click() + await page.waitForTimeout(1000) + return true + } + + console.warn('No model options available in dropdown') + await page.keyboard.press('Escape') + await page.waitForTimeout(500) return false } } diff --git a/frontend/e2e/tests/visual/settings-visual.spec.ts b/frontend/e2e/tests/visual/settings-visual.spec.ts index 48d8000d6..55861d34e 100644 --- a/frontend/e2e/tests/visual/settings-visual.spec.ts +++ b/frontend/e2e/tests/visual/settings-visual.spec.ts @@ -83,7 +83,10 @@ test.describe('Visual Regression - Settings Page', () => { test('settings models tab should match baseline @visual', async ({ page }) => { await page.goto('/settings?tab=models') await page.waitForLoadState('domcontentloaded') - await page.waitForTimeout(1000) + // Wait for the model management title to be visible + await expect( + page.locator('h2:has-text("Model Management"), h2:has-text("模型管理")').first() + ).toBeVisible({ timeout: 20000 }) // Visual regression tests are optional - pass if baseline doesn't exist const result = await expect(page) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d53a2db29..07e5e1146 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1258,7 +1258,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1275,7 +1274,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1292,7 +1290,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1309,7 +1306,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1326,7 +1322,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1343,7 +1338,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1360,7 +1354,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1377,7 +1370,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1394,7 +1386,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1411,7 +1402,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1428,7 +1418,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1445,7 +1434,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1462,7 +1450,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1479,7 +1466,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1496,7 +1482,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1513,7 +1498,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1530,7 +1514,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1547,7 +1530,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1564,7 +1546,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1581,7 +1562,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1598,7 +1578,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1615,7 +1594,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1632,7 +1610,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1649,7 +1626,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1666,7 +1642,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1683,7 +1658,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16308,7 +16282,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, diff --git a/frontend/src/components/ui/action-button.tsx b/frontend/src/components/ui/action-button.tsx index 311bb344a..4f999e376 100644 --- a/frontend/src/components/ui/action-button.tsx +++ b/frontend/src/components/ui/action-button.tsx @@ -17,6 +17,7 @@ interface ActionButtonProps { variant?: 'default' | 'outline' | 'loading' className?: string asChild?: boolean + 'data-testid'?: string } /** @@ -71,6 +72,7 @@ export function ActionButton({ label, variant = 'default', className = '', + 'data-testid': dataTestId, }: ActionButtonProps) { // Determine if this is an icon-only button or has a label const hasLabel = Boolean(label) @@ -87,6 +89,7 @@ export function ActionButton({ // Static loading state (non-clickable) return (
{icon} @@ -102,6 +105,7 @@ export function ActionButton({ return ( diff --git a/frontend/src/features/tasks/components/clarification/ClarificationQuestion.tsx b/frontend/src/features/tasks/components/clarification/ClarificationQuestion.tsx index 4bf4013a5..e364a0572 100644 --- a/frontend/src/features/tasks/components/clarification/ClarificationQuestion.tsx +++ b/frontend/src/features/tasks/components/clarification/ClarificationQuestion.tsx @@ -68,6 +68,7 @@ export default function ClarificationQuestion({ onValueChange={value => onChange({ answer_type: 'choice', value })} disabled={readonly} className="flex flex-col gap-2" + data-testid={`clarification-question-${question.question_id}-radio`} > {question.options?.map((option, index) => (
@@ -75,6 +76,7 @@ export default function ClarificationQuestion({ value={option.value} id={`question-${question.question_text}-option-${index}`} disabled={readonly} + data-testid={`clarification-option-${index}`} />