软件工程第二次结对作业 —— 校园失物招领程序实现

项目 内容
这个作业属于哪个课程 2026 秋软件工程(福州大学)
这个作业要求在哪里 第二次结对作业 · 程序实现
这个作业的目标 基于第一次结对作业的原型设计,完成「校园失物招领」核心模块的代码实现,并练习 GitHub 版本管理、单元测试、PSP 记录与软件工程文档撰写
学号 / 姓名(同学 A) 102401101 / 谢玲洁
结对搭档(同学 B) AI Agent
GitHub 仓库 https://github.com/xxxs111/102401101-AIagent
第一次作业原型 墨刀原型链接

一、具体分工

本次结对为人机结对:我负责需求把关与全部验收工作,AI 搭档负责代码实现。

成员 主要负责 具体内容
102401101 谢玲洁(同学 A) 需求与验收 需求梳理与范围界定、界面走查与功能测试、单元测试运行与截图、博客撰写、PSP 记录、本地运行调试与问题排查
AI Agent(同学 B) 程序实现 页面结构与样式、数据层与工具层、哈希路由与交互逻辑、表单校验、一键复制、单元测试代码、README 与开发文档、目录结构整理

开发过程中双方互换「驾驶员 / 领航员」角色:AI 先出一版可运行的实现,我当领航员逐条走查并提问题(必填项提示不够具体、已完成的信息还要不要显示联系方式、删除要不要二次确认、不是自己发的能不能改状态),AI 再改;有分歧时以「作业要求 + 真实使用场景」为准做取舍。


二、PSP 表格

表一:谢玲洁(102401101)—— 需求与验收

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 45 50
Estimate 估计这个任务需要多少时间 45 50
Development 开发 290 330
Analysis 需求分析(包括学习新技术) 40 45
Design Spec 生成设计文档 25 25
Design Review 设计复审(对照第一次作业原型与作业要求) 20 25
Coding Standard 代码规范(统一命名、注释、分层约定) 15 15
Design 具体设计(页面结构与交互方案确认) 35 40
Coding 具体编码(走查中的小修改与文案调整) 60 70
Code Review 代码复审(逐条读 AI 产出的代码) 45 50
Test 测试(自我测试、修改代码、单元测试运行与截图) 50 60
Reporting 报告 100 115
Test Report 测试报告 25 30
Size Measurement 计算工作量 10 10
Postmortem & Process Improvement Plan 事后总结 20 25
博客撰写与配图 45 50
合计 435 495

表二:AI Agent —— 程序实现

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 20 20
Estimate 估计这个任务需要多少时间 20 20
Development 开发 220 265
Analysis 需求分析(读第一次作业原型、确认功能边界) 20 25
Design Spec 生成设计文档(分层方案、数据模型) 20 20
Design Review 设计复审 10 10
Coding Standard 代码规范 10 10
Design 具体设计(路由表、数据流、交互细节) 30 35
Coding 具体编码(HTML/CSS/JS) 70 85
Code Review 代码复审 20 25
Test 测试(写 61 个用例、修被测试抓出的 bug) 40 55
Reporting 报告 55 60
Test Report 测试报告素材整理 15 15
Size Measurement 计算工作量 10 10
Postmortem & Process Improvement Plan 事后总结 15 20
README 与开发文档撰写 15 15
合计 295 345

PSP 小结:两张表都是"预估 < 实际"。超时最明显的是 AI 的具体编码(+15)与测试(+15)——编码的时间花在移动端布局与样式细节打磨上;测试超时是因为它真的抓出了两个 bug,修完还要重跑全部用例并补回归用例。我这边的超时集中在代码复审和测试:许多"流程细节"(已完成的信息还要不要显示联系方式、删除要不要二次确认、不是自己发的能不能改状态)在预估时完全没意识到需要专门确认,走查时才发现必须逐条定规则。下次我会在动手前先把这些边界行为列成清单。


三、解题思路描述与设计实现说明

3.1 需求回顾与范围界定

第一次结对作业已经明确:本次不做后台管理、实名认证、即时聊天、地图定位、账号体系。所以我直接砍掉这些模块,把力气全部放在作业点名的核心闭环上:

发布信息 → 浏览 / 搜索 → 查看详情 → 联系发布者 → 更新状态

