mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(html): prevent tsdown from stripping custom element registrations (#703)
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import { afterAll, beforeAll, describe, expect, it, type MockInstance, vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* Tests that composite define files register all expected custom elements
|
||||
* and that provider/parent elements are defined before consumer/child elements.
|
||||
*
|
||||
* Tests run sequentially. Each dynamically imports a composite define file and
|
||||
* checks the batch of `customElements.define()` calls that resulted. Because
|
||||
* modules are cached within a test file, shared sub-elements (e.g. slider parts)
|
||||
* are only registered by the first composite that imports them — subsequent
|
||||
* composites skip them via `safeDefine`. This is intentional and tested.
|
||||
*/
|
||||
describe('composite define registration', () => {
|
||||
let spy: MockInstance;
|
||||
|
||||
/** Tag names registered since `offset` (the call count before an import). */
|
||||
function batchSince(offset: number): string[] {
|
||||
return spy.mock.calls.slice(offset).map((call) => call[0] as string);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
spy = vi.spyOn(customElements, 'define');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
// ── Player composites ────────────────────────────────────────────────
|
||||
|
||||
describe('video/player', () => {
|
||||
it('registers video-player before media-container', async () => {
|
||||
const before = spy.mock.calls.length;
|
||||
await import('../video/player');
|
||||
const batch = batchSince(before);
|
||||
|
||||
expect(batch).toContain('video-player');
|
||||
expect(batch).toContain('media-container');
|
||||
expect(batch.indexOf('video-player')).toBeLessThan(batch.indexOf('media-container'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('audio/player', () => {
|
||||
it('registers audio-player', async () => {
|
||||
const before = spy.mock.calls.length;
|
||||
await import('../audio/player');
|
||||
const batch = batchSince(before);
|
||||
|
||||
expect(batch).toContain('audio-player');
|
||||
// media-container already registered by video/player — safeDefine skips it
|
||||
expect(batch).not.toContain('media-container');
|
||||
});
|
||||
});
|
||||
|
||||
describe('background/player', () => {
|
||||
it('registers background-video-player', async () => {
|
||||
const before = spy.mock.calls.length;
|
||||
await import('../background/player');
|
||||
const batch = batchSince(before);
|
||||
|
||||
expect(batch).toContain('background-video-player');
|
||||
expect(batch).not.toContain('media-container');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Slider composites ────────────────────────────────────────────────
|
||||
|
||||
describe('ui/time-slider', () => {
|
||||
it('registers media-time-slider before sub-elements', async () => {
|
||||
const before = spy.mock.calls.length;
|
||||
await import('../ui/time-slider');
|
||||
const batch = batchSince(before);
|
||||
|
||||
// Parent slider must be first (provider for sliderContext)
|
||||
expect(batch[0]).toBe('media-time-slider');
|
||||
|
||||
// All sub-elements registered (first composite to claim them)
|
||||
expect(batch).toContain('media-slider-buffer');
|
||||
expect(batch).toContain('media-slider-fill');
|
||||
expect(batch).toContain('media-slider-thumb');
|
||||
expect(batch).toContain('media-slider-track');
|
||||
expect(batch).toContain('media-slider-value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ui/volume-slider', () => {
|
||||
it('registers media-volume-slider and skips already-defined sub-elements', async () => {
|
||||
const before = spy.mock.calls.length;
|
||||
await import('../ui/volume-slider');
|
||||
const batch = batchSince(before);
|
||||
|
||||
expect(batch).toContain('media-volume-slider');
|
||||
|
||||
// Sub-elements already registered by time-slider — safeDefine skips them
|
||||
expect(batch).not.toContain('media-slider-fill');
|
||||
expect(batch).not.toContain('media-slider-thumb');
|
||||
expect(batch).not.toContain('media-slider-track');
|
||||
expect(batch).not.toContain('media-slider-value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ui/slider', () => {
|
||||
it('registers media-slider', async () => {
|
||||
const before = spy.mock.calls.length;
|
||||
await import('../ui/slider');
|
||||
const batch = batchSince(before);
|
||||
|
||||
expect(batch).toContain('media-slider');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Other composites ─────────────────────────────────────────────────
|
||||
|
||||
describe('ui/time', () => {
|
||||
it('registers media-time before sub-elements', async () => {
|
||||
const before = spy.mock.calls.length;
|
||||
await import('../ui/time');
|
||||
const batch = batchSince(before);
|
||||
|
||||
expect(batch[0]).toBe('media-time');
|
||||
expect(batch).toContain('media-time-group');
|
||||
expect(batch).toContain('media-time-separator');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ui/controls', () => {
|
||||
it('registers media-controls before sub-elements', async () => {
|
||||
const before = spy.mock.calls.length;
|
||||
await import('../ui/controls');
|
||||
const batch = batchSince(before);
|
||||
|
||||
expect(batch[0]).toBe('media-controls');
|
||||
expect(batch).toContain('media-controls-group');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Final state ──────────────────────────────────────────────────────
|
||||
|
||||
describe('registry completeness', () => {
|
||||
it('all expected elements are registered after importing all composites', () => {
|
||||
const expected = [
|
||||
// Players + container
|
||||
'video-player',
|
||||
'audio-player',
|
||||
'background-video-player',
|
||||
'media-container',
|
||||
// Sliders
|
||||
'media-slider',
|
||||
'media-time-slider',
|
||||
'media-volume-slider',
|
||||
// Slider sub-elements
|
||||
'media-slider-buffer',
|
||||
'media-slider-fill',
|
||||
'media-slider-thumb',
|
||||
'media-slider-track',
|
||||
'media-slider-value',
|
||||
// Time
|
||||
'media-time',
|
||||
'media-time-group',
|
||||
'media-time-separator',
|
||||
// Controls
|
||||
'media-controls',
|
||||
'media-controls-group',
|
||||
];
|
||||
|
||||
for (const tagName of expected) {
|
||||
expect(customElements.get(tagName), `${tagName} should be registered`).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { safeDefine } from '../safe-define';
|
||||
|
||||
describe('safeDefine', () => {
|
||||
it('registers a custom element', () => {
|
||||
class TestElement extends HTMLElement {
|
||||
static tagName = 'test-sd-register';
|
||||
}
|
||||
|
||||
expect(customElements.get('test-sd-register')).toBeUndefined();
|
||||
safeDefine(TestElement);
|
||||
expect(customElements.get('test-sd-register')).toBe(TestElement);
|
||||
});
|
||||
|
||||
it('does not throw when element is already registered', () => {
|
||||
class TestElement extends HTMLElement {
|
||||
static tagName = 'test-sd-no-throw';
|
||||
}
|
||||
|
||||
customElements.define('test-sd-no-throw', TestElement);
|
||||
expect(() => safeDefine(TestElement)).not.toThrow();
|
||||
});
|
||||
|
||||
it('does not replace an existing registration', () => {
|
||||
class Original extends HTMLElement {
|
||||
static tagName = 'test-sd-no-replace';
|
||||
}
|
||||
class Replacement extends HTMLElement {
|
||||
static tagName = 'test-sd-no-replace';
|
||||
}
|
||||
|
||||
safeDefine(Original);
|
||||
safeDefine(Replacement);
|
||||
expect(customElements.get('test-sd-no-replace')).toBe(Original);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user