@planet-matrix/mobius-model 0.5.0 → 0.6.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 (175) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +123 -36
  3. package/dist/index.js +45 -4
  4. package/dist/index.js.map +183 -11
  5. package/oxlint.config.ts +6 -0
  6. package/package.json +16 -10
  7. package/src/abort/README.md +92 -0
  8. package/src/abort/abort-manager.ts +278 -0
  9. package/src/abort/abort-signal-listener-manager.ts +81 -0
  10. package/src/abort/index.ts +2 -0
  11. package/src/basic/README.md +69 -118
  12. package/src/basic/function.ts +81 -62
  13. package/src/basic/is.ts +152 -71
  14. package/src/basic/promise.ts +29 -8
  15. package/src/basic/string.ts +2 -33
  16. package/src/color/README.md +105 -0
  17. package/src/color/index.ts +3 -0
  18. package/src/color/internal.ts +42 -0
  19. package/src/color/rgb/analyze.ts +236 -0
  20. package/src/color/rgb/construct.ts +130 -0
  21. package/src/color/rgb/convert.ts +227 -0
  22. package/src/color/rgb/derive.ts +303 -0
  23. package/src/color/rgb/index.ts +6 -0
  24. package/src/color/rgb/internal.ts +208 -0
  25. package/src/color/rgb/parse.ts +302 -0
  26. package/src/color/rgb/serialize.ts +144 -0
  27. package/src/color/types.ts +57 -0
  28. package/src/color/xyz/analyze.ts +80 -0
  29. package/src/color/xyz/construct.ts +19 -0
  30. package/src/color/xyz/convert.ts +71 -0
  31. package/src/color/xyz/index.ts +3 -0
  32. package/src/color/xyz/internal.ts +23 -0
  33. package/src/css/README.md +93 -0
  34. package/src/css/class.ts +559 -0
  35. package/src/css/index.ts +1 -0
  36. package/src/encoding/README.md +66 -79
  37. package/src/encoding/base64.ts +13 -4
  38. package/src/environment/README.md +97 -0
  39. package/src/environment/basic.ts +26 -0
  40. package/src/environment/device.ts +311 -0
  41. package/src/environment/feature.ts +285 -0
  42. package/src/environment/geo.ts +337 -0
  43. package/src/environment/index.ts +7 -0
  44. package/src/environment/runtime.ts +400 -0
  45. package/src/environment/snapshot.ts +60 -0
  46. package/src/environment/variable.ts +239 -0
  47. package/src/event/README.md +90 -0
  48. package/src/event/class-event-proxy.ts +228 -0
  49. package/src/event/common.ts +19 -0
  50. package/src/event/event-manager.ts +203 -0
  51. package/src/event/index.ts +4 -0
  52. package/src/event/instance-event-proxy.ts +186 -0
  53. package/src/event/internal.ts +24 -0
  54. package/src/exception/README.md +96 -0
  55. package/src/exception/browser.ts +219 -0
  56. package/src/exception/index.ts +4 -0
  57. package/src/exception/nodejs.ts +169 -0
  58. package/src/exception/normalize.ts +106 -0
  59. package/src/exception/types.ts +99 -0
  60. package/src/identifier/README.md +92 -0
  61. package/src/identifier/id.ts +119 -0
  62. package/src/identifier/index.ts +2 -0
  63. package/src/identifier/uuid.ts +187 -0
  64. package/src/index.ts +16 -1
  65. package/src/log/README.md +79 -0
  66. package/src/log/index.ts +5 -0
  67. package/src/log/log-emitter.ts +72 -0
  68. package/src/log/log-record.ts +10 -0
  69. package/src/log/log-scheduler.ts +74 -0
  70. package/src/log/log-type.ts +8 -0
  71. package/src/log/logger.ts +543 -0
  72. package/src/orchestration/README.md +89 -0
  73. package/src/orchestration/coordination/barrier.ts +214 -0
  74. package/src/orchestration/coordination/count-down-latch.ts +215 -0
  75. package/src/orchestration/coordination/errors.ts +98 -0
  76. package/src/orchestration/coordination/index.ts +16 -0
  77. package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
  78. package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
  79. package/src/orchestration/coordination/keyed-lock.ts +168 -0
  80. package/src/orchestration/coordination/mutex.ts +257 -0
  81. package/src/orchestration/coordination/permit.ts +127 -0
  82. package/src/orchestration/coordination/read-write-lock.ts +444 -0
  83. package/src/orchestration/coordination/semaphore.ts +280 -0
  84. package/src/orchestration/index.ts +1 -0
  85. package/src/random/README.md +55 -86
  86. package/src/random/index.ts +1 -1
  87. package/src/random/string.ts +35 -0
  88. package/src/reactor/README.md +4 -0
  89. package/src/reactor/reactor-core/primitive.ts +9 -9
  90. package/src/reactor/reactor-core/reactive-system.ts +5 -5
  91. package/src/singleton/README.md +79 -0
  92. package/src/singleton/factory.ts +55 -0
  93. package/src/singleton/index.ts +2 -0
  94. package/src/singleton/manager.ts +204 -0
  95. package/src/storage/README.md +107 -0
  96. package/src/storage/index.ts +1 -0
  97. package/src/storage/table.ts +449 -0
  98. package/src/timer/README.md +86 -0
  99. package/src/timer/expiration/expiration-manager.ts +594 -0
  100. package/src/timer/expiration/index.ts +3 -0
  101. package/src/timer/expiration/min-heap.ts +208 -0
  102. package/src/timer/expiration/remaining-manager.ts +241 -0
  103. package/src/timer/index.ts +1 -0
  104. package/src/type/README.md +54 -307
  105. package/src/type/class.ts +2 -2
  106. package/src/type/index.ts +14 -14
  107. package/src/type/is.ts +265 -2
  108. package/src/type/object.ts +37 -0
  109. package/src/type/string.ts +7 -2
  110. package/src/type/tuple.ts +6 -6
  111. package/src/type/union.ts +16 -0
  112. package/src/web/README.md +77 -0
  113. package/src/web/capture.ts +35 -0
  114. package/src/web/clipboard.ts +97 -0
  115. package/src/web/dom.ts +117 -0
  116. package/src/web/download.ts +16 -0
  117. package/src/web/event.ts +46 -0
  118. package/src/web/index.ts +10 -0
  119. package/src/web/local-storage.ts +113 -0
  120. package/src/web/location.ts +28 -0
  121. package/src/web/permission.ts +172 -0
  122. package/src/web/script-loader.ts +432 -0
  123. package/tests/unit/abort/abort-manager.spec.ts +225 -0
  124. package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -0
  125. package/tests/unit/basic/array.spec.ts +1 -1
  126. package/tests/unit/basic/stream.spec.ts +1 -1
  127. package/tests/unit/basic/string.spec.ts +0 -9
  128. package/tests/unit/color/rgb/analyze.spec.ts +110 -0
  129. package/tests/unit/color/rgb/construct.spec.ts +56 -0
  130. package/tests/unit/color/rgb/convert.spec.ts +60 -0
  131. package/tests/unit/color/rgb/derive.spec.ts +103 -0
  132. package/tests/unit/color/rgb/parse.spec.ts +66 -0
  133. package/tests/unit/color/rgb/serialize.spec.ts +46 -0
  134. package/tests/unit/color/xyz/analyze.spec.ts +33 -0
  135. package/tests/unit/color/xyz/construct.spec.ts +10 -0
  136. package/tests/unit/color/xyz/convert.spec.ts +18 -0
  137. package/tests/unit/css/class.spec.ts +157 -0
  138. package/tests/unit/environment/basic.spec.ts +20 -0
  139. package/tests/unit/environment/device.spec.ts +146 -0
  140. package/tests/unit/environment/feature.spec.ts +388 -0
  141. package/tests/unit/environment/geo.spec.ts +111 -0
  142. package/tests/unit/environment/runtime.spec.ts +364 -0
  143. package/tests/unit/environment/snapshot.spec.ts +4 -0
  144. package/tests/unit/environment/variable.spec.ts +190 -0
  145. package/tests/unit/event/class-event-proxy.spec.ts +225 -0
  146. package/tests/unit/event/event-manager.spec.ts +246 -0
  147. package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
  148. package/tests/unit/exception/browser.spec.ts +213 -0
  149. package/tests/unit/exception/nodejs.spec.ts +144 -0
  150. package/tests/unit/exception/normalize.spec.ts +57 -0
  151. package/tests/unit/identifier/id.spec.ts +71 -0
  152. package/tests/unit/identifier/uuid.spec.ts +85 -0
  153. package/tests/unit/log/log-emitter.spec.ts +33 -0
  154. package/tests/unit/log/log-scheduler.spec.ts +40 -0
  155. package/tests/unit/log/log-type.spec.ts +7 -0
  156. package/tests/unit/log/logger.spec.ts +222 -0
  157. package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
  158. package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
  159. package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
  160. package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
  161. package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
  162. package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
  163. package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
  164. package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -0
  165. package/tests/unit/random/string.spec.ts +11 -0
  166. package/tests/unit/reactor/alien-signals-effect.spec.ts +11 -10
  167. package/tests/unit/reactor/preact-signal.spec.ts +1 -2
  168. package/tests/unit/singleton/singleton.spec.ts +49 -0
  169. package/tests/unit/storage/table.spec.ts +620 -0
  170. package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
  171. package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
  172. package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
  173. package/.oxlintrc.json +0 -5
  174. package/src/random/uuid.ts +0 -103
  175. package/tests/unit/random/uuid.spec.ts +0 -37
