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,683 @@
import { Locator, Page, expect } from '@playwright/test'
import { BasePage } from '../../../core/pages/base.page'
import { fillSelect, waitForNetworkIdle } from '../../../core/utils/test-helpers'
/**
* Provider display names mapping - matches the UI's ProviderLabels
* Used for exact matching when selecting providers in dropdowns
*/
const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
openai: 'OpenAI',
anthropic: 'Anthropic',
azure: 'Azure',
bedrock: 'AWS Bedrock',
cohere: 'Cohere',
vertex: 'Vertex AI',
mistral: 'Mistral AI',
ollama: 'Ollama',
groq: 'Groq',
gemini: 'Gemini',
openrouter: 'OpenRouter',
huggingface: 'HuggingFace',
cerebras: 'Cerebras',
perplexity: 'Perplexity',
elevenlabs: 'Elevenlabs',
parasail: 'Parasail',
sgl: 'SGLang',
nebius: 'Nebius Token Factory',
xai: 'xAI',
}
/**
* Escape regex special characters in a string
*/
function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
/**
* Budget configuration
*/
export interface BudgetConfig {
maxLimit: number
resetDuration?: string
}
/**
* Rate limit configuration
*/
export interface RateLimitConfig {
tokenMaxLimit?: number
tokenResetDuration?: string
requestMaxLimit?: number
requestResetDuration?: string
}
/**
* Provider configuration for virtual key
*/
export interface ProviderConfig {
provider: string
weight?: number
allowedModels?: string[]
keyIds?: string[]
budget?: BudgetConfig
rateLimit?: RateLimitConfig
}
/**
* Virtual key configuration
*/
export interface VirtualKeyConfig {
name: string
description?: string
isActive?: boolean
providerConfigs?: ProviderConfig[]
budget?: BudgetConfig
rateLimit?: RateLimitConfig
entityType?: 'none' | 'team' | 'customer'
teamId?: string
customerId?: string
}
/**
* Page object for the Virtual Keys page
*/
export class VirtualKeysPage extends BasePage {
// Main page elements
readonly createBtn: Locator
readonly table: Locator
readonly emptyState: Locator
// Virtual key sheet elements
readonly sheet: Locator
readonly nameInput: Locator
readonly descriptionInput: Locator
readonly isActiveToggle: Locator
readonly providerSelect: Locator
readonly saveBtn: Locator
readonly cancelBtn: Locator
constructor(page: Page) {
super(page)
// Main page elements
this.createBtn = page.getByTestId('create-vk-btn')
this.table = page.getByTestId('vk-table')
this.emptyState = page.getByTestId('virtual-keys-empty-state')
// Virtual key sheet elements
this.sheet = page.getByTestId('vk-sheet')
this.nameInput = page.getByTestId('vk-name-input')
this.descriptionInput = page.getByTestId('vk-description-input')
this.isActiveToggle = page.getByTestId('vk-is-active-toggle')
this.providerSelect = page.getByTestId('vk-provider-select')
this.saveBtn = page.getByTestId('vk-save-btn')
this.cancelBtn = page.getByTestId('vk-cancel-btn')
}
/**
* Navigate to the virtual keys page
*/
async goto(): Promise<void> {
await this.page.goto('/workspace/virtual-keys')
await waitForNetworkIdle(this.page)
}
/**
* Get virtual key row locator by name
*/
getVirtualKeyRow(name: string): Locator {
return this.page.getByTestId(`vk-row-${name}`)
}
/**
* Check if a virtual key exists in the table
*/
async virtualKeyExists(name: string): Promise<boolean> {
const row = this.getVirtualKeyRow(name)
// Use count() to check if element exists in DOM (doesn't require visibility)
const count = await row.count()
return count > 0
}
/**
* Check if the key value is revealed (visible) or masked in the table.
* When masked, the display shows bullets (•); when revealed, it shows the full key.
*/
async isKeyRevealed(name: string): Promise<boolean> {
const row = this.getVirtualKeyRow(name)
const keyCell = row.getByTestId('vk-key-value')
await keyCell.waitFor({ state: 'visible', timeout: 5000 })
const text = (await keyCell.textContent())?.trim() ?? ''
// Masked keys contain bullet character; revealed keys do not
return text.length > 0 && !text.includes('•')
}
/**
* Create a new virtual key
*/
async createVirtualKey(config: VirtualKeyConfig): Promise<void> {
// Click create button
await this.createBtn.click()
// Wait for sheet to appear and animation to complete
await expect(this.sheet).toBeVisible()
await this.waitForSheetAnimation()
// Fill basic information using keyboard navigation
await this.nameInput.focus()
await this.page.keyboard.type(config.name)
if (config.description) {
await this.page.keyboard.press('Tab') // Move to description
await this.page.keyboard.type(config.description)
}
// Set active state if specified (default is true, so only toggle if we want inactive)
if (config.isActive === false) {
await this.isActiveToggle.focus()
await this.page.keyboard.press('Space') // Toggle the switch
}
// Add provider configurations
if (config.providerConfigs && config.providerConfigs.length > 0) {
for (const providerConfig of config.providerConfigs) {
await this.addProviderConfig(providerConfig)
}
}
// Set budget if specified
if (config.budget) {
await this.setBudget(config.budget)
}
// Set rate limits if specified
if (config.rateLimit) {
await this.setRateLimit(config.rateLimit)
}
// Set entity assignment if specified
if (config.entityType && config.entityType !== 'none') {
await this.setEntityAssignment(config.entityType, config.teamId, config.customerId)
}
// Save the virtual key by clicking the save button
await this.saveBtn.click()
// Wait for success toast
await this.waitForSuccessToast()
// Wait for toasts to disappear before continuing
await this.dismissToasts()
// Wait for sheet to close
await expect(this.sheet).not.toBeVisible({ timeout: 5000 })
// Wait for the new row to appear in the table (ensures table has refreshed)
const row = this.getVirtualKeyRow(config.name)
await row.waitFor({ state: 'attached', timeout: 10000 })
await row.scrollIntoViewIfNeeded()
}
/**
* Add a provider configuration to the virtual key form
*/
private async addProviderConfig(config: ProviderConfig): Promise<void> {
// Click the provider select dropdown
await this.providerSelect.click()
// Wait for dropdown content
await this.page.waitForSelector('[role="listbox"]', { timeout: 5000 })
// Get display name - use mapping for known providers, otherwise use exact name
const displayName = PROVIDER_DISPLAY_NAMES[config.provider.toLowerCase()] || config.provider
// First try exact match for base providers (e.g., "OpenAI", "Anthropic")
let option = this.page.getByRole('option', { name: displayName, exact: true })
if (await option.count() === 0) {
// Fallback: try partial match for custom providers (contains provider name)
// This handles custom providers like "test-anthropic-1234567890"
option = this.page.getByRole('option').filter({
hasText: new RegExp(escapeRegExp(config.provider), 'i')
}).first()
}
// Verify we found a matching option
const optionCount = await option.count()
if (optionCount === 0) {
throw new Error(`No provider option found matching "${config.provider}" (display name: "${displayName}")`)
}
await option.click()
// Wait for dropdown to close after selection
await this.page.waitForSelector('[role="listbox"]', { state: 'hidden', timeout: 5000 })
}
/**
* Set budget configuration in the form
*/
private async setBudget(budget: BudgetConfig): Promise<void> {
// Find budget max limit input and fill (fill() clears and sets atomically)
const budgetInput = this.page.locator('#budgetMaxLimit')
await budgetInput.fill(String(budget.maxLimit))
// Set reset duration if specified - skip for now as default is fine
// The reset duration select is complex and default "Monthly" is usually correct
}
/**
* Set rate limit configuration in the form
*/
private async setRateLimit(rateLimit: RateLimitConfig): Promise<void> {
// Set token limits (fill() clears and sets atomically)
if (rateLimit.tokenMaxLimit !== undefined) {
const tokenInput = this.page.locator('#tokenMaxLimit')
await tokenInput.fill(String(rateLimit.tokenMaxLimit))
}
// Set request limits (fill() clears and sets atomically)
if (rateLimit.requestMaxLimit !== undefined) {
const requestInput = this.page.locator('#requestMaxLimit')
await requestInput.fill(String(rateLimit.requestMaxLimit))
}
}
/**
* Set entity assignment (team or customer)
*/
private async setEntityAssignment(
entityType: 'team' | 'customer',
teamId?: string,
customerId?: string
): Promise<void> {
// Find and click entity type select
const entityTypeSelect = this.page.locator('[data-testid="vk-entity-type-select"]')
if (await entityTypeSelect.isVisible()) {
await fillSelect(
this.page,
'[data-testid="vk-entity-type-select"]',
entityType === 'team' ? 'Assign to Team' : 'Assign to Customer'
)
// Select team or customer
if (entityType === 'team' && teamId) {
const teamSelect = this.page.locator('[data-testid="vk-team-select"]')
if (await teamSelect.isVisible()) {
await fillSelect(this.page, '[data-testid="vk-team-select"]', teamId)
}
} else if (entityType === 'customer' && customerId) {
const customerSelect = this.page.locator('[data-testid="vk-customer-select"]')
if (await customerSelect.isVisible()) {
await fillSelect(this.page, '[data-testid="vk-customer-select"]', customerId)
}
}
}
}
/**
* Edit an existing virtual key
*/
async editVirtualKey(name: string, updates: Partial<VirtualKeyConfig>): Promise<void> {
// Wait for any existing toasts to disappear
await this.forceCloseToasts()
// Find and click the edit button using data-testid
const editBtn = this.page.getByTestId(`vk-edit-btn-${name}`)
await editBtn.waitFor({ state: 'visible', timeout: 10000 })
await editBtn.scrollIntoViewIfNeeded()
await editBtn.click()
// Wait for sheet to appear and animation to complete
await expect(this.sheet).toBeVisible()
await this.waitForSheetAnimation()
// Update name using clear() and fill() for cross-platform compatibility
if (updates.name) {
await this.nameInput.clear()
await this.nameInput.fill(updates.name)
}
// Update description using clear() and fill() for cross-platform compatibility
if (updates.description !== undefined) {
await this.descriptionInput.clear()
if (updates.description) {
await this.descriptionInput.fill(updates.description)
}
}
// Update toggle using click() and data-state attribute for reliability
if (updates.isActive !== undefined) {
// Check current state using data-state attribute (Radix Switch)
const isCurrentlyChecked = await this.isActiveToggle.getAttribute('data-state') === 'checked'
if (isCurrentlyChecked !== updates.isActive) {
await this.isActiveToggle.click()
}
}
if (updates.budget) {
await this.setBudget(updates.budget)
}
if (updates.rateLimit) {
await this.setRateLimit(updates.rateLimit)
}
// Save changes by clicking the save button
await this.saveBtn.click()
// Wait for success toast
await this.waitForSuccessToast()
// Wait for toasts to disappear before continuing
await this.dismissToasts()
// Check if sheet is still visible - it may not auto-close
const isSheetVisible = await this.sheet.isVisible().catch(() => false)
if (isSheetVisible) {
// Try clicking the close button or pressing Escape
const closeBtn = this.sheet.locator('button[aria-label*="close"], button:has(svg.lucide-x)').first()
if (await closeBtn.isVisible().catch(() => false)) {
await closeBtn.click()
} else {
await this.page.keyboard.press('Escape')
}
await expect(this.sheet).not.toBeVisible({ timeout: 5000 })
}
}
/**
* Poll until the virtual key row disappears from the table (e.g. after delete or refetch).
* Polls so we don't rely on a stale locator.
*/
async waitForVirtualKeyGone(name: string, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
if ((await this.getVirtualKeyRow(name).count()) === 0) return true
await this.page.waitForTimeout(500)
}
return false
}
async deleteVirtualKey(name: string, options?: { requireToast?: boolean }): Promise<void> {
// Check if virtual key exists first
const exists = await this.virtualKeyExists(name)
if (!exists) {
// Already deleted or doesn't exist, nothing to do
return
}
// Wait for any existing toasts to disappear
await this.forceCloseToasts()
// Find the delete button using data-testid (scroll row into view in case table just loaded)
const row = this.getVirtualKeyRow(name)
await row.scrollIntoViewIfNeeded().catch(() => {})
await this.page.waitForTimeout(300)
const deleteBtn = this.page.getByTestId(`vk-delete-btn-${name}`)
// Check if button exists; if not, give table a moment and re-check once
let btnCount = await deleteBtn.count()
if (btnCount === 0) {
await this.page.waitForTimeout(800)
btnCount = await deleteBtn.count()
}
if (btnCount === 0) {
const stillExists = await this.virtualKeyExists(name)
if (!stillExists) return
throw new Error(`Delete button not found for virtual key: ${name}`)
}
// Check if button is disabled
const isDisabled = await deleteBtn.isDisabled().catch(() => false)
if (isDisabled) {
throw new Error(`Delete button is disabled for virtual key: ${name} (likely due to RBAC permissions)`)
}
await deleteBtn.waitFor({ state: 'visible', timeout: 10000 })
await deleteBtn.scrollIntoViewIfNeeded()
await deleteBtn.click()
// Wait for confirmation dialog and confirm deletion (match "Delete" or "Deleting...")
const confirmDialog = this.page.locator('[role="alertdialog"]')
await confirmDialog.waitFor({ state: 'visible', timeout: 5000 })
const confirmBtn = confirmDialog.getByRole('button', { name: /Delete/i })
await confirmBtn.waitFor({ state: 'visible', timeout: 2000 })
// Wait for DELETE API response
const deleteResponsePromise = this.page.waitForResponse(
(response) => {
const url = response.url()
return url.includes('/api/virtual-keys/') && response.request().method() === 'DELETE'
},
{ timeout: 15000 }
)
await confirmBtn.click()
const deleteResponse = await deleteResponsePromise.catch((err) => {
console.warn(`[deleteVirtualKey] No DELETE response captured for "${name}": ${err}`)
return null
})
if (deleteResponse && !deleteResponse.ok()) {
console.warn(`[deleteVirtualKey] DELETE responded with ${deleteResponse.status()} for "${name}"`)
}
// Wait for table to refetch and row to disappear (poll fresh locator; avoid stale row reference)
const gone = await this.waitForVirtualKeyGone(name, 20000)
if (!gone) {
throw new Error(`Virtual key "${name}" still visible after delete`)
}
// Optionally wait for success toast (skip in cleanup to avoid false failures)
if (options?.requireToast !== false) {
await this.getToast().waitFor({ state: 'visible', timeout: 5000 }).catch(() => {})
}
await this.dismissToasts()
}
/**
* Click on a virtual key to view/edit details (opens via edit button)
*/
async viewVirtualKey(name: string): Promise<void> {
// Wait for any existing toasts to disappear
await this.forceCloseToasts()
// Use the edit button to open the detail sheet
const editBtn = this.page.getByTestId(`vk-edit-btn-${name}`)
await editBtn.waitFor({ state: 'visible', timeout: 10000 })
await editBtn.scrollIntoViewIfNeeded()
await editBtn.click()
// Wait for detail sheet to appear
await expect(this.sheet).toBeVisible({ timeout: 5000 })
}
/**
* Get the count of virtual keys in the table
*/
async getVirtualKeyCount(): Promise<number> {
const rows = this.table.locator('tbody tr')
const count = await rows.count()
if (count === 0) {
return 0
}
// Check if it's the empty state row
const firstRowText = await rows.first().textContent()
if (firstRowText?.includes('No virtual keys found')) {
return 0
}
return count
}
/**
* Copy virtual key value to clipboard
*/
async copyVirtualKeyValue(name: string): Promise<void> {
// Find and click the copy button using data-testid
const copyBtn = this.page.getByTestId(`vk-copy-btn-${name}`)
await copyBtn.waitFor({ state: 'attached', timeout: 10000 })
await copyBtn.scrollIntoViewIfNeeded()
await copyBtn.click()
await this.waitForSuccessToast('Copied')
}
/**
* Toggle key visibility (show/hide)
*/
async toggleKeyVisibility(name: string): Promise<void> {
// Find and click the visibility toggle button using data-testid
const toggleBtn = this.page.getByTestId(`vk-visibility-btn-${name}`)
await toggleBtn.waitFor({ state: 'attached', timeout: 10000 })
await toggleBtn.scrollIntoViewIfNeeded()
await toggleBtn.click()
}
/**
* Close any open sheet/dialog
*/
async closeSheet(): Promise<void> {
const isSheetVisible = await this.sheet.isVisible().catch(() => false)
if (isSheetVisible) {
// We have to click on the close button to close the sheet
const closeBtn = this.sheet.locator('button[aria-label*="close"], button:has(svg.lucide-x)').first()
if (await closeBtn.isVisible().catch(() => false)) {
await closeBtn.click()
} else {
await this.page.keyboard.press('Escape')
}
await expect(this.sheet).not.toBeVisible({ timeout: 5000 }).catch(() => {})
}
}
/**
* Get all virtual key names from the table
*/
async getAllVirtualKeyNames(): Promise<string[]> {
const names: string[] = []
const count = await this.getVirtualKeyCount()
if (count === 0) return names
// Find all delete buttons which have the VK name in their test-id
const deleteButtons = this.page.locator('[data-testid^="vk-delete-btn-"]')
const buttonCount = await deleteButtons.count()
for (let i = 0; i < buttonCount; i++) {
const testId = await deleteButtons.nth(i).getAttribute('data-testid')
if (testId) {
// Extract name from test-id: "vk-delete-btn-{name}"
const name = testId.replace('vk-delete-btn-', '')
names.push(name)
}
}
return names
}
/**
* Clean up all virtual keys (delete all)
*/
async cleanupAllVirtualKeys(): Promise<void> {
// First close any open sheet
await this.closeSheet()
// Wait for any toasts to clear
await this.dismissToasts()
// Keep trying until no more VKs exist
let attempts = 0
const maxAttempts = 10 // Prevent infinite loops
while (attempts < maxAttempts) {
// Get current VK names (refresh the list each iteration)
const names = await this.getAllVirtualKeyNames()
if (names.length === 0) {
// No more VKs to delete
break
}
// Delete each one
for (const name of names) {
try {
// Check if VK still exists before trying to delete
const exists = await this.virtualKeyExists(name)
if (!exists) {
// Already deleted, skip
continue
}
// Make sure sheet is closed before each delete
await this.closeSheet()
await this.deleteVirtualKey(name)
// Wait a bit for table to refresh
await this.page.waitForTimeout(500)
} catch (error) {
// If delete fails, try to close sheet and continue
await this.closeSheet()
const errorMsg = error instanceof Error ? error.message : String(error)
console.log(`Failed to delete virtual key: ${name} - ${errorMsg}`)
// Continue with next VK
}
}
attempts++
// Wait a bit before next iteration to allow table to refresh
await this.page.waitForTimeout(1000)
}
if (attempts >= maxAttempts) {
const remainingNames = await this.getAllVirtualKeyNames()
if (remainingNames.length > 0) {
console.log(`Warning: Could not delete all virtual keys after ${maxAttempts} attempts. Remaining: ${remainingNames.join(', ')}`)
}
}
}
/**
* Clean up specific virtual keys by name
*/
async cleanupVirtualKeys(names: string[]): Promise<void> {
if (names.length === 0) return
// Ensure we're on the virtual keys list with a fresh load so table is ready
await this.goto()
await this.closeSheet()
await this.dismissToasts()
await this.table.waitFor({ state: 'visible', timeout: 10000 }).catch(() => {})
await this.page.waitForTimeout(500)
for (const name of names) {
const tryDelete = async (): Promise<void> => {
const exists = await this.virtualKeyExists(name)
if (!exists) return
await this.closeSheet()
await this.deleteVirtualKey(name, { requireToast: false })
}
try {
await tryDelete()
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
console.error(`[CLEANUP ERROR] Failed to delete virtual key: ${name} - ${errorMsg}`)
await this.closeSheet()
await this.page.waitForTimeout(1000)
try {
await tryDelete()
} catch (retryError) {
const retryMsg = retryError instanceof Error ? retryError.message : String(retryError)
console.error(`[CLEANUP ERROR] Retry failed for virtual key: ${name} - ${retryMsg}`)
}
}
}
}
}

