@planet-matrix/mobius-model 0.6.0 → 0.9.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.
Files changed (233) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/index.js +706 -36
  3. package/dist/index.js.map +855 -59
  4. package/package.json +28 -16
  5. package/src/ai/README.md +1 -0
  6. package/src/ai/ai.ts +107 -0
  7. package/src/ai/chat-completion-ai/aihubmix-chat-completion.ts +78 -0
  8. package/src/ai/chat-completion-ai/chat-completion-ai.ts +270 -0
  9. package/src/ai/chat-completion-ai/chat-completion.ts +189 -0
  10. package/src/ai/chat-completion-ai/index.ts +7 -0
  11. package/src/ai/chat-completion-ai/lingyiwanwu-chat-completion.ts +78 -0
  12. package/src/ai/chat-completion-ai/ohmygpt-chat-completion.ts +78 -0
  13. package/src/ai/chat-completion-ai/openai-next-chat-completion.ts +78 -0
  14. package/src/ai/embedding-ai/embedding-ai.ts +63 -0
  15. package/src/ai/embedding-ai/embedding.ts +50 -0
  16. package/src/ai/embedding-ai/index.ts +4 -0
  17. package/src/ai/embedding-ai/openai-next-embedding.ts +23 -0
  18. package/src/ai/index.ts +4 -0
  19. package/src/aio/README.md +100 -0
  20. package/src/aio/content.ts +141 -0
  21. package/src/aio/index.ts +3 -0
  22. package/src/aio/json.ts +127 -0
  23. package/src/aio/prompt.ts +246 -0
  24. package/src/basic/README.md +20 -15
  25. package/src/basic/error.ts +19 -5
  26. package/src/basic/function.ts +2 -2
  27. package/src/basic/index.ts +1 -0
  28. package/src/basic/schedule.ts +111 -0
  29. package/src/basic/stream.ts +135 -25
  30. package/src/credential/README.md +107 -0
  31. package/src/credential/api-key.ts +158 -0
  32. package/src/credential/bearer.ts +73 -0
  33. package/src/credential/index.ts +4 -0
  34. package/src/credential/json-web-token.ts +96 -0
  35. package/src/credential/password.ts +170 -0
  36. package/src/cron/README.md +86 -0
  37. package/src/cron/cron.ts +87 -0
  38. package/src/cron/index.ts +1 -0
  39. package/src/drizzle/README.md +1 -0
  40. package/src/drizzle/drizzle.ts +1 -0
  41. package/src/drizzle/helper.ts +47 -0
  42. package/src/drizzle/index.ts +5 -0
  43. package/src/drizzle/infer.ts +52 -0
  44. package/src/drizzle/kysely.ts +8 -0
  45. package/src/drizzle/pagination.ts +200 -0
  46. package/src/email/README.md +1 -0
  47. package/src/email/index.ts +1 -0
  48. package/src/email/resend.ts +25 -0
  49. package/src/event/class-event-proxy.ts +6 -5
  50. package/src/event/common.ts +13 -3
  51. package/src/event/event-manager.ts +3 -3
  52. package/src/event/instance-event-proxy.ts +6 -5
  53. package/src/event/internal.ts +4 -4
  54. package/src/form/README.md +25 -0
  55. package/src/form/index.ts +1 -0
  56. package/src/form/inputor-controller/base.ts +874 -0
  57. package/src/form/inputor-controller/boolean.ts +39 -0
  58. package/src/form/inputor-controller/file.ts +39 -0
  59. package/src/form/inputor-controller/form.ts +181 -0
  60. package/src/form/inputor-controller/helper.ts +117 -0
  61. package/src/form/inputor-controller/index.ts +17 -0
  62. package/src/form/inputor-controller/multi-select.ts +99 -0
  63. package/src/form/inputor-controller/number.ts +116 -0
  64. package/src/form/inputor-controller/select.ts +109 -0
  65. package/src/form/inputor-controller/text.ts +82 -0
  66. package/src/http/READMD.md +1 -0
  67. package/src/http/api/api-core.ts +84 -0
  68. package/src/http/api/api-handler.ts +79 -0
  69. package/src/http/api/api-host.ts +47 -0
  70. package/src/http/api/api-result.ts +56 -0
  71. package/src/http/api/api-schema.ts +154 -0
  72. package/src/http/api/api-server.ts +130 -0
  73. package/src/http/api/api-test.ts +142 -0
  74. package/src/http/api/api-type.ts +37 -0
  75. package/src/http/api/api.ts +81 -0
  76. package/src/http/api/index.ts +11 -0
  77. package/src/http/api-adapter/api-core-node-http.ts +260 -0
  78. package/src/http/api-adapter/api-host-node-http.ts +156 -0
  79. package/src/http/api-adapter/api-result-arktype.ts +297 -0
  80. package/src/http/api-adapter/api-result-zod.ts +286 -0
  81. package/src/http/api-adapter/index.ts +5 -0
  82. package/src/http/bin/gen-api-list/gen-api-list.ts +126 -0
  83. package/src/http/bin/gen-api-list/index.ts +1 -0
  84. package/src/http/bin/gen-api-test/gen-api-test.ts +136 -0
  85. package/src/http/bin/gen-api-test/index.ts +1 -0
  86. package/src/http/bin/gen-api-type/calc-code.ts +25 -0
  87. package/src/http/bin/gen-api-type/gen-api-type.ts +127 -0
  88. package/src/http/bin/gen-api-type/index.ts +2 -0
  89. package/src/http/bin/index.ts +2 -0
  90. package/src/http/index.ts +3 -0
  91. package/src/huawei/README.md +1 -0
  92. package/src/huawei/index.ts +2 -0
  93. package/src/huawei/moderation/index.ts +1 -0
  94. package/src/huawei/moderation/moderation.ts +355 -0
  95. package/src/huawei/obs/esdk-obs-nodejs.d.ts +87 -0
  96. package/src/huawei/obs/index.ts +1 -0
  97. package/src/huawei/obs/obs.ts +42 -0
  98. package/src/index.ts +19 -2
  99. package/src/json/README.md +92 -0
  100. package/src/json/index.ts +1 -0
  101. package/src/json/repair.ts +18 -0
  102. package/src/log/logger.ts +15 -4
  103. package/src/openai/README.md +1 -0
  104. package/src/openai/index.ts +1 -0
  105. package/src/openai/openai.ts +510 -0
  106. package/src/orchestration/README.md +9 -7
  107. package/src/orchestration/dispatching/dispatcher.ts +83 -0
  108. package/src/orchestration/dispatching/index.ts +2 -0
  109. package/src/orchestration/dispatching/selector/base-selector.ts +39 -0
  110. package/src/orchestration/dispatching/selector/down-count-selector.ts +119 -0
  111. package/src/orchestration/dispatching/selector/index.ts +2 -0
  112. package/src/orchestration/index.ts +2 -0
  113. package/src/orchestration/scheduling/index.ts +2 -0
  114. package/src/orchestration/scheduling/scheduler.ts +103 -0
  115. package/src/orchestration/scheduling/task.ts +32 -0
  116. package/src/random/README.md +8 -7
  117. package/src/random/base.ts +66 -0
  118. package/src/random/index.ts +5 -1
  119. package/src/random/random-boolean.ts +40 -0
  120. package/src/random/random-integer.ts +60 -0
  121. package/src/random/random-number.ts +72 -0
  122. package/src/random/random-string.ts +66 -0
  123. package/src/request/README.md +108 -0
  124. package/src/request/fetch/base.ts +108 -0
  125. package/src/request/fetch/browser.ts +285 -0
  126. package/src/request/fetch/general.ts +20 -0
  127. package/src/request/fetch/index.ts +4 -0
  128. package/src/request/fetch/nodejs.ts +285 -0
  129. package/src/request/index.ts +2 -0
  130. package/src/request/request/base.ts +250 -0
  131. package/src/request/request/general.ts +64 -0
  132. package/src/request/request/index.ts +3 -0
  133. package/src/request/request/resource.ts +68 -0
  134. package/src/result/README.md +4 -0
  135. package/src/result/controller.ts +54 -0
  136. package/src/result/either.ts +193 -0
  137. package/src/result/index.ts +2 -0
  138. package/src/route/README.md +105 -0
  139. package/src/route/adapter/browser.ts +122 -0
  140. package/src/route/adapter/driver.ts +56 -0
  141. package/src/route/adapter/index.ts +2 -0
  142. package/src/route/index.ts +3 -0
  143. package/src/route/router/index.ts +2 -0
  144. package/src/route/router/route.ts +630 -0
  145. package/src/route/router/router.ts +1642 -0
  146. package/src/route/uri/hash.ts +308 -0
  147. package/src/route/uri/index.ts +7 -0
  148. package/src/route/uri/pathname.ts +376 -0
  149. package/src/route/uri/search.ts +413 -0
  150. package/src/socket/README.md +105 -0
  151. package/src/socket/client/index.ts +2 -0
  152. package/src/socket/client/socket-unit.ts +660 -0
  153. package/src/socket/client/socket.ts +203 -0
  154. package/src/socket/common/index.ts +2 -0
  155. package/src/socket/common/socket-unit-common.ts +23 -0
  156. package/src/socket/common/socket-unit-heartbeat.ts +427 -0
  157. package/src/socket/index.ts +3 -0
  158. package/src/socket/server/index.ts +3 -0
  159. package/src/socket/server/server.ts +183 -0
  160. package/src/socket/server/socket-unit.ts +449 -0
  161. package/src/socket/server/socket.ts +264 -0
  162. package/src/storage/table.ts +3 -3
  163. package/src/timer/expiration/expiration-manager.ts +3 -3
  164. package/src/timer/expiration/remaining-manager.ts +3 -3
  165. package/src/tube/README.md +99 -0
  166. package/src/tube/helper.ts +138 -0
  167. package/src/tube/index.ts +2 -0
  168. package/src/tube/tube.ts +880 -0
  169. package/src/weixin/README.md +1 -0
  170. package/src/weixin/index.ts +2 -0
  171. package/src/weixin/official-account/authorization.ts +159 -0
  172. package/src/weixin/official-account/index.ts +2 -0
  173. package/src/weixin/official-account/js-api.ts +134 -0
  174. package/src/weixin/open/index.ts +1 -0
  175. package/src/weixin/open/oauth2.ts +133 -0
  176. package/tests/unit/ai/ai.spec.ts +85 -0
  177. package/tests/unit/aio/content.spec.ts +105 -0
  178. package/tests/unit/aio/json.spec.ts +147 -0
  179. package/tests/unit/aio/prompt.spec.ts +111 -0
  180. package/tests/unit/basic/error.spec.ts +16 -4
  181. package/tests/unit/basic/schedule.spec.ts +74 -0
  182. package/tests/unit/basic/stream.spec.ts +90 -37
  183. package/tests/unit/credential/api-key.spec.ts +37 -0
  184. package/tests/unit/credential/bearer.spec.ts +23 -0
  185. package/tests/unit/credential/json-web-token.spec.ts +23 -0
  186. package/tests/unit/credential/password.spec.ts +41 -0
  187. package/tests/unit/cron/cron.spec.ts +84 -0
  188. package/tests/unit/event/class-event-proxy.spec.ts +3 -3
  189. package/tests/unit/event/event-manager.spec.ts +3 -3
  190. package/tests/unit/event/instance-event-proxy.spec.ts +3 -3
  191. package/tests/unit/form/inputor-controller/base.spec.ts +458 -0
  192. package/tests/unit/form/inputor-controller/boolean.spec.ts +30 -0
  193. package/tests/unit/form/inputor-controller/file.spec.ts +27 -0
  194. package/tests/unit/form/inputor-controller/form.spec.ts +120 -0
  195. package/tests/unit/form/inputor-controller/helper.spec.ts +67 -0
  196. package/tests/unit/form/inputor-controller/multi-select.spec.ts +34 -0
  197. package/tests/unit/form/inputor-controller/number.spec.ts +36 -0
  198. package/tests/unit/form/inputor-controller/select.spec.ts +49 -0
  199. package/tests/unit/form/inputor-controller/text.spec.ts +34 -0
  200. package/tests/unit/http/api/api-core-host.spec.ts +207 -0
  201. package/tests/unit/http/api/api-schema.spec.ts +120 -0
  202. package/tests/unit/http/api/api-server.spec.ts +363 -0
  203. package/tests/unit/http/api/api-test.spec.ts +117 -0
  204. package/tests/unit/http/api/api.spec.ts +121 -0
  205. package/tests/unit/http/api-adapter/node-http.spec.ts +191 -0
  206. package/tests/unit/json/repair.spec.ts +11 -0
  207. package/tests/unit/log/logger.spec.ts +19 -4
  208. package/tests/unit/openai/openai.spec.ts +64 -0
  209. package/tests/unit/orchestration/dispatching/dispatcher.spec.ts +41 -0
  210. package/tests/unit/orchestration/dispatching/selector/down-count-selector.spec.ts +81 -0
  211. package/tests/unit/orchestration/scheduling/scheduler.spec.ts +103 -0
  212. package/tests/unit/random/base.spec.ts +58 -0
  213. package/tests/unit/random/random-boolean.spec.ts +25 -0
  214. package/tests/unit/random/random-integer.spec.ts +32 -0
  215. package/tests/unit/random/random-number.spec.ts +33 -0
  216. package/tests/unit/random/random-string.spec.ts +22 -0
  217. package/tests/unit/request/fetch/browser.spec.ts +222 -0
  218. package/tests/unit/request/fetch/general.spec.ts +43 -0
  219. package/tests/unit/request/fetch/nodejs.spec.ts +225 -0
  220. package/tests/unit/request/request/base.spec.ts +385 -0
  221. package/tests/unit/request/request/general.spec.ts +161 -0
  222. package/tests/unit/route/router/route.spec.ts +431 -0
  223. package/tests/unit/route/router/router.spec.ts +407 -0
  224. package/tests/unit/route/uri/hash.spec.ts +72 -0
  225. package/tests/unit/route/uri/pathname.spec.ts +147 -0
  226. package/tests/unit/route/uri/search.spec.ts +107 -0
  227. package/tests/unit/socket/client.spec.ts +208 -0
  228. package/tests/unit/socket/server.spec.ts +135 -0
  229. package/tests/unit/socket/socket-unit-heartbeat.spec.ts +214 -0
  230. package/tests/unit/tube/helper.spec.ts +139 -0
  231. package/tests/unit/tube/tube.spec.ts +501 -0
  232. package/src/random/string.ts +0 -35
  233. package/tests/unit/random/string.spec.ts +0 -11
