Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,31 @@ jobs:
- name: Lint
run: npm run lint

- name: Typecheck
run: npm run typecheck

- name: Test
run: npm test

- name: Build
run: npm run build

# E2E 单列一个 job:需要下载浏览器,比 lint/test/build 慢,隔离开不拖慢主检查。
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm

- name: Install
run: npm ci

- name: Install Playwright browser
run: npx playwright install --with-deps chromium

- name: E2E
run: npm run test:e2e
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ _artifact_reviews/
*.njsproj
*.sln
*.sw?

# Playwright E2E 产物
/test-results/
/playwright-report/
/blob-report/
/.playwright/
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ src/
- 徽章云端持久化
- AI 助手 Edge Function 路径

详见 `supabase/README.md` `supabase/functions/ai-chat/README.md`。
详见 `supabase/README.md` 与表结构 `supabase/schema.sql`。

## 🌐 路由

Expand Down
60 changes: 60 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 贡献指南 · CS Hub

欢迎参与。本文档说明本地开发、质量门、代码约定与提交流程。

## 本地开发

```bash
npm install # Node ≥ 20(见 package.json engines)
npm run dev # http://localhost:5173,无需 Supabase 即可运行
```

## 质量门(提交前务必本地通过)

| 命令 | 作用 |
|---|---|
| `npm run lint` | ESLint(含 react-hooks 规则) |
| `npm test` | Vitest 全量单测 |
| `npm run typecheck` | 类型检查(渐进式,见下方「类型」) |
| `npm run build` | 生产构建 |
| `npm run check` | 一次性跑 lint + typecheck + test + build |
| `npm run test:e2e` | Playwright 端到端(首次需 `npx playwright install chromium`) |

**CI(`.github/workflows/ci.yml`)会在每次 push / PR 上跑同样的检查,红了不合并。**
所以本地先绿再推——这是 AI 辅助开发最重要的安全网。

## 代码约定

详见 [CLAUDE.md](CLAUDE.md),要点:

- **新组件强制 Tailwind**(`@theme` token / `@layer components` 复用类),老组件保留 inline style 直到重构。
- **可视化组件**:SVG 节点用稳定 `id` 作 key + `transform` 过渡,不要直接绑 `cx/cy`;颜色变化走 `fill transition`。
- **无障碍**:状态不要只靠颜色区分(照顾红绿色盲),配合形状/符号/文字;交互元素给 `aria-label`。
- **异步/存储/登录态**:任何涉及网络失败、账号切换、跨标签的改动,先想清失败场景(参考 `src/services/storage/SyncService.js` 的竞态处理与测试)。

## 新增一个算法

只需动 ~3 个文件(详情页/学科页/侧栏/搜索会自动列出):

1. **算法函数**(纯函数,返回步骤数组)→ `src/algorithms/<subject>/<name>.js`
2. **元数据 + 学习内容** → `src/data/algorithms/<subject>.js`
(必填:slug, name, category, difficulty, fn, viz, timeComplexity, description, intuition, pseudocode, code{cpp,python}, applications)
3. **Playground** → `src/components/playgrounds/<Name>Playground.jsx`,并注册到 `AlgorithmPlaygroundFor`
4. **课后题** → `src/data/quizzes.js`(3 题)
5. 新增 category 时 → `src/data/subjects.js` 加映射

新算法**必须**在 `src/data/algorithmSmoke.test.jsx` 的全量冒烟里能渲染+单步不崩;边界正确性参考 `src/data/sortingCorrectness.test.js`。

## 类型

项目为纯 JS,正在渐进迁移类型检查([FRONTEND_SELF_CHECK.md](docs/FRONTEND_SELF_CHECK.md) 维度 10):

- `jsconfig.json`:给编辑器提供跨文件跳转与 JSDoc 类型提示。
- `npm run typecheck`:对**已补 JSDoc 的子集**开 `checkJs` 强制检查(`tsconfig.typecheck.json` 的 `include` 列表)。
- 新写纯逻辑模块请补 JSDoc 并加入该 include 列表,让强制检查范围只增不减。

## 提交与 PR

