import { describe, it, expect } from 'vitest' import type { Handle } from '../lib/component.ts' import { clientEntry, isEntry } from '../lib/client-entries.ts' describe('clientEntry', () => { /* oxlint-disable eslint/no-unused-vars */ describe('types', () => { it('keeps original types', () => { function Input(handle: Handle, props: { defaultValue?: string }) { let value = props.defaultValue ?? '' return ({ label }: { label: string }) => ( ) } let HydratedInput = clientEntry('/js/test.js#Input', Input) // @ts-expect-error - should require default render prop let el = // @ts-expect-error - should require default render prop let el2 = expect(true).toBe(true) }) it('only allows serializable props', () => { function Input(handle: Handle, props: { defaultValue?: string; func: () => void }) { let value = props.defaultValue ?? '' return ({ label }: { label: string }) => ( ) } // @ts-expect-error - should disallow non-serializable function prop let HydratedInput = clientEntry('/js/test.js#Input', Input) function Input2(handle: Handle, props: { defaultValue?: string }) { let value = props.defaultValue ?? '' return ({ label }: { label: string; func: () => void }) => ( ) } // @ts-expect-error - should disallow non-serializable function prop let HydratedInput2 = clientEntry('/js/test.js#Input', Input2) expect(true).toBe(true) }) }) describe('basic functionality', () => { it('marks a component as an entry', () => { function TestComponent(handle: Handle, props: { count: number }) { return () =>
Count: {props.count}
} let EntryComponent = clientEntry('/js/test.js#TestComponent', TestComponent) expect(EntryComponent.$entry).toBe(true) expect(EntryComponent.$entryId).toBe('/js/test.js#TestComponent') }) it('stores the original entry ID', () => { function MyComponent() { return () =>
Hello
} let EntryComponent = clientEntry('my-custom-entry-id', MyComponent) expect(EntryComponent.$entryId).toBe('my-custom-entry-id') }) it('preserves the original component functionality', () => { function TestComponent(handle: Handle, props: { initialCount: number }) { let count = props.initialCount return (props: { label: string }) => ( ) } let EntryComponent = clientEntry('/js/test.js#TestComponent', TestComponent) // The entry component should still be callable expect(typeof EntryComponent).toBe('function') // Mock Handle for testing let mockHandle = {} as Handle // Should work the same as the original component let renderFn = EntryComponent(mockHandle, { initialCount: 5 }) expect(typeof renderFn).toBe('function') if (typeof renderFn === 'function') { let element = renderFn({ label: 'Count' }) expect(element).toEqual({ $rmx: true, type: 'button', props: { children: ['Count', ': ', 5], }, key: undefined, }) } }) }) describe('error handling', () => { it('throws error when no entry ID provided', () => { function TestComponent() { return () =>
Test
} expect(() => { clientEntry('', TestComponent) }).toThrow('clientEntry() requires an entry ID') }) it('preserves opaque entry IDs at declaration time', () => { let anonymousHttpComponent = function () { return () =>
Test
} let anonymousFileComponent = function () { return () =>
Test
} // Force the function name to be empty to simulate truly anonymous function Object.defineProperty(anonymousHttpComponent, 'name', { value: '' }) Object.defineProperty(anonymousFileComponent, 'name', { value: '' }) let httpEntry = clientEntry('/js/test.js', anonymousHttpComponent) let fileEntry = clientEntry( 'file:///app/components/test-component.tsx', anonymousFileComponent, ) expect(httpEntry.$entryId).toBe('/js/test.js') expect(fileEntry.$entryId).toBe('file:///app/components/test-component.tsx') }) }) describe('type constraints', () => { it('accepts components with serializable props', () => { // This should compile without errors function ValidComponent( handle: Handle, props: { str: string num: number bool: boolean obj: { nested: string } arr: number[] element: JSX.Element }, ) { return () =>
Valid
} let EntryComponent = clientEntry('/js/valid.js#ValidComponent', ValidComponent) expect(EntryComponent.$entry).toBe(true) }) // Type-level rejection: non-serializable props should be disallowed it('rejects components with non-serializable props', () => { function InvalidComponent(handle: Handle, props: { func: () => void }) { return () =>
Invalid
} // @ts-expect-error - non-serializable function prop should be rejected let HydratedInvalid = clientEntry('/js/invalid.js#InvalidComponent', InvalidComponent) expect(true).toBe(true) }) it('accepts primitive setup types', () => { // Setup can be a primitive like number, string, boolean function Counter(handle: Handle, setup: number) { let count = setup ?? 0 return () =>
Count: {count}
} let EntryCounter = clientEntry('/js/counter.js#Counter', Counter) expect(EntryCounter.$entry).toBe(true) }) it('accepts null and undefined setup types', () => { function NullSetup(handle: Handle, setup: null) { return () =>
Null setup
} function UndefinedSetup(handle: Handle, setup: undefined) { return () =>
Undefined setup
} let EntryNull = clientEntry('/js/null.js#NullSetup', NullSetup) let EntryUndefined = clientEntry('/js/undefined.js#UndefinedSetup', UndefinedSetup) expect(EntryNull.$entry).toBe(true) expect(EntryUndefined.$entry).toBe(true) }) it('accepts array setup types', () => { function ArraySetup(handle: Handle, setup: string[]) { return () =>
{setup.join(', ')}
} let EntryArray = clientEntry('/js/array.js#ArraySetup', ArraySetup) expect(EntryArray.$entry).toBe(true) }) }) /* oxlint-enable eslint/no-unused-vars */ describe('isEntry type guard', () => { it('returns true for entry components', () => { function TestComponent() { return () =>
Test
} let EntryComponent = clientEntry('/js/test.js#TestComponent', TestComponent) expect(isEntry(EntryComponent)).toBe(true) }) it('returns false for regular components', () => { function RegularComponent() { return () =>
Regular
} expect(isEntry(RegularComponent)).toBe(false) }) it('returns false for non-function values', () => { expect(isEntry(null)).toBe(false) expect(isEntry(undefined)).toBe(false) expect(isEntry('string')).toBe(false) expect(isEntry(123)).toBe(false) expect(isEntry({})).toBe(false) }) it('returns false for functions without entry metadata', () => { function normalFunction() {} expect(isEntry(normalFunction)).toBe(false) }) }) describe('complex components', () => { it('handles stateful components with setup and render phases', () => { function Counter(handle: Handle, setupProps: { initialCount: number }) { let count = setupProps.initialCount return (renderProps: { label: string }) => ( ) } let EntryCounter = clientEntry('/js/counter.js#Counter', Counter) expect(EntryCounter.$entry).toBe(true) expect(EntryCounter.$entryId).toBe('/js/counter.js#Counter') }) it('handles simple components that return JSX directly', () => { function SimpleComponent(handle: Handle, props: { message: string }) { return () =>
{props.message}
} let EntrySimple = clientEntry('/js/simple.js#SimpleComponent', SimpleComponent) expect(EntrySimple.$entry).toBe(true) expect(EntrySimple.$entryId).toBe('/js/simple.js#SimpleComponent') }) }) })