├── .git-crypt ├── .gitattributes └── keys │ └── default │ └── 0 │ └── 92555CDEB2C15E970AD8FDE85994A5A18F13626A.gpg ├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── karma.conf.js ├── package.json ├── sauce.json ├── src ├── dom-exception.js └── xpath.js └── test ├── adapter.js ├── fixtures └── xpath.html └── xpath_spec.js /.git-crypt/.gitattributes: -------------------------------------------------------------------------------- 1 | # Do not edit this file. To specify the files to encrypt, create your own 2 | # .gitattributes file in the directory where your files are. 3 | * !filter !diff 4 | *.gpg binary 5 | -------------------------------------------------------------------------------- /.git-crypt/keys/default/0/92555CDEB2C15E970AD8FDE85994A5A18F13626A.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tilgovi/simple-xpath-position/6001d1d213d99da5c64782aa6a9fdfca11b7ef74/.git-crypt/keys/default/0/92555CDEB2C15E970AD8FDE85994A5A18F13626A.gpg -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | sauce.json filter=git-crypt diff=git-crypt 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | lib 3 | node_modules 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git-crypt 2 | .gitattributes 3 | .gitignore 4 | .travis.yml 5 | coverage 6 | karma.conf.js 7 | sauce.json 8 | src 9 | test 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - stable 5 | after_success: 6 | - bash <(curl -s https://codecov.io/bash) 7 | cache: 8 | directories: 9 | - node_modules 10 | notifications: 11 | email: false 12 | irc: 13 | channels: 14 | - chat.freenode.net#annotator 15 | on_success: change 16 | on_failure: change 17 | env: 18 | global: 19 | - secure: ACD2rzQ1p9Du71KEaeEKIgBx/mOmK5iFuTOP0ctcS5LjgrhTbtIoIlYMId3gljwd/zBpvzUpoOpavNdBGIpPrfw0pGhBMvVfIO971z+NcOvj8fsX8qm6RPXvhnM1zyMU/GeG0FOq99sQTOsFRtLhYXGYyz9B7bqsBD1G4jCsTsH+tis2Qrr1Zy3zIn5MxBMJzYBZEY5gHd5zs25JZ7yr5vvGmlGRL8Z/74AZv3fgGcsVasxNMa+wdwv8SGqR6qMEY9UeGW02u+DVO+dna+E4D1DUPu+B3NTk+04xgxqGiBTCUOgKQhbzID6LLmA08GNKT2r1BbiMl4OeDKis2sbNYfEcoHPfDZDOT6vs4W8iIBxJU8Da1IIJqj6YpNhaeqfteGki4FV4RwYfVknCM18lnrPiU0emHKGlYuIg8ZA1F8RfJEcvJ34cY9ELbPhq00OcvsT8YYDLU3r/9PdSxFbqcQycsMOWmdOroytrjjFThciFJhMXtq10mg0zhOenRAnmPYi7M5vLn67smilaH+BWTpm/3g4th8ykhi+jAgIOew9P3wvbMb5iehO7plvEQorAqKtAnWw7Km0eyjQObk+1nTlm+BVXhXSqcR6XKt6eb3CQaMvWeWpBG/BxUmrfYWhqwtixeFtJ3JQgbtAGkrdgV2wfQq/Mw08OduCKAa2qk6c= 20 | - secure: hgtg/cnSoznPcqnkdI7InVFhVg7ASMdgqjUTg0bK0eoUAM7I0t1jMfxc7ZEgvcFpVHhn11YZngKA/vR6WyznIh2G82mKGUKLXAI1k2+eg2pzgFXY1JNB7xocI9V2ZES4zwsxE34vs8pXCpOqtdOeYUvduSColjnFMx5TEqRVn7JunXnAMUwACfAqe6HlTXUXD/mWSaenBlPyt02GAq9MEiFZ8BRhj6/utfOxkFP/Defajbi4fNY/vHqkkL8m+GUZWeP31XsVrxRQm/rSdJnG0rXrTpMxp7bB/qTV37PkjDjJabuw0saPeT5Agr+fLMRa983rowo2snDj10VSFFqkhReWj8S4uf5kJS6d7NMDz4DW2VXCQnzcUhRXeXr1otVYjbHgCEy4TIog6g8ep1Gqg4svwvvqA7sa7Q1VBYQi7XKZVjugE5qz+1BQTFBBUw+8ZAYkQJvmto3VFB+BVy2XkxxUtPEY9KftlKCPo2G4jBybUS99UBwSLleyJ2cG+kXOtkUfXFFbV4fQMZ96EYHg7LGa/lCmnkS0zTPoDvyjVCVBrnssO5VLQQXirZpt3jhaxzffQXbtwoONxTmPWWm8Hv8w5pd4MW0uiCzkd1f2xfYIVw2y4cjQlTKFVDKM/z2Lc7Uy020dtZvBEBzzdlOsuIekHrcUGysbY7TMErREsJ4= 21 | matrix: 22 | - BROWSER=PhantomJS 23 | - BROWSER=SL_Chrome 24 | - BROWSER=SL_Edge 25 | - BROWSER=SL_Firefox 26 | - BROWSER=SL_Safari 27 | - BROWSER=SL_IE_8 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2015 The Annotator project contributors 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the “Software”), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple XPath Position 2 | 3 | [![Build Status](https://travis-ci.org/tilgovi/simple-xpath-position.svg?branch=master)](https://travis-ci.org/tilgovi/simple-xpath-position) 4 | [![NPM Package](https://img.shields.io/npm/v/simple-xpath-position.svg)](https://www.npmjs.com/package/simple-xpath-position) 5 | [![Coverage](https://img.shields.io/codecov/c/github/tilgovi/simple-xpath-position/master.svg)](https://codecov.io/gh/tilgovi/simple-xpath-position) 6 | 7 | Create and evaluate simple XPath position expressions. 8 | 9 | ## Installation 10 | 11 | Using npm: 12 | 13 | npm install simple-xpath-position 14 | 15 | ## Usage 16 | 17 | The module provides functions for describing and locating a DOM Node using 18 | an XPath expression. 19 | 20 | ### API 21 | 22 | #### `fromNode(node, [root])` 23 | 24 | Convert a `Node` to an XPath expression. 25 | 26 | If the optional parameter `root` is supplied, the computed XPath expression will 27 | be relative to it. Otherwise, the root of the document to which `node` belongs 28 | is used as the root. 29 | 30 | Returns a string. 31 | 32 | #### `toNode(path, root, [resolver])` 33 | 34 | Locate a single `Node` that matches the given XPath expression. The XPath 35 | expressions are evaluated relative to the Node argument `root`. 36 | 37 | If the optional parameter `resolver` is supplied, it will be used to resolve 38 | any namespaces within the XPath expression. 39 | 40 | Returns a `Node` or `null`. 41 | 42 | ## Compatibility 43 | 44 | This library should work with any browser. 45 | 46 | The presence of a working XPath evaluator is not strictly required. Without it, 47 | the library will only support XPath expressions that use a child axis and 48 | node names with number literal positions. All XPath expressions generated by 49 | this library fit this description. For instance, the library can generate and 50 | consume an expression such as `/html/body/article/p[3]`. 51 | 52 | For better XPath support, consider bundling an implementation such as the 53 | [Wicked Good XPath](https://github.com/google/wicked-good-xpath) library. 54 | 55 | ### Internet Explorer version 8 56 | 57 | - There is no support for namespaces in X(HT)ML documents. 58 | 59 | ## Community 60 | 61 | Originally, this code was part of the 62 | [Annotator](http://annotatorjs.org/) project. 63 | 64 | Any discussion should happen on the 65 | [annotator-dev](https://lists.okfn.org/mailman/listinfo/annotator-dev) mailing 66 | list. 67 | 68 | ## Development 69 | 70 | To contribute, fork this repository and send a pull request with your changes, 71 | including any necessary test and documentation updates. 72 | 73 | ## Testing 74 | 75 | You can run the command-line test suite by executing `npm test`. 76 | 77 | To run the test suite, install the karma test runner with the command 78 | `npm install -g karma-cli` and then run `karma start`. Karma will print 79 | instructions for debugging the tests in a browser. 80 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/xpath') 2 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | browsers: ['PhantomJS'], 4 | browserify: {debug: true, transform: ['babelify']}, 5 | frameworks: ['browserify', 'fixture', 'mocha'], 6 | files: [ 7 | 'test/*.js', 8 | 'test/fixtures/*.html' 9 | ], 10 | preprocessors: { 11 | 'test/*.js': ['browserify'], 12 | 'test/fixtures/*.html': ['html2js'] 13 | }, 14 | reporters: ['progress', 'coverage', 'saucelabs'], 15 | coverageReporter: { 16 | reporters: [ 17 | {type: 'html', subdir: '.'}, 18 | {type: 'json', subdir: '.'}, 19 | {type: 'lcovonly', subdir: '.'}, 20 | {type: 'text', subdir: '.'} 21 | ] 22 | }, 23 | sauceLabs: {testName: 'Simple XPath Position test'}, 24 | 25 | customLaunchers: { 26 | 'SL_Chrome': { 27 | base: 'SauceLabs', 28 | browserName: 'chrome', 29 | }, 30 | 'SL_Edge': { 31 | base: 'SauceLabs', 32 | browserName: 'MicrosoftEdge', 33 | }, 34 | 'SL_Firefox': { 35 | base: 'SauceLabs', 36 | browserName: 'firefox', 37 | }, 38 | 'SL_Safari': { 39 | base: 'SauceLabs', 40 | browserName: 'safari', 41 | }, 42 | 'SL_IE_8': { 43 | base: 'SauceLabs', 44 | browserName: 'internet explorer', 45 | platform: 'Windows 7', 46 | } 47 | } 48 | }) 49 | 50 | try { 51 | var sauceCredentials = require('./sauce.json'); 52 | process.env.SAUCE_USERNAME = sauceCredentials.username; 53 | process.env.SAUCE_ACCESS_KEY = sauceCredentials.accessKey; 54 | } catch (e) { 55 | console.log('Note: run `git-crypt unlock` to use Sauce Labs credentials.'); 56 | } 57 | 58 | if (process.env.TRAVIS) config.set({ 59 | browsers: [process.env.BROWSER] 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-xpath-position", 3 | "version": "2.0.2", 4 | "description": "Create and evaluate simple XPath position expressions.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "eslint src && BABEL_ENV=test karma start --single-run", 8 | "prepublish": "babel -d lib src" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/tilgovi/simple-xpath-position.git" 13 | }, 14 | "keywords": [ 15 | "xpath" 16 | ], 17 | "author": "The Annotator project contributors", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/tilgovi/simple-xpath-position/issues" 21 | }, 22 | "homepage": "https://github.com/tilgovi/simple-xpath-position#readme", 23 | "dependencies": { 24 | "get-document": "^1.0.0" 25 | }, 26 | "devDependencies": { 27 | "assertive-chai": "^2.0.2", 28 | "babel-cli": "^6.14.0", 29 | "babel-eslint": "^6.1.0", 30 | "babel-plugin-istanbul": "^2.0.1", 31 | "babel-plugin-transform-es3-member-expression-literals": "^6.8.0", 32 | "babel-plugin-transform-es3-property-literals": "^6.8.0", 33 | "babel-preset-es2015": "^6.14.0", 34 | "babelify": "^7.3.0", 35 | "browserify": "^13.1.0", 36 | "codecov.io": "^0.1.6", 37 | "eslint": "^3.5.0", 38 | "karma": "^1.3.0", 39 | "karma-browserify": "^5.0.5", 40 | "karma-coverage": "^1.1.1", 41 | "karma-fixture": "^0.2.6", 42 | "karma-html2js-preprocessor": "^1.0.0", 43 | "karma-mocha": "^1.1.1", 44 | "karma-phantomjs-launcher": "^1.0.2", 45 | "karma-sauce-launcher": "^1.0.0", 46 | "mocha": "^3.0.2", 47 | "phantomjs-prebuilt": "^2.1.12", 48 | "watchify": "^3.7.0", 49 | "wicked-good-xpath": "^1.3.0" 50 | }, 51 | "babel": { 52 | "env": { 53 | "test": { 54 | "plugins": [ 55 | "istanbul", 56 | "transform-es3-member-expression-literals", 57 | "transform-es3-property-literals" 58 | ] 59 | } 60 | }, 61 | "plugins": [ 62 | "transform-es3-member-expression-literals", 63 | "transform-es3-property-literals" 64 | ], 65 | "presets": [ 66 | [ 67 | "es2015", 68 | { 69 | "loose": true 70 | } 71 | ] 72 | ], 73 | "sourceMaps": "inline" 74 | }, 75 | "eslintConfig": { 76 | "env": { 77 | "browser": true 78 | }, 79 | "extends": "eslint:recommended", 80 | "parser": "babel-eslint", 81 | "rules": { 82 | "comma-dangle": 0 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /sauce.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tilgovi/simple-xpath-position/6001d1d213d99da5c64782aa6a9fdfca11b7ef74/sauce.json -------------------------------------------------------------------------------- /src/dom-exception.js: -------------------------------------------------------------------------------- 1 | export default class DOMException { 2 | constructor(message, name) { 3 | this.message = message 4 | this.name = name 5 | this.stack = (new Error()).stack 6 | } 7 | } 8 | 9 | DOMException.prototype = new Error() 10 | 11 | DOMException.prototype.toString = function () { 12 | return `${this.name}: ${this.message}` 13 | } 14 | -------------------------------------------------------------------------------- /src/xpath.js: -------------------------------------------------------------------------------- 1 | import getDocument from 'get-document' 2 | 3 | import DOMException from './dom-exception' 4 | 5 | // https://developer.mozilla.org/en-US/docs/XPathResult 6 | const FIRST_ORDERED_NODE_TYPE = 9 7 | 8 | // Default namespace for XHTML documents 9 | const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml' 10 | 11 | 12 | /** 13 | * Compute an XPath expression for the given node. 14 | * 15 | * If the optional parameter `root` is supplied, the computed XPath expression 16 | * will be relative to it. Otherwise, the root element is the root of the 17 | * document to which `node` belongs. 18 | * 19 | * @param {Node} node The node for which to compute an XPath expression. 20 | * @param {Node} [root] The root context for the XPath expression. 21 | * @returns {string} 22 | */ 23 | export function fromNode(node, root = null) { 24 | if (node === undefined) { 25 | throw new Error('missing required parameter "node"') 26 | } 27 | 28 | root = root || getDocument(node) 29 | 30 | let path = '/' 31 | while (node !== root) { 32 | if (!node) { 33 | let message = 'The supplied node is not contained by the root node.' 34 | let name = 'InvalidNodeTypeError' 35 | throw new DOMException(message, name) 36 | } 37 | path = `/${nodeName(node)}[${nodePosition(node)}]${path}` 38 | node = node.parentNode 39 | } 40 | return path.replace(/\/$/, '') 41 | } 42 | 43 | 44 | /** 45 | * Find a node using an XPath relative to the given root node. 46 | * 47 | * The XPath expressions are evaluated relative to the Node argument `root`. 48 | * 49 | * If the optional parameter `resolver` is supplied, it will be used to resolve 50 | * any namespaces within the XPath. 51 | * 52 | * @param {string} path An XPath String to evaluate. 53 | * @param {Node} root The root context for the XPath expression. 54 | * @returns {Node|null} The first matching Node or null if none is found. 55 | */ 56 | export function toNode(path, root, resolver = null) { 57 | if (path === undefined) { 58 | throw new Error('missing required parameter "path"') 59 | } 60 | if (root === undefined) { 61 | throw new Error('missing required parameter "root"') 62 | } 63 | 64 | // Make the path relative to the root, if not the document. 65 | let document = getDocument(root) 66 | if (root !== document) path = path.replace(/^\//, './') 67 | 68 | // Make a default resolver. 69 | let documentElement = document.documentElement 70 | if (resolver === null && documentElement.lookupNamespaceURI) { 71 | let defaultNS = documentElement.lookupNamespaceURI(null) || HTML_NAMESPACE 72 | resolver = (prefix) => { 73 | let ns = {'_default_': defaultNS} 74 | return ns[prefix] || documentElement.lookupNamespaceURI(prefix) 75 | } 76 | } 77 | 78 | return resolve(path, root, resolver) 79 | } 80 | 81 | 82 | // Get the XPath node name. 83 | function nodeName(node) { 84 | switch (node.nodeName) { 85 | case '#text': return 'text()' 86 | case '#comment': return 'comment()' 87 | case '#cdata-section': return 'cdata-section()' 88 | default: return node.nodeName.toLowerCase() 89 | } 90 | } 91 | 92 | 93 | // Get the ordinal position of this node among its siblings of the same name. 94 | function nodePosition(node) { 95 | let name = node.nodeName 96 | let position = 1 97 | while ((node = node.previousSibling)) { 98 | if (node.nodeName === name) position += 1 99 | } 100 | return position 101 | } 102 | 103 | 104 | // Find a single node with XPath `path` 105 | function resolve(path, root, resolver) { 106 | try { 107 | // Add a default value to each path part lacking a prefix. 108 | let nspath = path.replace(/\/(?!\.)([^\/:\(]+)(?=\/|$)/g, '/_default_:$1') 109 | return platformResolve(nspath, root, resolver) 110 | } catch (err) { 111 | return fallbackResolve(path, root) 112 | } 113 | } 114 | 115 | 116 | // Find a single node with XPath `path` using the simple, built-in evaluator. 117 | function fallbackResolve(path, root) { 118 | let steps = path.split("/") 119 | let node = root 120 | while (node) { 121 | let step = steps.shift() 122 | if (step === undefined) break 123 | if (step === '.') continue 124 | let [name, position] = step.split(/[\[\]]/) 125 | name = name.replace('_default_:', '') 126 | position = position ? parseInt(position) : 1 127 | node = findChild(node, name, position) 128 | } 129 | return node 130 | } 131 | 132 | 133 | // Find a single node with XPath `path` using `document.evaluate`. 134 | function platformResolve(path, root, resolver) { 135 | let document = getDocument(root) 136 | let r = document.evaluate(path, root, resolver, FIRST_ORDERED_NODE_TYPE, null) 137 | return r.singleNodeValue 138 | } 139 | 140 | 141 | // Find the child of the given node by name and ordinal position. 142 | function findChild(node, name, position) { 143 | for (node = node.firstChild ; node ; node = node.nextSibling) { 144 | if (nodeName(node) === name && --position === 0) break 145 | } 146 | return node 147 | } 148 | -------------------------------------------------------------------------------- /test/adapter.js: -------------------------------------------------------------------------------- 1 | window.assert = require('assertive-chai').assert; 2 | require('wicked-good-xpath').install(); 3 | fixture.setBase('test/fixtures'); 4 | -------------------------------------------------------------------------------- /test/fixtures/xpath.html: -------------------------------------------------------------------------------- 1 |

