Feature: Feature and preset reference — E2E tests + implementation (#1248)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Darius Cepulis
2026-04-07 09:24:23 -05:00
committed by GitHub
co-authored by Claude Opus 4.6
parent d2c43db550
commit a4d8e2a255
26 changed files with 1112 additions and 1 deletions
@@ -35,10 +35,40 @@
* create* factory, mixin display name stripping, selector discovery,
* @label overloads, slug collision (react vs html create-player),
* framework assignment.
*
* Features (packages/core/src/dom/store/features/):
* playback.ts — Simple feature. Exercises: boolean state properties,
* void/Promise action methods, JSDoc description extraction.
* volume.ts — Complex feature. Exercises: numeric state, type alias
* (MediaFeatureAvailability), methods with params + returns,
* interface-level JSDoc → feature description.
* presets.ts — Feature bundles. Exercises: plural *Features naming
* (filtered out of feature discovery), array resolution
* for preset feature lists.
* feature.parts.ts — Short aliases (playbackFeature as playback, etc.).
* Exercises: namespace re-export filtering (export * as features).
* index.ts — Re-export barrel. Exercises: feature discovery filtering
* (singular *Feature only, not *Features or namespaces).
*
* Presets:
* HTML (packages/html/src/presets/):
* video.ts — Exercises: feature bundle export, multiple HTML skins
* (SkinElement inheritance), tailwind skin exclusion.
* audio.ts — Exercises: single skin, subset of features.
* React (packages/react/src/presets/):
* video/ — Exercises: feature bundle, React skins (*Skin naming),
* media element export, tailwind skin exclusion.
* audio/ — Exercises: single skin, different media element.
*/
import * as path from 'node:path';
import { describe, expect, it } from 'vitest';
import { generateComponentReferences } from '../pipeline';
import {
type FeatureResult,
generateComponentReferences,
generateFeatureReferences,
generatePresetReferences,
type PresetResult,
} from '../pipeline';
import { getUtilEntries, type UtilEntry } from '../util-handler';
const FIXTURE_ROOT = path.resolve(import.meta.dirname, 'fixtures/monorepo');
@@ -629,3 +659,306 @@ describe('Util pipeline (end-to-end)', () => {
});
});
});
// ═══════════════════════════════════════════════════════════════════════
// FEATURE PIPELINE
// ═══════════════════════════════════════════════════════════════════════
//
// Features are defined via `definePlayerFeature()` and discovered from
// the features index. Each feature's state interface is split into two
// records: `state` (non-method properties) and `actions` (methods).
//
// Key behaviors:
// - Discovery: singular *Feature exports from the features index
// - Filtering: plural *Features (feature bundles) are excluded
// - State extraction: interface properties → state record
// - Action extraction: interface methods → actions record
// - JSDoc: member descriptions flow through, interface-level JSDoc
// becomes the feature description
// - Type aliases: expanded in the output (MediaFeatureAvailability →
// 'available' | 'unavailable' | 'unsupported')
// - Slug: derived from feature name, used for cross-linking from presets
describe('Feature pipeline (end-to-end)', () => {
const results = generateFeatureReferences(FIXTURE_ROOT);
function findFeature(name: string): FeatureResult | undefined {
return results.find((r) => r.name === name);
}
// ─────────────────────────────────────────────────────────────────
// DISCOVERY
// ─────────────────────────────────────────────────────────────────
describe('Discovery', () => {
it('discovers features from the features index', () => {
const names = results.map((r) => r.name);
expect(names).toContain('playback');
expect(names).toContain('volume');
});
it('excludes feature bundles (plural *Features)', () => {
const names = results.map((r) => r.name);
expect(names).not.toContain('videoFeatures');
expect(names).not.toContain('audioFeatures');
});
it('excludes namespace re-exports (export * as features)', () => {
const names = results.map((r) => r.name);
expect(names).not.toContain('features');
});
it('produces one result per feature', () => {
expect(results.length).toBe(2);
});
});
// ─────────────────────────────────────────────────────────────────
// PLAYBACK FEATURE (simple: booleans + void methods)
// ─────────────────────────────────────────────────────────────────
//
// MediaPlaybackState has:
// - paused: boolean (state)
// - ended: boolean (state)
// - play(): Promise<void> (action)
// - pause(): void (action)
// No interface-level JSDoc → no feature description.
describe('playback (simple feature)', () => {
it('has name and slug', () => {
const playback = findFeature('playback');
expect(playback).toBeDefined();
expect(playback!.slug).toBe('playback');
expect(playback!.reference.name).toBe('playback');
expect(playback!.reference.slug).toBe('playback');
});
it('has no description (no interface-level JSDoc)', () => {
const ref = findFeature('playback')!.reference;
expect(ref.description).toBeUndefined();
});
it('extracts boolean properties as state', () => {
const state = findFeature('playback')!.reference.state;
expect(state.paused).toEqual({
type: 'boolean',
description: 'Whether playback is paused.',
});
expect(state.ended).toEqual({
type: 'boolean',
description: 'Whether playback has reached the end.',
});
});
it('extracts methods as actions', () => {
const actions = findFeature('playback')!.reference.actions;
expect(actions.play).toBeDefined();
expect(actions.play!.type).toContain('Promise');
expect(actions.play!.description).toBe('Start playback.');
expect(actions.pause).toBeDefined();
expect(actions.pause!.type).toContain('void');
expect(actions.pause!.description).toBe('Pause playback.');
});
it('does not mix state and actions', () => {
const ref = findFeature('playback')!.reference;
// Methods should not appear in state
expect(ref.state['play' as keyof typeof ref.state]).toBeUndefined();
expect(ref.state['pause' as keyof typeof ref.state]).toBeUndefined();
// Properties should not appear in actions
expect(ref.actions['paused' as keyof typeof ref.actions]).toBeUndefined();
expect(ref.actions['ended' as keyof typeof ref.actions]).toBeUndefined();
});
});
// ─────────────────────────────────────────────────────────────────
// VOLUME FEATURE (complex: types, params, returns, description)
// ─────────────────────────────────────────────────────────────────
//
// MediaVolumeState has interface-level JSDoc → feature description.
// - volume: number (state)
// - muted: boolean (state)
// - volumeAvailability: MediaFeatureAvailability (state, type alias)
// - setVolume(volume: number): number (action with param + return)
// - toggleMuted(): boolean (action with return)
describe('volume (complex feature)', () => {
it('has description from interface-level JSDoc', () => {
const ref = findFeature('volume')!.reference;
expect(ref.description).toBe('Controls audio volume and mute state.');
});
it('extracts state with various types', () => {
const state = findFeature('volume')!.reference.state;
expect(state.volume).toMatchObject({
type: 'number',
description: 'Volume level from 0 (silent) to 1 (max).',
});
expect(state.muted).toMatchObject({
type: 'boolean',
description: 'Whether audio is muted.',
});
});
it('expands type aliases in state', () => {
const state = findFeature('volume')!.reference.state;
// MediaFeatureAvailability should be expanded to the union
const avail = state.volumeAvailability!;
expect(avail.type).toContain("'available'");
expect(avail.type).toContain("'unavailable'");
expect(avail.type).toContain("'unsupported'");
});
it('extracts actions with parameters and return types', () => {
const actions = findFeature('volume')!.reference.actions;
// setVolume has a parameter and returns a number
expect(actions.setVolume).toBeDefined();
expect(actions.setVolume!.type).toContain('number');
expect(actions.setVolume!.description).toBe('Set volume (clamped 0-1). Returns the clamped value.');
// toggleMuted returns a boolean
expect(actions.toggleMuted).toBeDefined();
expect(actions.toggleMuted!.type).toContain('boolean');
expect(actions.toggleMuted!.description).toBe('Toggle mute state. Returns the new muted value.');
});
});
});
// ═══════════════════════════════════════════════════════════════════════
// PRESET PIPELINE
// ═══════════════════════════════════════════════════════════════════════
//
// Presets bundle features, skins, and media elements for a specific use
// case. They are discovered from directories under packages/{html,react}/
// src/presets/.
//
// Key behaviors:
// - Discovery: directories under both HTML and React preset paths
// - Feature bundle: *Features export → resolved to list of feature names
// - HTML skins: classes extending SkinElement, with tagName
// - React skins: exports matching *Skin naming
// - Media element: React exports that aren't bundles or skins
// - Tailwind exclusion: .tailwind files/exports are filtered out
// - HTML media element: implied by preset name (video → <video>)
describe('Preset pipeline (end-to-end)', () => {
const results = generatePresetReferences(FIXTURE_ROOT);
function findPreset(name: string): PresetResult | undefined {
return results.find((r) => r.name === name);
}
// ─────────────────────────────────────────────────────────────────
// DISCOVERY
// ─────────────────────────────────────────────────────────────────
describe('Discovery', () => {
it('discovers presets from preset directories', () => {
const names = results.map((r) => r.name).sort();
expect(names).toEqual(['audio', 'video']);
});
it('produces one result per preset', () => {
expect(results.length).toBe(2);
});
});
// ─────────────────────────────────────────────────────────────────
// VIDEO PRESET (full: multiple skins, tailwind exclusion)
// ─────────────────────────────────────────────────────────────────
describe('video preset', () => {
it('identifies the feature bundle', () => {
const ref = findPreset('video')!.reference;
expect(ref.featureBundle).toBe('videoFeatures');
});
it('resolves feature names from the bundle', () => {
const ref = findPreset('video')!.reference;
expect(ref.features).toEqual(expect.arrayContaining(['playback', 'volume']));
expect(ref.features.length).toBe(2);
});
it('detects HTML skins with tagNames', () => {
const skins = findPreset('video')!.reference.html.skins;
expect(skins).toEqual(
expect.arrayContaining([
{ name: 'VideoSkinElement', tagName: 'video-skin' },
{ name: 'MinimalVideoSkinElement', tagName: 'video-minimal-skin' },
])
);
});
it('excludes HTML tailwind skins', () => {
const skinNames = findPreset('video')!.reference.html.skins.map((s) => s.name);
expect(skinNames).not.toContain('VideoSkinTailwindElement');
});
it('detects React skins', () => {
const skins = findPreset('video')!.reference.react.skins;
expect(skins).toEqual(expect.arrayContaining([{ name: 'VideoSkin' }, { name: 'MinimalVideoSkin' }]));
});
it('excludes React tailwind skins', () => {
const skinNames = findPreset('video')!.reference.react.skins.map((s) => s.name);
expect(skinNames).not.toContain('VideoSkinTailwind');
});
it('detects React media element', () => {
const ref = findPreset('video')!.reference;
expect(ref.react.mediaElement).toBe('Video');
});
});
// ─────────────────────────────────────────────────────────────────
// AUDIO PRESET (minimal: single skin, subset of features)
// ─────────────────────────────────────────────────────────────────
describe('audio preset', () => {
it('identifies the feature bundle', () => {
const ref = findPreset('audio')!.reference;
expect(ref.featureBundle).toBe('audioFeatures');
});
it('resolves feature names (subset of video)', () => {
const ref = findPreset('audio')!.reference;
expect(ref.features).toEqual(['playback']);
});
it('detects single HTML skin', () => {
const skins = findPreset('audio')!.reference.html.skins;
expect(skins).toEqual([{ name: 'AudioSkinElement', tagName: 'audio-skin' }]);
});
it('detects single React skin', () => {
const skins = findPreset('audio')!.reference.react.skins;
expect(skins).toEqual([{ name: 'AudioSkin' }]);
});
it('detects React media element', () => {
const ref = findPreset('audio')!.reference;
expect(ref.react.mediaElement).toBe('Audio');
});
});
// ─────────────────────────────────────────────────────────────────
// CROSS-CUTTING: feature links
// ─────────────────────────────────────────────────────────────────
describe('Cross-cutting', () => {
it('feature names in presets match feature reference slugs', () => {
const featureResults = generateFeatureReferences(FIXTURE_ROOT);
const featureSlugs = featureResults.map((r) => r.slug);
const videoPreset = findPreset('video')!.reference;
for (const featureName of videoPreset.features) {
expect(featureSlugs).toContain(featureName);
}
});
});
});
@@ -0,0 +1,55 @@
/*
* Feature state interface fixtures.
*
* Exercises: property extraction (state), method extraction (actions),
* JSDoc description flow-through, type alias resolution (MediaFeatureAvailability),
* method parameter types, method return types, Promise return types.
*/
export interface MediaPlaybackState {
/**
* Whether playback is paused.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/paused
*/
paused: boolean;
/**
* Whether playback has reached the end.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended
*/
ended: boolean;
/**
* Start playback.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play
*/
play(): Promise<void>;
/** Pause playback. */
pause(): void;
}
/** Indicates whether a feature can be programmatically controlled on this platform. */
export type MediaFeatureAvailability = 'available' | 'unavailable' | 'unsupported';
/** Controls audio volume and mute state. */
export interface MediaVolumeState {
/**
* Volume level from 0 (silent) to 1 (max).
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volume
*/
volume: number;
/** Whether audio is muted. */
muted: boolean;
/** Whether volume can be programmatically set on this platform. */
volumeAvailability: MediaFeatureAvailability;
/**
* Set volume (clamped 0-1). Returns the clamped value.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volume
*/
setVolume(volume: number): number;
/** Toggle mute state. Returns the new muted value. */
toggleMuted(): boolean;
}
@@ -0,0 +1,9 @@
/**
* Mock definePlayerFeature identity function matching the real signature.
* The builder only needs the TypeScript types to resolve; it never runs this.
*/
export const definePlayerFeature = <State>(config: {
name?: string;
state: (ctx: any) => State;
attach?: (ctx: any) => void;
}) => config;
@@ -0,0 +1,8 @@
/**
* Short alias re-exports for features.
*
* Exercises: namespace re-export filtering `export * as features from './feature.parts'`
* in the index should NOT produce a feature entry named "features".
*/
export { playbackFeature as playback } from './playback';
export { volumeFeature as volume } from './volume';
@@ -0,0 +1,12 @@
/**
* Features index fixture.
*
* Exercises: feature discovery filters singular *Feature exports, ignores
* plural *Features (feature bundles) from presets, and ignores namespace
* re-exports (export * as features).
*/
export * as features from './feature.parts';
export * from './playback';
export * from './presets';
export * from './volume';
@@ -0,0 +1,20 @@
/**
* Mock playback feature.
*
* Exercises: simple boolean state properties, void and Promise<void> action methods,
* JSDoc description extraction from the state interface.
*/
import type { MediaPlaybackState } from '../../../core/media/state';
import { definePlayerFeature } from '../../feature';
export const playbackFeature = definePlayerFeature({
name: 'playback',
state: (): MediaPlaybackState => ({
paused: true,
ended: false,
play() {
return Promise.resolve();
},
pause() {},
}),
});
@@ -0,0 +1,13 @@
/**
* Mock feature bundles.
*
* Exercises: feature bundle arrays (plural *Features naming), feature list
* resolution from array elements. videoFeatures has both features,
* audioFeatures has only playback.
*/
import { playbackFeature } from './playback';
import { volumeFeature } from './volume';
export const videoFeatures = [playbackFeature, volumeFeature];
export const audioFeatures = [playbackFeature];
@@ -0,0 +1,23 @@
/**
* Mock volume feature.
*
* Exercises: numeric state property, type alias (MediaFeatureAvailability),
* methods with parameters and return values, boolean state property.
*/
import type { MediaVolumeState } from '../../../core/media/state';
import { definePlayerFeature } from '../../feature';
export const volumeFeature = definePlayerFeature({
name: 'volume',
state: (): MediaVolumeState => ({
volume: 1,
muted: false,
volumeAvailability: 'available',
setVolume(_volume: number) {
return 1;
},
toggleMuted() {
return false;
},
}),
});
@@ -0,0 +1,10 @@
/**
* Mock HTML audio skin element.
*
* Exercises: single skin per preset, skin detection via SkinElement inheritance.
*/
import { SkinElement } from '../skin-element';
export class AudioSkinElement extends SkinElement {
static readonly tagName = 'audio-skin';
}
@@ -0,0 +1,11 @@
/**
* Mock SkinElement base class.
*
* The builder detects HTML skins by checking if a class extends SkinElement.
* This fixture provides the base class for that inheritance check.
*/
export class SkinElement {
static shadowRootOptions: any;
static styles?: any;
static template?: any;
}
@@ -0,0 +1,10 @@
/**
* Mock HTML minimal video skin element.
*
* Exercises: multiple skins per preset, skin detection via SkinElement inheritance.
*/
import { SkinElement } from '../skin-element';
export class MinimalVideoSkinElement extends SkinElement {
static readonly tagName = 'video-minimal-skin';
}
@@ -0,0 +1,10 @@
/**
* Mock HTML video tailwind skin element.
*
* Exercises: tailwind skin exclusion this should NOT appear in the output.
*/
import { SkinElement } from '../skin-element';
export class VideoSkinTailwindElement extends SkinElement {
static readonly tagName = 'video-skin-tailwind';
}
@@ -0,0 +1,10 @@
/**
* Mock HTML video skin element.
*
* Exercises: skin detection via SkinElement inheritance, tagName extraction.
*/
import { SkinElement } from '../skin-element';
export class VideoSkinElement extends SkinElement {
static readonly tagName = 'video-skin';
}
@@ -0,0 +1,7 @@
/**
* Mock HTML audio preset.
*
* Exercises: preset with fewer features and a single skin.
*/
export { audioFeatures } from '../../../core/src/dom/store/features/presets';
export { AudioSkinElement } from '../define/audio/skin';
@@ -0,0 +1,11 @@
/**
* Mock HTML video preset.
*
* Exercises: preset discovery, feature bundle export, skin exports,
* tailwind skin exclusion. HTML presets do NOT export media elements
* (the native <video> is implied by the preset name).
*/
export { videoFeatures } from '../../../core/src/dom/store/features/presets';
export { MinimalVideoSkinElement } from '../define/video/minimal-skin';
export { VideoSkinElement } from '../define/video/skin';
export { VideoSkinTailwindElement } from '../define/video/skin.tailwind';
@@ -0,0 +1,4 @@
/**
* Mock React Audio media element.
*/
export function Audio(): void {}
@@ -0,0 +1,7 @@
/**
* Mock React Video media element.
*
* Exercises: media element detection in React presets. Media elements are
* exports that are not feature bundles (*Features) and not skins (*Skin).
*/
export function Video(): void {}
@@ -0,0 +1,8 @@
/**
* Mock React audio preset.
*
* Exercises: preset with fewer features, single skin, different media element.
*/
export { audioFeatures } from '../../../../core/src/dom/store/features/presets';
export { Audio } from '../../media/audio';
export { AudioSkin } from './skin';
@@ -0,0 +1,4 @@
/**
* Mock React AudioSkin component.
*/
export function AudioSkin(): void {}
@@ -0,0 +1,11 @@
/**
* Mock React video preset.
*
* Exercises: preset discovery, feature bundle export, skin exports (named),
* media element export, tailwind skin exclusion.
*/
export { videoFeatures } from '../../../../core/src/dom/store/features/presets';
export { Video } from '../../media/video';
export { MinimalVideoSkin } from './minimal-skin';
export { VideoSkin } from './skin';
export { VideoSkinTailwind } from './skin.tailwind';
@@ -0,0 +1,6 @@
/**
* Mock React MinimalVideoSkin component.
*
* Exercises: multiple skins per preset.
*/
export function MinimalVideoSkin(): void {}
@@ -0,0 +1,6 @@
/**
* Mock React VideoSkinTailwind component.
*
* Exercises: tailwind skin exclusion this should NOT appear in the output.
*/
export function VideoSkinTailwind(): void {}
@@ -0,0 +1,6 @@
/**
* Mock React VideoSkin component.
*
* Exercises: React skin detection via *Skin naming convention.
*/
export function VideoSkin(): void {}