428 lines
15 KiB
TypeScript
428 lines
15 KiB
TypeScript
import { expect, test } from '../../core/fixtures/base.fixture'
|
|
import { createLogSearchQuery, SAMPLE_MODELS, SAMPLE_PROVIDERS } from './logs.data'
|
|
|
|
test.describe('LLM Logs', () => {
|
|
test.beforeEach(async ({ logsPage }) => {
|
|
await logsPage.goto()
|
|
})
|
|
|
|
test.describe('Logs Display', () => {
|
|
test('should display logs table', async ({ logsPage }) => {
|
|
// Table should be visible after goto (which waits for load)
|
|
const tableExists = await logsPage.logsTable.isVisible().catch(() => false)
|
|
expect(tableExists).toBe(true)
|
|
})
|
|
|
|
test('should display stats cards', async ({ logsPage }) => {
|
|
const statsVisible = await logsPage.areStatsVisible()
|
|
expect(statsVisible).toBe(true)
|
|
})
|
|
|
|
test('should display filters section', async ({ logsPage }) => {
|
|
// Check if the search input or filters button is visible
|
|
// These are always visible when the page loads (not inside empty state)
|
|
const searchVisible = await logsPage.searchInput.isVisible().catch(() => false)
|
|
const filtersButtonVisible = await logsPage.filtersButton.isVisible().catch(() => false)
|
|
|
|
// Either search input OR filters button should be visible
|
|
expect(searchVisible || filtersButtonVisible).toBe(true)
|
|
})
|
|
})
|
|
|
|
test.describe('Log Filtering', () => {
|
|
test('should filter logs by provider', async ({ logsPage }) => {
|
|
// Try to filter by first available provider
|
|
const providerFilter = logsPage.providerFilter
|
|
const isVisible = await providerFilter.isVisible().catch(() => false)
|
|
|
|
if (!isVisible || SAMPLE_PROVIDERS.length === 0) {
|
|
test.skip(!isVisible || SAMPLE_PROVIDERS.length === 0, 'Provider filter not visible or no sample providers')
|
|
return
|
|
}
|
|
|
|
// Get initial filter state
|
|
const initialValue = await providerFilter.textContent().catch(() => '')
|
|
|
|
await logsPage.filterByProvider(SAMPLE_PROVIDERS[0])
|
|
|
|
// Check that filter value changed (or verify filter is applied via DOM)
|
|
const newValue = await providerFilter.textContent().catch(() => '')
|
|
// Filter should have changed or show selected provider
|
|
expect(newValue || initialValue).toBeTruthy()
|
|
})
|
|
|
|
test('should filter logs by model', async ({ logsPage }) => {
|
|
const modelFilter = logsPage.modelFilter
|
|
const isVisible = await modelFilter.isVisible().catch(() => false)
|
|
|
|
if (!isVisible || SAMPLE_MODELS.length === 0) {
|
|
test.skip(!isVisible || SAMPLE_MODELS.length === 0, 'Model filter not visible or no sample models')
|
|
return
|
|
}
|
|
|
|
// Get initial filter state
|
|
const initialValue = await modelFilter.textContent().catch(() => '')
|
|
|
|
await logsPage.filterByModel(SAMPLE_MODELS[0])
|
|
|
|
// Check that filter value changed (or verify filter is applied via DOM)
|
|
const newValue = await modelFilter.textContent().catch(() => '')
|
|
// Filter should have changed or show selected model
|
|
expect(newValue || initialValue).toBeTruthy()
|
|
})
|
|
|
|
test('should filter logs by status', async ({ logsPage, page }) => {
|
|
const filtersVisible = await logsPage.filtersButton.isVisible().catch(() => false)
|
|
if (!filtersVisible) {
|
|
test.skip(true, 'Filters button not visible')
|
|
return
|
|
}
|
|
|
|
await logsPage.filterByStatus('success')
|
|
|
|
// Assert status filter is applied: logs page persists filters in URL (e.g. status=success)
|
|
await expect
|
|
.poll(() => page.url(), { timeout: 5000, intervals: [200, 300, 500] })
|
|
.toMatch(/status=success/)
|
|
})
|
|
|
|
test('should search logs by content', async ({ logsPage }) => {
|
|
const searchInput = logsPage.searchInput
|
|
const isVisible = await searchInput.isVisible().catch(() => false)
|
|
|
|
if (!isVisible) {
|
|
test.skip(true, 'Search input not visible')
|
|
return
|
|
}
|
|
|
|
const query = createLogSearchQuery()
|
|
await logsPage.searchLogs(query)
|
|
|
|
// Check that search input contains the query (DOM state)
|
|
const inputValue = await searchInput.inputValue().catch(() => '')
|
|
expect(inputValue).toContain(query)
|
|
})
|
|
|
|
test('should clear search', async ({ logsPage }) => {
|
|
const searchInput = logsPage.searchInput
|
|
const isVisible = await searchInput.isVisible().catch(() => false)
|
|
|
|
if (!isVisible) {
|
|
test.skip(true, 'Search input not visible')
|
|
return
|
|
}
|
|
|
|
await logsPage.searchLogs('test query')
|
|
await logsPage.clearSearch()
|
|
|
|
// Search should be cleared
|
|
const inputValue = await searchInput.inputValue().catch(() => '')
|
|
expect(inputValue).toBe('')
|
|
})
|
|
|
|
test('should filter by time period', async ({ logsPage }) => {
|
|
const datePicker = logsPage.dateRangePicker
|
|
const isVisible = await datePicker.isVisible().catch(() => false)
|
|
|
|
if (!isVisible) {
|
|
test.skip(true, 'Date range picker not visible')
|
|
return
|
|
}
|
|
|
|
// Get initial date picker value
|
|
const initialValue = await datePicker.textContent().catch(() => '')
|
|
|
|
await logsPage.selectTimePeriod('7d')
|
|
|
|
// Check that date picker value changed (DOM state)
|
|
const newValue = await datePicker.textContent().catch(() => '')
|
|
// Date picker should show "Last 7 days" or similar
|
|
expect(newValue || initialValue).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
test.describe('Log Details', () => {
|
|
test('should open log details sheet', async ({ logsPage }) => {
|
|
// Wait a bit for logs to potentially load
|
|
await logsPage.page.waitForTimeout(1000)
|
|
|
|
const logCount = await logsPage.getLogCount()
|
|
|
|
if (logCount > 0) {
|
|
await logsPage.viewLogDetails(0)
|
|
|
|
// Wait for sheet animation
|
|
await logsPage.page.waitForTimeout(500)
|
|
|
|
// Detail sheet should be visible
|
|
const sheetVisible = await logsPage.logDetailSheet.isVisible().catch(() => false)
|
|
expect(sheetVisible).toBe(true)
|
|
|
|
// Close the sheet
|
|
await logsPage.closeLogDetails()
|
|
} else {
|
|
// If no logs exist, the test passes (nothing to click)
|
|
expect(logCount).toBe(0)
|
|
}
|
|
})
|
|
|
|
test('should close log details sheet', async ({ logsPage }) => {
|
|
const logCount = await logsPage.getLogCount()
|
|
|
|
if (logCount > 0) {
|
|
await logsPage.viewLogDetails(0)
|
|
await logsPage.closeLogDetails()
|
|
|
|
// Sheet should be closed
|
|
const sheetVisible = await logsPage.logDetailSheet.isVisible().catch(() => false)
|
|
expect(sheetVisible).toBe(false)
|
|
}
|
|
})
|
|
})
|
|
|
|
test.describe('Pagination', () => {
|
|
test('should navigate to next page', async ({ logsPage }) => {
|
|
// Wait for pagination to settle (useTablePageSize may adjust limit dynamically)
|
|
await logsPage.page.waitForTimeout(2000)
|
|
const paginationVisible = await logsPage.paginationControls.isVisible().catch(() => false)
|
|
if (!paginationVisible) {
|
|
test.skip(true, 'Pagination controls not visible')
|
|
return
|
|
}
|
|
const nextBtn = logsPage.nextPageBtn.first()
|
|
const isEnabled = await nextBtn.isEnabled().catch(() => false)
|
|
|
|
if (!isEnabled) {
|
|
test.skip(true, 'Only one page of results; skipping pagination test')
|
|
return
|
|
}
|
|
|
|
const initialPage = logsPage.getCurrentPageNumber()
|
|
expect(initialPage).toBe(1)
|
|
await logsPage.goToNextPage()
|
|
|
|
await expect
|
|
.poll(() => logsPage.getCurrentPageNumber(), { timeout: 5000 })
|
|
.toBe(initialPage + 1)
|
|
})
|
|
|
|
test('should navigate to previous page', async ({ logsPage }) => {
|
|
// Wait for pagination to settle (useTablePageSize may adjust limit dynamically)
|
|
await logsPage.page.waitForTimeout(2000)
|
|
const paginationVisible = await logsPage.paginationControls.isVisible().catch(() => false)
|
|
if (!paginationVisible) {
|
|
test.skip(true, 'Pagination controls not visible')
|
|
return
|
|
}
|
|
const nextBtn = logsPage.nextPageBtn.first()
|
|
const nextEnabled = await nextBtn.isEnabled().catch(() => false)
|
|
|
|
if (!nextEnabled) {
|
|
test.skip(true, 'Only one page of results; skipping pagination test')
|
|
return
|
|
}
|
|
|
|
await logsPage.goToNextPage()
|
|
|
|
await expect
|
|
.poll(() => logsPage.getCurrentPageNumber(), { timeout: 5000 })
|
|
.toBe(2)
|
|
|
|
const prevBtn = logsPage.prevPageBtn.first()
|
|
const prevEnabled = await prevBtn.isEnabled().catch(() => false)
|
|
|
|
if (!prevEnabled) {
|
|
test.skip(true, 'Only one page of results; skipping previous-page test')
|
|
return
|
|
}
|
|
|
|
await logsPage.goToPreviousPage()
|
|
|
|
await expect
|
|
.poll(() => logsPage.getCurrentPageNumber(), { timeout: 5000 })
|
|
.toBe(1)
|
|
})
|
|
})
|
|
|
|
test.describe('Table Sorting', () => {
|
|
test('should sort by timestamp', async ({ logsPage }) => {
|
|
// Timestamp is the default sort column (desc), so clicking it toggles to asc
|
|
await logsPage.sortBy('timestamp')
|
|
|
|
// Timestamp sort toggles order; wait for URL to reflect the change
|
|
await logsPage.page.waitForURL(/order=asc|sort_by=timestamp/, { timeout: 5000 })
|
|
})
|
|
|
|
test('should sort by latency', async ({ logsPage }) => {
|
|
await logsPage.sortBy('latency')
|
|
|
|
// Wait for URL to update
|
|
await logsPage.page.waitForURL(/sort_by=latency/, { timeout: 5000 })
|
|
|
|
// Check URL state for latency sort
|
|
const sortState = await logsPage.getSortState('latency')
|
|
expect(sortState).toBeTruthy()
|
|
})
|
|
|
|
test('should sort by cost', async ({ logsPage }) => {
|
|
await logsPage.sortBy('cost')
|
|
|
|
// Wait for URL to update
|
|
await logsPage.page.waitForURL(/sort_by=cost/, { timeout: 5000 })
|
|
|
|
// Check URL state for cost sort
|
|
const sortState = await logsPage.getSortState('cost')
|
|
expect(sortState).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
test.describe('Live Updates', () => {
|
|
test('should toggle live updates', async ({ logsPage }) => {
|
|
const liveToggle = logsPage.liveToggle
|
|
const isVisible = await liveToggle.isVisible().catch(() => false)
|
|
|
|
if (!isVisible) {
|
|
test.skip(true, 'Live toggle not visible')
|
|
return
|
|
}
|
|
|
|
// Default is live_enabled=true (but URL may not have it since it's the default)
|
|
// Check for live_enabled=false to determine if currently disabled
|
|
const initialUrl = logsPage.page.url()
|
|
const initialLiveDisabled = initialUrl.includes('live_enabled=false')
|
|
|
|
await logsPage.toggleLiveUpdates()
|
|
|
|
// Wait for URL to reflect live_enabled toggle
|
|
await logsPage.page.waitForURL(/live_enabled=/, { timeout: 5000 })
|
|
|
|
const newUrl = logsPage.page.url()
|
|
const newLiveDisabled = newUrl.includes('live_enabled=false')
|
|
|
|
// Live enabled state should have toggled
|
|
// If initially enabled (not disabled), after toggle it should be disabled
|
|
// If initially disabled, after toggle it should be enabled (no live_enabled=false)
|
|
expect(newLiveDisabled).not.toBe(initialLiveDisabled)
|
|
})
|
|
})
|
|
|
|
test.describe('Empty State', () => {
|
|
test('should show empty state when no logs', async ({ logsPage }) => {
|
|
// Try to filter by a non-existent provider
|
|
const searchInput = logsPage.searchInput
|
|
const isVisible = await searchInput.isVisible().catch(() => false)
|
|
|
|
if (!isVisible) {
|
|
test.skip(true, 'Search input not visible')
|
|
return
|
|
}
|
|
|
|
await logsPage.searchLogs(`nonexistent-query-${Date.now()}`)
|
|
|
|
// After searching for a non-existent query, empty state should appear (wait for API + render)
|
|
await expect(
|
|
logsPage.page.locator('text=/No results found|No logs found/i')
|
|
).toBeVisible({ timeout: 10000 })
|
|
})
|
|
})
|
|
|
|
test.describe('Advanced Filtering', () => {
|
|
test('should combine multiple filters', async ({ logsPage }) => {
|
|
// Apply multiple filters if they're visible
|
|
const searchVisible = await logsPage.searchInput.isVisible().catch(() => false)
|
|
const providerVisible = await logsPage.providerFilter.isVisible().catch(() => false)
|
|
|
|
if (!searchVisible || !providerVisible) {
|
|
test.skip(true, 'Search input or provider filter not visible')
|
|
return
|
|
}
|
|
|
|
// Apply search filter
|
|
await logsPage.searchLogs('test')
|
|
|
|
// Apply provider filter
|
|
if (SAMPLE_PROVIDERS.length > 0) {
|
|
await logsPage.filterByProvider(SAMPLE_PROVIDERS[0])
|
|
}
|
|
|
|
// Both filters should be applied
|
|
const searchValue = await logsPage.searchInput.inputValue().catch(() => '')
|
|
expect(searchValue).toContain('test')
|
|
})
|
|
|
|
test('should clear all filters', async ({ logsPage }) => {
|
|
const searchVisible = await logsPage.searchInput.isVisible().catch(() => false)
|
|
|
|
if (!searchVisible) {
|
|
test.skip(true, 'Search input not visible')
|
|
return
|
|
}
|
|
|
|
// Apply a filter first
|
|
await logsPage.searchLogs('test query to clear')
|
|
|
|
// Clear the search
|
|
await logsPage.clearSearch()
|
|
|
|
// Search should be empty
|
|
const searchValue = await logsPage.searchInput.inputValue().catch(() => '')
|
|
expect(searchValue).toBe('')
|
|
})
|
|
|
|
test('should search within filtered results', async ({ logsPage }) => {
|
|
const searchVisible = await logsPage.searchInput.isVisible().catch(() => false)
|
|
const statusVisible = await logsPage.statusFilter.isVisible().catch(() => false)
|
|
|
|
if (!searchVisible || !statusVisible) {
|
|
test.skip(true, 'Search input or status filter not visible')
|
|
return
|
|
}
|
|
|
|
// Apply status filter first
|
|
await logsPage.filterByStatus('success')
|
|
|
|
// Then apply search
|
|
await logsPage.searchLogs('api')
|
|
|
|
// Search input should contain the query
|
|
const searchValue = await logsPage.searchInput.inputValue().catch(() => '')
|
|
expect(searchValue).toContain('api')
|
|
})
|
|
})
|
|
|
|
test.describe('URL State Persistence', () => {
|
|
test('should persist filters in URL', async ({ logsPage }) => {
|
|
const searchVisible = await logsPage.searchInput.isVisible().catch(() => false)
|
|
if (!searchVisible) return
|
|
|
|
await logsPage.searchLogs('persistent-search')
|
|
|
|
// Search is debounced (500ms) then URL updates; wait for URL to contain the param
|
|
await expect
|
|
.poll(
|
|
() => logsPage.page.url(),
|
|
{ timeout: 8000, intervals: [300, 500, 500] }
|
|
)
|
|
.toContain('content_search=')
|
|
const url = logsPage.page.url()
|
|
// Value may be percent-encoded (e.g. persistent-search → persistent%2Dsearch)
|
|
expect(decodeURIComponent(url)).toContain('persistent-search')
|
|
})
|
|
|
|
test('should restore state from URL', async ({ logsPage, page }) => {
|
|
// Logs page uses start_time and end_time (unix timestamps), not period
|
|
const endTime = Math.floor(Date.now() / 1000)
|
|
const startTime = endTime - 7 * 24 * 60 * 60 // 7 days ago
|
|
await page.goto(`/workspace/logs?start_time=${startTime}&end_time=${endTime}`)
|
|
|
|
// Wait for page to load and URL to reflect state (nuqs may merge or keep params)
|
|
await expect
|
|
.poll(() => page.url(), { timeout: 5000, intervals: [200, 300, 500] })
|
|
.toMatch(/start_time=\d+/)
|
|
const url = page.url()
|
|
expect(url).toMatch(/start_time=\d+/)
|
|
expect(url).toMatch(/end_time=\d+/)
|
|
})
|
|
})
|
|
})
|