/* 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');
})
});