accessible/tests/mochitest/events/test_mutation.html

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 <html>
michael@0 2
michael@0 3 <head>
michael@0 4 <title>Accessible mutation events testing</title>
michael@0 5
michael@0 6 <link rel="stylesheet" type="text/css"
michael@0 7 href="chrome://mochikit/content/tests/SimpleTest/test.css" />
michael@0 8
michael@0 9 <style>
michael@0 10 div.displayNone a { display:none; }
michael@0 11 div.visibilityHidden a { visibility:hidden; }
michael@0 12 </style>
michael@0 13
michael@0 14 <script type="application/javascript"
michael@0 15 src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
michael@0 16 <script type="application/javascript"
michael@0 17 src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
michael@0 18
michael@0 19 <script type="application/javascript"
michael@0 20 src="../common.js"></script>
michael@0 21 <script type="application/javascript"
michael@0 22 src="../events.js"></script>
michael@0 23
michael@0 24 <script type="application/javascript">
michael@0 25 /**
michael@0 26 * Invokers.
michael@0 27 */
michael@0 28 var kNoEvents = 0;
michael@0 29
michael@0 30 var kShowEvent = 1;
michael@0 31 var kHideEvent = 2;
michael@0 32 var kReorderEvent = 4;
michael@0 33 var kShowEvents = kShowEvent | kReorderEvent;
michael@0 34 var kHideEvents = kHideEvent | kReorderEvent;
michael@0 35 var kHideAndShowEvents = kHideEvents | kShowEvent;
michael@0 36
michael@0 37 /**
michael@0 38 * Base class to test mutation a11y events.
michael@0 39 *
michael@0 40 * @param aNodeOrID [in] node invoker's action is executed for
michael@0 41 * @param aEventTypes [in] events to register (see constants above)
michael@0 42 * @param aDoNotExpectEvents [in] boolean indicates if events are expected
michael@0 43 */
michael@0 44 function mutateA11yTree(aNodeOrID, aEventTypes, aDoNotExpectEvents)
michael@0 45 {
michael@0 46 // Interface
michael@0 47 this.DOMNode = getNode(aNodeOrID);
michael@0 48 this.doNotExpectEvents = aDoNotExpectEvents;
michael@0 49 this.eventSeq = [];
michael@0 50 this.unexpectedEventSeq = [];
michael@0 51
michael@0 52 /**
michael@0 53 * Change default target (aNodeOrID) registered for the given event type.
michael@0 54 */
michael@0 55 this.setTarget = function mutateA11yTree_setTarget(aEventType, aTarget)
michael@0 56 {
michael@0 57 var type = this.getA11yEventType(aEventType);
michael@0 58 for (var idx = 0; idx < this.getEventSeq().length; idx++) {
michael@0 59 if (this.getEventSeq()[idx].type == type) {
michael@0 60 this.getEventSeq()[idx].target = aTarget;
michael@0 61 return idx;
michael@0 62 }
michael@0 63 }
michael@0 64 return -1;
michael@0 65 }
michael@0 66
michael@0 67 /**
michael@0 68 * Replace the default target currently registered for a given event type
michael@0 69 * with the nodes in the targets array.
michael@0 70 */
michael@0 71 this.setTargets = function mutateA11yTree_setTargets(aEventType, aTargets) {
michael@0 72 var targetIdx = this.setTarget(aEventType, aTargets[0]);
michael@0 73
michael@0 74 var type = this.getA11yEventType(aEventType);
michael@0 75 for (var i = 1; i < aTargets.length; i++) {
michael@0 76 var checker = new invokerChecker(type, aTargets[i]);
michael@0 77 this.getEventSeq().splice(++targetIdx, 0, checker);
michael@0 78 }
michael@0 79 }
michael@0 80
michael@0 81 // Implementation
michael@0 82 this.getA11yEventType = function mutateA11yTree_getA11yEventType(aEventType)
michael@0 83 {
michael@0 84 if (aEventType == kReorderEvent)
michael@0 85 return nsIAccessibleEvent.EVENT_REORDER;
michael@0 86
michael@0 87 if (aEventType == kHideEvent)
michael@0 88 return nsIAccessibleEvent.EVENT_HIDE;
michael@0 89
michael@0 90 if (aEventType == kShowEvent)
michael@0 91 return nsIAccessibleEvent.EVENT_SHOW;
michael@0 92 }
michael@0 93
michael@0 94 this.getEventSeq = function mutateA11yTree_getEventSeq()
michael@0 95 {
michael@0 96 return this.doNotExpectEvents ? this.unexpectedEventSeq : this.eventSeq;
michael@0 97 }
michael@0 98
michael@0 99 if (aEventTypes & kHideEvent) {
michael@0 100 var checker = new invokerChecker(this.getA11yEventType(kHideEvent),
michael@0 101 this.DOMNode);
michael@0 102 this.getEventSeq().push(checker);
michael@0 103 }
michael@0 104
michael@0 105 if (aEventTypes & kShowEvent) {
michael@0 106 var checker = new invokerChecker(this.getA11yEventType(kShowEvent),
michael@0 107 this.DOMNode);
michael@0 108 this.getEventSeq().push(checker);
michael@0 109 }
michael@0 110
michael@0 111 if (aEventTypes & kReorderEvent) {
michael@0 112 var checker = new invokerChecker(this.getA11yEventType(kReorderEvent),
michael@0 113 this.DOMNode.parentNode);
michael@0 114 this.getEventSeq().push(checker);
michael@0 115 }
michael@0 116 }
michael@0 117
michael@0 118 /**
michael@0 119 * Change CSS style for the given node.
michael@0 120 */
michael@0 121 function changeStyle(aNodeOrID, aProp, aValue, aEventTypes)
michael@0 122 {
michael@0 123 this.__proto__ = new mutateA11yTree(aNodeOrID, aEventTypes, false);
michael@0 124
michael@0 125 this.invoke = function changeStyle_invoke()
michael@0 126 {
michael@0 127 this.DOMNode.style[aProp] = aValue;
michael@0 128 }
michael@0 129
michael@0 130 this.getID = function changeStyle_getID()
michael@0 131 {
michael@0 132 return aNodeOrID + " change style " + aProp + " on value " + aValue;
michael@0 133 }
michael@0 134 }
michael@0 135
michael@0 136 /**
michael@0 137 * Change class name for the given node.
michael@0 138 */
michael@0 139 function changeClass(aParentNodeOrID, aNodeOrID, aClassName, aEventTypes)
michael@0 140 {
michael@0 141 this.__proto__ = new mutateA11yTree(aNodeOrID, aEventTypes, false);
michael@0 142
michael@0 143 this.invoke = function changeClass_invoke()
michael@0 144 {
michael@0 145 this.parentDOMNode.className = aClassName;
michael@0 146 }
michael@0 147
michael@0 148 this.getID = function changeClass_getID()
michael@0 149 {
michael@0 150 return aNodeOrID + " change class " + aClassName;
michael@0 151 }
michael@0 152
michael@0 153 this.parentDOMNode = getNode(aParentNodeOrID);
michael@0 154 }
michael@0 155
michael@0 156 /**
michael@0 157 * Clone the node and append it to its parent.
michael@0 158 */
michael@0 159 function cloneAndAppendToDOM(aNodeOrID, aEventTypes,
michael@0 160 aTargetsFunc, aReorderTargetFunc)
michael@0 161 {
michael@0 162 var eventTypes = aEventTypes || kShowEvents;
michael@0 163 var doNotExpectEvents = (aEventTypes == kNoEvents);
michael@0 164
michael@0 165 this.__proto__ = new mutateA11yTree(aNodeOrID, eventTypes,
michael@0 166 doNotExpectEvents);
michael@0 167
michael@0 168 this.invoke = function cloneAndAppendToDOM_invoke()
michael@0 169 {
michael@0 170 var newElm = this.DOMNode.cloneNode(true);
michael@0 171 newElm.removeAttribute('id');
michael@0 172
michael@0 173 var targets = aTargetsFunc ?
michael@0 174 aTargetsFunc.call(null, newElm) : [newElm];
michael@0 175 this.setTargets(kShowEvent, targets);
michael@0 176
michael@0 177 if (aReorderTargetFunc) {
michael@0 178 var reorderTarget = aReorderTargetFunc.call(null, this.DOMNode);
michael@0 179 this.setTarget(kReorderEvent, reorderTarget);
michael@0 180 }
michael@0 181
michael@0 182 this.DOMNode.parentNode.appendChild(newElm);
michael@0 183 }
michael@0 184
michael@0 185 this.getID = function cloneAndAppendToDOM_getID()
michael@0 186 {
michael@0 187 return aNodeOrID + " clone and append to DOM.";
michael@0 188 }
michael@0 189 }
michael@0 190
michael@0 191 /**
michael@0 192 * Removes the node from DOM.
michael@0 193 */
michael@0 194 function removeFromDOM(aNodeOrID, aEventTypes,
michael@0 195 aTargetsFunc, aReorderTargetFunc)
michael@0 196 {
michael@0 197 var eventTypes = aEventTypes || kHideEvents;
michael@0 198 var doNotExpectEvents = (aEventTypes == kNoEvents);
michael@0 199
michael@0 200 this.__proto__ = new mutateA11yTree(aNodeOrID, eventTypes,
michael@0 201 doNotExpectEvents);
michael@0 202
michael@0 203 this.invoke = function removeFromDOM_invoke()
michael@0 204 {
michael@0 205 this.DOMNode.parentNode.removeChild(this.DOMNode);
michael@0 206 }
michael@0 207
michael@0 208 this.getID = function removeFromDOM_getID()
michael@0 209 {
michael@0 210 return prettyName(aNodeOrID) + " remove from DOM.";
michael@0 211 }
michael@0 212
michael@0 213 if (aTargetsFunc && (eventTypes & kHideEvent))
michael@0 214 this.setTargets(kHideEvent, aTargetsFunc.call(null, this.DOMNode));
michael@0 215
michael@0 216 if (aReorderTargetFunc && (eventTypes & kReorderEvent))
michael@0 217 this.setTarget(kReorderEvent,
michael@0 218 aReorderTargetFunc.call(null, this.DOMNode));
michael@0 219 }
michael@0 220
michael@0 221 /**
michael@0 222 * Clone the node and replace the original node by cloned one.
michael@0 223 */
michael@0 224 function cloneAndReplaceInDOM(aNodeOrID)
michael@0 225 {
michael@0 226 this.__proto__ = new mutateA11yTree(aNodeOrID, kHideAndShowEvents,
michael@0 227 false);
michael@0 228
michael@0 229 this.invoke = function cloneAndReplaceInDOM_invoke()
michael@0 230 {
michael@0 231 this.DOMNode.parentNode.replaceChild(this.newElm, this.DOMNode);
michael@0 232 }
michael@0 233
michael@0 234 this.getID = function cloneAndReplaceInDOM_getID()
michael@0 235 {
michael@0 236 return aNodeOrID + " clone and replace in DOM.";
michael@0 237 }
michael@0 238
michael@0 239 this.newElm = this.DOMNode.cloneNode(true);
michael@0 240 this.newElm.removeAttribute('id');
michael@0 241 this.setTarget(kShowEvent, this.newElm);
michael@0 242 }
michael@0 243
michael@0 244 /**
michael@0 245 * Trigger content insertion (flush layout), removal and insertion of
michael@0 246 * the same element for the same parent.
michael@0 247 */
michael@0 248 function test1(aContainerID)
michael@0 249 {
michael@0 250 this.divNode = document.createElement("div");
michael@0 251 this.divNode.setAttribute("id", "div-test1");
michael@0 252 this.containerNode = getNode(aContainerID);
michael@0 253
michael@0 254 this.eventSeq = [
michael@0 255 new invokerChecker(EVENT_SHOW, this.divNode),
michael@0 256 new invokerChecker(EVENT_REORDER, this.containerNode)
michael@0 257 ];
michael@0 258
michael@0 259 this.invoke = function test1_invoke()
michael@0 260 {
michael@0 261 this.containerNode.appendChild(this.divNode);
michael@0 262 getComputedStyle(this.divNode, "").color;
michael@0 263 this.containerNode.removeChild(this.divNode);
michael@0 264 this.containerNode.appendChild(this.divNode);
michael@0 265 }
michael@0 266
michael@0 267 this.getID = function test1_getID()
michael@0 268 {
michael@0 269 return "fuzzy test #1: content insertion (flush layout), removal and" +
michael@0 270 "reinsertion";
michael@0 271 }
michael@0 272 }
michael@0 273
michael@0 274 /**
michael@0 275 * Trigger content insertion (flush layout), removal and insertion of
michael@0 276 * the same element for the different parents.
michael@0 277 */
michael@0 278 function test2(aContainerID, aTmpContainerID)
michael@0 279 {
michael@0 280 this.divNode = document.createElement("div");
michael@0 281 this.divNode.setAttribute("id", "div-test2");
michael@0 282 this.containerNode = getNode(aContainerID);
michael@0 283 this.tmpContainerNode = getNode(aTmpContainerID);
michael@0 284 this.container = getAccessible(this.containerNode);
michael@0 285 this.tmpContainer = getAccessible(this.tmpContainerNode);
michael@0 286
michael@0 287 this.eventSeq = [
michael@0 288 new invokerChecker(EVENT_SHOW, this.divNode),
michael@0 289 new invokerChecker(EVENT_REORDER, this.containerNode)
michael@0 290 ];
michael@0 291
michael@0 292 this.unexpectedEventSeq = [
michael@0 293 new invokerChecker(EVENT_REORDER, this.tmpContainerNode)
michael@0 294 ];
michael@0 295
michael@0 296 this.invoke = function test2_invoke()
michael@0 297 {
michael@0 298 this.tmpContainerNode.appendChild(this.divNode);
michael@0 299 getComputedStyle(this.divNode, "").color;
michael@0 300 this.tmpContainerNode.removeChild(this.divNode);
michael@0 301 this.containerNode.appendChild(this.divNode);
michael@0 302 }
michael@0 303
michael@0 304 this.getID = function test2_getID()
michael@0 305 {
michael@0 306 return "fuzzy test #2: content insertion (flush layout), removal and" +
michael@0 307 "reinsertion under another container";
michael@0 308 }
michael@0 309 }
michael@0 310
michael@0 311 /**
michael@0 312 * Content insertion (flush layout) and then removal (nothing was changed).
michael@0 313 */
michael@0 314 function test3(aContainerID)
michael@0 315 {
michael@0 316 this.divNode = document.createElement("div");
michael@0 317 this.divNode.setAttribute("id", "div-test3");
michael@0 318 this.containerNode = getNode(aContainerID);
michael@0 319
michael@0 320 this.unexpectedEventSeq = [
michael@0 321 new invokerChecker(EVENT_SHOW, this.divNode),
michael@0 322 new invokerChecker(EVENT_HIDE, this.divNode),
michael@0 323 new invokerChecker(EVENT_REORDER, this.containerNode)
michael@0 324 ];
michael@0 325
michael@0 326 this.invoke = function test3_invoke()
michael@0 327 {
michael@0 328 this.containerNode.appendChild(this.divNode);
michael@0 329 getComputedStyle(this.divNode, "").color;
michael@0 330 this.containerNode.removeChild(this.divNode);
michael@0 331 }
michael@0 332
michael@0 333 this.getID = function test3_getID()
michael@0 334 {
michael@0 335 return "fuzzy test #3: content insertion (flush layout) and removal";
michael@0 336 }
michael@0 337 }
michael@0 338
michael@0 339 /**
michael@0 340 * Target getters.
michael@0 341 */
michael@0 342 function getFirstChild(aNode)
michael@0 343 {
michael@0 344 return [aNode.firstChild];
michael@0 345 }
michael@0 346
michael@0 347 function getNEnsureFirstChild(aNode)
michael@0 348 {
michael@0 349 var node = aNode.firstChild;
michael@0 350 getAccessible(node);
michael@0 351 return [node];
michael@0 352 }
michael@0 353
michael@0 354 function getNEnsureChildren(aNode)
michael@0 355 {
michael@0 356 var children = [];
michael@0 357 var node = aNode.firstChild;
michael@0 358 do {
michael@0 359 children.push(node);
michael@0 360 getAccessible(node);
michael@0 361 node = node.nextSibling;
michael@0 362 } while (node);
michael@0 363
michael@0 364 return children;
michael@0 365 }
michael@0 366
michael@0 367 function getParent(aNode)
michael@0 368 {
michael@0 369 return aNode.parentNode;
michael@0 370 }
michael@0 371
michael@0 372 /**
michael@0 373 * Do tests.
michael@0 374 */
michael@0 375 var gQueue = null;
michael@0 376 //gA11yEventDumpToConsole = true; // debug stuff
michael@0 377
michael@0 378 function doTests()
michael@0 379 {
michael@0 380 gQueue = new eventQueue();
michael@0 381
michael@0 382 // Show/hide events by changing of display style of accessible DOM node
michael@0 383 // from 'inline' to 'none', 'none' to 'inline'.
michael@0 384 var id = "link1";
michael@0 385 getAccessible(id); // ensure accessible is created
michael@0 386 gQueue.push(new changeStyle(id, "display", "none", kHideEvents));
michael@0 387 gQueue.push(new changeStyle(id, "display", "inline", kShowEvents));
michael@0 388
michael@0 389 // Show/hide events by changing of visibility style of accessible DOM node
michael@0 390 // from 'visible' to 'hidden', 'hidden' to 'visible'.
michael@0 391 var id = "link2";
michael@0 392 getAccessible(id);
michael@0 393 gQueue.push(new changeStyle(id, "visibility", "hidden", kHideEvents));
michael@0 394 gQueue.push(new changeStyle(id, "visibility", "visible", kShowEvents));
michael@0 395
michael@0 396 // Show/hide events by changing of display style of accessible DOM node
michael@0 397 // from 'inline' to 'block', 'block' to 'inline'.
michael@0 398 var id = "link3";
michael@0 399 getAccessible(id); // ensure accessible is created
michael@0 400 gQueue.push(new changeStyle(id, "display", "block", kHideAndShowEvents));
michael@0 401 gQueue.push(new changeStyle(id, "display", "inline", kHideAndShowEvents));
michael@0 402
michael@0 403 // Show/hide events by changing of visibility style of accessible DOM node
michael@0 404 // from 'collapse' to 'visible', 'visible' to 'collapse'.
michael@0 405 var id = "link4";
michael@0 406 gQueue.push(new changeStyle(id, "visibility", "visible", kShowEvents));
michael@0 407 gQueue.push(new changeStyle(id, "visibility", "collapse", kHideEvents));
michael@0 408
michael@0 409 // Show/hide events by adding new accessible DOM node and removing old one.
michael@0 410 var id = "link5";
michael@0 411 gQueue.push(new cloneAndAppendToDOM(id));
michael@0 412 gQueue.push(new removeFromDOM(id));
michael@0 413
michael@0 414 // No show/hide events by adding new not accessible DOM node and removing
michael@0 415 // old one, no reorder event for their parent.
michael@0 416 var id = "child1";
michael@0 417 gQueue.push(new cloneAndAppendToDOM(id, kNoEvents));
michael@0 418 gQueue.push(new removeFromDOM(id, kNoEvents));
michael@0 419
michael@0 420 // Show/hide events by adding new accessible DOM node and removing
michael@0 421 // old one, there is reorder event for their parent.
michael@0 422 var id = "child2";
michael@0 423 gQueue.push(new cloneAndAppendToDOM(id));
michael@0 424 gQueue.push(new removeFromDOM(id));
michael@0 425
michael@0 426 // Show/hide events by adding new DOM node containing accessible DOM and
michael@0 427 // removing old one, there is reorder event for their parent.
michael@0 428 var id = "child3";
michael@0 429 gQueue.push(new cloneAndAppendToDOM(id, kShowEvents, getFirstChild,
michael@0 430 getParent));
michael@0 431
michael@0 432 // Hide event for accessible child of unaccessible removed DOM node and
michael@0 433 // reorder event for its parent.
michael@0 434 gQueue.push(new removeFromDOM(id, kHideEvents,
michael@0 435 getNEnsureFirstChild, getParent));
michael@0 436
michael@0 437 // Hide events for accessible children of unaccessible removed DOM node
michael@0 438 // and reorder event for its parent.
michael@0 439 gQueue.push(new removeFromDOM("child4", kHideEvents,
michael@0 440 getNEnsureChildren, getParent));
michael@0 441
michael@0 442 // Show/hide events by creating new accessible DOM node and replacing
michael@0 443 // old one.
michael@0 444 getAccessible("link6"); // ensure accessible is created
michael@0 445 gQueue.push(new cloneAndReplaceInDOM("link6"));
michael@0 446
michael@0 447 // Show/hide events by changing class name on the parent node.
michael@0 448 gQueue.push(new changeClass("container2", "link7", "", kShowEvents));
michael@0 449 gQueue.push(new changeClass("container2", "link7", "displayNone",
michael@0 450 kHideEvents));
michael@0 451
michael@0 452 gQueue.push(new changeClass("container3", "link8", "", kShowEvents));
michael@0 453 gQueue.push(new changeClass("container3", "link8", "visibilityHidden",
michael@0 454 kHideEvents));
michael@0 455
michael@0 456 gQueue.push(new test1("testContainer"));
michael@0 457 gQueue.push(new test2("testContainer", "testContainer2"));
michael@0 458 gQueue.push(new test2("testContainer", "testNestedContainer"));
michael@0 459 gQueue.push(new test3("testContainer"));
michael@0 460
michael@0 461 gQueue.invoke(); // Will call SimpleTest.finish();
michael@0 462 }
michael@0 463
michael@0 464 SimpleTest.waitForExplicitFinish();
michael@0 465 addA11yLoadEvent(doTests);
michael@0 466 </script>
michael@0 467 </head>
michael@0 468
michael@0 469 <body>
michael@0 470
michael@0 471 <a target="_blank"
michael@0 472 href="https://bugzilla.mozilla.org/show_bug.cgi?id=469985"
michael@0 473 title=" turn the test from bug 354745 into mochitest">
michael@0 474 Mozilla Bug 469985</a>
michael@0 475 <a target="_blank"
michael@0 476 href="https://bugzilla.mozilla.org/show_bug.cgi?id=472662"
michael@0 477 title="no reorder event when html:link display property is changed from 'none' to 'inline'">
michael@0 478 Mozilla Bug 472662</a>
michael@0 479 <a target="_blank"
michael@0 480 title="Rework accessible tree update code"
michael@0 481 href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275">
michael@0 482 Mozilla Bug 570275</a>
michael@0 483 <a target="_blank"
michael@0 484 title="Develop a way to handle visibility style"
michael@0 485 href="https://bugzilla.mozilla.org/show_bug.cgi?id=606125">
michael@0 486 Mozilla Bug 606125</a>
michael@0 487 <a target="_blank"
michael@0 488 title="Update accessible tree on content insertion after layout"
michael@0 489 href="https://bugzilla.mozilla.org/show_bug.cgi?id=498015">
michael@0 490 Mozilla Bug 498015</a>
michael@0 491
michael@0 492 <p id="display"></p>
michael@0 493 <div id="content" style="display: none"></div>
michael@0 494 <pre id="test">
michael@0 495 </pre>
michael@0 496 <div id="eventdump"></div>
michael@0 497
michael@0 498 <div id="testContainer">
michael@0 499 <a id="link1" href="http://www.google.com">Link #1</a>
michael@0 500 <a id="link2" href="http://www.google.com">Link #2</a>
michael@0 501 <a id="link3" href="http://www.google.com">Link #3</a>
michael@0 502 <a id="link4" href="http://www.google.com" style="visibility:collapse">Link #4</a>
michael@0 503 <a id="link5" href="http://www.google.com">Link #5</a>
michael@0 504
michael@0 505 <div id="container" role="list">
michael@0 506 <span id="child1"></span>
michael@0 507 <span id="child2" role="listitem"></span>
michael@0 508 <span id="child3"><span role="listitem"></span></span>
michael@0 509 <span id="child4"><span id="child4_1" role="listitem"></span><span id="child4_2" role="listitem"></span></span>
michael@0 510 </div>
michael@0 511
michael@0 512 <a id="link6" href="http://www.google.com">Link #6</a>
michael@0 513
michael@0 514 <div id="container2" class="displayNone"><a id="link7">Link #7</a></div>
michael@0 515 <div id="container3" class="visibilityHidden"><a id="link8">Link #8</a></div>
michael@0 516 <div id="testNestedContainer"></div>
michael@0 517 </div>
michael@0 518 <div id="testContainer2"></div>
michael@0 519 </body>
michael@0 520 </html>

mercurial