diff --git a/core/modules/utils/fakedom.js b/core/modules/utils/fakedom.js index f1f0cacfb..b3611ae2d 100755 --- a/core/modules/utils/fakedom.js +++ b/core/modules/utils/fakedom.js @@ -115,7 +115,9 @@ var TW_Element = function(tag, namespace) { this.children = []; this._style = {}; // Internal style object this.style = new TW_Style(this); // Proxy for style management - this.namespaceURI = namespace || "http://www.w3.org/1999/xhtml"; + // createElementNS with empty-string or null normalises to null (no namespace) per spec. + // https://dom.spec.whatwg.org/#dom-document-createelementns + this.namespaceURI = namespace !== undefined ? (namespace || null) : "http://www.w3.org/1999/xhtml"; }; @@ -206,7 +208,16 @@ TW_Element.prototype.addEventListener = function(type,listener,useCapture) { Object.defineProperty(TW_Element.prototype, "tagName", { get: function() { - return this.tag || ""; + if(!this.tag) { + return ""; + } + // HTML elements report uppercase tagName per DOM spec. Other namespaces + // preserve case. Fakedom only models HTML documents. + // https://dom.spec.whatwg.org/#dom-element-tagname + if(this.namespaceURI === "http://www.w3.org/1999/xhtml") { + return this.tag.toUpperCase(); + } + return this.tag; } }); diff --git a/editions/test/tiddlers/tests/test-fakedom.js b/editions/test/tiddlers/tests/test-fakedom.js index dd5c79793..c58fa8fa7 100644 --- a/editions/test/tiddlers/tests/test-fakedom.js +++ b/editions/test/tiddlers/tests/test-fakedom.js @@ -19,6 +19,56 @@ describe("fakedom tests", function() { expect($tw.fakeDocument.createTextNode("text").TEXT_NODE).toBe(3); }); + // Per DOM spec, tagName returns the HTML-uppercased qualified name for HTML + // elements. Other namespaces preserve case. + // https://dom.spec.whatwg.org/#dom-element-tagname + var HTML_NS = "http://www.w3.org/1999/xhtml"; + var SVG_NS = "http://www.w3.org/2000/svg"; + + it("tagName uppercases for HTML namespace", function() { + // Default namespace is HTML + expect($tw.fakeDocument.createElement("div").tagName).toBe("DIV"); + // The exact predicate the select widget relies on (#9839) + expect($tw.fakeDocument.createElement("optgroup").tagName === "OPTGROUP").toBe(true); + // Already-uppercase input stays uppercase + expect($tw.fakeDocument.createElement("OPTGROUP").tagName).toBe("OPTGROUP"); + // Mixed-case input is normalised + expect($tw.fakeDocument.createElement("Div").tagName).toBe("DIV"); + // Hyphenated custom-element names uppercase whole tag, hyphens survive + expect($tw.fakeDocument.createElement("my-button").tagName).toBe("MY-BUTTON"); + // Empty tag returns empty string + expect($tw.fakeDocument.createElement("").tagName).toBe(""); + // Explicit HTML namespace via createElementNS uppercases the same way + expect($tw.fakeDocument.createElementNS(HTML_NS,"Div").tagName).toBe("DIV"); + }); + + it("tagName preserves case for non-HTML namespaces", function() { + // SVG: lowercase preserved + expect($tw.fakeDocument.createElementNS(SVG_NS,"circle").tagName).toBe("circle"); + // SVG: camelCase preserved (linearGradient is the canonical example) + expect($tw.fakeDocument.createElementNS(SVG_NS,"linearGradient").tagName).toBe("linearGradient"); + // SVG: already-uppercase input is also preserved (NOT lowercased) + expect($tw.fakeDocument.createElementNS(SVG_NS,"DIV").tagName).toBe("DIV"); + // Empty namespace string is "no namespace", not HTML. Case preserved. + expect($tw.fakeDocument.createElementNS("","div").tagName).toBe("div"); + }); + + it("tagName reflects current state without mutating it", function() { + // Reading tagName must not overwrite the internal `tag` field + var el = $tw.fakeDocument.createElement("div"); + expect(el.tagName).toBe("DIV"); + expect(el.tag).toBe("div"); + // Idempotent: two reads return identical values + var first = el.tagName, second = el.tagName; + expect(first).toBe(second); + // Dynamic namespace change is reflected. The getter must read current + // state, not a value cached at construction time. + var dynamic = $tw.fakeDocument.createElement("foo"); + expect(dynamic.tagName).toBe("FOO"); + dynamic.namespaceURI = SVG_NS; + expect(dynamic.tagName).toBe("foo"); + }); + // Real CSSStyleDeclaration returns undefined for Symbol property keys. // Without a guard, the TW_Style Proxy throws on Symbol access. This bites // in practice when Jasmine pretty-prints fakedom elements on failure. diff --git a/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9843-fakedom-tagname-spec.tid b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9843-fakedom-tagname-spec.tid new file mode 100644 index 000000000..33993cae4 --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9843-fakedom-tagname-spec.tid @@ -0,0 +1,13 @@ +change-category: internal +change-type: bugfix +created: 20260711014241000 +description: fakedom tagName is uppercase for HTML elements, matching the DOM spec +github-contributors: pmario +github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9843 +release: 5.5.0 +tags: $:/tags/ChangeNote +title: $:/changenotes/5.5.0/#9843 +type: text/vnd.tiddlywiki + +* `$tw.fakeDocument` elements now report `tagName` in uppercase for HTML elements, matching the DOM specification and real browsers; code that compares fakedom `tagName` against lowercase strings must be updated +* Creating an element with an empty or null namespace now produces a plain element without a namespace