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 () =>