- 一个逻辑改动一个 commit,信息用 `type(scope): 简述`(feat/fix/perf/chore/test/docs…)。
- 修 bug 尽量配一个能复现的测试(防回归)。
- 提 PR 前确保 `npm run check` 与 `npm run typecheck` 均绿。
152 changes: 152 additions & 0 deletions docs/FRONTEND_SELF_CHECK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# 前端工业级自检清单 · CS Hub

> 给"零基础 vibe coding"的自己用。目的:把"我不知道自己不知道什么"变成一张可勾选的清单。
>
> **怎么用**:
> 1. 每个维度先读「合格长什么样」——这就是大厂项目默认会做、但小白根本不知道存在的东西。
> 2. 对照「当前状态」看自己处于哪一档。评级:🟢达标 / 🟡部分 / 🔴缺失 / ⚪暂不适用。
> 3. 想深挖某一维度,直接把该维度的「问 AI 的话术」粘给任意 AI。
> 4. 每隔一段时间(尤其大改后)重跑一遍——工业级不是一次达标,是**持续不塌**。
>
> 评估日期:2026-07(本轮性能优化 + 安全审计 + bug 修复之后)

---

## 🎯 核心心法(先读这段)

小白项目和大厂项目最大的差距**不在视觉,而在"看不见的地方"**。
让页面好看、动画顺滑,AI 随手就能做到——这是最容易达标、也最容易骗过自己的一层。
真正拉开差距的是下面这些**演示时永远遇不到、但线上一定会爆**的维度。

**判断一个维度做没做到,别问"看起来怎么样",要问"如果……会发生什么"**:
如果用户乱输入?如果断网?如果同时打开两个标签?如果换了账号?如果部署到一半?

---

## 维度清单

### 1. 正确性 & 边界输入 🟢

- **合格长什么样**:核心逻辑对"空、单个、重复、超大、负数、非法字符"等边界输入都不崩、不算错;关键路径有测试**真正运行代码**验证结果,而不只是"函数存在"。
- **当前状态**:🟡→🟢。基数排序负数崩溃已用偏移法修复 + 回归测试;并新增 `sortingCorrectness.test.js`——每个排序算法在其输入域内(空/单/重复/逆序/大值,负数仅测支持的)都断言"结果有序、长度守恒"。**这类边界基线本可提前抓到那个 bug,现在补上了。** 计数排序对负数仍算错,但其输入框挡负数、碰不到。
- **问 AI 的话术**:
> "把这个模块的核心函数,用边界输入(空/单元素/重复值/负数/超大值/非法字符)各跑一遍,验证结果正确性,不只是不崩。列出哪些输入会出问题。"

### 2. 数据一致性 & 并发 🟢

- **合格长什么样**:多个操作同时发生、网络失败、用户切换身份时,数据不丢、不串、不复活。删除能正确传播,失败能重试。
- **当前状态**:🔴→🟢 本轮重点修复。`SyncService.js` 原有 4 个竞态(失败丢数据 / 换账号数据串号 / 清空后僵尸复活 / 删除不跨设备传播)已全部修复,改用 LWW 合并 + 失败重试队列,并配了 5 个竞态测试。**这是本轮最有价值的改动——这类 bug 演示时 100% 遇不到。**
- **问 AI 的话术**:
> "审查所有涉及异步、网络、本地存储、登录态的代码:如果请求失败会怎样?如果用户操作到一半切换账号/登出会怎样?如果同时打开两个标签?列出具体失败场景。"

### 3. 错误处理 & 可观测性 🟢

- **合格长什么样**:任何组件崩溃不会白屏整站;线上报错开发者能**收到**(而不是等用户投诉);部署后旧页面能自愈。
- **当前状态**:🔴→🟢。已有全局 `ErrorBoundary` + `monitoring.js`(window error / unhandledrejection / 边界都上报到 Vercel,带去重和滑动窗口限额),chunk 加载失败会自动刷新恢复。原来这些**一个都没有**。
- **问 AI 的话术**:
> "如果某个组件运行时抛错,用户看到什么?我作为开发者能不能知道线上出了错?有没有全局错误边界和上报?"

### 4. 测试 🟢

