sbuffkin_hexmaker/tests/integration/e2e-mapPdf.integration.ts
isaprettycoolguy@protonmail.com 6aa885b5b2 feat: export module — PDF/Markdown for notes, hexes, maps, tables, workflows (1.2.0)
Adds a unified export pipeline under src/export/ with exporters for single notes,
structured hexes, maps (with reference table), random tables, and workflows
(with rolled samples). PDF rendering reuses Obsidian's MarkdownRenderer + theme
CSS; map PNG/PDF goes through a dedicated canvas renderer.

UI surface:
- "Export" tab in MapModal (PNG/PDF with overlay + size options)
- "Export" link in HexEditorModal + HexExportModal
- "Export PDF/Markdown" links in RandomTableView header
- "Export…" button in WorkflowEditorModal + WorkflowExportModal
- Commands: export current note (PDF/MD), current hex, current workflow
- File-menu items on any .md file: Export to PDF / Markdown

Other:
- New `exportFolder` setting (defaults to {worldFolder}/exports)
- hexGeometry: add hexSize / hexCenter / hexPolygonPoints / gridBoundingBox
- HexTableView + HexEditorModal: first-wins on duplicate palette names
  (matches HexMapView's .find() behaviour)
- Tests: unit + integration suites for all exporters; sim-vault fixture

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 20:46:41 -04:00

130 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* End-to-end integration test for the map → PDF builder (1.4).
* Drives the real `buildMapPdfMarkdown` against the simulated vault to
* exercise the section subdivision + per-section hex row collection +
* `computeSubdivisions` / `enumerateSections` together.
*
* The actual PDF + PNG rendering pipeline is tested separately by the
* chromium-driver integration test.
*/
import { describe, it, before, after } from "node:test";
import expect from "expect";
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { MockHexmakerPlugin } from "../helpers/simVault";
import {
buildMapPdfMarkdown,
computeSubdivisions,
enumerateSections,
} from "../../src/export/exporters/mapWithTable";
let tmpDir: string;
let plugin: MockHexmakerPlugin;
before(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "duckmage-e2e-map-"));
plugin = new MockHexmakerPlugin(tmpDir);
});
after(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
describe("e2e: map → PDF builder against sim-vault", () => {
it("produces a 1×1 layout for the small 3×2 sim-vault map", () => {
// 3×2 grid fits well within any sensible page area at minHexPxOnPage = 60.
const layout = computeSubdivisions({
gridCols: 3,
gridRows: 2,
pageWidthPx: 950,
pageHeightPx: 360,
minHexPxOnPage: 60,
});
expect(layout.colSections).toBe(1);
expect(layout.rowSections).toBe(1);
});
it("buildMapPdfMarkdown emits cover + TOC + one section page with hex rows", async () => {
const placeholderUri = "data:image/png;base64,AAA";
const layout = computeSubdivisions({
gridCols: 3,
gridRows: 2,
pageWidthPx: 950,
pageHeightPx: 360,
minHexPxOnPage: 60,
});
const sections = enumerateSections(layout, { cols: 3, rows: 2 });
const md = await buildMapPdfMarkdown(
plugin as never,
"the-coast",
placeholderUri,
sections,
// One placeholder uri per section (1×1 = 1 section)
sections.map(() => placeholderUri),
);
// Cover
expect(md).toMatch(/^# the-coast/m);
expect(md).toMatch(/duckmage-pdf-fullmap/);
// TOC table
expect(md).toMatch(/## Section index/);
expect(md).toMatch(/\| Section \| Hex range/);
expect(md).toMatch(/\*\*A1\*\*/);
// Section page heading + cropped image + reference table header
expect(md).toMatch(/## Section A1/);
expect(md).toMatch(/duckmage-pdf-sectionmap/);
expect(md).toMatch(
/\| Hex \| Terrain \| Towns \| Dungeons \| Features \| Quests \| Factions \| Encounters \| Description \|/,
);
// All six hexes appear — even 2,0 (only terrain: mountain) and 2,1 (only
// terrain: hills) get a row because terrain alone counts as "populated".
expect(md).toMatch(/\| 0,0 \| shallows/);
expect(md).toMatch(/\| 1,0 \| forest/);
expect(md).toMatch(/\| 0,1 \| ocean/);
expect(md).toMatch(/\| 1,1 \| grass/);
expect(md).toMatch(/\| 2,0 \| mountain/);
expect(md).toMatch(/\| 2,1 \| hills/);
// Notable cell content (joined basenames)
expect(md).toMatch(/Saltwatch/);
expect(md).toMatch(/The Old Spire/);
expect(md).toMatch(/Standing Stone/);
expect(md).toMatch(/The Reach/);
expect(md).toMatch(/forest-encounters/);
// Description excerpts (frontmatter not leaked, wikilinks stripped)
expect(md).toMatch(/Calm shallow water/);
expect(md).toMatch(/Dense pine forest/);
});
it("the palette duplicate 'ocean' regression: hex 0,1 (ocean) is still in the table", async () => {
// This row would have been silently mis-rendered as `ocean = grey` in the
// old code path. The MD builder doesn't render colors but it DOES look up
// terrain by name when emitting the row; if the duplicate handling were
// wrong, the row's terrain cell could be missing or empty.
const placeholderUri = "data:image/png;base64,AAA";
const layout = computeSubdivisions({
gridCols: 3,
gridRows: 2,
pageWidthPx: 950,
pageHeightPx: 360,
minHexPxOnPage: 60,
});
const sections = enumerateSections(layout, { cols: 3, rows: 2 });
const md = await buildMapPdfMarkdown(
plugin as never,
"the-coast",
placeholderUri,
sections,
sections.map(() => placeholderUri),
);
expect(md).toMatch(/\| 0,1 \| ocean \|/);
});
});