wkAccessibility.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.getAccessibilityTree = getAccessibilityTree;
  6. /**
  7. * Copyright (c) Microsoft Corporation.
  8. *
  9. * Licensed under the Apache License, Version 2.0 (the "License");
  10. * you may not use this file except in compliance with the License.
  11. * You may obtain a copy of the License at
  12. *
  13. * http://www.apache.org/licenses/LICENSE-2.0
  14. *
  15. * Unless required by applicable law or agreed to in writing, software
  16. * distributed under the License is distributed on an "AS IS" BASIS,
  17. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18. * See the License for the specific language governing permissions and
  19. * limitations under the License.
  20. */
  21. async function getAccessibilityTree(session, needle) {
  22. const objectId = needle ? needle._objectId : undefined;
  23. const {
  24. axNode
  25. } = await session.send('Page.accessibilitySnapshot', {
  26. objectId
  27. });
  28. const tree = new WKAXNode(axNode);
  29. return {
  30. tree,
  31. needle: needle ? tree._findNeedle() : null
  32. };
  33. }
  34. const WKRoleToARIARole = new Map(Object.entries({
  35. 'TextField': 'textbox'
  36. }));
  37. // WebKit localizes role descriptions on mac, but the english versions only add noise.
  38. const WKUnhelpfulRoleDescriptions = new Map(Object.entries({
  39. 'WebArea': 'HTML content',
  40. 'Summary': 'summary',
  41. 'DescriptionList': 'description list',
  42. 'ImageMap': 'image map',
  43. 'ListMarker': 'list marker',
  44. 'Video': 'video playback',
  45. 'Mark': 'highlighted',
  46. 'contentinfo': 'content information',
  47. 'Details': 'details',
  48. 'DescriptionListDetail': 'description',
  49. 'DescriptionListTerm': 'term',
  50. 'alertdialog': 'web alert dialog',
  51. 'dialog': 'web dialog',
  52. 'status': 'application status',
  53. 'tabpanel': 'tab panel',
  54. 'application': 'web application'
  55. }));
  56. class WKAXNode {
  57. constructor(payload) {
  58. this._payload = void 0;
  59. this._children = void 0;
  60. this._payload = payload;
  61. this._children = [];
  62. for (const payload of this._payload.children || []) this._children.push(new WKAXNode(payload));
  63. }
  64. children() {
  65. return this._children;
  66. }
  67. _findNeedle() {
  68. if (this._payload.found) return this;
  69. for (const child of this._children) {
  70. const found = child._findNeedle();
  71. if (found) return found;
  72. }
  73. return null;
  74. }
  75. isControl() {
  76. switch (this._payload.role) {
  77. case 'button':
  78. case 'checkbox':
  79. case 'ColorWell':
  80. case 'combobox':
  81. case 'DisclosureTriangle':
  82. case 'listbox':
  83. case 'menu':
  84. case 'menubar':
  85. case 'menuitem':
  86. case 'menuitemcheckbox':
  87. case 'menuitemradio':
  88. case 'radio':
  89. case 'scrollbar':
  90. case 'searchbox':
  91. case 'slider':
  92. case 'spinbutton':
  93. case 'switch':
  94. case 'tab':
  95. case 'textbox':
  96. case 'TextField':
  97. case 'tree':
  98. return true;
  99. default:
  100. return false;
  101. }
  102. }
  103. _isTextControl() {
  104. switch (this._payload.role) {
  105. case 'combobox':
  106. case 'searchfield':
  107. case 'textbox':
  108. case 'TextField':
  109. return true;
  110. }
  111. return false;
  112. }
  113. _name() {
  114. if (this._payload.role === 'text') return this._payload.value || '';
  115. return this._payload.name || '';
  116. }
  117. isInteresting(insideControl) {
  118. const {
  119. role,
  120. focusable
  121. } = this._payload;
  122. const name = this._name();
  123. if (role === 'ScrollArea') return false;
  124. if (role === 'WebArea') return true;
  125. if (focusable || role === 'MenuListOption') return true;
  126. // If it's not focusable but has a control role, then it's interesting.
  127. if (this.isControl()) return true;
  128. // A non focusable child of a control is not interesting
  129. if (insideControl) return false;
  130. return this.isLeafNode() && !!name;
  131. }
  132. _hasRedundantTextChild() {
  133. if (this._children.length !== 1) return false;
  134. const child = this._children[0];
  135. return child._payload.role === 'text' && this._payload.name === child._payload.value;
  136. }
  137. isLeafNode() {
  138. if (!this._children.length) return true;
  139. // WebKit on Linux ignores everything inside text controls, normalize this behavior
  140. if (this._isTextControl()) return true;
  141. // WebKit for mac has text nodes inside heading, li, menuitem, a, and p nodes
  142. if (this._hasRedundantTextChild()) return true;
  143. return false;
  144. }
  145. serialize() {
  146. const node = {
  147. role: WKRoleToARIARole.get(this._payload.role) || this._payload.role,
  148. name: this._name()
  149. };
  150. if ('description' in this._payload && this._payload.description !== node.name) node.description = this._payload.description;
  151. if ('roledescription' in this._payload) {
  152. const roledescription = this._payload.roledescription;
  153. if (roledescription !== this._payload.role && WKUnhelpfulRoleDescriptions.get(this._payload.role) !== roledescription) node.roledescription = roledescription;
  154. }
  155. if ('value' in this._payload && this._payload.role !== 'text') {
  156. if (typeof this._payload.value === 'string') node.valueString = this._payload.value;else if (typeof this._payload.value === 'number') node.valueNumber = this._payload.value;
  157. }
  158. if ('checked' in this._payload) node.checked = this._payload.checked === 'true' ? 'checked' : this._payload.checked === 'false' ? 'unchecked' : 'mixed';
  159. if ('pressed' in this._payload) node.pressed = this._payload.pressed === 'true' ? 'pressed' : this._payload.pressed === 'false' ? 'released' : 'mixed';
  160. const userStringProperties = ['keyshortcuts', 'valuetext'];
  161. for (const userStringProperty of userStringProperties) {
  162. if (!(userStringProperty in this._payload)) continue;
  163. node[userStringProperty] = this._payload[userStringProperty];
  164. }
  165. const booleanProperties = ['disabled', 'expanded', 'focused', 'modal', 'multiselectable', 'readonly', 'required', 'selected'];
  166. for (const booleanProperty of booleanProperties) {
  167. // WebArea and ScrollArea treat focus differently than other nodes. They report whether their frame has focus,
  168. // not whether focus is specifically on the root node.
  169. if (booleanProperty === 'focused' && (this._payload.role === 'WebArea' || this._payload.role === 'ScrollArea')) continue;
  170. const value = this._payload[booleanProperty];
  171. if (!value) continue;
  172. node[booleanProperty] = value;
  173. }
  174. const numericalProperties = ['level', 'valuemax', 'valuemin'];
  175. for (const numericalProperty of numericalProperties) {
  176. if (!(numericalProperty in this._payload)) continue;
  177. node[numericalProperty] = this._payload[numericalProperty];
  178. }
  179. const tokenProperties = ['autocomplete', 'haspopup', 'invalid'];
  180. for (const tokenProperty of tokenProperties) {
  181. const value = this._payload[tokenProperty];
  182. if (!value || value === 'false') continue;
  183. node[tokenProperty] = value;
  184. }
  185. const orientationIsApplicable = new Set(['ScrollArea', 'scrollbar', 'listbox', 'combobox', 'menu', 'tree', 'separator', 'slider', 'tablist', 'toolbar']);
  186. if (this._payload.orientation && orientationIsApplicable.has(this._payload.role)) node.orientation = this._payload.orientation;
  187. return node;
  188. }
  189. }