feat(site): add util reference pipeline (#537)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Darius Cepulis
2026-02-24 15:34:34 -06:00
committed by GitHub
co-authored by Claude Opus 4.6
parent c11395ece1
commit 78112fbefd
143 changed files with 7031 additions and 481 deletions
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { extractDataAttrs } from '../data-attrs-handler.js';
import { createTestProgram } from './test-utils.js';
import { createTestProgram, createTypedTestProgram } from './test-utils.js';
describe('extractDataAttrs', () => {
it('extracts from {Name}DataAttrs constant', () => {
@@ -141,6 +141,55 @@ describe('extractDataAttrs', () => {
expect(result!.attrs[0]!.description).toBe('Present when the component is focused.');
});
it('extracts @type JSDoc tag as type field', () => {
const code = `
export const MockComponentDataAttrs = {
/**
* The fill level.
* @type {'empty' | 'partial' | 'full'}
*/
fillState: 'data-fill-state',
} as const;
`;
const program = createTestProgram(code);
const result = extractDataAttrs('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.attrs[0]!.type).toBe("'empty' | 'partial' | 'full'");
});
it('separates description from @type line', () => {
const code = `
export const MockComponentDataAttrs = {
/**
* The fill level.
* @type {'empty' | 'partial' | 'full'}
*/
fillState: 'data-fill-state',
} as const;
`;
const program = createTestProgram(code);
const result = extractDataAttrs('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.attrs[0]!.description).toBe('The fill level.');
expect(result!.attrs[0]!.description).not.toContain('@type');
});
it('omits type when no @type tag present', () => {
const code = `
export const MockComponentDataAttrs = {
/** Present when the component is active. */
active: 'data-active',
} as const;
`;
const program = createTestProgram(code);
const result = extractDataAttrs('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.attrs[0]!.type).toBeUndefined();
});
it('falls back to data-{key} when value is not a string literal', () => {
const code = `
const PREFIX = 'data-';
@@ -154,4 +203,112 @@ describe('extractDataAttrs', () => {
expect(result).not.toBeNull();
expect(result!.attrs[0]!.name).toBe('data-active');
});
it('infers boolean as omitted type', () => {
const code = `
type StateAttrMap<State> = { [Key in keyof State]?: string };
interface MockComponentState {
active: boolean;
}
export const MockComponentDataAttrs = {
active: 'data-active',
} as const satisfies StateAttrMap<MockComponentState>;
`;
const program = createTypedTestProgram(code);
const result = extractDataAttrs('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.attrs[0]!.type).toBeUndefined();
});
it('infers string literal union from state type', () => {
const code = `
type StateAttrMap<State> = { [Key in keyof State]?: string };
interface MockComponentState {
level: 'low' | 'medium' | 'high';
}
export const MockComponentDataAttrs = {
level: 'data-level',
} as const satisfies StateAttrMap<MockComponentState>;
`;
const program = createTypedTestProgram(code);
const result = extractDataAttrs('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.attrs[0]!.type).toBe("'low' | 'medium' | 'high'");
});
it('infers number type from state', () => {
const code = `
type StateAttrMap<State> = { [Key in keyof State]?: string };
interface MockComponentState {
count: number;
}
export const MockComponentDataAttrs = {
count: 'data-count',
} as const satisfies StateAttrMap<MockComponentState>;
`;
const program = createTypedTestProgram(code);
const result = extractDataAttrs('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.attrs[0]!.type).toBe('number');
});
it('infers through type alias to expanded literals', () => {
const code = `
type StateAttrMap<State> = { [Key in keyof State]?: string };
type VolumeLevel = 'off' | 'low';
interface MockComponentState {
level: VolumeLevel;
}
export const MockComponentDataAttrs = {
level: 'data-level',
} as const satisfies StateAttrMap<MockComponentState>;
`;
const program = createTypedTestProgram(code);
const result = extractDataAttrs('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.attrs[0]!.type).toBe("'off' | 'low'");
});
it('JSDoc @type overrides inferred type', () => {
const code = `
type StateAttrMap<State> = { [Key in keyof State]?: string };
interface MockComponentState {
level: 'low' | 'medium' | 'high';
}
export const MockComponentDataAttrs = {
/**
* The volume level.
* @type {'quiet' | 'loud'}
*/
level: 'data-level',
} as const satisfies StateAttrMap<MockComponentState>;
`;
const program = createTypedTestProgram(code);
const result = extractDataAttrs('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.attrs[0]!.type).toBe("'quiet' | 'loud'");
});
it('no satisfies expression produces no inferred type', () => {
const code = `
export const MockComponentDataAttrs = {
active: 'data-active',
} as const;
`;
const program = createTypedTestProgram(code);
const result = extractDataAttrs('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.attrs[0]!.type).toBeUndefined();
});
});
@@ -0,0 +1,36 @@
interface MediaState {
playing: boolean;
volume: number;
currentTime: number;
duration: number;
buffered: number;
fullscreen: boolean;
}
interface PlaybackState {
playing: boolean;
}
interface VolumeState {
volume: number;
}
interface TimeState {
currentTime: number;
duration: number;
}
/** Select playback state from media state. */
export function selectPlayback(state: MediaState): PlaybackState {
return { playing: state.playing };
}
/** Select volume state from media state. */
export function selectVolume(state: MediaState): VolumeState {
return { volume: state.volume };
}
/** Select time state from media state. */
export function selectTime(state: MediaState): TimeState {
return { currentTime: state.currentTime, duration: state.duration };
}
@@ -0,0 +1,4 @@
export { playerContext } from './player/context';
export { createPlayer } from './player/create-player';
export { PlayerController } from './player/player-controller';
export { createContainerMixin } from './store/container-mixin';
@@ -0,0 +1,9 @@
interface PlayerContext {
readonly player: unknown;
}
/**
* The player context for dependency injection.
* @public
*/
export const playerContext: PlayerContext = { player: null };
@@ -0,0 +1,13 @@
interface HtmlPlayerInstance {
play(): void;
destroy(): void;
}
interface HtmlPlayerOptions {
element: HTMLElement;
}
/** Create an HTML player instance. */
export function createPlayer(options: HtmlPlayerOptions): HtmlPlayerInstance {
return {} as HtmlPlayerInstance;
}
@@ -0,0 +1,26 @@
interface ReactiveControllerHost {
addController(controller: ReactiveController): void;
requestUpdate(): void;
}
interface ReactiveController {
hostConnected?(): void;
hostDisconnected?(): void;
}
/** Manages the video player lifecycle. */
export class PlayerController implements ReactiveController {
#host: ReactiveControllerHost;
constructor(host: ReactiveControllerHost) {
this.#host = host;
}
/** Whether the player is ready. */
get ready(): boolean {
return false;
}
hostConnected(): void {}
hostDisconnected(): void {}
}
@@ -0,0 +1,10 @@
type Constructor<T = object> = new (...args: any[]) => T;
interface ContainerHost {
connectedCallback(): void;
}
/** Create a mixin that provides store container behavior. */
export function createContainerMixin<T extends Constructor<ContainerHost>>(Base: T): T {
return Base;
}
@@ -0,0 +1,4 @@
export { usePlayer } from './player/context';
export { createPlayer } from './player/create-player';
export { mergeProps } from './utils/merge-props';
export { useFormat } from './utils/use-format';
@@ -0,0 +1,16 @@
interface PlayerStore {
playing: boolean;
volume: number;
}
interface StoreState {
playing: boolean;
volume: number;
}
/** Access the player store or select state from it. */
export function usePlayer(): PlayerStore;
export function usePlayer<R>(selector: (state: StoreState) => R): R;
export function usePlayer<R>(selector?: (state: StoreState) => R): PlayerStore | R {
return {} as PlayerStore | R;
}
@@ -0,0 +1,13 @@
interface PlayerInstance {
play(): void;
pause(): void;
}
interface PlayerOptions {
autoplay?: boolean;
}
/** Create a React player instance. */
export function createPlayer(options?: PlayerOptions): PlayerInstance {
return {} as PlayerInstance;
}
@@ -0,0 +1,4 @@
/** @public Merge multiple prop objects into one. */
export function mergeProps<T extends Record<string, unknown>>(...args: T[]): T {
return Object.assign({}, ...args) as T;
}
@@ -0,0 +1,9 @@
/** Format a value for display. */
/** @label Number */
export function useFormat(value: number): string;
/** @label String */
export function useFormat(value: string): string;
export function useFormat(value: number | string): string {
return String(value);
}
@@ -0,0 +1,10 @@
interface Store<S> {
getState(): S;
}
type SelectorFn<S, R> = (state: S) => R;
/** Create a memoized selector function. */
export function createSelector<S, R>(fn: SelectorFn<S, R>): SelectorFn<S, R> {
return fn;
}
@@ -0,0 +1 @@
export { SnapshotController } from './snapshot-controller';
@@ -0,0 +1,44 @@
interface ReactiveControllerHost {
addController(controller: ReactiveController): void;
requestUpdate(): void;
}
interface ReactiveController {
hostConnected?(): void;
hostDisconnected?(): void;
}
interface Store<S> {
getState(): S;
}
/** Takes a snapshot of store state. */
export class SnapshotController<S, R = S> implements ReactiveController {
#host: ReactiveControllerHost;
/**
* @param host - The host element.
* @param state - The store to snapshot.
* @param selector - Derives a value from state.
*/
constructor(host: ReactiveControllerHost, state: Store<S>, selector: (state: S) => R);
/**
* @param host - The host element.
* @param state - The store to snapshot.
*/
constructor(host: ReactiveControllerHost, state: Store<S>);
constructor(host: ReactiveControllerHost, state: Store<S>, selector?: (state: S) => R) {
this.#host = host;
}
/** The current snapshot value. */
get value(): R {
return {} as R;
}
/** Track state changes. */
track(): void {}
hostConnected(): void {}
hostDisconnected(): void {}
}
@@ -0,0 +1 @@
export { useStore } from './use-store';
@@ -0,0 +1,10 @@
interface Store<S> {
getState(): S;
}
/** Subscribe to a store. */
export function useStore<S>(store: Store<S>): S;
export function useStore<S, R>(store: Store<S>, selector: (state: S) => R): R;
export function useStore<S, R>(store: Store<S>, selector?: (state: S) => R): S | R {
return {} as S | R;
}
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"declaration": true,
"skipLibCheck": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
}
}
@@ -1,59 +1,96 @@
import * as tae from 'typescript-api-extractor';
import { describe, expect, it } from 'vitest';
import { formatDetailedType, formatProperties, formatType, getShortPropType } from '../formatter';
import { abbreviateType, formatDetailedType, formatProperties, formatType } from '../formatter';
describe('abbreviateType', () => {
it("returns 'function' for pure function types (no union)", () => {
expect(abbreviateType('selector', '((state: UnknownState) => R)')).toBe('function');
expect(abbreviateType('subscribe', '((state: State) => void)')).toBe('function');
expect(abbreviateType('isEqual', '((a: R, b: R) => boolean)')).toBe('function');
expect(abbreviateType('config', '((options: Options) => Config)')).toBe('function');
});
it("returns 'function' for function types with union return", () => {
expect(abbreviateType('selector', '(state: object) => InferSliceState<S> | undefined')).toBe('function');
expect(abbreviateType('selector', '(state: object) => string | undefined')).toBe('function');
});
it("returns 'undefined | function' for true top-level union of undefined and function", () => {
expect(abbreviateType('selector', '((state: object) => string) | undefined')).toBe('undefined | function');
});
describe('getShortPropType', () => {
it("returns 'function' for callback props (onX with =>)", () => {
expect(getShortPropType('onClick', '(event: Event) => void')).toBe('function');
expect(getShortPropType('onChange', '(value: string) => void')).toBe('function');
expect(abbreviateType('onClick', '(event: Event) => void')).toBe('function');
expect(abbreviateType('onChange', '(value: string) => void')).toBe('function');
});
it("returns 'function' for getter props (getX with =>)", () => {
expect(getShortPropType('getValue', '() => string')).toBe('function');
expect(getShortPropType('getState', '() => State')).toBe('function');
expect(abbreviateType('getValue', '() => string')).toBe('function');
expect(abbreviateType('getState', '() => State')).toBe('function');
});
it("returns 'string | function' for className with =>", () => {
expect(getShortPropType('className', 'string | ((state: State) => string)')).toBe('string | function');
expect(abbreviateType('className', 'string | ((state: State) => string)')).toBe('string | function');
});
it("returns 'CSSProperties | function' for style with =>", () => {
expect(getShortPropType('style', 'CSSProperties | ((state: State) => CSSProperties)')).toBe(
expect(abbreviateType('style', 'CSSProperties | ((state: State) => CSSProperties)')).toBe(
'CSSProperties | function'
);
});
it("returns 'ReactElement | function' for render with =>", () => {
expect(getShortPropType('render', 'ReactElement | ((state: State) => ReactElement)')).toBe(
'ReactElement | function'
);
expect(abbreviateType('render', 'ReactElement | ((state: State) => ReactElement)')).toBe('ReactElement | function');
});
it('returns undefined for simple types (boolean, string, number)', () => {
expect(getShortPropType('disabled', 'boolean')).toBeUndefined();
expect(getShortPropType('label', 'string')).toBeUndefined();
expect(getShortPropType('count', 'number')).toBeUndefined();
expect(abbreviateType('disabled', 'boolean')).toBeUndefined();
expect(abbreviateType('label', 'string')).toBeUndefined();
expect(abbreviateType('count', 'number')).toBeUndefined();
});
it('returns undefined for short unions (< 3 members and < 40 chars)', () => {
expect(getShortPropType('size', "'small' | 'large'")).toBeUndefined();
expect(getShortPropType('value', 'string | number')).toBeUndefined();
expect(abbreviateType('size', "'small' | 'large'")).toBeUndefined();
expect(abbreviateType('value', 'string | number')).toBeUndefined();
});
it("returns 'type | function' for short callback unions (< 40 chars, 2 members)", () => {
const type = 'string | ((state: TimeState) => string)';
expect(getShortPropType('label', type)).toBe('string | function');
expect(abbreviateType('label', type)).toBe('string | function');
});
it("returns 'type | function' for unions containing functions", () => {
const type = "string | ((state: State) => string) | 'auto'";
expect(getShortPropType('label', type)).toBe("string | 'auto' | function");
expect(abbreviateType('label', type)).toBe("string | 'auto' | function");
});
it('returns undefined for complex unions (NOT "Union")', () => {
// Complex union with 3+ members, no function
const complexUnion = "'small' | 'medium' | 'large' | 'xlarge'";
expect(getShortPropType('size', complexUnion)).toBeUndefined();
expect(abbreviateType('size', complexUnion)).toBeUndefined();
});
it('returns "object" for object literals > 40 chars', () => {
const longObject = '{ volume: number; muted: boolean; level: string }';
expect(longObject.length).toBeGreaterThan(40);
expect(abbreviateType('result', longObject)).toBe('object');
});
it('returns undefined for object literals <= 40 chars', () => {
const shortObject = '{ x: number; y: number }';
expect(abbreviateType('point', shortObject)).toBeUndefined();
});
it('truncates other types > 40 chars', () => {
const longType = "'option-a' | 'option-b' | 'option-c' | 'option-d' | 'option-e'";
expect(longType.length).toBeGreaterThan(40);
expect(abbreviateType('choice', longType)).toBe(`${longType.slice(0, 37)}...`);
});
it('returns undefined for other types <= 40 chars', () => {
const shortType = "'small' | 'medium' | 'large' | 'xlarge'";
expect(shortType.length).toBeLessThanOrEqual(40);
expect(abbreviateType('size', shortType)).toBeUndefined();
});
});
@@ -100,7 +137,7 @@ describe('formatProperties', () => {
const result = formatProperties(props);
expect(result.simple).toEqual({ type: 'boolean' });
expect(Object.keys(result.simple!)).not.toContain('shortType');
expect(Object.keys(result.simple!)).not.toContain('detailedType');
expect(Object.keys(result.simple!)).not.toContain('default');
expect(Object.keys(result.simple!)).not.toContain('required');
});
@@ -151,7 +188,7 @@ describe('formatProperties', () => {
expect(result.type?.type).toBe("'current' | 'duration' | 'remaining'");
});
it('sets shortType for callback props', () => {
it('sets abbreviated type and detailedType for callback props', () => {
const fnType = createFunctionNode([
{
parameters: [
@@ -176,7 +213,8 @@ describe('formatProperties', () => {
const result = formatProperties([prop]);
expect(result.onClick?.shortType).toBe('function');
expect(result.onClick?.type).toBe('function');
expect(result.onClick?.detailedType).toBe('((event: Event) => void)');
});
});
@@ -352,6 +390,27 @@ describe('formatType', () => {
expect(formatType(node, false)).toBe('T');
});
it('returns type name for TypeParameterNode with large union constraint (>5 members)', () => {
const largeUnion = createUnionNode([
createLiteralNode("'a'"),
createLiteralNode("'b'"),
createLiteralNode("'c'"),
createLiteralNode("'d'"),
createLiteralNode("'e'"),
createLiteralNode("'f'"),
]);
const node = createTypeParameterNode('TagName', largeUnion);
expect(formatType(node, false)).toBe('TagName');
});
it('expands TypeParameterNode with small union constraint (<=5 members)', () => {
const smallUnion = createUnionNode([createLiteralNode("'a'"), createLiteralNode("'b'"), createLiteralNode("'c'")]);
const node = createTypeParameterNode('T', smallUnion);
expect(formatType(node, false)).toBe("'a' | 'b' | 'c'");
});
// --- UnionNode with typeName ---
it('formats UnionNode with typeName as fully qualified name', () => {
@@ -363,10 +422,10 @@ describe('formatType', () => {
// --- ObjectNode edge cases ---
it('formats empty ObjectNode as {}', () => {
it('formats empty ObjectNode as object', () => {
const node = createObjectNode([]);
expect(formatType(node, false)).toBe('{}');
expect(formatType(node, false)).toBe('object');
});
// --- Unknown node ---
@@ -391,13 +450,28 @@ describe('formatType', () => {
// --- TypeParameterNode constraint flattening in union ---
it('flattens TypeParameterNode constraint in union', () => {
it('flattens TypeParameterNode constraint in union when small (<=5 members)', () => {
const constraintUnion = createUnionNode([createIntrinsicNode('string'), createIntrinsicNode('number')]);
const typeParam = createTypeParameterNode('T', constraintUnion);
const union = createUnionNode([typeParam, createIntrinsicNode('boolean')]);
expect(formatType(union, false)).toBe('string | number | boolean');
});
it('does not flatten TypeParameterNode constraint in union when large (>5 members)', () => {
const largeConstraint = createUnionNode([
createLiteralNode("'a'"),
createLiteralNode("'b'"),
createLiteralNode("'c'"),
createLiteralNode("'d'"),
createLiteralNode("'e'"),
createLiteralNode("'f'"),
]);
const typeParam = createTypeParameterNode('TagName', largeConstraint);
const union = createUnionNode([typeParam, createIntrinsicNode('boolean')]);
expect(formatType(union, false)).toBe('TagName | boolean');
});
});
describe('formatDetailedType', () => {
@@ -11,3 +11,17 @@ export function createTestProgram(code: string, fileName = 'test.ts'): ts.Progra
compilerHost.fileExists = (name) => name === fileName;
return ts.createProgram([fileName], {}, compilerHost);
}
/** Suitable for tests that need type resolution via `getTypeChecker()`. */
export function createTypedTestProgram(code: string, fileName = 'test.ts'): ts.Program {
const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS);
const options: ts.CompilerOptions = { strict: true, target: ts.ScriptTarget.ESNext };
const compilerHost = ts.createCompilerHost(options);
const originalGetSourceFile = compilerHost.getSourceFile;
const originalFileExists = compilerHost.fileExists;
compilerHost.getSourceFile = (name, ...args) => {
return name === fileName ? sourceFile : originalGetSourceFile.call(compilerHost, name, ...args);
};
compilerHost.fileExists = (name) => name === fileName || originalFileExists.call(compilerHost, name);
return ts.createProgram([fileName], options, compilerHost);
}
@@ -0,0 +1,127 @@
import * as path from 'node:path';
import { describe, expect, it } from 'vitest';
import { getUtilEntries, type UtilEntry } from '../util-handler';
const FIXTURE_ROOT = path.resolve(import.meta.dirname, 'fixtures/monorepo');
describe('getUtilEntries', () => {
const entries = getUtilEntries(FIXTURE_ROOT);
function findByName(name: string, framework?: 'react' | 'html' | null): UtilEntry | undefined {
return entries.find((e) => e.data.name === name && (framework === undefined || e.framework === framework));
}
it('discovers hooks', () => {
expect(findByName('usePlayer', 'react')).toBeDefined();
expect(findByName('useStore', 'react')).toBeDefined();
});
it('discovers controllers', () => {
expect(findByName('PlayerController', 'html')).toBeDefined();
expect(findByName('SnapshotController', 'html')).toBeDefined();
});
it('discovers mixin with stripped display name', () => {
const mixin = findByName('ContainerMixin', 'html');
expect(mixin).toBeDefined();
expect(mixin!.slug).toBe('container-mixin');
});
it('discovers factories including createSelector', () => {
const reactCreate = findByName('createPlayer', 'react');
const htmlCreate = findByName('createPlayer', 'html');
const createSelector = findByName('createSelector', null);
expect(reactCreate).toBeDefined();
expect(htmlCreate).toBeDefined();
expect(createSelector).toBeDefined();
});
it('discovers @public utility and context', () => {
expect(findByName('mergeProps', 'react')).toBeDefined();
expect(findByName('playerContext', 'html')).toBeDefined();
});
it('discovers selectors as framework-agnostic', () => {
const selectorNames = ['selectPlayback', 'selectVolume', 'selectTime'];
for (const name of selectorNames) {
const entry = findByName(name, null);
expect(entry, `expected to find ${name}`).toBeDefined();
expect(entry!.framework).toBeNull();
}
});
it('assigns correct frameworks', () => {
// React
expect(findByName('usePlayer')!.framework).toBe('react');
expect(findByName('useStore')!.framework).toBe('react');
expect(findByName('mergeProps')!.framework).toBe('react');
// HTML
expect(findByName('PlayerController')!.framework).toBe('html');
expect(findByName('SnapshotController')!.framework).toBe('html');
expect(findByName('playerContext')!.framework).toBe('html');
// Framework-agnostic
expect(findByName('selectPlayback')!.framework).toBeNull();
expect(findByName('createSelector')!.framework).toBeNull();
});
it('handles slug collision', () => {
const reactCreate = entries.find((e) => e.slug === 'create-player');
const htmlCreate = entries.find((e) => e.slug === 'html-create-player');
expect(reactCreate).toBeDefined();
expect(reactCreate!.framework).toBe('react');
expect(htmlCreate).toBeDefined();
expect(htmlCreate!.framework).toBe('html');
});
it('extracts multi-overload signatures', () => {
const usePlayer = findByName('usePlayer', 'react');
expect(usePlayer!.data.overloads).toHaveLength(2);
const useStore = findByName('useStore', 'react');
expect(useStore!.data.overloads).toHaveLength(2);
});
it('preserves overloads with identical return types', () => {
const useFormat = findByName('useFormat', 'react');
expect(useFormat).toBeDefined();
expect(useFormat!.data.overloads).toHaveLength(2);
});
it('extracts @label from overload JSDoc', () => {
const useFormat = findByName('useFormat', 'react');
expect(useFormat!.data.overloads[0]!.label).toBe('Number');
expect(useFormat!.data.overloads[1]!.label).toBe('String');
});
it('omits label when @label is not present', () => {
const useStore = findByName('useStore', 'react');
expect(useStore!.data.overloads[0]!.label).toBeUndefined();
expect(useStore!.data.overloads[1]!.label).toBeUndefined();
});
it('strips "- " prefix from controller param descriptions', () => {
const snapshot = findByName('SnapshotController', 'html');
expect(snapshot).toBeDefined();
const firstOverload = snapshot!.data.overloads[0]!;
const hostParam = firstOverload.parameters.host;
expect(hostParam).toBeDefined();
expect(hostParam!.description).toBe('The host element.');
expect(hostParam!.description).not.toMatch(/^-\s/);
});
it('extracts JSDoc descriptions', () => {
const usePlayer = findByName('usePlayer', 'react');
expect(usePlayer!.data.description).toBeDefined();
const playerController = findByName('PlayerController', 'html');
expect(playerController!.data.description).toBeDefined();
const playerContext = findByName('playerContext', 'html');
expect(playerContext!.data.description).toBeDefined();
});
});