Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173import '@testing-library/jest-dom/vitest'import { extractMetadata } from './services/metadata/extract-metadata'import MiniSearch from 'minisearch'import { deriveLinkgraph } from './services/metadata/linkgraph'
// Vitest has no browser Worker. This is a test-only boundary for the// metadata, linkgraph, and search workers.if (typeof globalThis.Worker === 'undefined') { globalThis.Worker = class TestMetadataWorker { onmessage: ((event: MessageEvent) => void) | null = null onerror: ((event: ErrorEvent) => void) | null = null onmessageerror: ((event: MessageEvent) => void) | null = null private readonly search = new MiniSearch<{ id: string; url: string }>({ fields: ['name', 'path', 'content'], storeFields: ['url'] }) private readonly searchIds = new Map<string, string>()
constructor(_url: string | URL, _options?: WorkerOptions) {}
postMessage(message: { id: number; content?: string; type?: string; input?: any }): void { queueMicrotask(async () => { try { if (message.type === 'prepare') { const request = message as any this.onmessage?.({ data: { type: 'prepared', requestId: request.requestId, document: request.document, tokens: {} } } as MessageEvent) return } if (message.type === 'index') { const request = message as any const oldId = this.searchIds.get(request.document.url) const id = `${request.document.url}-${Date.now()}-${Math.random()}` this.search.add({ ...request.document, id }) if (oldId) this.search.discard(oldId) this.searchIds.set(request.document.url, id) return } if (message.type === 'remove') { const request = message as any const oldId = this.searchIds.get(request.url) if (oldId) this.search.discard(oldId) this.searchIds.delete(request.url) return } if (message.type === 'clear') { this.search.removeAll() this.searchIds.clear() return } if (message.type === 'query') { const request = message as any this.onmessage?.({ data: { type: 'query-result', requestId: request.requestId, results: this.search.search(request.query).map((result: any) => ({ id: String(result.id), url: result.url, score: result.score, match: result.match ?? {} })) } } as MessageEvent) return } if (message.type === 'derive' && Array.isArray((message as any).files)) { const request = message as any this.onmessage?.({ data: deriveLinkgraph(request.files, request.generation) } as MessageEvent) return } if (message.type === 'deriveTyped' && message.input) { const { projectRecordsFromSnapshot, buildTypedDocumentContribution, buildNodeIndexFromSnapshot } = await import('./types-records/record-projection') const built = message.input.nodeIndex ? undefined : message.input.blocks ? buildNodeIndexFromSnapshot(message.input.markdown, message.input.blocks, JSON.stringify([...message.input.heads].sort())) : undefined const projection = projectRecordsFromSnapshot({ ...message.input, ...(built?.status === 'available' ? { nodeIndex: built.index } : {}) }) if (projection.status !== 'available') throw new Error(`Typed Record projection unresolved: ${projection.reason}`) const nodeIndex = message.input.nodeIndex ? (() => { const snapshot = message.input.nodeIndex const entries = new Map(snapshot.nodes.map((node: any) => [node.id, node])) return { ...snapshot, getNode: (id: string) => entries.get(id), nodeAtStructure: () => undefined } })() : built?.status === 'available' ? built.index : undefined this.onmessage?.({ data: { id: message.id, contribution: buildTypedDocumentContribution({ ...message.input, projection, nodeIndex }), index: nodeIndex && { headsKey: nodeIndex.headsKey, nodes: nodeIndex.nodes, hasUnmappedCustomMarkers: nodeIndex.hasUnmappedCustomMarkers } } } as MessageEvent) return } if (message.type === 'derivePreview' && message.input) { const { projectRecordsFromSnapshot, buildNodeIndexFromSnapshot, hydrateNodeIndexSnapshot } = await import('./types-records/record-projection') const { readRootFrontmatterSurface, rootApplicationsFromSurface } = await import('./types-records/root-frontmatter-surface') const input = message.input const root = readRootFrontmatterSurface({ source: input.markdown, catalog: input.catalog, documentId: input.documentId }) const projection = projectRecordsFromSnapshot({ ...input, rootApplications: rootApplicationsFromSurface(root) }) const built = input.nodeIndex ? undefined : input.blocks ? buildNodeIndexFromSnapshot(input.markdown, input.blocks, JSON.stringify([...input.heads].sort())) : undefined const nodeIndex = input.nodeIndex ? hydrateNodeIndexSnapshot(input.nodeIndex) : built?.status === 'available' ? built.index : undefined this.onmessage?.({ data: { id: message.id, projection, nodeIndex: nodeIndex && { headsKey: nodeIndex.headsKey, nodes: nodeIndex.nodes, hasUnmappedCustomMarkers: nodeIndex.hasUnmappedCustomMarkers } } } as MessageEvent) return } if (message.type === 'deriveProjection' && message.input) { const { projectRecordsFromSnapshot, buildTypedDocumentContribution, buildNodeIndexFromSnapshot } = await import('./types-records/record-projection') const { readRootFrontmatterSurface, rootApplicationsFromSurface } = await import('./types-records/root-frontmatter-surface') const { extractMetadata } = await import('./services/metadata/extract-metadata') const input = message.input const built = input.nodeIndex ? undefined : input.blocks ? buildNodeIndexFromSnapshot(input.markdown, input.blocks, JSON.stringify([...input.heads].sort())) : undefined const nodeIndex = input.nodeIndex ?? (built?.status === 'available' ? built.index : undefined) const hydratedNodeIndex = nodeIndex && 'getNode' in nodeIndex ? nodeIndex : nodeIndex ? (await import('./types-records/record-projection')).hydrateNodeIndexSnapshot(nodeIndex) : undefined const root = readRootFrontmatterSurface({ source: input.markdown, catalog: input.catalog, documentId: input.documentId }) const projection = projectRecordsFromSnapshot({ ...input, ...(nodeIndex ? { nodeIndex } : {}), rootApplications: rootApplicationsFromSurface(root) }) this.onmessage?.({ data: { id: message.id, derivation: { metadata: extractMetadata(input.markdown), contentHash: '', contexts: {}, projection, contribution: projection.status === 'available' ? buildTypedDocumentContribution({ ...input, projection, nodeIndex: hydratedNodeIndex }) : undefined, nodeIndex: nodeIndex && { headsKey: nodeIndex.headsKey, nodes: nodeIndex.nodes, hasUnmappedCustomMarkers: nodeIndex.hasUnmappedCustomMarkers } } } } as MessageEvent) return } this.onmessage?.({ data: { id: (message as any).id, metadata: extractMetadata((message as any).content ?? '') } } as MessageEvent) } catch (error) { this.onerror?.({ error } as ErrorEvent) } }) }
terminate(): void {} } as unknown as typeof Worker}
// Initialize automerge-subduction WASM module before any tests run.// The @automerge/automerge-repo@2.6.0-subduction.20 version requires// the subduction WASM to be loaded before Repo construction. The main// (non-slim) entrypoint auto-initializes the WASM as a side effect.// Without this, tests fail with "undefined is not an object// (evaluating 'wasm.memorysigner_new')".import '@automerge/automerge-subduction'
// Repo singleton hygiene (plan Task 4 Step 6):// The plan provides `resetRepoForTesting()` for tests that initialize// the repo singleton. A global afterEach hook here was prototyped but// REGRESSED several plugin-loader tests (they stub global `URL` for// blob:url tests, and the dynamic import of repo.ts during teardown// interacts badly with vitest's stubGlobal/restore ordering — the// stub is still active when the hook fires, but vitest's later// restoration in another test file ran in an unexpected order).//// Since the current suite is already green without a global hook// (the repo singleton is lazily constructed; only the few tests in// src/__tests__/repo.test.ts touch it, and they reset in their own// beforeEach), we leave the cross-file isolation to those local// resets. New tests that initialize the repo singleton should follow// the same pattern: import { resetRepoForTesting } from '../repo' and// call it in beforeEach.
// Stub ResizeObserver — jsdom doesn't implement it. The callback is never// invoked automatically (no real layout engine), so widgets that depend on// ResizeObserver (e.g., image-gallery-widget) will keep their initial CSS// class. Tests that need to exercise the resize callback can call// `(observer as any)._callback(entries)` manually.if (typeof globalThis.ResizeObserver === 'undefined') { globalThis.ResizeObserver = class ResizeObserver { _callback: ResizeObserverCallback constructor(callback: ResizeObserverCallback) { this._callback = callback } observe() {} unobserve() {} disconnect() {} } as unknown as typeof globalThis.ResizeObserver}
// Stub DOM measurement APIs that CM6's EditorView uses in requestAnimationFrame// callbacks. jsdom doesn't implement these, causing "getClientRects is not a// function" errors that crash Stryker's test runner.if (typeof Range !== 'undefined') { Range.prototype.getClientRects = Range.prototype.getClientRects || (() => ({ length: 0, item: () => null, [Symbol.iterator]: function* () {} }) as DOMRectList) Range.prototype.getBoundingClientRect = Range.prototype.getBoundingClientRect || (() => ({ top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, x: 0, y: 0, toJSON: () => ({}) }) as DOMRect)}if (typeof document !== 'undefined' && document.createRange) { const origCreateRange = document.createRange.bind(document) document.createRange = () => { const range = origCreateRange() if (!range.getClientRects) { range.getClientRects = () => ({ length: 0, item: () => null, [Symbol.iterator]: function* () {} }) as DOMRectList } if (!range.getBoundingClientRect) { range.getBoundingClientRect = () => ({ top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, x: 0, y: 0, toJSON: () => ({}) }) as DOMRect } return range }}