create-fullstack-scaffold 0.4.25 → 0.5.1
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/cli/index.js +432 -15
- package/dist/cli/index.js.map +1 -1
- package/package.json +10 -13
- package/template/drizzle.config.ts +10 -4
- package/template/eslint-rules/__tests__/no-merged-api-type-export.test.ts +96 -0
- package/template/eslint-rules/no-cross-module-service-import.js +4 -0
- package/template/eslint-rules/no-merged-api-type-export.js +190 -0
- package/template/eslint.config.js +3 -0
- package/template/package.json +9 -6
- package/template/scripts/sync-agent-hooks.mjs +189 -0
- package/template/src/admin/components/__tests__/StatsCard.test.tsx +2 -2
- package/template/src/admin/pages/ContentPage.tsx +1 -1
- package/template/src/admin/pages/DisputesPage.tsx +1 -1
- package/template/src/admin/pages/OrdersPage.tsx +1 -1
- package/template/src/admin/pages/TicketsPage.tsx +1 -1
- package/template/src/admin/pages/__tests__/ContentPage.test.tsx +5 -1
- package/template/src/admin/pages/__tests__/DashboardPage.test.tsx +62 -8
- package/template/src/admin/pages/__tests__/DisputesPage.test.tsx +8 -1
- package/template/src/admin/pages/__tests__/OrdersPage.test.tsx +4 -2
- package/template/src/admin/pages/__tests__/RegisterPage.test.tsx +40 -43
- package/template/src/admin/pages/__tests__/SettingsPage.test.tsx +83 -21
- package/template/src/admin/pages/__tests__/TicketsPage.test.tsx +3 -1
- package/template/src/admin/services/apiClient.ts +2 -3
- package/template/src/cli/modules/content/index.ts +3 -3
- package/template/src/cli/modules/dispute/index.ts +3 -3
- package/template/src/cli/modules/ticket/index.ts +3 -3
- package/template/src/cli/modules/todo/index.ts +1 -1
- package/template/src/cli/rpc/client.ts +3 -4
- package/template/src/cli/rpc/index.ts +1 -1
- package/template/src/client/App.tsx +3 -39
- package/template/src/client/AppRoutes.tsx +53 -0
- package/template/src/client/entry-server.tsx +75 -0
- package/template/src/client/main.tsx +3 -1
- package/template/src/client/pages/SearchPage.tsx +1 -1
- package/template/src/client/services/apiClient.ts +12 -4
- package/template/src/client/stores/__tests__/todoStore.test.ts +12 -3
- package/template/src/client/stores/entry-stores.ts +36 -0
- package/template/src/client/stores/notificationStore.ts +4 -4
- package/template/src/client/stores/todoStore.ts +2 -2
- package/template/src/merchant/pages/DisputesPage.tsx +3 -3
- package/template/src/merchant/pages/OrdersPage.tsx +1 -1
- package/template/src/merchant/pages/ProductsPage.tsx +1 -1
- package/template/src/merchant/pages/SettingsPage.tsx +1 -1
- package/template/src/server/__tests__/integration/isr-full-flow.test.ts +251 -0
- package/template/src/server/__tests__/integration/todos-api.test.ts +7 -4
- package/template/src/server/app.ts +6 -4
- package/template/src/server/core/__tests__/isr-cache-cf.test.ts +96 -0
- package/template/src/server/core/__tests__/isr-cache.test.ts +96 -92
- package/template/src/server/core/__tests__/isr-invalidation.test.ts +29 -4
- package/template/src/server/core/__tests__/isr-registry.test.ts +215 -0
- package/template/src/server/core/__tests__/isr-renderer.test.ts +121 -0
- package/template/src/server/core/isr-cache.ts +61 -18
- package/template/src/server/core/isr-invalidation.ts +6 -12
- package/template/src/server/core/isr-registry.ts +104 -0
- package/template/src/server/core/isr-renderer.ts +75 -0
- package/template/src/server/db/schema/contents.ts +28 -20
- package/template/src/server/db/schema/disputes.ts +30 -22
- package/template/src/server/db/schema/notifications.ts +20 -14
- package/template/src/server/db/schema/orders.ts +23 -16
- package/template/src/server/db/schema/plugins.ts +56 -38
- package/template/src/server/db/schema/products.ts +24 -17
- package/template/src/server/db/schema/tickets.ts +44 -31
- package/template/src/server/db/schema/todos.ts +26 -18
- package/template/src/server/entries/cloudflare.ts +97 -21
- package/template/src/server/entries/node.ts +0 -2
- package/template/src/server/index.ts +0 -1
- package/template/src/server/isr-modules.ts +10 -0
- package/template/src/server/module-admin/__tests__/admin-service.test.ts +36 -12
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +2 -1
- package/template/src/server/module-admin/routes/admin-routes.ts +3 -0
- package/template/src/server/module-admin/routes/dashboard-routes.ts +3 -0
- package/template/src/server/module-admin/services/admin-service.ts +57 -13
- package/template/src/server/module-auth/routes/auth-routes.ts +3 -0
- package/template/src/server/module-auth/routes/profile-routes.ts +3 -0
- package/template/src/server/module-captcha/routes/captcha-routes.ts +3 -0
- package/template/src/server/module-chat/routes/chat-routes.ts +4 -1
- package/template/src/server/module-content/__tests__/content-route.test.ts +2 -1
- package/template/src/server/module-content/__tests__/content-service.test.ts +2 -1
- package/template/src/server/module-content/__tests__/isr.test.ts +138 -0
- package/template/src/server/module-content/isr.ts +63 -0
- package/template/src/server/module-content/routes/content-routes.ts +10 -10
- package/template/src/server/module-content/routes/public-content-routes.ts +8 -4
- package/template/src/server/module-content/routes/topics-routes.ts +3 -0
- package/template/src/server/module-content/services/content-service.ts +23 -10
- package/template/src/server/module-dispute/__tests__/dispute-route.test.ts +2 -1
- package/template/src/server/module-dispute/__tests__/dispute-service.test.ts +2 -1
- package/template/src/server/module-dispute/routes/dispute-routes.ts +11 -10
- package/template/src/server/module-dispute/services/dispute-service.ts +15 -3
- package/template/src/server/module-file/routes/file-routes.ts +3 -0
- package/template/src/server/module-merchant/routes/merchant-routes.ts +3 -0
- package/template/src/server/module-merchant/services/merchant-service.ts +8 -8
- package/template/src/server/module-notifications/routes/notification-routes.ts +5 -1
- package/template/src/server/module-order/__tests__/order-route.test.ts +8 -8
- package/template/src/server/module-order/__tests__/order-service.test.ts +4 -3
- package/template/src/server/module-order/routes/cart-routes.ts +3 -0
- package/template/src/server/module-order/routes/order-routes.ts +9 -4
- package/template/src/server/module-order/routes/orders-mock-routes.ts +3 -0
- package/template/src/server/module-order/services/order-service.ts +25 -13
- package/template/src/server/module-permission/__tests__/audit-log-service.test.ts +464 -0
- package/template/src/server/module-permission/__tests__/role-service.test.ts +348 -0
- package/template/src/server/module-permission/routes/audit-log-routes.ts +3 -0
- package/template/src/server/module-permission/routes/permission-routes.ts +3 -0
- package/template/src/server/module-permission/routes/role-routes.ts +3 -0
- package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +3 -0
- package/template/src/server/module-plugin/routes/plugin-routes.ts +3 -0
- package/template/src/server/module-plugin/services/admin-plugin-service.ts +10 -18
- package/template/src/server/module-plugin/services/admin-stats-service.ts +28 -9
- package/template/src/server/module-plugin/services/plugin-query-service.ts +42 -66
- package/template/src/server/module-tenant/routes/tenant-routes.ts +5 -1
- package/template/src/server/module-ticket/__tests__/ticket-route.test.ts +2 -1
- package/template/src/server/module-ticket/__tests__/ticket-service.test.ts +4 -3
- package/template/src/server/module-ticket/routes/ticket-routes.ts +11 -10
- package/template/src/server/module-ticket/services/ticket-service.ts +16 -5
- package/template/src/server/module-todos/__tests__/isr.test.ts +105 -0
- package/template/src/server/module-todos/__tests__/todo-service.test.ts +12 -9
- package/template/src/server/module-todos/__tests__/todos-route-rpc.test.ts +6 -6
- package/template/src/server/module-todos/isr.ts +41 -0
- package/template/src/server/module-todos/routes/todos-routes.ts +12 -5
- package/template/src/server/module-todos/services/todo-service.ts +31 -10
- package/template/src/server/route-registry.ts +0 -4
- package/template/src/server/rpc-merge.ts +27 -0
- package/template/src/server/rpc-surface.ts +117 -0
- package/template/src/server/rpc-type-canary.ts +80 -0
- package/template/src/server/test-utils/test-client.ts +7 -9
- package/template/src/server/test-utils/test-isr-helper.ts +140 -0
- package/template/src/shared/modules/content/schemas.ts +14 -0
- package/template/src/shared/modules/dispute/schemas.ts +14 -0
- package/template/src/shared/modules/order/schemas.ts +9 -1
- package/template/src/shared/modules/ticket/schemas.ts +14 -0
- package/template/src/shared/modules/todos/index.ts +4 -0
- package/template/src/shared/modules/todos/schemas.ts +14 -0
- package/template/src/shared/schemas/index.ts +18 -0
- package/template/tsup.config.ts +48 -1
- package/template/vitest.config.ts +19 -3
- package/template/vitest.setup.ts +68 -5
- package/template/patches/typescript+5.9.3.patch +0 -24
- /package/template/patches/{hono+4.12.16.patch → hono+4.12.34.patch} +0 -0
|
@@ -48,7 +48,7 @@ export const DisputesPage: React.FC = () => {
|
|
|
48
48
|
const response = await apiClient.api.disputes.$get({ query: {} })
|
|
49
49
|
const result = await response.json()
|
|
50
50
|
if (result.success) {
|
|
51
|
-
setDisputes(result.data)
|
|
51
|
+
setDisputes(result.data.disputes)
|
|
52
52
|
} else {
|
|
53
53
|
message.error(result.error || t('disputes.loadFailed'))
|
|
54
54
|
}
|
|
@@ -56,7 +56,7 @@ export const TicketsPage: React.FC = () => {
|
|
|
56
56
|
const response = await apiClient.api.tickets.$get({ query: {} })
|
|
57
57
|
const result = await response.json()
|
|
58
58
|
if (result.success) {
|
|
59
|
-
setTickets(result.data)
|
|
59
|
+
setTickets(result.data.tickets)
|
|
60
60
|
} else {
|
|
61
61
|
message.error(result.error || t('tickets.loadFailed'))
|
|
62
62
|
}
|
|
@@ -76,7 +76,11 @@ describe('ContentPage', () => {
|
|
|
76
76
|
beforeEach(() => {
|
|
77
77
|
vi.clearAllMocks()
|
|
78
78
|
mockContentsGet.mockResolvedValue({
|
|
79
|
-
json: () =>
|
|
79
|
+
json: () =>
|
|
80
|
+
Promise.resolve({
|
|
81
|
+
success: true,
|
|
82
|
+
data: { contents: mockContents, total: mockContents.length, page: 1, limit: 20 },
|
|
83
|
+
}),
|
|
80
84
|
})
|
|
81
85
|
mockContentPost.mockResolvedValue({
|
|
82
86
|
json: () => Promise.resolve({ success: true, data: { id: '3' } }),
|
|
@@ -24,14 +24,68 @@ vi.mock('@admin/services/apiClient', () => ({
|
|
|
24
24
|
}))
|
|
25
25
|
|
|
26
26
|
vi.mock('antd', async () => {
|
|
27
|
-
const
|
|
27
|
+
const React = await import('react')
|
|
28
28
|
return {
|
|
29
|
-
|
|
29
|
+
Button: ({
|
|
30
|
+
children,
|
|
31
|
+
onClick,
|
|
32
|
+
loading,
|
|
33
|
+
...rest
|
|
34
|
+
}: {
|
|
35
|
+
children: React.ReactNode
|
|
36
|
+
onClick?: () => void
|
|
37
|
+
loading?: boolean
|
|
38
|
+
[key: string]: unknown
|
|
39
|
+
}) => React.createElement('button', { onClick, 'data-loading': loading, ...rest }, children),
|
|
40
|
+
Select: ({
|
|
41
|
+
value,
|
|
42
|
+
onChange,
|
|
43
|
+
options,
|
|
44
|
+
...rest
|
|
45
|
+
}: {
|
|
46
|
+
value: string
|
|
47
|
+
onChange?: (v: string) => void
|
|
48
|
+
options?: { value: string; label: string }[]
|
|
49
|
+
[key: string]: unknown
|
|
50
|
+
}) =>
|
|
51
|
+
React.createElement(
|
|
52
|
+
'select',
|
|
53
|
+
{
|
|
54
|
+
value,
|
|
55
|
+
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => onChange?.(e.target.value),
|
|
56
|
+
'data-testid': 'notification-type-select',
|
|
57
|
+
...rest,
|
|
58
|
+
},
|
|
59
|
+
options?.map((o: { value: string; label: string }) =>
|
|
60
|
+
React.createElement('option', { key: o.value, value: o.value }, o.label)
|
|
61
|
+
)
|
|
62
|
+
),
|
|
30
63
|
message: {
|
|
31
64
|
success: vi.fn(),
|
|
32
65
|
error: vi.fn(),
|
|
33
66
|
},
|
|
67
|
+
theme: {
|
|
68
|
+
useToken: () => ({
|
|
69
|
+
token: {
|
|
70
|
+
colorText: '#000',
|
|
71
|
+
colorTextSecondary: '#666',
|
|
72
|
+
colorBgContainer: '#fff',
|
|
73
|
+
colorBorderSecondary: '#eee',
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
vi.mock('lucide-react', async () => {
|
|
81
|
+
const React = await import('react')
|
|
82
|
+
const icons = ['Activity', 'CheckCircle', 'Clock', 'TrendingUp', 'BellRing']
|
|
83
|
+
const mock: Record<string, React.FC<{ className?: string; style?: unknown }>> = {}
|
|
84
|
+
for (const name of icons) {
|
|
85
|
+
mock[name] = (props: { className?: string; style?: unknown }) =>
|
|
86
|
+
React.createElement('span', { 'data-testid': `icon-${name}`, ...props })
|
|
34
87
|
}
|
|
88
|
+
return mock
|
|
35
89
|
})
|
|
36
90
|
|
|
37
91
|
describe('DashboardPage', () => {
|
|
@@ -55,7 +109,7 @@ describe('DashboardPage', () => {
|
|
|
55
109
|
render(<DashboardPage />)
|
|
56
110
|
|
|
57
111
|
await waitFor(() => {
|
|
58
|
-
expect(screen.getByText('
|
|
112
|
+
expect(screen.getByText('仪表盘')).toBeInTheDocument()
|
|
59
113
|
})
|
|
60
114
|
})
|
|
61
115
|
|
|
@@ -63,20 +117,20 @@ describe('DashboardPage', () => {
|
|
|
63
117
|
mockStatsGet.mockReturnValue(new Promise(() => {}))
|
|
64
118
|
render(<DashboardPage />)
|
|
65
119
|
|
|
66
|
-
expect(screen.getByText('
|
|
120
|
+
expect(screen.getByText('加载中...')).toBeInTheDocument()
|
|
67
121
|
})
|
|
68
122
|
|
|
69
123
|
it('displays stats cards after data loads', async () => {
|
|
70
124
|
render(<DashboardPage />)
|
|
71
125
|
|
|
72
126
|
await waitFor(() => {
|
|
73
|
-
expect(screen.getByText('
|
|
127
|
+
expect(screen.getByText('总待办')).toBeInTheDocument()
|
|
74
128
|
expect(screen.getByText('42')).toBeInTheDocument()
|
|
75
|
-
expect(screen.getByText('
|
|
129
|
+
expect(screen.getByText('待处理')).toBeInTheDocument()
|
|
76
130
|
expect(screen.getByText('10')).toBeInTheDocument()
|
|
77
|
-
expect(screen.getByText('
|
|
131
|
+
expect(screen.getByText('已完成')).toBeInTheDocument()
|
|
78
132
|
expect(screen.getByText('32')).toBeInTheDocument()
|
|
79
|
-
expect(screen.getByText('
|
|
133
|
+
expect(screen.getByText('最后更新')).toBeInTheDocument()
|
|
80
134
|
expect(screen.getByText('2025-01-01')).toBeInTheDocument()
|
|
81
135
|
})
|
|
82
136
|
})
|
|
@@ -117,7 +117,14 @@ function createSuccessResponse(data: unknown) {
|
|
|
117
117
|
describe('DisputesPage', () => {
|
|
118
118
|
beforeEach(() => {
|
|
119
119
|
vi.clearAllMocks()
|
|
120
|
-
mockDisputesGet.mockReturnValue(
|
|
120
|
+
mockDisputesGet.mockReturnValue(
|
|
121
|
+
createSuccessResponse({
|
|
122
|
+
disputes: mockDisputes,
|
|
123
|
+
total: mockDisputes.length,
|
|
124
|
+
page: 1,
|
|
125
|
+
limit: 20,
|
|
126
|
+
})
|
|
127
|
+
)
|
|
121
128
|
mockDisputeResolvePut.mockReturnValue(createSuccessResponse(null))
|
|
122
129
|
})
|
|
123
130
|
|
|
@@ -114,7 +114,9 @@ function createSuccessResponse(data: unknown) {
|
|
|
114
114
|
describe('OrdersPage', () => {
|
|
115
115
|
beforeEach(() => {
|
|
116
116
|
vi.clearAllMocks()
|
|
117
|
-
mockOrdersGet.mockReturnValue(
|
|
117
|
+
mockOrdersGet.mockReturnValue(
|
|
118
|
+
createSuccessResponse({ orders: mockOrders, total: mockOrders.length, page: 1, limit: 20 })
|
|
119
|
+
)
|
|
118
120
|
mockOrderProcessPut.mockReturnValue(createSuccessResponse(null))
|
|
119
121
|
mockOrderCancelPut.mockReturnValue(createSuccessResponse(null))
|
|
120
122
|
})
|
|
@@ -153,7 +155,7 @@ describe('OrdersPage', () => {
|
|
|
153
155
|
render(<OrdersPage />)
|
|
154
156
|
|
|
155
157
|
await waitFor(() => {
|
|
156
|
-
expect(mockOrdersGet).
|
|
158
|
+
expect(mockOrdersGet).toHaveBeenCalled()
|
|
157
159
|
})
|
|
158
160
|
})
|
|
159
161
|
|
|
@@ -34,6 +34,10 @@ vi.mock('antd', async () => {
|
|
|
34
34
|
}
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
+
function getSubmitButton() {
|
|
38
|
+
return screen.getByRole('button', { name: /注\s*册/ })
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
describe('RegisterPage', () => {
|
|
38
42
|
beforeEach(() => {
|
|
39
43
|
vi.clearAllMocks()
|
|
@@ -42,19 +46,18 @@ describe('RegisterPage', () => {
|
|
|
42
46
|
it('renders registration form', () => {
|
|
43
47
|
render(<RegisterPage />)
|
|
44
48
|
|
|
45
|
-
expect(screen.getByText('
|
|
46
|
-
expect(
|
|
49
|
+
expect(screen.getByText('管理员注册')).toBeInTheDocument()
|
|
50
|
+
expect(getSubmitButton()).toBeInTheDocument()
|
|
47
51
|
})
|
|
48
52
|
|
|
49
53
|
it('validates required fields', async () => {
|
|
50
54
|
const user = userEvent.setup()
|
|
51
55
|
render(<RegisterPage />)
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
await user.click(submitButton)
|
|
57
|
+
await user.click(getSubmitButton())
|
|
55
58
|
|
|
56
59
|
await waitFor(() => {
|
|
57
|
-
expect(screen.getByText('
|
|
60
|
+
expect(screen.getByText('请输入用户名!')).toBeInTheDocument()
|
|
58
61
|
})
|
|
59
62
|
})
|
|
60
63
|
|
|
@@ -62,16 +65,15 @@ describe('RegisterPage', () => {
|
|
|
62
65
|
const user = userEvent.setup()
|
|
63
66
|
render(<RegisterPage />)
|
|
64
67
|
|
|
65
|
-
await user.type(screen.getByPlaceholderText('
|
|
66
|
-
await user.type(screen.getByPlaceholderText('
|
|
67
|
-
await user.type(screen.getByPlaceholderText('
|
|
68
|
-
await user.type(screen.getByPlaceholderText('
|
|
68
|
+
await user.type(screen.getByPlaceholderText('用户名'), 'testuser')
|
|
69
|
+
await user.type(screen.getByPlaceholderText('邮箱'), 'invalid-email')
|
|
70
|
+
await user.type(screen.getByPlaceholderText('密码'), 'password123')
|
|
71
|
+
await user.type(screen.getByPlaceholderText('确认密码'), 'password123')
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
await user.click(submitButton)
|
|
73
|
+
await user.click(getSubmitButton())
|
|
72
74
|
|
|
73
75
|
await waitFor(() => {
|
|
74
|
-
expect(screen.getByText('
|
|
76
|
+
expect(screen.getByText('请输入有效的邮箱!')).toBeInTheDocument()
|
|
75
77
|
})
|
|
76
78
|
})
|
|
77
79
|
|
|
@@ -79,15 +81,14 @@ describe('RegisterPage', () => {
|
|
|
79
81
|
const user = userEvent.setup()
|
|
80
82
|
render(<RegisterPage />)
|
|
81
83
|
|
|
82
|
-
await user.type(screen.getByPlaceholderText('
|
|
83
|
-
await user.type(screen.getByPlaceholderText('
|
|
84
|
-
await user.type(screen.getByPlaceholderText('
|
|
84
|
+
await user.type(screen.getByPlaceholderText('用户名'), 'testuser')
|
|
85
|
+
await user.type(screen.getByPlaceholderText('邮箱'), 'test@example.com')
|
|
86
|
+
await user.type(screen.getByPlaceholderText('密码'), '12345')
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
await user.click(submitButton)
|
|
88
|
+
await user.click(getSubmitButton())
|
|
88
89
|
|
|
89
90
|
await waitFor(() => {
|
|
90
|
-
expect(screen.getByText('
|
|
91
|
+
expect(screen.getByText('密码至少6个字符!')).toBeInTheDocument()
|
|
91
92
|
})
|
|
92
93
|
})
|
|
93
94
|
|
|
@@ -95,16 +96,15 @@ describe('RegisterPage', () => {
|
|
|
95
96
|
const user = userEvent.setup()
|
|
96
97
|
render(<RegisterPage />)
|
|
97
98
|
|
|
98
|
-
await user.type(screen.getByPlaceholderText('
|
|
99
|
-
await user.type(screen.getByPlaceholderText('
|
|
100
|
-
await user.type(screen.getByPlaceholderText('
|
|
101
|
-
await user.type(screen.getByPlaceholderText('
|
|
99
|
+
await user.type(screen.getByPlaceholderText('用户名'), 'testuser')
|
|
100
|
+
await user.type(screen.getByPlaceholderText('邮箱'), 'test@example.com')
|
|
101
|
+
await user.type(screen.getByPlaceholderText('密码'), 'password123')
|
|
102
|
+
await user.type(screen.getByPlaceholderText('确认密码'), 'different')
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
await user.click(submitButton)
|
|
104
|
+
await user.click(getSubmitButton())
|
|
105
105
|
|
|
106
106
|
await waitFor(() => {
|
|
107
|
-
expect(screen.getByText('
|
|
107
|
+
expect(screen.getByText('两次密码不一致!')).toBeInTheDocument()
|
|
108
108
|
})
|
|
109
109
|
})
|
|
110
110
|
|
|
@@ -118,13 +118,12 @@ describe('RegisterPage', () => {
|
|
|
118
118
|
|
|
119
119
|
render(<RegisterPage />)
|
|
120
120
|
|
|
121
|
-
await user.type(screen.getByPlaceholderText('
|
|
122
|
-
await user.type(screen.getByPlaceholderText('
|
|
123
|
-
await user.type(screen.getByPlaceholderText('
|
|
124
|
-
await user.type(screen.getByPlaceholderText('
|
|
121
|
+
await user.type(screen.getByPlaceholderText('用户名'), 'testuser')
|
|
122
|
+
await user.type(screen.getByPlaceholderText('邮箱'), 'test@example.com')
|
|
123
|
+
await user.type(screen.getByPlaceholderText('密码'), 'password123')
|
|
124
|
+
await user.type(screen.getByPlaceholderText('确认密码'), 'password123')
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
await user.click(submitButton)
|
|
126
|
+
await user.click(getSubmitButton())
|
|
128
127
|
|
|
129
128
|
await waitFor(() => {
|
|
130
129
|
expect(mockPost).toHaveBeenCalled()
|
|
@@ -143,13 +142,12 @@ describe('RegisterPage', () => {
|
|
|
143
142
|
|
|
144
143
|
render(<RegisterPage />)
|
|
145
144
|
|
|
146
|
-
await user.type(screen.getByPlaceholderText('
|
|
147
|
-
await user.type(screen.getByPlaceholderText('
|
|
148
|
-
await user.type(screen.getByPlaceholderText('
|
|
149
|
-
await user.type(screen.getByPlaceholderText('
|
|
145
|
+
await user.type(screen.getByPlaceholderText('用户名'), 'testuser')
|
|
146
|
+
await user.type(screen.getByPlaceholderText('邮箱'), 'test@example.com')
|
|
147
|
+
await user.type(screen.getByPlaceholderText('密码'), 'password123')
|
|
148
|
+
await user.type(screen.getByPlaceholderText('确认密码'), 'password123')
|
|
150
149
|
|
|
151
|
-
|
|
152
|
-
await user.click(submitButton)
|
|
150
|
+
await user.click(getSubmitButton())
|
|
153
151
|
|
|
154
152
|
await waitFor(() => {
|
|
155
153
|
expect(message.error).toHaveBeenCalled()
|
|
@@ -164,13 +162,12 @@ describe('RegisterPage', () => {
|
|
|
164
162
|
|
|
165
163
|
render(<RegisterPage />)
|
|
166
164
|
|
|
167
|
-
await user.type(screen.getByPlaceholderText('
|
|
168
|
-
await user.type(screen.getByPlaceholderText('
|
|
169
|
-
await user.type(screen.getByPlaceholderText('
|
|
170
|
-
await user.type(screen.getByPlaceholderText('
|
|
165
|
+
await user.type(screen.getByPlaceholderText('用户名'), 'testuser')
|
|
166
|
+
await user.type(screen.getByPlaceholderText('邮箱'), 'test@example.com')
|
|
167
|
+
await user.type(screen.getByPlaceholderText('密码'), 'password123')
|
|
168
|
+
await user.type(screen.getByPlaceholderText('确认密码'), 'password123')
|
|
171
169
|
|
|
172
|
-
|
|
173
|
-
await user.click(submitButton)
|
|
170
|
+
await user.click(getSubmitButton())
|
|
174
171
|
|
|
175
172
|
await waitFor(() => {
|
|
176
173
|
expect(message.error).toHaveBeenCalled()
|
|
@@ -1,8 +1,46 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
-
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
3
3
|
import userEvent from '@testing-library/user-event'
|
|
4
4
|
import { SettingsPage } from '../SettingsPage'
|
|
5
5
|
|
|
6
|
+
const mockApiFn = vi.fn()
|
|
7
|
+
|
|
8
|
+
// 页面里 apiClient.api.admin.settings.$get() 是急切求值——不 mock 传输层时
|
|
9
|
+
// 会发出真实 fetch(localhost:3000),孤儿 rejection 导致 vitest 以
|
|
10
|
+
// unhandled errors 退出(tests 全过也 exit 1)。
|
|
11
|
+
vi.mock('@admin/services/apiClient', () => {
|
|
12
|
+
const settingsResponse = () => Promise.resolve(new Response('{}'))
|
|
13
|
+
return {
|
|
14
|
+
api: (...args: unknown[]) => {
|
|
15
|
+
mockApiFn(...args)
|
|
16
|
+
return {
|
|
17
|
+
withLoading: () => ({
|
|
18
|
+
json: () =>
|
|
19
|
+
Promise.resolve({
|
|
20
|
+
siteName: 'Test',
|
|
21
|
+
siteDescription: '',
|
|
22
|
+
smtpHost: 'smtp.test.com',
|
|
23
|
+
smtpPort: 587,
|
|
24
|
+
emailFrom: 'test@test.com',
|
|
25
|
+
sessionTimeout: 30,
|
|
26
|
+
maxLoginAttempts: 5,
|
|
27
|
+
}),
|
|
28
|
+
}),
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
apiClient: {
|
|
32
|
+
api: {
|
|
33
|
+
admin: {
|
|
34
|
+
settings: {
|
|
35
|
+
$get: vi.fn(settingsResponse),
|
|
36
|
+
$put: vi.fn(settingsResponse),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
6
44
|
vi.mock('antd', async () => {
|
|
7
45
|
const actual = await vi.importActual('antd')
|
|
8
46
|
return {
|
|
@@ -19,48 +57,72 @@ describe('SettingsPage', () => {
|
|
|
19
57
|
vi.clearAllMocks()
|
|
20
58
|
})
|
|
21
59
|
|
|
22
|
-
it('renders General Settings card with form', () => {
|
|
60
|
+
it('renders General Settings card with form', async () => {
|
|
23
61
|
render(<SettingsPage />)
|
|
24
62
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
63
|
+
await waitFor(() => {
|
|
64
|
+
expect(screen.getByText('系统设置')).toBeInTheDocument()
|
|
65
|
+
expect(screen.getByText('通用设置')).toBeInTheDocument()
|
|
66
|
+
expect(screen.getByText('站点名称')).toBeInTheDocument()
|
|
67
|
+
expect(screen.getByText('站点描述')).toBeInTheDocument()
|
|
68
|
+
})
|
|
29
69
|
})
|
|
30
70
|
|
|
31
|
-
it('renders Notification Settings card with toggles', () => {
|
|
71
|
+
it('renders Notification Settings card with toggles', async () => {
|
|
32
72
|
render(<SettingsPage />)
|
|
33
73
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
74
|
+
await waitFor(() => {
|
|
75
|
+
expect(screen.getByText('通知设置')).toBeInTheDocument()
|
|
76
|
+
expect(screen.getByText('邮件通知')).toBeInTheDocument()
|
|
77
|
+
expect(screen.getByText('推送通知')).toBeInTheDocument()
|
|
78
|
+
})
|
|
37
79
|
})
|
|
38
80
|
|
|
39
|
-
it('renders Security Settings card', () => {
|
|
81
|
+
it('renders Security Settings card', async () => {
|
|
40
82
|
render(<SettingsPage />)
|
|
41
83
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
expect(screen.getByText('安全设置')).toBeInTheDocument()
|
|
86
|
+
expect(screen.getByText('会话超时')).toBeInTheDocument()
|
|
87
|
+
expect(screen.getByText('最大登录尝试次数')).toBeInTheDocument()
|
|
88
|
+
})
|
|
46
89
|
})
|
|
47
90
|
|
|
48
91
|
it('save button shows success message', async () => {
|
|
49
92
|
const user = userEvent.setup()
|
|
50
93
|
const { message } = await import('antd')
|
|
51
94
|
|
|
95
|
+
mockApiFn.mockReturnValue({
|
|
96
|
+
withLoading: () => ({
|
|
97
|
+
json: () => Promise.resolve({}),
|
|
98
|
+
}),
|
|
99
|
+
})
|
|
100
|
+
|
|
52
101
|
render(<SettingsPage />)
|
|
53
102
|
|
|
54
|
-
|
|
55
|
-
|
|
103
|
+
await waitFor(() => {
|
|
104
|
+
expect(screen.getByText('系统设置')).toBeInTheDocument()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// 等待 fetchSettings 填充表单(必填字段有值后 onFinish 才会被触发)
|
|
108
|
+
await waitFor(() => {
|
|
109
|
+
expect((screen.getByLabelText('站点名称') as HTMLInputElement).value).toBe('Test')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const saveButton = screen.getByRole('button', { name: /保\s*存\s*更\s*改/ })
|
|
113
|
+
await user.click(saveButton)
|
|
56
114
|
|
|
57
|
-
|
|
115
|
+
await waitFor(() => {
|
|
116
|
+
expect(message.success).toHaveBeenCalledWith('设置保存成功!')
|
|
117
|
+
})
|
|
58
118
|
})
|
|
59
119
|
|
|
60
|
-
it('toggle switches are present and interactive', () => {
|
|
120
|
+
it('toggle switches are present and interactive', async () => {
|
|
61
121
|
render(<SettingsPage />)
|
|
62
122
|
|
|
63
|
-
|
|
64
|
-
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
const switches = screen.getAllByRole('switch')
|
|
125
|
+
expect(switches.length).toBeGreaterThanOrEqual(2)
|
|
126
|
+
})
|
|
65
127
|
})
|
|
66
128
|
})
|
|
@@ -120,7 +120,9 @@ function createSuccessResponse(data: unknown) {
|
|
|
120
120
|
describe('TicketsPage', () => {
|
|
121
121
|
beforeEach(() => {
|
|
122
122
|
vi.clearAllMocks()
|
|
123
|
-
mockTicketsGet.mockReturnValue(
|
|
123
|
+
mockTicketsGet.mockReturnValue(
|
|
124
|
+
createSuccessResponse({ tickets: mockTickets, total: mockTickets.length, page: 1, limit: 20 })
|
|
125
|
+
)
|
|
124
126
|
mockTicketClosePut.mockReturnValue(createSuccessResponse(null))
|
|
125
127
|
})
|
|
126
128
|
|
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
* @impact loading 控制现在通过 api().withLoading() 链式调用实现
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { hc } from 'hono/client'
|
|
9
8
|
import { WSClientImpl } from '@shared/core/ws-client'
|
|
10
9
|
import { SSEClientImpl } from '@shared/core/sse-client'
|
|
11
10
|
import { createRequestInterceptor } from './requestInterceptor'
|
|
12
11
|
import { useCaptchaStore } from '../stores/captchaStore'
|
|
13
|
-
import
|
|
12
|
+
import { createApiFacade } from '@server/rpc-surface'
|
|
14
13
|
|
|
15
14
|
const baseUrl = import.meta.env.API_BASE_URL || window.location.origin
|
|
16
15
|
|
|
@@ -50,7 +49,7 @@ function createCustomFetch() {
|
|
|
50
49
|
})
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
export const apiClient =
|
|
52
|
+
export const apiClient = createApiFacade(baseUrl, {
|
|
54
53
|
fetch: createCustomFetch() as typeof fetch,
|
|
55
54
|
webSocket: url => new WSClientImpl(url) as unknown as WebSocket,
|
|
56
55
|
sse: url => {
|
|
@@ -8,14 +8,14 @@ export function registerContentCommands(site: SiteInstance) {
|
|
|
8
8
|
description: 'List all contents',
|
|
9
9
|
parameters: z.object({
|
|
10
10
|
limit: z.coerce.number().default(20).describe('Limit results'),
|
|
11
|
-
|
|
11
|
+
page: z.coerce.number().default(1).describe('Page number'),
|
|
12
12
|
}),
|
|
13
13
|
handler: async (params: unknown) => {
|
|
14
|
-
const p = params as { limit: number;
|
|
14
|
+
const p = params as { limit: number; page: number }
|
|
15
15
|
try {
|
|
16
16
|
const client = getClient()
|
|
17
17
|
const res = await client.api.contents.$get({
|
|
18
|
-
query: { limit: String(p.limit),
|
|
18
|
+
query: { limit: String(p.limit), page: String(p.page) },
|
|
19
19
|
})
|
|
20
20
|
const data = await res.json()
|
|
21
21
|
return ok(data)
|
|
@@ -8,14 +8,14 @@ export function registerDisputeCommands(site: SiteInstance) {
|
|
|
8
8
|
description: 'List all disputes',
|
|
9
9
|
parameters: z.object({
|
|
10
10
|
limit: z.coerce.number().default(20).describe('Limit results'),
|
|
11
|
-
|
|
11
|
+
page: z.coerce.number().default(1).describe('Page number'),
|
|
12
12
|
}),
|
|
13
13
|
handler: async (params: unknown) => {
|
|
14
|
-
const p = params as { limit: number;
|
|
14
|
+
const p = params as { limit: number; page: number }
|
|
15
15
|
try {
|
|
16
16
|
const client = getClient()
|
|
17
17
|
const res = await client.api.disputes.$get({
|
|
18
|
-
query: { limit: String(p.limit),
|
|
18
|
+
query: { limit: String(p.limit), page: String(p.page) },
|
|
19
19
|
})
|
|
20
20
|
const data = await res.json()
|
|
21
21
|
return ok(data)
|
|
@@ -8,14 +8,14 @@ export function registerTicketCommands(site: SiteInstance) {
|
|
|
8
8
|
description: 'List all tickets',
|
|
9
9
|
parameters: z.object({
|
|
10
10
|
limit: z.coerce.number().default(20).describe('Limit results'),
|
|
11
|
-
|
|
11
|
+
page: z.coerce.number().default(1).describe('Page number'),
|
|
12
12
|
}),
|
|
13
13
|
handler: async (params: unknown) => {
|
|
14
|
-
const p = params as { limit: number;
|
|
14
|
+
const p = params as { limit: number; page: number }
|
|
15
15
|
try {
|
|
16
16
|
const client = getClient()
|
|
17
17
|
const res = await client.api.tickets.$get({
|
|
18
|
-
query: { limit: String(p.limit),
|
|
18
|
+
query: { limit: String(p.limit), page: String(p.page) },
|
|
19
19
|
})
|
|
20
20
|
const data = await res.json()
|
|
21
21
|
return ok(data)
|
|
@@ -12,7 +12,7 @@ export function registerTodoCommands(site: SiteInstance) {
|
|
|
12
12
|
handler: async () => {
|
|
13
13
|
try {
|
|
14
14
|
const client = getClient()
|
|
15
|
-
const res = await client.api.todos.$get()
|
|
15
|
+
const res = await client.api.todos.$get({ query: {} })
|
|
16
16
|
const data = await res.json()
|
|
17
17
|
return ok(data)
|
|
18
18
|
} catch (err) {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { AppType } from '@server/index'
|
|
1
|
+
import { createApiFacade, type ApiFacade } from '@server/rpc-surface'
|
|
3
2
|
import { WSClientImpl } from '@shared/core/ws-client'
|
|
4
3
|
import { SSEClientImpl } from '@shared/core/sse-client'
|
|
5
4
|
|
|
6
|
-
export type {
|
|
5
|
+
export type { ApiFacade }
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* CLI RPC 请求扩展参数
|
|
@@ -16,7 +15,7 @@ export interface CliFetchExtendOptions {
|
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
export function createRPCClient(baseUrl: string) {
|
|
19
|
-
return
|
|
18
|
+
return createApiFacade(baseUrl, {
|
|
20
19
|
webSocket: url => new WSClientImpl(url) as unknown as WebSocket,
|
|
21
20
|
sse: url => new SSEClientImpl(url),
|
|
22
21
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { createRPCClient, type
|
|
1
|
+
export { createRPCClient, type ApiFacade } from './client'
|