diff --git a/demo/demo.js b/demo/demo.js new file mode 100644 index 0000000..170cddc --- /dev/null +++ b/demo/demo.js @@ -0,0 +1,277 @@ +/** + * XMLJSONTransformer Browser Demo + */ +import XMLJSONTransformer from '../dist/index.js'; +import samples from './samples.js'; + +document.addEventListener('DOMContentLoaded', () => { + // Predefined transformer functions + const transformers = { + none: null, + uppercase: (val, context) => { + return typeof val === 'string' ? val.toUpperCase() : val; + }, + lowercase: (val, context) => { + return typeof val === 'string' ? val.toLowerCase() : val; + }, + custom: null // Will be set from textarea + }; + + // Initialize the demo + // Set default sample + document.getElementById('xml-input').value = samples.library.xml; + updateCurrentConfig(); + + // Configuration toggle + const configToggleBtn = document.getElementById('toggle-config'); + const configPanel = document.querySelector('.config-panel'); + + configToggleBtn.addEventListener('click', () => { + const isVisible = configPanel.style.display !== 'none'; + configPanel.style.display = isVisible ? 'none' : 'block'; + configToggleBtn.textContent = isVisible ? 'Show Configuration Options' : 'Hide Configuration Options'; + }); + + // Transform function selector + const transformSelector = document.getElementById('transform-selector'); + const customTransformContainer = document.getElementById('custom-transform-container'); + + transformSelector.addEventListener('change', (event) => { + const selectedTransform = event.target.value; + if (selectedTransform === 'custom') { + customTransformContainer.style.display = 'block'; + } else { + customTransformContainer.style.display = 'none'; + } + + // Update configuration + updateCurrentConfig(getConfig()); + }); + + // Sample selector event listener + document.getElementById('sample-selector').addEventListener('change', (event) => { + const selectedSample = event.target.value; + if (selectedSample && samples[selectedSample]) { + document.getElementById('xml-input').value = samples[selectedSample].xml; + document.getElementById('json-output').value = ''; + } + }); + + // XML to JSON conversion + document.getElementById('xml-to-json').addEventListener('click', () => { + try { + const config = getConfig(); + const transformer = new XMLJSONTransformer(config); + const xmlInput = document.getElementById('xml-input').value; + const jsonOutput = transformer.xmlToJSON(xmlInput); + document.getElementById('json-output').value = JSON.stringify(jsonOutput, null, 2); + updateCurrentConfig(config); + } catch (error) { + showError('Error converting XML to JSON: ' + error.message); + } + }); + + // JSON to XML conversion + document.getElementById('json-to-xml').addEventListener('click', () => { + try { + const config = getConfig(); + const transformer = new XMLJSONTransformer(config); + const jsonInput = document.getElementById('json-output').value; + const jsonObj = JSON.parse(jsonInput); + const xmlOutput = transformer.jsonToXML(jsonObj); + document.getElementById('xml-input').value = xmlOutput; + updateCurrentConfig(config); + } catch (error) { + showError('Error converting JSON to XML: ' + error.message); + } + }); + + // Reset + document.getElementById('reset').addEventListener('click', () => { + const selectElement = document.getElementById('sample-selector'); + const selectedSample = selectElement.value; + if (selectedSample && samples[selectedSample]) { + document.getElementById('xml-input').value = samples[selectedSample].xml; + } else { + document.getElementById('xml-input').value = samples.library.xml; + selectElement.value = 'library'; + } + document.getElementById('json-output').value = ''; + }); + + // Configuration checkboxes and inputs - update current config on change + document.querySelectorAll('.config-panel input, .config-panel select, .config-panel textarea').forEach(input => { + input.addEventListener('change', () => { + updateCurrentConfig(getConfig()); + }); + }); + + // Helper function to get configuration from UI + function getConfig() { + const transformType = document.getElementById('transform-selector').value; + let transformFunction = transformers[transformType]; + + // Handle custom transform function + if (transformType === 'custom') { + const customCode = document.getElementById('transform-function').value.trim(); + if (customCode) { + try { + // Use Function constructor to create a function from the string + transformFunction = new Function('value', 'context', 'return ' + customCode); + } catch (error) { + showError('Error in custom transform function: ' + error.message); + transformFunction = null; + } + } + } + + return { + // Features to preserve + preserveNamespaces: document.getElementById('preserve-namespaces').checked, + preserveComments: document.getElementById('preserve-comments').checked, + preserveProcessingInstr: document.getElementById('preserve-pis').checked, + preserveCDATA: document.getElementById('preserve-cdata').checked, + preserveTextNodes: document.getElementById('preserve-text-nodes').checked, + preserveWhitespace: document.getElementById('preserve-whitespace').checked, + + // Type conversion options + grokBoolean: document.getElementById('grok-boolean').checked, + grokNumber: document.getElementById('grok-number').checked, + + // Transform function + transformFunction: transformFunction, + + // Strip prefixes option removed from configuration + + // Output options + outputOptions: { + prettyPrint: document.getElementById('pretty-print').checked, + indent: parseInt(document.getElementById('indent').value, 10), + + // JSON-specific options + json: { + compact: document.getElementById('compact-json').checked, + removeEmptyStrings: document.getElementById('remove-empty-strings').checked + }, + + // XML-specific options + xml: { + declaration: document.getElementById('xml-declaration').checked + } + }, + + // Property names + propNames: { + namespace: document.getElementById('namespace-prop').value, + prefix: document.getElementById('prefix-prop').value, + value: document.getElementById('value-prop').value, + attributes: document.getElementById('attributes-prop').value, + cdata: document.getElementById('cdata-prop').value, + comments: document.getElementById('comments-prop').value, + processing: document.getElementById('processing-prop').value, + children: document.getElementById('children-prop').value + } + }; + } + + // Helper function to update current config display + function updateCurrentConfig(config) { + const configElement = document.getElementById('current-config'); + if (!config) { + config = getConfig(); + } + + // Create a copy to prevent circular references when trying to stringify + const configCopy = JSON.parse(JSON.stringify(config)); + + // Add placeholder for transform function if it exists + if (config.transformFunction) { + const transformType = document.getElementById('transform-selector').value; + if (transformType === 'custom') { + configCopy.transformFunction = document.getElementById('transform-function').value; + } else { + configCopy.transformFunction = `[Function: ${transformType}]`; + } + } + + configElement.textContent = JSON.stringify(configCopy, null, 2); + } + + // Helper function to show errors + function showError(message) { + const errorDiv = document.createElement('div'); + errorDiv.className = 'error'; + errorDiv.textContent = message; + document.querySelector('.container').appendChild(errorDiv); + + // Remove error after 5 seconds + setTimeout(() => { + errorDiv.remove(); + }, 5000); + } + + // Function to load samples from external files + async function loadExternalSamples() { + try { + const response = await fetch('samples/index.json'); + if (!response.ok) { + throw new Error('Failed to load samples index'); + } + + const sampleIndex = await response.json(); + const selector = document.getElementById('sample-selector'); + + // Clear existing options except the default ones + const defaultOptions = Array.from(selector.options).slice(0, 7); // Keep the first 7 options + selector.innerHTML = ''; + defaultOptions.forEach(option => selector.appendChild(option)); + + // Add external samples + for (const sample of sampleIndex.samples) { + const option = document.createElement('option'); + option.value = `external:${sample.id}`; + option.textContent = sample.name; + selector.appendChild(option); + } + + // Update selection handler + selector.removeEventListener('change', selectorChangeHandler); + selector.addEventListener('change', selectorChangeHandler); + } catch (error) { + console.error('Error loading external samples:', error); + // Silently fail - user can still use built-in samples + } + } + + // Sample selector change handler (defined separately for removeEventListener) + async function selectorChangeHandler(event) { + const selectedValue = event.target.value; + + // Handle built-in samples + if (selectedValue && !selectedValue.startsWith('external:') && samples[selectedValue]) { + document.getElementById('xml-input').value = samples[selectedValue].xml; + document.getElementById('json-output').value = ''; + return; + } + + // Handle external samples + if (selectedValue && selectedValue.startsWith('external:')) { + const sampleId = selectedValue.split(':')[1]; + try { + const sampleResponse = await fetch(`samples/${sampleId}.xml`); + if (!sampleResponse.ok) { + throw new Error(`Failed to load sample ${sampleId}`); + } + + const sampleXml = await sampleResponse.text(); + document.getElementById('xml-input').value = sampleXml; + document.getElementById('json-output').value = ''; + } catch (error) { + showError(`Error loading external sample: ${error.message}`); + } + } + } + + // Try to load external samples + loadExternalSamples().catch(error => console.error('Error in loadExternalSamples:', error)); +}); \ No newline at end of file diff --git a/demo/index.html b/demo/index.html index fe0b615..1c0cd30 100644 --- a/demo/index.html +++ b/demo/index.html @@ -108,7 +108,7 @@ margin-bottom: 5px; font-size: 14px; } - .input-item input { + .input-item input, .input-item select { padding: 5px; border: 1px solid #ddd; border-radius: 4px; @@ -162,6 +162,11 @@ margin: 15px 0; border-radius: 0 4px 4px 0; } + textarea.transform-function { + height: 100px; + font-family: monospace; + font-size: 14px; + } @@ -230,13 +235,40 @@

Features to Preserve

+ +
-

Element Name Handling

+

Type Conversion

- - + +
+
+ + +
+
+
+ +
+

Transform Function

+
+
+ + +
+
+
@@ -252,7 +284,7 @@

Output Options

- +
@@ -261,16 +293,26 @@

Output Options

JSON-specific Options

- +
- +
+
+

XML-specific Options

+
+
+ + +
+
+
+

Property Names

@@ -278,6 +320,10 @@

Property Names

+
+ + +
@@ -322,346 +368,11 @@

JSON

- - - + + + \ No newline at end of file diff --git a/demo/samples.js b/demo/samples.js new file mode 100644 index 0000000..42a7254 --- /dev/null +++ b/demo/samples.js @@ -0,0 +1,134 @@ +/** + * XML samples for XMLJSONTransformer demo + */ +export const samples = { + library: { + name: "Library Catalog", + description: "XML example with namespaces, comments, CDATA and processing instructions", + xml: ` + + + + JavaScript: The Good Parts + Douglas Crockford + 2008 + Programming + + + + Clean Code + Robert C. Martin + 2008 + Programming + + + + ` + }, + soap: { + name: "SOAP Message", + description: "XML example with multiple namespaces and nested elements", + xml: ` + + + + 1234 + + + + + IBM + + + ` + }, + mixed: { + name: "Mixed Content", + description: "XML with mixed content (text and elements mixed)", + xml: `
+

This paragraph has emphasized text and strong text mixed with regular text.

+

Another paragraph with a link in the middle.

+ +
` + }, + rss: { + name: "RSS Feed", + description: "Example RSS 2.0 feed", + xml: ` + + + Example RSS Feed + https://example.com + This is an example RSS feed + en-us + Mon, 01 Jul 2023 12:00:00 GMT + + First Article + https://example.com/first-article + bold text]]> + Mon, 01 Jul 2023 10:00:00 GMT + https://example.com/first-article + + + Second Article + https://example.com/second-article + a link]]> + Mon, 01 Jul 2023 11:00:00 GMT + https://example.com/second-article + + + ` + }, + svg: { + name: "SVG Graphic", + description: "Scalable Vector Graphics example", + xml: ` + + + + + SVG Text + ` + }, + atom: { + name: "Atom Feed", + description: "Example Atom 1.0 feed", + xml: ` + + Example Atom Feed + + 2023-07-01T12:00:00Z + + John Doe + john@example.com + + urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6 + + + First Entry + + urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a + 2023-07-01T10:00:00Z + Summary of the first entry + This is the content of the first entry.

]]>
+
+ + + Second Entry + + urn:uuid:1225c695-cfb8-4ebb-bbbb-80da344efa6a + 2023-07-01T11:00:00Z + Summary of the second entry + This is the content of the second entry.

]]>
+
+
` + } + }; + + export default samples; \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 1657ac9..bde1774 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,7 @@ export default { testEnvironment: 'jsdom', verbose: true, - setupFilesAfterEnv: ['./test/customMatchers.js'], + setupFilesAfterEnv: ['./test/helpers/customMatchers.js'], reporters: [ 'default', ['jest-html-reporters', { diff --git a/package.json b/package.json index 6ac70ec..3012d75 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "xmltojson", "version": "2.0.0", "type": "module", - "main": "./dist/xml-json-transformer.js", + "main": "./dist/index.js", "exports": { - ".": "./dist/xml-json-transformer.js" + ".": "./dist/index.js" }, "files": ["dist"], "scripts": { diff --git a/rollup.config.js b/rollup.config.js index e2fb3f6..913fed0 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -21,11 +21,11 @@ const banner = `/*! */`; export default [ - // Main ESM build (for both browser and Node.js) + // Main ESM build { - input: "src/xml-json-transformer.js", + input: "src/index.js", output: { - file: "./dist/xml-json-transformer.js", + file: "./dist/index.js", format: "es", banner, sourcemap: true, @@ -39,14 +39,14 @@ export default [ filesize(), ], }, - // Minified ESM build (for production use) + // Minified ESM version { - input: "src/xml-json-transformer.js", + input: "src/index.js", output: { - file: "./dist/xml-json-transformer.min.js", + file: "./dist/index.min.js", format: "es", banner, - sourcemap: false, // No source map for production + sourcemap: false, }, plugins: [ resolve(), @@ -54,21 +54,20 @@ export default [ babelHelpers: "bundled", exclude: "node_modules/**", }), - terser(), // Minification + terser(), filesize(), ], }, - // Browser-specific build (UMD for direct ]]> + + + + More CDATA]]> + + + \ No newline at end of file diff --git a/test/customMatchers.js b/test/helpers/customMatchers.js similarity index 82% rename from test/customMatchers.js rename to test/helpers/customMatchers.js index 0ae15cc..ae34f55 100644 --- a/test/customMatchers.js +++ b/test/helpers/customMatchers.js @@ -1,8 +1,13 @@ +/** + * Custom Jest matchers for XMLJSONTransformer tests + */ + +// Helper function to normalize XML strings for comparison function normalizeSpace(xml) { - return String(xml) - .replace(/\s+/g, '') // collapse all types of whitespace (spaces, tabs, newlines) into a single space - .trim(); // trim leading and trailing spaces - } + return String(xml) + .replace(/\s+/g, "") // collapse all types of whitespace (spaces, tabs, newlines) into a single space + .trim(); // trim leading and trailing spaces +} expect.extend({ // Custom matcher to compare normalized XML equality @@ -52,4 +57,4 @@ expect.extend({ }; } }, -}); \ No newline at end of file +}); diff --git a/test/helpers/test-utils.js b/test/helpers/test-utils.js new file mode 100644 index 0000000..42fd0eb --- /dev/null +++ b/test/helpers/test-utils.js @@ -0,0 +1,142 @@ +/** + * Test utilities for XMLJSONTransformer tests + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// Get directory name in ESM +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +/** + * Read a fixture file from the fixtures directory + * @param {string} filename - Name of the fixture file + * @param {string} type - Type of fixture ('xml-samples' or 'json-samples') + * @returns {string} - Content of the fixture file + */ +export const readFixture = (filename, type = 'xml-samples') => { + const fixturePath = path.join(__dirname, '../fixtures', type, filename); + return fs.readFileSync(fixturePath, 'utf8'); +}; + +/** + * Normalize XML string by removing whitespace and comments + * @param {string} xml - XML string to normalize + * @returns {string} - Normalized XML string + */ +export const normalizeXML = (xml) => { + return xml + .replace(//g, '') // Remove comments + .replace(/>\s+<') // Remove whitespace between tags + .replace(/\s+/g, ' ') // Collapse whitespace + .trim(); // Trim leading/trailing whitespace +}; + +/** + * Create a JSON object with the most common properties pre-filled + * @param {string} elementName - Name of the root element + * @param {string} value - Text content of the element + * @param {Object} options - Additional options + * @returns {Object} - JSON object with common properties + */ +export const createBaseJSON = (elementName, value = '', options = {}) => { + const { + attributes = {}, + children = [], + namespace = '', + comments = [], + cdata = [], + processing = [] + } = options; + + // Convert attributes from simple object to XMLJSONTransformer format + const formattedAttrs = {}; + for (const [key, val] of Object.entries(attributes)) { + formattedAttrs[key] = { + '@val': val, + '@ns': '' + }; + } + + // Create base object + const result = {}; + result[elementName] = { + '@ns': namespace, + '@val': value, + '@attrs': formattedAttrs, + '@children': children, + '@comments': comments, + '@cdata': cdata, + '@processing': processing + }; + + return result; +}; + +/** + * Create a simple XML string for testing + * @param {string} elementName - Name of the root element + * @param {string} value - Text content of the element + * @param {Object} options - Additional options + * @returns {string} - XML string + */ +export const createSimpleXML = (elementName, value = '', options = {}) => { + const { + attributes = {}, + children = [], + declaration = true + } = options; + + // Create attributes string + const attrs = Object.entries(attributes) + .map(([key, val]) => `${key}="${val}"`) + .join(' '); + + // Create children string + const childrenStr = children + .map(([name, val]) => `<${name}>${val}`) + .join(''); + + // Create XML + const xml = `<${elementName}${attrs ? ' ' + attrs : ''}>${value}${childrenStr}`; + + // Add declaration if requested + return declaration ? `\n${xml}` : xml; +}; + +/** + * Parse XML string into DOM node for testing + * @param {string} xmlString - XML string to parse + * @returns {Document} - DOM document + */ +export const parseXML = (xmlString) => { + const parser = new DOMParser(); + return parser.parseFromString(xmlString, 'text/xml'); +}; + +/** + * Generate a random attribute map for testing + * @param {number} count - Number of attributes to generate + * @returns {Object} - Map of attribute names to values + */ +export const generateRandomAttributes = (count = 3) => { + const attrs = {}; + for (let i = 0; i < count; i++) { + attrs[`attr${i}`] = `value${i}`; + } + return attrs; +}; + +/** + * Generate a random array of child elements for testing + * @param {number} count - Number of child elements to generate + * @returns {Array} - Array of child name/value pairs + */ +export const generateRandomChildren = (count = 3) => { + const children = []; + for (let i = 0; i < count; i++) { + children.push([`child${i}`, `value${i}`]); + } + return children; +}; \ No newline at end of file diff --git a/test/unit/ConfigurationManager.test.js b/test/unit/ConfigurationManager.test.js new file mode 100644 index 0000000..e8484c5 --- /dev/null +++ b/test/unit/ConfigurationManager.test.js @@ -0,0 +1,143 @@ +/** + * Unit tests for the ConfigurationManager class + */ + +import ConfigurationManager from '../../src/components/ConfigurationManager.js'; + +describe('ConfigurationManager', () => { + let defaultConfig; + + beforeEach(() => { + // Create a manager with default config to test against + const defaultManager = new ConfigurationManager(); + defaultConfig = defaultManager.config; + }); + + test('should use default configuration when no config is provided', () => { + const manager = new ConfigurationManager(); + + // Check some key default properties + expect(manager.config.preserveNamespaces).toBe(true); + expect(manager.config.stripPrefixes).toBe(true); + expect(manager.config.outputOptions.prettyPrint).toBe(true); + + // Check default property names + expect(manager.config.propNames.namespace).toBe('@ns'); + expect(manager.config.propNames.value).toBe('@val'); + expect(manager.config.propNames.children).toBe('@children'); + }); + + test('should merge user config with defaults', () => { + const userConfig = { + preserveNamespaces: false, + outputOptions: { + indent: 4 + } + }; + + const manager = new ConfigurationManager(userConfig); + + // Changed properties should reflect user values + expect(manager.config.preserveNamespaces).toBe(false); + expect(manager.config.outputOptions.indent).toBe(4); + + // Unchanged properties should keep defaults + expect(manager.config.stripPrefixes).toBe(true); + expect(manager.config.propNames.namespace).toBe('@ns'); + }); + + test('should create reverse mapping for property names', () => { + const manager = new ConfigurationManager(); + + // Check reverse mappings + expect(manager.propNamesReverse['@ns']).toBe('namespace'); + expect(manager.propNamesReverse['@val']).toBe('value'); + expect(manager.propNamesReverse['@attrs']).toBe('attributes'); + }); + + test('should use custom property names when provided', () => { + const userConfig = { + propNames: { + namespace: '_ns', + value: '_val', + attributes: '_attrs' + } + }; + + const manager = new ConfigurationManager(userConfig); + + // Check custom property names + expect(manager.config.propNames.namespace).toBe('_ns'); + expect(manager.config.propNames.value).toBe('_val'); + expect(manager.config.propNames.attributes).toBe('_attrs'); + + // Check reverse mappings + expect(manager.propNamesReverse['_ns']).toBe('namespace'); + expect(manager.propNamesReverse['_val']).toBe('value'); + expect(manager.propNamesReverse['_attrs']).toBe('attributes'); + }); + + test('should calculate XML indent string from config', () => { + // Test with numeric indent + const manager1 = new ConfigurationManager({ outputOptions: { indent: 3 } }); + expect(manager1.xmlIndent).toBe(' '); // 3 spaces + + // Test with 0 indent + const manager2 = new ConfigurationManager({ outputOptions: { indent: 0 } }); + expect(manager2.xmlIndent).toBe(''); // Empty string + + // Test with non-numeric indent (should use default of 2 spaces) + const manager3 = new ConfigurationManager({ outputOptions: { indent: '----' } }); + expect(manager3.xmlIndent).toBe(' '); // Default 2 spaces + }); + + test('should handle deep merge of nested properties', () => { + const userConfig = { + outputOptions: { + prettyPrint: false, + json: { + compact: false + } + } + }; + + const manager = new ConfigurationManager(userConfig); + + // Check that specified properties were changed + expect(manager.config.outputOptions.prettyPrint).toBe(false); + expect(manager.config.outputOptions.json.compact).toBe(false); + + // Check that unspecified properties were preserved + expect(manager.config.outputOptions.json.removeEmptyStrings).toBe(true); + expect(manager.config.outputOptions.indent).toBe(3); + }); + + test('shouldPreserve method should return correct values', () => { + // Default manager preserves most features + const defaultManager = new ConfigurationManager(); + expect(defaultManager.shouldPreserve('namespace')).toBe(true); + expect(defaultManager.shouldPreserve('comments')).toBe(true); + expect(defaultManager.shouldPreserve('whitespace')).toBe(false); + + // Custom manager with specific features disabled + const customManager = new ConfigurationManager({ + preserveNamespaces: false, + preserveComments: false + }); + expect(customManager.shouldPreserve('namespace')).toBe(false); + expect(customManager.shouldPreserve('comments')).toBe(false); + expect(customManager.shouldPreserve('cdata')).toBe(true); + }); + + test('getPropName and getFeatureFromProp should return correct values', () => { + const manager = new ConfigurationManager(); + + // Get property name from feature + expect(manager.getPropName('namespace')).toBe('@ns'); + expect(manager.getPropName('children')).toBe('@children'); + + // Get feature from property name + expect(manager.getFeatureFromProp('@ns')).toBe('namespace'); + expect(manager.getFeatureFromProp('@children')).toBe('children'); + }); +}); \ No newline at end of file diff --git a/test/unit/NodeProcessor.test.js b/test/unit/NodeProcessor.test.js new file mode 100644 index 0000000..765eb45 --- /dev/null +++ b/test/unit/NodeProcessor.test.js @@ -0,0 +1,118 @@ +/** + * Unit tests for the NodeProcessor class + */ + +import ConfigurationManager from '../../src/components/ConfigurationManager.js'; +import NodeProcessor from '../../src/components/NodeProcessor.js'; +import DOMEnvironment from '../../src/components/DOMEnvironment.js'; + +describe('NodeProcessor', () => { + let configManager; + let nodeProcessor; + let parser; + + beforeEach(() => { + configManager = new ConfigurationManager(); + nodeProcessor = new NodeProcessor(configManager, DOMEnvironment); + parser = DOMEnvironment.createParser(); + }); + + // Helper to create a DOM node for testing + const createNode = (xmlString) => { + const doc = parser.parseFromString(xmlString, 'text/xml'); + return doc.documentElement; + }; + + test('should detect mixed content correctly', () => { + // Regular element with text + const simpleNode = createNode('Simple text'); + expect(nodeProcessor.hasMixedContent(simpleNode)).toBe(false); + + // Element with only child elements + const childElementNode = createNode('TextMore'); + expect(nodeProcessor.hasMixedContent(childElementNode)).toBe(false); + + // Element with mixed content + const mixedNode = createNode('Text emphasized and normal'); + expect(nodeProcessor.hasMixedContent(mixedNode)).toBe(true); + + // Element with whitespace but no actual mixed content + const whitespaceNode = createNode('\n Text\n More\n'); + expect(nodeProcessor.hasMixedContent(whitespaceNode)).toBe(false); + }); + + test('should handle HTML markup detection correctly', () => { + // Plain text + expect(nodeProcessor.containsHtmlMarkup('Just plain text')).toBe(false); + + // HTML markup + expect(nodeProcessor.containsHtmlMarkup('Emphasized text')).toBe(true); + expect(nodeProcessor.containsHtmlMarkup('
')).toBe(true); + + // XML-like but not HTML + expect(nodeProcessor.containsHtmlMarkup('2 < 5 and 10 > 3')).toBe(false); + + // Non-string input + expect(nodeProcessor.containsHtmlMarkup(123)).toBe(false); + expect(nodeProcessor.containsHtmlMarkup(null)).toBe(false); + }); + + test('should create transform context correctly', () => { + const node = createNode('Test'); + + // Default no transform case + expect(nodeProcessor.createTransformContext(node, 'root', 'xml-to-json')).toBeNull(); + + // With transform function + const configWithTransform = new ConfigurationManager({ + transformFunction: (val) => val.toUpperCase() + }); + const processorWithTransform = new NodeProcessor(configWithTransform, DOMEnvironment); + + const context = processorWithTransform.createTransformContext(node, 'root', 'xml-to-json'); + expect(context).toEqual({ + nodeName: 'root', + nodeType: DOMEnvironment.nodeTypes.ELEMENT_NODE, + namespaceURI: 'http://example.org', + attributes: node.attributes, + direction: 'xml-to-json' + }); + }); + + test('should apply transform function when configured', () => { + // Simple uppercase transform + const uppercaseConfig = new ConfigurationManager({ + transformFunction: (val) => typeof val === 'string' ? val.toUpperCase() : val + }); + const uppercaseProcessor = new NodeProcessor(uppercaseConfig, DOMEnvironment); + + // Test transform + const context = { direction: 'xml-to-json' }; + expect(uppercaseProcessor.applyTransform('test', context)).toBe('TEST'); + expect(uppercaseProcessor.applyTransform(null, context)).toBeNull(); + expect(uppercaseProcessor.applyTransform(123, context)).toBe(123); + + // Test transform that returns undefined + const undefinedConfig = new ConfigurationManager({ + transformFunction: () => undefined + }); + const undefinedProcessor = new NodeProcessor(undefinedConfig, DOMEnvironment); + + // Should return original value if transform returns undefined + expect(undefinedProcessor.applyTransform('test', context)).toBe('test'); + }); + + test('should get inner HTML correctly', () => { + // Simple node + const simpleNode = createNode('Text'); + expect(nodeProcessor.getInnerHTML(simpleNode)).toNormalizeEqual('Text'); + + // Mixed content node + const mixedNode = createNode('Text emphasized'); + expect(nodeProcessor.getInnerHTML(mixedNode)).toNormalizeEqual('Text emphasized'); + + // Node with CDATA + const cdataNode = createNode('Raw]]>'); + expect(nodeProcessor.getInnerHTML(cdataNode)).toNormalizeContain('Raw'); + }); +}); \ No newline at end of file diff --git a/test/unit/PathNavigator.test.js b/test/unit/PathNavigator.test.js new file mode 100644 index 0000000..612b547 --- /dev/null +++ b/test/unit/PathNavigator.test.js @@ -0,0 +1,174 @@ +/** + * Unit tests for the PathNavigator class + */ + +import ConfigurationManager from '../../src/components/ConfigurationManager.js'; +import PathNavigator from '../../src/components/PathNavigator.js'; + +describe('PathNavigator', () => { + let configManager; + let navigator; + let sampleJSON; + + beforeEach(() => { + // Create a default configuration + configManager = new ConfigurationManager(); + navigator = new PathNavigator(configManager); + + // Create a sample JSON structure similar to what would be produced by xmlToJSON + sampleJSON = { + "catalog": { + "@ns": "http://example.org/catalog", + "@val": "", + "@attrs": {}, + "@comments": ["This is a catalog of books"], + "@processing": [], + "@children": [ + { + "book": { + "@ns": "http://example.org/catalog", + "@val": "", + "@attrs": { + "id": { + "@val": "bk101", + "@ns": "" + }, + "category": { + "@val": "fiction", + "@ns": "" + } + }, + "@children": [ + { + "title": { + "@ns": "http://example.org/catalog", + "@val": "The Catcher in the Rye", + "@attrs": {}, + "@children": [] + } + }, + { + "author": { + "@ns": "http://example.org/catalog", + "@val": "J.D. Salinger", + "@attrs": {}, + "@children": [] + } + } + ] + } + }, + { + "book": { + "@ns": "http://example.org/catalog", + "@val": "", + "@attrs": { + "id": { + "@val": "bk102", + "@ns": "" + }, + "category": { + "@val": "fiction", + "@ns": "" + } + }, + "@children": [ + { + "title": { + "@ns": "http://example.org/catalog", + "@val": "To Kill a Mockingbird", + "@attrs": {}, + "@children": [] + } + }, + { + "author": { + "@ns": "http://example.org/catalog", + "@val": "Harper Lee", + "@attrs": {}, + "@children": [] + } + } + ] + } + } + ] + } + }; + }); + + describe('getPath method', () => { + test('should access root element properties directly', () => { + const namespace = navigator.getPath(sampleJSON, "catalog.@ns"); + expect(namespace).toBe("http://example.org/catalog"); + + const comments = navigator.getPath(sampleJSON, "catalog.@comments"); + expect(comments).toEqual(["This is a catalog of books"]); + }); + + test('should access child elements by path', () => { + // Access the first book's id attribute + const bookId = navigator.getPath(sampleJSON, "catalog.@children[0].book.@attrs.id.@val"); + expect(bookId).toBe("bk101"); + + // Access the second book's category attribute + const bookCategory = navigator.getPath(sampleJSON, "catalog.@children[1].book.@attrs.category.@val"); + expect(bookCategory).toBe("fiction"); + }); + + test('should access nested child elements', () => { + // Access the first book's title + const title = navigator.getPath(sampleJSON, "catalog.@children[0].book.@children[0].title.@val"); + expect(title).toBe("The Catcher in the Rye"); + + // Access the second book's author + const author = navigator.getPath(sampleJSON, "catalog.@children[1].book.@children[1].author.@val"); + expect(author).toBe("Harper Lee"); + }); + + test('should flatten results when accessing multiple elements', () => { + // Access all book titles + const titles = navigator.getPath(sampleJSON, "catalog.@children.book.@children.title.@val"); + expect(titles).toEqual(["The Catcher in the Rye", "To Kill a Mockingbird"]); + + // Access all book authors + const authors = navigator.getPath(sampleJSON, "catalog.@children.book.@children.author.@val"); + expect(authors).toEqual(["J.D. Salinger", "Harper Lee"]); + }); + + test('should return undefined for non-existent paths', () => { + const nonExistent = navigator.getPath(sampleJSON, "catalog.@children[0].book.@children[5].price.@val"); + expect(nonExistent).toBeUndefined(); + }); + + test('should use fallback value for non-existent paths when provided', () => { + const fallback = "Not Found"; + const nonExistent = navigator.getPath(sampleJSON, "catalog.@children[0].book.@children[5].price.@val", fallback); + expect(nonExistent).toBe(fallback); + }); + + test('should handle array indexing correctly', () => { + // Access specific array elements + const secondBook = navigator.getPath(sampleJSON, "catalog.@children[1].book"); + expect(secondBook["@attrs"].id["@val"]).toBe("bk102"); + + // Out of bounds index should return undefined + const outOfBounds = navigator.getPath(sampleJSON, "catalog.@children[5]"); + expect(outOfBounds).toBeUndefined(); + }); + + test('should handle null or undefined input gracefully', () => { + // Test with null object + const nullResult = navigator.getPath(null, "some.path"); + expect(nullResult).toBeUndefined(); + + // Test with undefined object + const undefinedResult = navigator.getPath(undefined, "some.path"); + expect(undefinedResult).toBeUndefined(); + + // Test with fallback values + const nullWithFallback = navigator.getPath(null, "some.path", "fallback"); + expect(nullWithFallback).toBe("fallback"); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/xml-json-getpath.test.js b/test/unit/xml-json-getpath.test.js deleted file mode 100644 index 3c8985f..0000000 --- a/test/unit/xml-json-getpath.test.js +++ /dev/null @@ -1,409 +0,0 @@ -/** - * Unit tests for the getPath method of XMLJSONTransformer - */ - -import { XMLJSONTransformer } from '../../src/xml-json-transformer.js'; - -describe("XMLJSONTransformer.getPath", () => { - let transformer; - let sampleJSON; - - beforeEach(() => { - // Create a fresh transformer instance before each test - transformer = new XMLJSONTransformer(); - - // Create a sample JSON structure similar to what would be produced by xmlToJSON - sampleJSON = { - "catalog": { - "@ns": "http://example.org/catalog", - "@val": "", - "@attrs": {}, - "@cdata": [], - "@comments": ["This is a catalog of books"], - "@processing": [], - "@children": [ - { - "book": { - "@ns": "http://example.org/catalog", - "@val": "", - "@attrs": { - "id": { - "@val": "bk101", - "@ns": "" - }, - "category": { - "@val": "fiction", - "@ns": "" - } - }, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [ - { - "title": { - "@ns": "http://example.org/catalog", - "@val": "The Catcher in the Rye", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - }, - { - "author": { - "@ns": "http://example.org/catalog", - "@val": "J.D. Salinger", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - }, - { - "year": { - "@ns": "http://example.org/catalog", - "@val": "1951", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - } - ] - } - }, - { - "book": { - "@ns": "http://example.org/catalog", - "@val": "", - "@attrs": { - "id": { - "@val": "bk102", - "@ns": "" - }, - "category": { - "@val": "fiction", - "@ns": "" - } - }, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [ - { - "title": { - "@ns": "http://example.org/catalog", - "@val": "To Kill a Mockingbird", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - }, - { - "author": { - "@ns": "http://example.org/catalog", - "@val": "Harper Lee", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - }, - { - "year": { - "@ns": "http://example.org/catalog", - "@val": "1960", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - } - ] - } - } - ] - } - }; - }); - - test("should access root element properties directly", () => { - const namespace = transformer.getPath(sampleJSON, "catalog.@ns"); - expect(namespace).toBe("http://example.org/catalog"); - - const comments = transformer.getPath(sampleJSON, "catalog.@comments"); - expect(comments).toEqual(["This is a catalog of books"]); - }); - - test("should access child elements by path", () => { - // Access the first book's id attribute - const bookId = transformer.getPath(sampleJSON, "catalog.@children[0].book.@attrs.id.@val"); - expect(bookId).toBe("bk101"); - - // Access the second book's category attribute - const bookCategory = transformer.getPath(sampleJSON, "catalog.@children[1].book.@attrs.category.@val"); - expect(bookCategory).toBe("fiction"); - }); - - test("should access nested child elements", () => { - // Access the first book's title - const title = transformer.getPath(sampleJSON, "catalog.@children[0].book.@children[0].title.@val"); - expect(title).toBe("The Catcher in the Rye"); - - // Access the second book's author - const author = transformer.getPath(sampleJSON, "catalog.@children[1].book.@children[1].author.@val"); - expect(author).toBe("Harper Lee"); - }); - - test("should flatten results when accessing multiple elements", () => { - // Access all book titles (should be flattened automatically) - const titles = transformer.getPath(sampleJSON, "catalog.@children.book.@children.title.@val"); - expect(titles).toEqual(["The Catcher in the Rye", "To Kill a Mockingbird"]); - - // Access all book years - const years = transformer.getPath(sampleJSON, "catalog.@children.book.@children.year.@val"); - expect(years).toEqual(["1951", "1960"]); - }); - - test("should return undefined for non-existent paths", () => { - const nonExistent = transformer.getPath(sampleJSON, "catalog.@children[0].book.@children[5].price.@val"); - expect(nonExistent).toBeUndefined(); - }); - - test("should use fallback value for non-existent paths when provided", () => { - const fallback = "Not Found"; - const nonExistent = transformer.getPath(sampleJSON, "catalog.@children[0].book.@children[5].price.@val", fallback); - expect(nonExistent).toBe(fallback); - }); - - test("should handle array indexing correctly", () => { - // Access specific array elements - const secondBook = transformer.getPath(sampleJSON, "catalog.@children[1].book"); - expect(secondBook["@attrs"].id["@val"]).toBe("bk102"); - - // Out of bounds index should return undefined - const outOfBounds = transformer.getPath(sampleJSON, "catalog.@children[5]"); - expect(outOfBounds).toBeUndefined(); - }); - - test("should handle accessing specific elements by direct reference", () => { - // Direct reference to all books - const books = transformer.getPath(sampleJSON, "catalog.@children.book"); - expect(books.length).toBe(2); - expect(books[0]["@attrs"].id["@val"]).toBe("bk101"); - expect(books[1]["@attrs"].id["@val"]).toBe("bk102"); - }); - - test("should handle shortcut paths for common access patterns", () => { - // Testing a shortcut path to get all book titles directly - const bookTitles = transformer.getPath(sampleJSON, "catalog.book.title.@val"); - expect(bookTitles).toEqual(["The Catcher in the Rye", "To Kill a Mockingbird"]); - - // Testing a shortcut path to get all author names directly - const authors = transformer.getPath(sampleJSON, "catalog.book.author.@val"); - expect(authors).toEqual(["J.D. Salinger", "Harper Lee"]); - }); - - test("should handle custom configuration of property names", () => { - // Create a transformer with custom property names - const customTransformer = new XMLJSONTransformer({ - propNames: { - namespace: "_ns", - value: "_val", - attributes: "_attrs", - children: "_children" - } - }); - - // Create a sample with custom property names - const customJSON = { - "catalog": { - "_ns": "http://example.org/catalog", - "_val": "", - "_attrs": {}, - "_children": [ - { - "book": { - "_ns": "http://example.org/catalog", - "_val": "", - "_attrs": { - "id": { - "_val": "bk101", - "_ns": "" - } - }, - "_children": [ - { - "title": { - "_ns": "http://example.org/catalog", - "_val": "The Catcher in the Rye", - "_attrs": {}, - "_children": [] - } - } - ] - } - } - ] - } - }; - - // Access properties using custom property names - const bookTitle = customTransformer.getPath(customJSON, "catalog._children[0].book._children[0].title._val"); - expect(bookTitle).toBe("The Catcher in the Rye"); - }); - - test("should handle complex nested structures", () => { - // Create a more complex nested structure - const complexJSON = { - "library": { - "@ns": "", - "@val": "", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [ - { - "section": { - "@ns": "", - "@val": "", - "@attrs": { - "name": { - "@val": "fiction", - "@ns": "" - } - }, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [ - { - "shelf": { - "@ns": "", - "@val": "", - "@attrs": { - "id": { - "@val": "A1", - "@ns": "" - } - }, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [ - { - "book": { - "@ns": "", - "@val": "", - "@attrs": { - "id": { - "@val": "book1", - "@ns": "" - } - }, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [ - { - "title": { - "@ns": "", - "@val": "Nested Book Title", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - } - ] - } - } - ] - } - } - ] - } - } - ] - } - }; - - // Access deeply nested elements - const shelfId = transformer.getPath(complexJSON, "library.@children[0].section.@children[0].shelf.@attrs.id.@val"); - expect(shelfId).toBe("A1"); - - // Access book title using shortcut path - const bookTitle = transformer.getPath(complexJSON, "library.section.shelf.book.title.@val"); - expect(bookTitle).toEqual(["Nested Book Title"]); - }); - - test("should handle empty arrays and objects gracefully", () => { - const emptyJSON = { - "root": { - "@ns": "", - "@val": "", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - }; - - // Access properties of an empty JSON structure - // Direct access to an empty array property should return that empty array - const children = transformer.getPath(emptyJSON, "root.@children"); - expect(children).toEqual([]); - - // Access non-existent child elements - // With the simplified implementation, this should return an empty array - const nonExistent = transformer.getPath(emptyJSON, "root.@children.element"); - expect(nonExistent).toEqual([]); - }); - - // Skip this test if the implementation has a 'this' binding issue - test.skip("should handle invalid paths gracefully - needs implementation fix", () => { - // Test with invalid path syntax - const invalidPath = transformer.getPath(sampleJSON, "catalog.$invalid..path"); - expect(invalidPath).toBeUndefined(); - - // Test with fallback value - const withFallback = transformer.getPath(sampleJSON, "catalog.$invalid..path", "fallback"); - expect(withFallback).toBe("fallback"); - }); - - // Alternative version with a valid but non-existent path - test("should handle non-existent paths gracefully", () => { - // Test with a valid syntax but non-existent path - const nonExistentPath = transformer.getPath(sampleJSON, "catalog.nonExistent.element"); - expect(nonExistentPath).toBeUndefined(); - - // Test with fallback value - const withFallback = transformer.getPath(sampleJSON, "catalog.nonExistent.element", "fallback"); - expect(withFallback).toBe("fallback"); - }); - - test("should handle null or undefined input gracefully", () => { - // Test with null object - const nullResult = transformer.getPath(null, "some.path"); - expect(nullResult).toBeUndefined(); - - // Test with undefined object - const undefinedResult = transformer.getPath(undefined, "some.path"); - expect(undefinedResult).toBeUndefined(); - - // Test with fallback values - const nullWithFallback = transformer.getPath(null, "some.path", "fallback"); - expect(nullWithFallback).toBe("fallback"); - }); -}); \ No newline at end of file diff --git a/test/unit/xml-json-transformFunction.test.js b/test/unit/xml-json-transformFunction.test.js deleted file mode 100644 index b1be909..0000000 --- a/test/unit/xml-json-transformFunction.test.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Simplified unit tests for the transform function feature - */ - -import { XMLJSONTransformer } from '../../src/xml-json-transformer.js'; - -describe("XMLJSONTransformer.transformFunction (Simple Case)", () => { - let transformer; - - // Simple transform function: - // - For XML to JSON: element values to uppercase, attribute values to lowercase - // - For JSON to XML: reverse operations - const simpleTransform = (value, context) => { - // Skip null/undefined values - if (value === null || value === undefined) return value; - - // Skip non-string values - if (typeof value !== 'string') return value; - - if (context.direction === 'xml-to-json') { - // XML to JSON transformation - if (context.isAttribute) { - // Attribute values to lowercase - return value.toLowerCase(); - } else { - // Element values to uppercase - return value.toUpperCase(); - } - } else if (context.direction === 'json-to-xml') { - // JSON to XML transformation - if (context.isAttribute) { - // Attribute values back to uppercase - return value.toUpperCase(); - } else { - // Element values back to lowercase - return value.toLowerCase(); - } - } - - // Default case - return value; - }; - - beforeEach(() => { - // Create a transformer with the simple transform function - transformer = new XMLJSONTransformer({ - transformFunction: simpleTransform - }); - }); - - test("should transform element values to uppercase and attribute values to lowercase", () => { - const xml = 'value'; - - const result = transformer.xmlToJSON(xml); - - // Element values should be uppercase - expect(result.root["@children"][0].child["@val"]).toBe("VALUE"); - - // Attribute values should be lowercase - expect(result.root["@attrs"].id["@val"]).toBe("id123"); - expect(result.root["@children"][0].child["@attrs"].name["@val"]).toBe("name"); - }); - - test("should reverse transformations when converting from JSON to XML", () => { - const json = { - "root": { - "@ns": "", - "@val": "ROOT VALUE", - "@attrs": { - "id": { - "@val": "id123", - "@ns": "" - } - }, - "@children": [ - { - "child": { - "@ns": "", - "@val": "CHILD VALUE", - "@attrs": { - "name": { - "@val": "name", - "@ns": "" - } - } - } - } - ] - } - }; - - const xml = transformer.jsonToXML(json); - - // Element values should be lowercase in XML - expect(xml).toNormalizeContain('>root value<'); - expect(xml).toNormalizeContain('>child value<'); - - // Attribute values should be uppercase in XML - expect(xml).toNormalizeContain('id="ID123"'); - expect(xml).toNormalizeContain('name="NAME"'); - }); - - test("should handle bidirectional transformation correctly", () => { - const originalXml = 'value'; - - // Convert XML to JSON - const json = transformer.xmlToJSON(originalXml); - - // Convert JSON back to XML - const newXml = transformer.jsonToXML(json); - - // Convert the new XML to JSON again to verify transformations - const newJson = transformer.xmlToJSON(newXml); - - // Check that values were transformed as expected - // Element values: value -> VALUE -> value -> VALUE - expect(newJson.root["@children"][0].child["@val"]).toNormalizeContain("VALUE"); - - // Attribute values: ID123 -> id123 -> ID123 -> id123 - expect(newJson.root["@attrs"].id["@val"]).toNormalizeContain("id123"); - expect(newJson.root["@children"][0].child["@attrs"].name["@val"]).toNormalizeContain("name"); - }); - - test("should handle mixed content", () => { - const xml = '

This is mixed content

'; - - const result = transformer.xmlToJSON(xml); - - // Mixed content is stored as a single value - // It should be transformed to uppercase - expect(result.p["@val"]).toNormalizeContain("THIS IS MIXED CONTENT"); - - // Convert back to XML - const newXml = transformer.jsonToXML(result); - - // Check the content is transformed to lowercase - expect(newXml).toNormalizeContain('>this is mixed content<'); - }); -}); \ No newline at end of file diff --git a/test/unit/xml-json-transformer.test.js b/test/unit/xml-json-transformer.test.js deleted file mode 100644 index 306d3c0..0000000 --- a/test/unit/xml-json-transformer.test.js +++ /dev/null @@ -1,452 +0,0 @@ -/** - * Unit tests for the XMLJSONTransformer class - * These tests can be run in a browser or Node.js environment with a test framework - * like Jest, Mocha, or Jasmine. - */ - -import { XMLJSONTransformer } from '../../src/xml-json-transformer.js'; - -describe("XMLJSONTransformer", () => { - let transformer; - - beforeEach(() => { - // Create a fresh transformer instance before each test - transformer = new XMLJSONTransformer(); - }); - - describe("Basic functionality", () => { - test("should transform a simple XML element to JSON", () => { - const xml = "Hello World"; - const expected = { - root: { - "@ns": "", - "@val": "Hello World", - }, - }; - - const result = transformer.xmlToJSON(xml); - expect(result).toEqual(expected); - }); - - test("should transform a simple JSON object to XML", () => { - const json = { - root: { - "@ns": "", - "@val": "Hello World", - }, - }; - const expected = "Hello World"; - - const result = transformer.jsonToXML(json); - // Remove whitespace for comparison - expect(result).toNormalizeEqual(expected); - }); - - test("should handle empty elements", () => { - const xml = ""; - const expected = { - empty: {}, - }; - - const result = transformer.xmlToJSON(xml); - expect(result).toEqual(expected); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip).toNormalizeEqual(""); - }); - - test("should return JSON as string when requested", () => { - const xml = "Hello World"; - - const result = transformer.xmlToJSON(xml, true); - expect(typeof result).toBe("string"); - expect(JSON.parse(result)).toEqual({ - root: { - "@ns": "", - "@val": "Hello World", - }, - }); - }); - - test("should format JSON with proper indentation", () => { - const json = { - root: { - "@val": "Hello World", - }, - }; - - const result = transformer.jsonToString(json); - console.log(result); - expect(result).toContain("\n"); - expect(result).toContain(" "); - }); - }); - - describe("Attributes", () => { - test("should handle elements with attributes", () => { - const xml = 'Product'; - const expected = { - item: { - "@ns": "", - "@val": "Product", - "@attrs": { - id: { - "@val": "123", - "@ns": "", - }, - category: { - "@val": "book", - "@ns": "", - }, - }, - }, - }; - - const result = transformer.xmlToJSON(xml); - expect(result).toNormalizeEqual(expected); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip).toNormalizeContain('id="123"'); - expect(roundTrip).toNormalizeContain('category="book"'); - }); - - test("should handle attributes with namespace", () => { - const xml = - 'Link'; - - const result = transformer.xmlToJSON(xml); - expect(result.item).toBeDefined(); - expect(result.item["@attrs"]).toBeDefined(); - - // With stripPrefixes=true (default), the attribute might be stored as "href" - // rather than "xlink:href" - const attrName = Object.keys(result.item["@attrs"]).find( - key => key === "xlink:href" || key === "href" - ); - expect(attrName).toBeDefined(); - - const attr = result.item["@attrs"][attrName]; - expect(attr).toBeDefined(); - expect(attr["@val"]).toBe("http://example.com"); - - // In some DOM implementations, the attribute might have a namespace - if (attr["@ns"]) { - expect(attr["@ns"]).toBe("http://www.w3.org/1999/xlink"); - } - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes('href="http://example.com"')).toBe(true); - }); - - test("should handle empty attributes", () => { - const xml = 'Empty value'; - - const result = transformer.xmlToJSON(xml); - // With compact mode and removeEmptyStrings, empty values might be undefined - // Check if the attribute exists but value might be undefined - expect(result.item["@attrs"].empty).toBeDefined(); - expect(result.item["@attrs"].zero["@val"]).toBe("0"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes('empty=""')).toBe(true); - expect(roundTrip.includes('zero="0"')).toBe(true); - }); - }); - - describe("Namespaces", () => { - test("should handle namespaces", () => { - const xml = - 'Content'; - - const result = transformer.xmlToJSON(xml); - // With stripPrefixes=true (default config), the prefix is removed - expect(result.root).toBeDefined(); - expect(result.root["@ns"]).toEqual("http://example.com/ns1"); - - const child = result.root["@children"][0].child; - expect(child).toBeDefined(); - expect(child["@ns"]).toEqual("http://example.com/ns1"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes('xmlns')).toBe(true); - expect(roundTrip.includes('http://example.com/ns1')).toBe(true); - }); - - test("should handle default namespaces", () => { - const xml = 'Content'; - - const result = transformer.xmlToJSON(xml); - expect(result.root["@ns"]).toEqual("http://example.com/default"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes('xmlns="http://example.com/default"')).toBe( - true - ); - }); - - test("should not include namespaces when configured", () => { - const nsTransformer = new XMLJSONTransformer({ - preserveNamespaces: false, - }); - - const xml = - 'Content'; - - const result = nsTransformer.xmlToJSON(xml); - // With stripPrefixes=true (default), we should have "root" not "x:root" - expect(result.root).toBeDefined(); - expect(result.root["@ns"]).toBeUndefined(); - - const child = result.root["@children"][0].child; - expect(child).toBeDefined(); - expect(child["@ns"]).toBeUndefined(); - - const roundTrip = nsTransformer.jsonToXML(result); - expect(roundTrip.includes('xmlns')).toBe(false); - }); - - test("should strip prefixes when configured", () => { - const prefixTransformer = new XMLJSONTransformer({ - stripPrefixes: true, - }); - - const xml = - 'Content'; - - const result = prefixTransformer.xmlToJSON(xml); - expect(result.root).toBeDefined(); - expect(result.root["@ns"]).toEqual("http://example.com/ns1"); - - const children = result.root["@children"]; - expect(children[0].child).toBeDefined(); - - const roundTrip = prefixTransformer.jsonToXML(result); - // The namespace should still be preserved even if prefixes are stripped - expect(roundTrip.includes("http://example.com/ns1")).toBe(true); - }); - - test("should both remove namespaces and strip prefixes when configured", () => { - const simpleTransformer = new XMLJSONTransformer({ - preserveNamespaces: false, - stripPrefixes: true, - }); - - const xml = - 'Content'; - - const result = simpleTransformer.xmlToJSON(xml); - expect(result.root).toBeDefined(); - expect(result.root["@ns"]).toBeUndefined(); - - const children = result.root["@children"]; - expect(children[0].child).toBeDefined(); - expect(children[0].child["@ns"]).toBeUndefined(); - - const roundTrip = simpleTransformer.jsonToXML(result); - expect(roundTrip.includes("xmlns")).toBe(false); - expect(roundTrip.includes("")).toBe(true); - expect(roundTrip.includes("")).toBe(true); - }); - - test("should handle multiple namespaces", () => { - const xml = ` - - A content - B content - - `; - - const result = transformer.xmlToJSON(xml); - - // With stripPrefixes=true (default), we should have "element" not "a:element" - expect(result.root["@children"]).toBeDefined(); - expect(result.root["@children"].length).toBe(2); - - const aElement = result.root["@children"][0].element; - const bElement = result.root["@children"][1].element; - - expect(aElement).toBeDefined(); - expect(bElement).toBeDefined(); - - expect(aElement["@ns"]).toBe("http://example.com/a"); - expect(bElement["@ns"]).toBe("http://example.com/b"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes('xmlns')).toBe(true); - expect(roundTrip.includes('http://example.com/a')).toBe(true); - expect(roundTrip.includes('http://example.com/b')).toBe(true); - }); - }); - - describe("Special node types", () => { - test("should handle CDATA sections", () => { - const xml = "Bold text]]>"; - - const result = transformer.xmlToJSON(xml); - expect(result.root["@cdata"]).toBeDefined(); - expect(result.root["@cdata"].length).toBe(1); - expect(result.root["@cdata"][0]).toBe("Bold text"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip).toNormalizeContain("Bold text]]>"); - }); - - test("should handle multiple CDATA sections", () => { - const xml = - ""; - - const result = transformer.xmlToJSON(xml); - expect(result.root["@cdata"]).toBeDefined(); - expect(result.root["@cdata"].length).toBe(2); - expect(result.root["@cdata"][0]).toBe("First section"); - expect(result.root["@cdata"][1]).toBe("Second section"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip).toNormalizeContain(""); - expect(roundTrip).toNormalizeContain(""); - }); - - test("should handle comments", () => { - const xml = ""; - - const result = transformer.xmlToJSON(xml); - expect(result.root["@comments"]).toBeDefined(); - expect(result.root["@comments"].length).toBe(1); - expect(result.root["@comments"][0]).toBe(" This is a comment "); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes("")).toBe(true); - }); - - test("should handle multiple comments", () => { - const xml = ""; - - const result = transformer.xmlToJSON(xml); - expect(result.root["@comments"]).toBeDefined(); - expect(result.root["@comments"].length).toBe(2); - expect(result.root["@comments"][0]).toBe(" First comment "); - expect(result.root["@comments"][1]).toBe(" Second comment "); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes("")).toBe(true); - expect(roundTrip.includes("")).toBe(true); - }); - - test("should handle processing instructions", () => { - const xml = ''; - - const result = transformer.xmlToJSON(xml); - expect(result.root["@processing"]).toBeDefined(); - expect(result.root["@processing"].length).toBe(1); - expect(result.root["@processing"][0]).toBe("custom-pi data"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes("")).toBe(true); - }); - - test("should handle multiple processing instructions", () => { - const xml = ""; - - const result = transformer.xmlToJSON(xml); - expect(result.root["@processing"]).toBeDefined(); - expect(result.root["@processing"].length).toBe(2); - expect(result.root["@processing"][0]).toBe("first-pi data1"); - expect(result.root["@processing"][1]).toBe("second-pi data2"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes("")).toBe(true); - expect(roundTrip.includes("")).toBe(true); - }); - - test("should not preserve special nodes when configured", () => { - const noSpecialNodesTransformer = new XMLJSONTransformer({ - preserveComments: false, - preserveCDATA: false, - preserveProcessingInstr: false, - }); - - const xml = ` - - - - - Text content - - `; - - const result = noSpecialNodesTransformer.xmlToJSON(xml); - - // When preserving is disabled, these collections might not exist in compact mode - expect(result.root["@comments"]).toBeUndefined(); - expect(result.root["@cdata"]).toBeUndefined(); - expect(result.root["@processing"]).toBeUndefined(); - - expect(result.root["@val"]).toBeDefined(); - expect(result.root["@val"].trim()).toBe("Text content"); - - const roundTrip = noSpecialNodesTransformer.jsonToXML(result); - expect(roundTrip).not.toNormalizeContain("Comment"); - expect(roundTrip).not.toNormalizeContain("CDATA"); - expect(roundTrip).not.toNormalizeContain("pi-target"); - expect(roundTrip).toNormalizeContain("Text content"); - }); - }); - - describe("Mixed Content Handling", () => { - test("should handle simple mixed content in transformation", () => { - const xml = "This has mixed content."; - - const result = transformer.xmlToJSON(xml); - - // The value should contain the entire content including markup - expect(result.paragraph["@val"]).toBe( - "This has mixed content." - ); - - // Children array should not exist for mixed content in compact mode - expect(result.paragraph["@children"]).toBeUndefined(); - - // Transform back to XML - const roundTrip = transformer.jsonToXML(result); - - // Remove whitespace for comparison - const normalizedOriginal = xml.replace(/\s+/g, ""); - const normalizedRoundTrip = roundTrip.replace(/\s+/g, ""); - - expect(normalizedRoundTrip).toBe(normalizedOriginal); - }); - - test("should handle complex mixed content", () => { - const xml = ` -
-

This paragraph has emphasized text and strong text mixed with regular text.

-

Another paragraph with a link in the middle.

-
- `; - - const result = transformer.xmlToJSON(xml); - - // The

elements should be processed as mixed content - const paragraphs = result.article["@children"]; - expect(paragraphs.length).toBe(2); - - const firstP = paragraphs[0].p; - const secondP = paragraphs[1].p; - - // Check that the paragraphs contain the markup - expect(firstP["@val"]).toNormalizeContain("emphasized"); - expect(firstP["@val"]).toNormalizeContain("strong"); - expect(secondP["@val"]).toNormalizeContain( - 'a link' - ); - - // Transform back to XML - const roundTrip = transformer.jsonToXML(result); - - // Verify the transformed XML contains the same elements - expect(roundTrip).toNormalizeContain("emphasized"); - expect(roundTrip).toNormalizeContain("strong"); - expect(roundTrip).toNormalizeContain('a link'); - }); - }); -}); \ No newline at end of file