├── .eslintrc.yml ├── .gitignore ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── karma.conf.js ├── package.json ├── src ├── .eslintrc.yml └── TreeWalker-polyfill.js └── test ├── TreeWalker.test.js ├── qunit-disable-native.js └── qunit-init.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: crockford 2 | env: 3 | browser: true 4 | rules: 5 | indent: [error, tab] 6 | no-continue: 0 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /dist 3 | /node_modules 4 | /package-lock.json 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | language: node_js 4 | 5 | node_js: 6 | - '8' 7 | 8 | addons: 9 | chrome: stable 10 | 11 | script: 12 | - npm run ci 13 | 14 | after_success: 15 | - npm install -g codecov@2.x 16 | - codecov 17 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = function (grunt) { 3 | grunt.loadNpmTasks('grunt-contrib-uglify'); 4 | 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | uglify: { 8 | all: { 9 | files: { 10 | 'dist/TreeWalker-polyfill.min.js': ['src/TreeWalker-polyfill.js'] 11 | }, 12 | options: { 13 | banner: '/*! TreeWalker v<%= pkg.version %> | krinkle.mit-license.org */\n' 14 | } 15 | } 16 | } 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013–2017 Timo Tijhof 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Krinkle/dom-TreeWalker-polyfill.svg?branch=master)](https://travis-ci.org/Krinkle/dom-TreeWalker-polyfill) 2 | [![Code coverage](https://img.shields.io/codecov/c/github/Krinkle/dom-TreeWalker-polyfill.svg)](https://codecov.io/gh/Krinkle/dom-TreeWalker-polyfill) 3 | [![Tested with QUnit](https://img.shields.io/badge/tested_with-qunit-9c3493.svg)](https://qunitjs.com/) 4 | 5 | # DOM TreeWalker polyfill 6 | 7 | JavaScript implementation of W3 DOM4 TreeWalker interface for browsers supporting ECMAScript 3 or higher. 8 | 9 | ## Specification 10 | 11 | `TreeWalker`: 12 | * https://dom.spec.whatwg.org/#interface-treewalker 13 | * http://www.w3.org/TR/dom/#interface-treewalker 14 | * http://www.w3.org/TR/DOM-Level-2-Traversal-Range/traversal.html#Traversal-TreeWalker 15 | 16 | `document.createTreeWalker`: 17 | * https://dom.spec.whatwg.org/#dom-document-createtreewalker 18 | * http://www.w3.org/TR/dom/#dom-document-createtreewalker 19 | * http://www.w3.org/TR/DOM-Level-2-Traversal-Range/traversal.html#NodeIteratorFactory-createTreeWalker 20 | 21 | During the development of this polyfill, I noticed a [defect](https://www.w3.org/Bugs/Public/show_bug.cgi?id=20445) in the WHATWG DOM specification ([patch 1](https://github.com/whatwg/dom/commit/ff9acf95c68efe5c6fbc718f814da31b4a891a6a), [patch 2](https://github.com/whatwg/dom/commit/4b60feb14bc91d1d0bcc463eb2b9488bf1071bad)). The finding is acknowledged at . 22 | 23 | ## Tests 24 | 25 | By default the tests run against the code in `src/`. Any native TreeWalker 26 | implementation is disabled as part of the test set up. 27 | 28 | * To test the native implementation, run `UNIT_USE_NATIVE=true npm test`. 29 | * To test the minified version, run `UNIT_USE_MIN=true npm test`. 30 | * Both, run `UNIT_USE_MIN=true UNIT_USE_NATIVE=true npm test`. 31 | * To debug tests in your browser (instead of Headless), use `npm run karma-debug`. Then press the "Debug" button in the browser window to start the tests. 32 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, es6 */ 2 | var grunt = require('grunt'); 3 | module.exports = function (config) { 4 | var options, suffix; 5 | suffix = process.env.UNIT_USE_NATIVE ? 'native' : 'polyfill'; 6 | if (process.env.UNIT_USE_MIN) { 7 | suffix += '-min'; 8 | } 9 | 10 | grunt.log.header('\n' + ('Unit tests (run: ' + suffix + ')').bold + '\n'); 11 | 12 | options = { 13 | // To debug, run `npm run karma-debug`. 14 | // Then press the "Debug" button in the browser window 15 | browsers: ['ChromeHeadless'], 16 | frameworks: ['qunit'], 17 | client: { 18 | clearContext: false, 19 | qunit: { 20 | showUI: true 21 | } 22 | }, 23 | files: [ 24 | 'test/qunit-disable-native.js', 25 | 'test/qunit-init.js', 26 | 'src/TreeWalker-polyfill.js', 27 | 'test/TreeWalker.test.js' 28 | ], 29 | autoWatch: false, 30 | singleRun: true, 31 | preprocessors: { 32 | 'src/*.js': ['coverage'] 33 | }, 34 | reporters: ['dots', 'coverage'], 35 | coverageReporter: { 36 | reporters: [ 37 | { type: 'text-summary' }, 38 | { type: 'html', dir: 'coverage/' + suffix }, 39 | { type: 'lcovonly', dir: 'coverage/' + suffix } 40 | ] 41 | } 42 | }; 43 | 44 | if (process.env.UNIT_USE_NATIVE) { 45 | options.files = options.files.filter(file => file !== 'test/qunit-disable-native.js'); 46 | options.coverageReporter.reporters = options.coverageReporter.reporters.filter(obj => { 47 | return obj.type === 'lcovonly'; 48 | }); 49 | } 50 | 51 | if (process.env.UNIT_USE_MIN) { 52 | options.reporters = options.reporters.filter(val => val !== 'coverage'); 53 | 54 | options.files = options.files.map(file => { 55 | return file === 'src/TreeWalker-polyfill.js' 56 | ? 'dist/TreeWalker-polyfill.min.js' 57 | : file; 58 | }); 59 | } 60 | 61 | config.set(options); 62 | }; 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dom-TreeWalker-polyfill", 3 | "version": "0.2.0", 4 | "description": "JavaScript implementation of W3 DOM4 TreeWalker interface.", 5 | "keywords": [ 6 | "dom", 7 | "domcore", 8 | "createTreeWalker", 9 | "TreeWalker", 10 | "html5", 11 | "polyfill" 12 | ], 13 | "homepage": "https://github.com/Krinkle/dom-TreeWalker-polyfill", 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/Krinkle/dom-TreeWalker-polyfillb.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/Krinkle/dom-TreeWalker-polyfill/issues" 20 | }, 21 | "author": "Timo Tijhof (https://github.com/Krinkle)", 22 | "license": "MIT", 23 | "scripts": { 24 | "build": "grunt uglify", 25 | "test": "eslint . && grunt uglify && rm -rf coverage/ && karma start", 26 | "karma-debug": "karma start --no-single-run --browsers Chrome --logLevel DEBUG", 27 | "ci": "npm run test && UNIT_USE_NATIVE=true karma start && UNIT_USE_MIN=true karma start" 28 | }, 29 | "eslintIgnore": [ 30 | "/coverage", 31 | "/dist" 32 | ], 33 | "devDependencies": { 34 | "eslint": "^4.3.0", 35 | "eslint-config-crockford": "1.0.0", 36 | "grunt": "^1.0.1", 37 | "grunt-contrib-uglify": "^3.0.1", 38 | "karma": "^1.7.0", 39 | "karma-chrome-launcher": "^2.2.0", 40 | "karma-cli": "^1.0.1", 41 | "karma-coverage": "^1.1.1", 42 | "karma-qunit": "1.2.1", 43 | "qunitjs": "2.4.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: ../.eslintrc.yml 2 | parserOptions: 3 | ecmaVersion: 3 4 | -------------------------------------------------------------------------------- /src/TreeWalker-polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JavaScript implementation of W3 DOM4 TreeWalker interface. 3 | * 4 | * See also: 5 | * - https://dom.spec.whatwg.org/#interface-treewalker 6 | * 7 | * Attributes like "read-only" and "private" are ignored in this implementation 8 | * due to ECMAScript 3 (as opposed to ES5) not supporting creation of such properties. 9 | * There are workarounds, but under "keep it simple" and "don't do stupid things" they 10 | * are ignored in this implementation. 11 | */ 12 | (function (win, doc) { 13 | var TreeWalker, NodeFilter, create, toString, is, mapChild, mapSibling, 14 | nodeFilter, traverseChildren, traverseSiblings, nextSkippingChildren; 15 | 16 | if (doc.createTreeWalker) { 17 | return; 18 | } 19 | 20 | // Cross-browser polyfill for these constants 21 | NodeFilter = { 22 | // Constants for acceptNode() 23 | FILTER_ACCEPT: 1, 24 | FILTER_REJECT: 2, 25 | FILTER_SKIP: 3, 26 | 27 | // Constants for whatToShow 28 | SHOW_ALL: 0xFFFFFFFF, 29 | SHOW_ELEMENT: 0x1, 30 | SHOW_ATTRIBUTE: 0x2, // historical 31 | SHOW_TEXT: 0x4, 32 | SHOW_CDATA_SECTION: 0x8, // historical 33 | SHOW_ENTITY_REFERENCE: 0x10, // historical 34 | SHOW_ENTITY: 0x20, // historical 35 | SHOW_PROCESSING_INSTRUCTION: 0x40, 36 | SHOW_COMMENT: 0x80, 37 | SHOW_DOCUMENT: 0x100, 38 | SHOW_DOCUMENT_TYPE: 0x200, 39 | SHOW_DOCUMENT_FRAGMENT: 0x400, 40 | SHOW_NOTATION: 0x800 // historical 41 | }; 42 | 43 | /* Local utilities */ 44 | 45 | create = Object.create || function (proto) { 46 | function Empty() {} 47 | Empty.prototype = proto; 48 | return new Empty(); 49 | }; 50 | 51 | mapChild = { 52 | first: 'firstChild', 53 | last: 'lastChild', 54 | next: 'firstChild', 55 | previous: 'lastChild' 56 | }; 57 | 58 | mapSibling = { 59 | next: 'nextSibling', 60 | previous: 'previousSibling' 61 | }; 62 | 63 | toString = mapChild.toString; 64 | 65 | is = function (x, type) { 66 | return toString.call(x).toLowerCase() === '[object ' + type.toLowerCase() + ']'; 67 | }; 68 | 69 | /* Private methods and helpers */ 70 | 71 | /** 72 | * See https://dom.spec.whatwg.org/#concept-node-filter 73 | * 74 | * @private 75 | * @method 76 | * @param {TreeWalker} tw 77 | * @param {Node} node 78 | */ 79 | nodeFilter = function (tw, node) { 80 | // Maps nodeType to whatToShow 81 | if (!(((1 << (node.nodeType - 1)) & tw.whatToShow))) { 82 | return NodeFilter.FILTER_SKIP; 83 | } 84 | 85 | if (tw.filter === null) { 86 | return NodeFilter.FILTER_ACCEPT; 87 | } 88 | 89 | return tw.filter.acceptNode(node); 90 | }; 91 | 92 | /** 93 | * See https://dom.spec.whatwg.org/#concept-traverse-children 94 | * 95 | * @private 96 | * @method 97 | * @param {TreeWalker} tw 98 | * @param {string} type One of 'first' or 'last'. 99 | * @return {Node|null} 100 | */ 101 | traverseChildren = function (tw, type) { 102 | var child, node, parent, result, sibling; 103 | node = tw.currentNode[ mapChild[ type ] ]; 104 | while (node !== null) { 105 | result = nodeFilter(tw, node); 106 | if (result === NodeFilter.FILTER_ACCEPT) { 107 | tw.currentNode = node; 108 | return node; 109 | } 110 | if (result === NodeFilter.FILTER_SKIP) { 111 | child = node[ mapChild[ type ] ]; 112 | if (child !== null) { 113 | node = child; 114 | continue; 115 | } 116 | } 117 | while (node !== null) { 118 | sibling = node[ mapChild[ type ] ]; 119 | if (sibling !== null) { 120 | node = sibling; 121 | break; 122 | } 123 | parent = node.parentNode; 124 | if (parent === null || parent === tw.root || parent === tw.currentNode) { 125 | return null; 126 | } else { 127 | node = parent; 128 | } 129 | } 130 | } 131 | return null; 132 | }; 133 | 134 | /** 135 | * See https://dom.spec.whatwg.org/#concept-traverse-siblings 136 | * 137 | * @private 138 | * @method 139 | * @param {TreeWalker} tw 140 | * @param {TreeWalker} type One of 'next' or 'previous'. 141 | * @return {Node|null} 142 | */ 143 | traverseSiblings = function (tw, type) { 144 | var node, result, sibling; 145 | node = tw.currentNode; 146 | if (node === tw.root) { 147 | return null; 148 | } 149 | while (true) { 150 | sibling = node[ mapSibling[ type ] ]; 151 | while (sibling !== null) { 152 | node = sibling; 153 | result = nodeFilter(tw, node); 154 | if (result === NodeFilter.FILTER_ACCEPT) { 155 | tw.currentNode = node; 156 | return node; 157 | } 158 | sibling = node[ mapChild[ type ] ]; 159 | if (result === NodeFilter.FILTER_REJECT) { 160 | sibling = node[ mapSibling[ type ] ]; 161 | } 162 | } 163 | node = node.parentNode; 164 | if (node === null || node === tw.root) { 165 | return null; 166 | } 167 | if (nodeFilter(tw, node) === NodeFilter.FILTER_ACCEPT) { 168 | return null; 169 | } 170 | } 171 | }; 172 | 173 | /** 174 | * Based on WebKit's NodeTraversal::nextSkippingChildren 175 | * https://trac.webkit.org/browser/trunk/Source/WebCore/dom/NodeTraversal.h?rev=137221#L103 176 | */ 177 | nextSkippingChildren = function (node, stayWithin) { 178 | if (node === stayWithin) { 179 | return null; 180 | } 181 | if (node.nextSibling !== null) { 182 | return node.nextSibling; 183 | } 184 | 185 | /** 186 | * Based on WebKit's NodeTraversal::nextAncestorSibling 187 | * https://trac.webkit.org/browser/trunk/Source/WebCore/dom/NodeTraversal.cpp?rev=137221#L43 188 | */ 189 | while (node.parentNode !== null) { 190 | node = node.parentNode; 191 | if (node === stayWithin) { 192 | return null; 193 | } 194 | if (node.nextSibling !== null) { 195 | return node.nextSibling; 196 | } 197 | } 198 | return null; 199 | }; 200 | 201 | /** 202 | * See https://dom.spec.whatwg.org/#interface-treewalker 203 | * 204 | * @constructor 205 | * @param {Node} root 206 | * @param {number} [whatToShow] 207 | * @param {Function} [filter] 208 | * @throws Error 209 | */ 210 | TreeWalker = function (root, whatToShow, filter) { 211 | var tw = this, active = false; 212 | 213 | if (!root || !root.nodeType) { 214 | throw new Error('DOMException: NOT_SUPPORTED_ERR'); 215 | } 216 | 217 | tw.root = root; 218 | tw.whatToShow = Number(whatToShow) || 0; 219 | 220 | tw.currentNode = root; 221 | 222 | if (!is(filter, 'function')) { 223 | tw.filter = null; 224 | } else { 225 | tw.filter = create(win.NodeFilter); 226 | 227 | /** 228 | * See https://dom.spec.whatwg.org/#dom-nodefilter-acceptnode 229 | * 230 | * @method 231 | * @member NodeFilter 232 | * @param {Node} node 233 | * @return {number} Constant NodeFilter.FILTER_ACCEPT, 234 | * NodeFilter.FILTER_REJECT or NodeFilter.FILTER_SKIP. 235 | */ 236 | tw.filter.acceptNode = function (node) { 237 | var result; 238 | if (active) { 239 | throw new Error('DOMException: INVALID_STATE_ERR'); 240 | } 241 | 242 | active = true; 243 | result = filter(node); 244 | active = false; 245 | 246 | return result; 247 | }; 248 | } 249 | }; 250 | 251 | TreeWalker.prototype = { 252 | 253 | constructor: TreeWalker, 254 | 255 | /** 256 | * See https://dom.spec.whatwg.org/#ddom-treewalker-parentnode 257 | * 258 | * @method 259 | * @return {Node|null} 260 | */ 261 | parentNode: function () { 262 | var node = this.currentNode; 263 | while (node !== null && node !== this.root) { 264 | node = node.parentNode; 265 | if (node !== null && nodeFilter(this, node) === NodeFilter.FILTER_ACCEPT) { 266 | this.currentNode = node; 267 | return node; 268 | } 269 | } 270 | return null; 271 | }, 272 | 273 | /** 274 | * See https://dom.spec.whatwg.org/#dom-treewalker-firstchild 275 | * 276 | * @method 277 | * @return {Node|null} 278 | */ 279 | firstChild: function () { 280 | return traverseChildren(this, 'first'); 281 | }, 282 | 283 | /** 284 | * See https://dom.spec.whatwg.org/#dom-treewalker-lastchild 285 | * 286 | * @method 287 | * @return {Node|null} 288 | */ 289 | lastChild: function () { 290 | return traverseChildren(this, 'last'); 291 | }, 292 | 293 | /** 294 | * See https://dom.spec.whatwg.org/#dom-treewalker-previoussibling 295 | * 296 | * @method 297 | * @return {Node|null} 298 | */ 299 | previousSibling: function () { 300 | return traverseSiblings(this, 'previous'); 301 | }, 302 | 303 | /** 304 | * See https://dom.spec.whatwg.org/#dom-treewalker-nextsibling 305 | * 306 | * @method 307 | * @return {Node|null} 308 | */ 309 | nextSibling: function () { 310 | return traverseSiblings(this, 'next'); 311 | }, 312 | 313 | /** 314 | * See https://dom.spec.whatwg.org/#dom-treewalker-previousnode 315 | * 316 | * @method 317 | * @return {Node|null} 318 | */ 319 | previousNode: function () { 320 | var node, result, sibling; 321 | node = this.currentNode; 322 | while (node !== this.root) { 323 | sibling = node.previousSibling; 324 | while (sibling !== null) { 325 | node = sibling; 326 | result = nodeFilter(this, node); 327 | while (result !== NodeFilter.FILTER_REJECT && node.lastChild !== null) { 328 | node = node.lastChild; 329 | result = nodeFilter(this, node); 330 | } 331 | if (result === NodeFilter.FILTER_ACCEPT) { 332 | this.currentNode = node; 333 | return node; 334 | } 335 | } 336 | if (node === this.root || node.parentNode === null) { 337 | return null; 338 | } 339 | node = node.parentNode; 340 | if (nodeFilter(this, node) === NodeFilter.FILTER_ACCEPT) { 341 | this.currentNode = node; 342 | return node; 343 | } 344 | } 345 | return null; 346 | }, 347 | 348 | /** 349 | * See https://dom.spec.whatwg.org/#dom-treewalker-nextnode 350 | * 351 | * @method 352 | * @return {Node|null} 353 | */ 354 | nextNode: function () { 355 | var node, result, following; 356 | node = this.currentNode; 357 | result = NodeFilter.FILTER_ACCEPT; 358 | 359 | while (true) { 360 | while (result !== NodeFilter.FILTER_REJECT && node.firstChild !== null) { 361 | node = node.firstChild; 362 | result = nodeFilter(this, node); 363 | if (result === NodeFilter.FILTER_ACCEPT) { 364 | this.currentNode = node; 365 | return node; 366 | } 367 | } 368 | following = nextSkippingChildren(node, this.root); 369 | if (following !== null) { 370 | node = following; 371 | } else { 372 | return null; 373 | } 374 | result = nodeFilter(this, node); 375 | if (result === NodeFilter.FILTER_ACCEPT) { 376 | this.currentNode = node; 377 | return node; 378 | } 379 | } 380 | } 381 | }; 382 | 383 | /** 384 | * See http://www.w3.org/TR/dom/#dom-document-createtreewalker 385 | * 386 | * @param {Node} root 387 | * @param {number} [whatToShow=NodeFilter.SHOW_ALL] 388 | * @param {Function|Object} [filter=null] 389 | * @return {TreeWalker} 390 | */ 391 | doc.createTreeWalker = function (root, whatToShow, filter) { 392 | whatToShow = whatToShow === undefined ? NodeFilter.SHOW_ALL : whatToShow; 393 | 394 | if (filter && is(filter.acceptNode, 'function')) { 395 | filter = filter.acceptNode; 396 | // Support Gecko-ism of filter being a function. 397 | // https://developer.mozilla.org/en-US/docs/DOM/document.createTreeWalker 398 | } else if (!is(filter, 'function')) { 399 | filter = null; 400 | } 401 | 402 | return new TreeWalker(root, whatToShow, filter); 403 | }; 404 | 405 | if (!win.NodeFilter) { 406 | win.NodeFilter = NodeFilter.constructor = NodeFilter.prototype = NodeFilter; 407 | } 408 | 409 | if (!win.TreeWalker) { 410 | win.TreeWalker = TreeWalker; 411 | } 412 | 413 | }(window, document)); 414 | -------------------------------------------------------------------------------- /test/TreeWalker.test.js: -------------------------------------------------------------------------------- 1 | /*global QUnit */ 2 | 3 | QUnit.module('NodeFilter'); 4 | 5 | QUnit.test('constants', function (assert) { 6 | var tw = document.createTreeWalker(document.body, 0xFFFFFFFF, function () {}); 7 | 8 | assert.isNodeFilter(tw.filter, 'filter property is instance of NodeFilter'); 9 | assert.validNodeFilterConstants(NodeFilter, 'Static members of native NodeFilter'); 10 | }); 11 | 12 | QUnit.module('TreeWalker', { 13 | beforeEach: function () { 14 | var fixture = document.getElementById('qunit-fixture'), 15 | div = document.createElement('div'); 16 | 17 | div.innerHTML = '' + 18 | '
' + 19 | 'Hello ' + 20 | 'World' + 21 | '
' + 22 | '

' + 23 | 'Foo, ' + 24 | 'bar' + 25 | '

'; 26 | fixture.appendChild(div); 27 | 28 | this.getFixture = function (html) { 29 | if (html) { 30 | div.innerHTML = html; 31 | } 32 | return div; 33 | }; 34 | } 35 | }); 36 | 37 | QUnit.test('constructor()', function (assert) { 38 | assert.equal(typeof window.TreeWalker, 'function', 'exposed on Window'); 39 | 40 | assert.throws(function () { 41 | return document.createTreeWalker(); 42 | }, 'Invalid arguments'); 43 | }); 44 | 45 | QUnit.test('constructor( Node )', function (assert) { 46 | var tw; 47 | tw = document.createTreeWalker(document.body); 48 | assert.equal(tw.root, document.body, 'root set'); 49 | assert.equal(tw.whatToShow, 0xFFFFFFFF, 'whatToShow default'); 50 | assert.equal(tw.filter, null, 'no filter'); 51 | assert.equal(tw.currentNode, document.body, 'currentNode set'); 52 | 53 | }); 54 | 55 | QUnit.test('constructor( Node, NodeFilter )', function (assert) { 56 | var tw; 57 | tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); 58 | assert.equal(tw.root, document.body, 'root set'); 59 | assert.equal(tw.whatToShow, NodeFilter.SHOW_TEXT, 'whatToShow set'); 60 | assert.equal(tw.filter, null, 'no filter'); 61 | assert.equal(tw.currentNode, document.body, 'currentNode set'); 62 | 63 | }); 64 | 65 | QUnit.test('constructor( Node, NodeFilter, null )', function (assert) { 66 | var tw; 67 | tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null); 68 | assert.equal(tw.root, document.body, 'root set'); 69 | assert.equal(tw.whatToShow, NodeFilter.SHOW_TEXT, 'whatToShow set'); 70 | assert.equal(tw.filter, null, 'no filter'); 71 | assert.equal(tw.currentNode, document.body, 'currentNode set'); 72 | }); 73 | 74 | QUnit.test('constructor( Node, NodeFilter, Function )', function (assert) { 75 | var tw; 76 | tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, function () {}); 77 | assert.equal(tw.root, document.body, 'root set'); 78 | assert.equal(tw.whatToShow, NodeFilter.SHOW_TEXT, 'whatToShow set'); 79 | assert.isNodeFilter(tw.filter, 'filter set'); 80 | assert.equal(tw.currentNode, document.body, 'currentNode set'); 81 | }); 82 | 83 | QUnit.test('constructor( Node, NodeFilter, Object )', function (assert) { 84 | var tw; 85 | tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { acceptNode: function () {} }); 86 | assert.equal(tw.root, document.body, 'root set'); 87 | assert.equal(tw.whatToShow, NodeFilter.SHOW_TEXT, 'whatToShow set'); 88 | assert.isNodeFilter(tw.filter, 'filter set'); 89 | assert.equal(tw.currentNode, document.body, 'currentNode set'); 90 | }); 91 | 92 | QUnit.test('acceptNode: NodeFilter.SHOW_ALL', function (assert) { 93 | var root, tw, expected, actual; 94 | root = this.getFixture(); 95 | tw = document.createTreeWalker(root, NodeFilter.SHOW_ALL); 96 | expected = [ 97 | root.firstChild, 98 | root.firstChild.firstChild, 99 | root.firstChild.lastChild, 100 | root.firstChild.lastChild.firstChild, 101 | root.lastChild, 102 | root.lastChild.firstChild, 103 | root.lastChild.lastChild, 104 | root.lastChild.lastChild.firstChild 105 | ]; 106 | actual = []; 107 | 108 | while (tw.nextNode() !== null) { 109 | actual.push(tw.currentNode); 110 | } 111 | 112 | assert.deepEqual(actual, expected); 113 | }); 114 | 115 | QUnit.test('acceptNode: NodeFilter.SHOW_ELEMENT', function (assert) { 116 | var root, tw, expected, actual; 117 | root = this.getFixture(); 118 | tw = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); 119 | expected = [ 120 | root.firstChild, 121 | root.firstChild.lastChild, 122 | root.lastChild, 123 | root.lastChild.lastChild 124 | ]; 125 | actual = []; 126 | 127 | while (tw.nextNode() !== null) { 128 | actual.push(tw.currentNode); 129 | } 130 | 131 | assert.deepEqual(actual, expected); 132 | }); 133 | 134 | QUnit.test('acceptNode: NodeFilter.SHOW_TEXT', function (assert) { 135 | var root, tw, expected, actual; 136 | root = this.getFixture(); 137 | tw = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); 138 | expected = [ 139 | root.firstChild.firstChild, 140 | root.firstChild.lastChild.firstChild, 141 | root.lastChild.firstChild, 142 | root.lastChild.lastChild.firstChild 143 | ]; 144 | actual = []; 145 | 146 | while (tw.nextNode() !== null) { 147 | actual.push(tw.currentNode); 148 | } 149 | 150 | assert.deepEqual(actual, expected); 151 | }); 152 | 153 | QUnit.test('acceptNode: NodeFilter.SHOW_ALL + Function', function (assert) { 154 | var root, tw, expected, actual; 155 | root = this.getFixture( 156 | '
' + 157 | '
' + 158 | '
' + 159 | '
' + 160 | '
' + 161 | '
' + 162 | '

' + 163 | '

' + 164 | 'bar' + 165 | '

' 166 | ); 167 | tw = document.createTreeWalker(root, NodeFilter.SHOW_ALL, { 168 | acceptNode: function (node) { 169 | return node.id ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; 170 | } 171 | }); 172 | expected = [ 173 | root.firstChild, 174 | root.firstChild.firstChild.firstChild, 175 | root.lastChild.previousSibling, 176 | root.lastChild.previousSibling.firstChild, 177 | root.lastChild 178 | ]; 179 | actual = []; 180 | 181 | while (tw.nextNode() !== null) { 182 | actual.push(tw.currentNode); 183 | } 184 | 185 | assert.deepEqual(actual, expected); 186 | }); 187 | 188 | QUnit.test('acceptNode: NodeFilter.SHOW_ELEMENT + Function + FILTER_REJECT', function (assert) { 189 | var root, tw, expected, actual; 190 | root = this.getFixture( 191 | '
' + 192 | '
' + 193 | '
' + 194 | '
' + 195 | '
' + 196 | '
' + 197 | '

' + 198 | '

' + 199 | 'bar' + 200 | '

' 201 | ); 202 | tw = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, { 203 | acceptNode: function (node) { 204 | return node.id ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; 205 | } 206 | }); 207 | 208 | // TreeWalker should ignore children of node that is given FILTER_REJECT 209 | // so root.firstChild.firstChild.firstChild (id="one-grantchild") is not expected. 210 | // and root.lastChild.previousSibling.firstChild (id="three-child") is not expected. 211 | expected = [ 212 | root.firstChild, 213 | root.firstChild.nextSibling, 214 | root.lastChild 215 | ]; 216 | actual = []; 217 | 218 | while (tw.nextNode() !== null) { 219 | actual.push(tw.currentNode); 220 | } 221 | 222 | assert.deepEqual(actual, expected); 223 | }); 224 | 225 | QUnit.test('acceptNode: NodeFilter.SHOW_TEXT + Function', function (assert) { 226 | var root, tw, expected, actual; 227 | root = this.getFixture(); 228 | tw = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { 229 | acceptNode: function (node) { 230 | var hasL = /l/.test(node.nodeValue); 231 | return hasL ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; 232 | } 233 | }); 234 | expected = [ 235 | root.firstChild.firstChild, 236 | root.firstChild.lastChild.firstChild 237 | ]; 238 | actual = []; 239 | 240 | while (tw.nextNode() !== null) { 241 | actual.push(tw.currentNode); 242 | } 243 | 244 | assert.deepEqual(actual, expected); 245 | }); 246 | 247 | QUnit.test('.parentNode()', function (assert) { 248 | var root, tw, ret, save; 249 | root = this.getFixture(); 250 | tw = document.createTreeWalker(root, NodeFilter.SHOW_ALL); 251 | 252 | ret = tw.parentNode(); 253 | assert.equal(ret, null, 'no parent from root'); 254 | assert.equal(tw.currentNode, root, 'Pointer not moved'); 255 | 256 | // Move pointer to <#text>bar 257 | tw.currentNode = root.lastChild.lastChild.lastChild; 258 | 259 | ret = tw.parentNode(); 260 | assert.equal(ret.id, 'bar', 'text node to parent'); 261 | assert.equal(tw.currentNode, ret, 'Pointer moved'); 262 | 263 | ret = tw.parentNode(); 264 | assert.equal(ret.id, 'foo', 'node to parent'); 265 | assert.equal(tw.currentNode, ret, 'Pointer moved'); 266 | 267 | ret = tw.parentNode(); 268 | assert.equal(ret, root, 'node to parent'); 269 | assert.equal(tw.currentNode, root, 'Pointer moved'); 270 | 271 | save = tw.currentNode; 272 | ret = tw.parentNode(); 273 | assert.equal(ret, null, 'no parent from root'); 274 | assert.equal(tw.currentNode, save, 'Pointer not moved'); 275 | }); 276 | 277 | QUnit.test('.firstChild()', function (assert) { 278 | var root, tw, ret, save; 279 | root = this.getFixture(); 280 | tw = document.createTreeWalker(root, NodeFilter.SHOW_ALL); 281 | 282 | ret = tw.firstChild(); 283 | assert.equal(ret.id, 'hello'); 284 | assert.equal(tw.currentNode, ret, 'Pointer moved'); 285 | 286 | ret = tw.firstChild(); 287 | assert.equal(ret.nodeValue, 'Hello '); 288 | assert.equal(tw.currentNode, ret, 'Pointer moved'); 289 | 290 | save = tw.currentNode; 291 | ret = tw.firstChild(); 292 | assert.equal(ret, null); 293 | assert.equal(tw.currentNode, save, 'Pointer not moved'); 294 | 295 | }); 296 | 297 | QUnit.test('.lastChild()', function (assert) { 298 | var root, tw, ret, save; 299 | root = this.getFixture(); 300 | tw = document.createTreeWalker(root, NodeFilter.SHOW_ALL); 301 | 302 | ret = tw.lastChild(); 303 | assert.equal(ret.id, 'foo'); 304 | assert.equal(tw.currentNode, ret, 'Pointer moved'); 305 | 306 | ret = tw.lastChild(); 307 | assert.equal(ret.id, 'bar'); 308 | assert.equal(tw.currentNode, ret, 'Pointer moved'); 309 | 310 | ret = tw.lastChild(); 311 | assert.equal(ret.nodeValue, 'bar'); 312 | assert.equal(tw.currentNode, ret, 'Pointer moved'); 313 | 314 | save = tw.currentNode; 315 | ret = tw.lastChild(); 316 | assert.equal(ret, null); 317 | assert.equal(tw.currentNode, save, 'Pointer not moved'); 318 | }); 319 | 320 | QUnit.test('.previousSibling()', function (assert) { 321 | var root, tw, ret, save; 322 | root = this.getFixture(); 323 | tw = document.createTreeWalker(root, NodeFilter.SHOW_ALL); 324 | 325 | ret = tw.previousSibling(); 326 | assert.equal(ret, null, 'no sibling from root'); 327 | assert.equal(tw.currentNode, root, 'Pointer not moved'); 328 | 329 | tw.currentNode = root.firstChild.firstChild; 330 | 331 | save = tw.currentNode; 332 | ret = tw.previousSibling(); 333 | assert.equal(ret, null, 'no previous from first child'); 334 | assert.equal(tw.currentNode, save, 'Pointer not moved'); 335 | 336 | // Move pointer to id="bar" 337 | tw.currentNode = root.lastChild.lastChild; 338 | 339 | ret = tw.previousSibling(); 340 | assert.equal(ret.nodeValue, 'Foo, ', 'child to previous child'); 341 | assert.equal(tw.currentNode, ret, 'Pointer moved'); 342 | }); 343 | 344 | QUnit.test('.nextSibling()', function (assert) { 345 | var root, tw, ret, save; 346 | root = this.getFixture(); 347 | tw = document.createTreeWalker(root, NodeFilter.SHOW_ALL); 348 | 349 | ret = tw.nextSibling(); 350 | assert.equal(ret, null, 'no sibling from root'); 351 | assert.equal(tw.currentNode, root, 'Pointer not moved'); 352 | 353 | tw.currentNode = root.lastChild.lastChild; 354 | 355 | save = tw.currentNode; 356 | ret = tw.nextSibling(); 357 | assert.equal(ret, null, 'no next from last child'); 358 | assert.equal(tw.currentNode, save, 'Pointer not moved'); 359 | 360 | // Move pointer to <#text>Hello 361 | tw.currentNode = root.firstChild.firstChild; 362 | 363 | ret = tw.nextSibling(); 364 | assert.equal(ret.id, 'world', 'child to next child'); 365 | assert.equal(tw.currentNode, ret, 'Pointer moved'); 366 | }); 367 | 368 | QUnit.test('.previousNode()', function (assert) { 369 | var root, tw, ret, save; 370 | root = this.getFixture(); 371 | tw = document.createTreeWalker(root, NodeFilter.SHOW_ALL); 372 | 373 | ret = tw.previousNode(); 374 | assert.equal(ret, null, 'no sibling from root'); 375 | assert.equal(tw.currentNode, root, 'Pointer not moved'); 376 | 377 | // Move pointer to <#text>Foo, 378 | tw.currentNode = root.lastChild.firstChild; 379 | 380 | save = tw.currentNode; 381 | ret = tw.previousNode(); 382 | assert.equal(ret.id, 'foo', 'first child to parent'); 383 | assert.equal(tw.currentNode, ret, 'Pointer moved'); 384 | 385 | ret = tw.previousNode(); 386 | assert.equal(ret.nodeValue, 'World', 'parent to previous sibling last child'); 387 | assert.equal(tw.currentNode, ret, 'Pointer moved'); 388 | }); 389 | 390 | QUnit.test('.nextNode()', function (assert) { 391 | var root, tw, ret, save; 392 | root = this.getFixture(); 393 | tw = document.createTreeWalker(root, NodeFilter.SHOW_ALL); 394 | 395 | ret = tw.nextNode(); 396 | assert.equal(ret.id, 'hello', 'root to first child'); 397 | assert.equal(tw.currentNode, ret, 'Pointer moved'); 398 | 399 | // Move pointer to id="world" 400 | tw.currentNode = root.firstChild.lastChild; 401 | 402 | save = tw.currentNode; 403 | ret = tw.nextNode(); 404 | assert.equal(ret.nodeValue, 'World', 'last child to inner first child'); 405 | assert.equal(tw.currentNode, ret, 'Pointer moved'); 406 | 407 | ret = tw.nextNode(); 408 | assert.equal(ret.id || ret.nodeValue, 'foo', 'last child to first child of parent with children'); 409 | assert.equal(tw.currentNode, ret, 'Pointer moved'); 410 | }); 411 | -------------------------------------------------------------------------------- /test/qunit-disable-native.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file include disables any native TreeWalker in the browser, 3 | * which ensures that (by default) the tests run against the polyfill. 4 | */ 5 | 6 | document.createTreeWalker = undefined; 7 | -------------------------------------------------------------------------------- /test/qunit-init.js: -------------------------------------------------------------------------------- 1 | /* eslint-env qunit */ 2 | 3 | /* Config */ 4 | 5 | QUnit.config.reorder = false; 6 | 7 | /* Assert extension */ 8 | 9 | /** 10 | * Should be able to simply use `assert.strictEqual(tw.filter.constructor, NodeFilter)` 11 | * but for some reason this causes bugs in PhantomJS (https://code.google.com/p/phantomjs/issues/detail?id=935) 12 | * 13 | * @param {Mixed} actual 14 | * @param {string} message 15 | */ 16 | QUnit.assert.isNodeFilter = function (actual, message) { 17 | var result = typeof actual === 'function' || ( 18 | typeof actual === 'object' && typeof actual.acceptNode === 'function' 19 | ); 20 | return this.pushResult({ 21 | result: result, 22 | actual: actual, 23 | expected: 'function', 24 | message: message 25 | }); 26 | }; 27 | 28 | QUnit.assert.validNodeFilterConstants = function (actual, message) { 29 | var actualMembers, expected, key; 30 | expected = { 31 | FILTER_ACCEPT: 1, 32 | FILTER_REJECT: 2, 33 | FILTER_SKIP: 3, 34 | 35 | SHOW_ALL: 0xFFFFFFFF, 36 | SHOW_ELEMENT: 0x1, 37 | SHOW_ATTRIBUTE: 0x2, 38 | SHOW_TEXT: 0x4, 39 | SHOW_CDATA_SECTION: 0x8, 40 | SHOW_ENTITY_REFERENCE: 0x10, 41 | SHOW_ENTITY: 0x20, 42 | SHOW_PROCESSING_INSTRUCTION: 0x40, 43 | SHOW_COMMENT: 0x80, 44 | SHOW_DOCUMENT: 0x100, 45 | SHOW_DOCUMENT_TYPE: 0x200, 46 | SHOW_DOCUMENT_FRAGMENT: 0x400, 47 | SHOW_NOTATION: 0x800 48 | }; 49 | 50 | actualMembers = {}; 51 | for (key in expected) { 52 | if (expected.hasOwnProperty(key)) { 53 | actualMembers[key] = actual[key]; 54 | } 55 | } 56 | 57 | this.pushResult({ 58 | result: QUnit.equiv(actualMembers, expected), 59 | actual: actualMembers, 60 | expected: expected, 61 | message: message 62 | }); 63 | }; 64 | --------------------------------------------------------------------------------