first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,350 @@
import { Locator, Page, expect } from '@playwright/test'
import { BasePage } from '../../../core/pages/base.page'
import { waitForNetworkIdle } from '../../../core/utils/test-helpers'
export interface PluginConfig {
name: string
path?: string
type?: string
enabled?: boolean
config?: Record<string, any>
}
export class PluginsPage extends BasePage {
readonly sidebar: Locator
readonly table: Locator // Alias for sidebar (plugins page doesn't have a traditional table)
readonly pluginList: Locator
readonly createBtn: Locator
readonly sheet: Locator
readonly nameInput: Locator
readonly pathInput: Locator
readonly enabledToggle: Locator
readonly saveBtn: Locator
readonly cancelBtn: Locator
constructor(page: Page) {
super(page)
// Plugins page has a sidebar with plugin buttons, not a table
// The sidebar contains the "Plugins" label and plugin list
this.sidebar = page.locator('div').filter({ hasText: /^Plugins$/ }).locator('..').first()
this.table = this.sidebar // Alias for backward compatibility
this.pluginList = page.locator('button[type="button"]').filter({ has: page.locator('svg.lucide-puzzle') })
this.createBtn = page.getByRole('button').filter({
hasText: /Install New Plugin/i
}).or(
page.locator('button').filter({ has: page.locator('svg.lucide-plus') }).filter({ hasText: /Install/i })
)
this.sheet = page.locator('[role="dialog"]')
this.nameInput = page.getByLabel(/Name/i).or(page.locator('input[name="name"]'))
this.pathInput = page.getByLabel(/Path/i).or(page.locator('input[name="path"]'))
this.enabledToggle = page.locator('button[role="switch"]')
this.saveBtn = page.getByRole('button', { name: /Install Plugin/i }).or(
page.getByRole('button', { name: /Update Plugin/i })
)
this.cancelBtn = page.getByRole('button', { name: /Cancel/i }).filter({
hasNot: page.locator('svg.lucide-trash-2')
})
}
/**
* Navigate to the plugins page
*/
async goto(): Promise<void> {
await this.page.goto('/workspace/plugins')
await waitForNetworkIdle(this.page)
// Wait for create button or empty state to be visible (indicates page loaded).
// Use .first() so the locator resolves to one element when both are visible (strict mode).
await this.page.getByTestId('plugins-create-button')
.or(this.page.getByTestId('plugins-empty-state'))
.first()
.waitFor({ state: 'visible', timeout: 10000 })
// Ensure sheet is closed (in case it was left open from previous test)
await this.ensureSheetClosed()
}
/**
* Ensure the plugin sheet is closed
*/
async ensureSheetClosed(): Promise<void> {
const isVisible = await this.sheet.isVisible().catch(() => false)
if (isVisible) {
// Try clicking cancel button first
const cancelVisible = await this.cancelBtn.isVisible().catch(() => false)
if (cancelVisible) {
await this.cancelBtn.click()
// Wait for sheet to close after cancel click
await expect(this.sheet).not.toBeVisible({ timeout: 5000 }).catch(() => {})
}
// Double-check: if still visible, try Escape key
const stillVisible = await this.sheet.isVisible().catch(() => false)
if (stillVisible) {
await this.page.keyboard.press('Escape')
// Wait for sheet to close after Escape
await expect(this.sheet).not.toBeVisible({ timeout: 5000 }).catch(() => {})
}
// Final check: wait for sheet to be detached or not visible
await this.page.waitForFunction(() => {
const sheet = document.querySelector('[role="dialog"]')
return !sheet || window.getComputedStyle(sheet).display === 'none'
}, { timeout: 3000 }).catch(() => {})
}
}
/**
* Get plugin button locator by name (plugins are shown as buttons in sidebar)
*/
getPluginButton(name: string): Locator {
// Find button that contains the plugin name and has a Puzzle icon
return this.page.locator('button[type="button"]')
.filter({ hasText: name })
.filter({ has: this.page.locator('svg.lucide-puzzle') })
.first()
}
/**
* Check if a plugin exists in the sidebar
*/
async pluginExists(name: string): Promise<boolean> {
return await this.getPluginButton(name).count() > 0
}
/**
* Get the count of plugins in the sidebar
*/
async getPluginCount(): Promise<number> {
// Check if it's empty state (no plugin list, only empty state is shown)
const emptyState = this.page.getByTestId('plugins-empty-state')
const isEmptyVisible = await emptyState.isVisible().catch(() => false)
if (isEmptyVisible) {
return 0
}
const buttons = this.page.getByTestId('plugin-list-item')
const count = await buttons.count()
return count
}
/**
* Create a new plugin.
* Returns true if the plugin was created successfully, false if the backend rejected it (e.g. .so load failure).
*/
async createPlugin(config: PluginConfig): Promise<boolean> {
await this.dismissToasts()
// Ensure sheet is closed before starting
await this.ensureSheetClosed()
await this.createBtn.waitFor({ state: 'visible' })
await this.createBtn.click()
await expect(this.sheet).toBeVisible({ timeout: 5000 })
await this.waitForSheetAnimation()
// Scope inputs to the dialog sheet to avoid matching background form inputs
// (PluginsView shows existing plugin form in background with same field names)
const sheetNameInput = this.sheet.getByRole('textbox', { name: /Plugin Name/i })
const sheetPathInput = this.sheet.getByRole('textbox', { name: /Plugin Path/i })
// Fill name (required)
await sheetNameInput.waitFor({ state: 'visible' })
await sheetNameInput.fill(config.name)
// Fill path (required) - use the path from config
await sheetPathInput.waitFor({ state: 'visible' })
const pluginPath = config.path || '/tmp/bifrost-test-plugin.so'
await sheetPathInput.fill(pluginPath)
// Note: enabled state is set to true by default when creating,
// and can't be changed during creation (only during edit)
// Save
await this.saveBtn.waitFor({ state: 'visible' })
await this.saveBtn.click()
// Wait for either a success toast or an error toast (backend may fail to load .so)
const successToast = this.getToast('success')
const errorToast = this.getToast('error')
await successToast.or(errorToast).waitFor({ state: 'visible', timeout: 15000 })
const hasError = await errorToast.isVisible().catch(() => false)
if (hasError) {
// Backend rejected plugin creation (e.g. failed to load .so)
console.warn(`[Plugin] Backend error on create "${config.name}" — plugin was not created`)
await this.dismissToasts()
// Sheet may stay open on error — close it manually
await this.ensureSheetClosed()
await waitForNetworkIdle(this.page)
return false
}
await this.dismissToasts()
// Wait for sheet to close with multiple checks
await expect(this.sheet).not.toBeVisible({ timeout: 10000 })
await this.ensureSheetClosed() // Double-check it's closed
await waitForNetworkIdle(this.page)
return true
}
/**
* Edit an existing plugin
* Note: Name cannot be changed after creation (it's read-only in edit mode)
* Only enabled state, path, and config can be updated
*/
async editPlugin(name: string, updates: Partial<PluginConfig>): Promise<void> {
await this.dismissToasts()
// Ensure sheet is closed before starting
await this.ensureSheetClosed()
// Click on the plugin button to select it (this opens the edit view)
const pluginBtn = this.getPluginButton(name)
await pluginBtn.waitFor({ state: 'visible' })
await pluginBtn.click()
// Wait for the plugin details view to load
await waitForNetworkIdle(this.page)
// The form is in PluginsView - wait for it to be visible
const form = this.page.locator('form')
await form.waitFor({ state: 'visible', timeout: 5000 })
// Update enabled state if provided
if (updates.enabled !== undefined) {
const toggle = this.page.locator('button[role="switch"]').first()
await toggle.waitFor({ state: 'visible' })
const isChecked = await toggle.getAttribute('data-state') === 'checked'
if (isChecked !== updates.enabled) {
await toggle.click()
}
}
// Update path if provided
if (updates.path) {
const pathInput = this.page.getByLabel(/Path/i).or(this.page.locator('input[name="path"]'))
await pathInput.waitFor({ state: 'visible' })
await pathInput.clear()
await pathInput.fill(updates.path)
}
// Wait for Save Changes button to be enabled (form.isDirty must be true)
const saveBtn = this.page.getByRole('button', { name: /Save Changes/i })
await expect(saveBtn).toBeEnabled({ timeout: 5000 })
await saveBtn.click()
await this.waitForSuccessToast()
await this.dismissToasts()
await waitForNetworkIdle(this.page)
// Ensure sheet is closed after edit
await this.ensureSheetClosed()
}
/**
* Delete a plugin
*/
async deletePlugin(name: string): Promise<void> {
await this.dismissToasts()
// Ensure sheet is closed before starting
await this.ensureSheetClosed()
// Click on the plugin button to select it (this opens the details view)
const pluginBtn = this.getPluginButton(name)
await pluginBtn.waitFor({ state: 'visible' })
await pluginBtn.click()
// Wait for the plugin details view to load
await waitForNetworkIdle(this.page)
// Find delete button in the PluginsView (has Trash2Icon)
const deleteBtn = this.page.getByRole('button', { name: /Delete Plugin/i })
await deleteBtn.waitFor({ state: 'visible' })
await deleteBtn.click()
// Wait for the AlertDialog confirmation to appear (uses role="alertdialog")
const alertDialog = this.page.locator('[role="alertdialog"]')
await alertDialog.waitFor({ state: 'visible', timeout: 5000 })
// Click the confirm Delete button inside the AlertDialog
const confirmBtn = alertDialog.getByRole('button', { name: /^Delete$/i })
await confirmBtn.waitFor({ state: 'visible' })
await confirmBtn.click()
await this.waitForSuccessToast('deleted')
await this.dismissToasts()
await waitForNetworkIdle(this.page)
// Ensure sheet is closed after delete
await this.ensureSheetClosed()
}
/**
* Toggle plugin enabled state
*/
async togglePluginEnabled(name: string): Promise<void> {
await this.dismissToasts()
// Ensure sheet is closed before starting
await this.ensureSheetClosed()
// Click on the plugin button to select it (this opens the details view)
const pluginBtn = this.getPluginButton(name)
await pluginBtn.waitFor({ state: 'visible' })
await pluginBtn.click()
// Wait for the plugin details view to load
await waitForNetworkIdle(this.page)
// Find toggle switch in the PluginsView form
const toggle = this.page.locator('button[role="switch"]').first()
await toggle.waitFor({ state: 'visible' })
await toggle.click()
// Wait for the Save Changes button to become enabled (form.isDirty must be true)
const saveBtn = this.page.getByRole('button', { name: /Save Changes/i })
await expect(saveBtn).toBeEnabled({ timeout: 5000 })
await saveBtn.click()
await this.waitForSuccessToast()
await this.dismissToasts()
await waitForNetworkIdle(this.page)
// Ensure sheet is closed after toggle
await this.ensureSheetClosed()
}
/**
* Get plugin enabled state
*/
async getPluginEnabledState(name: string): Promise<boolean> {
// Ensure sheet is closed before starting
await this.ensureSheetClosed()
// Click on the plugin button to select it
const pluginBtn = this.getPluginButton(name)
await pluginBtn.waitFor({ state: 'visible' })
await pluginBtn.click()
// Wait for the plugin details view to load
await waitForNetworkIdle(this.page)
// Find toggle switch in the PluginsView form
const toggle = this.page.locator('button[role="switch"]').first()
let result = false
if (await toggle.count() > 0) {
const dataState = await toggle.getAttribute('data-state')
result = dataState === 'checked'
}
await this.ensureSheetClosed()
return result
}
/**
* Cancel plugin creation/edit
*/
async cancelPlugin(): Promise<void> {
if (await this.sheet.isVisible()) {
await this.cancelBtn.click()
await expect(this.sheet).not.toBeVisible({ timeout: 5000 })
await this.page.waitForTimeout(300) // Small delay for animation
}
// Double-check it's closed
await this.ensureSheetClosed()
}
}