设计时重点回答了两个问题(也是第一次作业点评提醒过的地方):

  1. 联系之后如何结束这条信息? —— 由发布者在详情页或「我的发布」里标记为「已找到 / 已归还」;标记后该条在列表中置灰并排到后面,详情页不再展示联系入口(显示"请勿再联系发布者"),并且支持「恢复为进行中」以防误标。
  2. 没有登录系统,怎么知道谁是发布者? —— 用本机发布者标识:首次发布时生成一个随机 publisherKey 存在 localStorage,本人发布的信息都带上这个标识,「我的发布」只展示标识匹配的信息,详情页只有标识一致时才显示「标记完成 / 编辑 / 删除」。既回答了"谁改状态、从哪改",又没引入作业不要求的账号体系。

diagram-flow

3.2 技术选型

决策点 结论 理由
形态 单页 Web 应用(Chrome 打开 index.html) 与原型一致,助教双击即用
框架 原生 HTML/CSS/JS,不引入任何框架 作业要求"下载全部文件后用 Chrome 打开就能看到预期结果",引入构建工具反而抬高验收门槛
视图组织 location.hash 简易路由 + 字符串模板渲染 支持 #/detail/3 可直达链接,方便截图与复现问题;逻辑层可与 DOM 解耦,便于单元测试
数据持久化 localStorage 无后端前提下单机演示足够,刷新不丢数据;不可用时降级为内存存储
单元测试 原生 JS 手写断言 零依赖、离线可跑、控制台粘贴即可执行

diagram-arch

diagram-data

3.3 架构与数据模型

代码按「界面层 → 数据层 / 模板层 → 工具层」组织,数据层、模板层、工具层都不访问 DOM,这是能写出单元测试的关键:

app.js(界面层:哈希路由 / 渲染调度 / 事件委托)
   ├── 调用 ──► store.js(数据层:校验 / 增删改查 / 组合搜索 / 演示数据)
   ├── 取模板 ► view.js (模板层:数据 → HTML 字符串,所有插值经 escapeHtml)
   └── 依赖 ──► utils.js(工具层:HTML 转义 / 日期 / 状态文案 / 防抖)
                       │
                       ▼
        localStorage(lf_items_v1 信息列表 / lf_owner_v1 本机标识)

信息数据模型(item):

字段 含义 取值示例
id 自增主键 1, 2, 3…
type 信息类型 lost 寻物 / found 招领
name / category 物品名称 / 分类 校园卡 / 校园卡、电子产品、水杯…
time / place 丢失或拾取的时间 / 地点 2026-10-06 / 三区田径场看台
desc / image 补充描述 / 图片链接 蓝色卡套,卡号末尾 3721
contactName / contact 称呼 / 联系方式 李同学 / 13800001234
status 状态 active 进行中 / done 已完成
publisherKey 本机发布者标识 随机字符串
views / createdAt / updatedAt 浏览数 / 创建与更新时间戳 26 / 1727…

状态文案映射:status × type 两两组合出四种文案 —— 寻物+进行中=「寻找中」、寻物+已完成=「已找到」、招领+进行中=「待认领」、招领+已完成=「已归还」,统一由 Utils.statusOf() 输出,界面层只认这个函数,避免文案散落在十几个模板字符串里。

状态流转:

        ┌────────── updateStatus(id,'done') ──────────┐
        ▼                                             │
   ┌─────────┐                                  ┌──────────┐
   │ active  │◄──── updateStatus(id,'active') ───│   done   │
   │ 进行中   │                                  │  已完成   │
   └─────────┘                                  └──────────┘
   列表正常展示、显示联系入口                      列表置灰后置、隐藏联系入口

3.4 关键代码片段与解释

① 组合搜索(store.js)—— 搜索不是简单的 includes

