Skip to content

1.4 用 Mock 数据跑通首屏 ​

AI CODING · STAGE 1 · MOCK FIRST SCREEN

先让 domain test 失败,再补最小实现。规则变绿后接 component 和 page,最后看 diff、跑全量检查、走四条 H5 路径。

时间 2–3 小时Mock 5 records验收 11 tests + 4 paths
检查 learner branch →
RED
test 指向缺失的 module / export
保存真实失败输出
import 或环境错误先修
GREEN
stages 4 passed · recommendations 7 passed
H5 build exit 0
四条页面路径结果正确

确认工作分支 ​

bash
git branch --show-current
git status --short

当前 branch 应为 learner/week-01。开始前 working tree 应为空。若还在 practice/ai-workflow,回到 0.4 重新从 starter 建 branch。

本节的顺序固定:

text
stage test RED
  → stage implementation GREEN
  → recommendation test RED
  → recommendation domain + data GREEN
  → component + page
  → full test + build + browser

先完成 domain 和 data,再执行 1.3 的 component 任务卡。收藏、persistence、API 和 AI 解释不进入这次 diff。

Stage RED ​

先创建 apps/web/src/domain/stages.test.ts,写四项:

  1. STAGES 的 ID 唯一。
  2. isStageId 接受 pregnancy、baby-0-6、baby-6-12、toddler-1-3。
  3. isStageId 拒绝 contract 之外的 future-stage。
  4. getStage("baby-6-12").name 等于“宝宝 6–12 月”。

Implementation 还不存在。先看 test diff,再运行:

bash
git add apps/web/src/domain/stages.test.ts
git diff --cached -- apps/web/src/domain/stages.test.ts
npm run test:web -- stages.test.ts

合理的 RED 是找不到 ./stages,或所需 export 尚未定义。拼错 import、Node 版本错误或 dependency 安装失败不算业务 RED。

保留 terminal 中第一处失败。代码块里的“期望失败”不能代替实际输出。

Stage test 关键部分
ts
import { describe, expect, test } from "vitest";
import { getStage, isStageId, STAGES } from "./stages";

describe("stages", () => {
  test("has unique stage ids", () => {
    const ids = STAGES.map((stage) => stage.id);
    expect(new Set(ids).size).toBe(ids.length);
  });

  test("accepts all canonical stage ids", () => {
    expect(isStageId("pregnancy")).toBe(true);
    expect(isStageId("baby-0-6")).toBe(true);
    expect(isStageId("baby-6-12")).toBe(true);
    expect(isStageId("toddler-1-3")).toBe(true);
  });

  test("rejects an unknown stage id", () => {
    expect(isStageId("future-stage")).toBe(false);
  });

  test("gets the baby 6–12 month stage", () => {
    expect(getStage("baby-6-12").name).toBe("宝宝 6–12 月");
  });
});

第一次运行时,真实 RED 通常接近:

text
FAIL  src/domain/stages.test.ts
Error: Failed to load url ./stages
Test Files  1 failed

若输出是 npm: command not found、unsupported Node version 或 lockfile 安装失败,先修环境。那不是测试证明的业务缺口。

Stage GREEN ​

创建 apps/web/src/domain/stages.ts:

  • 导出四个固定阶段,顺序与 1.1 一致。
  • 从 STAGES 推导 StageId,不要写成 string。
  • isStageId(value) 使用 TypeScript type predicate。
  • getStage(id) 返回匹配阶段。

运行同一条定向测试:

bash
npm run test:web -- stages.test.ts

预期 4 passed。如果把 StageId 写成 string,runtime test 可能通过,调用方却失去类型收窄。看类型定义,不只看绿灯。

最小实现不需要 class 或 store:

ts
export const STAGES = [
  { id: "pregnancy", name: "孕期", shortName: "孕期" },
  { id: "baby-0-6", name: "宝宝 0–6 月", shortName: "0–6 月" },
  { id: "baby-6-12", name: "宝宝 6–12 月", shortName: "6–12 月" },
  { id: "toddler-1-3", name: "宝宝 1–3 岁", shortName: "1–3 岁" },
] as const;

export type StageId = (typeof STAGES)[number]["id"];
export type Stage = (typeof STAGES)[number];

export function isStageId(value: string): value is StageId {
  return STAGES.some((stage) => stage.id === value);
}

