openrosa-xpath-evaluator
Version:
Wrapper for browsers' XPath evaluator with added support for OpenRosa extensions.
229 lines (225 loc) • 8.22 kB
JavaScript
// TODO this can be moved to test/unit
const { assert } = require('chai');
const _ = require('lodash');
const ExtendedXPathEvaluator = require('../../src/extended-xpath');
const DATE_MATCH =
'(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\d\\d 20\\d\\d \\d\\d:\\d\\d:\\d\\d GMT([+-]\\d\\d\\d\\d (.+))?';
const examples = {
false: /false/,
true: /true/,
'"double-string"': /^double-string$/,
"'single-string'": /^single-string$/,
'"string(shhh)"': /^string\(shhh\)$/,
'date()': new RegExp(`^${DATE_MATCH}$`),
'upcase("spOtTy")': /^SPOTTY$/,
'concat("single")': /^single$/,
'concat("a","b")': /^ab$/,
'concat("a","b","c")': /^abc$/,
'concat("a", "b", "c")': /^abc$/,
'"plus" + "one"': /^NaN$/,
'upcase("what")': /^WHAT$/,
'concat(date())': new RegExp(`^${DATE_MATCH}$`),
'concat(date(), "X")': new RegExp(`^${DATE_MATCH}X$`),
'concat("X", date())': new RegExp(`^X${DATE_MATCH}$`),
'concat("Report::", "Today\'s date: ", date())': new RegExp(
`^Report::Today's date: ${DATE_MATCH}$`
),
'concat(concat(upcase("Big"), downcase("Little")), "Average", " by ", concat("Some", " ", "Author"))':
/^BIGlittleAverage by Some Author$/,
'/xpath/expression': /^<xpath:\/xpath\/expression>$/,
'concat("Evaluates to: ", /xpath/expression)':
/^Evaluates to: <xpath:\/xpath\/expression>$/,
3: /^3$/,
3.1416: /^3.1416$/,
'-3': /^-3$/,
'-3.1416': /^-3.1416$/,
'1 + 1': /^2$/,
'1 - 1': /^0$/,
'10 div 100': /^0.1$/,
'random()': /^(0\.\d+)|(\d\.\d+e-\d)$/,
'random() div 10': /^(0\.0\d+)|(\d\.\d+e-\d)$/,
'12 mod 5': /^2$/,
'reverse("hello")': /^olleh$/,
'native_function()': /^<xpath:native_function\(\)>$/,
'native_function(3)': /^<xpath:native_function\(3.00000000000000000000\)>$/,
'native_function("string-arg")':
/^<xpath:native_function\("string-arg"\)>$/,
"native_function('string-with-escaped-\"-arg')":
/^<xpath:native_function\('string-with-escaped-"-arg'\)>$/,
'native_function(1, 2, 3, "a", \'b\', "c")':
/^<xpath:native_function\(1.00000000000000000000,2.00000000000000000000,3.00000000000000000000,"a",'b',"c"\)>$/,
'native-function()': /^<xpath:native-function\(\)>$/,
'native-function(3)': /^<xpath:native-function\(3.00000000000000000000\)>$/,
'native-function("string-arg")':
/^<xpath:native-function\("string-arg"\)>$/,
'native-function(1, 2, 3, "a", \'b\', "c")':
/^<xpath:native-function\(1.00000000000000000000,2.00000000000000000000,3.00000000000000000000,"a",'b',"c"\)>$/,
/*
// Not clear what to do here as correcting this requires knowledge of return types of native functions.
'native-function1(native-function2() + native-function3()) + native-function4(native-function5() + native-function6())':
/^<xpath:native-function1\("<xpath:native-function2\(\)><xpath:native-function3\(\)>"\)><xpath:native-function4\("<xpath:native-function5\(\)><xpath:native-function6\(\)>"\)>$/,
*/
'native-function-with-space-before-bracket ()':
/^<xpath:native-function-with-space-before-bracket\(\)>$/,
'3 * 2 + 1': /^7$/,
'2 + 3 * 4 + 5 * 6 + 7 * 8 + 9': /^109$/,
'2+3*4+5*6+7*8+9': /^109$/,
'1 + 2 * 3': /^7$/,
'1 > 0': /^true$/,
'1 > 1': /^false$/,
'1 < 1': /^false$/,
'1 > -1': /^true$/,
'1 < -1': /^false$/,
'-1 > 1': /^false$/,
'-1 < 1': /^true$/,
'-1 > -1': /^false$/,
'-1 < -1': /^false$/,
'-1 < -2': /^false$/,
0.23: /^0.23$/,
'.23': /^0.23$/,
'concat(0+1,3-1,6 div 2,4*1)': /^1234$/,
'0 or 1': true,
'1 or throw-overkeen-error()': /^true/,
'1 or 1 + throw-overkeen-error()': /^true/,
'1 or throw-overkeen-error() + 1': /^true/,
'0 and 1': false,
'0 and throw-overkeen-error()': false,
'0 and 1 + throw-overkeen-error()': false,
'0 and throw-overkeen-error() + 1': false,
'(1 or 1) or 1': true,
'0 = 0 and false() != "true"': true,
'1 or 1 and 0': true,
'0 and 0 or 1': true,
'0 and 1 or 1': true,
'1 and 0 or 1': /^true/,
'0 and throw-overkeen-error() or 1': true,
'0 and (throw-overkeen-error())': false,
'0 and concat(throw-overkeen-error())': false,
'1 or /explode': /^true/,
'1 or 1 + /explode': /^true/,
'1 or /explode + 1': /^true/,
'0 and /explode': false,
'0 and 1 + /explode': false,
'0 and /explode + 1': false,
'0 and /explode or 1': true,
'0 and (/explode)': false,
'1 or 0 and /explode': true,
'1 and 0 or 0 or 1 and 1 or 0 or 0 or 0 and /explode or /explode or /explode and /explode': true,
'1 and 0 or 0 or 1 and 1 or 0 or 0 or 0 and ([/explode]) or /explode or /explode and /explode': true,
'0 and concat(/explode)': false,
'0 and /explode[/explode]': false,
'1 div 0': Infinity,
'-1 div 0': -Infinity,
};
const trickyStandardXpathSupported = [
'./author',
'author',
'first.name',
'/bookstore',
'//author',
'author/first-name',
'bookstore//title',
'bookstore/*/title',
'bookstore//book/excerpt//emph',
'.//title',
'book/*/last-name',
'@style',
'price/@exchange',
'price/@exchange/total',
'book/@style',
'./first-name',
'first-name',
'my:book',
'../../some-path',
'*/*',
'@*',
'@my:*',
'my:*',
];
const xp = {
str(v) {
return { t: 'str', v };
},
num(v) {
return { t: 'num', v };
},
};
const extendedXPathEvaluator = new ExtendedXPathEvaluator(
{
evaluate: (xpath) => {
if (xpath === 'throw-overkeen-error()')
throw new Error(
'This path should not have been evaluated, because and/or should be lazy!'
);
if (xpath === '/explode')
throw new Error('The forbidden path was accessed!');
return {
resultType: XPathResult.STRING_TYPE,
stringValue: `<xpath:${xpath}>`,
};
},
},
{
func: {
upcase(it) {
return xp.str(it.v.toUpperCase());
},
downcase(it) {
return xp.str(it.v.toLowerCase());
},
date() {
return xp.str(new Date().toString());
},
concat(...args) {
let i;
let acc = '';
for (i = 0; i < args.length; ++i) acc += args[i].v;
return xp.str(acc);
},
random() {
return xp.num(Math.random());
},
reverse(it) {
return xp.str(it.v.split('').reverse().join(''));
},
},
}
);
describe('ExtendedXpathEvaluator', () => {
_.map(examples, (expected, expr) => {
it(`${expr} should evaluate to ${expected}`, () => {
switch (typeof expected) {
case 'boolean':
return assert.equal(
extendedXPathEvaluator.evaluate(expr).booleanValue,
expected
);
case 'number':
return assert.equal(
extendedXPathEvaluator.evaluate(expr).numberValue,
expected
);
case 'string':
return assert.equal(
extendedXPathEvaluator.evaluate(expr).stringValue,
expected
);
default:
return assert.match(
extendedXPathEvaluator.evaluate(expr).stringValue,
expected
);
}
});
});
describe('Supported XPath expressions', () => {
_.each(trickyStandardXpathSupported, (expr) => {
it(`${expr} should be delegated to the regular XPath evaluator`, () => {
assert.equal(
extendedXPathEvaluator.evaluate(expr).stringValue,
`<xpath:${expr}>`
);
});
});
});
});