import assert from "node:assert/strict" import { describe, it } from "node:test" import { computeGraphicLayoutStyle, resizeWithHandle } from "./docs-graphic-layout.ts" import { normalizeImportedGraphics } from "./docs-graphic-import.ts" import { DOCS_GRAPHIC_DEFAULTS, parseGraphicAttrs, computeCropApplyPatch, computeCropDisplayGeometry, computeCropImageStyle, computeCropReeditInitialRegion, computeImageContentRect, computeImageFitStyle, layoutFitForImage, usesCropImageFit, resizeCropRegion, } from "./docs-graphic-types.ts" describe("docs-graphic", () => { it("computeGraphicLayoutStyle floats the wrapper so text wraps", () => { const layout = computeGraphicLayoutStyle({ ...DOCS_GRAPHIC_DEFAULTS, graphicType: "image", wrap: "square", floatSide: "left", }) assert.equal(layout.wrapper.float, "left") // Wrap gap sits on the text side of the float. assert.ok(typeof layout.wrapper.marginInlineEnd === "string") }) it("normalizeImportedGraphics preserves draw scene JSON", () => { const scene = '{"type":"excalidraw","elements":[{"id":"a","type":"rectangle"}]}' const normalized = normalizeImportedGraphics({ type: "doc", content: [ { type: "docsGraphic", attrs: { graphicType: "draw", drawScene: scene, src: "data:image/svg+xml;charset=utf-8,%3Csvg%3E%3C/svg%3E", width: 320, height: 240, }, }, ], }) const node = (normalized.content as Array>)[0] assert.equal((node.attrs as { drawScene?: string }).drawScene, scene) }) it("parseGraphicAttrs preserves draw scene JSON", () => { const attrs = parseGraphicAttrs({ graphicType: "draw", drawScene: '{"type":"excalidraw","elements":[]}', src: "data:image/svg+xml;charset=utf-8,%3Csvg%3E%3C/svg%3E", width: 320, height: 240, }) assert.equal(attrs.graphicType, "draw") assert.equal(attrs.drawScene, '{"type":"excalidraw","elements":[]}') assert.ok(attrs.src?.startsWith("data:image/svg+xml")) }) it("computeGraphicLayoutStyle applies absolute placement", () => { const layout = computeGraphicLayoutStyle({ ...DOCS_GRAPHIC_DEFAULTS, graphicType: "shape", placement: "absolute", x: 40, y: 20, }) assert.equal(layout.inner.position, "absolute") assert.equal(layout.inner.left, 40) assert.equal(layout.inner.top, 20) }) it("resizeWithHandle respects minimum size", () => { const next = resizeWithHandle("se", 120, 80, -200, -200) assert.equal(next.width, 24) assert.equal(next.height, 24) }) it("resizeWithHandle respects aspect lock", () => { const next = resizeWithHandle("se", 200, 100, 100, 50, 24, true) assert.equal(next.width, 300) assert.equal(next.height, 150) }) it("normalizeImportedGraphics upgrades standalone image paragraph", () => { const result = normalizeImportedGraphics({ type: "doc", content: [ { type: "paragraph", content: [{ type: "image", attrs: { src: "data:image/png;base64,abc", width: 200 } }], }, ], }) const first = (result.content as Array>)[0] assert.equal(first.type, "docsGraphic") assert.equal((first.attrs as { graphicType?: string }).graphicType, "image") }) it("computeGraphicLayoutStyle applies wrap margin from mm", () => { const layout = computeGraphicLayoutStyle({ ...DOCS_GRAPHIC_DEFAULTS, graphicType: "image", wrap: "square", floatSide: "left", wrapMarginMm: 6, }) assert.ok(typeof layout.wrapper.marginInlineEnd === "string") assert.notEqual(layout.wrapper.marginInlineEnd, "12px") }) it("computeGraphicLayoutStyle centers float side", () => { const layout = computeGraphicLayoutStyle({ ...DOCS_GRAPHIC_DEFAULTS, wrap: "square", floatSide: "center", }) assert.equal(layout.wrapper.float, "none") assert.equal(layout.wrapper.marginInline, "auto") }) it("computeGraphicLayoutStyle places fixed graphics with page coords", () => { const layout = computeGraphicLayoutStyle( { ...DOCS_GRAPHIC_DEFAULTS, positionMode: "fixed-on-page" as const, placement: "absolute" as const, pageIndex: 1, pageX: 30, pageY: 40, }, { pageHeight: 1000 } ) assert.equal(layout.usePageLayer, true) assert.equal(layout.inner.left, 30) assert.ok(typeof layout.inner.top === "number" && layout.inner.top > 1000) }) it("parseGraphicAttrs forces fixed-on-page for absolute placement", () => { const attrs = parseGraphicAttrs({ ...DOCS_GRAPHIC_DEFAULTS, placement: "absolute", positionMode: "move-with-text", }) assert.equal(attrs.positionMode, "fixed-on-page") }) it("parseGraphicAttrs migrates legacy absolute to fixed-on-page", () => { const attrs = parseGraphicAttrs({ ...DOCS_GRAPHIC_DEFAULTS, placement: "absolute", wrap: "behind", x: 20, y: 30, }) assert.equal(attrs.positionMode, "fixed-on-page") assert.equal(attrs.pageX, 20) assert.equal(attrs.pageY, 30) }) it("normalizeImportedGraphics maps docx wrap aliases", () => { const result = normalizeImportedGraphics({ type: "doc", content: [ { type: "docsGraphic", attrs: { graphicType: "image", src: "https://example.com/a.png", wrap: "topAndBottom", placement: "anchored", }, }, ], }) const attrs = (result.content as Array>)[0].attrs as { wrap?: string placement?: string } assert.equal(attrs.wrap, "top-bottom") assert.equal(attrs.placement, "absolute") }) it("computeImageContentRect letterboxes wide images", () => { const rect = computeImageContentRect(200, 100, 400, 200) assert.equal(rect.width, 200) assert.equal(rect.height, 100) assert.equal(rect.top, 0) }) it("computeImageContentRect letterboxes tall images", () => { const rect = computeImageContentRect(200, 100, 200, 400) assert.equal(rect.height, 100) assert.equal(rect.width, 50) assert.equal(rect.left, 75) }) it("computeImageContentRect cover fills the frame", () => { const rect = computeImageContentRect(200, 100, 200, 400, "cover") assert.equal(rect.width, 200) assert.equal(rect.height, 400) assert.equal(rect.top, -150) }) it("computeImageContentRect cover anchor shifts focal point", () => { const centered = computeImageContentRect(200, 100, 400, 200, "cover", 0.5, 0.5) const left = computeImageContentRect(200, 100, 400, 200, "cover", 0, 0.5) assert.ok(left.left > centered.left) }) it("parseGraphicAttrs accepts crop imageFit", () => { const attrs = parseGraphicAttrs({ ...DOCS_GRAPHIC_DEFAULTS, imageFit: "crop" }) assert.equal(attrs.imageFit, "crop") assert.equal(usesCropImageFit(attrs.imageFit), true) assert.equal(layoutFitForImage(attrs.imageFit), "contain") }) it("computeImageFitStyle maps anchors to object-position", () => { const style = computeImageFitStyle({ imageFit: "cover", imageFitAnchorH: 0, imageFitAnchorV: 1, }) assert.equal(style.objectFit, "cover") assert.equal(style.objectPosition, "0% 100%") }) it("resizeCropRegion shrinks from east without moving origin", () => { const next = resizeCropRegion("e", { cropX: 0.1, cropY: 0, cropWidth: 0.8, cropHeight: 1 }, -0.2, 0) assert.equal(next.cropX, 0.1) assert.equal(next.cropWidth, 0.6) }) it("resizeCropRegion shrinks from west and shifts cropX", () => { const next = resizeCropRegion("w", { cropX: 0.1, cropY: 0, cropWidth: 0.8, cropHeight: 1 }, 0.2, 0) assert.equal(next.cropX, 0.3) assert.equal(next.cropWidth, 0.6) }) it("computeCropImageStyle maps source crop 1:1 to frame", () => { const style = computeCropImageStyle( { cropX: 0, cropY: 0, cropWidth: 0.5, cropHeight: 1, cropShape: "rect", }, 100, 100, 400, 200 ) assert.equal(style.img.width, 200) assert.equal(style.img.height, 100) assert.equal(Math.abs(style.img.left as number), 0) assert.equal(style.img.objectFit, undefined) }) it("computeCropImageStyle offsets the image by the crop origin", () => { const style = computeCropImageStyle( { cropX: 0.25, cropY: 0.1, cropWidth: 0.5, cropHeight: 0.5, cropShape: "rect", }, 100, 100, 400, 400 ) assert.equal(style.img.position, "absolute") assert.equal(style.img.left, -50) assert.equal(style.img.top, -20) }) it("computeCropDisplayGeometry re-edit uses full image as crop window", () => { const baseSource = { cropX: 0, cropY: 0, cropWidth: 0.5, cropHeight: 1 } const initial = computeCropReeditInitialRegion(baseSource, 100, 100, 400, 200) const geometry = computeCropDisplayGeometry( { ...DOCS_GRAPHIC_DEFAULTS, ...initial, imageFit: "crop" }, 100, 100, 400, 200, baseSource ) assert.equal(geometry.windowRect.width, 200) assert.equal(geometry.windowRect.height, 100) assert.equal(geometry.cropRect.width, 100) assert.equal(geometry.imageRect.width, 200) assert.equal(initial.cropWidth, 0.5) }) it("computeCropApplyPatch re-edit can expand crop to full source", () => { const baseSource = { cropX: 0, cropY: 0, cropWidth: 0.5, cropHeight: 1 } const patch = computeCropApplyPatch( { ...DOCS_GRAPHIC_DEFAULTS, cropX: 0, cropY: 0, cropWidth: 1, cropHeight: 1, imageFit: "crop", width: 100, height: 100, }, 0, 0, 400, 200, baseSource ) assert.equal(patch.cropWidth, 1) assert.equal(patch.width, 200) assert.equal(patch.height, 100) }) it("computeCropApplyPatch resizes frame to cropped section", () => { const patch = computeCropApplyPatch( { ...DOCS_GRAPHIC_DEFAULTS, cropX: 0, cropY: 0, cropWidth: 0.5, cropHeight: 1, imageFit: "contain", width: 200, height: 100, }, 0, 0, 400, 200 ) assert.equal(patch.width, 100) assert.equal(patch.height, 100) assert.equal(patch.cropWidth, 0.5) assert.equal(patch.imageFit, "crop") }) })