Files
adam-acs/tests/unit/components/virtual_scroll.spec.ts
T
Kevin L'Hereec 0c83a4ccb2 feat: TR-069 Exchange Viewer - structured MongoDB storage with SSE streaming and sequence diagram UI
- MongoDB storage: cwmp_exchanges collection with indexes (deviceId+timestamp, deviceId+sequenceNumber unique, TTL 24h)
- ExchangeStoreService: CWMP method extraction, per-device queue for ordering, retry logic, message cap (5000/device)
- Exchange API: GET/DELETE/SSE stream endpoints with Last-Event-ID reconnection
- CwmpLoggerMiddleware: dual-write (MongoDB + legacy files) with fire-and-forget
- Frontend: SequenceViewer (CPE↔ACS lifelines, arrows, auto-scroll, virtual scroll) + InspectorPanel (formatted XML, copy, syntax highlighting)
- Tr069Trace page: integrated new viewer with col-6/col-6 layout
- DeviceDetail page: collapsible exchange section when device is traced
- 14 property-based tests, 40+ unit tests, 5 integration tests
- Health check includes index verification
- Config: feature flags, SSE timeouts, retry policy
2026-06-03 15:52:44 +02:00

202 lines
7.0 KiB
TypeScript

import { describe, it, expect } from 'vitest'
/**
* Unit tests for the virtual scrolling logic in SequenceViewer.vue.
* Tests the pure computation functions extracted from the component's computed properties.
*
* Virtual scrolling parameters:
* - ITEM_HEIGHT = 60px
* - BUFFER_SIZE = 5 items
* - VIRTUAL_SCROLL_THRESHOLD = 200 messages
*/
const ITEM_HEIGHT = 60
const BUFFER_SIZE = 5
const VIRTUAL_SCROLL_THRESHOLD = 200
// --- Pure logic functions (mirror of component computed properties) ---
function useVirtualScroll(messageCount: number): boolean {
return messageCount > VIRTUAL_SCROLL_THRESHOLD
}
function totalHeight(messageCount: number): number {
return messageCount * ITEM_HEIGHT
}
function visibleStart(scrollTop: number, messageCount: number): number {
if (!useVirtualScroll(messageCount)) return 0
const start = Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER_SIZE
return Math.max(0, start)
}
function visibleEnd(
scrollTop: number,
containerHeight: number,
messageCount: number
): number {
if (!useVirtualScroll(messageCount)) return messageCount
const visibleCount = Math.ceil(containerHeight / ITEM_HEIGHT)
const end = Math.floor(scrollTop / ITEM_HEIGHT) + visibleCount + BUFFER_SIZE
return Math.min(messageCount, end)
}
function offsetY(scrollTop: number, messageCount: number): number {
return visibleStart(scrollTop, messageCount) * ITEM_HEIGHT
}
describe('Virtual Scrolling Logic', () => {
describe('useVirtualScroll threshold', () => {
it('should NOT use virtual scroll for 200 messages or fewer', () => {
expect(useVirtualScroll(0)).toBe(false)
expect(useVirtualScroll(100)).toBe(false)
expect(useVirtualScroll(200)).toBe(false)
})
it('should use virtual scroll for more than 200 messages', () => {
expect(useVirtualScroll(201)).toBe(true)
expect(useVirtualScroll(500)).toBe(true)
expect(useVirtualScroll(5000)).toBe(true)
})
})
describe('totalHeight', () => {
it('should compute total height as messageCount * ITEM_HEIGHT', () => {
expect(totalHeight(0)).toBe(0)
expect(totalHeight(1)).toBe(60)
expect(totalHeight(100)).toBe(6000)
expect(totalHeight(1000)).toBe(60000)
expect(totalHeight(5000)).toBe(300000)
})
})
describe('visibleStart', () => {
it('should return 0 when virtual scroll is disabled (≤200 messages)', () => {
expect(visibleStart(0, 100)).toBe(0)
expect(visibleStart(1000, 200)).toBe(0)
})
it('should return 0 when scrolled to top with virtual scroll enabled', () => {
expect(visibleStart(0, 500)).toBe(0)
})
it('should include buffer items before the visible area', () => {
// scrollTop = 600 means first visible item is at index 10
// With buffer of 5, visibleStart = 10 - 5 = 5
expect(visibleStart(600, 500)).toBe(5)
})
it('should clamp to 0 when buffer would go negative', () => {
// scrollTop = 120 means first visible item is at index 2
// With buffer of 5, visibleStart = 2 - 5 = -3 → clamped to 0
expect(visibleStart(120, 500)).toBe(0)
})
it('should compute correctly for large scroll positions', () => {
// scrollTop = 30000 means first visible item is at index 500
// With buffer of 5, visibleStart = 500 - 5 = 495
expect(visibleStart(30000, 1000)).toBe(495)
})
})
describe('visibleEnd', () => {
it('should return messageCount when virtual scroll is disabled', () => {
expect(visibleEnd(0, 500, 100)).toBe(100)
expect(visibleEnd(0, 500, 200)).toBe(200)
})
it('should compute visible end based on container height and buffer', () => {
// containerHeight = 500, so visibleCount = ceil(500/60) = 9
// scrollTop = 0, so first visible = 0
// end = 0 + 9 + 5 = 14
expect(visibleEnd(0, 500, 500)).toBe(14)
})
it('should clamp to messageCount when end exceeds total', () => {
// With few messages relative to viewport
// scrollTop = 0, containerHeight = 500, visibleCount = 9
// end = 0 + 9 + 5 = 14, but messageCount = 201
// For scrollTop near the end:
// scrollTop = 12000 → first visible = 200, end = 200 + 9 + 5 = 214 → clamped to 201
expect(visibleEnd(12000, 500, 201)).toBe(201)
})
it('should compute correctly for mid-scroll positions', () => {
// scrollTop = 3000, containerHeight = 500
// first visible = floor(3000/60) = 50
// visibleCount = ceil(500/60) = 9
// end = 50 + 9 + 5 = 64
expect(visibleEnd(3000, 500, 1000)).toBe(64)
})
})
describe('offsetY', () => {
it('should return 0 when virtual scroll is disabled', () => {
expect(offsetY(0, 100)).toBe(0)
expect(offsetY(5000, 200)).toBe(0)
})
it('should return 0 when scrolled to top', () => {
expect(offsetY(0, 500)).toBe(0)
})
it('should compute offset based on visibleStart', () => {
// visibleStart(600, 500) = 5, so offsetY = 5 * 60 = 300
expect(offsetY(600, 500)).toBe(300)
})
it('should compute correctly for large scroll positions', () => {
// visibleStart(30000, 1000) = 495, so offsetY = 495 * 60 = 29700
expect(offsetY(30000, 1000)).toBe(29700)
})
})
describe('rendered item count', () => {
it('should render only a subset of items when virtual scrolling is active', () => {
const messageCount = 1000
const containerH = 500
// For any scroll position, rendered items = visibleEnd - visibleStart
const scrollPos = 3000
const start = visibleStart(scrollPos, messageCount)
const end = visibleEnd(scrollPos, containerH, messageCount)
const renderedCount = end - start
// visibleCount = ceil(500/60) = 9, plus 2*buffer = 10 → max ~19 items rendered
// Way less than the 1000 total messages
expect(renderedCount).toBeLessThan(30)
expect(renderedCount).toBeGreaterThan(0)
})
it('should render all items when below threshold', () => {
const messageCount = 150
const containerH = 500
const scrollPos = 0
const start = visibleStart(scrollPos, messageCount)
const end = visibleEnd(scrollPos, containerH, messageCount)
expect(start).toBe(0)
expect(end).toBe(messageCount)
})
it('should maintain render performance target (≤100ms render budget)', () => {
// With 5000 messages, virtual scroll renders only ~19 items max
// At 60px height, a 500px container shows ~8 items + 2*5 buffer = 18 items
// 18 items at worst case should render well under 100ms
const messageCount = 5000
const containerH = 500
for (let scrollPos = 0; scrollPos < totalHeight(messageCount); scrollPos += 6000) {
const start = visibleStart(scrollPos, messageCount)
const end = visibleEnd(scrollPos, containerH, messageCount)
const renderedCount = end - start
// Never render more than visibleCount + 2*buffer items
const maxRendered = Math.ceil(containerH / ITEM_HEIGHT) + 2 * BUFFER_SIZE
expect(renderedCount).toBeLessThanOrEqual(maxRendered)
}
})
})
})