- 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
202 lines
7.0 KiB
TypeScript
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)
|
|
}
|
|
})
|
|
})
|
|
})
|