View File

@@ -0,0 +1,178 @@
import { VirtualKeyConfig, ProviderConfig, BudgetConfig, RateLimitConfig } from './pages/virtual-keys.page'
/**
* Factory function to create virtual key test data
*/
export function createVirtualKeyData(overrides: Partial<VirtualKeyConfig> = {}): VirtualKeyConfig {
const timestamp = Date.now()
return {
name: `Test VK ${timestamp}`,
description: 'E2E test virtual key',
isActive: true,
providerConfigs: [],
...overrides,
}
}
/**
* Factory function to create virtual key with single provider
*/
export function createVirtualKeyWithProvider(
provider: string,
vkOverrides: Partial<VirtualKeyConfig> = {}
): VirtualKeyConfig {
const timestamp = Date.now()
return {
name: `Test VK ${provider} ${timestamp}`,
description: `Virtual key for ${provider}`,
isActive: true,
providerConfigs: [
{
provider,
weight: 1.0,
allowedModels: ['*'],
keyIds: ['*'],
},
],
...vkOverrides,
}
}
/**
* Factory function to create virtual key with budget
*/
export function createVirtualKeyWithBudget(
budget: BudgetConfig,
vkOverrides: Partial<VirtualKeyConfig> = {}
): VirtualKeyConfig {
const timestamp = Date.now()
return {
name: `Test VK Budget ${timestamp}`,
description: 'Virtual key with budget configuration',
isActive: true,
budget,
...vkOverrides,
}
}
/**
* Factory function to create virtual key with rate limits
*/
export function createVirtualKeyWithRateLimit(
rateLimit: RateLimitConfig,
vkOverrides: Partial<VirtualKeyConfig> = {}
): VirtualKeyConfig {
const timestamp = Date.now()
return {
name: `Test VK RateLimit ${timestamp}`,
description: 'Virtual key with rate limit configuration',
isActive: true,
rateLimit,
...vkOverrides,
}
}
/**
* Factory function to create virtual key with multiple providers
*/
export function createVirtualKeyWithMultipleProviders(
providers: string[],
vkOverrides: Partial<VirtualKeyConfig> = {}
): VirtualKeyConfig {
const timestamp = Date.now()
const weight = 1.0 / providers.length
return {
name: `Test VK Multi ${timestamp}`,
description: `Virtual key with ${providers.length} providers`,
isActive: true,
providerConfigs: providers.map((provider) => ({
provider,
weight,
allowedModels: ['*'],
keyIds: ['*'],
})),
...vkOverrides,
}
}
/**
* Factory function to create provider config
*/
export function createProviderConfig(overrides: Partial<ProviderConfig> = {}): ProviderConfig {
return {
provider: 'openai',
weight: 1.0,
allowedModels: ['*'],
keyIds: ['*'],
...overrides,
}
}
/**
* Sample budget configurations
*/
export const SAMPLE_BUDGETS: Record<string, BudgetConfig> = {
small: {
maxLimit: 10,
resetDuration: '1M',
},
medium: {
maxLimit: 100,
resetDuration: '1M',
},
large: {
maxLimit: 1000,
resetDuration: '1M',
},
daily: {
maxLimit: 50,
resetDuration: '1d',
},
weekly: {
maxLimit: 200,
resetDuration: '1w',
},
}
/**
* Sample rate limit configurations
*/
export const SAMPLE_RATE_LIMITS: Record<string, RateLimitConfig> = {
conservative: {
tokenMaxLimit: 10000,
tokenResetDuration: '1h',
requestMaxLimit: 100,
requestResetDuration: '1h',
},
moderate: {
tokenMaxLimit: 100000,
tokenResetDuration: '1h',
requestMaxLimit: 1000,
requestResetDuration: '1h',
},
aggressive: {
tokenMaxLimit: 1000000,
tokenResetDuration: '1h',
requestMaxLimit: 10000,
requestResetDuration: '1h',
},
tokenOnly: {
tokenMaxLimit: 50000,
tokenResetDuration: '1h',
},
requestOnly: {
requestMaxLimit: 500,
requestResetDuration: '1h',
},
}
/**
* Reset duration options
*/
export const RESET_DURATIONS = [
{ label: '1 Hour', value: '1h' },
{ label: '1 Day', value: '1d' },
{ label: '1 Week', value: '1w' },
{ label: '1 Month', value: '1M' },
] as const