the first para

2 |
    3 |
  1. lorem ipsum
  2. 4 |
  3. dolor sit
  4. 5 |
  5. amet constectuer
  6. 6 |
7 |

dolor sit amet. humpty dumpty. etc.

8 | -------------------------------------------------------------------------------- /test/xpath_spec.js: -------------------------------------------------------------------------------- 1 | import getDocument from 'get-document' 2 | 3 | import * as xpath from '../src/xpath' 4 | 5 | describe('xpath', test_toNode); 6 | describe('xpath without document.evaluate or namespace support', () => { 7 | let document = getDocument(fixture.el) 8 | let evaluate = document.evaluate 9 | before(() => document.evaluate = undefined) 10 | after(() => document.evaluate = evaluate) 11 | test_toNode() 12 | }) 13 | 14 | test_fromNode() 15 | 16 | 17 | function test_fromNode() { 18 | beforeEach(() => fixture.load('xpath.html')) 19 | afterEach(() => fixture.cleanup()) 20 | 21 | describe('#fromNode', () => { 22 | it("requires a node argument", () => { 23 | let call = () => xpath.fromNode() 24 | assert.throws(call, 'required parameter') 25 | }) 26 | 27 | it("generates an XPath expression for an element in the document", () => { 28 | let pathToFixHTML = '/html[1]/body[1]/div[1]' 29 | 30 | let pEl = fixture.el.getElementsByTagName('p')[0] 31 | let pPath = pathToFixHTML + '/p[1]' 32 | assert.equal(xpath.fromNode(pEl), pPath) 33 | 34 | let spanEl = fixture.el.getElementsByTagName('span')[0] 35 | let spanPath = pathToFixHTML + '/ol[1]/li[2]/span[1]' 36 | assert.equal(xpath.fromNode(spanEl), spanPath) 37 | 38 | let strongEl = fixture.el.getElementsByTagName('strong')[0] 39 | let strongPath = pathToFixHTML + '/p[2]/strong[1]' 40 | assert.equal(xpath.fromNode(strongEl), strongPath) 41 | }) 42 | 43 | it("takes an optional root parameter for a relative path root", () => { 44 | let ol = fixture.el.getElementsByTagName('ol')[0] 45 | let li = fixture.el.getElementsByTagName('li')[0] 46 | assert.deepEqual(xpath.fromNode(li, ol), '/li[1]') 47 | 48 | let span = fixture.el.getElementsByTagName('span')[0] 49 | assert.deepEqual(xpath.fromNode(span, ol), '/li[2]/span[1]') 50 | }) 51 | 52 | it('raises InvalidNodeTypeError if root does not contain node ', () => { 53 | let node = document.createElement('div') 54 | let check = () => xpath.fromNode(node, fixture.el) 55 | assert.throws(check, 'InvalidNodeTypeError') 56 | }) 57 | }) 58 | } 59 | 60 | 61 | 62 | function test_toNode() { 63 | let path = "/p[2]/strong" 64 | 65 | beforeEach(() => fixture.load('xpath.html')) 66 | afterEach(() => fixture.cleanup()) 67 | 68 | it("requires a path argument", () => { 69 | let call = () => xpath.toNode() 70 | assert.throws(call, 'required parameter') 71 | }) 72 | 73 | it("requires a root argument", () => { 74 | let call = () => xpath.toNode(path) 75 | assert.throws(call, 'required parameter') 76 | }) 77 | 78 | it("should parse a standard xpath string", () => { 79 | let node = xpath.toNode(path, fixture.el) 80 | let strong = document.getElementsByTagName('strong')[0] 81 | assert.strictEqual(node, strong) 82 | }) 83 | 84 | it("should return null when the xpath fails to identify an node", () => { 85 | let node = xpath.toNode('/p[3]', fixture.el) 86 | assert.isNull(node) 87 | }) 88 | } 89 | --------------------------------------------------------------------------------