#!/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();