function searchItems(opts) {
  opts = opts || {};
  var kw = String(opts.keyword || '').trim().toLowerCase();  // 处理首尾空格与大小写
  var list = loadItems();

  if (opts.type && TYPES.indexOf(opts.type) >= 0) list = list.filter(function (i) { return i.type === opts.type; });
  if (opts.category)                             list = list.filter(function (i) { return i.category === opts.category; });
  if (opts.onlyActive)                           list = list.filter(function (i) { return i.status === 'active'; });

  if (kw) {
    list = list.filter(function (i) {
      var hay = [i.name, i.desc, i.place, i.category, Utils.typeLabel(i.type)]
        .join(' ').toLowerCase();          // 关键词同时覆盖名称 / 描述 / 地点 / 分类 / 类型
      return hay.indexOf(kw) >= 0;
    });
  }
  return sortItems(list);   // 进行中在前,同状态按发布时间倒序(内部先 slice 再排序)
}

解释:把「关键词 + 类型 + 分类 + 只看进行中」做成自由组合的过滤器,最后统一排序。trim().toLowerCase() 保证 校园卡、airpods 这类输入也能命中,避免测试人员因为多打一个空格就以为搜索坏了。另外 sortItems 内部先 slice() 再排序,返回的是副本,不会就地改动原数组——这条副作用有专门的用例锁住。

② 一键复制联系方式(app.js)—— 主方案 + 降级方案

function copyText(text) {
  function fallback() {                       // 非 HTTPS / file:// 下的降级方案
    var ta = document.createElement('textarea');
    ta.value = text; ta.style.position = 'fixed'; ta.style.top = '-1000px';
    document.body.appendChild(ta); ta.select(); ta.setSelectionRange(0, ta.value.length);
    var ok = false;
    try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
    document.body.removeChild(ta);
    toast(ok ? '联系方式已复制:' + text : '复制失败,请手动长按选中复制');
  }
  if (navigator.clipboard && navigator.clipboard.writeText) {
    navigator.clipboard.writeText(String(text)).then(function () { toast('联系方式已复制:' + text); }, fallback);
  } else { fallback(); }
}

③ 防 XSS(utils.js + view.js)—— 用户输入全部转义

function escapeHtml(str) {
  if (str === null || str === undefined) return '';
  return String(str)
    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}

模板层所有插值都经过它,例如 '<span class="item-name">' + esc(item.name) + '</span>'。测试人员如果故意发布一条名字叫 <img src=x onerror=alert(1)> 的信息,它只会被当作普通文字显示。注意 & 必须最先替换,否则会把后面生成的实体再转义一遍。

④ 搜索输入的防抖与"局部重渲染"(app.js)

关键词输入用 300ms 防抖,而且只重渲染列表容器、不重渲染整页——否则每敲一个字输入框都会失焦、光标跳回开头。地址栏用 history.replaceState 同步筛选条件(不产生历史记录,也方便把搜索结果链接分享到班群)。

3.5 更多界面截图(与上面的设计决策一一对应)

下面几张图对应第三节的设计决策与第四节列出的附加功能:
「只看进行中」:6 条筛到 4 条,已完结的信息自动被过滤掉
11-home-filter
分类筛选:点「🎧 电子产品」只看这一类;筛选条件同步进地址栏,链接可直接分享到班群
12-search-category
空状态引导:搜索无结果时给出「清空筛选条件」,而不是干巴巴一行“暂无数据”
13-search-empty
「我的发布」空状态:没有发布过时给出「去发布一条」入口
14-mine-empty
已完成的信息(招领 → 已归还):详情页不再展示联系方式,改为「请勿再联系发布者」提示
15-detail-done


四、附加特点设计与展示

题目允许"尽情丰富内容",我围绕真实使用痛点做了几个不炫技但实用的设计:

  1. 一键复制联系方式:校园场景里联系靠的是"复制号码 → 打开微信/电话",详情页点一下就完成,省去手抄和误记。非 HTTPS 环境下自动降级为 execCommand,无论成功失败都弹 Toast 反馈。
  2. 状态驱动 UI:已完成的信息自动置灰、排到列表后方,详情页不再展示联系入口——从机制上减少无效联系,而不是靠大家自觉。
  3. "我的发布"+ 本机发布者标识:没有账号也能只管理自己发的内容,并且明确"只有发布者能改状态",让"更新状态"这条流程第一次在代码里有了明确答案。
  4. 可分享的搜索结果链接:index.html#/search?q=保温杯 发到班群里,别人点开就是同一批搜索结果。
  5. 空状态引导:搜索无结果时给出「清空筛选条件」按钮,发布列表为空时给出「去发布」入口,而不是干巴巴一行"暂无数据"。
  6. 健壮性细节:所有用户输入渲染前转义(防 XSS);localStorage 读写全部 try/catch,隐私模式下自动降级内存存储;存储内容被写坏时降级为空数组而不是白屏。