@@ -0,0 +1,84 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import { Cron } from "#Source/cron/index.ts"
4
+
5
+ const internalCreateFutureMinute = (minutesAhead: number): Date => {
6
+ const date = new Date(Date.now() + minutesAhead * 60_000)
7
+ date.setSeconds(0, 0)
8
+ return date
9
+ }
10
+
11
+ const internalCreatePastMinute = (minutesAgo: number): Date => {
12
+ const date = new Date(Date.now() - minutesAgo * 60_000)
13
+ date.setSeconds(0, 0)
14
+ return date
15
+ }
16
+
17
+ test("Cron creates schedule models from both cron expressions and one-shot dates", () => {
18
+ const expressionCron = new Cron({ pattern: "*/5 * * * *" })
19
+ const singleShotCron = new Cron({ pattern: internalCreateFutureMinute(5) })
20
+
21
+ expect(expressionCron).toBeInstanceOf(Cron)
22
+ expect(singleShotCron).toBeInstanceOf(Cron)
23
+ })
24
+
25
+ test("Cron.prevRun returns undefined before a one-shot schedule is reached", () => {
26
+ const scheduledAt = internalCreateFutureMinute(5)
27
+ const cron = new Cron({ pattern: scheduledAt })
28
+
29
+ expect(cron.prevRun()).toBeUndefined()
30
+ })
31
+
32
+ test("Cron.prevRuns returns historical one-shot runs that have already happened", () => {
33
+ const scheduledAt = internalCreatePastMinute(5)
34
+ const cron = new Cron({ pattern: scheduledAt })
35
+ const previousRuns = cron.prevRuns(2)
36
+
37
+ expect(previousRuns).toHaveLength(1)
38
+ expect(previousRuns[0]?.getTime()).toBe(scheduledAt.getTime())
39
+ })
40
+
41
+ test("Cron.currentRun stays undefined when the model is only queried and not executing a task", () => {
42
+ const cron = new Cron({ pattern: internalCreateFutureMinute(5) })
43
+
44
+ expect(cron.currentRun()).toBeUndefined()
45
+ })
46
+
47
+ test("Cron.nextRun returns the next upcoming one-shot run", () => {
48
+ const scheduledAt = internalCreateFutureMinute(5)
49
+ const cron = new Cron({ pattern: scheduledAt })
50
+
51
+ expect(cron.nextRun()?.getTime()).toBe(scheduledAt.getTime())
52
+ })
53
+
54
+ test("Cron.nextRuns returns upcoming expression runs in ascending order", () => {
55
+ const cron = new Cron({ pattern: "*/5 * * * *" })
56
+ const nextRuns = cron.nextRuns(2)
57
+
58
+ expect(nextRuns).toHaveLength(2)
59
+ expect(nextRuns[0]?.getTime()).toBeLessThan(nextRuns[1]?.getTime() ?? 0)
60
+ expect(cron.match(nextRuns[0]!)).toBe(true)
61
+ expect(cron.match(nextRuns[1]!)).toBe(true)
62
+ })
63
+
64
+ test("Cron.match reflects cron-expression semantics including domAndDow behavior", () => {
65
+ const yearlyCron = new Cron({ pattern: "0 0 1 1 *" })
66
+ const andCron = new Cron({ pattern: "0 0 1 * MON", domAndDow: true })
67
+ const orCron = new Cron({ pattern: "0 0 1 * MON", domAndDow: false })
68
+ const yearlyHit = new Date(2_026, 0, 1, 0, 0, 0, 0)
69
+ const yearlyMiss = new Date(2_026, 0, 1, 0, 1, 0, 0)
70
+ const mondayFirst = new Date(2_026, 5, 1, 0, 0, 0, 0)
71
+ const mondayNotFirst = new Date(2_026, 5, 8, 0, 0, 0, 0)
72
+ const firstNotMonday = new Date(2_026, 8, 1, 0, 0, 0, 0)
73
+
74
+ expect(yearlyCron.match(yearlyHit)).toBe(true)
75
+ expect(yearlyCron.match(yearlyMiss)).toBe(false)
76
+
77
+ expect(andCron.match(mondayFirst)).toBe(true)
78
+ expect(andCron.match(mondayNotFirst)).toBe(false)
79
+ expect(andCron.match(firstNotMonday)).toBe(false)
80
+
81
+ expect(orCron.match(mondayFirst)).toBe(true)
82
+ expect(orCron.match(mondayNotFirst)).toBe(true)
83
+ expect(orCron.match(firstNotMonday)).toBe(true)
84
+ })
@@ -1,15 +1,15 @@
1
1
  import { expect, test, vi } from "vitest"
