软件工程第二次结对作业

202601 软件工程课程二次结对作业 —— 校园失物招领 Web 应用(程序实现)

这个作业属于哪个课程 202601 软件工程
这个作业要求在哪里 第二次结对作业之程序实现
这个作业的目标 基于上次的原型设计,结对完成"校园失物招领"核心功能的编码与单元测试
结对成员 102401129 林诗航 / 102401127 张炜鑫
本作业博客链接 102401129 林诗航 软件工程第二次结对作业
结对同学博客链接 102401127 张炜鑫 软件工程第二次结对作业
GitHub 项目地址 102401129-102401127

一、具体分工

成员 主要分工
林诗航(102401129) 数据层与核心模块(core.js / validate.js / search.js / util.js / seed.js)、发布页与我的发布页、单元测试编写、端到端验收脚本、PSP 统计与博客排版
张炜鑫(102401127) 全站样式与公共组件(style.css / ui.js)、首页 / 搜索页 / 详情页、流程图绘制、测试用例设计与复审、博客走查

结对方式沿用上次的做法:两人共同讨论出数据结构和接口约定后,轮流担任"驾驶员"(写代码)和"领航员"(逐行审查、负责提问)。数据层与校验模块由一人主写、另一人逐条核对边界条件;页面部分按页面分工并行推进,每天固定时间同步联调。


二、PSP 表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 30
Estimate 估计这个任务需要多少时间 30 30
Development 开发 850 1120
Analysis 需求分析(包括学习新技术) 60 90
Design Spec 生成设计文档 40 60
Design Review 设计复审 30 30
Coding Standard 代码规范(为目前的开发制定合适的规范) 20 30
Design 具体设计 60 50
Coding 具体编码 480 600
Code Review 代码复审 60 60
Test 测试(自我测试,修改代码,提交修改) 120 240
Reporting 报告 100 140
Test Report 测试报告 30 40
Size Measurement 计算工作量 20 20
Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 50 80
合计 980 1290

差异原因:总耗时比预估多 310 分钟,主要在两处——

  1. Coding 低估(+120):预估时只算了"把功能写出来"的时间,没算依赖梳理。开发中期把转义、时间格式化从 ui.js 抽成独立的 util.js 时,五个页面漏引了新模块;又在一次 UMD 封装改造中把 global 参数弄丢(详见第六节),这两处排查加上回归测试花了约两小时;
  2. Test 低估(+120):原计划单元测试跑通即可,实际为了验证"助教下载后双击就能用",额外写了一套基于 puppeteer 的端到端走查脚本(24 项断言),还真抓到了三个单元测试抓不到的问题,脚本编写、问题定位与修复回归共花了一倍于预估的时间。

反过来看,Design 比预估省了 10 分钟——原型阶段已经把页面结构和字段定清楚了,这次直接照着实现,验证了上次作业"设计做扎实,编码才省力"的判断。


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

1. 代码实现思路

总体架构:两层分离,纯逻辑与页面解耦。

页面层(5 个 HTML + page-*.js)   —— 只负责取值、渲染、绑定事件
        │  调用
纯逻辑层(5 个 JS 模块,不依赖 DOM)—— 校验、搜索、数据存取、格式化
        │  读写
localStorage(不可用时自动降级为内存)

五个核心模块全部写成纯逻辑模块(UMD 导出,浏览器挂全局命名空间,Node 下可直接 require),这样单元测试可以同时跑在浏览器和 Node 两端,也是本次工程上最值得的一个决定——后面端到端走查出问题需要快速回归时,秒级的 Node 测试发挥了重要作用。

