diff --git a/.factory/mcp.json b/.factory/mcp.json index e69de29b..29585572 100644 --- a/.factory/mcp.json +++ b/.factory/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "shadcn": { + "command": "npx", + "args": ["-y", "shadcn@latest", "mcp"] + } + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7888459b..643fbdf2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,10 +42,10 @@ jobs: pnpm install --frozen-lockfile if [ "${{ steps.changes.outputs.code }}" == "true" ]; then - # 运行所有测试:单元测试 + Storybook 组件测试 + E2E 测试 + 主题检查 - TASKS="typecheck:run build i18n:run theme:run test:run test:storybook e2e:ci e2e:ci:mock e2e:ci:real" + # 运行所有测试:lint + 单元测试 + Storybook 组件测试 + E2E 测试 + 主题检查 + TASKS="lint:run typecheck:run build i18n:run theme:run test:run test:storybook e2e:audit e2e:ci e2e:ci:mock e2e:ci:real" else - TASKS="typecheck:run build i18n:run theme:run test:run test:storybook" + TASKS="lint:run typecheck:run build i18n:run theme:run test:run test:storybook" fi # Run turbo and capture output, pipefail ensures we get turbo's exit code @@ -115,10 +115,10 @@ jobs: E2E_TEST_MNEMONIC: ${{ secrets.E2E_TEST_MNEMONIC }} run: | if [ "${{ steps.changes.outputs.code }}" == "true" ]; then - # 运行所有测试:单元测试 + Storybook 组件测试 + E2E 测试 + 主题检查 - pnpm turbo run typecheck:run build i18n:run theme:run test:run test:storybook e2e:ci e2e:ci:mock e2e:ci:real + # 运行所有测试:lint + 单元测试 + Storybook 组件测试 + E2E 测试 + 主题检查 + pnpm turbo run lint:run typecheck:run build i18n:run theme:run test:run test:storybook e2e:audit e2e:ci e2e:ci:mock e2e:ci:real else - pnpm turbo run typecheck:run build i18n:run theme:run test:run test:storybook + pnpm turbo run lint:run typecheck:run build i18n:run theme:run test:run test:storybook fi checks-standard: diff --git a/.gitignore b/.gitignore index 49af91d8..923d15b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,17 @@ # See http://help.github.com/ignore-files/ for more about ignoring files. # compiled output -/dist -/dists -/dist-web -/dist-dweb -/tmp -/.tmp -/.chat -/out-tsc -/release +dist/ +dists/ +dist-web/ +dist-dweb/ +tmp/ +.tmp/ +.chat/ +out-tsc/ +release/ /gh-pages -/storybook-static +storybook-static/ /docs/por # VitePress @@ -57,12 +57,17 @@ migrations.json Thumbs.db # Playwright -/e2e/test-results/ -/e2e/report/ -/playwright-report/ +e2e/test-results/ +e2e/report/ +playwright-report/ /blob-report/ -/playwright/.cache/ +playwright/.cache/ +# Miniapps test results +**/test-results/ # 注意: e2e/__screenshots__/ 基线截图需要提交到 git + +# Temporary demo files +demo-*.html # Ignore CCCC runtime domain (Ephemeral mode) /.cccc/** diff --git "a/docs/white-book/00-\345\277\205\350\257\273/best-practices.md" "b/docs/white-book/00-\345\277\205\350\257\273/best-practices.md" index 9a697ce4..5d56b5eb 100644 --- "a/docs/white-book/00-\345\277\205\350\257\273/best-practices.md" +++ "b/docs/white-book/00-\345\277\205\350\257\273/best-practices.md" @@ -19,3 +19,9 @@ - ❌ password/Password(宽泛含义) → ✅ walletLock(钱包锁)/ twoStepSecret(安全密码)/ payPassword(支付密码)等具体命名 - 圆形元素必须使用 aspect-square 标记,与 w-*/h-*/size-* 不冲突,是规范要求 - 组件尺寸属性要考虑响应式布局,如 lg 尺寸应包含 @xs:w-12 等容器查询断点 +- ❌ JS scroll 事件监听 → ✅ CSS scroll-driven animations(animation-timeline: scroll()) +- 滚动驱动动画使用 `scroll-timeline: --name block` 定义 + `animation-timeline: --name` 绑定 +- 跨层级绑定使用 `timeline-scope: --name` 在公共祖先上声明 +- 紧凑头部效果使用 `animation-range: 0 80px` 限制动画范围 +- ⚠️ scroll-driven animations 是渐进增强:初始状态必须是可用的(如 opacity-0),不支持时保持初始状态 +- E2E 截图变更后运行 `pnpm e2e:audit` 检查残留截图,详见白皮书 08-测试篇/03-Playwright配置/e2e-best-practices diff --git "a/docs/white-book/08-\346\265\213\350\257\225\347\257\207/01-\346\265\213\350\257\225\347\255\226\347\225\245/index.md" "b/docs/white-book/08-\346\265\213\350\257\225\347\257\207/01-\346\265\213\350\257\225\347\255\226\347\225\245/index.md" index b9d620aa..d5dc451f 100644 --- "a/docs/white-book/08-\346\265\213\350\257\225\347\257\207/01-\346\265\213\350\257\225\347\255\226\347\225\245/index.md" +++ "b/docs/white-book/08-\346\265\213\350\257\225\347\257\207/01-\346\265\213\350\257\225\347\255\226\347\225\245/index.md" @@ -4,7 +4,39 @@ --- -## 23.1 测试分层 +## 23.1 测试环境的本质区别 + +**Storybook v10 的核心价值:与 Vitest 深度融合,为组件测试提供真实浏览器环境。** + +| 环境 | 技术栈 | 特点 | 适用场景 | +|-----|-------|------|---------| +| **jsdom/happy-dom** | Vitest 单元测试 | 虚拟 DOM,快速但非真实浏览器 | 纯逻辑、算法、工具函数 | +| **真实浏览器 (Storybook)** | Storybook + Vitest | 真实渲染、真实 CSS、真实事件 | 组件渲染、复杂交互、样式验证 | +| **真实浏览器 (E2E)** | Playwright | 完整应用实例 | 端到端用户流程 | + +### 为什么需要三层测试? + +``` +jsdom/happy-dom 的局限性: +- 不支持真实 CSS 计算(layout、动画) +- 不支持某些 Web API(ResizeObserver、IntersectionObserver 需 polyfill) +- 事件模型与真实浏览器有差异 + +Storybook 组件测试的价值: +- 在真实 Chromium 中渲染组件 +- 验证 CSS 样式、动画、响应式布局 +- 测试真实的用户交互(点击、输入、手势) +- 每个 Story 自动成为冒烟测试 + +E2E 与组件测试的区别: +- E2E 测试完整应用流程(页面导航、状态持久化) +- 组件测试聚焦单个组件的隔离行为 +- 组件测试更快、更稳定、更容易定位问题 +``` + +--- + +## 23.2 测试分层 | 层级 | 工具 | 测试内容 | 运行频率 | |-----|------|---------|---------| @@ -14,7 +46,7 @@ --- -## 23.2 测试命令 +## 23.3 测试命令 ```bash pnpm test # 单元测试 (*.test.ts) @@ -25,7 +57,7 @@ pnpm test:coverage # 单元测试 + 覆盖率报告 --- -## 23.3 Storybook + Vitest 集成 +## 23.4 Storybook + Vitest 集成 项目使用 `@storybook/addon-vitest` 将 Stories 作为测试用例运行。 @@ -93,7 +125,7 @@ export const Interactive: Story = { --- -## 23.4 覆盖率目标 +## 23.5 覆盖率目标 | 类型 | 目标 | |-----|------| @@ -104,7 +136,7 @@ export const Interactive: Story = { --- -## 23.5 测试优先级 +## 23.6 测试优先级 | 优先级 | 测试内容 | |-------|---------| @@ -115,7 +147,7 @@ export const Interactive: Story = { --- -## 23.6 测试命名规范 +## 23.7 测试命名规范 ```typescript describe('WalletStore', () => { @@ -129,7 +161,7 @@ describe('WalletStore', () => { --- -## 23.7 CI 集成 +## 23.8 CI 集成 CI 流水线运行以下测试: diff --git "a/docs/white-book/08-\346\265\213\350\257\225\347\257\207/03-Playwright\351\205\215\347\275\256/e2e-best-practices.md" "b/docs/white-book/08-\346\265\213\350\257\225\347\257\207/03-Playwright\351\205\215\347\275\256/e2e-best-practices.md" index a88f9a33..07ec1d8c 100644 --- "a/docs/white-book/08-\346\265\213\350\257\225\347\257\207/03-Playwright\351\205\215\347\275\256/e2e-best-practices.md" +++ "b/docs/white-book/08-\346\265\213\350\257\225\347\257\207/03-Playwright\351\205\215\347\275\256/e2e-best-practices.md" @@ -291,3 +291,50 @@ await page.click('[data-testid="submit-button"]') 4. 更新测试代码使用新的 testid 5. 本地运行测试确认通过 6. 更新 testid 文档列表 + +--- + +## 截图审计 + +### 残留截图检测 + +项目使用 `@biochain/e2e-tools` 检测残留截图(不再被测试引用的截图文件)。 + +```bash +# 主应用 +pnpm e2e:audit # 检查所有项目 +pnpm e2e:audit:run # 仅检查主应用 + +# miniapps +cd miniapps/forge && pnpm e2e:audit +cd miniapps/teleport && pnpm e2e:audit +``` + +### 工作原理 + +1. 扫描 `e2e/*.spec.ts` 文件 +2. 提取所有 `toHaveScreenshot('name.png')` 调用 +3. 对比 `e2e/__screenshots__/` 目录中的实际文件 +4. 报告未被引用的残留截图 + +### 处理残留截图 + +```bash +# 查看残留截图 +pnpm e2e:audit:run + +# 自动删除残留截图 +bunx @biochain/e2e-tools audit --fix +``` + +### CI 集成 + +`--strict` 模式下发现残留截图会导致 exit code 1,用于 CI 流程中阻止合并。 + +### 常见场景 + +| 场景 | 处理方式 | +|------|---------| +| 删除了测试用例 | 运行 `--fix` 删除对应截图 | +| 重命名了截图 | 删除旧截图或更新测试代码 | +| 截图未被引用 | 检查是否遗漏 `toHaveScreenshot` 调用 | diff --git "a/docs/white-book/10-\347\224\237\346\200\201\347\257\207/01-\346\236\266\346\236\204\350\256\276\350\256\241.md" "b/docs/white-book/10-\347\224\237\346\200\201\347\257\207/01-\346\236\266\346\236\204\350\256\276\350\256\241.md" new file mode 100644 index 00000000..771f5166 --- /dev/null +++ "b/docs/white-book/10-\347\224\237\346\200\201\347\257\207/01-\346\236\266\346\236\204\350\256\276\350\256\241.md" @@ -0,0 +1,132 @@ +# 架构设计 + +## 系统架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ KeyApp (Host) │ +│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────┐ │ +│ │ EcosystemTab│ │ BioProvider │ │ MiniappRegistry │ │ +│ │ (生态 Tab) │ │ (Server SDK) │ │ (订阅管理) │ │ +│ └──────┬──────┘ └──────┬───────┘ └─────────┬─────────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐│ +│ │ PostMessage Bridge ││ +│ │ (双向通信: request/response + events) ││ +│ └─────────────────────────────────────────────────────────┘│ +└────────────────────────────────────────────────────────────┘ + │ iframe sandbox +┌────────────────────────────┼────────────────────────────────┐ +│ MiniApp (Client) │ +│ ┌─────────────────────────┴───────────────────────────────┐│ +│ │ @biochain/bio-sdk ││ +│ │ window.bio = { request, on, off, ... } ││ +│ └─────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────┘ +``` + +## 通信协议 + +### 请求消息 + +```typescript +interface RequestMessage { + type: 'bio_request' + id: string // 唯一请求 ID + method: string // 方法名 (bio_requestAccounts, bio_signMessage, ...) + params?: unknown[] // 参数 +} +``` + +### 响应消息 + +```typescript +interface ResponseMessage { + type: 'bio_response' + id: string // 对应请求 ID + success: boolean // 是否成功 + result?: unknown // 成功时的结果 + error?: { // 失败时的错误 + code: number + message: string + data?: unknown + } +} +``` + +### 事件消息 + +```typescript +interface EventMessage { + type: 'bio_event' + event: string // 事件名 + args: unknown[] // 事件参数 +} +``` + +## 安全模型 + +### iframe 沙箱 + +```html +