* test(collab): add deterministic browser sessions - Extract a typed room transport boundary around Trystero - Add a development-only WebSocket relay transport for browser tests - Cover two-peer edits, awareness, departure cleanup, and reconnect convergence without public network services * fix(collab): harden browser test transport - Scope collaboration controls to explicit development test hooks - Validate bounded relay messages and reject duplicate action receivers - Remove only awareness clients owned by a departing peer - Guarantee relay and browser cleanup after setup failures * fix(collab): strengthen browser session coverage - Prevent remote awareness rebroadcast and track ownership by transport peer - Guard and validate the development relay lifecycle - Simulate a network partition with retained peers and concurrent offline edits - Make browser and relay setup and teardown failure-safe * test(collab): prove partitioned peer convergence - Queue and assert traffic during a deterministic network partition - Converge non-conflicting edits from retained host and guest sessions - Reject relay startup failures and guard test-only browser APIs
73 lines
2.1 KiB
TypeScript
73 lines
2.1 KiB
TypeScript
import type { ChatTransport, UIMessage } from 'ai'
|
|
|
|
import type { CollabReturn } from '@/app/collab/context'
|
|
import type { EditorStore } from '@/app/editor/session/create'
|
|
import { IS_BROWSER } from '@/constants'
|
|
|
|
export interface OpenPencilTestHooks {
|
|
writeCount?: () => number
|
|
mockHandle?: FileSystemFileHandle
|
|
savedOpen?: Window['open']
|
|
collab?: Pick<
|
|
CollabReturn,
|
|
'connect' | 'disconnect' | 'updateCursor' | 'updateSelection' | 'setLocalName'
|
|
> & {
|
|
peerCount: () => number
|
|
peerSelections: () => Array<string[] | undefined>
|
|
}
|
|
}
|
|
|
|
export interface OpenPencilWindowAPI {
|
|
getStore?: () => EditorStore
|
|
setChatTransport?: (factory: () => ChatTransport<UIMessage>) => void
|
|
openFile?: (path: string) => Promise<void>
|
|
test?: OpenPencilTestHooks
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
openPencil?: OpenPencilWindowAPI
|
|
}
|
|
}
|
|
|
|
let activeStore: EditorStore | null = null
|
|
|
|
function windowAPI(): OpenPencilWindowAPI {
|
|
window.openPencil ??= {}
|
|
window.openPencil.getStore ??= () => {
|
|
if (!activeStore) throw new Error('OpenPencil store not initialized')
|
|
return activeStore
|
|
}
|
|
return window.openPencil
|
|
}
|
|
|
|
export function setOpenPencilStore(store: EditorStore) {
|
|
activeStore = store
|
|
windowAPI()
|
|
}
|
|
|
|
export function exposeCollaborationActions(collab: CollabReturn) {
|
|
if (!IS_BROWSER || !import.meta.env.DEV) return
|
|
if (!new URLSearchParams(window.location.search).has('test')) return
|
|
const testHooks = (windowAPI().test ??= {})
|
|
testHooks.collab = {
|
|
connect: collab.connect,
|
|
disconnect: collab.disconnect,
|
|
updateCursor: collab.updateCursor,
|
|
updateSelection: collab.updateSelection,
|
|
setLocalName: collab.setLocalName,
|
|
peerCount: () => collab.remotePeers.value.length,
|
|
peerSelections: () => collab.remotePeers.value.map((peer) => peer.selection)
|
|
}
|
|
}
|
|
|
|
export function exposeChatTransportOverride(
|
|
setChatTransport: (factory: () => ChatTransport<UIMessage>) => void
|
|
) {
|
|
windowAPI().setChatTransport = setChatTransport
|
|
}
|
|
|
|
export function setOpenPencilOpenFileHandler(openFile: (path: string) => Promise<void>) {
|
|
windowAPI().openFile = openFile
|
|
}
|