Files
bifrost/tests/e2e/features/dashboard/dashboard.spec.ts
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

386 lines
15 KiB
TypeScript

import { expect, test } from '../../core/fixtures/base.fixture'
import { waitForNetworkIdle } from '../../core/utils/test-helpers'
import { DashboardPage } from './pages/dashboard.page'
test.describe('Dashboard', () => {
test.beforeEach(async ({ dashboardPage }) => {
await dashboardPage.goto()
})
test.describe('Dashboard Display', () => {
test('should display dashboard page', async ({ dashboardPage }) => {
await expect(dashboardPage.pageTitle).toBeVisible()
})
test('should display all chart cards', async ({ dashboardPage }) => {
// Check that all four main charts are visible
await expect(dashboardPage.logVolumeChart).toBeVisible()
await expect(dashboardPage.tokenUsageChart).toBeVisible()
await expect(dashboardPage.costChart).toBeVisible()
await expect(dashboardPage.modelUsageChart).toBeVisible()
})
test('should display date time picker', async ({ dashboardPage }) => {
// Date picker should be visible (may be a button with date text)
const datePicker = dashboardPage.page.locator('button').filter({ hasText: /Last/i }).or(
dashboardPage.page.locator('[data-testid="dashboard-date-picker"]')
)
await expect(datePicker.first()).toBeVisible()
})
})
test.describe('Time Period Selection', () => {
test('should filter by time period (full flow)', async ({ dashboardPage }) => {
// Time period control must exist and be visible (no skip)
const trigger = dashboardPage.getDatePickerTrigger()
await expect(trigger).toBeVisible({ timeout: 10000 })
// Let initial chart load finish so the refetch we wait for is the one from the period change
await dashboardPage.waitForChartsToLoad()
// Wait for the chart data request that fires when we change the period (proves filter is applied)
const responsePromise = dashboardPage.page.waitForResponse(
(res) => res.url().includes('/logs/histogram') && res.status() === 200,
{ timeout: 15000 }
)
await dashboardPage.selectTimePeriod('1h')
// UI: trigger shows the selected period
const label = await dashboardPage.getSelectedPeriodLabel()
expect(label).toContain('Last hour')
// URL: selection is reflected in query state
const url = dashboardPage.page.url()
expect(url).toMatch(/period=1h|start_time=\d+&end_time=\d+/)
// Data: dashboard refetched with the new range
await responsePromise
})
test('should change time period to last hour', async ({ dashboardPage }) => {
await dashboardPage.selectTimePeriod('1h')
const label = await dashboardPage.getSelectedPeriodLabel()
expect(label).toContain('Last hour')
const url = dashboardPage.page.url()
expect(url).toMatch(/period=1h|start_time=\d+&end_time=\d+/)
})
test('should change time period to last 7 days', async ({ dashboardPage }) => {
await dashboardPage.selectTimePeriod('7d')
const label = await dashboardPage.getSelectedPeriodLabel()
expect(label).toContain('Last 7 days')
const url = dashboardPage.page.url()
expect(url).toMatch(/period=7d|start_time=\d+&end_time=\d+/)
})
test('should change time period to last 30 days', async ({ dashboardPage }) => {
await dashboardPage.selectTimePeriod('30d')
const label = await dashboardPage.getSelectedPeriodLabel()
expect(label).toContain('Last 30 days')
const url = dashboardPage.page.url()
expect(url).toMatch(/period=30d|start_time=\d+&end_time=\d+/)
})
})
test.describe('Chart Type Toggling', () => {
test('should toggle volume chart type', async ({ dashboardPage }) => {
// Get initial toggle state from DOM
const initialToggle = dashboardPage.volumeChartToggle
const initialState = await dashboardPage.getChartToggleState(initialToggle)
// Toggle the chart (method handles waiting internally)
await dashboardPage.toggleVolumeChartType()
// Get new toggle state
const newToggle = dashboardPage.volumeChartToggle
const newState = await dashboardPage.getChartToggleState(newToggle)
// Chart type should have changed (state should be different)
expect(newState).not.toBe(initialState)
})
test('should toggle token chart type', async ({ dashboardPage }) => {
const initialToggle = dashboardPage.tokenChartToggle
const initialState = await dashboardPage.getChartToggleState(initialToggle)
await dashboardPage.toggleTokenChartType()
const newToggle = dashboardPage.tokenChartToggle
const newState = await dashboardPage.getChartToggleState(newToggle)
expect(newState).not.toBe(initialState)
})
test('should toggle cost chart type', async ({ dashboardPage }) => {
const initialToggle = dashboardPage.costChartToggle
const initialState = await dashboardPage.getChartToggleState(initialToggle)
await dashboardPage.toggleCostChartType()
const newToggle = dashboardPage.costChartToggle
const newState = await dashboardPage.getChartToggleState(newToggle)
expect(newState).not.toBe(initialState)
})
test('should toggle model chart type', async ({ dashboardPage }) => {
const initialToggle = dashboardPage.modelChartToggle
const initialState = await dashboardPage.getChartToggleState(initialToggle)
await dashboardPage.toggleModelChartType()
const newToggle = dashboardPage.modelChartToggle
const newState = await dashboardPage.getChartToggleState(newToggle)
expect(newState).not.toBe(initialState)
})
})
test.describe('Model Filtering', () => {
test('should filter cost chart by model', async ({ dashboardPage }) => {
// Wait for charts to fully load
await dashboardPage.waitForChartsToLoad()
// Try to filter by a specific model if available
const costModelFilter = dashboardPage.costModelFilter
const isVisible = await costModelFilter.isVisible().catch(() => false)
if (isVisible) {
await dashboardPage.filterCostChartByModel('all')
// Check that filter value is "All Models"
const newSelected = await dashboardPage.getSelectedModel(costModelFilter)
expect(newSelected).toContain('All Models')
}
})
test('should filter usage chart by model', async ({ dashboardPage }) => {
// Wait for charts to fully load
await dashboardPage.waitForChartsToLoad()
const usageModelFilter = dashboardPage.usageModelFilter
const isVisible = await usageModelFilter.isVisible().catch(() => false)
if (isVisible) {
await dashboardPage.filterUsageChartByModel('all')
// Check that filter value is "All Models"
const newSelected = await dashboardPage.getSelectedModel(usageModelFilter)
expect(newSelected).toContain('All Models')
}
})
})
test.describe('Chart Loading States', () => {
test('should show loading state initially', async ({ dashboardPage }) => {
// Navigate to a fresh dashboard
await dashboardPage.page.reload()
await dashboardPage.waitForPageLoad()
// Charts may show loading state briefly
// This test verifies the page loads without errors
await expect(dashboardPage.pageTitle).toBeVisible({ timeout: 10000 })
})
})
test.describe('URL State Management', () => {
test('should preserve chart state in URL', async ({ dashboardPage }) => {
// Change some settings
await dashboardPage.selectTimePeriod('7d')
await dashboardPage.toggleVolumeChartType()
// Check URL for period (time period should still be in URL)
const url = dashboardPage.page.url()
expect(url).toContain('period=7d')
// Check DOM state for chart toggle (may or may not be in URL)
const toggleState = await dashboardPage.getChartToggleState(dashboardPage.volumeChartToggle)
expect(toggleState).toBeTruthy()
})
test('should restore state from URL on page load', async ({ dashboardPage }) => {
// Set URL with specific state
await dashboardPage.page.goto('/workspace/dashboard?period=7d&volume_chart=line')
await waitForNetworkIdle(dashboardPage.page)
await dashboardPage.waitForChartsToLoad()
// Verify page loaded with correct state from URL
const url = dashboardPage.page.url()
expect(url).toContain('period=7d')
// volume_chart=line was in the URL - verify exact value persisted
expect(url).toContain('volume_chart=line')
})
})
test.describe('Chart Data Validation', () => {
test('should render chart elements after data loads', async ({ dashboardPage }) => {
// Wait for charts to load
await dashboardPage.waitForChartsToLoad()
// Check that each chart card has a canvas or chart surface SVG (recharts-surface = actual chart, not icons)
const volumeChartContent = dashboardPage.logVolumeChart.locator('canvas, svg.recharts-surface')
const tokenChartContent = dashboardPage.tokenUsageChart.locator('canvas, svg.recharts-surface')
const costChartContent = dashboardPage.costChart.locator('canvas, svg.recharts-surface')
const modelChartContent = dashboardPage.modelUsageChart.locator('canvas, svg.recharts-surface')
// Each chart card should have canvas or SVG content (chart library renders into these)
const volumeCount = await volumeChartContent.count()
const tokenCount = await tokenChartContent.count()
const costCount = await costChartContent.count()
const modelCount = await modelChartContent.count()
// All four chart cards should have rendered content (count > 0)
expect(volumeCount).toBeGreaterThan(0)
expect(tokenCount).toBeGreaterThan(0)
expect(costCount).toBeGreaterThan(0)
expect(modelCount).toBeGreaterThan(0)
})
test('should show chart legends', async ({ dashboardPage }) => {
await dashboardPage.waitForChartsToLoad()
// Check that chart actions (legends/toggles) are visible
const volumeActions = dashboardPage.page.locator('[data-testid="chart-log-volume-actions"]')
const tokenActions = dashboardPage.page.locator('[data-testid="chart-token-usage-actions"]')
// Actions should be visible (they contain legends and toggles)
await expect(volumeActions).toBeVisible()
await expect(tokenActions).toBeVisible()
})
test('should not show loading skeletons after data loads', async ({ dashboardPage }) => {
await dashboardPage.waitForChartsToLoad()
// Check that no skeletons are visible (data has loaded)
const skeletons = dashboardPage.page.locator('[data-testid="skeleton"]')
const skeletonCount = await skeletons.count()
expect(skeletonCount).toBe(0)
})
})
test.describe('Chart Interactions', () => {
test('should toggle between bar and line chart for volume', async ({ dashboardPage }) => {
await dashboardPage.waitForChartsToLoad()
// Get initial toggle state
const initialState = await dashboardPage.getChartToggleState(dashboardPage.volumeChartToggle)
// Toggle volume chart type
await dashboardPage.toggleVolumeChartType()
// DOM state should change (chart type toggles are in DOM, not URL)
const newState = await dashboardPage.getChartToggleState(dashboardPage.volumeChartToggle)
expect(newState).not.toBe(initialState)
})
test('should update chart when time period changes', async ({ dashboardPage }) => {
await dashboardPage.waitForChartsToLoad()
const initialUrl = dashboardPage.page.url()
await dashboardPage.selectTimePeriod('1h')
// Trigger should show new period (filter was applied)
const label = await dashboardPage.getSelectedPeriodLabel()
expect(label).toContain('Last hour')
const newUrl = dashboardPage.page.url()
expect(newUrl).toMatch(/period=1h|start_time=\d+&end_time=\d+/)
expect(newUrl).not.toBe(initialUrl)
})
test('should sync model filter between cost and usage charts', async ({ dashboardPage }) => {
await dashboardPage.waitForChartsToLoad()
// Check if model filters are visible
const costFilterVisible = await dashboardPage.costModelFilter.isVisible().catch(() => false)
const usageFilterVisible = await dashboardPage.usageModelFilter.isVisible().catch(() => false)
if (costFilterVisible && usageFilterVisible) {
// Filter cost chart
await dashboardPage.filterCostChartByModel('all')
// Verify filter was applied (check DOM state, not URL)
const selectedModel = await dashboardPage.getSelectedModel(dashboardPage.costModelFilter)
expect(selectedModel).toContain('All Models')
}
})
test('should display correct time period labels', async ({ dashboardPage }) => {
const periods: Array<'1h' | '6h' | '24h' | '7d' | '30d'> = ['1h', '6h', '24h', '7d', '30d']
for (const period of periods) {
await dashboardPage.selectTimePeriod(period)
// Assert the date picker trigger shows the selected period (actual selected value)
const label = await dashboardPage.getSelectedPeriodLabel()
const expected = DashboardPage.PERIOD_LABELS[period]
expect(label).toContain(expected)
}
})
})
test.describe('Error States', () => {
test('should handle empty data gracefully', async ({ dashboardPage }) => {
// Navigate with very short time range that may have no data
await dashboardPage.page.goto('/workspace/dashboard?period=1h')
await waitForNetworkIdle(dashboardPage.page)
await dashboardPage.waitForChartsToLoad()
// Page should still render without errors
await expect(dashboardPage.pageTitle).toBeVisible()
// All chart containers should still be visible
await expect(dashboardPage.logVolumeChart).toBeVisible()
await expect(dashboardPage.tokenUsageChart).toBeVisible()
})
})
test.describe('Custom Date Range', () => {
test('should open custom date range picker', async ({ dashboardPage }) => {
await dashboardPage.waitForChartsToLoad()
// Look for date picker button
const datePicker = dashboardPage.page.getByRole('button').filter({ hasText: /Last|Custom/i }).first()
const isVisible = await datePicker.isVisible().catch(() => false)
if (isVisible) {
await datePicker.click()
// Should see date range options or calendar
const calendarVisible = await dashboardPage.page.locator('[role="dialog"], [role="listbox"]').isVisible().catch(() => false)
const optionsVisible = await dashboardPage.page.getByRole('option').first().isVisible().catch(() => false)
expect(calendarVisible || optionsVisible).toBe(true)
// Close the picker
await dashboardPage.page.keyboard.press('Escape')
}
})
test('should handle empty data for custom range', async ({ dashboardPage }) => {
// Set a custom time range that likely has no data
await dashboardPage.page.goto('/workspace/dashboard?period=1h')
await waitForNetworkIdle(dashboardPage.page)
await dashboardPage.waitForChartsToLoad()
// Charts should still be visible even with no data
await expect(dashboardPage.logVolumeChart).toBeVisible()
await expect(dashboardPage.costChart).toBeVisible()
// Page should not show error alerts (not matching chart legend "Error")
const errorAlert = dashboardPage.page.locator('[role="alert"][data-variant="destructive"], .text-destructive, [data-sonner-toast][data-type="error"]')
const hasErrorAlert = await errorAlert.count() > 0
expect(hasErrorAlert).toBe(false)
})
})
})