first commit
This commit is contained in:
683
tests/e2e/features/virtual-keys/pages/virtual-keys.page.ts
Normal file
683
tests/e2e/features/virtual-keys/pages/virtual-keys.page.ts
Normal 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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
178
tests/e2e/features/virtual-keys/virtual-keys.data.ts
Normal file
178
tests/e2e/features/virtual-keys/virtual-keys.data.ts
Normal 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
|
||||
557
tests/e2e/features/virtual-keys/virtual-keys.spec.ts
Normal file
557
tests/e2e/features/virtual-keys/virtual-keys.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user