View File

@@ -0,0 +1,20 @@
import { existsSync } from 'fs'
import { join, resolve } from 'path'
// Same location as Makefile build-test-plugin and global setup (repo root tmp/)
const REPO_ROOT = resolve(__dirname, '..', '..', '..', '..')
const TEST_PLUGIN_PATH = join(REPO_ROOT, 'tmp', 'bifrost-test-plugin.so')
/**
* Gets the test plugin path.
* The plugin is built by global setup / make build-test-plugin at repo_root/tmp/bifrost-test-plugin.so.
*/
export function ensureTestPluginExists(): string {
if (!existsSync(TEST_PLUGIN_PATH)) {
throw new Error(
`Test plugin not found at ${TEST_PLUGIN_PATH}. ` +
`Please build it first: make build-test-plugin (from repo root)`
)
}
return TEST_PLUGIN_PATH
}

View File

@@ -0,0 +1,35 @@
import { PluginConfig } from './pages/plugins.page'
import { ensureTestPluginExists } from './plugins-test-helper'
/**
* Sanitize plugin name to only contain letters, numbers, hyphens, and underscores
*/
function sanitizePluginName(name: string): string {
return name
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/[^A-Za-z0-9-_]/g, '') // Remove any invalid characters
.replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
}
/**
* Get the test plugin path (builds if necessary)
*/
let testPluginPath: string | null = null
function getTestPluginPath(): string {
if (!testPluginPath) {
testPluginPath = ensureTestPluginExists()
}
return testPluginPath
}
export function createPluginData(overrides: Partial<PluginConfig> = {}): PluginConfig {
const baseName = overrides.name || `test-plugin-${Date.now()}`
const pluginPath = overrides.path || getTestPluginPath()
return {
name: sanitizePluginName(baseName),
path: pluginPath,
...overrides,
}
}

View File

@@ -0,0 +1,367 @@
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()
}
})
})
})