五、目录说明和使用说明

5.1 目录结构

102401101-AIagent/
├── README.md                  # 项目说明与使用指南
├── index.html                 # 单页应用入口(Chrome 直接打开)
├── css/
│   └── style.css              # 全部样式(移动端优先,桌面居中手机形态)
├── js/
│   ├── utils.js               # 工具层:转义 / 日期 / 状态文案 / 防抖(不依赖 DOM)
│   ├── store.js               # 数据层:localStorage / 校验 / 增删改查 / 搜索(不依赖 DOM)
│   ├── view.js                # 模板层:数据 → HTML 字符串(不碰 DOM、不读存储)
│   └── app.js                 # 界面层:哈希路由 / 渲染 / 事件委托
├── test/
│   ├── test.html              # 单元测试运行页(打开即跑,结果显示在页面上)
│   └── unit-test.js           # 61 个用例,原生 JS 手写断言
└── docs/
    ├── 开发文档.md             # 架构、数据模型、关键实现、技术决策与踩坑清单
    ├── 验收与提交指南.md        # 验收走查清单、测试报告、博客骨架
    ├── PSP记录.md              # PSP 表格
    ├── 博客正文(可直接粘贴).html
    └── screenshots/            # 效果截图

分层的目的:utils.js / store.js / view.js 都不碰 DOM,业务规则(校验、搜索、状态流转、转义、模板输出)全是纯函数,只有 app.js 和浏览器打交道。这是本项目能写出有意义单元测试的前提——如果一开始把逻辑都写进 onclick 回调,后面就只能靠人工点测了。

5.2 测试人员如何运行

  1. 从 GitHub 下载全部文件(或 git clone),用 Chrome 双击打开 index.html,无需服务器、无需 Node、无需任何构建步骤;
  2. 首次打开自动载入 6 条演示数据,可直接浏览、搜索、查看详情;
  3. 想体验完整闭环:首页「发布寻物」→ 先什么都不填点「立即发布」看校验提示 → 填完发布 → 底部「我的发布」→ 点「标记为已找到」→ 回首页看该条置灰并排到最后;
  4. 运行单元测试:打开 test/test.html,页面顶部直接显示"用例总数 / 通过 / 失败";也可按 F12 把 test/unit-test.js 粘贴进 Console 后输入 runTest();
  5. 重置数据:底部「关于」→「清空本机数据」或「重新载入演示数据」。

六、单元测试

6.1 选用的测试工具与学习过程

工具:原生 JavaScript 手写断言(assert.ok / equal / notEqual / deepEqual / throws),零框架、零依赖、离线可运行。

为什么不用现成框架:作业要求"Chrome 直接打开 html 就能看到预期结果",引入 QUnit 需要额外下载框架文件,引入 Jest / Mocha 需要 Node 环境,都会抬高助教的验收门槛;而本次要测的都是纯函数,5 个断言方法已经够用。

学习过程:先读了单元测试相关的博客(邹欣《单元测试之道》)理解"自动化、每天可运行"的思想,再对照本项目的逻辑层设计用例。

一份简易教程(三步上手):

  1. 用 Chrome 打开 index.html,按 F12 进入 Console;
  2. 把 test/unit-test.js 的全部内容粘贴进 Console 回车(浏览器若提示禁止粘贴,先输入 allow pasting 解锁粘贴权限);
  3. 输入 runTest() 回车:绿色 ✔ 为通过,红色 ✘ 会带上失败原因与出错位置。

更省事的方式:直接打开 test/test.html,加载即自动运行,并把结果按模块分组显示在页面上,刷新就重跑。

6.2 测试思路与用例设计

一共 61 个用例,覆盖 6 个模块:

