@teamix-evo/skills 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.json +61 -45
- package/package.json +2 -2
- package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/SKILL.md +18 -18
- package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/checklist.md +2 -2
- package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/reuse-first.md +25 -17
- package/src/teamix-evo-code-uni-manager/SKILL.md +95 -0
- package/src/teamix-evo-code-uni-manager/api-layering.md +370 -0
- package/src/teamix-evo-code-uni-manager/checklist.md +193 -0
- package/src/teamix-evo-code-uni-manager/error-and-loading.md +389 -0
- package/src/teamix-evo-code-uni-manager/file-structure.md +339 -0
- package/src/teamix-evo-code-uni-manager/forms-and-validation.md +459 -0
- package/src/teamix-evo-code-uni-manager/reuse-first.md +188 -0
- package/src/teamix-evo-code-uni-manager/routing-and-codesplit.md +450 -0
- package/src/teamix-evo-code-uni-manager/testing.md +396 -0
- package/src/teamix-evo-design-opentrek/SKILL.md +71 -0
- package/src/teamix-evo-design-opentrek/boundaries.md +513 -0
- package/src/teamix-evo-design-opentrek/brand.md +154 -0
- package/src/teamix-evo-design-opentrek/checklist.md +83 -0
- package/src/teamix-evo-design-opentrek/components.md +245 -0
- package/src/teamix-evo-design-opentrek/flows.md +51 -0
- package/src/teamix-evo-design-opentrek/foundations.md +271 -0
- package/src/teamix-evo-design-opentrek/generation-flow.md +185 -0
- package/src/teamix-evo-design-opentrek/patterns/dashboard.md +31 -0
- package/src/teamix-evo-design-opentrek/patterns/detail-page.md +202 -0
- package/src/teamix-evo-design-opentrek/patterns/form-page.md +289 -0
- package/src/teamix-evo-design-opentrek/patterns/list-page.md +334 -0
- package/src/teamix-evo-design-opentrek/patterns/page-types.md +154 -0
- package/src/teamix-evo-design-opentrek/philosophy.md +96 -0
- package/src/teamix-evo-design-opentrek/rules/README.md +39 -0
- package/src/teamix-evo-design-opentrek/rules/boundaries.rules.json +391 -0
- package/src/teamix-evo-design-uni-manager/SKILL.md +73 -0
- package/src/teamix-evo-design-uni-manager/boundaries.md +564 -0
- package/src/teamix-evo-design-uni-manager/brand.md +202 -0
- package/src/teamix-evo-design-uni-manager/checklist.md +115 -0
- package/src/teamix-evo-design-uni-manager/components.md +254 -0
- package/src/teamix-evo-design-uni-manager/flows.md +63 -0
- package/src/teamix-evo-design-uni-manager/foundations.md +258 -0
- package/src/teamix-evo-design-uni-manager/generation-flow.md +194 -0
- package/src/teamix-evo-design-uni-manager/patterns/dashboard.md +95 -0
- package/src/teamix-evo-design-uni-manager/patterns/detail-page.md +224 -0
- package/src/teamix-evo-design-uni-manager/patterns/form-page.md +329 -0
- package/src/teamix-evo-design-uni-manager/patterns/list-page.md +356 -0
- package/src/teamix-evo-design-uni-manager/patterns/page-types.md +165 -0
- package/src/teamix-evo-design-uni-manager/philosophy.md +106 -0
- package/src/teamix-evo-design-uni-manager/rules/README.md +49 -0
- package/src/teamix-evo-design-uni-manager/rules/boundaries.rules.json +418 -0
- package/src/teamix-evo-manage/SKILL.md +281 -0
- package/skills/teamix-evo-design-rules/SKILL.md +0 -86
- package/skills/teamix-evo-design-rules/boundaries.md +0 -89
- package/skills/teamix-evo-design-rules/checklist.md +0 -108
- package/skills/teamix-evo-design-rules/generation-flow.md +0 -142
- package/skills/teamix-evo-design-rules/prompts/page-design.md +0 -148
- package/skills/teamix-evo-design-rules-opentrek/SKILL.md +0 -48
- package/skills/teamix-evo-design-rules-opentrek/brand-rules.md +0 -74
- package/skills/teamix-evo-design-rules-uni-manager/SKILL.md +0 -51
- package/skills/teamix-evo-design-rules-uni-manager/ai-scenarios.md +0 -51
- package/skills/teamix-evo-design-rules-uni-manager/command-center.md +0 -108
- package/skills/teamix-evo-design-rules-uni-manager/danger-ops.md +0 -87
- package/skills/teamix-evo-manage/SKILL.md +0 -178
- package/skills/teamix-evo-ui-upgrade/SKILL.md +0 -75
- /package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/api-layering.md +0 -0
- /package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/error-and-loading.md +0 -0
- /package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/file-structure.md +0 -0
- /package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/forms-and-validation.md +0 -0
- /package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/routing-and-codesplit.md +0 -0
- /package/{skills/teamix-evo-coding-conventions → src/teamix-evo-code-opentrek}/testing.md +0 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# 测试规范(uni-manager)
|
|
2
|
+
|
|
3
|
+
> **核心约定**:`vitest` + `@testing-library/react` + `msw`,文件就近 `*.test.ts(x)`。**不追求覆盖率数字**,追求"关键路径必有测试 + 纯函数全测"。**uni-manager 额外要求 msw handler 校验 X-Tenant-Id / X-Region-Id / X-Cloud-Provider 请求头。**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 选型(业界主流)
|
|
8
|
+
|
|
9
|
+
| 类型 | 工具 | 理由 |
|
|
10
|
+
| ----------- | -------------------------------------------------------- | ------------------------------------------------ |
|
|
11
|
+
| 测试 runner | `vitest` | 与 Vite 原生集成,ESM 友好,API 与 Jest 兼容 |
|
|
12
|
+
| 组件测试 | `@testing-library/react` + `@testing-library/user-event` | 测行为不测实现,业界事实标准(Kent C. Dodds) |
|
|
13
|
+
| API mock | `msw`(Mock Service Worker) | 拦截真实 fetch / axios,测试与生产共用 service 层 |
|
|
14
|
+
| 断言 | `vitest` 内置 `expect`(兼容 Jest) | 无需 chai |
|
|
15
|
+
| E2E(可选) | `playwright` | 关键流程冒烟,**不在本 skill 范围** |
|
|
16
|
+
|
|
17
|
+
**不要混用**:已经选 vitest 就不要再加 jest;已经用 msw 就不要再自己 mock axios。
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## §1 · 文件位置
|
|
22
|
+
|
|
23
|
+
测试文件**就近**放在被测文件同目录,`<name>.test.ts(x)`:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
src/
|
|
27
|
+
├── utils/
|
|
28
|
+
│ ├── format.ts
|
|
29
|
+
│ └── format.test.ts # ← 紧邻
|
|
30
|
+
├── services/
|
|
31
|
+
│ ├── instance.ts
|
|
32
|
+
│ └── instance.test.ts
|
|
33
|
+
├── hooks/
|
|
34
|
+
│ ├── useInstanceList.ts
|
|
35
|
+
│ └── useInstanceList.test.ts
|
|
36
|
+
└── components/
|
|
37
|
+
├── InstanceStatusBadge.tsx
|
|
38
|
+
└── InstanceStatusBadge.test.tsx
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**不要**集中放在 `tests/` / `__tests__/` 顶层 —— 业界共识已偏向就近,便于维护与移动。
|
|
42
|
+
|
|
43
|
+
测试辅助资源(fixtures、mock handler)放 `src/test/`:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
src/test/
|
|
47
|
+
├── setup.ts # vitest setup(jest-dom、msw server、active-context 重置)
|
|
48
|
+
├── handlers.ts # msw handler 集合(必须断言 X-Tenant-Id 等 header)
|
|
49
|
+
├── render.tsx # 自定义 render(包 Provider:QueryClient + Tenant + Region)
|
|
50
|
+
└── fixtures/
|
|
51
|
+
├── instance.ts # 测试数据工厂
|
|
52
|
+
└── tenant.ts # 租户工厂
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## §2 · 应该写什么测试(优先级)
|
|
58
|
+
|
|
59
|
+
按 ROI 排序:
|
|
60
|
+
|
|
61
|
+
| 优先级 | 类型 | 例子 | 必测? |
|
|
62
|
+
| ------ | --------------------------------------- | ---------------------------------------------- | -------------- |
|
|
63
|
+
| P0 | 纯函数(`src/utils/`、`src/services/`) | `formatMoney`、`isValidInstance` | ✅ **必测** |
|
|
64
|
+
| P0 | zod schema | `CreateInstanceInputSchema.parse(...)` | ✅ **必测** |
|
|
65
|
+
| P0 | 关键业务路径(创建实例 / 释放 / 切租户) | E2E-style 组件测 | ✅ **必测** |
|
|
66
|
+
| P0 | **interceptor / active-context 切换** | 切租户后请求带新 X-Tenant-Id,旧缓存 invalidate | ✅ **必测** |
|
|
67
|
+
| P0 | **danger 流程(useDangerConfirm)** | 输入正确名才 enable 按钮、关闭后状态重置 | ✅ **必测** |
|
|
68
|
+
| P1 | Hook(数据 hook + 自定义 UI hook) | `useInstanceList`、`useDebounce` | 推荐 |
|
|
69
|
+
| P1 | 复用业务组件(`src/components/`) | `CloudBadge` 各 cloud 渲染 | 推荐 |
|
|
70
|
+
| P2 | 页面组件 | 用 RTL 渲染 + msw mock | 可选 |
|
|
71
|
+
| ❌ | ui 包源码 | 已在 `@teamix-evo/ui` 包内测过 | **不要重复测** |
|
|
72
|
+
| ❌ | biz-ui/uni-manager 包源码(如 um-topbar) | 已在 biz-ui 包内测过 | **不要重复测** |
|
|
73
|
+
| ❌ | 三方库 | `react-router`、`react-query` | **不要测** |
|
|
74
|
+
|
|
75
|
+
**红线**:不要为了凑覆盖率数字测 `Button` 能不能渲染、`React.useState` 能不能用。
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## §3 · 标准测试模式
|
|
80
|
+
|
|
81
|
+
### 纯函数
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// src/utils/format.test.ts
|
|
85
|
+
import { describe, it, expect } from 'vitest';
|
|
86
|
+
import { formatCloudProvider } from './cloud';
|
|
87
|
+
|
|
88
|
+
describe('formatCloudProvider', () => {
|
|
89
|
+
it('aws → Amazon Web Services', () => {
|
|
90
|
+
expect(formatCloudProvider('aws')).toBe('Amazon Web Services');
|
|
91
|
+
});
|
|
92
|
+
it('未知 cloud 回退到原值', () => {
|
|
93
|
+
expect(formatCloudProvider('unknown' as never)).toBe('unknown');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
约定:
|
|
99
|
+
|
|
100
|
+
- 一个 `describe` 一个函数
|
|
101
|
+
- 一个 `it` 一个行为(不堆 5 个 expect 测无关行为)
|
|
102
|
+
- 测试名是**行为描述**,不是函数名
|
|
103
|
+
|
|
104
|
+
### Service 函数(用 msw,**必须校验 tenant/region header**)
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// src/test/handlers.ts
|
|
108
|
+
import { http, HttpResponse } from 'msw';
|
|
109
|
+
|
|
110
|
+
// uni-manager 强约束:每个 handler 都要断言 tenant header
|
|
111
|
+
function assertTenantHeader(request: Request): string {
|
|
112
|
+
const tenantId = request.headers.get('X-Tenant-Id');
|
|
113
|
+
if (!tenantId) {
|
|
114
|
+
throw new Error('Missing X-Tenant-Id header — interceptor 未注入或被绕过');
|
|
115
|
+
}
|
|
116
|
+
return tenantId;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const handlers = [
|
|
120
|
+
http.get('/api/instances', ({ request }) => {
|
|
121
|
+
const tenantId = assertTenantHeader(request);
|
|
122
|
+
const regionId = request.headers.get('X-Region-Id');
|
|
123
|
+
return HttpResponse.json({
|
|
124
|
+
list: [{ id: 'i-1', tenantId, regionId, status: 'running' }],
|
|
125
|
+
});
|
|
126
|
+
}),
|
|
127
|
+
http.post('/api/instances', async ({ request }) => {
|
|
128
|
+
assertTenantHeader(request);
|
|
129
|
+
const body = (await request.json()) as Record<string, unknown>;
|
|
130
|
+
return HttpResponse.json({ id: 'i-new', ...body });
|
|
131
|
+
}),
|
|
132
|
+
// 跨云聚合:cloud header 必须存在
|
|
133
|
+
http.get('/api/cloud/instances', ({ request }) => {
|
|
134
|
+
const cloud = request.headers.get('X-Cloud-Provider');
|
|
135
|
+
if (!cloud) throw new Error('Missing X-Cloud-Provider header');
|
|
136
|
+
return HttpResponse.json({ list: [], cloud });
|
|
137
|
+
}),
|
|
138
|
+
];
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// src/test/setup.ts
|
|
143
|
+
import { setupServer } from 'msw/node';
|
|
144
|
+
import { afterAll, afterEach, beforeAll, beforeEach } from 'vitest';
|
|
145
|
+
import '@testing-library/jest-dom/vitest';
|
|
146
|
+
import { handlers } from './handlers';
|
|
147
|
+
import { setActiveTenant, setActiveRegion } from '@/lib/active-context';
|
|
148
|
+
|
|
149
|
+
const server = setupServer(...handlers);
|
|
150
|
+
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
|
|
151
|
+
afterEach(() => server.resetHandlers());
|
|
152
|
+
afterAll(() => server.close());
|
|
153
|
+
|
|
154
|
+
// uni-manager 必须:每个 test 前重置 active-context,避免上一 test 污染
|
|
155
|
+
beforeEach(() => {
|
|
156
|
+
setActiveTenant({ id: 't-default', name: 'Default Tenant' });
|
|
157
|
+
setActiveRegion({ id: 'cn-hangzhou', name: '华东1' });
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// src/services/instance.test.ts
|
|
163
|
+
import { describe, it, expect } from 'vitest';
|
|
164
|
+
import { listInstances } from './instance';
|
|
165
|
+
import { setActiveTenant } from '@/lib/active-context';
|
|
166
|
+
|
|
167
|
+
describe('listInstances', () => {
|
|
168
|
+
it('返回实例列表(自动带当前 tenant header)', async () => {
|
|
169
|
+
const instances = await listInstances();
|
|
170
|
+
expect(instances).toHaveLength(1);
|
|
171
|
+
expect(instances[0].tenantId).toBe('t-default');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('切换 active tenant 后,后续请求带新 tenantId', async () => {
|
|
175
|
+
setActiveTenant({ id: 't-other', name: 'Other Tenant' });
|
|
176
|
+
const instances = await listInstances();
|
|
177
|
+
expect(instances[0].tenantId).toBe('t-other');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Hook(包 Provider 树)
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
// src/test/render.tsx
|
|
186
|
+
import { render, renderHook, type RenderOptions } from '@testing-library/react';
|
|
187
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
188
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
189
|
+
import { TenantProvider } from '@/contexts/TenantContext';
|
|
190
|
+
import { RegionProvider } from '@/contexts/RegionContext';
|
|
191
|
+
|
|
192
|
+
function createWrapper(route = '/') {
|
|
193
|
+
return function Wrapper({ children }: { children: React.ReactNode }) {
|
|
194
|
+
const qc = new QueryClient({
|
|
195
|
+
defaultOptions: { queries: { retry: false } },
|
|
196
|
+
});
|
|
197
|
+
return (
|
|
198
|
+
<QueryClientProvider client={qc}>
|
|
199
|
+
<TenantProvider>
|
|
200
|
+
<RegionProvider>
|
|
201
|
+
<MemoryRouter initialEntries={[route]}>{children}</MemoryRouter>
|
|
202
|
+
</RegionProvider>
|
|
203
|
+
</TenantProvider>
|
|
204
|
+
</QueryClientProvider>
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function renderWithProviders(
|
|
210
|
+
ui: React.ReactElement,
|
|
211
|
+
{ route = '/', ...options }: RenderOptions & { route?: string } = {},
|
|
212
|
+
) {
|
|
213
|
+
return render(ui, { wrapper: createWrapper(route), ...options });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function renderHookWithProviders<R, P>(
|
|
217
|
+
hook: (props: P) => R,
|
|
218
|
+
{ route = '/' }: { route?: string } = {},
|
|
219
|
+
) {
|
|
220
|
+
return renderHook(hook, { wrapper: createWrapper(route) });
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
// src/hooks/useInstanceList.test.tsx
|
|
226
|
+
import { waitFor } from '@testing-library/react';
|
|
227
|
+
import { renderHookWithProviders } from '@/test/render';
|
|
228
|
+
import { useInstanceList } from './useInstanceList';
|
|
229
|
+
|
|
230
|
+
it('加载实例列表(queryKey 含 tenantId)', async () => {
|
|
231
|
+
const { result } = renderHookWithProviders(() =>
|
|
232
|
+
useInstanceList({ status: 'all' }),
|
|
233
|
+
);
|
|
234
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
235
|
+
expect(result.current.data).toHaveLength(1);
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 组件(testing-library 范式:测行为不测实现)
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
// src/components/InstanceStatusBadge.test.tsx
|
|
243
|
+
import { render, screen } from '@testing-library/react';
|
|
244
|
+
import { InstanceStatusBadge } from './InstanceStatusBadge';
|
|
245
|
+
|
|
246
|
+
it('running 状态显示"运行中"', () => {
|
|
247
|
+
render(<InstanceStatusBadge status="running" />);
|
|
248
|
+
expect(screen.getByText('运行中')).toBeInTheDocument();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('terminated 状态有 destructive 视觉', () => {
|
|
252
|
+
render(<InstanceStatusBadge status="terminated" />);
|
|
253
|
+
expect(screen.getByText('已销毁')).toHaveClass('bg-destructive');
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### 用户交互(`user-event`)
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import userEvent from '@testing-library/user-event';
|
|
261
|
+
import { renderWithProviders } from '@/test/render';
|
|
262
|
+
|
|
263
|
+
it('释放实例:必须输入完整名称才能确认', async () => {
|
|
264
|
+
const onConfirm = vi.fn();
|
|
265
|
+
renderWithProviders(
|
|
266
|
+
<DangerConfirmDialog
|
|
267
|
+
open
|
|
268
|
+
title="释放实例 i-12345"
|
|
269
|
+
resourceName="i-12345"
|
|
270
|
+
onConfirm={onConfirm}
|
|
271
|
+
onClose={() => {}}
|
|
272
|
+
/>,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const confirmBtn = screen.getByRole('button', { name: '确认释放' });
|
|
276
|
+
expect(confirmBtn).toBeDisabled();
|
|
277
|
+
|
|
278
|
+
await userEvent.type(screen.getByLabelText(/输入实例 ID 确认/), 'i-1234');
|
|
279
|
+
expect(confirmBtn).toBeDisabled(); // 不完全匹配
|
|
280
|
+
|
|
281
|
+
await userEvent.type(screen.getByLabelText(/输入实例 ID 确认/), '5');
|
|
282
|
+
expect(confirmBtn).toBeEnabled();
|
|
283
|
+
|
|
284
|
+
await userEvent.click(confirmBtn);
|
|
285
|
+
expect(onConfirm).toHaveBeenCalledOnce();
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**不要**用 `fireEvent` —— 它不模拟真实键盘 / 鼠标事件序列,`user-event` 才贴近真实用户。
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## §4 · 关键集成测试:切租户 → invalidate → 新数据
|
|
294
|
+
|
|
295
|
+
uni-manager 的核心交互之一是切换租户,**必须**有集成测试覆盖:
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
// src/contexts/TenantContext.test.tsx
|
|
299
|
+
import { renderWithProviders } from '@/test/render';
|
|
300
|
+
import userEvent from '@testing-library/user-event';
|
|
301
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
302
|
+
|
|
303
|
+
it('切换租户:invalidate 旧 query + 新请求带新 tenantId', async () => {
|
|
304
|
+
renderWithProviders(<TenantSwitcherTestHarness />);
|
|
305
|
+
|
|
306
|
+
// 初始:t-default 的实例
|
|
307
|
+
await waitFor(() => screen.getByText(/i-1.*t-default/));
|
|
308
|
+
|
|
309
|
+
// 切换到 t-other
|
|
310
|
+
await userEvent.click(screen.getByRole('button', { name: '切换到 t-other' }));
|
|
311
|
+
|
|
312
|
+
// 应该看到新 tenant 的实例(msw 根据 header 返回不同数据)
|
|
313
|
+
await waitFor(() => screen.getByText(/i-1.*t-other/));
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## §5 · 反模式速查
|
|
320
|
+
|
|
321
|
+
| 反模式 | 为什么禁 | 应该 |
|
|
322
|
+
| ---------------------------------------- | --------------------------------- | ----------------------------------------------- |
|
|
323
|
+
| 测 `setState` 是否被调用 | 测了实现,重构即崩 | 测渲染结果 / 用户行为 |
|
|
324
|
+
| `getByTestId` 当默认查询 | data-testid 跟可访问性绑不上 | `getByRole` / `getByLabelText` 优先 |
|
|
325
|
+
| 一个 it 里 expect 10 次无关断言 | 失败定位难 | 拆多个 it |
|
|
326
|
+
| mock 整个 hook(`vi.mock('@/hooks/...')`) | 失去集成价值 | msw mock 后端,hook 真跑 |
|
|
327
|
+
| 自己 mock fetch / axios | 与生产代码路径不一致 | 用 msw |
|
|
328
|
+
| msw handler 不校验 X-Tenant-Id | interceptor 失效不会被发现 | 每个 handler `assertTenantHeader(request)` |
|
|
329
|
+
| 测试间不重置 active-context | 上一 test 污染下一 test 的 tenant | `beforeEach` 重置 |
|
|
330
|
+
| 测试里写 `setTimeout(..., 1000)` 等异步 | 慢且不稳定 | `await waitFor` / `findBy*` |
|
|
331
|
+
| 全局 mock console.error 静默 | 错过真实告警 | 让测试输出 console.error,但 setup 里 fail on it |
|
|
332
|
+
| 追求 80% 覆盖率刷数字 | 测了没价值的代码 | 按"优先级表"测,不看覆盖率盲冲 |
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## §6 · 装机指引
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
pnpm add -D vitest @vitest/ui @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom msw
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
// vite.config.ts
|
|
344
|
+
import { defineConfig } from 'vitest/config';
|
|
345
|
+
|
|
346
|
+
export default defineConfig({
|
|
347
|
+
test: {
|
|
348
|
+
environment: 'jsdom',
|
|
349
|
+
setupFiles: ['./src/test/setup.ts'],
|
|
350
|
+
globals: true, // 可选:免 import describe / it / expect
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
// package.json scripts
|
|
357
|
+
{
|
|
358
|
+
"test": "vitest",
|
|
359
|
+
"test:run": "vitest run",
|
|
360
|
+
"test:ui": "vitest --ui"
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
`msw` 还需要初始化 worker(用于 dev 环境)或 server(node):
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
pnpm exec msw init public/ --save # 浏览器 worker
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## §7 · 何时跑测试
|
|
373
|
+
|
|
374
|
+
- **本地开发**:`pnpm test`(watch 模式),改文件即跑相邻 test
|
|
375
|
+
- **commit 前**:pre-commit hook 跑改动文件的 test(可选,慢就关)
|
|
376
|
+
- **CI**:`pnpm test:run`(必须,跑全量)
|
|
377
|
+
- **PR**:CI 红 → 不合并
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## AI 必须输出的测试日志
|
|
382
|
+
|
|
383
|
+
新增 / 修改业务代码时,AI 必须在响应里说明测试覆盖情况:
|
|
384
|
+
|
|
385
|
+
```
|
|
386
|
+
## 测试覆盖
|
|
387
|
+
|
|
388
|
+
- 新增 src/utils/cloud.ts → 同目录 cloud.test.ts(formatCloudProvider 4 case)
|
|
389
|
+
- 新增 src/services/instance.ts:releaseInstance → instance.test.ts(成功 / 422 / tenant header 校验)
|
|
390
|
+
- 新增 src/hooks/useReleaseInstance.ts → useReleaseInstance.test.tsx(success / error / 未确认 dangerName 不调 mutate)
|
|
391
|
+
- 复用 src/components/CloudBadge.tsx → 已有 test 覆盖 aws/aliyun/azure,无需补
|
|
392
|
+
- 切租户集成测试: ✅ TenantContext.test.tsx(invalidate + 新 tenantId header)
|
|
393
|
+
- 跳过测: src/pages/instances/new/index.tsx(纯组合,关键逻辑已在 hook / form 层测)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
如果跳过测试,**必须**写明原因(纯组合 / ui 包已测 / biz-ui 已测 / 一次性脚本)。
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: teamix-evo-design-opentrek
|
|
3
|
+
description: |
|
|
4
|
+
Apply OpenTrek design system rules (philosophy, patterns, page-types, brand tone/voice, visual foundations) when AI generates or reviews a full UI screen / page in an OpenTrek-variant project.
|
|
5
|
+
TRIGGER when: user asks to "新建 / 优化 / 重构 一个页面"、"做一个列表页 / 详情页 / 表单页 / 仪表盘"、"create / review / refactor a page、screen、dashboard、template"; intent involves layout structure, page-level information density, or multi-component composition; file write under `src/pages/**` or `src/templates/**`.
|
|
6
|
+
SKIP: single-component edits like "加个按钮"、"改 input 的 label"; pure tokens/theme overrides; pure code refactor with no visual change; teamix-evo lifecycle commands (defer to teamix-evo-manage).
|
|
7
|
+
Coordinates with: teamix-evo-code-opentrek (run alongside when the screen also creates new files).
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# teamix-evo-design-opentrek
|
|
11
|
+
|
|
12
|
+
> OpenTrek 设计体系 — 面向云控制台与企业级 B2B 产品的现代设计语言。
|
|
13
|
+
> 沉稳、克制、可预期,强调高密度信息下的清晰与效率。
|
|
14
|
+
>
|
|
15
|
+
> 本技能是 **完整自包含** 的 OpenTrek 设计规则(非 overlay)。
|
|
16
|
+
> Token 具体值请参照 `tokens/theme.css` 中的 CSS 变量,组件详细 API 通过 MCP `get_component_meta` 按需查询。
|
|
17
|
+
|
|
18
|
+
<!-- teamix-evo:managed:start id="core" -->
|
|
19
|
+
|
|
20
|
+
## 意图路由
|
|
21
|
+
|
|
22
|
+
收到页面相关请求时,按意图分流:
|
|
23
|
+
|
|
24
|
+
| 意图 | 识别关键词 | 执行路径 |
|
|
25
|
+
| -------- | -------------------------------------------------- | ------------------------------------------- |
|
|
26
|
+
| **生成** | "生成" / "创建" / "新建" / "设计一个" / "做一个" | → generation-flow.md 六步流程 |
|
|
27
|
+
| **翻新** | "改造" / "升级" / "翻新" / "对齐规范" / "重新生成" | → generation-flow.md §翻新路径 |
|
|
28
|
+
| **验证** | "检查" / "验证" / "评估" / "是否符合规范" | → checklist.md 逐项对照 |
|
|
29
|
+
| **查询** | "token" / "色值" / "间距" / "组件尺寸" | → foundations.md(MCP `tokens_*` 工具规划中) |
|
|
30
|
+
|
|
31
|
+
## 三阶段 Dispatch
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
阶段 ① 确定页面类型
|
|
35
|
+
→ 读 patterns/page-types.md 决策树
|
|
36
|
+
→ 读 components.md 确认组件集
|
|
37
|
+
|
|
38
|
+
阶段 ② 匹配子类型 + 布局
|
|
39
|
+
- List / Detail / Form → patterns/{list|detail|form}-page.md
|
|
40
|
+
- Dashboard → patterns/dashboard.md
|
|
41
|
+
- Dialog/Sheet/Drawer → 视为复合组件(非页面);走 components.md §3.1 选型
|
|
42
|
+
+ boundaries.md F8 / F9 / S7 / C2 / FF1-FF4
|
|
43
|
+
→ 读 patterns/page-types.md §6 确认页间流转(主流转图 / 规则 / CRUD 速查)
|
|
44
|
+
→ 涉及用户旅程时读 flows.md(A 资源查找 / B 创建 / C 异常 / D 批量)
|
|
45
|
+
|
|
46
|
+
阶段 ③ 生成代码
|
|
47
|
+
→ 读 foundations.md (Token 约束)
|
|
48
|
+
→ 读 boundaries.md (硬规则)
|
|
49
|
+
→ MCP get_component_meta(id) 查组件 Props
|
|
50
|
+
→ 对照 checklist.md 自检
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 文件索引
|
|
54
|
+
|
|
55
|
+
| 文件 | 职责 |
|
|
56
|
+
| ---------------------------------------------------- | -------------------------------------------------------- |
|
|
57
|
+
| [philosophy.md](./philosophy.md) | 设计哲学 + 四大原则 + 方法论 |
|
|
58
|
+
| [generation-flow.md](./generation-flow.md) | AI 六步生成/翻新/验证流程 |
|
|
59
|
+
| [boundaries.md](./boundaries.md) | 硬约束:F/FF/S/C/I 五组共 38 条规则([ERROR]/[WARN]) |
|
|
60
|
+
| [checklist.md](./checklist.md) | 10 项自检清单 |
|
|
61
|
+
| [foundations.md](./foundations.md) | Token 用法 + 排版 + 间距 + 色彩 + 圆角 + 阴影 + 动效 |
|
|
62
|
+
| [components.md](./components.md) | 组件选型决策树 + 双层架构索引 + 组合规则 |
|
|
63
|
+
| [patterns/page-types.md](./patterns/page-types.md) | 5 种页面类型 + 识别决策树 + Zone Map + 页间流转(§6) |
|
|
64
|
+
| [patterns/list-page.md](./patterns/list-page.md) | 列表页模式 (6 种子类型) |
|
|
65
|
+
| [patterns/detail-page.md](./patterns/detail-page.md) | 详情页模式 |
|
|
66
|
+
| [patterns/form-page.md](./patterns/form-page.md) | 表单页模式 (含 wizard) |
|
|
67
|
+
| [patterns/dashboard.md](./patterns/dashboard.md) | Dashboard 数据概览(StatCard / Chart 标准结构) |
|
|
68
|
+
| [flows.md](./flows.md) | 4 条核心用户旅程 + 流转自检 |
|
|
69
|
+
| [brand.md](./brand.md) | OpenTrek 品牌调性 + 文案语气 + 正反例 |
|
|
70
|
+
|
|
71
|
+
<!-- teamix-evo:managed:end -->
|