getStage 仍应按 ID 查找,并在 impossible state 抛出包含 ID 的错误。不要为了避免 throw 把返回值改成 Stage | undefined;调用方传入的是已经收窄的 StageId。

Recommendation RED ​

创建 apps/web/src/domain/recommendations.test.ts。它应覆盖:

  • baby-6-12 返回两条推荐。
  • 空输入返回空数组。
  • 能按 ID 找到“南瓜泥入门”。
  • 未知 ID 返回空结果。
  • 5 个 recommendation ID 唯一。
  • Mock 数据保持不可变。
  • “南瓜泥入门”保留可执行的注意事项。

此时不要创建 recommendation implementation 或 Mock data。先暂存 test 并运行:

bash
git add apps/web/src/domain/recommendations.test.ts
git diff --cached -- apps/web/src/domain/recommendations.test.ts
npm run test:web -- recommendations.test.ts

合理的 RED 是缺少 recommendations module 或 export。保留实际失败输出。

两条最能锁住用户行为的 test 是:

ts
test("returns two recommendations for babies 6–12 months old", () => {
  const results = recommendationsForStage(recommendations, "baby-6-12");

  expect(results).toHaveLength(2);
  expect(results.every((item) => item.stageIds.includes("baby-6-12"))).toBe(true);
});

test("returns undefined for an unknown recommendation id", () => {
  expect(findRecommendation(recommendations, "rec-unknown")).toBeUndefined();
});

第一条对应首页默认结果,第二条对应详情空状态。其余五条检查数据安全和内容契约,不需要在 page 再写一遍。

Recommendation GREEN ​

依次创建:

text
apps/web/src/domain/recommendations.ts
apps/web/src/data/recommendations.ts

Domain 文件定义 Recommendation,实现按 StageId 筛选和按 recommendation ID 查找。Data 文件保存 5 条 Mock 内容,并引用 domain type。

推荐项使用 stageIds。首页不再写一份筛选条件,component 也不判断推荐属于哪个阶段。

bash
npm run test:web -- recommendations.test.ts

预期 7 passed。若为了变绿删除 assertion、放宽数量或把数据复制进 test,退回修改。

Domain 只保留纯函数:

ts
export function recommendationsForStage(
  items: readonly Recommendation[],
  stageId: StageId,
): readonly Recommendation[] {
  return items.filter((item) => item.stageIds.includes(stageId));
}

export function findRecommendation(
  items: readonly Recommendation[],
  id: string,
): Recommendation | undefined {
  return items.find((item) => item.id === id);
}

Mock data 的单条结构如下。五条内容都使用同一接口,不为某个页面增加特有字段:

ts
{
  id: "rec-pumpkin-puree",
  stageIds: ["baby-6-12"],
  category: "food",
  title: "南瓜泥入门",
  summary: "蒸熟南瓜后压成细泥,少量单一食材开始尝试。",
  reason: "质地柔软、味道清甜,便于观察宝宝对新食材的接受度。",
  notice: "首次少量尝试;出现皮疹、呕吐、腹泻等不适时暂停添加;症状明显、持续或呼吸困难时及时就医。",
}

测试要求 dataset、item 和嵌套的 stageIds 都不可变。只冻结最外层数组不够。

接 component 和 page ​

依次执行 1.3 保存的 component 任务卡和详情页任务卡。第一张卡的 Allowed files 只有:

text
apps/web/src/components/StagePicker.vue
apps/web/src/components/RecommendationCard.vue
apps/web/src/pages/index/index.vue

StagePicker 接收 stages、modelValue,发出 update:modelValue(StageId)。RecommendationCard 接收 item,发出 open(recommendationId)。首页持有状态、调用筛选函数并处理导航。

两张任务卡分开执行。不要把 component、首页、详情页和 pages.json 合并成一次无法定位问题的大改动。

首页接线的关键行只有这些:

ts
const selectedStage = ref<StageId>("baby-6-12");
const visibleRecommendations = computed(() =>
  recommendationsForStage(RECOMMENDATIONS, selectedStage.value),
);

function openRecommendation(id: string) {
  uni.navigateTo({ url: `/pages/detail/index?id=${encodeURIComponent(id)}` });
}
vue
<StagePicker v-model="selectedStage" :stages="STAGES" />
<RecommendationCard
  v-for="item in visibleRecommendations"
  :key="item.id"
  :item="item"
  @open="openRecommendation"
