xpath-ts2
Version:
DOM 3 and 4 XPath 1.0 implementation for browser and Node.js environment with support for typescript 5.
1,242 lines (973 loc) • 45.3 kB
text/typescript
import { expect } from 'chai';
import * as xpath from '../src';
import { isAttribute, isElement, isText } from '../src/utils/types';
import { XPathException } from '../src/xpath-exception';
// tslint:disable:max-line-length
// tslint:disable:quotemark
// tslint:disable:no-unused-expression
const xhtmlNs = 'http://www.w3.org/1999/xhtml';
export function executeTests(implName: string, dom: typeof DOMParser, useDom4: boolean) {
describe(`XPath library tests for ${implName}`, () => {
it('api', () => {
expect(xpath.evaluate).to.exist;
expect(xpath.select).to.exist;
expect(xpath.parse).to.exist;
});
it('evaluate', () => {
const xml = '<book><title>Harry Potter</title></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const result = xpath.evaluate('//title', doc, null, xpath.XPathResult.ANY_TYPE, null);
const nodes = (result as xpath.XPathResult).nodes;
expect((nodes[0] as Element).localName).to.equal('title');
expect((nodes[0].firstChild as Text).data).to.equal('Harry Potter');
expect(toString(nodes[0])).to.equal('<title>Harry Potter</title>');
});
it('select', () => {
const xml =
'<?book title="Harry Potter"?><?series title="Harry Potter"?><?series books="7"?><book><!-- This is a great book --><title>Harry Potter</title></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const nodes = asNodes(xpath.select('//title', doc));
expect((nodes[0] as Element).localName).to.equal('title');
expect((nodes[0].firstChild as Text).data).to.equal('Harry Potter');
expect(toString(nodes[0])).to.equal('<title>Harry Potter</title>');
const nodes2 = asNodes(xpath.select('//node()', doc));
expect(nodes2).to.have.length(7);
const pis = asNodes(xpath.select("/processing-instruction('series')", doc));
expect(pis).to.have.length(2);
expect((pis[1] as Text).data).to.equal('books="7"');
});
it('select single node', () => {
const xml = '<book><title>Harry Potter</title></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
expect((asNodes(xpath.select('//title[1]', doc))[0] as Element).localName).to.equal('title');
});
it('select text node', () => {
const xml = '<book><title>Harry</title><title>Potter</title></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
expect(xpath.select('local-name(/book)', doc)).to.equal('book');
expect(toString(xpath.select('//title/text()', doc))).to.equal('Harry,Potter');
});
it('select number value', () => {
const xml = '<book><title>Harry</title><title>Potter</title></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
expect(xpath.select('count(//title)', doc)).to.eq(2);
});
it('select xpath with namespaces', () => {
const xml = '<book><title xmlns="myns">Harry Potter</title></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const nodes = asNodes(xpath.select('//*[local-name(.)="title" and namespace-uri(.)="myns"]', doc));
expect((nodes[0] as Element).localName).to.equal('title');
expect((nodes[0] as Element).namespaceURI).to.equal('myns');
const nodes2 = asNodes(xpath.select('/*/title', doc));
expect(nodes2).to.have.length(0);
});
it('select xpath with namespaces, using a resolver', () => {
const xml =
'<book xmlns:testns="http://example.com/test" xmlns:otherns="http://example.com/other"><otherns:title>Narnia</otherns:title><testns:title>Harry Potter</testns:title><testns:field testns:type="author">JKR</testns:field></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const mappings: { [key: string]: string | null } = {
testns: 'http://example.com/test'
};
const resolver = {
mappings,
lookupNamespaceURI(prefix: string) {
return this.mappings[prefix];
}
};
expect(asNodes(xpath.selectWithResolver('//testns:title/text()', doc, resolver))[0].nodeValue).to.equal(
'Harry Potter'
);
expect(
asNodes(xpath.selectWithResolver('//testns:field[@testns:type="author"]/text()', doc, resolver))[0].nodeValue
).to.equal('JKR');
const nodes2 = asNodes(xpath.selectWithResolver('/*/testns:*', doc, resolver));
expect(nodes2).to.have.length(2);
});
it('select xpath with namespaces, with default resolver', () => {
const xml =
'<book xmlns:testns="http://example.com/test" xmlns:otherns="http://example.com/other"><otherns:title>Narnia</otherns:title><testns:title>Harry Potter</testns:title><testns:field testns:type="author">JKR</testns:field></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const evaluator = new xpath.XPathEvaluator({});
evaluator.createExpression('//testns:title/text()', null);
expect(asNodes(xpath.select('//testns:title/text()', doc))[0].nodeValue).to.equal('Harry Potter');
expect(asNodes(xpath.select('//testns:field[@testns:type="author"]/text()', doc))[0].nodeValue).to.equal('JKR');
const nodes2 = asNodes(xpath.select('/*/testns:*', doc));
expect(nodes2).to.have.length(2);
});
it('select xpath with default namespace, using a resolver', () => {
const xml =
'<book xmlns="http://example.com/test"><title>Harry Potter</title><field type="author">JKR</field></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const mappings: { [key: string]: string | null } = {
testns: 'http://example.com/test'
};
const resolver = {
mappings,
lookupNamespaceURI(prefix: string) {
return this.mappings[prefix];
}
};
expect(asNodes(xpath.selectWithResolver('//testns:title/text()', doc, resolver))[0].nodeValue).to.equal(
'Harry Potter'
);
expect(
asNodes(xpath.selectWithResolver('//testns:field[@type="author"]/text()', doc, resolver))[0].nodeValue
).to.equal('JKR');
});
it('select xpath with namespaces, prefixes different in xml and xpath, using a resolver', () => {
const xml =
'<book xmlns:testns="http://example.com/test"><testns:title>Harry Potter</testns:title><testns:field testns:type="author">JKR</testns:field></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const mappings: { [key: string]: string | null } = {
ns: 'http://example.com/test'
};
const resolver = {
mappings,
lookupNamespaceURI(prefix: string) {
return this.mappings[prefix];
}
};
expect(asNodes(xpath.selectWithResolver('//ns:title/text()', doc, resolver))[0].nodeValue).to.equal(
'Harry Potter'
);
expect(
asNodes(xpath.selectWithResolver('//ns:field[@ns:type="author"]/text()', doc, resolver))[0].nodeValue
).to.equal('JKR');
});
it('select xpath with namespaces, using namespace mappings', () => {
const xml =
'<book xmlns:testns="http://example.com/test"><testns:title>Harry Potter</testns:title><testns:field testns:type="author">JKR</testns:field></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const select = xpath.useNamespaces({ testns: 'http://example.com/test' });
expect(asNodes(select('//testns:title/text()', doc))[0].nodeValue).to.equal('Harry Potter');
expect(asNodes(select('//testns:field[@testns:type="author"]/text()', doc))[0].nodeValue).to.equal('JKR');
});
it('select attribute', () => {
const xml = '<author name="J. K. Rowling"></author>';
const doc = new dom().parseFromString(xml, 'text/xml');
const author = (xpath.select1('/author/@name', doc) as Attr).value;
expect(author).to.equal('J. K. Rowling');
});
it('select with multiple predicates', () => {
const xml =
'<characters><character name="Snape" sex="M" age="50" /><character name="McGonnagal" sex="F" age="65" /><character name="Harry" sex="M" age="14" /></characters>';
const doc = new dom().parseFromString(xml, 'text/xml');
const characters = asNodes(xpath.select('/*/character[@sex = "M"][@age > 40]/@name', doc));
expect(characters).to.have.length(1);
expect(characters[0].textContent).to.equal('Snape');
});
// https://github.com/goto100/xpath/issues/37
it('select multiple attributes', () => {
const xml = '<authors><author name="J. K. Rowling" /><author name="Saeed Akl" /></authors>';
let doc = new dom().parseFromString(xml, 'text/xml');
const authors = asNodes(xpath.select('/authors/author/@name', doc));
expect(authors).to.have.length(2);
expect((authors[0] as Attr).value).to.equal('J. K. Rowling');
// https://github.com/goto100/xpath/issues/41
doc = new dom().parseFromString(
'<chapters><chapter v="1"/><chapter v="2"/><chapter v="3"/></chapters>',
'text/xml'
);
const nodes = asNodes(xpath.select('/chapters/chapter/@v', doc));
const values = nodes.map((n: Node) => {
return (n as Attr).value;
});
expect(values).to.have.length(3);
expect(values[0]).to.equal('1');
expect(values[1]).to.equal('2');
expect(values[2]).to.equal('3');
});
it('XPathException acts like Error', () => {
expect(() => {
xpath.evaluate('1', null as any, null, null as any, undefined as any);
}).to.throw(XPathException);
});
it('string() with no arguments', () => {
const doc = new dom().parseFromString('<book>Harry Potter</book>', 'text/xml');
const rootElement = xpath.select1('/book', doc);
expect(rootElement).to.exist;
expect(xpath.select1('string()', doc)).to.equal('Harry Potter');
});
it('string value of document fragment', () => {
const doc = new dom().parseFromString('<n />', 'text/xml');
const docFragment = doc.createDocumentFragment();
const el = doc.createElement('book');
docFragment.appendChild(el);
const testValue = 'Harry Potter';
el.appendChild(doc.createTextNode(testValue));
expect(xpath.select1('string()', docFragment)).to.equal(testValue);
});
it('compare string of a number with a number', () => {
expect(xpath.select1('"000" = 0')).to.be.true;
expect(xpath.select1('"45.0" = 45')).to.be.true;
});
it('string(boolean) is a string', () => {
expect(typeof xpath.select1('string(true())')).to.equal('string');
expect(typeof xpath.select1('string(false())')).to.equal('string');
expect(typeof xpath.select1('string(1 = 2)')).to.equal('string');
expect(xpath.select1('"true" = string(true())')).to.be.true;
});
it('string should downcast to boolean', () => {
expect(xpath.select1('"false" = false()')).to.equal(false);
expect(xpath.select1('"a" = true()')).to.equal(true);
expect(xpath.select1('"" = false()')).to.equal(true);
});
it('string(number) is a string', () => {
expect(typeof xpath.select1('string(45)')).to.equal('string');
expect(xpath.select1('"45" = string(45)')).to.be.true;
});
it('correct string to number conversion', () => {
expect(xpath.select1('number("45.200")')).to.equal(45.2);
expect(xpath.select1('number("000055")')).to.equal(55.0);
expect(xpath.select1('number(" 65 ")')).to.equal(65.0);
expect(xpath.select1('"" != 0')).to.equal(true);
expect(xpath.select1('"" = 0')).to.equal(false);
expect(xpath.select1('0 = ""')).to.equal(false);
expect(xpath.select1('0 = " "')).to.equal(false);
expect(xpath.select('number("")') as number).to.be.NaN;
expect(xpath.select('number("45.8g")') as number).to.be.NaN;
expect(xpath.select('number("2e9")') as number).to.be.NaN;
expect(xpath.select('number("+33")') as number).to.be.NaN;
});
it('correct number to string conversion', () => {
expect(xpath.parse('0.525 div 1000000 div 1000000 div 1000000 div 1000000').evaluateString()).to.equal(
'0.0000000000000000000000005250000000000001'
);
expect(xpath.parse('0.525 * 1000000 * 1000000 * 1000000 * 1000000').evaluateString()).to.equal(
'525000000000000000000000'
);
});
it('local-name() and name() of processing instruction', () => {
const xml = '<?book-record added="2015-01-16" author="J.K. Rowling" ?><book>Harry Potter</book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const expectedName = 'book-record';
const localName = xpath.select('local-name(/processing-instruction())', doc);
const name = xpath.select('name(/processing-instruction())', doc);
expect(localName).to.equal(expectedName);
expect(name).to.equal(expectedName);
});
it('evaluate substring-after', () => {
const xml = '<classmate>Hermione</classmate>';
const doc = new dom().parseFromString(xml, 'text/xml');
const part = xpath.select('substring-after(/classmate, "Her")', doc);
expect(part).to.equal('mione');
});
it('parsed expression with no options', () => {
const parsed = xpath.parse('5 + 7');
expect(typeof parsed).to.equal('object');
expect(typeof parsed.evaluate).to.equal('function');
expect(typeof parsed.evaluateNumber).to.equal('function');
expect(parsed.evaluateNumber()).to.equal(12);
// evaluating twice should yield the same result
expect(parsed.evaluateNumber()).to.equal(12);
});
it('select1() on parsed expression', () => {
const xml = '<book><title>Harry Potter</title></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const parsed = xpath.parse('/*/title');
expect(typeof parsed).to.equal('object');
expect(typeof parsed.select1).to.equal('function');
const single = parsed.select1({ node: doc });
expect((single as Element).localName).to.equal('title');
expect((single.firstChild as Text).data).to.equal('Harry Potter');
expect(toString(single)).to.equal('<title>Harry Potter</title>');
});
it('select() on parsed expression', () => {
const xml = '<book><title>Harry Potter</title></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const parsed = xpath.parse('/*/title');
expect(typeof parsed).to.equal('object');
expect(typeof parsed.select).to.equal('function');
const nodes = parsed.select({ node: doc });
expect(nodes).to.exist;
expect(nodes).to.have.length(1);
expect((nodes[0] as Element).localName).to.equal('title');
expect((nodes[0].firstChild as Text).data).to.equal('Harry Potter');
expect(toString(nodes[0])).to.equal('<title>Harry Potter</title>');
});
it('evaluateString(), and evaluateNumber() on parsed expression with node', () => {
const xml = '<book><title>Harry Potter</title><numVolumes>7</numVolumes></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const parsed = xpath.parse('/*/numVolumes');
expect(typeof parsed).to.equal('object');
expect(typeof parsed.evaluateString).to.equal('function');
expect(parsed.evaluateString({ node: doc })).to.equal('7');
expect(typeof parsed.evaluateBoolean).to.equal('function');
expect(parsed.evaluateBoolean({ node: doc })).to.equal(true);
expect(typeof parsed.evaluateNumber).to.equal('function');
expect(parsed.evaluateNumber({ node: doc })).to.equal(7);
});
it('evaluateBoolean() on parsed empty node set and boolean expressions', () => {
const xml = '<book><title>Harry Potter</title></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
function evaluate(path: string) {
return xpath.parse(path).evaluateBoolean({ node: doc });
}
expect(evaluate('/*/myrtle')).to.equal(false);
expect(evaluate('not(/*/myrtle)')).to.equal(true);
expect(evaluate('/*/title')).to.equal(true);
expect(evaluate('/*/title = "Harry Potter"')).to.equal(true);
expect(evaluate('/*/title != "Harry Potter"')).to.equal(false);
expect(evaluate('/*/title = "Percy Jackson"')).to.equal(false);
});
it('namespaces with parsed expression', () => {
const xml =
'<characters xmlns:ps="http://philosophers-stone.com" xmlns:cs="http://chamber-secrets.com">' +
'<ps:character>Quirrell</ps:character><ps:character>Fluffy</ps:character>' +
'<cs:character>Myrtle</cs:character><cs:character>Tom Riddle</cs:character>' +
'</characters>';
const doc = new dom().parseFromString(xml, 'text/xml');
const expr = xpath.parse('/characters/c:character');
const countExpr = xpath.parse('count(/characters/c:character)');
const csns = 'http://chamber-secrets.com';
function resolve(prefix: string) {
if (prefix === 'c') {
return csns;
}
}
function testContext(context: xpath.EvalOptions, description: string) {
try {
const value = expr.evaluateString(context);
const count = countExpr.evaluateNumber(context);
expect(value).to.equal('Myrtle');
expect(count).to.equal(2);
} catch (e: any) {
e.message = description + ': ' + (e.message || '');
throw e;
}
}
testContext(
{
node: doc,
namespaces: {
c: csns
}
},
'Namespace map'
);
testContext(
{
node: doc,
namespaces: resolve
},
'Namespace function'
);
testContext(
{
node: doc,
namespaces: {
getNamespace: resolve
}
},
'Namespace object'
);
});
it('custom functions', () => {
const xml = '<book><title>Harry Potter</title></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const parsed = xpath.parse('concat(double(/*/title), " is cool")');
function doubleString(_context: xpath.EvalOptions, ...args: xpath.Expression[]) {
expect(args).to.have.length(1);
const value = args[0];
const str = value.stringValue;
return str + str;
}
function functions(name: string, _namespace: string) {
if (name === 'double') {
return doubleString;
}
return null;
}
function testContext(context: xpath.EvalOptions, description: string) {
try {
const actual = parsed.evaluateString(context);
const expected = 'Harry PotterHarry Potter is cool';
expect(actual).to.equal(expected);
} catch (e: any) {
e.message = description + ': ' + (e.message || '');
throw e;
}
}
testContext(
{
node: doc,
functions
},
'Functions function'
);
testContext(
{
node: doc,
functions: {
getFunction: functions
}
},
'Functions object'
);
testContext(
{
node: doc,
functions: {
double: doubleString
}
},
'Functions map'
);
});
it('custom function namespaces', () => {
const xml =
'<book><title>Harry Potter</title><friend>Ron</friend><friend>Hermione</friend><friend>Neville</friend></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const parsed = xpath.parse('concat(hp:double(/*/title), " is 2 cool ", hp:square(2), " school")');
const hpns = 'http://harry-potter.com';
const context = {
node: doc,
namespaces: {
hp: hpns
},
functions(name: string, namespace: string) {
if (namespace === hpns) {
switch (name) {
case 'double':
return (_context: xpath.XPathContext, ...args: xpath.Expression[]) => {
expect(args).to.have.length(1);
const value = args[0];
const str = value.stringValue;
return str + str;
};
case 'square':
return (_context: xpath.XPathContext, value: xpath.Expression) => {
const num = value.numberValue;
return num * num;
};
case 'xor':
return (_context: xpath.XPathContext, ...args: xpath.Expression[]) => {
expect(args).to.have.length(2);
const l = args[0];
const r = args[1];
const lbool = l.booleanValue;
const rbool = r.booleanValue;
return (lbool || rbool) && !(lbool && rbool);
};
case 'second':
return (_context: xpath.XPathContext, nodes: xpath.XNodeSet) => {
const nodesArr = nodes.toArray();
const second = nodesArr[1];
return second ? [second] : [];
};
}
}
return null;
}
};
expect(parsed.evaluateString(context)).to.equal('Harry PotterHarry Potter is 2 cool 4 school');
expect(xpath.parse('hp:xor(false(), false())').evaluateBoolean(context)).to.equal(false);
expect(xpath.parse('hp:xor(false(), true())').evaluateBoolean(context)).to.equal(true);
expect(xpath.parse('hp:xor(true(), false())').evaluateBoolean(context)).to.equal(true);
expect(xpath.parse('hp:xor(true(), true())').evaluateBoolean(context)).to.equal(false);
expect(xpath.parse('hp:second(/*/friend)').evaluateString(context)).to.equal('Hermione');
expect(xpath.parse('count(hp:second(/*/friend))').evaluateNumber(context)).to.equal(1);
expect(xpath.parse('count(hp:second(/*/friendz))').evaluateNumber(context)).to.equal(0);
});
it('xpath variables', () => {
const xml = '<book><title>Harry Potter</title><volumes>7</volumes></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const variables: { [key: string]: any } = {
title: 'Harry Potter',
notTitle: 'Percy Jackson',
houses: 4
};
function variableFunction(name: string) {
return variables[name];
}
function testContext(context: xpath.EvalOptions, description: string) {
try {
expect(xpath.parse('$title = /*/title').evaluateBoolean(context)).to.equal(true);
expect(xpath.parse('$notTitle = /*/title').evaluateBoolean(context)).to.equal(false);
expect(xpath.parse('$houses + /*/volumes').evaluateNumber(context)).to.equal(11);
} catch (e: any) {
e.message = description + ': ' + (e.message || '');
throw e;
}
}
testContext(
{
node: doc,
variables: variableFunction
},
'Variables function'
);
testContext(
{
node: doc,
variables: {
getVariable: variableFunction
}
},
'Variables object'
);
testContext(
{
node: doc,
variables
},
'Variables map'
);
});
it('xpath variable namespaces', () => {
const xml = '<book><title>Harry Potter</title><volumes>7</volumes></book>';
const doc = new dom().parseFromString(xml, 'text/xml');
const hpns = 'http://harry-potter.com';
const context = {
node: doc,
namespaces: {
hp: hpns
},
variables(name: string, namespace: string) {
if (namespace === hpns) {
switch (name) {
case 'title':
return 'Harry Potter';
case 'houses':
return 4;
case 'false':
return false;
case 'falseStr':
return 'false';
}
} else if (namespace === '') {
switch (name) {
case 'title':
return 'World';
}
}
return null;
}
};
expect(xpath.parse('$hp:title = /*/title').evaluateBoolean(context)).to.equal(true);
expect(xpath.parse('$title = /*/title').evaluateBoolean(context)).to.equal(false);
expect(xpath.parse('$title').evaluateString(context)).to.equal('World');
expect(xpath.parse('$hp:false').evaluateBoolean(context)).to.equal(false);
expect(xpath.parse('$hp:falseStr').evaluateBoolean(context)).not.to.equal(false);
expect(() => {
xpath.parse('$hp:hello').evaluateString(context);
}).to.throw(XPathException);
});
it('detect unterminated string literals', () => {
function testUnterminated(path: string) {
expect(() => {
xpath.evaluate(path);
}).to.throw(XPathException);
}
testUnterminated('"Hello');
testUnterminated("'Hello");
testUnterminated('self::text() = """');
testUnterminated('"""');
});
if (!useDom4) {
it('string value for CDATA sections', () => {
const xml =
'<people><person><![CDATA[Harry Potter]]></person><person>Ron <![CDATA[Weasley]]></person></people>';
const doc = new dom().parseFromString(xml, 'text/xml');
const person1 = xpath.parse('/people/person').evaluateString({ node: doc });
const person2 = xpath.parse('/people/person/text()').evaluateString({ node: doc });
const person3 = xpath.select('string(/people/person/text())', doc);
const person4 = xpath.parse('/people/person[2]').evaluateString({ node: doc });
expect(person1).to.equal('Harry Potter');
expect(person2).to.equal('Harry Potter');
expect(person3).to.equal('Harry Potter');
expect(person4).to.equal('Ron Weasley');
});
}
it('string value of various node types', () => {
const xml =
"<book xmlns:hp='http://harry'><!-- This describes the Harry Potter Book --><?author name='J.K. Rowling' ?><title lang='en'><![CDATA[Harry Potter & the Philosopher's Stone]]></title><character>Harry Potter</character></book>";
const doc = new dom().parseFromString(xml, 'text/xml');
const allText = xpath.parse('.').evaluateString({ node: doc });
const ns = xpath.parse('*/namespace::*[name() = "hp"]').evaluateString({ node: doc });
const title = xpath.parse('*/title').evaluateString({ node: doc });
const child = xpath.parse('*/*').evaluateString({ node: doc });
const titleLang = xpath.parse('*/*/@lang').evaluateString({ node: doc });
const pi = xpath.parse('*/processing-instruction()').evaluateString({ node: doc });
const comment = xpath.parse('*/comment()').evaluateString({ node: doc });
expect(allText).to.equal("Harry Potter & the Philosopher's StoneHarry Potter");
expect(ns).to.equal('http://harry');
expect(title).to.equal("Harry Potter & the Philosopher's Stone");
expect(child).to.equal("Harry Potter & the Philosopher's Stone");
expect(titleLang).to.equal('en');
expect(pi.trim()).to.equal("name='J.K. Rowling'");
expect(comment).to.equal(' This describes the Harry Potter Book ');
});
it('exposes custom types', () => {
expect(xpath.XPath).to.exist;
expect(xpath.XPathParser).to.exist;
expect(xpath.XPathResult).to.exist;
expect(xpath.Step).to.exist;
expect(xpath.NodeTest).to.exist;
expect(xpath.BarOperation).to.exist;
expect(xpath.NamespaceResolver).to.exist;
expect(xpath.FunctionResolver).to.exist;
expect(xpath.VariableResolver).to.exist;
expect(xpath.XPathContext).to.exist;
expect(xpath.XNodeSet).to.exist;
expect(xpath.XBoolean).to.exist;
expect(xpath.XString).to.exist;
expect(xpath.XNumber).to.exist;
});
it('work with nodes created using DOM1 createElement()', () => {
const doc = new dom().parseFromString('<book />', 'text/xml');
doc.documentElement.appendChild(doc.createElement('characters'));
expect(xpath.select1('/book/characters', doc)).to.be.ok;
expect(xpath.select1('local-name(/book/characters)', doc)).to.equal('characters');
});
it('preceding:: axis works on document fragments', () => {
const doc = new dom().parseFromString('<n />', 'text/xml');
const df = doc.createDocumentFragment();
const root = doc.createElement('book');
df.appendChild(root);
for (let i = 0; i < 10; i += 1) {
root.appendChild(doc.createElement('chapter'));
}
const chapter = xpath.select1('book/chapter[5]', df) as Element;
expect(chapter).to.be.ok;
expect(xpath.select('count(preceding::chapter)', chapter)).to.equal(4);
});
it('node set sorted and unsorted arrays', () => {
const doc = new dom().parseFromString(
'<book><character>Harry</character><character>Ron</character><character>Hermione</character></book>',
'text/xml'
);
const path = xpath.parse('/*/*[3] | /*/*[2] | /*/*[1]');
const nset = path.evaluateNodeSet({ node: doc });
const sorted = nset.toArray();
const unsorted = nset.toUnsortedArray();
expect(sorted).to.have.length(3);
expect(unsorted).to.have.length(3);
expect(sorted[0].textContent).to.equal('Harry');
expect(sorted[1].textContent).to.equal('Ron');
expect(sorted[2].textContent).to.equal('Hermione');
expect(sorted[0]).not.to.equal(unsorted[0]);
});
it('meaningful error for invalid function', () => {
const path = xpath.parse('invalidFunc()');
expect(() => {
path.evaluateString();
}).to.throw(Error);
const path2 = xpath.parse('funcs:invalidFunc()');
expect(() => {
path2.evaluateString({
namespaces: {
funcs: 'myfunctions'
}
});
}).to.throw(Error);
});
// https://github.com/goto100/xpath/issues/32
it('supports contains() function on attributes', () => {
const doc = new dom().parseFromString(
"<books><book title='Harry Potter and the Philosopher\"s Stone' /><book title='Harry Potter and the Chamber of Secrets' /></books>",
'text/xml'
);
const andTheBooks = xpath.select("/books/book[contains(@title, ' ')]", doc);
const secretBooks = xpath.select("/books/book[contains(@title, 'Secrets')]", doc);
expect(andTheBooks).to.have.length(2);
expect(secretBooks).to.have.length(1);
});
it('compare multiple nodes to multiple nodes (equals)', () => {
const xml =
'<school><houses>' +
'<house name="Gryffindor"><student>Harry</student><student>Hermione</student></house>' +
'<house name="Slytherin"><student>Draco</student><student>Crabbe</student></house>' +
'<house name="Ravenclaw"><student>Luna</student><student>Cho</student></house>' +
'</houses>' +
'<honorStudents><student>Hermione</student><student>Luna</student></honorStudents></school>';
const doc = new dom().parseFromString(xml, 'text/xml');
const houses = xpath.parse('/school/houses/house[student = /school/honorStudents/student]').select({ node: doc });
expect(houses).to.have.length(2);
const houseNames = houses
.map((node) => {
return (node as Element).getAttribute('name');
})
.sort();
expect(houseNames[0]).to.equal('Gryffindor');
expect(houseNames[1]).to.equal('Ravenclaw');
});
it('compare multiple nodes to multiple nodes (gte)', () => {
const xml =
'<school><houses>' +
'<house name="Gryffindor"><student level="5">Harry</student><student level="9">Hermione</student></house>' +
'<house name="Slytherin"><student level="1">Goyle</student><student level="1">Crabbe</student></house>' +
'<house name="Ravenclaw"><student level="4">Luna</student><student level="3">Cho</student></house>' +
'</houses>' +
'<courses><course minLevel="9">DADA</course><course minLevel="4">Charms</course></courses>' +
'</school>';
const doc = new dom().parseFromString(xml, 'text/xml');
const houses = xpath
.parse('/school/houses/house[student/@level >= /school/courses/course/@minLevel]')
.select({ node: doc });
expect(houses).to.have.length(2);
const houseNames = houses
.map((node) => {
return (node as Element).getAttribute('name');
})
.sort();
expect(houseNames[0]).to.equal('Gryffindor');
expect(houseNames[1]).to.equal('Ravenclaw');
});
it('inequality comparisons with nodesets', () => {
const xml =
"<books><book num='1' title='PS' /><book num='2' title='CoS' /><book num='3' title='PoA' /><book num='4' title='GoF' /><book num='5' title='OotP' /><book num='6' title='HBP' /><book num='7' title='DH' /></books>";
const doc = new dom().parseFromString(xml, 'text/xml');
const options = { node: doc, variables: { theNumber: 3, theString: '3', theBoolean: true } };
const numberPaths = [
'/books/book[$theNumber <= @num]',
'/books/book[$theNumber < @num]',
'/books/book[$theNumber >= @num]',
'/books/book[$theNumber > @num]'
];
const stringPaths = [
'/books/book[$theString <= @num]',
'/books/book[$theString < @num]',
'/books/book[$theString >= @num]',
'/books/book[$theString > @num]'
];
const booleanPaths = [
'/books/book[$theBoolean <= @num]',
'/books/book[$theBoolean < @num]',
'/books/book[$theBoolean >= @num]',
'/books/book[$theBoolean > @num]'
];
const lhsPaths = ['/books/book[@num <= $theNumber]', '/books/book[@num < $theNumber]'];
function countNodes(paths: string[]) {
return paths
.map(xpath.parse)
.map((path) => {
return path.select(options);
})
.map((arr) => {
return arr.length;
});
}
expect(countNodes(numberPaths)).to.deep.equal([5, 4, 3, 2]);
expect(countNodes(stringPaths)).to.deep.equal([5, 4, 3, 2]);
expect(countNodes(booleanPaths)).to.deep.equal([7, 6, 1, 0]);
expect(countNodes(lhsPaths)).to.deep.equal([3, 2]);
});
it('error when evaluating boolean as number', () => {
const num = xpath.parse('"a" = "b"').evaluateNumber();
expect(num).to.equal(0);
const str = xpath.select('substring("expelliarmus", 1, "a" = "a")');
expect(str).to.equal('e');
});
it('string values of parsed expressions', () => {
const parser = new xpath.XPathParser();
const simpleStep = parser.parse('my:book');
expect(simpleStep.toString()).to.equal('child::my:book');
const precedingSib = parser.parse('preceding-sibling::my:chapter');
expect(precedingSib.toString()).to.equal('preceding-sibling::my:chapter');
const withPredicates = parser.parse('book[number > 3][contains(title, "and the")]');
expect(withPredicates.toString()).to.equal("child::book[(child::number > 3)][contains(child::title, 'and the')]");
const parenthesisWithPredicate = parser.parse('(/books/book/chapter)[7]');
expect(parenthesisWithPredicate.toString()).to.equal('(/child::books/child::book/child::chapter)[7]');
const charactersOver20 = parser.parse('heroes[age > 20] | villains[age > 20]');
expect(charactersOver20.toString()).to.equal(
'child::heroes[(child::age > 20)] | child::villains[(child::age > 20)]'
);
});
it('context position should work correctly', () => {
const doc = new dom().parseFromString(
"<books><book><chapter>The boy who lived</chapter><chapter>The vanishing glass</chapter></book><book><chapter>The worst birthday</chapter><chapter>Dobby's warning</chapter><chapter>The burrow</chapter></book></books>",
'text/xml'
);
const chapters = xpath.parse('/books/book/chapter[2]').select({ node: doc });
expect(chapters).to.have.length(2);
expect(chapters[0].textContent).to.equal('The vanishing glass');
expect(chapters[1].textContent).to.equal("Dobby's warning");
const lastChapters = xpath.parse('/books/book/chapter[last()]').select({ node: doc });
expect(lastChapters).to.have.length(2);
expect(lastChapters[0].textContent).to.equal('The vanishing glass');
expect(lastChapters[1].textContent).to.equal('The burrow');
const secondChapter = xpath.parse('(/books/book/chapter)[2]').select({ node: doc });
expect(secondChapter).to.have.length(1);
expect(chapters[0].textContent).to.equal('The vanishing glass');
const lastChapter = xpath.parse('(/books/book/chapter)[last()]').select({ node: doc });
expect(lastChapter).to.have.length(1);
expect(lastChapter[0].textContent).to.equal('The burrow');
});
it('should allow null namespaces for null prefixes', () => {
const markup =
'<html><head></head><body><p>Hi Ron!</p><my:p xmlns:my="http://www.example.com/my">Hi Draco!</my:p><p>Hi Hermione!</p></body></html>';
const docHtml = new dom().parseFromString(markup, 'text/html');
const noPrefixPath = xpath.parse('/html/body/p[2]');
const greetings1 = noPrefixPath.select({ node: docHtml, allowAnyNamespaceForNoPrefix: false });
expect(greetings1).to.have.length(0);
const allowAnyNamespaceOptions = { node: docHtml, allowAnyNamespaceForNoPrefix: true };
// if allowAnyNamespaceForNoPrefix specified, allow using prefix-less node tests to match nodes with no prefix
const greetings2 = noPrefixPath.select(allowAnyNamespaceOptions);
expect(greetings2).to.have.length(1);
expect(greetings2[0].textContent).to.equal('Hi Hermione!');
const allGreetings = xpath.parse('/html/body/p').select(allowAnyNamespaceOptions);
expect(allGreetings).to.have.length(2);
const nsm = { html: xhtmlNs, other: 'http://www.example.com/other' };
const prefixPath = xpath.parse('/html:html/body/html:p');
const optionsWithNamespaces = { node: docHtml, allowAnyNamespaceForNoPrefix: true, namespaces: nsm };
// if the path uses prefixes, they have to match
const greetings3 = prefixPath.select(optionsWithNamespaces);
expect(greetings3).to.have.length(2);
const badPrefixPath = xpath.parse('/html:html/other:body/html:p');
const greetings4 = badPrefixPath.select(optionsWithNamespaces);
expect(greetings4).to.have.length(0);
});
it('support isHtml option', () => {
const markup =
'<html><head></head><body><p>Hi Ron!</p><my:p xmlns:my="http://www.example.com/my">Hi Draco!</my:p><p>Hi Hermione!</p></body></html>';
const docHtml = new dom().parseFromString(markup, 'text/html');
const ns = { h: xhtmlNs };
// allow matching on unprefixed nodes
const greetings1 = xpath.parse('/html/body/p').select({ node: docHtml, isHtml: true });
expect(greetings1).to.have.length(2);
// allow case insensitive match
const greetings2 = xpath.parse('/h:html/h:bOdY/h:p').select({ node: docHtml, namespaces: ns, isHtml: true });
expect(greetings2).to.have.length(2);
// non-html mode: allow select if case and namespaces match
const greetings3 = xpath.parse('/h:html/h:body/h:p').select({ node: docHtml, namespaces: ns });
expect(greetings3).to.have.length(2);
// non-html mode: require namespaces
const greetings4 = xpath.parse('/html/body/p').select({ node: docHtml, namespaces: ns });
expect(greetings4).to.have.length(0);
// non-html mode: require case to match
const greetings5 = xpath.parse('/h:html/h:bOdY/h:p').select({ node: docHtml, namespaces: ns });
expect(greetings5).to.have.length(0);
});
it('builtin functions', () => {
const translated = xpath.parse('translate("hello", "lhho", "yHb")').evaluateString();
expect(translated).to.equal('Heyy');
const characters = new dom().parseFromString(
'<characters><character>Harry</character><character>Ron</character><character>Hermione</character></characters>',
'text/xml'
);
const firstTwo = xpath.parse('/characters/character[position() <= 2]').select({ node: characters });
expect(firstTwo).to.have.length(2);
expect(firstTwo[0].textContent).to.equal('Harry');
expect(firstTwo[1].textContent).to.equal('Ron');
const last = xpath.parse('/characters/character[last()]').select({ node: characters });
expect(last).to.have.length(1);
expect(last[0].textContent).to.equal('Hermione');
});
it('id string function', () => {
const xmlString =
'<body>' +
'<div id="test1" />' +
'<div id="testid">test1</div>' +
'<a id="jshref" href="javascript:doFoo(\'a\', \'b\')">' +
' javascript href with spaces' +
' </a>' +
' <span id="u1" class="u" />' +
' <span id="u2" class="u" />' +
' <span id="u3" class="u" />' +
' <span style="visibility: visible">do not squint!</span>' +
'</body>';
const ex = "count(id('testid'))";
const xml = new dom().parseFromString(xmlString, 'text/xml');
const result = xpath.evaluate(ex, xml);
expect(result.numberValue).to.equal(1);
});
it('id node-set function', () => {
const xmlString =
'<body>' +
'<div id="test1" />' +
'<div id="testid">test1</div>' +
'<a id="jshref" href="javascript:doFoo(\'a\', \'b\')">' +
' javascript href with spaces' +
' </a>' +
' <span id="u1" class="u" />' +
' <span id="u2" class="u" />' +
' <span id="u3" class="u" />' +
' <span style="visibility: visible">do not squint!</span>' +
'</body>';
const ex = "count(id(//*[@id='testid']))";
const xml = new dom().parseFromString(xmlString, 'text/xml');
const result = xpath.evaluate(ex, xml);
expect(result.numberValue).to.equal(1);
});
describe('Axis tests', () => {
const xmlString = [
'<page>',
' <p></p>',
' <list id="parent">',
' <item></item>',
' <item id="self"><d><d></d></d></item>',
' <item></item>',
' <item></item>',
' <item></item>',
' </list>',
' <f></f>',
'</page>'
].join('');
const xml = new dom().parseFromString(xmlString, 'text/xml');
it('following', () => {
const ex = "count(//*[@id='self']/following::*)";
const result = xpath.evaluate(ex, xml);
expect(result.numberValue).to.equal(4);
});
it('following sibling', () => {
const ex = "count(//*[@id='self']/following-sibling::*)";
const result = xpath.evaluate(ex, xml);
expect(result.numberValue).to.equal(3);
});
it('following sibling 2', () => {
const ex = "count(//*[@id='self']/@*/following-sibling::*)";
const result = xpath.evaluate(ex, xml);
expect(result.numberValue).to.equal(0);
});
it('proceeding', () => {
const ex = "count(//*[@id='self']/preceding::*)";
const result = xpath.evaluate(ex, xml);
expect(result.numberValue).to.equal(2);
});
it('proceeding sibling', () => {
const ex = "count(//*[@id='self']/preceding-sibling::*)";
const result = xpath.evaluate(ex, xml);
expect(result.numberValue).to.equal(1);
});
it('proceeding sibling 2', () => {
const ex = "count(//*[@id='self']/@*/preceding-sibling::*)";
const result = xpath.evaluate(ex, xml);
expect(result.numberValue).to.equal(0);
});
it('parent', () => {
const ex = "//*[@id='self']/parent::*/@id";
const result = xpath.evaluate(ex, xml);
expect(result.stringValue).to.equal('parent');
});
it('parent2', () => {
const ex = 'count(/parent::*)';
const result = xpath.evaluate(ex, xml);
expect(result.numberValue).to.equal(0);
});
it('self', () => {
const ex = "//*[@id='self']/self::*/@id";
const result = xpath.evaluate(ex, xml);
expect(result.stringValue).to.equal('self');
});
});
});
}
function asNodes(result: any) {
return result as Node[];
}
function toString(node: string | number | boolean | Node | Node[]): string {
if (typeof node === 'string' || typeof node === 'number' || typeof node === 'boolean') {
return String(node);
} else if (Array.isArray(node)) {
return node.map((n) => toString(n)).join(',');
} else if (isElement(node)) {
return node.outerHTML !== undefined ? node.outerHTML : node.toString();
} else if (isAttribute(node)) {
return node.value;
} else if (isText(node)) {
return node.nodeValue!;
} else {
return '';
}
}