diff --git a/web/tests/e2e/bot-session-tool-timeline.spec.ts b/web/tests/e2e/bot-session-tool-timeline.spec.ts index 060e10f93..ae397007e 100644 --- a/web/tests/e2e/bot-session-tool-timeline.spec.ts +++ b/web/tests/e2e/bot-session-tool-timeline.spec.ts @@ -152,8 +152,7 @@ test.describe('bot session request recovery', () => { await page.getByRole('button', { name: /Recovery user 1/ }).click(); else await page - .getByRole('button', { name: 'Next', exact: true }) - .last() + .getByRole('button', { name: 'Next page', exact: true }) .click(); } } diff --git a/web/tests/e2e/codex-subscription.spec.ts b/web/tests/e2e/codex-subscription.spec.ts index 5a67867f9..6a8ad70c4 100644 --- a/web/tests/e2e/codex-subscription.spec.ts +++ b/web/tests/e2e/codex-subscription.spec.ts @@ -196,7 +196,7 @@ for (const width of [1280, 390, 320]) { await page.getByRole('button', { name: 'Done', exact: true }).click(); await expect(page.getByText('My Codex', { exact: true })).toBeVisible(); await expect( - page.getByRole('button', { name: 'Add Model', exact: true }), + page.getByRole('button', { name: 'Add', exact: true }), ).toBeVisible(); expect(state.creates).toBe(1); expect( @@ -309,7 +309,7 @@ test('model test retains the connected provider identity', async ({ page }) => { }); await page.goto('/home/bots'); await page.getByRole('button', { name: 'Models', exact: true }).click(); - await page.getByRole('button', { name: 'Add Model', exact: true }).click(); + await page.getByRole('button', { name: 'Add', exact: true }).click(); await page .getByPlaceholder('Model Name', { exact: true }) .fill('fixture-codex-model'); diff --git a/web/tests/e2e/contextual-guides.spec.ts b/web/tests/e2e/contextual-guides.spec.ts index d41d5e122..b4da33b88 100644 --- a/web/tests/e2e/contextual-guides.spec.ts +++ b/web/tests/e2e/contextual-guides.spec.ts @@ -319,7 +319,7 @@ test('bot adapter gallery keeps categories and fits desktop and mobile', async ( ) .toBe(true); - await gallery.getByRole('button', { name: /Legacy adapters/ }).click(); + await gallery.getByRole('button', { name: /^Legacy adapters \d+$/ }).click(); await expect( gallery.getByRole('button', { name: /Legacy Adapter/ }), ).toBeVisible(); @@ -642,11 +642,11 @@ for (const available of [true, false]) { guide.getByRole('heading', { name: 'Review execution results' }), ).toBeVisible(); await expect( - page.locator('[data-guide="event-processor-form-tab-logs"]'), + page.getByRole('tab', { name: 'Run logs', exact: true }), ).toHaveAttribute('data-state', 'active'); await guide.getByRole('button', { name: 'Previous', exact: true }).click(); await expect( - page.locator('[data-guide="event-processor-form-tab-config"]'), + page.getByRole('tab', { name: 'Configure & debug', exact: true }), ).toHaveAttribute('data-state', 'active'); await guide.getByRole('button', { name: 'Next', exact: true }).click(); if (available) { @@ -656,7 +656,7 @@ for (const available of [true, false]) { ).toBeVisible(); await expect(guide).toContainText('no per-event routing needed'); await expect( - page.locator('[data-guide="event-processor-form-tab-config"]'), + page.getByRole('tab', { name: 'Configure & debug', exact: true }), ).toHaveAttribute('data-state', 'active'); } await guide.getByRole('button', { name: 'Finish', exact: true }).click(); diff --git a/web/tests/e2e/crud-smoke.spec.ts b/web/tests/e2e/crud-smoke.spec.ts index 5fa7f155f..ba0d3b176 100644 --- a/web/tests/e2e/crud-smoke.spec.ts +++ b/web/tests/e2e/crud-smoke.spec.ts @@ -6,7 +6,7 @@ import { } from './fixtures/langbot-api'; async function save(page: Page) { - const button = page.getByRole('button', { name: /^Save$/ }); + const button = page.getByRole('button', { name: /^Save(?: and enable)?$/ }); await expect(button).toBeEnabled(); await button.click(); } @@ -127,10 +127,10 @@ test.describe('frontend CRUD smoke flows', () => { await page.goto('/home/agents?id=new'); await page.getByRole('radio', { name: /^Pipeline/ }).click(); + await page.locator('#agent-create-form').getByRole('combobox').click(); + await page.getByRole('option', { name: /Local Agent/ }).click(); await page.locator('input[name="name"]').fill('Viewer Pipeline'); - await page - .locator('input[name="description"]') - .fill('Viewer monitoring permission regression.'); + await submit(page); await expect(page).toHaveURL(/\/home\/agents\?id=pipeline-1$/); @@ -218,6 +218,7 @@ test.describe('frontend CRUD smoke flows', () => { 'data-slot', 'toggle-group-item', ); + await agentTypeCard.click(); await expect(agentTypeCard).toHaveAttribute('aria-checked', 'true'); await expect(page.getByTestId('agent-diagram')).toBeVisible(); await expect( @@ -238,6 +239,8 @@ test.describe('frontend CRUD smoke flows', () => { '0.5', ); await page.getByRole('radio', { name: /^Pipeline/ }).click(); + await page.locator('#agent-create-form').getByRole('combobox').click(); + await page.getByRole('option', { name: /Local Agent/ }).click(); await expect(pipelineTypeCard).toHaveAttribute('aria-checked', 'true'); await expect(page.getByTestId('pipeline-diagram')).toBeVisible(); await expect( @@ -246,9 +249,7 @@ test.describe('frontend CRUD smoke flows', () => { await expect(page.getByTestId('agent-diagram')).toHaveCount(0); await expect(page.locator('input[name="name"]')).toBeVisible(); await page.locator('input[name="name"]').fill('Escalation Pipeline'); - await page - .locator('input[name="description"]') - .fill('Routes urgent customer issues.'); + await submit(page); await expect(page).toHaveURL(/\/home\/agents\?id=pipeline-1$/); @@ -550,10 +551,13 @@ test.describe('bot advanced flows', () => { ); await page.keyboard.press('Escape'); - await page.getByRole('button', { name: 'Refresh status' }).hover(); + await page + .getByRole('button', { name: 'Match records', exact: true }) + .click(); await expect( page.getByText('Failed to refresh route status.'), ).toBeVisible(); + await page.keyboard.press('Escape'); await page.getByRole('button', { name: 'Check route' }).click(); const routeDialog = page.getByRole('dialog'); @@ -612,7 +616,7 @@ test.describe('bot advanced flows', () => { }); const listenBox = await listenButton.boundingBox(); const saveBox = await page - .getByRole('button', { name: /^Save$/ }) + .getByRole('button', { name: /^Save(?: and enable)?$/ }) .boundingBox(); expect(listenBox).not.toBeNull(); expect(saveBox).not.toBeNull(); @@ -741,6 +745,7 @@ test.describe('bot advanced flows', () => { // Select an adapter but leave the required name empty. await selectPlaywrightAdapter(page); + await page.locator('input[name="name"]').fill(''); await submit(page); await expect(page.getByText(/cannot be empty/i)).toBeVisible(); @@ -790,6 +795,8 @@ test.describe('pipeline advanced flows', () => { // Create a pipeline await page.goto('/home/agents?id=new'); await page.getByRole('radio', { name: /^Pipeline/ }).click(); + await page.locator('#agent-create-form').getByRole('combobox').click(); + await page.getByRole('option', { name: /Local Agent/ }).click(); await page.locator('input[name="name"]').fill('Tab Test Pipeline'); await submit(page); @@ -820,6 +827,8 @@ test.describe('pipeline advanced flows', () => { // Create a pipeline await page.goto('/home/agents?id=new'); await page.getByRole('radio', { name: /^Pipeline/ }).click(); + await page.locator('#agent-create-form').getByRole('combobox').click(); + await page.getByRole('option', { name: /Local Agent/ }).click(); await page.locator('input[name="name"]').fill('Dirty Form Pipeline'); await submit(page); @@ -842,8 +851,11 @@ test.describe('pipeline advanced flows', () => { await page.goto('/home/agents?id=new'); await page.getByRole('radio', { name: /^Pipeline/ }).click(); + await page.locator('#agent-create-form').getByRole('combobox').click(); + await page.getByRole('option', { name: /Local Agent/ }).click(); - // Submit without filling name + // Clear the name suggested by the selected runner. + await page.locator('input[name="name"]').fill(''); await submit(page); // Should show validation error for name (zod validation) @@ -1415,6 +1427,8 @@ test.describe('cross-resource flows', () => { // Create a pipeline first await page.goto('/home/agents?id=new'); await page.getByRole('radio', { name: /^Pipeline/ }).click(); + await page.locator('#agent-create-form').getByRole('combobox').click(); + await page.getByRole('option', { name: /Local Agent/ }).click(); await page.locator('input[name="name"]').fill('Production Pipeline'); await submit(page); await expect(page).toHaveURL(/\/home\/agents\?id=pipeline-1$/); diff --git a/web/tests/e2e/entity-loading.spec.ts b/web/tests/e2e/entity-loading.spec.ts index f3e681200..27a7f190e 100644 --- a/web/tests/e2e/entity-loading.spec.ts +++ b/web/tests/e2e/entity-loading.spec.ts @@ -240,7 +240,7 @@ for (const scenario of cases) { await page.goto(scenario.url); const error = page .getByRole('alert') - .filter({ hasText: 'Failed to load. Please try again.' }); + .filter({ hasText: 'Unable to load this content' }); await expect(error).toBeVisible(); fail = false; await error.getByRole('button', { name: 'Retry', exact: true }).click(); diff --git a/web/tests/e2e/event-processor.spec.ts b/web/tests/e2e/event-processor.spec.ts index 9a4f6d1f1..6e095d274 100644 --- a/web/tests/e2e/event-processor.spec.ts +++ b/web/tests/e2e/event-processor.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import { installLangBotApiMocks } from './fixtures/langbot-api'; -test('create first, select a plugin in the header, debug beside scrollable logs', async ({ +test('select a plugin before creation and retain debug state while browsing run logs', async ({ page, }) => { await installLangBotApiMocks(page, { authenticated: true }); @@ -45,11 +45,7 @@ test('create first, select a plugin in the header, debug beside scrollable logs' ) { const payload = route.request().postDataJSON(); creations.push(payload); - Object.assign(processor, payload, { - component_ref: null, - config: {}, - supported_event_patterns: [], - }); + Object.assign(processor, payload); await route.fulfill({ json: { code: 0, data: { uuid: processor.uuid, kind: processor.kind } }, }); @@ -222,6 +218,8 @@ test('create first, select a plugin in the header, debug beside scrollable logs' await page .getByRole('textbox', { name: 'Name', exact: false }) .fill('Welcome processor'); + await page.locator('#agent-create-form').getByRole('combobox').click(); + await page.getByRole('option').filter({ hasText: 'Welcome' }).click(); await page.getByRole('button', { name: 'Submit', exact: true }).click(); await expect(page).toHaveURL(/id=processor-qa/); expect(creations).toHaveLength(1); @@ -229,35 +227,33 @@ test('create first, select a plugin in the header, debug beside scrollable logs' kind: 'event_processor', name: 'Welcome processor', }); - expect(creations[0]).not.toHaveProperty('component_ref'); + expect(creations[0]).toHaveProperty('component_ref', ref); expect(creations[0]).not.toHaveProperty('config'); const panel = page.getByRole('region', { name: 'Event Debug' }); - const logs = page.getByRole('region', { name: 'Plugin processor' }); + const settings = page.getByRole('region', { name: 'Plugin processor' }); + const logs = page.getByRole('region', { name: 'Run logs', exact: true }); + const configTab = page.getByRole('tab', { + name: /^Configure & debug/, + }); + const logsTab = page.getByRole('tab', { name: 'Run logs', exact: true }); await expect(panel).toBeVisible(); - await expect(logs).toBeVisible(); - await expect(logs.getByRole('tab')).toHaveCount(2); - await expect( - logs.getByRole('tab', { name: 'Configuration', exact: true }), - ).toHaveAttribute('data-state', 'active'); + await expect(settings).toBeVisible(); + await expect(configTab).toHaveAttribute('data-state', 'active'); await expect(page.getByRole('dialog')).toHaveCount(0); - await expect(panel.getByRole('button', { name: 'Run test' })).toHaveCount(0); + await expect(panel.getByRole('button', { name: 'Run test' })).toBeVisible(); await page.getByRole('combobox', { name: 'Plugin processor' }).click(); await page.getByRole('option').filter({ hasText: 'Welcome' }).click(); await expect( panel.getByRole('combobox', { name: 'Event type' }), ).toContainText('group.member_joined'); - const settings = logs.getByRole('tabpanel', { - name: 'Configuration', - exact: true, - }); await expect(settings.getByText('Greeting *', { exact: true })).toBeVisible(); await expect( page.getByRole('button', { name: 'Plugin settings', exact: true }), ).toHaveCount(0); await settings.getByRole('textbox').fill('Welcome'); - await logs.getByRole('tab', { name: 'Logs', exact: true }).click(); + await logsTab.click(); await expect(settings).toBeHidden(); - await logs.getByRole('tab', { name: 'Configuration', exact: true }).click(); + await configTab.click(); await expect(settings.getByRole('textbox')).toHaveValue('Welcome'); await panel .getByRole('textbox', { name: 'Member ID' }) @@ -265,9 +261,9 @@ test('create first, select a plugin in the header, debug beside scrollable logs' await panel.getByRole('button', { name: 'Save and run' }).click(); await expect(panel.getByText('Debug handler invoked once')).toBeVisible(); expect(operations).toEqual(['save', 'debug']); - await expect( - logs.getByRole('tab', { name: 'Logs', exact: true }), - ).toHaveAttribute('data-state', 'active'); + await expect(configTab).toHaveAttribute('data-state', 'active'); + await logsTab.click(); + await expect(logs).toBeVisible(); expect(debugRequests).toHaveLength(1); expect(debugRequests[0]).toMatchObject({ event_type: 'group.member_joined', @@ -298,10 +294,8 @@ test('create first, select a plugin in the header, debug beside scrollable logs' await logs.getByText('Final log after pagination').scrollIntoViewIfNeeded(); await expect(logs.getByText('Final log after pagination')).toBeInViewport(); expect(cursors).toContain('100'); + await configTab.click(); await expect(panel.getByText('Debug handler invoked once')).toBeVisible(); - const debugBox = await panel.boundingBox(); - const logBox = await logs.boundingBox(); - expect(debugBox!.x).toBeLessThan(logBox!.x); await page.setViewportSize({ width: 390, height: 700 }); await panel .getByRole('button', { name: 'Run test' }) @@ -309,6 +303,7 @@ test('create first, select a plugin in the header, debug beside scrollable logs' await expect( panel.getByRole('button', { name: 'Run test' }), ).toBeInViewport(); + await logsTab.click(); await logs.getByText('Final log after pagination').scrollIntoViewIfNeeded(); await expect(logs.getByText('Final log after pagination')).toBeInViewport(); }); diff --git a/web/tests/e2e/monitoring-executions.spec.ts b/web/tests/e2e/monitoring-executions.spec.ts index 11e894c98..6e253bad6 100644 --- a/web/tests/e2e/monitoring-executions.spec.ts +++ b/web/tests/e2e/monitoring-executions.spec.ts @@ -127,10 +127,17 @@ test('renders typed and custom events without duplicate text or empty attachment .filter({ hasText: 'Open this execution' }) .click(); const sheet = page.getByRole('dialog'); - await expect(sheet.getByText('你好~', { exact: true })).toHaveCount(2); + await expect( + sheet.getByText('你好~', { exact: true }).filter({ visible: true }), + ).toHaveCount(2); await expect(sheet.getByText('[Empty message]', { exact: true })).toHaveCount( 0, ); + for (const details of await sheet + .locator('summary') + .filter({ hasText: /^Details$/ }) + .all()) + await details.click(); for (const text of [ 'Agent answer', 'member-42', @@ -147,7 +154,9 @@ test('renders typed and custom events without duplicate text or empty attachment 'custom-value', 'event_reply', ]) { - await expect(sheet.getByText(text, { exact: true })).toBeVisible(); + await expect( + sheet.getByText(text, { exact: true }).filter({ visible: true }).first(), + ).toBeVisible(); } await expect( sheet.getByRole('img', { name: 'Message attachment' }), @@ -222,12 +231,16 @@ test('pages a long trace without dropping inputs and opens related executions', .filter({ hasText: 'Open this execution' }) .click(); const sheet = page.getByRole('dialog'); - await expect(sheet.getByText('Generated but not sent')).toBeVisible(); + await expect( + sheet.getByText('Generated but not sent').filter({ visible: true }), + ).toBeVisible(); await expect(sheet.getByText('Delivery records (0)')).toBeVisible(); await sheet.getByText('Execution events (1+)', { exact: true }).click(); await sheet.getByRole('button', { name: 'Load more' }).click(); await expect(sheet.getByText('run.completed', { exact: true })).toBeVisible(); - await expect(sheet.getByText('Original input')).toBeVisible(); + await expect( + sheet.getByText('Original input').filter({ visible: true }), + ).toBeVisible(); expect( requests.some( (query) => @@ -236,8 +249,12 @@ test('pages a long trace without dropping inputs and opens related executions', ).toBe(true); await sheet.getByText('Related executions (1)', { exact: true }).click(); await sheet.getByRole('button', { name: /Processor B/ }).click(); - await expect(sheet.getByText('Sibling input')).toBeVisible(); - await expect(sheet.getByText('Original input')).toHaveCount(0); + await expect( + sheet.getByText('Sibling input').filter({ visible: true }), + ).toBeVisible(); + await expect( + sheet.getByText('Original input').filter({ visible: true }), + ).toHaveCount(0); }); test('shows non-message events without fabricating a reply or a processor', async ({ @@ -298,6 +315,11 @@ test('shows non-message events without fabricating a reply or a processor', asyn await expect( sheet.getByRole('heading', { name: 'No processor run' }), ).toBeVisible(); + await sheet + .locator('summary') + .filter({ hasText: /^Details$/ }) + .first() + .click(); await expect(sheet.getByText(/member-42/)).toBeVisible(); await expect(sheet.getByText(/No matching route/)).toBeVisible(); await expect(sheet.getByText('Delivery records (0)')).toBeVisible(); @@ -326,34 +348,33 @@ test('filters all three processor kinds with the sidebar icons', async ({ route.fulfill({ json: { code: 0, data: { agents: processors } } }), ); await page.goto('/home/monitoring'); - const filter = page.getByRole('combobox', { name: 'Processor', exact: true }); + const filter = page.locator('#monitoring-filter-processor'); + await filter.click(); + const popup = page.getByRole('dialog'); for (const processor of processors) { - await filter.click(); - const option = page.getByRole('option', { + const option = popup.getByRole('checkbox', { name: processor.name, exact: true, }); await expect( - option.getByText(processor.emoji, { exact: true }), + option.locator('..').getByText(processor.emoji, { exact: true }), ).toBeVisible(); const request = page.waitForRequest((req) => { const url = new URL(req.url()); return ( url.pathname.endsWith('/monitoring/executions') && - url.searchParams.get('pipelineId') === processor.uuid + url.searchParams.getAll('pipelineId').includes(processor.uuid) ); }); - await option.click(); + await option.check(); await request; - await expect(filter).toContainText(processor.name); - await expect( - filter.getByText(processor.emoji, { exact: true }), - ).toBeVisible(); + await expect(option).toBeChecked(); } - await filter.click(); - await page - .getByRole('option', { name: 'All Processors', exact: true }) + await expect(filter).toContainText('3'); + await popup + .getByRole('button', { name: 'All Processors', exact: true }) .click(); + await expect(popup.getByRole('checkbox', { checked: true })).toHaveCount(0); await expect(filter).toContainText('All Processors'); }); @@ -423,11 +444,24 @@ test('remembers all filters on reload and persists resetting them', async ({ ]; for (const [id, label] of selections) { await page.locator(`#monitoring-filter-${id}`).click(); - await page.getByRole('option', { name: label, exact: true }).click(); + if (id === 'processor') { + await page.getByRole('checkbox', { name: label, exact: true }).check(); + await page.keyboard.press('Escape'); + } else await page.getByRole('option', { name: label, exact: true }).click(); } await page.reload(); - for (const [id, label] of selections) - await expect(page.locator(`#monitoring-filter-${id}`)).toContainText(label); + for (const [id, label] of selections) { + if (id === 'processor') { + await page.locator('#monitoring-filter-processor').click(); + await expect( + page.getByRole('checkbox', { name: label, exact: true }), + ).toBeChecked(); + await page.keyboard.press('Escape'); + } else + await expect(page.locator(`#monitoring-filter-${id}`)).toContainText( + label, + ); + } await page .getByRole('button', { name: 'Reset Filters', exact: true }) .click(); diff --git a/web/tests/e2e/monitoring-turns.spec.ts b/web/tests/e2e/monitoring-turns.spec.ts index a9e92b97e..f81b69d6a 100644 --- a/web/tests/e2e/monitoring-turns.spec.ts +++ b/web/tests/e2e/monitoring-turns.spec.ts @@ -336,7 +336,7 @@ test.describe('monitoring request contracts', () => { }); await page.goto('/home/monitoring'); await expect.poll(() => pending.length).toBe(2); - await page.getByRole('combobox').last().click(); + await page.locator('#monitoring-filter-time').click(); await page.getByRole('option', { name: /Last 7 days/i }).click(); await expect.poll(() => pending.length).toBe(3); await respond(pending[2], 'Latest filter data'); @@ -362,7 +362,7 @@ test.describe('monitoring request contracts', () => { await expect( page.getByText('Obsolete filter data', { exact: true }), ).toHaveCount(0); - await page.getByRole('combobox').last().click(); + await page.locator('#monitoring-filter-time').click(); await page.getByRole('option', { name: /Last 24 hours/i }).click(); await expect.poll(() => pending.length).toBe(5); await respond(pending[4], 'Current result'); @@ -636,9 +636,15 @@ test.describe('monitoring conversation turn grouping', () => { await expect( sheet.locator('span.break-all').filter({ hasText: /^repo_search$/ }), ).toBeVisible(); - await sheet.getByText('Errors (1)', { exact: true }).click(); - await expect(sheet.getByText('Tool retry failed').first()).toBeVisible(); + await expect( + sheet.getByText('Errors (1)', { exact: true }).locator('..'), + ).toHaveAttribute('open', ''); + await expect( + sheet.getByText('Tool retry failed').filter({ visible: true }).first(), + ).toBeVisible(); await sheet.getByText('Conversation context (6)', { exact: true }).click(); - await expect(sheet.getByText('Rollback plan ready')).toBeVisible(); + await expect( + sheet.getByText('Rollback plan ready').filter({ visible: true }), + ).toBeVisible(); }); }); diff --git a/web/tests/e2e/owner-space-billing.spec.ts b/web/tests/e2e/owner-space-billing.spec.ts index 0c5f7c5e5..4db78e3f6 100644 --- a/web/tests/e2e/owner-space-billing.spec.ts +++ b/web/tests/e2e/owner-space-billing.spec.ts @@ -56,8 +56,9 @@ test('an OSS local-only owner is prompted to bind before using LangBot Models', await page.goto('/home?action=showModelSettings'); await expect( - page.getByRole('button', { - name: 'The Workspace owner must connect a LangBot Account for LangBot Models.', - }), + page.getByText( + 'The Workspace owner must connect a LangBot Account for LangBot Models.', + { exact: true }, + ), ).toBeVisible(); }); diff --git a/web/tests/e2e/pipeline-monitoring-turns.spec.ts b/web/tests/e2e/pipeline-monitoring-turns.spec.ts index 306398c62..2ebb71b76 100644 --- a/web/tests/e2e/pipeline-monitoring-turns.spec.ts +++ b/web/tests/e2e/pipeline-monitoring-turns.spec.ts @@ -169,27 +169,36 @@ test.describe('pipeline monitoring conversation turns', () => { await expect( page.getByText('Pipeline single user message without reply'), ).toBeVisible(); - await expect( - page.getByText('Pipeline needs a deployment plan'), - ).toBeVisible(); - await expect( - page.getByText('Pipeline agent step 1: inspect repository'), - ).toBeVisible(); - await expect(page.getByText('Assistant +2')).toBeVisible(); - await expect(page.getByText('2 tools')).toBeVisible(); - - const agentTurn = page - .locator('div[role="button"]') - .filter({ hasText: 'Pipeline needs a deployment plan' }); - await expect(agentTurn).toHaveCount(1); + const agentTurn = page.getByRole('button', { + name: /Pipeline Bot Success Pipeline needs a deployment plan/, + }); await agentTurn.click(); - - await expect(page.getByText('Tool Calls (2)')).toBeVisible(); - await expect(page.getByText('#1 repo_search')).toBeVisible(); - await expect(page.getByText('#2 run_tests')).toBeVisible(); - await expect(page.getByText('Arguments')).toHaveCount(0); - await page.getByText('#1 repo_search').click(); - await expect(page.getByText('Arguments')).toBeVisible(); - await expect(page.getByText('Result')).toBeVisible(); + const detail = page.getByRole('article'); + await expect( + detail.getByText('Pipeline needs a deployment plan', { exact: true }), + ).toBeVisible(); + for (const text of [ + 'Pipeline agent step 1: inspect repository', + 'Pipeline agent step 2: run tests', + 'Pipeline final answer: deployment ready', + ]) + await expect(detail.getByText(text, { exact: true })).toBeVisible(); + await expect( + detail.getByText('Tool Calls 2', { exact: true }), + ).toBeVisible(); + const search = detail + .getByRole('listitem') + .filter({ hasText: 'repo_search' }); + await expect( + search.getByText('repo_search', { exact: true }), + ).toBeVisible(); + await expect(detail.getByText('run_tests', { exact: true })).toBeVisible(); + await expect(search.getByText(/"query"/)).toHaveCount(0); + await search + .getByRole('button', { name: 'Arguments', exact: true }) + .click(); + await expect(search.getByText(/"query"/)).toBeVisible(); + await search.getByRole('button', { name: 'Result', exact: true }).click(); + await expect(search.getByText(/"ok"/)).toBeVisible(); }); }); diff --git a/web/tests/e2e/plugin-subscriptions.spec.ts b/web/tests/e2e/plugin-subscriptions.spec.ts index 61a1bdb46..3992c69cd 100644 --- a/web/tests/e2e/plugin-subscriptions.spec.ts +++ b/web/tests/e2e/plugin-subscriptions.spec.ts @@ -84,9 +84,11 @@ test('creates a configured processor, persists subscriptions separately and reus const request = page.waitForRequest( (r) => r.method() === 'PUT' && r.url().endsWith('/platform/bots/bot-1'), ); - await page.getByRole('button', { name: /^Save$/ }).click(); + await page.getByRole('button', { name: /^Save(?: and enable)?$/ }).click(); const body = (await request).postDataJSON(); - await expect(page.getByRole('button', { name: /^Save$/ })).toBeDisabled(); + await expect( + page.getByRole('button', { name: /^Save(?: and enable)?$/ }), + ).toBeDisabled(); // Move away so hovering the next control does not pause an overlapping toast. await page.mouse.move(0, 0); await expect( diff --git a/web/tests/e2e/workspace-assistant.spec.ts b/web/tests/e2e/workspace-assistant.spec.ts index e90f6d6b8..1f0a4edcc 100644 --- a/web/tests/e2e/workspace-assistant.spec.ts +++ b/web/tests/e2e/workspace-assistant.spec.ts @@ -232,7 +232,7 @@ test('sending survives model labels appearing and disappearing in stream snapsho await page.goto('/home/bots'); const assistant = page.getByRole('dialog', { name: 'Workspace assistant' }); const model = assistant.getByRole('combobox', { name: 'Select Model' }); - await expect(model).toContainText('Recommended model'); + await expect(model).toContainText('Valid Mock Model'); for (let i = 1; i <= 3; i++) { await assistant.getByRole('textbox').fill(`Hello ${i}`); await assistant.getByRole('button', { name: 'Send', exact: true }).click(); @@ -246,7 +246,7 @@ test('sending survives model labels appearing and disappearing in stream snapsho expect(errors).toEqual([]); }); -test('selects the recommendation on entry and submits the visible model', async ({ +test('selects the available local model on entry and submits the visible model', async ({ page, }) => { await installLangBotApiMocks(page, { @@ -290,7 +290,7 @@ test('selects the recommendation on entry and submits the visible model', async const assistant = page.getByRole('dialog', { name: 'Workspace assistant' }); await expect( assistant.getByRole('combobox', { name: 'Select Model' }), - ).toContainText('Recommended model'); + ).toContainText('Valid Mock Model'); await assistant.getByRole('textbox').fill('Hello'); await assistant.getByRole('button', { name: 'Send', exact: true }).click(); await expect( @@ -460,7 +460,7 @@ test('renders streaming text and tool progress before completion', async ({ const assistant = page.getByRole('dialog', { name: 'Workspace assistant' }); await expect( assistant.getByRole('combobox', { name: 'Select Model' }), - ).toContainText('Recommended model'); + ).toContainText('Valid Mock Model'); await assistant.getByRole('textbox').fill('Inspect resources'); await assistant.getByRole('button', { name: 'Send', exact: true }).click(); await expect( @@ -492,7 +492,7 @@ test('renders streaming text and tool progress before completion', async ({ } }); -test('without providers shows setup only and rechecks after model settings close', async ({ +test('without usable models shows setup only and rechecks after model settings close', async ({ page, }) => { await installLangBotApiMocks(page, { @@ -504,6 +504,10 @@ test('without providers shows setup only and rechecks after model settings close ); let configured = false; let recommendations = 0; + await page.route('**/api/v1/provider/models/llm', (route) => { + if (configured) return route.fallback(); + return route.fulfill({ json: { code: 0, data: { models: [] } } }); + }); await page.route('**/api/v1/provider/providers', (route) => route.fulfill({ json: { @@ -530,25 +534,25 @@ test('without providers shows setup only and rechecks after model settings close }); }); await page.goto('/home/bots'); - const setup = page.getByRole('complementary', { + const setup = page.getByRole('dialog', { name: 'Workspace assistant', }); await expect( setup.getByRole('button', { name: 'Sign in to LangBot Account' }), ).toBeVisible(); - await expect( - page.getByRole('dialog', { name: 'Workspace assistant' }), - ).toHaveCount(0); + await expect(setup.getByRole('textbox')).toHaveCount(0); expect(recommendations).toBe(0); await setup.getByRole('button', { name: 'Configure models' }).click(); - await expect(page.getByRole('dialog')).toBeVisible(); + await expect( + page.getByRole('dialog', { name: 'Settings', exact: true }), + ).toBeVisible(); configured = true; await page.keyboard.press('Escape'); await expect( page .getByRole('dialog', { name: 'Workspace assistant' }) .getByRole('combobox', { name: 'Select Model' }), - ).toContainText('Recommended model'); + ).toContainText('Valid Mock Model'); }); test('setup login starts the existing LangBot Account authorization flow', async ({ @@ -563,13 +567,16 @@ test('setup login starts the existing LangBot Account authorization flow', async json: { code: 0, data: { providers: [] } }, }), ); + await page.route('**/api/v1/provider/models/llm', (route) => + route.fulfill({ json: { code: 0, data: { models: [] } } }), + ); await page.goto('/home/bots'); const request = page.waitForRequest( (request) => request.url().includes('/space/') && request.url().includes('authorize'), ); await page - .getByRole('complementary', { name: 'Workspace assistant' }) + .getByRole('dialog', { name: 'Workspace assistant' }) .getByRole('button', { name: 'Sign in to LangBot Account' }) .click(); expect((await request).url()).toContain('authorize');