可访问性与性能预算:让流式界面对所有人可用
流式输出对屏幕阅读器是一场灾难,而把实时区域直接指向流式元素恰恰是最常见也最错的做法。这一天用分段播报解决它,再给整个工作台立一份性能预算,最后把七天的成果收成一个能放进作品集的项目。
今日目标
- 能说出把实时区域指向流式元素为什么会失败,并实现按句子完成的分段播报
- 能为工作台设定并测量性能预算,说清哪些指标对 Agent 界面最关键
- 能完成键盘可达与焦点管理,让不用鼠标的用户也能走完审批流程
最后一天,补上两件一直欠着的事。读完回到页面顶部把三条目标勾掉。
小白版讲解
给听不见的人的字幕:不能每来一个字就重播整段
同传会场通常还会配字幕,给听障观众看。
假设字幕员的工作方式是:每听到译员说一个新词,就把整句话从头到尾重新打一遍到屏幕上。看字幕的人会怎样?他们永远读不完一句话——刚读到一半,整句话被擦掉重来。
这正是流式界面对屏幕阅读器做的事,如果你用了那个最自然的做法:给流式渲染的那个元素加上 aria-live。
而这一天要讲的核心结论是:那个最自然的做法是错的。
实时区域的两个陷阱
屏幕阅读器通过 aria-live 属性感知内容变化。给流式消息元素加上它,看起来天经地义。但有两种配置,两种都不行:
| 配置 | 后果 |
|---|---|
aria-atomic="true" | 每来一个增量就重念整段,用户听到的是不断从头开始的噪音 |
aria-atomic="false" | 高频 DOM 变化被整个跳过,用户什么都听不到 |
第一种是上面那个字幕员。第二种更隐蔽:屏幕阅读器有自己的节流机制,每秒几十次的变化超出了它的处理节奏,它会直接放弃。
更麻烦的是,NVDA、JAWS、VoiceOver 三家对高频变化的处理各不相同。所以「我在我的电脑上试过,能读」在这里特别不可靠——你只验证了三分之一。
正确做法:流式期间静默,按句子分段播报
思路是把视觉呈现和听觉呈现解耦:
- 视觉上,文字继续逐字出现(视觉用户需要这个反馈)
- 听觉上,流式期间完全静默,等一个句子完整了,才把这一句推进实时区域
function flush(fullText: string, force: boolean) {
let rest = fullText.slice(consumed) // 只处理没播报过的部分
while (rest.length > 0) {
const match = /[。!?;.!?;]\s*/.exec(rest)
if (match) {
const end = match.index + match[0].length
emit(rest.slice(0, end).trim()) // 推一个完整句子
consumed += end
rest = rest.slice(end)
continue
}
// 没遇到标点:超长或收尾时强制切一段,否则等下一批增量
if (force || rest.length >= maxChars) { emit(rest.trim()); consumed += rest.length }
return
}
}两个细节:
consumed 记录已播报到哪里,只推新增部分,绝不重复。这是「不重念」的实现基础。
没有标点的长文本要有兜底。否则一段没有句号的文字会一直不播报,用户干等着。
效果对比很直观:本课 lab 里一段三句话的文本,正确实现触发 3 次播报;退化成「每次播报全文」的话是 37 次,每次都从头开始。
实时区域本身也有三个坑
写对了分段还不够,承载播报的那个元素有三个容易错的地方。
一,视觉隐藏不能用 display: none。
/* 错:屏幕阅读器也读不到,等于把实时区域废掉了 */
.hidden { display: none; }
/* 对:视觉隐藏,辅助技术仍可读。这是无障碍领域的标准写法 */
.visually-hidden {
position: absolute;
width: 1px; height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}visibility: hidden 同理,也会让屏幕阅读器忽略它。
二,页面加载时必须是空的。 带着内容出现的实时区域不会被播报——规范要求区域先存在、内容后变化才触发。
三,用 polite 而不是 assertive。 assertive 会打断用户当前正在听的内容。Agent 的回复是信息不是警报,打断用户是很粗鲁的。只有错误和需要立即决定的审批才配用 assertive。
键盘可达:不用鼠标能不能走完审批
Agent 界面有一类特别关键的键盘场景:审批。
如果用户只能用键盘(视力障碍、手部障碍、或者单纯习惯键盘),而你的审批按钮 Tab 不到,那他就无法批准或拒绝任何操作——这不是体验问题,是功能性排除。
几条具体要求:
焦点必须看得见。 很多项目为了「好看」把 outline 去掉,那会让纯键盘用户彻底迷路。用 :focus-visible 可以只在键盘操作时显示,鼠标点击时不显示,两全其美。
给一个跳转链接。 页面第一个可聚焦元素是「跳到主内容」,让键盘用户不必每次穿过整个导航。它平时藏在屏幕外,聚焦时才出现。
焦点管理要克制。 新消息到达时要不要把焦点移过去?不要。用户可能正在输入框里打字,抢走焦点是很粗暴的。正确做法是用实时区域告知,而不是用焦点强迫。
只有一种情况该主动移焦点:打开了一个模态对话框——因为那时用户的其余操作本来就被阻断了。
两条容易漏的无障碍要求
除了播报和键盘,还有两条在流式界面里特别相关。
尊重「减少动态效果」偏好。 系统设置里开启这一项的用户,对动画敏感,严重时会引发眩晕或偏头痛。而流式界面天然动得多:闪烁的光标、平滑滚动、内容不断出现。
@media (prefers-reduced-motion: reduce) {
.cursor { animation: none; }
* { scroll-behavior: auto !important; }
}注意这个偏好不是要求你停掉流式本身——内容逐字出现是功能不是装饰。要停的是纯装饰性的动画。
状态不能只靠颜色表达。 工具卡片的成功与失败如果只有绿和红的区别,色觉障碍用户就区分不了。本课的卡片同时用了文字标签(「完成」「失败」「已拒绝」),颜色只是辅助。这条规则简单到常被忽略:任何用颜色传达的信息,都要有一个不依赖颜色的等价表达。
性能预算:给 Agent 界面立四条能测的线
前面几天做了很多优化,但「优化到什么程度算够」一直没答案。性能预算就是那个答案。
为什么要有预算而不是「尽量快」:没有数字就没有判据,每次讨论都会变成主观感受之争。
本课用的四条,是按 Agent 界面的实际瓶颈选的,不是通用 Web 指标的照搬:
| 指标 | 预算 | 为什么是它 |
|---|---|---|
| 首字延迟 | 1000ms | 用户等的是模型开口,这是 Agent 界面最关键的一条 |
| 最长阻塞 | 50ms | 超过这个数用户就能感到输入卡顿 |
| 每秒状态更新 | 70 次 | D2 的按帧合并就是为了这条,超了说明合并没生效 |
| DOM 节点数 | 5000 | 长会话的内存与渲染成本,超了该上虚拟化了 |
注意首屏渲染时间不在这个表里。传统 Web 最看重的指标,在 Agent 界面里远不如首字延迟重要。预算要按你的场景选,不是抄一份通用清单。
一个实现细节:没测到的指标要如实显示「没数据」,而不是显示 0。显示 0 会让人以为达标了。
收尾:把七天的成果包装成作品集项目
最后一节讲怎么把这个工作台变成一个能拿出手的东西。
讲问题,不要讲功能。 「实现了流式渲染」是功能,「解决了每秒 80 个增量下的渲染风暴,把渲染次数从 176 次降到 57 次」是问题加结果。后者才说明你理解了自己做的东西。
每个决定都要能说出为什么。 面试官会挑一处问「为什么这么做」。本课七天里每个刻意的取舍——审批为什么是中断恢复、replace 为什么必须严格失败、长任务为什么不画百分比、播报为什么不指向流式元素——都是这类问题的好素材。
诚实标注没做的部分。 开放式生成式界面没做、虚拟化只做了原理没上库、屏幕阅读器只在 VoiceOver 上验过。主动说出边界比假装完备更可信,而且往往能把话题引到你想聊的地方。
七天到此结束。你手里现在有一个对齐公开协议、不依赖任何 Agent 前端 SDK、并且在性能与可访问性上都有明确交代的工作台——这比多数「AI 聊天页面」的完成度高出不止一个档次。
源码导读
动手实验
这是全课最需要真人验的一天。请务必真的打开一次屏幕阅读器(macOS 按 Cmd 加 F5 开 VoiceOver),把 aria-atomic 在 true 和 false 之间切一次,亲耳听听差别。读十遍文档不如听一遍。
- 实现分段播报器,按句子完成推入实时区域
- 在 VoiceOver 上实测一次,记录实际听到的效果
- 补齐键盘可达与焦点管理,走通一次纯键盘的审批流程
- 设定性能预算并测量,记录三条线的实测值
- 写作品集说明,讲清楚这个工作台解决了哪些问题
面试题
今天 4 道题在下方题库区,侧重流式内容的无障碍播报、性能预算的设定与测量、交互可达性。展开后先看"分析过程"再看要点——照着推导练,比背要点管用。标注"国内高频 / 海外高频"方便按目标市场取舍。
检查清单与明日预告
- 能说出把实时区域指向流式元素为什么会失败,并实现按句子完成的分段播报
- 能为工作台设定并测量性能预算,说清哪些指标对 Agent 界面最关键
- 能完成键盘可达与焦点管理,让不用鼠标的用户也能走完审批流程
- 能说出为什么新消息到达时不该抢走键盘焦点
- 实验的 6 条验收标准全部通过
- 4 道面试题不看要点也能答出至少 3 道
七天结束了。回头看这条线:D1 读通一条流,D2 让它不抖,D3 让用户看见并拦住工具调用,D4 让它显示思考与状态,D5 让模型参与界面生成,D6 让会话可控可分支,D7 让所有人都能用。贯穿始终的其实只有一句话——把「不知道」变成「看得见」,剩下的都是这句话在不同层面的展开。接下来最值得做的一件事,是把这个工作台接到一个真实的模型后端上(D7 的 lab 里有说明),你会立刻发现哪些地方是被离线剧本的确定性掩盖掉的。
面试题库
为什么把实时区域直接指向流式渲染的元素是错的?正确做法是什么?Why is pointing a live region at the streaming element wrong, and what should you do instead?
国内高频海外高频深入#accessibility#aria-live#streaming分析过程 · 先想清楚再作答
- 这题的区分度极高,因为那个错误做法是几乎所有人的第一反应,而且它看起来完全合理——内容在变,加个 aria-live 让屏幕阅读器知道,有什么问题?
- 问题在于两种配置都不行,要能把两种都说出来:aria-atomic 为 true 时,每来一个增量就**重念整段**,用户听到的是不断从头开始的噪音;为 false 时,每秒几十次的 DOM 变化超出了屏幕阅读器的处理节奏,它会**直接跳过**,用户什么都听不到。
- 还有一层加重了问题:NVDA、JAWS、VoiceOver 三家对高频变化的处理各不相同。所以「我在我电脑上试过能读」在这里特别不可靠——你只验证了三分之一。
- 正确做法是把**视觉呈现与听觉呈现解耦**:视觉上文字继续逐字出现,听觉上流式期间完全静默,等一个句子完整了才把这一句推进实时区域。视觉用户逐字看,屏幕阅读器用户按句子听,节奏相当而且都不被淹没。
- 实现上有两个必须做对的细节:用一个已播报位置的游标,只推新增部分绝不重复;以及给没有句末标点的长文本一个字符数兜底,否则一段没有句号的文字会一直不播报,用户干等着。
- 可预期的追问是「实时区域元素本身有什么讲究」——三个:视觉隐藏不能用 display 为 none 或 visibility 为 hidden(那样屏幕阅读器也读不到),要用绝对定位加裁剪的标准写法;页面加载时必须是空的,带着内容出现的区域不会被播报;用 polite 不用 assertive,因为 assertive 会打断用户正在听的内容,而 Agent 的回复是信息不是警报。
How to reason about it · think before answering
- A strong discriminator, because the wrong approach is nearly everyone's first instinct and looks entirely reasonable — content is changing, so add aria-live and let the screen reader know.
- The problem is that both configurations fail, and you should name both: with aria-atomic true, every delta **re-announces the whole passage**, producing a stutter of restarts; with false, dozens of DOM mutations per second exceed the screen reader's pacing and it **skips them entirely**, so the user hears nothing.
- One more aggravating factor: NVDA, JAWS, and VoiceOver each handle high-frequency changes differently. 'It read fine on my machine' is especially unreliable here — you verified one third of the field.
- The right approach **decouples visual from auditory presentation**: visually the text keeps appearing character by character, while audibly nothing is announced until a sentence completes, at which point that sentence is pushed to the live region. Sighted users read progressively, screen reader users hear sentence by sentence, comparable pacing and neither is flooded.
- Two implementation details matter: keep a cursor of what has already been announced and push only the new portion, and add a character-count fallback for long text without terminal punctuation, or an unpunctuated passage never announces at all.
- Expect the follow-up on the live region element itself: three things. Visual hiding must not use display none or visibility hidden, since screen readers ignore those; use the standard absolutely-positioned clipped pattern. The region must be empty on page load, because a region that appears with content in it is not announced. And use polite rather than assertive, since assertive interrupts what the user is currently hearing, and an agent's reply is information, not an alarm.
答题要点
- atomic 为 true 会每个增量重念整段,为 false 会被屏幕阅读器整个跳过,两种都不行。
- 三家屏幕阅读器对高频变化处理各不相同,单机验证不可靠。
- 正确做法是视觉与听觉解耦:流式期间静默,按句子完成时分段播报。
- 实现要点是只推新增部分不重复,以及给没有标点的长文本加字符数兜底。
- 实时区域本身:视觉隐藏不能用 display none,加载时必须为空,用 polite 不用 assertive。
Key points
- aria-atomic true re-announces everything per delta; false gets skipped entirely. Neither works.
- The three major screen readers differ on high-frequency changes, so single-machine verification is unreliable.
- Decouple visual from auditory: stay silent while streaming and announce sentence by sentence.
- Track what has been announced to avoid repeats, and add a character-count fallback for unpunctuated text.
- The region itself: never hide with display none, keep it empty on load, and use polite rather than assertive.
你会给一个 Agent 聊天界面设哪几条性能预算?分别怎么测?What performance budgets would you set for an agent chat interface, and how would you measure each?
国内高频海外高频进阶#performance#metrics#agent-ui分析过程 · 先想清楚再作答
- 这题在考你会不会按场景选指标,而不是背一份通用 Web 性能清单。直接答 LCP、FID、CLS 那几个,说明没想过 Agent 界面特殊在哪。
- 先说为什么要有预算:**没有数字就没有判据**,每次关于「够不够快」的讨论都会变成主观感受之争。定下线之后,超了就是超了。
- 四条按 Agent 界面实际瓶颈选的:首字延迟(用户等的是模型开口,这是最关键的一条,用点击到第一个增量到达的时间差测);最长单次阻塞(超过 50ms 用户就能感到输入卡顿,用长任务观察器或性能面板测);每秒状态更新次数(按帧合并有没有生效的直接指标,自己计数除以耗时);DOM 节点数(长会话的内存与渲染成本,超了说明该上虚拟化)。
- 值得主动说出来的是**首屏渲染时间不在这个表里**。传统 Web 最看重的指标在这里远不如首字延迟重要,因为用户的等待焦虑来自模型什么时候开口,不是页面什么时候画完。这一句能说明你是按场景思考的。
- 实现上有个小而重要的细节:没测到的指标要如实显示「没数据」而不是 0,显示 0 会让人误以为达标。
- 可预期的追问是「预算超了怎么办」——按 D2 的顺序处理:先量清楚瓶颈在渲染、解析还是布局,再依次上按帧合并、降低更新优先级、memo、两趟渲染,虚拟化放最后因为它会让滚动锚定和搜索全部变复杂。
How to reason about it · think before answering
- This tests whether you choose metrics for the scenario rather than reciting a generic web performance list. Answering LCP, FID, and CLS suggests you have not considered what makes agent interfaces different.
- Start with why budgets exist: **without numbers there is no criterion**, and every 'is it fast enough' discussion degenerates into competing impressions. With a line drawn, over is over.
- Four budgets chosen for actual agent bottlenecks: time to first token (users are waiting for the model to speak, the single most important one, measured from click to first delta); longest single blocking task (beyond about 50ms typing feels laggy, measured with a long-task observer or the profiler); state updates per second (a direct signal of whether per-frame batching works, counted and divided by elapsed time); and DOM node count (memory and render cost in long sessions, a threshold for adopting virtualization).
- Worth volunteering: **first contentful paint is not on this list**. The metric traditional web performance cares most about matters far less here, because the user's anxiety is about when the model starts talking, not when the page finishes painting. Saying this shows you reason from the scenario.
- A small but important detail: metrics you have not measured should display as 'no data', never zero, since zero reads as passing.
- Expect the follow-up on exceeding budget: follow day two's order — profile first to locate the cost in rendering, parsing, or layout, then apply per-frame batching, lowered update priority, memoization, and two-pass rendering, leaving virtualization last since it complicates scroll anchoring and search.
答题要点
- 先说原则:没有数字就没有判据,预算的价值是终结主观感受之争。
- 四条是首字延迟、最长单次阻塞、每秒状态更新次数、DOM 节点数。
- 首屏渲染刻意不在表里:用户等的是模型开口,不是页面画完。
- 没测到的指标显示「没数据」而不是 0,显示 0 会被误读成达标。
- 超标时按先量后调的顺序处理,虚拟化放最后因为它会让别的功能变复杂。
Key points
- State the principle: without numbers there is no criterion, and budgets end arguments from impression.
- Four budgets: time to first token, longest blocking task, state updates per second, DOM node count.
- First contentful paint is deliberately absent: users await the model speaking, not the paint.
- Unmeasured metrics show 'no data', never zero, which would read as passing.
- When over budget, measure before tuning, and leave virtualization last since it complicates other features.
新消息流式到达时,键盘焦点应该跟着走吗?说出你的判断和理由。When a new message streams in, should keyboard focus follow it? Justify your answer.
国内高频海外高频进阶#accessibility#keyboard#focus-management分析过程 · 先想清楚再作答
- 这题是个陷阱题,因为「让焦点跟随新内容」听起来像是在做无障碍优化,实际上是帮倒忙。
- 答案是**不该**,理由一句话就够:用户可能正在输入框里打字,抢走焦点是很粗暴的。而且流式内容每秒都在变,焦点跟着跑会让键盘用户完全无法操作。
- 正确做法是用实时区域**告知**,而不是用焦点**强迫**。这是一条通用原则:通知用途用实时区域,焦点只用于用户主动发起的导航。
- 唯一该主动移焦点的情况是**打开了模态对话框**——因为那时用户的其余操作本来就被阻断了,把焦点移进去反而是必须的(还要记住关闭时把焦点还回原来的触发元素)。
- 顺带说两件相关的键盘要求会加分:焦点必须**看得见**,很多项目为了好看去掉 outline,那会让纯键盘用户彻底迷路,用 focus-visible 可以只在键盘操作时显示;以及给一个跳转链接作为页面第一个可聚焦元素,让键盘用户不必每次穿过整个导航。
- 可预期的追问是「Agent 界面里键盘可达最关键的是哪里」——**审批**。如果审批按钮 Tab 不到,纯键盘用户就无法批准或拒绝任何操作,这不是体验问题而是功能性排除。
How to reason about it · think before answering
- A trap question, because 'move focus to new content' sounds like an accessibility improvement while actually doing harm.
- The answer is **no**, and one sentence suffices: the user may be typing in the input, and stealing focus is hostile. Worse, streaming content changes constantly, so focus chasing it makes keyboard operation impossible.
- The right approach is to **inform** via a live region rather than **compel** via focus. That generalizes: live regions for notification, focus only for navigation the user initiated.
- The one case that warrants moving focus is **opening a modal dialog**, since the user's other interactions are already blocked; moving focus in is then required, as is restoring it to the triggering element on close.
- Two related keyboard requirements earn extra credit: focus must be **visible** — many projects remove the outline for aesthetics and leave keyboard users lost, whereas focus-visible shows it only for keyboard interaction — and a skip link as the first focusable element spares keyboard users from tabbing through the whole navigation.
- Expect the follow-up on where keyboard access matters most in an agent UI: **approval**. If the approve and reject buttons cannot be reached by keyboard, keyboard-only users cannot authorize or decline anything, which is functional exclusion rather than an inconvenience.
答题要点
- 不该跟随:用户可能正在打字,抢焦点很粗暴,而且流式内容每秒都在变。
- 用实时区域告知,不要用焦点强迫;通知用实时区域,焦点只用于用户主动发起的导航。
- 唯一例外是打开模态框,那时该移焦点进去,关闭时还要把焦点还回触发元素。
- 焦点必须看得见,用 focus-visible 可以只在键盘操作时显示 outline。
- Agent 界面里键盘可达最关键的是审批,按钮 Tab 不到等于功能性排除。
Key points
- No: the user may be typing, stealing focus is hostile, and streaming content changes constantly.
- Inform with a live region rather than compelling with focus; focus is for user-initiated navigation.
- The one exception is a modal dialog, where focus should move in and be restored to the trigger on close.
- Focus must be visible; focus-visible shows the outline only for keyboard interaction.
- Approval is the critical keyboard path in an agent UI; unreachable buttons are functional exclusion.
你的流式界面在自动化测试里全绿,但用户报告说屏幕阅读器上有问题。这说明什么?Your streaming UI is green across automated tests, yet users report problems with screen readers. What does that tell you?
国内高频海外高频深入#testing#accessibility#engineering-practice分析过程 · 先想清楚再作答
- 这题考的是对测试边界的认识,不是某个具体技术点。它也是个很好的自我检验:你有没有真的打开过屏幕阅读器。
- 根本原因是**自动化测试验的是 DOM 结构,而屏幕阅读器的行为是时序性的**。你可以断言实时区域存在、属性正确、内容变了,但断言不了「用户实际听到了什么」——播报会不会被节流吃掉、会不会重复、两段之间有没有留够间隔,这些 DOM 上都看不出来。
- 我自己写这门课的 lab 时就踩到过一个具体例子:逻辑层自检全绿,浏览器里听到的第二句却是「速高于行业均值」——开头被吞了。原因是播报器按消息追踪已播报位置,第二条消息开始时没重置,沿用了第一条的偏移量。单条消息的分段完全正确,所以逻辑测试发现不了。
- 第二个原因是**屏幕阅读器之间行为不一致**。NVDA、JAWS、VoiceOver 对高频变化的处理各不相同,在一个上验过不代表另外两个也行。
- 所以结论是:这类界面必须有**手动验收清单**,而且要真的执行。清单上该有的项目包括真开一次屏幕阅读器听播报节奏、纯键盘走完一遍关键流程、把关键属性改错一次感受差别。
- 可预期的追问是「那自动化测试还有什么用」——有用,它守住的是结构层的回归:区域存在、加载时为空、属性没被改错、播报段数远少于增量数。它是必要不充分条件。**能自动验的尽量自动验,验不了的要诚实列进手动清单,而不是假装覆盖到了。**
How to reason about it · think before answering
- This probes your understanding of testing boundaries rather than a specific technique, and doubles as a self-check on whether you have ever actually turned a screen reader on.
- The root cause is that **automated tests verify DOM structure while screen reader behavior is temporal**. You can assert the live region exists, has the right attributes, and changed content, but not what the user actually heard — whether announcements were throttled away, repeated, or spaced far enough apart. None of that is visible in the DOM.
- A concrete example from building this course's lab: the logic-layer selftest was fully green, yet the second sentence came out as a fragment with its opening swallowed. The announcer tracked its position per message and was not reset when a new message began, so it reused the previous offset. Sentence splitting within one message was perfectly correct, which is why logic tests missed it.
- The second cause is **inconsistency between screen readers**. NVDA, JAWS, and VoiceOver handle high-frequency changes differently, so passing on one says little about the other two.
- The conclusion: interfaces like this require a **manual acceptance checklist** that is actually executed. It should include listening to announcement pacing with a real screen reader, completing the critical flow with keyboard only, and deliberately breaking a key attribute once to hear the difference.
- Expect the follow-up on what automation is still worth: it guards structural regressions — the region exists, starts empty, keeps its attributes, and announces far fewer times than there are deltas. It is necessary but not sufficient. **Automate what can be automated, and list the rest honestly rather than pretending it is covered.**
答题要点
- 自动化测试验的是 DOM 结构,而屏幕阅读器行为是时序性的,听到什么断言不了。
- 具体例子:单条消息分段正确但跨消息没重置偏移,第二条开头被吞,逻辑测试全绿。
- 三家屏幕阅读器对高频变化处理不同,在一个上验过不代表另外两个也行。
- 结论是必须有真正执行的手动验收清单,包括真开屏幕阅读器和纯键盘走一遍。
- 自动化仍然有用,它守结构层回归;能自动验的自动验,验不了的诚实列进清单。
Key points
- Automation checks DOM structure, but screen reader behavior is temporal and what was heard cannot be asserted.
- Concrete case: per-message splitting was correct but the offset was not reset across messages, swallowing the second message's opening while tests stayed green.
- The three major screen readers differ on high-frequency changes, so one passing proves little.
- You need a manual acceptance checklist that is actually run, including a real screen reader and a keyboard-only pass.
- Automation still guards structural regressions; automate what you can and list the rest honestly.