模块 用例数 覆盖的函数
工具函数 10 escapeHtml / toDateStr / parseDate / isFutureDate / friendlyDate / timeAgo / statusOf / typeLabel / categoryIcon / uid
表单校验 11 isValidPublishData(名称、分类、日期、地点、描述、类型、联系方式逐字段)、isValidContact
数据读写 20 loadItems / saveItems / generateId / getItem / createItem / updateItem / deleteItem / incrementViews
状态与搜索 11 updateStatus(含非法状态、不存在 id)、searchItems、sortItems、countByType、getMyItems、isMine
演示数据 5 seedIfEmpty(含"不覆盖用户数据"回归用例)、clearAll
其它 4 中文与 emoji 往返、存储损坏降级等

构造测试数据的思路:

  • 确定性优先:每个用例前先 resetStorage() 清库保证隔离,再用 fixtures() 固定写入 4 条数据(含寻物/招领、进行中/已完成、不同地点与联系方式格式),保证用例可重复、可预期、可乱序执行;
  • 日期不写死:用"相对今天偏移 N 天"生成,避免用例过一段时间就失效;
  • 白盒边界穷举:名称刚好 30 字 / 地点刚好 50 字 / 描述刚好 200 字 / 日期刚好是今天 / 关键词带首尾空格 / QQ 与微信号位数边界 / 存储内容被写坏;
  • 异常与对抗:空表单、纯空格、超长、未来日期、非法联系方式、不存在的 id、非法状态值——"测试人员会怎么刁难"是我们设计用例时最花心思的地方。

测试代码摘录一:校验函数边界

test('[校验] 日期:空 / 格式错误 / 未来日期', function () {
  assert.ok(Store.isValidPublishData(data({ time: '' })).errors.time, '空日期应报错');
  assert.ok(Store.isValidPublishData(data({ time: '2026/09/20' })).errors.time, '斜杠格式应报错');
  assert.ok(Store.isValidPublishData(data({ time: dayStr(1) })).errors.time, '明天应报错');
  assert.notOk(Store.isValidPublishData(data({ time: dayStr(0) })).errors.time, '今天应通过');
});

test('[校验] 联系方式:非法格式应被拦截(防止放行垃圾数据)', function () {
  ['', '   ', '12', '12!@#', 'abc', '1234', '1380000123', '138000012345678', '@example.com', 'a@b']
    .forEach(function (c) {
      assert.notOk(Store.isValidContact(c), '"' + c + '" 应被判为非法');
    });
});

测试代码摘录二:状态更新与文案映射联动

test('[数据] updateStatus:招领标记 done 后文案变「已归还」,可再恢复', function () {
  resetStorage(); seed(fixtures());
  var done = Store.updateStatus(2, 'done');
  assert.equal(Utils.statusOf(done), '已归还', '招领 + done = 已归还');
  var back = Store.updateStatus(2, 'active');
  assert.equal(Utils.statusOf(back), '待认领', '恢复后应回到待认领');
});

test('[数据] updateStatus:不存在 id 返回 null 且数据不变', function () {
  resetStorage(); seed(fixtures());
  var before = JSON.stringify(Store.loadItems());
  assert.equal(Store.updateStatus(999, 'done'), null, '应返回 null');
  assert.equal(JSON.stringify(Store.loadItems()), before, '数据不应被改动');
});

一个容易被忽略的工程细节:用例会反复清库,如果直接跑就会把用户真实数据抹掉。所以 runTest() 在运行前会备份 lf_items_v1 / lf_owner_v1,在 finally 里原样还原——测试可以随时运行而不会弄丢已发布的信息。

6.3 测试运行结果

  • 用例总数 61,通过 61,失败 0,耗时约 8–15 ms。
  • 运行截图(test/test.html 全览,页面上直接显示"用例总数 61 / 通过 61 / 失败 0"):见下方图 20。
  • 应用内一键运行(关于页)结果:见下方图 20
    10-unit-test-all-pass

6.4 测试抓出来的三个真实 bug

问题一:少一位的手机号被当成合法 QQ 放行

  • 现象:断言 1380000123(10 位)应判为非法,结果失败——它被判定为合法。
  • 排查:原规则是"手机号 或 邮箱 或 QQ(5-12 位数字) 或 微信号",这串数字满足"8 位纯数字",于是被当成合法 QQ 放行了。
  • 修复:所有纯数字串先按"手机号 / QQ"这一组规则判定,顺序与失败条件都要明确——1[3-9] 开头但只有 10 位,说明用户想写手机号却少打一位,直接判非法;QQ 收窄到 5–11 位。
  • 收获:边界规则要"先写测试再写实现",比写完再补测试更容易暴露漏洞。

