Files
LangBot/web/tests/e2e/crud-smoke.spec.ts
T
huanghuoguoguo b3c6de2072 [codex] cover frontend CRUD smoke flows (#2253)
* test: cover frontend CRUD smoke flows

* test: add bot CRUD smoke coverage

* test: add bot/pipeline advanced flows and cross-resource tests

- Bot enable/disable toggle with state persistence
- Bot detail tab switching (Configuration, Logs, Sessions)
- Bot form dirty state and save button behavior
- Bot name validation error display
- Pipeline tab switching (Configuration, Dashboard)
- Pipeline form dirty state
- Pipeline name validation error display
- Cross-resource flow: create pipeline then bind to bot
- Empty states for bots, pipelines, knowledge bases, MCP servers
2026-06-16 21:34:17 +08:00

456 lines
16 KiB
TypeScript

import { expect, Page, test } from '@playwright/test';
import { installLangBotApiMocks } from './fixtures/langbot-api';
async function save(page: Page) {
const button = page.getByRole('button', { name: /^Save$/ });
await expect(button).toBeEnabled();
await button.click();
}
async function submit(page: Page) {
await page.getByRole('button', { name: /^Submit$/ }).click();
}
async function confirmDelete(page: Page) {
await page
.getByRole('dialog')
.getByRole('button', { name: /^Confirm Delete$/ })
.click();
}
test.describe('frontend CRUD smoke flows', () => {
test('creates, edits, and deletes a bot', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/bots?id=new');
await expect(page.locator('input[name="name"]')).toBeVisible();
await page.locator('input[name="name"]').fill('Support Bot');
await page
.locator('input[name="description"]')
.fill('Answers customer support questions.');
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
await submit(page);
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
await page.reload();
await expect(page.locator('input[name="name"]')).toHaveValue('Support Bot');
await page
.locator('input[name="description"]')
.fill('Answers customer support questions with context.');
await save(page);
await expect(page.locator('input[name="description"]')).toHaveValue(
'Answers customer support questions with context.',
);
await page.getByRole('button', { name: /^Delete$/ }).click();
await confirmDelete(page);
await expect(page).toHaveURL(/\/home\/bots$/);
await expect(page.getByText('Select a bot from the sidebar')).toBeVisible();
});
test('creates, edits, and deletes a pipeline', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/pipelines?id=new');
await expect(page.locator('input[name="basic.name"]')).toBeVisible();
await page.locator('input[name="basic.name"]').fill('Escalation Pipeline');
await page
.locator('input[name="basic.description"]')
.fill('Routes urgent customer issues.');
await submit(page);
await expect(page).toHaveURL(/\/home\/pipelines\?id=pipeline-1$/);
await page.reload();
await expect(page.locator('input[name="basic.name"]')).toHaveValue(
'Escalation Pipeline',
);
await page
.locator('input[name="basic.description"]')
.fill('Routes urgent customer issues to operators.');
await save(page);
await expect(page.locator('input[name="basic.description"]')).toHaveValue(
'Routes urgent customer issues to operators.',
);
await page.getByRole('button', { name: /^Delete$/ }).click();
await confirmDelete(page);
await expect(page).toHaveURL(/\/home\/pipelines$/);
await expect(
page.getByText('Select a pipeline from the sidebar'),
).toBeVisible();
});
test('creates, edits, and deletes a knowledge base', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/knowledge?id=new');
await expect(page.locator('input[name="name"]')).toBeVisible();
await page.locator('input[name="name"]').fill('Support Knowledge');
await page
.locator('input[name="description"]')
.fill('Source material for support answers.');
await submit(page);
await expect(page).toHaveURL(/\/home\/knowledge\?id=knowledge-1$/);
await page.reload();
await expect(page.locator('input[name="name"]')).toHaveValue(
'Support Knowledge',
);
await page.waitForTimeout(600);
await page
.locator('input[name="description"]')
.fill('Updated source material for support answers.');
await save(page);
await expect(page.locator('input[name="description"]')).toHaveValue(
'Updated source material for support answers.',
);
await page.getByRole('button', { name: /^Delete$/ }).click();
await confirmDelete(page);
await expect(page).toHaveURL(/\/home\/knowledge$/);
await expect(
page.getByText('Select a knowledge base from the sidebar'),
).toBeVisible();
});
test('creates, edits, and deletes an MCP server', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/mcp?id=new');
await expect(page.locator('input[name="name"]')).toBeVisible();
await page.locator('input[name="name"]').fill('playwright-mcp');
await page
.locator('input[name="url"]')
.fill('https://mcp.example.test/sse');
await submit(page);
await expect(page).toHaveURL(/\/home\/mcp\?id=playwright-mcp$/);
await page.reload();
await expect(page.locator('input[name="name"]')).toHaveValue(
'playwright-mcp',
);
await page
.locator('input[name="url"]')
.fill('https://mcp.example.test/updated-sse');
await save(page);
await expect(page.locator('input[name="url"]')).toHaveValue(
'https://mcp.example.test/updated-sse',
);
await page.getByRole('button', { name: /^Delete$/ }).click();
await confirmDelete(page);
await expect(page).toHaveURL(/\/home\/mcp$/);
await expect(
page.getByText('Select an MCP server from the sidebar'),
).toBeVisible();
});
test('updates and deletes a manually-created skill', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/skills?action=create');
await page.locator('#display_name').fill('Release Notes');
await page.locator('#name').fill('release_notes');
await page.locator('#description').fill('Drafts release notes.');
await page
.locator('#instructions')
.fill('Summarize merged changes for the next release.');
await save(page);
await expect(page).toHaveURL(/\/home\/skills\?id=release_notes$/);
await page.reload();
await expect(page.locator('#description')).toHaveValue(
'Drafts release notes.',
);
await page
.locator('#description')
.fill('Drafts concise release notes for maintainers.');
await expect(page.locator('#description')).toHaveValue(
'Drafts concise release notes for maintainers.',
);
await save(page);
await page.reload();
await expect(page.locator('#description')).toHaveValue(
'Drafts concise release notes for maintainers.',
);
await expect(page.locator('#instructions')).toHaveValue(
'Summarize merged changes for the next release.',
);
await page.getByRole('button', { name: /^Delete$/ }).click();
await confirmDelete(page);
await expect(page).toHaveURL(/\/home\/add-extension$/);
});
});
test.describe('bot advanced flows', () => {
test('toggles bot enable/disable state', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
// Create a bot first
await page.goto('/home/bots?id=new');
await page.locator('input[name="name"]').fill('Toggle Test Bot');
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
await submit(page);
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
// Wait for the enable switch to load (it's fetched via getBot)
await expect(page.locator('#bot-enable-switch')).toBeVisible({
timeout: 5000,
});
// Verify initial state is enabled
await expect(page.locator('#bot-enable-switch')).toBeChecked();
// Toggle to disabled
await page.locator('#bot-enable-switch').click();
await expect(page.locator('#bot-enable-switch')).not.toBeChecked();
// Reload and verify state persisted
await page.reload();
await expect(page.locator('#bot-enable-switch')).not.toBeChecked();
});
test('switches between bot detail tabs', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
// Create a bot
await page.goto('/home/bots?id=new');
await page.locator('input[name="name"]').fill('Tab Test Bot');
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
await submit(page);
// Verify we're on the Configuration tab
await expect(
page.getByRole('tab', { name: /Configuration/ }),
).toHaveAttribute('data-state', 'active');
await expect(page.locator('input[name="name"]')).toBeVisible();
// Switch to Logs tab
await page.getByRole('tab', { name: /Logs/ }).click();
await expect(page.getByRole('tab', { name: /Logs/ })).toHaveAttribute(
'data-state',
'active',
);
// Switch to Sessions tab
await page.getByRole('tab', { name: /Sessions/ }).click();
await expect(page.getByRole('tab', { name: /Sessions/ })).toHaveAttribute(
'data-state',
'active',
);
// Switch back to Configuration
await page.getByRole('tab', { name: /Configuration/ }).click();
await expect(page.locator('input[name="name"]')).toBeVisible();
});
test('save button is disabled when form is clean', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
// Create a bot
await page.goto('/home/bots?id=new');
await page.locator('input[name="name"]').fill('Clean Form Bot');
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
await submit(page);
// After creation, save button should be disabled (form is clean)
const saveButton = page.getByRole('button', { name: /^Save$/ });
await expect(saveButton).toBeDisabled();
// Edit the form
await page.locator('input[name="description"]').fill('New description');
await expect(saveButton).toBeEnabled();
// Save
await saveButton.click();
await expect(saveButton).toBeDisabled();
});
test('shows validation error when bot name is empty', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/bots?id=new');
// Select adapter but leave name empty
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
await submit(page);
// Should show validation error for name (zod validation)
await expect(page.getByText(/cannot be empty/i)).toBeVisible();
await expect(page).toHaveURL(/\/home\/bots\?id=new$/);
});
});
test.describe('pipeline advanced flows', () => {
test('switches to monitoring tab from pipeline detail', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
// Create a pipeline
await page.goto('/home/pipelines?id=new');
await page.locator('input[name="basic.name"]').fill('Tab Test Pipeline');
await submit(page);
// Verify we're on the Configuration tab
await expect(
page.getByRole('tab', { name: /Configuration/ }),
).toHaveAttribute('data-state', 'active');
// Switch to Monitoring tab (labeled "Dashboard" in the pipeline context)
// Skip Debug tab as it requires WebSocket connection
await page.getByRole('tab', { name: /Dashboard/ }).click();
await expect(page.getByRole('tab', { name: /Dashboard/ })).toHaveAttribute(
'data-state',
'active',
);
// Switch back to Configuration
await page.getByRole('tab', { name: /Configuration/ }).click();
await expect(page.locator('input[name="basic.name"]')).toBeVisible();
});
test('save button reflects form dirty state', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
// Create a pipeline
await page.goto('/home/pipelines?id=new');
await page.locator('input[name="basic.name"]').fill('Dirty Form Pipeline');
await submit(page);
// Wait for the page to fully load and form to reset
await page.waitForTimeout(500);
// Edit the form - use the name field which definitely triggers dirty state
await page
.locator('input[name="basic.name"]')
.fill('Dirty Form Pipeline Updated');
const saveButton = page.getByRole('button', { name: /^Save$/ });
await expect(saveButton).toBeEnabled();
// Save
await saveButton.click();
// Wait for save to complete
await page.waitForTimeout(500);
});
test('shows validation error when pipeline name is empty', async ({
page,
}) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/pipelines?id=new');
// Submit without filling name
await submit(page);
// Should show validation error for name (zod validation)
await expect(page.getByText(/cannot be empty/i)).toBeVisible();
await expect(page).toHaveURL(/\/home\/pipelines\?id=new$/);
});
});
test.describe('cross-resource flows', () => {
test('creates a pipeline then binds it to a bot', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
// Create a pipeline first
await page.goto('/home/pipelines?id=new');
await page.locator('input[name="basic.name"]').fill('Production Pipeline');
await submit(page);
await expect(page).toHaveURL(/\/home\/pipelines\?id=pipeline-1$/);
// Create a bot
await page.goto('/home/bots?id=new');
await page.locator('input[name="name"]').fill('Bound Bot');
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
await submit(page);
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
// Wait for form to fully load
await expect(page.locator('input[name="name"]')).toHaveValue('Bound Bot');
// Find the pipeline select by its label "Bind Pipeline"
const pipelineCard = page.getByText('Bind Pipeline').locator('..');
await expect(pipelineCard).toBeVisible({ timeout: 5000 });
// Click on the select trigger within the pipeline binding card
// The select trigger shows "Select Pipeline" placeholder initially
const pipelineSelectTrigger = page.getByText('Select Pipeline').first();
await pipelineSelectTrigger.click();
// Select the pipeline option
await page.getByRole('option', { name: 'Production Pipeline' }).click();
// Save the bot
await save(page);
// Reload and verify binding persisted
await page.reload();
// The pipeline name should appear in the select trigger (not in sidebar or options)
await expect(
page
.locator('[data-slot="select-trigger"]')
.filter({ hasText: 'Production Pipeline' }),
).toBeVisible();
});
});
test.describe('empty states', () => {
test('shows empty state when no bots exist', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/bots');
await expect(page.getByText('Select a bot from the sidebar')).toBeVisible();
});
test('shows empty state when no pipelines exist', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/pipelines');
await expect(
page.getByText('Select a pipeline from the sidebar'),
).toBeVisible();
});
test('shows empty state when no knowledge bases exist', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/knowledge');
await expect(
page.getByText('Select a knowledge base from the sidebar'),
).toBeVisible();
});
test('shows empty state when no MCP servers exist', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/mcp');
await expect(
page.getByText('Select an MCP server from the sidebar'),
).toBeVisible();
});
});