From ebf50bcc6e1dd08a8810654587954656bce048f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 12:50:25 +0000 Subject: [PATCH 1/4] Keep radios checked when a form holding a radio with a form attribute moves or is removed Chromium and Firefox briefly reset the form of a radio with a form attribute while a form around it moves or leaves, so a checked one unchecked a radio in another group. Those radios now move or leave unchecked and are checked again straight after. Fixes #162 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SZddej5CrkTfW4RWeFsvyU --- AGENTS.md | 2 +- src/morphlex.ts | 15 ++++ test/new/moved-form-radios.browser.test.ts | 93 ++++++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 test/new/moved-form-radios.browser.test.ts diff --git a/AGENTS.md b/AGENTS.md index b59c6a8..11e64c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,7 @@ When `preserveChanges` is false, `#resetFormProperties` runs after the attribute Moves complete out of document order, and a radio that moves can join another radio group, so resetting radios one at a time can leave a group showing the wrong radio. Adding `checked` to a radio unchecks the rest of its group too, even radios later in the document, and a node list morph adds its first node after the others. So when a reset changes a radio's `.checked`, the target checks a visited radio, a radio moves, or a node list morph adds radios, its group is synced when the morph settles: each radio's `.checked` is set from its `checked` attribute in document order, so the last one the markup checks wins, as when parsing. Radios outside the morph are left alone, and so is a whole group with a radio in a vetoed subtree or with a vetoed `checked` update, since checking one radio unchecks the rest. Nothing is synced under `preserveChanges`. -A move can also change radio groups outside the morph: a checked radio that joins a group unchecks the rest of it, and moving a form briefly takes the radios outside it that use its `form` attribute out of its group. Adding or removing a form with an id, or changing a form's id or a radio's `form` attribute, changes which form such radios belong to too (and changing a checked radio's name, or turning a checked input into a radio, is handled the same way), and Firefox and Safari briefly put a radio that changes form in the group of radios without a form, where a checked one unchecks a radio it never joins. So these changes first uncheck the checked radios outside the changing node whose `form` attribute names a form id it holds (or the radio itself, or the checked radios inside a moving element that change form), and check them again straight after, in both modes, except that without `preserveChanges` the morph's own radios (or, inside a `morphlex-clobber` element, that element's radios) wait until it settles, so a radio the markup unchecks never unchecks the rest of its new group. A radio that's checked again straight after and still follows its `checked` attribute is unchecked by removing the attribute and checked by adding it back, because setting `.checked` would stop it following the markup. A radio outside the morph that joins a group with a checked radio inside the morph leaves that one checked, as when the page is parsed with the markup's radio later. When the morph settles, the radios that such a recheck unchecked (or that a checked radio inside the morph took the group from, including one the morph checks by adding its `checked` attribute or resetting `.checked`, and a new radio with a `form` attribute, which is inserted unchecked and checked straight after) are checked again if the radio that unchecked them has left their group (removed, or moved to another form or name) or is no longer checked, unless another radio in their group is checked or the markup decides their own `checked` (it removed the attribute, or the default reset visited them), so removing a form before a radio that names it, or changing a form's id before the radio's `form` attribute, doesn't lose a radio it only passed by. A live target also unchecks the radios where it is (in its own document or shadow root, and inside it) that name its forms, before anything takes it or its forms out of where they are, and checks them again straight after it's inserted. Without `preserveChanges` it also unchecks the checked radios inside it whose form changes, and checks them again when the morph settles if the markup or a veto keeps them checked. +A move can also change radio groups outside the morph: a checked radio that joins a group unchecks the rest of it, and moving a form briefly takes the radios outside it that use its `form` attribute out of its group. Adding or removing a form with an id, or changing a form's id or a radio's `form` attribute, changes which form such radios belong to too (and changing a checked radio's name, or turning a checked input into a radio, is handled the same way), and Firefox and Safari briefly put a radio that changes form in the group of radios without a form, where a checked one unchecks a radio it never joins. Chromium and Firefox also briefly reset the form of a radio with a `form` attribute while a form around it moves or is removed, so a checked one can uncheck a radio in another group for that moment. So these changes first uncheck the checked radios outside the changing node whose `form` attribute names a form id it holds (or the radio itself, or the checked radios inside a moving element that change form, or the checked radios with a `form` attribute inside a moving or removed node that holds a form, which are checked again straight after in both modes), and check them again straight after, in both modes, except that without `preserveChanges` the morph's own radios (or, inside a `morphlex-clobber` element, that element's radios) wait until it settles, so a radio the markup unchecks never unchecks the rest of its new group. A radio that's checked again straight after and still follows its `checked` attribute is unchecked by removing the attribute and checked by adding it back, because setting `.checked` would stop it following the markup. A radio outside the morph that joins a group with a checked radio inside the morph leaves that one checked, as when the page is parsed with the markup's radio later. When the morph settles, the radios that such a recheck unchecked (or that a checked radio inside the morph took the group from, including one the morph checks by adding its `checked` attribute or resetting `.checked`, and a new radio with a `form` attribute, which is inserted unchecked and checked straight after) are checked again if the radio that unchecked them has left their group (removed, or moved to another form or name) or is no longer checked, unless another radio in their group is checked or the markup decides their own `checked` (it removed the attribute, or the default reset visited them), so removing a form before a radio that names it, or changing a form's id before the radio's `form` attribute, doesn't lose a radio it only passed by. A live target also unchecks the radios where it is (in its own document or shadow root, and inside it) that name its forms, before anything takes it or its forms out of where they are, and checks them again straight after it's inserted. Without `preserveChanges` it also unchecks the checked radios inside it whose form changes, and checks them again when the morph settles if the markup or a veto keeps them checked. ### `open` on `details` and `dialog` diff --git a/src/morphlex.ts b/src/morphlex.ts index 28cad1f..0f7865e 100644 --- a/src/morphlex.ts +++ b/src/morphlex.ts @@ -2306,10 +2306,12 @@ class Morph { if (!shouldNotMove[matchInd]) { const outsideRadios = this.#uncheckRadiosNamingFormsIn(match, getRootNode(match)) + const insideRadios = this.#uncheckRadiosInFormsIn(match) const focus = this.#watchFocus(match, parent) moveBefore(parent, match, insertionPoint) if (focus) this.#restoreFocus(focus) this.#checkRadios(outsideRadios) + this.#checkRadios(insideRadios, true) } // Read this before the morph, which can replace the match. A match that moved itself // elsewhere when it reconnected leaves the insertion point where it was. @@ -2559,6 +2561,15 @@ class Morph { return ids && this.#uncheckRadiosNaming(ids, root, inside ? null : node) } + // Chromium and Firefox briefly reset the form of a radio with a `form` attribute while a form around it + // moves or leaves, and a checked one then unchecks the radio in the group it joins for that moment. So + // when a node holding a form moves or leaves, these radios go unchecked, and are checked again straight + // after, back in the group they were in. + #uncheckRadiosInFormsIn(node: Node): Array | null { + if (!isElement(node) || (!isFormElement(node) && getElementsByTagName(node, "form").length === 0)) return null + return this.#uncheckRadiosWithForm(node) + } + // A new radio with a `form` attribute is checked in the group it joins, but can leave that group when the // morph adds or changes its form later. So it's inserted unchecked and checked again straight after, which // notes the radios it unchecks, to give them their check back if it leaves. @@ -2710,8 +2721,10 @@ class Morph { #removeChild(node: ChildNode): void { const radios = this.#uncheckRadiosNamingFormsIn(node, getRootNode(node)) + const insideRadios = this.#uncheckRadiosInFormsIn(node) remove(node) this.#checkRadios(radios) + this.#checkRadios(insideRadios, true) } // Check each radio the markup checks, in document order, so the last one wins as when parsing. @@ -2980,11 +2993,13 @@ class Morph { if (!inCycle && this.#liveElementsById.get(idOf(target)) === live && !contains(live, parent)) { this.#liveElementsById.delete(idOf(target)) const radios = this.#uncheckRadiosForMove(live, parent) + const insideRadios = this.#uncheckRadiosInFormsIn(live) const focus = this.#watchFocus(live, parent) moveInto(parent, live, placeholder) if (focus) this.#restoreFocus(focus) remove(placeholder) this.#checkRadios(radios) + this.#checkRadios(insideRadios, true) if (!this.#preserveChanges) this.#noteRadioGroups(live) this.#morphOneToOne(live, target) } else { diff --git a/test/new/moved-form-radios.browser.test.ts b/test/new/moved-form-radios.browser.test.ts new file mode 100644 index 0000000..4ddf195 --- /dev/null +++ b/test/new/moved-form-radios.browser.test.ts @@ -0,0 +1,93 @@ +import { expect, onTestFinished, test } from "vitest" +import { morph } from "../../src/morphlex" + +// Chromium and Firefox briefly reset the form of a radio with a `form` attribute while an ancestor of it moves. +// A checked one then joins another group for that moment and unchecks the radio there. + +function mount(html: string): HTMLElement { + const host = document.createElement("section") + host.innerHTML = html + document.body.append(host) + onTestFinished(() => host.remove()) + return host +} + +function checked(host: Element, selector: string): boolean { + return host.querySelector(selector)!.checked +} + +test("moving a form holding a checked radio of another form keeps the user's pick in the moved form", () => { + const host = mount( + `