问题二:非法日期被 new Date() 静默接受

  • 现象:断言 parseDate('2026-13-01') 应返回 null,实际返回了 2027-01-01。
  • 原因:new Date() 对越界月份不报错,而是静默溢出到下一年的 1 月。
  • 修复:parseDate 自己校验月份 / 日期范围,并检查构造结果是否与输入一致(2026-02-30 这类溢出日期一并拦掉),非法直接返回 null。
  • 收获:不要盲信标准库的"容错",日期解析必须自己校验。

问题三(数据安全隐患):演示数据播种会覆盖用户已有数据

  • 现象:回归用例 seedIfEmpty:已有数据时不覆盖用户数据 断言失败。
  • 原因:播种逻辑最初只检查"是否播种过",用户清掉标记但保留自己发布的信息时,刷新后 6 条演示数据会整体覆盖用户数据。
  • 修复:加"已有数据一律不播种"的前置守卫,并保留该回归用例永久守护。
  • 收获:存储层"不覆盖用户数据"是底线,必须用测试锁死。

6.5 测试评估

  • 覆盖程度:逻辑层(校验、存储、搜索、状态流转、转义、模板输出)的全部核心函数与主要异常分支都有用例,能够满足本程序的测试要求。
  • 不足之处:app.js 的视图渲染、事件绑定、剪贴板交互尚未被自动化覆盖,这部分用浏览器人工走查与截图补足(本文第八节的 9 张效果图就是实测记录)。
  • 改进方向:若后续要做端到端自动化,可引入 Playwright 覆盖界面层。

七、GitHub 代码签入记录

仓库地址:https://github.com/xxxs111/102401101-AIagent

我们按功能划分提交,每完成一个可运行的功能点就 commit 一次,提交信息遵循 <type>: <描述> 规范(参考阮一峰《Commit message 写法》)。提交序列:

chore: 初始化项目结构与 README 骨架
feat: 工具层 utils.js(HTML转义/日期/状态文案/防抖)
feat: 数据层 store.js(localStorage封装、校验、增删改查、模拟数据)
feat: 首页信息流与类型/状态筛选
feat: 搜索页——关键词+类型+分类组合筛选与空状态
feat: 发布表单与逐字段校验回显
feat: 详情页——浏览数、一键复制(含降级)、状态更新
feat: 我的发布——统计卡与信息管理(编辑/标记/删除)
feat: 附加设计(可分享搜索链接、空状态引导、防XSS)
test: 单元测试61用例(原生JS断言)+ 测试运行页
docs: README / 开发文档 / 验收指南 / PSP 记录 / 效果截图
fix: 联系方式校验放行少一位手机号的问题
fix: 非法日期溢出未拦截的问题
fix: 演示数据播种覆盖用户数据的问题

图 10:GitHub 仓库提交记录

image


八、实现成果展示

8.1 界面截图

图 11:首页(搜索触发框、发布寻物 / 发布招领入口、全部/寻物/招领计数标签、倒序信息流;进行中的信息在前并带绿色状态,已完成的置灰后置)
01-home

图 12:搜索结果(热门标签 + 类型与分类筛选,关键词实时命中,输入过程中焦点不跳)
02-search

图 13:详情页(本人发布)(分类渐变大图、类型与状态徽章、时间地点行、描述、一键复制、标记已找到 / 编辑 / 删除)
03-detail-owner

图 14:详情页(非本人发布)(只读:没有管理按钮,并提示"只有发布者本人可以修改状态或删除")
04-detail-visitor

图 15:发布表单校验(什么都不填直接提交:名称、分类、地点、联系方式同时标红并给出具体提示,焦点跳到第一个出错字段)
05-publish-validation

图 16:发布表单填写完成(分类下拉、日期控件限制不能选未来日期、字符计数)
06-publish-filled

图 17:发布成功页(可直接查看这条信息或去我的发布)
07-publish-success

图 18:我的发布(共发布 / 进行中 / 已完成统计卡 + 每条信息的查看详情、标记完成、编辑、删除)
08-mine

8.2 测试结果截图

