import assert from "node:assert/strict" import { readFileSync, writeFileSync, mkdirSync } from "node:fs" import vm from "node:vm" import { webcrypto } from "node:crypto" import { createServer } from "vite" const vite = await createServer({ server: { middlewareMode: true }, appType: "custom", logLevel: "error" }) try { const { prepareMediaForExport } = await vite.ssrLoadModule("/src/utils/aiPptExport.ts") // 加载生产导出器,在测试副本暴露其转换函数;不改写或替换生产转换逻辑。 const source = readFileSync("public/pptworker/pptJson.js", "utf8") .replace('r((r.s = "9a7e"))', 'globalThis.bundleRequire = r') .replace('Sr = function (e, t) {', 'Sr = globalThis.renderPptForTest = function (e, t) {') const context = vm.createContext({ console, setTimeout, clearTimeout, setInterval, clearInterval, TextEncoder, TextDecoder, Uint8Array, ArrayBuffer, Buffer, Blob, crypto: webcrypto, fetch: () => { throw new Error("离线导出不得请求网络") } }) context.self = context vm.runInContext(source, context) context.bundleRequire("f5f5") assert.equal(typeof context.renderPptForTest, "function") const png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" const wav = Buffer.alloc(44 + 8000 * 2) wav.write("RIFF"); wav.writeUInt32LE(wav.length - 8, 4); wav.write("WAVEfmt ", 8) wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22) wav.writeUInt32LE(8000, 24); wav.writeUInt32LE(16000, 28); wav.writeUInt16LE(2, 32); wav.writeUInt16LE(16, 34) wav.write("data", 36); wav.writeUInt32LE(16000, 40) for (let i = 0; i < 8000; i++) wav.writeInt16LE(Math.round(Math.sin(i * Math.PI * 2 * 440 / 8000) * 5000), 44 + i * 2) const audio = `data:audio/wav;base64,${wav.toString("base64")}` const base = { left: 80, top: 180, width: 60, height: 60, rotate: 0 } const slides = [{ id: "offline-test", remark: "参考答案:轻声哼唱\n解析:比较声音的强弱。", elements: [ { ...base, id: "title", type: "text", width: 1000, height: 90, content: '
离线媒体测试
', defaultFontName: "Microsoft Yahei", defaultColor: "#222222" }, { ...base, top: 320, id: "audio-a", type: "elf", subtype: "elf-audio", src: audio, color: "#333333", autoplay: false, loop: false, fixedRatio: true }, { ...base, left: 300, top: 320, id: "legacy-audio", type: "elf", subtype: "elf-enjoy", src: audio, enjoyList: [{ src: audio }] }, { ...base, top: 500, id: "image", type: "image", src: png, fixedRatio: true } ] }] const prepared = await prepareMediaForExport(slides) assert.equal(prepared[0].elements.filter(element => element.subtype === "elf-audio").length, 2) const ppt = await context.renderPptForTest({ width: 1920, height: 1080, slides: prepared, theme: { fontName: "Microsoft Yahei", fontColor: "#222222", viewportRatio: 0.5625 } }, { viewportRatio: 0.5625 }) const bytes = Buffer.from(await ppt.write({ outputType: "arraybuffer" })) assert.equal(bytes.subarray(0, 2).toString(), "PK") mkdirSync("artifacts/ai-ppt-tests", { recursive: true }) writeFileSync("artifacts/ai-ppt-tests/offline-media.pptx", bytes) await assert.rejects(() => prepareMediaForExport([{ id: "score", elements: [{ ...base, id: "score", type: "elf", subtype: "elf-sing-play", sid: "1" }] }]), /静态|谱图/) console.log(JSON.stringify({ passed: true, bytes: bytes.length, output: "artifacts/ai-ppt-tests/offline-media.pptx", checks: ["native-audio", "legacy-audio", "offline-embedded-assets", "reject-online-only-score"] })) } finally { await vite.close() }