View File

@@ -0,0 +1,557 @@
import { expect, test } from '../../core/fixtures/base.fixture'
import {
createVirtualKeyData,
createVirtualKeyWithBudget,
createVirtualKeyWithMultipleProviders,
createVirtualKeyWithProvider,
createVirtualKeyWithRateLimit,
SAMPLE_BUDGETS,
SAMPLE_RATE_LIMITS,
} from './virtual-keys.data'
// Track created VKs for cleanup
const createdVKs: string[] = []
test.describe('Virtual Keys', () => {
test.beforeEach(async ({ virtualKeysPage }) => {
await virtualKeysPage.goto()
})
test.afterEach(async ({ virtualKeysPage }) => {
// Close any open sheets first
await virtualKeysPage.closeSheet()
// Clean up all tracked VKs
if (createdVKs.length > 0) {
await virtualKeysPage.cleanupVirtualKeys([...createdVKs])
createdVKs.length = 0 // Clear the array
}
})
test.describe('Virtual Key Creation', () => {
test('should display create virtual key button', async ({ virtualKeysPage }) => {
await expect(virtualKeysPage.createBtn).toBeVisible()
})
test('should open virtual key creation sheet', async ({ virtualKeysPage }) => {
await virtualKeysPage.createBtn.click()
// Verify sheet is visible
await expect(virtualKeysPage.sheet).toBeVisible()
// Verify form fields are present
await expect(virtualKeysPage.nameInput).toBeVisible()
await expect(virtualKeysPage.descriptionInput).toBeVisible()
})
test('should create a basic virtual key', async ({ virtualKeysPage }) => {
const vkData = createVirtualKeyData({
name: `Basic VK ${Date.now()}`,
description: 'A basic virtual key for testing',
})
createdVKs.push(vkData.name)
await virtualKeysPage.createVirtualKey(vkData)
// Verify virtual key appears in table
const vkExists = await virtualKeysPage.virtualKeyExists(vkData.name)
expect(vkExists).toBe(true)
})
test('should create virtual key with single provider', async ({ virtualKeysPage }) => {
const vkData = createVirtualKeyWithProvider('openai', {
name: `OpenAI VK ${Date.now()}`,
})
createdVKs.push(vkData.name)
await virtualKeysPage.createVirtualKey(vkData)
const vkExists = await virtualKeysPage.virtualKeyExists(vkData.name)
expect(vkExists).toBe(true)
})
test('should create inactive virtual key', async ({ virtualKeysPage }) => {
const vkData = createVirtualKeyData({
name: `Inactive VK ${Date.now()}`,
isActive: false,
})
createdVKs.push(vkData.name)
await virtualKeysPage.createVirtualKey(vkData)
const vkExists = await virtualKeysPage.virtualKeyExists(vkData.name)
expect(vkExists).toBe(true)
})
test('should cancel virtual key creation', async ({ virtualKeysPage }) => {
await virtualKeysPage.createBtn.click()
await expect(virtualKeysPage.sheet).toBeVisible()
// Fill some data
const testName = `Cancelled VK ${Date.now()}`
await virtualKeysPage.nameInput.fill(testName)
// Cancel
await virtualKeysPage.cancelBtn.click()
// Sheet should close
await expect(virtualKeysPage.sheet).not.toBeVisible()
// Virtual key should not exist
const vkExists = await virtualKeysPage.virtualKeyExists(testName)
expect(vkExists).toBe(false)
})
})
test.describe('Virtual Key with Budget', () => {
test('should create virtual key with small budget', async ({ virtualKeysPage }) => {
const vkData = createVirtualKeyWithBudget(SAMPLE_BUDGETS.small, {
name: `Small Budget VK ${Date.now()}`,
})
createdVKs.push(vkData.name)
await virtualKeysPage.createVirtualKey(vkData)
const vkExists = await virtualKeysPage.virtualKeyExists(vkData.name)
expect(vkExists).toBe(true)
// Verify budget was saved correctly
await virtualKeysPage.viewVirtualKey(vkData.name)
await virtualKeysPage.waitForSheetAnimation()
const budgetInput = virtualKeysPage.page.locator('#budgetMaxLimit')
await expect(budgetInput).toHaveValue(String(SAMPLE_BUDGETS.small.maxLimit))
await virtualKeysPage.closeSheet()
})
test('should create virtual key with medium budget', async ({ virtualKeysPage }) => {
const vkData = createVirtualKeyWithBudget(SAMPLE_BUDGETS.medium, {
name: `Medium Budget VK ${Date.now()}`,
})
createdVKs.push(vkData.name)
await virtualKeysPage.createVirtualKey(vkData)
const vkExists = await virtualKeysPage.virtualKeyExists(vkData.name)
expect(vkExists).toBe(true)
})
test('should create virtual key with daily budget', async ({ virtualKeysPage }) => {
const vkData = createVirtualKeyWithBudget(SAMPLE_BUDGETS.daily, {
name: `Daily Budget VK ${Date.now()}`,
})
createdVKs.push(vkData.name)
await virtualKeysPage.createVirtualKey(vkData)
const vkExists = await virtualKeysPage.virtualKeyExists(vkData.name)
expect(vkExists).toBe(true)
})
})
test.describe('Virtual Key with Rate Limits', () => {
test('should create virtual key with token rate limit', async ({ virtualKeysPage }) => {
const vkData = createVirtualKeyWithRateLimit(SAMPLE_RATE_LIMITS.tokenOnly, {
name: `Token Limit VK ${Date.now()}`,
})
createdVKs.push(vkData.name)
await virtualKeysPage.createVirtualKey(vkData)
const vkExists = await virtualKeysPage.virtualKeyExists(vkData.name)
expect(vkExists).toBe(true)
// Verify rate limit was saved correctly
await virtualKeysPage.viewVirtualKey(vkData.name)
await virtualKeysPage.waitForSheetAnimation()
const tokenLimitInput = virtualKeysPage.page.locator('#tokenMaxLimit')
await expect(tokenLimitInput).toHaveValue(String(SAMPLE_RATE_LIMITS.tokenOnly.tokenMaxLimit))
await virtualKeysPage.closeSheet()
})
test('should create virtual key with request rate limit', async ({ virtualKeysPage }) => {
const vkData = createVirtualKeyWithRateLimit(SAMPLE_RATE_LIMITS.requestOnly, {
name: `Request Limit VK ${Date.now()}`,
})
createdVKs.push(vkData.name)
await virtualKeysPage.createVirtualKey(vkData)
const vkExists = await virtualKeysPage.virtualKeyExists(vkData.name)
expect(vkExists).toBe(true)
})
test('should create virtual key with combined rate limits', async ({ virtualKeysPage }) => {
const vkData = createVirtualKeyWithRateLimit(SAMPLE_RATE_LIMITS.conservative, {
name: `Combined Limits VK ${Date.now()}`,
})
createdVKs.push(vkData.name)
await virtualKeysPage.createVirtualKey(vkData)
const vkExists = await virtualKeysPage.virtualKeyExists(vkData.name)
expect(vkExists).toBe(true)
})
})
test.describe('Virtual Key with Multiple Providers', () => {
test('should create virtual key with two providers', async ({ virtualKeysPage }) => {
const vkData = createVirtualKeyWithMultipleProviders(['openai', 'anthropic'], {
name: `Multi Provider VK ${Date.now()}`,
})
createdVKs.push(vkData.name)
await virtualKeysPage.createVirtualKey(vkData)
const vkExists = await virtualKeysPage.virtualKeyExists(vkData.name)
expect(vkExists).toBe(true)
})
})
test.describe('Virtual Key with Budget and Rate Limits', () => {
test('should create virtual key with budget and rate limits', async ({ virtualKeysPage }) => {
const vkData = createVirtualKeyData({
name: `Full Config VK ${Date.now()}`,
description: 'Virtual key with all configurations',
isActive: true,
budget: SAMPLE_BUDGETS.medium,
rateLimit: SAMPLE_RATE_LIMITS.moderate,
})
createdVKs.push(vkData.name)
await virtualKeysPage.createVirtualKey(vkData)
const vkExists = await virtualKeysPage.virtualKeyExists(vkData.name)
expect(vkExists).toBe(true)
})
})
})
// Track created VKs for management tests
const managementVKs: string[] = []
test.describe('Virtual Key Management', () => {
test.beforeEach(async ({ virtualKeysPage }) => {
await virtualKeysPage.goto()
})
test.afterEach(async ({ virtualKeysPage }) => {
// Close any open sheets first
await virtualKeysPage.closeSheet()
// Clean up all tracked VKs
if (managementVKs.length > 0) {
await virtualKeysPage.cleanupVirtualKeys([...managementVKs])
managementVKs.length = 0
}
})
test('should edit virtual key name', async ({ virtualKeysPage }) => {
// First create a virtual key
const originalName = `Edit Test VK ${Date.now()}`
const vkData = createVirtualKeyData({ name: originalName })
await virtualKeysPage.createVirtualKey(vkData)
// Now edit it
const updatedName = `${originalName} Updated`
managementVKs.push(updatedName) // Track the updated name for cleanup
await virtualKeysPage.editVirtualKey(originalName, {
name: updatedName,
})
// Verify updated name exists
const vkExists = await virtualKeysPage.virtualKeyExists(updatedName)
expect(vkExists).toBe(true)
})
test('should edit virtual key description', async ({ virtualKeysPage }) => {
const vkName = `Desc Edit VK ${Date.now()}`
const vkData = createVirtualKeyData({
name: vkName,
description: 'Original description',
})
managementVKs.push(vkName)
await virtualKeysPage.createVirtualKey(vkData)
await virtualKeysPage.editVirtualKey(vkName, {
description: 'Updated description for testing',
})
// Virtual key should still exist
const vkExists = await virtualKeysPage.virtualKeyExists(vkName)
expect(vkExists).toBe(true)
})
test('should toggle virtual key active state', async ({ virtualKeysPage }) => {
const vkName = `Toggle Active VK ${Date.now()}`
const vkData = createVirtualKeyData({
name: vkName,
isActive: true,
})
managementVKs.push(vkName)
await virtualKeysPage.createVirtualKey(vkData)
// Toggle to inactive
await virtualKeysPage.editVirtualKey(vkName, {
isActive: false,
})
const vkExists = await virtualKeysPage.virtualKeyExists(vkName)
expect(vkExists).toBe(true)
})
test('should delete virtual key', async ({ virtualKeysPage }) => {
const vkName = `Delete Test VK ${Date.now()}`
const vkData = createVirtualKeyData({ name: vkName })
await virtualKeysPage.createVirtualKey(vkData)
// Verify it exists
let vkExists = await virtualKeysPage.virtualKeyExists(vkName)
expect(vkExists).toBe(true)
// Delete it (this is the test - no need to track for cleanup)
await virtualKeysPage.deleteVirtualKey(vkName)
// Verify it's gone
vkExists = await virtualKeysPage.virtualKeyExists(vkName)
expect(vkExists).toBe(false)
})
test('should view virtual key details', async ({ virtualKeysPage }) => {
const vkName = `View Details VK ${Date.now()}`
const vkData = createVirtualKeyData({
name: vkName,
description: 'Detailed description for viewing',
})
managementVKs.push(vkName)
await virtualKeysPage.createVirtualKey(vkData)
// Click to view details
await virtualKeysPage.viewVirtualKey(vkName)
// Detail sheet should be visible with correct content
await expect(virtualKeysPage.sheet).toBeVisible()
await expect(virtualKeysPage.nameInput).toHaveValue(vkName)
await expect(virtualKeysPage.descriptionInput).toHaveValue('Detailed description for viewing')
// Close the sheet (will be handled by afterEach if not)
await virtualKeysPage.closeSheet()
})
test('should copy virtual key value', async ({ virtualKeysPage }) => {
const vkName = `Copy Value VK ${Date.now()}`
const vkData = createVirtualKeyData({ name: vkName })
managementVKs.push(vkName)
await virtualKeysPage.createVirtualKey(vkData)
// Copy the key value - method waits for success toast
await virtualKeysPage.copyVirtualKeyValue(vkName)
// Verify copy succeeded: row still exists and key is intact
const vkExists = await virtualKeysPage.virtualKeyExists(vkName)
expect(vkExists).toBe(true)
})
test('should toggle key visibility', async ({ virtualKeysPage }) => {
const vkName = `Toggle Visibility VK ${Date.now()}`
const vkData = createVirtualKeyData({ name: vkName })
managementVKs.push(vkName)
await virtualKeysPage.createVirtualKey(vkData)
// Initially key is masked
let isRevealed = await virtualKeysPage.isKeyRevealed(vkName)
expect(isRevealed).toBe(false)
// Toggle visibility (show key)
await virtualKeysPage.toggleKeyVisibility(vkName)
isRevealed = await virtualKeysPage.isKeyRevealed(vkName)
expect(isRevealed).toBe(true)
// Toggle again (hide key)
await virtualKeysPage.toggleKeyVisibility(vkName)
isRevealed = await virtualKeysPage.isKeyRevealed(vkName)
expect(isRevealed).toBe(false)
})
})
// Track VKs created in Virtual Keys Table tests for cleanup
const tableTestVKs: string[] = []
test.describe('Virtual Keys Table', () => {
test.beforeEach(async ({ virtualKeysPage }) => {
await virtualKeysPage.goto()
})
test.afterEach(async ({ virtualKeysPage }) => {
await virtualKeysPage.closeSheet()
if (tableTestVKs.length > 0) {
await virtualKeysPage.cleanupVirtualKeys([...tableTestVKs])
tableTestVKs.length = 0
}
})
test('should display virtual keys table', async ({ virtualKeysPage }) => {
await virtualKeysPage.page.getByRole('heading', { name: /Virtual Keys/i }).or(virtualKeysPage.emptyState).first().waitFor({ state: 'visible', timeout: 10000 })
const hadTable = await virtualKeysPage.table.isVisible().catch(() => false)
if (!hadTable) {
await expect(virtualKeysPage.emptyState).toBeVisible({ timeout: 10000 })
} else {
await expect(virtualKeysPage.table).toBeVisible({ timeout: 10000 })
}
const vkData = createVirtualKeyData({ name: `Table test VK ${Date.now()}`, description: 'For table display test' })
tableTestVKs.push(vkData.name)
await virtualKeysPage.createVirtualKey(vkData)
await expect(virtualKeysPage.table).toBeVisible({ timeout: 10000 })
await expect(virtualKeysPage.table.locator('th', { hasText: 'Name' })).toBeVisible()
await expect(virtualKeysPage.table.locator('th', { hasText: 'Key' })).toBeVisible()
})
test('should show empty state when no virtual keys', async ({ virtualKeysPage }) => {
await virtualKeysPage.page.getByRole('heading', { name: /Virtual Keys/i }).or(virtualKeysPage.emptyState).first().waitFor({ state: 'visible', timeout: 10000 })
const tableVisible = await virtualKeysPage.table.isVisible().catch(() => false)
if (tableVisible) {
test.skip(true, 'Pre-existing virtual keys found; empty-state assertion requires isolated data.')
return
}
await expect(virtualKeysPage.emptyState).toBeVisible({ timeout: 10000 })
})
})
test.describe('Form Validation', () => {
test.beforeEach(async ({ virtualKeysPage }) => {
await virtualKeysPage.goto()
})
test.afterEach(async ({ virtualKeysPage }) => {
// Close any open sheets
await virtualKeysPage.closeSheet()
})
test('should require name for virtual key', async ({ virtualKeysPage }) => {
await virtualKeysPage.dismissToasts()
await virtualKeysPage.createBtn.click()
await expect(virtualKeysPage.sheet).toBeVisible()
// Wait for sheet animation to complete
await virtualKeysPage.waitForSheetAnimation()
// Save button should be disabled when name is empty
await expect(virtualKeysPage.saveBtn).toBeDisabled()
})
test('should accept valid budget values', async ({ virtualKeysPage }) => {
await virtualKeysPage.dismissToasts()
await virtualKeysPage.createBtn.click()
await expect(virtualKeysPage.sheet).toBeVisible()
// Wait for sheet animation to complete
await virtualKeysPage.waitForSheetAnimation()
// Fill name (required field)
await virtualKeysPage.nameInput.fill(`Valid Budget Test ${Date.now()}`)
// Fill budget
const budgetInput = virtualKeysPage.page.locator('#budgetMaxLimit')
await expect(budgetInput).toBeVisible({ timeout: 5000 })
await budgetInput.fill('100')
// Save button should be enabled if form is valid
await expect(virtualKeysPage.saveBtn).toBeEnabled()
})
})
// Track created VKs for provider tests
const providerVKs: string[] = []
test.describe('Provider Management', () => {
test.beforeEach(async ({ virtualKeysPage }) => {
await virtualKeysPage.goto()
})
test.afterEach(async ({ virtualKeysPage }) => {
// Close any open sheets first
await virtualKeysPage.closeSheet()
// Clean up all tracked VKs
if (providerVKs.length > 0) {
await virtualKeysPage.cleanupVirtualKeys([...providerVKs])
providerVKs.length = 0
}
})
test('should add provider to existing virtual key', async ({ virtualKeysPage }) => {
// Create a virtual key first
const vkName = `Add Provider VK ${Date.now()}`
const vkData = createVirtualKeyWithProvider('openai', { name: vkName })
providerVKs.push(vkName)
await virtualKeysPage.createVirtualKey(vkData)
// View the virtual key
await virtualKeysPage.viewVirtualKey(vkName)
// Check if we can see provider configuration
const providerSection = virtualKeysPage.page.getByText(/Providers|Provider/i).first()
const isVisible = await providerSection.isVisible().catch(() => false)
if (isVisible) {
// Provider section is available
expect(isVisible).toBe(true)
}
// Close sheet (handled by afterEach as well)
await virtualKeysPage.closeSheet()
})
test('should remove provider from virtual key', async ({ virtualKeysPage }) => {
// Create a virtual key with multiple providers
const vkName = `Remove Provider VK ${Date.now()}`
const vkData = createVirtualKeyWithMultipleProviders(['openai', 'anthropic'], { name: vkName })
providerVKs.push(vkName)
await virtualKeysPage.createVirtualKey(vkData)
// View the virtual key
await virtualKeysPage.viewVirtualKey(vkName)
// Check if we can see and interact with providers
const removeProviderBtn = virtualKeysPage.page.locator('button').filter({
has: virtualKeysPage.page.locator('svg.lucide-trash, svg.lucide-x, svg.lucide-trash-2')
}).first()
const isVisible = await removeProviderBtn.isVisible().catch(() => false)
if (isVisible) {
// Remove provider is available - this is expected behavior
expect(isVisible).toBe(true)
}
// Close sheet (handled by afterEach as well)
await virtualKeysPage.closeSheet()
})
test('should update provider-specific budget', async ({ virtualKeysPage }) => {
// Create a virtual key with budget
const vkName = `Provider Budget VK ${Date.now()}`
const vkData = createVirtualKeyWithProvider('openai', {
name: vkName,
budget: SAMPLE_BUDGETS.small,
})
providerVKs.push(vkName)
await virtualKeysPage.createVirtualKey(vkData)
// Edit the virtual key
await virtualKeysPage.editVirtualKey(vkName, {
budget: SAMPLE_BUDGETS.large,
})
// Verify it still exists
const vkExists = await virtualKeysPage.virtualKeyExists(vkName)
expect(vkExists).toBe(true)
})
})