Skip to content

Commit d6a5bc3

Browse files
committed
test: tests for safeSelect helper fn
also add safeSelectFirst variant
1 parent ae07b2e commit d6a5bc3

2 files changed

Lines changed: 78 additions & 1 deletion

File tree

collection-export/strict-mods.test.js

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import xpath from 'xpath'
44
import { DOMParser as xmldom } from '@xmldom/xmldom'
55

66
import { removeBadNameUsageAttrs, unwrapSimpleElement, fixTitleAttributes, unwrapDateCreated, unwrapDateOther, fixDateCreatedKeyDate, fixDateCreatedQualifer, renameElement, removeElement, removeEmptyElements, removeAttribute, convertAuthorityElement, moveClassificationToSubject, wrapElement, wrapTextWithChild, moveAndRenameElement, convertNamePartDate, convertSubNameWrapper, wrapCopyInformation, removeEmptyClassifications, convertSpeakerReleaseDetail, convertArchivesWrapper, toStrictMODS } from './strict-mods.js'
7-
import { hasDirectTextContent } from './xml-helpers.js'
7+
import { hasDirectTextContent, safeSelect, safeSelectFirst } from './xml-helpers.js'
88

99
// Test fixtures
1010
const fixtures = {
@@ -4279,5 +4279,64 @@ describe('Strict MODS Conversion', () => {
42794279
assert.strictEqual(hasDirectTextContent(location), false)
42804280
})
42814281
})
4282+
4283+
describe('safeSelect', () => {
4284+
it('should return empty array for null document', () => {
4285+
const result = safeSelect('//mods', null)
4286+
assert.deepStrictEqual(result, [])
4287+
})
4288+
4289+
it('should return empty array for undefined document', () => {
4290+
const result = safeSelect('//mods', undefined)
4291+
assert.deepStrictEqual(result, [])
4292+
})
4293+
4294+
it('should return empty array for XPath with no matches', () => {
4295+
const parser = new xmldom()
4296+
const doc = parser.parseFromString('<xml><mods/></xml>', 'text/xml')
4297+
const result = safeSelect('//invalid_xpath', doc)
4298+
assert.deepStrictEqual(result, [])
4299+
})
4300+
4301+
it('should return matching elements for valid XPath', () => {
4302+
const parser = new xmldom()
4303+
const doc = parser.parseFromString('<xml><mods/><mods/></xml>', 'text/xml')
4304+
const result = safeSelect('//mods', doc)
4305+
assert.strictEqual(result.length, 2)
4306+
})
4307+
4308+
it('should not return elements without parents', () => {
4309+
const parser = new xmldom()
4310+
const doc = parser.parseFromString('<mods><child/></mods>', 'text/xml')
4311+
const child = xpath.select1("//child", doc)
4312+
// Manually remove the parent to simulate an orphaned element
4313+
child.parentNode.parentNode.removeChild(child.parentNode)
4314+
assert.deepStrictEqual(safeSelect('//child', doc), [])
4315+
})
4316+
})
4317+
4318+
describe('safeSelectFirst', () => {
4319+
it('should return null for null document', () => {
4320+
const result = safeSelectFirst('//mods', null)
4321+
assert.strictEqual(result, null)
4322+
})
4323+
4324+
it('should return null for XPath with no matches', () => {
4325+
const parser = new xmldom()
4326+
const doc = parser.parseFromString('<xml><mods/></xml>', 'text/xml')
4327+
const result = safeSelectFirst('//invalid_xpath', doc)
4328+
assert.strictEqual(result, null)
4329+
})
4330+
4331+
it('should return the first matching element for valid XPath', () => {
4332+
const parser = new xmldom()
4333+
const doc = parser.parseFromString('<xml><mods/><mods/></xml>', 'text/xml')
4334+
const modsElements = xpath.select('//mods', doc)
4335+
const firstMods = safeSelectFirst('//mods', doc)
4336+
assert.ok(firstMods)
4337+
assert.strictEqual(firstMods.nodeName, 'mods')
4338+
assert.deepEqual(firstMods, modsElements[0])
4339+
})
4340+
})
42824341
})
42834342
})

collection-export/xml-helpers.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,30 @@ import xpath from 'xpath'
1515
* @returns {Element[]} Array of elements with valid parents
1616
*/
1717
export function safeSelect(xpathExpression, context, namespaces = {}) {
18+
if (!context) return []
1819
const select = xpath.useNamespaces(namespaces)
1920
const elements = select(xpathExpression, context)
2021
// Filter out elements without parents upfront to avoid null checks in loops
2122
return Array.from(elements).filter(el => el && el.parentNode)
2223
}
2324

25+
/**
26+
* Like safeSelect but returns only the first element with a valid parent
27+
*
28+
* @param {string} xpathExpression - XPath expression to select elements
29+
* @param {Document|Element} context - DOM context to search within
30+
* @param {Object} [namespaces={}] - Optional namespace mapping
31+
* @returns {Element|null} First element with a valid parent, or null if none found
32+
*/
33+
export function safeSelectFirst(xpathExpression, context, namespaces = {}) {
34+
if (!context) return null
35+
const select = xpath.useNamespaces(namespaces)
36+
const elements = select(xpathExpression, context)
37+
// Filter out elements without parents upfront to avoid null checks in loops
38+
const filtered = Array.from(elements).filter(el => el && el.parentNode)
39+
return filtered.length > 0 ? filtered[0] : null
40+
}
41+
2442
/**
2543
* Copy all attributes from source element to target element
2644
* Optionally skip certain attribute names

0 commit comments

Comments
 (0)