Files
zeekay bc4ddf332b chore: rebase the tree onto upstream master fa1a9a60
Mechanically re-derived: bin/debrand runs over a fresh import of
posthog/posthog@fa1a9a60d4 (2026-08-06).
The pipeline (.hanzo/workflows), the Dockerfiles and bin/docker*
entrypoints, compose files, LLM.md and the squashed initial migration
are restored from the shipped tree, same as the previous import.

The prune's import-closure guard kept 21 products the new core now
imports; dropping them is a refactor now, recorded rather than forced.
2026-08-06 01:25:36 -07:00

213 lines
7.3 KiB
TypeScript

import 'jest-canvas-mock'
import { configure } from '@testing-library/react'
import { TextDecoder, TextEncoder } from 'util'
import { deserialize, serialize } from 'v8'
// Jest/JSDom don't know about TextEncoder but the browsers we support do
// https://github.com/jsdom/jsdom/issues/2524
global.TextDecoder = TextDecoder as any
global.TextEncoder = TextEncoder as any
window.scrollTo = jest.fn()
window.matchMedia = jest.fn(
(query: string) =>
({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}) as any
)
type StorageLike = {
getItem: (key: string) => string | null
setItem: (key: string, value: string) => void
removeItem: (key: string) => void
clear: () => void
}
const createInMemoryStorage = (): StorageLike => {
const store = new Map<string, string>()
return {
getItem: (key) => (store.has(key) ? store.get(key)! : null),
setItem: (key, value) => {
store.set(key, String(value))
},
removeItem: (key) => {
store.delete(key)
},
clear: () => {
store.clear()
},
}
}
// Some Jest/JSDom environments might not provide an unqualified `sessionStorage`
// binding. `sceneLogic` and other Kea logic reference `sessionStorage` as a
// free variable, so we need to ensure the identifier exists.
if (typeof sessionStorage === 'undefined') {
const sessionStorageStub = createInMemoryStorage()
;(globalThis as any).sessionStorage = sessionStorageStub
;(window as any).sessionStorage = sessionStorageStub
;(global as any).sessionStorage = sessionStorageStub
}
if (typeof localStorage === 'undefined') {
const localStorageStub = createInMemoryStorage()
;(globalThis as any).localStorage = localStorageStub
;(window as any).localStorage = localStorageStub
;(global as any).localStorage = localStorageStub
}
// jsdom does not implement structuredClone — polyfill via Node's v8 serializer
if (typeof globalThis.structuredClone !== 'function') {
globalThis.structuredClone = <T>(value: T): T => deserialize(serialize(value))
}
// jsdom does not implement AbortSignal.timeout — polyfill for tests
if (typeof AbortSignal.timeout !== 'function') {
AbortSignal.timeout = (ms: number): AbortSignal => {
const controller = new AbortController()
setTimeout(() => controller.abort(new DOMException('TimeoutError', 'TimeoutError')), ms)
return controller.signal
}
}
// Base UI's ScrollArea calls getAnimations() which jsdom doesn't support
if (typeof Element.prototype.getAnimations !== 'function') {
Element.prototype.getAnimations = () => []
}
// Menu calls scrollIntoView which jsdom doesn't support
if (typeof Element.prototype.scrollIntoView !== 'function') {
Element.prototype.scrollIntoView = () => {}
}
// maplibre-gl probes window.URL.createObjectURL at module-eval time, but jsdom
// doesn't implement it. Provide a noop so importing modules that transitively
// pull in maplibre-gl (e.g. NotebookNodeMap) doesn't throw.
if (typeof window !== 'undefined' && typeof window.URL.createObjectURL !== 'function') {
window.URL.createObjectURL = () => ''
window.URL.revokeObjectURL = () => {}
}
// we use CSS.escape in the toolbar, but Jest/JSDom doesn't support it
if (typeof (globalThis as any).CSS === 'undefined') {
;(globalThis as any).CSS = {}
}
if (typeof (globalThis as any).CSS.escape !== 'function') {
;(globalThis as any).CSS.escape = (value: string) => value
}
const mockIntersectionObserver = jest.fn()
mockIntersectionObserver.mockReturnValue({
observe: () => null,
unobserve: () => null,
disconnect: () => null,
})
;(globalThis as any).IntersectionObserver = mockIntersectionObserver
const mockResizeObserver = jest.fn()
mockResizeObserver.mockReturnValue({
observe: () => null,
unobserve: () => null,
disconnect: () => null,
})
;(globalThis as any).ResizeObserver = mockResizeObserver
// jsdom has no layout, so hit-testing at a point can't return anything
document.elementsFromPoint = document.elementsFromPoint ?? ((): Element[] => [])
// Tell React Testing Library to use "data-attr" as the test ID attribute
configure({ testIdAttribute: 'data-attr' })
// Mock DecompressionWorkerManager globally to avoid import.meta.url issues in tests
jest.mock('scenes/session-recordings/player/snapshot-processing/DecompressionWorkerManager')
// Mock ConcurrencyController to be a pass-through. The module-level singleton in
// dataNodeLogic persists across test files on the same Jest worker (resetModules is
// disabled). Under CI load, stale items can block subsequent tests' queries indefinitely.
// Tests don't need concurrency limiting, so we bypass it entirely.
jest.mock('lib/utils/concurrencyController', () => {
class ConcurrencyController {
run = <T>({ fn }: { fn: () => Promise<T> }): Promise<T> => fn()
setConcurrencyLimit = (): void => {}
}
return { ConcurrencyController }
})
// Mock insights-js surveys-preview to avoid ESM import issues in tests
jest.mock('insights-js/dist/surveys-preview', () => ({
renderFeedbackWidgetPreview: jest.fn(),
renderSurveysPreview: jest.fn(),
getNextSurveyStep: jest.fn(),
}))
// Mock insights-js product-tours-preview to avoid ESM import issues in tests
jest.mock('insights-js/dist/product-tours-preview', () => ({
renderProductTourPreview: jest.fn(),
}))
// Mock insights-js element-inference to avoid ESM import issues in tests
jest.mock('insights-js/dist/element-inference', () => ({
findElement: jest.fn(),
getElementPath: jest.fn(),
}))
// Mock insights-js to avoid issues in tests
jest.mock('insights-js', () => {
// Get the actual module to preserve type exports (enums, etc.)
const actual = jest.requireActual('insights-js')
const mock: Record<string, any> = {
capture: jest.fn(),
captureException: jest.fn(),
captureRaw: jest.fn(),
opt_in_capturing: jest.fn(),
identify: jest.fn(),
getFeatureFlag: jest.fn(),
getFeatureFlagPayload: jest.fn(),
getFeatureFlagResult: jest.fn(),
getAllFlags: jest.fn(),
isFeatureEnabled: jest.fn(),
getEarlyAccessFeatures: jest.fn(),
getSurveys: jest.fn(),
onFeatureFlags: jest.fn(() => () => {}),
debug: jest.fn(),
get_session_id: jest.fn(),
get_session_replay_url: jest.fn(),
get_distinct_id: jest.fn(),
register: jest.fn(),
reset: jest.fn(),
group: jest.fn(),
updateEarlyAccessFeatureEnrollment: jest.fn(),
people: { set: jest.fn() },
featureFlags: { override: jest.fn() },
metrics: { count: jest.fn(), gauge: jest.fn(), histogram: jest.fn() },
}
mock.init = jest.fn(() => mock)
// Return mock functions but preserve actual type exports
return { ...actual, __esModule: true, default: mock, insights: mock }
})
jest.mock('@tiptap/extension-code-block-lowlight', () => {
const mockExtension = {
configure: jest.fn(() => ({})),
extend: jest.fn(() => ({
configure: jest.fn(() => ({})),
})),
}
return {
__esModule: true,
default: mockExtension,
CodeBlockLowlight: mockExtension,
}
})