`, + ) + host.querySelector("#c0")!.checked = true + + morph( + host, + `

`, + { preserveChanges: true }, + ) + + expect(checked(host, "#c0")).toBe(true) + expect(checked(host, "input[value='2']")).toBe(true) +}) + +test("moving a form holding a checked radio of a missing form keeps the user's pick in the moved form", () => { + const form = `
` + const host = mount(`${form}`) + host.querySelector("input.x")!.checked = true + + morph(host, `
${form}
`, { preserveChanges: true }) + + expect(checked(host, "input.x")).toBe(true) + expect(checked(host, "#c0")).toBe(true) +}) + +test("moving a form holding a radio the user checked that names the form keeps a formless radio checked", () => { + const html = `

` + const form = `
` + + for (const preserveChanges of [false, true]) { + const host = mount(`${html}${form}`) + host.querySelector("#c2")!.checked = true + + morph(host, `
${form}${html}
`, { preserveChanges }) + + expect(checked(host, "#c0")).toBe(true) + expect(checked(host, "#c2")).toBe(preserveChanges) + } +}) + +test("moving a form holding a checked radio that names the form keeps an untouched formless radio checked", () => { + const form = `
` + const radio = `` + const host = mount(`${form}${radio}`) + host.querySelector("input[type=email]")!.value = "typed" + + morph(host, `
${form}${radio}
`, { preserveChanges: true }) + + expect(checked(host, "input[value='1']")).toBe(true) + expect(checked(host, "input[value='3']")).toBe(true) +}) + +test("moving a form holding a radio the user checked that names the form keeps a radio of a missing form checked", () => { + const radio = `` + const form = `
` + const host = mount(`${radio}

