* fix(ai): confirm deleting a conversation in the standard dialog Deleting a conversation asked inside the chat panel's header, unlike every other destructive confirmation, which uses AppConfirmationDialog. It now opens that dialog, which names the conversation; Cancel keeps it and Delete removes it. * feat(ai): dim a reverted reply's content A reverted reply still showed its tool cards with their success marks and its text as if the edits were there; only the line below said otherwise. Its content is now dimmed, while the Changes reverted or Restore changes line and the other actions stay at full strength. * ci: typecheck the test suites in Code quality bun run check runs check:test-types, but PR CI did not, so tests that no longer type-checked could merge and break bun run check on master, as the grid fixtures from #866 did. It runs as a step of Code quality, which already builds the package declarations it needs; the check takes about two seconds, less than a separate job's setup. * test(ai): time the reasoning auto-close with the fake clock The test slept 1200 ms of real time past the 1000 ms auto-close to show that a manually opened block stays open, which was timing-dependent under load. It now advances Playwright's clock past the timer. With manual opening ignored, it fails. * test(ai): skip file identity on Playwright's bundled Chromium on macOS There, reading a FileSystemFileHandle back from IndexedDB closes the page before any app code runs, so the test always failed locally on a Mac. Installed Google Chrome on the same machine reads and compares the handle correctly, and the app's resolveFile returns one ID for twelve concurrent calls there. The skip is limited to the bundled build on macOS. * fix(settings): keep a model picker open while the editor slides in The models panel focused the editor's first field after its enter transition, even when focus had already moved into the editor, closing a picker opened during the animation. It now leaves focus alone once it has moved since the editor opened. * test: serve a fixed model catalog and wait for the editor before Settings shortcuts Chat tests fetched the live models.dev catalog, so the model list changed under them; they now get a small fixed one. The credentials test pressed the Settings shortcut before the editor was ready. * fix(settings): move drill-in focus with Reka's FocusScope The drill-in focused the detail's first field after its slide-in ended, found by querying the DOM, and restored focus by hand, so a picker opened during the slide closed. FocusScope now focuses the detail as it mounts and returns focus to what opened it. A caller can prevent open-auto-focus to focus its own field, which the models panel does for the profile name through the editor's exposed focus(). * test(ai): answer the Vision model's request in chat tests A message with images is analyzed by the Vision model, which calls OpenRouter directly rather than the mocked chat transport. With the test key the request failed, and since #916 a failure before dispatch takes the message back, so the WebKit history test never got a reply. The chat fixture now answers that request with fixed findings. * test(ai): check that deletion persists and reverted replies dim The delete test passed even if the conversation stayed in storage, since the panel clears its messages before the store removes it; it now reloads and checks the history. The revert test checked only data-reverted; it now checks the rendered opacity too.
256 lines
12 KiB
TypeScript
256 lines
12 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
|
|
|
import { CanvasHelper } from '#tests/helpers/canvas'
|
|
|
|
test('storage settings keep secrets behind the credential manager', async ({ page }) => {
|
|
await page.goto('/?test')
|
|
const canvas = new CanvasHelper(page)
|
|
await canvas.waitForInit()
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await page.getByTestId('settings-section-storage').click()
|
|
const connection = page.getByRole('button', { name: /S3 storage/ })
|
|
await connection.click()
|
|
await page.getByLabel('Endpoint', { exact: true }).fill('https://s3.example.com')
|
|
await page.getByLabel('Bucket', { exact: true }).fill('designs')
|
|
await expect(page.getByRole('button', { name: 'Copy CORS JSON' })).toBeHidden()
|
|
const secret = page.getByLabel('Secret access key', { exact: true })
|
|
await secret.fill('storage-secret')
|
|
const save = page.getByRole('button', { name: 'Save', exact: true })
|
|
await save.click()
|
|
await connection.click()
|
|
await expect(secret).toHaveValue('')
|
|
await expect(secret).toHaveAttribute('placeholder', /Key saved/)
|
|
expect(
|
|
await page.evaluate(() => {
|
|
// oxlint-disable-next-line open-pencil/no-direct-storage-access -- Inspect persisted bytes to detect plaintext credential leaks.
|
|
return Object.values(localStorage).some((value) => value.includes('storage-secret'))
|
|
})
|
|
).toBe(false)
|
|
await page.getByRole('button', { name: 'Clear', exact: true }).click()
|
|
await page.getByRole('button', { name: 'Cancel', exact: true }).click()
|
|
await connection.click()
|
|
await expect(secret).toHaveAttribute('placeholder', /Key saved/)
|
|
await page.getByRole('button', { name: 'Clear', exact: true }).click()
|
|
await save.click()
|
|
await page.getByTestId('app-settings-done').click()
|
|
await page.reload()
|
|
await canvas.waitForInit()
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await page.getByTestId('settings-section-storage').click()
|
|
await connection.click()
|
|
await expect(page.getByLabel('Endpoint', { exact: true })).toHaveValue('https://s3.example.com')
|
|
await expect(secret).not.toHaveAttribute('placeholder', /Key saved/)
|
|
})
|
|
|
|
test('MCP connections keep bearer tokens out of ordinary settings', async ({ page }) => {
|
|
await page.goto('/?test')
|
|
const canvas = new CanvasHelper(page)
|
|
await canvas.waitForInit()
|
|
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await page.getByTestId('settings-section-mcp').click()
|
|
const section = page.getByTestId('app-settings-dialog')
|
|
await section.getByRole('button', { name: 'Add connection' }).click()
|
|
await section.getByLabel('Connection name').fill('GitHub')
|
|
await section.getByLabel('MCP server URL').fill('http://example.com/mcp')
|
|
await section.getByRole('button', { name: 'Save' }).click()
|
|
await expect(section.getByLabel('MCP server URL')).toHaveAttribute('aria-invalid', 'true')
|
|
|
|
await section.getByLabel('MCP server URL').fill('https://example.com/mcp')
|
|
await section.getByRole('switch', { name: 'Enable for ACP agents' }).click()
|
|
await section.getByRole('switch', { name: 'Use bearer authentication' }).click()
|
|
await section.getByLabel('Bearer token').fill('secret-mcp-token')
|
|
await section.getByRole('button', { name: 'Save' }).click()
|
|
|
|
await expect(section).toContainText('GitHub')
|
|
await expect(section).toContainText('Enabled')
|
|
await expect(section).not.toContainText('secret-mcp-token')
|
|
|
|
await page.getByTestId('app-settings-done').click()
|
|
await page.reload()
|
|
await canvas.waitForInit()
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await page.getByTestId('settings-section-mcp').click()
|
|
await expect(section).toContainText('https://example.com/mcp')
|
|
await expect(section).toContainText('Enabled')
|
|
await section.getByRole('button', { name: /GitHub/ }).click()
|
|
await expect(section.getByPlaceholder(/Key saved/)).toBeVisible()
|
|
await section.getByRole('button', { name: 'Clear', exact: true }).click()
|
|
await section.getByRole('button', { name: 'Cancel', exact: true }).click()
|
|
await section.getByRole('button', { name: /GitHub/ }).click()
|
|
await expect(section.getByPlaceholder(/Key saved/)).toBeVisible()
|
|
await section.getByRole('button', { name: 'Clear', exact: true }).click()
|
|
await section.getByRole('button', { name: 'Save', exact: true }).click()
|
|
await section.getByRole('button', { name: /GitHub/ }).click()
|
|
await expect(section.getByPlaceholder(/Key saved/)).toHaveCount(0)
|
|
await section.getByRole('button', { name: 'Delete connection' }).click()
|
|
const confirmation = page.getByRole('alertdialog')
|
|
await expect(confirmation).toContainText('remove its saved bearer token')
|
|
await confirmation.getByRole('button', { name: 'Delete connection' }).click()
|
|
await expect(section).toContainText('No external MCP connections configured')
|
|
})
|
|
|
|
test('MCP automation settings filter and persist tool availability', async ({ page }) => {
|
|
await page.route('**/health', (route) =>
|
|
route.fulfill({
|
|
json: {
|
|
status: 'ok',
|
|
tools: [
|
|
{
|
|
name: 'create_shape',
|
|
description: 'Create a shape',
|
|
effect: 'write',
|
|
availability: 'default',
|
|
capabilities: ['document:write'],
|
|
enabled: true
|
|
},
|
|
{
|
|
name: 'get_page_tree',
|
|
description: 'Inspect the page',
|
|
effect: 'read',
|
|
availability: 'default',
|
|
capabilities: ['document:read'],
|
|
enabled: true
|
|
}
|
|
]
|
|
}
|
|
})
|
|
)
|
|
await page.goto('/?test')
|
|
const canvas = new CanvasHelper(page)
|
|
await canvas.waitForInit()
|
|
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await page.getByTestId('settings-section-mcp').click()
|
|
|
|
const authentication = page.getByTestId('settings-mcp-authentication')
|
|
await expect(authentication).toHaveAttribute('data-state', 'checked')
|
|
await authentication.click()
|
|
await page.reload()
|
|
await canvas.waitForInit()
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await page.getByTestId('settings-section-mcp').click()
|
|
await expect(authentication).toHaveAttribute('data-state', 'unchecked')
|
|
await authentication.click()
|
|
|
|
await page.getByRole('button', { name: 'Tool access', exact: true }).click()
|
|
const search = page.getByRole('searchbox', { name: 'Search tools' })
|
|
await search.fill('create_shape')
|
|
await expect(search).toHaveValue('create_shape')
|
|
const createShape = page.getByRole('switch', { name: 'create_shape', exact: true })
|
|
await expect(createShape).toBeVisible()
|
|
await expect(page.getByRole('switch', { name: 'get_page_tree', exact: true })).toBeHidden()
|
|
|
|
await createShape.click()
|
|
await page.reload()
|
|
await canvas.waitForInit()
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await page.getByTestId('settings-section-mcp').click()
|
|
await page.getByRole('button', { name: 'Tool access', exact: true }).click()
|
|
await expect(createShape).toHaveAttribute('data-state', 'unchecked')
|
|
|
|
await page.getByRole('button', { name: 'Restore defaults' }).click()
|
|
await expect(createShape).toHaveAttribute('data-state', 'checked')
|
|
})
|
|
|
|
test('model library keeps reusable profiles and role assignments', async ({ page }) => {
|
|
await page.goto('/?test')
|
|
const canvas = new CanvasHelper(page)
|
|
await canvas.waitForInit()
|
|
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await page.getByTestId('settings-section-ai').click()
|
|
await page.getByTestId('settings-add-model').click()
|
|
await page.getByLabel('Name').fill('Fast model')
|
|
await page.getByTestId('settings-model-provider').click()
|
|
await page.getByRole('option', { name: 'Google AI' }).click()
|
|
await page.getByLabel('Model ID').click()
|
|
await page.getByRole('option').first().click()
|
|
await page.getByRole('button', { name: 'Save model' }).click()
|
|
|
|
await page.getByTestId('settings-add-model').click()
|
|
await page.getByLabel('Name').fill('Vision model')
|
|
await page.getByTestId('settings-model-provider').click()
|
|
await page.getByRole('option', { name: 'OpenRouter' }).click()
|
|
await page.getByLabel('Model ID').first().click()
|
|
await page.getByRole('option').first().click()
|
|
await page.getByRole('button', { name: 'Save model' }).click()
|
|
|
|
await page.getByTestId('settings-model-assignment-fast').click()
|
|
await page.getByRole('option', { name: 'Fast model' }).click()
|
|
await page.getByTestId('settings-model-assignment-vision').click()
|
|
await page.getByRole('option', { name: 'Vision model' }).click()
|
|
await page.getByTestId('app-settings-done').click()
|
|
|
|
await page.reload()
|
|
await canvas.waitForInit()
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await page.getByTestId('settings-section-ai').click()
|
|
await expect(page.getByTestId('settings-model-list')).toContainText('Fast model')
|
|
await expect(page.getByTestId('settings-model-list')).toContainText('Vision model')
|
|
await expect(page.getByTestId('settings-model-assignment-fast')).toContainText('Fast model')
|
|
await expect(page.getByTestId('settings-model-assignment-vision')).toContainText('Vision model')
|
|
})
|
|
|
|
test('remembered browser credentials survive reload and clear centrally', async ({ page }) => {
|
|
await page.goto('/?test')
|
|
const canvas = new CanvasHelper(page)
|
|
await canvas.waitForInit()
|
|
|
|
await page.getByRole('tab', { name: 'AI' }).click()
|
|
await page.getByTestId('provider-setup-open-settings').click()
|
|
|
|
await page.getByTestId('settings-section-general').click()
|
|
const remember = page.getByRole('switch', { name: 'Remember API keys on this device' })
|
|
await expect(remember).toHaveAttribute('aria-checked', 'true')
|
|
await page.getByTestId('settings-section-ai').click()
|
|
|
|
await page.locator('[data-model-id]').first().click()
|
|
await page.getByTestId('settings-model-provider').click()
|
|
await page.getByRole('option', { name: 'OpenRouter' }).click()
|
|
await page.getByLabel('Name').fill('Claude Sonnet')
|
|
await page.getByTestId('provider-settings-api-key').fill('sk-or-remembered-test-key')
|
|
await page.getByRole('button', { name: 'Save model' }).click()
|
|
await page.getByTestId('app-settings-done').click()
|
|
await expect(page.getByTestId('chat-input')).toBeVisible()
|
|
|
|
await page.reload()
|
|
await canvas.waitForInit()
|
|
await page.getByRole('tab', { name: 'AI' }).click()
|
|
await expect(page.getByTestId('chat-input')).toBeVisible()
|
|
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await page.getByTestId('settings-section-ai').click()
|
|
await page.locator('[data-model-id]').first().click()
|
|
await page.getByTestId('provider-settings-clear-key').click()
|
|
await page
|
|
.getByTestId('settings-model-editor')
|
|
.getByRole('button', { name: 'Cancel', exact: true })
|
|
.click()
|
|
await page.getByTestId('settings-section-general').click()
|
|
await remember.click()
|
|
await page.getByTestId('app-settings-done').click()
|
|
|
|
await page.reload()
|
|
await canvas.waitForInit()
|
|
await page.getByRole('tab', { name: 'AI' }).click()
|
|
await expect(page.getByTestId('provider-setup-open-settings')).toBeVisible()
|
|
})
|
|
|
|
test('browser credential preferences live in General, not the footer', async ({ page }) => {
|
|
await page.goto('/?test')
|
|
await new CanvasHelper(page).waitForInit()
|
|
await page.keyboard.press('ControlOrMeta+,')
|
|
const panel = page.getByTestId('settings-general-panel')
|
|
const remember = panel.getByRole('switch', { name: 'Remember API keys on this device' })
|
|
await expect(remember).toBeVisible()
|
|
if ((await remember.getAttribute('aria-checked')) === 'true') await remember.click()
|
|
await expect(remember).toHaveAttribute('aria-checked', 'false')
|
|
await expect(panel.getByText('Keys are kept only until you close this session.')).toBeVisible()
|
|
await remember.click()
|
|
await expect(remember).toHaveAttribute('aria-checked', 'true')
|
|
await expect(panel.getByText('Keys are kept only until you close this session.')).toBeHidden()
|
|
await expect(page.getByText('system credential store', { exact: false })).toHaveCount(0)
|
|
await page.getByTestId('app-settings-done').click()
|
|
})
|