Files
Web_FreeCAD_Bitbybit/tests/facade.test.ts

150 lines
6.7 KiB
TypeScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { createMockFacade } from '../src/facade/mockFacade'
import { PROJECT_SCHEMA_MIGRATIONS, PROJECT_SCHEMA_SQL, PROJECT_SCHEMA_VERSION } from '../src/facade/projectSchema'
import { PersistenceWriteQueue } from '../src/facade/projectStore'
test('facade exposes a stable initial document projection', () => {
const facade = createMockFacade()
const state = facade.getState()
assert.equal(state.apiVersion, '0.1')
assert.equal(state.document.id, 'doc-pump-housing')
assert.equal(state.document.tree.find((item) => item.id === 'body')?.type, 'body')
assert.equal(facade.gui.workbench.getActive(), 'Part Design')
})
test('project schema is versioned and covers the FreeCAD document graph', () => {
assert.equal(PROJECT_SCHEMA_VERSION, 1)
assert.equal(PROJECT_SCHEMA_MIGRATIONS[0].version, PROJECT_SCHEMA_VERSION)
for (const table of ['projects', 'documents', 'objects', 'object_properties', 'dependencies', 'transactions', 'resources']) assert.match(PROJECT_SCHEMA_SQL, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`))
assert.match(PROJECT_SCHEMA_SQL, /CREATE INDEX IF NOT EXISTS objects_document_ordinal/)
})
test('persistence writes are serialized and continue after a failed write', async () => {
const queue = new PersistenceWriteQueue()
let inFlight = 0
let maximumInFlight = 0
const order: string[] = []
const write = (id: string, shouldFail = false) => queue.run(async () => {
inFlight += 1
maximumInFlight = Math.max(maximumInFlight, inFlight)
await new Promise((resolve) => setTimeout(resolve, 2))
order.push(id)
inFlight -= 1
if (shouldFail) throw new Error('expected write failure')
return id
})
const results = await Promise.allSettled([write('one'), write('two', true), write('three')])
assert.equal(maximumInFlight, 1)
assert.deepEqual(order, ['one', 'two', 'three'])
assert.equal(results[0].status, 'fulfilled')
assert.equal(results[1].status, 'rejected')
assert.equal(results[2].status, 'fulfilled')
})
test('workbench and selection changes are event driven', () => {
const facade = createMockFacade()
const events: string[] = []
const unsubscribe = facade.subscribe((event) => events.push(event.type))
facade.gui.workbench.setActive('Sketcher')
facade.selection.select('sketch')
unsubscribe()
assert.equal(facade.getState().activeWorkbench, 'Sketcher')
assert.equal(facade.selection.getObjectId(), 'sketch')
assert.ok(events.includes('state.changed'))
assert.ok(events.includes('notice'))
})
test('commands and tasks preserve apply/cancel lifecycle', () => {
const facade = createMockFacade()
facade.gui.command.execute({ commandId: 'create-sketch' })
assert.equal(facade.task.getActive()?.status, 'preview')
facade.task.update({ support: 'XY_Plane' })
assert.equal(facade.task.getActive()?.draft.support, 'XY_Plane')
facade.task.cancel()
assert.equal(facade.task.getActive()?.status, 'cancelled')
})
test('disabled commands return a reason instead of mutating the document', () => {
const facade = createMockFacade()
const before = facade.getState().document.version
const state = facade.gui.command.getState('pad')
assert.equal(state.status, 'enabled')
facade.selection.clear()
facade.gui.workbench.setActive('Sketcher')
const disabled = facade.gui.command.getState('pad')
assert.equal(disabled.status, 'disabled')
assert.match(disabled.reason || '', /Part Design|compatible object/)
facade.gui.command.execute({ commandId: 'pad' })
assert.equal(facade.getState().diagnostics.at(-1)?.code, 'COMMAND_DISABLED')
assert.equal(facade.getState().document.version, before)
})
test('command events carry a document-scoped request context', () => {
const facade = createMockFacade()
const events: string[] = []
let requestId = ''
facade.subscribe((event) => { events.push(event.type); if (event.type === 'command.started') requestId = event.context.requestId })
facade.gui.command.execute({ commandId: 'create-sketch' })
assert.ok(events.includes('command.started'))
assert.ok(events.includes('command.completed'))
assert.match(requestId, /^req-/)
})
test('document mutations use the facade history boundary', () => {
const facade = createMockFacade()
facade.gui.command.execute({ commandId: 'new-document' })
assert.equal(facade.app.document.getActive().label, 'Untitled document')
assert.equal(facade.history.canUndo(), true)
facade.history.undo()
assert.equal(facade.app.document.getActive().label, 'Pump Housing')
assert.equal(facade.history.canRedo(), true)
facade.history.redo()
assert.equal(facade.app.document.getActive().label, 'Untitled document')
})
test('project persistence remains behind the facade contract', async () => {
const facade = createMockFacade()
const saved = await facade.project.save()
assert.equal(saved.documentId, 'doc-pump-housing')
const loaded = await facade.project.load('doc-pump-housing')
assert.equal(loaded?.label, 'Pump Housing')
assert.equal(facade.project.capabilities().mode, 'sqlite-memory')
})
test('project resources use content identity and reference counting', async () => {
const facade = createMockFacade()
const bytes = new Uint8Array([1, 2, 3, 5, 8])
const first = await facade.project.resource.put(bytes, 'application/octet-stream')
const second = await facade.project.resource.put(bytes, 'application/octet-stream')
assert.equal(first.hash, second.hash)
assert.equal(second.refCount, 2)
bytes[0] = 99
assert.deepEqual([...((await facade.project.resource.get(first.hash)) || [])], [1, 2, 3, 5, 8])
await facade.project.resource.release(first.hash)
assert.deepEqual([...((await facade.project.resource.get(first.hash)) || [])], [1, 2, 3, 5, 8])
await facade.project.resource.release(first.hash)
assert.equal(await facade.project.resource.get(first.hash), null)
})
test('Part Design feature tasks commit a document object and remain undoable', () => {
const facade = createMockFacade()
const before = facade.app.document.getActive()
facade.gui.command.execute({ commandId: 'pad' })
assert.equal(facade.task.getActive()?.commandId, 'pad')
facade.task.update({ length: 42, reversed: false })
facade.task.apply()
const committed = facade.app.document.getActive()
const created = committed.tree.find((item) => item.id === 'pad001')
assert.equal(created?.label, 'Pad')
assert.equal(committed.version, before.version + 1)
assert.equal(facade.selection.getObjectId(), 'pad001')
assert.equal(committed.dirty, true)
facade.history.undo()
assert.equal(facade.app.document.getActive().tree.some((item) => item.id === 'pad001'), false)
facade.history.redo()
assert.equal(facade.app.document.getActive().tree.some((item) => item.id === 'pad001'), true)
})