fix: boolean props w/ bare attr as true
Some checks are pending
Tests / Unit (push) Waiting to run
Tests / E2E (push) Waiting to run

This commit is contained in:
ayo 2026-07-19 18:14:13 +02:00
parent 5635e6eb4a
commit 737ef54763
4 changed files with 57 additions and 5 deletions

View file

@ -101,7 +101,7 @@
"size-limit": [
{
"path": "./dist/WebComponent.js",
"limit": "1.35 KB"
"limit": "1.4 KB"
},
{
"path": "./dist/html.js",

View file

@ -5,8 +5,11 @@
*/
export function deserialize(value, type) {
switch (type) {
case 'number':
case 'boolean':
// bare presence follows the HTML convention: `<el flag>` / `flag=""` is true.
if (value === '') return true
// falls through
case 'number':
case 'object':
case 'undefined':
return JSON.parse(value)

View file

@ -35,6 +35,11 @@ describe('WebComponent', () => {
* and returns it. Remove it with `el.remove()` to trigger disconnect.
*/
let tagSeq = 0
/**
*
* @param Class
* @param attributes
*/
function mount(Class, attributes = {}) {
const tag = `wc-test-${tagSeq++}`
window.customElements.define(tag, Class)
@ -265,6 +270,38 @@ describe('reactive props (documented contract)', () => {
expect(el.props.count).toBe(5)
})
it('turns a boolean prop on from a bare attribute (<el active>)', () => {
const el = connected()
// `<el active>` and `<el active="">` are the same to the DOM
el.setAttribute('active', '')
expect(el.props.active).toBe(true)
})
it('honors an explicit "true"/"false" boolean attribute value', () => {
const el = connected()
el.setAttribute('active', 'true')
expect(el.props.active).toBe(true)
el.setAttribute('active', 'false')
expect(el.props.active).toBe(false)
})
it('resets a boolean prop to its default when the attribute is removed', () => {
const el = connected()
el.setAttribute('active', '')
el.removeAttribute('active')
expect(el.props.active).toBe(false)
})
it('round-trips a boolean prop write through its reflected attribute', () => {
const el = connected()
el.props.active = true
expect(el.getAttribute('active')).toBe('true')
expect(el.props.active).toBe(true)
el.props.active = false
expect(el.getAttribute('active')).toBe('false')
expect(el.props.active).toBe(false)
})
it('fires onChanges with property/attribute/previousValue/currentValue', () => {
const changes = []
class WithChanges extends WebComponent {
@ -475,7 +512,10 @@ describe('attribute value & removal handling', () => {
expect(el.props.label).toBe('')
// regression: must NOT echo back as "true"
expect(el.getAttribute('label')).toBe('')
expect(changes.at(-1)).toMatchObject({ property: 'label', currentValue: '' })
expect(changes.at(-1)).toMatchObject({
property: 'label',
currentValue: '',
})
})
it('resets a prop to its static default when the attribute is removed', () => {
@ -518,7 +558,10 @@ describe('attribute value & removal handling', () => {
// malformed value must not throw and must still fire onChanges/render
expect(() => el.setAttribute('count', 'abc')).not.toThrow()
expect(changes.at(-1)).toMatchObject({ property: 'count', currentValue: 'abc' })
expect(changes.at(-1)).toMatchObject({
property: 'count',
currentValue: 'abc',
})
error.mockRestore()
})

View file

@ -13,8 +13,14 @@ describe('deserialize', () => {
expect(deserialize('false', 'boolean')).toBe(false)
})
test('treats a bare/empty boolean attribute as true', () => {
expect(deserialize('', 'boolean')).toBe(true)
})
test('parses object strings', () => {
expect(deserialize('{"hello":"world"}', 'object')).toEqual({ hello: 'world' })
expect(deserialize('{"hello":"world"}', 'object')).toEqual({
hello: 'world',
})
expect(deserialize('[1,2,3]', 'object')).toEqual([1, 2, 3])
expect(deserialize('null', 'object')).toBeNull()
})