Repository: capricorn86/happy-dom (ts)

Issue:
document.createNodeIterator is not a function
It seems that the node iterator APIs are not supported by Happy-Dom

Pull request description:
#297@minor: Add method Document.createNodeIterator.
fix #297

- Create class `NodeIterator` (uses `TreeWalker` internally to forward "nextNode" and "previosNode").
- Add `NodeIterator` to global scope.
- Add method `Document.createNodeIterator`.
- Add plain function type to `INodeFilter`.
- Add unit tests for method and class (dumb copy&paste from TreeWalker tests).

Patch:
diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts
index 7bd88cf5..0ac4d28d 100644
--- a/packages/happy-dom/src/index.ts
+++ b/packages/happy-dom/src/index.ts
@@ -106,6 +106,7 @@ import Comment from './nodes/comment/Comment';
 import IComment from './nodes/comment/IComment';
 import DocumentType from './nodes/document-type/DocumentType';
 import IDocumentType from './nodes/document-type/IDocumentType';
+import NodeIterator from './tree-walker/NodeIterator';
 import TreeWalker from './tree-walker/TreeWalker';
 import CustomElementRegistry from './custom-element/CustomElementRegistry';
 import XMLParser from './xml-parser/XMLParser';
@@ -250,6 +251,7 @@ export {
 	IComment,
 	DocumentType,
 	IDocumentType,
+	NodeIterator,
 	TreeWalker,
 	CustomElementRegistry,
 	XMLParser,
diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts
index 4d0ae465..458d88ec 100644
--- a/packages/happy-dom/src/nodes/document/Document.ts
+++ b/packages/happy-dom/src/nodes/document/Document.ts
@@ -4,6 +4,7 @@ import Text from '../text/Text';
 import Comment from '../comment/Comment';
 import IWindow from '../../window/IWindow';
 import Node from '../node/Node';
+import NodeIterator from '../../tree-walker/NodeIterator';
 import TreeWalker from '../../tree-walker/TreeWalker';
 import DocumentFragment from '../document-fragment/DocumentFragment';
 import XMLParser from '../../xml-parser/XMLParser';
@@ -836,6 +837,21 @@ export default class Document extends Node implements IDocument {
 		return new DocumentFragment();
 	}
 
+	/**
+	 * Creates a node iterator.
+	 *
+	 * @param root Root.
+	 * @param [whatToShow] What to show.
+	 * @param [filter] Filter.
+	 */
+	public createNodeIterator(
+		root: INode,
+		whatToShow = -1,
+		filter: INodeFilter = null
+	): NodeIterator {
+		return new NodeIterator(root, whatToShow, filter);
+	}
+
 	/**
 	 * Creates a Tree Walker.
 	 *
diff --git a/packages/happy-dom/src/nodes/document/IDocument.ts b/packages/happy-dom/src/nodes/document/IDocument.ts
index b345a341..0b627a3f 100644
--- a/packages/happy-dom/src/nodes/document/IDocument.ts
+++ b/packages/happy-dom/src/nodes/document/IDocument.ts
@@ -1,6 +1,7 @@
 import IElement from '../element/IElement';
 import IHTMLElement from '../html-element/IHTMLElement';
 import IWindow from '../../window/IWindow';
+import NodeIterator from '../../tree-walker/NodeIterator';
 import TreeWalker from '../../tree-walker/TreeWalker';
 import Event from '../../event/Event';
 import DOMImplementation from '../../dom-implementation/DOMImplementation';
@@ -218,6 +219,15 @@ export default interface IDocument extends IParentNode {
 	 */
 	createDocumentFragment(): IDocumentFragment;
 
+	/**
+	 * Creates a node iterator.
+	 *
+	 * @param root Root.
+	 * @param [whatToShow] What to show.
+	 * @param [filter] Filter.
+	 */
+	createNodeIterator(root: INode, whatToShow: number, filter: INodeFilter): NodeIterator;
+
 	/**
 	 * Creates a Tree Walker.
 	 *
diff --git a/packages/happy-dom/src/tree-walker/INodeFilter.ts b/packages/happy-dom/src/tree-walker/INodeFilter.ts
index ddbb205b..343ae73a 100644
--- a/packages/happy-dom/src/tree-walker/INodeFilter.ts
+++ b/packages/happy-dom/src/tree-walker/INodeFilter.ts
@@ -1,5 +1,5 @@
 import INode from '../nodes/node/INode';
 
-export default interface INodeFilter {
-	acceptNode(node: INode): number;
-}
+type INodeFilter = ((node: INode) => number) | { acceptNode(node: INode): number };
+
+export default INodeFilter;
diff --git a/packages/happy-dom/src/tree-walker/NodeIterator.ts b/packages/happy-dom/src/tree-walker/NodeIterator.ts
new file mode 100644
index 00000000..a1b3eb48
--- /dev/null
+++ b/packages/happy-dom/src/tree-walker/NodeIterator.ts
@@ -0,0 +1,49 @@
+import INodeFilter from './INodeFilter';
+import TreeWalker from './TreeWalker';
+import INode from '../nodes/node/INode';
+
+/**
+ * The NodeIterator object represents the nodes of a document subtree and a position within them.
+ *
+ * Reference:
+ * https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator
+ */
+export default class NodeIterator {
+	public root: INode = null;
+	public whatToShow = -1;
+	public filter: INodeFilter = null;
+
+	private readonly _walker: TreeWalker;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param root Root.
+	 * @param [whatToShow] What to show.
+	 * @param [filter] Filter.
+	 */
+	constructor(root: INode, whatToShow = -1, filter: INodeFilter = null) {
+		this.root = root;
+		this.whatToShow = whatToShow;
+		this.filter = filter;
+		this._walker = new TreeWalker(root, whatToShow, filter);
+	}
+
+	/**
+	 * Moves the current Node to the next visible node in the document order.
+	 *
+	 * @returns Current node.
+	 */
+	public nextNode(): INode {
+		return this._walker.nextNode();
+	}
+
+	/**
+	 * Moves the current Node to the previous visible node in the document order, and returns the found node. It also moves the current node to this one. If no such node exists, or if it is before that the root node defined at the object construction, returns null and the current node is not changed.
+	 *
+	 * @returns Current node.
+	 */
+	public previousNode(): INode {
+		return this._walker.previousNode();
+	}
+}
diff --git a/packages/happy-dom/src/tree-walker/TreeWalker.ts b/packages/happy-dom/src/tree-walker/TreeWalker.ts
index 6a94b8ff..5d246c14 100644
--- a/packages/happy-dom/src/tree-walker/TreeWalker.ts
+++ b/packages/happy-dom/src/tree-walker/TreeWalker.ts
@@ -181,7 +181,11 @@ export default class TreeWalker {
 
 		if (mask && (this.whatToShow & mask) == 0) {
 			return NodeFilter.FILTER_SKIP;
-		} else if (this.filter) {
+		}
+		if (typeof this.filter === 'function') {
+			return this.filter(node);
+		}
+		if (this.filter) {
 			return this.filter.acceptNode(node);
 		}
 
diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts
index c0b220f4..c7f10993 100644
--- a/packages/happy-dom/src/window/IWindow.ts
+++ b/packages/happy-dom/src/window/IWindow.ts
@@ -33,6 +33,7 @@ import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement';
 import Image from '../nodes/html-image-element/Image';
 import DocumentFragment from '../nodes/document-fragment/DocumentFragment';
 import CharacterData from '../nodes/character-data/CharacterData';
+import NodeIterator from '../tree-walker/NodeIterator';
 import TreeWalker from '../tree-walker/TreeWalker';
 import Event from '../event/Event';
 import CustomEvent from '../event/events/CustomEvent';
@@ -171,6 +172,7 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal {
 	readonly CharacterData: typeof CharacterData;
 	readonly ProcessingInstruction: typeof ProcessingInstruction;
 	readonly NodeFilter: typeof NodeFilter;
+	readonly NodeIterator: typeof NodeIterator;
 	readonly TreeWalker: typeof TreeWalker;
 	readonly DOMParser: typeof DOMParser;
 	readonly MutationObserver: typeof MutationObserver;
diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts
index 51c27580..1d9d3913 100644
--- a/packages/happy-dom/src/window/Window.ts
+++ b/packages/happy-dom/src/window/Window.ts
@@ -36,6 +36,7 @@ import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement';
 import { default as ImageImplementation } from '../nodes/html-image-element/Image';
 import DocumentFragment from '../nodes/document-fragment/DocumentFragment';
 import CharacterData from '../nodes/character-data/CharacterData';
+import NodeIterator from '../tree-walker/NodeIterator';
 import TreeWalker from '../tree-walker/TreeWalker';
 import Event from '../event/Event';
 import CustomEvent from '../event/events/CustomEvent';
@@ -214,6 +215,7 @@ export default class Window extends EventTarget implements IWindow {
 	public readonly DocumentFragment = DocumentFragment;
 	public readonly CharacterData = CharacterData;
 	public readonly NodeFilter = NodeFilter;
+	public readonly NodeIterator = NodeIterator;
 	public readonly TreeWalker = TreeWalker;
 	public readonly MutationObserver = MutationObserver;
 	public readonly Document = Document;


Test patch:
diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts
index aa261f34..6d1049b9 100644
--- a/packages/happy-dom/test/nodes/document/Document.test.ts
+++ b/packages/happy-dom/test/nodes/document/Document.test.ts
@@ -5,6 +5,7 @@ import HTMLElement from '../../../src/nodes/html-element/HTMLElement';
 import Text from '../../../src/nodes/text/Text';
 import Comment from '../../../src/nodes/comment/Comment';
 import DocumentFragment from '../../../src/nodes/document-fragment/DocumentFragment';
+import NodeIterator from '../../../src/tree-walker/NodeIterator';
 import TreeWalker from '../../../src/tree-walker/TreeWalker';
 import Node from '../../../src/nodes/node/Node';
 import IDocument from '../../../src/nodes/document/IDocument';
@@ -976,8 +977,28 @@ describe('Document', () => {
 		});
 	});
 
+	describe('createNodeIterator()', () => {
+		it('Creates a node iterator.', () => {
+			const root = document.createElement('div');
+			const whatToShow = 1;
+			const filter = {
+				acceptNode(node) {
+					if (node === Node.ELEMENT_NODE) {
+						return NodeFilter.FILTER_ACCEPT;
+					}
+					return NodeFilter.FILTER_REJECT;
+				}
+			};
+			const nodeIterator = document.createNodeIterator(root, whatToShow, filter);
+			expect(nodeIterator.root).toBe(root);
+			expect(nodeIterator.whatToShow).toBe(whatToShow);
+			expect(nodeIterator.filter).toBe(filter);
+			expect(nodeIterator).toBeInstanceOf(NodeIterator);
+		});
+	});
+
 	describe('createTreeWalker()', () => {
-		it('Creates a document fragment.', () => {
+		it('Creates a tree walker.', () => {
 			const root = document.createElement('div');
 			const whatToShow = 1;
 			const filter = {
@@ -989,10 +1010,10 @@ describe('Document', () => {
 				}
 			};
 			const treeWalker = document.createTreeWalker(root, whatToShow, filter);
-			expect(treeWalker.root === root).toBe(true);
+			expect(treeWalker.root).toBe(root);
 			expect(treeWalker.whatToShow).toBe(whatToShow);
 			expect(treeWalker.filter).toBe(filter);
-			expect(treeWalker instanceof TreeWalker).toBe(true);
+			expect(treeWalker).toBeInstanceOf(TreeWalker);
 		});
 	});
 
diff --git a/packages/happy-dom/test/tree-walker/NodeIterator.test.ts b/packages/happy-dom/test/tree-walker/NodeIterator.test.ts
new file mode 100644
index 00000000..a4834461
--- /dev/null
+++ b/packages/happy-dom/test/tree-walker/NodeIterator.test.ts
@@ -0,0 +1,181 @@
+import Window from '../../src/window/Window';
+import NodeFilter from '../../src/tree-walker/NodeFilter';
+import Element from '../../src/nodes/element/Element';
+import Comment from '../../src/nodes/comment/Comment';
+import Node from '../../src/nodes/node/Node';
+import TreeWalkerHTML from './data/TreeWalkerHTML';
+
+const NODE_TO_STRING = (node: Node): string => {
+	if (node instanceof Element) {
+		return node.outerHTML;
+	} else if (node instanceof Comment) {
+		return '<!--' + node.textContent + '-->';
+	} else {
+		return node['textContent'];
+	}
+};
+
+describe('NodeIterator', () => {
+	let window;
+	let document;
+
+	beforeEach(() => {
+		window = new Window();
+		document = window.document;
+		document.write(TreeWalkerHTML);
+	});
+
+	describe('nextNode()', () => {
+		it('Walks into each node in the DOM tree.', () => {
+			const nodeIterator = document.createNodeIterator(document.body);
+			const html = [];
+			let currentNode;
+
+			while ((currentNode = nodeIterator.nextNode())) {
+				html.push(NODE_TO_STRING(currentNode));
+			}
+
+			expect(html).toEqual([
+				'\n\t\t\t',
+				'<div class="class1 class2" id="id">\n\t\t\t\t<!-- Comment 1 !-->\n\t\t\t\t<b>Bold</b>\n\t\t\t\t<!-- Comment 2 !-->\n\t\t\t\t<span>Span</span>\n\t\t\t</div>',
+				'\n\t\t\t\t',
+				'<!-- Comment 1 !-->',
+				'\n\t\t\t\t',
+				'<b>Bold</b>',
+				'Bold',
+				'\n\t\t\t\t',
+				'<!-- Comment 2 !-->',
+				'\n\t\t\t\t',
+				'<span>Span</span>',
+				'Span',
+				'\n\t\t\t',
+				'\n\t\t\t',
+				'<article class="class1 class2" id="id">\n\t\t\t\t<!-- Comment 1 !-->\n\t\t\t\t<b>Bold</b>\n\t\t\t\t<!-- Comment 2 !-->\n\t\t\t</article>',
+				'\n\t\t\t\t',
+				'<!-- Comment 1 !-->',
+				'\n\t\t\t\t',
+				'<b>Bold</b>',
+				'Bold',
+				'\n\t\t\t\t',
+				'<!-- Comment 2 !-->',
+				'\n\t\t\t',
+				'\n\t\t'
+			]);
+		});
+
+		it('Walks into each HTMLElement in the DOM tree when whatToShow is set to NodeFilter.SHOW_ELEMENT.', () => {
+			const nodeIterator = document.createNodeIterator(document.body, NodeFilter.SHOW_ELEMENT);
+			const html = [];
+			let currentNode;
+
+			while ((currentNode = nodeIterator.nextNode())) {
+				html.push(currentNode.outerHTML);
+			}
+
+			expect(html).toEqual([
+				'<div class="class1 class2" id="id">\n\t\t\t\t<!-- Comment 1 !-->\n\t\t\t\t<b>Bold</b>\n\t\t\t\t<!-- Comment 2 !-->\n\t\t\t\t<span>Span</span>\n\t\t\t</div>',
+				'<b>Bold</b>',
+				'<span>Span</span>',
+				'<article class="class1 class2" id="id">\n\t\t\t\t<!-- Comment 1 !-->\n\t\t\t\t<b>Bold</b>\n\t\t\t\t<!-- Comment 2 !-->\n\t\t\t</article>',
+				'<b>Bold</b>'
+			]);
+		});
+
+		it('Walks into each HTMLElement and Comment in the DOM tree when whatToShow is set to NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_COMMENT.', () => {
+			const nodeIterator = document.createNodeIterator(
+				document.body,
+				NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_COMMENT
+			);
+			const html = [];
+			let currentNode;
+
+			while ((currentNode = nodeIterator.nextNode())) {
+				html.push(NODE_TO_STRING(currentNode));
+			}
+
+			expect(html).toEqual([
+				'<div class="class1 class2" id="id">\n\t\t\t\t<!-- Comment 1 !-->\n\t\t\t\t<b>Bold</b>\n\t\t\t\t<!-- Comment 2 !-->\n\t\t\t\t<span>Span</span>\n\t\t\t</div>',
+				'<!-- Comment 1 !-->',
+				'<b>Bold</b>',
+				'<!-- Comment 2 !-->',
+				'<span>Span</span>',
+				'<article class="class1 class2" id="id">\n\t\t\t\t<!-- Comment 1 !-->\n\t\t\t\t<b>Bold</b>\n\t\t\t\t<!-- Comment 2 !-->\n\t\t\t</article>',
+				'<!-- Comment 1 !-->',
+				'<b>Bold</b>',
+				'<!-- Comment 2 !-->'
+			]);
+		});
+
+		it('Walks into each HTMLElement in the DOM tree when whatToShow is set to NodeFilter.SHOW_ALL and provided filter function returns NodeFilter.FILTER_SKIP if not an HTMLElement and NodeFilter.FILTER_ACCEPT if it is.', () => {
+			const nodeIterator = document.createNodeIterator(document.body, NodeFilter.SHOW_ALL, {
+				acceptNode: (node: Node) =>
+					node.nodeType === Node.ELEMENT_NODE ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
+			});
+			const html = [];
+			let currentNode;
+
+			while ((currentNode = nodeIterator.nextNode())) {
+				html.push(NODE_TO_STRING(currentNode));
+			}
+
+			expect(html).toEqual([
+				'<div class="class1 class2" id="id">\n\t\t\t\t<!-- Comment 1 !-->\n\t\t\t\t<b>Bold</b>\n\t\t\t\t<!-- Comment 2 !-->\n\t\t\t\t<span>Span</span>\n\t\t\t</div>',
+				'<b>Bold</b>',
+				'<span>Span</span>',
+				'<article class="class1 class2" id="id">\n\t\t\t\t<!-- Comment 1 !-->\n\t\t\t\t<b>Bold</b>\n\t\t\t\t<!-- Comment 2 !-->\n\t\t\t</article>',
+				'<b>Bold</b>'
+			]);
+		});
+
+		it('Rejects the two first nodes when provided filter function returns NodeFilter.FILTER_REJECT on the two first nodes.', () => {
+			let rejected = 0;
+			const NodeIterator = document.createNodeIterator(document.body, NodeFilter.SHOW_ALL, {
+				acceptNode: () => {
+					if (rejected < 2) {
+						rejected++;
+						return NodeFilter.FILTER_REJECT;
+					}
+					return NodeFilter.FILTER_ACCEPT;
+				}
+			});
+			const html = [];
+			let currentNode;
+
+			while ((currentNode = NodeIterator.nextNode())) {
+				html.push(NODE_TO_STRING(currentNode));
+			}
+
+			expect(html).toEqual([
+				'\n\t\t\t',
+				'<article class="class1 class2" id="id">\n\t\t\t\t<!-- Comment 1 !-->\n\t\t\t\t<b>Bold</b>\n\t\t\t\t<!-- Comment 2 !-->\n\t\t\t</article>',
+				'\n\t\t\t\t',
+				'<!-- Comment 1 !-->',
+				'\n\t\t\t\t',
+				'<b>Bold</b>',
+				'Bold',
+				'\n\t\t\t\t',
+				'<!-- Comment 2 !-->',
+				'\n\t\t\t',
+				'\n\t\t'
+			]);
+		});
+	});
+
+	describe('previousNode()', () => {
+		it('Returns the previous node when executed after a nextNode() call.', () => {
+			const NodeIterator = document.createNodeIterator(document.body);
+			let expectedPreviousNode = null;
+			let previousNode = null;
+			let currentNode = null;
+
+			while ((currentNode = NodeIterator.nextNode())) {
+				if (previousNode) {
+					previousNode = NodeIterator.previousNode();
+					expect(previousNode === expectedPreviousNode).toBe(true);
+					NodeIterator.nextNode();
+				}
+				expectedPreviousNode = currentNode;
+			}
+		});
+	});
+});