/>

详情页只负责读 query 和调用查找函数:

ts
const recommendationId = ref("");
const item = computed(() =>
  findRecommendation(RECOMMENDATIONS, recommendationId.value),
);

onLoad((query) => {
  recommendationId.value = typeof query?.id === "string" ? query.id : "";
});

展示分支使用 v-if="item";空分支显示固定文案。不要用可选链把整个页面静默渲染为空白。

先看 Diff,再跑全量 ​

bash
git diff --stat
git diff

检查这些问题:

  • 是否出现 allowed files 以外的 package、config 或 lockfile 改动。
  • 阶段常量是否只在 domain/stages.ts。
  • 筛选和查找是否只在 domain。
  • component 是否只通过 props / events 通信。
  • 详情页是否读取 query.id,而不是复制 Mock 数据。
  • test expectation 是否保持原契约。

Diff 没有越界后再跑:

bash
npm run test:web
npm run build:web

预期 11 项测试通过,H5 build exit 0。

教学仓 checkpoint 的关键输出如下,版本号和耗时可能不同:

text
Test Files  2 passed (2)
Tests       11 passed (11)
text
Compiler version: 5.03(vue3)
Compiling...
DONE  Build complete.

uni-app 有新版本 或 Vite CJS deprecation 是当前工具链 warning,不等于 build 失败。判断标准仍是 command exit code 和最后的 build 结果。

两个不能接受的“修复” ​

为了让数量测试变绿而改 expectation:

diff
-expect(results).toHaveLength(2)
+expect(results.length).toBeGreaterThan(0)

这会把明确需求放宽成“有数据就行”。恢复原 assertion,修 Mock stageIds 或筛选函数。

为了让详情显示内容而在 page 复制数据:

diff
-const item = computed(() => findRecommendation(RECOMMENDATIONS, recommendationId.value))
+const item = computed(() => ({
+  id: recommendationId.value,
+  title: "南瓜泥入门",
+  reason: "...",
+  notice: "...",
+}))

这绕过了 domain lookup,也让不存在 ID 永远无法进入空状态。详情页继续使用 findRecommendation。

浏览器走四条路径 ​

启动 H5:

bash
npm run dev:web

按顺序操作:

  1. 首次打开首页,默认选中“6–12 月”,列表显示两条推荐。
  2. 切换到“孕期”,列表只显示“产检资料收纳清单”。
  3. 打开“南瓜泥入门”,详情显示推荐依据和注意事项。
  4. 把地址中的详情 ID 改成不存在的值,页面显示“没有找到这条推荐”。

测试通过只说明 domain 规则没有跑偏。这四条路径检查 component event、页面跳转和详情查询。

再用 Tab 聚焦阶段按钮和推荐卡片,按 Enter、Space,行为应与点击一致。这是额外的 accessibility check,不替代四条主路径。

提交阶段 1 ​

bash
git status --short
git add apps/web/src/domain/stages.test.ts \
  apps/web/src/domain/stages.ts \
  apps/web/src/domain/recommendations.test.ts \
  apps/web/src/domain/recommendations.ts \
  apps/web/src/data/recommendations.ts \
  apps/web/src/components/StagePicker.vue \
  apps/web/src/components/RecommendationCard.vue \
  apps/web/src/pages/index/index.vue \
  apps/web/src/pages/detail/index.vue \
  apps/web/src/pages.json
git diff --cached

重新读 cached diff。列表中出现不存在的文件,说明对应任务还没有完成;不要用 git add -A 把未知文件一起带进 commit。提交信息写清 domain 或用户链路,不用“AI 已完成”这类描述。提交后保存短 hash:

bash
git rev-parse --short HEAD
常见失败定位
症状先查重跑
test 找不到 moduletest 的相对 import、文件名与 export对应定向 test
默认数量或孕期数量不对Mock stageIds、筛选函数、首页默认阶段npm run test:web -- recommendations.test.ts
点击卡片没有正确详情card 的 open event、首页的 id 导航npm run dev:web
详情始终为空query.id 和 domain 查找函数打开“南瓜泥入门”
H5 build 报多个错误terminal 第一处文件和行号npm run build:web

先修第一处错误。不要修改 test expectation,别在 page 或 component 里复制数据绕过问题。

四条路径和全量检查都通过后,进入 阶段 2 · 前端业务闭环。

晨悦 AI 实践手册