@@ -0,0 +1,66 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ colorStringToSrgbColorValue,
5
+ hexColorStringToSrgbColorValue,
6
+ hslColorStringToHslColorValue,
7
+ hslaColorStringToHslColorValue,
8
+ hsvColorStringToHsvColorValue,
9
+ hsvaColorStringToHsvColorValue,
10
+ isHexColorString,
11
+ rgbColorStringToSrgbColorValue,
12
+ rgbaColorStringToSrgbColorValue,
13
+ } from "#Source/color/index.ts"
14
+
15
+ test("isHexColorString validates supported hex formats", () => {
16
+ expect(isHexColorString("#F0A")).toBe(true)
17
+ expect(isHexColorString("11223344")).toBe(true)
18
+ expect(isHexColorString("#12")).toBe(false)
19
+ })
20
+
21
+ test("hexColorStringToSrgbColorValue supports short and long hex formats", () => {
22
+ expect(hexColorStringToSrgbColorValue("#F0A")).toEqual({ red: 255, green: 0, blue: 170, alpha: 1 })
23
+ expect(hexColorStringToSrgbColorValue("#112233")).toEqual({ red: 17, green: 34, blue: 51, alpha: 1 })
24
+ expect(hexColorStringToSrgbColorValue("#11223344")).toEqual({ red: 17, green: 34, blue: 51, alpha: 0.266_7 })
25
+ expect(() => hexColorStringToSrgbColorValue("#12")).toThrow(TypeError)
26
+ })
27
+
28
+ test("rgbColorStringToSrgbColorValue returns sRGB values", () => {
29
+ expect(rgbColorStringToSrgbColorValue("rgb(51, 102, 153)")).toEqual({ red: 51, green: 102, blue: 153, alpha: 1 })
30
+ expect(rgbColorStringToSrgbColorValue("rgb(0, 0, 0)")).toEqual({ red: 0, green: 0, blue: 0, alpha: 1 })
31
+ })
32
+
33
+ test("rgbaColorStringToSrgbColorValue returns sRGB values", () => {
34
+ expect(rgbaColorStringToSrgbColorValue("rgba(51, 102, 153, 0.5)")).toEqual({ red: 51, green: 102, blue: 153, alpha: 0.5 })
35
+ expect(() => rgbaColorStringToSrgbColorValue("rgba(51, 102, 153, 1.5)")).toThrow(RangeError)
36
+ })
37
+
38
+ test("hslColorStringToHslColorValue returns HSL values", () => {
39
+ expect(hslColorStringToHslColorValue("hsl(210, 50%, 40%)")).toEqual({ hue: 210, saturation: 0.5, lightness: 0.4, alpha: 1 })
40
+ expect(hslColorStringToHslColorValue("hsl(-30, 50%, 40%)")).toEqual({ hue: 330, saturation: 0.5, lightness: 0.4, alpha: 1 })
41
+ expect(() => hslColorStringToHslColorValue("hsl(210, 120%, 40%)")).toThrow(RangeError)
42
+ })
43
+
44
+ test("hslaColorStringToHslColorValue returns HSL values", () => {
45
+ expect(hslaColorStringToHslColorValue("hsla(210, 50%, 40%, 0.5)")).toEqual({ hue: 210, saturation: 0.5, lightness: 0.4, alpha: 0.5 })
46
+ expect(hslaColorStringToHslColorValue("hsla(0, 0%, 100%, 1)")).toEqual({ hue: 0, saturation: 0, lightness: 1, alpha: 1 })
47
+ })
48
+
49
+ test("hsvColorStringToHsvColorValue returns HSV values", () => {
50
+ expect(hsvColorStringToHsvColorValue("hsv(210, 66.67%, 60%)")).toEqual({ hue: 210, saturation: 0.666_7, value: 0.6, alpha: 1 })
51
+ expect(hsvColorStringToHsvColorValue("hsv(390, 50%, 60%)")).toEqual({ hue: 30, saturation: 0.5, value: 0.6, alpha: 1 })
52
+ })
53
+
54
+ test("hsvaColorStringToHsvColorValue returns HSV values", () => {
55
+ expect(hsvaColorStringToHsvColorValue("hsva(210, 66.67%, 60%, 0.5)")).toEqual({ hue: 210, saturation: 0.666_7, value: 0.6, alpha: 0.5 })
56
+ expect(hsvaColorStringToHsvColorValue("hsva(0, 0%, 100%, 1)")).toEqual({ hue: 0, saturation: 0, value: 1, alpha: 1 })
57
+ expect(() => hsvaColorStringToHsvColorValue("hsva(210, 66.67%, 60%, -0.1)")).toThrow(RangeError)
58
+ })
59
+
60
+ test("colorStringToSrgbColorValue folds supported string inputs into sRGB", () => {
61
+ expect(colorStringToSrgbColorValue("#336699")).toEqual({ red: 51, green: 102, blue: 153, alpha: 1 })
62
+ expect(colorStringToSrgbColorValue("hsl(210, 50%, 40%)")).toEqual({ red: 51, green: 102, blue: 153, alpha: 1 })
63
+ expect(colorStringToSrgbColorValue("hsv(210, 66.67%, 60%)")).toEqual({ red: 51, green: 102, blue: 153, alpha: 1 })
64
+ expect(colorStringToSrgbColorValue("rgb(51, 102, 153)")).toEqual({ red: 51, green: 102, blue: 153, alpha: 1 })
65
+ expect(() => colorStringToSrgbColorValue("foo(0, 0%, 0%)")).toThrow(TypeError)
66
+ })
@@ -0,0 +1,46 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ hslColorValueToHslColorString,
5
+ hslColorValueToHslaColorString,
6
+ hsvColorValueToHsvColorString,
7
+ hsvColorValueToHsvaColorString,
8
+ srgbColorValueToHexColorString,
9
+ srgbColorValueToRgbColorString,
10
+ srgbColorValueToRgbaColorString,
11
+ } from "#Source/color/index.ts"
12
+
13
+ test("srgbColorValueToHexColorString serializes hex output", () => {
14
+ expect(srgbColorValueToHexColorString({ red: 51, green: 102, blue: 153, alpha: 0.5 })).toBe("#336699")
15
+ expect(srgbColorValueToHexColorString({ red: 51, green: 102, blue: 153, alpha: 0.5 }, { includeAlpha: true })).toBe("#33669980")
16
+ })
17
+
18
+ test("srgbColorValueToRgbColorString serializes rgb output", () => {
19
+ expect(srgbColorValueToRgbColorString({ red: 51, green: 102, blue: 153, alpha: 0.5 })).toBe("rgb(51, 102, 153)")
20
+ expect(srgbColorValueToRgbColorString({ red: 0, green: 0, blue: 0, alpha: 1 })).toBe("rgb(0, 0, 0)")
21
+ })
22
+
23
+ test("srgbColorValueToRgbaColorString serializes rgba output", () => {
24
+ expect(srgbColorValueToRgbaColorString({ red: 51, green: 102, blue: 153, alpha: 0.5 })).toBe("rgba(51, 102, 153, 0.5)")
25
+ expect(srgbColorValueToRgbaColorString({ red: 255, green: 255, blue: 255, alpha: 1 })).toBe("rgba(255, 255, 255, 1)")
26
+ })
27
+
28
+ test("hslColorValueToHslColorString serializes hsl output", () => {
29
+ expect(hslColorValueToHslColorString({ hue: 210, saturation: 0.5, lightness: 0.4, alpha: 0.5 })).toBe("hsl(210, 50%, 40%)")
30
+ expect(hslColorValueToHslColorString({ hue: 0, saturation: 0, lightness: 1, alpha: 1 })).toBe("hsl(0, 0%, 100%)")
31
+ })
32
+
33
+ test("hslColorValueToHslaColorString serializes hsla output", () => {
34
+ expect(hslColorValueToHslaColorString({ hue: 210, saturation: 0.5, lightness: 0.4, alpha: 0.5 })).toBe("hsla(210, 50%, 40%, 0.5)")
35
+ expect(hslColorValueToHslaColorString({ hue: 0, saturation: 0, lightness: 1, alpha: 1 })).toBe("hsla(0, 0%, 100%, 1)")
36
+ })
37
+
38
+ test("hsvColorValueToHsvColorString serializes hsv output", () => {
39
+ expect(hsvColorValueToHsvColorString({ hue: 210, saturation: 0.666_7, value: 0.6, alpha: 0.5 })).toBe("hsv(210, 66.67%, 60%)")
40
+ expect(hsvColorValueToHsvColorString({ hue: 0, saturation: 0, value: 1, alpha: 1 })).toBe("hsv(0, 0%, 100%)")
41
+ })
42
+
43
+ test("hsvColorValueToHsvaColorString serializes hsva output", () => {
44
+ expect(hsvColorValueToHsvaColorString({ hue: 210, saturation: 0.666_7, value: 0.6, alpha: 0.5 })).toBe("hsva(210, 66.67%, 60%, 0.5)")
45
+ expect(hsvColorValueToHsvaColorString({ hue: 0, saturation: 0, value: 1, alpha: 1 })).toBe("hsva(0, 0%, 100%, 1)")
46
+ })
@@ -0,0 +1,33 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ xyzColorValueToRelativeLuminance,
5
+ xyzColorValueToTristimulusSum,
6
+ xyzColorValueToXyChromaticityValue,
7
+ } from "#Source/color/index.ts"
8
+
9
+ test("xyzColorValueToRelativeLuminance returns y channel", () => {
10
+ expect(xyzColorValueToRelativeLuminance({ x: 0.25, y: 0.3, z: 0.4, alpha: 1 })).toBe(0.3)
11
+ expect(xyzColorValueToRelativeLuminance({ x: 0, y: 0, z: 0, alpha: 1 })).toBe(0)
12
+ })
13
+
14
+ test("xyzColorValueToTristimulusSum returns x plus y plus z", () => {
15
+ expect(xyzColorValueToTristimulusSum({ x: 0.25, y: 0.3, z: 0.4, alpha: 1 })).toBe(0.95)
16
+ expect(xyzColorValueToTristimulusSum({ x: 0, y: 0, z: 0, alpha: 1 })).toBe(0)
17
+ })
18
+
19
+ test("xyzColorValueToXyChromaticityValue returns xy chromaticity coordinates", () => {
20
+ expect(xyzColorValueToXyChromaticityValue({ x: 0.25, y: 0.3, z: 0.4, alpha: 1 })).toEqual({
21
+ x: 0.263_158,
22
+ y: 0.315_789,
23
+ })
24
+
25
+ expect(xyzColorValueToXyChromaticityValue({ x: 0.412_456, y: 0.212_673, z: 0.019_334, alpha: 1 })).toEqual({
26
+ x: 0.64,
27
+ y: 0.33,
28
+ })
29
+
30
+ expect(() => xyzColorValueToXyChromaticityValue({ x: 0, y: 0, z: 0, alpha: 1 })).toThrow(
31
+ "XYZ tristimulus sum must be greater than 0 to compute xy chromaticity",
32
+ )
33
+ })
@@ -0,0 +1,10 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import { createXyzColorValue } from "#Source/color/index.ts"
4
+
5
+ test("createXyzColorValue validates non-negative channels", () => {
6
+ expect(createXyzColorValue({ x: 0.25, y: 0.3, z: 0.4, alpha: 1 }))
7
+ .toEqual({ x: 0.25, y: 0.3, z: 0.4, alpha: 1 })
8
+
9
+ expect(() => createXyzColorValue({ x: -0.1, y: 0.3, z: 0.4, alpha: 1 })).toThrow(RangeError)
10
+ })
@@ -0,0 +1,18 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ linearRgbColorValueToXyzColorValue,
5
+ xyzColorValueToLinearRgbColorValue,
6
+ } from "#Source/color/index.ts"
7
+
8
+ test("xyzColorValueToLinearRgbColorValue converts XYZ values to Linear RGB", () => {
9
+ expect(xyzColorValueToLinearRgbColorValue({ x: 0.333_784_22, y: 0.371_900_46, z: 0.621_726_04, alpha: 1 }))
10
+ .toEqual({ red: 0.2, green: 0.4, blue: 0.6, alpha: 1 })
11
+ })
12
+
13
+ test("linearRgbColorValueToXyzColorValue converts Linear RGB values to XYZ", () => {
14
+ expect(linearRgbColorValueToXyzColorValue({ red: 0.2, green: 0.4, blue: 0.6, alpha: 1 }))
15
+ .toEqual({ x: 0.333_784, y: 0.371_9, z: 0.621_726, alpha: 1 })
16
+
17
+ expect(() => linearRgbColorValueToXyzColorValue({ red: Number.NaN, green: 0.4, blue: 0.6, alpha: 1 })).toThrow(RangeError)
18
+ })
@@ -0,0 +1,157 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ addClass,
5
+ classArrayToClassObject,
6
+ classArrayToClassString,
7
+ classObjectToClassArray,
8
+ classObjectToClassString,
9
+ classStringToClassArray,
10
+ classStringToClassObject,
11
+ containClass,
12
+ formatClassToTarget,
13
+ neatenClassString,
14
+ prefixClassWith,
15
+ removeClass,
16
+ removePrefixOfClass,
17
+ replaceClass,
18
+ toClassArray,
19
+ toClassObject,
20
+ toClassString,
21
+ toggleClass,
22
+ } from "#Source/css/index.ts"
23
+
24
+ test("neatenClassString normalizes dots and spaces", () => {
25
+ expect(neatenClassString("mobius-base mobius-theme--light")).toBe("mobius-base mobius-theme--light")
26
+ expect(neatenClassString(".mobius-base.mobius-theme--light")).toBe("mobius-base mobius-theme--light")
27
+ expect(neatenClassString(" .mobius-base mobius-theme--light ")).toBe("mobius-base mobius-theme--light")
28
+ })
29
+
30
+ test("classStringToClassArray splits normalized class strings", () => {
31
+ expect(classStringToClassArray("mobius-base mobius-theme--light")).toEqual(["mobius-base", "mobius-theme--light"])
32
+ expect(classStringToClassArray(".mobius-base.mobius-theme--light")).toEqual(["mobius-base", "mobius-theme--light"])
33
+ expect(classStringToClassArray(" .mobius-base mobius-theme--light ")).toEqual(["mobius-base", "mobius-theme--light"])
34
+ })
35
+
36
+ test("classArrayToClassObject marks every class as true", () => {
37
+ expect(classArrayToClassObject(["mobius-base", "mobius-theme--light"]))
38
+ .toEqual({ "mobius-base": true, "mobius-theme--light": true })
39
+ expect(classArrayToClassObject(["mobius-base", ""]))
40
+ .toEqual({ "mobius-base": true })
41
+ expect(classArrayToClassObject([])).toEqual({})
42
+ })
43
+
44
+ test("classStringToClassObject converts strings through normalized array form", () => {
45
+ expect(classStringToClassObject("mobius-base mobius-theme--light"))
46
+ .toEqual({ "mobius-base": true, "mobius-theme--light": true })
47
+ expect(classStringToClassObject(".mobius-base.mobius-theme--light"))
48
+ .toEqual({ "mobius-base": true, "mobius-theme--light": true })
49
+ })
50
+
51
+ test("classObjectToClassArray keeps only truthy class names", () => {
52
+ expect(classObjectToClassArray({ active: true, disabled: false, primary: true }))
53
+ .toEqual(["active", "primary"])
54
+ expect(classObjectToClassArray({ "": true, active: true, disabled: false })).toEqual(["active"])
55
+ expect(classObjectToClassArray({ disabled: false })).toEqual([])
56
+ })
57
+
58
+ test("classArrayToClassString joins non-empty items with spaces", () => {
59
+ expect(classArrayToClassString(["mobius-base", "mobius-theme--light"]))
60
+ .toBe("mobius-base mobius-theme--light")
61
+ expect(classArrayToClassString(["mobius-base", "", "mobius-theme--light"]))
62
+ .toBe("mobius-base mobius-theme--light")
63
+ })
64
+
65
+ test("classObjectToClassString serializes only truthy keys", () => {
66
+ expect(classObjectToClassString({ active: true, disabled: false, primary: true }))
67
+ .toBe("active primary")
68
+ expect(classObjectToClassString({ "": true, active: true, disabled: false })).toBe("active")
69
+ expect(classObjectToClassString({ disabled: false })).toBe("")
70
+ })
71
+
72
+ test("toClassString preserves string input and serializes other shapes", () => {
73
+ expect(toClassString(" .button active ")).toBe(" .button active ")
74
+ expect(toClassString(["button", "active"])).toBe("button active")
75
+ expect(toClassString({ button: true, active: true, disabled: false })).toBe("button active")
76
+ })
77
+
78
+ test("toClassArray normalizes strings and clones arrays", () => {
79
+ const classArray = ["button", "", "active"]
80
+ const result = toClassArray(classArray)
81
+
82
+ expect(toClassArray(".button.active")).toEqual(["button", "active"])
83
+ expect(result).toEqual(["button", "active"])
84
+ expect(result).not.toBe(classArray)
85
+ expect(toClassArray({ button: true, active: true, disabled: false })).toEqual(["button", "active"])
86
+ })
87
+
88
+ test("toClassObject normalizes strings and clones objects", () => {
89
+ const classObject = { button: true, active: false, "": true }
90
+ const result = toClassObject(classObject)
91
+
92
+ expect(toClassObject("button active")).toEqual({ button: true, active: true })
93
+ expect(toClassObject(["button", "active"])).toEqual({ button: true, active: true })
94
+ expect(result).toEqual({ button: true, active: false })
95
+ expect(result).not.toBe(classObject)
96
+ })
97
+
98
+ test("formatClassToTarget returns the same external shape as target", () => {
99
+ expect(formatClassToTarget("", ["button", "active"])).toBe("button active")
100
+ expect(formatClassToTarget([], "button active")).toEqual(["button", "active"])
101
+ expect(formatClassToTarget({}, ["button", "active"])).toEqual({ button: true, active: true })
102
+ })
103
+
104
+ test("prefixClassWith adds prefixes only when missing", () => {
105
+ expect(prefixClassWith("pm-", "button active")).toBe("pm-button pm-active")
106
+ expect(prefixClassWith("pm-", ["button", "", "pm-active"])).toEqual(["pm-button", "pm-active"])
107
+ expect(prefixClassWith("pm-", { button: true, "": true, "pm-active": false }))
108
+ .toEqual({ "pm-button": true, "pm-active": false })
109
+ })
110
+
111
+ test("removePrefixOfClass removes prefixes only when present", () => {
112
+ expect(removePrefixOfClass("pm-", "pm-button pm-active plain")).toBe("button active plain")
113
+ expect(removePrefixOfClass("pm-", ["pm-button", "pm-", "plain"]))
114
+ .toEqual(["button", "plain"])
115
+ expect(removePrefixOfClass("pm-", { "pm-button": true, plain: false, "pm-": true }))
116
+ .toEqual({ button: true, plain: false })
117
+ })
118
+
119
+ test("addClass merges class names while preserving target shape", () => {
120
+ expect(addClass("button", "active primary")).toBe("button active primary")
121
+ expect(addClass(["button", ""], { active: true, primary: true, "": false }))
122
+ .toEqual(["button", "active", "primary"])
123
+ expect(addClass({ button: true }, ["active", "primary"]))
124
+ .toEqual({ button: true, active: true, primary: true })
125
+ })
126
+
127
+ test("removeClass removes selected class names while preserving target shape", () => {
128
+ expect(removeClass("button active primary", "active")).toBe("button primary")
129
+ expect(removeClass(["button", "active", "primary"], { active: true })).toEqual(["button", "primary"])
130
+ expect(removeClass({ button: true, active: true, primary: true }, ["active"]))
131
+ .toEqual({ button: true, active: false, primary: true })
132
+ })
133
+
134
+ test("toggleClass flips membership for each target class", () => {
135
+ expect(toggleClass("button active", "active primary")).toBe("button primary")
136
+ expect(toggleClass(["button", "active"], ["active", "", "primary"]))
137
+ .toEqual(["button", "primary"])
138
+ expect(toggleClass({ button: true, active: true }, ["active", "primary"]))
139
+ .toEqual({ button: true, active: false, primary: true })
140
+ })
141
+
142
+ test("replaceClass supports tuple arrays, string arrays, records, and string removals", () => {
143
+ expect(replaceClass("button active", [["active", "selected"]])).toBe("button selected")
144
+ expect(replaceClass("button active primary", ["active", "primary"])).toBe("button")
145
+ expect(replaceClass(["button", "active"], { active: "selected" })).toEqual(["button", "selected"])
146
+ expect(replaceClass({ button: true, active: true }, "active")).toEqual({ active: false, button: true })
147
+ expect(replaceClass({ "": true, button: true, active: false }, { "": "selected", active: "featured" }))
148
+ .toEqual({ button: true, featured: false })
149
+ expect(replaceClass("button active primary", [["active", ""], ["primary", "featured"]]))
150
+ .toBe("button featured")
151
+ })
152
+
153
+ test("containClass checks whether all required class names are present", () => {
154
+ expect(containClass("button active", "button active primary")).toBe(true)
155
+ expect(containClass(["button", "missing"], { button: true, active: true })).toBe(false)
156
+ expect(containClass({ button: true }, ["button", "active"])).toBe(true)
157
+ })
@@ -0,0 +1,20 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import { useFactory } from "#Source/environment/basic.ts"
4
+
5
+ test("useFactory executes selected branch by availability", () => {
6
+ const useEnabled = useFactory(() => 42, () => true)
7
+ const useDisabled = useFactory(() => 42, () => false)
8
+
9
+ const enabledResult = useEnabled(
10
+ (value) => value + 1,
11
+ () => -1,
12
+ )
13
+ const disabledResult = useDisabled(
14
+ (value) => value + 1,
15
+ () => -1,
16
+ )
17
+
18
+ expect(enabledResult).toBe(43)
19
+ expect(disabledResult).toBe(-1)
20
+ })
@@ -0,0 +1,146 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import {
4
+ getDeviceInfo,
5
+ getDeviceNavigatorInfo,
6
+ getDeviceScreenInfo,
7
+ parseUserAgent,
8
+ } from "#Source/environment/device.ts"
9
+
10
+ const setupForGetDeviceNavigatorInfo = (): (() => void) => {
11
+ vi.stubGlobal("navigator", {
12
+ oscpu: "Windows NT 10.0",
13
+ hardwareConcurrency: 8,
14
+ deviceMemory: 16,
15
+ maxTouchPoints: 5,
16
+ platform: "Win32",
17
+ devicePosture: { type: "continuous" },
18
+ connection: {
19
+ downlink: 10,
20
+ downlinkMax: 100,
21
+ effectiveType: "4g",
22
+ rtt: 40,
23
+ saveData: false,
24
+ type: "wifi",
25
+ },
26
+ onLine: true,
27
+ cookieEnabled: true,
28
+ language: "en-US",
29
+ languages: ["en-US", "zh-CN"],
30
+ pdfViewerEnabled: true,
31
+ userAgent:
32
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
33
+ })
34
+
35
+ return () => {
36
+ vi.unstubAllGlobals()
37
+ }
38
+ }
39
+ test("getDeviceNavigatorInfo collects navigator metadata", () => {
40
+ const restore = setupForGetDeviceNavigatorInfo()
41
+
42
+ const info = getDeviceNavigatorInfo()
43
+
44
+ expect(info.oscpu).toBe("Windows NT 10.0")
45
+ expect(info.hardwareConcurrency).toBe(8)
46
+ expect(info.connectionType).toBe("wifi")
47
+ expect(info.languages).toEqual(["en-US", "zh-CN"])
48
+
49
+ restore()
50
+ })
51
+
52
+ test("parseUserAgent normalizes user-agent metadata", () => {
53
+ const userAgent =
54
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
55
+
56
+ const info = parseUserAgent(userAgent)
57
+
58
+ expect(info.ua).toBe(userAgent)
59
+ expect(typeof info.browserIsBot).toBe("boolean")
60
+ expect(typeof info.browserIsChromeFamily).toBe("boolean")
61
+ expect(typeof info.deviceIsAppleSilicon).toBe("boolean")
62
+ })
63
+
64
+ const setupForGetDeviceScreenInfo = (): (() => void) => {
65
+ vi.stubGlobal("document", {
66
+ documentElement: {
67
+ clientWidth: 1_200,
68
+ clientHeight: 700,
69
+ scrollWidth: 2_000,
70
+ scrollHeight: 3_000,
71
+ },
72
+ })
73
+ vi.stubGlobal("window", {
74
+ devicePixelRatio: 2,
75
+ screen: {
76
+ width: 1_920,
77
+ height: 1_080,
78
+ availWidth: 1_900,
79
+ availHeight: 1_060,
80
+ orientation: { type: "landscape-primary" },
81
+ },
82
+ outerWidth: 1_400,
83
+ outerHeight: 900,
84
+ innerWidth: 1_200,
85
+ innerHeight: 700,
86
+ visualViewport: {
87
+ width: 1_180,
88
+ height: 680,
89
+ offsetLeft: 10,
90
+ offsetTop: 8,
91
+ pageLeft: 0,
92
+ pageTop: 0,
93
+ scale: 1,
94
+ },
95
+ })
96
+ vi.stubGlobal("getComputedStyle", () => ({
97
+ getPropertyValue: (name: string): string => {
98
+ if (name.includes("top")) {
99
+ return "10"
100
+ }
101
+ if (name.includes("bottom")) {
102
+ return "20"
103
+ }
104
+ if (name.includes("left")) {
105
+ return "30"
106
+ }
107
+ if (name.includes("right")) {
108
+ return "40"
109
+ }
110
+ return "0"
111
+ },
112
+ }))
113
+
114
+ return () => {
115
+ vi.unstubAllGlobals()
116
+ }
117
+ }
118
+ test("getDeviceScreenInfo collects screen and viewport metadata", () => {
119
+ const restore = setupForGetDeviceScreenInfo()
120
+
121
+ const info = getDeviceScreenInfo()
122
+
123
+ expect(info.pixelRatio).toBe(2)
124
+ expect(info.screenWidth).toBe(1_920)
125
+ expect(info.viewportAvailableWidth).toBe(1_200)
126
+ expect(info.safeAreaTop).toBe(10)
127
+ expect(info.safeAreaRight).toBe(40)
128
+
129
+ restore()
130
+ })
131
+
132
+ test("getDeviceInfo aggregates navigator, user-agent, and screen metadata", () => {
133
+ const restore1 = setupForGetDeviceNavigatorInfo()
134
+ const restore2 = setupForGetDeviceScreenInfo()
135
+
136
+ const userAgent =
137
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
138
+ const info = getDeviceInfo(userAgent)
139
+
140
+ expect(info.navigator.language).toBe("en-US")
141
+ expect(info.userAgent.ua).toBe(userAgent)
142
+ expect(info.screen.viewportHeight).toBe(700)
143
+
144
+ restore1()
145
+ restore2()
146
+ })