- **合格长什么样**:测试**真正执行业务逻辑**并断言结果;覆盖关键交互路径,不只是数据结构;有"模拟真人"的端到端流程;改坏了会红。
- **当前状态**:🟡→🟢。单测层 405 个(20 文件):`src/data/algorithmSmoke.test.jsx` 渲染全部 123 个算法 playground 并单步走抓崩溃,`sortingCorrectness.test.js` 跑边界正确性,`docConsistency.test.js` 守文档漂移。**新增 E2E 层**(`e2e/smoke.spec.js` + `playwright.config.js`)——真实 Chromium 跑 4 个关键流程:首页加载、算法单步描述变化、主题切换 `data-theme` 翻转、**收藏跨刷新持久化 + 落 localStorage**。CI 里 E2E 单列一个 job(装浏览器、隔离开不拖慢主检查)。这层是 jsdom 覆盖不到的(真路由、真持久化、真浏览器)。
- **问 AI 的话术**:
> "我的测试是真的运行了业务代码并断言结果,还是只检查'函数/组件存在'?帮我找出哪些关键路径完全没测到。要不要加一个'渲染所有页面+基本交互'的冒烟测试?"

### 5. CI/CD & 自动化守门 🟢

- **合格长什么样**:每次提交/PR 自动跑 lint+test+build,红了不许合;有依赖安全的自动巡检。
- **当前状态**:🔴→🟢。本轮新增 `.github/workflows/ci.yml`(push/PR 跑 lint+test+build)+ `dependabot.yml`(每周依赖安全更新)。**对 AI 辅助开发这是最重要的安全网**——AI 改坏了什么,由机器拦,而不是等你部署后自己发现(本轮 CI 上线第一天就抓出了 lockfile 不同步)。
- **问 AI 的话术**:
> "帮我配一个最简单的 CI,每次 push 自动跑 lint、测试、构建。哪一步失败就挡住。"

### 6. 安全 🟢

- **合格长什么样**:用户输入不会变成可执行脚本(XSS);数据库有行级权限(别人偷不到你的数据);密钥不进代码库;不自己存明文密码。
- **当前状态**:🟢(本来就好)。数据库 RLS 每张表都配对且写法正确;拼 HTML 的地方都经 `escapeHtml` + URL 白名单,还有测试;只用 GitHub OAuth 不存密码;`.env` 未入库;`npm audit` 0 漏洞。**这块你做得比多数小白好。**
- **问 AI 的话术**:
> "以安全视角审查:用户输入有没有可能变成可执行脚本?后端数据是不是每张表都有权限控制?有没有密钥/token 意外提交进了仓库?"

### 7. 性能 🟢

- **合格长什么样**:首屏只加载必要代码;大模块按需拉;避免无意义的重复渲染。
- **当前状态**:🟢(意识在线)。33 个页面全部路由懒加载 + 代码分割;AI 课程从 160KB 巨石拆成按章动态加载(目录页 -92% 负载);导航 hover 预热 chunk;`ProgressContext` 改 store 化避免全局重渲染。
- **问 AI 的话术**:
> "帮我看构建产物:首屏加载了哪些不该在首屏加载的东西?哪些大模块可以按需加载?有没有组件在无意义地反复重渲染?"

### 8. 无障碍(a11y) 🟢

- **合格长什么样**:屏幕阅读器能读、键盘能全程操作、尊重"减弱动态"偏好、不靠纯颜色传递信息(照顾色盲)、`lang` 标对。
- **当前状态**:🟡→🟢。`lang=zh-CN`、`prefers-reduced-motion` 全局块、键盘快捷键作用域均已就位。三大 viz 家族都补齐了**非颜色语义通道 + 屏幕阅读器摘要**:
- 排序 viz:形状指针(比较◇/交换⇄/完成✓),`Legend` 支持 `symbol`。
- 图 viz(`GraphViz`):未访问节点虚线、已访问实线(灰度可辨);`SvgCanvas` 新增 `ariaLabel` → `role="img"`,动态朗读"当前访问节点 X,已访问 N 个"。
- 树 viz(`SvgTreeViz`):红黑树节点加 `R`/`B` 字母角标(红黑是纯颜色语义,红绿色盲难辨);同样输出 aria 摘要。
- **模式已固化**(`SvgCanvas.ariaLabel` + 策略返回 `badge`),新 viz 复用即可。剩余长尾小 viz 可按此增量补。
- **问 AI 的话术**:
> "以无障碍标准审视:键盘能不能操作全部功能?屏幕阅读器读起来通顺吗?有没有只靠颜色传递信息的地方(色盲会看不懂)?`lang` 标对了吗?"