关键技术决策:

  • 数据模型:一条信息是一个扁平对象。type 区分寻物(lost)/ 招领(found);status 只有 open(进行中)/ resolved(已完成)两态,"已找到 / 已归还"是同一状态在两种类型下的展示文案(statusLabel()),避免出现两个需要同步的布尔字段;createdAt 时间戳用于排序与相对时间显示。

    { id, type: 'lost'|'found', name, cat, place, date,      // date 为丢失/拾获日期
      nickname, contact, note, status: 'open'|'resolved', createdAt }
    
  • 发布者身份:作业明确不做账号和实名认证,"我的发布"采用设备归属——发布时把信息 id 记入本机 clf_my_ids,凡是在这个 id 集合里的信息才显示状态操作面板。这是作业范围内的简化,README 里已向测试人员说明边界。

  • 状态闭环:这是上次讲评点名的问题,这次在产品层面给了完整答案——谁来改(仅发布者,本机判定)、从哪里改(详情页"发布者操作面板" + 我的发布页行内按钮两个入口)、改了别人在哪里看到(首页/搜索卡片灰显+徽章、详情页绿色横幅、我的发布统计),并支持误标记后一键恢复。

  • 健壮性:localStorage 访问包在探测逻辑里,被禁用时降级为内存存储并提示"数据仅本次会话有效";所有用户输入渲染前经 escapeHTML 转义,防止备注里写一段脚本把信息墙打挂;JSON 解析失败按空数据处理,不让页面白屏。

  • 零依赖运行:不引入任何框架与 CDN,图标全部是手绘的内联 SVG,Mocha/Chai 下载后本地化到 test/lib/——保证助教断网、双击 HTML 也能得到完整体验。

2. 关键实现的流程图 / 数据流图

① 总体数据流图——页面层不直接碰存储,全部经 core.js;这也是单测能抽离纯逻辑的原因:

总体数据流图

② 发布信息流程——表单提交先过 validatePost 逐字段校验,出错字段就地红框提示,通过才落库:

发布信息流程图

③ 搜索与筛选流程——关键词拆词后多词 AND 匹配,空结果有兜底引导,不会停在死胡同:

搜索筛选流程图

④ 信息状态流转图——呼应作业"更新状态"要求:两个状态、两个修改入口、三处结果展示,外加删除支线:

状态流转图

3. 重要代码片段与解释

(1)存储层的探测与降级(core.js)——所有读写都走这一个入口。探测失败不是简单报错,而是降级为内存存储,页面功能照常可用,只提示数据不持久化:

var memoryStore = {};                       // 降级用内存存储
var storage = (function () {
  try {
    var probe = '__clf_probe__';
    global.localStorage.setItem(probe, '1'); // 试写探测
    global.localStorage.removeItem(probe);
    return global.localStorage;
  } catch (e) {
    return null;                             // 部分环境禁用 file:// 存储
  }
})();
var isPersistent = !!storage;                // 首页据此显示"数据仅本次会话有效"提示

这段代码还藏着一个我们踩过的坑,放在第六节"遇到的困难"里细说。

(2)状态流转(core.js)——"已找到 / 已归还"不是两个字段,而是同一状态按类型映射的文案。恢复为进行中防止误标记无法撤回:

function resolveItem(id) {
  var item = getItem(id);
  if (!item) return null;                    // id 不存在不抛异常,返回 null 让页面提示
  return updateItem(id, { status: 'resolved' });
}
function statusLabel(item) {
  if (!item || item.status !== 'resolved') return '';
  return item.type === 'lost' ? '已找到' : '已归还';   // 寻物/招领各自的完成文案
}

(3)多关键词 AND 搜索(search.js)——关键词按空白拆词、转小写后,一条信息要在"名称 + 地点 + 备注 + 昵称"里全部命中才算匹配:"蓝色 耳机"能精确到一条,而不会把所有蓝色东西都翻出来:

function splitKeywords(q) {
  if (typeof q !== 'string') return [];
  var parts = q.trim().toLowerCase().split(/\s+/);
  return parts.filter(function (p) { return p; });
}
function matches(item, terms) {
  var text = [item.name, item.place, item.note, item.nickname].join(' ').toLowerCase();
  return terms.every(function (t) { return text.indexOf(t) >= 0; });
}

query() 组合函数把关键词、类型、分类、状态四个条件串起来,首页和搜索页调用的是同一份代码——单元测试覆盖的就是它。

(4)表单校验的日期边界(validate.js)——日期必须是真实存在的 YYYY-MM-DD(2 月 30 日直接拒绝),且不允许晚于今天;today 作为参数注入而不是在函数里取系统时间,测试用例因此可以固定日期、稳定复现:

function isValidDateStr(s) {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return false;
  var p = s.split('-'), y = +p[0], m = +p[1], d = +p[2];
  if (m < 1 || m > 12 || d < 1 || d > 31) return false;
  var dt = new Date(y, m - 1, d);            // 2月30日会被 Date 自动进位到 3 月
  return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d;
}
// 校验入口:validatePost(input, today),today 缺省才取系统当天

(5)渲染前统一转义(util.js + ui.js)——信息墙全部由用户输入构成,< > " 若不转义,一条"标题写 <script>"的信息就能打挂整个页面。转义抽成纯函数并配了专门用例:

function escapeHTML(s) {
  if (s === null || s === undefined) return '';
  return String(s)
    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
// 卡片渲染:'<h3>' + escapeHTML(item.name) + '</h3>' —— 所有用户字段一律先转义再拼接

4. 程序运行成果展示

按作业主流程「发布 → 浏览/搜索 → 详情 → 联系 → 更新状态」,在 Chrome 中以 file:// 直接打开实测(与助教评分方式一致),另配 24 项断言的自动化走查全部通过。运行结果如下:

① 首页信息墙——8 条信息按时间倒序,类型 / 分类 / 状态筛选与计数实时联动:

首页信息墙

② 发布招领——表单提交后弹出成功提示,可直达详情、返回首页或再发一条:

发布成功

③ 查看详情——完整信息 + 联系方式卡片,支持一键复制与拨打电话:

信息详情

④ 更新状态(发布者)——发布者标记「已归还」后,详情页出现绿色横幅,误标记可一键恢复为进行中:

标记已归还

⑤ 结果对所有人可见——首页与搜索结果中,已完成信息灰显并带状态徽章,避免其他同学继续无效联系:

首页灰显与徽章

⑥ 关键词搜索——搜索「雨伞」精确命中 1 条,无结果时另有兜底引导(见附加特点):

搜索结果


四、附加特点设计与展示

特点一:一键复制联系方式

意义:失物招领的"最后一公里"是联系。手机上看不清、抄错一位号码,线索就断了。详情页提供【一键复制联系方式】,把物品、类型、发布人、联系方式拼成一段话复制进剪贴板,贴到 QQ / 微信就能发起联系;纯数字手机号额外给【拨打电话】直达拨号盘。这一步把"查到信息"变成"联系上人",是对主流程的直接增强。

实现思路:优先用异步 Clipboard API;file:// 等环境可能权限受限,自动降级为隐藏 textarea + execCommand('copy') 的传统方案;再不行弹 toast 请用户手动复制——三级兜底,保证按钮永远有明确的反馈:

UI.copyText = function (text) {
  function legacyCopy() {
    var ta = document.createElement('textarea');
    ta.value = text;
    ta.style.position = 'fixed';
    ta.style.opacity = '0';
    document.body.appendChild(ta);
    ta.select();
    var ok = false;
    try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
    document.body.removeChild(ta);
    return ok;
  }
  if (navigator.clipboard && navigator.clipboard.writeText) {
    return navigator.clipboard.writeText(text).then(function () { return true; })
      .catch(function () { return legacyCopy(); });
  }
  return Promise.resolve(legacyCopy());
};

成果(详情页联系方式卡片 + 复制成功的 toast):

一键复制联系方式

特点二:分类 × 状态组合筛选 + 空状态兜底

意义:信息多了以后,"浏览"不能只靠滚动。首页提供"类型(全部/寻物/招领)× 物品分类 × 状态(进行中/已完成)"三重筛选,丢同学可以只看"进行中"的招领、按"证件卡类"快速缩小范围;已完成的信息默认可见(灰显 + 徽章),让"已找到/已归还"的结果对所有人透明——这正是状态闭环里"别人在哪里看到"的答案。搜索空结果时不留白屏,给出"清空重搜 / 去发布寻物"两条出路,把可能的流失转化为新发布。

实现思路:筛选逻辑全部收敛在 search.js 的 query(items, { type, cat, status, q }),页面只传条件对象;空状态是渲染层的约定——列表为空时必须给标题、说明和行动按钮,写进了 ui.js 的渲染约定。

成果(寻物 × 已完成的组合筛选结果;卡片灰显并带「已找到」徽章):

组合筛选

(搜索无结果的兜底页面):

搜索空结果兜底

特点三:演示数据一键重置(为测试人员设计)

首次打开自动灌入 8 条演示数据(含已完成的样本,日期基于当前时间动态生成、永不过期);【我的】页底部提供"载入演示数据 / 清空全部数据",数据如果测试乱了可以一键还原。

我的发布与数据工具


五、目录说明和使用说明

完整版在项目 README.md,此处为摘要,测试人员请以 README 为准。

目录组织:页面(5 个 HTML)+ 样式(css/style.css)+ 逻辑(js/ 下 5 个纯逻辑模块、1 个 UI 组件模块、5 个页面入口脚本)+ 测试(test/ 含本地化的 Mocha/Chai 与 5 个测试文件)+ 截图素材(screenshots/)。模块依赖关系:页面 → UI 组件 → 纯逻辑模块 → localStorage。

第四次作业/
├── index.html      # 首页:浏览 + 三重筛选(入口)
├── publish.html    # 发布页
├── search.html     # 搜索页
├── detail.html     # 详情页(?id=xxx)
├── my-posts.html   # 我的发布:状态管理 + 数据工具
├── css/style.css   # 全站样式
├── js/             # core 数据层 / validate 校验 / search 搜索 / util 工具 / seed 演示数据 / ui 组件 / page-*.js 页面逻辑
├── test/           # test.html 双击即跑单元测试;lib/ 已本地化 Mocha+Chai
└── screenshots/    # 流程图与运行截图

如何运行:

  1. 下载仓库全部文件,保持目录结构;
  2. 用谷歌浏览器双击打开 index.html——无需联网、无需安装、无需构建;
  3. 首次打开自动载入 8 条演示数据,按 README 第四节的走查说明即可体验完整流程;
  4. 单元测试:双击 test/test.html 查看测试报告(44 个用例应全部绿色通过)。

若浏览器限制 file:// 本地存储(页面顶部会出现提示条),可用 python -m http.server 8080 等任意静态服务器访问,功能不受影响。


六、单元测试

1. 测试工具的选择与学习过程

我们选用 Mocha(测试框架)+ Chai(断言库),原因有三点:

  • 博客园课程推荐资料里就有《测试框架 Mocha 实例教程》,入门资料充足;
  • describe / it / expect 的组织方式和我们按模块划分的代码天然对应;
  • 两者都有浏览器 UMD 版本,可以本地化后双击 HTML 运行,符合"助教下载即可验证"的交付要求。

学习路径:先读阮一峰的《测试框架 Mocha 实例教程》过一遍概念(BDD 接口、钩子函数),再看廖雪峰的 JavaScript 教程中关于浏览器测试的部分,然后直接在我们的 validate.js 上写第一个用例试手。Chai 我们用的是 expect 风格(expect(x).to.equal(y)),可读性最好。

我们的三步简易教程:

# 第 1 步:准备。项目 test/lib/ 已放好 mocha.js、mocha.css、chai.js(本地化,无需 npm)。
# 第 2 步:写用例。新建 test/validate.test.js:
var expect = (typeof window !== 'undefined' ? window : require('./lib/chai.js')).expect;
var V = typeof window !== 'undefined' ? window.CLFValidate : require('../js/validate.js');

describe('发布校验(validate.js)', function () {
  it('日期不能晚于今天(明天报错、今天通过)', function () {
    var input = { type: 'lost', name: '校园卡', cat: '证件卡类', place: '食堂',
                  date: '2026-09-30', nickname: '小林', contact: '13800138821' };
    expect(V.validatePost(input, '2026-09-29').errors).to.have.property('date');
    input.date = '2026-09-29';
    expect(V.validatePost(input, '2026-09-29').valid).to.equal(true);
  });
});
<!-- 第 3 步:运行。方式 A(零环境):test.html 里按顺序引入被测模块和测试文件后双击打开;
     方式 B(Node):npx mocha@10.8.2 test/validate.test.js -->

要点:被测模块写成 UMD(浏览器挂 window.XXX,Node 走 module.exports),同一份用例就能双端运行;beforeEach 里重置数据层,用例之间互不污染。

2. 测试代码展示与说明

共 44 个用例,覆盖五个纯逻辑模块 + 渲染安全。这里贴两组代表性的。

数据层状态流转(core.test.js)——验证"标记已完成"在两种类型下产出正确文案、误标记可恢复、删除不可逆:

it('resolveItem:寻物标记为「已找到」,招领标记为「已归还」', function () {
  var lost = CLF.addItem(BASE);
  var found = CLF.addItem(Object.assign({}, BASE, { type: 'found' }));
  expect(CLF.statusLabel(CLF.resolveItem(lost.id))).to.equal('已找到');
  expect(CLF.statusLabel(CLF.resolveItem(found.id))).to.equal('已归还');
  expect(CLF.resolveItem('不存在')).to.equal(null);      // 异常 id 不抛异常
});
it('reopenItem 可把已完成的恢复为进行中(防误标记)', function () {
  var item = CLF.addItem(BASE);
  CLF.resolveItem(item.id);
  expect(CLF.reopenItem(item.id).status).to.equal('open');
});

表单校验边界值(validate.test.js)——名称 30/31 字、备注 100/101 字、日期今天/明天,边界两侧都测:

it('物品名称:为空、超长均报错;30 字为边界可通过', function () {
  expect(V.validatePost(Object.assign(okInput(), { name: '   ' }), TODAY).errors).to.have.property('name');
  expect(V.validatePost(Object.assign(okInput(), { name: '卡'.repeat(31) }), TODAY).errors).to.have.property('name');
  expect(V.validatePost(Object.assign(okInput(), { name: '卡'.repeat(30) }), TODAY).valid).to.equal(true);
});

渲染安全(ui.test.js,浏览器环境)——把 <script>alert(1)</script> 塞进物品名称,断言渲染结果里只可能出现转义后的实体:

it('物品名称中的脚本标签被转义,不会注入 HTML', function () {
  var item = { id: 'xss1', type: 'lost', name: '<script>alert(1)</script>', /* … */ };
  var html = UI.cardHTML(item);
  expect(html).to.not.contain('<script>');
  expect(html).to.contain('&lt;script&gt;');
});

浏览器端运行结果(44 passing / 0 failing):

单元测试报告

3. 测试数据构造思路与自我评价

构造思路:以白盒方法为主——先看代码里每一个 if,让每个错误分支至少有一条用例踩到;再叠加三类系统性数据:

  • 边界值:所有长度限制(30/31 字、100/101 字)、日期临界(今天/明天)、时间格式化的区间端点(59 秒/1 分钟/59 分钟/1 小时);
  • 等价类与异常输入:合法等价类(手机号 / QQ / 微信号)、非法等价类(空、全空格、2026/09/29 这类格式错误、2026-02-30 这类不存在的日期);
  • 攻击性输入:<script>、<img onerror>、引号与 &——验证转义,模拟将来测试人员最可能使用的"刁难"手段。此外多关键词 AND("蓝色 耳机"命中 / "蓝色 雨伞"不命中)、大小写混排、排序稳定性(排序后原数组不被修改)也各有用例。

自我评价:这套用例能较好地守住纯逻辑层——数据存取、状态流转、校验规则、搜索语义都有分支级覆盖,将来改动这些模块,回归只需数秒。但它有明确的边界:页面交互(点击、表单联动、跨页面数据流)不在其覆盖范围内。为补上这一层,我们额外写了基于 puppeteer 的端到端走查脚本(24 项断言,模拟"双击打开 file:// 页面"的真实使用),并且事实证明单测全绿不等于应用可用——E2E 抓到了三个单测永远抓不到的问题(见第七节)。测试人员将来若继续刁难,可能的突破口还有:跨浏览器差异(我们只保证 Chrome)、存储配额写满、双开页面同时修改数据——前两者已有降级或提示,最后一点是纯前端方案的固有限制,只能靠真实账号体系解决。


七、GitHub 代码签入记录

按"每完成一个功能模块提交一次"的约定签入,commit message 描述当次功能或修复的问题:

b540858 Merge pull request #1 from OooneCloud/main
0607e8e 清理仓库:移除发布前内部待办清单(非产品文档,README 未引用)
b8d7937 同步张炜鑫本地修订:精简开篇引言、软化两处措辞、测试工具原因改为列表
42532a9 精简结对过程为事实概述(评分规则未要求展示结对过程)
b615828 补充结对过程:四节点对话整理(定契约/结对编码/用例评审/E2E走查)
2720c61 个人总结与队友评价定稿(张炜鑫)
d5f34d7 博客图片全部托管博客园(16/16 回读可用),签入记录截图入博,清单同步勾选
972efd8 博客补充「程序运行成果展示」小节:主流程 6 张实测截图
0169104 博客与发布清单:GitHub 地址更新为结对仓库 102401129-102401127
1864db5 第四次作业:README、发布清单、结对作业博客(含四张流程图与全流程截图)
51e303f 详情页分类图标跟随物品分类(与卡片一致)
67c6097 修复 E2E 走查发现的三个问题:UMD 工厂未传 global 致浏览器端存储降级、sticky 底部导航遮挡底部按钮、我的发布页误用通用卡片模板
9976370 第四次作业:单元测试(Mocha + Chai,42 个用例覆盖数据层/校验/搜索/工具/渲染安全)
9acf87a 第四次作业:发布页、搜索页、详情页与我的发布页(表单校验、关键词搜索、状态流转入口)
5e5a605 第四次作业:公共样式与 UI 组件(内联 SVG 图标、卡片渲染、toast、一键复制)+ 首页浏览与筛选
418e969 第四次作业:数据层与核心模块(localStorage 封装、发布校验、搜索筛选、演示数据)

(仓库:github.com/lin634/102401129-102401127)

GitHub 代码签入记录(含 PR 合并,两位成员的提交)

八、结对过程

本次按"先定契约 → 结对编码 → 交叉走查"推进:开工前共同评审数据结构与公共接口(status 两态建模、ui.js 函数签名);编码阶段按模块轮换驾驶员 / 领航员——数据层与校验由林诗航主写、张炜鑫逐条核对边界,页面与样式由张炜鑫主写、林诗航审查;测试阶段先由张炜鑫按分支设计用例(列用例过程中即发现两处漏判),再用 Puppeteer 端到端走查兜底,连抓三个单元测试覆盖不到的问题(详见第九节)。全部讨论结论均以功能粒度签入仓库,记录见第七节。

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

问题一:单元测试全绿,浏览器里数据却"活不过一个页面"

  • 问题描述:端到端走查时发现,发布的信息跳到详情页就"查无此信息",所有页面的数据都只在单页内有效——等于状态闭环整个断了。诡异的是,44 个单元测试全部通过。
  • 做过哪些尝试:先怀疑无头浏览器的 file:// 限制,做了最小实验页证明解析期、加载后访问 localStorage 都正常、中文路径也不影响;又在 core.js 的探测失败分支里把被吞掉的异常打印出来,拿到了关键线索:TypeError: Cannot read properties of undefined (reading 'localStorage')。
  • 是否解决:已解决。根因是 core.js 的 UMD 封装——外层 IIFE 调用 factory() 时忘了把 global 传给工厂函数,工厂里 global.localStorage 因此抛了 ReferenceError;而这个错误恰好被我们自己写的"探测失败就降级为内存存储"的 try/catch 吞掉了,于是应用在浏览器里一直安静地跑在"内存模式"。修复只需一行:factory() 改为 factory(global)。
  • 有何收获:这是本次作业印象最深的一课——防御式编程会把 bug 藏进降级路径里。try/catch 兜底方案必须有"兜底是否生效"的可观测信号(我们后来在首页加了"数据仅本次会话有效"的提示条,它其实也是降级发生时的报警器);同时"单测全绿"只覆盖你想到的路径,必须有一层模拟真实使用的端到端走查。

问题二:真实点击"立即发布",表单被清空且页面刷新

  • 问题描述:E2E 走查中,模拟真实坐标点击提交按钮时页面整体刷新、填好的内容全部丢失;改用 DOM 层 click() 却一切正常。
  • 做过哪些尝试:先后怀疑了设备像素比导致的坐标偏移、Windows 显示缩放、表单默认提交行为,都排除后,在页面里注册导航监听发现:点击确实触发了一次导航——按钮被滚动到视口底部时,恰好压在悬浮的 sticky 底部导航栏下面,点击落在了底部导航的"发布"标签上,等于点了链接跳转回自己。
  • 是否解决:已解决,且这不是自动化工具的假问题——真实用户把页面滚到同样位置一样会误触。修复:三个滚动内容区增加 88px 底部留白,并给 html 加 scroll-padding-bottom,保证任何按钮都能滚动到底部导航之上。
  • 有何收获:老师上次说"界面画出来和用户能够用起来是两回事",这个 bug 是最生动的注脚——原型阶段我们给底部导航和内容区的遮挡问题画过重点,实现时还是栽在了同样的地方。悬浮元素必须有对应的布局补偿,这类约束应该进代码规范。

问题三:模块重构后的"silent miss"

  • 问题描述:把转义、时间格式化从 ui.js 抽成 util.js 后,五个页面忘了引入新文件,ui.js 找不到 CLFUtil 直接报错,页面数据区空白。因为测试页 test.html 引入了全部模块,单测依然全绿。
  • 做过哪些尝试:E2E 报错后直接读浏览器控制台,一眼定位到缺失的模块引用。
  • 是否解决:已解决(五个页面补引 util.js),并顺带把"页面脚本清单"固定为 HTML 模板注释。收获:重构的破坏面要用自动化走查兜住,"测试能跑"和"页面能跑"的依赖集合并不相同。

结对协作上的困难:两人页面分工并行时,公共组件(卡片渲染、toast)的接口一度各自为政,合并时出现两套样式。解决办法是把"公共层先定契约、页面只消费"立为规矩:ui.js 的函数签名由两人共同评审后才动工,之后再没出现样式打架。另外调试 E2E 时时区、日期等因素让复现不稳定,我们约定"能注入的依赖一律注入"(如 validate 的 today 参数),测试稳定性明显提升。


十、评价队友

林诗航评价张炜鑫(102401127)

值得学习的地方:炜鑫在页面样式和交互细节上把控得很不错,审美在线。首页信息墙卡片、状态徽章还有已完成条目灰显这些效果,初稿完成度就很高,基本不需要反复修改。在测试这块他很擅长挑问题,算是我的“把关人”,我写的校验逻辑,他会主动去试各种边界场景,像文本长度上限、非法日期这类情况,不少我遗漏的分支都是他先发现的。流程图也画得很清晰,把状态修改权限、操作入口、展示位置这些关键点都完整表达出来。

需要改进的地方:遇到底层代码问题时,有时会满足于功能跑通,不太愿意深挖背后的原理。就像localStorage降级那个bug,一开始他觉得只要页面能用就可以,后面我们一起排查才找到被try-catch隐藏的报错。另外写页面代码的时候注释可以再多补充一点,后续我接手调试他负责的页面会更容易看懂。

十一、个人总结

林诗航(102401129)

做完这次结对作业,我真切感受到“测试左移”不是书本上的空话。编码阶段就同步编写单元测试,原本以为这样就能保证程序质量,没想到端到端走查还是找出了三处严重bug。特别是UMD封装的问题,我们自己写的降级逻辑直接掩盖了报错,单元测试完全发现不了。这也让我读懂《构建之法》里的观点:开发自测和完整的用户场景测试是两回事,单元测试用来保证底层逻辑正确,端到端走查验证真实用户操作流程,二者缺一不可。

这次也锻炼了我的排错思路:碰到“单测全部通过,但网页实际运行异常”这类问题,优先构造最小复现案例、把被捕获隐藏的异常打印出来,远比靠经验猜测高效。后续开发项目,我会把两条规则加到个人代码规范里:降级处理逻辑必须带上提示告警;悬浮导航这类UI组件,一定要预留布局补偿防止遮挡交互按钮。

posted @ 2026-10-05 22:29  gnahihsnil  阅读(11)  评论(0)    收藏  举报