@ploykit/ui 0.5.0 → 0.6.2

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 (222) hide show
  1. package/dist/AppProviders.js +2 -2
  2. package/dist/components/ConfirmDialog.js +1 -1
  3. package/dist/components/CronInput.js +3 -3
  4. package/dist/components/DataTable.js +4 -4
  5. package/dist/components/FormField.js +1 -1
  6. package/dist/components/ImpersonationBanner.js +1 -1
  7. package/dist/components/NotificationBell.js +1 -1
  8. package/dist/components/OnboardingChecklist.js +3 -3
  9. package/dist/components/Page.js +2 -2
  10. package/dist/components/ReauthDialog.js +4 -4
  11. package/dist/components/SecretModal.js +2 -2
  12. package/dist/components/SettingsCard.js +2 -2
  13. package/dist/components/SiteBanner.js +1 -1
  14. package/dist/components/SiteBannerLayer.js +1 -1
  15. package/dist/components/StatusBadge.js +1 -1
  16. package/dist/components/UserMenu.js +3 -3
  17. package/dist/components/WorkspaceSwitcher.js +7 -7
  18. package/dist/components/ui/alert-dialog.js +2 -2
  19. package/dist/components/ui/avatar.js +1 -1
  20. package/dist/components/ui/badge.js +1 -1
  21. package/dist/components/ui/breadcrumb.js +1 -1
  22. package/dist/components/ui/button.js +1 -1
  23. package/dist/components/ui/card.js +1 -1
  24. package/dist/components/ui/checkbox.js +1 -1
  25. package/dist/components/ui/dialog.js +2 -2
  26. package/dist/components/ui/dropdown-menu.js +1 -1
  27. package/dist/components/ui/empty.js +1 -1
  28. package/dist/components/ui/field.js +3 -3
  29. package/dist/components/ui/input.js +1 -1
  30. package/dist/components/ui/label.js +1 -1
  31. package/dist/components/ui/popover.js +1 -1
  32. package/dist/components/ui/progress.js +1 -1
  33. package/dist/components/ui/select.js +1 -1
  34. package/dist/components/ui/separator.js +1 -1
  35. package/dist/components/ui/sheet.js +2 -2
  36. package/dist/components/ui/sidebar.js +8 -8
  37. package/dist/components/ui/skeleton.js +1 -1
  38. package/dist/components/ui/switch.js +1 -1
  39. package/dist/components/ui/tabs.js +1 -1
  40. package/dist/components/ui/textarea.js +1 -1
  41. package/dist/components/ui/tooltip.js +1 -1
  42. package/dist/index.js +78 -78
  43. package/dist/layouts/AppShell.js +4 -4
  44. package/dist/layouts/SettingsShell.js +4 -4
  45. package/dist/pages/LoginPage.js +5 -5
  46. package/dist/pages/account/NotificationPreferencesPage.js +3 -3
  47. package/dist/pages/account/ProfileSettingsPage.js +9 -9
  48. package/dist/pages/account/SecuritySettingsPage.js +13 -13
  49. package/dist/pages/account/TokensPage.js +12 -12
  50. package/dist/pages/admin/AdminSettingsPage.js +11 -11
  51. package/dist/pages/admin/AdminShell.js +3 -3
  52. package/dist/pages/admin/AuditPage.js +8 -8
  53. package/dist/pages/admin/OrdersPage.js +12 -12
  54. package/dist/pages/admin/OverviewPage.js +13 -13
  55. package/dist/pages/admin/SSOPage.js +6 -6
  56. package/dist/pages/admin/UserDetailPage.js +13 -13
  57. package/dist/pages/admin/UsersPage.js +13 -13
  58. package/dist/pages/admin/WebhookDeliveriesPage.js +9 -9
  59. package/dist/pages/admin/WorkspaceDetailPage.js +14 -14
  60. package/dist/pages/admin/WorkspacesPage.js +9 -9
  61. package/dist/pages/admin/shared.js +1 -1
  62. package/dist/pages/auth/ForgotPasswordPage.js +4 -4
  63. package/dist/pages/auth/InviteAcceptPage.js +6 -6
  64. package/dist/pages/auth/RegisterPage.js +7 -7
  65. package/dist/pages/auth/ResetPasswordPage.js +5 -5
  66. package/dist/pages/auth/VerifyEmailPage.js +3 -3
  67. package/dist/pages/workspace/BillingPage.js +11 -11
  68. package/dist/pages/workspace/BillingSuccessPage.js +2 -2
  69. package/dist/pages/workspace/MembersPage.js +15 -15
  70. package/dist/pages/workspace/RolePermissionsPage.js +9 -9
  71. package/dist/pages/workspace/SchedulesPage.js +12 -12
  72. package/dist/pages/workspace/UsagePage.js +6 -6
  73. package/dist/pages/workspace/WebhooksPage.js +16 -16
  74. package/dist/pages/workspace/WorkspaceAuditPage.js +7 -7
  75. package/dist/pages/workspace/WorkspaceGeneralPage.js +9 -9
  76. package/package.json +6 -5
  77. package/src/AppProviders.tsx +29 -0
  78. package/src/components/ConfirmDialog.tsx +77 -0
  79. package/src/components/CronInput.tsx +195 -0
  80. package/src/components/DataTable.tsx +152 -0
  81. package/src/components/ErrorBoundary.tsx +46 -0
  82. package/src/components/FormField.tsx +56 -0
  83. package/src/components/ImpersonationBanner.tsx +23 -0
  84. package/src/components/NotificationBell.tsx +135 -0
  85. package/src/components/OnboardingChecklist.tsx +114 -0
  86. package/src/components/Page.tsx +67 -0
  87. package/src/components/ReauthDialog.tsx +94 -0
  88. package/src/components/SecretModal.tsx +147 -0
  89. package/src/components/SettingsCard.tsx +39 -0
  90. package/src/components/SiteBanner.tsx +32 -0
  91. package/src/components/SiteBannerLayer.tsx +29 -0
  92. package/src/components/StatusBadge.tsx +12 -0
  93. package/src/components/UserMenu.tsx +103 -0
  94. package/src/components/WorkspaceSwitcher.tsx +159 -0
  95. package/src/components/__tests__/AppProviders.test.tsx +111 -0
  96. package/src/components/__tests__/CronInput.test.tsx +180 -0
  97. package/src/components/__tests__/DataTable.test.tsx +208 -0
  98. package/src/components/__tests__/ErrorBoundary.test.tsx +61 -0
  99. package/src/components/__tests__/ImpersonationBanner.test.tsx +59 -0
  100. package/src/components/__tests__/NotificationBell.test.tsx +207 -0
  101. package/src/components/__tests__/OnboardingChecklist.test.tsx +97 -0
  102. package/src/components/__tests__/SecretModal.test.tsx +112 -0
  103. package/src/components/__tests__/SiteBannerLayer.test.tsx +44 -0
  104. package/src/components/__tests__/UserMenu.test.tsx +98 -0
  105. package/src/components/__tests__/WorkspaceSwitcher.test.tsx +162 -0
  106. package/src/components/__tests__/form.test.tsx +133 -0
  107. package/src/components/__tests__/semantic-tokens.test.ts +41 -0
  108. package/src/components/__tests__/smoke.test.tsx +11 -0
  109. package/src/components/__tests__/toast.test.tsx +27 -0
  110. package/src/components/toast.tsx +23 -0
  111. package/src/components/ui/__tests__/alert-dialog.test.tsx +66 -0
  112. package/src/components/ui/__tests__/avatar.test.tsx +30 -0
  113. package/src/components/ui/__tests__/checkbox.test.tsx +18 -0
  114. package/src/components/ui/__tests__/dropdown-menu.test.tsx +33 -0
  115. package/src/components/ui/__tests__/field.test.tsx +43 -0
  116. package/src/components/ui/__tests__/overlay-size.test.tsx +84 -0
  117. package/src/components/ui/__tests__/popover.test.tsx +32 -0
  118. package/src/components/ui/__tests__/progress.test.tsx +24 -0
  119. package/src/components/ui/__tests__/select-callback.test.tsx +72 -0
  120. package/src/components/ui/__tests__/select.test.tsx +48 -0
  121. package/src/components/ui/__tests__/sidebar.test.tsx +121 -0
  122. package/src/components/ui/__tests__/stock-mirror.test.ts +39 -0
  123. package/src/components/ui/__tests__/subpath.test.tsx +38 -0
  124. package/src/components/ui/__tests__/switch.test.tsx +24 -0
  125. package/src/components/ui/__tests__/tabs.test.tsx +24 -0
  126. package/src/components/ui/__tests__/textarea.test.tsx +41 -0
  127. package/src/components/ui/alert-dialog.tsx +187 -0
  128. package/src/components/ui/avatar.tsx +108 -0
  129. package/src/components/ui/badge.tsx +51 -0
  130. package/src/components/ui/breadcrumb.tsx +124 -0
  131. package/src/components/ui/button.tsx +57 -0
  132. package/src/components/ui/card.tsx +102 -0
  133. package/src/components/ui/checkbox.tsx +28 -0
  134. package/src/components/ui/dialog.tsx +166 -0
  135. package/src/components/ui/dropdown-menu.tsx +265 -0
  136. package/src/components/ui/empty.tsx +103 -0
  137. package/src/components/ui/field.tsx +238 -0
  138. package/src/components/ui/input.tsx +19 -0
  139. package/src/components/ui/label.tsx +17 -0
  140. package/src/components/ui/popover.tsx +89 -0
  141. package/src/components/ui/progress.tsx +82 -0
  142. package/src/components/ui/select.tsx +235 -0
  143. package/src/components/ui/separator.tsx +24 -0
  144. package/src/components/ui/sheet.tsx +146 -0
  145. package/src/components/ui/sidebar.tsx +723 -0
  146. package/src/components/ui/skeleton.tsx +13 -0
  147. package/src/components/ui/switch.tsx +29 -0
  148. package/src/components/ui/tabs.tsx +79 -0
  149. package/src/components/ui/textarea.tsx +26 -0
  150. package/src/components/ui/tooltip.tsx +63 -0
  151. package/src/dev.css +1 -0
  152. package/src/hooks/use-mobile.ts +19 -0
  153. package/src/index.ts +150 -0
  154. package/src/layouts/AppShell.tsx +163 -0
  155. package/src/layouts/AuthShell.tsx +24 -0
  156. package/src/layouts/SettingsShell.tsx +129 -0
  157. package/src/layouts/__tests__/AppShell.test.tsx +163 -0
  158. package/src/layouts/__tests__/SettingsShell.test.tsx +146 -0
  159. package/src/lib/__tests__/api-error.test.ts +163 -0
  160. package/src/lib/__tests__/utils.test.ts +21 -0
  161. package/src/lib/api-error.ts +98 -0
  162. package/src/lib/utils.ts +109 -0
  163. package/src/pages/LandingPage.tsx +88 -0
  164. package/src/pages/LoginPage.tsx +291 -0
  165. package/src/pages/__tests__/LandingPage.test.tsx +58 -0
  166. package/src/pages/__tests__/LoginPage.test.tsx +229 -0
  167. package/src/pages/__tests__/RegisterPage.test.tsx +138 -0
  168. package/src/pages/__tests__/ssr-smoke.test.tsx +99 -0
  169. package/src/pages/account/NotificationPreferencesPage.tsx +138 -0
  170. package/src/pages/account/ProfileSettingsPage.tsx +189 -0
  171. package/src/pages/account/SecuritySettingsPage.tsx +214 -0
  172. package/src/pages/account/TokensPage.tsx +211 -0
  173. package/src/pages/account/__tests__/NotificationPreferencesPage.test.tsx +150 -0
  174. package/src/pages/account/__tests__/ProfileSettingsPage.test.tsx +173 -0
  175. package/src/pages/account/__tests__/SecuritySettingsPage.test.tsx +212 -0
  176. package/src/pages/account/__tests__/TokensPage.test.tsx +180 -0
  177. package/src/pages/admin/AdminSettingsPage.tsx +564 -0
  178. package/src/pages/admin/AdminShell.tsx +199 -0
  179. package/src/pages/admin/AuditPage.tsx +338 -0
  180. package/src/pages/admin/OrdersPage.tsx +642 -0
  181. package/src/pages/admin/OverviewPage.tsx +559 -0
  182. package/src/pages/admin/SSOPage.tsx +166 -0
  183. package/src/pages/admin/UserDetailPage.tsx +641 -0
  184. package/src/pages/admin/UsersPage.tsx +462 -0
  185. package/src/pages/admin/WebhookDeliveriesPage.tsx +318 -0
  186. package/src/pages/admin/WorkspaceDetailPage.tsx +551 -0
  187. package/src/pages/admin/WorkspacesPage.tsx +299 -0
  188. package/src/pages/admin/__tests__/AdminSettingsPage.test.tsx +260 -0
  189. package/src/pages/admin/__tests__/OrdersPageMarkPaid.test.tsx +207 -0
  190. package/src/pages/admin/__tests__/OverviewPageActivity.test.tsx +213 -0
  191. package/src/pages/admin/__tests__/SSOPage.test.tsx +128 -0
  192. package/src/pages/admin/__tests__/UsersPageActions.test.tsx +188 -0
  193. package/src/pages/admin/__tests__/WorkspaceDetailPageTransfer.test.tsx +170 -0
  194. package/src/pages/admin/__tests__/WorkspacesPagePlans.test.tsx +138 -0
  195. package/src/pages/admin/shared.ts +42 -0
  196. package/src/pages/auth/ForgotPasswordPage.tsx +83 -0
  197. package/src/pages/auth/InviteAcceptPage.tsx +105 -0
  198. package/src/pages/auth/RegisterPage.tsx +204 -0
  199. package/src/pages/auth/ResetPasswordPage.tsx +150 -0
  200. package/src/pages/auth/VerifyEmailPage.tsx +76 -0
  201. package/src/pages/auth/__tests__/ForgotPasswordPage.test.tsx +67 -0
  202. package/src/pages/auth/__tests__/InviteAcceptPage.test.tsx +86 -0
  203. package/src/pages/auth/__tests__/ResetPasswordPage.test.tsx +115 -0
  204. package/src/pages/auth/__tests__/VerifyEmailPage.test.tsx +74 -0
  205. package/src/pages/workspace/BillingPage.tsx +361 -0
  206. package/src/pages/workspace/BillingSuccessPage.tsx +115 -0
  207. package/src/pages/workspace/MembersPage.tsx +568 -0
  208. package/src/pages/workspace/RolePermissionsPage.tsx +202 -0
  209. package/src/pages/workspace/SchedulesPage.tsx +387 -0
  210. package/src/pages/workspace/UsagePage.tsx +207 -0
  211. package/src/pages/workspace/WebhooksPage.tsx +506 -0
  212. package/src/pages/workspace/WorkspaceAuditPage.tsx +229 -0
  213. package/src/pages/workspace/WorkspaceGeneralPage.tsx +184 -0
  214. package/src/pages/workspace/__tests__/BillingPage.test.tsx +302 -0
  215. package/src/pages/workspace/__tests__/BillingSuccessPage.test.tsx +134 -0
  216. package/src/pages/workspace/__tests__/MembersPage.test.tsx +347 -0
  217. package/src/pages/workspace/__tests__/RolePermissionsPage.test.tsx +138 -0
  218. package/src/pages/workspace/__tests__/SchedulesPage.test.tsx +239 -0
  219. package/src/pages/workspace/__tests__/UsagePage.test.tsx +223 -0
  220. package/src/pages/workspace/__tests__/WebhooksPage.test.tsx +309 -0
  221. package/src/pages/workspace/__tests__/WorkspaceAuditPage.test.tsx +112 -0
  222. package/src/pages/workspace/__tests__/WorkspaceGeneralPage.test.tsx +164 -0
