/* Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to you under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {expect} from 'chai'; import {describe, it} from 'mocha'; import {ArrayCollector, DomQuery, DomQueryCollector, Lang, LazyStream, Stream} from "../../main/typescript"; import {ElementAttribute, Style} from "../../main/typescript/DomQuery"; import {from} from "rxjs"; const trim = Lang.trim; import {tobagoSheetWithHeader} from "./markups/tobago-with-header"; import {tobagoSheetWithoutHeader} from "./markups/tobago-without-header"; const jsdom = require("jsdom"); const {JSDOM} = jsdom; (global as any).window = {} let dom = null; describe('DOMQuery tests', function () { beforeEach(function () { dom = new JSDOM(` Title
`, { contentType: "text/html", runScripts: "dangerously", resources: "usable", url: `file://${__dirname}/index.html` }); let window = dom.window; (global as any).dom = dom; (global as any).window = window; (global as any).body = window.document.body; (global as any).document = window.document; }); this.afterEach(function () { }); it('basic init', function () { let probe1 = new DomQuery(window.document.body); let probe2 = DomQuery.querySelectorAll("div"); let probe3 = new DomQuery(probe1, probe2); let probe4 = new DomQuery(window.document.body, probe3); expect(probe1.length).to.be.eq(1); expect(probe2.length == 4).to.be.true; expect(probe3.length == 5).to.be.true; //still under discussion (we might index to avoid doubles) expect(probe4.length == 6).to.be.true; }); it('proper iterator api and rxjs mapping', function () { let probe1 = new DomQuery(window.document.body); let probe2 = DomQuery.querySelectorAll("div"); let o1 = from(Stream.ofDataSource(probe1)); let o2 = from(Stream.ofDataSource(probe2)); let cnt1 = 0; let isDQuery = false; let cnt2 = 0; o1.subscribe((item: any) => { cnt1++; }); o2.subscribe((item: any) => { cnt2++; isDQuery = (item.length == 1) && (item instanceof DomQuery) }) expect(probe1.length).to.be.eq(1); expect(probe2.length == 4).to.be.true; expect(isDQuery).to.be.true; }); it('proper iterator api and rxjs mapping with observable', function () { let probe1 = new DomQuery(window.document.body); let probe2 = DomQuery.querySelectorAll("div"); let o1 = from(Stream.ofDataSource(probe1)); let o2 = from(Stream.ofDataSource(probe2)); let cnt1 = 0; let isDQuery = false; let cnt2 = 0; o1.subscribe((item: any) => { cnt1++; }); o2.subscribe((item: any) => { cnt2++; isDQuery = (item.length == 1) && (item instanceof DomQuery) }) expect(probe1.length).to.be.eq(1); expect(probe2.length == 4).to.be.true; expect(isDQuery).to.be.true; }); it('domquery ops test filter', function () { let probe2 = DomQuery.querySelectorAll("div"); probe2 = probe2.filter((item: DomQuery) => item.id.match((id) => id != "id_1")); expect(probe2.length == 3); }); it('global eval test', function () { let probe2 = DomQuery.querySelectorAll("div"); probe2 = probe2.filter((item: DomQuery) => item.id.match((id) => id != "id_1")); expect(probe2.length == 3); }); it('must detach', function () { let probe2 = DomQuery.querySelectorAll("div#id_1"); probe2.detach(); expect(DomQuery.querySelectorAll("div#id_1").isPresent()).to.be.false; probe2.appendTo(DomQuery.querySelectorAll("body")); expect(DomQuery.querySelectorAll("div#id_1").isPresent()).to.be.true; }); it('domquery ops test2 each', () => { let probe2 = DomQuery.querySelectorAll("div#id_1"); DomQuery.globalEval("document.getElementById('id_1').innerHTML = 'hello'"); expect(probe2.html().value).to.eq("hello"); expect(DomQuery.byId(document.head).innerHTML.indexOf("document.getElementById('id_1').innerHTML = 'hello'")).to.eq(-1); DomQuery.globalEval("document.getElementById('id_1').innerHTML = 'hello2'", "nonci"); expect(probe2.html().value).to.eq("hello2"); }); it('domquery ops test2 with sticky eval code', () => { let probe2 = DomQuery.querySelectorAll("div#id_1"); DomQuery.globalEvalSticky("document.getElementById('id_1').innerHTML = 'hello'"); expect(probe2.html().value).to.eq("hello"); expect(DomQuery.byId(document.head).innerHTML.indexOf("document.getElementById('id_1').innerHTML = 'hello'")).not.to.eq(-1); DomQuery.globalEvalSticky("document.getElementById('id_1').innerHTML = 'hello2'", "nonci"); expect(probe2.html().value).to.eq("hello2"); expect(DomQuery.byId(document.head).innerHTML.indexOf("document.getElementById('id_1').innerHTML = 'hello2'")).not.to.eq(-1); }); it('domquery ops test2 eachNode', function () { let probe2 = DomQuery.querySelectorAll("div"); let noIter = 0; probe2 = probe2.each((item, cnt) => { expect(item instanceof DomQuery).to.be.true; expect(noIter == cnt).to.be.true; noIter++; }); expect(noIter == 4).to.be.true; }); it('domquery ops test2 byId', function () { let probe2 = DomQuery.byId("id_1"); expect(probe2.length == 1).to.be.true; probe2 = DomQuery.byTagName("div"); expect(probe2.length == 4).to.be.true; }); it('outerhtml and eval tests', function () { let probe1 = new DomQuery(window.document.body); probe1.querySelectorAll("#id_1").outerHTML(`
`, true, true); expect(window.document.body.innerHTML.indexOf("hello world") != -1).to.be.true; expect(window.document.head.innerHTML.indexOf("hello world") == -1).to.be.true; expect(window.document.body.innerHTML.indexOf("id_1") == -1).to.be.true; expect(window.document.body.innerHTML.indexOf("blarg") != -1).to.be.true; }); it('outerHTML must not reset the caret of a focused input outside the replaced subtree', function () { // regression: updating an unrelated component reset the caret of a different, // still focused input field to position 0 const doc = window.document; const input = doc.createElement("input"); input.id = "focusedInput"; input.value = "123"; doc.body.appendChild(input); input.focus(); input.setSelectionRange(1, 1); new DomQuery(doc.getElementById("id_2")).outerHTML(`
updated
`); expect(doc.activeElement?.id).to.eq("focusedInput"); expect((doc.getElementById("focusedInput") as HTMLInputElement).selectionStart).to.eq(1); }); it('outerHTML restores the caret when the focused input is part of the replaced subtree', function () { const doc = window.document; const container = doc.getElementById("id_1") as HTMLElement; container.innerHTML = ``; const inner = doc.getElementById("inner") as HTMLInputElement; inner.focus(); inner.setSelectionRange(2, 2); new DomQuery(container).outerHTML(`
`); const restored = doc.getElementById("inner") as HTMLInputElement; expect(restored.selectionStart).to.eq(2); }); it('tobago scenario: typing digits stays in order across partial updates', function () { // Reproduces the Tobago " renders only " case: // every keystroke triggers an ajax request that re-renders ONLY the output, // not the focused input. The caret of the input must survive each update so the // next typed digit lands behind the previous one ("12") and not in front ("21"). const doc = window.document; const input = doc.createElement("input"); input.id = "page:inputAjax::field"; doc.body.appendChild(input); input.focus(); input.setSelectionRange(0, 0); // simulates the browser inserting a character at the current caret position const typeChar = (el: HTMLInputElement, ch: string) => { const pos = el.selectionStart ?? el.value.length; el.value = el.value.slice(0, pos) + ch + el.value.slice(pos); el.setSelectionRange(pos + 1, pos + 1); }; // simulates the partial-response update that re-renders only the output component const renderOutput = () => { new DomQuery(doc.getElementById("page:outputAjax") as HTMLElement) .outerHTML(`${input.value}`); }; const output = doc.createElement("span"); output.id = "page:outputAjax"; doc.body.appendChild(output); typeChar(input, "1"); renderOutput(); // caret must still sit behind the "1", the input is untouched by the output update expect(doc.activeElement?.id).to.eq("page:inputAjax::field"); expect(input.selectionStart).to.eq(1); typeChar(input, "2"); renderOutput(); expect(input.selectionStart).to.eq(2); typeChar(input, "3"); renderOutput(); expect(input.value).to.eq("123"); expect(input.selectionStart).to.eq(3); expect((doc.getElementById("page:outputAjax") as HTMLElement).innerHTML).to.eq("123"); }); it('attr test and eval tests', function () { let probe1 = new DomQuery(document); probe1.querySelectorAll("div#id_2").attr("style").value = "border=1;"; let blarg = probe1.querySelectorAll("div#id_2").attr("booga").value; let style = probe1.querySelectorAll("div#id_2").attr("style").value; let nonexistent = probe1.querySelectorAll("div#id_2").attr("buhaha").value; expect(blarg).to.be.eq("blarg"); expect(style).to.be.eq("border=1;"); expect(nonexistent).to.be.eq(null); }); it('style must work ', function () { let probe1 = new DomQuery(document); let probe = probe1.querySelectorAll("div#id_2"); probe.style("border").value = "10px solid red"; probe.style("color").value = "blue"; let styleNodeLevel = (probe.getAsElem(0).value as HTMLElement).style['color']; expect(probe.style("border").value).to.eq("10px solid red") expect(probe.style("color").value).to.eq("blue"); expect(styleNodeLevel).to.eq('blue'); }); it('Style.fromNullable must return a Style instance not an ElementAttribute', function () { let probe = new DomQuery(document).querySelectorAll("div#id_2"); const styleInstance = Style.fromNullable(probe, "color"); expect(styleInstance instanceof Style).to.be.true; expect(styleInstance instanceof ElementAttribute).to.be.false; }); it('Style.fromNullable must write via element.style not setAttribute', function () { let probe = new DomQuery(document).querySelectorAll("div#id_2"); const styleInstance = Style.fromNullable(probe, "color"); (styleInstance as Style).value = "red"; const elem = probe.getAsElem(0).value as HTMLElement; // written via element.style — visible on style object expect(elem.style.color).to.eq("red"); // NOT written as an attribute — getAttribute("color") should be null expect(elem.getAttribute("color")).to.be.null; }); it('Style getClass must return Style not ElementAttribute', function () { let probe = new DomQuery(document).querySelectorAll("div#id_2"); const styleInstance = new Style(probe, "color"); expect((styleInstance as any).getClass()).to.eq(Style); expect((styleInstance as any).getClass()).to.not.eq(ElementAttribute); }); it('must perform addClass and hasClass correctly', function () { let probe1 = new DomQuery(document); let element = probe1.querySelectorAll("div#id_2"); element.addClass("booga").addClass("Booga2"); let classdef = element.attr("class").value; expect(classdef).to.eq("blarg2 booga Booga2"); element.removeClass("booga2") expect(element.hasClass("booga2")).to.be.false; expect(element.hasClass("booga")).to.be.true; }); it('must perform addClass and hasClass correctly 2', function () { let probe1 = new DomQuery(document); let element = probe1.querySelectorAll(".blarg2"); element.addClass("booga").addClass("Booga2"); let classdef = element.attr("class").value; expect(classdef).to.eq("blarg2 booga Booga2"); element.removeClass("booga2") expect(element.hasClass("booga2")).to.be.false; expect(element.hasClass("booga")).to.be.true; expect(element.hasClass("blarg2")).to.be.true; }); it('must perform addClass and hasClass correctly 2', function () { let probe1 = new DomQuery(document); let element = probe1.querySelectorAll(".blarg2"); element.addClass("booga").addClass("Booga2"); expect(probe1.querySelectorAll(".Booga2").length).eq(2); }); it('must perform insert before and insert after correctly', function () { let probe1 = new DomQuery(document).querySelectorAll("#id_2"); let insert = DomQuery.fromMarkup("
") let insert2 = DomQuery.fromMarkup("
") probe1.insertBefore(insert); probe1.insertAfter(insert2); expect(DomQuery.querySelectorAll("#insertedBefore").isPresent()).to.be.true; expect(DomQuery.querySelectorAll("#insertedBefore2").isPresent()).to.be.true; expect(DomQuery.querySelectorAll("#id_2").isPresent()).to.be.true; expect(DomQuery.querySelectorAll("#insertedAfter").isPresent()).to.be.true; expect(DomQuery.querySelectorAll("#insertedAfter2").isPresent()).to.be.true; }); it('do not create new tag on hello`); expect(result.tagName.value).to.eq("DIV"); expect(result.id.value).to.eq("testDiv"); }); it('fromMarkup: must parse full html document (doctype branch)', function () { const result = DomQuery.fromMarkup(`
content
`); expect(result.tagName.value).to.eq("HTML"); }); it('fromMarkup: must parse markup starting with

text

`); expect(result.tagName.value).to.eq("HTML"); }); it('fromMarkup: must parse markup starting with test`); expect(result.tagName.value).to.eq("HTML"); }); it('fromMarkup: must parse markup starting with

text

`); expect(result.tagName.value).to.eq("HTML"); }); it('fromMarkup: must parse thead markup', function () { const result = DomQuery.fromMarkup(`header`); expect(result.tagName.value).to.eq("THEAD"); expect(result.querySelectorAll("th").length).to.eq(1); }); it('fromMarkup: must parse tbody markup', function () { const result = DomQuery.fromMarkup(`data`); expect(result.tagName.value).to.eq("TBODY"); expect(result.querySelectorAll("td").length).to.eq(1); }); it('fromMarkup: must parse tfoot markup', function () { const result = DomQuery.fromMarkup(`foot data`); expect(result.tagName.value).to.eq("TFOOT"); expect(result.querySelectorAll("td").length).to.eq(1); }); it('fromMarkup: must parse tr markup', function () { const result = DomQuery.fromMarkup(`cell`); expect(result.tagName.value).to.eq("TR"); expect(result.querySelectorAll("td").length).to.eq(1); }); it('fromMarkup: must parse td markup', function () { const result = DomQuery.fromMarkup(`cell content`); expect(result.tagName.value).to.eq("TD"); }); it('fromMarkup: must parse th markup', function () { const result = DomQuery.fromMarkup(`header cell`); expect(result.tagName.value).to.eq("TH"); }); it('fromMarkup: must parse th markup with attributes', function () { const result = DomQuery.fromMarkup(`header cell`); expect(result.tagName.value).to.eq("TH"); expect(result.attr("colspan").value).to.eq("2"); }); it('fromMarkup: must handle tags with attributes (startsWithTag attribute variant)', function () { const result = DomQuery.fromMarkup(`header`); expect(result.tagName.value).to.eq("THEAD"); }); it('fromMarkup: must parse multiple sibling elements', function () { const result = DomQuery.fromMarkup(`
`); expect(result.length).to.eq(2); }); it('do not falsely assume standard tag', function () { const fromMarkup1 = DomQuery.fromMarkup(` booga `); const fromMarkup2 = DomQuery.fromMarkup(` booga `); expect(fromMarkup1.tagName.value === "HTML").to.be.false; expect(fromMarkup1.tagName.value === "HTML").to.be.false; expect(fromMarkup1.tagName.value === "HEAD").to.be.false; expect(fromMarkup2.tagName.value === "BODY").to.be.false; }); it('it must stream', function () { let probe1 = new DomQuery(document).querySelectorAll("div"); let coll: Array = Stream.ofDomQuery(probe1).collect(new ArrayCollector()); expect(coll.length == 4).to.be.true; coll = LazyStream.ofDomQuery(probe1).collect(new ArrayCollector()); expect(coll.length == 4).to.be.true; }); it('it must stream - DQ API (dynamically added)', function () { let probe1 = new DomQuery(document).querySelectorAll("div"); let coll: Array = probe1.stream.collect(new ArrayCollector()); expect(coll.length == 4).to.be.true; coll = probe1.lazyStream.collect(new ArrayCollector()); expect(coll.length == 4).to.be.true; }); it('it must stream to a domquery', function () { let probe1 = new DomQuery(document).querySelectorAll("div"); let coll: DomQuery = Stream.ofDataSource(probe1).collect(new DomQueryCollector()); expect(coll.length == 4).to.be.true; probe1.reset(); coll = LazyStream.ofStreamDataSource(probe1).collect(new DomQueryCollector()); expect(coll.length == 4).to.be.true; }); it('it must have parents', function () { let probe1 = new DomQuery(document).querySelectorAll("div"); let coll: Array = Stream.ofDataSource(probe1.parentsWhileMatch("body")).collect(new ArrayCollector()); expect(coll.length == 1).to.be.true; }); it("must have a working insertBefore and insertAfter", function () { let probe1 = new DomQuery(document).byId("id_2"); probe1.insertBefore(DomQuery.fromMarkup(`
`)); probe1.insertAfter(DomQuery.fromMarkup(`
`)); expect(DomQuery.querySelectorAll("div").length).to.eq(8); DomQuery.querySelectorAll("body").innerHTML = trim(DomQuery.querySelectorAll("body").innerHTML.replace(/>\s*<")); expect(DomQuery.querySelectorAll("body").childNodes.length).to.eq(8); let innerHtml = DomQuery.querySelectorAll("body").innerHTML; expect(innerHtml.indexOf("id_x_0") < innerHtml.indexOf("id_x_1")).to.be.true; expect(innerHtml.indexOf("id_x_0") < innerHtml.indexOf("id_2")).to.be.true; expect(innerHtml.indexOf("id_x_0") > 0).to.be.true; expect(innerHtml.indexOf("id_x_0_1") > innerHtml.indexOf("id_2")).to.be.true; expect(innerHtml.indexOf("id_x_1_1") > innerHtml.indexOf("id_x_0_1")).to.be.true; }) it("must have a working replace", function () { let probe1 = new DomQuery(document).byId("id_1"); probe1.replace(DomQuery.fromMarkup(`
`)); expect(DomQuery.querySelectorAll("div").length).to.eq(5); let innerHtml = DomQuery.querySelectorAll("body").innerHTML; expect(innerHtml.indexOf("id_x_0") > 0).to.be.true; expect(innerHtml.indexOf("id_x_0") < innerHtml.indexOf("id_2")).to.be.true; expect(innerHtml.indexOf("id_x_0") < innerHtml.indexOf("id_3")).to.be.true; expect(innerHtml.indexOf("id_x_0") < innerHtml.indexOf("id_x_1")).to.be.true; expect(innerHtml.indexOf("id_x_1") > 0).to.be.true; expect(innerHtml.indexOf("id_x_1") < innerHtml.indexOf("id_2")).to.be.true; expect(innerHtml.indexOf("id_x_1") < innerHtml.indexOf("id_3")).to.be.true; expect(innerHtml.indexOf("id_1") == -1).to.be.true; }) it("must have a working replace - 2", function () { let probe1 = new DomQuery(document).byId("id_2"); probe1.replace(DomQuery.fromMarkup(`
`)); expect(DomQuery.querySelectorAll("div").length).to.eq(5); let innerHtml = DomQuery.querySelectorAll("body").innerHTML; expect(innerHtml.indexOf("id_x_0") > innerHtml.indexOf("id_1")).to.be.true; expect(innerHtml.indexOf("id_x_0") > 0).to.be.true; expect(innerHtml.indexOf("id_x_0") > innerHtml.indexOf("id_0")).to.be.true; expect(innerHtml.indexOf("id_x_0") < innerHtml.indexOf("id_3")).to.be.true; expect(innerHtml.indexOf("id_x_1") > 0).to.be.true; expect(innerHtml.indexOf("id_x_1") > innerHtml.indexOf("id_0")).to.be.true; expect(innerHtml.indexOf("id_x_1") < innerHtml.indexOf("id_3")).to.be.true; expect(innerHtml.indexOf("id_2") == -1).to.be.true; }) it("must have a working replace - 3", function () { let probe1 = new DomQuery(document).byId("id_4"); probe1.replace(DomQuery.fromMarkup(`
`)); expect(DomQuery.querySelectorAll("div").length).to.eq(5); let innerHtml = DomQuery.querySelectorAll("body").innerHTML; expect(innerHtml.indexOf("id_x_0") > 0).to.be.true; expect(innerHtml.indexOf("id_x_0") > innerHtml.indexOf("id_1")).to.be.true; expect(innerHtml.indexOf("id_x_0") > innerHtml.indexOf("id_2")).to.be.true; expect(innerHtml.indexOf("id_x_0") > innerHtml.indexOf("id_3")).to.be.true; expect(innerHtml.indexOf("id_x_0") < innerHtml.indexOf("id_x_1")).to.be.true; expect(innerHtml.indexOf("id_x_1") > 0).to.be.true; expect(innerHtml.indexOf("id_x_1") > innerHtml.indexOf("id_1")).to.be.true; expect(innerHtml.indexOf("id_x_1") > innerHtml.indexOf("id_2")).to.be.true; expect(innerHtml.indexOf("id_x_1") > innerHtml.indexOf("id_3")).to.be.true; expect(innerHtml.indexOf("id_4") == -1).to.be.true; }) it("must have a working input handling", function () { DomQuery.querySelectorAll("body").innerHTML = `
`; let length = DomQuery.querySelectorAll("form").elements.length; expect(length == 12).to.be.true; let length1 = DomQuery.querySelectorAll("body").elements.length; expect(length1 == 12).to.be.true; let length2 = DomQuery.byId("embed1").elements.length; expect(length2 == 12).to.be.true; let count = Stream.ofDataSource(DomQuery.byId("embed1").elements) .map(item => item.disabled ? 1 : 0) .reduce((val1, val2) => val1 + val2, 0); expect(count.value).to.eq(1); Stream.ofDataSource(DomQuery.byId("embed1").elements) .filter(item => item.disabled) .each(item => item.disabled = false); count = Stream.ofDataSource(DomQuery.byId("embed1").elements) .map(item => item.disabled ? 1 : 0) .reduce((val1, val2) => val1 + val2, 0); expect(count.value).to.eq(0); count = Stream.ofDataSource(DomQuery.byId("embed1").elements) .map(item => item.attr("checked").isPresent() ? 1 : 0) .reduce((val1, val2) => val1 + val2, 0); expect(count.value).to.eq(3); expect(DomQuery.byId("id_1").inputValue.value == "id_1_val").to.be.true; DomQuery.byId("id_1").inputValue.value = "booga"; expect(DomQuery.byId("id_1").inputValue.value == "booga").to.be.true; expect(DomQuery.byId("id_3").inputValue.value).to.eq("textareaVal"); DomQuery.byId("id_3").inputValue.value = "hello world"; expect(DomQuery.byId("id_3").inputValue.value).to.eq("hello world"); let data = DomQuery.querySelectorAll("form").elements.encodeFormElement(); expect(data?.["id_1"][0]).to.eq("booga"); expect(data?.["id_2"][0]).to.eq("id_2_val"); expect(data?.["id_3"][0]).to.eq("hello world"); expect(data?.["cc_1"][0]).to.eq("Mastercard"); expect(data?.["val_5"][0]).to.eq("akaka"); expect(data?.["page:animals"].length).to.eq(2); expect(data?.["page:animals"][0]).to.eq("Cat"); expect(data?.["page:animals"][1]).to.eq("Fox"); }) it("setCaretPosition must call setSelectionRange on the control", function () { const calls: number[][] = []; const mockCtrl = { focus: () => {}, setSelectionRange: (start: number, end: number) => calls.push([start, end]) }; DomQuery.setCaretPosition(mockCtrl, 5); expect(calls.length).to.eq(1); expect(calls[0][0]).to.eq(5); expect(calls[0][1]).to.eq(5); }); it("setCaretPosition must silently do nothing when control has no setSelectionRange", function () { const mockCtrl = { focus: () => {} }; expect(() => DomQuery.setCaretPosition(mockCtrl, 3)).not.to.throw(); }); it("setCaretPosition must silently do nothing for null control", function () { expect(() => DomQuery.setCaretPosition(null, 3)).not.to.throw(); }); it("setCaretPosition must not throw for inputs whose setSelectionRange rejects selection (checkbox/radio)", function () { // regression: checkbox/radio inputs expose setSelectionRange but throw // InvalidStateError when it is called; the focus must still be applied let focused = false; const mockCtrl = { focus: () => { focused = true; }, setSelectionRange: () => { throw new DOMException( "Failed to execute 'setSelectionRange' on 'HTMLInputElement': " + "The input element's type ('checkbox') does not support selection.", "InvalidStateError"); } }; expect(() => DomQuery.setCaretPosition(mockCtrl, 1)).not.to.throw(); expect(focused).to.eq(true); }); it("getCaretPosition must read selectionStart on modern browsers", function () { // regression: getCaretPosition only had the legacy IE document.selection branch // and therefore always returned 0 on modern browsers, which reset the caret to the // start of the input after a partial update const mockCtrl = {selectionStart: 4}; expect(DomQuery.getCaretPosition(mockCtrl)).to.eq(4); }); it("getCaretPosition returns 0 for controls without a caret", function () { expect(DomQuery.getCaretPosition({})).to.eq(0); expect(DomQuery.getCaretPosition(null)).to.eq(0); }); it("byIdDeep resolves a light-DOM id", function () { let res = new DomQuery(window.document).byIdDeep("id_3"); expect(res.length).to.eq(1); expect(res.id.value).to.eq("id_3"); }); it("byIdDeep returns matches from every scope when the same id lives in both light and shadow DOM", function () { // ids are unique per node-tree only, so the same id may exist in the // light DOM and inside a shadow root at the same time; the deep search // must return both let host = window.document.createElement("div"); host.id = "host_dup"; window.document.body.appendChild(host); let shadow = host.attachShadow({mode: "open"}); let innerDup = window.document.createElement("span"); innerDup.id = "id_3"; // same id as the light-DOM div in the fixture shadow.appendChild(innerDup); let res = new DomQuery(window.document).byIdDeep("id_3"); expect(res.length).to.eq(2); }); it("byIdDeep still descends into shadow roots when the light DOM misses", function () { let host = window.document.createElement("div"); host.id = "host"; window.document.body.appendChild(host); let shadow = host.attachShadow({mode: "open"}); let inner = window.document.createElement("span"); inner.id = "deep_id"; shadow.appendChild(inner); let res = new DomQuery(window.document).byIdDeep("deep_id"); expect(res.length).to.eq(1); expect(res.id.value).to.eq("deep_id"); }); it("byIdDeep returns an empty result for an unknown id", function () { let res = new DomQuery(window.document).byIdDeep("does_not_exist"); expect(res.length).to.eq(0); }); it("querySelectorAllDeep collects matches across several nested shadow roots", function () { // builds host1 -> #shadow1 (host2 -> #shadow2 (host3 -> #shadow3)), // each shadow level plus the light DOM carries one ".deepHit" element. // querySelectorAllDeep must descend through every nesting level and // return all four matches. const doc = window.document; const makeHit = (id: string) => { let el = doc.createElement("span"); el.className = "deepHit"; el.id = id; return el; }; // light DOM match doc.body.appendChild(makeHit("light_hit")); // nested shadow chain, three levels deep let parentScope: any = doc.body; for (let level = 1; level <= 3; level++) { let host = doc.createElement("div"); host.id = "host_" + level; parentScope.appendChild(host); let shadow = host.attachShadow({mode: "open"}); shadow.appendChild(makeHit("shadow_hit_" + level)); parentScope = shadow; } let res = new DomQuery(doc).querySelectorAllDeep(".deepHit"); expect(res.length).to.eq(4); let ids = res.allElems().map(el => el.id).sort(); expect(ids).to.deep.eq(["light_hit", "shadow_hit_1", "shadow_hit_2", "shadow_hit_3"]); }); it("childNodes aggregates the children of every root node in document order", function () { const doc = window.document; let p1 = doc.createElement("div"); // no inter-element whitespace -> only element children, deterministic count p1.innerHTML = ``; let p2 = doc.createElement("div"); p2.innerHTML = ``; doc.body.appendChild(p1); doc.body.appendChild(p2); let kids = new DomQuery(p1, p2).childNodes; expect(kids.length).to.eq(3); let ids = kids.allElems().map(el => el.id); expect(ids).to.deep.eq(["a", "b", "c"]); }); it("byTagName with includeRoot returns the matching roots plus their descendants", function () { const doc = window.document; let s1 = doc.createElement("section"); s1.id = "s1"; let s1a = doc.createElement("section"); s1a.id = "s1a"; s1.appendChild(s1a); let s2 = doc.createElement("section"); s2.id = "s2"; let s2a = doc.createElement("section"); s2a.id = "s2a"; s2.appendChild(s2a); doc.body.appendChild(s1); doc.body.appendChild(s2); // includeRoot matches s1/s2 (tagName is upper-case "SECTION"); the // descendant query adds the nested s1a/s2a let res = new DomQuery(s1, s2).byTagName("SECTION", true); expect(res.length).to.eq(4); let ids = res.allElems().map(el => el.id).sort(); expect(ids).to.deep.eq(["s1", "s1a", "s2", "s2a"]); }); it("must have a proper loadScriptEval execution", function (done) { DomQuery.byTagName("body").loadScriptEval("./fixtures/test.js"); setTimeout(() => { expect(DomQuery.byId("id_1").innerHTML == "hello world").to.be.true; done(); }, 100) }); it("must have first etc working", function () { expect(DomQuery.querySelectorAll("div").first().id.value).to.eq("id_1"); }); it("firstElem must call func with first element and index 0", function () { DomQuery.byTagName("body").innerHTML = `
`; const visited: {id: string, idx: number}[] = []; DomQuery.querySelectorAll("div").firstElem((el, idx) => visited.push({id: el.id, idx: idx ?? 0})); expect(visited.length).to.eq(1); expect(visited[0].id).to.eq("a"); expect(visited[0].idx).to.eq(0); }); it("lastElem must call func with last element and its correct index", function () { DomQuery.byTagName("body").innerHTML = `
`; const visited: {id: string, idx: number}[] = []; DomQuery.querySelectorAll("div").lastElem((el, idx) => visited.push({id: el.id, idx: idx ?? 0})); expect(visited.length).to.eq(1); expect(visited[0].id).to.eq("c"); expect(visited[0].idx).to.eq(2); }); it("firstElem must call func when there is exactly one element", function () { DomQuery.byTagName("body").innerHTML = `
`; const visited: string[] = []; DomQuery.querySelectorAll("div").firstElem(el => visited.push(el.id)); expect(visited.length).to.eq(1); expect(visited[0]).to.eq("only"); }); it("lastElem must call func when there is exactly one element", function () { DomQuery.byTagName("body").innerHTML = `
`; const visited: string[] = []; DomQuery.querySelectorAll("div").lastElem(el => visited.push(el.id)); expect(visited.length).to.eq(1); expect(visited[0]).to.eq("only"); }); it("firstElem must not call func on empty DomQuery", function () { const visited: string[] = []; DomQuery.querySelectorAll(".nonexistent").firstElem(el => visited.push(el.id)); expect(visited.length).to.eq(0); }); it("lastElem must not call func on empty DomQuery", function () { const visited: string[] = []; DomQuery.querySelectorAll(".nonexistent").lastElem(el => visited.push(el.id)); expect(visited.length).to.eq(0); }); it("runscript runcss", function (done) { DomQuery.byTagName("body").innerHTML = `
`; let content = DomQuery.byTagName("body").runScripts().runCss(); expect(content.byId("first").innerHTML).to.eq("hello world"); expect(content.byId("second").innerHTML).to.eq("hello world"); expect(content.byId("third").innerHTML).to.eq("hello world"); expect(content.byId("fourth").innerHTML).to.eq("hello world"); expect(DomQuery.byTagName("body") .querySelectorAll("link[rel='stylesheet'][href='./fixtures/simple.css']").length).to.eq(1); // must be evaled const cstyle = window.getComputedStyle(content.byId("first").getAsElem(0).value, null) expect(cstyle.getPropertyValue("border")).to.eq("10px solid rgb(0, 0, 0)"); DomQuery.byTagName("body").waitUntilDom(() => { const cstyle2 = window.getComputedStyle(content.byId("second").getAsElem(0).value, null) return cstyle2.getPropertyValue("border") == "5px solid red"; }); done(); }); //TODO defer does not work in jsdom it("must have a proper loadScriptEval deferred", function (done) { DomQuery.byId(document.body).loadScriptEval("./fixtures/test2.js", 200); setTimeout(() => { expect(DomQuery.byId("id_1").innerHTML == "hello world").to.be.false; }, 100) setTimeout(() => { expect(DomQuery.byId("id_1").innerHTML == "hello world").to.be.true; done(); }, 1500) }) it("it must handle events properly", function () { let clicked = 0; let listener = (evt: any) => { clicked++; }; let eventReceiver = DomQuery.byId("id_1"); eventReceiver.addEventListener("click", listener); eventReceiver.click(); expect(clicked).to.eq(1); eventReceiver.removeEventListener("click", listener); eventReceiver.click(); expect(clicked).to.eq(1); }); it("it must handle innerText properly", function (done) { // jsdom 29+ has native innerText but returns empty for non-rendered elements; // override on HTMLElement.prototype so DomQuery.innerText() returns textContent in tests Object.defineProperty((window as any).HTMLElement.prototype, 'innerText', { get() { return this.textContent; }, configurable: true }); let probe = DomQuery.byId("id_1"); probe.innerHTML = "
hello
world
"; expect(probe.innerText()).to.eq("helloworld"); done(); }); it("it must handle textContent properly", function () { let probe = DomQuery.byId("id_1"); probe.innerHTML = "
hello
world
"; expect(probe.textContent()).to.eq("helloworld"); }); it("it must handle iterations properly", function () { let probe = DomQuery.byTagName("div"); let resArr = LazyStream.ofStreamDataSource(probe).collect(new ArrayCollector()); expect(resArr.length).to.eq(4); probe.reset(); while (probe.hasNext()) { let el = probe.next(); expect(el.tagName.value.toLowerCase()).to.eq("div"); } expect(probe.next()).to.eq(null); let probe2 = DomQuery.byTagName("div").limits(2); resArr = LazyStream.ofStreamDataSource(probe2).collect(new ArrayCollector() as any); expect(resArr.length).to.eq(2); }); it("it must handle subnodes properly", function () { let probe = DomQuery.byTagName("div"); expect(probe.subNodes(1, 3).length).to.eq(2); probe = DomQuery.byTagName("body").childNodes.subNodes(0, 2); expect(probe.length).to.eq(2); probe = DomQuery.byTagName("div").subNodes(2); expect(probe.length).to.eq(2); }) it("it must ensure shadow dom creation works properly", function () { let probe = DomQuery.byTagName("div"); try { //probably not testable atm, mocha does not have shadow dom support //we might be able to shim it in one way or the other let element = probe.attachShadow(); expect(element.length > 0).to.eq(true); } catch (e) { //not supported we still need to get an error here expect((e as Error).message.indexOf("not supported") != -1).to.be.true; } }) it("parent must break shadow barriers", function () { let probe = DomQuery.fromMarkup("
hello
'"); try { //probably not testable atm, mocha does not have shadow dom support //we might be able to shim it in one way or the other let element = DomQuery.byId("id_1").attachShadow(); element.append(probe); expect(probe.firstParent("#id_1").length > 0).to.eq(true); } catch (e) { //not supported we still need to get an error here expect((e as Error).message.indexOf("not supported") != -1).to.be.true; } }) it('it must resolve immediately when condition is already true (fast path)', async function () { let probe = DomQuery.byId('id_1'); probe.innerHTML = 'true'; const start = Date.now(); let ret = await probe.waitUntilDom((element) => element.innerHTML.indexOf('true') != -1); expect(ret.isPresent()).to.be.true; expect(Date.now() - start).to.be.lessThan(100); }); it('it must have a working wait for dom with mut observer and must detect condition after change', async function () { let probe = DomQuery.byId('id_1'); probe.innerHTML = 'true'; let ret = await probe.waitUntilDom((element) => element.innerHTML.indexOf('true') != -1); expect(ret.isPresent()).to.be.true; probe = DomQuery.byId('bosushsdhs'); ret = await probe.waitUntilDom((element) => element.isAbsent()); expect(ret.isAbsent()).to.be.true; }); it('it must have a working wait for dom with mut observer', async function () { let probe = DomQuery.byId('id_1'); setTimeout(() => probe.innerHTML = 'true', 300); let ret = await probe.waitUntilDom((element) => element.innerHTML.indexOf('true') != -1); delete (window as any).MutationObserver; delete (global as any).MutationObserver; probe.innerHTML = ""; setTimeout(() => probe.innerHTML = 'true', 300); let ret2 = await probe.waitUntilDom((element) => element.innerHTML.indexOf('true') != -1); expect(ret.isPresent()).to.be.true; expect(ret2.isPresent()).to.be.true; }); it('it must have a timeout', async function () { let probe = DomQuery.byId('booga'); try { setTimeout(() => probe.innerHTML = 'true', 300); await probe.waitUntilDom((element) => element.innerHTML.indexOf('true') != -1); expect.fail("must have a timeout"); } catch (ex) { expect(ex).to.be.instanceOf(Error); } try { delete (window as any).MutationObserver; delete (global as any).MutationObserver; probe.innerHTML = ""; setTimeout(() => probe.innerHTML = 'true', 300); await probe.waitUntilDom((element) => element.innerHTML.indexOf('true') != -1); expect.fail("must have a timeout"); } catch (ex2) { expect(ex2).to.be.instanceOf(Error); } }); it('must handle null inputs correctly', function () { const dq = new DomQuery(null as any); expect(dq.isAbsent()).to.eq(true); }) it('concat must work as expected resulting', function () { let probe = DomQuery.querySelectorAll("div"); let probe2 = DomQuery.querySelectorAll("body"); let result = probe.concat(probe2); expect(result.length).to.eq(probe.length + probe2.length); //lets now check for filter double probe2 = DomQuery.querySelectorAll('div'); result = probe.concat(probe2); expect(result.length).to.eq(probe.length); }) it('must handle match correctly', function () { let probe = DomQuery.querySelectorAll("div").first(); let probe2 = DomQuery.querySelectorAll("body").first(); expect(probe.matchesSelector("div")).to.eq(true); expect(probe2.matchesSelector("body")).to.eq(true); expect(probe2.matchesSelector("div")).to.eq(false); }) it('must by recycleable', function () { let probe = DomQuery.querySelectorAll("div"); let probe2 = DomQuery.querySelectorAll("body"); let res1 = probe.filter(item => item.matchesSelector("div")); expect(res1.length).to.eq(4); let res2 = probe.filter(item => item.matchesSelector("div")); expect(res2.length).to.eq(4); }) it('delete must work', function () { let probe = DomQuery.querySelectorAll("body"); let probe2 = DomQuery.fromMarkup("
snafu
"); probe2.appendTo(probe); expect(probe.querySelectorAll("#deleteprobe1").isPresent()).to.eq(true); probe2.delete(); expect(probe.querySelectorAll("#deleteprobe1").isAbsent()).to.eq(true); }) it('must work with rxjs and domquery', function () { let probe = DomQuery.querySelectorAll("div"); let probe2 = DomQuery.querySelectorAll("div"); let probeCnt = 0; let probe2Cnt = 0; from(probe).subscribe(el => probeCnt++); from(Stream.ofDataSource(probe2)).subscribe(el => probe2Cnt++); expect(probeCnt).to.be.above(0); expect(probeCnt).to.eq(probe2Cnt); }); it('must handle closest properly', function() { let probe = DomQuery.byId("id_1"); probe.innerHTML = "
hello world
"; let probe2 = DomQuery.byId("inner_elem"); expect(probe2.closest("div#id_1").id.value).to.eq("id_1"); expect(probe2.parent().closest("div").id.value).to.eq("id_1"); probe2 = DomQuery.byId("inner_elem2"); expect(probe2.closest("div").id.value).to.eq("inner_elem2"); expect(probe2.closest("div#id_1").id.value).to.eq("id_1"); expect(probe2.parent().parent().closest("div").id.value).to.eq("id_1"); }); it("copy attributes must traverse nonce properly", function () { let probe = DomQuery.byId("id_1"); probe.innerHTML = "
hello world
"; let probe2 = DomQuery.byId("inner_elem2"); expect((probe2.getAsElem(0).value as any).nonce).to.be.eq('nonceValue'); let element2 = DomQuery.fromMarkup('
'); element2.copyAttrs(probe2); expect((element2.getAsElem(0).value as any).nonce).to.be.eq('nonceValue'); expect(element2.nonce.value).to.be.eq('nonceValue'); }) });