2
2
 
3
- import type { BaseEvents, ClassEventProxyOptions } from "#Source/event/index.ts"
3
+ import type { BuildEvents, ClassEventProxyOptions } from "#Source/event/index.ts"
4
4
  import {
5
5
  ClassEventProxy,
6
6
  EventManager,
7
7
  } from "#Source/event/index.ts"
8
8
 
9
- interface TestEvents extends BaseEvents {
9
+ type TestEvents = BuildEvents<{
10
10
  change: (value: number) => void
11
11
  close: (reason: string) => void
12
- }
12
+ }>
13
13
 
14
14
  interface TestTarget {
15
15
  events: EventManager<TestEvents>
@@ -1,12 +1,12 @@
1
1
  import { expect, test, vi } from "vitest"
2
2
 
3
- import type { BaseEvents } from "#Source/event/index.ts"
3
+ import type { BuildEvents } from "#Source/event/index.ts"
4
4
  import { EventManager } from "#Source/event/index.ts"
5
5
 
6
- interface TestEvents extends BaseEvents {
6
+ type TestEvents = BuildEvents<{
7
7
  ready: (value: number) => void | Promise<void>
8
8
  done: (label: string) => void | Promise<void>
9
- }
9
+ }>
10
10
  const createEventManager = (): EventManager<TestEvents> => {
11
11
  return new EventManager<TestEvents>()
12
12
  }
@@ -1,15 +1,15 @@
1
1
  import { expect, test, vi } from "vitest"
2
2
 
3
- import type { BaseEvents, InstanceEventProxyOptions } from "#Source/event/index.ts"
3
+ import type { BuildEvents, InstanceEventProxyOptions } from "#Source/event/index.ts"
4
4
  import {
5
5
  EventManager,
6
6
  InstanceEventProxy,
7
7
  } from "#Source/event/index.ts"
8
8
 
9
- interface TestEvents extends BaseEvents {
9
+ type TestEvents = BuildEvents<{
10
10
  change: (value: number) => void
11
11
  error: (message: string) => void
12
- }
12
+ }>
13
13
 
14
14
  const createInstanceEventProxyHarness = (options?: {
15
15
  onSubscriberError?: (error: unknown) => void
@@ -0,0 +1,458 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import type {
4
+ BaseInputorContext,
5
+ BaseInputorControllerOptions,
6
+ BaseInputorSchema,
7
+ } from "#Source/form/index.ts"
8
+ import { BaseInputorController } from "#Source/form/index.ts"
9
+
10
+ const TEST_INPUTOR_TYPE_NAME = "test" as const
11
+
12
+ type TestExternalValue = string | undefined
13
+ type TestInternalValue = string | undefined
14
+
15
+ interface TestInputorSchema extends BaseInputorSchema<TestExternalValue> {
16
+ inputorTypeName: typeof TEST_INPUTOR_TYPE_NAME
17
+ }
18
+
19
+ interface TestInputorContext extends BaseInputorContext<TestExternalValue, TestInternalValue> {
20
+ }
21
+
22
+ interface TestInputorControllerOptions extends BaseInputorControllerOptions<TestExternalValue, TestInputorSchema> {
23
+ }
24
+
25
+ class TestInputorController extends BaseInputorController<
26
+ TestExternalValue,
27
+ TestInternalValue,
28
+ TestInputorSchema,
29
+ TestInputorContext
30
+ > {
31
+ declare schemaType: TestInputorSchema
32
+ declare optionsType: TestInputorControllerOptions
33
+ declare constructorType: typeof TestInputorController
34
+
35
+ static inputorTypeName = TEST_INPUTOR_TYPE_NAME
36
+
37
+ inputorTypeName = TEST_INPUTOR_TYPE_NAME
38
+ protected voidExternalValue = undefined
39
+ protected voidInternalValue = undefined
40
+
41
+ protected override async constrainExternalValue(value: TestExternalValue): Promise<TestExternalValue> {
42
+ if (value === undefined) {
43
+ return await Promise.resolve(undefined)
44
+ }
45
+ return await Promise.resolve(value.trim())
46
+ }
47
+
48
+ protected override async constrainInternalValue(value: TestInternalValue): Promise<TestInternalValue> {
49
+ if (value === undefined) {
50
+ return await Promise.resolve(undefined)
51
+ }
52
+ return await Promise.resolve(value.trim())
53
+ }
54
+ }
55
+
56
+ const createTestInputor = (
57
+ overrides: Partial<TestInputorControllerOptions> = {},
58
+ ): TestInputorController => {
59
+ return new TestInputorController({
60
+ id: "test-id",
61
+ name: "test-name",
62
+ placeholder: "test-placeholder",
63
+ value: " initial ",
64
+ ...overrides,
65
+ })
66
+ }
67
+
68
+ test("BaseInputorController.isReady and waitForReady initialize defaults and emit initial hooks", async () => {
69
+ const valueChanges: Array<{ newValue: TestExternalValue; oldValue: TestExternalValue }> = []
70
+ const schemaChanges: Array<{ newSchema: TestInputorSchema; oldSchema: TestInputorSchema }> = []
71
+ const controller = createTestInputor({
72
+ onValueChange: (context) => {
73
+ valueChanges.push(context)
74
+ },
75
+ onSchemaChange: (context) => {
76
+ schemaChanges.push(context)
77
+ },
78
+ })
79
+
80
+ expect(controller.isReady()).toBe(false)
81
+
82
+ await controller.waitForReady()
83
+
84
+ expect(controller.isReady()).toBe(true)
85
+ expect(await controller.getSchema()).toEqual({
86
+ inputorTypeName: TEST_INPUTOR_TYPE_NAME,
87
+ id: "test-id",
88
+ name: "test-name",
89
+ placeholder: "test-placeholder",
90
+ value: "initial",
91
+ isVoid: false,
92
+ isFocused: false,
93
+ isTouched: false,
94
+ isDirty: false,
95
+ isDisabled: false,
96
+ isDebugMode: false,
97
+ })
98
+ expect(valueChanges).toEqual([
99
+ {
100
+ newValue: "initial",
101
+ oldValue: "initial",
102
+ },
103
+ ])
104
+ expect(schemaChanges).toHaveLength(1)
105
+ expect(schemaChanges[0]!.newSchema).toEqual(schemaChanges[0]!.oldSchema)
106
+ expect(schemaChanges[0]!.newSchema.value).toBe("initial")
107
+ })
108
+
109
+ test("BaseInputorController.getInternalValue, getContext, and isEqualSchema expose current state", async () => {
110
+ const controller = createTestInputor({
111
+ value: " alpha ",
112
+ })
113
+
114
+ await controller.waitForReady()
115
+
116
+ expect(controller.getInternalValue()).toBe("alpha")
117
+ expect(await controller.getContext()).toEqual({
118
+ inputorTypeName: TEST_INPUTOR_TYPE_NAME,
119
+ id: "test-id",
120
+ name: "test-name",
121
+ placeholder: "test-placeholder",
122
+ value: "alpha",
123
+ isVoid: false,
124
+ isFocused: false,
125
+ isTouched: false,
126
+ isDirty: false,
127
+ isDisabled: false,
128
+ isDebugMode: false,
129
+ internalValue: "alpha",
130
+ isReady: true,
131
+ })
132
+ expect(await controller.isEqualSchema({ value: "alpha", placeholder: "test-placeholder" })).toBe(true)
133
+ expect(await controller.isEqualSchema({ value: "beta" })).toBe(false)
134
+ expect(await controller.isEqualSchema(null)).toBe(false)
135
+ })
136
+
137
+ test("BaseInputorController.subscribeInputChange and updateInputValue emit input updates after debounce", async () => {
138
+ vi.useFakeTimers()
139
+
140
+ try {
141
+ const controller = createTestInputor({
142
+ value: "start",
143
+ inputDebounce: 20,
144
+ })
145
+ const inputChanges: Array<{ newInput: TestExternalValue; oldInput: TestExternalValue; oldValue: TestExternalValue }> = []
146
+
147
+ await controller.waitForReady()
148
+ controller.subscribeInputChange((context) => {
149
+ inputChanges.push(context)
150
+ })
151
+
152
+ await controller.updateInputValue(" next ")
153
+
154
+ expect(inputChanges).toEqual([])
155
+
156
+ await vi.advanceTimersByTimeAsync(20)
157
+
158
+ expect(inputChanges).toEqual([
159
+ {
160
+ newInput: " next ",
161
+ oldInput: "start",
162
+ oldValue: "start",
163
+ },
164
+ ])
165
+ }
166
+ finally {
167
+ vi.useRealTimers()
168
+ }
169
+ })
170
+
171
+ test("BaseInputorController.unsubscribeInputChange removes one input subscriber", async () => {
172
+ vi.useFakeTimers()
173
+
174
+ try {
175
+ const controller = createTestInputor({ value: "start" })
176
+ let calls = 0
177
+ const subscriber = (): void => {
178
+ calls = calls + 1
179
+ }
180
+
181
+ await controller.waitForReady()
182
+ controller.subscribeInputChange(subscriber)
183
+ controller.unsubscribeInputChange(subscriber)
184
+
185
+ await controller.updateInputValue("next")
186
+ await vi.advanceTimersByTimeAsync(0)
187
+
188
+ expect(calls).toBe(0)
189
+ }
190
+ finally {
191
+ vi.useRealTimers()
192
+ }
193
+ })
194
+
195
+ test("BaseInputorController.clearInputChangeSubscribers removes all input subscribers", async () => {
196
+ vi.useFakeTimers()
197
+
198
+ try {
199
+ const controller = createTestInputor({ value: "start" })
200
+ let calls = 0
201
+
202
+ await controller.waitForReady()
203
+ controller.subscribeInputChange(() => {
204
+ calls = calls + 1
205
+ })
206
+ controller.subscribeInputChange(() => {
207
+ calls = calls + 1
208
+ })
209
+ controller.clearInputChangeSubscribers()
210
+
211
+ await controller.updateInputValue("next")
212
+ await vi.advanceTimersByTimeAsync(0)
213
+
214
+ expect(calls).toBe(0)
215
+ }
216
+ finally {
217
+ vi.useRealTimers()
218
+ }
219
+ })
220
+
221
+ test("BaseInputorController.subscribeValueChange, applyInputValue, and updateValue emit value changes", async () => {
222
+ const controller = createTestInputor({ value: "start" })
223
+ const valueChanges: Array<{ newValue: TestExternalValue; oldValue: TestExternalValue }> = []
224
+
225
+ await controller.waitForReady()
226
+ controller.subscribeValueChange((context) => {
227
+ valueChanges.push(context)
228
+ })
229
+
230
+ await controller.updateInputValue(" next ")
231
+ await controller.applyInputValue()
232
+ await controller.updateValue(" final ")
233
+
234
+ expect(valueChanges).toEqual([
235
+ {
236
+ newValue: "next",
237
+ oldValue: "start",
238
+ },
239
+ {
240
+ newValue: "final",
241
+ oldValue: "next",
242
+ },
243
+ ])
244
+ })
245
+
246
+ test("BaseInputorController.unsubscribeValueChange removes one value subscriber", async () => {
247
+ const controller = createTestInputor({ value: "start" })
248
+ let calls = 0
249
+ const subscriber = (): void => {
250
+ calls = calls + 1
251
+ }
252
+
253
+ await controller.waitForReady()
254
+ controller.subscribeValueChange(subscriber)
255
+ controller.unsubscribeValueChange(subscriber)
256
+ await controller.updateValue("next")
257
+
258
+ expect(calls).toBe(0)
259
+ })
260
+
261
+ test("BaseInputorController.clearValueChangeSubscribers removes all value subscribers", async () => {
262
+ const controller = createTestInputor({ value: "start" })
263
+ let calls = 0
264
+
265
+ await controller.waitForReady()
266
+ controller.subscribeValueChange(() => {
267
+ calls = calls + 1
268
+ })
269
+ controller.subscribeValueChange(() => {
270
+ calls = calls + 1
271
+ })
272
+ controller.clearValueChangeSubscribers()
273
+ await controller.updateValue("next")
274
+
275
+ expect(calls).toBe(0)
276
+ })
277
+
278
+ test("BaseInputorController.subscribeSchemaChange and updatePlaceholder emit schema changes", async () => {
279
+ const controller = createTestInputor({ value: "start" })
280
+ const schemaChanges: Array<{ newSchema: TestInputorSchema; oldSchema: TestInputorSchema }> = []
281
+
282
+ await controller.waitForReady()
283
+ await Promise.resolve()
284
+ controller.subscribeSchemaChange((context) => {
285
+ schemaChanges.push(context)
286
+ })
287
+ await controller.updatePlaceholder("next-placeholder")
288
+
289
+ expect(schemaChanges.length).toBeGreaterThanOrEqual(1)
290
+ expect(schemaChanges.at(-1)!.oldSchema.placeholder).toBe("test-placeholder")
291
+ expect(schemaChanges.at(-1)!.newSchema.placeholder).toBe("next-placeholder")
292
+ })
293
+
294
+ test("BaseInputorController.unsubscribeSchemaChange removes one schema subscriber", async () => {
295
+ const controller = createTestInputor({ value: "start" })
296
+ let calls = 0
297
+ const subscriber = (): void => {
298
+ calls = calls + 1
299
+ }
300
+
301
+ await controller.waitForReady()
302
+ controller.subscribeSchemaChange(subscriber)
303
+ controller.unsubscribeSchemaChange(subscriber)
304
+ await controller.updatePlaceholder("next-placeholder")
305
+
306
+ expect(calls).toBe(0)
307
+ })
308
+
309
+ test("BaseInputorController.clearSchemaChangeSubscribers removes all schema subscribers", async () => {
310
+ const controller = createTestInputor({ value: "start" })
311
+ let calls = 0
312
+
313
+ await controller.waitForReady()
314
+ controller.subscribeSchemaChange(() => {
315
+ calls = calls + 1
316
+ })
317
+ controller.subscribeSchemaChange(() => {
318
+ calls = calls + 1
319
+ })
320
+ controller.clearSchemaChangeSubscribers()
321
+ await controller.updatePlaceholder("next-placeholder")
322
+
323
+ expect(calls).toBe(0)
324
+ })
325
+
326
+ test("BaseInputorController.updateIsVoid resets the value to its void value", async () => {
327
+ const controller = createTestInputor({ value: "start" })
328
+
329
+ await controller.waitForReady()
330
+ await controller.updateIsVoid(true)
331
+
332
+ expect(await controller.getSchema()).toMatchObject({
333
+ value: undefined,
334
+ isVoid: true,
335
+ isDirty: true,
336
+ })
337
+ })
338
+
339
+ test("BaseInputorController.updateIsFocused also marks the controller as touched", async () => {
340
+ const controller = createTestInputor({ value: "start" })
341
+
342
+ await controller.waitForReady()
343
+ await controller.updateIsFocused(true)
344
+
345
+ expect(await controller.getSchema()).toMatchObject({
346
+ isFocused: true,
347
+ isTouched: true,
348
+ })
349
+ })
350
+
351
+ test("BaseInputorController.updateIsTouched updates touched state directly", async () => {
352
+ const controller = createTestInputor({ value: "start" })
353
+
354
+ await controller.waitForReady()
355
+ await controller.updateIsTouched(true)
356
+
357
+ expect(await controller.getSchema()).toMatchObject({
358
+ isTouched: true,
359
+ })
360
+ })
361
+
362
+ test("BaseInputorController.updateIsDirty updates dirty state directly", async () => {
363
+ const controller = createTestInputor({ value: "start" })
364
+
365
+ await controller.waitForReady()
366
+ await controller.updateIsDirty(true)
367
+
368
+ expect(await controller.getSchema()).toMatchObject({
369
+ isDirty: true,
370
+ })
371
+ })
372
+
373
+ test("BaseInputorController.updateIsDisabled updates disabled state directly", async () => {
374
+ const controller = createTestInputor({ value: "start" })
375
+
376
+ await controller.waitForReady()
377
+ await controller.updateIsDisabled(true)
378
+
379
+ expect(await controller.getSchema()).toMatchObject({
380
+ isDisabled: true,
381
+ })
382
+ })
383
+
384
+ test("BaseInputorController.updateIsDebugMode updates debug mode directly", async () => {
385
+ const controller = createTestInputor({ value: "start" })
386
+
387
+ await controller.waitForReady()
388
+ await controller.updateIsDebugMode(true)
389
+
390
+ expect(await controller.getSchema()).toMatchObject({
391
+ isDebugMode: true,
392
+ })
393
+ })
394
+
395
+ test("BaseInputorController.updateSchema applies multiple schema fields in one pass", async () => {
396
+ const controller = createTestInputor({ value: "start" })
397
+
398
+ await controller.waitForReady()
399
+ await controller.updateSchema({
400
+ ...(await controller.getSchema()),
401
+ placeholder: "schema-placeholder",
402
+ value: " schema-value ",
403
+ isFocused: true,
404
+ isTouched: true,
405
+ isDirty: true,
406
+ isDisabled: true,
407
+ isDebugMode: true,
408
+ })
409
+
410
+ expect(await controller.getSchema()).toEqual({
411
+ inputorTypeName: TEST_INPUTOR_TYPE_NAME,
412
+ id: "test-id",
413
+ name: "test-name",
414
+ placeholder: "schema-placeholder",
415
+ value: "schema-value",
416
+ isVoid: false,
417
+ isFocused: true,
418
+ isTouched: true,
419
+ isDirty: true,
420
+ isDisabled: true,
421
+ isDebugMode: true,
422
+ })
423
+ })
424
+
425
+ test("BaseInputorController.destroy clears every subscriber collection", async () => {
426
+ vi.useFakeTimers()
427
+
428
+ try {
429
+ const controller = createTestInputor({ value: "start" })
430
+ let inputCalls = 0
431
+ let valueCalls = 0
432
+ let schemaCalls = 0
433
+
434
+ await controller.waitForReady()
435
+ controller.subscribeInputChange(() => {
436
+ inputCalls = inputCalls + 1
437
+ })
438
+ controller.subscribeValueChange(() => {
439
+ valueCalls = valueCalls + 1
440
+ })
441
+ controller.subscribeSchemaChange(() => {
442
+ schemaCalls = schemaCalls + 1
443
+ })
444
+ controller.destroy()
445
+
446
+ await controller.updateInputValue("next")
447
+ await vi.advanceTimersByTimeAsync(0)
448
+ await controller.updateValue("final")
449
+ await controller.updatePlaceholder("after-destroy")
450
+
451
+ expect(inputCalls).toBe(0)
452
+ expect(valueCalls).toBe(0)
453
+ expect(schemaCalls).toBe(0)
454
+ }
455
+ finally {
456
+ vi.useRealTimers()
457
+ }
458
+ })
@@ -0,0 +1,30 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ BOOLEAN_INPUTOR_TYPE_NAME,
5
+ BooleanInputorController,
6
+ } from "#Source/form/index.ts"
7
+
8
+ test("BooleanInputorController.getSchema exposes boolean schema state and updateValue toggles it", async () => {
9
+ const controller = new BooleanInputorController({
10
+ id: "boolean-id",
11
+ value: false,
12
+ })
13
+
14
+ await controller.waitForReady()
15
+ await controller.updateValue(true)
16
+
17
+ expect(await controller.getSchema()).toEqual({
18
+ inputorTypeName: BOOLEAN_INPUTOR_TYPE_NAME,
19
+ id: "boolean-id",
20
+ name: `boolean-boolean-id`,
21
+ placeholder: "",
22
+ value: true,
23
+ isVoid: false,
24
+ isFocused: false,
25
+ isTouched: false,
26
+ isDirty: true,
27
+ isDisabled: false,
28
+ isDebugMode: false,
29
+ })
30
+ })
@@ -0,0 +1,27 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ FILE_INPUTOR_TYPE_NAME,
5
+ FileInputorController,
6
+ } from "#Source/form/index.ts"
7
+
8
+ test("FileInputorController.getSchema stores file values and reports file metadata", async () => {
9
+ const file = new File(["alpha"], "alpha.txt", {
10
+ type: "text/plain",
11
+ })
12
+ const controller = new FileInputorController({
13
+ id: "file-id",
14
+ value: file,
15
+ })
16
+
17
+ await controller.waitForReady()
18
+
19
+ const schema = await controller.getSchema()
20
+
21
+ expect(schema.inputorTypeName).toBe(FILE_INPUTOR_TYPE_NAME)
22
+ expect(schema.id).toBe("file-id")
23
+ expect(schema.name).toBe("file-file-id")
24
+ expect(schema.value?.name).toBe("alpha.txt")
25
+ expect(schema.value?.type).toBe("text/plain")
26
+ expect(schema.isDirty).toBe(false)
27
+ })