Element legacy test passing

This commit is contained in:
Karl Seguin
2025-12-11 12:51:56 +08:00
parent b25e46de2e
commit 34f0857b4f
12 changed files with 356 additions and 44 deletions

View File

@@ -181,3 +181,80 @@
document.cookie = 'IgnoreMy=Ghost; HttpOnly';
testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie);
</script>
<script id=createAttribute>
{
var attr = document.createAttribute('hello');
testing.expectEqual('hello', attr.name);
testing.expectEqual('', attr.value);
testing.withError((err) => {
testing.expectEqual(5, err.code);
testing.expectEqual("InvalidCharacterError", err.name);
}, () => document.createAttribute(''));
testing.withError((err) => {
testing.expectEqual(5, err.code);
testing.expectEqual("InvalidCharacterError", err.name);
}, () => document.createAttribute('.over'));
}
</script>
<script id=append>
{
const doc = new Document();
const html = doc.createElement('html');
const body = doc.createElement('body');
const div1 = doc.createElement('div');
const div2 = doc.createElement('div');
doc.append(html);
testing.expectEqual(1, doc.childNodes.length);
testing.expectEqual(html, doc.childNodes[0]);
html.append(body);
testing.expectEqual(1, html.childNodes.length);
testing.expectEqual(body, html.childNodes[0]);
body.append(div1, div2);
testing.expectEqual(2, body.childNodes.length);
testing.expectEqual(div1, body.childNodes[0]);
testing.expectEqual(div2, body.childNodes[1]);
body.append('text node');
testing.expectEqual(3, body.childNodes.length);
testing.expectEqual(3, body.childNodes[2].nodeType);
testing.expectEqual('text node', body.childNodes[2].textContent);
}
</script>
<script id=prepend>
{
const doc = new Document();
const html = doc.createElement('html');
const body = doc.createElement('body');
const div1 = doc.createElement('div');
const div2 = doc.createElement('div');
const div3 = doc.createElement('div');
doc.prepend(html);
testing.expectEqual(1, doc.childNodes.length);
testing.expectEqual(html, doc.childNodes[0]);
html.prepend(body);
testing.expectEqual(1, html.childNodes.length);
testing.expectEqual(body, html.childNodes[0]);
body.append(div1);
body.prepend(div2, div3);
testing.expectEqual(3, body.childNodes.length);
testing.expectEqual(div2, body.childNodes[0]);
testing.expectEqual(div3, body.childNodes[1]);
testing.expectEqual(div1, body.childNodes[2]);
body.prepend('text node');
testing.expectEqual(4, body.childNodes.length);
testing.expectEqual(3, body.childNodes[0].nodeType);
testing.expectEqual('text node', body.childNodes[0].textContent);
}
</script>

View File

@@ -165,3 +165,84 @@
testing.expectEqual(false, div.hasAttributes());
}
</script>
<script id=invalidAttributeNames>
{
const div = document.createElement('div');
testing.withError((err) => {
testing.expectEqual(5, err.code);
testing.expectEqual("InvalidCharacterError", err.name);
}, () => div.setAttribute('0abc', 'value'));
testing.withError((err) => {
testing.expectEqual(5, err.code);
testing.expectEqual("InvalidCharacterError", err.name);
}, () => div.setAttribute('123', 'value'));
testing.withError((err) => {
testing.expectEqual(5, err.code);
testing.expectEqual("InvalidCharacterError", err.name);
}, () => div.setAttribute('-invalid', 'value'));
testing.withError((err) => {
testing.expectEqual(5, err.code);
testing.expectEqual("InvalidCharacterError", err.name);
}, () => div.setAttribute('.foo', 'value'));
testing.withError((err) => {
testing.expectEqual(5, err.code);
testing.expectEqual("InvalidCharacterError", err.name);
}, () => div.setAttribute('my attr', 'value'));
testing.withError((err) => {
testing.expectEqual(5, err.code);
testing.expectEqual("InvalidCharacterError", err.name);
}, () => div.setAttribute('my@attr', 'value'));
testing.withError((err) => {
testing.expectEqual(5, err.code);
testing.expectEqual("InvalidCharacterError", err.name);
}, () => div.setAttribute('attr!', 'value'));
testing.withError((err) => {
testing.expectEqual(5, err.code);
testing.expectEqual("InvalidCharacterError", err.name);
}, () => div.setAttribute('~', 'value'));
testing.withError((err) => {
testing.expectEqual(5, err.code);
testing.expectEqual("InvalidCharacterError", err.name);
}, () => div.setAttribute('', 'value'));
div.setAttribute('valid-name', 'value1');
testing.expectEqual('value1', div.getAttribute('valid-name'));
div.setAttribute('valid_name', 'value2');
testing.expectEqual('value2', div.getAttribute('valid_name'));
div.setAttribute('valid.name', 'value3');
testing.expectEqual('value3', div.getAttribute('valid.name'));
div.setAttribute('a123', 'value4');
testing.expectEqual('value4', div.getAttribute('a123'));
div.setAttribute('data-test-123', 'value5');
testing.expectEqual('value5', div.getAttribute('data-test-123'));
testing.withError((err) => {
testing.expectEqual(5, err.code);
testing.expectEqual("InvalidCharacterError", err.name);
}, () => div.toggleAttribute('.invalid'));
testing.withError((err) => {
testing.expectEqual(5, err.code);
testing.expectEqual("InvalidCharacterError", err.name);
}, () => div.toggleAttribute('0invalid'));
testing.withError((err) => {
testing.expectEqual(5, err.code);
testing.expectEqual("InvalidCharacterError", err.name);
}, () => div.toggleAttribute('-invalid'));
}
</script>

