Files
LocalAI/core/http/react-ui/e2e/agents-edit.spec.js
T
Stefan Walcz 773cae7259 fix(ui): keep focus in settings sheets when the page re-renders
The chat settings sheet, the Talk session sheet and the agent and task
preview sheets set up their focus handling in an effect that depended on
onClose. Every parent passes onClose as an inline arrow, so each re-render
of the page gave it a new identity: the effect cleanup sent focus back to
the opener and the effect ran again and focused the Close button.

Any edit in a sheet re-renders the page, so focus left the control being
used. In the chat sheet, typing in the system prompt moved focus to Close
after the first key, and a following space activated Close and dismissed
the sheet; an open native dropdown in the sheet closed by itself.

Keep the latest onClose in a ref and run the focus effect once per open.
Escape still calls the current onClose; initial focus on Close, the Tab
trap and restoring focus to the opener are unchanged.

The e2e specs type key by key (or switch preview tabs) and assert that
focus stays put; they fail without this change.

Docs-Not-Needed: bug fix, no user-facing behaviour change beyond the fix
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
2026-10-21 15:40:43 +02:00

129 lines
7.4 KiB
JavaScript

import { test, expect } from './coverage-fixtures.js'
import { installFakeStream, mockAgents, RESEARCH_CONFIG } from './agents-fixtures.js'
test.describe('Create and edit an agent', () => {
test('sections fold, show ready marks and a one-line summary', async ({ page }) => {
await installFakeStream(page)
await mockAgents(page)
await page.goto('/app/agents/research-assistant/edit')
await expect(page.getByRole('heading', { name: 'Edit Agent: research-assistant' })).toBeVisible()
const basics = page.locator('[data-section="BasicInfo"]')
await expect(basics.locator('.ag-fold__button')).toHaveAttribute('aria-expanded', 'true')
const model = page.locator('[data-section="ModelSettings"]')
await expect(model.locator('.ag-fold__button')).toHaveAttribute('aria-expanded', 'false')
await expect(model.locator('.ag-fold__sum')).toHaveText('qwen3-14b-instruct')
await expect(model.locator('.ag-ready')).toHaveAttribute('data-state', 'ready')
await expect(model.locator('.ag-fold__body')).toHaveCount(0)
await model.locator('.ag-fold__button').click()
await expect(model.locator('.ag-fold__body')).toBeVisible()
await model.locator('.ag-fold__button').click()
await expect(model.locator('.ag-fold__body')).toHaveCount(0)
})
test('a new agent starts with sections that need a name and a model', async ({ page }) => {
await installFakeStream(page)
await mockAgents(page)
await page.goto('/app/agents/new')
await expect(page.locator('[data-section="BasicInfo"] .ag-ready')).toHaveAttribute('data-state', 'needs')
await expect(page.locator('[data-section="ModelSettings"] .ag-fold__sum')).toHaveText('Needs a model')
await expect(page.locator('[data-section="MemorySettings"] .ag-ready')).toHaveAttribute('data-state', 'empty')
})
test('a template fills the form and saves nothing', async ({ page }) => {
await installFakeStream(page)
const seen = await mockAgents(page)
await page.goto('/app/agents/new')
await page.getByRole('button', { name: 'Code reviewer' }).click()
await expect(page.locator('[data-section="BasicInfo"] .ag-fold__sum')).toContainText('code-reviewer')
expect(seen.saves).toEqual([])
})
test('a template link from the launcher opens the form already filled', async ({ page }) => {
await installFakeStream(page)
await mockAgents(page)
await page.goto('/app/agents/new?template=digest')
await expect(page.locator('[data-section="BasicInfo"] .ag-fold__sum')).toContainText('daily-digest')
})
test('the preview sheet shows the config and, when editing, what changed', async ({ page }) => {
await installFakeStream(page)
const seen = await mockAgents(page, { agents: { 'research-assistant': { active: true, config: { ...RESEARCH_CONFIG, api_key: 'sk-secret-value', mcp_servers: [{ url: 'https://x.example.org', token: 'tok-123' }] } } } })
await page.goto('/app/agents/research-assistant/edit')
await page.locator('[data-section="PromptsGoals"] .ag-fold__button').click()
await page.locator('[data-section="PromptsGoals"] textarea').fill('A new instruction.')
await expect(page.locator('[data-section="PromptsGoals"] .ag-badge-changed')).toHaveText('1 changed')
await page.getByTestId('agent-preview-open').click()
const sheet = page.getByTestId('agent-preview')
await expect(sheet.getByTestId('agent-preview-config')).toContainText('"system_prompt": "A new instruction."')
const cfg = await sheet.getByTestId('agent-preview-config').textContent()
expect(cfg).not.toContain('sk-secret-value')
expect(cfg).not.toContain('tok-123')
// Switching tabs re-renders the page; focus stays on the tab.
await sheet.getByRole('tab', { name: /Changes/ }).click()
await expect(sheet.getByRole('tab', { name: /Changes/ })).toBeFocused()
const changes = sheet.getByTestId('agent-preview-changes')
await expect(changes.locator('[data-key]')).toHaveCount(1)
await expect(changes.locator('[data-key="system_prompt"]')).toContainText('You are a careful research assistant')
await expect(changes.locator('[data-key="system_prompt"]')).toContainText('A new instruction.')
await page.keyboard.press('Escape')
await expect(sheet).toHaveCount(0)
await page.getByTestId('agent-preview-open').click()
await page.getByTestId('agent-preview').getByRole('button', { name: 'Save Changes' }).click()
await expect.poll(() => seen.saves.length).toBe(1)
expect(seen.saves[0]).toMatchObject({ method: 'PUT', name: 'research-assistant' })
expect(seen.saves[0].body.system_prompt).toBe('A new instruction.')
})
test('with no edits the Changes tab says the form matches the saved agent', async ({ page }) => {
await installFakeStream(page)
await mockAgents(page)
await page.goto('/app/agents/research-assistant/edit')
await page.getByTestId('agent-preview-open').click()
await page.getByRole('tab', { name: /Changes/ }).click()
await expect(page.getByTestId('agent-preview-changes')).toContainText('No changes')
})
test('creating an agent sends the form and offers no Dry run it cannot run', async ({ page }) => {
await installFakeStream(page)
const seen = await mockAgents(page)
await page.goto('/app/agents/new')
await page.locator('[data-section="BasicInfo"] input').first().fill('my-agent')
await page.locator('[data-section="ModelSettings"] .ag-fold__button').click()
await page.locator('[data-section="ModelSettings"] input').first().fill('qwen3-8b-instruct')
await page.getByTestId('agent-preview-open').click()
await expect(page.getByRole('tab', { name: /Dry run/ })).toHaveCount(0)
await page.getByRole('tab', { name: /Changes/ }).click()
await expect(page.getByTestId('agent-preview-changes')).toContainText('qwen3-8b-instruct')
await page.keyboard.press('Escape')
await page.getByRole('button', { name: 'Create Agent' }).first().click()
await expect.poll(() => seen.saves.length).toBe(1)
expect(seen.saves[0].body).toMatchObject({ name: 'my-agent', model: 'qwen3-8b-instruct' })
})
test('draft from a sentence needs a model first and is labelled optional', async ({ page }) => {
await installFakeStream(page)
await mockAgents(page)
await page.goto('/app/agents/new')
await expect(page.getByTestId('agent-start')).toContainText('Optional')
await page.getByLabel('Describe the agent in one sentence').fill('reads release notes')
await page.getByRole('button', { name: 'Draft' }).click()
await expect(page.getByText(/Pick a model in the Model section first/)).toBeVisible()
})
test('draft from a sentence fills name, description and instructions from the chosen model', async ({ page }) => {
await installFakeStream(page)
await mockAgents(page)
await page.route('**/v1/chat/completions', route => route.fulfill({ json: { choices: [{ message: { content: '```json\n{"name":"release-reader","description":"Reads release notes.","system_prompt":"List breaking changes."}\n```' } }] } }))
await page.goto('/app/agents/new')
await page.locator('[data-section="ModelSettings"] .ag-fold__button').click()
await page.locator('[data-section="ModelSettings"] input').first().fill('qwen3-8b-instruct')
await page.getByLabel('Describe the agent in one sentence').fill('reads release notes')
await page.getByRole('button', { name: 'Draft' }).click()
await expect(page.locator('[data-section="BasicInfo"] .ag-fold__sum')).toContainText('release-reader')
})
})