### 9. SEO & 分享元数据 🟢

- **合格长什么样**:有 title/description、Open Graph(转发有预览卡)、路由切换更新标题。
- **当前状态**:🟢(本轮补齐)。title/description/OG 标签都有,路由切换会更新 `document.title`。**可选加强**:sitemap.xml / robots.txt、给算法详情页做各自的 OG(目前是全站统一)。
- **问 AI 的话术**:
> "分享到微信/推特时预览长什么样?搜索引擎能正确识别每个页面吗?路由切换标题会变吗?"

### 10. 类型安全 & 代码可维护性 🟢

- **合格长什么样**:有类型系统(TypeScript)或至少 JSDoc + checkJs 兜底,改一处不会悄悄弄坏另一处;单文件不过长;风格统一。
- **当前状态**:🔴→🟢。`jsconfig.json` 给编辑器提供跳转/补全/JSDoc 提示;更进一步,`npm run typecheck`(`tsconfig.typecheck.json`)对一组**已补 JSDoc、当前全绿的纯逻辑模块开 `checkJs` 强制检查**,并接入 `npm run check` 与 CI——类型检查从"编辑器摆设"变成了"改坏会红的门禁"。接入时 checkJs 顺带抓出并修了几处真实类型缺口(`import.meta.env` 缺 Vite 类型、`SyncService` 选项无 JSDoc 等)。include 列表**只增不减**(strangler-fig 渐进式),被强制的表面积随时间铺满全项目。剩余长尾文件与最终迁 TS 是后续增量。
- **问 AI 的话术**:
> "我这个纯 JS 项目,怎么用最低成本引入类型检查(先 jsconfig + checkJs + JSDoc,再逐步迁 TS)?先从哪些文件迁最划算?"

### 11. 依赖 & 供应链 🟢

- **合格长什么样**:锁文件与声明同步(`npm ci` 能过)、无已知高危漏洞、有自动更新机制。
- **当前状态**:🟢。dependabot 每周巡检,0 漏洞,lockfile 已同步(本轮 CI 抓出过一次不同步并修复)。
- **问 AI 的话术**:
> "`npm ci` 能干净通过吗(锁文件同步)?有没有已知高危漏洞?有没有自动的依赖更新机制?"

### 12. 文档 & 协作 🟡

- **合格长什么样**:README 讲清怎么跑;架构/约定有记录且**不过期**;有贡献指南/变更记录(团队协作时)。
- **当前状态**:🟡。有 README、CLAUDE.md、ARCHITECTURE.md(本轮修过一处文档漂移:把"48 个 playground"更新成实际的 86 个)。缺 CONTRIBUTING / CHANGELOG,但个人项目非必需。**注意**:AI 会读文档并当真,文档过期比没文档更坑。
- **问 AI 的话术**:
> "对照代码检查我的文档有没有过期/说谎的地方(数量、路径、已删的功能)?新人照 README 能不能一次跑起来?"

---

## 📊 总评(2026-07)

| 维度 | 评级 | 一句话 |
|---|---|---|
| 1 正确性/边界 | 🟢 | radix 修复 + 全排序边界回归基线 |
| 2 数据一致性/并发 | 🟢 | 4 个竞态清零 |
| 3 错误处理/可观测 | 🟢 | 从 0 到有监控+自愈 |
| 4 测试 | 🟢 | 单测 405 + Playwright E2E 关键流程 |
| 5 CI/CD | 🟢 | 守门员就位 |
| 6 安全 | 🟢 | 一直是强项 |
| 7 性能 | 🟢 | 意识和手段都在线 |
| 8 无障碍 | 🟢 | 排序/图/树三大 viz 补齐色盲通道 + aria |
| 9 SEO/元数据 | 🟢 | 已补齐 |
| 10 类型/可维护 | 🟢 | checkJs 强制子集接入 CI(渐进扩) |
| 11 依赖/供应链 | 🟢 | dependabot + 0 漏洞 |
| 12 文档/协作 | 🟡 | 有但需防漂移 |