View File

@@ -9,7 +9,7 @@
<span id="span1">Span 1</span>
<p id="p2">Paragraph 2</p>
</div>
<div id="empty"></div>
<div id="empty" dir=ltr></div>
<script id="relatedElements">
const container = $('#container');
@@ -65,3 +65,91 @@
p.textContent = 'XAnge\xa0Privacy';
testing.expectEqual('<p>XAnge&nbsp;Privacy</p>', p.outerHTML);
</script>
<script id=element>
{
const empty = $('#empty');
testing.expectEqual('empty', empty.id);
testing.expectEqual('ltr', empty.dir);
// good enough that it doesn't throw
empty.scrollIntoViewIfNeeded();
empty.scrollIntoViewIfNeeded(false);
}
</script>
<script id=before>
{
const parent = document.createElement('div');
const existing = document.createElement('span');
parent.appendChild(existing);
const div1 = document.createElement('div');
const div2 = document.createElement('div');
existing.before(div1, div2);
testing.expectEqual(3, parent.childNodes.length);
testing.expectEqual(div1, parent.childNodes[0]);
testing.expectEqual(div2, parent.childNodes[1]);
testing.expectEqual(existing, parent.childNodes[2]);
existing.before('text node');
testing.expectEqual(4, parent.childNodes.length);
testing.expectEqual(3, parent.childNodes[2].nodeType);
testing.expectEqual('text node', parent.childNodes[2].textContent);
testing.expectEqual(existing, parent.childNodes[3]);
const detached = document.createElement('div');
detached.before(document.createElement('p'));
testing.expectEqual(null, detached.parentNode);
}
</script>
<script id=after>
{
const parent = document.createElement('div');
const existing = document.createElement('span');
parent.appendChild(existing);
const div1 = document.createElement('div');
const div2 = document.createElement('div');
existing.after(div1, div2);
testing.expectEqual(3, parent.childNodes.length);
testing.expectEqual(existing, parent.childNodes[0]);
testing.expectEqual(div1, parent.childNodes[1]);
testing.expectEqual(div2, parent.childNodes[2]);
existing.after('text node');
testing.expectEqual(4, parent.childNodes.length);
testing.expectEqual(existing, parent.childNodes[0]);
testing.expectEqual(3, parent.childNodes[1].nodeType);
testing.expectEqual('text node', parent.childNodes[1].textContent);
const detached = document.createElement('div');
detached.after(document.createElement('p'));
testing.expectEqual(null, detached.parentNode);
}
</script>
<script id=beforeAfterOrdering>
{
const parent = document.createElement('div');
const a = document.createElement('a');
const b = document.createElement('b');
const c = document.createElement('c');
const d = document.createElement('d');
parent.appendChild(b);
parent.appendChild(c);
b.before(a);
c.after(d);
testing.expectEqual(4, parent.childNodes.length);
testing.expectEqual(a, parent.childNodes[0]);
testing.expectEqual(b, parent.childNodes[1]);
testing.expectEqual(c, parent.childNodes[2]);
testing.expectEqual(d, parent.childNodes[3]);
}
</script>

