From 0adcbde1e2f005e4606627288b3be7d9d14c7eca Mon Sep 17 00:00:00 2001 From: Anne van Kesteren Date: Sat, 11 Jul 2026 13:42:34 +0200 Subject: [PATCH 1/4] Add tracking test for relative in about:blank documents Per the fallback base URL algorithm an about:blank document's fallback base URL is its initiator's base URL, and a element resolves its href against the fallback base URL. So a relative should resolve against the inherited base URL, both for the document and its clones, but browsers currently resolve against about:blank. See https://github.com/whatwg/dom/issues/454 --- ...loneNode-about-base-url-relative.window.js | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 dom/nodes/Document-cloneNode-about-base-url-relative.window.js diff --git a/dom/nodes/Document-cloneNode-about-base-url-relative.window.js b/dom/nodes/Document-cloneNode-about-base-url-relative.window.js new file mode 100644 index 00000000000000..3b7fb651c09be8 --- /dev/null +++ b/dom/nodes/Document-cloneNode-about-base-url-relative.window.js @@ -0,0 +1,43 @@ +// Tracking test for a relative href in an about:blank document, and how +// it interacts with cloning. +// +// Per the "fallback base URL" algorithm, an about:blank document's fallback +// base URL is its initiator's base URL, and a element resolves its href +// against the fallback base URL. So a relative should resolve +// against the inherited base URL. Browsers currently resolve it against +// "about:blank" instead, which is an open question: fix the browsers or special +// case about:blank in the specification. +// +// See https://github.com/whatwg/dom/issues/454 + +function aboutBlankDocument(t) { + return new Promise(resolve => { + const iframe = document.createElement("iframe"); + iframe.src = "about:blank"; + iframe.onload = () => resolve(iframe); + t.add_cleanup(() => iframe.remove()); + document.body.appendChild(iframe); + }); +} + +promise_test(async t => { + const doc = (await aboutBlankDocument(t)).contentDocument; + const expected = new URL("sub/", doc.baseURI).href; + + const base = doc.createElement("base"); + base.setAttribute("href", "sub/"); + doc.head.appendChild(base); + assert_equals(doc.baseURI, expected); +}, "A relative in an about:blank document resolves against the inherited base URL"); + +promise_test(async t => { + const doc = (await aboutBlankDocument(t)).contentDocument; + const expected = new URL("sub/", doc.baseURI).href; + + const base = doc.createElement("base"); + base.setAttribute("href", "sub/"); + doc.head.appendChild(base); + + assert_equals(doc.cloneNode(true).baseURI, expected, "deep"); + assert_equals(doc.cloneNode(false).baseURI, expected, "shallow"); +}, "Clone of an about:blank document with a relative "); From 2b5651fd3c899630096cac08e2fb397ccc8a29aa Mon Sep 17 00:00:00 2001 From: Anne van Kesteren Date: Sat, 11 Jul 2026 14:38:32 +0200 Subject: [PATCH 2/4] Add srcdoc counterpart tracking test --- ...cument-cloneNode-srcdoc-base-url.window.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 dom/nodes/Document-cloneNode-srcdoc-base-url.window.js diff --git a/dom/nodes/Document-cloneNode-srcdoc-base-url.window.js b/dom/nodes/Document-cloneNode-srcdoc-base-url.window.js new file mode 100644 index 00000000000000..148931ca2eddc7 --- /dev/null +++ b/dom/nodes/Document-cloneNode-srcdoc-base-url.window.js @@ -0,0 +1,34 @@ +// Tracking test for a relative href in an about:srcdoc document, and how +// it interacts with cloning. Parallels the about:blank case. +// +// Per the "fallback base URL" algorithm, an iframe srcdoc document's base URL is +// its container's base URL, and a element resolves its href against the +// fallback base URL. So a relative should resolve against the +// inherited base URL, both for the document and its clones. Cloning drops the +// container relationship, which is where this gets interesting. +// +// See https://github.com/whatwg/dom/issues/454 + +function srcdocDocument(t, srcdoc) { + return new Promise(resolve => { + const iframe = document.createElement("iframe"); + iframe.srcdoc = srcdoc; + iframe.onload = () => resolve(iframe); + t.add_cleanup(() => iframe.remove()); + document.body.appendChild(iframe); + }); +} + +promise_test(async t => { + const doc = (await srcdocDocument(t, "x")).contentDocument; + const expected = new URL("sub/", document.baseURI).href; + assert_equals(doc.baseURI, expected); +}, "A relative in an about:srcdoc document resolves against the inherited base URL"); + +promise_test(async t => { + const doc = (await srcdocDocument(t, "x")).contentDocument; + const expected = new URL("sub/", document.baseURI).href; + + assert_equals(doc.cloneNode(true).baseURI, expected, "deep"); + assert_equals(doc.cloneNode(false).baseURI, expected, "shallow"); +}, "Clone of an about:srcdoc document with a relative "); From fd4320763e6827e04775af439b758e68db86fe7c Mon Sep 17 00:00:00 2001 From: Anne van Kesteren Date: Sat, 11 Jul 2026 16:31:22 +0200 Subject: [PATCH 3/4] Add about:blank/about:srcdoc base URL not-liveness tests --- .../Document-base-url-not-live.window.js | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 dom/nodes/Document-base-url-not-live.window.js diff --git a/dom/nodes/Document-base-url-not-live.window.js b/dom/nodes/Document-base-url-not-live.window.js new file mode 100644 index 00000000000000..f2d19882d75bc4 --- /dev/null +++ b/dom/nodes/Document-base-url-not-live.window.js @@ -0,0 +1,54 @@ +// Confirms that the base URL an about:blank or about:srcdoc document inherits +// from its initiator/container is a snapshot taken at creation, not a live view +// of the initiator's current base URL. +// +// Reading baseURI is not enough on its own: implementations cache the base URL, +// so a live-but-cached implementation would still return the creation-time value +// on read. To distinguish snapshot from live, the container's base URL is changed +// and the child is then forced to recompute its base URL (by inserting and +// removing a ). A snapshot yields the creation-time value; a live view +// yields the container's new base URL. +// +// See https://github.com/whatwg/dom/issues/454 + +function makeIframe(doc, configure) { + return new Promise(resolve => { + const iframe = doc.createElement("iframe"); + configure(iframe); + iframe.addEventListener("load", () => resolve(iframe), { once: true }); + doc.body.appendChild(iframe); + }); +} + +// Inserting then removing a makes the document recompute its base URL. +function forceBaseURLRecompute(doc) { + const base = doc.createElement("base"); + base.setAttribute("href", "http://example.org/forced/"); + doc.head.appendChild(base); + doc.head.removeChild(base); +} + +function notLiveTest(configureChild) { + return async t => { + const container = await makeIframe(document, f => f.src = "about:blank"); + t.add_cleanup(() => container.remove()); + const cdoc = container.contentDocument; + + const child = await makeIframe(cdoc, configureChild); + const childDoc = child.contentDocument; + const inherited = childDoc.baseURI; + + const cbase = cdoc.createElement("base"); + cbase.setAttribute("href", "http://example.com/changed/"); + cdoc.head.appendChild(cbase); + assert_equals(cdoc.baseURI, "http://example.com/changed/", "container base URL changed"); + + forceBaseURLRecompute(childDoc); + assert_equals(childDoc.baseURI, inherited, + "child base URL is a snapshot from creation, not the container's current base URL"); + }; +} + +promise_test(notLiveTest(f => f.src = "about:blank"), "about:blank base URL is not live"); + +promise_test(notLiveTest(f => f.srcdoc = "x"), "about:srcdoc base URL is not live"); From 5339ce15d47d79fdee871ceab88965e72852c533 Mon Sep 17 00:00:00 2001 From: Anne van Kesteren Date: Sun, 12 Jul 2026 11:05:11 +0200 Subject: [PATCH 4/4] Flip relative expectations to resolve against the document's own URL --- ...loneNode-about-base-url-relative.window.js | 25 ++++++++----------- ...cument-cloneNode-srcdoc-base-url.window.js | 24 ++++++++---------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/dom/nodes/Document-cloneNode-about-base-url-relative.window.js b/dom/nodes/Document-cloneNode-about-base-url-relative.window.js index 3b7fb651c09be8..deadfd9f2ffbbf 100644 --- a/dom/nodes/Document-cloneNode-about-base-url-relative.window.js +++ b/dom/nodes/Document-cloneNode-about-base-url-relative.window.js @@ -1,12 +1,11 @@ -// Tracking test for a relative href in an about:blank document, and how -// it interacts with cloning. +// A relative href in an about:blank document, and how it interacts with +// cloning. // -// Per the "fallback base URL" algorithm, an about:blank document's fallback -// base URL is its initiator's base URL, and a element resolves its href -// against the fallback base URL. So a relative should resolve -// against the inherited base URL. Browsers currently resolve it against -// "about:blank" instead, which is an open question: fix the browsers or special -// case about:blank in the specification. +// The base URL an about:blank document inherits from its initiator is exposed +// through the no- path (base URL override), so ordinary relative URLs +// resolve against it. A element, however, resolves its href against the +// document's own URL, so a relative resolves against +// about:blank rather than the inherited base URL. Cloning does not change this. // // See https://github.com/whatwg/dom/issues/454 @@ -22,22 +21,20 @@ function aboutBlankDocument(t) { promise_test(async t => { const doc = (await aboutBlankDocument(t)).contentDocument; - const expected = new URL("sub/", doc.baseURI).href; const base = doc.createElement("base"); base.setAttribute("href", "sub/"); doc.head.appendChild(base); - assert_equals(doc.baseURI, expected); -}, "A relative in an about:blank document resolves against the inherited base URL"); + assert_equals(doc.baseURI, "about:blank"); +}, "A relative in an about:blank document resolves against about:blank"); promise_test(async t => { const doc = (await aboutBlankDocument(t)).contentDocument; - const expected = new URL("sub/", doc.baseURI).href; const base = doc.createElement("base"); base.setAttribute("href", "sub/"); doc.head.appendChild(base); - assert_equals(doc.cloneNode(true).baseURI, expected, "deep"); - assert_equals(doc.cloneNode(false).baseURI, expected, "shallow"); + assert_equals(doc.cloneNode(true).baseURI, "about:blank", "deep"); + assert_equals(doc.cloneNode(false).baseURI, "about:blank", "shallow"); }, "Clone of an about:blank document with a relative "); diff --git a/dom/nodes/Document-cloneNode-srcdoc-base-url.window.js b/dom/nodes/Document-cloneNode-srcdoc-base-url.window.js index 148931ca2eddc7..4a78497631a2d3 100644 --- a/dom/nodes/Document-cloneNode-srcdoc-base-url.window.js +++ b/dom/nodes/Document-cloneNode-srcdoc-base-url.window.js @@ -1,11 +1,11 @@ -// Tracking test for a relative href in an about:srcdoc document, and how -// it interacts with cloning. Parallels the about:blank case. +// A relative href in an about:srcdoc document, and how it interacts with +// cloning. Parallels the about:blank case. // -// Per the "fallback base URL" algorithm, an iframe srcdoc document's base URL is -// its container's base URL, and a element resolves its href against the -// fallback base URL. So a relative should resolve against the -// inherited base URL, both for the document and its clones. Cloning drops the -// container relationship, which is where this gets interesting. +// The base URL an about:srcdoc document inherits from its container is exposed +// through the no- path (base URL override), so ordinary relative URLs +// resolve against it. A element, however, resolves its href against the +// document's own URL, so a relative resolves against +// about:srcdoc rather than the inherited base URL. Cloning does not change this. // // See https://github.com/whatwg/dom/issues/454 @@ -21,14 +21,12 @@ function srcdocDocument(t, srcdoc) { promise_test(async t => { const doc = (await srcdocDocument(t, "x")).contentDocument; - const expected = new URL("sub/", document.baseURI).href; - assert_equals(doc.baseURI, expected); -}, "A relative in an about:srcdoc document resolves against the inherited base URL"); + assert_equals(doc.baseURI, "about:srcdoc"); +}, "A relative in an about:srcdoc document resolves against about:srcdoc"); promise_test(async t => { const doc = (await srcdocDocument(t, "x")).contentDocument; - const expected = new URL("sub/", document.baseURI).href; - assert_equals(doc.cloneNode(true).baseURI, expected, "deep"); - assert_equals(doc.cloneNode(false).baseURI, expected, "shallow"); + assert_equals(doc.cloneNode(true).baseURI, "about:srcdoc", "deep"); + assert_equals(doc.cloneNode(false).baseURI, "about:srcdoc", "shallow"); }, "Clone of an about:srcdoc document with a relative ");