|
1 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
2 |
|
3 //////////////////////////////////////////////////////////////////////////////// |
|
4 // Constants |
|
5 |
|
6 const PREFILTER_INVISIBLE = nsIAccessibleTraversalRule.PREFILTER_INVISIBLE; |
|
7 const PREFILTER_ARIA_HIDDEN = nsIAccessibleTraversalRule.PREFILTER_ARIA_HIDDEN; |
|
8 const PREFILTER_TRANSPARENT = nsIAccessibleTraversalRule.PREFILTER_TRANSPARENT; |
|
9 const FILTER_MATCH = nsIAccessibleTraversalRule.FILTER_MATCH; |
|
10 const FILTER_IGNORE = nsIAccessibleTraversalRule.FILTER_IGNORE; |
|
11 const FILTER_IGNORE_SUBTREE = nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE; |
|
12 const CHAR_BOUNDARY = nsIAccessiblePivot.CHAR_BOUNDARY; |
|
13 const WORD_BOUNDARY = nsIAccessiblePivot.WORD_BOUNDARY; |
|
14 |
|
15 const NS_ERROR_NOT_IN_TREE = 0x80780026; |
|
16 const NS_ERROR_INVALID_ARG = 0x80070057; |
|
17 |
|
18 //////////////////////////////////////////////////////////////////////////////// |
|
19 // Traversal rules |
|
20 |
|
21 /** |
|
22 * Rule object to traverse all focusable nodes and text nodes. |
|
23 */ |
|
24 var HeadersTraversalRule = |
|
25 { |
|
26 getMatchRoles: function(aRules) |
|
27 { |
|
28 aRules.value = [ROLE_HEADING]; |
|
29 return aRules.value.length; |
|
30 }, |
|
31 |
|
32 preFilter: PREFILTER_INVISIBLE, |
|
33 |
|
34 match: function(aAccessible) |
|
35 { |
|
36 return FILTER_MATCH; |
|
37 }, |
|
38 |
|
39 QueryInterface: XPCOMUtils.generateQI([nsIAccessibleTraversalRule]) |
|
40 } |
|
41 |
|
42 /** |
|
43 * Traversal rule for all focusable nodes or leafs. |
|
44 */ |
|
45 var ObjectTraversalRule = |
|
46 { |
|
47 getMatchRoles: function(aRules) |
|
48 { |
|
49 aRules.value = []; |
|
50 return 0; |
|
51 }, |
|
52 |
|
53 preFilter: PREFILTER_INVISIBLE | PREFILTER_ARIA_HIDDEN | PREFILTER_TRANSPARENT, |
|
54 |
|
55 match: function(aAccessible) |
|
56 { |
|
57 var rv = FILTER_IGNORE; |
|
58 var role = aAccessible.role; |
|
59 if (hasState(aAccessible, STATE_FOCUSABLE) && |
|
60 (role != ROLE_DOCUMENT && role != ROLE_INTERNAL_FRAME)) |
|
61 rv = FILTER_IGNORE_SUBTREE | FILTER_MATCH; |
|
62 else if (aAccessible.childCount == 0 && |
|
63 role != ROLE_STATICTEXT && aAccessible.name.trim()) |
|
64 rv = FILTER_MATCH; |
|
65 |
|
66 return rv; |
|
67 }, |
|
68 |
|
69 QueryInterface: XPCOMUtils.generateQI([nsIAccessibleTraversalRule]) |
|
70 }; |
|
71 |
|
72 //////////////////////////////////////////////////////////////////////////////// |
|
73 // Virtual state invokers and checkers |
|
74 |
|
75 /** |
|
76 * A checker for virtual cursor changed events. |
|
77 */ |
|
78 function VCChangedChecker(aDocAcc, aIdOrNameOrAcc, aTextOffsets, aPivotMoveMethod) |
|
79 { |
|
80 this.__proto__ = new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc); |
|
81 |
|
82 this.match = function VCChangedChecker_check(aEvent) |
|
83 { |
|
84 var event = null; |
|
85 try { |
|
86 event = aEvent.QueryInterface(nsIAccessibleVirtualCursorChangeEvent); |
|
87 } catch (e) { |
|
88 return false; |
|
89 } |
|
90 |
|
91 var expectedReason = VCChangedChecker.methodReasonMap[aPivotMoveMethod] || |
|
92 nsIAccessiblePivot.REASON_NONE; |
|
93 |
|
94 return event.reason == expectedReason; |
|
95 }; |
|
96 |
|
97 this.check = function VCChangedChecker_check(aEvent) |
|
98 { |
|
99 SimpleTest.info("VCChangedChecker_check"); |
|
100 |
|
101 var event = null; |
|
102 try { |
|
103 event = aEvent.QueryInterface(nsIAccessibleVirtualCursorChangeEvent); |
|
104 } catch (e) { |
|
105 SimpleTest.ok(false, "Does not support correct interface: " + e); |
|
106 } |
|
107 |
|
108 var position = aDocAcc.virtualCursor.position; |
|
109 var idMatches = position && position.DOMNode.id == aIdOrNameOrAcc; |
|
110 var nameMatches = position && position.name == aIdOrNameOrAcc; |
|
111 var accMatches = position == aIdOrNameOrAcc; |
|
112 |
|
113 SimpleTest.ok(idMatches || nameMatches || accMatches, "id or name matches", |
|
114 "expecting " + aIdOrNameOrAcc + ", got '" + |
|
115 prettyName(position)); |
|
116 |
|
117 if (aTextOffsets) { |
|
118 SimpleTest.is(aDocAcc.virtualCursor.startOffset, aTextOffsets[0], |
|
119 "wrong start offset"); |
|
120 SimpleTest.is(aDocAcc.virtualCursor.endOffset, aTextOffsets[1], |
|
121 "wrong end offset"); |
|
122 } |
|
123 |
|
124 var prevPosAndOffset = VCChangedChecker. |
|
125 getPreviousPosAndOffset(aDocAcc.virtualCursor); |
|
126 |
|
127 if (prevPosAndOffset) { |
|
128 SimpleTest.is(event.oldAccessible, prevPosAndOffset.position, |
|
129 "previous position does not match"); |
|
130 SimpleTest.is(event.oldStartOffset, prevPosAndOffset.startOffset, |
|
131 "previous start offset does not match"); |
|
132 SimpleTest.is(event.oldEndOffset, prevPosAndOffset.endOffset, |
|
133 "previous end offset does not match"); |
|
134 } |
|
135 }; |
|
136 } |
|
137 |
|
138 VCChangedChecker.prevPosAndOffset = {}; |
|
139 |
|
140 VCChangedChecker.storePreviousPosAndOffset = |
|
141 function storePreviousPosAndOffset(aPivot) |
|
142 { |
|
143 VCChangedChecker.prevPosAndOffset[aPivot] = |
|
144 {position: aPivot.position, |
|
145 startOffset: aPivot.startOffset, |
|
146 endOffset: aPivot.endOffset}; |
|
147 }; |
|
148 |
|
149 VCChangedChecker.getPreviousPosAndOffset = |
|
150 function getPreviousPosAndOffset(aPivot) |
|
151 { |
|
152 return VCChangedChecker.prevPosAndOffset[aPivot]; |
|
153 }; |
|
154 |
|
155 VCChangedChecker.methodReasonMap = { |
|
156 'moveNext': nsIAccessiblePivot.REASON_NEXT, |
|
157 'movePrevious': nsIAccessiblePivot.REASON_PREV, |
|
158 'moveFirst': nsIAccessiblePivot.REASON_FIRST, |
|
159 'moveLast': nsIAccessiblePivot.REASON_LAST, |
|
160 'setTextRange': nsIAccessiblePivot.REASON_TEXT, |
|
161 'moveNextByText': nsIAccessiblePivot.REASON_TEXT, |
|
162 'movePreviousByText': nsIAccessiblePivot.REASON_TEXT, |
|
163 'moveToPoint': nsIAccessiblePivot.REASON_POINT |
|
164 }; |
|
165 |
|
166 /** |
|
167 * Set a text range in the pivot and wait for virtual cursor change event. |
|
168 * |
|
169 * @param aDocAcc [in] document that manages the virtual cursor |
|
170 * @param aTextAccessible [in] accessible to set to virtual cursor's position |
|
171 * @param aTextOffsets [in] start and end offsets of text range to set in |
|
172 * virtual cursor. |
|
173 */ |
|
174 function setVCRangeInvoker(aDocAcc, aTextAccessible, aTextOffsets) |
|
175 { |
|
176 this.invoke = function virtualCursorChangedInvoker_invoke() |
|
177 { |
|
178 VCChangedChecker. |
|
179 storePreviousPosAndOffset(aDocAcc.virtualCursor); |
|
180 SimpleTest.info(prettyName(aTextAccessible) + " " + aTextOffsets); |
|
181 aDocAcc.virtualCursor.setTextRange(aTextAccessible, |
|
182 aTextOffsets[0], |
|
183 aTextOffsets[1]); |
|
184 }; |
|
185 |
|
186 this.getID = function setVCRangeInvoker_getID() |
|
187 { |
|
188 return "Set offset in " + prettyName(aTextAccessible) + |
|
189 " to (" + aTextOffsets[0] + ", " + aTextOffsets[1] + ")"; |
|
190 }; |
|
191 |
|
192 this.eventSeq = [ |
|
193 new VCChangedChecker(aDocAcc, aTextAccessible, aTextOffsets, "setTextRange") |
|
194 ]; |
|
195 } |
|
196 |
|
197 /** |
|
198 * Move the pivot and wait for virtual cursor change event. |
|
199 * |
|
200 * @param aDocAcc [in] document that manages the virtual cursor |
|
201 * @param aPivotMoveMethod [in] method to test (ie. "moveNext", "moveFirst", etc.) |
|
202 * @param aRule [in] traversal rule object |
|
203 * @param aIdOrNameOrAcc [in] id, accessible or accessible name to expect |
|
204 * virtual cursor to land on after performing move method. |
|
205 * false if no move is expected. |
|
206 */ |
|
207 function setVCPosInvoker(aDocAcc, aPivotMoveMethod, aRule, aIdOrNameOrAcc) |
|
208 { |
|
209 var expectMove = (aIdOrNameOrAcc != false); |
|
210 this.invoke = function virtualCursorChangedInvoker_invoke() |
|
211 { |
|
212 VCChangedChecker. |
|
213 storePreviousPosAndOffset(aDocAcc.virtualCursor); |
|
214 if (aPivotMoveMethod && aRule) { |
|
215 var moved = aDocAcc.virtualCursor[aPivotMoveMethod](aRule); |
|
216 SimpleTest.is(!!moved, !!expectMove, |
|
217 "moved pivot with " + aPivotMoveMethod + |
|
218 " to " + aIdOrNameOrAcc); |
|
219 } else { |
|
220 aDocAcc.virtualCursor.position = getAccessible(aIdOrNameOrAcc); |
|
221 } |
|
222 }; |
|
223 |
|
224 this.getID = function setVCPosInvoker_getID() |
|
225 { |
|
226 return "Do " + (expectMove ? "" : "no-op ") + aPivotMoveMethod; |
|
227 }; |
|
228 |
|
229 if (expectMove) { |
|
230 this.eventSeq = [ |
|
231 new VCChangedChecker(aDocAcc, aIdOrNameOrAcc, null, aPivotMoveMethod) |
|
232 ]; |
|
233 } else { |
|
234 this.eventSeq = []; |
|
235 this.unexpectedEventSeq = [ |
|
236 new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc) |
|
237 ]; |
|
238 } |
|
239 } |
|
240 |
|
241 /** |
|
242 * Move the pivot by text and wait for virtual cursor change event. |
|
243 * |
|
244 * @param aDocAcc [in] document that manages the virtual cursor |
|
245 * @param aPivotMoveMethod [in] method to test (ie. "moveNext", "moveFirst", etc.) |
|
246 * @param aBoundary [in] boundary constant |
|
247 * @param aTextOffsets [in] start and end offsets of text range to set in |
|
248 * virtual cursor. |
|
249 * @param aIdOrNameOrAcc [in] id, accessible or accessible name to expect |
|
250 * virtual cursor to land on after performing move method. |
|
251 * false if no move is expected. |
|
252 */ |
|
253 function setVCTextInvoker(aDocAcc, aPivotMoveMethod, aBoundary, aTextOffsets, aIdOrNameOrAcc) |
|
254 { |
|
255 var expectMove = (aIdOrNameOrAcc != false); |
|
256 this.invoke = function virtualCursorChangedInvoker_invoke() |
|
257 { |
|
258 VCChangedChecker.storePreviousPosAndOffset(aDocAcc.virtualCursor); |
|
259 SimpleTest.info(aDocAcc.virtualCursor.position); |
|
260 var moved = aDocAcc.virtualCursor[aPivotMoveMethod](aBoundary); |
|
261 SimpleTest.is(!!moved, !!expectMove, |
|
262 "moved pivot by text with " + aPivotMoveMethod + |
|
263 " to " + aIdOrNameOrAcc); |
|
264 }; |
|
265 |
|
266 this.getID = function setVCPosInvoker_getID() |
|
267 { |
|
268 return "Do " + (expectMove ? "" : "no-op ") + aPivotMoveMethod + " in " + |
|
269 prettyName(aIdOrNameOrAcc) + ", " + boundaryToString(aBoundary) + |
|
270 ", [" + aTextOffsets + "]"; |
|
271 }; |
|
272 |
|
273 if (expectMove) { |
|
274 this.eventSeq = [ |
|
275 new VCChangedChecker(aDocAcc, aIdOrNameOrAcc, aTextOffsets, aPivotMoveMethod) |
|
276 ]; |
|
277 } else { |
|
278 this.eventSeq = []; |
|
279 this.unexpectedEventSeq = [ |
|
280 new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc) |
|
281 ]; |
|
282 } |
|
283 } |
|
284 |
|
285 |
|
286 /** |
|
287 * Move the pivot to the position under the point. |
|
288 * |
|
289 * @param aDocAcc [in] document that manages the virtual cursor |
|
290 * @param aX [in] screen x coordinate |
|
291 * @param aY [in] screen y coordinate |
|
292 * @param aIgnoreNoMatch [in] don't unset position if no object was found at |
|
293 * point. |
|
294 * @param aRule [in] traversal rule object |
|
295 * @param aIdOrNameOrAcc [in] id, accessible or accessible name to expect |
|
296 * virtual cursor to land on after performing move method. |
|
297 * false if no move is expected. |
|
298 */ |
|
299 function moveVCCoordInvoker(aDocAcc, aX, aY, aIgnoreNoMatch, |
|
300 aRule, aIdOrNameOrAcc) |
|
301 { |
|
302 var expectMove = (aIdOrNameOrAcc != false); |
|
303 this.invoke = function virtualCursorChangedInvoker_invoke() |
|
304 { |
|
305 VCChangedChecker. |
|
306 storePreviousPosAndOffset(aDocAcc.virtualCursor); |
|
307 var moved = aDocAcc.virtualCursor.moveToPoint(aRule, aX, aY, |
|
308 aIgnoreNoMatch); |
|
309 SimpleTest.ok((expectMove && moved) || (!expectMove && !moved), |
|
310 "moved pivot"); |
|
311 }; |
|
312 |
|
313 this.getID = function setVCPosInvoker_getID() |
|
314 { |
|
315 return "Do " + (expectMove ? "" : "no-op ") + "moveToPoint " + aIdOrNameOrAcc; |
|
316 }; |
|
317 |
|
318 if (expectMove) { |
|
319 this.eventSeq = [ |
|
320 new VCChangedChecker(aDocAcc, aIdOrNameOrAcc, null, 'moveToPoint') |
|
321 ]; |
|
322 } else { |
|
323 this.eventSeq = []; |
|
324 this.unexpectedEventSeq = [ |
|
325 new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc) |
|
326 ]; |
|
327 } |
|
328 } |
|
329 |
|
330 /** |
|
331 * Change the pivot modalRoot |
|
332 * |
|
333 * @param aDocAcc [in] document that manages the virtual cursor |
|
334 * @param aModalRootAcc [in] accessible of the modal root, or null |
|
335 * @param aExpectedResult [in] error result expected. 0 if expecting success |
|
336 */ |
|
337 function setModalRootInvoker(aDocAcc, aModalRootAcc, aExpectedResult) |
|
338 { |
|
339 this.invoke = function setModalRootInvoker_invoke() |
|
340 { |
|
341 var errorResult = 0; |
|
342 try { |
|
343 aDocAcc.virtualCursor.modalRoot = aModalRootAcc; |
|
344 } catch (x) { |
|
345 SimpleTest.ok( |
|
346 x.result, "Unexpected exception when changing modal root: " + x); |
|
347 errorResult = x.result; |
|
348 } |
|
349 |
|
350 SimpleTest.is(errorResult, aExpectedResult, |
|
351 "Did not get expected result when changing modalRoot"); |
|
352 }; |
|
353 |
|
354 this.getID = function setModalRootInvoker_getID() |
|
355 { |
|
356 return "Set modalRoot to " + prettyName(aModalRootAcc); |
|
357 }; |
|
358 |
|
359 this.eventSeq = []; |
|
360 this.unexpectedEventSeq = [ |
|
361 new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc) |
|
362 ]; |
|
363 } |
|
364 |
|
365 /** |
|
366 * Add invokers to a queue to test a rule and an expected sequence of element ids |
|
367 * or accessible names for that rule in the given document. |
|
368 * |
|
369 * @param aQueue [in] event queue in which to push invoker sequence. |
|
370 * @param aDocAcc [in] the managing document of the virtual cursor we are |
|
371 * testing |
|
372 * @param aRule [in] the traversal rule to use in the invokers |
|
373 * @param aModalRoot [in] a modal root to use in this traversal sequence |
|
374 * @param aSequence [in] a sequence of accessible names or element ids to expect |
|
375 * with the given rule in the given document |
|
376 */ |
|
377 function queueTraversalSequence(aQueue, aDocAcc, aRule, aModalRoot, aSequence) |
|
378 { |
|
379 aDocAcc.virtualCursor.position = null; |
|
380 |
|
381 // Add modal root (if any) |
|
382 aQueue.push(new setModalRootInvoker(aDocAcc, aModalRoot, 0)); |
|
383 |
|
384 aQueue.push(new setVCPosInvoker(aDocAcc, "moveFirst", aRule, aSequence[0])); |
|
385 |
|
386 for (var i = 1; i < aSequence.length; i++) { |
|
387 var invoker = |
|
388 new setVCPosInvoker(aDocAcc, "moveNext", aRule, aSequence[i]); |
|
389 aQueue.push(invoker); |
|
390 } |
|
391 |
|
392 // No further more matches for given rule, expect no virtual cursor changes. |
|
393 aQueue.push(new setVCPosInvoker(aDocAcc, "moveNext", aRule, false)); |
|
394 |
|
395 for (var i = aSequence.length-2; i >= 0; i--) { |
|
396 var invoker = |
|
397 new setVCPosInvoker(aDocAcc, "movePrevious", aRule, aSequence[i]); |
|
398 aQueue.push(invoker); |
|
399 } |
|
400 |
|
401 // No previous more matches for given rule, expect no virtual cursor changes. |
|
402 aQueue.push(new setVCPosInvoker(aDocAcc, "movePrevious", aRule, false)); |
|
403 |
|
404 aQueue.push(new setVCPosInvoker(aDocAcc, "moveLast", aRule, |
|
405 aSequence[aSequence.length - 1])); |
|
406 |
|
407 // No further more matches for given rule, expect no virtual cursor changes. |
|
408 aQueue.push(new setVCPosInvoker(aDocAcc, "moveNext", aRule, false)); |
|
409 |
|
410 aQueue.push(new setVCPosInvoker(aDocAcc, "moveFirst", aRule, aSequence[0])); |
|
411 |
|
412 // No previous more matches for given rule, expect no virtual cursor changes. |
|
413 aQueue.push(new setVCPosInvoker(aDocAcc, "movePrevious", aRule, false)); |
|
414 |
|
415 // Remove modal root (if any). |
|
416 aQueue.push(new setModalRootInvoker(aDocAcc, null, 0)); |
|
417 } |
|
418 |
|
419 /** |
|
420 * A checker for removing an accessible while the virtual cursor is on it. |
|
421 */ |
|
422 function removeVCPositionChecker(aDocAcc, aHiddenParentAcc) |
|
423 { |
|
424 this.__proto__ = new invokerChecker(EVENT_REORDER, aHiddenParentAcc); |
|
425 |
|
426 this.check = function removeVCPositionChecker_check(aEvent) { |
|
427 var errorResult = 0; |
|
428 try { |
|
429 aDocAcc.virtualCursor.moveNext(ObjectTraversalRule); |
|
430 } catch (x) { |
|
431 errorResult = x.result; |
|
432 } |
|
433 SimpleTest.is( |
|
434 errorResult, NS_ERROR_NOT_IN_TREE, |
|
435 "Expecting NOT_IN_TREE error when moving pivot from invalid position."); |
|
436 }; |
|
437 } |
|
438 |
|
439 /** |
|
440 * Put the virtual cursor's position on an object, and then remove it. |
|
441 * |
|
442 * @param aDocAcc [in] document that manages the virtual cursor |
|
443 * @param aPosNode [in] DOM node to hide after virtual cursor's position is |
|
444 * set to it. |
|
445 */ |
|
446 function removeVCPositionInvoker(aDocAcc, aPosNode) |
|
447 { |
|
448 this.accessible = getAccessible(aPosNode); |
|
449 this.invoke = function removeVCPositionInvoker_invoke() |
|
450 { |
|
451 aDocAcc.virtualCursor.position = this.accessible; |
|
452 aPosNode.parentNode.removeChild(aPosNode); |
|
453 }; |
|
454 |
|
455 this.getID = function removeVCPositionInvoker_getID() |
|
456 { |
|
457 return "Bring virtual cursor to accessible, and remove its DOM node."; |
|
458 }; |
|
459 |
|
460 this.eventSeq = [ |
|
461 new removeVCPositionChecker(aDocAcc, this.accessible.parent) |
|
462 ]; |
|
463 } |
|
464 |
|
465 /** |
|
466 * A checker for removing the pivot root and then calling moveFirst, and |
|
467 * checking that an exception is thrown. |
|
468 */ |
|
469 function removeVCRootChecker(aPivot) |
|
470 { |
|
471 this.__proto__ = new invokerChecker(EVENT_REORDER, aPivot.root.parent); |
|
472 |
|
473 this.check = function removeVCRootChecker_check(aEvent) { |
|
474 var errorResult = 0; |
|
475 try { |
|
476 aPivot.moveLast(ObjectTraversalRule); |
|
477 } catch (x) { |
|
478 errorResult = x.result; |
|
479 } |
|
480 SimpleTest.is( |
|
481 errorResult, NS_ERROR_NOT_IN_TREE, |
|
482 "Expecting NOT_IN_TREE error when moving pivot from invalid position."); |
|
483 }; |
|
484 } |
|
485 |
|
486 /** |
|
487 * Create a pivot, remove its root, and perform an operation where the root is |
|
488 * needed. |
|
489 * |
|
490 * @param aRootNode [in] DOM node of which accessible will be the root of the |
|
491 * pivot. Should have more than one child. |
|
492 */ |
|
493 function removeVCRootInvoker(aRootNode) |
|
494 { |
|
495 this.pivot = gAccRetrieval.createAccessiblePivot(getAccessible(aRootNode)); |
|
496 this.invoke = function removeVCRootInvoker_invoke() |
|
497 { |
|
498 this.pivot.position = this.pivot.root.firstChild; |
|
499 aRootNode.parentNode.removeChild(aRootNode); |
|
500 }; |
|
501 |
|
502 this.getID = function removeVCRootInvoker_getID() |
|
503 { |
|
504 return "Remove root of pivot from tree."; |
|
505 }; |
|
506 |
|
507 this.eventSeq = [ |
|
508 new removeVCRootChecker(this.pivot) |
|
509 ]; |
|
510 } |
|
511 |
|
512 /** |
|
513 * A debug utility for writing proper sequences for queueTraversalSequence. |
|
514 */ |
|
515 function dumpTraversalSequence(aPivot, aRule) |
|
516 { |
|
517 var sequence = []; |
|
518 if (aPivot.moveFirst(aRule)) { |
|
519 do { |
|
520 sequence.push("'" + prettyName(aPivot.position) + "'"); |
|
521 } while (aPivot.moveNext(aRule)) |
|
522 } |
|
523 SimpleTest.info("\n[" + sequence.join(", ") + "]\n"); |
|
524 } |