Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | <html> |
michael@0 | 2 | <head> |
michael@0 | 3 | <title>Test for IME state controling and focus moving for iframes</title> |
michael@0 | 4 | <script type="text/javascript" |
michael@0 | 5 | src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> |
michael@0 | 6 | <link rel="stylesheet" type="text/css" |
michael@0 | 7 | href="chrome://mochikit/content/tests/SimpleTest/test.css" /> |
michael@0 | 8 | <style type="text/css"> |
michael@0 | 9 | iframe { |
michael@0 | 10 | border: none; |
michael@0 | 11 | height: 100px; |
michael@0 | 12 | } |
michael@0 | 13 | </style> |
michael@0 | 14 | </head> |
michael@0 | 15 | <body onunload="onUnload();"> |
michael@0 | 16 | <p id="display"> |
michael@0 | 17 | <!-- Use input[readonly] because it isn't affected by the partial focus |
michael@0 | 18 | movement on Mac --> |
michael@0 | 19 | <input id="prev" readonly><br> |
michael@0 | 20 | <iframe id="iframe_not_editable" |
michael@0 | 21 | src="data:text/html,<html><body><input id='editor'></body></html>"></iframe><br> |
michael@0 | 22 | |
michael@0 | 23 | <!-- Testing IME state and focus movement, the anchor elements cannot get focus --> |
michael@0 | 24 | <iframe id="iframe_html" |
michael@0 | 25 | src="data:text/html,<html id='editor' contenteditable='true'><body><a href='about:blank'>about:blank;</a></body></html>"></iframe><br> |
michael@0 | 26 | <iframe id="iframe_designMode" |
michael@0 | 27 | src="data:text/html,<body id='editor' onload='document.designMode="on";'><a href='about:blank'>about:blank;</a></body>"></iframe><br> |
michael@0 | 28 | <iframe id="iframe_body" |
michael@0 | 29 | src="data:text/html,<body id='editor' contenteditable='true'><a href='about:blank'>about:blank;</a></body>"></iframe><br> |
michael@0 | 30 | <iframe id="iframe_p" |
michael@0 | 31 | src="data:text/html,<body><p id='editor' contenteditable='true'><a href='about:blank'>about:blank;</a></p></body>"></iframe><br> |
michael@0 | 32 | |
michael@0 | 33 | <input id="next" readonly><br> |
michael@0 | 34 | </p> |
michael@0 | 35 | <script class="testbody" type="application/javascript"> |
michael@0 | 36 | |
michael@0 | 37 | window.opener.wrappedJSObject.SimpleTest.waitForFocus(runTests, window); |
michael@0 | 38 | |
michael@0 | 39 | function ok(aCondition, aMessage) |
michael@0 | 40 | { |
michael@0 | 41 | window.opener.wrappedJSObject.SimpleTest.ok(aCondition, aMessage); |
michael@0 | 42 | } |
michael@0 | 43 | |
michael@0 | 44 | function is(aLeft, aRight, aMessage) |
michael@0 | 45 | { |
michael@0 | 46 | window.opener.wrappedJSObject.SimpleTest.is(aLeft, aRight, aMessage); |
michael@0 | 47 | } |
michael@0 | 48 | |
michael@0 | 49 | function onUnload() |
michael@0 | 50 | { |
michael@0 | 51 | window.opener.wrappedJSObject.onFinish(); |
michael@0 | 52 | } |
michael@0 | 53 | |
michael@0 | 54 | var gFocusObservingElement = null; |
michael@0 | 55 | var gBlurObservingElement = null; |
michael@0 | 56 | |
michael@0 | 57 | function onFocus(aEvent) |
michael@0 | 58 | { |
michael@0 | 59 | if (aEvent.target != gFocusObservingElement) { |
michael@0 | 60 | return; |
michael@0 | 61 | } |
michael@0 | 62 | ok(gFocusObservingElement.willFocus, |
michael@0 | 63 | "focus event is fired on unexpected element"); |
michael@0 | 64 | gFocusObservingElement.willFocus = false; |
michael@0 | 65 | } |
michael@0 | 66 | |
michael@0 | 67 | function onBlur(aEvent) |
michael@0 | 68 | { |
michael@0 | 69 | if (aEvent.target != gBlurObservingElement) { |
michael@0 | 70 | return; |
michael@0 | 71 | } |
michael@0 | 72 | ok(gBlurObservingElement.willBlur, |
michael@0 | 73 | "blur event is fired on unexpected element"); |
michael@0 | 74 | gBlurObservingElement.willBlur = false; |
michael@0 | 75 | } |
michael@0 | 76 | |
michael@0 | 77 | function observeFocusBlur(aNextFocusedNode, aWillFireFocusEvent, |
michael@0 | 78 | aNextBlurredNode, aWillFireBlurEvent) |
michael@0 | 79 | { |
michael@0 | 80 | if (gFocusObservingElement) { |
michael@0 | 81 | if (gFocusObservingElement.willFocus) { |
michael@0 | 82 | ok(false, "focus event was never fired on " + gFocusObservingElement); |
michael@0 | 83 | } |
michael@0 | 84 | gFocusObservingElement.removeEventListener("focus", onFocus, true); |
michael@0 | 85 | gFocusObservingElement.willFocus = NaN; |
michael@0 | 86 | gFocusObservingElement = null; |
michael@0 | 87 | } |
michael@0 | 88 | if (gBlurObservingElement) { |
michael@0 | 89 | if (gBlurObservingElement.willBlur) { |
michael@0 | 90 | ok(false, "blur event was never fired on " + gBlurObservingElement); |
michael@0 | 91 | } |
michael@0 | 92 | gBlurObservingElement.removeEventListener("blur", onBlur, true); |
michael@0 | 93 | gBlurObservingElement.willBlur = NaN; |
michael@0 | 94 | gBlurObservingElement = null; |
michael@0 | 95 | } |
michael@0 | 96 | if (aNextFocusedNode) { |
michael@0 | 97 | gFocusObservingElement = aNextFocusedNode; |
michael@0 | 98 | gFocusObservingElement.willFocus = aWillFireFocusEvent; |
michael@0 | 99 | gFocusObservingElement.addEventListener("focus", onFocus, true); |
michael@0 | 100 | } |
michael@0 | 101 | if (aNextBlurredNode) { |
michael@0 | 102 | gBlurObservingElement = aNextBlurredNode; |
michael@0 | 103 | gBlurObservingElement.willBlur = aWillFireBlurEvent; |
michael@0 | 104 | gBlurObservingElement.addEventListener("blur", onBlur, true); |
michael@0 | 105 | } |
michael@0 | 106 | } |
michael@0 | 107 | |
michael@0 | 108 | function runTests() |
michael@0 | 109 | { |
michael@0 | 110 | var utils = |
michael@0 | 111 | window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) |
michael@0 | 112 | .getInterface(Components.interfaces.nsIDOMWindowUtils); |
michael@0 | 113 | var fm = |
michael@0 | 114 | Components.classes["@mozilla.org/focus-manager;1"] |
michael@0 | 115 | .getService(Components.interfaces.nsIFocusManager); |
michael@0 | 116 | |
michael@0 | 117 | var iframe, editor, root, input; |
michael@0 | 118 | var prev = document.getElementById("prev"); |
michael@0 | 119 | var next = document.getElementById("next"); |
michael@0 | 120 | var html = document.documentElement; |
michael@0 | 121 | |
michael@0 | 122 | function resetFocusToInput(aDescription) |
michael@0 | 123 | { |
michael@0 | 124 | observeFocusBlur(null, false, null, false); |
michael@0 | 125 | prev.focus(); |
michael@0 | 126 | is(fm.focusedElement, prev, |
michael@0 | 127 | "input#prev[readonly] element didn't get focus: " + aDescription); |
michael@0 | 128 | is(utils.IMEStatus, utils.IME_STATUS_DISABLED, |
michael@0 | 129 | "IME enabled on input#prev[readonly]: " + aDescription); |
michael@0 | 130 | } |
michael@0 | 131 | |
michael@0 | 132 | function resetFocusToParentHTML(aDescription) |
michael@0 | 133 | { |
michael@0 | 134 | observeFocusBlur(null, false, null, false); |
michael@0 | 135 | html.focus(); |
michael@0 | 136 | is(fm.focusedElement, html, |
michael@0 | 137 | "Parent html element didn't get focus: " + aDescription); |
michael@0 | 138 | is(utils.IMEStatus, utils.IME_STATUS_DISABLED, |
michael@0 | 139 | "IME enabled on parent html element: " + aDescription); |
michael@0 | 140 | } |
michael@0 | 141 | |
michael@0 | 142 | function testTabKey(aForward, |
michael@0 | 143 | aNextFocusedNode, aWillFireFocusEvent, |
michael@0 | 144 | aNextBlurredNode, aWillFireBlurEvent, |
michael@0 | 145 | aIMEShouldBeEnabled, aTestingCaseDescription) |
michael@0 | 146 | { |
michael@0 | 147 | observeFocusBlur(aNextFocusedNode, aWillFireFocusEvent, |
michael@0 | 148 | aNextBlurredNode, aWillFireBlurEvent); |
michael@0 | 149 | synthesizeKey("VK_TAB", { shiftKey: !aForward }); |
michael@0 | 150 | var description = "Tab key test: "; |
michael@0 | 151 | if (!aForward) { |
michael@0 | 152 | description = "Shift-" + description; |
michael@0 | 153 | } |
michael@0 | 154 | description += aTestingCaseDescription + ": "; |
michael@0 | 155 | is(fm.focusedElement, aNextFocusedNode, |
michael@0 | 156 | description + "didn't move focus as expected"); |
michael@0 | 157 | is(utils.IMEStatus, |
michael@0 | 158 | aIMEShouldBeEnabled ? |
michael@0 | 159 | utils.IME_STATUS_ENABLED : utils.IME_STATUS_DISABLED, |
michael@0 | 160 | description + "didn't set IME state as expected"); |
michael@0 | 161 | } |
michael@0 | 162 | |
michael@0 | 163 | function testMouseClick(aNextFocusedNode, aWillFireFocusEvent, |
michael@0 | 164 | aWillAllNodeLostFocus, |
michael@0 | 165 | aNextBlurredNode, aWillFireBlurEvent, |
michael@0 | 166 | aIMEShouldBeEnabled, aTestingCaseDescription) |
michael@0 | 167 | { |
michael@0 | 168 | observeFocusBlur(aNextFocusedNode, aWillFireFocusEvent, |
michael@0 | 169 | aNextBlurredNode, aWillFireBlurEvent); |
michael@0 | 170 | // We're relying on layout inside the iframe being up to date, so make it so |
michael@0 | 171 | iframe.contentDocument.documentElement.getBoundingClientRect(); |
michael@0 | 172 | synthesizeMouse(iframe, 10, 80, { }); |
michael@0 | 173 | var description = "Click test: " + aTestingCaseDescription + ": "; |
michael@0 | 174 | is(fm.focusedElement, !aWillAllNodeLostFocus ? aNextFocusedNode : null, |
michael@0 | 175 | description + "didn't move focus as expected"); |
michael@0 | 176 | is(utils.IMEStatus, |
michael@0 | 177 | aIMEShouldBeEnabled ? |
michael@0 | 178 | utils.IME_STATUS_ENABLED : utils.IME_STATUS_DISABLED, |
michael@0 | 179 | description + "didn't set IME state as expected"); |
michael@0 | 180 | } |
michael@0 | 181 | |
michael@0 | 182 | function testOnEditorFlagChange(aDescription, aIsInDesignMode) |
michael@0 | 183 | { |
michael@0 | 184 | const kReadonly = |
michael@0 | 185 | Components.interfaces.nsIPlaintextEditor.eEditorReadonlyMask; |
michael@0 | 186 | var description = "testOnEditorFlagChange: " + aDescription; |
michael@0 | 187 | resetFocusToParentHTML(description); |
michael@0 | 188 | var htmlEditor = |
michael@0 | 189 | iframe.contentWindow. |
michael@0 | 190 | QueryInterface(Components.interfaces.nsIInterfaceRequestor). |
michael@0 | 191 | getInterface(Components.interfaces.nsIWebNavigation). |
michael@0 | 192 | QueryInterface(Components.interfaces.nsIDocShell).editor; |
michael@0 | 193 | var e = aIsInDesignMode ? root : editor; |
michael@0 | 194 | e.focus(); |
michael@0 | 195 | is(fm.focusedElement, e, |
michael@0 | 196 | description + ": focus() of editor didn't move focus as expected"); |
michael@0 | 197 | is(utils.IMEStatus, utils.IME_STATUS_ENABLED, |
michael@0 | 198 | description + ": IME isn't enabled when the editor gets focus"); |
michael@0 | 199 | var flags = htmlEditor.flags; |
michael@0 | 200 | htmlEditor.flags |= kReadonly; |
michael@0 | 201 | is(fm.focusedElement, e, |
michael@0 | 202 | description + ": when editor becomes readonly, focus moved unexpectedly"); |
michael@0 | 203 | is(utils.IMEStatus, utils.IME_STATUS_DISABLED, |
michael@0 | 204 | description + ": when editor becomes readonly, IME is still enabled"); |
michael@0 | 205 | htmlEditor.flags = flags; |
michael@0 | 206 | is(fm.focusedElement, e, |
michael@0 | 207 | description + ": when editor becomes read-write, focus moved unexpectedly"); |
michael@0 | 208 | is(utils.IMEStatus, utils.IME_STATUS_ENABLED, |
michael@0 | 209 | description + ": when editor becomes read-write, IME is still disabled"); |
michael@0 | 210 | } |
michael@0 | 211 | |
michael@0 | 212 | // hide all iframes |
michael@0 | 213 | document.getElementById("iframe_not_editable").style.display = "none"; |
michael@0 | 214 | document.getElementById("iframe_html").style.display = "none"; |
michael@0 | 215 | document.getElementById("iframe_designMode").style.display = "none"; |
michael@0 | 216 | document.getElementById("iframe_body").style.display = "none"; |
michael@0 | 217 | document.getElementById("iframe_p").style.display = "none"; |
michael@0 | 218 | |
michael@0 | 219 | // non editable HTML element and input element can get focus. |
michael@0 | 220 | iframe = document.getElementById("iframe_not_editable"); |
michael@0 | 221 | iframe.style.display = "inline"; |
michael@0 | 222 | editor = iframe.contentDocument.getElementById("editor"); |
michael@0 | 223 | root = iframe.contentDocument.documentElement; |
michael@0 | 224 | resetFocusToInput("initializing for iframe_not_editable"); |
michael@0 | 225 | |
michael@0 | 226 | testTabKey(true, root, false, prev, true, |
michael@0 | 227 | false, "input#prev[readonly] -> html"); |
michael@0 | 228 | testTabKey(true, editor, true, root, false, |
michael@0 | 229 | true, "html -> input in the subdoc"); |
michael@0 | 230 | testTabKey(true, next, true, editor, true, |
michael@0 | 231 | false, "input in the subdoc -> input#next[readonly]"); |
michael@0 | 232 | testTabKey(false, editor, true, next, true, |
michael@0 | 233 | true, "input#next[readonly] -> input in the subdoc"); |
michael@0 | 234 | testTabKey(false, root, false, editor, true, |
michael@0 | 235 | false, "input in the subdoc -> html"); |
michael@0 | 236 | testTabKey(false, prev, true, root, false, |
michael@0 | 237 | false, "html -> input#next[readonly]"); |
michael@0 | 238 | |
michael@0 | 239 | iframe.style.display = "none"; |
michael@0 | 240 | |
michael@0 | 241 | // HTML element (of course, it's root) must enables IME. |
michael@0 | 242 | iframe = document.getElementById("iframe_html"); |
michael@0 | 243 | iframe.style.display = "inline"; |
michael@0 | 244 | editor = iframe.contentDocument.getElementById("editor"); |
michael@0 | 245 | root = iframe.contentDocument.documentElement; |
michael@0 | 246 | resetFocusToInput("initializing for iframe_html"); |
michael@0 | 247 | |
michael@0 | 248 | testTabKey(true, editor, true, prev, true, |
michael@0 | 249 | true, "input#prev[readonly] -> html[contentediable=true]"); |
michael@0 | 250 | testTabKey(true, next, true, editor, true, |
michael@0 | 251 | false, "html[contentediable=true] -> input#next[readonly]"); |
michael@0 | 252 | testTabKey(false, editor, true, next, true, |
michael@0 | 253 | true, "input#next[readonly] -> html[contentediable=true]"); |
michael@0 | 254 | testTabKey(false, prev, true, editor, true, |
michael@0 | 255 | false, "html[contenteditable=true] -> input[readonly]"); |
michael@0 | 256 | |
michael@0 | 257 | prev.style.display = "none"; |
michael@0 | 258 | resetFocusToParentHTML("testing iframe_html"); |
michael@0 | 259 | testTabKey(true, editor, true, html, false, |
michael@0 | 260 | true, "html of parent -> html[contentediable=true]"); |
michael@0 | 261 | testTabKey(false, html, false, editor, true, |
michael@0 | 262 | false, "html[contenteditable=true] -> html of parent"); |
michael@0 | 263 | prev.style.display = "inline"; |
michael@0 | 264 | resetFocusToInput("after parent html <-> html[contenteditable=true]"); |
michael@0 | 265 | |
michael@0 | 266 | testMouseClick(editor, true, false, prev, true, true, "iframe_html"); |
michael@0 | 267 | |
michael@0 | 268 | testOnEditorFlagChange("html[contentediable=true]", false); |
michael@0 | 269 | |
michael@0 | 270 | iframe.style.display = "none"; |
michael@0 | 271 | |
michael@0 | 272 | // designMode should behave like <html contenteditable="true"></html> |
michael@0 | 273 | // but focus/blur events shouldn't be fired on its root element because |
michael@0 | 274 | // any elements shouldn't be focused state in designMode. |
michael@0 | 275 | iframe = document.getElementById("iframe_designMode"); |
michael@0 | 276 | iframe.style.display = "inline"; |
michael@0 | 277 | iframe.contentDocument.designMode = "on"; |
michael@0 | 278 | editor = iframe.contentDocument.getElementById("editor"); |
michael@0 | 279 | root = iframe.contentDocument.documentElement; |
michael@0 | 280 | resetFocusToInput("initializing for iframe_designMode"); |
michael@0 | 281 | |
michael@0 | 282 | testTabKey(true, root, false, prev, true, |
michael@0 | 283 | true, "input#prev[readonly] -> html in designMode"); |
michael@0 | 284 | testTabKey(true, next, true, root, false, |
michael@0 | 285 | false, "html in designMode -> input#next[readonly]"); |
michael@0 | 286 | testTabKey(false, root, false, next, true, |
michael@0 | 287 | true, "input#next[readonly] -> html in designMode"); |
michael@0 | 288 | testTabKey(false, prev, true, root, false, |
michael@0 | 289 | false, "html in designMode -> input#prev[readonly]"); |
michael@0 | 290 | |
michael@0 | 291 | prev.style.display = "none"; |
michael@0 | 292 | resetFocusToParentHTML("testing iframe_designMode"); |
michael@0 | 293 | testTabKey(true, root, false, html, false, |
michael@0 | 294 | true, "html of parent -> html in designMode"); |
michael@0 | 295 | testTabKey(false, html, false, root, false, |
michael@0 | 296 | false, "html in designMode -> html of parent"); |
michael@0 | 297 | prev.style.display = "inline"; |
michael@0 | 298 | resetFocusToInput("after parent html <-> html in designMode"); |
michael@0 | 299 | |
michael@0 | 300 | testMouseClick(editor, false, true, prev, true, true, "iframe_designMode"); |
michael@0 | 301 | |
michael@0 | 302 | testOnEditorFlagChange("html in designMode", true); |
michael@0 | 303 | |
michael@0 | 304 | iframe.style.display = "none"; |
michael@0 | 305 | |
michael@0 | 306 | // When there is no HTML element but the BODY element is editable, |
michael@0 | 307 | // the body element should get focus and enables IME. |
michael@0 | 308 | iframe = document.getElementById("iframe_body"); |
michael@0 | 309 | iframe.style.display = "inline"; |
michael@0 | 310 | editor = iframe.contentDocument.getElementById("editor"); |
michael@0 | 311 | root = iframe.contentDocument.documentElement; |
michael@0 | 312 | resetFocusToInput("initializing for iframe_body"); |
michael@0 | 313 | |
michael@0 | 314 | testTabKey(true, editor, true, prev, true, |
michael@0 | 315 | true, "input#prev[readonly] -> body[contentediable=true]"); |
michael@0 | 316 | testTabKey(true, next, true, editor, true, |
michael@0 | 317 | false, "body[contentediable=true] -> input#next[readonly]"); |
michael@0 | 318 | testTabKey(false, editor, true, next, true, |
michael@0 | 319 | true, "input#next[readonly] -> body[contentediable=true]"); |
michael@0 | 320 | testTabKey(false, prev, true, editor, true, |
michael@0 | 321 | false, "body[contenteditable=true] -> input#prev[readonly]"); |
michael@0 | 322 | |
michael@0 | 323 | prev.style.display = "none"; |
michael@0 | 324 | resetFocusToParentHTML("testing iframe_body"); |
michael@0 | 325 | testTabKey(true, editor, true, html, false, |
michael@0 | 326 | true, "html of parent -> body[contentediable=true]"); |
michael@0 | 327 | testTabKey(false, html, false, editor, true, |
michael@0 | 328 | false, "body[contenteditable=true] -> html of parent"); |
michael@0 | 329 | prev.style.display = "inline"; |
michael@0 | 330 | resetFocusToInput("after parent html <-> body[contenteditable=true]"); |
michael@0 | 331 | |
michael@0 | 332 | testMouseClick(editor, true, false, prev, true, true, "iframe_body"); |
michael@0 | 333 | |
michael@0 | 334 | testOnEditorFlagChange("body[contentediable=true]", false); |
michael@0 | 335 | |
michael@0 | 336 | iframe.style.display = "none"; |
michael@0 | 337 | |
michael@0 | 338 | // When HTML/BODY elements are not editable, focus shouldn't be moved to |
michael@0 | 339 | // the editable content directly. |
michael@0 | 340 | iframe = document.getElementById("iframe_p"); |
michael@0 | 341 | iframe.style.display = "inline"; |
michael@0 | 342 | editor = iframe.contentDocument.getElementById("editor"); |
michael@0 | 343 | root = iframe.contentDocument.documentElement; |
michael@0 | 344 | resetFocusToInput("initializing for iframe_p"); |
michael@0 | 345 | |
michael@0 | 346 | testTabKey(true, root, false, prev, true, |
michael@0 | 347 | false, "input#prev[readonly] -> html (has p[contenteditable=true])"); |
michael@0 | 348 | testTabKey(true, editor, true, root, false, |
michael@0 | 349 | true, "html (has p[contenteditable=true]) -> p[contentediable=true]"); |
michael@0 | 350 | testTabKey(true, next, true, editor, true, |
michael@0 | 351 | false, "p[contentediable=true] -> input#next[readonly]"); |
michael@0 | 352 | testTabKey(false, editor, true, next, true, |
michael@0 | 353 | true, "input#next[readonly] -> p[contentediable=true]"); |
michael@0 | 354 | testTabKey(false, root, false, editor, true, |
michael@0 | 355 | false, "p[contenteditable=true] -> html (has p[contenteditable=true])"); |
michael@0 | 356 | testTabKey(false, prev, true, root, false, |
michael@0 | 357 | false, "html (has p[contenteditable=true]) -> input#prev[readonly]"); |
michael@0 | 358 | prev.style.display = "none"; |
michael@0 | 359 | |
michael@0 | 360 | resetFocusToParentHTML("testing iframe_p"); |
michael@0 | 361 | testTabKey(true, root, false, html, false, |
michael@0 | 362 | false, "html of parent -> html (has p[contentediable=true])"); |
michael@0 | 363 | testTabKey(false, html, false, root, false, |
michael@0 | 364 | false, "html (has p[contentediable=true]) -> html of parent"); |
michael@0 | 365 | prev.style.display = "inline"; |
michael@0 | 366 | resetFocusToInput("after parent html <-> html (has p[contentediable=true])"); |
michael@0 | 367 | |
michael@0 | 368 | testMouseClick(root, false, true, prev, true, false, "iframe_p"); |
michael@0 | 369 | |
michael@0 | 370 | testOnEditorFlagChange("p[contenteditable=true]", false); |
michael@0 | 371 | |
michael@0 | 372 | iframe.style.display = "none"; |
michael@0 | 373 | |
michael@0 | 374 | window.close(); |
michael@0 | 375 | } |
michael@0 | 376 | |
michael@0 | 377 | </script> |
michael@0 | 378 | </body> |
michael@0 | 379 | |
michael@0 | 380 | </html> |