buninu-browser 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/happy-dom/inert-link.js +26 -0
- package/lib/happy-dom/node-iterator-removal.js +36 -0
- package/lib/happy-dom/node-replacement.js +60 -0
- package/lib/happy-dom/parser.js +4 -1
- package/lib/layout/text-layout.js +4 -2
- package/lib/renderer/bindings.js +105 -3
- package/lib/renderer/page-renderer.js +10 -4
- package/package.json +1 -1
- package/test/compare-node-iterator.js +42 -0
- package/test/fixtures/fractional-text-scale.js +38 -0
- package/test/fixtures/node-iterator-cases.js +59 -0
- package/test/fixtures/node-iterator-chromium.json +315 -0
- package/test/fractional-text-scale.test.js +6 -0
- package/test/inert-link.test.js +37 -0
- package/test/node-iterator.test.js +81 -0
- package/test/page-scripts.test.js +116 -1
- package/test/text-layout.test.js +15 -0
- package/test/window-resize.test.js +29 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as PropertySymbol from '../../vendor/happy-dom/packages/happy-dom/src/PropertySymbol.ts';
|
|
2
|
+
|
|
3
|
+
// Happy DOM shares HTMLLinkElement.prototype between windows. Install one
|
|
4
|
+
// dispatch hook, but bypass its loader only for windows owned by our renderer.
|
|
5
|
+
const inertWindows = new WeakSet();
|
|
6
|
+
let installed = false;
|
|
7
|
+
export function makeLinksInert(window) {
|
|
8
|
+
inertWindows.add(window);
|
|
9
|
+
if (installed) return;
|
|
10
|
+
installed = true;
|
|
11
|
+
const base = window.HTMLElement.prototype;
|
|
12
|
+
const prototype = window.HTMLLinkElement.prototype;
|
|
13
|
+
const connected = prototype[PropertySymbol.connectedToDocument];
|
|
14
|
+
const setAttribute = prototype[PropertySymbol.onSetAttribute];
|
|
15
|
+
prototype[PropertySymbol.connectedToDocument] = function() {
|
|
16
|
+
if (!inertWindows.has(this[PropertySymbol.window])) return connected.call(this);
|
|
17
|
+
// The renderer owns load/error dispatch; the disabled parser loader must
|
|
18
|
+
// not emit a premature error when a dynamic stylesheet is connected.
|
|
19
|
+
base[PropertySymbol.connectedToDocument].call(this);
|
|
20
|
+
this.ownerDocument[PropertySymbol.clearComputedStyleCache]();
|
|
21
|
+
};
|
|
22
|
+
prototype[PropertySymbol.onSetAttribute] = function(attribute, replacedAttribute) {
|
|
23
|
+
if (!inertWindows.has(this[PropertySymbol.window])) return setAttribute.call(this, attribute, replacedAttribute);
|
|
24
|
+
base[PropertySymbol.onSetAttribute].call(this, attribute, replacedAttribute);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Node from '../../vendor/happy-dom/packages/happy-dom/src/nodes/node/Node.ts';
|
|
2
|
+
import * as PropertySymbol from '../../vendor/happy-dom/packages/happy-dom/src/PropertySymbol.ts';
|
|
3
|
+
|
|
4
|
+
// NodeIterator's pre-removing steps must run synchronously, including removals
|
|
5
|
+
// by innerHTML, normalize and reparenting. All reach Happy DOM's internal hook.
|
|
6
|
+
const roots = new WeakMap();
|
|
7
|
+
const cleanup = new FinalizationRegistry(({ entries, ref }) => entries.delete(ref));
|
|
8
|
+
let installed = false;
|
|
9
|
+
export function observeIteratorRemoval(root, iterator) {
|
|
10
|
+
let entries = roots.get(root);
|
|
11
|
+
if (!entries) roots.set(root, entries = new Set());
|
|
12
|
+
const ref = new WeakRef(iterator);
|
|
13
|
+
entries.add(ref);
|
|
14
|
+
cleanup.register(iterator, { entries, ref });
|
|
15
|
+
if (installed) return;
|
|
16
|
+
installed = true;
|
|
17
|
+
const remove = Node.prototype[PropertySymbol.removeChild];
|
|
18
|
+
Node.prototype[PropertySymbol.removeChild] = function(node) {
|
|
19
|
+
node = node?.[PropertySymbol.proxy] ?? node;
|
|
20
|
+
// select/form methods are bound to the raw target, while parentNode and
|
|
21
|
+
// iterator roots expose its proxy. Compare and walk the public identity.
|
|
22
|
+
const parent = this[PropertySymbol.proxy] ?? this;
|
|
23
|
+
if (node?.parentNode === parent) {
|
|
24
|
+
for (let ancestor = parent; ancestor; ancestor = ancestor.parentNode) {
|
|
25
|
+
const entries = roots.get(ancestor);
|
|
26
|
+
if (!entries) continue;
|
|
27
|
+
for (const ref of entries) {
|
|
28
|
+
const iterator = ref.deref();
|
|
29
|
+
if (iterator) iterator.preRemove(node);
|
|
30
|
+
else entries.delete(ref);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return remove.call(this, node);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** DOM replace: validate first, detach the incoming node, remove the outgoing
|
|
2
|
+
* node, then insert. Happy DOM's insert-then-remove order skips replacements
|
|
3
|
+
* in live NodeIterators. Keep this on the renderer bindings' mutation path.
|
|
4
|
+
* https://dom.spec.whatwg.org/#concept-node-replace */
|
|
5
|
+
function validate(parent, node, child) {
|
|
6
|
+
const fail = (name) => { throw new DOMException('Invalid node replacement', name); };
|
|
7
|
+
if (![1,9,11].includes(parent.nodeType)) fail('HierarchyRequestError');
|
|
8
|
+
for (let ancestor = parent; ancestor; ancestor = ancestor.parentNode ?? ancestor.host) {
|
|
9
|
+
if (ancestor === node) fail('HierarchyRequestError');
|
|
10
|
+
}
|
|
11
|
+
if (child.parentNode !== parent) fail('NotFoundError');
|
|
12
|
+
if (![1,3,4,7,8,10,11].includes(node.nodeType)) fail('HierarchyRequestError');
|
|
13
|
+
if (node.nodeType === 10 && parent.nodeType !== 9) fail('HierarchyRequestError');
|
|
14
|
+
if (parent.nodeType !== 9) return;
|
|
15
|
+
// Validate the final document, allowing the outgoing child and an incoming
|
|
16
|
+
// sibling to be replaced/moved without counting either twice.
|
|
17
|
+
const added = node.nodeType === 11 ? [...node.childNodes] : [node];
|
|
18
|
+
const final = [];
|
|
19
|
+
for (const current of parent.childNodes) {
|
|
20
|
+
if (current === child) final.push(...added);
|
|
21
|
+
else if (current !== node) final.push(current);
|
|
22
|
+
}
|
|
23
|
+
let element = false, doctype = false;
|
|
24
|
+
for (const current of final) {
|
|
25
|
+
if (current.nodeType === 3 || current.nodeType === 4) fail('HierarchyRequestError');
|
|
26
|
+
if (current.nodeType === 1) {
|
|
27
|
+
if (element) fail('HierarchyRequestError');
|
|
28
|
+
element = true;
|
|
29
|
+
} else if (current.nodeType === 10) {
|
|
30
|
+
if (doctype || element) fail('HierarchyRequestError');
|
|
31
|
+
doctype = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function replaceNode(parent, node, child) {
|
|
37
|
+
validate(parent, node, child);
|
|
38
|
+
let reference = child.nextSibling;
|
|
39
|
+
if (reference === node) reference = node.nextSibling;
|
|
40
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
41
|
+
if (child.parentNode) child.parentNode.removeChild(child);
|
|
42
|
+
parent.insertBefore(node, reference);
|
|
43
|
+
return child;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function replaceWithNodes(child, nodes) {
|
|
47
|
+
const parent = child.parentNode;
|
|
48
|
+
if (!parent) return;
|
|
49
|
+
let reference = child.nextSibling;
|
|
50
|
+
while (reference && nodes.includes(reference)) reference = reference.nextSibling;
|
|
51
|
+
// DOM's convert-nodes algorithm moves multiple arguments into a fragment
|
|
52
|
+
// first, including the receiver if it occurs among the arguments.
|
|
53
|
+
let node = nodes[0];
|
|
54
|
+
if (nodes.length !== 1) {
|
|
55
|
+
node = child.ownerDocument.createDocumentFragment();
|
|
56
|
+
for (const item of nodes) node.appendChild(item);
|
|
57
|
+
}
|
|
58
|
+
if (child.parentNode === parent) replaceNode(parent, node, child);
|
|
59
|
+
else parent.insertBefore(node, reference);
|
|
60
|
+
}
|
package/lib/happy-dom/parser.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { makeLinksInert } from './inert-link.js';
|
|
1
2
|
import HTMLParser from "../../vendor/happy-dom/packages/happy-dom/src/html-parser/HTMLParser.ts";
|
|
2
3
|
import Window from "../../vendor/happy-dom/packages/happy-dom/src/window/Window.ts";
|
|
3
4
|
import * as PropertySymbol from "../../vendor/happy-dom/packages/happy-dom/src/PropertySymbol.ts";
|
|
@@ -26,7 +27,7 @@ export function parseHTMLDocument(html, url = "about:blank", referrer = "") {
|
|
|
26
27
|
|
|
27
28
|
/** An isolated parser context: no script execution, resource loads or page custom elements. */
|
|
28
29
|
export function createInertWindow(url = "about:blank") {
|
|
29
|
-
|
|
30
|
+
const window = new Window({
|
|
30
31
|
url,
|
|
31
32
|
settings: {
|
|
32
33
|
disableJavaScriptEvaluation: true,
|
|
@@ -40,6 +41,8 @@ export function createInertWindow(url = "about:blank") {
|
|
|
40
41
|
navigation: { disableFallbackToSetURL: true },
|
|
41
42
|
},
|
|
42
43
|
});
|
|
44
|
+
makeLinksInert(window);
|
|
45
|
+
return window;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
/**
|
|
@@ -1858,8 +1858,10 @@ function resolveHorizontal(ctx, box, containingX, containingWidth, shrink, perce
|
|
|
1858
1858
|
return { x: containingX + marginLeft, width, marginLeft, marginRight };
|
|
1859
1859
|
}
|
|
1860
1860
|
|
|
1861
|
+
// Anonymous flex/grid items share their parent RenderNode, but measure only
|
|
1862
|
+
// their own text run. Never reuse the parent's intrinsic width for these boxes.
|
|
1861
1863
|
function maxContentWidth(ctx, box) {
|
|
1862
|
-
const cacheKey = box.node ?? box;
|
|
1864
|
+
const cacheKey = box.anonymous ? box : box.node ?? box;
|
|
1863
1865
|
const cached = ctx.maxContent.get(cacheKey);
|
|
1864
1866
|
if (cached != null) return cached;
|
|
1865
1867
|
const style = box.style;
|
|
@@ -1902,7 +1904,7 @@ function maxContentWidth(ctx, box) {
|
|
|
1902
1904
|
}
|
|
1903
1905
|
|
|
1904
1906
|
function minContentWidth(ctx, box) {
|
|
1905
|
-
const cacheKey = box.node ?? box;
|
|
1907
|
+
const cacheKey = box.anonymous ? box : box.node ?? box;
|
|
1906
1908
|
const cached = ctx.minContent.get(cacheKey);
|
|
1907
1909
|
if (cached != null) return cached;
|
|
1908
1910
|
const style = box.style;
|
package/lib/renderer/bindings.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Coverage is the commonly used subset of DOM, not the full WebIDL surface.
|
|
12
12
|
*/
|
|
13
|
+
import { replaceNode, replaceWithNodes } from '../happy-dom/node-replacement.js';
|
|
14
|
+
import { observeIteratorRemoval } from '../happy-dom/node-iterator-removal.js';
|
|
13
15
|
import { renderedText, setRenderedText } from "./rendered-text.js";
|
|
14
16
|
import { assignedSlot, slotAssignedNodes, assignSlotNodes } from "./slot-assignment.js";
|
|
15
17
|
import { scrollIntoViewOptions, scrollAlignmentDelta } from "./scroll-into-view.js";
|
|
@@ -503,7 +505,7 @@ class NodeBinding {
|
|
|
503
505
|
replaceChild(child, old) {
|
|
504
506
|
const node = requireNode(child);
|
|
505
507
|
const list = insertionList([node]);
|
|
506
|
-
this.#node
|
|
508
|
+
replaceNode(this.#node, node, requireNode(old));
|
|
507
509
|
inserted(this, list);
|
|
508
510
|
return old;
|
|
509
511
|
}
|
|
@@ -1053,7 +1055,7 @@ class ElementBinding extends ParentBinding {
|
|
|
1053
1055
|
replaceWith(...values) {
|
|
1054
1056
|
const nodes = values.map((value) => toNode(this, value));
|
|
1055
1057
|
const list = insertionList(nodes);
|
|
1056
|
-
nodeOf(this)
|
|
1058
|
+
replaceWithNodes(nodeOf(this), nodes);
|
|
1057
1059
|
inserted(this, list);
|
|
1058
1060
|
}
|
|
1059
1061
|
|
|
@@ -1690,6 +1692,10 @@ class DocumentBinding extends ParentBinding {
|
|
|
1690
1692
|
return realmOf(this).wrap(nodeOf(this).createComment(String(text)));
|
|
1691
1693
|
}
|
|
1692
1694
|
|
|
1695
|
+
createNodeIterator(root, whatToShow = NodeFilter.SHOW_ALL, filter = null) {
|
|
1696
|
+
return new NodeIteratorBinding(CONSTRUCT, realmOf(this), requireNode(root), whatToShow, filter);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1693
1699
|
createTreeWalker(root, whatToShow = NodeFilter.SHOW_ALL, filter = null) {
|
|
1694
1700
|
return new TreeWalkerBinding(CONSTRUCT, realmOf(this), requireNode(root), whatToShow, filter);
|
|
1695
1701
|
}
|
|
@@ -2033,6 +2039,101 @@ export const NodeFilter = Object.freeze({
|
|
|
2033
2039
|
});
|
|
2034
2040
|
const { FILTER_ACCEPT, FILTER_REJECT, FILTER_SKIP } = NodeFilter;
|
|
2035
2041
|
|
|
2042
|
+
/** DOM §6.1: a cursor between nodes, including the root; REJECT skips
|
|
2043
|
+
* only that node, unlike TreeWalker where it also prunes descendants. */
|
|
2044
|
+
class NodeIteratorBinding {
|
|
2045
|
+
#realm; #root; #reference; #before = true; #whatToShow; #filter; #active = false;
|
|
2046
|
+
#candidate = null;
|
|
2047
|
+
constructor(key, realm, root, whatToShow, filter) {
|
|
2048
|
+
if (key !== CONSTRUCT) throw new TypeError("Illegal constructor");
|
|
2049
|
+
this.#realm = realm; this.#root = this.#reference = root;
|
|
2050
|
+
this.#whatToShow = +whatToShow >>> 0; this.#filter = filter ?? null;
|
|
2051
|
+
// WebIDL callback interfaces accept objects/functions or null, but no
|
|
2052
|
+
// primitives; validate even when whatToShow would skip every node.
|
|
2053
|
+
if (this.#filter !== null && typeof filter !== "object" && typeof filter !== "function") {
|
|
2054
|
+
throw new TypeError("NodeIterator filter must be an object or null");
|
|
2055
|
+
}
|
|
2056
|
+
// Keep the internal removal callback off the page-visible interface.
|
|
2057
|
+
observeIteratorRemoval(root, this.#removal);
|
|
2058
|
+
}
|
|
2059
|
+
#removal = { preRemove: node => this.#preRemove(node) };
|
|
2060
|
+
get root() { return this.#realm.wrap(this.#root); }
|
|
2061
|
+
get referenceNode() { return this.#realm.wrap(this.#reference); }
|
|
2062
|
+
get pointerBeforeReferenceNode() { return this.#before; }
|
|
2063
|
+
get whatToShow() { return this.#whatToShow; }
|
|
2064
|
+
get filter() { return this.#filter; }
|
|
2065
|
+
detach() {} // Legacy no-op, as required by DOM.
|
|
2066
|
+
nextNode() { return this.#traverse(true); }
|
|
2067
|
+
previousNode() { return this.#traverse(false); }
|
|
2068
|
+
#following(node, children = true) {
|
|
2069
|
+
if (children && node.firstChild) return node.firstChild;
|
|
2070
|
+
while (node && node !== this.#root) {
|
|
2071
|
+
if (node.nextSibling) return node.nextSibling;
|
|
2072
|
+
node = node.parentNode;
|
|
2073
|
+
}
|
|
2074
|
+
return null;
|
|
2075
|
+
}
|
|
2076
|
+
#preceding(node) {
|
|
2077
|
+
if (node === this.#root) return null;
|
|
2078
|
+
if (!node.previousSibling) return node.parentNode;
|
|
2079
|
+
node = node.previousSibling;
|
|
2080
|
+
while (node.lastChild) node = node.lastChild;
|
|
2081
|
+
return node;
|
|
2082
|
+
}
|
|
2083
|
+
#accept(node) {
|
|
2084
|
+
if (this.#active) throw new DOMException("The iterator filter is already active", "InvalidStateError");
|
|
2085
|
+
if (!(this.#whatToShow & (1 << (node.nodeType - 1)))) return FILTER_SKIP;
|
|
2086
|
+
if (this.#filter === null) return FILTER_ACCEPT;
|
|
2087
|
+
this.#active = true;
|
|
2088
|
+
try {
|
|
2089
|
+
const filter = this.#filter, wrapper = this.#realm.wrap(node);
|
|
2090
|
+
return +(typeof filter === "function" ? Reflect.apply(filter, undefined, [wrapper])
|
|
2091
|
+
: Reflect.apply(filter.acceptNode, filter, [wrapper])) & 0xffff;
|
|
2092
|
+
} finally { this.#active = false; }
|
|
2093
|
+
}
|
|
2094
|
+
#traverse(forward) {
|
|
2095
|
+
if (this.#active) throw new DOMException("The iterator filter is already active", "InvalidStateError");
|
|
2096
|
+
// A filter can remove its candidate or an ancestor synchronously. Track
|
|
2097
|
+
// the traversal cursor separately so pre-removal adjusts both positions.
|
|
2098
|
+
const cursor = this.#candidate = { node: this.#reference, before: this.#before };
|
|
2099
|
+
try {
|
|
2100
|
+
for (;;) {
|
|
2101
|
+
if (forward) {
|
|
2102
|
+
if (cursor.before) cursor.before = false;
|
|
2103
|
+
else cursor.node = this.#following(cursor.node);
|
|
2104
|
+
} else {
|
|
2105
|
+
if (!cursor.before) cursor.before = true;
|
|
2106
|
+
else cursor.node = this.#preceding(cursor.node);
|
|
2107
|
+
}
|
|
2108
|
+
const node = cursor.node;
|
|
2109
|
+
if (!node) return null;
|
|
2110
|
+
if (this.#accept(node) === FILTER_ACCEPT) {
|
|
2111
|
+
this.#reference = cursor.node; this.#before = cursor.before;
|
|
2112
|
+
// Return the accepted node even if the filter removed it; the next
|
|
2113
|
+
// traversal resumes at the repaired position in the remaining tree.
|
|
2114
|
+
return this.#realm.wrap(node);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
} finally { this.#candidate = null; }
|
|
2118
|
+
}
|
|
2119
|
+
#preRemove(node) {
|
|
2120
|
+
if (node === this.#root) return;
|
|
2121
|
+
const adjust = cursor => {
|
|
2122
|
+
if (!cursor?.node || !node.contains(cursor.node)) return;
|
|
2123
|
+
if (cursor.before) {
|
|
2124
|
+
const next = this.#following(node, false);
|
|
2125
|
+
if (next) { cursor.node = next; return; }
|
|
2126
|
+
cursor.before = false;
|
|
2127
|
+
}
|
|
2128
|
+
cursor.node = this.#preceding(node);
|
|
2129
|
+
};
|
|
2130
|
+
const reference = { node: this.#reference, before: this.#before };
|
|
2131
|
+
adjust(reference);
|
|
2132
|
+
this.#reference = reference.node; this.#before = reference.before;
|
|
2133
|
+
adjust(this.#candidate);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2036
2137
|
/**
|
|
2037
2138
|
* DOM §6.2 TreeWalker over the real nodes. The page's filter (a function or
|
|
2038
2139
|
* an object with acceptNode) sees node wrappers, and its exceptions reach the
|
|
@@ -2833,7 +2934,7 @@ function cssEscape(value) {
|
|
|
2833
2934
|
* Must run after SES lockdown() (which provides `harden`).
|
|
2834
2935
|
*/
|
|
2835
2936
|
const BINDINGS = [NodeBinding, CharacterDataBinding, TextBinding, ParentBinding, FragmentBinding, ShadowRootBinding, ElementBinding,
|
|
2836
|
-
DocumentBinding, EventBinding, ClassListBinding, TreeWalkerBinding, RangeBinding];
|
|
2937
|
+
DocumentBinding, EventBinding, ClassListBinding, TreeWalkerBinding, NodeIteratorBinding, RangeBinding];
|
|
2837
2938
|
|
|
2838
2939
|
// Members defined with Object.defineProperty({ get() {...} }) are named
|
|
2839
2940
|
// "get"; Web IDL names an attribute's accessors "get x" / "set x" and an
|
|
@@ -2869,5 +2970,6 @@ export const INTERFACES = Object.freeze({
|
|
|
2869
2970
|
DocumentFragment: FragmentBinding,
|
|
2870
2971
|
ShadowRoot: ShadowRootBinding,
|
|
2871
2972
|
TreeWalker: TreeWalkerBinding,
|
|
2973
|
+
NodeIterator: NodeIteratorBinding,
|
|
2872
2974
|
DOMTokenList: ClassListBinding,
|
|
2873
2975
|
});
|
|
@@ -438,6 +438,8 @@ function buildPageRenderer({
|
|
|
438
438
|
[...page.renderTree.nodesById.values()].filter(node => node.type === "text"));
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
// Keep fractional CSS sizes through measurement and painting. Rounding each
|
|
442
|
+
// scale independently can widen painted glyphs beyond the measured line.
|
|
441
443
|
function makeParagraph(text, computedStyle = null, fontSize = computedStyle?.fontSize ?? 16, scale = 1, foregroundPaint = null) {
|
|
442
444
|
const color = canvasColor(CanvasKit, computedStyle?.color ?? "rgba(0, 0, 0, 1)");
|
|
443
445
|
const weight = computedStyle?.fontWeight ?? 400;
|
|
@@ -460,7 +462,7 @@ function buildPageRenderer({
|
|
|
460
462
|
textStyle: {
|
|
461
463
|
color,
|
|
462
464
|
fontFamilies: families,
|
|
463
|
-
fontSize: Math.max(1,
|
|
465
|
+
fontSize: Math.max(1, fontSize) * scale,
|
|
464
466
|
fontStyle: { weight, slant, width },
|
|
465
467
|
// Roboto is a variable font; static fonts ignore the axis and match by weight.
|
|
466
468
|
fontVariations: [{ axis: "wght", value: weight }, { axis: "wdth", value: stretch }],
|
|
@@ -475,7 +477,7 @@ function buildPageRenderer({
|
|
|
475
477
|
const pushTextStyle = foregroundPaint
|
|
476
478
|
? value => builder.pushPaintStyle(value, foregroundPaint, backgroundPaint) : value => builder.pushStyle(value);
|
|
477
479
|
const textStyle = (fontFamilies) => CanvasKit.TextStyle({ color, ...decoration, fontFamilies,
|
|
478
|
-
fontSize: Math.max(1,
|
|
480
|
+
fontSize: Math.max(1, fontSize) * scale, fontStyle: { weight, slant, width },
|
|
479
481
|
fontVariations: [{ axis: "wght", value: weight }, { axis: "wdth", value: stretch }] });
|
|
480
482
|
function append(text, fonts) {
|
|
481
483
|
let emojiStyle;
|
|
@@ -518,7 +520,7 @@ function buildPageRenderer({
|
|
|
518
520
|
return paragraph;
|
|
519
521
|
}
|
|
520
522
|
|
|
521
|
-
// Text widths depend only on the font (families,
|
|
523
|
+
// Text widths depend only on the font (families, fractional size, weight),
|
|
522
524
|
// not on the element, so they are cached per font across elements,
|
|
523
525
|
// relayouts and documents. The cache is dropped when it grows too large.
|
|
524
526
|
const MAX_MEASURED_TEXTS = 200_000;
|
|
@@ -557,7 +559,7 @@ function buildPageRenderer({
|
|
|
557
559
|
const weight = computedStyle?.fontWeight ?? 400;
|
|
558
560
|
const fontStyle = computedStyle?.fontStyle ?? "normal";
|
|
559
561
|
const stretch = computedStyle?.fontStretch ?? 100;
|
|
560
|
-
const key = `${Math.max(1,
|
|
562
|
+
const key = `${Math.max(1, computedStyle?.fontSize ?? 16)}/${weight}/${fontStyle}/${stretch}/${
|
|
561
563
|
resolveFontFamilies(computedStyle?.fontFamily, weight, fontStyle, null, stretch).join(",")}`;
|
|
562
564
|
if (computedStyle && typeof computedStyle === "object") fontKeys.set(computedStyle, key);
|
|
563
565
|
return key;
|
|
@@ -2786,6 +2788,10 @@ function buildPageRenderer({
|
|
|
2786
2788
|
? deviceScaleFactor
|
|
2787
2789
|
: 1;
|
|
2788
2790
|
viewport.resize(width, height);
|
|
2791
|
+
// Viewport dimensions are layout input even when DOM/stylesheets did
|
|
2792
|
+
// not mutate (including CSS resizes that keep the same pixel size by
|
|
2793
|
+
// changing DPR). Bypass retained-layout shortcuts in that case.
|
|
2794
|
+
if (resized) page.resourceLayoutDirty = true;
|
|
2789
2795
|
rebuildRenderTree();
|
|
2790
2796
|
// Listeners must observe the new viewport and layout. A repeated metrics
|
|
2791
2797
|
// request (or DPR-only change) does not resize the CSS viewport.
|
package/package.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Manual black-box reference check (no Chromium source code copied):
|
|
2
|
+
// bun test/compare-node-iterator.js /usr/local/bin/chromium-headless-shell
|
|
3
|
+
// Optional --snapshot writes Chromium's raw results for the regression test.
|
|
4
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { RendererHost } from '../lib/renderer/host.js';
|
|
9
|
+
import { nodeIteratorCases } from './fixtures/node-iterator-cases.js';
|
|
10
|
+
|
|
11
|
+
const binary = process.argv[2] || Bun.which('chromium-headless-shell');
|
|
12
|
+
if (!binary) throw new Error('Pass the Chromium Headless Shell executable as the first argument');
|
|
13
|
+
const dir = await mkdtemp(join(tmpdir(), 'node-iterator-'));
|
|
14
|
+
const host = new RendererHost({timeout:20000,fetch:async()=>new Response('')});
|
|
15
|
+
let child;
|
|
16
|
+
try {
|
|
17
|
+
const expression = `JSON.stringify((${nodeIteratorCases})())`;
|
|
18
|
+
const path = join(dir, 'cases.html');
|
|
19
|
+
await Bun.write(path, `<body><script>document.body.textContent=${expression}</script>`);
|
|
20
|
+
child = Bun.spawn([binary,'--no-sandbox','--disable-gpu','--dump-dom',pathToFileURL(path).href],
|
|
21
|
+
{stdout:'pipe',stderr:'ignore'});
|
|
22
|
+
const html = await new Response(child.stdout).text();
|
|
23
|
+
if (await child.exited) throw new Error('Chromium failed');
|
|
24
|
+
const body = html.match(/<body>(.*?)<\/body>/s)?.[1];
|
|
25
|
+
const chromium = JSON.parse(body?.replaceAll('<','<').replaceAll('>','>').replaceAll('&','&'));
|
|
26
|
+
await host.call('loadDocument',{url:'https://iterator.test/',source:'<body></body>',contentType:'text/html',status:200});
|
|
27
|
+
await host.call('whenLoaded');
|
|
28
|
+
const browser = JSON.parse(await host.call('evaluate',expression));
|
|
29
|
+
let differences = 0;
|
|
30
|
+
for (let i = 0; i < chromium.length; i++) {
|
|
31
|
+
if (!Bun.deepEquals(browser[i], chromium[i])) { differences++;console.log({browser:browser[i],chromium:chromium[i]}); }
|
|
32
|
+
}
|
|
33
|
+
console.log(`${chromium.length} cases; ${differences} differences (see node-iterator.test.js for documented DOM-spec exceptions)`);
|
|
34
|
+
if (process.argv.includes('--snapshot')) {
|
|
35
|
+
await Bun.write(new URL('./fixtures/node-iterator-chromium.json',import.meta.url),
|
|
36
|
+
'[\n'+chromium.map(row=>' '+JSON.stringify(row)).join(',\n')+'\n]\n');
|
|
37
|
+
}
|
|
38
|
+
} finally {
|
|
39
|
+
child?.kill();
|
|
40
|
+
host.close();
|
|
41
|
+
await rm(dir,{recursive:true,force:true});
|
|
42
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {strict as assert} from 'node:assert';
|
|
2
|
+
import {createPageRenderer,loadGraphics} from '../../lib/renderer/page-renderer.js';
|
|
3
|
+
const {CanvasKit}=await loadGraphics();
|
|
4
|
+
const original=CanvasKit.ParagraphBuilder.MakeFromFontCollection;
|
|
5
|
+
let checked=0;const missing=[];
|
|
6
|
+
CanvasKit.ParagraphBuilder.MakeFromFontCollection=function(...args){
|
|
7
|
+
const builder=original.apply(this,args),add=builder.addText.bind(builder),build=builder.build.bind(builder);
|
|
8
|
+
let text='';
|
|
9
|
+
builder.addText=value=>{text+=value;return add(value)};
|
|
10
|
+
builder.build=()=>{
|
|
11
|
+
const paragraph=build(),layout=paragraph.layout.bind(paragraph);
|
|
12
|
+
paragraph.layout=width=>{
|
|
13
|
+
layout(width);
|
|
14
|
+
if(width<10000 && text.trim()){
|
|
15
|
+
const last=text.trimEnd().length-1;
|
|
16
|
+
const rects=paragraph.getRectsForRange(last,last+1,CanvasKit.RectHeightStyle.Max,CanvasKit.RectWidthStyle.Tight);
|
|
17
|
+
if(!rects.length)missing.push({text,width});
|
|
18
|
+
checked++;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
return paragraph;
|
|
22
|
+
};
|
|
23
|
+
return builder;
|
|
24
|
+
};
|
|
25
|
+
const r=await createPageRenderer({});
|
|
26
|
+
try{
|
|
27
|
+
for(const scale of [1,1.125,1.25,1.5,2]){
|
|
28
|
+
r.resize(900,300,scale);
|
|
29
|
+
await r.loadDocument({url:'https://text.test/',contentType:'text/html',source:`<body style="margin:0">
|
|
30
|
+
<div style="display:flex;gap:24px;font-size:15.2px"><span>Contact</span><span>Ecosystem</span><span>Contributors</span></div>
|
|
31
|
+
<div style="display:flex;gap:24px;font-size:14.4px"><span>Buninu Linux</span><span>Long navigation label</span></div>
|
|
32
|
+
<button style="font:14.4px monospace">Show email address</button>
|
|
33
|
+
</body>`});
|
|
34
|
+
await r.whenLoaded();await r.screenshot();
|
|
35
|
+
}
|
|
36
|
+
assert.deepEqual(missing,[],'All painted lines must retain their final character');
|
|
37
|
+
assert.ok(checked>=30,'All scales must exercise painted paragraphs');
|
|
38
|
+
}finally{r.dispose();CanvasKit.ParagraphBuilder.MakeFromFontCollection=original}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Runs unchanged in the renderer and Chromium; keep this function standalone.
|
|
2
|
+
export function nodeIteratorCases() {
|
|
3
|
+
const results = [];
|
|
4
|
+
const label = n => n ? n.id || n.nodeName : null;
|
|
5
|
+
for (const tag of ['div', 'form', 'select'])
|
|
6
|
+
for (const operation of ['remove', 'replace', 'replaceWith', 'self', 'sibling', 'fragment', 'descendant', 'multiple'])
|
|
7
|
+
for (const position of [0, 1, 2, 3]) for (const before of [false, true]) {
|
|
8
|
+
const root = document.createElement(tag);root.id = 'root';
|
|
9
|
+
for (const id of ['a', 'b']) {
|
|
10
|
+
const node = document.createElement('option');node.id = id;root.append(node);
|
|
11
|
+
}
|
|
12
|
+
const nested = document.createElement('span');nested.id = 'nested';root.firstChild.append(nested);
|
|
13
|
+
const it = document.createNodeIterator(root, 1);
|
|
14
|
+
for (let i = 0; i <= position; i++) it.nextNode();
|
|
15
|
+
if (before) it.previousNode();
|
|
16
|
+
const old = root.firstChild, x = document.createElement('em');x.id = 'x';
|
|
17
|
+
if (operation === 'remove') old.remove();
|
|
18
|
+
if (operation === 'replace') root.replaceChild(x, old);
|
|
19
|
+
if (operation === 'replaceWith') old.replaceWith(x);
|
|
20
|
+
if (operation === 'self') root.replaceChild(old, old);
|
|
21
|
+
if (operation === 'sibling') root.replaceChild(root.lastChild, old);
|
|
22
|
+
if (operation === 'descendant') root.replaceChild(nested, old);
|
|
23
|
+
if (operation === 'fragment') {
|
|
24
|
+
const fragment = document.createDocumentFragment();fragment.append(x);
|
|
25
|
+
const y = document.createElement('em');y.id = 'y';fragment.append(y);
|
|
26
|
+
root.replaceChild(fragment, old);
|
|
27
|
+
}
|
|
28
|
+
if (operation === 'multiple') old.replaceWith(x, old, root.lastChild);
|
|
29
|
+
results.push([tag, operation, position, before, label(it.referenceNode),
|
|
30
|
+
it.pointerBeforeReferenceNode, label(it.nextNode()), label(it.previousNode()),
|
|
31
|
+
[...root.childNodes].map(label).join(',')]);
|
|
32
|
+
}
|
|
33
|
+
for (const whatToShow of [0, 1, -1, 4294967297, NaN, Infinity, '1', null, 1n, Object(1n), Symbol('x')]) {
|
|
34
|
+
try { results.push(['mask', String(whatToShow), document.createNodeIterator(document.body, whatToShow).whatToShow]); }
|
|
35
|
+
catch (e) { results.push(['mask', String(whatToShow), e.name]); }
|
|
36
|
+
}
|
|
37
|
+
for (const verdict of [1, 65537, -65535, NaN, Infinity, '1', null, 1n, Object(1n), Symbol('x')]) {
|
|
38
|
+
try { results.push(['filter', String(verdict), !!document.createNodeIterator(document.body, 1, () => verdict).nextNode()]); }
|
|
39
|
+
catch (e) { results.push(['filter', String(verdict), e.name]); }
|
|
40
|
+
}
|
|
41
|
+
// Filtering and root types: rejection must not prune descendants.
|
|
42
|
+
for (const rootKind of ['element', 'fragment', 'text', 'comment', 'doctype'])
|
|
43
|
+
for (const mask of [0, 1, 4, 128, 0xffffffff]) for (const mode of ['accept', 'reject', 'skip', 'alternating']) {
|
|
44
|
+
const root = rootKind === 'fragment' ? document.createDocumentFragment()
|
|
45
|
+
: rootKind === 'text' ? document.createTextNode('text')
|
|
46
|
+
: rootKind === 'comment' ? document.createComment('comment')
|
|
47
|
+
: rootKind === 'doctype' ? document.implementation.createDocumentType('html', '', '') : document.createElement('div');
|
|
48
|
+
if (rootKind === 'element' || rootKind === 'fragment') {
|
|
49
|
+
const child = document.createElement('b');child.append('text');root.append(child, document.createComment('comment'));
|
|
50
|
+
}
|
|
51
|
+
const it = document.createNodeIterator(root, mask, n => mode === 'accept' ? 1
|
|
52
|
+
: mode === 'reject' ? 2 : mode === 'skip' ? 3 : n.nodeType === 1 ? 2 : 1);
|
|
53
|
+
const forward = [], backward = [];
|
|
54
|
+
for (let n; (n = it.nextNode());) forward.push(n.nodeType);
|
|
55
|
+
for (let n; (n = it.previousNode());) backward.push(n.nodeType);
|
|
56
|
+
results.push(['walk', rootKind, mask, mode, forward.join(','), backward.join(',')]);
|
|
57
|
+
}
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
[
|
|
2
|
+
["div","remove",0,false,"root",false,"b","b","b"],
|
|
3
|
+
["div","remove",0,true,"root",true,"root","root","b"],
|
|
4
|
+
["div","remove",1,false,"root",false,"b","b","b"],
|
|
5
|
+
["div","remove",1,true,"b",true,"b","b","b"],
|
|
6
|
+
["div","remove",2,false,"root",false,"b","b","b"],
|
|
7
|
+
["div","remove",2,true,"b",true,"b","b","b"],
|
|
8
|
+
["div","remove",3,false,"b",false,null,"b","b"],
|
|
9
|
+
["div","remove",3,true,"b",true,"b","b","b"],
|
|
10
|
+
["div","replace",0,false,"root",false,"x","x","x,b"],
|
|
11
|
+
["div","replace",0,true,"root",true,"root","root","x,b"],
|
|
12
|
+
["div","replace",1,false,"root",false,"x","x","x,b"],
|
|
13
|
+
["div","replace",1,true,"b",true,"b","b","x,b"],
|
|
14
|
+
["div","replace",2,false,"root",false,"x","x","x,b"],
|
|
15
|
+
["div","replace",2,true,"b",true,"b","b","x,b"],
|
|
16
|
+
["div","replace",3,false,"b",false,null,"b","x,b"],
|
|
17
|
+
["div","replace",3,true,"b",true,"b","b","x,b"],
|
|
18
|
+
["div","replaceWith",0,false,"root",false,"x","x","x,b"],
|
|
19
|
+
["div","replaceWith",0,true,"root",true,"root","root","x,b"],
|
|
20
|
+
["div","replaceWith",1,false,"root",false,"x","x","x,b"],
|
|
21
|
+
["div","replaceWith",1,true,"b",true,"b","b","x,b"],
|
|
22
|
+
["div","replaceWith",2,false,"root",false,"x","x","x,b"],
|
|
23
|
+
["div","replaceWith",2,true,"b",true,"b","b","x,b"],
|
|
24
|
+
["div","replaceWith",3,false,"b",false,null,"b","x,b"],
|
|
25
|
+
["div","replaceWith",3,true,"b",true,"b","b","x,b"],
|
|
26
|
+
["div","self",0,false,"root",false,"a","a","a,b"],
|
|
27
|
+
["div","self",0,true,"root",true,"root","root","a,b"],
|
|
28
|
+
["div","self",1,false,"root",false,"a","a","a,b"],
|
|
29
|
+
["div","self",1,true,"b",true,"b","b","a,b"],
|
|
30
|
+
["div","self",2,false,"root",false,"a","a","a,b"],
|
|
31
|
+
["div","self",2,true,"b",true,"b","b","a,b"],
|
|
32
|
+
["div","self",3,false,"b",false,null,"b","a,b"],
|
|
33
|
+
["div","self",3,true,"b",true,"b","b","a,b"],
|
|
34
|
+
["div","sibling",0,false,"root",false,"b","b","b"],
|
|
35
|
+
["div","sibling",0,true,"root",true,"root","root","b"],
|
|
36
|
+
["div","sibling",1,false,"root",false,"b","b","b"],
|
|
37
|
+
["div","sibling",1,true,"a",true,"a","a","b"],
|
|
38
|
+
["div","sibling",2,false,"root",false,"b","b","b"],
|
|
39
|
+
["div","sibling",2,true,"nested",true,"nested","nested","b"],
|
|
40
|
+
["div","sibling",3,false,"root",false,"b","b","b"],
|
|
41
|
+
["div","sibling",3,true,"root",false,"b","b","b"],
|
|
42
|
+
["div","fragment",0,false,"root",false,"x","x","x,y,b"],
|
|
43
|
+
["div","fragment",0,true,"root",true,"root","root","x,y,b"],
|
|
44
|
+
["div","fragment",1,false,"root",false,"x","x","x,y,b"],
|
|
45
|
+
["div","fragment",1,true,"b",true,"b","b","x,y,b"],
|
|
46
|
+
["div","fragment",2,false,"root",false,"x","x","x,y,b"],
|
|
47
|
+
["div","fragment",2,true,"b",true,"b","b","x,y,b"],
|
|
48
|
+
["div","fragment",3,false,"b",false,null,"b","x,y,b"],
|
|
49
|
+
["div","fragment",3,true,"b",true,"b","b","x,y,b"],
|
|
50
|
+
["div","descendant",0,false,"root",false,"nested","nested","nested,b"],
|
|
51
|
+
["div","descendant",0,true,"root",true,"root","root","nested,b"],
|
|
52
|
+
["div","descendant",1,false,"root",false,"nested","nested","nested,b"],
|
|
53
|
+
["div","descendant",1,true,"b",true,"b","b","nested,b"],
|
|
54
|
+
["div","descendant",2,false,"root",false,"nested","nested","nested,b"],
|
|
55
|
+
["div","descendant",2,true,"b",true,"b","b","nested,b"],
|
|
56
|
+
["div","descendant",3,false,"b",false,null,"b","nested,b"],
|
|
57
|
+
["div","descendant",3,true,"b",true,"b","b","nested,b"],
|
|
58
|
+
["div","multiple",0,false,"root",false,"x","x","x,a,b"],
|
|
59
|
+
["div","multiple",0,true,"root",true,"root","root","x,a,b"],
|
|
60
|
+
["div","multiple",1,false,"root",false,"x","x","x,a,b"],
|
|
61
|
+
["div","multiple",1,true,"root",false,"x","x","x,a,b"],
|
|
62
|
+
["div","multiple",2,false,"root",false,"x","x","x,a,b"],
|
|
63
|
+
["div","multiple",2,true,"root",false,"x","x","x,a,b"],
|
|
64
|
+
["div","multiple",3,false,"root",false,"x","x","x,a,b"],
|
|
65
|
+
["div","multiple",3,true,"root",false,"x","x","x,a,b"],
|
|
66
|
+
["form","remove",0,false,"root",false,"b","b","b"],
|
|
67
|
+
["form","remove",0,true,"root",true,"root","root","b"],
|
|
68
|
+
["form","remove",1,false,"root",false,"b","b","b"],
|
|
69
|
+
["form","remove",1,true,"b",true,"b","b","b"],
|
|
70
|
+
["form","remove",2,false,"root",false,"b","b","b"],
|
|
71
|
+
["form","remove",2,true,"b",true,"b","b","b"],
|
|
72
|
+
["form","remove",3,false,"b",false,null,"b","b"],
|
|
73
|
+
["form","remove",3,true,"b",true,"b","b","b"],
|
|
74
|
+
["form","replace",0,false,"root",false,"x","x","x,b"],
|
|
75
|
+
["form","replace",0,true,"root",true,"root","root","x,b"],
|
|
76
|
+
["form","replace",1,false,"root",false,"x","x","x,b"],
|
|
77
|
+
["form","replace",1,true,"b",true,"b","b","x,b"],
|
|
78
|
+
["form","replace",2,false,"root",false,"x","x","x,b"],
|
|
79
|
+
["form","replace",2,true,"b",true,"b","b","x,b"],
|
|
80
|
+
["form","replace",3,false,"b",false,null,"b","x,b"],
|
|
81
|
+
["form","replace",3,true,"b",true,"b","b","x,b"],
|
|
82
|
+
["form","replaceWith",0,false,"root",false,"x","x","x,b"],
|
|
83
|
+
["form","replaceWith",0,true,"root",true,"root","root","x,b"],
|
|
84
|
+
["form","replaceWith",1,false,"root",false,"x","x","x,b"],
|
|
85
|
+
["form","replaceWith",1,true,"b",true,"b","b","x,b"],
|
|
86
|
+
["form","replaceWith",2,false,"root",false,"x","x","x,b"],
|
|
87
|
+
["form","replaceWith",2,true,"b",true,"b","b","x,b"],
|
|
88
|
+
["form","replaceWith",3,false,"b",false,null,"b","x,b"],
|
|
89
|
+
["form","replaceWith",3,true,"b",true,"b","b","x,b"],
|
|
90
|
+
["form","self",0,false,"root",false,"a","a","a,b"],
|
|
91
|
+
["form","self",0,true,"root",true,"root","root","a,b"],
|
|
92
|
+
["form","self",1,false,"root",false,"a","a","a,b"],
|
|
93
|
+
["form","self",1,true,"b",true,"b","b","a,b"],
|
|
94
|
+
["form","self",2,false,"root",false,"a","a","a,b"],
|
|
95
|
+
["form","self",2,true,"b",true,"b","b","a,b"],
|
|
96
|
+
["form","self",3,false,"b",false,null,"b","a,b"],
|
|
97
|
+
["form","self",3,true,"b",true,"b","b","a,b"],
|
|
98
|
+
["form","sibling",0,false,"root",false,"b","b","b"],
|
|
99
|
+
["form","sibling",0,true,"root",true,"root","root","b"],
|
|
100
|
+
["form","sibling",1,false,"root",false,"b","b","b"],
|
|
101
|
+
["form","sibling",1,true,"a",true,"a","a","b"],
|
|
102
|
+
["form","sibling",2,false,"root",false,"b","b","b"],
|
|
103
|
+
["form","sibling",2,true,"nested",true,"nested","nested","b"],
|
|
104
|
+
["form","sibling",3,false,"root",false,"b","b","b"],
|
|
105
|
+
["form","sibling",3,true,"root",false,"b","b","b"],
|
|
106
|
+
["form","fragment",0,false,"root",false,"x","x","x,y,b"],
|
|
107
|
+
["form","fragment",0,true,"root",true,"root","root","x,y,b"],
|
|
108
|
+
["form","fragment",1,false,"root",false,"x","x","x,y,b"],
|
|
109
|
+
["form","fragment",1,true,"b",true,"b","b","x,y,b"],
|
|
110
|
+
["form","fragment",2,false,"root",false,"x","x","x,y,b"],
|
|
111
|
+
["form","fragment",2,true,"b",true,"b","b","x,y,b"],
|
|
112
|
+
["form","fragment",3,false,"b",false,null,"b","x,y,b"],
|
|
113
|
+
["form","fragment",3,true,"b",true,"b","b","x,y,b"],
|
|
114
|
+
["form","descendant",0,false,"root",false,"nested","nested","nested,b"],
|
|
115
|
+
["form","descendant",0,true,"root",true,"root","root","nested,b"],
|
|
116
|
+
["form","descendant",1,false,"root",false,"nested","nested","nested,b"],
|
|
117
|
+
["form","descendant",1,true,"b",true,"b","b","nested,b"],
|
|
118
|
+
["form","descendant",2,false,"root",false,"nested","nested","nested,b"],
|
|
119
|
+
["form","descendant",2,true,"b",true,"b","b","nested,b"],
|
|
120
|
+
["form","descendant",3,false,"b",false,null,"b","nested,b"],
|
|
121
|
+
["form","descendant",3,true,"b",true,"b","b","nested,b"],
|
|
122
|
+
["form","multiple",0,false,"root",false,"x","x","x,a,b"],
|
|
123
|
+
["form","multiple",0,true,"root",true,"root","root","x,a,b"],
|
|
124
|
+
["form","multiple",1,false,"root",false,"x","x","x,a,b"],
|
|
125
|
+
["form","multiple",1,true,"root",false,"x","x","x,a,b"],
|
|
126
|
+
["form","multiple",2,false,"root",false,"x","x","x,a,b"],
|
|
127
|
+
["form","multiple",2,true,"root",false,"x","x","x,a,b"],
|
|
128
|
+
["form","multiple",3,false,"root",false,"x","x","x,a,b"],
|
|
129
|
+
["form","multiple",3,true,"root",false,"x","x","x,a,b"],
|
|
130
|
+
["select","remove",0,false,"root",false,"b","b","b"],
|
|
131
|
+
["select","remove",0,true,"root",true,"root","root","b"],
|
|
132
|
+
["select","remove",1,false,"root",false,"b","b","b"],
|
|
133
|
+
["select","remove",1,true,"b",true,"b","b","b"],
|
|
134
|
+
["select","remove",2,false,"root",false,"b","b","b"],
|
|
135
|
+
["select","remove",2,true,"b",true,"b","b","b"],
|
|
136
|
+
["select","remove",3,false,"b",false,null,"b","b"],
|
|
137
|
+
["select","remove",3,true,"b",true,"b","b","b"],
|
|
138
|
+
["select","replace",0,false,"root",false,"x","x","x,b"],
|
|
139
|
+
["select","replace",0,true,"root",true,"root","root","x,b"],
|
|
140
|
+
["select","replace",1,false,"root",false,"x","x","x,b"],
|
|
141
|
+
["select","replace",1,true,"b",true,"b","b","x,b"],
|
|
142
|
+
["select","replace",2,false,"root",false,"x","x","x,b"],
|
|
143
|
+
["select","replace",2,true,"b",true,"b","b","x,b"],
|
|
144
|
+
["select","replace",3,false,"b",false,null,"b","x,b"],
|
|
145
|
+
["select","replace",3,true,"b",true,"b","b","x,b"],
|
|
146
|
+
["select","replaceWith",0,false,"root",false,"x","x","x,b"],
|
|
147
|
+
["select","replaceWith",0,true,"root",true,"root","root","x,b"],
|
|
148
|
+
["select","replaceWith",1,false,"root",false,"x","x","x,b"],
|
|
149
|
+
["select","replaceWith",1,true,"b",true,"b","b","x,b"],
|
|
150
|
+
["select","replaceWith",2,false,"root",false,"x","x","x,b"],
|
|
151
|
+
["select","replaceWith",2,true,"b",true,"b","b","x,b"],
|
|
152
|
+
["select","replaceWith",3,false,"b",false,null,"b","x,b"],
|
|
153
|
+
["select","replaceWith",3,true,"b",true,"b","b","x,b"],
|
|
154
|
+
["select","self",0,false,"root",false,"a","a","a,b"],
|
|
155
|
+
["select","self",0,true,"root",true,"root","root","a,b"],
|
|
156
|
+
["select","self",1,false,"root",false,"a","a","a,b"],
|
|
157
|
+
["select","self",1,true,"b",true,"b","b","a,b"],
|
|
158
|
+
["select","self",2,false,"root",false,"a","a","a,b"],
|
|
159
|
+
["select","self",2,true,"b",true,"b","b","a,b"],
|
|
160
|
+
["select","self",3,false,"b",false,null,"b","a,b"],
|
|
161
|
+
["select","self",3,true,"b",true,"b","b","a,b"],
|
|
162
|
+
["select","sibling",0,false,"root",false,"b","b","b"],
|
|
163
|
+
["select","sibling",0,true,"root",true,"root","root","b"],
|
|
164
|
+
["select","sibling",1,false,"root",false,"b","b","b"],
|
|
165
|
+
["select","sibling",1,true,"a",true,"a","a","b"],
|
|
166
|
+
["select","sibling",2,false,"root",false,"b","b","b"],
|
|
167
|
+
["select","sibling",2,true,"nested",true,"nested","nested","b"],
|
|
168
|
+
["select","sibling",3,false,"root",false,"b","b","b"],
|
|
169
|
+
["select","sibling",3,true,"root",false,"b","b","b"],
|
|
170
|
+
["select","fragment",0,false,"root",false,"x","x","x,y,b"],
|
|
171
|
+
["select","fragment",0,true,"root",true,"root","root","x,y,b"],
|
|
172
|
+
["select","fragment",1,false,"root",false,"x","x","x,y,b"],
|
|
173
|
+
["select","fragment",1,true,"b",true,"b","b","x,y,b"],
|
|
174
|
+
["select","fragment",2,false,"root",false,"x","x","x,y,b"],
|
|
175
|
+
["select","fragment",2,true,"b",true,"b","b","x,y,b"],
|
|
176
|
+
["select","fragment",3,false,"b",false,null,"b","x,y,b"],
|
|
177
|
+
["select","fragment",3,true,"b",true,"b","b","x,y,b"],
|
|
178
|
+
["select","descendant",0,false,"root",false,"nested","nested","nested,b"],
|
|
179
|
+
["select","descendant",0,true,"root",true,"root","root","nested,b"],
|
|
180
|
+
["select","descendant",1,false,"root",false,"nested","nested","nested,b"],
|
|
181
|
+
["select","descendant",1,true,"b",true,"b","b","nested,b"],
|
|
182
|
+
["select","descendant",2,false,"root",false,"nested","nested","nested,b"],
|
|
183
|
+
["select","descendant",2,true,"b",true,"b","b","nested,b"],
|
|
184
|
+
["select","descendant",3,false,"b",false,null,"b","nested,b"],
|
|
185
|
+
["select","descendant",3,true,"b",true,"b","b","nested,b"],
|
|
186
|
+
["select","multiple",0,false,"root",false,"x","x","x,a,b"],
|
|
187
|
+
["select","multiple",0,true,"root",true,"root","root","x,a,b"],
|
|
188
|
+
["select","multiple",1,false,"root",false,"x","x","x,a,b"],
|
|
189
|
+
["select","multiple",1,true,"root",false,"x","x","x,a,b"],
|
|
190
|
+
["select","multiple",2,false,"root",false,"x","x","x,a,b"],
|
|
191
|
+
["select","multiple",2,true,"root",false,"x","x","x,a,b"],
|
|
192
|
+
["select","multiple",3,false,"root",false,"x","x","x,a,b"],
|
|
193
|
+
["select","multiple",3,true,"root",false,"x","x","x,a,b"],
|
|
194
|
+
["mask","0",0],
|
|
195
|
+
["mask","1",1],
|
|
196
|
+
["mask","-1",4294967295],
|
|
197
|
+
["mask","4294967297",1],
|
|
198
|
+
["mask","NaN",0],
|
|
199
|
+
["mask","Infinity",0],
|
|
200
|
+
["mask","1",1],
|
|
201
|
+
["mask","null",0],
|
|
202
|
+
["mask","1","TypeError"],
|
|
203
|
+
["mask","1","TypeError"],
|
|
204
|
+
["mask","Symbol(x)","TypeError"],
|
|
205
|
+
["filter","1",true],
|
|
206
|
+
["filter","65537",true],
|
|
207
|
+
["filter","-65535",true],
|
|
208
|
+
["filter","NaN",false],
|
|
209
|
+
["filter","Infinity",false],
|
|
210
|
+
["filter","1",true],
|
|
211
|
+
["filter","null",false],
|
|
212
|
+
["filter","1","TypeError"],
|
|
213
|
+
["filter","1","TypeError"],
|
|
214
|
+
["filter","Symbol(x)","TypeError"],
|
|
215
|
+
["walk","element",0,"accept","",""],
|
|
216
|
+
["walk","element",0,"reject","",""],
|
|
217
|
+
["walk","element",0,"skip","",""],
|
|
218
|
+
["walk","element",0,"alternating","",""],
|
|
219
|
+
["walk","element",1,"accept","1,1","1,1"],
|
|
220
|
+
["walk","element",1,"reject","",""],
|
|
221
|
+
["walk","element",1,"skip","",""],
|
|
222
|
+
["walk","element",1,"alternating","",""],
|
|
223
|
+
["walk","element",4,"accept","3","3"],
|
|
224
|
+
["walk","element",4,"reject","",""],
|
|
225
|
+
["walk","element",4,"skip","",""],
|
|
226
|
+
["walk","element",4,"alternating","3","3"],
|
|
227
|
+
["walk","element",128,"accept","8","8"],
|
|
228
|
+
["walk","element",128,"reject","",""],
|
|
229
|
+
["walk","element",128,"skip","",""],
|
|
230
|
+
["walk","element",128,"alternating","8","8"],
|
|
231
|
+
["walk","element",4294967295,"accept","1,1,3,8","8,3,1,1"],
|
|
232
|
+
["walk","element",4294967295,"reject","",""],
|
|
233
|
+
["walk","element",4294967295,"skip","",""],
|
|
234
|
+
["walk","element",4294967295,"alternating","3,8","8,3"],
|
|
235
|
+
["walk","fragment",0,"accept","",""],
|
|
236
|
+
["walk","fragment",0,"reject","",""],
|
|
237
|
+
["walk","fragment",0,"skip","",""],
|
|
238
|
+
["walk","fragment",0,"alternating","",""],
|
|
239
|
+
["walk","fragment",1,"accept","1","1"],
|
|
240
|
+
["walk","fragment",1,"reject","",""],
|
|
241
|
+
["walk","fragment",1,"skip","",""],
|
|
242
|
+
["walk","fragment",1,"alternating","",""],
|
|
243
|
+
["walk","fragment",4,"accept","3","3"],
|
|
244
|
+
["walk","fragment",4,"reject","",""],
|
|
245
|
+
["walk","fragment",4,"skip","",""],
|
|
246
|
+
["walk","fragment",4,"alternating","3","3"],
|
|
247
|
+
["walk","fragment",128,"accept","8","8"],
|
|
248
|
+
["walk","fragment",128,"reject","",""],
|
|
249
|
+
["walk","fragment",128,"skip","",""],
|
|
250
|
+
["walk","fragment",128,"alternating","8","8"],
|
|
251
|
+
["walk","fragment",4294967295,"accept","11,1,3,8","8,3,1,11"],
|
|
252
|
+
["walk","fragment",4294967295,"reject","",""],
|
|
253
|
+
["walk","fragment",4294967295,"skip","",""],
|
|
254
|
+
["walk","fragment",4294967295,"alternating","11,3,8","8,3,11"],
|
|
255
|
+
["walk","text",0,"accept","",""],
|
|
256
|
+
["walk","text",0,"reject","",""],
|
|
257
|
+
["walk","text",0,"skip","",""],
|
|
258
|
+
["walk","text",0,"alternating","",""],
|
|
259
|
+
["walk","text",1,"accept","",""],
|
|
260
|
+
["walk","text",1,"reject","",""],
|
|
261
|
+
["walk","text",1,"skip","",""],
|
|
262
|
+
["walk","text",1,"alternating","",""],
|
|
263
|
+
["walk","text",4,"accept","3","3"],
|
|
264
|
+
["walk","text",4,"reject","",""],
|
|
265
|
+
["walk","text",4,"skip","",""],
|
|
266
|
+
["walk","text",4,"alternating","3","3"],
|
|
267
|
+
["walk","text",128,"accept","",""],
|
|
268
|
+
["walk","text",128,"reject","",""],
|
|
269
|
+
["walk","text",128,"skip","",""],
|
|
270
|
+
["walk","text",128,"alternating","",""],
|
|
271
|
+
["walk","text",4294967295,"accept","3","3"],
|
|
272
|
+
["walk","text",4294967295,"reject","",""],
|
|
273
|
+
["walk","text",4294967295,"skip","",""],
|
|
274
|
+
["walk","text",4294967295,"alternating","3","3"],
|
|
275
|
+
["walk","comment",0,"accept","",""],
|
|
276
|
+
["walk","comment",0,"reject","",""],
|
|
277
|
+
["walk","comment",0,"skip","",""],
|
|
278
|
+
["walk","comment",0,"alternating","",""],
|
|
279
|
+
["walk","comment",1,"accept","",""],
|
|
280
|
+
["walk","comment",1,"reject","",""],
|
|
281
|
+
["walk","comment",1,"skip","",""],
|
|
282
|
+
["walk","comment",1,"alternating","",""],
|
|
283
|
+
["walk","comment",4,"accept","",""],
|
|
284
|
+
["walk","comment",4,"reject","",""],
|
|
285
|
+
["walk","comment",4,"skip","",""],
|
|
286
|
+
["walk","comment",4,"alternating","",""],
|
|
287
|
+
["walk","comment",128,"accept","8","8"],
|
|
288
|
+
["walk","comment",128,"reject","",""],
|
|
289
|
+
["walk","comment",128,"skip","",""],
|
|
290
|
+
["walk","comment",128,"alternating","8","8"],
|
|
291
|
+
["walk","comment",4294967295,"accept","8","8"],
|
|
292
|
+
["walk","comment",4294967295,"reject","",""],
|
|
293
|
+
["walk","comment",4294967295,"skip","",""],
|
|
294
|
+
["walk","comment",4294967295,"alternating","8","8"],
|
|
295
|
+
["walk","doctype",0,"accept","",""],
|
|
296
|
+
["walk","doctype",0,"reject","",""],
|
|
297
|
+
["walk","doctype",0,"skip","",""],
|
|
298
|
+
["walk","doctype",0,"alternating","",""],
|
|
299
|
+
["walk","doctype",1,"accept","",""],
|
|
300
|
+
["walk","doctype",1,"reject","",""],
|
|
301
|
+
["walk","doctype",1,"skip","",""],
|
|
302
|
+
["walk","doctype",1,"alternating","",""],
|
|
303
|
+
["walk","doctype",4,"accept","",""],
|
|
304
|
+
["walk","doctype",4,"reject","",""],
|
|
305
|
+
["walk","doctype",4,"skip","",""],
|
|
306
|
+
["walk","doctype",4,"alternating","",""],
|
|
307
|
+
["walk","doctype",128,"accept","",""],
|
|
308
|
+
["walk","doctype",128,"reject","",""],
|
|
309
|
+
["walk","doctype",128,"skip","",""],
|
|
310
|
+
["walk","doctype",128,"alternating","",""],
|
|
311
|
+
["walk","doctype",4294967295,"accept","10","10"],
|
|
312
|
+
["walk","doctype",4294967295,"reject","",""],
|
|
313
|
+
["walk","doctype",4294967295,"skip","",""],
|
|
314
|
+
["walk","doctype",4294967295,"alternating","10","10"]
|
|
315
|
+
]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
test('fractional font sizes retain the final glyph at different device scales',async()=>{
|
|
3
|
+
const child=Bun.spawn([process.execPath,new URL('./fixtures/fractional-text-scale.js',import.meta.url).pathname],{stdout:'pipe',stderr:'pipe'});
|
|
4
|
+
const [exit,stderr]=await Promise.all([child.exited,new Response(child.stderr).text()]);
|
|
5
|
+
expect({exit,stderr}).toEqual({exit:0,stderr:''});
|
|
6
|
+
},30000);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {test, expect} from 'bun:test';
|
|
2
|
+
import Window from '../vendor/happy-dom/packages/happy-dom/src/window/Window.ts';
|
|
3
|
+
import {createInertWindow} from '../lib/happy-dom/parser.js';
|
|
4
|
+
|
|
5
|
+
test('inert links leave existing and subsequent ordinary windows resource loading intact', async () => {
|
|
6
|
+
const requests=[];
|
|
7
|
+
const server=Bun.serve({port:0,fetch(req){requests.push(new URL(req.url).pathname);return new Response('body{color:red}',{headers:{'content-type':'text/css'}})}});
|
|
8
|
+
const windows=[];
|
|
9
|
+
const ordinary=()=>{const w=new Window({url:server.url.href});windows.push(w);return w};
|
|
10
|
+
const addLink=(w,path)=>{
|
|
11
|
+
const link=w.document.createElement('link'),events=[];
|
|
12
|
+
link.rel='stylesheet';link.href=path;
|
|
13
|
+
link.onload=()=>events.push('load');link.onerror=()=>events.push('error');
|
|
14
|
+
w.document.head.append(link);return {link,events};
|
|
15
|
+
};
|
|
16
|
+
try {
|
|
17
|
+
const before=ordinary();
|
|
18
|
+
const inert=createInertWindow(server.url.href);windows.push(inert);
|
|
19
|
+
const after=ordinary();
|
|
20
|
+
// Repeated setup must not replace the saved native hooks with wrappers.
|
|
21
|
+
windows.push(createInertWindow(server.url.href));
|
|
22
|
+
for(const [w,path] of [[before,'/before.css'],[after,'/after.css']]){
|
|
23
|
+
const {link,events}=addLink(w,path);
|
|
24
|
+
await w.happyDOM.waitUntilComplete();
|
|
25
|
+
expect(events).toEqual(['load']);expect(link.sheet).not.toBeNull();
|
|
26
|
+
link.href=path+'?changed';await w.happyDOM.waitUntilComplete();
|
|
27
|
+
expect(events).toEqual(['load','load']);
|
|
28
|
+
}
|
|
29
|
+
const {link,events}=addLink(inert,'/inert.css');
|
|
30
|
+
link.href='/inert-changed.css';
|
|
31
|
+
await inert.happyDOM.waitUntilComplete();
|
|
32
|
+
expect(events).toEqual([]);expect(link.sheet).toBeNull();
|
|
33
|
+
expect(requests).toEqual(['/before.css','/before.css','/after.css','/after.css']);
|
|
34
|
+
} finally {
|
|
35
|
+
await Promise.all(windows.map(w=>w.happyDOM.abort()));server.stop(true);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { afterAll, beforeAll, expect, test } from 'bun:test';
|
|
2
|
+
import { RendererHost } from '../lib/renderer/host.js';
|
|
3
|
+
import { nodeIteratorCases } from './fixtures/node-iterator-cases.js';
|
|
4
|
+
import chromium from './fixtures/node-iterator-chromium.json';
|
|
5
|
+
|
|
6
|
+
const host = new RendererHost({timeout: 20000, fetch: async () => new Response('')});
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
await host.call('loadDocument', {url:'https://iterator.test/',source:'<body></body>',contentType:'text/html',status:200});
|
|
9
|
+
await host.call('whenLoaded');
|
|
10
|
+
});
|
|
11
|
+
afterAll(() => host.close());
|
|
12
|
+
const evaluate = async expression => JSON.parse(await host.call('evaluate', `JSON.stringify(${expression})`));
|
|
13
|
+
|
|
14
|
+
test('NodeIterator traversal, replacement and WebIDL conversion match Chromium and DOM', async () => {
|
|
15
|
+
const actual = await evaluate(`(${nodeIteratorCases})()`);
|
|
16
|
+
const expected = chromium.map(row => {
|
|
17
|
+
// Chromium retains a detached reference when replacing a node with its
|
|
18
|
+
// following sibling while the pointer is before it. DOM replace adopts
|
|
19
|
+
// (detaches) that sibling first, then removes the old node: the repaired
|
|
20
|
+
// reference must be root, not the now-detached old node/descendant.
|
|
21
|
+
// https://dom.spec.whatwg.org/#concept-node-replace steps 6–7
|
|
22
|
+
if (row[1] === 'sibling' && [1,2].includes(row[2]) && row[3]) {
|
|
23
|
+
return [...row.slice(0,4), 'root', false, 'b', 'b', 'b'];
|
|
24
|
+
}
|
|
25
|
+
return row;
|
|
26
|
+
});
|
|
27
|
+
expect(actual).toEqual(expected);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('failed replacements leave the tree and iterator position unchanged', async () => {
|
|
31
|
+
expect(await evaluate(`(() => {
|
|
32
|
+
const results=[];
|
|
33
|
+
for(const kind of ['ancestor','missing','document','textDocument','twoElements']) {
|
|
34
|
+
const root=document.createElement('div');root.id='root';root.innerHTML='<b id="a"></b><i id="b"></i>';
|
|
35
|
+
const doc=document.implementation.createHTMLDocument('');
|
|
36
|
+
const parent=kind==='textDocument'||kind==='twoElements'?doc:root;
|
|
37
|
+
const old=parent.firstChild;
|
|
38
|
+
const it=document.createNodeIterator(parent);it.nextNode();it.nextNode();
|
|
39
|
+
const reference=it.referenceNode,before=it.pointerBeforeReferenceNode;
|
|
40
|
+
const children=[...parent.childNodes];
|
|
41
|
+
let incoming=kind==='ancestor'?root:kind==='document'?document:document.createElement('em');
|
|
42
|
+
if(kind==='textDocument')incoming=document.createTextNode('invalid');
|
|
43
|
+
if(kind==='twoElements'){incoming=document.createDocumentFragment();incoming.append(document.createElement('i'),document.createElement('b'));}
|
|
44
|
+
let error;
|
|
45
|
+
try{parent.replaceChild(incoming,kind==='missing'?document.createElement('b'):old)}catch(e){error=e.name}
|
|
46
|
+
results.push([kind,error,it.referenceNode===reference,it.pointerBeforeReferenceNode===before,
|
|
47
|
+
children.length===parent.childNodes.length&&children.every((n,i)=>n===parent.childNodes[i])]);
|
|
48
|
+
}
|
|
49
|
+
return results;
|
|
50
|
+
})()`)).toEqual([
|
|
51
|
+
['ancestor','HierarchyRequestError',true,true,true],['missing','NotFoundError',true,true,true],
|
|
52
|
+
['document','HierarchyRequestError',true,true,true],['textDocument','HierarchyRequestError',true,true,true],
|
|
53
|
+
['twoElements','HierarchyRequestError',true,true,true]
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('filter replacement repairs both active and stored iterator cursors', async () => {
|
|
58
|
+
expect(await evaluate(`(() => {
|
|
59
|
+
const root=document.createElement('div');root.id='root';root.innerHTML='<b id="a"></b><i id="b"></i>';
|
|
60
|
+
const it=document.createNodeIterator(root,1,n=>{
|
|
61
|
+
if(n.id==='a'){const replacement=document.createElement('em');replacement.id='x';root.replaceChild(replacement,n)}
|
|
62
|
+
return 1;
|
|
63
|
+
});
|
|
64
|
+
const got=[];for(let n,i=0;i<8&&(n=it.nextNode());i++)got.push(n.id);
|
|
65
|
+
return got;
|
|
66
|
+
})()`)).toEqual(['root','a','x','b']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('batch removal follows DOM tree order and keeps references attached', async () => {
|
|
70
|
+
expect(await evaluate(`(() => {
|
|
71
|
+
const results=[];
|
|
72
|
+
for(const operation of ['replaceChildren','innerHTML','textContent'])for(const before of [false,true]) {
|
|
73
|
+
const root=document.createElement('div');root.innerHTML='<i>a</i><i>b</i>';
|
|
74
|
+
const it=document.createNodeIterator(root);it.nextNode();it.nextNode();it.nextNode();
|
|
75
|
+
if(before)it.previousNode();
|
|
76
|
+
if(operation==='replaceChildren')root.replaceChildren();else root[operation]='';
|
|
77
|
+
results.push([it.referenceNode===root,it.pointerBeforeReferenceNode,it.nextNode()]);
|
|
78
|
+
}
|
|
79
|
+
return results;
|
|
80
|
+
})()`)).toEqual(Array.from({length:6},()=>[true,false,null]));
|
|
81
|
+
});
|
|
@@ -1997,14 +1997,24 @@ test("a click inside an iframe goes to the element under it in the frame's docum
|
|
|
1997
1997
|
test("dynamic linked stylesheets fire load after their rules become available", async () => {
|
|
1998
1998
|
await load(`<div id="target"></div><script>
|
|
1999
1999
|
window.sheetLoaded = false;
|
|
2000
|
+
window.sheetEvents = [];
|
|
2000
2001
|
const link = document.createElement('link');
|
|
2001
2002
|
link.rel = 'stylesheet';
|
|
2002
2003
|
link.href = '/slow.css';
|
|
2003
|
-
|
|
2004
|
+
window.styleTestLink = link;
|
|
2005
|
+
link.onerror = () => sheetEvents.push('error');
|
|
2006
|
+
link.onload = () => { sheetEvents.push('load'); window.sheetLoaded = getComputedStyle(document.body).color; };
|
|
2004
2007
|
document.head.appendChild(link);
|
|
2005
2008
|
</script>`);
|
|
2006
2009
|
for (let i = 0; i < 60 && await evaluate('sheetLoaded') === false; i++) await Bun.sleep(25);
|
|
2007
2010
|
expect(await evaluate('sheetLoaded')).toBe('rgba(255, 0, 0, 1)');
|
|
2011
|
+
expect(await evaluate('sheetEvents.join()')).toBe('load');
|
|
2012
|
+
await evaluate("styleTestLink.href='/missing.css'; window.eventsAfterHref=sheetEvents.join()");
|
|
2013
|
+
expect(await evaluate('eventsAfterHref')).toBe('load');
|
|
2014
|
+
expect(await waitFor("sheetEvents.join() === 'load,error'")).toBe(true);
|
|
2015
|
+
await evaluate("styleTestLink.href='/slow.css'; window.eventsAfterHref=sheetEvents.join()");
|
|
2016
|
+
expect(await evaluate('eventsAfterHref')).toBe('load,error');
|
|
2017
|
+
expect(await waitFor("sheetEvents.join() === 'load,error,load'")).toBe(true);
|
|
2008
2018
|
});
|
|
2009
2019
|
|
|
2010
2020
|
test("decoded dynamic images report natural dimensions and fire load", async () => {
|
|
@@ -2154,3 +2164,108 @@ test("the page scrolls over content that overflows a 100vh layout, not over clip
|
|
|
2154
2164
|
expect(await evaluate(`(scrollTo(0, 99999), [scrollY, Math.round(document.getElementById("end").getBoundingClientRect().bottom)])`))
|
|
2155
2165
|
.toEqual([2090 - 300, 300]);
|
|
2156
2166
|
});
|
|
2167
|
+
|
|
2168
|
+
test("NodeIterator includes its root, preserves direction, and skips rejected nodes without pruning", async () => {
|
|
2169
|
+
await load('<div id="root"><p id="a"><b id="child"></b></p><!--note--><p id="b"></p></div>');
|
|
2170
|
+
expect(await evaluate(`(() => {
|
|
2171
|
+
const root = document.getElementById('root');
|
|
2172
|
+
const filter = {acceptNode(n) {return n.id === 'a' ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT}};
|
|
2173
|
+
const it = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, filter);
|
|
2174
|
+
const initial = [it instanceof NodeIterator,it.root === root,it.referenceNode === root,it.pointerBeforeReferenceNode,it.filter === filter];
|
|
2175
|
+
const forward=[];let n;while(n=it.nextNode())forward.push(n.id);
|
|
2176
|
+
const reverse=[];while(n=it.previousNode())reverse.push(n.id);
|
|
2177
|
+
it.detach();
|
|
2178
|
+
return JSON.stringify([initial,forward,reverse,it.nextNode().id]);
|
|
2179
|
+
})()`)).toBe(JSON.stringify([[true,true,true,true,true],['root','child','b'],['b','child','root'],'root']));
|
|
2180
|
+
});
|
|
2181
|
+
|
|
2182
|
+
test("NodeIterator survives sanitizer removals, reparenting, and rejected reentrant filters", async () => {
|
|
2183
|
+
await load('<div id="root"><i id="a"><b id="child"></b></i><i id="b"></i><i id="c"></i></div>');
|
|
2184
|
+
expect(await evaluate(`(() => {
|
|
2185
|
+
const root=document.getElementById('root'),it=document.createNodeIterator(root,1);
|
|
2186
|
+
it.nextNode();it.nextNode();it.nextNode();
|
|
2187
|
+
root.firstChild.remove();
|
|
2188
|
+
const removed=[it.referenceNode.id,it.nextNode().id];
|
|
2189
|
+
it.previousNode();root.firstChild.remove();
|
|
2190
|
+
const before=[it.referenceNode.id,it.pointerBeforeReferenceNode,it.nextNode().id];
|
|
2191
|
+
document.body.appendChild(root.firstChild);
|
|
2192
|
+
const moved=[it.referenceNode.id,it.nextNode()];
|
|
2193
|
+
let error;const recur=document.createNodeIterator(root,1,()=>{try{recur.nextNode()}catch(e){error=e.name}return 1});
|
|
2194
|
+
recur.nextNode();
|
|
2195
|
+
const fresh=document.createNodeIterator(root,NodeFilter.SHOW_TEXT);
|
|
2196
|
+
root.innerHTML='hello<b>world</b>';
|
|
2197
|
+
return JSON.stringify([removed,before,moved,error,fresh.nextNode().data,fresh.nextNode().data]);
|
|
2198
|
+
})()`)).toBe(JSON.stringify([['root','b'],['c',true,'c'],['root',null],'InvalidStateError','hello','world']));
|
|
2199
|
+
});
|
|
2200
|
+
|
|
2201
|
+
// Expectations independently checked against Chromium Headless Shell.
|
|
2202
|
+
test("NodeIterator repairs an active filter cursor when the candidate or its ancestor is removed", async () => {
|
|
2203
|
+
await load("<body></body>");
|
|
2204
|
+
expect(await evaluate(`JSON.stringify((()=>{const results=[];for(const direction of ['nextNode','previousNode'])for(const remove of ['self','ancestor'])for(const verdict of [1,3]){
|
|
2205
|
+
const root=document.createElement('div');root.id='root';root.innerHTML='<i id="a"></i><section id="parent"><b id="b"></b><b id="c"></b></section><i id="d"></i>';document.body.append(root);
|
|
2206
|
+
let armed=false,once=false;const seen=[];const it=document.createNodeIterator(root,1,n=>{if(armed)seen.push(n.id);if(armed&&!once&&n.id==='b'){once=true;(remove==='self'?n:n.parentNode).remove();return verdict;}return 1});
|
|
2207
|
+
if(direction==='previousNode')while(it.nextNode());armed=true;
|
|
2208
|
+
const got=[];for(let n,k=0;k<15&&(n=it[direction]());k++)got.push(n.id);
|
|
2209
|
+
results.push({direction,remove,verdict,seen,got,reference:it.referenceNode.id,before:it.pointerBeforeReferenceNode});root.remove();
|
|
2210
|
+
}return results.map(r=>[r.direction,r.remove,r.verdict,r.seen.join(','),r.got.join(','),r.reference,r.before])})())`)).toBe(JSON.stringify([
|
|
2211
|
+
["nextNode", "self", 1, "root,a,parent,b,c,d", "root,a,parent,b,c,d", "d", false],
|
|
2212
|
+
["nextNode", "self", 3, "root,a,parent,b,c,d", "root,a,parent,c,d", "d", false],
|
|
2213
|
+
["nextNode", "ancestor", 1, "root,a,parent,b,d", "root,a,parent,b,d", "d", false],
|
|
2214
|
+
["nextNode", "ancestor", 3, "root,a,parent,b,d", "root,a,parent,d", "d", false],
|
|
2215
|
+
["previousNode", "self", 1, "d,c,b,parent,a,root", "d,c,b,parent,a,root", "root", true],
|
|
2216
|
+
["previousNode", "self", 3, "d,c,b,parent,a,root", "d,c,parent,a,root", "root", true],
|
|
2217
|
+
["previousNode", "ancestor", 1, "d,c,b,a,root", "d,c,b,a,root", "root", true],
|
|
2218
|
+
["previousNode", "ancestor", 3, "d,c,b,a,root", "d,c,a,root", "root", true]
|
|
2219
|
+
]));
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2222
|
+
test("NodeIterator releases its active cursor after a filter throws", async () => {
|
|
2223
|
+
await load('<div id="root"><b id="child"></b></div>');
|
|
2224
|
+
expect(await evaluate(`(() => {
|
|
2225
|
+
const root=document.getElementById('root');let first=true;
|
|
2226
|
+
const it=document.createNodeIterator(root,1,()=>{if(first){first=false;throw Error('filter failed')}return 1});
|
|
2227
|
+
let message;try{it.nextNode()}catch(e){message=e.message}
|
|
2228
|
+
return [message,it.nextNode().id,it.nextNode().id].join(',');
|
|
2229
|
+
})()`)).toBe('filter failed,root,child');
|
|
2230
|
+
});
|
|
2231
|
+
|
|
2232
|
+
test("NodeIterator repairs removals through proxy-backed select and form parents", async () => {
|
|
2233
|
+
await load('<body></body>');
|
|
2234
|
+
expect(await evaluate(`(() => {
|
|
2235
|
+
const results=[];
|
|
2236
|
+
for(const tag of ['div','select','form']) for(const outerRoot of [false,true])
|
|
2237
|
+
for(const operation of ['remove','removeChild','innerHTML','reparent']) {
|
|
2238
|
+
const outer=document.createElement('div'),parent=document.createElement(tag);
|
|
2239
|
+
outer.append(parent);document.body.append(outer);
|
|
2240
|
+
parent.innerHTML='<option id="a"></option><option id="b"></option>';
|
|
2241
|
+
const it=document.createNodeIterator(outerRoot?outer:parent,1);
|
|
2242
|
+
while(it.nextNode()?.id!=='a') {}
|
|
2243
|
+
const child=parent.firstChild;
|
|
2244
|
+
if(operation==='remove')child.remove();
|
|
2245
|
+
else if(operation==='removeChild')parent.removeChild(child);
|
|
2246
|
+
else if(operation==='innerHTML')parent.innerHTML='';
|
|
2247
|
+
else outer.append(child);
|
|
2248
|
+
results.push([it.referenceNode===parent,it.nextNode()?.id??null]);
|
|
2249
|
+
outer.remove();
|
|
2250
|
+
}
|
|
2251
|
+
return JSON.stringify(results);
|
|
2252
|
+
})()`)).toBe(JSON.stringify(Array.from({length:6},()=>[
|
|
2253
|
+
[true,'b'],[true,'b'],[true,null],[true,'b']
|
|
2254
|
+
]).flat()));
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
test("NodeIterator validates nullable callback-interface filters at creation", async () => {
|
|
2258
|
+
await load('<body></body>');
|
|
2259
|
+
expect(await evaluate(`(() => {
|
|
2260
|
+
const results=[];
|
|
2261
|
+
for(const filter of [0,1,false,true,'','x',Symbol('filter'),1n]) {
|
|
2262
|
+
try { document.createNodeIterator(document.body,0,filter);results.push('accepted'); }
|
|
2263
|
+
catch(error) { results.push(error.name); }
|
|
2264
|
+
}
|
|
2265
|
+
for(const filter of [null,undefined,()=>1,{acceptNode(){return 1}},{}]) {
|
|
2266
|
+
const it=document.createNodeIterator(document.body,0,filter);
|
|
2267
|
+
results.push(it.filter===(filter??null) && it.nextNode()===null);
|
|
2268
|
+
}
|
|
2269
|
+
return JSON.stringify(results);
|
|
2270
|
+
})()`)).toBe(JSON.stringify([...Array(8).fill('TypeError'),...Array(5).fill(true)]));
|
|
2271
|
+
});
|
package/test/text-layout.test.js
CHANGED
|
@@ -675,3 +675,18 @@ test("an out-of-flow box keeps its static position when the block around its pla
|
|
|
675
675
|
(document) => { document.getElementById("edit").textContent = "a longer paragraph that wraps onto two lines"; });
|
|
676
676
|
expect(rankedLayout(retained)).toEqual(rankedLayout(clean));
|
|
677
677
|
});
|
|
678
|
+
|
|
679
|
+
test("anonymous flex text items do not reuse their parent's intrinsic width", () => {
|
|
680
|
+
const { document, window } = parseHTMLDocument(`<body style="margin:0"><div style="display:flex">
|
|
681
|
+
<span style="display:flex"><a>hash</a>·<time>Today</time>!</span><b>History</b>
|
|
682
|
+
</div></body>`);
|
|
683
|
+
const tree = new RenderTreeBuilder().build(document, new StyleEngine().compute(document));
|
|
684
|
+
const engine = new LayoutEngine({ measureText: text => text.length * 10 });
|
|
685
|
+
for (const width of [500, 180, 500]) {
|
|
686
|
+
const layout = engine.layout(tree, { x:0, y:0, width });
|
|
687
|
+
expect(layout.fragments.map(f => [f.text, f.x, f.width])).toEqual([
|
|
688
|
+
["hash", 0, 40], ["·", 40, 10], ["Today", 50, 50], ["!", 100, 10], ["History", 110, 70],
|
|
689
|
+
]);
|
|
690
|
+
}
|
|
691
|
+
window.happyDOM.abort();
|
|
692
|
+
});
|
|
@@ -34,3 +34,32 @@ test('viewport resize notifies window listeners with fresh geometry and repaints
|
|
|
34
34
|
expect(await host.call('evaluate','propertyEvents')).toBe(2);
|
|
35
35
|
}finally{host.close();}
|
|
36
36
|
},30000);
|
|
37
|
+
|
|
38
|
+
test('same-pixel-size DPR changes relayout a quiet page and update viewport CSS',async()=>{
|
|
39
|
+
const host=new RendererHost({timeout:20000});
|
|
40
|
+
try {
|
|
41
|
+
await host.call('resize',1920,1035,1);
|
|
42
|
+
await host.call('loadDocument',{url:'https://resize.test/',contentType:'text/html',source:`
|
|
43
|
+
<style>body{margin:0}#center{width:1000px;margin:0 auto;height:20px}
|
|
44
|
+
#units{width:50vw;height:10vh}#media{width:10px}
|
|
45
|
+
@media(max-width:1400px){#media{width:30px}}</style>
|
|
46
|
+
<div id="center">Contact — Buninu Linux</div><div id="units"></div><div id="media"></div>`});
|
|
47
|
+
await host.call('whenLoaded');
|
|
48
|
+
const geometry=()=>host.call('evaluate',`JSON.stringify(['center','units','media'].map(id=>{
|
|
49
|
+
const r=document.getElementById(id).getBoundingClientRect();return [r.x,r.width,r.height];
|
|
50
|
+
}))`);
|
|
51
|
+
expect(await geometry()).toBe('[[460,1000,20],[0,960,103.5],[0,10,0]]');
|
|
52
|
+
for(const [width,height,dpr,expected] of [
|
|
53
|
+
[1280,690,1.5,'[[140,1000,20],[0,640,69],[0,30,0]]'],
|
|
54
|
+
[1920,1035,1,'[[460,1000,20],[0,960,103.5],[0,10,0]]'],
|
|
55
|
+
[1280,690,1.5,'[[140,1000,20],[0,640,69],[0,30,0]]'],
|
|
56
|
+
// DPR-only changes repaint without changing CSS geometry.
|
|
57
|
+
[1280,690,2,'[[140,1000,20],[0,640,69],[0,30,0]]'],
|
|
58
|
+
]) {
|
|
59
|
+
await host.call('resize',width,height,dpr);
|
|
60
|
+
expect(await geometry()).toBe(expected);
|
|
61
|
+
const png=Buffer.from(await host.composite('screenshot',{format:'png'}),'base64');
|
|
62
|
+
expect([png.readUInt32BE(16),png.readUInt32BE(20)]).toEqual([width*dpr,height*dpr]);
|
|
63
|
+
}
|
|
64
|
+
}finally{host.close()}
|
|
65
|
+
},30000);
|