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,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
}
}

View 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()
})
})
})

View 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()
}
}