/** * el() has to tell an attribute from a property, and there is nothing to catch * when it does not. * * It used to be one Object.assign, so `"aria-label": "Camo name"` put a * property literally called "aria-label" on the element. No error, no * attribute, and the camo name field reached the accessibility tree as an * unnamed textbox. The same key in the same shape sat in the rename field * beside it. Both looked right in the source and neither did anything. * * So the rule is the split itself: a hyphenated key goes through * setAttribute, everything else is assigned. The second test is the reason * the first one is enough — one el() in the app, not three. * * Run with `npm test`. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { readdir, readFile } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; // Enough of an element for el(): a bag of attributes, a list of children, and // ordinary property assignment for everything else. Installed before el() is // called rather than before it is imported, which dom.ts allows because it // touches the document only inside a function. class FakeElement { constructor(tag) { this.tagName = tag.toUpperCase(); this.attributes = {}; this.children = []; } setAttribute(name, value) { this.attributes[name] = value; } append(...nodes) { this.children.push(...nodes); } } globalThis.document = { createElement: (tag) => new FakeElement(tag) }; const { el } = await import("../src/dom.ts"); test("a hyphenated key becomes an attribute", () => { const input = el("input", { "aria-label": "Camo name", "data-testid": "camo-name", }); assert.equal(input.attributes["aria-label"], "Camo name"); assert.equal(input.attributes["data-testid"], "camo-name"); // The bug: set as a property, it is invisible to everything that reads the // element, and an assertion on the property alone would have passed anyway. assert.equal( Object.hasOwn(input, "aria-label"), false, "aria-label was assigned as a property instead of set as an attribute", ); }); test("every other key stays a property", () => { const input = el( "input", { className: "camo-name", placeholder: "Name this camo", maxLength: 64 }, ["child"], ); assert.equal(input.className, "camo-name"); assert.equal(input.placeholder, "Name this camo"); assert.equal(input.maxLength, 64); assert.deepEqual(input.attributes, {}); assert.deepEqual(input.children, ["child"]); }); test("there is one el() in the app", async () => { const src = fileURLToPath(new URL("../src/", import.meta.url)); const files = []; for (const entry of await readdir(src, { recursive: true })) { // Generated by lexgen and gitignored; it has no el() and no reason to be // read on every test run. if (entry.startsWith("lexicons/")) continue; if (entry.endsWith(".ts")) files.push(entry); } const defining = []; for (const file of files) { const source = await readFile(join(src, file), "utf8"); if (/function el