${form}`) + host.querySelector("#c2")!.checked = true + + morph(host, `
${form}${radio}

`) + + expect(checked(host, "#c0")).toBe(true) + expect(checked(host, "#c2")).toBe(false) +}) + +test("removing a form holding a radio the user checked that names the form keeps a formless radio checked", () => { + const html = `` + const host = mount(`${html}
`) + host.querySelector("#r6")!.checked = true + + morph(host, `
${html}
`) + + expect(checked(host, "#c1")).toBe(true) +}) From 6356c72af205d559fc603db2db787eabc3672434 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 12:54:18 +0000 Subject: [PATCH 2/4] Let a later radio win when a radio checked again at settle joined its group Without preserveChanges, a checked radio that a form id change moved into another group was checked again when the morph settled, unchecking a later radio the markup checks there. Its group is now synced, so the last checked radio wins as when parsing. Fixes #180 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SZddej5CrkTfW4RWeFsvyU --- AGENTS.md | 2 +- src/morphlex.ts | 2 ++ test/new/renamed-form-radios.browser.test.ts | 21 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 test/new/renamed-form-radios.browser.test.ts diff --git a/AGENTS.md b/AGENTS.md index 11e64c6..3cde5b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,7 @@ When `preserveChanges` is false, `#resetFormProperties` runs after the attribute Moves complete out of document order, and a radio that moves can join another radio group, so resetting radios one at a time can leave a group showing the wrong radio. Adding `checked` to a radio unchecks the rest of its group too, even radios later in the document, and a node list morph adds its first node after the others. So when a reset changes a radio's `.checked`, the target checks a visited radio, a radio moves, or a node list morph adds radios, its group is synced when the morph settles: each radio's `.checked` is set from its `checked` attribute in document order, so the last one the markup checks wins, as when parsing. Radios outside the morph are left alone, and so is a whole group with a radio in a vetoed subtree or with a vetoed `checked` update, since checking one radio unchecks the rest. Nothing is synced under `preserveChanges`. -A move can also change radio groups outside the morph: a checked radio that joins a group unchecks the rest of it, and moving a form briefly takes the radios outside it that use its `form` attribute out of its group. Adding or removing a form with an id, or changing a form's id or a radio's `form` attribute, changes which form such radios belong to too (and changing a checked radio's name, or turning a checked input into a radio, is handled the same way), and Firefox and Safari briefly put a radio that changes form in the group of radios without a form, where a checked one unchecks a radio it never joins. Chromium and Firefox also briefly reset the form of a radio with a `form` attribute while a form around it moves or is removed, so a checked one can uncheck a radio in another group for that moment. So these changes first uncheck the checked radios outside the changing node whose `form` attribute names a form id it holds (or the radio itself, or the checked radios inside a moving element that change form, or the checked radios with a `form` attribute inside a moving or removed node that holds a form, which are checked again straight after in both modes), and check them again straight after, in both modes, except that without `preserveChanges` the morph's own radios (or, inside a `morphlex-clobber` element, that element's radios) wait until it settles, so a radio the markup unchecks never unchecks the rest of its new group. A radio that's checked again straight after and still follows its `checked` attribute is unchecked by removing the attribute and checked by adding it back, because setting `.checked` would stop it following the markup. A radio outside the morph that joins a group with a checked radio inside the morph leaves that one checked, as when the page is parsed with the markup's radio later. When the morph settles, the radios that such a recheck unchecked (or that a checked radio inside the morph took the group from, including one the morph checks by adding its `checked` attribute or resetting `.checked`, and a new radio with a `form` attribute, which is inserted unchecked and checked straight after) are checked again if the radio that unchecked them has left their group (removed, or moved to another form or name) or is no longer checked, unless another radio in their group is checked or the markup decides their own `checked` (it removed the attribute, or the default reset visited them), so removing a form before a radio that names it, or changing a form's id before the radio's `form` attribute, doesn't lose a radio it only passed by. A live target also unchecks the radios where it is (in its own document or shadow root, and inside it) that name its forms, before anything takes it or its forms out of where they are, and checks them again straight after it's inserted. Without `preserveChanges` it also unchecks the checked radios inside it whose form changes, and checks them again when the morph settles if the markup or a veto keeps them checked. +A move can also change radio groups outside the morph: a checked radio that joins a group unchecks the rest of it, and moving a form briefly takes the radios outside it that use its `form` attribute out of its group. Adding or removing a form with an id, or changing a form's id or a radio's `form` attribute, changes which form such radios belong to too (and changing a checked radio's name, or turning a checked input into a radio, is handled the same way), and Firefox and Safari briefly put a radio that changes form in the group of radios without a form, where a checked one unchecks a radio it never joins. Chromium and Firefox also briefly reset the form of a radio with a `form` attribute while a form around it moves or is removed, so a checked one can uncheck a radio in another group for that moment. So these changes first uncheck the checked radios outside the changing node whose `form` attribute names a form id it holds (or the radio itself, or the checked radios inside a moving element that change form, or the checked radios with a `form` attribute inside a moving or removed node that holds a form, which are checked again straight after in both modes), and check them again straight after, in both modes, except that without `preserveChanges` the morph's own radios (or, inside a `morphlex-clobber` element, that element's radios) wait until it settles, so a radio the markup unchecks never unchecks the rest of its new group. A radio the morph checks again when it settles joins its group's radio sync, so a later radio the markup checks there still wins. A radio that's checked again straight after and still follows its `checked` attribute is unchecked by removing the attribute and checked by adding it back, because setting `.checked` would stop it following the markup. A radio outside the morph that joins a group with a checked radio inside the morph leaves that one checked, as when the page is parsed with the markup's radio later. When the morph settles, the radios that such a recheck unchecked (or that a checked radio inside the morph took the group from, including one the morph checks by adding its `checked` attribute or resetting `.checked`, and a new radio with a `form` attribute, which is inserted unchecked and checked straight after) are checked again if the radio that unchecked them has left their group (removed, or moved to another form or name) or is no longer checked, unless another radio in their group is checked or the markup decides their own `checked` (it removed the attribute, or the default reset visited them), so removing a form before a radio that names it, or changing a form's id before the radio's `form` attribute, doesn't lose a radio it only passed by. A live target also unchecks the radios where it is (in its own document or shadow root, and inside it) that name its forms, before anything takes it or its forms out of where they are, and checks them again straight after it's inserted. Without `preserveChanges` it also unchecks the checked radios inside it whose form changes, and checks them again when the morph settles if the markup or a veto keeps them checked. ### `open` on `details` and `dialog` diff --git a/src/morphlex.ts b/src/morphlex.ts index 0f7865e..b532b69 100644 --- a/src/morphlex.ts +++ b/src/morphlex.ts @@ -1258,6 +1258,8 @@ class Morph { // Checking it would uncheck the rest of its new group, where a checked radio that's vetoed stays checked. if (radioGroupOf(radio, groups).some((member) => member.checked && this.#isVetoed(member))) continue radio.checked = true + // The new group can have a later radio the markup checks, which wins as when parsing. + ;(this.#radiosToSync ??= new Set()).add(radio) } } diff --git a/test/new/renamed-form-radios.browser.test.ts b/test/new/renamed-form-radios.browser.test.ts new file mode 100644 index 0000000..4dac4a1 --- /dev/null +++ b/test/new/renamed-form-radios.browser.test.ts @@ -0,0 +1,21 @@ +import { expect, onTestFinished, test } from "vitest" +import { morph } from "../../src/morphlex" + +// A checked radio that a form id change moves into another group joins it as when parsing, so the last +// radio the markup checks there stays checked. + +test("a user-checked radio moved into another group by a form id change leaves that group's later radio checked", () => { + const host = document.createElement("section") + host.innerHTML = `
` + document.body.append(host) + onTestFinished(() => host.remove()) + host.querySelector("#c1")!.checked = true + + morph( + host, + `
`, + ) + + expect(host.querySelector("#c1")!.checked).toBe(false) + expect(host.querySelector("#c3")!.checked).toBe(true) +}) From e5e63a56058238f28da43b8846b3b498c103db6b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 12:55:36 +0000 Subject: [PATCH 3/4] Test a moved form whose radio names a missing form (fuzzer seed 778738) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SZddej5CrkTfW4RWeFsvyU --- test/new/moved-form-radios.browser.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/new/moved-form-radios.browser.test.ts b/test/new/moved-form-radios.browser.test.ts index 4ddf195..eaec030 100644 --- a/test/new/moved-form-radios.browser.test.ts +++ b/test/new/moved-form-radios.browser.test.ts @@ -91,3 +91,15 @@ test("removing a form holding a radio the user checked that names the form keeps expect(checked(host, "#c1")).toBe(true) }) + +test("moving a form holding a radio the user checked that names a missing form keeps the form's radio checked", () => { + const form = `
` + const checkbox = `` + const host = mount(`${checkbox}${form}`) + host.querySelector("#c4")!.checked = true + + morph(host, `
${form}${checkbox}
`) + + expect(checked(host, "#c2")).toBe(true) + expect(checked(host, "#c4")).toBe(false) +}) From e8f4b0b82a8adb027a6d92ce94ea2b3a34d98f82 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 13:04:42 +0000 Subject: [PATCH 4/4] Keep restored radios following their checked attribute, and cover live forms Radios with a form attribute that are unchecked for a move and checked again straight after now go through the attribute, so an untouched one keeps following it without preserveChanges. Inserting a live target that holds a form protects those radios too, even when the target's markup checks nothing. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SZddej5CrkTfW4RWeFsvyU --- src/morphlex.ts | 16 +++++++++---- test/new/moved-form-radios.browser.test.ts | 27 +++++++++++++++++++++- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/morphlex.ts b/src/morphlex.ts index b532b69..40628e4 100644 --- a/src/morphlex.ts +++ b/src/morphlex.ts @@ -2581,7 +2581,7 @@ class Morph { for (let i = 0; i < inputs.length; i++) { const input = inputs[i]! if (isCheckedRadio(input) && input.hasAttribute("form")) { - this.#uncheckRadio(input) + this.#uncheckRadio(input, true) ;(unchecked ??= []).push(input) } } @@ -2625,8 +2625,8 @@ class Morph { // Setting `.checked` stops a radio from following its `checked` attribute. So a radio that's checked // again straight after, and still follows the attribute, is unchecked by removing the attribute. - #uncheckRadio(radio: HTMLInputElement): void { - const value = this.#defersRadio(radio) ? null : radio.getAttribute("checked") + #uncheckRadio(radio: HTMLInputElement, immediate = false): void { + const value = !immediate && this.#defersRadio(radio) ? null : radio.getAttribute("checked") if (value !== null) { radio.removeAttribute("checked") if (!radio.checked) { @@ -2939,14 +2939,20 @@ class Morph { // A live target takes its forms away from the radios where it is, in its own document or shadow root // and inside it, including forms that live elements claim out of it next. Those inside it are // checked again straight away, since they're the target's own state, not markup the morph resets. - const sourceRadios = isConnected(node) ? this.#uncheckRadiosNamingFormsIn(node, getRootNode(node), true) : null + const live = isConnected(node) + const sourceRadios = live ? this.#uncheckRadiosNamingFormsIn(node, getRootNode(node), true) : null // A live target can hold the focused element, which its claimed descendants take out of it next. const focus = this.#watchFocus(node, parent) clearImplicitSelection(node, parent) this.#placeMovableDescendants(node, parent) if (this.#targetOpensDetails && isElement(node)) this.#noteAddedDetails(node) const radios = this.#uncheckRadiosNamingFormsIn(node, getRootNode(parent)) - const addedRadios = this.#targetChecksInputs && isElement(node) ? this.#uncheckRadiosWithForm(node) : null + const addedRadios = + this.#targetChecksInputs && isElement(node) + ? this.#uncheckRadiosWithForm(node) + : live + ? this.#uncheckRadiosInFormsIn(node) + : null moveInto(parent, node, insertionPoint) if (focus) this.#restoreFocus(focus) this.#checkRadios(radios) diff --git a/test/new/moved-form-radios.browser.test.ts b/test/new/moved-form-radios.browser.test.ts index eaec030..76b27ed 100644 --- a/test/new/moved-form-radios.browser.test.ts +++ b/test/new/moved-form-radios.browser.test.ts @@ -1,5 +1,5 @@ import { expect, onTestFinished, test } from "vitest" -import { morph } from "../../src/morphlex" +import { morph, morphInner } from "../../src/morphlex" // Chromium and Firefox briefly reset the form of a radio with a `form` attribute while an ancestor of it moves. // A checked one then joins another group for that moment and unchecks the radio there. @@ -103,3 +103,28 @@ test("moving a form holding a radio the user checked that names a missing form k expect(checked(host, "#c2")).toBe(true) expect(checked(host, "#c4")).toBe(false) }) + +test("an untouched radio with a form attribute in a moved form still follows its checked attribute", () => { + const form = `
` + const host = mount(`

${form}`) + + morph(host, `
${form}

`) + host.querySelector("#r")!.removeAttribute("checked") + + expect(checked(host, "#r")).toBe(false) +}) + +test("inserting a live form holding radios the user checked keeps them checked when one names a missing form", () => { + for (const preserveChanges of [false, true]) { + const host = mount( + `
`, + ) + host.querySelector("#c2")!.checked = true + host.querySelector("#c4")!.checked = true + + morphInner(host.querySelector("#root")!, host.querySelector("#source")!, { preserveChanges }) + + expect(checked(host, "#c2")).toBe(true) + expect(checked(host, "#c4")).toBe(true) + } +})