ultisuite-client/lib/drive/docs-graphic.test.ts
R3D347HR4Y 303b2b1074
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
wow
2026-06-11 01:22:40 +02:00

350 lines
10 KiB
TypeScript

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<Record<string, unknown>>)[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<Record<string, unknown>>)[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<Record<string, unknown>>)[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")
})
})