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 inputValues: Record 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 { await this.page.goto(`/workspace/config/${path}`) await waitForNetworkIdle(this.page) } async saveSettings(): Promise { await this.saveBtn.click() await this.waitForSuccessToast() } /** * Check if save button is enabled (changes pending) */ async hasPendingChanges(): Promise { const isDisabled = await this.saveBtn.isDisabled() return !isDisabled } /** * Toggle a switch element */ async toggleSwitch(switchLocator: Locator): Promise { await switchLocator.click() } /** * Get the state of a switch */ async getSwitchState(switchLocator: Locator): Promise { const state = await switchLocator.getAttribute('data-state') return state === 'checked' } /** * Set input value */ async setInputValue(inputLocator: Locator, value: string): Promise { await inputLocator.clear() await inputLocator.fill(value) } /** * Get input value */ async getInputValue(inputLocator: Locator): Promise { return await inputLocator.inputValue() } /** * Capture current settings state for a config page */ async getCurrentSettings(configPath: string): Promise { 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 { // 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 { await this.dropExcessRequestsSwitch.click() } async toggleLiteLLMFallbacks(): Promise { await this.enableLiteLLMFallbacksSwitch.click() } async toggleDisableDBPings(): Promise { await this.disableDBPingsSwitch.click() } // === Logging Settings Methods === async toggleEnableLogging(): Promise { await this.enableLoggingSwitch.click() } async toggleDisableContentLogging(): Promise { await this.disableContentLoggingSwitch.click() } async toggleHideDeletedVirtualKeysInFilters(): Promise { await this.hideDeletedVirtualKeysInFiltersSwitch.click() } async setLogRetentionDays(days: number): Promise { const input = this.page.locator('input[type="number"]').first() await input.clear() await input.fill(days.toString()) } async getLogRetentionDays(): Promise { const input = this.page.locator('input[type="number"]').first() const value = await input.inputValue() return parseInt(value, 10) } // === Security Settings Methods === async isRateLimitingSectionVisible(): Promise { return await this.page.getByText(/Rate Limiting/i).isVisible() } async toggleEnforceAuthOnInference(): Promise { await this.enforceAuthOnInferenceSwitch.click() } async setRequiredHeaders(value: string): Promise { await this.requiredHeadersTextarea.clear() await this.requiredHeadersTextarea.fill(value) } async setWorkspaceLoggingHeaders(value: string): Promise { await this.workspaceLoggingHeadersTextarea.clear() await this.workspaceLoggingHeadersTextarea.fill(value) } async setAsyncJobResultTtl(value: string): Promise { await this.asyncJobResultTtlInput.clear() await this.asyncJobResultTtlInput.fill(value) } // === Observability Settings Methods === async getObservabilityConnectors(): Promise { 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 { 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 { await this.pricingDatasheetUrlInput.clear() await this.pricingDatasheetUrlInput.fill(url) } async triggerForceSync(): Promise { await this.pricingForceSyncBtn.click() await this.waitForSuccessToast() } async savePricingConfig(): Promise { await this.pricingSaveBtn.click() await this.waitForSuccessToast() } }