Skip to content

Commit 84a4631

Browse files
committed
Improve test coverage with AI
1 parent ece220c commit 84a4631

File tree

7 files changed

+553
-3
lines changed

7 files changed

+553
-3
lines changed

src/morphlex.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -538,9 +538,9 @@ class Morph {
538538
if (
539539
isElement(candidate) &&
540540
element.localName === candidate.localName &&
541-
((name !== "" && name === candidate.getAttribute("name")) ||
542-
(href !== "" && href === candidate.getAttribute("href")) ||
543-
(src !== "" && src === candidate.getAttribute("src")))
541+
((name && name === candidate.getAttribute("name")) ||
542+
(href && href === candidate.getAttribute("href")) ||
543+
(src && src === candidate.getAttribute("src")))
544544
) {
545545
matches[i] = candidate
546546
candidateElements.delete(candidate)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { test, expect, describe } from "vitest"
2+
import { morph } from "../../src/morphlex"
3+
import { dom } from "../new/utils"
4+
5+
describe("attribute removal edge cases", () => {
6+
test("removing selected attribute from option with multiple options selected", () => {
7+
const a = dom(`<select multiple><option value="a" selected>A</option><option value="b" selected>B</option></select>`) as HTMLSelectElement
8+
const b = dom(`<select multiple><option value="a">A</option><option value="b" selected>B</option></select>`) as HTMLSelectElement
9+
10+
morph(a, b, { preserveChanges: true })
11+
12+
// Line 417-418: when selected === defaultSelected, we set selected = false
13+
expect(a.options[0].hasAttribute("selected")).toBe(false)
14+
expect(a.options[0].selected).toBe(false)
15+
})
16+
17+
test("removing selected attribute preserves user selection when it differs from default", () => {
18+
const a = dom(`<select><option value="a">A</option><option value="b" selected>B</option></select>`) as HTMLSelectElement
19+
const b = dom(`<select><option value="a">A</option><option value="b">B</option></select>`) as HTMLSelectElement
20+
21+
// User changes selection - now selected !== defaultSelected
22+
a.options[0].selected = true
23+
24+
morph(a, b, { preserveChanges: true })
25+
26+
// Should preserve the user's selection
27+
expect(a.options[1].hasAttribute("selected")).toBe(false)
28+
expect(a.options[0].selected).toBe(true)
29+
})
30+
31+
test("removing checked attribute from checkbox when preserveChanges disabled", () => {
32+
const a = dom(`<input type="checkbox" checked>`) as HTMLInputElement
33+
const b = dom(`<input type="checkbox">`) as HTMLInputElement
34+
35+
a.checked = false
36+
37+
morph(a, b, { preserveChanges: false })
38+
39+
expect(a.hasAttribute("checked")).toBe(false)
40+
expect(a.checked).toBe(false)
41+
})
42+
})
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { test, expect } from "vitest"
2+
import { morph } from "../../src/morphlex"
3+
import { dom } from "../new/utils"
4+
5+
test("morphing inputs by localName with same types matches correctly", () => {
6+
// This test ensures lines 566-568 are covered (the non-continue path)
7+
// Inputs without id or name attributes fall through to localName matching
8+
const a = dom(`<form><input type="email" class="first"><input type="email" class="second"></form>`) as HTMLElement
9+
const b = dom(`<form><input type="email" placeholder="a"><input type="email" placeholder="b"></form>`) as HTMLElement
10+
11+
const first = a.children[0] as HTMLInputElement
12+
const second = a.children[1] as HTMLInputElement
13+
14+
morph(a, b)
15+
16+
// Same type inputs should be reused via localName matching
17+
expect(a.children[0]).toBe(first)
18+
expect(a.children[1]).toBe(second)
19+
expect((a.children[0] as HTMLInputElement).placeholder).toBe("a")
20+
expect((a.children[1] as HTMLInputElement).placeholder).toBe("b")
21+
})
22+
23+
test("morphing inputs with different types by localName skips mismatched types", () => {
24+
// This test specifically targets lines 562-564 in morphlex.ts (the continue path)
25+
// We need inputs without IDs or name attributes, so they fall through to localName matching
26+
const a = dom(
27+
`<div><input type="text" class="a"><input type="checkbox" class="b"><input type="text" class="c"></div>`,
28+
) as HTMLElement
29+
const b = dom(
30+
`<div><input type="checkbox" class="x"><input type="text" class="y"><input type="text" class="z"></div>`,
31+
) as HTMLElement
32+
33+
morph(a, b)
34+
35+
// The first input (text) can't match the first target (checkbox) due to type mismatch (line 563-564)
36+
// So different elements should be created/replaced
37+
const inputs = Array.from(a.children) as HTMLInputElement[]
38+
39+
expect(inputs[0].type).toBe("checkbox")
40+
expect(inputs[0].className).toBe("x")
41+
expect(inputs[1].type).toBe("text")
42+
expect(inputs[1].className).toBe("y")
43+
expect(inputs[2].type).toBe("text")
44+
expect(inputs[2].className).toBe("z")
45+
})
46+
47+
test("morphing option with selected attribute removed when matches default", () => {
48+
// This test targets line 418 in morphlex.ts
49+
const a = dom(
50+
`<select multiple><option value="a" selected>A</option><option value="b" selected>B</option><option value="c">C</option></select>`,
51+
) as HTMLSelectElement
52+
const b = dom(
53+
`<select multiple><option value="a">A</option><option value="b" selected>B</option><option value="c">C</option></select>`,
54+
) as HTMLSelectElement
55+
56+
// First option has selected attribute, so selected === defaultSelected (both true)
57+
const firstOption = a.options[0]
58+
expect(firstOption.selected).toBe(true)
59+
60+
morph(a, b, { preserveChanges: true })
61+
62+
// Line 418: since selected === defaultSelected, we set selected = false
63+
expect(a.options[0].selected).toBe(false)
64+
expect(a.options[0].hasAttribute("selected")).toBe(false)
65+
})
66+
67+
test("morphing option with selected attribute removed with preserveChanges false", () => {
68+
// This test targets line 418 with preserveChanges: false branch
69+
const a = dom(`<select><option value="a" selected>A</option><option value="b">B</option></select>`) as HTMLSelectElement
70+
const b = dom(`<select><option value="a">A</option><option value="b">B</option></select>`) as HTMLSelectElement
71+
72+
// First option has selected attribute
73+
expect(a.options[0].selected).toBe(true)
74+
75+
morph(a, b, { preserveChanges: false })
76+
77+
// Line 418: with preserveChanges false, we set selected = false
78+
expect(a.options[0].hasAttribute("selected")).toBe(false)
79+
})
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { test, expect } from "vitest"
2+
import { morph } from "../../src/morphlex"
3+
import { dom } from "../new/utils"
4+
5+
test("input type mismatch triggers continue - then finds matching type", () => {
6+
// This test ensures we hit the continue statement on line 564
7+
// by having two input candidates where:
8+
// 1. First candidate has wrong type (continue is executed)
9+
// 2. Second candidate has correct type (match succeeds)
10+
const a = dom(
11+
`<div>
12+
<input type="text" data-marker="1">
13+
<input type="checkbox" data-marker="2">
14+
</div>`,
15+
) as HTMLElement
16+
17+
const b = dom(
18+
`<div>
19+
<input type="checkbox" data-marker="new">
20+
</div>`,
21+
) as HTMLElement
22+
23+
morph(a, b)
24+
25+
// The checkbox should be reused, text input removed
26+
expect(a.children.length).toBe(1)
27+
expect((a.children[0] as HTMLInputElement).type).toBe("checkbox")
28+
expect((a.children[0] as HTMLInputElement).getAttribute("data-marker")).toBe("new")
29+
})
30+
31+
test("input type mismatch with multiple wrong types before match", () => {
32+
// Multiple candidates with wrong types, continue is executed multiple times
33+
const a = dom(
34+
`<div>
35+
<input type="text">
36+
<input type="radio">
37+
<input type="number">
38+
<input type="email" data-id="target">
39+
</div>`,
40+
) as HTMLElement
41+
42+
const b = dom(
43+
`<div>
44+
<input type="email" data-id="new">
45+
</div>`,
46+
) as HTMLElement
47+
48+
morph(a, b)
49+
50+
// Text, radio, and number inputs trigger continue, email matches
51+
expect(a.children.length).toBe(1)
52+
expect((a.children[0] as HTMLInputElement).type).toBe("email")
53+
expect((a.children[0] as HTMLInputElement).getAttribute("data-id")).toBe("new")
54+
})
55+
56+
test("input type mismatch with no matching type - all trigger continue", () => {
57+
// All candidates have wrong type, continue is executed for all, no match found
58+
const a = dom(
59+
`<div>
60+
<input type="text">
61+
<input type="checkbox">
62+
<input type="radio">
63+
</div>`,
64+
) as HTMLElement
65+
66+
const b = dom(
67+
`<div>
68+
<input type="email">
69+
</div>`,
70+
) as HTMLElement
71+
72+
morph(a, b)
73+
74+
// No type matches, so new element is created, old ones removed
75+
expect(a.children.length).toBe(1)
76+
expect((a.children[0] as HTMLInputElement).type).toBe("email")
77+
})
78+
79+
test("input with matching type does not trigger continue", () => {
80+
// When types match, the continue branch is NOT taken
81+
const a = dom(
82+
`<div>
83+
<input type="text" data-value="old">
84+
<input type="text" data-value="old2">
85+
</div>`,
86+
) as HTMLElement
87+
88+
const b = dom(
89+
`<div>
90+
<input type="text" data-value="new">
91+
</div>`,
92+
) as HTMLElement
93+
94+
const firstInput = a.children[0]
95+
96+
morph(a, b)
97+
98+
// First text input matches without triggering continue
99+
expect(a.children.length).toBe(1)
100+
expect(a.children[0]).toBe(firstInput)
101+
expect((a.children[0] as HTMLInputElement).getAttribute("data-value")).toBe("new")
102+
})
103+
104+
test("non-input elements skip the type check entirely", () => {
105+
// isInputElement checks prevent non-inputs from entering the type check
106+
const a = dom(
107+
`<div>
108+
<button data-test="1">A</button>
109+
<button data-test="2">B</button>
110+
</div>`,
111+
) as HTMLElement
112+
113+
const b = dom(
114+
`<div>
115+
<button data-test="new">C</button>
116+
</div>`,
117+
) as HTMLElement
118+
119+
const firstButton = a.children[0]
120+
121+
morph(a, b)
122+
123+
// Buttons match by localName without any type checking
124+
expect(a.children.length).toBe(1)
125+
expect(a.children[0]).toBe(firstButton)
126+
expect(a.children[0].getAttribute("data-test")).toBe("new")
127+
})
128+
129+
test("mixed inputs and non-inputs in localName matching", () => {
130+
// Ensure the logic handles both inputs and non-inputs correctly
131+
const a = dom(
132+
`<div>
133+
<input type="text">
134+
<button>Button</button>
135+
<input type="email" class="target">
136+
</div>`,
137+
) as HTMLElement
138+
139+
const b = dom(
140+
`<div>
141+
<input type="email" class="new">
142+
<button>New Button</button>
143+
</div>`,
144+
) as HTMLElement
145+
146+
morph(a, b)
147+
148+
// Email input and button should both be matched
149+
expect(a.children.length).toBe(2)
150+
expect((a.children[0] as HTMLInputElement).type).toBe("email")
151+
expect(a.children[1].tagName).toBe("BUTTON")
152+
})
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { test, expect, describe } from "vitest"
2+
import { morph } from "../../src/morphlex"
3+
import { dom } from "../new/utils"
4+
5+
describe("input type mismatch", () => {
6+
test("morphing inputs with different types treats them as different elements", () => {
7+
const a = dom(`<div><input type="text" id="a"><input type="checkbox" id="b"></div>`) as HTMLElement
8+
const b = dom(`<div><input type="checkbox" id="a"><input type="text" id="b"></div>`) as HTMLElement
9+
10+
morph(a, b)
11+
12+
const firstInput = a.querySelector("#a") as HTMLInputElement
13+
const secondInput = a.querySelector("#b") as HTMLInputElement
14+
15+
expect(firstInput.type).toBe("checkbox")
16+
expect(secondInput.type).toBe("text")
17+
})
18+
19+
test("morphing inputs with same type but different order", () => {
20+
const a = dom(`<div><input type="text" id="a" value="first"><input type="text" id="b" value="second"></div>`) as HTMLElement
21+
const b = dom(`<div><input type="text" id="b" value="second"><input type="text" id="a" value="first"></div>`) as HTMLElement
22+
23+
morph(a, b)
24+
25+
const firstInput = a.firstElementChild as HTMLInputElement
26+
const secondInput = a.lastElementChild as HTMLInputElement
27+
28+
expect(firstInput.id).toBe("b")
29+
expect(firstInput.value).toBe("second")
30+
expect(secondInput.id).toBe("a")
31+
expect(secondInput.value).toBe("first")
32+
})
33+
34+
test("morphing text input to number input creates new element", () => {
35+
const a = dom(`<div><input type="text" value="hello"></div>`) as HTMLElement
36+
const b = dom(`<div><input type="number" value="123"></div>`) as HTMLElement
37+
38+
const originalInput = a.firstElementChild
39+
morph(a, b)
40+
const newInput = a.firstElementChild as HTMLInputElement
41+
42+
// Different types should result in element replacement
43+
expect(newInput.type).toBe("number")
44+
expect(newInput.value).toBe("123")
45+
})
46+
47+
test("morphing checkbox to radio creates new element", () => {
48+
const a = dom(`<div><input type="checkbox" checked></div>`) as HTMLElement
49+
const b = dom(`<div><input type="radio" name="test"></div>`) as HTMLElement
50+
51+
morph(a, b)
52+
const newInput = a.firstElementChild as HTMLInputElement
53+
54+
expect(newInput.type).toBe("radio")
55+
expect(newInput.name).toBe("test")
56+
})
57+
58+
test("morphing inputs with same type uses localName matching", () => {
59+
const a = dom(`<div><input type="text" value="a"><input type="text" value="b"></div>`) as HTMLElement
60+
const b = dom(`<div><input type="text" value="x"><input type="text" value="y"></div>`) as HTMLElement
61+
62+
const firstInput = a.children[0] as HTMLInputElement
63+
const secondInput = a.children[1] as HTMLInputElement
64+
65+
morph(a, b)
66+
67+
// Lines 566-568: inputs match by localName and type, so they're reused
68+
expect(a.children[0]).toBe(firstInput)
69+
expect(a.children[1]).toBe(secondInput)
70+
expect((a.children[0] as HTMLInputElement).value).toBe("x")
71+
expect((a.children[1] as HTMLInputElement).value).toBe("y")
72+
})
73+
74+
test("morphing mixed inputs where some types match and some don't", () => {
75+
const a = dom(`<div><input type="text" id="1"><input type="checkbox" id="2"><input type="text" id="3"></div>`) as HTMLElement
76+
const b = dom(`<div><input type="text" id="a"><input type="radio" id="b"><input type="text" id="c"></div>`) as HTMLElement
77+
78+
morph(a, b)
79+
80+
// First and third should reuse text inputs, middle should be replaced
81+
const inputs = Array.from(a.children) as HTMLInputElement[]
82+
expect(inputs[0].type).toBe("text")
83+
expect(inputs[0].id).toBe("a")
84+
expect(inputs[1].type).toBe("radio")
85+
expect(inputs[1].id).toBe("b")
86+
expect(inputs[2].type).toBe("text")
87+
expect(inputs[2].id).toBe("c")
88+
})
89+
90+
test("morphing inputs without IDs triggers localName matching with type check", () => {
91+
const a = dom(`<div><input type="text" class="a"><input type="number" class="b"></div>`) as HTMLElement
92+
const b = dom(`<div><input type="text" class="x"><input type="number" class="y"></div>`) as HTMLElement
93+
94+
const firstInput = a.children[0] as HTMLInputElement
95+
const secondInput = a.children[1] as HTMLInputElement
96+
97+
morph(a, b)
98+
99+
// Lines 566-568: same-type inputs are matched and reused via localName
100+
expect(a.children[0]).toBe(firstInput)
101+
expect(a.children[1]).toBe(secondInput)
102+
expect((a.children[0] as HTMLInputElement).className).toBe("x")
103+
expect((a.children[1] as HTMLInputElement).className).toBe("y")
104+
})
105+
})

0 commit comments

Comments
 (0)