368 lines
14 KiB
TypeScript
368 lines
14 KiB
TypeScript
import { expect, test } from '../../core/fixtures/base.fixture'
|
|
import { createPluginData } from './plugins.data'
|
|
import { ensureTestPluginExists } from './plugins-test-helper'
|
|
|
|
// Track created plugins for cleanup
|
|
const createdPlugins: string[] = []
|
|
|
|
test.describe('Plugins', () => {
|
|
test.beforeEach(async ({ pluginsPage }) => {
|
|
await pluginsPage.goto()
|
|
// Ensure sheet is closed before each test (in case it was left open)
|
|
await pluginsPage.ensureSheetClosed()
|
|
})
|
|
|
|
test.afterEach(async ({ pluginsPage }) => {
|
|
// Clean up any plugins created during tests
|
|
for (const pluginName of [...createdPlugins]) {
|
|
try {
|
|
const exists = await pluginsPage.pluginExists(pluginName)
|
|
if (exists) {
|
|
await pluginsPage.deletePlugin(pluginName)
|
|
}
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
// Clear the array
|
|
createdPlugins.length = 0
|
|
})
|
|
|
|
test.describe('Plugin Display', () => {
|
|
test('should display plugins table', async ({ pluginsPage }) => {
|
|
// Ensure at least one plugin exists so the table (sidebar) is shown
|
|
const count = await pluginsPage.getPluginCount()
|
|
if (count === 0) {
|
|
const pluginData = createPluginData({ name: `e2e-display-table-${Date.now()}` })
|
|
const created = await pluginsPage.createPlugin(pluginData)
|
|
if (!created) {
|
|
test.skip(true, 'Backend rejected plugin creation (failed to load .so)')
|
|
return
|
|
}
|
|
createdPlugins.push(pluginData.name)
|
|
}
|
|
// Plugins page has a sidebar (aliased as table), not a traditional table
|
|
await expect(pluginsPage.table).toBeVisible()
|
|
})
|
|
|
|
test('should display create plugin button', async ({ pluginsPage }) => {
|
|
await expect(pluginsPage.createBtn).toBeVisible()
|
|
})
|
|
|
|
test('should show empty state or plugin list', async ({ pluginsPage }) => {
|
|
const count = await pluginsPage.getPluginCount()
|
|
const emptyState = pluginsPage.page.getByTestId('plugins-empty-state')
|
|
const isEmptyStateVisible = await emptyState.isVisible().catch(() => false)
|
|
|
|
if (count === 0) {
|
|
expect(isEmptyStateVisible).toBe(true)
|
|
} else {
|
|
expect(count).toBeGreaterThan(0)
|
|
expect(isEmptyStateVisible).toBe(false)
|
|
}
|
|
})
|
|
})
|
|
|
|
test.describe('CRUD Operations', () => {
|
|
test('should create a basic plugin', async ({ pluginsPage }) => {
|
|
const pluginData = createPluginData({
|
|
name: `e2e-test-plugin-${Date.now()}`,
|
|
})
|
|
createdPlugins.push(pluginData.name) // Track for cleanup
|
|
|
|
const created = await pluginsPage.createPlugin(pluginData)
|
|
if (!created) {
|
|
test.skip(true, 'Backend rejected plugin creation (failed to load .so)')
|
|
return
|
|
}
|
|
|
|
const pluginExists = await pluginsPage.pluginExists(pluginData.name)
|
|
expect(pluginExists).toBe(true)
|
|
})
|
|
|
|
test('should create a disabled plugin', async ({ pluginsPage }) => {
|
|
const pluginData = createPluginData({
|
|
name: `disabled-plugin-${Date.now()}`,
|
|
enabled: false,
|
|
})
|
|
createdPlugins.push(pluginData.name) // Track for cleanup
|
|
|
|
const created = await pluginsPage.createPlugin(pluginData)
|
|
if (!created) {
|
|
test.skip(true, 'Backend rejected plugin creation (failed to load .so)')
|
|
return
|
|
}
|
|
|
|
const pluginExists = await pluginsPage.pluginExists(pluginData.name)
|
|
expect(pluginExists).toBe(true)
|
|
|
|
// Note: Plugins are created with enabled=true by default
|
|
// We need to disable it after creation
|
|
const initialState = await pluginsPage.getPluginEnabledState(pluginData.name)
|
|
expect(initialState).toBe(true) // Created enabled by default
|
|
|
|
// Now disable it
|
|
await pluginsPage.togglePluginEnabled(pluginData.name)
|
|
const isEnabled = await pluginsPage.getPluginEnabledState(pluginData.name)
|
|
expect(isEnabled).toBe(false)
|
|
})
|
|
|
|
test('should toggle plugin enabled state', async ({ pluginsPage }) => {
|
|
const originalName = `edit-test-plugin-${Date.now()}`
|
|
const pluginData = createPluginData({ name: originalName })
|
|
createdPlugins.push(originalName) // Track for cleanup
|
|
|
|
const created = await pluginsPage.createPlugin(pluginData)
|
|
if (!created) {
|
|
test.skip(true, 'Backend rejected plugin creation (failed to load .so)')
|
|
return
|
|
}
|
|
|
|
// Verify plugin exists; we verify editability via enabled-state toggling (name is read-only after creation)
|
|
const pluginExists = await pluginsPage.pluginExists(originalName)
|
|
expect(pluginExists).toBe(true)
|
|
|
|
// Toggle enabled state to verify the plugin is editable
|
|
const initialState = await pluginsPage.getPluginEnabledState(originalName)
|
|
await pluginsPage.togglePluginEnabled(originalName)
|
|
const newState = await pluginsPage.getPluginEnabledState(originalName)
|
|
expect(newState).not.toBe(initialState)
|
|
})
|
|
|
|
test('should delete plugin', async ({ pluginsPage }) => {
|
|
const pluginData = createPluginData({
|
|
name: `delete-test-plugin-${Date.now()}`,
|
|
})
|
|
createdPlugins.push(pluginData.name) // Track for cleanup (in case test fails before delete)
|
|
|
|
const created = await pluginsPage.createPlugin(pluginData)
|
|
if (!created) {
|
|
test.skip(true, 'Backend rejected plugin creation (failed to load .so)')
|
|
return
|
|
}
|
|
|
|
// Verify it exists
|
|
let pluginExists = await pluginsPage.pluginExists(pluginData.name)
|
|
expect(pluginExists).toBe(true)
|
|
|
|
// Delete it
|
|
await pluginsPage.deletePlugin(pluginData.name)
|
|
|
|
// Verify it's gone
|
|
pluginExists = await pluginsPage.pluginExists(pluginData.name)
|
|
expect(pluginExists).toBe(false)
|
|
})
|
|
|
|
test('should change plugin enabled state when toggled', async ({ pluginsPage }) => {
|
|
const pluginData = createPluginData({
|
|
name: `toggle-test-plugin-${Date.now()}`,
|
|
enabled: true,
|
|
})
|
|
createdPlugins.push(pluginData.name) // Track for cleanup
|
|
|
|
const created = await pluginsPage.createPlugin(pluginData)
|
|
if (!created) {
|
|
test.skip(true, 'Backend rejected plugin creation (failed to load .so)')
|
|
return
|
|
}
|
|
|
|
// Get initial state
|
|
const initialState = await pluginsPage.getPluginEnabledState(pluginData.name)
|
|
|
|
// Toggle state
|
|
await pluginsPage.togglePluginEnabled(pluginData.name)
|
|
|
|
// Verify state changed
|
|
const newState = await pluginsPage.getPluginEnabledState(pluginData.name)
|
|
expect(newState).not.toBe(initialState)
|
|
})
|
|
})
|
|
|
|
test.describe('Form Validation', () => {
|
|
test('should require name for plugin', async ({ pluginsPage }) => {
|
|
await pluginsPage.dismissToasts()
|
|
await pluginsPage.createBtn.click()
|
|
await expect(pluginsPage.sheet).toBeVisible()
|
|
await pluginsPage.waitForSheetAnimation()
|
|
|
|
// Save button should be disabled when name is empty
|
|
await expect(pluginsPage.saveBtn).toBeDisabled()
|
|
|
|
await pluginsPage.cancelPlugin()
|
|
})
|
|
|
|
test('should cancel plugin creation', async ({ pluginsPage }) => {
|
|
await pluginsPage.createBtn.click()
|
|
await expect(pluginsPage.sheet).toBeVisible()
|
|
|
|
// Fill some data (scope to sheet to avoid matching background form)
|
|
const testName = `cancelled-plugin-${Date.now()}`
|
|
const sheetNameInput = pluginsPage.sheet.getByRole('textbox', { name: /Plugin Name/i })
|
|
await sheetNameInput.fill(testName)
|
|
|
|
// Cancel
|
|
await pluginsPage.cancelPlugin()
|
|
|
|
// Sheet should close
|
|
await expect(pluginsPage.sheet).not.toBeVisible()
|
|
|
|
// Plugin should not exist
|
|
const pluginExists = await pluginsPage.pluginExists(testName)
|
|
expect(pluginExists).toBe(false)
|
|
})
|
|
|
|
test('should open and close plugin sheet', async ({ pluginsPage }) => {
|
|
// Open sheet
|
|
await pluginsPage.createBtn.click()
|
|
await expect(pluginsPage.sheet).toBeVisible()
|
|
|
|
// Close sheet
|
|
await pluginsPage.cancelPlugin()
|
|
await expect(pluginsPage.sheet).not.toBeVisible()
|
|
})
|
|
})
|
|
|
|
test.describe('Error Handling', () => {
|
|
test('should handle duplicate plugin name gracefully', async ({ pluginsPage }) => {
|
|
const pluginName = `duplicate-test-${Date.now()}`
|
|
const pluginData = createPluginData({ name: pluginName })
|
|
createdPlugins.push(pluginName) // Track for cleanup
|
|
|
|
// Create first plugin
|
|
const created = await pluginsPage.createPlugin(pluginData)
|
|
if (!created) {
|
|
test.skip(true, 'Backend rejected plugin creation (failed to load .so)')
|
|
return
|
|
}
|
|
expect(await pluginsPage.pluginExists(pluginName)).toBe(true)
|
|
|
|
// Try to create duplicate
|
|
await pluginsPage.dismissToasts()
|
|
await pluginsPage.createBtn.click()
|
|
await expect(pluginsPage.sheet).toBeVisible()
|
|
await pluginsPage.waitForSheetAnimation()
|
|
// Scope to sheet to avoid matching background form
|
|
const sheetNameInput = pluginsPage.sheet.getByRole('textbox', { name: /Plugin Name/i })
|
|
const sheetPathInput = pluginsPage.sheet.getByRole('textbox', { name: /Plugin Path/i })
|
|
await sheetNameInput.fill(pluginName)
|
|
await sheetPathInput.fill(ensureTestPluginExists()) // Path is required; use same path as build
|
|
await pluginsPage.saveBtn.click()
|
|
|
|
// Duplicate creation should be rejected: either sheet stays open with error OR error toast appears
|
|
const inlineError = pluginsPage.sheet.locator('[role="alert"], .text-destructive').first()
|
|
const errorToast = pluginsPage.page.locator('[data-sonner-toast][data-type="error"]').first()
|
|
await inlineError.or(errorToast).waitFor({ state: 'visible', timeout: 10000 })
|
|
const hasInlineError = await inlineError.isVisible().catch(() => false)
|
|
|
|
if (hasInlineError) {
|
|
await expect(pluginsPage.sheet).toBeVisible()
|
|
await expect(inlineError).toBeVisible()
|
|
await pluginsPage.cancelPlugin()
|
|
} else {
|
|
await expect(errorToast).toBeVisible()
|
|
}
|
|
})
|
|
})
|
|
|
|
test.describe('Plugin Configuration', () => {
|
|
test('should view plugin details', async ({ pluginsPage }) => {
|
|
const pluginData = createPluginData({
|
|
name: `view-details-${Date.now()}`,
|
|
})
|
|
createdPlugins.push(pluginData.name)
|
|
|
|
const created = await pluginsPage.createPlugin(pluginData)
|
|
if (!created) {
|
|
test.skip(true, 'Backend rejected plugin creation (failed to load .so)')
|
|
return
|
|
}
|
|
|
|
// Click on the plugin to view details
|
|
const pluginItem = pluginsPage.page.locator('button').filter({ hasText: pluginData.name })
|
|
await pluginItem.click()
|
|
|
|
// Should see plugin details/form
|
|
const detailsVisible = await pluginsPage.page.locator('form, [role="tabpanel"]').isVisible().catch(() => false)
|
|
expect(detailsVisible).toBe(true)
|
|
})
|
|
|
|
test('should edit plugin configuration', async ({ pluginsPage }) => {
|
|
const pluginData = createPluginData({
|
|
name: `edit-config-${Date.now()}`,
|
|
})
|
|
createdPlugins.push(pluginData.name)
|
|
|
|
const created = await pluginsPage.createPlugin(pluginData)
|
|
if (!created) {
|
|
test.skip(true, 'Backend rejected plugin creation (failed to load .so)')
|
|
return
|
|
}
|
|
|
|
// Get initial enabled state
|
|
const initialState = await pluginsPage.getPluginEnabledState(pluginData.name)
|
|
|
|
// Toggle enabled state (a form of editing)
|
|
await pluginsPage.togglePluginEnabled(pluginData.name)
|
|
|
|
// State should have changed
|
|
const newState = await pluginsPage.getPluginEnabledState(pluginData.name)
|
|
expect(newState).toBe(!initialState)
|
|
})
|
|
|
|
test('should validate plugin path', async ({ pluginsPage }) => {
|
|
await pluginsPage.createBtn.click()
|
|
await expect(pluginsPage.sheet).toBeVisible()
|
|
await pluginsPage.waitForSheetAnimation()
|
|
|
|
// Fill name
|
|
const sheetNameInput = pluginsPage.sheet.getByRole('textbox', { name: /Plugin Name/i })
|
|
await sheetNameInput.fill(`path-validation-${Date.now()}`)
|
|
|
|
// Fill invalid path (doesn't start with / or http)
|
|
const sheetPathInput = pluginsPage.sheet.getByRole('textbox', { name: /Plugin Path/i })
|
|
await sheetPathInput.fill('invalid-path-no-extension')
|
|
|
|
// Validation should disable the save button
|
|
await expect(pluginsPage.saveBtn).toBeDisabled()
|
|
|
|
// Validation message should be visible
|
|
const validationMessage = pluginsPage.sheet.getByText(/valid absolute file path|valid path/i)
|
|
await expect(validationMessage).toBeVisible()
|
|
|
|
await pluginsPage.cancelPlugin()
|
|
})
|
|
|
|
test('should handle invalid plugin path', async ({ pluginsPage }) => {
|
|
await pluginsPage.createBtn.click()
|
|
await expect(pluginsPage.sheet).toBeVisible()
|
|
await pluginsPage.waitForSheetAnimation()
|
|
|
|
const sheetNameInput = pluginsPage.sheet.getByRole('textbox', { name: /Plugin Name/i })
|
|
const sheetPathInput = pluginsPage.sheet.getByRole('textbox', { name: /Plugin Path/i })
|
|
|
|
await sheetNameInput.fill(`invalid-plugin-${Date.now()}`)
|
|
await sheetPathInput.fill('/nonexistent/path/to/plugin.so')
|
|
|
|
// Try to save - this might succeed (path not validated until load) or fail
|
|
await pluginsPage.saveBtn.click()
|
|
|
|
// Wait for response
|
|
await pluginsPage.page.waitForTimeout(1000)
|
|
|
|
// Invalid path: sheet may close with error toast or stay open with error
|
|
const inlineError = pluginsPage.sheet.locator('[role="alert"], .text-destructive').first()
|
|
const errorToast = pluginsPage.page.locator('[data-sonner-toast][data-type="error"]').first()
|
|
await inlineError.or(errorToast).waitFor({ state: 'visible', timeout: 10000 })
|
|
const hasInlineError = await inlineError.isVisible().catch(() => false)
|
|
|
|
if (hasInlineError) {
|
|
await expect(pluginsPage.sheet).toBeVisible()
|
|
await expect(inlineError).toBeVisible()
|
|
await pluginsPage.cancelPlugin()
|
|
} else {
|
|
await expect(errorToast).toBeVisible()
|
|
}
|
|
})
|
|
})
|
|
})
|