mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-17 11:14:19 +00:00
b3c6de2072
* 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
456 lines
16 KiB
TypeScript
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();
|
|
});
|
|
});
|