@ploykit/ui 0.6.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.
- package/dist/AppProviders.js +2 -2
- package/dist/components/ConfirmDialog.js +1 -1
- package/dist/components/CronInput.js +3 -3
- package/dist/components/DataTable.js +4 -4
- package/dist/components/FormField.js +1 -1
- package/dist/components/ImpersonationBanner.js +1 -1
- package/dist/components/NotificationBell.js +1 -1
- package/dist/components/OnboardingChecklist.js +3 -3
- package/dist/components/Page.js +2 -2
- package/dist/components/ReauthDialog.js +4 -4
- package/dist/components/SecretModal.js +2 -2
- package/dist/components/SettingsCard.js +2 -2
- package/dist/components/SiteBanner.js +1 -1
- package/dist/components/SiteBannerLayer.js +1 -1
- package/dist/components/StatusBadge.js +1 -1
- package/dist/components/UserMenu.js +3 -3
- package/dist/components/WorkspaceSwitcher.js +7 -7
- package/dist/components/ui/alert-dialog.js +2 -2
- package/dist/components/ui/avatar.js +1 -1
- package/dist/components/ui/badge.js +1 -1
- package/dist/components/ui/breadcrumb.js +1 -1
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/card.js +1 -1
- package/dist/components/ui/checkbox.js +1 -1
- package/dist/components/ui/dialog.js +2 -2
- package/dist/components/ui/dropdown-menu.js +1 -1
- package/dist/components/ui/empty.js +1 -1
- package/dist/components/ui/field.js +3 -3
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/label.js +1 -1
- package/dist/components/ui/popover.js +1 -1
- package/dist/components/ui/progress.js +1 -1
- package/dist/components/ui/select.js +1 -1
- package/dist/components/ui/separator.js +1 -1
- package/dist/components/ui/sheet.js +2 -2
- package/dist/components/ui/sidebar.js +8 -8
- package/dist/components/ui/skeleton.js +1 -1
- package/dist/components/ui/switch.js +1 -1
- package/dist/components/ui/tabs.js +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/tooltip.js +1 -1
- package/dist/index.js +78 -78
- package/dist/layouts/AppShell.js +4 -4
- package/dist/layouts/SettingsShell.js +4 -4
- package/dist/pages/LoginPage.js +5 -5
- package/dist/pages/account/NotificationPreferencesPage.js +3 -3
- package/dist/pages/account/ProfileSettingsPage.js +9 -9
- package/dist/pages/account/SecuritySettingsPage.js +13 -13
- package/dist/pages/account/TokensPage.js +12 -12
- package/dist/pages/admin/AdminSettingsPage.js +11 -11
- package/dist/pages/admin/AdminShell.js +3 -3
- package/dist/pages/admin/AuditPage.js +8 -8
- package/dist/pages/admin/OrdersPage.js +12 -12
- package/dist/pages/admin/OverviewPage.js +13 -13
- package/dist/pages/admin/SSOPage.js +6 -6
- package/dist/pages/admin/UserDetailPage.js +13 -13
- package/dist/pages/admin/UsersPage.js +13 -13
- package/dist/pages/admin/WebhookDeliveriesPage.js +9 -9
- package/dist/pages/admin/WorkspaceDetailPage.js +14 -14
- package/dist/pages/admin/WorkspacesPage.js +9 -9
- package/dist/pages/admin/shared.js +1 -1
- package/dist/pages/auth/ForgotPasswordPage.js +4 -4
- package/dist/pages/auth/InviteAcceptPage.js +6 -6
- package/dist/pages/auth/RegisterPage.js +7 -7
- package/dist/pages/auth/ResetPasswordPage.js +5 -5
- package/dist/pages/auth/VerifyEmailPage.js +3 -3
- package/dist/pages/workspace/BillingPage.js +11 -11
- package/dist/pages/workspace/BillingSuccessPage.js +2 -2
- package/dist/pages/workspace/MembersPage.js +15 -15
- package/dist/pages/workspace/RolePermissionsPage.js +9 -9
- package/dist/pages/workspace/SchedulesPage.js +12 -12
- package/dist/pages/workspace/UsagePage.js +6 -6
- package/dist/pages/workspace/WebhooksPage.js +16 -16
- package/dist/pages/workspace/WorkspaceAuditPage.js +7 -7
- package/dist/pages/workspace/WorkspaceGeneralPage.js +9 -9
- package/package.json +6 -5
- package/src/AppProviders.tsx +29 -0
- package/src/components/ConfirmDialog.tsx +77 -0
- package/src/components/CronInput.tsx +195 -0
- package/src/components/DataTable.tsx +152 -0
- package/src/components/ErrorBoundary.tsx +46 -0
- package/src/components/FormField.tsx +56 -0
- package/src/components/ImpersonationBanner.tsx +23 -0
- package/src/components/NotificationBell.tsx +135 -0
- package/src/components/OnboardingChecklist.tsx +114 -0
- package/src/components/Page.tsx +67 -0
- package/src/components/ReauthDialog.tsx +94 -0
- package/src/components/SecretModal.tsx +147 -0
- package/src/components/SettingsCard.tsx +39 -0
- package/src/components/SiteBanner.tsx +32 -0
- package/src/components/SiteBannerLayer.tsx +29 -0
- package/src/components/StatusBadge.tsx +12 -0
- package/src/components/UserMenu.tsx +103 -0
- package/src/components/WorkspaceSwitcher.tsx +159 -0
- package/src/components/__tests__/AppProviders.test.tsx +111 -0
- package/src/components/__tests__/CronInput.test.tsx +180 -0
- package/src/components/__tests__/DataTable.test.tsx +208 -0
- package/src/components/__tests__/ErrorBoundary.test.tsx +61 -0
- package/src/components/__tests__/ImpersonationBanner.test.tsx +59 -0
- package/src/components/__tests__/NotificationBell.test.tsx +207 -0
- package/src/components/__tests__/OnboardingChecklist.test.tsx +97 -0
- package/src/components/__tests__/SecretModal.test.tsx +112 -0
- package/src/components/__tests__/SiteBannerLayer.test.tsx +44 -0
- package/src/components/__tests__/UserMenu.test.tsx +98 -0
- package/src/components/__tests__/WorkspaceSwitcher.test.tsx +162 -0
- package/src/components/__tests__/form.test.tsx +133 -0
- package/src/components/__tests__/semantic-tokens.test.ts +41 -0
- package/src/components/__tests__/smoke.test.tsx +11 -0
- package/src/components/__tests__/toast.test.tsx +27 -0
- package/src/components/toast.tsx +23 -0
- package/src/components/ui/__tests__/alert-dialog.test.tsx +66 -0
- package/src/components/ui/__tests__/avatar.test.tsx +30 -0
- package/src/components/ui/__tests__/checkbox.test.tsx +18 -0
- package/src/components/ui/__tests__/dropdown-menu.test.tsx +33 -0
- package/src/components/ui/__tests__/field.test.tsx +43 -0
- package/src/components/ui/__tests__/overlay-size.test.tsx +84 -0
- package/src/components/ui/__tests__/popover.test.tsx +32 -0
- package/src/components/ui/__tests__/progress.test.tsx +24 -0
- package/src/components/ui/__tests__/select-callback.test.tsx +72 -0
- package/src/components/ui/__tests__/select.test.tsx +48 -0
- package/src/components/ui/__tests__/sidebar.test.tsx +121 -0
- package/src/components/ui/__tests__/stock-mirror.test.ts +39 -0
- package/src/components/ui/__tests__/subpath.test.tsx +38 -0
- package/src/components/ui/__tests__/switch.test.tsx +24 -0
- package/src/components/ui/__tests__/tabs.test.tsx +24 -0
- package/src/components/ui/__tests__/textarea.test.tsx +41 -0
- package/src/components/ui/alert-dialog.tsx +187 -0
- package/src/components/ui/avatar.tsx +108 -0
- package/src/components/ui/badge.tsx +51 -0
- package/src/components/ui/breadcrumb.tsx +124 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/card.tsx +102 -0
- package/src/components/ui/checkbox.tsx +28 -0
- package/src/components/ui/dialog.tsx +166 -0
- package/src/components/ui/dropdown-menu.tsx +265 -0
- package/src/components/ui/empty.tsx +103 -0
- package/src/components/ui/field.tsx +238 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/label.tsx +17 -0
- package/src/components/ui/popover.tsx +89 -0
- package/src/components/ui/progress.tsx +82 -0
- package/src/components/ui/select.tsx +235 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/sheet.tsx +146 -0
- package/src/components/ui/sidebar.tsx +723 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +79 -0
- package/src/components/ui/textarea.tsx +26 -0
- package/src/components/ui/tooltip.tsx +63 -0
- package/src/dev.css +1 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +150 -0
- package/src/layouts/AppShell.tsx +163 -0
- package/src/layouts/AuthShell.tsx +24 -0
- package/src/layouts/SettingsShell.tsx +129 -0
- package/src/layouts/__tests__/AppShell.test.tsx +163 -0
- package/src/layouts/__tests__/SettingsShell.test.tsx +146 -0
- package/src/lib/__tests__/api-error.test.ts +163 -0
- package/src/lib/__tests__/utils.test.ts +21 -0
- package/src/lib/api-error.ts +98 -0
- package/src/lib/utils.ts +109 -0
- package/src/pages/LandingPage.tsx +88 -0
- package/src/pages/LoginPage.tsx +291 -0
- package/src/pages/__tests__/LandingPage.test.tsx +58 -0
- package/src/pages/__tests__/LoginPage.test.tsx +229 -0
- package/src/pages/__tests__/RegisterPage.test.tsx +138 -0
- package/src/pages/__tests__/ssr-smoke.test.tsx +99 -0
- package/src/pages/account/NotificationPreferencesPage.tsx +138 -0
- package/src/pages/account/ProfileSettingsPage.tsx +189 -0
- package/src/pages/account/SecuritySettingsPage.tsx +214 -0
- package/src/pages/account/TokensPage.tsx +211 -0
- package/src/pages/account/__tests__/NotificationPreferencesPage.test.tsx +150 -0
- package/src/pages/account/__tests__/ProfileSettingsPage.test.tsx +173 -0
- package/src/pages/account/__tests__/SecuritySettingsPage.test.tsx +212 -0
- package/src/pages/account/__tests__/TokensPage.test.tsx +180 -0
- package/src/pages/admin/AdminSettingsPage.tsx +564 -0
- package/src/pages/admin/AdminShell.tsx +199 -0
- package/src/pages/admin/AuditPage.tsx +338 -0
- package/src/pages/admin/OrdersPage.tsx +642 -0
- package/src/pages/admin/OverviewPage.tsx +559 -0
- package/src/pages/admin/SSOPage.tsx +166 -0
- package/src/pages/admin/UserDetailPage.tsx +641 -0
- package/src/pages/admin/UsersPage.tsx +462 -0
- package/src/pages/admin/WebhookDeliveriesPage.tsx +318 -0
- package/src/pages/admin/WorkspaceDetailPage.tsx +551 -0
- package/src/pages/admin/WorkspacesPage.tsx +299 -0
- package/src/pages/admin/__tests__/AdminSettingsPage.test.tsx +260 -0
- package/src/pages/admin/__tests__/OrdersPageMarkPaid.test.tsx +207 -0
- package/src/pages/admin/__tests__/OverviewPageActivity.test.tsx +213 -0
- package/src/pages/admin/__tests__/SSOPage.test.tsx +128 -0
- package/src/pages/admin/__tests__/UsersPageActions.test.tsx +188 -0
- package/src/pages/admin/__tests__/WorkspaceDetailPageTransfer.test.tsx +170 -0
- package/src/pages/admin/__tests__/WorkspacesPagePlans.test.tsx +138 -0
- package/src/pages/admin/shared.ts +42 -0
- package/src/pages/auth/ForgotPasswordPage.tsx +83 -0
- package/src/pages/auth/InviteAcceptPage.tsx +105 -0
- package/src/pages/auth/RegisterPage.tsx +204 -0
- package/src/pages/auth/ResetPasswordPage.tsx +150 -0
- package/src/pages/auth/VerifyEmailPage.tsx +76 -0
- package/src/pages/auth/__tests__/ForgotPasswordPage.test.tsx +67 -0
- package/src/pages/auth/__tests__/InviteAcceptPage.test.tsx +86 -0
- package/src/pages/auth/__tests__/ResetPasswordPage.test.tsx +115 -0
- package/src/pages/auth/__tests__/VerifyEmailPage.test.tsx +74 -0
- package/src/pages/workspace/BillingPage.tsx +361 -0
- package/src/pages/workspace/BillingSuccessPage.tsx +115 -0
- package/src/pages/workspace/MembersPage.tsx +568 -0
- package/src/pages/workspace/RolePermissionsPage.tsx +202 -0
- package/src/pages/workspace/SchedulesPage.tsx +387 -0
- package/src/pages/workspace/UsagePage.tsx +207 -0
- package/src/pages/workspace/WebhooksPage.tsx +506 -0
- package/src/pages/workspace/WorkspaceAuditPage.tsx +229 -0
- package/src/pages/workspace/WorkspaceGeneralPage.tsx +184 -0
- package/src/pages/workspace/__tests__/BillingPage.test.tsx +302 -0
- package/src/pages/workspace/__tests__/BillingSuccessPage.test.tsx +134 -0
- package/src/pages/workspace/__tests__/MembersPage.test.tsx +347 -0
- package/src/pages/workspace/__tests__/RolePermissionsPage.test.tsx +138 -0
- package/src/pages/workspace/__tests__/SchedulesPage.test.tsx +239 -0
- package/src/pages/workspace/__tests__/UsagePage.test.tsx +223 -0
- package/src/pages/workspace/__tests__/WebhooksPage.test.tsx +309 -0
- package/src/pages/workspace/__tests__/WorkspaceAuditPage.test.tsx +112 -0
- 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
|
+
})
|