feat(store): lit bindings (#289)

This commit is contained in:
rahim
2026-01-06 22:26:09 +11:00
committed by GitHub
parent 36ea73f424
commit 648aae7e31
50 changed files with 3561 additions and 412 deletions
@@ -0,0 +1,77 @@
import { describe, expect, it } from 'vitest';
import { isHTMLAudioElement, isHTMLMediaElement, isHTMLVideoElement } from '../predicates';
describe('DOM predicates', () => {
describe('isHTMLVideoElement', () => {
it('returns true for video elements', () => {
const video = document.createElement('video');
expect(isHTMLVideoElement(video)).toBe(true);
});
it('returns false for audio elements', () => {
const audio = document.createElement('audio');
expect(isHTMLVideoElement(audio)).toBe(false);
});
it('returns false for other elements', () => {
const div = document.createElement('div');
expect(isHTMLVideoElement(div)).toBe(false);
});
it('returns false for non-elements', () => {
expect(isHTMLVideoElement(null)).toBe(false);
expect(isHTMLVideoElement(undefined)).toBe(false);
expect(isHTMLVideoElement('video')).toBe(false);
expect(isHTMLVideoElement({})).toBe(false);
});
});
describe('isHTMLAudioElement', () => {
it('returns true for audio elements', () => {
const audio = document.createElement('audio');
expect(isHTMLAudioElement(audio)).toBe(true);
});
it('returns false for video elements', () => {
const video = document.createElement('video');
expect(isHTMLAudioElement(video)).toBe(false);
});
it('returns false for other elements', () => {
const div = document.createElement('div');
expect(isHTMLAudioElement(div)).toBe(false);
});
it('returns false for non-elements', () => {
expect(isHTMLAudioElement(null)).toBe(false);
expect(isHTMLAudioElement(undefined)).toBe(false);
expect(isHTMLAudioElement('audio')).toBe(false);
expect(isHTMLAudioElement({})).toBe(false);
});
});
describe('isHTMLMediaElement', () => {
it('returns true for video elements', () => {
const video = document.createElement('video');
expect(isHTMLMediaElement(video)).toBe(true);
});
it('returns true for audio elements', () => {
const audio = document.createElement('audio');
expect(isHTMLMediaElement(audio)).toBe(true);
});
it('returns false for other elements', () => {
const div = document.createElement('div');
expect(isHTMLMediaElement(div)).toBe(false);
});
it('returns false for non-elements', () => {
expect(isHTMLMediaElement(null)).toBe(false);
expect(isHTMLMediaElement(undefined)).toBe(false);
expect(isHTMLMediaElement('video')).toBe(false);
expect(isHTMLMediaElement({})).toBe(false);
});
});
});
@@ -0,0 +1,175 @@
import { afterEach, describe, expect, it } from 'vitest';
import { getSlottedElement } from '../slotted';
describe('getSlottedElement', () => {
let tagCounter = 0;
function uniqueTag(base: string): string {
return `${base}-${Date.now()}-${tagCounter++}`;
}
afterEach(() => {
document.body.innerHTML = '';
});
describe('default slot', () => {
function createHost(slotHtml = '<slot></slot>'): HTMLElement {
const tagName = uniqueTag('test-host');
class TestHost extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot!.innerHTML = slotHtml;
}
}
customElements.define(tagName, TestHost);
const host = document.createElement(tagName);
document.body.appendChild(host);
return host;
}
it('finds slotted element matching predicate', () => {
const host = createHost();
const video = document.createElement('video');
host.appendChild(video);
const result = getSlottedElement(host.shadowRoot!, '', el => (el instanceof HTMLVideoElement ? el : null));
expect(result).toBe(video);
});
it('returns first match when multiple elements exist', () => {
const host = createHost();
const video1 = document.createElement('video');
const video2 = document.createElement('video');
host.appendChild(video1);
host.appendChild(video2);
const result = getSlottedElement(host.shadowRoot!, '', el => (el instanceof HTMLVideoElement ? el : null));
expect(result).toBe(video1);
});
it('returns null when no match found', () => {
const host = createHost();
const span = document.createElement('span');
host.appendChild(span);
const result = getSlottedElement(host.shadowRoot!, '', el => (el instanceof HTMLVideoElement ? el : null));
expect(result).toBeNull();
});
it('returns null when slot is empty', () => {
const host = createHost();
const result = getSlottedElement(host.shadowRoot!, '', el => (el instanceof HTMLVideoElement ? el : null));
expect(result).toBeNull();
});
it('supports predicates returning false', () => {
const host = createHost();
const div = document.createElement('div');
host.appendChild(div);
const result = getSlottedElement(host.shadowRoot!, '', el => (el instanceof HTMLVideoElement ? el : false));
expect(result).toBeNull();
});
it('works with type guard predicates', () => {
const host = createHost();
const video = document.createElement('video');
host.appendChild(video);
const isMedia = (el: Element): el is HTMLMediaElement => el instanceof HTMLMediaElement;
const result = getSlottedElement(host.shadowRoot!, '', el => (isMedia(el) ? el : null));
expect(result).toBe(video);
expect(result?.play).toBeDefined();
});
});
describe('named slot', () => {
function createHostWithNamedSlot(): HTMLElement {
const tagName = uniqueTag('test-named');
class TestHost extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot!.innerHTML = '<slot name="media"></slot><slot></slot>';
}
}
customElements.define(tagName, TestHost);
const host = document.createElement(tagName);
document.body.appendChild(host);
return host;
}
it('finds element in named slot', () => {
const host = createHostWithNamedSlot();
const video = document.createElement('video');
video.slot = 'media';
host.appendChild(video);
const result = getSlottedElement(host.shadowRoot!, 'media', el => (el instanceof HTMLVideoElement ? el : null));
expect(result).toBe(video);
});
it('ignores elements in other slots', () => {
const host = createHostWithNamedSlot();
const video = document.createElement('video');
// No slot attribute - goes to default slot
host.appendChild(video);
const result = getSlottedElement(host.shadowRoot!, 'media', el => (el instanceof HTMLVideoElement ? el : null));
expect(result).toBeNull();
});
it('returns null when named slot does not exist', () => {
const host = createHostWithNamedSlot();
const video = document.createElement('video');
video.slot = 'nonexistent';
host.appendChild(video);
const result = getSlottedElement(host.shadowRoot!, 'nonexistent', el =>
el instanceof HTMLVideoElement ? el : null);
expect(result).toBeNull();
});
});
describe('no slot in shadow root', () => {
it('returns null when shadow root has no matching slot', () => {
const tagName = uniqueTag('test-no-slot');
class TestHost extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot!.innerHTML = '<div>No slot here</div>';
}
}
customElements.define(tagName, TestHost);
const host = document.createElement(tagName);
document.body.appendChild(host);
const video = document.createElement('video');
host.appendChild(video);
const result = getSlottedElement(host.shadowRoot!, '', el => (el instanceof HTMLVideoElement ? el : null));
expect(result).toBeNull();
});
});
});