- Page size reduced from 30 → 20 commits per fetch - Backend: add `skip` param to get_vault_pulse (git log --skip) → true pagination instead of re-fetching everything - Frontend: IntersectionObserver sentinel at bottom of feed → auto-loads next page when user scrolls near end - Append-only updates (no full re-render on load more) - Add IntersectionObserver mock to test setup
102 lines
3.6 KiB
TypeScript
102 lines
3.6 KiB
TypeScript
import '@testing-library/jest-dom/vitest'
|
|
import { vi } from 'vitest'
|
|
import { createElement, type ReactNode, type ComponentType } from 'react'
|
|
|
|
// Stub fetch to prevent jsdom@28 + Node 22 undici incompatibility.
|
|
// jsdom's JSDOMDispatcher passes an onError handler that Node 22's bundled
|
|
// undici rejects with InvalidArgumentError (UND_ERR_INVALID_ARG).
|
|
// Tests should never make real network requests — individual tests can
|
|
// override this stub via vi.mocked(fetch).mockImplementation(...).
|
|
globalThis.fetch = vi.fn(() =>
|
|
Promise.resolve(new Response(null, { status: 418 })),
|
|
) as typeof globalThis.fetch
|
|
|
|
// Stub WebSocket to prevent Node 22 + undici WebSocket incompatibility.
|
|
// undici's WebSocket dispatchEvent crashes with "The event argument must be
|
|
// an instance of Event" when running in jsdom environment.
|
|
// Tests should never open real WebSocket connections.
|
|
globalThis.WebSocket = class MockWebSocket {
|
|
static CONNECTING = 0
|
|
static OPEN = 1
|
|
static CLOSING = 2
|
|
static CLOSED = 3
|
|
readyState = MockWebSocket.OPEN
|
|
onopen: ((event: Event) => void) | null = null
|
|
onclose: ((event: CloseEvent) => void) | null = null
|
|
onmessage: ((event: MessageEvent) => void) | null = null
|
|
onerror: ((event: Event) => void) | null = null
|
|
constructor(/* url: string, protocols?: string | string[] */) {
|
|
// No-op: don't open real connections in tests
|
|
}
|
|
send(/* data: unknown */) {}
|
|
close() { this.readyState = MockWebSocket.CLOSED }
|
|
addEventListener() {}
|
|
removeEventListener() {}
|
|
dispatchEvent() { return true }
|
|
} as unknown as typeof WebSocket
|
|
|
|
// Mock scrollIntoView for jsdom (not implemented)
|
|
Element.prototype.scrollIntoView = vi.fn()
|
|
|
|
// Mock ResizeObserver for jsdom (not implemented)
|
|
globalThis.ResizeObserver = class {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
} as unknown as typeof ResizeObserver
|
|
|
|
// Mock IntersectionObserver for jsdom (not implemented)
|
|
globalThis.IntersectionObserver = class {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
} as unknown as typeof IntersectionObserver
|
|
|
|
// Mock @tauri-apps/plugin-opener for test environment
|
|
vi.mock('@tauri-apps/plugin-opener', () => ({
|
|
openUrl: vi.fn(),
|
|
}))
|
|
|
|
// Mock react-day-picker: Calendar component uses DayPicker which needs real DOM APIs not available in jsdom
|
|
vi.mock('react-day-picker', () => ({
|
|
DayPicker: () => null,
|
|
getDefaultClassNames: () => ({}),
|
|
}))
|
|
|
|
// Mock react-virtuoso: JSDOM has no real viewport, so render all items directly
|
|
vi.mock('react-virtuoso', () => ({
|
|
Virtuoso: ({ data, itemContent, components }: {
|
|
data?: unknown[]
|
|
itemContent?: (index: number, item: unknown) => ReactNode
|
|
components?: { Header?: ComponentType }
|
|
}) => {
|
|
const Header = components?.Header
|
|
return createElement('div', { 'data-testid': 'virtuoso-mock' },
|
|
Header ? createElement(Header) : null,
|
|
data?.map((item: unknown, index: number) =>
|
|
createElement('div', { key: index }, itemContent?.(index, item))
|
|
)
|
|
)
|
|
},
|
|
GroupedVirtuoso: ({ groupCounts, groupContent, itemContent }: {
|
|
groupCounts: number[]
|
|
groupContent: (index: number) => ReactNode
|
|
itemContent: (index: number, groupIndex: number) => ReactNode
|
|
}) => {
|
|
let globalIndex = 0
|
|
return createElement('div', { 'data-testid': 'grouped-virtuoso-mock' },
|
|
groupCounts?.map((count: number, groupIndex: number) => {
|
|
const items = []
|
|
for (let i = 0; i < count; i++) {
|
|
items.push(createElement('div', { key: globalIndex }, itemContent(globalIndex, groupIndex)))
|
|
globalIndex++
|
|
}
|
|
return createElement('div', { key: `group-${groupIndex}` },
|
|
groupContent(groupIndex),
|
|
...items
|
|
)
|
|
})
|
|
)
|
|
},
|
|
}))
|