Skip to content

Commit c34b8a3

Browse files
committed
feat: extend e2e ui tests
1 parent ffff50f commit c34b8a3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1692
-224
lines changed

tests/e2e/core/fixtures/base.fixture.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import { MCPRegistryPage } from '../../features/mcp-registry/pages/mcp-registry.
1010
import { PluginsPage } from '../../features/plugins/pages/plugins.page'
1111
import { ObservabilityPage } from '../../features/observability/pages/observability.page'
1212
import { ConfigSettingsPage } from '../../features/config/pages/config-settings.page'
13+
import { GovernancePage } from '../../features/governance/pages/governance.page'
14+
import { MCPAuthConfigPage } from '../../features/mcp-auth-config/pages/mcp-auth-config.page'
15+
import { MCPSettingsPage } from '../../features/mcp-settings/pages/mcp-settings.page'
16+
import { MCPToolGroupsPage } from '../../features/mcp-tool-groups/pages/mcp-tool-groups.page'
17+
import { ModelLimitsPage } from '../../features/model-limits/pages/model-limits.page'
1318

1419
/**
1520
* Custom test fixtures type
@@ -27,6 +32,11 @@ type BifrostFixtures = {
2732
pluginsPage: PluginsPage
2833
observabilityPage: ObservabilityPage
2934
configSettingsPage: ConfigSettingsPage
35+
governancePage: GovernancePage
36+
modelLimitsPage: ModelLimitsPage
37+
mcpSettingsPage: MCPSettingsPage
38+
mcpToolGroupsPage: MCPToolGroupsPage
39+
mcpAuthConfigPage: MCPAuthConfigPage
3040
}
3141

3242
/**
@@ -88,6 +98,26 @@ export const test = base.extend<BifrostFixtures>({
8898
configSettingsPage: async ({ page }, use) => {
8999
await use(new ConfigSettingsPage(page))
90100
},
101+
102+
governancePage: async ({ page }, use) => {
103+
await use(new GovernancePage(page))
104+
},
105+
106+
modelLimitsPage: async ({ page }, use) => {
107+
await use(new ModelLimitsPage(page))
108+
},
109+
110+
mcpSettingsPage: async ({ page }, use) => {
111+
await use(new MCPSettingsPage(page))
112+
},
113+
114+
mcpToolGroupsPage: async ({ page }, use) => {
115+
await use(new MCPToolGroupsPage(page))
116+
},
117+
118+
mcpAuthConfigPage: async ({ page }, use) => {
119+
await use(new MCPAuthConfigPage(page))
120+
},
91121
})
92122

93123
export { expect }

tests/e2e/features/config/config.spec.ts

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,90 @@ test.describe('Config Settings', () => {
4242
await expect(configSettingsPage.saveBtn).toBeVisible()
4343
await expect(configSettingsPage.page.getByRole('heading', { name: /Pricing/i })).toBeVisible()
4444
})
45+
46+
test('should navigate to MCP settings', async ({ configSettingsPage }) => {
47+
await configSettingsPage.goto('mcp-gateway')
48+
await expect(configSettingsPage.page.getByTestId('mcp-settings-view')).toBeVisible()
49+
await expect(configSettingsPage.page.getByLabel('Max Agent Depth')).toBeVisible()
50+
await expect(configSettingsPage.page.getByLabel('Tool Execution Timeout (seconds)')).toBeVisible()
51+
})
52+
})
53+
54+
test.describe('MCP Settings', () => {
55+
test('should display MCP settings form', async ({ configSettingsPage }) => {
56+
await configSettingsPage.goto('mcp-gateway')
57+
58+
await expect(configSettingsPage.page.getByTestId('mcp-settings-view')).toBeVisible()
59+
await expect(configSettingsPage.page.locator('#mcp-agent-depth')).toBeVisible()
60+
await expect(configSettingsPage.page.locator('#mcp-tool-execution-timeout')).toBeVisible()
61+
await expect(configSettingsPage.page.locator('#mcp-binding-level')).toBeVisible()
62+
})
63+
64+
test('should have save button disabled when no changes', async ({ configSettingsPage }) => {
65+
await configSettingsPage.goto('mcp-gateway')
66+
67+
const saveBtn = configSettingsPage.page.getByTestId('mcp-settings-save-btn')
68+
await expect(saveBtn).toBeVisible()
69+
await expect(saveBtn).toBeDisabled()
70+
})
71+
})
72+
73+
test.describe('Pricing Config', () => {
74+
let originalPricingUrl: string
75+
76+
test.beforeEach(async ({ configSettingsPage }) => {
77+
await configSettingsPage.goto('pricing-config')
78+
originalPricingUrl = await configSettingsPage.pricingDatasheetUrlInput.inputValue()
79+
})
80+
81+
test.afterEach(async ({ configSettingsPage }) => {
82+
await configSettingsPage.goto('pricing-config')
83+
await configSettingsPage.setPricingDatasheetUrl(originalPricingUrl)
84+
const isSaveEnabled = await configSettingsPage.pricingSaveBtn.isDisabled().then((d) => !d)
85+
if (isSaveEnabled) {
86+
await configSettingsPage.savePricingConfig()
87+
await configSettingsPage.dismissToasts()
88+
}
89+
})
90+
91+
test('should display pricing config view', async ({ configSettingsPage }) => {
92+
await expect(configSettingsPage.pricingConfigView).toBeVisible()
93+
await expect(configSettingsPage.pricingDatasheetUrlInput).toBeVisible()
94+
await expect(configSettingsPage.pricingForceSyncBtn).toBeVisible()
95+
await expect(configSettingsPage.pricingSaveBtn).toBeVisible()
96+
})
97+
98+
test('should set and save datasheet URL', async ({ configSettingsPage }) => {
99+
const testUrl = 'https://example.com/pricing.json'
100+
await configSettingsPage.setPricingDatasheetUrl(testUrl)
101+
102+
const isSaveEnabled = await configSettingsPage.pricingSaveBtn.isDisabled().then((d) => !d)
103+
if (!isSaveEnabled) {
104+
test.skip(true, 'Save button disabled (no changes detected or RBAC)')
105+
return
106+
}
107+
108+
await configSettingsPage.savePricingConfig()
109+
await configSettingsPage.dismissToasts()
110+
})
111+
112+
test('should trigger force sync', async ({ configSettingsPage }) => {
113+
const isForceSyncEnabled = await configSettingsPage.pricingForceSyncBtn.isDisabled().then((d) => !d)
114+
if (!isForceSyncEnabled) {
115+
test.skip(true, 'Force sync button disabled (RBAC or no datasheet URL)')
116+
return
117+
}
118+
119+
await configSettingsPage.triggerForceSync()
120+
await configSettingsPage.dismissToasts()
121+
})
122+
123+
test('should validate URL format', async ({ configSettingsPage }) => {
124+
await configSettingsPage.pricingDatasheetUrlInput.fill('invalid-url-no-http')
125+
await configSettingsPage.pricingSaveBtn.click()
126+
127+
await expect(configSettingsPage.page.getByText(/URL must start with http|valid URL/i)).toBeVisible()
128+
})
45129
})
46130

47131
test.describe('Client Settings', () => {
@@ -67,6 +151,15 @@ test.describe('Config Settings', () => {
67151
await expect(configSettingsPage.disableDBPingsSwitch).toBeVisible()
68152
})
69153

154+
test('should display async job result TTL input when available', async ({ configSettingsPage }) => {
155+
const isVisible = await configSettingsPage.asyncJobResultTtlInput.isVisible().catch(() => false)
156+
if (isVisible) {
157+
await expect(configSettingsPage.asyncJobResultTtlInput).toBeVisible()
158+
} else {
159+
test.skip(true, 'Async job result TTL not available')
160+
}
161+
})
162+
70163
test('should toggle drop excess requests', async ({ configSettingsPage }) => {
71164
const initialState = await configSettingsPage.getSwitchState(configSettingsPage.dropExcessRequestsSwitch)
72165

@@ -164,6 +257,15 @@ test.describe('Config Settings', () => {
164257
await expect(configSettingsPage.page.getByText(/Log Retention/i)).toBeVisible()
165258
})
166259

260+
test('should display workspace logging headers textarea when available', async ({ configSettingsPage }) => {
261+
const isVisible = await configSettingsPage.workspaceLoggingHeadersTextarea.isVisible().catch(() => false)
262+
if (isVisible) {
263+
await expect(configSettingsPage.workspaceLoggingHeadersTextarea).toBeVisible()
264+
} else {
265+
test.skip(true, 'Workspace logging headers not available (depends on log connector)')
266+
}
267+
})
268+
167269
test('should toggle content logging when available', async ({ configSettingsPage }) => {
168270
// Check if the switch is available (depends on logs being connected)
169271
const disableContentLoggingVisible = await configSettingsPage.disableContentLoggingSwitch.isVisible().catch(() => false)
@@ -272,9 +374,42 @@ test.describe('Config Settings', () => {
272374
await expect(configSettingsPage.saveBtn).toBeVisible()
273375
})
274376

377+
test('should display enforce auth on inference switch', async ({ configSettingsPage }) => {
378+
const isVisible = await configSettingsPage.enforceAuthOnInferenceSwitch.isVisible().catch(() => false)
379+
if (!isVisible) {
380+
test.skip(true, 'Enforce auth on inference not available')
381+
return
382+
}
383+
await expect(configSettingsPage.enforceAuthOnInferenceSwitch).toBeVisible()
384+
})
385+
386+
test('should toggle enforce auth on inference', async ({ configSettingsPage }) => {
387+
const isVisible = await configSettingsPage.enforceAuthOnInferenceSwitch.isVisible().catch(() => false)
388+
if (!isVisible) {
389+
test.skip(true, 'Enforce auth on inference not available')
390+
return
391+
}
392+
const initialState = await configSettingsPage.getSwitchState(configSettingsPage.enforceAuthOnInferenceSwitch)
393+
await configSettingsPage.toggleEnforceAuthOnInference()
394+
const newState = await configSettingsPage.getSwitchState(configSettingsPage.enforceAuthOnInferenceSwitch)
395+
expect(newState).toBe(!initialState)
396+
await configSettingsPage.toggleEnforceAuthOnInference()
397+
if (await configSettingsPage.hasPendingChanges()) {
398+
await configSettingsPage.saveSettings()
399+
}
400+
})
401+
402+
test('should display required headers textarea', async ({ configSettingsPage }) => {
403+
const isVisible = await configSettingsPage.requiredHeadersTextarea.isVisible().catch(() => false)
404+
if (!isVisible) {
405+
test.skip(true, 'Required headers control not available')
406+
return
407+
}
408+
await expect(configSettingsPage.requiredHeadersTextarea).toBeVisible()
409+
})
410+
275411
test('should display rate limiting section', async ({ configSettingsPage }) => {
276412
const isVisible = await configSettingsPage.isRateLimitingSectionVisible()
277-
// Rate limiting section should exist
278413
expect(isVisible).toBeDefined()
279414
})
280415
})

tests/e2e/features/config/pages/config-settings.page.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@ export class ConfigSettingsPage extends BasePage {
1818
readonly dropExcessRequestsSwitch: Locator
1919
readonly enableLiteLLMFallbacksSwitch: Locator
2020
readonly disableDBPingsSwitch: Locator
21+
readonly asyncJobResultTtlInput: Locator
2122

2223
// Logging Settings
2324
readonly enableLoggingSwitch: Locator
2425
readonly disableContentLoggingSwitch: Locator
2526
readonly logRetentionDaysInput: Locator
27+
readonly workspaceLoggingHeadersTextarea: Locator
2628

2729
// Security Settings
2830
readonly rateLimitingSection: Locator
31+
readonly enforceAuthOnInferenceSwitch: Locator
32+
readonly requiredHeadersTextarea: Locator
2933

3034
// Performance Tuning Settings
3135
readonly workerPoolSizeInput: Locator
@@ -34,6 +38,12 @@ export class ConfigSettingsPage extends BasePage {
3438
// Observability Settings
3539
readonly observabilityToggles: Locator
3640

41+
// Pricing Config
42+
readonly pricingConfigView: Locator
43+
readonly pricingDatasheetUrlInput: Locator
44+
readonly pricingForceSyncBtn: Locator
45+
readonly pricingSaveBtn: Locator
46+
3747
constructor(page: Page) {
3848
super(page)
3949
this.saveBtn = page.getByRole('button', { name: /Save/i })
@@ -42,23 +52,33 @@ export class ConfigSettingsPage extends BasePage {
4252
this.dropExcessRequestsSwitch = page.locator('#drop-excess-requests')
4353
this.enableLiteLLMFallbacksSwitch = page.locator('#enable-litellm-fallbacks')
4454
this.disableDBPingsSwitch = page.locator('#disable-db-pings-in-health')
55+
this.asyncJobResultTtlInput = page.getByTestId('client-settings-async-job-result-ttl-input')
4556

4657
// Logging Settings locators
4758
this.enableLoggingSwitch = page.locator('#enable-logging')
4859
this.disableContentLoggingSwitch = page.locator('#disable-content-logging')
4960
this.logRetentionDaysInput = page.getByLabel(/Log Retention Days/i).or(
5061
page.locator('#log-n-days')
5162
)
63+
this.workspaceLoggingHeadersTextarea = page.getByTestId('workspace-logging-headers-textarea')
5264

5365
// Security Settings locators
5466
this.rateLimitingSection = page.locator('text=Rate Limiting').locator('..')
67+
this.enforceAuthOnInferenceSwitch = page.getByTestId('enforce-auth-on-inference-switch')
68+
this.requiredHeadersTextarea = page.getByTestId('required-headers-textarea')
5569

5670
// Performance Tuning locators
5771
this.workerPoolSizeInput = page.getByLabel(/Worker Pool Size/i)
5872
this.maxRequestBodySizeInput = page.getByLabel(/Max Request Body Size/i)
5973

6074
// Observability locators
6175
this.observabilityToggles = page.locator('button[role="switch"]')
76+
77+
// Pricing Config locators
78+
this.pricingConfigView = page.getByTestId('pricing-config-view')
79+
this.pricingDatasheetUrlInput = page.getByTestId('pricing-datasheet-url-input')
80+
this.pricingForceSyncBtn = page.getByTestId('pricing-force-sync-btn')
81+
this.pricingSaveBtn = page.getByTestId('pricing-save-btn')
6282
}
6383

6484
async goto(path: string): Promise<void> {
@@ -258,6 +278,25 @@ export class ConfigSettingsPage extends BasePage {
258278
return await this.page.getByText(/Rate Limiting/i).isVisible()
259279
}
260280

281+
async toggleEnforceAuthOnInference(): Promise<void> {
282+
await this.enforceAuthOnInferenceSwitch.click()
283+
}
284+
285+
async setRequiredHeaders(value: string): Promise<void> {
286+
await this.requiredHeadersTextarea.clear()
287+
await this.requiredHeadersTextarea.fill(value)
288+
}
289+
290+
async setWorkspaceLoggingHeaders(value: string): Promise<void> {
291+
await this.workspaceLoggingHeadersTextarea.clear()
292+
await this.workspaceLoggingHeadersTextarea.fill(value)
293+
}
294+
295+
async setAsyncJobResultTtl(value: string): Promise<void> {
296+
await this.asyncJobResultTtlInput.clear()
297+
await this.asyncJobResultTtlInput.fill(value)
298+
}
299+
261300
// === Observability Settings Methods ===
262301

263302
async getObservabilityConnectors(): Promise<string[]> {
@@ -278,4 +317,21 @@ export class ConfigSettingsPage extends BasePage {
278317
const toggleSwitch = connectorSection.locator('button[role="switch"]').first()
279318
await toggleSwitch.click()
280319
}
320+
321+
// === Pricing Config Methods ===
322+
323+
async setPricingDatasheetUrl(url: string): Promise<void> {
324+
await this.pricingDatasheetUrlInput.clear()
325+
await this.pricingDatasheetUrlInput.fill(url)
326+
}
327+
328+
async triggerForceSync(): Promise<void> {
329+
await this.pricingForceSyncBtn.click()
330+
await this.waitForSuccessToast()
331+
}
332+
333+
async savePricingConfig(): Promise<void> {
334+
await this.pricingSaveBtn.click()
335+
await this.waitForSuccessToast()
336+
}
281337
}

tests/e2e/features/dashboard/dashboard.spec.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -211,14 +211,11 @@ test.describe('Dashboard', () => {
211211
await waitForNetworkIdle(dashboardPage.page)
212212
await dashboardPage.waitForChartsToLoad()
213213

214-
// Verify page loaded with correct state
214+
// Verify page loaded with correct state from URL
215215
const url = dashboardPage.page.url()
216216
expect(url).toContain('period=7d')
217-
// Chart state may be in URL or DOM - check both
218-
const hasChartInUrl = url.includes('volume_chart=')
219-
const toggleState = await dashboardPage.getChartToggleState(dashboardPage.volumeChartToggle)
220-
// Either URL contains chart state OR DOM has toggle state
221-
expect(hasChartInUrl || toggleState).toBeTruthy()
217+
// volume_chart=line was in the URL - verify it persisted or chart rendered
218+
expect(url).toMatch(/volume_chart=/)
222219
})
223220
})
224221

@@ -233,14 +230,17 @@ test.describe('Dashboard', () => {
233230
const costChartContent = dashboardPage.costChart.locator('canvas, svg')
234231
const modelChartContent = dashboardPage.modelUsageChart.locator('canvas, svg')
235232

236-
// At least one of these should be visible (depends on data availability)
237-
const hasVolumeChart = await volumeChartContent.count() > 0
238-
const hasTokenChart = await tokenChartContent.count() > 0
239-
const hasCostChart = await costChartContent.count() > 0
240-
const hasModelChart = await modelChartContent.count() > 0
241-
242-
// All charts should have rendered content
243-
expect(hasVolumeChart || hasTokenChart || hasCostChart || hasModelChart).toBe(true)
233+
// Each chart card should have canvas or SVG content (chart library renders into these)
234+
const volumeCount = await volumeChartContent.count()
235+
const tokenCount = await tokenChartContent.count()
236+
const costCount = await costChartContent.count()
237+
const modelCount = await modelChartContent.count()
238+
239+
// All four chart cards should have rendered content (count > 0)
240+
expect(volumeCount).toBeGreaterThan(0)
241+
expect(tokenCount).toBeGreaterThan(0)
242+
expect(costCount).toBeGreaterThan(0)
243+
expect(modelCount).toBeGreaterThan(0)
244244
})
245245

246246
test('should show chart legends', async ({ dashboardPage }) => {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { CustomerConfig, TeamConfig } from './pages/governance.page'
2+
3+
export function createTeamData(overrides: Partial<TeamConfig> = {}): TeamConfig {
4+
const timestamp = Date.now()
5+
return {
6+
name: `E2E Team ${timestamp}`,
7+
budget: { maxLimit: 100, resetDuration: '1M' },
8+
...overrides,
9+
}
10+
}
11+
12+
export function createCustomerData(overrides: Partial<CustomerConfig> = {}): CustomerConfig {
13+
const timestamp = Date.now()
14+
return {
15+
name: `E2E Customer ${timestamp}`,
16+
budget: { maxLimit: 50, resetDuration: '1d' },
17+
...overrides,
18+
}
19+
}

0 commit comments

Comments
 (0)