ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Puppeteer 可访问性树(Accessibility Tree)检测指南:page.accessibility.snapshot() 全解

Puppeteer 可访问性树(Accessibility Tree)检测指南:page.accessibility.snapshot() 全解 Puppeteer 可访问性树Accessibility Tree检测指南page.accessibility.snapshot() 全解【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteerPuppeteer 的Accessibility类用于检查浏览器渲染引擎内部维护的可访问性树Accessibility Tree简称 AX Tree它是屏幕阅读器等辅助技术理解页面的基础。本文以 docs/api/puppeteer.accessibility.md 与 snapshot 方法文档 为主线结合 packages/puppeteer-core/src/cdp/Accessibility.ts 的源码实现完整讲清page.accessibility.snapshot()的用法、SnapshotOptions各参数含义、返回的SerializedAXNode字段结构以及底层 CDP 调用链和有趣节点过滤算法帮助你在 E2E 测试与页面审计中可靠地断言页面的无障碍语义。Accessibility 类给开发者打开的 Blink 可访问性树官方文档对Accessibility类的定义是export declare class Accessibility该类提供检查浏览器可访问性树的方法。可访问性树被屏幕阅读器、开关控制switch access等辅助技术使用。文档同时给出了三点关键背景理解它们对正确解读snapshot()的输出至关重要可访问性高度依赖平台不同平台上有不同的屏幕阅读器输出的可访问性树差异可能非常大Blink 层的中间表示Chrome 的渲染引擎 Blink 内部维护一棵 accessibility tree随后再被翻译成各平台专属的可访问性 API。Puppeteer 的Accessibility命名空间暴露的是Blink 层的 AX Tree而非某个特定平台的最终形态默认模拟平台过滤Blink AX Tree 转换为平台树、或被辅助技术消费时大部分节点会被过滤掉。Puppeteer 默认会近似模拟这一过滤过程只暴露树中有趣interesting的节点。文档还明确了一个使用约束Accessibility的构造函数被标记为内部 APIinternal第三方代码不应直接调用构造函数也不应继承它——实例只能从Page或Frame上获取。从哪里获取 Accessibility 实例从源码结构看每个Frame都会持有一个Accessibility实例。在 CDP 实现中packages/puppeteer-core/src/cdp/Frame.ts#L91-L95 的构造函数里创建了它this.accessibility new Accessibility( this.worlds[MAIN_WORLD], frameId, logger, );它绑定了主世界 Realm、所在 frame 的frameId和日志器。对外暴露有两条路径页面级packages/puppeteer-core/src/api/Page.ts#L1015-L1017 中的 getter 直接委托给主 frameget accessibility(): Accessibility { return this.mainFrame().accessibility; }frame 级packages/puppeteer-core/src/api/Frame.ts#L415 声明了抽象成员abstract get accessibility(): Accessibility因此page.frames()中的每个 frame包括 iframe也都有独立的accessibility可以单独抓取某个 frame 的树。日常使用中最常见的是page.accessibility。snapshot() 方法签名与参数snapshot 方法文档给出的签名是class Accessibility { snapshot(options?: SnapshotOptions): PromiseSerializedAXNode | null; }snapshot()捕获可访问性树的当前状态返回对象代表页面根可访问节点。可能的返回值为null例如root指定的节点在当前树中不存在时源码 Accessibility.ts#L311-L313 直接返回null。完整的参数定义见 SnapshotOptions 接口文档结合 packages/puppeteer-core/src/cdp/Accessibility.ts#L146-L164 的源码注释三个可选参数如下参数类型默认值说明interestingOnlybooleantrue从树中裁剪掉不感兴趣的节点includeIframesbooleanfalse为 frame 子树中的每个 iframe 获取可访问性树rootElementHandleNode整页根节点指定从哪个节点开始获取可访问性树方法文档中有一条重要备注文档 puppeteer.accessibility.md 中同样出现NOTEChrome 的可访问性树包含大量在大多数平台、大多数屏幕阅读器上不会使用的节点。除非interestingOnly被设为falsePuppeteer 也会丢弃这些节点以得到更容易处理的树。参数在源码中的解构Accessibility.ts#L241-L248 中默认值的解构一目了然public async snapshot( options: SnapshotOptions {}, ): PromiseSerializedAXNode | null { const { interestingOnly true, root null, includeIframes false, } options; // ... }底层实现流程一次 CDP 调用与树重建读懂snapshot()的内部流程能解释为什么返回的树长这样、以及某些边界情况如 iframe 断连为何被静默处理。完整流程在 packages/puppeteer-core/src/cdp/Accessibility.ts#L241-L323拉取原始树向 CDP 发送Accessibility.getFullAXTree参数为当前实例绑定的frameIdconst {nodes} await this.#realm.environment.client.send( Accessibility.getFullAXTree, {frameId: this.#frameId}, );也就是说page.accessibility抓的是主 frame的完整 CDP AX 树iframe 内容默认不在这批节点里这正是includeIframes存在的原因。解析root可选若传入了root先用DOM.describeNode把ElementHandle的objectId换回backendNodeId用于后续在树中定位该节点L255-L264。重建树结构AXNode.createTree()把所有扁平的Protocol.Accessibility.AXNode载荷按nodeId建索引再依据每个节点的childIds挂接父子关系L770-L787取第一个节点为树根。填充 iframe可选includeIframes: true时populateIframes()递归遍历树遇到role为Iframe的节点通过其backendDOMNodeId调realm.adoptBackendNode()拿到ElementHandle再经handle.contentFrame()取得子 frame递归调用子 frame 自己的frame.accessibility.snapshot(options)结果挂到父节点的iframeSnapshot上L266-L294。注意其中try/catch会静默记录错误——注释写明frame 可能随时被断开detached因此 iframe 抓取是尽力而为的测试代码不应依赖某个跨文档 iframe 一定存在。定位目标节点若设置了root从树根用find()按backendDOMNodeId查找L305-L309找不到则返回null。过滤与序列化interestingOnly为false时直接serializeTree(needle)输出全树否则先通过collectInterestingNodes()收集有趣节点集合再带着该集合序列化L315-L322。序列化时非有趣节点自身会被跳过但它的子孙仍会被收集serializeTree先递归子节点再决定是否输出自己因此裁剪并不会丢失有趣的深层后代。什么是有趣节点interestingOnly 的过滤算法collectInterestingNodes()L351-L366的核心是AXNode.isInteresting()L566-L600其判定规则按顺序为一律排除role为Ignored、节点hidden或 CDP 标记为ignored的节点landmark地标必留banner、complementary、contentinfo、form、main、navigation、region、search这 8 种 landmark role 直接判定为有趣见isLandmark()L550-L564交互/状态相关必留focusable、富文本可编辑richtext、busy、live且不为off、modal、带errormessage、带details、有roledescription的节点控件 role 必留isControl()列举了button、checkbox、combobox、listbox、menu、menuitem*、radio、scrollbar、searchbox、slider、spinbutton、switch、tab、textbox、tree、treeitem等L521-L548控件内部剪枝insideControl标记会沿子树传递——一个控件内部的非 focusable子节点例如按钮里的纯装饰StaticText不会被单独暴露因为屏幕阅读器把控件整体朗读兜底规则其余节点中是叶子节点且有name或description的才保留。叶子节点的定义同样有讲究isLeafNode()L478-L519纯文本框textbox/searchbox、纯文本角色LineBreak、text、InlineTextBox、StaticText以及img、progressbar、slider等按 ARIA/HTML 规范子节点仅作展示的角色即便 CDP 树里有子节点也会被视作叶子——注释解释这是为了避免屏幕阅读器被内部实现细节的子节点绕晕。一个直观的例子来自测试用例 test/src/accessibility.test.ts#L124-L150对聚焦的textareahi/textarea使用snapshot({interestingOnly: false})后聚焦的textbox节点下能看到{role: generic}包裹的{role: StaticText, name: hi}子结构——这正是默认模式下会被剪掉的不有趣内容。返回值 SerializedAXNode字段结构与 elementHandle()snapshot()返回PromiseSerializedAXNode | null。完整的字段定义见 SerializedAXNode 接口文档与源码 Accessibility.ts#L18-L141 一一对应。字段按语义可分组如下分组字段说明基本标识role必填、name?、value?、description?、roledescription?、valuetext?、keyshortcuts?、url?节点角色、人类可读名称、当前值等url专用于链接元素布尔状态disabled?、expanded?、focused?、modal?、multiline?、multiselectable?、readonly?、required?、selected?、busy?、atomic?节点交互状态三态checked?: boolean \| mixed、pressed?: boolean \| mixed复选框/开关的半选状态会序列化为字符串mixed数值level?标题级别、valuemin?、valuemax?如h1对应level: 1字符串令牌autocomplete?、haspopup?、invalid?、orientation?、live?、relevant?、errormessage?、details?对应aria-*语义空值或false会在序列化时被丢弃结构children?: SerializedAXNode[]子节点列表内部backendNodeId?、loaderIdinternalCDP 的 DOM 节点 ID 与跨导航唯一标识一般无需关注序列化逻辑在AXNode.serialize()L602-L768它把 CDP 返回的properties数组按字符串/布尔/三态/数值/令牌五类分别写入结果对象值按 CDP 给出的字符串如true、mixed解释为对应的 JS 类型。有一个值得注意的细节RootWebArea节点会跳过focused属性的写入L698-L709因为根节点上报的 focused 语义是该 frame 是否持有焦点而不是焦点落在根节点本身上——用focused找聚焦元素时应在树的内部节点上找。每个节点还带有一个方法SerializedAXNode.elementHandle 文档elementHandle(): PromiseElementHandle | null;它利用节点的backendDOMNodeId通过adoptBackendNode恢复出对应的ElementHandle若 AX 节点指向的是文本节点非元素源码会在页面上下文里改返回其parentElementL619-L632。文档提醒如果底层 DOM 元素已被销毁该方法可能返回错误。这意味着你不仅能读可访问性树还能从语义节点跳转回 DOM 元素做进一步操作。实战用法以下示例继承自 snapshot 方法文档并结合仓库测试用例补充了各参数的实战形态。示例 1转储整棵可访问性树const snapshot await page.accessibility.snapshot(); console.log(snapshot);示例 2打印获得焦点节点的名称const snapshot await page.accessibility.snapshot(); const node findFocusedNode(snapshot); console.log(node node.name); function findFocusedNode(node) { if (node.focused) return node; for (const child of node.children || []) { const foundNode findFocusedNode(child); return foundNode; } return null; }这是焦点在哪断言的典型写法也是 E2E 中验证键盘导航的常用手段。示例 3root—— 只取某个元素子树的语义名称测试用例中的用法root optiondescribe 块test/src/accessibility.test.ts#L655-L668await page.setContent(buttonMy Button/button); using button await page.$(button); // 返回 { role: button, name: My Button, ... } expect(await page.accessibility.snapshot({root: button})).toMatchObject( {role: button, name: My Button}, );root非常适合回答某个元素对屏幕阅读器报出的可访问名称是什么这类问题——比如按钮文案切换前后分别调用一次验证name随之变化对应测试 L187-L203 中Show/Hide按钮的场景。若root元素不在 AX 树中如已被移除snapshot()返回null。示例 4includeIframes—— 把 iframe 的树挂进来测试用例展示了跨文档 iframe 的形态await attachFrame(page, frame1, server.EMPTY_PAGE); const frame1 page.frames()[1]; await frame1.evaluate(() { const button document.createElement(button); button.innerText value1; document.body.appendChild(button); }); const snapshot await page.accessibility.snapshot({ interestingOnly: true, includeIframes: true, }); // snapshot.children 中会出现 // { role: Iframe, children: [{ role: button, name: value1 }] }从源码看populateIframes前文第 4 步iframe 的子树是作为Iframe节点的children插入的serializeTree会把iframeSnapshotpush 进childrenL342-L347所以断言时 iframe 内容位于该Iframe节点的子节点之下而不是与主文档节点平级。典型断言输出长什么样test/src/accessibility.test.ts#L18-L88 的should work用例给出了一个很好的输出参照。页面含h1、多个input、select和链接时默认snapshot()匹配出的结构节选为{ role: RootWebArea, name: Accessibility Test, children: [ {role: StaticText, name: Hello World}, {role: heading, name: Inputs, level: 1}, {role: textbox, name: Empty input, focused: true}, {role: textbox, name: readonly input, readonly: true}, {role: textbox, name: disabled input, disabled: true}, { role: combobox, name: , value: First Option, haspopup: menu, expanded: false, children: [ {role: option, name: First Option, selected: true}, {role: option, name: Second Option} ] }, {name: example, role: link, url: https://example.com/} ] }这个例子恰好验证了字段映射aria-describedby映射到description、aria-placeholder/placeholder参与name计算、select变成comboboxoption子树且selected: true、链接的href归一化后落在url上。适用前提与注意事项结合源码与测试使用page.accessibility时需注意以下边界默认输出是近似平台树而非原始 CDP 树。interestingOnly默认true会按前文的有趣节点规则剪枝写脆弱断言前先想清楚你在断言语义视图还是原始结构需要完整结构时显式传{interestingOnly: false}。跨 Chrome 版本AX 树细节可能变化。测试文件中明确存在版本分支处理test/src/accessibility.test.ts#L96-L121 针对 landmarks 页面写了State before 149.0.7819.0 / State after两套期望form是否包裹search节点。如果断言深层 landmark 结构建议用toMatchObject这类宽松匹配而非全等比较。iframe 抓取是尽力而为的。includeIframes递归中frame 在遍历中途被导航或断开只会记录一条错误日志DEBUG_PREFIXES.error对应Iframe节点会缺少子树同时includeIframes默认false主文档的snapshot()不含任何 iframe 内部内容。返回null是合法结果root定位失败或主 frame 无树时都会返回null断言前需要判空。语义来源是 Blink 的 AX Tree如文档所述它只是各平台最终可访问性输出的一个上游近似。它足以支撑可访问名称是否正确角色是否符合预期焦点位置这类断言但不能替代在真实屏幕阅读器上的人工/工具化审计。相关文档索引类定义docs/api/puppeteer.accessibility.md方法详情docs/api/puppeteer.accessibility.snapshot.md参数接口docs/api/puppeteer.snapshotoptions.md返回结构docs/api/puppeteer.serializedaxnode.md、docs/api/puppeteer.serializedaxnode.elementhandle.md核心实现packages/puppeteer-core/src/cdp/Accessibility.ts测试用例test/src/accessibility.test.ts【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表