Render selected labels from reactive options rather than cached menu text. Refine action-row hover and cursor states, add a connection-test icon, simplify media credentials, and share semantic external links across Settings. Cover locale switching and link destinations with focused regressions and Storybook states.
229 lines
10 KiB
TypeScript
229 lines
10 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test'
|
|
|
|
import { CanvasHelper } from '#tests/helpers/canvas'
|
|
|
|
async function openSettings(page: Page) {
|
|
await page.goto('/?test')
|
|
await new CanvasHelper(page).waitForInit()
|
|
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+,' : 'Control+,')
|
|
}
|
|
|
|
test('model Save and Test explain missing fields and focus the first error', async ({ page }) => {
|
|
await page.emulateMedia({ colorScheme: 'dark' })
|
|
await openSettings(page)
|
|
await page.getByTestId('settings-section-ai').click()
|
|
await page.getByTestId('settings-add-model').click()
|
|
const editor = page.getByTestId('settings-model-editor')
|
|
const name = editor.getByRole('textbox', { name: 'Name', exact: true })
|
|
const save = page.getByRole('button', { name: 'Save model', exact: true })
|
|
await expect(editor.locator('[aria-invalid="true"]')).toHaveCount(0)
|
|
await name.fill('')
|
|
await name.press('Tab')
|
|
await expect(name).toHaveAttribute('aria-invalid', 'true')
|
|
await expect(editor.locator('[aria-invalid="true"]')).toHaveCount(1)
|
|
await expect(save).toBeEnabled()
|
|
await save.click()
|
|
await expect(name).toBeFocused()
|
|
await expect(name).toHaveAttribute('aria-invalid', 'true')
|
|
await expect(name).toHaveAccessibleDescription(/^This field is required\./)
|
|
expect(
|
|
await name.evaluate((control) => {
|
|
const [errorID, hintID] = control.getAttribute('aria-describedby')?.split(' ') ?? []
|
|
if (!errorID || !hintID) return false
|
|
const error = document.getElementById(errorID)
|
|
const hint = document.getElementById(hintID)
|
|
return Boolean(
|
|
error && hint && error.getBoundingClientRect().bottom <= hint.getBoundingClientRect().top
|
|
)
|
|
})
|
|
).toBe(true)
|
|
await expect(name).toHaveCSS('border-top-color', 'rgb(248, 113, 113)')
|
|
await expect(editor.getByRole('alert').first()).toHaveCSS('color', 'rgb(248, 113, 113)')
|
|
await name.fill('My model')
|
|
await expect(name).toHaveAttribute('aria-invalid', 'false')
|
|
const testConnection = page.getByTestId('provider-test-connection')
|
|
await expect(testConnection).toBeEnabled()
|
|
await testConnection.click()
|
|
await expect(editor.locator('[aria-invalid="true"]').first()).toBeFocused()
|
|
})
|
|
|
|
test('storage validates preferences and test credentials without saving', async ({ page }) => {
|
|
await openSettings(page)
|
|
await page.getByTestId('settings-section-storage').click()
|
|
await expect(
|
|
page.getByText('Configure the connection before opening the workspace.')
|
|
).toBeVisible()
|
|
await page.getByRole('button', { name: /S3 storage/ }).click()
|
|
const endpoint = page.getByRole('textbox', { name: 'Endpoint', exact: true })
|
|
const bucket = page.getByRole('textbox', { name: 'Bucket', exact: true })
|
|
await page.getByRole('button', { name: 'Save', exact: true }).click()
|
|
await expect(endpoint).toBeFocused()
|
|
await expect(endpoint).toHaveAttribute('aria-invalid', 'true')
|
|
await endpoint.fill('https://example.com')
|
|
await bucket.fill('designs')
|
|
await expect(endpoint).toHaveAttribute('aria-invalid', 'false')
|
|
await page.getByTestId('settings-storage-test').click()
|
|
const key = page.getByRole('textbox', { name: 'Access key ID', exact: true })
|
|
await expect(key).toBeFocused()
|
|
await expect(key).toHaveAttribute('aria-invalid', 'true')
|
|
await expect(key).toHaveAccessibleDescription('This field is required.')
|
|
await page.getByRole('button', { name: 'Cancel', exact: true }).click()
|
|
await expect(
|
|
page.getByText('Configure the connection before opening the workspace.')
|
|
).toBeVisible()
|
|
})
|
|
|
|
async function failCredentialWrites(page: Page) {
|
|
return page.evaluateHandle(async () => {
|
|
const path = '/src/app/settings/credentials/app.ts'
|
|
const { appCredentialServices } = await import(path)
|
|
const original = appCredentialServices.manager.set
|
|
appCredentialServices.manager.set = async () => {
|
|
throw new Error('Test credential write failure')
|
|
}
|
|
return {
|
|
restore() {
|
|
appCredentialServices.manager.set = original
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
test('media explains optional keys and preserves input after a failed save', async ({ page }) => {
|
|
await openSettings(page)
|
|
await page.getByTestId('settings-section-media').click()
|
|
await page.getByRole('button', { name: /Pexels/ }).click()
|
|
const key = page.getByRole('textbox', { name: 'API key', exact: true })
|
|
await expect(page.getByRole('link', { name: 'Get API key' })).toHaveAttribute(
|
|
'href',
|
|
'https://www.pexels.com/api/'
|
|
)
|
|
await expect(key).toHaveAccessibleDescription('Optional. Add a key to enable this service.')
|
|
await key.fill('test-only-replacement')
|
|
const failure = await failCredentialWrites(page)
|
|
try {
|
|
await page.getByRole('button', { name: 'Save', exact: true }).click()
|
|
await expect(key).toHaveValue('test-only-replacement')
|
|
await expect(key).not.toHaveAttribute('aria-invalid', 'true')
|
|
await expect(
|
|
page.getByRole('alert', {
|
|
name: 'Could not save these changes. Check the settings and try again.',
|
|
exact: true
|
|
})
|
|
).toBeVisible()
|
|
await expect(key).toHaveAccessibleDescription('Optional. Add a key to enable this service.')
|
|
await expect(page.getByRole('button', { name: 'Save', exact: true })).toBeEnabled()
|
|
} finally {
|
|
await failure.evaluate((handle) => handle.restore())
|
|
await failure.dispose()
|
|
}
|
|
await page.getByRole('button', { name: 'Save', exact: true }).click()
|
|
await expect(page.getByRole('button', { name: /Pexels/ })).toBeVisible()
|
|
})
|
|
|
|
test('MCP requires a token only for enabled bearer connections and preserves failed saves', async ({
|
|
page
|
|
}) => {
|
|
await openSettings(page)
|
|
await page.getByTestId('settings-section-mcp').click()
|
|
await page.getByRole('button', { name: 'Add connection', exact: true }).click()
|
|
await page.getByRole('textbox', { name: 'Connection name', exact: true }).fill('Test server')
|
|
await page
|
|
.getByRole('textbox', { name: 'MCP server URL', exact: true })
|
|
.fill('https://example.com/mcp')
|
|
await page.getByRole('switch', { name: 'Enable for ACP agents' }).click()
|
|
await page.getByRole('switch', { name: 'Use bearer authentication' }).click()
|
|
const key = page.getByRole('textbox', { name: 'Bearer token', exact: true })
|
|
await page.getByRole('button', { name: 'Save', exact: true }).click()
|
|
await expect(key).toBeFocused()
|
|
await expect(key).toHaveAttribute('aria-invalid', 'true')
|
|
await key.fill('test-only-replacement')
|
|
await expect(key).toHaveAttribute('aria-invalid', 'false')
|
|
const failure = await failCredentialWrites(page)
|
|
try {
|
|
await page.getByRole('button', { name: 'Save', exact: true }).click()
|
|
await expect(
|
|
page.getByText('Could not save these changes. Check the settings and try again.', {
|
|
exact: true
|
|
})
|
|
).toBeVisible()
|
|
await expect(key).toHaveValue('test-only-replacement')
|
|
await expect(
|
|
page.getByText('Some changes may already be saved. Review the settings and try again.')
|
|
).toBeVisible()
|
|
await expect(page.getByRole('button', { name: 'Save', exact: true })).toBeEnabled()
|
|
} finally {
|
|
await failure.evaluate((handle) => handle.restore())
|
|
await failure.dispose()
|
|
}
|
|
})
|
|
|
|
test('a partial model save is explained and retry does not duplicate the profile', async ({
|
|
page
|
|
}) => {
|
|
await openSettings(page)
|
|
await page.getByTestId('settings-section-ai').click()
|
|
await page.getByTestId('settings-add-model').click()
|
|
await page.getByLabel('Name', { exact: true }).fill('Partial model')
|
|
await page.getByTestId('settings-model-provider').click()
|
|
await page.getByRole('option', { name: 'Google AI', exact: true }).click()
|
|
await page.getByLabel('Model ID', { exact: true }).click()
|
|
await page.getByRole('option').first().click()
|
|
await page.getByTestId('provider-settings-api-key').fill('test-only-key')
|
|
const failure = await failCredentialWrites(page)
|
|
try {
|
|
await page.getByRole('button', { name: 'Save model', exact: true }).click()
|
|
await expect(
|
|
page.getByText('Some changes may already be saved. Review the settings and try again.')
|
|
).toBeVisible()
|
|
await expect(page.getByTestId('provider-settings-api-key')).toHaveValue('test-only-key')
|
|
} finally {
|
|
await failure.evaluate((handle) => handle.restore())
|
|
await failure.dispose()
|
|
}
|
|
await page.getByRole('button', { name: 'Save model', exact: true }).click()
|
|
await expect(page.getByRole('button', { name: /Partial model/ })).toHaveCount(1)
|
|
})
|
|
|
|
test('the sole design-capable profile cannot be deleted even with other profiles present', async ({
|
|
page
|
|
}) => {
|
|
await openSettings(page)
|
|
const designId = await page.evaluate(async () => {
|
|
const path = '/src/app/ai/models/index.ts'
|
|
const models = await import(path)
|
|
const design = models.resolveAIModelRole('design')
|
|
if (!design) throw new Error('Missing Design profile')
|
|
const draft = models.createModelProfileDraft()
|
|
draft.name = 'Text only'
|
|
draft.providerID = 'google'
|
|
draft.customModelID = 'text-only'
|
|
draft.capabilities = []
|
|
models.saveModelProfileDraft(draft)
|
|
return design.profile.id
|
|
})
|
|
await page.getByTestId('settings-section-ai').click()
|
|
await page.locator(`[data-model-id="${designId}"]`).click()
|
|
await expect(page.getByRole('button', { name: 'Save model', exact: true })).toBeVisible()
|
|
await expect(page.getByRole('button', { name: 'Delete model', exact: true })).toHaveCount(0)
|
|
})
|
|
|
|
test('MCP field feedback is translated after switching the locale', async ({ page }) => {
|
|
await openSettings(page)
|
|
await page.getByTestId('settings-language').click()
|
|
await page.getByRole('option', { name: 'Русский', exact: true }).click()
|
|
await page.getByTestId('settings-section-mcp').click()
|
|
await page.getByRole('button', { name: 'Добавить подключение', exact: true }).click()
|
|
await page.getByRole('button', { name: 'Сохранить', exact: true }).click()
|
|
const name = page.getByRole('textbox', { name: 'Название подключения', exact: true })
|
|
await expect(name).toBeFocused()
|
|
await expect(name).toHaveAccessibleDescription(/Это поле обязательно\./)
|
|
await name.fill('Мой сервер')
|
|
await expect(name).toHaveAttribute('aria-invalid', 'false')
|
|
const url = page.getByRole('textbox', { name: 'URL сервера MCP', exact: true })
|
|
await url.fill('http://example.com/mcp')
|
|
await url.press('Tab')
|
|
await expect(url).toHaveAccessibleDescription(/Используйте HTTPS/)
|
|
await expect(page.getByText('This field is required.', { exact: true })).toHaveCount(0)
|
|
})
|