{
+ const source = `
+ export default function TestSkin() {
+ const styles = { A: 'class-a', B: 'class-b', C: 'class-c' };
+ return (
+
+
+
+
+ );
+ }
+ `;
+
+ const result = compile(source);
+
+ // Template literals are not fully resolved at compile time
+ // This is expected - we just extract what we can
+ expect(result.html).toContain('');
+ expect(result.html).toContain(' {
+ const componentTests = [
+ { react: 'PlayButton', html: 'media-play-button' },
+ { react: 'MuteButton', html: 'media-mute-button' },
+ { react: 'FullscreenButton', html: 'media-fullscreen-button' },
+ { react: 'MediaContainer', html: 'media-container' },
+ { react: 'CurrentTimeDisplay', html: 'media-current-time-display' },
+ { react: 'DurationDisplay', html: 'media-duration-display' },
+ { react: 'PreviewTimeDisplay', html: 'media-preview-time-display' },
+ { react: 'PlayIcon', html: 'media-play-icon' },
+ { react: 'PauseIcon', html: 'media-pause-icon' },
+ { react: 'VolumeHighIcon', html: 'media-volume-high-icon' },
+ { react: 'VolumeLowIcon', html: 'media-volume-low-icon' },
+ { react: 'VolumeOffIcon', html: 'media-volume-off-icon' },
+ { react: 'FullscreenEnterIcon', html: 'media-fullscreen-enter-icon' },
+ { react: 'FullscreenExitIcon', html: 'media-fullscreen-exit-icon' },
+ ];
+
+ componentTests.forEach(({ react, html }) => {
+ const source = `import { ${react} } from '@videojs/react';
+export default function Test() { return <${react} />; }`;
+ const result = compile(source);
+ expect(result.html).toBe(`<${html}>${html}>`);
+ });
+ });
+
+ it('verifies all slider compound components transform correctly', () => {
+ const compoundTests = [
+ // TimeSlider
+ { react: 'TimeSlider.Root', html: 'media-time-slider', base: 'TimeSlider' },
+ { react: 'TimeSlider.Track', html: 'media-time-slider-track', base: 'TimeSlider' },
+ { react: 'TimeSlider.Progress', html: 'media-time-slider-progress', base: 'TimeSlider' },
+ { react: 'TimeSlider.Pointer', html: 'media-time-slider-pointer', base: 'TimeSlider' },
+ { react: 'TimeSlider.Thumb', html: 'media-time-slider-thumb', base: 'TimeSlider' },
+ // VolumeSlider
+ { react: 'VolumeSlider.Root', html: 'media-volume-slider', base: 'VolumeSlider' },
+ { react: 'VolumeSlider.Track', html: 'media-volume-slider-track', base: 'VolumeSlider' },
+ { react: 'VolumeSlider.Progress', html: 'media-volume-slider-progress', base: 'VolumeSlider' },
+ { react: 'VolumeSlider.Thumb', html: 'media-volume-slider-thumb', base: 'VolumeSlider' },
+ ];
+
+ compoundTests.forEach(({ react, html, base }) => {
+ const source = `import { ${base} } from '@videojs/react';
+export default function Test() { return <${react} />; }`;
+ const result = compile(source);
+ expect(result.html).toBe(`<${html}>${html}>`);
+ });
+ });
+});
diff --git a/packages/compiler/test/skins/simple-skin.test.ts b/packages/compiler/test/skins/simple-skin.test.ts
new file mode 100644
index 000000000..3e51a8043
--- /dev/null
+++ b/packages/compiler/test/skins/simple-skin.test.ts
@@ -0,0 +1,145 @@
+import { readFileSync } from 'node:fs';
+import { join } from 'node:path';
+import { describe, expect, it } from 'vitest';
+import { compileForTest as compile } from '../helpers/compile';
+import { elementExists, getClasses, parseElement, querySelector, querySelectorAll } from '../helpers/dom';
+
+describe('skin: Simple', () => {
+ it('compiles SimpleSkin.tsx with correct DOM structure', () => {
+ const source = readFileSync(
+ join(__dirname, '../fixtures/skins/simple/SimpleSkin.tsx'),
+ 'utf-8',
+ );
+
+ const result = compile(source);
+ const root = parseElement(result.html);
+
+ // Root element
+ expect(root.tagName.toLowerCase()).toBe('media-container');
+ expect(getClasses(root)).toContain('container');
+
+ // Children slot
+ const slot = querySelector(root, 'slot[name="media"]');
+ expect(slot.getAttribute('slot')).toBe('media');
+
+ // Controls container
+ const controls = querySelector(root, 'div.controls');
+ expect(controls).toBeDefined();
+
+ // Play button - className is PlayButton (component-match, omitted)
+ const playButton = querySelector(controls, 'media-play-button');
+ expect(playButton).toBeDefined();
+
+ // Time slider - verify Root → base element (no -root suffix)
+ const timeSlider = querySelector(controls, 'media-time-slider');
+ expect(getClasses(timeSlider)).toContain('slider-root'); // CSS module KEY (SliderRoot → slider-root)
+
+ // Time slider track
+ const track = querySelector(timeSlider, 'media-time-slider-track');
+ expect(getClasses(track)).toContain('slider-track'); // CSS module KEY (SliderTrack → slider-track)
+
+ // Time slider progress
+ const progress = querySelector(track, 'media-time-slider-progress');
+ expect(getClasses(progress)).toContain('slider-progress'); // CSS module KEY (SliderProgress → slider-progress)
+
+ // Verify no media-time-slider-root exists (should be media-time-slider)
+ expect(elementExists(root, 'media-time-slider-root')).toBe(false);
+ });
+
+ it('verifies all expected elements exist in DOM tree', () => {
+ const source = readFileSync(
+ join(__dirname, '../fixtures/skins/simple/SimpleSkin.tsx'),
+ 'utf-8',
+ );
+
+ const result = compile(source);
+ const root = parseElement(result.html);
+
+ // Check root itself
+ expect(root.tagName.toLowerCase()).toBe('media-container');
+
+ const expectedSelectors = [
+ 'slot[name="media"]',
+ 'div.controls',
+ 'media-play-button',
+ 'media-time-slider', // Not media-time-slider-root!
+ 'media-time-slider-track',
+ 'media-time-slider-progress',
+ ];
+
+ expectedSelectors.forEach((selector) => {
+ expect(
+ elementExists(root, selector),
+ `Expected element to exist: ${selector}`,
+ ).toBe(true);
+ });
+ });
+
+ it('verifies DOM nesting structure', () => {
+ const source = readFileSync(
+ join(__dirname, '../fixtures/skins/simple/SimpleSkin.tsx'),
+ 'utf-8',
+ );
+
+ const result = compile(source);
+ const root = parseElement(result.html);
+
+ // Verify parent-child relationships
+ const controls = querySelector(root, 'div.controls');
+ const playButton = querySelector(controls, 'media-play-button');
+ const timeSlider = querySelector(controls, 'media-time-slider');
+
+ // Play button should be direct child of controls
+ expect(playButton.parentElement).toBe(controls);
+
+ // Time slider should be direct child of controls
+ expect(timeSlider.parentElement).toBe(controls);
+
+ // Track should be child of time slider
+ const track = querySelector(timeSlider, 'media-time-slider-track');
+ expect(track.parentElement).toBe(timeSlider);
+
+ // Progress should be child of track
+ const progress = querySelector(track, 'media-time-slider-progress');
+ expect(progress.parentElement).toBe(track);
+ });
+
+ it('verifies all classNames are extracted', () => {
+ const source = readFileSync(
+ join(__dirname, '../fixtures/skins/simple/SimpleSkin.tsx'),
+ 'utf-8',
+ );
+
+ const result = compile(source);
+
+ // Expected classes: CSS module KEYS (not values), kebab-cased, component-match omitted
+ const expectedClasses = [
+ 'container', // Container (generic-style, kebab-case)
+ 'controls', // Controls (generic-style, kebab-case)
+ // play-button omitted (component-match)
+ 'slider-progress', // SliderProgress (generic-style, kebab-case)
+ 'slider-root', // SliderRoot (generic-style, kebab-case)
+ 'slider-track', // SliderTrack (generic-style, kebab-case)
+ ];
+
+ expect(result.classNames).toEqual(expectedClasses);
+
+ // Verify classes exist in DOM
+ const root = parseElement(result.html);
+
+ // Include root element in search
+ const allElements = [root, ...querySelectorAll(root, '*')];
+ const domClasses = new Set();
+
+ allElements.forEach((el) => {
+ getClasses(el).forEach(cls => domClasses.add(cls));
+ });
+
+ expectedClasses.forEach((className) => {
+ expect(
+ domClasses.has(className),
+ `Expected class to exist in DOM: ${className}`,
+ ).toBe(true);
+ });
+ });
+});
diff --git a/packages/compiler/test/utils.ts b/packages/compiler/test/utils.ts
new file mode 100644
index 000000000..80206028d
--- /dev/null
+++ b/packages/compiler/test/utils.ts
@@ -0,0 +1,47 @@
+/**
+ * Test Utilities
+ *
+ * Helper functions for testing the compiler
+ */
+
+import type { SourceContext } from '../src/phases/types';
+
+/**
+ * Create a SourceContext for testing
+ * Used throughout tests - analyze() will add projectionState
+ *
+ * @param source - Source code to compile
+ * @param overrides - Optional overrides for context fields
+ * @returns SourceContext ready for compilation
+ *
+ * @example
+ * const context = createInitialContext(`import { PlayButton } from '@videojs/react';`);
+ * const result = analyze(context);
+ */
+export function createInitialContext(
+ source: string,
+ overrides?: Partial,
+): SourceContext {
+ return {
+ input: {
+ source,
+ path: undefined,
+ ...overrides?.input,
+ },
+ };
+}
+
+/**
+ * Alias for createInitialContext
+ * Used for end-to-end compilation tests
+ *
+ * @param source - Source code to compile
+ * @param overrides - Optional overrides
+ * @returns SourceContext
+ */
+export function createSourceContext(
+ source: string,
+ overrides?: Partial