import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' import test from 'node:test' import ts from 'typescript' const sourceUrl = new URL('../../unreal_tran_web/src/features/scene-legacy/sceneCoverCapture.ts', import.meta.url) const source = await readFile(sourceUrl, 'utf8') const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 }, reportDiagnostics: true, }) assert.deepEqual(output.diagnostics, []) const { captureSceneCover } = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString('base64')}`) function fixture({ width = 1600, height = 900, renderError, drawError, encodeError, blob } = {}) { const events = [] const transformHelper = { visible: true } const grid = { visible: false } const boundsHelper = { visible: true } const materials = [ { original: { id: 'single-original' }, current: { id: 'single-highlighted' } }, { original: [{ id: 'array-original-1' }, { id: 'array-original-2' }], current: [{ id: 'array-highlighted-1' }, { id: 'array-highlighted-2' }] }, ] const entries = materials.map(({ original, current }) => ({ child: { material: current }, original, highlighted: current })) const sourceCanvas = { width, height } const scene = { children: [transformHelper, grid, boundsHelper, ...entries.map(entry => entry.child)] } const camera = Object.freeze({ position: Object.freeze({ x: 1, y: 2, z: 3 }) }) const selected = entries[0].child const visibility = () => [transformHelper.visible, grid.visible, boundsHelper.visible] const snapshot = operation => ({ operation, visibility: visibility(), materials: entries.map(entry => entry.child.material) }) let renders = 0 const neverCalled = () => assert.fail('capture must not reset, detach, or dispose selection') const editor = { scene, camera, destroyed: false, selected, controls: { target: Object.freeze({ x: 0, y: 0, z: 0 }), update: neverCalled }, transform: { getHelper: () => transformHelper, detach: neverCalled }, grid, selectionHighlight: { helper: boundsHelper, materialEntries: entries, object: selected, setObject: neverCalled, clear: neverCalled, update: neverCalled, }, renderer: { domElement: sourceCanvas, render(actualScene, actualCamera) { assert.equal(actualScene, scene) assert.equal(actualCamera, camera) events.push(snapshot('render')) renders += 1 if (renders === 1 && renderError) throw renderError }, }, } const canvas = { width: 0, height: 0, getContext(kind) { assert.equal(kind, '2d') return { drawImage(...args) { assert.deepEqual(args, [sourceCanvas, 0, 0, canvas.width, canvas.height]) events.push(snapshot('copy')) if (drawError) throw drawError }, } }, toBlob(callback, type, quality) { events.push({ ...snapshot('encode'), type, quality }) if (encodeError) throw encodeError queueMicrotask(() => callback(blob === undefined ? new Blob(['jpeg'], { type }) : blob)) }, } const document = { createElement: tag => { assert.equal(tag, 'canvas'); return canvas } } const assertRestored = () => { assert.deepEqual(visibility(), [true, false, true]) entries.forEach((entry, index) => { assert.equal(entry.child.material, materials[index].current) assert.equal(entry.original, materials[index].original) assert.equal(entry.highlighted, materials[index].current) }) assert.equal(editor.selectionHighlight.materialEntries, entries) assert.equal(editor.selectionHighlight.object, selected) assert.equal(editor.selected, selected) assert.equal(scene.children.length, 5) assert.deepEqual(camera.position, { x: 1, y: 2, z: 3 }) assert.deepEqual(editor.controls.target, { x: 0, y: 0, z: 0 }) } return { editor, document, events, canvas, materials, entries, assertRestored } } test('copies synchronously without bounds, gizmo, grid or selection glow, then restores before JPEG encoding', async () => { const f = fixture() const pending = captureSceneCover(f.editor, { document: f.document }) assert.deepEqual(f.events.map(event => event.operation), ['render', 'copy', 'render', 'encode']) for (const event of f.events.slice(0, 2)) { assert.deepEqual(event.visibility, [false, false, false]) event.materials.forEach((material, index) => assert.equal(material, f.materials[index].original)) } for (const event of f.events.slice(2)) { assert.deepEqual(event.visibility, [true, false, true]) event.materials.forEach((material, index) => assert.equal(material, f.materials[index].current)) } f.assertRestored() assert.equal(f.events[3].type, 'image/jpeg') assert.equal(f.events[3].quality, 0.85) const result = await pending assert.equal(result.type, 'image/jpeg') assert.equal(result.size, 4) }) for (const { width, height, maxWidth, expected } of [ { width: 1600, height: 900, expected: [640, 360] }, { width: 900, height: 1600, expected: [360, 640] }, { width: 320, height: 180, expected: [320, 180] }, { width: 1600, height: 900, maxWidth: 320, expected: [320, 180] }, ]) { test(`preserves the ${width} x ${height} ratio within ${maxWidth ?? 640}px and never upscales`, async () => { const f = fixture({ width, height }) await captureSceneCover(f.editor, { document: f.document, maxWidth }) assert.deepEqual([f.canvas.width, f.canvas.height], expected) f.assertRestored() }) } for (const failure of ['drawError', 'renderError', 'encodeError']) { test(`restores helper visibility, scalar/array materials and selection after ${failure}`, async () => { const error = new Error(failure) const f = fixture({ [failure]: error }) await assert.rejects(captureSceneCover(f.editor, { document: f.document }), actual => actual === error) f.assertRestored() assert.equal(f.events.filter(event => event.operation === 'render').length, 2) if (failure !== 'encodeError') assert.equal(f.events.some(event => event.operation === 'encode'), false) }) } test('restores the actual material reference even if it differs from the recorded highlighted clone', async () => { const f = fixture() const replacement = { id: 'material-changed-after-selection' } f.entries[0].child.material = replacement await captureSceneCover(f.editor, { document: f.document }) assert.equal(f.entries[0].child.material, replacement) assert.equal(f.events[0].materials[0], f.materials[0].original) }) test('supports a ready scene without selection helpers', async () => { const f = fixture() delete f.editor.transform delete f.editor.grid delete f.editor.selectionHighlight assert.equal((await captureSceneCover(f.editor, { document: f.document })).type, 'image/jpeg') }) test('deduplicates shared helper objects before recording visibility', async () => { const f = fixture() f.editor.transform.getHelper = () => f.editor.selectionHighlight.helper await captureSceneCover(f.editor, { document: f.document }) f.assertRestored() }) for (const blob of [null, new Blob([])]) { test(`rejects ${blob === null ? 'missing' : 'empty'} encoded images without disturbing the editor`, async () => { const f = fixture({ blob }) await assert.rejects(captureSceneCover(f.editor, { document: f.document }), /编码结果为空/) f.assertRestored() }) } test('refuses an unready or destroyed renderer before changing any scene state', async () => { for (const change of [ f => { f.editor.destroyed = true }, f => { f.editor.renderer = null }, f => { f.editor.renderer.domElement.width = 0 }, f => { f.editor.renderer.domElement.height = Number.NaN }, f => { f.editor.scene = null }, f => { f.editor.camera = null }, ]) { const f = fixture() change(f) await assert.rejects(captureSceneCover(f.editor, { document: f.document }), /视口尚未准备好/) f.assertRestored() assert.equal(f.events.length, 0) } }) test('refuses invalid dimensions and unavailable 2D canvas before changing scene state', async () => { const f = fixture() await assert.rejects(captureSceneCover(f.editor, { document: f.document, maxWidth: 0 }), /正数像素/) f.canvas.getContext = () => null await assert.rejects(captureSceneCover(f.editor, { document: f.document }), /不支持场景封面编码/) f.assertRestored() assert.equal(f.events.length, 0) })