head.js (9207B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /* exported createTextLeafPoint, DIRECTION_NEXT, DIRECTION_PREVIOUS, 8 BOUNDARY_FLAG_DEFAULT, BOUNDARY_FLAG_INCLUDE_ORIGIN, 9 BOUNDARY_FLAG_STOP_IN_EDITABLE, BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER, 10 readablePoint, testPointEqual, textBoundaryGenerator, testBoundarySequence, 11 isFinalValueCorrect, isFinalValueCorrect, testInsertText, testDeleteText, 12 testCopyText, testPasteText, testCutText, testSetTextContents, 13 textAttrRangesMatch */ 14 15 // Load the shared-head file first. 16 Services.scriptloader.loadSubScript( 17 "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", 18 this 19 ); 20 21 // Loading and common.js from accessible/tests/mochitest/ for all tests, as 22 // well as promisified-events.js. 23 24 /* import-globals-from ../../mochitest/role.js */ 25 26 loadScripts( 27 { name: "common.js", dir: MOCHITESTS_DIR }, 28 { name: "text.js", dir: MOCHITESTS_DIR }, 29 { name: "role.js", dir: MOCHITESTS_DIR }, 30 { name: "promisified-events.js", dir: MOCHITESTS_DIR } 31 ); 32 33 const DIRECTION_NEXT = Ci.nsIAccessibleTextLeafPoint.DIRECTION_NEXT; 34 const DIRECTION_PREVIOUS = Ci.nsIAccessibleTextLeafPoint.DIRECTION_PREVIOUS; 35 36 const BOUNDARY_FLAG_DEFAULT = 37 Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_DEFAULT; 38 const BOUNDARY_FLAG_INCLUDE_ORIGIN = 39 Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_INCLUDE_ORIGIN; 40 const BOUNDARY_FLAG_STOP_IN_EDITABLE = 41 Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_STOP_IN_EDITABLE; 42 const BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER = 43 Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER; 44 45 function createTextLeafPoint(acc, offset) { 46 let accService = Cc["@mozilla.org/accessibilityService;1"].getService( 47 nsIAccessibilityService 48 ); 49 50 return accService.createTextLeafPoint(acc, offset); 51 } 52 53 // Converts an nsIAccessibleTextLeafPoint into a human/machine 54 // readable tuple with a readable accessible and the offset within it. 55 // For a point text leaf it would look like this: ["hello", 2], 56 // For a point in an empty input it would look like this ["input#name", 0] 57 function readablePoint(point) { 58 const readableLeaf = acc => { 59 let tagName = getAccessibleTagName(acc); 60 if (tagName && !tagName.startsWith("_moz_generated")) { 61 let domNodeID = getAccessibleDOMNodeID(acc); 62 if (domNodeID) { 63 return `${tagName}#${domNodeID}`; 64 } 65 return tagName; 66 } 67 68 return acc.name; 69 }; 70 71 return [readableLeaf(point.accessible), point.offset]; 72 } 73 74 function sequenceEqual(val, expected, msg) { 75 Assert.deepEqual(val, expected, msg); 76 } 77 78 // eslint-disable-next-line camelcase 79 function sequenceEqualTodo(val, expected, msg) { 80 todo_is(JSON.stringify(val), JSON.stringify(expected), msg); 81 } 82 83 function pointsEqual(pointA, pointB) { 84 return ( 85 pointA.offset == pointB.offset && pointA.accessible == pointB.accessible 86 ); 87 } 88 89 function testPointEqual(pointA, pointB, msg) { 90 is(pointA.offset, pointB.offset, `Offset mismatch - ${msg}`); 91 is(pointA.accessible, pointB.accessible, `Accessible mismatch - ${msg}`); 92 } 93 94 function* textBoundaryGenerator( 95 firstPoint, 96 boundaryType, 97 direction, 98 flags = BOUNDARY_FLAG_DEFAULT 99 ) { 100 // Our start point should be inclusive of the given point. 101 let nextLeafPoint = firstPoint.findBoundary( 102 boundaryType, 103 direction, 104 flags | BOUNDARY_FLAG_INCLUDE_ORIGIN 105 ); 106 let textLeafPoint = null; 107 108 do { 109 textLeafPoint = nextLeafPoint; 110 yield textLeafPoint; 111 nextLeafPoint = textLeafPoint.findBoundary(boundaryType, direction, flags); 112 } while (!pointsEqual(textLeafPoint, nextLeafPoint)); 113 } 114 115 // This function takes FindBoundary arguments and an expected sequence 116 // of boundary points formatted with readablePoint. 117 // For example, word starts would look like this: 118 // [["one two", 0], ["one two", 4], ["one two", 7]] 119 function testBoundarySequence( 120 startPoint, 121 boundaryType, 122 direction, 123 expectedSequence, 124 msg, 125 options = {} 126 ) { 127 let sequence = [ 128 ...textBoundaryGenerator( 129 startPoint, 130 boundaryType, 131 direction, 132 options.flags ? options.flags : BOUNDARY_FLAG_DEFAULT 133 ), 134 ]; 135 (options.todo ? sequenceEqualTodo : sequenceEqual)( 136 sequence.map(readablePoint), 137 expectedSequence, 138 msg 139 ); 140 } 141 142 /////////////////////////////////////////////////////////////////////////////// 143 // Editable text 144 145 async function waitForCopy(browser) { 146 await BrowserTestUtils.waitForContentEvent(browser, "copy", false, () => { 147 return true; 148 }); 149 150 let clipboardData = await invokeContentTask(browser, [], async () => { 151 let text = await content.navigator.clipboard.readText(); 152 return text; 153 }); 154 155 return clipboardData; 156 } 157 158 async function isFinalValueCorrect( 159 browser, 160 acc, 161 expectedTextLeafs, 162 msg = "Value is correct" 163 ) { 164 let value = 165 acc.role == ROLE_ENTRY 166 ? acc.value 167 : await invokeContentTask(browser, [], () => { 168 return content.document.body.textContent; 169 }); 170 171 let [before, text, after] = expectedTextLeafs; 172 let finalValue = 173 before && after && !text 174 ? [before, after].join(" ") 175 : [before, text, after].join(""); 176 177 is(value.replace("\xa0", " "), finalValue, msg); 178 } 179 180 function waitForTextChangeEvents(acc, eventSeq) { 181 let events = eventSeq.map(eventType => { 182 return [eventType, acc]; 183 }); 184 185 if (acc.role == ROLE_ENTRY) { 186 events.push([EVENT_TEXT_VALUE_CHANGE, acc]); 187 } 188 189 return waitForEvents(events); 190 } 191 192 async function testSetTextContents(acc, text, staticContentOffset, events) { 193 acc.QueryInterface(nsIAccessibleEditableText); 194 let evtPromise = waitForTextChangeEvents(acc, events); 195 acc.setTextContents(text); 196 let evt = (await evtPromise)[0]; 197 evt.QueryInterface(nsIAccessibleTextChangeEvent); 198 is(evt.start, staticContentOffset); 199 } 200 201 async function testInsertText( 202 acc, 203 textToInsert, 204 insertOffset, 205 staticContentOffset 206 ) { 207 acc.QueryInterface(nsIAccessibleEditableText); 208 209 let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_INSERTED]); 210 acc.insertText(textToInsert, staticContentOffset + insertOffset); 211 let evt = (await evtPromise)[0]; 212 evt.QueryInterface(nsIAccessibleTextChangeEvent); 213 is(evt.start, staticContentOffset + insertOffset); 214 } 215 216 async function testDeleteText( 217 acc, 218 startOffset, 219 endOffset, 220 staticContentOffset 221 ) { 222 acc.QueryInterface(nsIAccessibleEditableText); 223 224 let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_REMOVED]); 225 acc.deleteText( 226 staticContentOffset + startOffset, 227 staticContentOffset + endOffset 228 ); 229 let evt = (await evtPromise)[0]; 230 evt.QueryInterface(nsIAccessibleTextChangeEvent); 231 is(evt.start, staticContentOffset + startOffset); 232 } 233 234 async function testCopyText( 235 acc, 236 startOffset, 237 endOffset, 238 staticContentOffset, 239 browser, 240 aExpectedClipboard = null 241 ) { 242 acc.QueryInterface(nsIAccessibleEditableText); 243 let copied = waitForCopy(browser); 244 acc.copyText( 245 staticContentOffset + startOffset, 246 staticContentOffset + endOffset 247 ); 248 let clipboardText = await copied; 249 if (aExpectedClipboard != null) { 250 is(clipboardText, aExpectedClipboard, "Correct text in clipboard"); 251 } 252 } 253 254 async function testPasteText(acc, insertOffset, staticContentOffset) { 255 acc.QueryInterface(nsIAccessibleEditableText); 256 let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_INSERTED]); 257 acc.pasteText(staticContentOffset + insertOffset); 258 259 let evt = (await evtPromise)[0]; 260 evt.QueryInterface(nsIAccessibleTextChangeEvent); 261 // XXX: In non-headless mode pasting text produces several text leaves 262 // and the offset is not what we expect. 263 // is(evt.start, staticContentOffset + insertOffset); 264 } 265 266 async function testCutText(acc, startOffset, endOffset, staticContentOffset) { 267 acc.QueryInterface(nsIAccessibleEditableText); 268 let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_REMOVED]); 269 acc.cutText( 270 staticContentOffset + startOffset, 271 staticContentOffset + endOffset 272 ); 273 274 let evt = (await evtPromise)[0]; 275 evt.QueryInterface(nsIAccessibleTextChangeEvent); 276 is(evt.start, staticContentOffset + startOffset); 277 } 278 279 /** 280 * Check if the given ranges match the ranges in the given text accessible which 281 * expose the given attribute names/values. 282 */ 283 function textAttrRangesMatch(acc, ranges, attrs) { 284 acc.QueryInterface(nsIAccessibleText); 285 let offset = 0; 286 let expectedRanges = [...ranges]; 287 let charCount = acc.characterCount; 288 while (offset < charCount) { 289 let start = {}; 290 let end = {}; 291 let attributes = acc.getTextAttributes(false, offset, start, end); 292 offset = end.value; 293 let matched = true; 294 for (const attr in attrs) { 295 try { 296 if (attributes.getStringProperty(attr) != attrs[attr]) { 297 // Attribute value doesn't match. 298 matched = false; 299 } 300 } catch (err) { 301 // Attribute doesn't exist. 302 matched = false; 303 } 304 } 305 if (!matched) { 306 continue; 307 } 308 let expected = expectedRanges.shift(); 309 if (!expected || expected[0] != start.value || expected[1] != end.value) { 310 return false; 311 } 312 } 313 314 return !expectedRanges.length; 315 }