@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.
- package/README.md +101 -142
- package/package.json +124 -128
- package/scripts/ui-audit.mjs +179 -0
- package/src/app/__tests__/app-provider.test.tsx +232 -0
- package/src/app/__tests__/date-format-labels.test.ts +36 -0
- package/src/app/__tests__/date-formats.test.ts +44 -0
- package/src/app/__tests__/timezones.test.ts +65 -0
- package/src/app/app-provider.tsx +227 -0
- package/src/app/date-format-labels.ts +21 -0
- package/src/app/date-formats.ts +30 -0
- package/src/app/index.ts +40 -0
- package/src/app/locales.ts +32 -0
- package/src/app/request-headers.ts +31 -0
- package/src/app/storage.ts +44 -0
- package/src/app/time-format-labels.ts +19 -0
- package/src/app/time-formats.ts +15 -0
- package/src/app/timezones.ts +208 -0
- package/src/app/types.ts +39 -0
- package/src/app/use-formatting.ts +47 -0
- package/src/components/__tests__/accessibility-primitives.test.tsx +65 -0
- package/src/components/__tests__/docs-parity.test.ts +41 -0
- package/src/components/__tests__/shadcn-release-guardrails.test.ts +71 -0
- package/src/components/__tests__/theme-axes-integration.test.tsx +242 -0
- package/src/components/admin/index.ts +76 -0
- package/src/components/data-display/__tests__/card-table.test.tsx +328 -0
- package/src/components/data-display/__tests__/data-display.test.tsx +73 -0
- package/src/components/data-display/__tests__/data-table.test.tsx +84 -0
- package/src/components/data-display/__tests__/popover.test.tsx +92 -0
- package/src/components/data-display/__tests__/scroll-area-collapsible.test.tsx +66 -0
- package/src/components/data-display/badge.tsx +27 -0
- package/src/components/data-display/card.tsx +194 -0
- package/src/components/data-display/code-badge.tsx +28 -0
- package/src/components/data-display/collapsible.tsx +5 -0
- package/src/components/data-display/data-table.tsx +476 -0
- package/src/components/data-display/empty-state.tsx +22 -0
- package/src/components/data-display/index.ts +41 -0
- package/src/components/data-display/key-value-grid.tsx +46 -0
- package/src/components/data-display/popover.tsx +62 -0
- package/src/components/data-display/progress-meter.tsx +20 -0
- package/src/components/data-display/scan-panel.tsx +16 -0
- package/src/components/data-display/scroll-area.tsx +42 -0
- package/src/components/data-display/status-badge.tsx +83 -0
- package/src/components/data-display/table.tsx +59 -0
- package/src/components/data-display/timeline.tsx +42 -0
- package/src/components/data-display/tree-list.tsx +42 -0
- package/src/components/data-entry/__fixtures__/tree-options.ts +80 -0
- package/src/components/data-entry/__tests__/cascader-tree-transfer.test.tsx +417 -0
- package/src/components/data-entry/__tests__/checkbox-group.test.tsx +40 -0
- package/src/components/data-entry/__tests__/checkbox.test.tsx +20 -0
- package/src/components/data-entry/__tests__/date-autocomplete.test.tsx +94 -0
- package/src/components/data-entry/__tests__/form-field.test.tsx +49 -0
- package/src/components/data-entry/__tests__/input-textarea.test.tsx +38 -0
- package/src/components/data-entry/__tests__/label-select.test.tsx +62 -0
- package/src/components/data-entry/__tests__/pickers.test.tsx +74 -0
- package/src/components/data-entry/__tests__/radio.test.tsx +46 -0
- package/src/components/data-entry/__tests__/search-input.test.tsx +32 -0
- package/src/components/data-entry/__tests__/switch-field.test.tsx +52 -0
- package/src/components/data-entry/__tests__/upload.test.tsx +125 -0
- package/src/components/data-entry/autocomplete.tsx +91 -0
- package/src/components/data-entry/calendar.tsx +90 -0
- package/src/components/data-entry/cascader.tsx +305 -0
- package/src/components/data-entry/checkbox-group.tsx +90 -0
- package/src/components/data-entry/checkbox.tsx +30 -0
- package/src/components/data-entry/choice-field.tsx +27 -0
- package/src/components/data-entry/choice-option.ts +20 -0
- package/src/components/data-entry/color-picker.tsx +75 -0
- package/src/components/data-entry/command.tsx +56 -0
- package/src/components/data-entry/country-select.tsx +88 -0
- package/src/components/data-entry/date-picker.tsx +69 -0
- package/src/components/data-entry/date-range-picker.tsx +75 -0
- package/src/components/data-entry/form-field.tsx +59 -0
- package/src/components/data-entry/index.ts +62 -0
- package/src/components/data-entry/input.tsx +26 -0
- package/src/components/data-entry/label.tsx +25 -0
- package/src/components/data-entry/radio.tsx +109 -0
- package/src/components/data-entry/search-input.tsx +103 -0
- package/src/components/data-entry/select.tsx +149 -0
- package/src/components/data-entry/slider.tsx +38 -0
- package/src/components/data-entry/switch-field.tsx +91 -0
- package/src/components/data-entry/switch.tsx +24 -0
- package/src/components/data-entry/textarea.tsx +12 -0
- package/src/components/data-entry/time-picker.tsx +214 -0
- package/src/components/data-entry/transfer.tsx +231 -0
- package/src/components/data-entry/tree-select-strategy.ts +6 -0
- package/src/components/data-entry/tree-select.tsx +279 -0
- package/src/components/data-entry/tree-utils.ts +221 -0
- package/src/components/data-entry/upload-crop-dialog.tsx +109 -0
- package/src/components/data-entry/upload-types.ts +86 -0
- package/src/components/data-entry/upload.tsx +498 -0
- package/src/components/data-entry/use-upload-draft.ts +93 -0
- package/src/components/feedback/__tests__/alert.test.tsx +127 -0
- package/src/components/feedback/__tests__/dialog.test.tsx +290 -0
- package/src/components/feedback/__tests__/sheet.test.tsx +94 -0
- package/src/components/feedback/__tests__/skeleton.test.tsx +25 -0
- package/src/components/feedback/__tests__/toast.test.tsx +52 -0
- package/src/components/feedback/alert.tsx +167 -0
- package/src/components/feedback/dialog.tsx +325 -0
- package/src/components/feedback/index.ts +53 -0
- package/src/components/feedback/sheet.tsx +130 -0
- package/src/components/feedback/skeleton.tsx +95 -0
- package/src/components/feedback/sonner.tsx +54 -0
- package/src/components/feedback/toaster.tsx +1 -0
- package/src/components/feedback/use-toast.ts +62 -0
- package/src/components/general/__tests__/button.test.tsx +71 -0
- package/src/components/general/button.tsx +61 -0
- package/src/components/general/index.ts +2 -0
- package/src/components/layout/__tests__/page-container.test.tsx +69 -0
- package/src/components/layout/__tests__/page-inset.test.tsx +14 -0
- package/src/components/layout/__tests__/stack-inline.test.tsx +39 -0
- package/src/components/layout/app-shell.tsx +42 -0
- package/src/components/layout/breadcrumb.tsx +35 -0
- package/src/components/layout/index.ts +31 -0
- package/src/components/layout/inline.tsx +13 -0
- package/src/components/layout/menu.tsx +34 -0
- package/src/components/layout/mobile-frame.tsx +57 -0
- package/src/components/layout/page-container.tsx +81 -0
- package/src/components/layout/page-inset.tsx +16 -0
- package/src/components/layout/responsive-grid.tsx +14 -0
- package/src/components/layout/shell-app.tsx +30 -0
- package/src/components/layout/sidebar.tsx +98 -0
- package/src/components/layout/split-pane.tsx +16 -0
- package/src/components/layout/stack.tsx +13 -0
- package/src/components/layout/topbar.tsx +108 -0
- package/src/components/navigation/__tests__/app-pickers.test.tsx +118 -0
- package/src/components/navigation/__tests__/dropdown-menu.test.tsx +104 -0
- package/src/components/navigation/__tests__/navigation.test.tsx +61 -0
- package/src/components/navigation/__tests__/pagination-steps-tabs.test.tsx +76 -0
- package/src/components/navigation/date-format-picker.tsx +55 -0
- package/src/components/navigation/dropdown-menu.tsx +190 -0
- package/src/components/navigation/filter-bar.tsx +38 -0
- package/src/components/navigation/index.ts +28 -0
- package/src/components/navigation/locale-picker.tsx +49 -0
- package/src/components/navigation/page-header.tsx +50 -0
- package/src/components/navigation/pagination-utils.ts +35 -0
- package/src/components/navigation/pagination.tsx +168 -0
- package/src/components/navigation/steps.tsx +163 -0
- package/src/components/navigation/tabs-items.tsx +69 -0
- package/src/components/navigation/tabs.tsx +67 -0
- package/src/components/navigation/time-format-picker.tsx +55 -0
- package/src/components/navigation/timezone-picker.tsx +63 -0
- package/src/components/query/__tests__/data-state.test.tsx +214 -0
- package/src/components/query/__tests__/infinite-prefetch.test.tsx +105 -0
- package/src/components/query/__tests__/query-helpers.test.tsx +61 -0
- package/src/components/query/data-state.tsx +58 -0
- package/src/components/query/index.ts +10 -0
- package/src/components/query/infinite-query-state.tsx +99 -0
- package/src/components/query/mutation-feedback.tsx +31 -0
- package/src/components/query/prefetch-link.tsx +45 -0
- package/src/components/query/query-refetch-button.tsx +41 -0
- package/src/components/ui/alert-dialog.tsx +1 -0
- package/src/components/ui/alert.tsx +1 -0
- package/src/components/ui/autocomplete.tsx +1 -0
- package/src/components/ui/badge.tsx +1 -0
- package/src/components/ui/button.tsx +1 -0
- package/src/components/ui/calendar.tsx +1 -0
- package/src/components/ui/card.tsx +1 -0
- package/src/components/ui/checkbox.tsx +1 -0
- package/src/components/ui/color-picker.tsx +1 -0
- package/src/components/ui/command.tsx +1 -0
- package/src/components/ui/date-picker.tsx +1 -0
- package/src/components/ui/date-range-picker.tsx +1 -0
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/dropdown-menu.tsx +1 -0
- package/src/components/ui/index.tsx +31 -0
- package/src/components/ui/input.tsx +1 -0
- package/src/components/ui/label.tsx +1 -0
- package/src/components/ui/pagination.tsx +1 -0
- package/src/components/ui/popover.tsx +1 -0
- package/src/components/ui/radio.tsx +1 -0
- package/src/components/ui/scroll-area.tsx +1 -0
- package/src/components/ui/select.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/ui/slider.tsx +1 -0
- package/src/components/ui/sonner.tsx +1 -0
- package/src/components/ui/switch.tsx +1 -0
- package/src/components/ui/table.tsx +1 -0
- package/src/components/ui/tabs-items.tsx +1 -0
- package/src/components/ui/tabs.tsx +1 -0
- package/src/components/ui/textarea.tsx +1 -0
- package/src/components/ui/time-picker.tsx +1 -0
- package/src/components/ui/upload.tsx +1 -0
- package/src/form/__tests__/use-zod-form.test.tsx +97 -0
- package/src/form/form-field-control.tsx +44 -0
- package/src/form/form-root.tsx +29 -0
- package/src/form/index.ts +7 -0
- package/src/form/use-zod-form.ts +29 -0
- package/src/i18n/__tests__/translate.test.ts +23 -0
- package/src/i18n/index.ts +9 -0
- package/src/i18n/messages/en.json +171 -0
- package/src/i18n/messages/ja.json +171 -0
- package/src/i18n/messages/vi.json +171 -0
- package/src/i18n/translate.ts +74 -0
- package/src/i18n/use-translation.ts +53 -0
- package/src/index.ts +3 -0
- package/src/lib/__tests__/control-styles.test.ts +78 -0
- package/src/lib/__tests__/datetime.test.ts +77 -0
- package/src/lib/__tests__/format-date.test.ts +97 -0
- package/src/lib/__tests__/format.test.ts +62 -0
- package/src/lib/__tests__/theme-tokens-audit.test.ts +176 -0
- package/src/lib/__tests__/theme-tokens-css.test.ts +118 -0
- package/src/lib/__tests__/token-governance.test.ts +191 -0
- package/src/lib/__tests__/variants.test.ts +18 -0
- package/src/lib/control-styles.ts +33 -0
- package/src/lib/datetime/detect.ts +25 -0
- package/src/lib/datetime/format-date.ts +100 -0
- package/src/lib/datetime/format.ts +140 -0
- package/src/lib/datetime/index.ts +25 -0
- package/src/lib/datetime/parse.ts +51 -0
- package/src/lib/datetime/sync.ts +48 -0
- package/src/lib/format.ts +114 -0
- package/src/lib/hooks.ts +54 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/variants.ts +40 -0
- package/src/props/components/app.prop.ts +99 -0
- package/src/props/components/data-display.prop.ts +73 -0
- package/src/props/components/data-entry.prop.ts +334 -0
- package/src/props/components/feedback.prop.ts +80 -0
- package/src/props/components/form.prop.ts +46 -0
- package/src/props/components/general.prop.ts +18 -0
- package/src/props/components/index.ts +99 -0
- package/src/props/components/layout.prop.ts +130 -0
- package/src/props/components/navigation.prop.ts +88 -0
- package/src/props/components/query.prop.ts +94 -0
- package/src/props/index.ts +17 -0
- package/src/props/registry.ts +603 -0
- package/src/props/vocabulary/content.prop.ts +35 -0
- package/src/props/vocabulary/data.prop.ts +46 -0
- package/src/props/vocabulary/index.ts +73 -0
- package/src/props/vocabulary/interaction.prop.ts +42 -0
- package/src/props/vocabulary/layout.prop.ts +25 -0
- package/src/props/vocabulary/navigation.prop.ts +19 -0
- package/src/props/vocabulary/shared.prop.ts +59 -0
- package/src/styles/alert-layout.css +191 -0
- package/src/styles/badge-layout.css +22 -0
- package/src/styles/card-layout.css +373 -0
- package/src/styles/control.css +504 -0
- package/src/styles/data-display-layout.css +246 -0
- package/src/styles/density.css +43 -0
- package/src/styles/dialog-layout.css +84 -0
- package/src/styles/index.css +105 -0
- package/src/styles/layout.css +479 -0
- package/src/styles/shell-layout.css +604 -0
- package/src/styles/table-layout.css +109 -0
- package/src/test/__tests__/render-loop-guard.test.tsx +38 -0
- package/src/test/jest-dom.d.ts +4 -0
- package/src/test/render-loop-guard.tsx +50 -0
- package/src/test/render.tsx +29 -0
- package/src/test/theme-globals.test.ts +77 -0
- package/src/test/theme-globals.ts +134 -0
- package/src/test/theme-test-utils.tsx +67 -0
- package/src/theme/example.service.css +37 -0
- package/src/tokens/base.css +13 -0
- package/src/tokens/foundation.css +151 -0
- package/src/tokens/primitives/badge.css +13 -0
- package/src/tokens/primitives/card.css +29 -0
- package/src/tokens/primitives/control.css +55 -0
- package/src/tokens/primitives/feedback.css +17 -0
- package/src/tokens/primitives/layout.css +20 -0
- package/src/tokens/primitives/navigation.css +13 -0
- package/src/tokens/primitives/table.css +10 -0
- package/BRAND.md +0 -296
- package/CHANGELOG.md +0 -668
- package/config/eslint.js +0 -54
- package/config/prettier.cjs +0 -20
- package/config/tsconfig.base.json +0 -22
- package/config/vitest.base.ts +0 -26
- package/dist/MiniMonth-YAmPGEpC.d.ts +0 -143
- package/dist/Table.types-BbsxoIYE.d.ts +0 -352
- package/dist/color-DO0qqUAb.d.ts +0 -38
- package/dist/components/composites.d.ts +0 -963
- package/dist/components/composites.js +0 -7343
- package/dist/components/composites.js.map +0 -1
- package/dist/components/primitives.d.ts +0 -2744
- package/dist/components/primitives.js +0 -7356
- package/dist/components/primitives.js.map +0 -1
- package/dist/components/shell.d.ts +0 -182
- package/dist/components/shell.js +0 -774
- package/dist/components/shell.js.map +0 -1
- package/dist/hooks.d.ts +0 -100
- package/dist/hooks.js +0 -558
- package/dist/hooks.js.map +0 -1
- package/dist/i18n.d.ts +0 -61
- package/dist/i18n.js +0 -860
- package/dist/i18n.js.map +0 -1
- package/dist/index.d.ts +0 -33
- package/dist/index.js +0 -13062
- package/dist/index.js.map +0 -1
- package/dist/padding-DY0JV5Ja.d.ts +0 -16
- package/dist/preferences.d.ts +0 -132
- package/dist/preferences.js +0 -262
- package/dist/preferences.js.map +0 -1
- package/dist/props.d.ts +0 -86
- package/dist/props.js +0 -16
- package/dist/props.js.map +0 -1
- package/dist/size-CQwNvOWd.d.ts +0 -19
- package/dist/types-LTj-2bl-.d.ts +0 -30
- package/dist/useTableViews-D5NIAJ7h.d.ts +0 -154
- 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
|
+
});
|