@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,127 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { renderWithUi, screen, userEvent } from "@/test/render";
3
+ import { Button } from "../../general/button";
4
+ import { Alert, AlertActions, AlertContent, AlertDescription, AlertTitle } from "../alert";
5
+
6
+ describe("Alert", () => {
7
+ it("renders with role=alert and variant styling", () => {
8
+ renderWithUi(
9
+ <Alert variant="warning">
10
+ <AlertContent>
11
+ <AlertTitle>Pin lithium</AlertTitle>
12
+ <AlertDescription>MSDS bắt buộc trước khi xuất kho.</AlertDescription>
13
+ </AlertContent>
14
+ </Alert>,
15
+ );
16
+ const alert = screen.getByRole("alert");
17
+ expect(alert).toHaveAttribute("data-variant", "warning");
18
+ expect(screen.getByText("Pin lithium")).toBeInTheDocument();
19
+ expect(screen.getByText(/MSDS/)).toBeInTheDocument();
20
+ });
21
+
22
+ it("renders destructive variant for API errors", () => {
23
+ renderWithUi(
24
+ <Alert variant="destructive">
25
+ <AlertContent>
26
+ <AlertTitle>Không tải được</AlertTitle>
27
+ <AlertDescription>503 Service Unavailable</AlertDescription>
28
+ </AlertContent>
29
+ </Alert>,
30
+ );
31
+ expect(screen.getByRole("alert")).toHaveAttribute("data-variant", "destructive");
32
+ expect(screen.getByText("Không tải được")).toHaveAttribute("data-slot", "alert-title");
33
+ });
34
+
35
+ it("renders actions slot", async () => {
36
+ const user = userEvent.setup();
37
+ const onAction = vi.fn();
38
+ renderWithUi(
39
+ <Alert variant="default">
40
+ <AlertContent>
41
+ <AlertTitle>Bảo trì</AlertTitle>
42
+ <AlertDescription>Pub/Sub emulator restart lúc 02:00 JST.</AlertDescription>
43
+ </AlertContent>
44
+ <AlertActions>
45
+ <Button size="sm" onClick={onAction}>
46
+ Xem lịch
47
+ </Button>
48
+ </AlertActions>
49
+ </Alert>,
50
+ );
51
+ await user.click(screen.getByRole("button", { name: "Xem lịch" }));
52
+ expect(onAction).toHaveBeenCalledOnce();
53
+ });
54
+
55
+ it("calls onDismiss when dismiss control clicked", async () => {
56
+ const user = userEvent.setup();
57
+ const onDismiss = vi.fn();
58
+ renderWithUi(
59
+ <Alert variant="success" onDismiss={onDismiss}>
60
+ <AlertContent>
61
+ <AlertTitle>Đã lưu template Zalo</AlertTitle>
62
+ <AlertDescription>Thay đổi có hiệu lực ngay.</AlertDescription>
63
+ </AlertContent>
64
+ </Alert>,
65
+ );
66
+ await user.click(screen.getByRole("button", { name: "Dismiss" }));
67
+ expect(onDismiss).toHaveBeenCalledOnce();
68
+ });
69
+
70
+ it("hides icon when icon={false}", () => {
71
+ const { container } = renderWithUi(
72
+ <Alert variant="warning" icon={false}>
73
+ <AlertContent>
74
+ <AlertTitle>No icon</AlertTitle>
75
+ </AlertContent>
76
+ </Alert>,
77
+ );
78
+ expect(container.querySelector("svg")).toBeNull();
79
+ });
80
+
81
+ it("renders success variant styling", () => {
82
+ renderWithUi(
83
+ <Alert variant="success">
84
+ <AlertContent>
85
+ <AlertTitle>Template saved</AlertTitle>
86
+ <AlertDescription>Zalo OA preview ready.</AlertDescription>
87
+ </AlertContent>
88
+ </Alert>,
89
+ );
90
+ expect(screen.getByRole("alert")).toHaveAttribute("data-variant", "success");
91
+ });
92
+
93
+ it("renders default variant without destructive classes", () => {
94
+ renderWithUi(
95
+ <Alert variant="default">
96
+ <AlertContent>
97
+ <AlertTitle>Maintenance</AlertTitle>
98
+ </AlertContent>
99
+ </Alert>,
100
+ );
101
+ const alert = screen.getByRole("alert");
102
+ expect(alert).toHaveAttribute("data-variant", "default");
103
+ });
104
+ });
105
+
106
+ describe("Alert.QueryError", () => {
107
+ it("shows message and retry", async () => {
108
+ const user = userEvent.setup();
109
+ const onRetry = vi.fn();
110
+ renderWithUi(<Alert.QueryError error={new Error("boom")} onRetry={onRetry} />);
111
+ expect(screen.getByRole("alert")).toBeInTheDocument();
112
+ expect(screen.getByText(/boom/)).toBeInTheDocument();
113
+ await user.click(screen.getByRole("button", { name: /thử lại/i }));
114
+ expect(onRetry).toHaveBeenCalledOnce();
115
+ });
116
+
117
+ it("omits retry button when onRetry is omitted", () => {
118
+ renderWithUi(<Alert.QueryError error="Validation failed: declared_value must be > 0" />);
119
+ expect(screen.getByRole("alert")).toBeInTheDocument();
120
+ expect(screen.queryByRole("button", { name: /thử lại/i })).not.toBeInTheDocument();
121
+ });
122
+
123
+ it("uses destructive Alert styling", () => {
124
+ renderWithUi(<Alert.QueryError error={new Error("503")} onRetry={() => undefined} />);
125
+ expect(screen.getByRole("alert")).toHaveAttribute("data-variant", "destructive");
126
+ });
127
+ });
@@ -0,0 +1,290 @@
1
+ import * as React from "react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { renderWithUi, screen, userEvent, waitFor } from "@/test/render";
4
+ import {
5
+ Dialog,
6
+ DialogAction,
7
+ DialogCancel,
8
+ DialogConfirm,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogFooter,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ DialogTrigger,
15
+ } from "../dialog";
16
+ import { Button } from "../../general/button";
17
+
18
+ function PhraseResetHarness() {
19
+ const [open, setOpen] = React.useState(true);
20
+ return (
21
+ <>
22
+ <button type="button" onClick={() => setOpen(true)}>
23
+ Reopen
24
+ </button>
25
+ <DialogConfirm
26
+ open={open}
27
+ onOpenChange={setOpen}
28
+ title="Purge?"
29
+ confirmPhrase="DELETE"
30
+ confirmLabel="Purge"
31
+ onConfirm={() => undefined}
32
+ />
33
+ </>
34
+ );
35
+ }
36
+
37
+ describe("Dialog form mode", () => {
38
+ it("opens on trigger click and closes via escape", async () => {
39
+ const user = userEvent.setup();
40
+ renderWithUi(
41
+ <Dialog>
42
+ <DialogTrigger asChild>
43
+ <Button type="button">Mở</Button>
44
+ </DialogTrigger>
45
+ <DialogContent>
46
+ <DialogHeader>
47
+ <DialogTitle>Xác nhận manifest</DialogTitle>
48
+ <DialogDescription>Chốt 12 kiện Osaka?</DialogDescription>
49
+ </DialogHeader>
50
+ <DialogFooter>
51
+ <Button type="button" variant="ghost">
52
+ Hủy
53
+ </Button>
54
+ </DialogFooter>
55
+ </DialogContent>
56
+ </Dialog>,
57
+ );
58
+
59
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
60
+ await user.click(screen.getByRole("button", { name: "Mở" }));
61
+ expect(screen.getByRole("dialog")).toHaveAttribute("data-slot", "dialog-content");
62
+ expect(screen.getByText("Chốt 12 kiện Osaka?")).toBeInTheDocument();
63
+ await user.keyboard("{Escape}");
64
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
65
+ });
66
+
67
+ it("calls onOpenChange when controlled", async () => {
68
+ const user = userEvent.setup();
69
+ const onOpenChange = vi.fn();
70
+ renderWithUi(
71
+ <Dialog open onOpenChange={onOpenChange}>
72
+ <DialogContent>
73
+ <DialogTitle>Controlled</DialogTitle>
74
+ </DialogContent>
75
+ </Dialog>,
76
+ );
77
+ await user.keyboard("{Escape}");
78
+ expect(onOpenChange).toHaveBeenCalledWith(false);
79
+ });
80
+ });
81
+
82
+ describe("Dialog confirm mode (compound)", () => {
83
+ it("uses alertdialog role and has no close icon", () => {
84
+ renderWithUi(
85
+ <Dialog mode="confirm" open onOpenChange={() => undefined}>
86
+ <DialogContent>
87
+ <DialogHeader>
88
+ <DialogTitle>Proceed?</DialogTitle>
89
+ <DialogDescription>This cannot be undone.</DialogDescription>
90
+ </DialogHeader>
91
+ </DialogContent>
92
+ </Dialog>,
93
+ );
94
+ expect(screen.getByRole("alertdialog")).toHaveAttribute("data-slot", "dialog-content");
95
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
96
+ expect(screen.queryByRole("button", { name: /close/i })).not.toBeInTheDocument();
97
+ });
98
+
99
+ it("calls onOpenChange(false) when cancel clicked", async () => {
100
+ const user = userEvent.setup();
101
+ const onOpenChange = vi.fn();
102
+ renderWithUi(
103
+ <Dialog mode="confirm" open onOpenChange={onOpenChange}>
104
+ <DialogContent>
105
+ <DialogHeader>
106
+ <DialogTitle>Proceed?</DialogTitle>
107
+ </DialogHeader>
108
+ <DialogFooter>
109
+ <DialogCancel asChild>
110
+ <Button variant="ghost">Cancel</Button>
111
+ </DialogCancel>
112
+ </DialogFooter>
113
+ </DialogContent>
114
+ </Dialog>,
115
+ );
116
+ await user.click(screen.getByRole("button", { name: "Cancel" }));
117
+ expect(onOpenChange).toHaveBeenCalledWith(false);
118
+ });
119
+
120
+ it("renders DialogAction", () => {
121
+ renderWithUi(
122
+ <Dialog mode="confirm" open onOpenChange={() => undefined}>
123
+ <DialogContent>
124
+ <DialogHeader>
125
+ <DialogTitle>Seal manifest</DialogTitle>
126
+ </DialogHeader>
127
+ <DialogFooter>
128
+ <DialogAction asChild>
129
+ <Button>Seal</Button>
130
+ </DialogAction>
131
+ </DialogFooter>
132
+ </DialogContent>
133
+ </Dialog>,
134
+ );
135
+ expect(screen.getByRole("button", { name: "Seal" })).toBeInTheDocument();
136
+ });
137
+ });
138
+
139
+ describe("Dialog.Confirm preset", () => {
140
+ it("renders alertdialog with title and description", () => {
141
+ renderWithUi(
142
+ <DialogConfirm
143
+ open
144
+ onOpenChange={() => undefined}
145
+ title="Chốt manifest?"
146
+ description="47 kiện sẽ bị khóa."
147
+ onConfirm={() => undefined}
148
+ />,
149
+ );
150
+
151
+ expect(screen.getByRole("alertdialog")).toBeInTheDocument();
152
+ expect(screen.getByText("Chốt manifest?")).toBeInTheDocument();
153
+ expect(screen.getByText("47 kiện sẽ bị khóa.")).toBeInTheDocument();
154
+ });
155
+
156
+ it("calls onConfirm and closes when confirmed", async () => {
157
+ const user = userEvent.setup();
158
+ const onOpenChange = vi.fn();
159
+ const onConfirm = vi.fn();
160
+ renderWithUi(
161
+ <DialogConfirm
162
+ open
163
+ onOpenChange={onOpenChange}
164
+ title="Confirm?"
165
+ description="Sure?"
166
+ onConfirm={onConfirm}
167
+ />,
168
+ );
169
+
170
+ await user.click(screen.getByRole("button", { name: "Tiếp tục" }));
171
+ await waitFor(() => {
172
+ expect(onConfirm).toHaveBeenCalledOnce();
173
+ });
174
+ expect(onOpenChange).toHaveBeenCalledWith(false);
175
+ });
176
+
177
+ it("closes on cancel without calling onConfirm", async () => {
178
+ const user = userEvent.setup();
179
+ const onOpenChange = vi.fn();
180
+ const onConfirm = vi.fn();
181
+ renderWithUi(
182
+ <DialogConfirm open onOpenChange={onOpenChange} title="Confirm?" onConfirm={onConfirm} />,
183
+ );
184
+
185
+ await user.click(screen.getByRole("button", { name: "Hủy" }));
186
+ expect(onOpenChange).toHaveBeenCalledWith(false);
187
+ expect(onConfirm).not.toHaveBeenCalled();
188
+ });
189
+
190
+ it("disables both buttons when pending", () => {
191
+ renderWithUi(
192
+ <DialogConfirm
193
+ open
194
+ onOpenChange={() => undefined}
195
+ title="Wait"
196
+ pending
197
+ onConfirm={() => undefined}
198
+ />,
199
+ );
200
+ expect(screen.getByRole("button", { name: "Đang xử lý…" })).toBeDisabled();
201
+ expect(screen.getByRole("button", { name: "Hủy" })).toBeDisabled();
202
+ });
203
+
204
+ it("keeps dialog open when keepOpenOnConfirm is true", async () => {
205
+ const user = userEvent.setup();
206
+ const onOpenChange = vi.fn();
207
+ renderWithUi(
208
+ <DialogConfirm
209
+ open
210
+ onOpenChange={onOpenChange}
211
+ title="Next step"
212
+ keepOpenOnConfirm
213
+ onConfirm={() => undefined}
214
+ />,
215
+ );
216
+
217
+ await user.click(screen.getByRole("button", { name: "Tiếp tục" }));
218
+ expect(onOpenChange).not.toHaveBeenCalledWith(false);
219
+ });
220
+
221
+ describe("confirmPhrase", () => {
222
+ it("shows type-to-confirm field and defaults confirm label to delete", () => {
223
+ renderWithUi(
224
+ <DialogConfirm
225
+ open
226
+ onOpenChange={() => undefined}
227
+ title="Purge?"
228
+ confirmPhrase="DELETE"
229
+ onConfirm={() => undefined}
230
+ />,
231
+ );
232
+
233
+ expect(screen.getByPlaceholderText("DELETE")).toBeInTheDocument();
234
+ expect(screen.getByRole("button", { name: "Xóa" })).toBeDisabled();
235
+ });
236
+
237
+ it("enables confirm only when phrase matches exactly", async () => {
238
+ const user = userEvent.setup();
239
+ renderWithUi(
240
+ <DialogConfirm
241
+ open
242
+ onOpenChange={() => undefined}
243
+ title="Purge?"
244
+ confirmPhrase="DELETE"
245
+ confirmLabel="Purge"
246
+ onConfirm={() => undefined}
247
+ />,
248
+ );
249
+
250
+ const confirmBtn = screen.getByRole("button", { name: "Purge" });
251
+ const input = screen.getByPlaceholderText("DELETE");
252
+ expect(confirmBtn).toBeDisabled();
253
+
254
+ await user.type(input, "DELET");
255
+ expect(confirmBtn).toBeDisabled();
256
+
257
+ await user.type(input, "E");
258
+ expect(confirmBtn).toBeEnabled();
259
+ });
260
+
261
+ it("clears typed phrase when dialog closes and reopens", async () => {
262
+ const user = userEvent.setup();
263
+ renderWithUi(<PhraseResetHarness />);
264
+
265
+ await user.type(screen.getByPlaceholderText("DELETE"), "DELETE");
266
+ await user.click(screen.getByRole("button", { name: "Hủy" }));
267
+
268
+ await user.click(screen.getByRole("button", { name: "Reopen" }));
269
+ expect(screen.getByPlaceholderText("DELETE")).toHaveValue("");
270
+ expect(screen.getByRole("button", { name: "Purge" })).toBeDisabled();
271
+ });
272
+ });
273
+ });
274
+
275
+ describe("Dialog namespace", () => {
276
+ it("exposes Confirm preset on Dialog.Confirm", () => {
277
+ expect(Dialog.Confirm).toBe(DialogConfirm);
278
+ });
279
+
280
+ it("supports shadcn showCloseButton naming", () => {
281
+ renderWithUi(
282
+ <Dialog open onOpenChange={() => undefined}>
283
+ <DialogContent showCloseButton={false}>
284
+ <DialogTitle>No close</DialogTitle>
285
+ </DialogContent>
286
+ </Dialog>,
287
+ );
288
+ expect(screen.queryByRole("button", { name: /close/i })).not.toBeInTheDocument();
289
+ });
290
+ });
@@ -0,0 +1,94 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderWithUi, screen, userEvent } from "@/test/render";
3
+ import { Button } from "../../general/button";
4
+ import {
5
+ Sheet,
6
+ SheetContent,
7
+ SheetDescription,
8
+ SheetFooter,
9
+ SheetHeader,
10
+ SheetTitle,
11
+ SheetTrigger,
12
+ } from "../sheet";
13
+
14
+ describe("Sheet", () => {
15
+ it("opens side panel from trigger", async () => {
16
+ const user = userEvent.setup();
17
+ renderWithUi(
18
+ <Sheet>
19
+ <SheetTrigger asChild>
20
+ <Button type="button">Bộ lọc</Button>
21
+ </SheetTrigger>
22
+ <SheetContent>
23
+ <SheetHeader>
24
+ <SheetTitle>Lọc khách CRM</SheetTitle>
25
+ </SheetHeader>
26
+ </SheetContent>
27
+ </Sheet>,
28
+ );
29
+ await user.click(screen.getByRole("button", { name: "Bộ lọc" }));
30
+ expect(screen.getByRole("dialog", { name: "Lọc khách CRM" })).toHaveAttribute(
31
+ "data-slot",
32
+ "sheet-content",
33
+ );
34
+ });
35
+
36
+ it("renders description inside panel", async () => {
37
+ const user = userEvent.setup();
38
+ renderWithUi(
39
+ <Sheet>
40
+ <SheetTrigger asChild>
41
+ <Button type="button">Chi tiết</Button>
42
+ </SheetTrigger>
43
+ <SheetContent>
44
+ <SheetHeader>
45
+ <SheetTitle>Đơn hàng #1024</SheetTitle>
46
+ <SheetDescription>Xem thông tin vận chuyển</SheetDescription>
47
+ </SheetHeader>
48
+ </SheetContent>
49
+ </Sheet>,
50
+ );
51
+ await user.click(screen.getByRole("button", { name: "Chi tiết" }));
52
+ expect(screen.getByText("Xem thông tin vận chuyển")).toBeInTheDocument();
53
+ });
54
+
55
+ it("closes when pressing Escape", async () => {
56
+ const user = userEvent.setup();
57
+ renderWithUi(
58
+ <Sheet>
59
+ <SheetTrigger asChild>
60
+ <Button type="button">Bộ lọc</Button>
61
+ </SheetTrigger>
62
+ <SheetContent>
63
+ <SheetHeader>
64
+ <SheetTitle>Lọc khách CRM</SheetTitle>
65
+ </SheetHeader>
66
+ </SheetContent>
67
+ </Sheet>,
68
+ );
69
+ await user.click(screen.getByRole("button", { name: "Bộ lọc" }));
70
+ expect(screen.getByRole("dialog", { name: "Lọc khách CRM" })).toBeInTheDocument();
71
+ await user.keyboard("{Escape}");
72
+ expect(screen.queryByRole("dialog", { name: "Lọc khách CRM" })).not.toBeInTheDocument();
73
+ });
74
+
75
+ it("exposes footer slot and optional close button", async () => {
76
+ const user = userEvent.setup();
77
+ renderWithUi(
78
+ <Sheet>
79
+ <SheetTrigger asChild>
80
+ <Button type="button">Footer</Button>
81
+ </SheetTrigger>
82
+ <SheetContent showCloseButton={false}>
83
+ <SheetHeader>
84
+ <SheetTitle>Panel</SheetTitle>
85
+ </SheetHeader>
86
+ <SheetFooter>Actions</SheetFooter>
87
+ </SheetContent>
88
+ </Sheet>,
89
+ );
90
+ await user.click(screen.getByRole("button", { name: "Footer" }));
91
+ expect(screen.getByText("Actions")).toHaveAttribute("data-slot", "sheet-footer");
92
+ expect(screen.queryByRole("button", { name: /close/i })).not.toBeInTheDocument();
93
+ });
94
+ });
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderWithUi } from "@/test/render";
3
+ import { SkeletonCard, SkeletonDetail, SkeletonRows, SkeletonTable } from "../skeleton";
4
+
5
+ describe("Skeleton", () => {
6
+ it("SkeletonRows marks busy state", () => {
7
+ const { container } = renderWithUi(<SkeletonRows rows={2} columns={3} />);
8
+ expect(container.querySelector("[aria-busy='true']")).toBeInTheDocument();
9
+ });
10
+
11
+ it("SkeletonTable renders header and body placeholders", () => {
12
+ const { container } = renderWithUi(<SkeletonTable rows={3} columns={4} />);
13
+ expect(container.querySelector(".ui-skeleton-table")).toBeInTheDocument();
14
+ });
15
+
16
+ it("SkeletonDetail renders title blocks", () => {
17
+ const { container } = renderWithUi(<SkeletonDetail />);
18
+ expect(container.querySelectorAll(".ui-skeleton-block").length).toBeGreaterThan(0);
19
+ });
20
+
21
+ it("SkeletonCard renders stat tile shape", () => {
22
+ const { container } = renderWithUi(<SkeletonCard />);
23
+ expect(container.querySelector(".ui-skeleton-card")).toBeInTheDocument();
24
+ });
25
+ });
@@ -0,0 +1,52 @@
1
+ import * as React from "react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { renderWithUi, screen } from "@/test/render";
4
+ import { Toaster } from "../sonner";
5
+ import { toast, useToast } from "../use-toast";
6
+
7
+ const sonnerMock = vi.hoisted(() => {
8
+ const fn = vi.fn();
9
+ return Object.assign(fn, {
10
+ success: vi.fn(),
11
+ error: vi.fn(),
12
+ dismiss: vi.fn(),
13
+ });
14
+ });
15
+
16
+ vi.mock("sonner", () => ({
17
+ toast: sonnerMock,
18
+ Toaster: ({ className, ...props }: { className?: string; children?: React.ReactNode }) => (
19
+ <div data-testid="sonner-mock" className={className} {...props} />
20
+ ),
21
+ }));
22
+
23
+ describe("Toaster", () => {
24
+ it("renders Sonner container", () => {
25
+ renderWithUi(<Toaster />);
26
+ expect(screen.getByTestId("sonner-mock")).toBeInTheDocument();
27
+ });
28
+ });
29
+
30
+ describe("toast adapter", () => {
31
+ beforeEach(() => {
32
+ sonnerMock.mockClear();
33
+ sonnerMock.success.mockClear();
34
+ sonnerMock.error.mockClear();
35
+ });
36
+
37
+ it("maps legacy success variant to sonner.success", () => {
38
+ toast({ title: "Đã lưu manifest", variant: "success" });
39
+ expect(sonnerMock.success).toHaveBeenCalledWith("Đã lưu manifest", expect.any(Object));
40
+ });
41
+
42
+ it("maps legacy destructive variant to sonner.error", () => {
43
+ toast({ title: "Lỗi xuất kho", variant: "destructive" });
44
+ expect(sonnerMock.error).toHaveBeenCalledWith("Lỗi xuất kho", expect.any(Object));
45
+ });
46
+
47
+ it("useToast exposes legacy toast helper", () => {
48
+ const { toast: legacy } = useToast();
49
+ legacy({ title: "Ping", variant: "default" });
50
+ expect(sonnerMock).toHaveBeenCalledWith("Ping", expect.any(Object));
51
+ });
52
+ });