View File

@@ -10,6 +10,8 @@
<script id=querySelector">
const p1 = $('#p1');
testing.expectEqual(null, p1.querySelector('#p1'));
testing.expectError("Syntax Error", () => p1.querySelector(''));
testing.withError((err) => {
testing.expectEqual(12, err.code);

View File

@@ -85,6 +85,9 @@
const all = root.querySelectorAll('*');
testing.expectEqual(true, all.length >= 6);
testing.expectEqual('Item 1', all[0].textContent);
testing.expectEqual('Item 1', all.item(0).textContent);
testing.expectEqual(null, all.item(99));
testing.expectEqual(undefined, all[99]);
const items = root.querySelectorAll('*.item');
testing.expectEqual(4, items.length);

View File

@@ -43,13 +43,11 @@
testing.expectEqual(el2, el2.closest('#closest'));
testing.expectEqual(el2, el2.closest('.ok'));
testing.expectEqual(null, el2.closest('#9000'));
testing.expectEqual(null, el2.closest('.notok'));
const sp = document.createElement('span');
el2.appendChild(sp);
testing.expectEqual(el2, sp.closest('#closest'));
testing.expectEqual(null, sp.closest('#9000'));
</script>
<script id=attributes>
@@ -73,7 +71,7 @@
testing.expectEqual('bar', content.getAttribute('foo'));
testing.expectEqual(['id', 'dir', 'foo'], content.getAttributeNames());
testing.expectError('Error: InvalidCharacterError', () => {
testing.expectError('Error: Invalid Character', () => {
content.setAttribute('.foo', 'invalid')
});
@@ -123,7 +121,6 @@
testing.expectEqual('link', content.querySelector('#link').id);
testing.expectEqual('para', content.querySelector('#para').id);
testing.expectEqual('link', content.querySelector('*').id);
testing.expectEqual(null, content.querySelector(''));
testing.expectEqual('link', content.querySelector('*').id);
testing.expectEqual(null, content.querySelector('#content'));
testing.expectEqual('para', content.querySelector('#para').id);
@@ -166,46 +163,12 @@
testing.expectEqual('<p id="para"> And</p>', $('#para').outerHTML);
</script>
<script id=dimensions>
const para = document.getElementById('para');
testing.expectEqual(5, para.clientWidth);
testing.expectEqual(5, para.clientHeight);
let r1 = document.getElementById('para').getBoundingClientRect();
testing.expectEqual(0, r1.x);
testing.expectEqual(0, r1.y);
testing.expectEqual(5, r1.width);
testing.expectEqual(5, r1.height);
let r2 = document.getElementById('content').getBoundingClientRect();
testing.expectEqual(5, r2.x);
testing.expectEqual(0, r2.y);
testing.expectEqual(5, r2.width);
testing.expectEqual(5, r2.height);
let r3 = document.getElementById('para').getBoundingClientRect();
testing.expectEqual(0, r3.x);
testing.expectEqual(0, r3.y);
testing.expectEqual(5, r3.width);
testing.expectEqual(5, r3.height);
testing.expectEqual(10, para.clientWidth);
testing.expectEqual(5, para.clientHeight);
let r4 = document.createElement('div').getBoundingClientRect();
testing.expectEqual(0, r4.x);
testing.expectEqual(0, r4.y);
testing.expectEqual(0, r4.width);
testing.expectEqual(0, r4.height);
</script>
<script id=matches>
const el = document.createElement('div');
el.id = 'matches';
el.className = 'ok';
testing.expectEqual(true, el.matches('#matches'));
testing.expectEqual(true, el.matches('.ok'));
testing.expectEqual(false, el.matches('#9000'));
testing.expectEqual(false, el.matches('.notok'));
</script>
@@ -338,4 +301,4 @@
const p = document.createElement('p');
p.textContent = 'XAnge\xa0Privacy';
testing.expectEqual('<p>XAnge&nbsp;Privacy</p>', p.outerHTML);
</script>
</script> -->

View File

@@ -57,7 +57,7 @@ pub fn getName(self: *const DOMException) []const u8 {
pub fn getMessage(self: *const DOMException) []const u8 {
return switch (self._code) {
.none => "",
.invalid_character_error => "Invalid Character",
.invalid_character_error => "Error: Invalid Character",
.index_size_error => "IndexSizeError: Index or size is negative or greater than the allowed amount",
.syntax_error => "Syntax Error",
.not_supported => "Not Supported",

View File

@@ -125,6 +125,16 @@ pub fn createElementNS(_: *const Document, namespace: ?[]const u8, name: []const
return node.as(Element);
}
pub fn createAttribute(_: *const Document, name: []const u8, page: *Page) !?*Element.Attribute {
try Element.Attribute.validateAttributeName(name);
return page._factory.node(Element.Attribute{
._proto = undefined,
._name = try page.dupeString(name),
._value = "",
._element = null,
});
}
pub fn getElementById(self: *const Document, id_: ?[]const u8) ?*Element {
const id = id_ orelse return null;
return self._elements_by_id.get(id);
@@ -317,6 +327,24 @@ pub fn importNode(_: *const Document, node: *Node, deep_: ?bool, page: *Page) !*
return node.cloneNode(deep_, page);
}
pub fn append(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
const parent = self.asNode();
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.appendChild(child, page);
}
}
pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
const parent = self.asNode();
var i = nodes.len;
while (i > 0) {
i -= 1;
const child = try nodes[i].toNode(page);
_ = try parent.insertBefore(child, parent.firstChild(), page);
}
}
const ReadyState = enum {
loading,
interactive,
@@ -360,6 +388,7 @@ pub const JsApi = struct {
pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{});
pub const createComment = bridge.function(Document.createComment, .{});
pub const createTextNode = bridge.function(Document.createTextNode, .{});
pub const createAttribute = bridge.function(Document.createAttribute, .{ .dom_exception = true });
pub const createCDATASection = bridge.function(Document.createCDATASection, .{ .dom_exception = true });
pub const createRange = bridge.function(Document.createRange, .{});
pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true });
@@ -373,6 +402,8 @@ pub const JsApi = struct {
pub const getElementsByName = bridge.function(Document.getElementsByName, .{});
pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true });
pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true });
pub const append = bridge.function(Document.append, .{});
pub const prepend = bridge.function(Document.prepend, .{});
pub const defaultView = bridge.accessor(struct {
fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") {

View File

@@ -343,6 +343,14 @@ pub fn setId(self: *Element, value: []const u8, page: *Page) !void {
return self.setAttributeSafe("id", value, page);
}
pub fn getDir(self: *const Element) []const u8 {
return self.getAttributeSafe("dir") orelse "";
}
pub fn setDir(self: *Element, value: []const u8, page: *Page) !void {
return self.setAttributeSafe("dir", value, page);
}
pub fn getClassName(self: *const Element) []const u8 {
return self.getAttributeSafe("class") orelse "";
}
@@ -388,6 +396,7 @@ pub fn getAttributeNode(self: *Element, name: []const u8, page: *Page) !?*Attrib
}
pub fn setAttribute(self: *Element, name: []const u8, value: []const u8, page: *Page) !void {
try Attribute.validateAttributeName(name);
const attributes = try self.getOrCreateAttributeList(page);
_ = try attributes.put(name, value, self, page);
}
@@ -503,6 +512,7 @@ pub fn removeAttribute(self: *Element, name: []const u8, page: *Page) !void {
}
pub fn toggleAttribute(self: *Element, name: []const u8, force: ?bool, page: *Page) !bool {
try Attribute.validateAttributeName(name);
const has = try self.hasAttribute(name, page);
const should_add = force orelse !has;
@@ -647,6 +657,27 @@ pub fn prepend(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !voi
}
}
pub fn before(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.insertBefore(child, node, page);
}
}
pub fn after(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
const next = node.nextSibling();
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.insertBefore(child, next, page);
}
}
pub fn firstElementChild(self: *Element) ?*Element {
var maybe_child = self.asNode().firstChild();
while (maybe_child) |child| {
@@ -946,6 +977,10 @@ pub fn cloneElement(self: *Element, deep: bool, page: *Page) !*Node {
return node;
}
pub fn scrollIntoViewIfNeeded(_: *const Element, center_if_needed: ?bool) void {
_ = center_if_needed;
}
pub fn format(self: *Element, writer: *std.Io.Writer) !void {
try writer.writeByte('<');
try writer.writeAll(self.getTagNameDump());
@@ -1136,6 +1171,7 @@ pub const JsApi = struct {
pub const localName = bridge.accessor(Element.getLocalName, null, .{});
pub const id = bridge.accessor(Element.getId, Element.setId, .{});
pub const dir = bridge.accessor(Element.getDir, Element.setDir, .{});
pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{});
pub const classList = bridge.accessor(Element.getClassList, null, .{});
pub const dataset = bridge.accessor(Element.getDataset, null, .{});
@@ -1145,10 +1181,10 @@ pub const JsApi = struct {
pub const hasAttributes = bridge.function(Element.hasAttributes, .{});
pub const getAttribute = bridge.function(Element.getAttribute, .{});
pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
pub const setAttribute = bridge.function(Element.setAttribute, .{});
pub const setAttribute = bridge.function(Element.setAttribute, .{ .dom_exception = true });
pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{});
pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{});
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true });
pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{});
pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true });
pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{});
@@ -1167,6 +1203,8 @@ pub const JsApi = struct {
pub const remove = bridge.function(Element.remove, .{});
pub const append = bridge.function(Element.append, .{});
pub const prepend = bridge.function(Element.prepend, .{});
pub const before = bridge.function(Element.before, .{});
pub const after = bridge.function(Element.after, .{});
pub const firstElementChild = bridge.accessor(Element.firstElementChild, null, .{});
pub const lastElementChild = bridge.accessor(Element.lastElementChild, null, .{});
pub const nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{});
@@ -1188,6 +1226,7 @@ pub const JsApi = struct {
pub const children = bridge.accessor(Element.getChildren, null, .{});
pub const focus = bridge.function(Element.focus, .{});
pub const blur = bridge.function(Element.blur, .{});
pub const scrollIntoViewIfNeeded = bridge.function(Element.scrollIntoViewIfNeeded, .{});
};
pub const Build = struct {

View File

@@ -111,6 +111,7 @@ pub const JsApi = struct {
pub const length = bridge.accessor(NodeList.length, null, .{});
pub const @"[]" = bridge.indexed(NodeList.getAtIndex, .{ .null_as_undefined = true });
pub const item = bridge.function(NodeList.getAtIndex, .{});
pub const keys = bridge.function(NodeList.keys, .{});
pub const values = bridge.function(NodeList.values, .{});
pub const entries = bridge.function(NodeList.entries, .{});

View File

@@ -343,6 +343,32 @@ fn shouldAddToIdMap(normalized_name: []const u8, element: *Element) bool {
return node.isConnected();
}
pub fn validateAttributeName(name: []const u8) !void {
if (name.len == 0) {
return error.InvalidCharacterError;
}
const first = name[0];
if ((first >= '0' and first <= '9') or first == '-' or first == '.') {
return error.InvalidCharacterError;
}
for (name) |c| {
if (c == 0 or c == '/' or c == '=' or c == '>' or std.ascii.isWhitespace(c)) {
return error.InvalidCharacterError;
}
const is_valid = (c >= 'a' and c <= 'z') or
(c >= 'A' and c <= 'Z') or
(c >= '0' and c <= '9') or
c == '_' or c == '-' or c == '.' or c == ':';
if (!is_valid) {
return error.InvalidCharacterError;
}
}
}
pub fn normalizeNameForLookup(name: []const u8, page: *Page) ![]const u8 {
if (!needsLowerCasing(name)) {
return name;

View File

@@ -38,7 +38,8 @@ pub fn querySelector(root: *Node, input: []const u8, page: *Page) !?*Node.Elemen
if (first == .id) {
const el = page.getElementByIdFromNode(root, first.id) orelse continue;
// Check if the element is within the root subtree
if (root.contains(el.asNode())) {
const node = el.asNode();
if (node != root and root.contains(node)) {
return el;
}
continue;