@godxjp/ui 5.0.2 → 6.0.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 (298) hide show
  1. package/README.md +101 -142
  2. package/package.json +124 -128
  3. package/scripts/ui-audit.mjs +179 -0
  4. package/src/app/__tests__/app-provider.test.tsx +232 -0
  5. package/src/app/__tests__/date-format-labels.test.ts +36 -0
  6. package/src/app/__tests__/date-formats.test.ts +44 -0
  7. package/src/app/__tests__/timezones.test.ts +65 -0
  8. package/src/app/app-provider.tsx +227 -0
  9. package/src/app/date-format-labels.ts +21 -0
  10. package/src/app/date-formats.ts +30 -0
  11. package/src/app/index.ts +40 -0
  12. package/src/app/locales.ts +32 -0
  13. package/src/app/request-headers.ts +31 -0
  14. package/src/app/storage.ts +44 -0
  15. package/src/app/time-format-labels.ts +19 -0
  16. package/src/app/time-formats.ts +15 -0
  17. package/src/app/timezones.ts +208 -0
  18. package/src/app/types.ts +39 -0
  19. package/src/app/use-formatting.ts +47 -0
  20. package/src/components/__tests__/accessibility-primitives.test.tsx +65 -0
  21. package/src/components/__tests__/docs-parity.test.ts +41 -0
  22. package/src/components/__tests__/shadcn-release-guardrails.test.ts +71 -0
  23. package/src/components/__tests__/theme-axes-integration.test.tsx +242 -0
  24. package/src/components/admin/index.ts +76 -0
  25. package/src/components/data-display/__tests__/card-table.test.tsx +328 -0
  26. package/src/components/data-display/__tests__/data-display.test.tsx +73 -0
  27. package/src/components/data-display/__tests__/data-table.test.tsx +84 -0
  28. package/src/components/data-display/__tests__/popover.test.tsx +92 -0
  29. package/src/components/data-display/__tests__/scroll-area-collapsible.test.tsx +66 -0
  30. package/src/components/data-display/badge.tsx +27 -0
  31. package/src/components/data-display/card.tsx +194 -0
  32. package/src/components/data-display/code-badge.tsx +28 -0
  33. package/src/components/data-display/collapsible.tsx +5 -0
  34. package/src/components/data-display/data-table.tsx +476 -0
  35. package/src/components/data-display/empty-state.tsx +22 -0
  36. package/src/components/data-display/index.ts +41 -0
  37. package/src/components/data-display/key-value-grid.tsx +46 -0
  38. package/src/components/data-display/popover.tsx +62 -0
  39. package/src/components/data-display/progress-meter.tsx +20 -0
  40. package/src/components/data-display/scan-panel.tsx +16 -0
  41. package/src/components/data-display/scroll-area.tsx +42 -0
  42. package/src/components/data-display/status-badge.tsx +83 -0
  43. package/src/components/data-display/table.tsx +59 -0
  44. package/src/components/data-display/timeline.tsx +42 -0
  45. package/src/components/data-display/tree-list.tsx +42 -0
  46. package/src/components/data-entry/__fixtures__/tree-options.ts +80 -0
  47. package/src/components/data-entry/__tests__/cascader-tree-transfer.test.tsx +417 -0
  48. package/src/components/data-entry/__tests__/checkbox-group.test.tsx +40 -0
  49. package/src/components/data-entry/__tests__/checkbox.test.tsx +20 -0
  50. package/src/components/data-entry/__tests__/date-autocomplete.test.tsx +94 -0
  51. package/src/components/data-entry/__tests__/form-field.test.tsx +49 -0
  52. package/src/components/data-entry/__tests__/input-textarea.test.tsx +38 -0
  53. package/src/components/data-entry/__tests__/label-select.test.tsx +62 -0
  54. package/src/components/data-entry/__tests__/pickers.test.tsx +74 -0
  55. package/src/components/data-entry/__tests__/radio.test.tsx +46 -0
  56. package/src/components/data-entry/__tests__/search-input.test.tsx +32 -0
  57. package/src/components/data-entry/__tests__/switch-field.test.tsx +52 -0
  58. package/src/components/data-entry/__tests__/upload.test.tsx +125 -0
  59. package/src/components/data-entry/autocomplete.tsx +91 -0
  60. package/src/components/data-entry/calendar.tsx +90 -0
  61. package/src/components/data-entry/cascader.tsx +305 -0
  62. package/src/components/data-entry/checkbox-group.tsx +90 -0
  63. package/src/components/data-entry/checkbox.tsx +30 -0
  64. package/src/components/data-entry/choice-field.tsx +27 -0
  65. package/src/components/data-entry/choice-option.ts +20 -0
  66. package/src/components/data-entry/color-picker.tsx +75 -0
  67. package/src/components/data-entry/command.tsx +56 -0
  68. package/src/components/data-entry/country-select.tsx +88 -0
  69. package/src/components/data-entry/date-picker.tsx +69 -0
  70. package/src/components/data-entry/date-range-picker.tsx +75 -0
  71. package/src/components/data-entry/form-field.tsx +59 -0
  72. package/src/components/data-entry/index.ts +62 -0
  73. package/src/components/data-entry/input.tsx +26 -0
  74. package/src/components/data-entry/label.tsx +25 -0
  75. package/src/components/data-entry/radio.tsx +109 -0
  76. package/src/components/data-entry/search-input.tsx +103 -0
  77. package/src/components/data-entry/select.tsx +149 -0
  78. package/src/components/data-entry/slider.tsx +38 -0
  79. package/src/components/data-entry/switch-field.tsx +91 -0
  80. package/src/components/data-entry/switch.tsx +24 -0
  81. package/src/components/data-entry/textarea.tsx +12 -0
  82. package/src/components/data-entry/time-picker.tsx +214 -0
  83. package/src/components/data-entry/transfer.tsx +231 -0
  84. package/src/components/data-entry/tree-select-strategy.ts +6 -0
  85. package/src/components/data-entry/tree-select.tsx +279 -0
  86. package/src/components/data-entry/tree-utils.ts +221 -0
  87. package/src/components/data-entry/upload-crop-dialog.tsx +109 -0
  88. package/src/components/data-entry/upload-types.ts +86 -0
  89. package/src/components/data-entry/upload.tsx +498 -0
  90. package/src/components/data-entry/use-upload-draft.ts +93 -0
  91. package/src/components/feedback/__tests__/alert.test.tsx +127 -0
  92. package/src/components/feedback/__tests__/dialog.test.tsx +290 -0
  93. package/src/components/feedback/__tests__/sheet.test.tsx +94 -0
  94. package/src/components/feedback/__tests__/skeleton.test.tsx +25 -0
  95. package/src/components/feedback/__tests__/toast.test.tsx +52 -0
  96. package/src/components/feedback/alert.tsx +167 -0
  97. package/src/components/feedback/dialog.tsx +325 -0
  98. package/src/components/feedback/index.ts +53 -0
  99. package/src/components/feedback/sheet.tsx +130 -0
  100. package/src/components/feedback/skeleton.tsx +95 -0
  101. package/src/components/feedback/sonner.tsx +54 -0
  102. package/src/components/feedback/toaster.tsx +1 -0
  103. package/src/components/feedback/use-toast.ts +62 -0
  104. package/src/components/general/__tests__/button.test.tsx +71 -0
  105. package/src/components/general/button.tsx +61 -0
  106. package/src/components/general/index.ts +2 -0
  107. package/src/components/layout/__tests__/page-container.test.tsx +69 -0
  108. package/src/components/layout/__tests__/page-inset.test.tsx +14 -0
  109. package/src/components/layout/__tests__/stack-inline.test.tsx +39 -0
  110. package/src/components/layout/app-shell.tsx +42 -0
  111. package/src/components/layout/breadcrumb.tsx +35 -0
  112. package/src/components/layout/index.ts +31 -0
  113. package/src/components/layout/inline.tsx +13 -0
  114. package/src/components/layout/menu.tsx +34 -0
  115. package/src/components/layout/mobile-frame.tsx +57 -0
  116. package/src/components/layout/page-container.tsx +81 -0
  117. package/src/components/layout/page-inset.tsx +16 -0
  118. package/src/components/layout/responsive-grid.tsx +14 -0
  119. package/src/components/layout/shell-app.tsx +30 -0
  120. package/src/components/layout/sidebar.tsx +98 -0
  121. package/src/components/layout/split-pane.tsx +16 -0
  122. package/src/components/layout/stack.tsx +13 -0
  123. package/src/components/layout/topbar.tsx +108 -0
  124. package/src/components/navigation/__tests__/app-pickers.test.tsx +118 -0
  125. package/src/components/navigation/__tests__/dropdown-menu.test.tsx +104 -0
  126. package/src/components/navigation/__tests__/navigation.test.tsx +61 -0
  127. package/src/components/navigation/__tests__/pagination-steps-tabs.test.tsx +76 -0
  128. package/src/components/navigation/date-format-picker.tsx +55 -0
  129. package/src/components/navigation/dropdown-menu.tsx +190 -0
  130. package/src/components/navigation/filter-bar.tsx +38 -0
  131. package/src/components/navigation/index.ts +28 -0
  132. package/src/components/navigation/locale-picker.tsx +49 -0
  133. package/src/components/navigation/page-header.tsx +50 -0
  134. package/src/components/navigation/pagination-utils.ts +35 -0
  135. package/src/components/navigation/pagination.tsx +168 -0
  136. package/src/components/navigation/steps.tsx +163 -0
  137. package/src/components/navigation/tabs-items.tsx +69 -0
  138. package/src/components/navigation/tabs.tsx +67 -0
  139. package/src/components/navigation/time-format-picker.tsx +55 -0
  140. package/src/components/navigation/timezone-picker.tsx +63 -0
  141. package/src/components/query/__tests__/data-state.test.tsx +214 -0
  142. package/src/components/query/__tests__/infinite-prefetch.test.tsx +105 -0
  143. package/src/components/query/__tests__/query-helpers.test.tsx +61 -0
  144. package/src/components/query/data-state.tsx +58 -0
  145. package/src/components/query/index.ts +10 -0
  146. package/src/components/query/infinite-query-state.tsx +99 -0
  147. package/src/components/query/mutation-feedback.tsx +31 -0
  148. package/src/components/query/prefetch-link.tsx +45 -0
  149. package/src/components/query/query-refetch-button.tsx +41 -0
  150. package/src/components/ui/alert-dialog.tsx +1 -0
  151. package/src/components/ui/alert.tsx +1 -0
  152. package/src/components/ui/autocomplete.tsx +1 -0
  153. package/src/components/ui/badge.tsx +1 -0
  154. package/src/components/ui/button.tsx +1 -0
  155. package/src/components/ui/calendar.tsx +1 -0
  156. package/src/components/ui/card.tsx +1 -0
  157. package/src/components/ui/checkbox.tsx +1 -0
  158. package/src/components/ui/color-picker.tsx +1 -0
  159. package/src/components/ui/command.tsx +1 -0
  160. package/src/components/ui/date-picker.tsx +1 -0
  161. package/src/components/ui/date-range-picker.tsx +1 -0
  162. package/src/components/ui/dialog.tsx +1 -0
  163. package/src/components/ui/dropdown-menu.tsx +1 -0
  164. package/src/components/ui/index.tsx +31 -0
  165. package/src/components/ui/input.tsx +1 -0
  166. package/src/components/ui/label.tsx +1 -0
  167. package/src/components/ui/pagination.tsx +1 -0
  168. package/src/components/ui/popover.tsx +1 -0
  169. package/src/components/ui/radio.tsx +1 -0
  170. package/src/components/ui/scroll-area.tsx +1 -0
  171. package/src/components/ui/select.tsx +1 -0
  172. package/src/components/ui/sheet.tsx +1 -0
  173. package/src/components/ui/slider.tsx +1 -0
  174. package/src/components/ui/sonner.tsx +1 -0
  175. package/src/components/ui/switch.tsx +1 -0
  176. package/src/components/ui/table.tsx +1 -0
  177. package/src/components/ui/tabs-items.tsx +1 -0
  178. package/src/components/ui/tabs.tsx +1 -0
  179. package/src/components/ui/textarea.tsx +1 -0
  180. package/src/components/ui/time-picker.tsx +1 -0
  181. package/src/components/ui/upload.tsx +1 -0
  182. package/src/form/__tests__/use-zod-form.test.tsx +97 -0
  183. package/src/form/form-field-control.tsx +44 -0
  184. package/src/form/form-root.tsx +29 -0
  185. package/src/form/index.ts +7 -0
  186. package/src/form/use-zod-form.ts +29 -0
  187. package/src/i18n/__tests__/translate.test.ts +23 -0
  188. package/src/i18n/index.ts +9 -0
  189. package/src/i18n/messages/en.json +171 -0
  190. package/src/i18n/messages/ja.json +171 -0
  191. package/src/i18n/messages/vi.json +171 -0
  192. package/src/i18n/translate.ts +74 -0
  193. package/src/i18n/use-translation.ts +53 -0
  194. package/src/index.ts +3 -0
  195. package/src/lib/__tests__/control-styles.test.ts +78 -0
  196. package/src/lib/__tests__/datetime.test.ts +77 -0
  197. package/src/lib/__tests__/format-date.test.ts +97 -0
  198. package/src/lib/__tests__/format.test.ts +62 -0
  199. package/src/lib/__tests__/theme-tokens-audit.test.ts +176 -0
  200. package/src/lib/__tests__/theme-tokens-css.test.ts +118 -0
  201. package/src/lib/__tests__/token-governance.test.ts +191 -0
  202. package/src/lib/__tests__/variants.test.ts +18 -0
  203. package/src/lib/control-styles.ts +33 -0
  204. package/src/lib/datetime/detect.ts +25 -0
  205. package/src/lib/datetime/format-date.ts +100 -0
  206. package/src/lib/datetime/format.ts +140 -0
  207. package/src/lib/datetime/index.ts +25 -0
  208. package/src/lib/datetime/parse.ts +51 -0
  209. package/src/lib/datetime/sync.ts +48 -0
  210. package/src/lib/format.ts +114 -0
  211. package/src/lib/hooks.ts +54 -0
  212. package/src/lib/utils.ts +6 -0
  213. package/src/lib/variants.ts +40 -0
  214. package/src/props/components/app.prop.ts +99 -0
  215. package/src/props/components/data-display.prop.ts +73 -0
  216. package/src/props/components/data-entry.prop.ts +334 -0
  217. package/src/props/components/feedback.prop.ts +80 -0
  218. package/src/props/components/form.prop.ts +46 -0
  219. package/src/props/components/general.prop.ts +18 -0
  220. package/src/props/components/index.ts +99 -0
  221. package/src/props/components/layout.prop.ts +130 -0
  222. package/src/props/components/navigation.prop.ts +88 -0
  223. package/src/props/components/query.prop.ts +94 -0
  224. package/src/props/index.ts +17 -0
  225. package/src/props/registry.ts +603 -0
  226. package/src/props/vocabulary/content.prop.ts +35 -0
  227. package/src/props/vocabulary/data.prop.ts +46 -0
  228. package/src/props/vocabulary/index.ts +73 -0
  229. package/src/props/vocabulary/interaction.prop.ts +42 -0
  230. package/src/props/vocabulary/layout.prop.ts +25 -0
  231. package/src/props/vocabulary/navigation.prop.ts +19 -0
  232. package/src/props/vocabulary/shared.prop.ts +59 -0
  233. package/src/styles/alert-layout.css +191 -0
  234. package/src/styles/badge-layout.css +22 -0
  235. package/src/styles/card-layout.css +373 -0
  236. package/src/styles/control.css +504 -0
  237. package/src/styles/data-display-layout.css +246 -0
  238. package/src/styles/density.css +43 -0
  239. package/src/styles/dialog-layout.css +84 -0
  240. package/src/styles/index.css +105 -0
  241. package/src/styles/layout.css +479 -0
  242. package/src/styles/shell-layout.css +604 -0
  243. package/src/styles/table-layout.css +109 -0
  244. package/src/test/__tests__/render-loop-guard.test.tsx +38 -0
  245. package/src/test/jest-dom.d.ts +4 -0
  246. package/src/test/render-loop-guard.tsx +50 -0
  247. package/src/test/render.tsx +29 -0
  248. package/src/test/theme-globals.test.ts +77 -0
  249. package/src/test/theme-globals.ts +134 -0
  250. package/src/test/theme-test-utils.tsx +67 -0
  251. package/src/theme/example.service.css +37 -0
  252. package/src/tokens/base.css +13 -0
  253. package/src/tokens/foundation.css +151 -0
  254. package/src/tokens/primitives/badge.css +13 -0
  255. package/src/tokens/primitives/card.css +29 -0
  256. package/src/tokens/primitives/control.css +55 -0
  257. package/src/tokens/primitives/feedback.css +17 -0
  258. package/src/tokens/primitives/layout.css +20 -0
  259. package/src/tokens/primitives/navigation.css +13 -0
  260. package/src/tokens/primitives/table.css +10 -0
  261. package/BRAND.md +0 -296
  262. package/CHANGELOG.md +0 -668
  263. package/config/eslint.js +0 -54
  264. package/config/prettier.cjs +0 -20
  265. package/config/tsconfig.base.json +0 -22
  266. package/config/vitest.base.ts +0 -26
  267. package/dist/MiniMonth-YAmPGEpC.d.ts +0 -143
  268. package/dist/Table.types-BbsxoIYE.d.ts +0 -352
  269. package/dist/color-DO0qqUAb.d.ts +0 -38
  270. package/dist/components/composites.d.ts +0 -963
  271. package/dist/components/composites.js +0 -7343
  272. package/dist/components/composites.js.map +0 -1
  273. package/dist/components/primitives.d.ts +0 -2744
  274. package/dist/components/primitives.js +0 -7356
  275. package/dist/components/primitives.js.map +0 -1
  276. package/dist/components/shell.d.ts +0 -182
  277. package/dist/components/shell.js +0 -774
  278. package/dist/components/shell.js.map +0 -1
  279. package/dist/hooks.d.ts +0 -100
  280. package/dist/hooks.js +0 -558
  281. package/dist/hooks.js.map +0 -1
  282. package/dist/i18n.d.ts +0 -61
  283. package/dist/i18n.js +0 -860
  284. package/dist/i18n.js.map +0 -1
  285. package/dist/index.d.ts +0 -33
  286. package/dist/index.js +0 -13062
  287. package/dist/index.js.map +0 -1
  288. package/dist/padding-DY0JV5Ja.d.ts +0 -16
  289. package/dist/preferences.d.ts +0 -132
  290. package/dist/preferences.js +0 -262
  291. package/dist/preferences.js.map +0 -1
  292. package/dist/props.d.ts +0 -86
  293. package/dist/props.js +0 -16
  294. package/dist/props.js.map +0 -1
  295. package/dist/size-CQwNvOWd.d.ts +0 -19
  296. package/dist/types-LTj-2bl-.d.ts +0 -30
  297. package/dist/useTableViews-D5NIAJ7h.d.ts +0 -154
  298. package/src/tokens/tailwind.css +0 -158
