aiPptExportSelfTest.mjs 3.6 KB

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