图 19:单元测试全部通过(test/test.html:用例总数 61 / 通过 61 / 失败 0)
09-about-tests


九、遇到的代码模块异常或结对困难及解决方法

除了第六节的三个 bug,开发过程中还踩了这些坑:
问题 1:一个用例抛异常导致后面全部不执行

  • 最初用一个 try/catch 包住整个 for 循环,第一个断言失败后面的用例就全被跳过,一次只能看到一个错误。
  • 解决:改成每个用例独立 try/catch,失败互不影响,一次运行就能看到全部红色用例及其原因。

问题 2:搜索框输入每个字都失焦

  • 原因:输入时整页重渲染,新节点顶掉了输入框的焦点。
  • 解决:只重渲染列表容器,并加 300ms 防抖;同时用 history.replaceState 同步地址栏,不产生多余的历史记录。

问题 3:用 hidden 属性藏返回按钮却不生效

  • 现象:首页明明设了 hidden,返回按钮还在。
  • 排查:用 getComputedStyle 一看是 display: grid——.icon-btn 里的 display 覆盖了浏览器对 [hidden] 的默认样式。
  • 解决:补 .icon-btn[hidden] { display: none !important; }。
  • 收获:不要凭感觉说"刷新一下就好了",先用计算样式确认,否则会浪费大量时间在错误的方向上。

问题 4:页面内跑测试时输出里混着 %c 和 color:...

  • 原因:console.log 的样式参数被一起收集进结果框。
  • 解决:收集日志时过滤掉样式参数,并去掉残留的 %c。

结对困难 5:对"他人信息是否显示管理按钮"产生分歧

  • 分歧:为了演示方便,是否所有详情页都显示「标记完成」?
  • 讨论结果:只有 publisherKey 与本机一致才显示管理按钮,同时演示数据默认归到本机标识名下,这样助教打开就能体验状态流转,又不会出现"任何人都能改别人信息"的问题。这个约束也让"更新状态"到底谁改、从哪改第一次有了明确答案。
  • 收获:结对的价值就在这种分歧里——一个人做容易顺着"演示方便"走,两个人互相质疑才能守住业务边界。

十、评价你的队友

AI Agent 值得学习的地方:

  • 分层做得干净,utils / store / view 三个文件都不碰 DOM,所以单元测试能直接引用、写得下去;
  • 写代码时主动考虑异常路径(存储不可用降级、剪贴板不可用降级、存储内容损坏降级、XSS 转义),这些不是作业硬性要求,但直接影响"能不能被测试人员刁难住";
  • 被测试抓出的 bug 会顺手补一条回归用例,而不是改完就算。

AI Agent 需要改进的地方:

  • 前期对交互细节的把握不够,一些"流程规则"(已完成的信息还要不要显示联系方式、删除要不要二次确认、非发布者能不能改状态)需要我走查时逐条指出才补齐,下次应该在设计阶段就把这些边界行为列出来确认;
  • 样式上反复微调(对齐、间距、圆角),耗时容易超预估,建议先锁死"功能可用"再做美化。

十一、总结

这次作业让我从头到尾走完了「原型 → 可运行程序 → 测试 → 文档」的完整流程,最大的三点收获:

  1. "流程细节"只有真正写代码时才被逼着回答清楚。 上次画原型时觉得"标记完成"一个按钮就够了,这次才发现背后有一串必须定的规则:谁来改(发布者)、从哪改(详情页和我的发布)、改完别人看到什么(置灰后置、隐藏联系入口)、改错了能不能恢复。这些不是画图能想明白的。
  2. 单元测试不是交作业的装饰,它真的抓出了 bug。 少一位的手机号被当成合法 QQ 放行、非法日期被 new Date() 静默接受、演示数据会覆盖用户数据——三条都是测试先跑出红色断言才被发现的,修复后都变成了永久回归用例。
  3. 人机结对时"逐条走查"比"一次性生成"更靠得住。 AI 出实现很快,但边界行为、文案口径、异常路径这些地方必须由人拿着需求一条条对。我把它当成"代码复审 + 验收测试"来做,才把"能用"推到了"经得起刁难"。
posted @ 2026-10-08 15:53  ab1573  阅读(6)  评论(0)    收藏  举报