first commit
This commit is contained in:
57
tests/e2e/features/config/config.data.ts
Normal file
57
tests/e2e/features/config/config.data.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Test data factories for config settings tests
|
||||
*/
|
||||
|
||||
/**
|
||||
* Config toggle state interface
|
||||
*/
|
||||
export interface ConfigToggleState {
|
||||
name: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Client settings data factory
|
||||
*/
|
||||
export function createClientSettingsData(overrides: Partial<{
|
||||
dropExcessRequests: boolean
|
||||
enableLiteLLMFallbacks: boolean
|
||||
disableDBPings: boolean
|
||||
}> = {}) {
|
||||
return {
|
||||
dropExcessRequests: false,
|
||||
enableLiteLLMFallbacks: true,
|
||||
disableDBPings: false,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logging settings data factory
|
||||
*/
|
||||
export function createLoggingSettingsData(overrides: Partial<{
|
||||
enableLogging: boolean
|
||||
disableContentLogging: boolean
|
||||
retentionDays: number
|
||||
}> = {}) {
|
||||
return {
|
||||
enableLogging: true,
|
||||
disableContentLogging: false,
|
||||
retentionDays: 30,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performance tuning settings data factory
|
||||
*/
|
||||
export function createPerformanceTuningData(overrides: Partial<{
|
||||
workerPoolSize: number
|
||||
maxRequestBodySize: number
|
||||
}> = {}) {
|
||||
return {
|
||||
workerPoolSize: 100,
|
||||
maxRequestBodySize: 10485760, // 10MB
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
547
tests/e2e/features/config/config.spec.ts
Normal file
547
tests/e2e/features/config/config.spec.ts
Normal file
@@ -0,0 +1,547 @@
|
||||
import { expect, test } from '../../core/fixtures/base.fixture'
|
||||
import { ConfigSettingsState } from './pages/config-settings.page'
|
||||
|
||||
test.describe('Config Settings', () => {
|
||||
// Run all config tests serially to avoid parallel writes to the same config/store
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test.describe('Navigation', () => {
|
||||
test('should navigate to client settings', async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('client-settings')
|
||||
await expect(configSettingsPage.saveBtn).toBeVisible()
|
||||
// Use heading to avoid matching sidebar link
|
||||
await expect(configSettingsPage.page.getByRole('heading', { name: /Client Settings/i })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should navigate to caching config', async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('caching')
|
||||
// Caching page exists - verify page loaded
|
||||
await expect(configSettingsPage.page.getByRole('heading', { name: /Caching/i })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should navigate to logging config', async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('logging')
|
||||
await expect(configSettingsPage.saveBtn).toBeVisible()
|
||||
await expect(configSettingsPage.page.getByRole('heading', { name: /Logging/i })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should navigate to security config', async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('security')
|
||||
await expect(configSettingsPage.saveBtn).toBeVisible()
|
||||
await expect(configSettingsPage.page.getByRole('heading', { name: /Security/i })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should navigate to performance tuning config', async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('performance-tuning')
|
||||
await expect(configSettingsPage.saveBtn).toBeVisible()
|
||||
await expect(configSettingsPage.page.getByRole('heading', { name: /Performance Tuning/i })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should navigate to pricing config', async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('pricing-config')
|
||||
await expect(configSettingsPage.saveBtn).toBeVisible()
|
||||
await expect(configSettingsPage.page.getByRole('heading', { name: /Pricing/i })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should navigate to MCP settings', async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('mcp-gateway')
|
||||
await expect(configSettingsPage.page.getByTestId('mcp-settings-view')).toBeVisible()
|
||||
await expect(configSettingsPage.page.getByTestId('mcp-agent-depth-input')).toBeVisible()
|
||||
await expect(configSettingsPage.page.getByTestId('mcp-tool-timeout-input')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('MCP Settings', () => {
|
||||
test('should display MCP settings form', async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('mcp-gateway')
|
||||
|
||||
await expect(configSettingsPage.page.getByTestId('mcp-settings-view')).toBeVisible()
|
||||
await expect(configSettingsPage.page.getByTestId('mcp-agent-depth-input')).toBeVisible()
|
||||
await expect(configSettingsPage.page.getByTestId('mcp-tool-timeout-input')).toBeVisible()
|
||||
await expect(configSettingsPage.page.getByTestId('mcp-binding-level')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should have save button disabled when no changes', async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('mcp-gateway')
|
||||
|
||||
const saveBtn = configSettingsPage.page.getByTestId('mcp-settings-save-btn')
|
||||
await expect(saveBtn).toBeVisible()
|
||||
await expect(saveBtn).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Pricing Config', () => {
|
||||
let originalPricingUrl: string | null = null
|
||||
|
||||
test.beforeEach(async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('pricing-config')
|
||||
originalPricingUrl = await configSettingsPage.pricingDatasheetUrlInput.inputValue()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('pricing-config')
|
||||
const canEdit = await configSettingsPage.pricingDatasheetUrlInput.isEditable().catch(() => false)
|
||||
if (!canEdit || originalPricingUrl === null) return
|
||||
await configSettingsPage.setPricingDatasheetUrl(originalPricingUrl)
|
||||
const isSaveEnabled = await configSettingsPage.pricingSaveBtn.isDisabled().then((d) => !d)
|
||||
if (isSaveEnabled) {
|
||||
await configSettingsPage.savePricingConfig()
|
||||
await configSettingsPage.dismissToasts()
|
||||
}
|
||||
})
|
||||
|
||||
test('should display pricing config view', async ({ configSettingsPage }) => {
|
||||
await expect(configSettingsPage.pricingConfigView).toBeVisible()
|
||||
await expect(configSettingsPage.pricingDatasheetUrlInput).toBeVisible()
|
||||
await expect(configSettingsPage.pricingForceSyncBtn).toBeVisible()
|
||||
await expect(configSettingsPage.pricingSaveBtn).toBeVisible()
|
||||
})
|
||||
|
||||
test('should set and save datasheet URL', async ({ configSettingsPage }) => {
|
||||
const testUrl = 'https://example.com/pricing.json'
|
||||
await configSettingsPage.setPricingDatasheetUrl(testUrl)
|
||||
|
||||
const isSaveEnabled = await configSettingsPage.pricingSaveBtn.isDisabled().then((d) => !d)
|
||||
if (!isSaveEnabled) {
|
||||
test.skip(true, 'Save button disabled (no changes detected or RBAC)')
|
||||
return
|
||||
}
|
||||
|
||||
await configSettingsPage.savePricingConfig()
|
||||
await configSettingsPage.dismissToasts()
|
||||
})
|
||||
|
||||
test('should trigger force sync', async ({ configSettingsPage }) => {
|
||||
const isForceSyncEnabled = await configSettingsPage.pricingForceSyncBtn.isDisabled().then((d) => !d)
|
||||
if (!isForceSyncEnabled) {
|
||||
test.skip(true, 'Force sync button disabled (RBAC or no datasheet URL)')
|
||||
return
|
||||
}
|
||||
|
||||
await configSettingsPage.triggerForceSync()
|
||||
await configSettingsPage.dismissToasts()
|
||||
})
|
||||
|
||||
test('should validate URL format', async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.pricingDatasheetUrlInput.fill('invalid-url-no-http')
|
||||
const canSave = await configSettingsPage.pricingSaveBtn.isDisabled().then((d) => !d)
|
||||
if (!canSave) {
|
||||
test.skip(true, 'Save button disabled (RBAC)')
|
||||
return
|
||||
}
|
||||
await configSettingsPage.pricingSaveBtn.click()
|
||||
|
||||
await expect(configSettingsPage.page.getByText(/URL must start with http|valid URL/i)).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Client Settings', () => {
|
||||
let originalState: ConfigSettingsState
|
||||
|
||||
test.beforeEach(async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('client-settings')
|
||||
// Capture original state for restoration
|
||||
originalState = await configSettingsPage.getCurrentSettings('client-settings')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ configSettingsPage }) => {
|
||||
// Restore original settings
|
||||
if (originalState) {
|
||||
await configSettingsPage.restoreSettings(originalState)
|
||||
}
|
||||
})
|
||||
|
||||
test('should display client settings controls', async ({ configSettingsPage }) => {
|
||||
// Check for main controls
|
||||
await expect(configSettingsPage.dropExcessRequestsSwitch).toBeVisible()
|
||||
await expect(configSettingsPage.enableLiteLLMFallbacksSwitch).toBeVisible()
|
||||
await expect(configSettingsPage.disableDBPingsSwitch).toBeVisible()
|
||||
})
|
||||
|
||||
test('should display async job result TTL input when available', async ({ configSettingsPage }) => {
|
||||
const isVisible = await configSettingsPage.asyncJobResultTtlInput.isVisible().catch(() => false)
|
||||
if (isVisible) {
|
||||
await expect(configSettingsPage.asyncJobResultTtlInput).toBeVisible()
|
||||
} else {
|
||||
test.skip(true, 'Async job result TTL not available')
|
||||
}
|
||||
})
|
||||
|
||||
test('should toggle drop excess requests', async ({ configSettingsPage }) => {
|
||||
const initialState = await configSettingsPage.getSwitchState(configSettingsPage.dropExcessRequestsSwitch)
|
||||
|
||||
await configSettingsPage.toggleDropExcessRequests()
|
||||
|
||||
const newState = await configSettingsPage.getSwitchState(configSettingsPage.dropExcessRequestsSwitch)
|
||||
expect(newState).toBe(!initialState)
|
||||
|
||||
// Verify changes are pending
|
||||
const hasChanges = await configSettingsPage.hasPendingChanges()
|
||||
expect(hasChanges).toBe(true)
|
||||
})
|
||||
|
||||
test('should save and persist drop excess requests toggle', async ({ configSettingsPage }) => {
|
||||
const initialState = await configSettingsPage.getSwitchState(configSettingsPage.dropExcessRequestsSwitch)
|
||||
|
||||
await configSettingsPage.toggleDropExcessRequests()
|
||||
await configSettingsPage.saveSettings()
|
||||
await configSettingsPage.goto('client-settings')
|
||||
|
||||
const expectedState = !initialState
|
||||
await expect(configSettingsPage.dropExcessRequestsSwitch).toHaveAttribute(
|
||||
'data-state',
|
||||
expectedState ? 'checked' : 'unchecked'
|
||||
)
|
||||
})
|
||||
|
||||
test('should toggle LiteLLM fallbacks', async ({ configSettingsPage }) => {
|
||||
const initialState = await configSettingsPage.getSwitchState(configSettingsPage.enableLiteLLMFallbacksSwitch)
|
||||
|
||||
await configSettingsPage.toggleLiteLLMFallbacks()
|
||||
|
||||
const newState = await configSettingsPage.getSwitchState(configSettingsPage.enableLiteLLMFallbacksSwitch)
|
||||
expect(newState).toBe(!initialState)
|
||||
})
|
||||
|
||||
test('should save and persist LiteLLM fallbacks toggle', async ({ configSettingsPage }) => {
|
||||
const initialState = await configSettingsPage.getSwitchState(configSettingsPage.enableLiteLLMFallbacksSwitch)
|
||||
|
||||
await configSettingsPage.toggleLiteLLMFallbacks()
|
||||
await configSettingsPage.saveSettings()
|
||||
await configSettingsPage.goto('client-settings')
|
||||
|
||||
// Wait for persisted state (form is populated async after navigation)
|
||||
const expectedState = !initialState
|
||||
await expect(configSettingsPage.enableLiteLLMFallbacksSwitch).toHaveAttribute(
|
||||
'data-state',
|
||||
expectedState ? 'checked' : 'unchecked'
|
||||
)
|
||||
})
|
||||
|
||||
test('should toggle disable DB pings', async ({ configSettingsPage }) => {
|
||||
const initialState = await configSettingsPage.getSwitchState(configSettingsPage.disableDBPingsSwitch)
|
||||
|
||||
await configSettingsPage.toggleDisableDBPings()
|
||||
|
||||
const newState = await configSettingsPage.getSwitchState(configSettingsPage.disableDBPingsSwitch)
|
||||
expect(newState).toBe(!initialState)
|
||||
})
|
||||
|
||||
test('should save and persist disable DB pings toggle', async ({ configSettingsPage }) => {
|
||||
const initialState = await configSettingsPage.getSwitchState(configSettingsPage.disableDBPingsSwitch)
|
||||
|
||||
await configSettingsPage.toggleDisableDBPings()
|
||||
await configSettingsPage.saveSettings()
|
||||
await configSettingsPage.goto('client-settings')
|
||||
|
||||
const expectedState = !initialState
|
||||
await expect(configSettingsPage.disableDBPingsSwitch).toHaveAttribute(
|
||||
'data-state',
|
||||
expectedState ? 'checked' : 'unchecked'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Logging Settings', () => {
|
||||
let originalState: ConfigSettingsState
|
||||
|
||||
test.beforeEach(async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('logging')
|
||||
// Capture original state for restoration
|
||||
originalState = await configSettingsPage.getCurrentSettings('logging')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ configSettingsPage }) => {
|
||||
// Restore original settings
|
||||
if (originalState) {
|
||||
await configSettingsPage.restoreSettings(originalState)
|
||||
}
|
||||
})
|
||||
|
||||
test('should display logging settings controls', async ({ configSettingsPage }) => {
|
||||
// Check for main logging controls
|
||||
await expect(configSettingsPage.page.getByText(/Enable Logs/i)).toBeVisible()
|
||||
await expect(configSettingsPage.page.getByText(/Log Retention/i)).toBeVisible()
|
||||
await expect(configSettingsPage.hideDeletedVirtualKeysInFiltersSwitch).toBeVisible()
|
||||
})
|
||||
|
||||
test('should toggle hide deleted virtual keys in filters', async ({ configSettingsPage }) => {
|
||||
const initialState = await configSettingsPage.getSwitchState(configSettingsPage.hideDeletedVirtualKeysInFiltersSwitch)
|
||||
|
||||
await configSettingsPage.toggleHideDeletedVirtualKeysInFilters()
|
||||
|
||||
const newState = await configSettingsPage.getSwitchState(configSettingsPage.hideDeletedVirtualKeysInFiltersSwitch)
|
||||
expect(newState).toBe(!initialState)
|
||||
|
||||
const hasChanges = await configSettingsPage.hasPendingChanges()
|
||||
expect(hasChanges).toBe(true)
|
||||
})
|
||||
|
||||
test('should save and persist hide deleted virtual keys in filters toggle', async ({ configSettingsPage }) => {
|
||||
const initialState = await configSettingsPage.getSwitchState(configSettingsPage.hideDeletedVirtualKeysInFiltersSwitch)
|
||||
|
||||
await configSettingsPage.toggleHideDeletedVirtualKeysInFilters()
|
||||
await configSettingsPage.saveSettings()
|
||||
await configSettingsPage.goto('logging')
|
||||
|
||||
const expectedState = !initialState
|
||||
await expect(configSettingsPage.hideDeletedVirtualKeysInFiltersSwitch).toHaveAttribute(
|
||||
'data-state',
|
||||
expectedState ? 'checked' : 'unchecked'
|
||||
)
|
||||
})
|
||||
|
||||
test('should display workspace logging headers textarea when available', async ({ configSettingsPage }) => {
|
||||
const isVisible = await configSettingsPage.workspaceLoggingHeadersTextarea.isVisible().catch(() => false)
|
||||
if (isVisible) {
|
||||
await expect(configSettingsPage.workspaceLoggingHeadersTextarea).toBeVisible()
|
||||
} else {
|
||||
test.skip(true, 'Workspace logging headers not available (depends on log connector)')
|
||||
}
|
||||
})
|
||||
|
||||
test('should toggle content logging when available', async ({ configSettingsPage }) => {
|
||||
// Check if the switch is available (depends on logs being connected)
|
||||
const disableContentLoggingVisible = await configSettingsPage.disableContentLoggingSwitch.isVisible().catch(() => false)
|
||||
|
||||
if (disableContentLoggingVisible) {
|
||||
const initialState = await configSettingsPage.getSwitchState(configSettingsPage.disableContentLoggingSwitch)
|
||||
|
||||
await configSettingsPage.toggleDisableContentLogging()
|
||||
|
||||
const newState = await configSettingsPage.getSwitchState(configSettingsPage.disableContentLoggingSwitch)
|
||||
expect(newState).toBe(!initialState)
|
||||
} else {
|
||||
// Skip if logging not available
|
||||
test.skip()
|
||||
}
|
||||
})
|
||||
|
||||
test('should save and persist content logging toggle when available', async ({ configSettingsPage }) => {
|
||||
// Check if the switch is available (depends on logs being connected)
|
||||
const disableContentLoggingVisible = await configSettingsPage.disableContentLoggingSwitch.isVisible().catch(() => false)
|
||||
|
||||
if (disableContentLoggingVisible) {
|
||||
const initialState = await configSettingsPage.getSwitchState(configSettingsPage.disableContentLoggingSwitch)
|
||||
|
||||
// Toggle
|
||||
await configSettingsPage.toggleDisableContentLogging()
|
||||
|
||||
// Save
|
||||
await configSettingsPage.saveSettings()
|
||||
|
||||
// Reload the page
|
||||
await configSettingsPage.goto('logging')
|
||||
|
||||
// Verify change persisted
|
||||
const savedState = await configSettingsPage.getSwitchState(configSettingsPage.disableContentLoggingSwitch)
|
||||
expect(savedState).toBe(!initialState)
|
||||
} else {
|
||||
// Skip if logging not available
|
||||
test.skip()
|
||||
}
|
||||
})
|
||||
|
||||
test('should change log retention days', async ({ configSettingsPage }) => {
|
||||
const retentionInput = configSettingsPage.logRetentionDaysInput
|
||||
const isVisible = await retentionInput.isVisible().catch(() => false)
|
||||
|
||||
if (isVisible) {
|
||||
const originalValue = await retentionInput.inputValue()
|
||||
const newValue = originalValue === '30' ? '60' : '30'
|
||||
|
||||
await retentionInput.clear()
|
||||
await retentionInput.fill(newValue)
|
||||
|
||||
const currentValue = await retentionInput.inputValue()
|
||||
expect(currentValue).toBe(newValue)
|
||||
|
||||
// Verify changes are pending
|
||||
const hasChanges = await configSettingsPage.hasPendingChanges()
|
||||
expect(hasChanges).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test('should save and persist log retention days', async ({ configSettingsPage }) => {
|
||||
const retentionInput = configSettingsPage.logRetentionDaysInput
|
||||
const isVisible = await retentionInput.isVisible().catch(() => false)
|
||||
|
||||
if (isVisible) {
|
||||
const originalValue = await retentionInput.inputValue()
|
||||
const newValue = originalValue === '30' ? '60' : '30'
|
||||
|
||||
// Change value
|
||||
await retentionInput.clear()
|
||||
await retentionInput.fill(newValue)
|
||||
|
||||
// Save
|
||||
await configSettingsPage.saveSettings()
|
||||
|
||||
// Reload the page
|
||||
await configSettingsPage.goto('logging')
|
||||
|
||||
// Verify change persisted
|
||||
const savedValue = await retentionInput.inputValue()
|
||||
expect(savedValue).toBe(newValue)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Security Settings', () => {
|
||||
let originalState: ConfigSettingsState
|
||||
|
||||
test.beforeEach(async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('security')
|
||||
// Capture original state for restoration
|
||||
originalState = await configSettingsPage.getCurrentSettings('security')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ configSettingsPage }) => {
|
||||
// Restore original settings
|
||||
if (originalState) {
|
||||
await configSettingsPage.restoreSettings(originalState)
|
||||
}
|
||||
})
|
||||
|
||||
test('should display security settings', async ({ configSettingsPage }) => {
|
||||
await expect(configSettingsPage.page.getByRole('heading', { name: /Security/i })).toBeVisible()
|
||||
await expect(configSettingsPage.saveBtn).toBeVisible()
|
||||
})
|
||||
|
||||
test('should display enforce auth on inference switch', async ({ configSettingsPage }) => {
|
||||
const isVisible = await configSettingsPage.enforceAuthOnInferenceSwitch.isVisible().catch(() => false)
|
||||
if (!isVisible) {
|
||||
test.skip(true, 'Enforce auth on inference not available')
|
||||
return
|
||||
}
|
||||
await expect(configSettingsPage.enforceAuthOnInferenceSwitch).toBeVisible()
|
||||
})
|
||||
|
||||
test('should toggle enforce auth on inference', async ({ configSettingsPage }) => {
|
||||
const isVisible = await configSettingsPage.enforceAuthOnInferenceSwitch.isVisible().catch(() => false)
|
||||
if (!isVisible) {
|
||||
test.skip(true, 'Enforce auth on inference not available')
|
||||
return
|
||||
}
|
||||
const initialState = await configSettingsPage.getSwitchState(configSettingsPage.enforceAuthOnInferenceSwitch)
|
||||
await configSettingsPage.toggleEnforceAuthOnInference()
|
||||
const newState = await configSettingsPage.getSwitchState(configSettingsPage.enforceAuthOnInferenceSwitch)
|
||||
expect(newState).toBe(!initialState)
|
||||
await configSettingsPage.toggleEnforceAuthOnInference()
|
||||
if (await configSettingsPage.hasPendingChanges()) {
|
||||
await configSettingsPage.saveSettings()
|
||||
}
|
||||
})
|
||||
|
||||
test('should display required headers textarea', async ({ configSettingsPage }) => {
|
||||
const isVisible = await configSettingsPage.requiredHeadersTextarea.isVisible().catch(() => false)
|
||||
if (!isVisible) {
|
||||
test.skip(true, 'Required headers control not available')
|
||||
return
|
||||
}
|
||||
await expect(configSettingsPage.requiredHeadersTextarea).toBeVisible()
|
||||
})
|
||||
|
||||
test('should display rate limiting section', async ({ configSettingsPage }) => {
|
||||
const isVisible = await configSettingsPage.isRateLimitingSectionVisible()
|
||||
expect(isVisible).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Performance Tuning Settings', () => {
|
||||
let originalState: ConfigSettingsState
|
||||
|
||||
test.beforeEach(async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('performance-tuning')
|
||||
originalState = await configSettingsPage.getCurrentSettings('performance-tuning')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ configSettingsPage }) => {
|
||||
if (originalState) {
|
||||
await configSettingsPage.restoreSettings(originalState)
|
||||
}
|
||||
})
|
||||
|
||||
test('should display performance tuning settings', async ({ configSettingsPage }) => {
|
||||
await expect(configSettingsPage.page.getByRole('heading', { name: /Performance Tuning/i })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should change worker pool size', async ({ configSettingsPage }) => {
|
||||
const workerPoolInput = configSettingsPage.workerPoolSizeInput
|
||||
const isVisible = await workerPoolInput.isVisible().catch(() => false)
|
||||
|
||||
if (isVisible) {
|
||||
const originalValue = await workerPoolInput.inputValue()
|
||||
const newValue = parseInt(originalValue) === 100 ? '200' : '100'
|
||||
|
||||
await workerPoolInput.clear()
|
||||
await workerPoolInput.fill(newValue)
|
||||
|
||||
const currentValue = await workerPoolInput.inputValue()
|
||||
expect(currentValue).toBe(newValue)
|
||||
}
|
||||
})
|
||||
|
||||
test('should save and persist worker pool size', async ({ configSettingsPage }) => {
|
||||
const workerPoolInput = configSettingsPage.workerPoolSizeInput
|
||||
const isVisible = await workerPoolInput.isVisible().catch(() => false)
|
||||
|
||||
if (isVisible) {
|
||||
const originalValue = await workerPoolInput.inputValue()
|
||||
const newValue = parseInt(originalValue) === 100 ? '200' : '100'
|
||||
|
||||
// Change value
|
||||
await workerPoolInput.clear()
|
||||
await workerPoolInput.fill(newValue)
|
||||
|
||||
// Save
|
||||
await configSettingsPage.saveSettings()
|
||||
|
||||
// Reload the page
|
||||
await configSettingsPage.goto('performance-tuning')
|
||||
|
||||
// Verify change persisted
|
||||
const savedValue = await workerPoolInput.inputValue()
|
||||
expect(savedValue).toBe(newValue)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Pricing Config Settings', () => {
|
||||
let originalState: ConfigSettingsState
|
||||
|
||||
test.beforeEach(async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('pricing-config')
|
||||
originalState = await configSettingsPage.getCurrentSettings('pricing-config')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ configSettingsPage }) => {
|
||||
if (originalState) {
|
||||
await configSettingsPage.restoreSettings(originalState)
|
||||
}
|
||||
})
|
||||
|
||||
test('should display pricing config settings', async ({ configSettingsPage }) => {
|
||||
await expect(configSettingsPage.page.getByRole('heading', { name: /Pricing/i })).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Caching Settings', () => {
|
||||
let originalState: ConfigSettingsState
|
||||
|
||||
test.beforeEach(async ({ configSettingsPage }) => {
|
||||
await configSettingsPage.goto('caching')
|
||||
originalState = await configSettingsPage.getCurrentSettings('caching')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ configSettingsPage }) => {
|
||||
if (originalState) {
|
||||
await configSettingsPage.restoreSettings(originalState)
|
||||
}
|
||||
})
|
||||
|
||||
test('should display caching settings', async ({ configSettingsPage }) => {
|
||||
await expect(configSettingsPage.page.getByRole('heading', { name: /Caching/i })).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
343
tests/e2e/features/config/pages/config-settings.page.ts
Normal file
343
tests/e2e/features/config/pages/config-settings.page.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import { BasePage } from '../../../core/pages/base.page'
|
||||
import { waitForNetworkIdle } from '../../../core/utils/test-helpers'
|
||||
|
||||
/**
|
||||
* Config settings state interface
|
||||
*/
|
||||
export interface ConfigSettingsState {
|
||||
toggleStates: Record<string, boolean>
|
||||
inputValues: Record<string, string>
|
||||
configPath: string
|
||||
}
|
||||
|
||||
export class ConfigSettingsPage extends BasePage {
|
||||
readonly saveBtn: Locator
|
||||
|
||||
// Client Settings
|
||||
readonly dropExcessRequestsSwitch: Locator
|
||||
readonly enableLiteLLMFallbacksSwitch: Locator
|
||||
readonly disableDBPingsSwitch: Locator
|
||||
readonly asyncJobResultTtlInput: Locator
|
||||
|
||||
// Logging Settings
|
||||
readonly enableLoggingSwitch: Locator
|
||||
readonly disableContentLoggingSwitch: Locator
|
||||
readonly hideDeletedVirtualKeysInFiltersSwitch: Locator
|
||||
readonly logRetentionDaysInput: Locator
|
||||
readonly workspaceLoggingHeadersTextarea: Locator
|
||||
|
||||
// Security Settings
|
||||
readonly rateLimitingSection: Locator
|
||||
readonly enforceAuthOnInferenceSwitch: Locator
|
||||
readonly requiredHeadersTextarea: Locator
|
||||
|
||||
// Performance Tuning Settings
|
||||
readonly workerPoolSizeInput: Locator
|
||||
readonly maxRequestBodySizeInput: Locator
|
||||
|
||||
// Observability Settings
|
||||
readonly observabilityToggles: Locator
|
||||
|
||||
// Pricing Config
|
||||
readonly pricingConfigView: Locator
|
||||
readonly pricingDatasheetUrlInput: Locator
|
||||
readonly pricingForceSyncBtn: Locator
|
||||
readonly pricingSaveBtn: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page)
|
||||
this.saveBtn = page.getByRole('button', { name: /Save/i })
|
||||
|
||||
// Client Settings locators
|
||||
this.dropExcessRequestsSwitch = page.locator('#drop-excess-requests')
|
||||
this.enableLiteLLMFallbacksSwitch = page.locator('#enable-litellm-fallbacks')
|
||||
this.disableDBPingsSwitch = page.locator('#disable-db-pings-in-health')
|
||||
this.asyncJobResultTtlInput = page.getByTestId('client-settings-async-job-result-ttl-input')
|
||||
|
||||
// Logging Settings locators
|
||||
this.enableLoggingSwitch = page.locator('#enable-logging')
|
||||
this.disableContentLoggingSwitch = page.locator('#disable-content-logging')
|
||||
this.hideDeletedVirtualKeysInFiltersSwitch = page.getByTestId('hide-deleted-virtual-keys-in-filters-switch')
|
||||
this.logRetentionDaysInput = page.getByLabel(/Log Retention Days/i).or(
|
||||
page.locator('#log-n-days')
|
||||
)
|
||||
this.workspaceLoggingHeadersTextarea = page.getByTestId('workspace-logging-headers-textarea')
|
||||
|
||||
// Security Settings locators
|
||||
this.rateLimitingSection = page.locator('text=Rate Limiting').locator('..')
|
||||
this.enforceAuthOnInferenceSwitch = page.getByTestId('enforce-auth-on-inference-switch')
|
||||
this.requiredHeadersTextarea = page.getByTestId('required-headers-textarea')
|
||||
|
||||
// Performance Tuning locators
|
||||
this.workerPoolSizeInput = page.getByLabel(/Worker Pool Size/i)
|
||||
this.maxRequestBodySizeInput = page.getByLabel(/Max Request Body Size/i)
|
||||
|
||||
// Observability locators
|
||||
this.observabilityToggles = page.locator('button[role="switch"]')
|
||||
|
||||
// Pricing Config locators
|
||||
this.pricingConfigView = page.getByTestId('pricing-config-view')
|
||||
this.pricingDatasheetUrlInput = page.getByTestId('pricing-datasheet-url-input')
|
||||
this.pricingForceSyncBtn = page.getByTestId('pricing-force-sync-btn')
|
||||
this.pricingSaveBtn = page.getByTestId('pricing-save-btn')
|
||||
}
|
||||
|
||||
async goto(path: string): Promise<void> {
|
||||
await this.page.goto(`/workspace/config/${path}`)
|
||||
await waitForNetworkIdle(this.page)
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.saveBtn.click()
|
||||
await this.waitForSuccessToast()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if save button is enabled (changes pending)
|
||||
*/
|
||||
async hasPendingChanges(): Promise<boolean> {
|
||||
const isDisabled = await this.saveBtn.isDisabled()
|
||||
return !isDisabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle a switch element
|
||||
*/
|
||||
async toggleSwitch(switchLocator: Locator): Promise<void> {
|
||||
await switchLocator.click()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the state of a switch
|
||||
*/
|
||||
async getSwitchState(switchLocator: Locator): Promise<boolean> {
|
||||
const state = await switchLocator.getAttribute('data-state')
|
||||
return state === 'checked'
|
||||
}
|
||||
|
||||
/**
|
||||
* Set input value
|
||||
*/
|
||||
async setInputValue(inputLocator: Locator, value: string): Promise<void> {
|
||||
await inputLocator.clear()
|
||||
await inputLocator.fill(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get input value
|
||||
*/
|
||||
async getInputValue(inputLocator: Locator): Promise<string> {
|
||||
return await inputLocator.inputValue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture current settings state for a config page
|
||||
*/
|
||||
async getCurrentSettings(configPath: string): Promise<ConfigSettingsState> {
|
||||
const state: ConfigSettingsState = {
|
||||
toggleStates: {},
|
||||
inputValues: {},
|
||||
configPath,
|
||||
}
|
||||
|
||||
// Get all switch states on the page
|
||||
const switches = this.page.locator('button[role="switch"]')
|
||||
const switchCount = await switches.count()
|
||||
|
||||
for (let i = 0; i < switchCount; i++) {
|
||||
const switchEl = switches.nth(i)
|
||||
const elId = await switchEl.getAttribute('id')
|
||||
if (!elId) {
|
||||
console.warn(`Switch at index ${i} has no id attribute — using positional fallback "switch-${i}" which may mismatch on restore`)
|
||||
}
|
||||
const id = elId || `switch-${i}`
|
||||
const isChecked = await switchEl.getAttribute('data-state') === 'checked'
|
||||
state.toggleStates[id] = isChecked
|
||||
}
|
||||
|
||||
// Get all number input values on the page
|
||||
const numberInputs = this.page.locator('input[type="number"]')
|
||||
const inputCount = await numberInputs.count()
|
||||
|
||||
for (let i = 0; i < inputCount; i++) {
|
||||
const input = numberInputs.nth(i)
|
||||
const elId = await input.getAttribute('id')
|
||||
if (!elId) {
|
||||
console.warn(`Input at index ${i} has no id attribute — using positional fallback "input-${i}" which may mismatch on restore`)
|
||||
}
|
||||
const id = elId || `input-${i}`
|
||||
const value = await input.inputValue()
|
||||
state.inputValues[id] = value
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore settings to a previous state
|
||||
*/
|
||||
async restoreSettings(state: ConfigSettingsState): Promise<void> {
|
||||
// Navigate to the config page if not already there
|
||||
const currentUrl = this.page.url()
|
||||
if (!currentUrl.includes(state.configPath)) {
|
||||
await this.goto(state.configPath)
|
||||
}
|
||||
|
||||
let hasChanges = false
|
||||
|
||||
// Restore switch states
|
||||
const switches = this.page.locator('button[role="switch"]')
|
||||
const switchCount = await switches.count()
|
||||
|
||||
for (let i = 0; i < switchCount; i++) {
|
||||
const switchEl = switches.nth(i)
|
||||
const elId = await switchEl.getAttribute('id')
|
||||
if (!elId) {
|
||||
console.warn(`Switch at index ${i} has no id attribute — using positional fallback "switch-${i}" which may mismatch on restore`)
|
||||
}
|
||||
const id = elId || `switch-${i}`
|
||||
|
||||
if (state.toggleStates[id] !== undefined) {
|
||||
const currentState = await switchEl.getAttribute('data-state') === 'checked'
|
||||
if (currentState !== state.toggleStates[id]) {
|
||||
await switchEl.click()
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore input values
|
||||
const numberInputs = this.page.locator('input[type="number"]')
|
||||
const inputCount = await numberInputs.count()
|
||||
|
||||
for (let i = 0; i < inputCount; i++) {
|
||||
const input = numberInputs.nth(i)
|
||||
const elId = await input.getAttribute('id')
|
||||
if (!elId) {
|
||||
console.warn(`Input at index ${i} has no id attribute — using positional fallback "input-${i}" which may mismatch on restore`)
|
||||
}
|
||||
const id = elId || `input-${i}`
|
||||
|
||||
if (state.inputValues[id] !== undefined) {
|
||||
const currentValue = await input.inputValue()
|
||||
if (currentValue !== state.inputValues[id]) {
|
||||
await input.clear()
|
||||
await input.fill(state.inputValues[id])
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save if changes were made
|
||||
if (hasChanges) {
|
||||
const canSave = await this.hasPendingChanges()
|
||||
if (canSave) {
|
||||
await this.saveSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Client Settings Methods ===
|
||||
|
||||
async toggleDropExcessRequests(): Promise<void> {
|
||||
await this.dropExcessRequestsSwitch.click()
|
||||
}
|
||||
|
||||
async toggleLiteLLMFallbacks(): Promise<void> {
|
||||
await this.enableLiteLLMFallbacksSwitch.click()
|
||||
}
|
||||
|
||||
async toggleDisableDBPings(): Promise<void> {
|
||||
await this.disableDBPingsSwitch.click()
|
||||
}
|
||||
|
||||
// === Logging Settings Methods ===
|
||||
|
||||
async toggleEnableLogging(): Promise<void> {
|
||||
await this.enableLoggingSwitch.click()
|
||||
}
|
||||
|
||||
async toggleDisableContentLogging(): Promise<void> {
|
||||
await this.disableContentLoggingSwitch.click()
|
||||
}
|
||||
|
||||
async toggleHideDeletedVirtualKeysInFilters(): Promise<void> {
|
||||
await this.hideDeletedVirtualKeysInFiltersSwitch.click()
|
||||
}
|
||||
|
||||
async setLogRetentionDays(days: number): Promise<void> {
|
||||
const input = this.page.locator('input[type="number"]').first()
|
||||
await input.clear()
|
||||
await input.fill(days.toString())
|
||||
}
|
||||
|
||||
async getLogRetentionDays(): Promise<number> {
|
||||
const input = this.page.locator('input[type="number"]').first()
|
||||
const value = await input.inputValue()
|
||||
return parseInt(value, 10)
|
||||
}
|
||||
|
||||
// === Security Settings Methods ===
|
||||
|
||||
async isRateLimitingSectionVisible(): Promise<boolean> {
|
||||
return await this.page.getByText(/Rate Limiting/i).isVisible()
|
||||
}
|
||||
|
||||
async toggleEnforceAuthOnInference(): Promise<void> {
|
||||
await this.enforceAuthOnInferenceSwitch.click()
|
||||
}
|
||||
|
||||
async setRequiredHeaders(value: string): Promise<void> {
|
||||
await this.requiredHeadersTextarea.clear()
|
||||
await this.requiredHeadersTextarea.fill(value)
|
||||
}
|
||||
|
||||
async setWorkspaceLoggingHeaders(value: string): Promise<void> {
|
||||
await this.workspaceLoggingHeadersTextarea.clear()
|
||||
await this.workspaceLoggingHeadersTextarea.fill(value)
|
||||
}
|
||||
|
||||
async setAsyncJobResultTtl(value: string): Promise<void> {
|
||||
await this.asyncJobResultTtlInput.clear()
|
||||
await this.asyncJobResultTtlInput.fill(value)
|
||||
}
|
||||
|
||||
// === Observability Settings Methods ===
|
||||
|
||||
async getObservabilityConnectors(): Promise<string[]> {
|
||||
const connectorHeadings = this.page.locator('h3, h4').filter({ hasText: /Datadog|New Relic|OTel|OpenTelemetry|Maxim/i })
|
||||
const count = await connectorHeadings.count()
|
||||
const connectors: string[] = []
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const text = await connectorHeadings.nth(i).textContent()
|
||||
if (text) connectors.push(text)
|
||||
}
|
||||
|
||||
return connectors
|
||||
}
|
||||
|
||||
async toggleObservabilityConnector(connectorName: string): Promise<void> {
|
||||
const connectorSection = this.page.locator('div').filter({ hasText: new RegExp(connectorName, 'i') }).first()
|
||||
const toggleSwitch = connectorSection.locator('button[role="switch"]').first()
|
||||
await toggleSwitch.click()
|
||||
}
|
||||
|
||||
// === Pricing Config Methods ===
|
||||
|
||||
async setPricingDatasheetUrl(url: string): Promise<void> {
|
||||
await this.pricingDatasheetUrlInput.clear()
|
||||
await this.pricingDatasheetUrlInput.fill(url)
|
||||
}
|
||||
|
||||
async triggerForceSync(): Promise<void> {
|
||||
await this.pricingForceSyncBtn.click()
|
||||
await this.waitForSuccessToast()
|
||||
}
|
||||
|
||||
async savePricingConfig(): Promise<void> {
|
||||
await this.pricingSaveBtn.click()
|
||||
await this.waitForSuccessToast()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user