**结论**:本轮把"检测出的问题"逐项动到底——正确性回归、类型强制检查接入 CI、a11y 三大 viz 补齐色盲通道 + aria、E2E 关键流程上线。**12 个维度现已全绿**。注意"绿"不等于"完工":类型迁 TS、E2E 扩流程、长尾 viz 的 a11y 仍是可持续增量——绿的含义是"这条线上有门禁守着、不会悄悄退化"。真正的护城河是那套习惯:**造 → AI 红队审 → 每修配一测 → CI 守门**——你已经在跑这个循环了。

**别被评级绑架**:这份表是给"知道该往哪看"用的,不是 KPI。真正的工业级习惯是那个循环——**造 → 让 AI 红队审 → 修(每个修配一个测试)→ CI 守门**——你已经在这么做了,把它变成肌肉记忆就行。

---

## 🔁 复用模板:一次性红队审视(可直接粘给任意 AI)

```
以大厂资深前端工程师的标准,红队审视这个项目,找出我作为
非资深开发者发现不了的问题。按这 12 个维度逐一过:
正确性/边界、数据一致性/并发、错误处理/可观测、测试、
CI/CD、安全、性能、无障碍、SEO、类型/可维护性、依赖、文档。

每一项告诉我:合格标准是什么 → 我现在处于什么状态(举代码为证)
→ 一个具体的失败场景(用户做了什么会出什么事)。
按严重程度排序,先只列问题、不要改。
```
65 changes: 65 additions & 0 deletions e2e/smoke.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { test, expect } from '@playwright/test'

// 关键用户流程 E2E(自检维度 4)。模拟真人从头点到尾、跨页、跨刷新——
// 这是单测/jsdom 冒烟覆盖不到的一层(真实浏览器、真实路由、真实持久化)。

test('首页加载且渲染出学科入口', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveTitle(/CS Hub/)
// 首页应有若干可点击链接(学科/导航)
await expect(page.locator('a').first()).toBeVisible()
})

test('算法页可单步:点"下一步"后步骤描述变化', async ({ page }) => {
await page.goto('/algo/bubblesort')
const next = page.getByRole('button', { name: '下一步' }).first()
await expect(next).toBeVisible()

// STEP 徽标后面那段描述
const stepBadge = page.locator('span', { hasText: /^STEP$/ }).first()
const desc = stepBadge.locator('xpath=following-sibling::span[1]')
const before = (await desc.textContent())?.trim()

await next.click()
await next.click()
await expect(desc).not.toHaveText(before || '')
})

test('主题切换:点按钮后 data-theme 翻转', async ({ page }) => {
await page.goto('/algo/bubblesort')
const themeBtn = page.locator('button[title="切换深色"], button[title="切换浅色"]').first()
await expect(themeBtn).toBeVisible()

const before = await page.evaluate(() => document.documentElement.getAttribute('data-theme'))
// 顶栏按钮是 fixed 定位 + 动画,Playwright 会判定"在视口外";这里测的是翻转
// 行为而非物理可点性,直接触发 DOM click 更稳。
await themeBtn.evaluate((el) => el.click())
await expect
.poll(() => page.evaluate(() => document.documentElement.getAttribute('data-theme')))
.not.toBe(before)
})

test('收藏持久化:收藏后刷新仍是已收藏 + 写进 localStorage', async ({ page }) => {
await page.goto('/algo/bubblesort')

const favBtn = page.getByRole('button', { name: /收藏/ }).first()
await expect(favBtn).toBeVisible()

// 若已是"已收藏"(历史状态),先取消,回到干净起点
if ((await favBtn.textContent())?.includes('已收藏')) {
await favBtn.click()
await expect(favBtn).toHaveText(/^收藏|收藏$/)
}

await favBtn.click()
await expect(favBtn).toContainText('已收藏')

// 落库
const fav = await page.evaluate(() => localStorage.getItem('algoviz-favorites'))
expect(fav).toContain('bubblesort')

// 刷新后仍保持
await page.reload()
const favAfter = page.getByRole('button', { name: /收藏/ }).first()
await expect(favAfter).toContainText('已收藏')
})
Loading