@pyreon/solid-compat 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,26 +7,36 @@ import {
7
7
  createEffect,
8
8
  createMemo,
9
9
  createRenderEffect,
10
+ createResource,
10
11
  createRoot,
11
12
  createSelector,
12
13
  createSignal,
14
+ createStore,
13
15
  ErrorBoundary,
14
16
  For,
17
+ from,
15
18
  getOwner,
19
+ indexArray,
16
20
  lazy,
21
+ mapArray,
17
22
  Match,
18
23
  mergeProps,
24
+ observable,
19
25
  on,
20
26
  onCleanup,
21
27
  onMount,
28
+ produce,
22
29
  runWithOwner,
23
30
  Show,
31
+ startTransition,
24
32
  Suspense,
25
33
  Switch,
26
34
  splitProps,
27
35
  untrack,
28
36
  useContext,
37
+ useTransition,
29
38
  } from '../index'
39
+ import type { Accessor, Component, ParentComponent, Setter, Signal } from '../index'
30
40
  import type { RenderContext } from '../jsx-runtime'
31
41
  import { beginRender, endRender } from '../jsx-runtime'
32
42
 
@@ -620,6 +630,11 @@ describe('@pyreon/solid-compat', () => {
620
630
  expect(useContext(Ctx)).toBe('default-value')
621
631
  })
622
632
 
633
+ it('createContext returns object with Provider', () => {
634
+ const Ctx = createContext('test')
635
+ expect(typeof Ctx.Provider).toBe('function')
636
+ })
637
+
623
638
  // ─── Re-exports ───────────────────────────────────────────────────────
624
639
 
