diff --git a/packages/inula/src/index.test.ts b/packages/inula/src/index.test.ts new file mode 100644 index 00000000..43425f2d --- /dev/null +++ b/packages/inula/src/index.test.ts @@ -0,0 +1,198 @@ + +// BEGIN GENERATED TESTS: inula index exports +// Note: Detected testing framework: jest. These tests use the BDD API (describe/it/expect) +// and should run under both Vitest and Jest without additional imports. +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Auto-generated tests focusing on the index.ts aggregator exports. + * Primary goals: + * - Validate default export shape and that named exports align with default members. + * - Ensure important utilities are exported and have expected runtime types. + * - Confirm type-only exports (e.g., Action) are not present at runtime exports. + */ + +import Inula, * as IndexModule from './index'; + +describe('packages/inula/src/index exports', () => { + it('default export is an object and import did not throw', () => { + expect(typeof Inula).toBe('object'); + expect(Inula).toBeTruthy(); + }); + + it('version is a non-empty string and matches default export', () => { + expect(typeof IndexModule.version).toBe('string'); + expect(IndexModule.version.length).toBeGreaterThan(0); + expect(IndexModule.version).toBe(Inula.version); + }); + + const expectedDefaultKeys = [ + 'Children', + 'createRef', + 'Component', + 'PureComponent', + 'createContext', + 'forwardRef', + 'lazy', + 'memo', + 'useDebugValue', + 'useCallback', + 'useContext', + 'useEffect', + 'useImperativeHandle', + 'useLayoutEffect', + 'useMemo', + 'useReducer', + 'useRef', + 'useState', + 'createElement', + 'cloneElement', + 'isValidElement', + 'render', + 'createRoot', + 'createPortal', + 'unstable_batchedUpdates', + 'findDOMNode', + 'unmountComponentAtNode', + 'act', + 'flushSync', + 'createStore', + 'useStore', + 'clearStore', + 'reduxAdapter', + 'watch', + 'isFragment', + 'isElement', + 'isValidElementType', + 'isForwardRef', + 'isLazy', + 'isMemo', + 'isPortal', + 'isContextProvider', + 'isContextConsumer', + 'ForwardRef', + 'Memo', + 'Fragment', + 'Profiler', + 'StrictMode', + 'Suspense', + 'version', + ] as const; + + it('exposes expected keys on default export', () => { + expectedDefaultKeys.forEach((k) => { + expect((Inula as any)).toHaveProperty(k); + }); + }); + + it('named exports equal default export properties for all shared members', () => { + expectedDefaultKeys.forEach((k) => { + expect((IndexModule as any)[k]).toBe((Inula as any)[k]); + }); + }); + + it('toRaw is exported as a named export but not present on default export', () => { + expect('toRaw' in (Inula as any)).toBe(false); + expect((IndexModule as any).toRaw).toBeDefined(); + expect(typeof (IndexModule as any).toRaw).toBe('function'); + }); + + it('type-only export Action does not exist at runtime', () => { + // Type-only re-exports must not appear on the JS module namespace object. + expect('Action' in (IndexModule as any)).toBe(false); + }); +}); + +describe('basic element and utility behaviors (non-DOM)', () => { + const { + createElement, + cloneElement, + isValidElement, + isValidElementType, + isElement, + Fragment, + createRef, + } = IndexModule as any; + + it('createRef returns an object with a current property', () => { + const ref = createRef(); + expect(ref).toBeTruthy(); + expect(ref).toHaveProperty('current'); + }); + + it('createElement produces a value recognized by isValidElement', () => { + const el = createElement('div', { id: 'x' }, 'hello'); + expect(isValidElement(el)).toBe(true); + }); + + it('cloneElement returns a distinct element that is still valid', () => { + const el = createElement('span', null, 'a'); + const cloned = cloneElement(el, { id: 'b' }); + expect(cloned).not.toBe(el); + expect(isValidElement(cloned)).toBe(true); + }); + + it('Fragment can be used to create a valid element and its type is valid', () => { + const fragEl = createElement(Fragment, null); + expect(isValidElement(fragEl)).toBe(true); + expect(isValidElementType(Fragment)).toBe(true); + }); + + it('isElement identifies non-elements as false', () => { + expect(isElement({})).toBe(false); + expect(isElement(42 as any)).toBe(false); + expect(isElement(null as any)).toBe(false); + expect(isElement(undefined as any)).toBe(false); + }); +}); + +describe('exported function types are callable (smoke checks only)', () => { + const functionLikeKeys = [ + 'createRef', + 'createContext', + 'forwardRef', + 'lazy', + 'memo', + 'useDebugValue', + 'useCallback', + 'useContext', + 'useEffect', + 'useImperativeHandle', + 'useLayoutEffect', + 'useMemo', + 'useReducer', + 'useRef', + 'useState', + 'createElement', + 'cloneElement', + 'createPortal', + 'unstable_batchedUpdates', + 'findDOMNode', + 'unmountComponentAtNode', + 'act', + 'flushSync', + 'createStore', + 'useStore', + 'clearStore', + 'watch', + 'isFragment', + 'isElement', + 'isValidElementType', + 'isForwardRef', + 'isLazy', + 'isMemo', + 'isPortal', + 'isContextProvider', + 'isContextConsumer', + ] as const; + + it('exports listed keys as functions (or callable entities)', () => { + functionLikeKeys.forEach((k) => { + const v = (Inula as any)[k]; + expect(v).toBeDefined(); + expect(typeof v).toBe('function'); + }); + }); +}); + +// END GENERATED TESTS +// END GENERATED TESTS: inula index exports \ No newline at end of file diff --git a/packages/inula/src/inulax/__tests__/inulax-types.test.ts b/packages/inula/src/inulax/__tests__/inulax-types.test.ts new file mode 100644 index 00000000..612ac78c --- /dev/null +++ b/packages/inula/src/inulax/__tests__/inulax-types.test.ts @@ -0,0 +1,241 @@ +/* + Inulax Types – Type-Level Test Suite + Detected framework: unknown (repository scan in CI will determine). + This file is compatible with Jest and Vitest: + - If Vitest globals are disabled, uncomment the import below: + // import { describe, it, expect } from "vitest"; + + These tests focus on compile-time type contracts. Runtime expectations are no-ops, + but the TypeScript compiler will fail if type assertions are violated. +*/ + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { + IObserver, + StoreConfig, + UserActions, + ActionFunction, + StoreActions, + Action, + StoreObj, + PlannedAction, + UserComputedValues, + AsyncAction, + QueuedStoreActions, + ComputedValues +} from "../types.test"; + +/* ---------- Type-level assertion helpers ---------- */ +type Expect = T; +// Exact type equality (including tuples with optional elements) +type Equal = + (() => T extends A ? 1 : 2) extends + (() => T extends B ? 1 : 2) ? ( + (() => T extends B ? 1 : 2) extends + (() => T extends A ? 1 : 2) ? true : false + ) : false; + +describe("inulax/types - core generics", () => { + it("Action removes the first parameter (state) and preserves rest and return type", () => { + type S = { count: number; name: string }; + + // Original user action function type with explicit params + type IncT = (this: StoreObj, state: S, amount: number, tag?: string) => number; + + // Derived Action should drop the first (state) parameter and keep the rest + type IncAction = Action; + + type P = Parameters; + type R = ReturnType; + type TThis = ThisParameterType; + + type _p = Expect>; + type _r = Expect>; + type _this = Expect>>; + + expect(true).toBe(true); + }); + + it("AsyncAction mirrors Action parameters and wraps return type in Promise", () => { + type S = { count: number; name: string }; + type GetLenT = (this: StoreObj, state: S, input: string) => number; + + type GetLenAsync = AsyncAction; + + type P = Parameters; + type R = ReturnType; + type _p = Expect>; + type _r = Expect>>; + + expect(true).toBe(true); + }); + + it("Action with no extra params results in a zero-arity function", () => { + type S = { a: 1 }; + type NoArgsT = (this: StoreObj, state: S) => boolean; + + type NoArgsAction = Action; + type P = Parameters; + type R = ReturnType; + + type _p = Expect>; + type _r = Expect>; + + expect(true).toBe(true); + }); + + it("StoreActions maps UserActions to concrete Action signatures", () => { + type S = { count: number; name: string }; + + type ActionsDef = { + inc: (this: StoreObj, state: S, amount: number, tag?: string) => number; + setName: (this: StoreObj, state: S, newName: string) => void; + noArgs: (this: StoreObj, state: S) => boolean; + }; + + type SA = StoreActions; + + // Validate derived signatures + type _incParams = Expect, [number, string?]>>; + type _incRet = Expect, number>>; + type _setNameParams = Expect, [string]>>; + type _setNameRet = Expect, void>>; + type _noArgsParams = Expect, []>>; + type _noArgsRet = Expect, boolean>>; + + expect(true).toBe(true); + }); + + it("QueuedStoreActions maps to AsyncAction variants", () => { + type S = { count: number; name: string }; + + type ActionsDef = { + inc: (this: StoreObj, state: S, amount: number) => number; + readName: (this: StoreObj, state: S) => string; + }; + + type Q = QueuedStoreActions; + + type _incParams = Expect, [number]>>; + type _incRet = Expect, Promise>>; + type _readNameParams = Expect, []>>; + type _readNameRet = Expect, Promise>>; + + expect(true).toBe(true); + }); + + it("ComputedValues extracts return types from computed function map", () => { + type S = { count: number; name: string }; + type C = { + doubleCount: (state: S) => number; + isLongName: (state: S) => boolean; + }; + + type CV = ComputedValues; + type _d = Expect>; + type _l = Expect>; + + expect(true).toBe(true); + }); + + it("StoreObj merges state props, action callables, computed values, and exposes $-metadata", () => { + type S = { count: number; name: string }; + type C = { + doubleCount: (state: S) => number; + nameUpper: (state: S) => string; + }; + type A = { + inc: (this: StoreObj, state: S, amount: number, label?: string) => number; + setName: (this: StoreObj, state: S, newName: string) => void; + }; + + type Store = StoreObj; + const store = {} as Store; + + // State properties are directly accessible + const sCount: number = store.count; + const sName: string = store.name; + + // Computed values appear as concrete properties on the store + const cDouble: number = store.doubleCount; + const cUpper: string = store.nameUpper; + + // Actions are callable directly on the store + const incResult: number = store.inc(2); + const setResult: void = store.setName("Alice"); + + // $s mirrors state type + const $state: S = store.$s; + + // $a exposes mapped store actions + const inc2: number = store.$a.inc(3); + + // $queue exposes async versions of actions + const queued: Promise = store.$queue.inc(4); + + // $c contains the computed function map, not their results + const computedFnMap: UserComputedValues = store.$c; + const doubleFn: (state: S) => number = computedFnMap.doubleCount; + + // $subscribe/$unsubscribe accept listeners (any-typed mutation) + store.$subscribe((_m) => {}); + store.$unsubscribe((_m) => {}); + + expect(incResult).toBeDefined(); + expect(queued).toBeInstanceOf(Promise); + }); + + it("StoreConfig allows optional fields with proper generics", () => { + type S = { count: number; name: string }; + type A = { + noop: (this: StoreObj, state: S) => void; + }; + type C = { + id: (state: S) => string; + }; + + const cfg1: StoreConfig = { + id: "inula-store", + state: { count: 0, name: "x" }, + actions: {} as any, + computed: {} as any, + options: { isReduxAdapter: true }, + }; + + const cfg2: StoreConfig = {}; // all optional + + expect(cfg1).toBeTruthy(); + expect(cfg2).toBeTruthy(); + }); + + it("PlannedAction captures the ReturnType of the provided action function", () => { + type S = { v: number }; + type AddT = (this: StoreObj, state: S, x: number, y: number) => number; + + type PA = PlannedAction; + // resolve should be number, payload is any[], and action is string + type _resolve = Expect>; + type _payload = Expect>; + type _action = Expect>; + + expect(true).toBe(true); + }); + + it("IObserver requires the expected method signatures", () => { + const obs: IObserver = { + useProp: (_key: string | symbol) => {}, + addListener: (_l: (mutation: any) => void) => {}, + removeListener: (_l: (mutation: any) => void) => {}, + setProp: (_key: string | symbol, _mutation: any) => {}, + triggerChangeListeners: (_mutation: any) => {}, + triggerUpdate: (_vNode: any) => {}, + allChange: () => {}, + clearByVNode: (_vNode: any) => {}, + }; + + // Basic runtime sanity to ensure object shape exists + expect(typeof obs.useProp).toBe("function"); + expect(typeof obs.allChange).toBe("function"); + }); +}); \ No newline at end of file