@@ -0,0 +1,417 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import * as React from "react";
3
+ import { renderWithUi, screen, userEvent, waitFor } from "@/test/render";
4
+ import { Cascader } from "../cascader";
5
+ import { TreeSelect } from "../tree-select";
6
+ import { Transfer } from "../transfer";
7
+ import { REGION_OPTIONS, ORG_TREE, TRANSFER_MOCK } from "../__fixtures__/tree-options";
8
+ import {
9
+ collectAllPaths,
10
+ collectLeafPaths,
11
+ filterTreeOptions,
12
+ formatPathLabels,
13
+ getNodeByPath,
14
+ normalizeTreeOptions,
15
+ pathKey,
16
+ } from "../tree-utils";
17
+
18
+ describe("tree-utils", () => {
19
+ const options = normalizeTreeOptions(REGION_OPTIONS);
20
+
21
+ it("resolves path labels", () => {
22
+ const path = ["vn", "hcm", "q1"];
23
+ expect(formatPathLabels(getNodeByPath(options, path))).toBe(
24
+ "Việt Nam / TP. Hồ Chí Minh / Quận 1",
25
+ );
26
+ });
27
+
28
+ it("collects leaf paths", () => {
29
+ const paths = collectAllPaths(options);
30
+ expect(paths.some((p) => pathKey(p.path) === pathKey(["jp", "tokyo", "shibuya"]))).toBe(true);
31
+ });
32
+
33
+ it("collects nested leaf path labels from tree root", () => {
34
+ const paths = collectLeafPaths(options);
35
+ const shinjuku = paths.find((p) => pathKey(p.path) === pathKey(["jp", "tokyo", "shinjuku"]));
36
+ expect(shinjuku?.labels.join(" / ")).toBe("日本 / 東京都 / 新宿区");
37
+ });
38
+
39
+ it("collects nested node labels from tree root", () => {
40
+ const paths = collectAllPaths(options);
41
+ const tokyo = paths.find((p) => pathKey(p.path) === pathKey(["jp", "tokyo"]));
42
+ expect(tokyo?.labels.join(" / ")).toBe("日本 / 東京都");
43
+ });
44
+
45
+ it("filters leaf paths by label (case-insensitive)", () => {
46
+ const matches = filterTreeOptions(options, "QUẬN 1");
47
+ expect(matches).toHaveLength(1);
48
+ expect(matches[0]?.labels.join(" / ")).toBe("Việt Nam / TP. Hồ Chí Minh / Quận 1");
49
+ });
50
+
51
+ it("returns empty array for blank search query", () => {
52
+ expect(filterTreeOptions(options, " ")).toEqual([]);
53
+ });
54
+
55
+ it("matches any segment in the path chain", () => {
56
+ const matches = filterTreeOptions(options, "東京都");
57
+ expect(matches.some((m) => pathKey(m.path) === pathKey(["jp", "tokyo", "shinjuku"]))).toBe(
58
+ true,
59
+ );
60
+ expect(matches.some((m) => pathKey(m.path) === pathKey(["jp", "tokyo", "shibuya"]))).toBe(true);
61
+ });
62
+ });
63
+
64
+ describe("Cascader", () => {
65
+ it("renders trigger with placeholder", () => {
66
+ renderWithUi(<Cascader options={REGION_OPTIONS} />);
67
+ expect(screen.getByRole("combobox")).toBeInTheDocument();
68
+ expect(screen.getByText("Chọn…")).toBeInTheDocument();
69
+ });
70
+
71
+ it("selects a leaf path", async () => {
72
+ const user = userEvent.setup();
73
+ const onChange = vi.fn();
74
+
75
+ renderWithUi(<Cascader options={REGION_OPTIONS} onChange={onChange} />);
76
+ await user.click(screen.getByRole("combobox"));
77
+
78
+ await user.click(screen.getByRole("button", { name: /việt nam/i }));
79
+ await user.click(screen.getByRole("button", { name: /tp\. hồ chí minh/i }));
80
+ await user.click(screen.getByRole("button", { name: /quận 1/i }));
81
+
82
+ expect(onChange).toHaveBeenCalledWith(["vn", "hcm", "q1"], expect.any(Array));
83
+ });
84
+
85
+ it("shows cascade columns when showSearch is on and query is empty", async () => {
86
+ const user = userEvent.setup();
87
+
88
+ renderWithUi(
89
+ <Cascader options={REGION_OPTIONS} showSearch defaultValue={["jp", "tokyo", "shinjuku"]} />,
90
+ );
91
+ await user.click(screen.getByRole("combobox"));
92
+
93
+ expect(screen.getByRole("button", { name: /việt nam/i })).toBeInTheDocument();
94
+ expect(screen.getByRole("button", { name: /^日本$/i })).toBeInTheDocument();
95
+ expect(screen.queryByRole("option")).not.toBeInTheDocument();
96
+ });
97
+
98
+ it("filters leaf paths when searching", async () => {
99
+ const user = userEvent.setup();
100
+
101
+ renderWithUi(<Cascader options={REGION_OPTIONS} showSearch />);
102
+ await user.click(screen.getByRole("combobox"));
103
+ await user.type(screen.getByPlaceholderText(/tìm kiếm/i), "新宿");
104
+
105
+ expect(await screen.findByRole("button", { name: /新宿区/i })).toBeInTheDocument();
106
+ expect(screen.queryByRole("button", { name: /^việt nam$/i })).not.toBeInTheDocument();
107
+ });
108
+
109
+ it("selects a path from search results", async () => {
110
+ const user = userEvent.setup();
111
+ const onChange = vi.fn();
112
+
113
+ renderWithUi(<Cascader options={REGION_OPTIONS} showSearch onChange={onChange} />);
114
+ await user.click(screen.getByRole("combobox"));
115
+ await user.type(screen.getByPlaceholderText(/tìm kiếm/i), "quận 3");
116
+
117
+ await user.click(await screen.findByRole("button", { name: /quận 3/i }));
118
+
119
+ expect(onChange).toHaveBeenCalledWith(["vn", "hcm", "q3"], expect.any(Array));
120
+ });
121
+
122
+ it("shows default value label on trigger", () => {
123
+ renderWithUi(
124
+ <Cascader options={REGION_OPTIONS} showSearch defaultValue={["jp", "tokyo", "shinjuku"]} />,
125
+ );
126
+
127
+ expect(screen.getByRole("combobox")).toHaveTextContent("日本 / 東京都 / 新宿区");
128
+ });
129
+
130
+ it("clears search when popover closes", async () => {
131
+ const user = userEvent.setup();
132
+
133
+ renderWithUi(<Cascader options={REGION_OPTIONS} showSearch />);
134
+ await user.click(screen.getByRole("combobox"));
135
+ await user.type(screen.getByPlaceholderText(/tìm kiếm/i), "新宿");
136
+ expect(await screen.findByRole("button", { name: /新宿区/i })).toBeInTheDocument();
137
+
138
+ await user.keyboard("{Escape}");
139
+ await user.click(screen.getByRole("combobox"));
140
+
141
+ expect(screen.getByPlaceholderText(/tìm kiếm/i)).toHaveValue("");
142
+ expect(screen.getByRole("button", { name: /việt nam/i })).toBeInTheDocument();
143
+ });
144
+
145
+ it("expands cascade columns when clicking a parent node", async () => {
146
+ const user = userEvent.setup();
147
+
148
+ renderWithUi(<Cascader options={REGION_OPTIONS} />);
149
+ await user.click(screen.getByRole("combobox"));
150
+ await user.click(screen.getByRole("button", { name: /việt nam/i }));
151
+
152
+ expect(screen.getByRole("button", { name: /tp\. hồ chí minh/i })).toBeInTheDocument();
153
+ expect(screen.getByRole("button", { name: /hà nội/i })).toBeInTheDocument();
154
+ });
155
+
156
+ it("commits intermediate path when changeOnSelect is enabled", async () => {
157
+ const user = userEvent.setup();
158
+ const onChange = vi.fn();
159
+
160
+ renderWithUi(<Cascader options={REGION_OPTIONS} changeOnSelect onChange={onChange} />);
161
+ await user.click(screen.getByRole("combobox"));
162
+ await user.click(screen.getByRole("button", { name: /việt nam/i }));
163
+
164
+ expect(onChange).toHaveBeenCalledWith(["vn"], expect.any(Array));
165
+ });
166
+
167
+ it("clears selected value via clear icon", async () => {
168
+ const user = userEvent.setup();
169
+ const onChange = vi.fn();
170
+
171
+ renderWithUi(
172
+ <Cascader options={REGION_OPTIONS} defaultValue={["vn", "hcm", "q1"]} onChange={onChange} />,
173
+ );
174
+
175
+ const combobox = screen.getByRole("combobox");
176
+ const clearIcon = combobox.querySelector("svg.lucide-x");
177
+ expect(clearIcon).toBeTruthy();
178
+ await user.click(clearIcon!);
179
+
180
+ expect(onChange).toHaveBeenCalledWith([], expect.any(Array));
181
+ expect(combobox).toHaveTextContent("Chọn…");
182
+ });
183
+
184
+ it("shows empty state when search has no matches", async () => {
185
+ const user = userEvent.setup();
186
+
187
+ renderWithUi(<Cascader options={REGION_OPTIONS} showSearch />);
188
+ await user.click(screen.getByRole("combobox"));
189
+ await user.type(screen.getByPlaceholderText(/tìm kiếm/i), "xyz-no-match");
190
+
191
+ expect(await screen.findByText(/không có kết quả/i)).toBeInTheDocument();
192
+ });
193
+
194
+ it("does not open when disabled", async () => {
195
+ const user = userEvent.setup();
196
+
197
+ renderWithUi(<Cascader options={REGION_OPTIONS} disabled defaultValue={["vn", "hcm", "q1"]} />);
198
+ await user.click(screen.getByRole("combobox"));
199
+
200
+ expect(screen.queryByRole("button", { name: /việt nam/i })).not.toBeInTheDocument();
201
+ });
202
+
203
+ it("supports multiple selection without closing panel", async () => {
204
+ const user = userEvent.setup();
205
+ const onChange = vi.fn();
206
+
207
+ renderWithUi(<Cascader options={REGION_OPTIONS} multiple showSearch onChange={onChange} />);
208
+ await user.click(screen.getByRole("combobox"));
209
+ await user.type(screen.getByPlaceholderText(/tìm kiếm/i), "quận 1");
210
+ await user.click(await screen.findByRole("button", { name: /quận 1/i }));
211
+
212
+ expect(onChange).toHaveBeenCalledWith([["vn", "hcm", "q1"]], expect.any(Array));
213
+ expect(screen.getAllByRole("combobox")[0]).toHaveTextContent(
214
+ "Việt Nam / TP. Hồ Chí Minh / Quận 1",
215
+ );
216
+
217
+ const search = screen.getByPlaceholderText(/tìm kiếm/i);
218
+ await user.clear(search);
219
+ await user.type(search, "quận 3");
220
+ await user.click(await screen.findByRole("button", { name: /quận 3/i }));
221
+
222
+ expect(onChange).toHaveBeenLastCalledWith(
223
+ expect.arrayContaining([
224
+ ["vn", "hcm", "q1"],
225
+ ["vn", "hcm", "q3"],
226
+ ]),
227
+ expect.any(Array),
228
+ );
229
+ });
230
+
231
+ it("reflects controlled single value on trigger", () => {
232
+ renderWithUi(
233
+ <Cascader options={REGION_OPTIONS} value={["jp", "tokyo", "shibuya"]} onChange={vi.fn()} />,
234
+ );
235
+ expect(screen.getByRole("combobox")).toHaveTextContent("日本 / 東京都 / 渋谷区");
236
+ });
237
+ });
238
+
239
+ describe("TreeSelect", () => {
240
+ it("renders tree trigger", () => {
241
+ renderWithUi(<TreeSelect treeData={ORG_TREE} />);
242
+ expect(screen.getByRole("combobox")).toBeInTheDocument();
243
+ expect(screen.getByText("Chọn…")).toBeInTheDocument();
244
+ });
245
+
246
+ it("selects a leaf in single mode and closes panel", async () => {
247
+ const user = userEvent.setup();
248
+ const onChange = vi.fn();
249
+
250
+ renderWithUi(<TreeSelect treeData={ORG_TREE} treeDefaultExpandAll onChange={onChange} />);
251
+ await user.click(screen.getByRole("combobox"));
252
+ await user.click(screen.getByRole("button", { name: /kho osaka/i }));
253
+
254
+ expect(onChange).toHaveBeenCalledWith("warehouse-osaka");
255
+ expect(screen.queryByRole("button", { name: /kho osaka/i })).not.toBeInTheDocument();
256
+ });
257
+
258
+ it("filters visible nodes when showSearch is enabled", async () => {
259
+ const user = userEvent.setup();
260
+
261
+ renderWithUi(<TreeSelect treeData={ORG_TREE} showSearch treeDefaultExpandAll />);
262
+ await user.click(screen.getByRole("combobox"));
263
+ await user.type(screen.getByPlaceholderText(/tìm kiếm/i), "media");
264
+
265
+ expect(screen.getByRole("button", { name: /^media$/i })).toBeInTheDocument();
266
+ expect(screen.queryByRole("button", { name: /kho osaka/i })).not.toBeInTheDocument();
267
+ });
268
+
269
+ it("expands and collapses branch nodes", async () => {
270
+ const user = userEvent.setup();
271
+
272
+ renderWithUi(<TreeSelect treeData={ORG_TREE} />);
273
+ await user.click(screen.getByRole("combobox"));
274
+
275
+ expect(screen.queryByRole("button", { name: /^logistics$/i })).not.toBeInTheDocument();
276
+ await user.click(screen.getAllByRole("button", { name: /mở rộng/i })[0]);
277
+ expect(screen.getByRole("button", { name: /^logistics$/i })).toBeInTheDocument();
278
+ await user.click(screen.getAllByRole("button", { name: /thu gọn/i })[0]);
279
+ expect(screen.queryByRole("button", { name: /^logistics$/i })).not.toBeInTheDocument();
280
+ });
281
+
282
+ it("selects a node in checkable mode", async () => {
283
+ const user = userEvent.setup();
284
+ const onChange = vi.fn();
285
+
286
+ function Demo() {
287
+ const [value, setValue] = React.useState<string[]>([]);
288
+ return (
289
+ <TreeSelect
290
+ treeData={ORG_TREE}
291
+ treeCheckable
292
+ treeDefaultExpandAll
293
+ value={value}
294
+ onChange={(v) => {
295
+ setValue(Array.isArray(v) ? v : v ? [v] : []);
296
+ onChange(v);
297
+ }}
298
+ />
299
+ );
300
+ }
301
+
302
+ renderWithUi(<Demo />);
303
+ await user.click(screen.getByRole("combobox"));
304
+ await user.click(screen.getByRole("checkbox", { name: /kho osaka/i }));
305
+
306
+ expect(onChange).toHaveBeenCalledWith(expect.arrayContaining(["warehouse-osaka"]));
307
+ });
308
+
309
+ it("clears selection via clear icon", async () => {
310
+ const user = userEvent.setup();
311
+ const onChange = vi.fn();
312
+
313
+ renderWithUi(
314
+ <TreeSelect
315
+ treeData={ORG_TREE}
316
+ treeDefaultExpandAll
317
+ defaultValue="warehouse-osaka"
318
+ onChange={onChange}
319
+ />,
320
+ );
321
+
322
+ const combobox = screen.getByRole("combobox");
323
+ const clearIcon = combobox.querySelector("svg.lucide-x");
324
+ expect(clearIcon).toBeTruthy();
325
+ await user.click(clearIcon!);
326
+
327
+ expect(onChange).toHaveBeenCalledWith(undefined);
328
+ expect(combobox).toHaveTextContent("Chọn…");
329
+ });
330
+ });
331
+
332
+ describe("Transfer", () => {
333
+ it("moves items to target", async () => {
334
+ const user = userEvent.setup();
335
+ const onChange = vi.fn();
336
+
337
+ function Demo() {
338
+ const [targetKeys, setTargetKeys] = React.useState<string[]>([]);
339
+ return (
340
+ <Transfer
341
+ dataSource={TRANSFER_MOCK}
342
+ targetKeys={targetKeys}
343
+ onChange={(next, direction, moveKeys) => {
344
+ setTargetKeys(next);
345
+ onChange(next, direction, moveKeys);
346
+ }}
347
+ showSearch
348
+ />
349
+ );
350
+ }
351
+
352
+ renderWithUi(<Demo />);
353
+ await user.click(screen.getByText("NV-001"));
354
+ await user.click(screen.getByRole("button", { name: /chuyển sang đích/i }));
355
+
356
+ expect(onChange).toHaveBeenCalledWith(["user-1"], "right", ["user-1"]);
357
+ });
358
+
359
+ it("filters source list via search", async () => {
360
+ const user = userEvent.setup();
361
+
362
+ renderWithUi(
363
+ <Transfer dataSource={TRANSFER_MOCK} targetKeys={[]} onChange={vi.fn()} showSearch />,
364
+ );
365
+ const searchInputs = screen.getAllByRole("searchbox");
366
+ await user.type(searchInputs[0], "NV-012");
367
+
368
+ await waitFor(() => {
369
+ expect(screen.getByText("NV-012")).toBeInTheDocument();
370
+ expect(screen.queryByText("NV-001")).not.toBeInTheDocument();
371
+ });
372
+ });
373
+
374
+ it("moves items back to source", async () => {
375
+ const user = userEvent.setup();
376
+ const onChange = vi.fn();
377
+
378
+ function Demo() {
379
+ const [targetKeys, setTargetKeys] = React.useState<string[]>(["user-1"]);
380
+ return (
381
+ <Transfer
382
+ dataSource={TRANSFER_MOCK}
383
+ targetKeys={targetKeys}
384
+ onChange={(next, direction, moveKeys) => {
385
+ setTargetKeys(next);
386
+ onChange(next, direction, moveKeys);
387
+ }}
388
+ />
389
+ );
390
+ }
391
+
392
+ renderWithUi(<Demo />);
393
+ await user.click(screen.getByText("NV-001"));
394
+ await user.click(screen.getByRole("button", { name: /chuyển về nguồn/i }));
395
+
396
+ expect(onChange).toHaveBeenCalledWith([], "left", ["user-1"]);
397
+ });
398
+
399
+ it("selects all enabled items in a panel", async () => {
400
+ const user = userEvent.setup();
401
+ const onChange = vi.fn();
402
+
403
+ renderWithUi(
404
+ <Transfer dataSource={TRANSFER_MOCK} targetKeys={[]} onChange={onChange} showSearch />,
405
+ );
406
+
407
+ const selectAll = screen.getAllByRole("checkbox", { name: /select all source/i })[0];
408
+ await user.click(selectAll);
409
+ await user.click(screen.getByRole("button", { name: /chuyển sang đích/i }));
410
+
411
+ expect(onChange).toHaveBeenCalledWith(
412
+ expect.arrayContaining(TRANSFER_MOCK.map((item) => item.key)),
413
+ "right",
414
+ expect.any(Array),
415
+ );
416
+ });
417
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { renderWithUi, screen, userEvent } from "@/test/render";
3
+ import { Checkbox } from "../checkbox";
4
+
5
+ const exportDocs = [
6
+ { label: "Commercial invoice", value: "invoice" },
7
+ { label: "Packing list", value: "packing" },
8
+ { label: "MSDS (pin lithium)", value: "msds" },
9
+ ];
10
+
11
+ describe("Checkbox.Group", () => {
12
+ it("renders group from options with default selection", () => {
13
+ renderWithUi(
14
+ <Checkbox.Group options={exportDocs} defaultValue={["invoice"]} aria-label="Export docs" />,
15
+ );
16
+ expect(screen.getByRole("group")).toBeInTheDocument();
17
+ expect(screen.getByRole("checkbox", { name: /Commercial invoice/ })).toBeChecked();
18
+ expect(screen.getByRole("checkbox", { name: /Packing list/ })).not.toBeChecked();
19
+ });
20
+
21
+ it("toggles values and calls onChange with array", async () => {
22
+ const user = userEvent.setup();
23
+ const onChange = vi.fn();
24
+ renderWithUi(
25
+ <Checkbox.Group options={exportDocs} defaultValue={["invoice"]} onChange={onChange} />,
26
+ );
27
+
28
+ await user.click(screen.getByRole("checkbox", { name: /Packing list/ }));
29
+ expect(onChange).toHaveBeenLastCalledWith(["invoice", "packing"]);
30
+
31
+ await user.click(screen.getByRole("checkbox", { name: /Commercial invoice/ }));
32
+ expect(onChange).toHaveBeenLastCalledWith(["packing"]);
33
+ });
34
+ });
35
+
36
+ describe("Checkbox namespace", () => {
37
+ it("exposes Group on Checkbox.Group", () => {
38
+ expect(Checkbox.Group).toBeDefined();
39
+ });
40
+ });
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderWithUi, screen } from "@/test/render";
3
+ import { Checkbox } from "../checkbox";
4
+
5
+ describe("Checkbox", () => {
6
+ it("renders checkbox role", () => {
7
+ renderWithUi(<Checkbox aria-label="accept" />);
8
+ expect(screen.getByRole("checkbox")).toHaveAttribute("data-slot", "checkbox");
9
+ });
10
+
11
+ it("can be checked", async () => {
12
+ const { userEvent } = await import("@/test/render");
13
+ const user = userEvent.setup();
14
+ renderWithUi(<Checkbox aria-label="accept" />);
15
+ const box = screen.getByRole("checkbox");
16
+ expect(box).not.toBeChecked();
17
+ await user.click(box);
18
+ expect(box).toBeChecked();
19
+ });
20
+ });
@@ -0,0 +1,94 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { renderWithUi, screen, userEvent, waitFor } from "@/test/render";
3
+ import { Calendar } from "../calendar";
4
+ import { DatePicker } from "../date-picker";
5
+ import { DateRangePicker } from "../date-range-picker";
6
+ import { Autocomplete } from "../autocomplete";
7
+ import { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from "../command";
8
+
9
+ describe("Calendar", () => {
10
+ it("renders month grid", () => {
11
+ renderWithUi(<Calendar mode="single" />);
12
+ expect(screen.getByRole("grid")).toBeInTheDocument();
13
+ });
14
+ });
15
+
16
+ describe("DatePicker", () => {
17
+ it("shows placeholder and opens calendar popover", async () => {
18
+ const user = userEvent.setup();
19
+ renderWithUi(<DatePicker placeholder="Chọn ngày ETD" />);
20
+ const trigger = screen.getByRole("button", { name: /chọn ngày etd/i });
21
+ expect(trigger).toBeInTheDocument();
22
+ await user.click(trigger);
23
+ await waitFor(() => {
24
+ expect(screen.getByRole("grid")).toBeInTheDocument();
25
+ });
26
+ });
27
+
28
+ it("calls onChange when a day is selected", async () => {
29
+ const user = userEvent.setup();
30
+ const onChange = vi.fn();
31
+ renderWithUi(<DatePicker value={new Date(2026, 4, 1)} onChange={onChange} />);
32
+ await user.click(screen.getByRole("button"));
33
+ const dayButtons = screen.getAllByRole("gridcell").filter((cell) => cell.textContent === "15");
34
+ const dayButton = dayButtons[0]?.querySelector("button");
35
+ expect(dayButton).toBeTruthy();
36
+ if (dayButton) await user.click(dayButton);
37
+ expect(onChange).toHaveBeenCalled();
38
+ });
39
+ });
40
+
41
+ describe("DateRangePicker", () => {
42
+ it("renders trigger with formatted range", () => {
43
+ renderWithUi(
44
+ <DateRangePicker
45
+ value={{ from: new Date(2026, 4, 1), to: new Date(2026, 4, 10) }}
46
+ onChange={() => undefined}
47
+ />,
48
+ );
49
+ expect(screen.getByRole("button")).toHaveTextContent("01/05/2026");
50
+ expect(screen.getByRole("button")).toHaveTextContent("10/05/2026");
51
+ });
52
+ });
53
+
54
+ describe("Autocomplete", () => {
55
+ const options = [
56
+ { value: "osaka", label: "Osaka Hub" },
57
+ { value: "tokyo", label: "Tokyo Hub" },
58
+ ];
59
+
60
+ it("opens list and selects option", async () => {
61
+ const user = userEvent.setup();
62
+ const onValueChange = vi.fn();
63
+ renderWithUi(
64
+ <Autocomplete
65
+ options={options}
66
+ value="osaka"
67
+ onValueChange={onValueChange}
68
+ placeholder="Chọn hub"
69
+ />,
70
+ );
71
+ await user.click(screen.getByRole("combobox"));
72
+ await user.click(screen.getByRole("option", { name: "Tokyo Hub" }));
73
+ expect(onValueChange).toHaveBeenCalledWith("tokyo");
74
+ });
75
+ });
76
+
77
+ describe("Command", () => {
78
+ it("filters items by search input", async () => {
79
+ const user = userEvent.setup();
80
+ renderWithUi(
81
+ <Command>
82
+ <CommandInput placeholder="Tìm HAWB" />
83
+ <CommandList>
84
+ <CommandEmpty>Không có kết quả</CommandEmpty>
85
+ <CommandItem value="GX-001">GX-001</CommandItem>
86
+ <CommandItem value="GX-002">GX-002</CommandItem>
87
+ </CommandList>
88
+ </Command>,
89
+ );
90
+ await user.type(screen.getByPlaceholderText("Tìm HAWB"), "002");
91
+ expect(screen.getByText("GX-002")).toBeInTheDocument();
92
+ expect(screen.queryByText("GX-001")).not.toBeInTheDocument();
93
+ });
94
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderWithUi, screen } from "@/test/render";
3
+ import { FormField } from "../form-field";
4
+ import { Input } from "../input";
5
+
6
+ describe("FormField", () => {
7
+ it("links label to input via htmlFor/id", () => {
8
+ renderWithUi(
9
+ <FormField id="email" label="Email">
10
+ <Input id="email" />
11
+ </FormField>,
12
+ );
13
+ expect(screen.getByLabelText("Email")).toHaveAttribute("id", "email");
14
+ });
15
+
16
+ it("shows required asterisk when required", () => {
17
+ renderWithUi(
18
+ <FormField id="name" label="Name" required>
19
+ <Input id="name" />
20
+ </FormField>,
21
+ );
22
+ expect(screen.getByText("*")).toHaveAttribute("aria-hidden", "true");
23
+ expect(screen.getByRole("textbox")).toHaveAttribute("aria-required", "true");
24
+ });
25
+
26
+ it("shows helper text and wires aria-describedby", () => {
27
+ renderWithUi(
28
+ <FormField id="x" label="Field" helper="Hint text">
29
+ <Input id="x" />
30
+ </FormField>,
31
+ );
32
+ const input = screen.getByRole("textbox");
33
+ expect(screen.getByText("Hint text")).toHaveAttribute("id", "x-helper");
34
+ expect(input).toHaveAttribute("aria-describedby", "x-helper");
35
+ });
36
+
37
+ it("shows error instead of helper and sets aria-invalid", () => {
38
+ renderWithUi(
39
+ <FormField id="x" label="Field" helper="Hint" error="Required">
40
+ <Input id="x" />
41
+ </FormField>,
42
+ );
43
+ const input = screen.getByRole("textbox");
44
+ expect(screen.getByRole("alert")).toHaveTextContent("Required");
45
+ expect(screen.queryByText("Hint")).not.toBeInTheDocument();
46
+ expect(input).toHaveAttribute("aria-invalid", "true");
47
+ expect(input).toHaveAttribute("aria-describedby", "x-error");
48
+ });
49
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { renderWithUi, screen, userEvent } from "@/test/render";
3
+ import { Input } from "../input";
4
+ import { Textarea } from "../textarea";
5
+
6
+ describe("Input", () => {
7
+ it("accepts typed value", async () => {
8
+ const user = userEvent.setup();
9
+ const onChange = vi.fn();
10
+ renderWithUi(<Input aria-label="test" onChange={onChange} />);
11
+ await user.type(screen.getByRole("textbox"), "abc");
12
+ expect(onChange).toHaveBeenCalled();
13
+ });
14
+
15
+ it("applies ui-control class", () => {
16
+ renderWithUi(<Input aria-label="test" />);
17
+ expect(screen.getByRole("textbox")).toHaveClass("ui-control");
18
+ });
19
+
20
+ it("exposes shadcn data-slot and invalid state classes", () => {
21
+ renderWithUi(<Input aria-label="test" aria-invalid />);
22
+ const input = screen.getByRole("textbox");
23
+ expect(input).toHaveAttribute("data-slot", "input");
24
+ expect(input).toHaveClass("aria-invalid:border-destructive");
25
+ });
26
+ });
27
+
28
+ describe("Textarea", () => {
29
+ it("renders multiline control", () => {
30
+ renderWithUi(<Textarea aria-label="notes" />);
31
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
32
+ });
33
+
34
+ it("applies ui-control-multiline class", () => {
35
+ renderWithUi(<Textarea aria-label="notes" />);
36
+ expect(screen.getByRole("textbox")).toHaveClass("ui-control-multiline");
37
+ });
38
+ });