@@ -0,0 +1,84 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { useState } from 'react'
4
+ import { describe, it, expect } from 'vitest'
5
+ import {
6
+ Sheet, SheetTrigger, SheetContent,
7
+ } from '../sheet'
8
+ import {
9
+ Dialog, DialogTrigger, DialogContent,
10
+ } from '../dialog'
11
+
12
+ function SheetDemo({ size }: { size?: 'sm' | 'md' | 'lg' }) {
13
+ const [open, setOpen] = useState(true)
14
+ return (
15
+ <Sheet open={open} onOpenChange={setOpen}>
16
+ <SheetTrigger>打开</SheetTrigger>
17
+ <SheetContent size={size} data-testid="sheet-content">
18
+ <div>面板内容</div>
19
+ </SheetContent>
20
+ </Sheet>
21
+ )
22
+ }
23
+
24
+ function DialogDemo({ size }: { size?: 'sm' | 'md' | 'lg' | 'xl' }) {
25
+ const [open, setOpen] = useState(true)
26
+ return (
27
+ <Dialog open={open} onOpenChange={setOpen}>
28
+ <DialogTrigger>打开</DialogTrigger>
29
+ <DialogContent size={size} data-testid="dialog-content">
30
+ <div>弹窗内容</div>
31
+ </DialogContent>
32
+ </Dialog>
33
+ )
34
+ }
35
+
36
+ describe('sheet/dialog size 档位(data-size 落地)', () => {
37
+ it('Sheet 默认 size=sm → data-size="sm"', () => {
38
+ render(<SheetDemo />)
39
+ expect(screen.getByTestId('sheet-content')).toHaveAttribute('data-size', 'sm')
40
+ })
41
+
42
+ it('Sheet size=lg → data-size="lg",类含 data-[size=lg] 档位与 overflow-y-auto', () => {
43
+ render(<SheetDemo size="lg" />)
44
+ const el = screen.getByTestId('sheet-content')
45
+ expect(el).toHaveAttribute('data-size', 'lg')
46
+ expect(el).toHaveAttribute('data-side', 'right')
47
+ expect(el.className).toContain('data-[side=right]:data-[size=lg]:sm:max-w-2xl')
48
+ expect(el.className).toContain('overflow-y-auto')
49
+ })
50
+
51
+ it('Sheet top 侧 lg → max-h 档位', () => {
52
+ render(
53
+ <Sheet open>
54
+ <SheetContent side="top" size="lg" data-testid="sheet-top">
55
+ 内容
56
+ </SheetContent>
57
+ </Sheet>,
58
+ )
59
+ const el = screen.getByTestId('sheet-top')
60
+ expect(el.className).toContain('data-[side=top]:data-[size=lg]:max-h-[80vh]')
61
+ })
62
+
63
+ it('Dialog 默认 size=sm → data-size="sm",保留 sm:max-w-sm 基础类', () => {
64
+ render(<DialogDemo />)
65
+ const el = screen.getByTestId('dialog-content')
66
+ expect(el).toHaveAttribute('data-size', 'sm')
67
+ expect(el.className).toContain('sm:max-w-sm')
68
+ })
69
+
70
+ it('Dialog size=xl → data-size="xl"', () => {
71
+ render(<DialogDemo size="xl" />)
72
+ const el = screen.getByTestId('dialog-content')
73
+ expect(el).toHaveAttribute('data-size', 'xl')
74
+ expect(el.className).toContain('data-[size=xl]:sm:max-w-4xl')
75
+ })
76
+
77
+ it('Dialog size=md / lg → 对应档位类', () => {
78
+ const { rerender } = render(<DialogDemo size="md" />)
79
+ expect(screen.getByTestId('dialog-content').className).toContain('data-[size=md]:sm:max-w-lg')
80
+
81
+ rerender(<DialogDemo size="lg" />)
82
+ expect(screen.getByTestId('dialog-content').className).toContain('data-[size=lg]:sm:max-w-2xl')
83
+ })
84
+ })
@@ -0,0 +1,32 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
3
+ import { describe, it, expect } from 'vitest'
4
+ import {
5
+ Popover, PopoverTrigger, PopoverContent, PopoverHeader,
6
+ PopoverTitle, PopoverDescription,
7
+ } from '../popover'
8
+
9
+ describe('popover 冒烟', () => {
10
+ it('Trigger 打开浮层;Escape 关闭', async () => {
11
+ render(
12
+ <Popover>
13
+ <PopoverTrigger>打开</PopoverTrigger>
14
+ <PopoverContent>
15
+ <PopoverHeader>
16
+ <PopoverTitle>浮层标题</PopoverTitle>
17
+ <PopoverDescription>浮层描述</PopoverDescription>
18
+ </PopoverHeader>
19
+ </PopoverContent>
20
+ </Popover>,
21
+ )
22
+
23
+ fireEvent.click(screen.getByRole('button', { name: '打开' }))
24
+ expect(await screen.findByText('浮层标题')).toBeVisible()
25
+ expect(screen.getByText('浮层描述')).toBeVisible()
26
+
27
+ fireEvent.keyDown(document.body, { key: 'Escape' })
28
+ await waitFor(() =>
29
+ expect(screen.queryByText('浮层标题')).not.toBeInTheDocument(),
30
+ )
31
+ })
32
+ })
@@ -0,0 +1,24 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { describe, it, expect } from 'vitest'
4
+ import {
5
+ Progress, ProgressLabel, ProgressValue,
6
+ } from '../progress'
7
+
8
+ describe('progress 冒烟', () => {
9
+ it('value=50 渲染轨道/指示条/标签/数值', () => {
10
+ const { container } = render(
11
+ <Progress value={50}>
12
+ <ProgressLabel>用量</ProgressLabel>
13
+ <ProgressValue />
14
+ </Progress>,
15
+ )
16
+ expect(screen.getByText('用量')).toBeInTheDocument()
17
+ expect(screen.getByText('50%')).toBeInTheDocument()
18
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '50')
19
+
20
+
21
+ expect(container.querySelector('[data-slot="progress-track"]')).toBeInTheDocument()
22
+ expect(container.querySelector('[data-slot="progress-indicator"]')).toBeInTheDocument()
23
+ })
24
+ })
@@ -0,0 +1,72 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
3
+ import { useState } from 'react'
4
+ import { describe, it, expect } from 'vitest'
5
+ import {
6
+ Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
7
+ } from '../select'
8
+ import type * as React from 'react'
9
+
10
+ const items = [
11
+ { value: 'a', label: '选项 A' },
12
+ { value: 'b', label: '选项 B' },
13
+ ]
14
+
15
+ /** 产品侧目标写法:回调参数已是 string,无需 `v ?? ''` 卫语句。 */
16
+ function Demo({ onValueChange }: { onValueChange: (v: string) => void }) {
17
+ const [value, setValue] = useState<string | null>('a')
18
+ const handle = (v: string) => {
19
+ received.push(v)
20
+ setValue(v || null)
21
+ onValueChange(v)
22
+ }
23
+ return (
24
+ <Select items={items} value={value} onValueChange={handle}>
25
+ <SelectTrigger>
26
+ <SelectValue />
27
+ </SelectTrigger>
28
+ <SelectContent>
29
+ {items.map((item) => (
30
+ <SelectItem key={item.value} value={item.value}>{item.label}</SelectItem>
31
+ ))}
32
+ </SelectContent>
33
+ </Select>
34
+ )
35
+ }
36
+
37
+ const received: string[] = []
38
+
39
+ describe('select 包装层:onValueChange 永远收到 string', () => {
40
+ it('受控交互:点击选项后回调收到 string(非 null)', async () => {
41
+ render(<Demo onValueChange={() => {}} />)
42
+ const trigger = screen.getByRole('combobox')
43
+ expect(trigger).toHaveTextContent('选项 A')
44
+
45
+ fireEvent.click(trigger)
46
+ const option = await screen.findByRole('option', { name: '选项 B' })
47
+ fireEvent.pointerDown(option, { button: 0, pointerType: 'mouse' })
48
+ fireEvent.pointerUp(option, { button: 0, pointerType: 'mouse' })
49
+ fireEvent.click(option)
50
+
51
+ await waitFor(() => expect(trigger).toHaveTextContent('选项 B'))
52
+ await waitFor(() => expect(screen.queryByRole('listbox')).not.toBeInTheDocument())
53
+ expect(received).toEqual(['b'])
54
+ received.length = 0
55
+ })
56
+
57
+ it('类型契约:onValueChange 参数为 string,value 仍允许 null(编译期断言)', () => {
58
+ // 1) string 回调可直接作为 onValueChange 使用
59
+ const onString = (_v: string) => {}
60
+ const ok: React.ComponentProps<typeof Select>['onValueChange'] = onString
61
+ expect(ok).toBeTypeOf('function')
62
+
63
+ // 2) 受控 value 保留 null 能力(base-ui 原生语义不收窄)
64
+ const nullValue: React.ComponentProps<typeof Select>['value'] = null
65
+ expect(nullValue).toBeNull()
66
+
67
+ // 3) 旧卫语句写法 `(v) => setX(v ?? '')` 对 string 参数仍合法(?? 的左操作数可为非空类型)
68
+ const legacy = (_v: string) => {}
69
+ const tolerant = (v: string) => legacy(v ?? '')
70
+ expect(tolerant).toBeTypeOf('function')
71
+ })
72
+ })
@@ -0,0 +1,48 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
3
+ import { useState } from 'react'
4
+ import { describe, it, expect } from 'vitest'
5
+ import {
6
+ Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
7
+ } from '../select'
8
+
9
+ const items = [
10
+ { value: 'a', label: '选项 A' },
11
+ { value: 'b', label: '选项 B' },
12
+ ]
13
+
14
+ function Demo() {
15
+ const [value, setValue] = useState<string | null>('a')
16
+ return (
17
+ <Select items={items} value={value} onValueChange={(v) => setValue(v)}>
18
+ <SelectTrigger>
19
+ <SelectValue />
20
+ </SelectTrigger>
21
+ <SelectContent>
22
+ {items.map((item) => (
23
+ <SelectItem key={item.value} value={item.value}>{item.label}</SelectItem>
24
+ ))}
25
+ </SelectContent>
26
+ </Select>
27
+ )
28
+ }
29
+
30
+ describe('select 冒烟', () => {
31
+ it('Trigger 显示当前值;点击选项后切换', async () => {
32
+ render(<Demo />)
33
+ const trigger = screen.getByRole('combobox')
34
+ expect(trigger).toHaveTextContent('选项 A')
35
+
36
+ fireEvent.click(trigger)
37
+ const option = await screen.findByRole('option', { name: '选项 B' })
38
+ expect(option).toBeVisible()
39
+
40
+
41
+ fireEvent.pointerDown(option, { button: 0, pointerType: 'mouse' })
42
+ fireEvent.pointerUp(option, { button: 0, pointerType: 'mouse' })
43
+ fireEvent.click(option)
44
+
45
+ await waitFor(() => expect(trigger).toHaveTextContent('选项 B'))
46
+ await waitFor(() => expect(screen.queryByRole('listbox')).not.toBeInTheDocument())
47
+ })
48
+ })
@@ -0,0 +1,121 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import { fireEvent, render, screen } from '@testing-library/react'
3
+ import { describe, it, expect, beforeEach } from 'vitest'
4
+ import {
5
+ Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarMenu,
6
+ SidebarMenuItem, SidebarMenuButton, SidebarMenuSkeleton, SidebarProvider, useSidebar,
7
+ } from '../sidebar'
8
+
9
+
10
+
11
+
12
+ function StateProbe() {
13
+ const { state, open } = useSidebar()
14
+ return (
15
+ <div>
16
+ <div data-testid="state">{state}</div>
17
+ <div data-testid="open">{String(open)}</div>
18
+ </div>
19
+ )
20
+ }
21
+
22
+ function renderSidebar(props: { defaultOpen?: boolean } = {}) {
23
+ return render(
24
+ <SidebarProvider {...props}>
25
+ <Sidebar>
26
+ <SidebarContent>
27
+ <SidebarGroup>
28
+ <SidebarGroupContent>
29
+ <SidebarMenu>
30
+ <SidebarMenuItem>
31
+ <SidebarMenuButton>菜单项</SidebarMenuButton>
32
+ </SidebarMenuItem>
33
+ </SidebarMenu>
34
+ </SidebarGroupContent>
35
+ </SidebarGroup>
36
+ </SidebarContent>
37
+ </Sidebar>
38
+ <StateProbe />
39
+ <input aria-label="输入框" />
40
+ </SidebarProvider>,
41
+ )
42
+ }
43
+
44
+ beforeEach(() => {
45
+ document.cookie = 'sidebar_state=; path=/; max-age=0'
46
+ })
47
+
48
+ describe('SidebarProvider 状态持久化(P2-17)', () => {
49
+ it('stock 语义:provider 不回读 cookie(回读在 AppShell 组合层,P2-17 见 AppShell 测试)', () => {
50
+ document.cookie = 'sidebar_state=false; path=/'
51
+ renderSidebar()
52
+ expect(screen.getByTestId('state').textContent).toBe('expanded')
53
+ })
54
+
55
+ it('cookie 缺省 → 默认展开', () => {
56
+ renderSidebar()
57
+ expect(screen.getByTestId('state').textContent).toBe('expanded')
58
+ })
59
+
60
+ it('显式 defaultOpen 优先于 cookie', () => {
61
+ document.cookie = 'sidebar_state=false; path=/'
62
+ renderSidebar({ defaultOpen: true })
63
+ expect(screen.getByTestId('state').textContent).toBe('expanded')
64
+ })
65
+
66
+ it('切换写回 cookie(持久化不再是死功能)', () => {
67
+ renderSidebar()
68
+ fireEvent.keyDown(window, { key: 'b', ctrlKey: true })
69
+ expect(document.cookie).toContain('sidebar_state=false')
70
+ fireEvent.keyDown(window, { key: 'b', ctrlKey: true })
71
+ expect(document.cookie).toContain('sidebar_state=true')
72
+ })
73
+ })
74
+
75
+ describe('Ctrl/Cmd+B 快捷键(P3-1)', () => {
76
+ it('全局 Cmd/Ctrl+B 切换折叠', () => {
77
+ renderSidebar()
78
+ expect(screen.getByTestId('state').textContent).toBe('expanded')
79
+ fireEvent.keyDown(window, { key: 'b', metaKey: true })
80
+ expect(screen.getByTestId('state').textContent).toBe('collapsed')
81
+ fireEvent.keyDown(window, { key: 'b', ctrlKey: true })
82
+ expect(screen.getByTestId('state').textContent).toBe('expanded')
83
+ })
84
+
85
+ it('stock 语义:input 内 Ctrl+B 同样折叠(产品级输入保护在 AppShell,P3-1 见 AppShell 测试)', () => {
86
+ renderSidebar()
87
+ const input = screen.getByRole('textbox', { name: '输入框' })
88
+ fireEvent.keyDown(input, { key: 'b', ctrlKey: true })
89
+ expect(screen.getByTestId('state').textContent).toBe('collapsed')
90
+ })
91
+ })
92
+
93
+ describe('SidebarMenuSkeleton 确定性(P2-11)', () => {
94
+ it('宽度来自固定序列,且同一实例跨重渲染稳定(渲染期随机数的判别性质)', () => {
95
+ function Tree({ tag }: { tag: string }) {
96
+ return (
97
+ <SidebarMenu>
98
+ {['a', 'b', 'c', 'd'].map((k) => (
99
+ <SidebarMenuSkeleton key={`${tag}-${k}`} />
100
+ ))}
101
+ </SidebarMenu>
102
+ )
103
+ }
104
+ const widthsOf = (container: HTMLElement) =>
105
+ Array.from(container.querySelectorAll('[data-sidebar="menu-skeleton-text"]')).map(
106
+ (el) => (el as HTMLElement).style.getPropertyValue('--skeleton-width'),
107
+ )
108
+ const allowed = Array.from({ length: 40 }, (_, i) => `${50 + i}%`) // stock: floor(rand*40)+50
109
+
110
+ const { rerender, container } = render(<Tree tag="t" />)
111
+ const w1 = widthsOf(container)
112
+ expect(w1).toHaveLength(4)
113
+
114
+ for (const w of w1) expect(allowed).toContain(w)
115
+
116
+
117
+
118
+ rerender(<Tree tag="t" />)
119
+ expect(widthsOf(container)).toEqual(w1)
120
+ })
121
+ })
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { readFileSync, readdirSync } from 'node:fs'
3
+ import { join, dirname } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ // stock 镜像不变式:components/ui 与 shadcn registry 输出保持一致(唯一允许的差异
7
+ // 是 adopt-shadcn.mjs 的 import 路径改写),业务概念禁止进入该目录。
8
+ // 本守卫钉住 import 白名单;未来 re-sync 时若 diff 出现白名单外依赖,说明有人
9
+ // 手改了 stock 文件——去组合层(AppShell/业务组件)实现,而不是改这里。
10
+ const uiDir = join(dirname(fileURLToPath(import.meta.url)), '..')
11
+
12
+ describe('components/ui stock 镜像守卫', () => {
13
+ const files = readdirSync(uiDir).filter((f) => f.endsWith('.tsx') || f.endsWith('.ts'))
14
+
15
+ it('目录非空且为 23+ 件 stock 组件', () => {
16
+ expect(files.length).toBeGreaterThanOrEqual(23)
17
+ })
18
+
19
+ for (const f of files) {
20
+ it(`${f} 的 import 全部在白名单内`, () => {
21
+ const src = readFileSync(join(uiDir, f), 'utf8')
22
+ const imports = [...src.matchAll(/from ["']([^"']+)["']/g)].map((m) => m[1])
23
+ for (const spec of imports) {
24
+ const ok =
25
+ spec === 'react' || spec.startsWith('react/') ||
26
+ spec === '@base-ui/react' || spec.startsWith('@base-ui/react/') ||
27
+ spec.startsWith('@base-ui/') ||
28
+ spec === 'class-variance-authority' || spec === 'clsx' || spec === 'tailwind-merge' ||
29
+ spec === 'lucide-react' ||
30
+ spec === '../../lib/utils' || spec === '../../hooks/use-mobile' ||
31
+ /^\.\.?\//.test(spec) // 同目录兄弟 ui 件(./button、./separator 等)
32
+ // 白名单之外的"包级"或"跨层"导入直接失败:
33
+ // - @ploykit/*(业务包) - ../../provider、../../pages(包内跨层)
34
+ expect(ok, `${f} 引用了非白名单模块: ${spec}`).toBe(true)
35
+ expect(spec.startsWith('@ploykit/'), `${f} 引入了业务包 ${spec}`).toBe(false)
36
+ }
37
+ })
38
+ }
39
+ })
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ // 子路径导出合同:components/ui 的全部组件必须能经
4
+ // '@ploykit/ui/components/ui/<name>' 深路径导入(exports map wildcard)。
5
+ // 这里逐个 import + 存在性断言——解析失败(exports map 配错)会让整个文件挂掉。
6
+ import * as alertDialog from '@ploykit/ui/components/ui/alert-dialog'
7
+ import * as avatar from '@ploykit/ui/components/ui/avatar'
8
+ import * as breadcrumb from '@ploykit/ui/components/ui/breadcrumb'
9
+ import * as button from '@ploykit/ui/components/ui/button'
10
+ import * as badge from '@ploykit/ui/components/ui/badge'
11
+ import * as input from '@ploykit/ui/components/ui/input'
12
+ import * as card from '@ploykit/ui/components/ui/card'
13
+ import * as checkbox from '@ploykit/ui/components/ui/checkbox'
14
+ import * as popover from '@ploykit/ui/components/ui/popover'
15
+ import * as progress from '@ploykit/ui/components/ui/progress'
16
+ import * as select from '@ploykit/ui/components/ui/select'
17
+ import * as sw from '@ploykit/ui/components/ui/switch'
18
+
19
+ describe('子路径导出 @ploykit/ui/components/ui/*', () => {
20
+ const modules = {
21
+ 'alert-dialog': alertDialog,
22
+ avatar,
23
+ breadcrumb,
24
+ card,
25
+ checkbox,
26
+ popover,
27
+ progress,
28
+ select,
29
+ switch: sw,
30
+ } as const
31
+
32
+ for (const [name, mod] of Object.entries(modules)) {
33
+ it(`${name} 深路径可解析且导出组件`, () => {
34
+ const components = Object.values(mod).filter((v) => typeof v === 'function')
35
+ expect(components.length, `${name} 应至少导出一个组件函数`).toBeGreaterThan(0)
36
+ })
37
+ }
38
+ })
@@ -0,0 +1,24 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import { fireEvent, render, screen } from '@testing-library/react'
3
+ import { useState } from 'react'
4
+ import { describe, it, expect } from 'vitest'
5
+ import { Switch } from '../switch'
6
+
7
+ function Demo() {
8
+ const [on, setOn] = useState(false)
9
+ return <Switch checked={on} onCheckedChange={setOn} aria-label="通知开关" />
10
+ }
11
+
12
+ describe('switch 冒烟', () => {
13
+ it('点击切换 aria-checked', () => {
14
+ render(<Demo />)
15
+ const sw = screen.getByRole('switch', { name: '通知开关' })
16
+ expect(sw).toHaveAttribute('aria-checked', 'false')
17
+
18
+ fireEvent.click(sw)
19
+ expect(sw).toHaveAttribute('aria-checked', 'true')
20
+
21
+ fireEvent.click(sw)
22
+ expect(sw).toHaveAttribute('aria-checked', 'false')
23
+ })
24
+ })
@@ -0,0 +1,24 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import { fireEvent, render, screen } from '@testing-library/react'
3
+ import { describe, it, expect } from 'vitest'
4
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from '../tabs'
5
+
6
+ describe('tabs 冒烟', () => {
7
+ it('点击 Tab 切换面板', () => {
8
+ render(
9
+ <Tabs defaultValue="a">
10
+ <TabsList>
11
+ <TabsTrigger value="a">Tab A</TabsTrigger>
12
+ <TabsTrigger value="b">Tab B</TabsTrigger>
13
+ </TabsList>
14
+ <TabsContent value="a">Content A</TabsContent>
15
+ <TabsContent value="b">Content B</TabsContent>
16
+ </Tabs>,
17
+ )
18
+ expect(screen.getByText('Content A')).toBeVisible()
19
+ expect(screen.getByRole('tab', { name: 'Tab B' })).toBeInTheDocument()
20
+
21
+ fireEvent.click(screen.getByRole('tab', { name: 'Tab B' }))
22
+ expect(screen.getByText('Content B')).toBeVisible()
23
+ })
24
+ })
@@ -0,0 +1,41 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { createRef } from 'react'
4
+ import { describe, it, expect } from 'vitest'
5
+ import { Textarea } from '../textarea'
6
+
7
+ describe('textarea 冒烟', () => {
8
+ it('渲染为 textarea 且 data-slot 正确', () => {
9
+ render(<Textarea data-testid="ta" placeholder="备注" />)
10
+ const ta = screen.getByTestId('ta')
11
+ expect(ta.tagName).toBe('TEXTAREA')
12
+ expect(ta).toHaveAttribute('data-slot', 'textarea')
13
+ expect(ta).toHaveAttribute('placeholder', '备注')
14
+ })
15
+
16
+ it('className 合并:自定义类与基础类共存,同族类后者覆盖', () => {
17
+ render(<Textarea data-testid="ta" className="min-h-32 custom-x" />)
18
+ const cls = screen.getByTestId('ta').className
19
+ expect(cls).toContain('custom-x')
20
+ expect(cls).toContain('border-input')
21
+ // tailwind-merge:同族冲突时用户类覆盖基础类
22
+ expect(cls).not.toMatch(/(^|\s)min-h-16(\s|$)/)
23
+ expect(cls).toMatch(/(^|\s)min-h-32(\s|$)/)
24
+ })
25
+
26
+ it('ref 正确转发到 DOM 节点', () => {
27
+ const ref = createRef<HTMLTextAreaElement>()
28
+ render(<Textarea ref={ref} defaultValue="hello" />)
29
+ expect(ref.current).not.toBeNull()
30
+ expect(ref.current?.tagName).toBe('TEXTAREA')
31
+ expect(ref.current?.value).toBe('hello')
32
+ })
33
+
34
+ it('aria-invalid 语义类在基础类中(token 合同同族)', () => {
35
+ render(<Textarea data-testid="ta" aria-invalid />)
36
+ const cls = screen.getByTestId('ta').className
37
+ expect(cls).toContain('aria-invalid:border-destructive')
38
+ expect(cls).toContain('dark:bg-input/30')
39
+ expect(cls).toContain('min-h-16')
40
+ })
41
+ })