625
640
  it('Show is exported', () => {
@@ -783,6 +798,357 @@ describe('@pyreon/solid-compat', () => {
783
798
  expect(result).toBe('hello')
784
799
  })
785
800
 
801
+ // ─── createSignal equals option ────────────────────────────────────────
802
+
803
+ it('createSignal default skips update on same value (===)', () => {
804
+ let effectRuns = 0
805
+ createRoot((dispose) => {
806
+ const [count, setCount] = createSignal(5)
807
+ createEffect(() => {
808
+ count()
809
+ effectRuns++
810
+ })
811
+ expect(effectRuns).toBe(1)
812
+ setCount(5) // same value
813
+ expect(effectRuns).toBe(1) // no re-run
814
+ setCount(6)
815
+ expect(effectRuns).toBe(2)
816
+ dispose()
817
+ })
818
+ })
819
+
820
+ it('createSignal equals: false always notifies', () => {
821
+ let effectRuns = 0
822
+ createRoot((dispose) => {
823
+ const [count, setCount] = createSignal(5, { equals: false })
824
+ createEffect(() => {
825
+ count()
826
+ effectRuns++
827
+ })
828
+ expect(effectRuns).toBe(1)
829
+ setCount(5) // same value but equals: false
830
+ expect(effectRuns).toBe(2)
831
+ dispose()
832
+ })
833
+ })
834
+
835
+ it('createSignal custom equals function', () => {
836
+ let effectRuns = 0
837
+ createRoot((dispose) => {
838
+ const [obj, setObj] = createSignal(
839
+ { id: 1, name: 'a' },
840
+ { equals: (prev, next) => prev.id === next.id },
841
+ )
842
+ createEffect(() => {
843
+ obj()
844
+ effectRuns++
845
+ })
846
+ expect(effectRuns).toBe(1)
847
+ setObj({ id: 1, name: 'b' }) // same id
848
+ expect(effectRuns).toBe(1)
849
+ setObj({ id: 2, name: 'b' }) // different id
850
+ expect(effectRuns).toBe(2)
851
+ dispose()
852
+ })
853
+ })
854
+
855
+ // ─── createSignal getter/setter identity stability ────────────────────
856
+
857
+ it('createSignal returns stable getter/setter in component context', () => {
858
+ const runner = createHookRunner()
859
+ const [getter1, setter1] = runner.run(() => createSignal(0))
860
+ const [getter2, setter2] = runner.run(() => createSignal(0))
861
+ expect(getter1).toBe(getter2)
862
+ expect(setter1).toBe(setter2)
863
+ })
864
+
865
+ // ─── createResource ──────────────────────────────────────────────────
866
+
867
+ it('createResource with fetcher only', async () => {
868
+ const [data] = createResource(() => Promise.resolve(42))
869
+ // While loading, data() throws the fetch promise (Suspense integration)
870
+ expect(() => data()).toThrow()
871
+ expect(data.loading).toBe(true)
872
+
873
+ await new Promise((r) => setTimeout(r, 10))
874
+
875
+ expect(data()).toBe(42)
876
+ expect(data.loading).toBe(false)
877
+ expect(data.error).toBeUndefined()
878
+ expect(data.latest).toBe(42)
879
+ })
880
+
881
+ it('createResource with sync fetcher', () => {
882
+ const [data] = createResource(() => 'sync-value')
883
+ expect(data()).toBe('sync-value')
884
+ expect(data.loading).toBe(false)
885
+ })
886
+
887
+ it('createResource with source and fetcher', async () => {
888
+ const [userId, setUserId] = createSignal(1)
889
+
890
+ createRoot(async (dispose) => {
891
+ const [data] = createResource(userId, async (id) => `user-${id}`)
892
+
893
+ await new Promise((r) => setTimeout(r, 10))
894
+ expect(data()).toBe('user-1')
895
+
896
+ setUserId(2)
897
+ await new Promise((r) => setTimeout(r, 10))
898
+ expect(data()).toBe('user-2')
899
+
900
+ dispose()
901
+ })
902
+ })
903
+
904
+ it('createResource mutate updates data', async () => {
905
+ const [data, { mutate }] = createResource(() => Promise.resolve(10))
906
+ await new Promise((r) => setTimeout(r, 10))
907
+ expect(data()).toBe(10)
908
+
909
+ mutate(99)
910
+ expect(data()).toBe(99)
911
+ expect(data.latest).toBe(99)
912
+ })
913
+
914
+ it('createResource mutate with updater function', async () => {
915
+ const [data, { mutate }] = createResource(() => Promise.resolve(10))
916
+ await new Promise((r) => setTimeout(r, 10))
917
+
918
+ mutate((prev) => (prev ?? 0) + 5)
919
+ expect(data()).toBe(15)
920
+ })
921
+
922
+ it('createResource handles errors', async () => {
923
+ const [data] = createResource(() => Promise.reject(new Error('fail')))
924
+ await new Promise((r) => setTimeout(r, 10))
925
+
926
+ // data() throws the error (ErrorBoundary catches this)
927
+ expect(() => data()).toThrow('fail')
928
+ expect(data.error).toBeInstanceOf(Error)
929
+ expect(data.error?.message).toBe('fail')
930
+ expect(data.loading).toBe(false)
931
+ })
932
+
933
+ it('createResource handles sync errors', () => {
934
+ const [data] = createResource(() => {
935
+ throw new Error('sync-fail')
936
+ })
937
+
938
+ // data() throws the error
939
+ expect(() => data()).toThrow('sync-fail')
940
+ expect(data.error?.message).toBe('sync-fail')
941
+ expect(data.loading).toBe(false)
942
+ })
943
+
944
+ it('createResource with falsy source skips fetch', () => {
945
+ let fetchCount = 0
946
+ const [enabled] = createSignal(false)
947
+
948
+ createRoot((dispose) => {
949
+ createResource(enabled, () => {
950
+ fetchCount++
951
+ return 'fetched'
952
+ })
953
+ expect(fetchCount).toBe(0)
954
+ dispose()
955
+ })
956
+ })
957
+
958
+ it('createResource refetch re-fetches', async () => {
959
+ let fetchCount = 0
960
+ const [, { refetch }] = createResource(async () => {
961
+ fetchCount++
962
+ return fetchCount
963
+ })
964
+
965
+ await new Promise((r) => setTimeout(r, 10))
966
+ expect(fetchCount).toBe(1)
967
+
968
+ refetch()
969
+ await new Promise((r) => setTimeout(r, 10))
970
+ expect(fetchCount).toBe(2)
971
+ })
972
+
973
+ // ─── createStore / produce ────────────────────────────────────────────
974
+
975
+ it('createStore returns reactive proxy and setter', () => {
976
+ const [store, setStore] = createStore({ count: 0, name: 'test' })
977
+ expect(store.count).toBe(0)
978
+ expect(store.name).toBe('test')
979
+
980
+ setStore((s: { count: number; name: string }) => {
981
+ s.count = 5
982
+ })
983
+ expect(store.count).toBe(5)
984
+ })
985
+
986
+ it('createStore proxy is reactive in effects', () => {
987
+ let effectRuns = 0
988
+ createRoot((dispose) => {
989
+ const [store, setStore] = createStore({ value: 1 })
990
+ createEffect(() => {
991
+ void store.value // read through proxy triggers signal read
992
+ effectRuns++
993
+ })
994
+ expect(effectRuns).toBe(1)
995
+ setStore((s: { value: number }) => {
996
+ s.value = 2
997
+ })
998
+ expect(effectRuns).toBe(2)
999
+ dispose()
1000
+ })
1001
+ })
1002
+
1003
+ it('createStore proxy prevents direct mutation', () => {
1004
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
1005
+ const [store] = createStore({ count: 0 })
1006
+ ;(store as Record<string, unknown>).count = 5
1007
+ expect(warn).toHaveBeenCalledWith(
1008
+ '[Pyreon] Direct mutation on store is not supported. Use the setter function.',
1009
+ )
1010
+ warn.mockRestore()
1011
+ })
1012
+
1013
+ it('produce creates a reusable updater', () => {
1014
+ const increment = produce<{ count: number }>((s) => {
1015
+ s.count++
1016
+ })
1017
+ const result = increment({ count: 5 })
1018
+ expect(result.count).toBe(6)
1019
+ })
1020
+
1021
+ // ─── startTransition / useTransition ──────────────────────────────────
1022
+
1023
+ it('startTransition runs fn synchronously', () => {
1024
+ let ran = false
1025
+ startTransition(() => {
1026
+ ran = true
1027
+ })
1028
+ expect(ran).toBe(true)
1029
+ })
1030
+
1031
+ it('useTransition returns [isPending, start]', () => {
1032
+ const [isPending, start] = useTransition()
1033
+ expect(isPending()).toBe(false)
1034
+ let ran = false
1035
+ start(() => {
1036
+ ran = true
1037
+ })
1038
+ expect(ran).toBe(true)
1039
+ expect(isPending()).toBe(false)
1040
+ })
1041
+
1042
+ // ─── observable ───────────────────────────────────────────────────────
1043
+
1044
+ it('observable converts signal to subscribable', () => {
1045
+ createRoot((dispose) => {
1046
+ const [count, setCount] = createSignal(0)
1047
+ const obs = observable(count)
1048
+ const values: number[] = []
1049
+ const sub = obs.subscribe({ next: (v) => values.push(v) })
1050
+
1051
+ expect(values).toEqual([0])
1052
+ setCount(1)
1053
+ expect(values).toEqual([0, 1])
1054
+
1055
+ sub.unsubscribe()
1056
+ setCount(2)
1057
+ expect(values).toEqual([0, 1]) // no more updates
1058
+
1059
+ dispose()
1060
+ })
1061
+ })
1062
+
1063
+ // ─── from ─────────────────────────────────────────────────────────────
1064
+
1065
+ it('from with producer function', () => {
1066
+ createRoot((dispose) => {
1067
+ let setter: ((v: number) => void) | undefined
1068
+ const value = from<number>((set) => {
1069
+ setter = set
1070
+ return () => {} // cleanup
1071
+ })
1072
+ expect(value()).toBeUndefined()
1073
+ setter!(42)
1074
+ expect(value()).toBe(42)
1075
+ dispose()
1076
+ })
1077
+ })
1078
+
1079
+ it('from with observable', () => {
1080
+ createRoot((dispose) => {
1081
+ const [count, setCount] = createSignal(0)
1082
+ const obs = observable(count)
1083
+ const derived = from(obs)
1084
+
1085
+ // from subscribes to observable, initial value propagated
1086
+ expect(derived()).toBe(0)
1087
+ setCount(5)
1088
+ expect(derived()).toBe(5)
1089
+
1090
+ dispose()
1091
+ })
1092
+ })
1093
+
1094
+ // ─── mapArray ─────────────────────────────────────────────────────────
1095
+
1096
+ it('mapArray maps items with reactive index', () => {
1097
+ createRoot((dispose) => {
1098
+ const [list] = createSignal(['a', 'b', 'c'])
1099
+ const mapped = mapArray(list, (item, index) => `${item}-${index()}`)
1100
+ expect(mapped()).toEqual(['a-0', 'b-1', 'c-2'])
1101
+ dispose()
1102
+ })
1103
+ })
1104
+
1105
+ it('mapArray updates when source changes', () => {
1106
+ createRoot((dispose) => {
1107
+ const [list, setList] = createSignal([1, 2, 3])
1108
+ const doubled = mapArray(list, (item) => item * 2)
1109
+ expect(doubled()).toEqual([2, 4, 6])
1110
+ setList([4, 5])
1111
+ expect(doubled()).toEqual([8, 10])
1112
+ dispose()
1113
+ })
1114
+ })
1115
+
1116
+ // ─── indexArray ───────────────────────────────────────────────────────
1117
+
1118
+ it('indexArray maps items with static index', () => {
1119
+ createRoot((dispose) => {
1120
+ const [list] = createSignal(['x', 'y', 'z'])
1121
+ const mapped = indexArray(list, (item, index) => `${item()}-${index}`)
1122
+ expect(mapped()).toEqual(['x-0', 'y-1', 'z-2'])
1123
+ dispose()
1124
+ })
1125
+ })
1126
+
1127
+ it('indexArray updates when source changes', () => {
1128
+ createRoot((dispose) => {
1129
+ const [list, setList] = createSignal([10, 20])
1130
+ const doubled = indexArray(list, (item) => item() * 2)
1131
+ expect(doubled()).toEqual([20, 40])
1132
+ setList([30])
1133
+ expect(doubled()).toEqual([60])
1134
+ dispose()
1135
+ })
1136
+ })
1137
+
1138
+ // ─── Type exports ─────────────────────────────────────────────────────
1139
+
1140
+ it('type exports are usable', () => {
1141
+ // These are compile-time checks — just verify they don't cause runtime errors
1142
+ const _accessor: Accessor<number> = () => 42
1143
+ const _setter: Setter<number> = () => {}
1144
+ const _signal: Signal<number> = [_accessor, _setter]
1145
+ const _component: Component<{ name: string }> = () => null
1146
+ const _parent: ParentComponent<{ title: string }> = () => null
1147
+ expect(_signal).toHaveLength(2)
1148
+ expect(typeof _component).toBe('function')
1149
+ expect(typeof _parent).toBe('function')
1150
+ })
1151
+
786
1152
  // ─── JSX runtime ───────────────────────────────────────────────────────
787
1153
 
788
1154
  it('jsx-runtime exports are available', async () => {