Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 135 additions & 1 deletion packages/webui-framework/src/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,46 @@
import { strict as assert } from 'node:assert';
import { describe, test } from 'node:test';

import { toKebabCase } from './decorators.js';
import {
attr,
getObservableNames,
isAttributeProperty,
observable,
toKebabCase,
} from './decorators.js';

class FakeElement {
$ready = true;
isConnected = false;
private readonly attrs = new Map<string, string>();

getAttribute(name: string): string | null {
return this.attrs.get(name) ?? null;
}

hasAttribute(name: string): boolean {
return this.attrs.has(name);
}

setAttribute(name: string, value: string): void {
const oldValue = this.getAttribute(name);
const newValue = String(value);
this.attrs.set(name, newValue);
this.attributeChangedCallback?.(name, oldValue, newValue);
}

removeAttribute(name: string): void {
const oldValue = this.getAttribute(name);
this.attrs.delete(name);
this.attributeChangedCallback?.(name, oldValue, null);
}

attributeChangedCallback?(
name: string,
oldValue: string | null,
newValue: string | null,
): void;
}

describe('toKebabCase', () => {
test('converts multi-word ARIA properties to correct attribute names', () => {
Expand Down Expand Up @@ -79,3 +118,98 @@ describe('toKebabCase', () => {
assert.equal(toKebabCase('dataTitle'), 'data-title');
});
});

describe('observable decorators', () => {
test('@observable registers reactive property names', () => {
class TestElement {}
observable(TestElement.prototype, 'count');

assert.equal(getObservableNames(TestElement).has('count'), true);
});

test('@observable names inherit through subclasses', () => {
class BaseElement {}
observable(BaseElement.prototype, 'baseCount');
class TestElement extends BaseElement {}
observable(TestElement.prototype, 'count');

const names = getObservableNames(TestElement);
assert.equal(names.has('baseCount'), true);
assert.equal(names.has('count'), true);
});

test('@attr registers reactive property names', () => {
class TestElement {}
attr(TestElement.prototype, 'label');

assert.equal(getObservableNames(TestElement).has('label'), true);
});

test('@attr reflects property values without stringifying backing state', () => {
class TestElement extends FakeElement {}
attr(TestElement.prototype, 'count');

const element = new TestElement() as TestElement & { count: number };
element.count = 5;

assert.equal(element.getAttribute('count'), '5');
assert.equal(element.count, 5);
});

test('@attr reacts to string attribute changes', () => {
class TestElement extends FakeElement {}
attr({ attribute: 'display-value' })(TestElement.prototype, 'displayValue');

const element = new TestElement() as TestElement & { displayValue: string | null };
element.setAttribute('display-value', 'Ready');

assert.equal(element.displayValue, 'Ready');
});

test('@attr boolean mode reflects presence and removal', () => {
class TestElement extends FakeElement {}
attr({ mode: 'boolean', attribute: 'is-active' })(TestElement.prototype, 'isActive');

const element = new TestElement() as TestElement & { isActive: boolean };
element.isActive = true;
assert.equal(element.hasAttribute('is-active'), true);

element.isActive = false;
assert.equal(element.hasAttribute('is-active'), false);

element.setAttribute('is-active', '');
assert.equal(element.isActive, true);

element.removeAttribute('is-active');
assert.equal(element.isActive, false);
});

test('@attr metadata inherits through subclasses', () => {
class BaseElement extends FakeElement {}
attr(BaseElement.prototype, 'baseLabel');
class TestElement extends BaseElement {}
attr(TestElement.prototype, 'label');

const names = getObservableNames(TestElement);
assert.equal(names.has('baseLabel'), true);
assert.equal(names.has('label'), true);
assert.equal(isAttributeProperty(TestElement, 'baseLabel'), true);
assert.equal(isAttributeProperty(TestElement, 'label'), true);
assert.equal(isAttributeProperty(TestElement, 'missing'), false);

const element = new TestElement() as TestElement & {
baseLabel: string;
label: string;
};
element.setAttribute('base-label', 'Base');
element.setAttribute('label', 'Child');

assert.equal(element.baseLabel, 'Base');
assert.equal(element.label, 'Child');

element.baseLabel = 'Updated Base';
element.label = 'Updated Child';
assert.equal(element.getAttribute('base-label'), 'Updated Base');
assert.equal(element.getAttribute('label'), 'Updated Child');
});
});
Loading
Loading