From f1ea76abb44832e7f471d766113e141f318e4a4a Mon Sep 17 00:00:00 2001 From: Leonel Sanches da Silva <53848829+leonelsanchesdasilva@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:02:34 -0700 Subject: [PATCH 1/3] Fix XPath arithmetic on element nodes; reorganize tests by topic (closes #189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump src/xpath/lib submodule to 1e9b871: - Add node-utils.ts with getStringValueFromNode() helper - Fix atomize() in arithmetic-expression.ts and unary-expression.ts to extract text content from DOM nodes before numeric conversion, fixing expressions like `number + 1` returning NaN when `number` is a child element (issue #189) Rename issue-specific test files to topic-based names: - issue-183.test.ts → template-match.test.ts - issue-187.test.ts → copy-of.test.ts - issue-189.test.ts → arithmetic.test.ts Co-Authored-By: Claude Sonnet 4.6 --- src/xpath/lib | 2 +- tests/xslt/arithmetic.test.ts | 133 ++++++++++++++++++ .../{issue-187.test.ts => copy-of.test.ts} | 4 +- ...sue-183.test.ts => template-match.test.ts} | 26 ++-- 4 files changed, 147 insertions(+), 18 deletions(-) create mode 100644 tests/xslt/arithmetic.test.ts rename tests/xslt/{issue-187.test.ts => copy-of.test.ts} (95%) rename tests/xslt/{issue-183.test.ts => template-match.test.ts} (74%) diff --git a/src/xpath/lib b/src/xpath/lib index 5ec18e4..1e9b871 160000 --- a/src/xpath/lib +++ b/src/xpath/lib @@ -1 +1 @@ -Subproject commit 5ec18e4561f754e78d94d0dc6a3c12b020230c64 +Subproject commit 1e9b871bd6b328fa4a130d5b4c0088adc842ed93 diff --git a/tests/xslt/arithmetic.test.ts b/tests/xslt/arithmetic.test.ts new file mode 100644 index 0000000..298f72a --- /dev/null +++ b/tests/xslt/arithmetic.test.ts @@ -0,0 +1,133 @@ +import { Xslt, XmlParser } from '../../src/index'; + +describe('XPath arithmetic operations', () => { + let xslt: Xslt; + let xmlParser: XmlParser; + + beforeEach(() => { + xslt = new Xslt(); + xmlParser = new XmlParser(); + }); + + it('addition: number + 1 returns 2', async () => { + const xml = xmlParser.xmlParse('1'); + const stylesheet = xmlParser.xmlParse(` + + + +`); + const result = await xslt.xsltProcess(xml, stylesheet); + expect(result).toBe('2'); + }); + + it('subtraction: number - 1 returns 0', async () => { + const xml = xmlParser.xmlParse('1'); + const stylesheet = xmlParser.xmlParse(` + + + +`); + const result = await xslt.xsltProcess(xml, stylesheet); + expect(result).toBe('0'); + }); + + it('multiplication: number * 2 returns 2', async () => { + const xml = xmlParser.xmlParse('1'); + const stylesheet = xmlParser.xmlParse(` + + + +`); + const result = await xslt.xsltProcess(xml, stylesheet); + expect(result).toBe('2'); + }); + + it('division: number div 2 returns 5', async () => { + const xml = xmlParser.xmlParse('10'); + const stylesheet = xmlParser.xmlParse(` + + + +`); + const result = await xslt.xsltProcess(xml, stylesheet); + expect(result).toBe('5'); + }); + + it('modulo: number mod 3 returns 1', async () => { + const xml = xmlParser.xmlParse('10'); + const stylesheet = xmlParser.xmlParse(` + + + +`); + const result = await xslt.xsltProcess(xml, stylesheet); + expect(result).toBe('1'); + }); + + it('node + node: first + second returns correct sum', async () => { + const xml = xmlParser.xmlParse('34'); + const stylesheet = xmlParser.xmlParse(` + + + +`); + const result = await xslt.xsltProcess(xml, stylesheet); + expect(result).toBe('7'); + }); + + it('decimal node: price * 2 returns 7', async () => { + const xml = xmlParser.xmlParse('3.5'); + const stylesheet = xmlParser.xmlParse(` + + + +`); + const result = await xslt.xsltProcess(xml, stylesheet); + expect(result).toBe('7'); + }); + + it('attribute node: @value + 1 returns 6', async () => { + const xml = xmlParser.xmlParse(''); + const stylesheet = xmlParser.xmlParse(` + + + +`); + const result = await xslt.xsltProcess(xml, stylesheet); + expect(result).toBe('6'); + }); + + it('unary negation on node: -number returns -5', async () => { + const xml = xmlParser.xmlParse('5'); + const stylesheet = xmlParser.xmlParse(` + + + +`); + const result = await xslt.xsltProcess(xml, stylesheet); + expect(result).toBe('-5'); + }); + + it('empty node-set in arithmetic does not throw', async () => { + // XPath 2.0 empty-sequence semantics: empty node-set → null → no output. + // (XPath 1.0 would yield NaN, but this implementation follows 2.0 rules.) + const xml = xmlParser.xmlParse('1'); + const stylesheet = xmlParser.xmlParse(` + + + +`); + await expect(xslt.xsltProcess(xml, stylesheet)).resolves.not.toThrow(); + }); + + it('plain number select still works: select="number" returns 1', async () => { + const xml = xmlParser.xmlParse('1'); + const stylesheet = xmlParser.xmlParse(` + + + +`); + const result = await xslt.xsltProcess(xml, stylesheet); + expect(result).toBe('1'); + }); +}); diff --git a/tests/xslt/issue-187.test.ts b/tests/xslt/copy-of.test.ts similarity index 95% rename from tests/xslt/issue-187.test.ts rename to tests/xslt/copy-of.test.ts index f95cc46..0d78cba 100644 --- a/tests/xslt/issue-187.test.ts +++ b/tests/xslt/copy-of.test.ts @@ -1,6 +1,6 @@ import { Xslt, XmlParser } from '../../src/index'; -describe('Issue #187: Identity transformation adds spaces to comments', () => { +describe('xsl:copy-of', () => { it('keeps comment content stable across repeated identity transforms', async () => { const xslt = new Xslt(); const xmlParser = new XmlParser(); @@ -40,4 +40,4 @@ describe('Issue #187: Identity transformation adds spaces to comments', () => { const secondPass = await xslt.xsltProcess(secondInputDoc, stylesheet); expect(secondPass).toBe(firstPass); }); -}); \ No newline at end of file +}); diff --git a/tests/xslt/issue-183.test.ts b/tests/xslt/template-match.test.ts similarity index 74% rename from tests/xslt/issue-183.test.ts rename to tests/xslt/template-match.test.ts index 0aea773..5a6c0b1 100644 --- a/tests/xslt/issue-183.test.ts +++ b/tests/xslt/template-match.test.ts @@ -1,47 +1,43 @@ import { Xslt, XmlParser } from '../../src/index'; -describe('Issue #183: Template match with nested elements', () => { - it('should match //message when nested under ', async () => { - const xslt = new Xslt(); - const xmlParser = new XmlParser(); +describe('xsl:template match patterns', () => { + let xslt: Xslt; + let xmlParser: XmlParser; + beforeEach(() => { + xslt = new Xslt(); + xmlParser = new XmlParser(); + }); + + it('matches //message when nested under a child element', async () => { const xml = xmlParser.xmlParse('Hello World.'); const stylesheet = xmlParser.xmlParse(`
`); - const result = await xslt.xsltProcess(xml, stylesheet); expect(result).toBe('
Hello World.
'); }); - it('should match with explicit path /page/child/message', async () => { - const xslt = new Xslt(); - const xmlParser = new XmlParser(); - + it('matches with explicit absolute path /page/child/message', async () => { const xml = xmlParser.xmlParse('Hello World.'); const stylesheet = xmlParser.xmlParse(`
`); - const result = await xslt.xsltProcess(xml, stylesheet); expect(result).toBe('
Hello World.
'); }); - it('should match //message when NOT nested (direct child of root)', async () => { - const xslt = new Xslt(); - const xmlParser = new XmlParser(); - + it('matches //message when element is a direct child of root', async () => { const xml = xmlParser.xmlParse('Hello World.'); const stylesheet = xmlParser.xmlParse(`
`); - const result = await xslt.xsltProcess(xml, stylesheet); expect(result).toBe('
Hello World.
'); }); From ecc62bc990af40619494768f0e442a92fccb790b Mon Sep 17 00:00:00 2001 From: Leonel Sanches da Silva <53848829+leonelsanchesdasilva@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:55:05 -0700 Subject: [PATCH 2/3] Fix CI failure: merge copy-of.test.ts into pre-existing copy-of.test.tsx Having both copy-of.test.ts and copy-of.test.tsx in the same directory caused a TypeScript module name collision, making ts-jest fail with a misleading "outDir" error in CI. Merged the comment-stability tests (from issue #187) into the existing copy-of.test.tsx and removed the duplicate .ts file. Also unified the import to use src/index for consistency. Co-Authored-By: Claude Sonnet 4.6 --- tests/xslt/copy-of.test.ts | 43 ------------------------------------- tests/xslt/copy-of.test.tsx | 43 +++++++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 45 deletions(-) delete mode 100644 tests/xslt/copy-of.test.ts diff --git a/tests/xslt/copy-of.test.ts b/tests/xslt/copy-of.test.ts deleted file mode 100644 index 0d78cba..0000000 --- a/tests/xslt/copy-of.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Xslt, XmlParser } from '../../src/index'; - -describe('xsl:copy-of', () => { - it('keeps comment content stable across repeated identity transforms', async () => { - const xslt = new Xslt(); - const xmlParser = new XmlParser(); - - const xmlInput = ''; - const stylesheet = xmlParser.xmlParse(` - - - -`); - - const firstInputDoc = xmlParser.xmlParse(xmlInput); - const firstPass = await xslt.xsltProcess(firstInputDoc, stylesheet); - expect(firstPass).toBe(xmlInput); - - const secondInputDoc = xmlParser.xmlParse(firstPass); - const secondPass = await xslt.xsltProcess(secondInputDoc, stylesheet); - expect(secondPass).toBe(firstPass); - }); - - it('keeps empty comments stable across repeated identity transforms', async () => { - const xslt = new Xslt(); - const xmlParser = new XmlParser(); - - const xmlInput = ''; - const stylesheet = xmlParser.xmlParse(` - - - -`); - - const firstInputDoc = xmlParser.xmlParse(xmlInput); - const firstPass = await xslt.xsltProcess(firstInputDoc, stylesheet); - expect(firstPass).toBe(xmlInput); - - const secondInputDoc = xmlParser.xmlParse(firstPass); - const secondPass = await xslt.xsltProcess(secondInputDoc, stylesheet); - expect(secondPass).toBe(firstPass); - }); -}); diff --git a/tests/xslt/copy-of.test.tsx b/tests/xslt/copy-of.test.tsx index 56098cd..5384712 100644 --- a/tests/xslt/copy-of.test.tsx +++ b/tests/xslt/copy-of.test.tsx @@ -1,9 +1,48 @@ import assert from 'assert'; -import { XmlParser } from '../../src/dom'; -import { Xslt } from '../../src/xslt'; +import { Xslt, XmlParser } from '../../src/index'; describe('xsl:copy-of', () => { + it('keeps comment content stable across repeated identity transforms', async () => { + const xslt = new Xslt(); + const xmlParser = new XmlParser(); + + const xmlInput = ''; + const stylesheet = xmlParser.xmlParse(` + + + +`); + + const firstInputDoc = xmlParser.xmlParse(xmlInput); + const firstPass = await xslt.xsltProcess(firstInputDoc, stylesheet); + assert.equal(firstPass, xmlInput); + + const secondInputDoc = xmlParser.xmlParse(firstPass); + const secondPass = await xslt.xsltProcess(secondInputDoc, stylesheet); + assert.equal(secondPass, firstPass); + }); + + it('keeps empty comments stable across repeated identity transforms', async () => { + const xslt = new Xslt(); + const xmlParser = new XmlParser(); + + const xmlInput = ''; + const stylesheet = xmlParser.xmlParse(` + + + +`); + + const firstInputDoc = xmlParser.xmlParse(xmlInput); + const firstPass = await xslt.xsltProcess(firstInputDoc, stylesheet); + assert.equal(firstPass, xmlInput); + + const secondInputDoc = xmlParser.xmlParse(firstPass); + const secondPass = await xslt.xsltProcess(secondInputDoc, stylesheet); + assert.equal(secondPass, firstPass); + }); + it('Trivial', async () => { const xmlSource = ` From 926f0cc405648dcfeb3760d671fc94f1bcec30f0 Mon Sep 17 00:00:00 2001 From: Leonel Sanches da Silva <53848829+leonelsanchesdasilva@users.noreply.github.com> Date: Sun, 22 Mar 2026 17:10:38 -0700 Subject: [PATCH 3/3] Fix incorrect Jest assertion and misleading comment in arithmetic tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace resolves.not.toThrow() with a plain await — toThrow() expects a function, not a resolved value (Copilot review #190) - Update the empty node-set comment to accurately describe XPath 1.0 behavior rather than attributing it to XPath 2.0 semantics Co-Authored-By: Claude Sonnet 4.6 --- tests/xslt/arithmetic.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/xslt/arithmetic.test.ts b/tests/xslt/arithmetic.test.ts index 298f72a..b574ab6 100644 --- a/tests/xslt/arithmetic.test.ts +++ b/tests/xslt/arithmetic.test.ts @@ -109,15 +109,15 @@ describe('XPath arithmetic operations', () => { }); it('empty node-set in arithmetic does not throw', async () => { - // XPath 2.0 empty-sequence semantics: empty node-set → null → no output. - // (XPath 1.0 would yield NaN, but this implementation follows 2.0 rules.) + // XPath 1.0 typically yields NaN for arithmetic on an empty node-set. + // This test only asserts that xsltProcess resolves without throwing. const xml = xmlParser.xmlParse('1'); const stylesheet = xmlParser.xmlParse(` `); - await expect(xslt.xsltProcess(xml, stylesheet)).resolves.not.toThrow(); + await xslt.xsltProcess(xml, stylesheet); }); it('plain number select still works: select="number" returns 1', async () => {