如何测试英文打字速度?用状态机组织计时、字符反馈与 WPM 统计
深圳技术大学
代码仓库
打字测试页面很容易做出一个输入框和倒计数字样,但统计是否可信,取决于一系列相互关联的状态:何时开始计时、退格后如何计算正确率、输入结束后是否继续累计时间、重新开始是否留下旧定时器、更换文本是否重置进度。任何一个环节遗漏,都可能让最终数字与用户操作脱节。
这个项目选择短篇英文文本和经过时间模式。点击开始就计时,当前输入逐字符匹配,输入长度达到目标长度后结束,并显示位置正确率和按正确字符折算的 WPM。它不是考试认证系统,也不提供排名。
先明确指标,再实现界面
WPM 的换算使用每五个正确字符等价一个单词,再除以经过分钟数。这里的字符包括空格和标点,不是通过空格分词统计英文单词。正确率按当前输入各位置与目标文本的匹配比例计算;已被退格删除的错误不继续累计。页面把它标成“当前位置正确率”,避免用户误以为它记录了整个过程中每一次错误按键。
空输入时显示百分之百,表示当前没有错误,不意味着完成了练习。零时间时 WPM 显示零,避免除以零产生 Infinity。开始按钮点击后即记录 performance.now,所以阅读文本后迟迟不输入也会增加用时。工具没有把计时悄悄延后到第一个字符,也没有同时混用倒计时和经过时间。
结束条件是输入长度达到目标长度,允许带错误完成。这样用户可以看到完整练习中的位置准确率,而不是必须修正所有错误才能结束。输入框设置最大长度,不能继续追加多余字符。结束后冻结输入与时间,只有重新开始或更换文本才能进入新一轮。结束规则、WPM 与正确率定义都写在界面中,让结果可以被解释。
三种状态足以约束主要交互
页面只有 ready、running 和 finished 三个阶段。准备阶段输入禁用、开始可用,点击开始记录起点并启动唯一的刷新定时器。运行阶段输入可用、开始禁用,避免多次点击叠加计时器。结束阶段清除定时器、禁用输入,保留最后一次统计。按钮状态跟随阶段变化,而不是分别维护一组互不关联的布尔变量。
重新开始先 clearInterval,再清空时间、输入和统计,回到 ready。更换文本复用同一个重置流程,额外选择另一段文本。文本索引的选择会避开当前段落,所以点击更换不会碰巧得到原来的内容。准备状态不会自动开启新计时器,用户可以先阅读新的短文,再主动开始。
计时采用“当前高精度时间减起点”,而不是每次定时器回调加零点一秒。浏览器可能延迟或合并回调,后台标签页的刷新也可能变慢。如果靠回调次数累计时间,显示就会低估真实经过时间。定时器仅负责刷新界面,时间差才是用时来源。完成输入时再次读取时间,确保最终结果取自完成瞬间。
以下代码摘自交付项目的 core.js:
(function(root){'use strict';constpassages=['Small steps make complex ideas easier to understand. Keep learning and build something useful every day.','Clean code tells a clear story. Choose simple names, test the important parts, and leave room for change.','A browser turns text into living interfaces. Every event is a chance to make the experience more helpful.'];functionscore(target,input,elapsed){letcorrect=0;for(leti=0;i<input.length;i++)if(input[i]===target[i])correct++;constms=Math.max(0,Number(elapsed)||0);return{correct,errors:input.length-correct,accuracy:input.length?correct/input.length*100:100,wpm:ms>0?(correct/5)/(ms/60000):0,elapsed:ms,complete:input.length===target.length};}functionnextIndex(current,length,random=Math.random){if(length<2)return0;return(current+1+Math.min(length-2,Math.floor(random()*(length-1))))%length;}root.ToolCore={passages,score,nextIndex};if(typeofmodule!=='undefined')module.exports=root.ToolCore;})(globalThis);字符高亮如何与输入修正同步
每次 input 后重新比较当前字符串。目标文本的每个字符都生成独立 span,已输入且匹配的位置标为绿色,不匹配的位置标为红色,下一位置使用下划线提示。所有字符通过 textContent 放入元素,空格和标点也按原文保留。页面采用等宽字体与保留空白的排版,便于观察连续空格和位置偏移。
使用 input 而非只监听 keydown,让删除、选区替换和常见编辑操作自然反映到当前文本。组合输入阶段暂不处理尚未提交的值,compositionend 后再计算。由于练习目标是英文,输入法提交的其他字符通常会被标为不匹配,而不是被工具猜测为某个目标字符。粘贴和拖入被明确阻止,提示用户逐字键入。
阻止粘贴并不构成防作弊系统。开发者工具或自动化仍然可以直接设置输入,这也是本次浏览器测试重放完整目标文本的方法。截图中的完成 WPM 来自自动化填入,不是人工真实打字成绩,不能拿来宣传某个人的速度。工具的目标是学习计时与输入联动,指标验证和成绩可信来源是两件不同的事。
完成与重置是最重要的验证点
浏览器验证先开始计时,再输入一个错误字符,确认错误高亮出现。随后通过自动化填入目标文本,检查阶段变成完成、正确率为百分之百,并等待一段时间再次读取用时,要求保持不变。这个检查能够发现“视觉上已经结束,但定时器仍然在后台更新”的问题。单凭看到完成提示,不能证明时间已经冻结。
重置验证检查用时归零、输入为空、输入框禁用,再等待确认没有旧定时器把数字改回来。更换文本验证检查新文本确实不同,并再次开始一轮。粘贴事件测试则确认界面提示出现;它验证事件处理,不声称覆盖所有外部输入途径。核心测试另外精确检查正确字符数量、错误数、零时间和随机索引边界。
哪些设计仍可继续扩展
当前内容来自内置的三段短文,因此无需网络请求,也不会因为远程服务失败而不能练习。随机选择只用于文本顺序,不涉及安全随机需求。所有练习记录停留在当前页面内存,刷新后不会自动保留历史成绩。希望保存历史时可以增加本地记录,但应先定义成绩的版本、文本长度和指标口径,避免把不同规则产生的分数混在一起。
如果增加累计错误率,就需要在每次输入变更时记录事件历史,并定义退格、替换与组合输入的计算方式;不能仅把当前正确率改一个标签。若增加倒计时,则要明确计时到零与输入完成同时发生时的结束优先级。状态机可以继续扩展,但每个新增状态都应对应明确按钮可用性和定时器生命周期。
这个项目说明,即使界面规模很小,也值得把时间来源、数据口径和状态转换写清楚。核心公式可以单独测试,页面负责呈现当前实验状态,真实浏览器再验证事件与计时之间的连接。做到这些,最终显示的数字才具有可解释性,而不是一个看起来不断变化的装饰。
当前正确率与累计行为记录的区别
当前位置匹配有一个直观后果:如果漏掉一个字符,之后多个位置都可能变红;补回该字符后,后续位置可以重新变绿。这反映字符串位置比较,而不是逐词容错或编辑距离。工具不自动纠正文法,也不忽略大小写和标点。读者据此可以理解为什么一个漏字会影响多处颜色,而不是怀疑每个字符都独立输入错了。
计时器与统计函数也各有责任。纯函数只接收目标、当前输入与经过毫秒,不自行访问页面时钟;应用层决定何时开始、刷新和冻结。这样测试可以用一分钟的固定时间检查 WPM,而不必真的等待一分钟。浏览器层再验证完成后数字不变,才能覆盖定时器是否关闭的问题。数学测试和生命周期测试互相不能替代。
后续若做历史成绩比较,短文长度和错误处理规则必须随成绩保存。二十个字符快速完成的 WPM 波动很大,不能与长时间测试直接比较;开始后阅读时间也会被计入。当前界面选择短文是为了演示,并未定义排名规则。因此文章只解释本工具计算口径,不把自动化截图里的极高数值解释成稳定的人类输入能力。
运行、文件职责与本次验证记录
本项目目录为08-typing-test,采用原生 HTML、CSS 和 JavaScript,没有后端、外部字体或运行时 CDN。直接打开 index.html 可以使用主要功能;本次也通过真实浏览器验证了 file 协议下的主要交互。更推荐进入该目录执行下列命令,以 localhost 方式运行,便于获得一致的网页源和浏览器 API 行为。静态服务只监听本机回环地址,不需要配置账户或上传个人数据。
node serve.cjs.4279# 打开 http://127.0.0.1:4279/node--test test.cjsindex.html 负责语义结构和资源引用;styles.css 负责卡片、表单与移动宽度布局;core.js 保存本工具独立逻辑,同时导出给 Node 测试;app.js 负责事件绑定、状态变化和页面渲染。ui.js 提供状态提示、复制及本地下载等通用能力。脚本使用 defer 并按核心、通用界面、应用顺序加载,既等待 DOM 就绪,也让依赖关系明确。修改核心规则时先运行对应测试,再检查界面是否继续解释同一规则。
测试结果分成两层:Node 核心测试本次执行2 项,2 项通过、0 项失败、0 项跳过;真实 Chrome 155.0.8059.84 浏览器验证4 项通过、0 项失败、0 项跳过。浏览器的每项记录是一个包含多个断言的验证场景,不应与纯逻辑测试条数混算成同一种数量。原始记录保存在 docs/unit-results.tap 与 docs/browser-results.json,可以核对日期、场景和控制台检查,而不是只引用文章中的结论。
本次浏览器场景如下:
- 计时与字符反馈,完成后冻结结果
- 重新开始与更换文本清空计时和状态,禁止粘贴
- file:// 独立加载并执行主要交互,无页面或控制台错误
- 390px 手机宽度没有页面横向溢出
浏览器检查通过 Playwright 驱动实际 Chrome 页面,执行输入、点击、键盘或文件操作,并保存本文三张截图。相关脚本位于工具合集的 scripts/browser-check.cjs;使用合集复现时先运行 npm start,再在另一终端安装对应 Playwright 依赖并执行 npm run test:browser。本次使用环境已有运行库,没有要求运行页面的读者安装构建链。单独项目只需现代浏览器;Node 用于本地静态服务与纯逻辑测试,两个用途彼此独立。
本次页面错误与控制台错误记录均为空。三百九十像素宽度检查确认页面没有整体横向溢出,局部代码区、预览或表格允许自身滚动。这只是当前 Chrome 环境的布局证据,并非已经实测所有手机、旧浏览器或辅助技术。其他现代浏览器预计可以使用相关原生 API,但 Firefox、Safari 与实体移动设备本次未实测;若业务依赖特定平台,应按目标环境追加验证。