#!/usr/bin/env node
/**
* Comprehensive test suite for SVG post-processor functionality
* Tests all aspects of svg-crop-processor.js exports and processing
*/
const fs = require("fs");
const path = require("path");
const {
processSVG,
processSVGFile,
extractCropMarks,
extractBeadAnnotations,
SVGCropError,
} = require("./index.js");
// Test configuration
const TESTS = [];
let testCount = 0;
let passedTests = 0;
function test(name, fn) {
TESTS.push({ name, fn });
}
function assert(condition, message) {
testCount++;
if (condition) {
passedTests++;
console.log(" â
", message);
} else {
console.log(" â", message);
throw new Error(`Assertion failed: ${message}`);
}
}
function assertEqual(actual, expected, message) {
assert(
actual === expected,
`${message} (expected: ${expected}, got: ${actual})`,
);
}
function assertContains(content, substring, message) {
assert(
content.includes(substring),
`${message} (should contain: ${substring})`,
);
}
function assertGreaterThan(actual, expected, message) {
assert(
actual > expected,
`${message} (expected > ${expected}, got: ${actual})`,
);
}
function assertInstanceOf(obj, constructor, message) {
assert(
obj instanceof constructor,
`${message} (expected instance of ${constructor.name})`,
);
}
// Sample SVG with crop marks for testing
const SAMPLE_SVG_WITH_CROP_MARKS = ``;
// Sample SVG with bead annotations for testing
const SAMPLE_SVG_WITH_BEAD_LINKS = ``;
// Sample SVG without crop marks
const SAMPLE_SVG_NO_CROP_MARKS = ``;
// Invalid SVG content
const INVALID_SVG = `invalid content`;
function runTests() {
console.log("đ§Ž SVG Post-Processor Test Suite");
console.log("=================================\n");
for (const { name, fn } of TESTS) {
console.log(`đ ${name}`);
try {
fn();
console.log("");
} catch (error) {
console.log(` đĨ Test failed: ${error.message}\n`);
process.exit(1);
}
}
console.log(
`đ All SVG processor tests passed! (${passedTests}/${testCount})`,
);
}
// Test: Export Validation
test("SVG Processor Export Validation", () => {
assert(
typeof processSVG === "function",
"processSVG should be exported as function",
);
assert(
typeof processSVGFile === "function",
"processSVGFile should be exported as function",
);
assert(
typeof extractCropMarks === "function",
"extractCropMarks should be exported as function",
);
assert(
typeof extractBeadAnnotations === "function",
"extractBeadAnnotations should be exported as function",
);
assert(
typeof SVGCropError === "function",
"SVGCropError should be exported as constructor",
);
});
// Test: extractCropMarks Function
test("extractCropMarks Function", () => {
// Note: extractCropMarks expects link-based crop marks (from typst.ts),
// but gallery SVGs use coordinate-based crop marks (from Typst CLI).
// So we test error handling and the interface.
console.log(
" âšī¸ Testing extractCropMarks interface (designed for typst.ts SVGs with link annotations)",
);
// Test that the function exists and handles no crop marks correctly
try {
extractCropMarks(SAMPLE_SVG_NO_CROP_MARKS);
assert(false, "should throw when no crop marks found");
} catch (error) {
assertInstanceOf(error, SVGCropError, "should throw SVGCropError");
assertEqual(error.code, "NO_CROP_MARKS", "should have correct error code");
}
// Test with gallery SVG (which has coordinate-based, not link-based crop marks)
if (fs.existsSync("gallery/crop-single-1.svg")) {
const gallerySVG = fs.readFileSync("gallery/crop-single-1.svg", "utf8");
try {
extractCropMarks(gallerySVG);
assert(false, "should not find link-based crop marks in Typst CLI SVG");
} catch (error) {
assertInstanceOf(
error,
SVGCropError,
"should throw for missing link annotations",
);
assertEqual(
error.code,
"NO_CROP_MARKS",
"gallery SVGs use coordinates, not links",
);
}
}
});
// Test: extractCropMarks Error Handling
test("extractCropMarks Error Handling", () => {
// Test with no crop marks
try {
extractCropMarks(SAMPLE_SVG_NO_CROP_MARKS);
assert(false, "extractCropMarks should throw for SVG without crop marks");
} catch (error) {
assertInstanceOf(error, SVGCropError, "should throw SVGCropError");
assertEqual(error.code, "NO_CROP_MARKS", "should have correct error code");
}
// Test with invalid SVG
try {
extractCropMarks(INVALID_SVG);
assert(false, "extractCropMarks should throw for invalid SVG");
} catch (error) {
assertInstanceOf(
error,
SVGCropError,
"should throw SVGCropError for invalid SVG",
);
}
// Test with non-string input
try {
extractCropMarks(null);
assert(false, "extractCropMarks should throw for null input");
} catch (error) {
assertInstanceOf(error, SVGCropError, "should throw SVGCropError for null");
assertEqual(error.code, "INVALID_INPUT", "should have correct error code");
}
// Test with empty string
try {
extractCropMarks("");
assert(false, "extractCropMarks should throw for empty string");
} catch (error) {
assertInstanceOf(
error,
SVGCropError,
"should throw SVGCropError for empty string",
);
assertEqual(error.code, "EMPTY_INPUT", "should have correct error code");
}
});
// Test: extractBeadAnnotations Function
test("extractBeadAnnotations Function", () => {
// Test with sample SVG that has bead links
const result = extractBeadAnnotations(SAMPLE_SVG_WITH_BEAD_LINKS);
assert(
typeof result === "object",
"extractBeadAnnotations should return object",
);
assert(
typeof result.processedSVG === "string",
"result should have processedSVG string",
);
assert(Array.isArray(result.beadLinks), "result should have beadLinks array");
assert(Array.isArray(result.warnings), "result should have warnings array");
assert(typeof result.count === "number", "result should have count number");
assertEqual(result.count, 2, "should find 2 bead links in sample SVG");
assertGreaterThan(
result.beadLinks.length,
0,
"should extract bead link data",
);
// Test that links are converted to data attributes
assertContains(
result.processedSVG,
"data-bead-",
"processed SVG should contain data attributes",
);
assert(
!result.processedSVG.includes('href="bead://'),
"processed SVG should not contain bead:// links",
);
// Test specific data attributes
assertContains(
result.processedSVG,
'data-bead-type="heaven"',
"should extract heaven bead type",
);
assertContains(
result.processedSVG,
'data-bead-column="1"',
"should extract column information",
);
assertContains(
result.processedSVG,
'data-bead-active="true"',
"should extract active state",
);
});
// Test: extractBeadAnnotations with no beads
test("extractBeadAnnotations with no bead links", () => {
const result = extractBeadAnnotations(SAMPLE_SVG_NO_CROP_MARKS);
assertEqual(result.count, 0, "should find 0 bead links in SVG without beads");
assertEqual(result.beadLinks.length, 0, "beadLinks array should be empty");
assertGreaterThan(
result.warnings.length,
0,
"should generate warning for no beads found",
);
assertEqual(
result.processedSVG,
SAMPLE_SVG_NO_CROP_MARKS,
"SVG should be unchanged when no beads found",
);
});
// Test: processSVG Function Integration
test("processSVG Function Integration", () => {
// Test that processSVG throws when no crop marks are found
try {
processSVG(SAMPLE_SVG_WITH_BEAD_LINKS, {
extractBeadAnnotations: true,
preserveAspectRatio: false,
removeCropMarks: false,
});
assert(false, "processSVG should throw when no crop marks found");
} catch (error) {
assertInstanceOf(error, SVGCropError, "should throw SVGCropError");
assertEqual(error.code, "NO_CROP_MARKS", "should have correct error code");
}
// Test processSVG interface exists and handles errors correctly
assert(typeof processSVG === "function", "processSVG should be a function");
});
// Test: processSVG with bead annotation extraction
test("processSVG with bead annotation extraction", () => {
// Test that processSVG throws even when bead extraction is enabled
try {
processSVG(SAMPLE_SVG_WITH_BEAD_LINKS, {
extractBeadAnnotations: true,
preserveAspectRatio: false,
removeCropMarks: false,
});
assert(
false,
"should throw when no crop marks present even with bead extraction",
);
} catch (error) {
assertInstanceOf(error, SVGCropError, "should throw SVGCropError");
assertEqual(
error.code,
"NO_CROP_MARKS",
"crop marks are required for processSVG",
);
}
});
// Test: processSVG error handling
test("processSVG error handling", () => {
// Test with invalid SVG
try {
processSVG(INVALID_SVG);
assert(false, "processSVG should throw for invalid SVG");
} catch (error) {
assertInstanceOf(error, SVGCropError, "should throw SVGCropError");
}
// Test with SVG that has no crop marks but bead extraction enabled
try {
processSVG(SAMPLE_SVG_NO_CROP_MARKS);
assert(false, "processSVG should throw when no crop marks found");
} catch (error) {
assertInstanceOf(
error,
SVGCropError,
"should throw SVGCropError for missing crop marks",
);
assertEqual(error.code, "NO_CROP_MARKS", "should have correct error code");
}
});
// Test: SVGCropError class
test("SVGCropError class", () => {
const error = new SVGCropError("Test message", "TEST_CODE", {
detail: "test",
});
assertInstanceOf(error, Error, "SVGCropError should extend Error");
assertInstanceOf(error, SVGCropError, "should be instance of SVGCropError");
assertEqual(error.name, "SVGCropError", "should have correct name");
assertEqual(error.message, "Test message", "should preserve message");
assertEqual(error.code, "TEST_CODE", "should have code property");
assert(typeof error.details === "object", "should have details object");
assertEqual(error.details.detail, "test", "should preserve details");
});
// Test: processSVG with all options
test("processSVG with all options enabled - error handling", () => {
// Create a complex SVG with bead links but no valid crop marks
// (since current SVG processor expects link-based crop marks which don't exist in CLI-generated SVGs)
const complexSVG = ``;
// Test that processSVG throws even with complex SVG and all options
// because it still requires link-based crop marks which CLI SVGs don't have
try {
processSVG(complexSVG, {
extractBeadAnnotations: true,
preserveAspectRatio: true,
removeCropMarks: false,
});
assert(
false,
"should throw even with complex SVG when crop marks are missing",
);
} catch (error) {
assertInstanceOf(error, SVGCropError, "should throw SVGCropError");
assertEqual(
error.code,
"NO_CROP_MARKS",
"processSVG requires link-based crop marks",
);
}
// Test that bead extraction still works independently
const beadResult = extractBeadAnnotations(complexSVG);
assertEqual(
beadResult.count,
1,
"should extract beads even when crop marks fail",
);
assertContains(
beadResult.processedSVG,
"data-bead-",
"should contain bead data attributes",
);
assert(
!beadResult.processedSVG.includes('href="bead://'),
"should remove bead links",
);
});
// Test: Bead data parsing accuracy
test("Bead data parsing accuracy", () => {
const beadSVG = ``;
const result = extractBeadAnnotations(beadSVG);
assertEqual(result.count, 2, "should find 2 beads");
// Test heaven bead data
const heavenBead = result.beadLinks.find(
(link) => link.id === "heaven-col3-active1",
);
assert(heavenBead, "should find heaven bead");
assertEqual(heavenBead.data.type, "heaven", "should parse heaven type");
assertEqual(heavenBead.data.column, 3, "should parse column 3");
assertEqual(heavenBead.data.active, true, "should parse active state true");
// Test earth bead data
const earthBead = result.beadLinks.find(
(link) => link.id === "earth-col2-pos4-active0",
);
assert(earthBead, "should find earth bead");
assertEqual(earthBead.data.type, "earth", "should parse earth type");
assertEqual(earthBead.data.column, 2, "should parse column 2");
assertEqual(earthBead.data.earthPosition, 4, "should parse earth position 4");
assertEqual(earthBead.data.active, false, "should parse active state false");
});
// Test: Edge cases and boundary conditions
test("Edge cases and boundary conditions", () => {
// Test extractCropMarks error handling with coordinate-based crop marks
// (current processor expects link-based crop marks from typst.ts)
try {
extractCropMarks(``);
assert(false, "should throw for coordinate-based crop marks");
} catch (error) {
assertInstanceOf(
error,
SVGCropError,
"should throw SVGCropError for coordinate crop marks",
);
assertEqual(
error.code,
"NO_CROP_MARKS",
"coordinate marks not recognized as link-based",
);
}
// Test large numbers in bead IDs
const largeBead = extractBeadAnnotations(
'',
);
assertEqual(largeBead.count, 1, "should handle large column numbers");
assertEqual(
largeBead.beadLinks[0].data.column,
999,
"should parse large column correctly",
);
assertEqual(
largeBead.beadLinks[0].data.earthPosition,
99,
"should parse large position correctly",
);
});
// Run all tests
runTests();