Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 42 additions & 21 deletions frontend/e2e/tests/settings-model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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 }) => {
Expand All @@ -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)
}
Comment on lines 99 to 106
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test doesn't verify save success or failure.

After clicking submit, the test waits 2 seconds and ends without verifying the dialog closed or a success toast appeared. Per the context snippet from ModelEditDialog.tsx, onClose() is only called on success.

🔧 Proposed fix to verify dialog closure
     const submitButton = page.locator('button:has-text("Save"), button:has-text("保存")').first()
     if (await submitButton.isVisible({ timeout: 5000 }).catch(() => false)) {
       await submitButton.click()
 
-      // Wait for dialog to close or validation error
-      await page.waitForTimeout(2000)
+      // Verify dialog closes on success (or remains open on validation error)
+      const dialogClosed = await page.locator('[role="dialog"]').isHidden({ timeout: 5000 }).catch(() => false)
+      if (!dialogClosed) {
+        console.log('Dialog still visible - may have validation errors')
+      }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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)
}
// Submit form
const submitButton = page.locator('button:has-text("Save"), button:has-text("保存")').first()
if (await submitButton.isVisible({ timeout: 5000 }).catch(() => false)) {
await submitButton.click()
// Verify dialog closes on success (or remains open on validation error)
const dialogClosed = await page.locator('[role="dialog"]').isHidden({ timeout: 5000 }).catch(() => false)
if (!dialogClosed) {
console.log('Dialog still visible - may have validation errors')
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/e2e/tests/settings-model.spec.ts` around lines 99 - 106, After
clicking the submitButton in the test, replace the blind wait with an explicit
assertion that verifies the dialog actually closed or a success notification
appeared: wait for the ModelEditDialog to be detached/hidden (use the dialog's
root selector or the same locator used to open it) or wait for the success
toast/toast text that your app emits on successful save, since ModelEditDialog
calls onClose() only on success; update the test around submitButton.click() to
await that dialog-closed or success-toast condition instead of
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()
Expand All @@ -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()

Expand Down
106 changes: 83 additions & 23 deletions frontend/e2e/tests/tasks/chat-image-browser-e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => {
*/
async function dismissOnboardingTour(page: Page): Promise<void> {
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)) {
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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}`)
Expand All @@ -390,46 +405,91 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => {
*/
async function selectTestModel(page: Page): Promise<boolean> {
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
}
}
Expand Down
5 changes: 4 additions & 1 deletion frontend/e2e/tests/visual/settings-visual.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading