import JSZip from 'jszip'; import { describe, expect, it } from 'vitest'; import { buildPptxBlob } from '@/lib/export/use-export-pptx'; import { applySlideEditOperation } from '@/lib/edit/slide-ops'; import type { Scene, SlideContent } from '@/lib/types/stage'; import { makeSlideFixture, RATIO_PX2_INCH, RATIO_PX2_PT, VIEWPORT_RATIO, VIEWPORT_SIZE, } from './fixtures'; /** * Per-op round-trip for the `element.update` geometry op (PR1's only * editing op). Asserts the moved/resized/rotated geometry survives the * export pipeline as the exact PPTX `` / `` / `rot` * attributes — and that the un-edited fixture does NOT already carry them, * so the assertion proves the op drove the change rather than matching * boilerplate. See the harness note in `text-content.test.ts`. */ async function exportSlideContent(content: SlideContent, scene: Scene): Promise { return buildPptxBlob( [content.canvas], [scene], VIEWPORT_RATIO, VIEWPORT_SIZE, RATIO_PX2_INCH, RATIO_PX2_PT, ); } async function readPptxEntry(blob: Blob, path: string): Promise { const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const entry = zip.file(path); if (!entry) throw new Error(`PPTX did not contain entry: ${path}`); return entry.async('string'); } // PPTX uses EMU for position/size and 60000ths of a degree for rotation. const px2emu = (px: number) => Math.round((px * 914400) / RATIO_PX2_INCH); const deg2rot = (deg: number) => deg * 60000; describe('round-trip: element.update geometry', () => { it('serializes moved/resized/rotated geometry into the slide xml', async () => { const { scene, content, textElementId } = makeSlideFixture(); const GEO = { left: 333, top: 222, width: 444, height: 111, rotate: 45 }; const before = await readPptxEntry( await exportSlideContent(content, scene), 'ppt/slides/slide1.xml', ); const offNeedle = ``; const extNeedle = ``; const rotNeedle = `rot="${deg2rot(GEO.rotate)}"`; // Guard against a tautology: the default fixture must not already match. expect(before).not.toContain(offNeedle); expect(before).not.toContain(rotNeedle); const after = applySlideEditOperation(content, { type: 'element.update', elementId: textElementId, patch: GEO, }); const slideXml = await readPptxEntry( await exportSlideContent(after, scene), 'ppt/slides/slide1.xml', ); expect(slideXml).toContain(offNeedle); expect(slideXml).toContain(extNeedle); expect(slideXml).toContain(rotNeedle); }); });