diff options
Diffstat (limited to 'testing/mochitest/tests')
165 files changed, 32220 insertions, 0 deletions
diff --git a/testing/mochitest/tests/Harness_sanity/.eslintrc.js b/testing/mochitest/tests/Harness_sanity/.eslintrc.js new file mode 100644 index 0000000000..a858d9de4c --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/.eslintrc.js @@ -0,0 +1,12 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/xpcshell-test"], + + overrides: [ + { + files: "*.html", + env: { browser: true }, + }, + ], +}; diff --git a/testing/mochitest/tests/Harness_sanity/SpecialPowersLoadChromeScript.js b/testing/mochitest/tests/Harness_sanity/SpecialPowersLoadChromeScript.js new file mode 100644 index 0000000000..9591f3bca4 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/SpecialPowersLoadChromeScript.js @@ -0,0 +1,17 @@ +/* eslint-env mozilla/chrome-script */ + +// Just receive 'foo' message and forward it back +// as 'bar' message +addMessageListener("foo", function (message) { + sendAsyncMessage("bar", message); +}); + +addMessageListener("valid-assert", function (message) { + assert.ok(true, "valid assertion"); + assert.equal(1, 1, "another valid assertion"); + sendAsyncMessage("valid-assert-done"); +}); + +addMessageListener("sync-message", () => { + return "Received a synchronous message."; +}); diff --git a/testing/mochitest/tests/Harness_sanity/empty.js b/testing/mochitest/tests/Harness_sanity/empty.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/empty.js diff --git a/testing/mochitest/tests/Harness_sanity/file_SpecialPowersFrame1.html b/testing/mochitest/tests/Harness_sanity/file_SpecialPowersFrame1.html new file mode 100644 index 0000000000..f6d7046e9a --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/file_SpecialPowersFrame1.html @@ -0,0 +1,14 @@ +<html> + <head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <div id="content" style="display: none"> + <script type="text/javascript"> + is(SpecialPowers.sanityCheck(), "foo", "Check Special Powers in iframe"); + </script> + </div> + </body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/file_spawn.html b/testing/mochitest/tests/Harness_sanity/file_spawn.html new file mode 100644 index 0000000000..b34317b024 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/file_spawn.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title></title> +</head> +<body> + <span id="span">Hello there.</span> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/mochitest.toml b/testing/mochitest/tests/Harness_sanity/mochitest.toml new file mode 100644 index 0000000000..a910407dc8 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/mochitest.toml @@ -0,0 +1,75 @@ +[DEFAULT] + +["test_SimpletestGetTestFileURL.html"] + +["test_SpecialPowersExtension.html"] + +["test_SpecialPowersExtension2.html"] +support-files = "file_SpecialPowersFrame1.html" + +["test_SpecialPowersLoadChromeScript.html"] +support-files = "SpecialPowersLoadChromeScript.js" + +["test_SpecialPowersLoadChromeScript_function.html"] + +["test_SpecialPowersLoadPrivilegedScript.html"] + +["test_SpecialPowersPushPermissions.html"] +support-files = "specialPowers_framescript.js" + +["test_SpecialPowersPushPrefEnv.html"] + +["test_SpecialPowersSandbox.html"] + +["test_SpecialPowersSpawn.html"] +support-files = "file_spawn.html" + +["test_SpecialPowersSpawnChrome.html"] + +["test_TestsRunningAfterSimpleTestFinish.html"] +skip-if = ["true"] #depends on fix for bug 1048446 + +["test_bug649012.html"] + +["test_createFiles.html"] + +["test_getweakmapkeys.html"] + +["test_sanity.html"] + +["test_sanityEventUtils.html"] +skip-if = [ + "verify && (os == 'win')", # bug 688052 + "fission && xorigin", # Bug 1716411 - New fission platform triage +] + +["test_sanityException.html"] + +["test_sanityException2.html"] + +["test_sanityParams.html"] + +["test_sanityRegisteredServiceWorker.html"] +support-files = "empty.js" + +["test_sanityRegisteredServiceWorker2.html"] +skip-if = [ + "verify", +] +support-files = "empty.js" + +["test_sanitySimpletest.html"] + +["test_sanityWindowSnapshot.html"] + +["test_sanity_cleanup.html"] + +["test_sanity_cleanup2.html"] + +["test_sanity_manifest.html"] +fail-if = ["true"] + +["test_sanity_manifest_pf.html"] +fail-if = ["true"] + +["test_sanity_waitForCondition.html"] diff --git a/testing/mochitest/tests/Harness_sanity/specialPowers_framescript.js b/testing/mochitest/tests/Harness_sanity/specialPowers_framescript.js new file mode 100644 index 0000000000..efc017099b --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/specialPowers_framescript.js @@ -0,0 +1,13 @@ +/* eslint-env mozilla/chrome-script */ + +var permChangedObs = { + observe(subject, topic, data) { + if (topic == "perm-changed") { + var permission = subject.QueryInterface(Ci.nsIPermission); + var msg = { op: data, type: permission.type }; + sendAsyncMessage("perm-changed", msg); + } + }, +}; + +Services.obs.addObserver(permChangedObs, "perm-changed"); diff --git a/testing/mochitest/tests/Harness_sanity/test_SimpletestGetTestFileURL.html b/testing/mochitest/tests/Harness_sanity/test_SimpletestGetTestFileURL.html new file mode 100644 index 0000000000..ef368c0ab5 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SimpletestGetTestFileURL.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers extension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var filename = "MyTestDataFile.txt"; +var url = SimpleTest.getTestFileURL(filename); +is(url, document.location.href.replace(/test_SimpletestGetTestFileURL\.html.*/, filename)); + +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersExtension.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersExtension.html new file mode 100644 index 0000000000..34b75933a0 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersExtension.html @@ -0,0 +1,198 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers extension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="starttest();"> + +<div id="content" style="display: none"> + <canvas id="testcanvas" width="200" height="200"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var eventCount = 0; +function testEventListener(e) { + ++eventCount; +} + +function testEventListener2(e) { + ++eventCount; +} + +function dispatchTestEvent() { + var e = document.createEvent("Event"); + e.initEvent("TestEvent", true, true); + window.dispatchEvent(e); +} + +dump("\nSPECIALPTEST:::Test script loaded " + (new Date).getTime() + "\n"); +SimpleTest.waitForExplicitFinish(); +var startTime = new Date(); +async function starttest(){ + dump("\nSPECIALPTEST:::Test script running after load " + (new Date).getTime() + "\n"); + + /** Test for SpecialPowers extension **/ + is(SpecialPowers.sanityCheck(), "foo", "check to see whether the Special Powers extension is installed."); + + // Test a sync call into chrome + await SpecialPowers.setBoolPref('extensions.checkCompatibility', true); + is(SpecialPowers.getBoolPref('extensions.checkCompatibility'), true, "Check to see if we can set a preference properly"); + await SpecialPowers.clearUserPref('extensions.checkCompatibility'); + + // Test a int pref + await SpecialPowers.setIntPref('extensions.foobar', 42); + is(SpecialPowers.getIntPref('extensions.foobar'), 42, "Check int pref"); + await SpecialPowers.clearUserPref('extensions.foobar'); + + // Test a string pref + await SpecialPowers.setCharPref("extensions.foobaz", "hi there"); + is(SpecialPowers.getCharPref("extensions.foobaz"), "hi there", "Check string pref"); + await SpecialPowers.clearUserPref("extensions.foobaz"); + + // Test an invalid pref + var retVal = null; + // eslint-disable-next-line mozilla/use-default-preference-values + try { + retVal = SpecialPowers.getBoolPref('extensions.checkCompat0123456789'); + } catch (ex) { + retVal = ex; + } + is(retVal.result, SpecialPowers.Cr.NS_ERROR_UNEXPECTED, + "received an exception trying to get an unset preference value"); + + SpecialPowers.addChromeEventListener("TestEvent", testEventListener, true, true); + SpecialPowers.addChromeEventListener("TestEvent", testEventListener2, true, false); + dispatchTestEvent(); + is(eventCount, 1, "Should have got an event!"); + + SpecialPowers.removeChromeEventListener("TestEvent", testEventListener, true); + SpecialPowers.removeChromeEventListener("TestEvent", testEventListener2, true); + dispatchTestEvent(); + is(eventCount, 1, "Shouldn't have got an event!"); + + // Test Complex Pref - TODO: Without chrome access, I don't know how you'd actually + // set this preference since you have to create an XPCOM object. + // Leaving untested for now. + + // Test a DOMWindowUtils method and property + is(SpecialPowers.DOMWindowUtils.getClassName(window), "Proxy"); + is(SpecialPowers.DOMWindowUtils.docCharsetIsForced, false); + + // QueryInterface and getPrivilegedProps tests + is(SpecialPowers.can_QI(SpecialPowers), false); + let doc = SpecialPowers.wrap(document); + is(SpecialPowers.getPrivilegedProps(doc, "baseURIObject.fileName"), null, + "Should not have a fileName property yet"); + let uri = SpecialPowers.getPrivilegedProps(doc, "baseURIObject"); + ok(SpecialPowers.can_QI(uri)); + ok(SpecialPowers.do_QueryInterface(uri, "nsIURL")); + is(SpecialPowers.getPrivilegedProps(doc, "baseURIObject.fileName"), + "test_SpecialPowersExtension.html", + "Should have a fileName property now"); + + //try to run garbage collection + SpecialPowers.gc(); + + // + // Test the SpecialPowers wrapper. + // + + let fp = SpecialPowers.Cc["@mozilla.org/filepicker;1"].createInstance(SpecialPowers.Ci.nsIFilePicker); + is(fp.mode, SpecialPowers.Ci.nsIFilePicker.modeOpen, "Should be able to get props off privileged objects"); + var testURI = SpecialPowers.Cc['@mozilla.org/network/standard-url-mutator;1'] + .createInstance(SpecialPowers.Ci.nsIURIMutator) + .setSpec("http://www.foobar.org/") + .finalize(); + is(testURI.spec, "http://www.foobar.org/", "Getters/Setters should work correctly"); + is(SpecialPowers.wrap(document).getElementsByTagName('details').length, 0, "Should work with proxy-based DOM bindings."); + + // Play with the window object. + var docShell = SpecialPowers.wrap(window).docShell; + ok(docShell.browsingContext, "Able to pull properties off of docshell!"); + + // Make sure Xray-wrapped functions work. + try { + SpecialPowers.wrap(SpecialPowers.Components).ID('{00000000-0000-0000-0000-000000000000}'); + ok(true, "Didn't throw"); + } + catch (e) { + ok(false, "Threw while trying to call Xray-wrapped function."); + } + + // Check constructors. + var BinaryInputStream = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/binaryinputstream;1"); + var bis = new BinaryInputStream(); + ok(/nsISupports/.exec(bis.toString()), "Should get the proper object out of the constructor"); + function TestConstructor() { + SpecialPowers.wrap(this).foo = 2; + } + var WrappedConstructor = SpecialPowers.wrap(TestConstructor); + is((new WrappedConstructor()).foo, 2, "JS constructors work properly when wrapped"); + + // Try messing around with QuickStubbed getters/setters and make sure the wrapper deals. + var ctx = SpecialPowers.wrap(document).getElementById('testcanvas').getContext('2d'); + var pixels = ctx.getImageData(0,0,1,1); + try { + pixels.data; + ok(true, "Didn't throw getting quickstubbed accessor prop from proto"); + } + catch (e) { + ok(false, "Threw while getting quickstubbed accessor prop from proto"); + } + + // Check functions that return null. + var returnsNull = function() { return null; } + is(SpecialPowers.wrap(returnsNull)(), null, "Should be able to handle functions that return null."); + + // Check a function that throws. + var thrower = function() { throw new Error('hah'); } + try { + SpecialPowers.wrap(thrower)(); + ok(false, "Should have thrown"); + } catch (e) { + ok(SpecialPowers.isWrapper(e), "Exceptions should be wrapped for call"); + is(e.message, 'hah', "Correct message"); + } + try { + var ctor = SpecialPowers.wrap(thrower); + new ctor(); + ok(false, "Should have thrown"); + } catch (e) { + ok(SpecialPowers.isWrapper(e), "Exceptions should be wrapped for construct"); + is(e.message, 'hah', "Correct message"); + } + + // Play around with a JS object to check the non-xray path. + var noxray_proto = {a: 3, b: 12}; + var noxray = {a: 5, c: 32}; + noxray.__proto__ = noxray_proto; + var noxray_wrapper = SpecialPowers.wrap(noxray); + is(noxray_wrapper.c, 32, "Regular properties should work."); + is(noxray_wrapper.a, 5, "Shadow properties should work."); + is(noxray_wrapper.b, 12, "Proto properties should work."); + noxray.b = 122; + is(noxray_wrapper.b, 122, "Should be able to shadow."); + + // Try setting file input values via an Xray wrapper. + SpecialPowers.wrap(document).title = "foo"; + is(document.title, "foo", "Set property correctly on Xray-wrapped DOM object"); + is(SpecialPowers.wrap(document).URI, document.URI, "Got property correctly on Xray-wrapped DOM object"); + + info("\nProfile::SpecialPowersRunTime: " + (new Date() - startTime) + "\n"); + + // bug 855192 + ok(SpecialPowers.MockPermissionPrompt, "check mock permission prompt"); + + // Set a pref using pushPrefEnv to make sure that flushPrefEnv is + // automatically called before we invoke + // test_SpecialPowersExtension2.html. + SpecialPowers.pushPrefEnv({set: [['testing.some_arbitrary_pref', true]]}, + function() { SimpleTest.finish(); }); +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersExtension2.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersExtension2.html new file mode 100644 index 0000000000..1adee718f1 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersExtension2.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers extension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<div id="content" class="testbody"> + <script type="text/javascript"> + dump("\nSPECIALPTEST2:::Loading test2 file now " + (new Date).getTime() + "\n"); + is(SpecialPowers.sanityCheck(), "foo", "Special Powers top level"); + ok(!SpecialPowers.Services.prefs.prefHasUserValue('testing.some_arbitrary_pref'), + "should not retain pref from previous test"); + </script> + <iframe id="frame1" src="file_SpecialPowersFrame1.html"> + </iframe> +</div> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript.html new file mode 100644 index 0000000000..7242bd19f5 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers.loadChromeScript</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var url = SimpleTest.getTestFileURL("SpecialPowersLoadChromeScript.js"); +var script = SpecialPowers.loadChromeScript(url); + +var MESSAGE = { bar: true }; +script.addMessageListener("bar", function (message) { + is(JSON.stringify(message), JSON.stringify(MESSAGE), + "received back message from the chrome script"); + + checkAssert(); +}); + +function checkAssert() { + script.sendAsyncMessage("valid-assert"); + script.addMessageListener("valid-assert-done", endOfFirstTest); +} + +var script2; + +function endOfFirstTest() { + script.destroy(); + + // wantGlobalProperties should add the specified properties to the sandbox + // that is used to run the chrome script. + script2 = SpecialPowers.loadChromeScript(_ => { + /* eslint-env mozilla/chrome-script */ + addMessageListener("valid-assert", function (message) { + assert.equal(typeof XMLHttpRequest, "function", "XMLHttpRequest is defined"); + assert.equal(typeof CSS, "undefined", "CSS is not defined"); + sendAsyncMessage("valid-assert-done"); + }); + }, { wantGlobalProperties: ["ChromeUtils", "XMLHttpRequest"] }); + + script2.sendAsyncMessage("valid-assert"); + script2.addMessageListener("valid-assert-done", endOfTest); + +} + +async function endOfTest() { + is(await script.sendQuery("sync-message"), "Received a synchronous message.", + "Check sync return value"); + + script2.destroy(); + SimpleTest.finish(); +} + +script.sendAsyncMessage("foo", MESSAGE); + +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript_function.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript_function.html new file mode 100644 index 0000000000..af31d7b25a --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript_function.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers.loadChromeScript</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + + +var script = SpecialPowers.loadChromeScript(function loadChromeScriptTest() { + /* eslint-env mozilla/chrome-script */ + // Copied from SpecialPowersLoadChromeScript.js + + // Just receive 'foo' message and forward it back + // as 'bar' message + addMessageListener("foo", function (message) { + sendAsyncMessage("bar", message); + }); + + addMessageListener("valid-assert", function (message) { + assert.ok(true, "valid assertion"); + assert.equal(1, 1, "another valid assertion"); + sendAsyncMessage("valid-assert-done"); + }); + + addMessageListener("sync-message", () => { + return "Received a synchronous message."; + }); +}); + +var MESSAGE = { bar: true }; +script.addMessageListener("bar", function (message) { + is(JSON.stringify(message), JSON.stringify(MESSAGE), + "received back message from the chrome script"); + + checkAssert(); +}); + +function checkAssert() { + script.sendAsyncMessage("valid-assert"); + script.addMessageListener("valid-assert-done", endOfTest); +} + +async function endOfTest() { + is(await script.sendQuery("sync-message"), "Received a synchronous message.", + "Check sync return value"); + + script.destroy(); + SimpleTest.finish(); +} + +script.sendAsyncMessage("foo", MESSAGE); + +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadPrivilegedScript.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadPrivilegedScript.html new file mode 100644 index 0000000000..6294c44dfe --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadPrivilegedScript.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers.loadChromeScript</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +/* eslint-disable mozilla/use-services */ +function loadPrivilegedScriptTest() { + function isMainProcess() { + return Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsIXULRuntime). + processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + } + port.postMessage({'isMainProcess': isMainProcess()}); +} + +var contentProcessType = SpecialPowers.isMainProcess(); +var port; +try { + port = SpecialPowers.loadPrivilegedScript(loadPrivilegedScriptTest.toString()); +} catch (e) { + ok(false, "loadPrivilegedScript shoulde not throw"); +} +port.onmessage = (e) => { + is(contentProcessType, e.data.isMainProcess, "content and the script should be in the same process"); + SimpleTest.finish(); +}; +</script> diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersPushPermissions.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersPushPermissions.html new file mode 100644 index 0000000000..3aeebb22ff --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersPushPermissions.html @@ -0,0 +1,232 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers extension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="starttest();"> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +const ALLOW_ACTION = SpecialPowers.Ci.nsIPermissionManager.ALLOW_ACTION; +const DENY_ACTION = SpecialPowers.Ci.nsIPermissionManager.DENY_ACTION; +const UNKNOWN_ACTION = SpecialPowers.Ci.nsIPermissionManager.UNKNOWN_ACTION; +const PROMPT_ACTION = SpecialPowers.Ci.nsIPermissionManager.PROMPT_ACTION; +const ACCESS_SESSION = SpecialPowers.Ci.nsICookiePermission.ACCESS_SESSION; + +const EXPIRE_TIME = SpecialPowers.Ci.nsIPermissionManager.EXPIRE_TIME; +// expire Setting: +// start expire time point +// ----|------------------------|----------- +// <------------------------> +// PERIOD +var start; +// PR_Now() that called in PermissionManager to get the system time +// is sometimes 100ms~600s more than Date.now() on Android 4.3 API11. +// Thus, the PERIOD should be larger than 600ms in this test. +const PERIOD = 900; +var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('specialPowers_framescript.js')); +SimpleTest.requestFlakyTimeout("untriaged"); + +function starttest(){ + SpecialPowers.addPermission("pPROMPT", PROMPT_ACTION, document); + SpecialPowers.addPermission("pALLOW", ALLOW_ACTION, document); + SpecialPowers.addPermission("pDENY", DENY_ACTION, document); + SpecialPowers.addPermission("pREMOVE", ALLOW_ACTION, document); + SpecialPowers.addPermission("pSESSION", ACCESS_SESSION, document); + + setTimeout(test1, 0); +} + +SimpleTest.waitForExplicitFinish(); + +async function test1() { + if (!await SpecialPowers.testPermission('pALLOW', ALLOW_ACTION, document)) { + dump('/**** allow not set ****/\n'); + setTimeout(test1, 0); + } else if (!await SpecialPowers.testPermission('pDENY', DENY_ACTION, document)) { + dump('/**** deny not set ****/\n'); + setTimeout(test1, 0); + } else if (!await SpecialPowers.testPermission('pPROMPT', PROMPT_ACTION, document)) { + dump('/**** prompt not set ****/\n'); + setTimeout(test1, 0); + } else if (!await SpecialPowers.testPermission('pREMOVE', ALLOW_ACTION, document)) { + dump('/**** remove not set ****/\n'); + setTimeout(test1, 0); + } else if (!await SpecialPowers.testPermission('pSESSION', ACCESS_SESSION, document)) { + dump('/**** ACCESS_SESSION not set ****/\n'); + setTimeout(test1, 0); + } else { + test2(); + } +} + +async function test2() { + ok(await SpecialPowers.testPermission('pUNKNOWN', UNKNOWN_ACTION, document), 'pUNKNOWN value should have UNKOWN permission'); + SpecialPowers.pushPermissions([ + {'type': 'pUNKNOWN', 'allow': true, 'context': document}, + {'type': 'pALLOW', 'allow': false, 'context': document}, + {'type': 'pDENY', 'allow': true, 'context': document}, + {'type': 'pPROMPT', 'allow': true, 'context': document}, + {'type': 'pSESSION', 'allow': true, 'context': document}, + {'type': 'pREMOVE', 'remove': true, 'context': document}, + ], test3); +} + +async function test3() { + ok(await SpecialPowers.testPermission('pUNKNOWN', ALLOW_ACTION, document), 'pUNKNOWN value should have ALLOW permission'); + ok(await SpecialPowers.testPermission('pPROMPT', ALLOW_ACTION, document), 'pPROMPT value should have ALLOW permission'); + ok(await SpecialPowers.testPermission('pALLOW', DENY_ACTION, document), 'pALLOW should have DENY permission'); + ok(await SpecialPowers.testPermission('pDENY', ALLOW_ACTION, document), 'pDENY should have ALLOW permission'); + ok(await SpecialPowers.testPermission('pREMOVE', UNKNOWN_ACTION, document), 'pREMOVE should have REMOVE permission'); + ok(await SpecialPowers.testPermission('pSESSION', ALLOW_ACTION, document), 'pSESSION should have ALLOW permission'); + + // only pPROMPT (last one) is different, the other stuff is just to see if it doesn't cause test failures + SpecialPowers.pushPermissions([ + {'type': 'pUNKNOWN', 'allow': true, 'context': document}, + {'type': 'pALLOW', 'allow': false, 'context': document}, + {'type': 'pDENY', 'allow': true, 'context': document}, + {'type': 'pPROMPT', 'allow': false, 'context': document}, + {'type': 'pREMOVE', 'remove': true, 'context': document}, + ], test3b); +} + +async function test3b() { + ok(await SpecialPowers.testPermission('pPROMPT', DENY_ACTION, document), 'pPROMPT value should have DENY permission'); + SpecialPowers.pushPermissions([ + {'type': 'pUNKNOWN', 'allow': DENY_ACTION, 'context': document}, + {'type': 'pALLOW', 'allow': PROMPT_ACTION, 'context': document}, + {'type': 'pDENY', 'allow': PROMPT_ACTION, 'context': document}, + {'type': 'pPROMPT', 'allow': ALLOW_ACTION, 'context': document}, + ], test4); +} + +async function test4() { + ok(await SpecialPowers.testPermission('pUNKNOWN', DENY_ACTION, document), 'pUNKNOWN value should have DENY permission'); + ok(await SpecialPowers.testPermission('pPROMPT', ALLOW_ACTION, document), 'pPROMPT value should have ALLOW permission'); + ok(await SpecialPowers.testPermission('pALLOW', PROMPT_ACTION, document), 'pALLOW should have PROMPT permission'); + ok(await SpecialPowers.testPermission('pDENY', PROMPT_ACTION, document), 'pDENY should have PROMPT permission'); + //this should reset all the permissions to before all the pushPermissions calls + SpecialPowers.flushPermissions(test5); +} + +async function test5() { + ok(await SpecialPowers.testPermission('pUNKNOWN', UNKNOWN_ACTION, document), 'pUNKNOWN should have UNKNOWN permission'); + ok(await SpecialPowers.testPermission('pALLOW', ALLOW_ACTION, document), 'pALLOW should have ALLOW permission'); + ok(await SpecialPowers.testPermission('pDENY', DENY_ACTION, document), 'pDENY should have DENY permission'); + ok(await SpecialPowers.testPermission('pPROMPT', PROMPT_ACTION, document), 'pPROMPT should have PROMPT permission'); + ok(await SpecialPowers.testPermission('pREMOVE', ALLOW_ACTION, document), 'pREMOVE should have ALLOW permission'); + ok(await SpecialPowers.testPermission('pSESSION', ACCESS_SESSION, document), 'pSESSION should have ACCESS_SESSION permission'); + + SpecialPowers.removePermission("pPROMPT", document); + SpecialPowers.removePermission("pALLOW", document); + SpecialPowers.removePermission("pDENY", document); + SpecialPowers.removePermission("pREMOVE", document); + SpecialPowers.removePermission("pSESSION", document); + + setTimeout(test6, 0); +} + +async function test6() { + if (!await SpecialPowers.testPermission('pALLOW', UNKNOWN_ACTION, document)) { + dump('/**** allow still set ****/\n'); + setTimeout(test6, 0); + } else if (!await SpecialPowers.testPermission('pDENY', UNKNOWN_ACTION, document)) { + dump('/**** deny still set ****/\n'); + setTimeout(test6, 0); + } else if (!await SpecialPowers.testPermission('pPROMPT', UNKNOWN_ACTION, document)) { + dump('/**** prompt still set ****/\n'); + setTimeout(test6, 0); + } else if (!await SpecialPowers.testPermission('pREMOVE', UNKNOWN_ACTION, document)) { + dump('/**** remove still set ****/\n'); + setTimeout(test6, 0); + } else if (!await SpecialPowers.testPermission('pSESSION', UNKNOWN_ACTION, document)) { + dump('/**** pSESSION still set ****/\n'); + setTimeout(test6, 0); + } else { + test7(); + } +} + +function test7() { + afterPermissionChanged('pEXPIRE', 'deleted', test8); + afterPermissionChanged('pEXPIRE', 'added', permissionPollingCheck); + start = Number(Date.now()); + SpecialPowers.addPermission('pEXPIRE', + true, + document, + EXPIRE_TIME, + (start + PERIOD + getPlatformInfo().timeCompensation)); +} + +function test8() { + afterPermissionChanged('pEXPIRE', 'deleted', SimpleTest.finish); + afterPermissionChanged('pEXPIRE', 'added', permissionPollingCheck); + start = Number(Date.now()); + SpecialPowers.pushPermissions([ + { 'type': 'pEXPIRE', + 'allow': true, + 'expireType': EXPIRE_TIME, + 'expireTime': (start + PERIOD + getPlatformInfo().timeCompensation), + 'context': document + }], function() { + info("Wait for permission-changed signal!"); + } + ); +} + +function afterPermissionChanged(type, op, callback) { + // handle the message from specialPowers_framescript.js + gScript.addMessageListener('perm-changed', function onChange(msg) { + if (msg.type == type && msg.op == op) { + gScript.removeMessageListener('perm-changed', onChange); + callback(); + } + }); +} + +async function permissionPollingCheck() { + var now = Number(Date.now()); + if (now < (start + PERIOD)) { + if (await SpecialPowers.testPermission('pEXPIRE', ALLOW_ACTION, document)) { + // To make sure that permission will be expired in next round, + // the next permissionPollingCheck calling will be fired 100ms later after + // permission is out-of-period. + setTimeout(permissionPollingCheck, PERIOD + 100); + return; + } + + errorHandler('unexpired permission should be allowed!'); + } + + // The permission is already expired! + if (await SpecialPowers.testPermission('pEXPIRE', ALLOW_ACTION, document)) { + errorHandler('expired permission should be removed!'); + } +} + +function getPlatformInfo() { + var version = SpecialPowers.Services.sysinfo.getProperty('version'); + version = parseFloat(version); + + // PR_Now() that called in PermissionManager to get the system time and + // Date.now() are out of sync on win32 platform(XP/win7). The PR_Now() is + // 15~20ms less than Date.now(). Unfortunately, this time skew can't be + // avoided, so it needs to add a time buffer to compensate. + // Version 5.1 is win XP, 6.1 is win7 + if (navigator.platform.startsWith('Win32') && (version <= 6.1)) { + return { platform: "Win32", timeCompensation: -100 }; + } + + return { platform: "NoMatter", timeCompensation: 0 }; +} + +function errorHandler(msg) { + ok(false, msg); + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersPushPrefEnv.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersPushPrefEnv.html new file mode 100644 index 0000000000..727f17349b --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersPushPrefEnv.html @@ -0,0 +1,232 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers extension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="starttest();"> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +async function starttest() { + try { + await SpecialPowers.setBoolPref("test.bool", 1); + } catch(e) { + await SpecialPowers.setBoolPref("test.bool", true); + } + try { + await SpecialPowers.setIntPref("test.int", true); + } catch(e) { + await SpecialPowers.setIntPref("test.int", 1); + } + await SpecialPowers.setCharPref("test.char", 'test'); + await SpecialPowers.setBoolPref("test.cleanup", false); + + setTimeout(test1, 0, 0); +} + +SimpleTest.waitForExplicitFinish(); + +function test1(aCount) { + if (aCount >= 20) { + ok(false, "Too many times attempting to set pref, aborting"); + SimpleTest.finish(); + return; + } + + try { + is(SpecialPowers.getBoolPref('test.bool'), true, 'test.bool should be true'); + } catch(e) { + setTimeout(test1, 0, ++aCount); + return; + } + + try { + is(SpecialPowers.getIntPref('test.int'), 1, 'test.int should be 1'); + } catch(e) { + setTimeout(test1, 0, ++aCount); + return; + } + + try { + is(SpecialPowers.getCharPref('test.char'), 'test', 'test.char should be test'); + } catch(e) { + setTimeout(test1, 0, ++aCount); + return; + } + + test2(); +} + +function test2() { + // test non-changing values + SpecialPowers.pushPrefEnv({"set": [["test.bool", true], ["test.int", 1], ["test.char", "test"]]}, test3); +} + +function test3() { + // test changing char pref using the Promise + is(SpecialPowers.getBoolPref('test.bool'), true, 'test.bool should be true'); + is(SpecialPowers.getIntPref('test.int'), 1, 'test.int should be 1'); + is(SpecialPowers.getCharPref('test.char'), 'test', 'test.char should be test'); + SpecialPowers.pushPrefEnv({"set": [["test.bool", true], ["test.int", 1], ["test.char", "test2"]]}).then(test4); +} + +function test4() { + // test changing all values and adding test.char2 pref + is(SpecialPowers.getCharPref('test.char'), 'test2', 'test.char should be test2'); + SpecialPowers.pushPrefEnv({"set": [["test.bool", false], ["test.int", 10], ["test.char", "test2"], ["test.char2", "test"]]}, test5); +} + +function test5() { + // test flushPrefEnv + is(SpecialPowers.getBoolPref('test.bool'), false, 'test.bool should be false'); + is(SpecialPowers.getIntPref('test.int'), 10, 'test.int should be 10'); + is(SpecialPowers.getCharPref('test.char'), 'test2', 'test.char should be test2'); + is(SpecialPowers.getCharPref('test.char2'), 'test', 'test.char2 should be test'); + SpecialPowers.flushPrefEnv(test6); +} + +function test6() { + // test clearing prefs + is(SpecialPowers.getBoolPref('test.bool'), true, 'test.bool should be true'); + is(typeof SpecialPowers.getBoolPref('test.bool'), typeof true, 'test.bool should be boolean'); + is(SpecialPowers.getIntPref('test.int'), 1, 'test.int should be 1'); + is(typeof SpecialPowers.getIntPref('test.int'), typeof 1, 'test.int should be integer'); + is(SpecialPowers.getCharPref('test.char'), 'test', 'test.char should be test'); + is(typeof SpecialPowers.getCharPref('test.char'), typeof 'test', 'test.char should be String'); + try { + SpecialPowers.getCharPref('test.char2'); + ok(false, 'This ok should not be reached!'); + } catch(e) { + ok(true, 'getCharPref("test.char2") should throw'); + } + SpecialPowers.pushPrefEnv({"clear": [["test.bool"], ["test.int"], ["test.char"], ["test.char2"]]}, test6b); +} + +function test6b() { + // test if clearing another time doesn't cause issues + SpecialPowers.pushPrefEnv({"clear": [["test.bool"], ["test.int"], ["test.char"], ["test.char2"]]}, test7); +} + +function test7() { + try { + SpecialPowers.getBoolPref('test.bool'); + ok(false, 'This ok should not be reached!'); + } catch(e) { + ok(true, 'getBoolPref("test.bool") should throw'); + } + + try { + SpecialPowers.getIntPref('test.int'); + ok(false, 'This ok should not be reached!'); + } catch(e) { + ok(true, 'getIntPref("test.int") should throw'); + } + + try { + SpecialPowers.getCharPref('test.char'); + ok(false, 'This ok should not be reached!'); + } catch(e) { + ok(true, 'getCharPref("test.char") should throw'); + } + + try { + SpecialPowers.getCharPref('test.char2'); + ok(false, 'This ok should not be reached!'); + } catch(e) { + ok(true, 'getCharPref("test.char2") should throw'); + } + + SpecialPowers.flushPrefEnv().then(test8); +} + +function test8() { + is(SpecialPowers.getBoolPref('test.bool'), true, 'test.bool should be true'); + is(typeof SpecialPowers.getBoolPref('test.bool'), typeof true, 'test.bool should be boolean'); + is(SpecialPowers.getIntPref('test.int'), 1, 'test.int should be 1'); + is(typeof SpecialPowers.getIntPref('test.int'), typeof 1, 'test.int should be integer'); + is(SpecialPowers.getCharPref('test.char'), 'test', 'test.char should be test'); + is(typeof SpecialPowers.getCharPref('test.char'), typeof 'test', 'test.char should be String'); + try { + SpecialPowers.getCharPref('test.char2'); + ok(false, 'This ok should not be reached!'); + } catch(e) { + ok(true, 'getCharPref("test.char2") should throw'); + } + SpecialPowers.clearUserPref("test.bool"); + SpecialPowers.clearUserPref("test.int"); + SpecialPowers.clearUserPref("test.char"); + setTimeout(test9, 0, 0); +} + +function test9(aCount) { + if (aCount >= 20) { + ok(false, "Too many times attempting to set pref, aborting"); + SimpleTest.finish(); + return; + } + + try { + SpecialPowers.getBoolPref('test.bool'); + setTimeout(test9, 0, ++aCount); + } catch(e) { + test10(0); + } +} + +function test10(aCount) { + if (aCount >= 20) { + ok(false, "Too many times attempting to set pref, aborting"); + SimpleTest.finish(); + return; + } + + try { + SpecialPowers.getIntPref('test.int'); + setTimeout(test10, 0, ++aCount); + } catch(e) { + test11(0); + } +} + +function test11(aCount) { + if (aCount >= 20) { + ok(false, "Too many times attempting to set pref, aborting"); + SimpleTest.finish(); + return; + } + + try { + SpecialPowers.getCharPref('test.char'); + setTimeout(test11, 0, ++aCount); + } catch(e) { + test12(); + } +} + +function test12() { + // Set test.cleanup to true via pushPrefEnv, while its default value is false. + SpecialPowers.pushPrefEnv({"set": [["test.cleanup", true]]}, () => { + // Update the preference manually back to its original value. + SpecialPowers.setBoolPref("test.cleanup", false); + setTimeout(test13, 0); + }); +} + +function test13() { + // Try to flush preferences. Since test.cleanup has manually been set to false, there + // will not be any visible update. Check that the flush does not timeout. + SpecialPowers.flushPrefEnv(() => { + ok(true, 'flushPrefEnv did not time out'); + is(SpecialPowers.getBoolPref('test.cleanup'), false, 'test.cleanup should be false'); + SimpleTest.finish(); + }); +} + +// todo - test non-changing values, test complex values, test mixing of pushprefEnv 'set' and 'clear' +// When bug 776424 gets fixed, getPref doesn't throw anymore, so this test would have to be changed accordingly +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.html new file mode 100644 index 0000000000..44efb70eeb --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.html @@ -0,0 +1,153 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers sandboxes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<iframe id="iframe"></iframe> + +<script> +/** + * Tests that the shared sandbox functionality for cross-process script + * execution works as expected. In particular, ensures that Assert methods + * report the correct diagnostics in the caller scope. + */ + +/* globals SpecialPowers, Assert */ + +async function interceptDiagnostics(func) { + let originalRecord = SimpleTest.record; + try { + let diags = []; + + SimpleTest.record = (condition, name, diag, stack) => { + diags.push({condition, name, diag, stack}); + }; + + await func(); + + return diags; + } finally { + SimpleTest.record = originalRecord; + } +} + +add_task(async function() { + const frameSrc = "https://example.com/tests/testing/mochitest/tests/Harness_sanity/file_spawn.html"; + const subframeSrc = "https://example.org/tests/testing/mochitest/tests/Harness_sanity/file_spawn.html"; + + let frame = document.getElementById("iframe"); + frame.src = frameSrc; + + await new Promise(resolve => { + frame.addEventListener("load", resolve, {once: true}); + }); + + let expected = [ + [false, "Thing - 1 == 2", "got 1, expected 2 (operator ==)"], + [true, "Hmm - 1 == 1", undefined], + [true, "Yay. - true == true", undefined], + [false, "Boo!. - false == true", "got false, expected true (operator ==)"], + [false, "Missing expected exception Rej_bad", "got null, expected /./ (operator undefined)"], + [true, "Rej_ok", undefined], + ]; + + // Test that a representative variety of assertions work as expected, and + // trigger the expected calls to the harness's reporting function. + // + // Note: Assert.sys.mjs has its own tests, and defers all of its reporting to a + // single reporting function, so we don't need to test it comprehensively. We + // just need to make sure that the general functionality works as expected. + let tests = { + "SpecialPowers.spawn": () => { + return SpecialPowers.spawn(frame, [], async () => { + Assert.equal(1, 2, "Thing"); + Assert.equal(1, 1, "Hmm"); + Assert.ok(true, "Yay."); + Assert.ok(false, "Boo!."); + await Assert.rejects(Promise.resolve(), /./, "Rej_bad"); + await Assert.rejects(Promise.reject(new Error("k")), /k/, "Rej_ok"); + }); + }, + "SpecialPowers.spawn-subframe": () => { + return SpecialPowers.spawn(frame, [subframeSrc], async src => { + let subFrame = this.content.document.createElement("iframe"); + subFrame.src = src; + this.content.document.body.appendChild(subFrame); + + await new Promise(resolve => { + subFrame.addEventListener("load", resolve, { once: true }); + }); + + await SpecialPowers.spawn(subFrame, [], async () => { + Assert.equal(1, 2, "Thing"); + Assert.equal(1, 1, "Hmm"); + Assert.ok(true, "Yay."); + Assert.ok(false, "Boo!."); + await Assert.rejects(Promise.resolve(), /./, "Rej_bad"); + await Assert.rejects(Promise.reject(new Error("k")), /k/, "Rej_ok"); + }); + }); + }, + "SpecialPowers.spawnChrome": () => { + return SpecialPowers.spawnChrome([], async () => { + Assert.equal(1, 2, "Thing"); + Assert.equal(1, 1, "Hmm"); + Assert.ok(true, "Yay."); + Assert.ok(false, "Boo!."); + await Assert.rejects(Promise.resolve(), /./, "Rej_bad"); + await Assert.rejects(Promise.reject(new Error("k")), /k/, "Rej_ok"); + }); + }, + "SpecialPowers.loadChromeScript": async () => { + let script = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + const resultPromise = (async () => { + Assert.equal(1, 2, "Thing"); + Assert.equal(1, 1, "Hmm"); + Assert.ok(true, "Yay."); + Assert.ok(false, "Boo!."); + await Assert.rejects(Promise.resolve(), /./, "Rej_bad"); + await Assert.rejects(Promise.reject(new Error("k")), /k/, "Rej_ok"); + })(); + this.addMessageListener("ping", () => resultPromise); + }); + + await script.sendQuery("ping"); + script.destroy(); + }, + }; + + for (let [name, func] of Object.entries(tests)) { + info(`Starting task: ${name}`); + + let diags = await interceptDiagnostics(func); + + let results = diags.map(diag => [diag.condition, diag.name, diag.diag]); + + isDeeply(results, expected, "Got expected assertions"); + for (let { name: diagName, stack } of diags) { + ok(stack, `Got stack for: ${diagName}`); + // Unlike test_SpecialPowersSandbox.js, expectedFilenamePart does not end + // with ":" (separator between file name and line number). This is because + // xorigin tests run this test file with a query param ("currentTestURL"), + // and the name therefore looks like "test_SpecialPowersSandbox.html?...:" + // instead of "test_SpecialPowersSandbox.html:" + let expectedFilenamePart = "/test_SpecialPowersSandbox.html"; + if (name === "SpecialPowers.loadChromeScript") { + // Unfortunately, the original file name is not included; + // the function name or a dummy value is used instead. + expectedFilenamePart = "loadChromeScript anonymous function>"; + } + if (!stack.includes(expectedFilenamePart)) { + ok(false, `Stack does not contain ${expectedFilenamePart}: ${stack}`); + } + } + } +}); +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.js b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.js new file mode 100644 index 0000000000..60db9440b0 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.js @@ -0,0 +1,165 @@ +"use strict"; + +/* eslint-disable @microsoft/sdl/no-insecure-url */ + +const { XPCShellContentUtils } = ChromeUtils.importESModule( + "resource://testing-common/XPCShellContentUtils.sys.mjs" +); + +XPCShellContentUtils.init(this); + +const HTML = String.raw`<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title></title> +</head> +<body> + <span id="span">Hello there.</span> +</body> +</html>`; + +const server = XPCShellContentUtils.createHttpServer({ + hosts: ["example.com", "example.org"], +}); + +server.registerPathHandler("/", (request, response) => { + response.setHeader("Content-Type", "text/html"); + response.write(HTML); +}); +/** + * Tests that the shared sandbox functionality for cross-process script + * execution works as expected. In particular, ensures that Assert methods + * report the correct diagnostics in the caller scope. + */ + +let scope = this; + +async function interceptDiagnostics(func) { + let originalRecord = scope.do_report_result; + try { + let diags = []; + + scope.do_report_result = (passed, msg, stack) => { + diags.push({ passed, msg, stack }); + }; + + await func(); + + return diags; + } finally { + scope.do_report_result = originalRecord; + } +} + +add_task(async function () { + const frameSrc = "http://example.com/"; + const subframeSrc = "http://example.org/"; + + let page = await XPCShellContentUtils.loadContentPage(frameSrc, { + remote: true, + remoteSubframes: true, + }); + + let { SpecialPowers, browsingContext } = page; + + let expected = [ + [false, "Thing - 1 == 2"], + [true, "Hmm - 1 == 1"], + [true, "Yay. - true == true"], + [false, "Boo!. - false == true"], + [false, "Missing expected exception Rej_bad"], + [true, "Rej_ok"], + ]; + + // Test that a representative variety of assertions work as expected, and + // trigger the expected calls to the harness's reporting function. + // + // Note: Assert.sys.mjs has its own tests, and defers all of its reporting to a + // single reporting function, so we don't need to test it comprehensively. We + // just need to make sure that the general functionality works as expected. + let tests = { + "SpecialPowers.spawn": () => { + return SpecialPowers.spawn(browsingContext, [], async () => { + Assert.equal(1, 2, "Thing"); + Assert.equal(1, 1, "Hmm"); + Assert.ok(true, "Yay."); + Assert.ok(false, "Boo!."); + await Assert.rejects(Promise.resolve(), /./, "Rej_bad"); + await Assert.rejects(Promise.reject(new Error("k")), /k/, "Rej_ok"); + }); + }, + "SpecialPowers.spawn-subframe": () => { + return SpecialPowers.spawn(browsingContext, [subframeSrc], async src => { + let subFrame = this.content.document.createElement("iframe"); + subFrame.src = src; + this.content.document.body.appendChild(subFrame); + + await new Promise(resolve => { + subFrame.addEventListener("load", resolve, { once: true }); + }); + + await SpecialPowers.spawn(subFrame, [], async () => { + Assert.equal(1, 2, "Thing"); + Assert.equal(1, 1, "Hmm"); + Assert.ok(true, "Yay."); + Assert.ok(false, "Boo!."); + await Assert.rejects(Promise.resolve(), /./, "Rej_bad"); + await Assert.rejects(Promise.reject(new Error("k")), /k/, "Rej_ok"); + }); + }); + }, + "SpecialPowers.spawnChrome": () => { + return SpecialPowers.spawnChrome([], async () => { + Assert.equal(1, 2, "Thing"); + Assert.equal(1, 1, "Hmm"); + Assert.ok(true, "Yay."); + Assert.ok(false, "Boo!."); + await Assert.rejects(Promise.resolve(), /./, "Rej_bad"); + await Assert.rejects(Promise.reject(new Error("k")), /k/, "Rej_ok"); + }); + }, + "SpecialPowers.loadChromeScript": async () => { + let script = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + const resultPromise = (async () => { + Assert.equal(1, 2, "Thing"); + Assert.equal(1, 1, "Hmm"); + Assert.ok(true, "Yay."); + Assert.ok(false, "Boo!."); + await Assert.rejects(Promise.resolve(), /./, "Rej_bad"); + await Assert.rejects(Promise.reject(new Error("k")), /k/, "Rej_ok"); + })(); + this.addMessageListener("ping", () => resultPromise); + }); + + await script.sendQuery("ping"); + script.destroy(); + }, + }; + + for (let [name, func] of Object.entries(tests)) { + info(`Starting task: ${name}`); + + let diags = await interceptDiagnostics(func); + + let results = diags.map(diag => [diag.passed, diag.msg]); + + deepEqual(results, expected, "Got expected assertions"); + for (let { msg, stack } of diags) { + ok(stack, `Got stack for: ${msg}`); + // Unlike the html version of this test, this one does not include a "/" + // in front of the file name, because somehow Android only includes the + // file name, and not the fuller path. + let expectedFilenamePart = "test_SpecialPowersSandbox.js:"; + if (name === "SpecialPowers.loadChromeScript") { + // Unfortunately, the original file name is not included; + // the function name or a dummy value is used instead. + expectedFilenamePart = "loadChromeScript anonymous function>:"; + } + if (!stack.includes(expectedFilenamePart)) { + ok(false, `Stack does not contain ${expectedFilenamePart}: ${stack}`); + } + } + } +}); diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSpawn.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSpawn.html new file mode 100644 index 0000000000..fe93c1fe83 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSpawn.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers.spawn</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<iframe id="iframe"></iframe> + +<span id="hello">World.</span> + +<script> +/* globals SpecialPowers, content */ + + add_task(async function() { + let frame = document.getElementById("iframe"); + frame.src = "https://example.com/tests/testing/mochitest/tests/Harness_sanity/file_spawn.html"; + + await new Promise(resolve => { + frame.addEventListener("load", resolve, {once: true}); + }); + + let result = await SpecialPowers.spawn(frame, ["#span"], selector => { + let elem = content.document.querySelector(selector); + return elem.textContent; + }); + + is(result, "Hello there.", "Got correct element text from frame"); + + /* eslint-disable no-shadow */ + result = await SpecialPowers.spawn(frame, ["#hello"], selector => { + return SpecialPowers.spawn(content.parent, [selector], selector => { + let elem = content.document.querySelector(selector); + return elem.textContent; + }); + }); + + is(result, "World.", "Got correct element text from frame's window.parent"); + + result = await SpecialPowers.spawn(frame.contentWindow, ["#span"], selector => { + let elem = content.document.querySelector(selector); + return elem.textContent; + }); + + is(result, "Hello there.", "Got correct element text from window proxy"); + + result = await SpecialPowers.spawn(SpecialPowers.getPrivilegedProps(frame, "browsingContext"), + ["#span"], selector => { + let elem = content.document.querySelector(selector); + return elem.textContent; + }); + + is(result, "Hello there.", "Got correct element text from browsing context"); + + let line = 58; // Keep this in sync with the line number where the callback function starts. + let callback = () => { + let e = new Error("Hello."); + return { filename: e.fileName, lineNumber: e.lineNumber }; + }; + + let loc = await SpecialPowers.spawn(frame, [], callback); + is(loc.filename, location.href, "Error should have correct script filename"); + is(loc.lineNumber, line + 2, "Error should have correct script line number"); + }); +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSpawn.js b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSpawn.js new file mode 100644 index 0000000000..b910f94436 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSpawn.js @@ -0,0 +1,68 @@ +"use strict"; + +const { XPCShellContentUtils } = ChromeUtils.importESModule( + "resource://testing-common/XPCShellContentUtils.sys.mjs" +); + +XPCShellContentUtils.init(this); + +const HTML = String.raw`<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title></title> +</head> +<body> + <span id="span">Hello there.</span> +</body> +</html>`; + +const server = XPCShellContentUtils.createHttpServer({ + hosts: ["example.com"], +}); + +server.registerPathHandler("/", (request, response) => { + response.setHeader("Content-Type", "text/html"); + response.write(HTML); +}); + +add_task(async function () { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let page = await XPCShellContentUtils.loadContentPage("http://example.com/", { + remote: true, + remoteSubframes: true, + }); + + let { SpecialPowers, browsingContext } = page; + + let result = await SpecialPowers.spawn( + browsingContext, + ["#span"], + selector => { + let elem = content.document.querySelector(selector); + return elem.textContent; + } + ); + + equal(result, "Hello there.", "Got correct element text from frame"); + + let line = Components.stack.lineNumber + 1; + let callback = () => { + let e = new Error("Hello."); + return { filename: e.fileName, lineNumber: e.lineNumber }; + }; + + let loc = await SpecialPowers.spawn(browsingContext, [], callback); + equal( + loc.filename, + Components.stack.filename, + "Error should have correct script filename" + ); + equal( + loc.lineNumber, + line + 2, + "Error should have correct script line number" + ); + + await page.close(); +}); diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSpawnChrome.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSpawnChrome.html new file mode 100644 index 0000000000..9c3f1ee658 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSpawnChrome.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers.spawnChrome</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script> + add_task(async function() { + let { browsingContextId, innerWindowId } = await SpecialPowers.spawnChrome([12, { b: 42 }], (a, b) => { + Assert.equal(a, 12, "Arg 1"); + Assert.equal(b.b, 42, "Arg 2"); + + Assert.equal(Services.appinfo.processType, + Services.appinfo.PROCESS_TYPE_DEFAULT, + "Task runs in correct process"); + + return { + browsingContextId: browsingContext.id, + innerWindowId: windowGlobalParent.innerWindowId, + }; + }); + + let wgc = SpecialPowers.wrap(window).windowGlobalChild; + is(browsingContextId, wgc.browsingContext.id, "Correct browsing context id"); + is(innerWindowId, wgc.innerWindowId, "Correct inner window id"); + }); +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_TestsRunningAfterSimpleTestFinish.html b/testing/mochitest/tests/Harness_sanity/test_TestsRunningAfterSimpleTestFinish.html new file mode 100644 index 0000000000..aa0a7d39ef --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_TestsRunningAfterSimpleTestFinish.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for whether SimpLeTest.ok after SimpleTest.finish is causing an error to be logged</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<div id="content" class="testbody"> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + addLoadEvent(function() { + ok(true, "This should pass"); + SimpleTest.finish(); + }); + window.onbeforeunload = function() { + ok(true, "This should cause failures in the harness, because it's run after SimpleTest.finish()"); + } + </script> +</div> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_bug649012.html b/testing/mochitest/tests/Harness_sanity/test_bug649012.html new file mode 100644 index 0000000000..8807aa56f3 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_bug649012.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=649012 +--> +<head> + <title>Test for Bug 649012</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=649012">Mozilla Bug 649012</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 649012 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + // Test that setTimeout(f, 0) doesn't raise an error + setTimeout(function() { + // Test that setTimeout(f, t) where t > 0 doesn't raise an error if we've used + // SimpleTest.requestFlakyTimeout + SimpleTest.requestFlakyTimeout("Just testing to make sure things work. I would never do this in real life of course!"); + setTimeout(function() { + SimpleTest.finish(); + }, 1); + }, 0); +}); + +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_createFiles.html b/testing/mochitest/tests/Harness_sanity/test_createFiles.html new file mode 100644 index 0000000000..0e0637a230 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_createFiles.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers.createFiles</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<div id="content" class="testbody"> + <script type="text/javascript"> + // Creating one file, followed by failing to create a file. + function test1() { + const fileType = "some file type"; + let fdata = "this is same data for a file"; + SpecialPowers.createFiles([{name: "test1.txt", data:fdata, options:{type:fileType}}], + function (files) { + is(files.length, 1, "Created 1 file"); + let f = files[0]; + is("[object File]", f.toString(), "first thing in array is a file"); + is(f.size, fdata.length, "test1 size of first file should be length of its data"); + is("test1.txt", f.name, "test1 test file should have the right name"); + is(f.type, fileType, "File should have the specified type"); + test2(); + }, + function (msg) { ok(false, "Should be able to create a file without an error"); test2(); } + ); + } + + // Failing to create a file, followed by creating a file. + function test2() { + function test3Check(passed) { + ok(passed, "Should trigger the error handler for a bad file name."); + test3(); + }; + + SpecialPowers.createFiles([{name: "/\/\/\/\/\/\/\/\/\/\/\invalidname",}], + function () { test3Check(false); }, + function (msg) { test3Check(true); } + ); + } + + // Creating two files at the same time. + function test3() { + let f1data = "hello"; + SpecialPowers.createFiles([{name: "test3_file.txt", data:f1data}, {name: "emptyfile.txt"}], + function (files) { + is(files.length, 2, "Expected two files to be created"); + let f1 = files[0]; + let f2 = files[1]; + is("[object File]", f1.toString(), "first thing in array is a file"); + is("[object File]", f2.toString(), "second thing in array is a file"); + is("test3_file.txt", f1.name, "first test3 test file should have the right name"); + is("emptyfile.txt", f2.name, "second test3 test file should have the right name"); + is(f1.size, f1data.length, "size of first file should be length of its data"); + is(f2.size, 0, "size of second file should be 0"); + test4(); + }, + function (msg) { + ok(false, "Failed to create files: " + msg); + test4(); + } + ); + }; + + // Creating a file without specifying a name should work. + function test4() { + let fdata = "this is same data for a file"; + SpecialPowers.createFiles([{data:fdata}], + function (files) { + is(files.length, 1, "Created 1 file"); + let f = files[0]; + is("[object File]", f.toString(), "first thing in array is a file"); + is(f.size, fdata.length, "test4 size of first file should be length of its data"); + ok(f.name, "test4 test file should have a name"); + SimpleTest.finish(); + }, + function (msg) { + ok(false, "Should be able to create a file without a name without an error"); + SimpleTest.finish(); + } + ); + } + + SimpleTest.waitForExplicitFinish(); + test1(); + + </script> +</div> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_getweakmapkeys.html b/testing/mochitest/tests/Harness_sanity/test_getweakmapkeys.html new file mode 100644 index 0000000000..58722d977f --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_getweakmapkeys.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers.nondeterministicGetWeakMapKeys</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<div id="content" class="testbody"> + <script type="text/javascript"> + var emptyMap = new WeakMap; + is(SpecialPowers.nondeterministicGetWeakMapKeys(emptyMap).length, 0, "Empty map has no keys"); + var twoMap = new WeakMap; + var x = {}; + var y = {}; + twoMap.set(x, 1); + twoMap.set(y, 2); + var twoMapKeys = SpecialPowers.nondeterministicGetWeakMapKeys(twoMap); + is(twoMapKeys.length, 2, "Map with two things should have two keys"); + ok(twoMapKeys[0] == x || twoMapKeys[1] == x, "One of the keys should be x"); + ok(twoMapKeys[0] == y || twoMapKeys[1] == y, "One of the keys should be y"); + </script> +</div> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_sanity.html b/testing/mochitest/tests/Harness_sanity/test_sanity.html new file mode 100644 index 0000000000..e40ccca35b --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanity.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for mochitest harness sanity</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<p id="display"> + <input id="testKeyEvent1" onkeypress="press1 = true"> + <input id="testKeyEvent2" onkeydown="return false;" onkeypress="press2 = true"> + <input id="testKeyEvent3" onkeypress="press3 = true"> + <input id="testKeyEvent4" onkeydown="return false;" onkeypress="press4 = true"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for sanity **/ +ok(true, "true must be ok"); +isnot(1, true, "1 must not be true"); +isnot(1, false, "1 must not be false"); +isnot(0, false, "0 must not be false"); +isnot(0, true, "0 must not be true"); +isnot("", 0, "Empty string must not be 0"); +isnot("1", 1, "Numeric string must not equal the number"); +isnot("", null, "Empty string must not be null"); +isnot(undefined, null, "Undefined must not be null"); + +var press1 = false; +$("testKeyEvent1").focus(); +sendString("x"); +is($("testKeyEvent1").value, "x", "synthesizeKey should work"); +is(press1, true, "synthesizeKey should dispatch keyPress"); + +var press2 = false; +$("testKeyEvent2").focus(); +sendString("x"); +is($("testKeyEvent2").value, "", "synthesizeKey should respect keydown preventDefault"); +is(press2, false, "synthesizeKey should not dispatch keyPress with default prevented"); + +var press3 = false; +$("testKeyEvent3").focus(); +sendChar("x") +is($("testKeyEvent3").value, "x", "sendChar should work"); +is(press3, true, "sendChar should dispatch keyPress"); + +var press4 = false; +$("testKeyEvent4").focus(); +sendChar("x") +is($("testKeyEvent4").value, "", "sendChar should respect keydown preventDefault"); +is(press4, false, "sendChar should not dispatch keyPress with default prevented"); + + +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_sanityEventUtils.html b/testing/mochitest/tests/Harness_sanity/test_sanityEventUtils.html new file mode 100644 index 0000000000..b24251b227 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanityEventUtils.html @@ -0,0 +1,694 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Profiling test suite for EventUtils</title> + <script type="text/javascript"> + var start = new Date(); + </script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript"> + var loadTime = new Date(); + </script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="starttest()"> +<input type="radio" id="radioTarget1" name="group">Radio Target 1</input> +<input id="textBoxA"> +<input id="textBoxB"> +<input id="testMouseEvent" type="button" value="click"> +<input id="testKeyEvent" > +<input id="testStrEvent" > +<div id="scrollB" style="width: 190px;height: 250px;overflow:auto"> +<p>blah blah blah blah</p> +<p>blah blah blah blah</p> +<p>blah blah blah blah</p> +<p>blah blah blah blah</p> +<p>blah blah blah blah</p> +<p>blah blah blah blah</p> +<p>blah blah blah blah</p> +<p>blah blah blah blah</p> +</div> +<script class="testbody" type="text/javascript"> +info("\nProfile::EventUtilsLoadTime: " + (loadTime - start) + "\n"); +function starttest() { + SimpleTest.waitForFocus( + async function () { + SimpleTest.waitForExplicitFinish(); + var startTime = new Date(); + var check = false; + function doCheck() { + check = true; + } + + const kIsHeadless = await SpecialPowers.spawnChrome([], () => { + return Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless; + }); + + if (navigator.appVersion.includes("Android")) { + // This is the workaround for test failure on debug build. + await SpecialPowers.pushPrefEnv({set: [["formhelper.autozoom.force-disable.test-only", true]]}); + } + + /* test send* functions */ + $("testMouseEvent").addEventListener("click", doCheck, {once: true}); + sendMouseEvent({type:'click'}, "testMouseEvent"); + is(check, true, 'sendMouseEvent should dispatch click event'); + + await (async function testSynthesizeNativeMouseEvent() { + let events = []; + let listener = event => events.push(event); + let preventDefault = event => event.preventDefault(); + // promiseNativeMouseEventAndWaitForEvent uses capturing listeners + // internally, so use capturing listeners also here. + $("testMouseEvent").addEventListener("mousedown", listener, true); + $("testMouseEvent").addEventListener("mouseup", listener, true); + // Clicking with modifiers may open context menu so that we should prevent to open it. + window.addEventListener("contextmenu", preventDefault, { capture: true }); + for (const test of [ + { + description: "ShiftLeft", + modifiers: { shiftKey: true }, + }, + { + description: "ShiftRight", + modifiers: { shiftRightKey: true }, + }, + { + description: "CtrlLeft", + modifiers: { ctrlKey: true }, + }, + { + description: "CtrlRight", + modifiers: { ctrlRightKey: true }, + }, + { + description: "AltLeft", + modifiers: { altKey: true }, + }, + { + description: "AltRight", + modifiers: { altRightKey: true }, + }, + { + description: "MetaLeft", + modifiers: { metaKey: true }, + skip: () => { + // We've not supported "Meta" as Windows logo key or Super/Hyper keys. + return navigator.platform.includes("Win") || navigator.platform.includes("Linux"); + }, + }, + { + description: "MetaRight", + modifiers: { metaRightKey: true }, + skip: () => { + // We've not supported "Meta" as Windows logo key or Super/Hyper keys. + return navigator.platform.includes("Win") || navigator.platform.includes("Linux"); + }, + }, + { + description: "CapsLock", + modifiers: { capsLockKey: true }, + }, + { + description: "NumLock", + modifiers: { numLockKey: true }, + skip: () => { + // macOS does not have `NumLock` key nor state. + return navigator.platform.includes("Mac"); + }, + }, + { + description: "Ctrl+Shift", + modifiers: { ctrlKey: true, shiftKey: true }, + skip: () => { + // We forcibly open context menu on macOS so the following test + // will fail to receive mouse events. + return navigator.platform.includes("Mac"); + }, + }, + { + description: "Alt+Shift", + modifiers: { altKey: true, shiftKey: true }, + }, + { + description: "Meta+Shift", + modifiers: { metaKey: true, shiftKey: true }, + skip: () => { + // We've not supported "Meta" as Windows logo key or Super/Hyper keys. + return navigator.platform.includes("Win") || navigator.platform.includes("Linux"); + }, + }, + ]) { + if (test.skip && test.skip()) { + continue; + } + events = []; + info(`testSynthesizeNativeMouseEvent: sending native mouse click (${test.description})`); + await promiseNativeMouseEventAndWaitForEvent({ + type: "click", + target: $("testMouseEvent"), + atCenter: true, + modifiers: test.modifiers, + eventTypeToWait: "mouseup", + }); + is(events.length, 2, + `testSynthesizeNativeMouseEvent: a pair of "mousedown" and "mouseup" events should be fired (${test.description})`); + is(events[0]?.type, "mousedown", + `testSynthesizeNativeMouseEvent: "mousedown" should be fired (${test.description})`); + is(events[1]?.type, "mouseup", + `testSynthesizeNativeMouseEvent: "mouseup" should be fired (${test.description})`); + if (events.length !== 2) { + continue; + } + for (const mod of [{ keyName: "Alt", propNames: [ "altKey", "altRightKey" ]}, + { keyName: "Control", propNames: [ "ctrlKey", "ctrlRightKey" ]}, + { keyName: "Shift", propNames: [ "shiftKey", "shiftRightKey" ]}, + { keyName: "Meta", propNames: [ "metaKey", "metaRightKey" ]}, + { keyName: "CapsLock", propNames: [ "capsLockKey" ]}, + { keyName: "NumLock", propNames: [ "numLockKey" ]}, + ]) { + const activeExpected = + (test.modifiers.hasOwnProperty(mod.propNames[0]) && + test.modifiers[mod.propNames[0]]) || + (mod.propNames.length !== 1 && + test.modifiers.hasOwnProperty(mod.propNames[1]) && + test.modifiers[mod.propNames[1]]); + const checkFn = activeExpected && ( + // Bug 1693240: We don't support setting modifiers while posting a mouse event on Windows. + navigator.platform.includes("Win") || + // Bug 1693237: We don't support setting modifiers on Android. + navigator.appVersion.includes("Android") || + // In Headless mode, modifiers are not supported by this kind of APIs. + kIsHeadless) ? todo_is : is; + checkFn(events[0]?.getModifierState(mod.keyName), activeExpected, + `testSynthesizeNativeMouseEvent: "mousedown".getModifierState("${mod.keyName}") should return ${activeExpected} (${test.description}`); + checkFn(events[1]?.getModifierState(mod.keyName), activeExpected, + `testSynthesizeNativeMouseEvent: "mouseup".getModifierState("${mod.keyName}") should return ${activeExpected} (${test.description}`); + } + } + const supportsX1AndX2Buttons = + // On Windows, it triggers APP_COMMAND. Therefore, this test is unloaded. + !navigator.platform.includes("Win") && + // On macOS, it seems that no API to specify X1 and X2 button at creating an NSEvent. + !navigator.platform.includes("Mac") && + // On Linux, it seems that X1 button and X2 button events are not synthesized correctly. + !navigator.platform.includes("Linux"); + for (let i = 0; i < (supportsX1AndX2Buttons ? 5 : 3); i++) { + events = []; + info(`testSynthesizeNativeMouseEvent: sending native mouse click (button=${i})`); + await promiseNativeMouseEventAndWaitForEvent({ + type: "click", + target: $("testMouseEvent"), + atCenter: true, + button: i, + eventTypeToWait: "mouseup", + }); + is(events.length, 2, + `testSynthesizeNativeMouseEvent: a pair of "mousedown" and "mouseup" events should be fired (button=${i})`); + is(events[0]?.type, "mousedown", + `testSynthesizeNativeMouseEvent: "mousedown" should be fired (button=${i})`); + is(events[1]?.type, "mouseup", + `testSynthesizeNativeMouseEvent: "mouseup" should be fired (button=${i})`); + if (events.length !== 2) { + continue; + } + is(events[0].button, i, + `testSynthesizeNativeMouseEvent: button of "mousedown" event should be ${i}`); + is(events[1].button, i, + `testSynthesizeNativeMouseEvent: button of "mouseup" event should be ${i}`); + } + $("testMouseEvent").removeEventListener("mousedown", listener); + $("testMouseEvent").removeEventListener("mouseup", listener); + window.removeEventListener("contextmenu", preventDefault, { capture: true }); + })(); + + check = false; + $("testKeyEvent").addEventListener("keypress", doCheck, {once: true}); + $("testKeyEvent").focus(); + sendChar("x"); + is($("testKeyEvent").value, "x", "sendChar should work"); + is(check, true, "sendChar should dispatch keyPress"); + $("testKeyEvent").value = ""; + + $("testStrEvent").focus(); + sendString("string"); + is($("testStrEvent").value, "string", "sendString should work"); + $("testStrEvent").value = ""; + + var keydown = false; + var keypress = false; + $("testKeyEvent").focus(); + $("testKeyEvent").addEventListener("keydown", function() { keydown = true; }, {once: true}); + $("testKeyEvent").addEventListener("keypress", function() { keypress = true; }, {once: true}); + sendKey("DOWN"); + ok(keydown, "sendKey should dispatch keyDown"); + ok(!keypress, "sendKey shouldn't dispatch keyPress for non-printable key"); + + /* test synthesizeMouse* */ + //focus trick enables us to run this in iframes + $("radioTarget1").addEventListener('focus', function (aEvent) { + synthesizeMouse($("radioTarget1"), 1, 1, {}); + is($("radioTarget1").checked, true, "synthesizeMouse should work") + $("radioTarget1").checked = false; + disableNonTestMouseEvents(true); + synthesizeMouse($("radioTarget1"), 1, 1, {}); + is($("radioTarget1").checked, true, "synthesizeMouse should still work with non-test mouse events disabled"); + $("radioTarget1").checked = false; + disableNonTestMouseEvents(false); + }, {once: true}); + $("radioTarget1").focus(); + + //focus trick enables us to run this in iframes + $("textBoxA").addEventListener("focus", function (aEvent) { + check = false; + $("textBoxA").addEventListener("click", function() { check = true; }, { once: true }); + synthesizeMouseAtCenter($("textBoxA"), {}); + is(check, true, 'synthesizeMouse should dispatch mouse event'); + + check = false; + $("scrollB").addEventListener("click", function() { check = true; }, { once: true }); + synthesizeMouseExpectEvent($("scrollB"), 1, 1, {}, $("scrollB"), "click", "synthesizeMouseExpectEvent should fire click event"); + is(check, true, 'synthesizeMouse should dispatch mouse event'); + }, {once: true}); + $("textBoxA").focus(); + + /** + * TODO: testing synthesizeWheel requires a setTimeout + * since there is delay between the scroll event and a check, so for now just test + * that we can successfully call it to avoid having setTimeout vary the runtime metric. + * Testing of this method is currently done here: + * toolkit/content/tests/chrome/test_mousescroll.xul + */ + synthesizeWheel($("scrollB"), 5, 5, {'deltaY': 10.0, deltaMode: WheelEvent.DOM_DELTA_LINE}); + + /* test synthesizeKey* */ + check = false; + $("testKeyEvent").addEventListener("keypress", doCheck, {once:true}); + $("testKeyEvent").focus(); + sendString("a"); + is($("testKeyEvent").value, "a", "synthesizeKey should work"); + is(check, true, "synthesizeKey should dispatch keyPress"); + $("testKeyEvent").value = ""; + + // If |.code| value is not specified explicitly, it should be computed + // from the |.key| value or |.keyCode| value. If a printable key is + // specified, the |.code| value should be guessed with US-English + // keyboard layout. + for (let test of [{ arg: "KEY_Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }, + { arg: "VK_RETURN", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }, + { arg: "KEY_Backspace", code: "Backspace", keyCode: KeyboardEvent.DOM_VK_BACK_SPACE }, + { arg: "KEY_Delete", code: "Delete", keyCode: KeyboardEvent.DOM_VK_DELETE }, + { arg: "KEY_Home", code: "Home", keyCode: KeyboardEvent.DOM_VK_HOME }, + { arg: "KEY_End", code: "End", keyCode: KeyboardEvent.DOM_VK_END }, + { arg: "KEY_ArrowDown", code: "ArrowDown", keyCode: KeyboardEvent.DOM_VK_DOWN }, + { arg: "KEY_ArrowUp", code: "ArrowUp", keyCode: KeyboardEvent.DOM_VK_UP }, + { arg: "KEY_ArrowLeft", code: "ArrowLeft", keyCode: KeyboardEvent.DOM_VK_LEFT }, + { arg: "KEY_ArrowRight", code: "ArrowRight", keyCode: KeyboardEvent.DOM_VK_RIGHT }, + { arg: "KEY_Shift", code: "ShiftLeft", keyCode: KeyboardEvent.DOM_VK_SHIFT }, + { arg: "KEY_Control", code: "ControlLeft", keyCode: KeyboardEvent.DOM_VK_CONTROL }, + { arg: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }, + { arg: "B", code: "KeyB", keyCode: KeyboardEvent.DOM_VK_B }, + { arg: " ", code: "Space", keyCode: KeyboardEvent.DOM_VK_SPACE }, + { arg: "0", code: "Digit0", keyCode: KeyboardEvent.DOM_VK_0 }, + { arg: "(", code: "Digit9", keyCode: KeyboardEvent.DOM_VK_9 }, + { arg: "!", code: "Digit1", keyCode: KeyboardEvent.DOM_VK_1 }, + { arg: "[", code: "BracketLeft", keyCode: KeyboardEvent.DOM_VK_OPEN_BRACKET }, + { arg: ";", code: "Semicolon", keyCode: KeyboardEvent.DOM_VK_SEMICOLON }, + { arg: "\"", code: "Quote", keyCode: KeyboardEvent.DOM_VK_QUOTE }, + { arg: "~", code: "Backquote", keyCode: KeyboardEvent.DOM_VK_BACK_QUOTE }, + { arg: "<", code: "Comma", keyCode: KeyboardEvent.DOM_VK_COMMA }, + { arg: ".", code: "Period", keyCode: KeyboardEvent.DOM_VK_PERIOD }]) { + let testKeydown, keyup; + $("testKeyEvent").focus(); + $("testKeyEvent").addEventListener("keydown", (e) => { testKeydown = e; }, {once: true}); + $("testKeyEvent").addEventListener("keyup", (e) => { keyup = e; }, {once: true}); + synthesizeKey(test.arg); + is(testKeydown.code, test.code, `Synthesizing "${test.arg}" should set code value of "keydown" to "${test.code}"`); + is(testKeydown.keyCode, test.keyCode, `Synthesizing "${test.arg}" should set keyCode value of "keydown" to "${test.keyCode}"`); + is(keyup.code, test.code, `Synthesizing "${test.arg}" key should set code value of "keyup" to "${test.code}"`); + is(keyup.keyCode, test.keyCode, `Synthesizing "${test.arg}" key should set keyCode value of "keyup" to "${test.keyCode}"`); + $("testKeyEvent").value = ""; + } + + /* test synthesizeComposition */ + var description = ""; + var keydownEvent = null; + var keyupEvent = null; + function onKeyDown(aEvent) { + ok(!keydownEvent, description + "keydown should be fired only once" + (keydownEvent ? keydownEvent.key : "") + ", " + (keyupEvent ? keyupEvent.key : "")); + keydownEvent = aEvent; + } + function onKeyUp(aEvent) { + ok(!keyupEvent, description + "keyup should be fired only once"); + keyupEvent = aEvent; + } + function resetKeyDownAndKeyUp(aDescription) { + description = aDescription + ": "; + keydownEvent = null; + keyupEvent = null; + check = false; + } + function checkKeyDownAndKeyUp(aKeyDown, aKeyUp) { + if (aKeyDown) { + is(keydownEvent.keyCode, aKeyDown.keyCode, + description + "keydown event should be dispatched (checking keyCode)"); + is(keydownEvent.key, aKeyDown.key, + description + "keydown event should be dispatched (checking key)"); + } else { + is(keydownEvent, null, + description + "keydown event shouldn't be fired"); + } + if (aKeyUp) { + is(keyupEvent.keyCode, aKeyUp.keyCode, + description + "keyup event should be dispatched (checking keyCode)"); + is(keyupEvent.key, aKeyUp.key, + description + "keyup event should be dispatched (checking key)"); + } else { + is(keyupEvent, null, + description + "keyup event shouldn't be fired"); + } + } + $("textBoxB").addEventListener("keydown", onKeyDown); + $("textBoxB").addEventListener("keyup", onKeyUp); + + $("textBoxB").focus(); + + // If key event is not specified, fake keydown and keyup events which are + // marked as "processed by IME" should be fired. + resetKeyDownAndKeyUp("synthesizing eCompositionStart without specifying keyboard event"); + window.addEventListener("compositionstart", doCheck, {once: true}); + synthesizeComposition({type: "compositionstart"}); + ok(check, description + "synthesizeComposition() should dispatch compositionstart"); + checkKeyDownAndKeyUp({inComposition: false, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + {inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}); + + resetKeyDownAndKeyUp("trying to synthesize eCompositionUpdate directly without specifying keyboard event"); + window.addEventListener("compositionupdate", doCheck, {once: true}); + synthesizeComposition({type: "compositionupdate", data: "a"}); + ok(!check, description + "synthesizeComposition() should not dispatch compositionupdate without error"); + checkKeyDownAndKeyUp(null, null); + + resetKeyDownAndKeyUp("synthesizing eCompositionChange without specifying keyboard event"); + window.addEventListener("text", doCheck, {once: true}); + synthesizeCompositionChange( + { "composition": + { "string": "a", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + } + ); + ok(check, description + "synthesizeCompositionChange should cause dispatching a DOM text event"); + checkKeyDownAndKeyUp({inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + {inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}); + + resetKeyDownAndKeyUp("synthesizing eCompositionChange for removing clauses without specifying keyboard event"); + synthesizeCompositionChange( + { "composition": + { "string": "a", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 1, "length": 0 } + } + ); + checkKeyDownAndKeyUp({inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + {inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}); + + resetKeyDownAndKeyUp("trying to synthesize eCompositionEnd directly without specifying keyboard event"); + window.addEventListener("compositionend", doCheck, {once: true}); + synthesizeComposition({type: "compositionend", data: "a"}); + ok(!check, description + "synthesizeComposition() should not dispatch compositionend"); + checkKeyDownAndKeyUp(null, null); + + resetKeyDownAndKeyUp("synthesizing eCompositionCommit without specifying keyboard event"); + synthesizeComposition({type: "compositioncommit", data: "a"}); + ok(check, description + "synthesizeComposition() should dispatch compositionend"); + checkKeyDownAndKeyUp({inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + {inComposition: false, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}); + + var querySelectedText = synthesizeQuerySelectedText(); + ok(querySelectedText, "query selected text event result is null"); + ok(querySelectedText.succeeded, "query selected text event failed"); + is(querySelectedText.offset, 1, + "query selected text event returns wrong offset"); + is(querySelectedText.text, "", + "query selected text event returns wrong selected text"); + $("textBoxB").value = ""; + + querySelectedText = synthesizeQuerySelectedText(); + ok(querySelectedText, "query selected text event result is null"); + ok(querySelectedText.succeeded, "query selected text event failed"); + is(querySelectedText.offset, 0, + "query selected text event returns wrong offset"); + is(querySelectedText.text, "", + "query selected text event returns wrong selected text"); + var endTime = new Date(); + info("\nProfile::EventUtilsRunTime: " + (endTime-startTime) + "\n"); + + // In most cases, automated tests shouldn't try to synthesize + // compositionstart manually. Let's check if synthesizeCompositionChange() + // dispatches compositionstart automatically. + resetKeyDownAndKeyUp("synthesizing eCompositionChange without specifying keyboard event when there is no composition"); + window.addEventListener("compositionstart", doCheck, {once: true}); + synthesizeCompositionChange( + { "composition": + { "string": "a", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + } + ); + ok(check, description + "synthesizeCompositionChange should dispatch \"compositionstart\" automatically if there is no composition"); + checkKeyDownAndKeyUp({inComposition: false, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + {inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}); + + resetKeyDownAndKeyUp("synthesizing eCompositionCommitAsIs without specifying keyboard event"); + synthesizeComposition({type: "compositioncommitasis"}); + checkKeyDownAndKeyUp({inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + {inComposition: false, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}); + + // If key event is specified, keydown event which is marked as "processed + // by IME" should be fired and keyup event which is NOT marked as so + // should be fired too. + resetKeyDownAndKeyUp("synthesizing eCompositionStart with specifying keyboard event"); + synthesizeComposition({type: "compositionstart", key: {key: "a"}}); + checkKeyDownAndKeyUp({inComposition: false, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + {inComposition: true, keyCode: KeyboardEvent.DOM_VK_A, key: "a"}); + + resetKeyDownAndKeyUp("synthesizing eCompositionChange with specifying keyboard event"); + synthesizeCompositionChange( + {"composition": + {"string": "b", "clauses": [ + {"length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE} + ]}, + "caret": {"start": 1, "length": 0}, + "key": {key: "b"}, + } + ); + checkKeyDownAndKeyUp({inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + {inComposition: true, keyCode: KeyboardEvent.DOM_VK_B, key: "b"}); + + resetKeyDownAndKeyUp("synthesizing eCompositionCommit with specifying keyboard event"); + synthesizeComposition({type: "compositioncommit", data: "c", key: {key: "KEY_Enter"}}); + checkKeyDownAndKeyUp({inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + {inComposition: false, keyCode: KeyboardEvent.DOM_VK_RETURN, key: "Enter"}); + + // keyup shouldn't be dispatched automatically if type is specified as keydown + resetKeyDownAndKeyUp("synthesizing eCompositionStart with specifying keyboard event whose type is keydown"); + synthesizeComposition({type: "compositionstart", key: {key: "a", type: "keydown"}}); + checkKeyDownAndKeyUp({inComposition: false, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + null); + + resetKeyDownAndKeyUp("synthesizing eCompositionChange with specifying keyboard event whose type is keydown"); + synthesizeCompositionChange( + {"composition": + {"string": "b", "clauses": [ + {"length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE} + ]}, + "caret": {"start": 1, "length": 0}, + "key": {key: "b", type: "keydown"}, + } + ); + checkKeyDownAndKeyUp({inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + null); + + resetKeyDownAndKeyUp("synthesizing eCompositionCommit with specifying keyboard event whose type is keydown"); + synthesizeComposition({type: "compositioncommit", data: "c", key: {key: "KEY_Enter", type: "keydown"}}); + checkKeyDownAndKeyUp({inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + null); + + // keydown shouldn't be dispatched automatically if type is specified as keyup + resetKeyDownAndKeyUp("synthesizing eCompositionStart with specifying keyboard event whose type is keyup"); + synthesizeComposition({type: "compositionstart", key: {key: "a", type: "keyup"}}); + checkKeyDownAndKeyUp(null, + {inComposition: true, keyCode: KeyboardEvent.DOM_VK_A, key: "a"}); + + resetKeyDownAndKeyUp("synthesizing eCompositionChange with specifying keyboard event whose type is keyup"); + synthesizeCompositionChange( + {"composition": + {"string": "b", "clauses": [ + {"length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE} + ]}, + "caret": {"start": 1, "length": 0}, + "key": {key: "b", type: "keyup"}, + } + ); + checkKeyDownAndKeyUp(null, + {inComposition: true, keyCode: KeyboardEvent.DOM_VK_B, key: "b"}); + + resetKeyDownAndKeyUp("synthesizing eCompositionCommit with specifying keyboard event whose type is keyup"); + synthesizeComposition({type: "compositioncommit", data: "c", key: {key: "KEY_Enter", type: "keyup"}}); + checkKeyDownAndKeyUp(null, + {inComposition: false, keyCode: KeyboardEvent.DOM_VK_RETURN, key: "Enter"}); + + // keydown event shouldn't be marked as "processed by IME" if doNotMarkKeydownAsProcessed is true + resetKeyDownAndKeyUp("synthesizing eCompositionStart with specifying keyboard event whose doNotMarkKeydownAsProcessed is true"); + synthesizeComposition({type: "compositionstart", key: {key: "a", doNotMarkKeydownAsProcessed: true}}); + checkKeyDownAndKeyUp({inComposition: false, keyCode: KeyboardEvent.DOM_VK_A, key: "a"}, + {inComposition: true, keyCode: KeyboardEvent.DOM_VK_A, key: "a"}); + + resetKeyDownAndKeyUp("synthesizing eCompositionChange with specifying keyboard event whose doNotMarkKeydownAsProcessed is true"); + synthesizeCompositionChange( + {"composition": + {"string": "b", "clauses": [ + {"length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE} + ]}, + "caret": {"start": 1, "length": 0}, + "key": {key: "b", doNotMarkKeydownAsProcessed: true}, + } + ); + checkKeyDownAndKeyUp({inComposition: true, keyCode: KeyboardEvent.DOM_VK_B, key: "b"}, + {inComposition: true, keyCode: KeyboardEvent.DOM_VK_B, key: "b"}); + + resetKeyDownAndKeyUp("synthesizing eCompositionCommit with specifying keyboard event whose doNotMarkKeydownAsProcessed is true"); + synthesizeComposition({type: "compositioncommit", data: "c", key: {key: "KEY_Enter", doNotMarkKeydownAsProcessed: true}}); + checkKeyDownAndKeyUp({inComposition: true, keyCode: KeyboardEvent.DOM_VK_RETURN, key: "Enter"}, + {inComposition: false, keyCode: KeyboardEvent.DOM_VK_RETURN, key: "Enter"}); + + // keyup event should be marked as "processed by IME" if markKeyupAsProcessed is true + resetKeyDownAndKeyUp("synthesizing eCompositionStart with specifying keyboard event whose markKeyupAsProcessed is true"); + synthesizeComposition({type: "compositionstart", key: {key: "a", markKeyupAsProcessed: true}}); + checkKeyDownAndKeyUp({inComposition: false, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + {inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}); + + resetKeyDownAndKeyUp("synthesizing eCompositionChange with specifying keyboard event whose markKeyupAsProcessed is true"); + synthesizeCompositionChange( + {"composition": + {"string": "b", "clauses": [ + {"length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE} + ]}, + "caret": {"start": 1, "length": 0}, + "key": {key: "b", markKeyupAsProcessed: true}, + } + ); + checkKeyDownAndKeyUp({inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + {inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}); + + resetKeyDownAndKeyUp("synthesizing eCompositionCommit with specifying keyboard event whose markKeyupAsProcessed is true"); + synthesizeComposition({type: "compositioncommit", data: "c", key: {key: "KEY_Enter", markKeyupAsProcessed: true}}); + checkKeyDownAndKeyUp({inComposition: true, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}, + {inComposition: false, keyCode: KeyboardEvent.DOM_VK_PROCESSKEY, key: "Process"}); + + // If key event is explicitly declared with null, keyboard events shouldn't + // be fired for emulating text inputs without keyboard such as voice input or something. + resetKeyDownAndKeyUp("synthesizing eCompositionStart with specifying keyboard event as null"); + synthesizeComposition({type: "compositionstart", key: null}); + checkKeyDownAndKeyUp(null, null); + + resetKeyDownAndKeyUp("synthesizing eCompositionChange with specifying keyboard event as null"); + synthesizeCompositionChange( + {"composition": + {"string": "b", "clauses": [ + {"length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE} + ]}, + "caret": {"start": 1, "length": 0}, + "key": null, + } + ); + checkKeyDownAndKeyUp(null, null); + + resetKeyDownAndKeyUp("synthesizing eCompositionCommit with specifying keyboard event as null"); + synthesizeComposition({type: "compositioncommit", data: "c", key: null}); + checkKeyDownAndKeyUp(null, null); + + // If key event is explicitly declared with empty object, keyboard events + // shouldn't be fired for emulating text inputs without keyboard such as + // voice input or something. + resetKeyDownAndKeyUp("synthesizing eCompositionStart with specifying keyboard event as empty"); + synthesizeComposition({type: "compositionstart", key: {}}); + checkKeyDownAndKeyUp(null, null); + + resetKeyDownAndKeyUp("synthesizing eCompositionChange with specifying keyboard event as empty"); + synthesizeCompositionChange( + {"composition": + {"string": "b", "clauses": [ + {"length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE} + ]}, + "caret": {"start": 1, "length": 0}, + "key": {}, + } + ); + checkKeyDownAndKeyUp(null, null); + + resetKeyDownAndKeyUp("synthesizing eCompositionCommit with specifying keyboard event as empty"); + synthesizeComposition({type: "compositioncommit", data: "c", key: {}}); + checkKeyDownAndKeyUp(null, null); + + $("textBoxB").removeEventListener("keydown", onKeyDown); + $("textBoxB").removeEventListener("keyup", onKeyUp); + + + // Async event synthesizing. + // On Android this does not work. + if (navigator.userAgent.includes("Android")) { + SimpleTest.finish(); + return; + } + + await (async function () { + await SpecialPowers.pushPrefEnv({set: [["test.events.async.enabled", true]]}); + try { + disableNonTestMouseEvents(true); + let mouseMoveCount = 0; + let waitForAllSynthesizedMouseMove = + new Promise(resolve => { + window.addEventListener("mousemove", function onMouseMove(aEvent) { + mouseMoveCount++; + is(aEvent.target, $("testMouseEvent"), + `The mousemove event target of ${ + mouseMoveCount + } should be the input#testMouseEvent, but ${ + aEvent.target.nodeName + }`); + if (mouseMoveCount === 30) { + window.removeEventListener("mousemove", onMouseMove, { capture: true }); + resolve(); + } + }, { capture: true }); + }); + for (let i = 0; i < 30; i++) { + synthesizeMouse($("testMouseEvent"), 3 + i % 2, 3 + i % 2, { type: "mousemove" }); + } + await waitForAllSynthesizedMouseMove; + } finally { + disableNonTestMouseEvents(false); + } + })(); + + SimpleTest.finish(); + } + ); +}; +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_sanityException.html b/testing/mochitest/tests/Harness_sanity/test_sanityException.html new file mode 100644 index 0000000000..463f51d80e --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanityException.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test that uncaught exceptions in mochitests cause failures</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=670817">Mozilla Bug 670817</a> +<script> +ok(true, "a call to ok"); +SimpleTest.expectUncaughtException(); +// eslint-disable-next-line no-throw-literal +throw "an uncaught exception"; +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_sanityException2.html b/testing/mochitest/tests/Harness_sanity/test_sanityException2.html new file mode 100644 index 0000000000..1136b73916 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanityException2.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test that uncaught exceptions in mochitests cause failures</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=670817">Mozilla Bug 670817</a> +<script> +SimpleTest.waitForExplicitFinish(); +ok(true, "a call to ok"); +SimpleTest.executeSoon(function() { + SimpleTest.expectUncaughtException(); + // eslint-disable-next-line no-throw-literal + throw "an uncaught exception"; +}); +SimpleTest.executeSoon(function() { + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_sanityParams.html b/testing/mochitest/tests/Harness_sanity/test_sanityParams.html new file mode 100644 index 0000000000..192d6ef96a --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanityParams.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for exposing test suite information</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +ok(SimpleTest.harnessParameters, "Should have parameters."); +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_sanityRegisteredServiceWorker.html b/testing/mochitest/tests/Harness_sanity/test_sanityRegisteredServiceWorker.html new file mode 100644 index 0000000000..f3c6d422d9 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanityRegisteredServiceWorker.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test that service worker registrations not cleaned up in mochitests cause failures</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.expectRegisteredServiceWorker(); +SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] +]}, function() { + navigator.serviceWorker.register("empty.js", {scope: "scope"}) + .then(function(registration) { + ok(registration, "Registration succeeded"); + SimpleTest.finish(); + }); +}); +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_sanityRegisteredServiceWorker2.html b/testing/mochitest/tests/Harness_sanity/test_sanityRegisteredServiceWorker2.html new file mode 100644 index 0000000000..f46944c2e5 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanityRegisteredServiceWorker2.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test that service worker registrations not cleaned up in mochitests cause failures</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<script> +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] +]}, function() { + navigator.serviceWorker.getRegistration("scope") + .then(function(registration) { + ok(registration, "Registration successfully obtained"); + return registration.unregister(); + }).then(function() { + SimpleTest.finish(); + }); +}); +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_sanitySimpletest.html b/testing/mochitest/tests/Harness_sanity/test_sanitySimpletest.html new file mode 100644 index 0000000000..2b289f1387 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanitySimpletest.html @@ -0,0 +1,103 @@ +<!--This test should be updated each time new functionality is added to SimpleTest--> +<!DOCTYPE HTML> +<html> +<head> + <title>Profiling test suite for SimpleTest</title> + <script type="text/javascript"> + var start = new Date(); + </script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript"> + var loadTime = new Date(); + </script> +</head> +<body> +<input id="textB"/> +<script class="testbody" type="text/javascript"> +info("Profile::SimpleTestLoadTime: " + (loadTime - start)); +var startTime = new Date(); +SimpleTest.waitForExplicitFinish(); +function starttest() { + SimpleTest.waitForFocus( + function() { + //test log + info("Logging some info") + + //basic usage + ok(true, "test ok"); + SimpleTest.record(true, "test ok", "diagnostic information"); + is(0, 0, "is() test failed"); + isnot(0, 1, "isnot() test failed"); + + //todo tests + todo(false, "test todo", "todo() test should not pass"); + todo_is(false, true, "test todo_is"); + todo_isnot(true, true, "test todo_isnot"); + + //misc + SimpleTest.requestLongerTimeout(1); + + //note: this test may alter runtimes as it waits + var check = false; + $('textB').focus(); + SimpleTest.waitForClipboard("a", + function () { + SpecialPowers.clipboardCopyString("a"); + }, + function () { + check = true; + is(check, true, "waitForClipboard should work"); + manipulateElements(); + }, + function () { + check = false; + is(check, false, "waitForClipboard should work"); + manipulateElements(); + } + ); + + //use helper functions + function manipulateElements() + { + var div1 = createEl('div', {'id': 'somediv', 'display': 'block'}, "I am a div"); + document.body.appendChild(div1); + var divObj = this.getElement('somediv'); + is(divObj, div1, 'createEl did not create element as expected'); + is($('somediv'), divObj, '$ helper did not get element as expected'); + is(computedStyle(divObj, 'display'), 'block', 'computedStyle did not get right display value'); + document.body.removeChild(div1); + + /* note: expectChildProcessCrash is not being tested here, as it causes wildly variable + * run times. It is currently being tested in: + * dom/plugins/test/test_hanging.html and dom/plugins/test/test_crashing.html + */ + + //note: this also adds a short wait period + SimpleTest.executeSoon( + function () { + //finish() calls a slew of SimpleTest functions + SimpleTest.finish(); + //call this after finish so we can make sure it works and doesn't hang our process + var endTime = new Date(); + info("Profile::SimpleTestRunTime: " + (endTime-startTime)); + //expect and throw exception here. Otherwise, any code that follows the throw call will never be executed + SimpleTest.expectUncaughtException(); + //make sure we catch this error + // eslint-disable-next-line no-throw-literal + throw "i am an uncaught exception" + } + ); + } + } + ); +}; +//use addLoadEvent +addLoadEvent( + function() { + starttest(); + } +); +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_sanityWindowSnapshot.html b/testing/mochitest/tests/Harness_sanity/test_sanityWindowSnapshot.html new file mode 100644 index 0000000000..6960fc2104 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanityWindowSnapshot.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Profiling test suite for WindowSnapshot</title> + <script type="text/javascript"> + var start = new Date(); + </script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <script type="text/javascript"> + var loadTime = new Date(); + </script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="starttest()"> +<script class="testbody" type="text/javascript"> +info("\nProfile::WindowSnapshotLoadTime: " + (loadTime - start) + "\n"); +function starttest() { + SimpleTest.waitForExplicitFinish(); + var startTime = new Date(); + var snap = snapshotWindow(window, false); + var snap2 = snapshotWindow(window, false); + is(compareSnapshots(snap, snap2, true)[0], true, "this should be true"); + var div1 = createEl('div', {'id': 'somediv', 'display': 'block'}, "I am a div"); + document.body.appendChild(div1); + snap2 = snapshotWindow(window, false); + is(compareSnapshots(snap, snap2, true)[0], false, "this should be false"); + document.body.removeChild(div1); + var endTime = new Date(); + info("\nProfile::WindowSnapshotRunTime: " + (endTime-startTime) + "\n"); + SimpleTest.finish(); +}; +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_sanity_cleanup.html b/testing/mochitest/tests/Harness_sanity/test_sanity_cleanup.html new file mode 100644 index 0000000000..644be67674 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanity_cleanup.html @@ -0,0 +1,30 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>SimpleTest.registerCleanupFunction test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +// Not a great example, since we have the pushPrefEnv API to cover +// this use case, but I want to be able to test that the cleanup +// function gets run, so setting and clearing a pref seems straightforward. +function do_cleanup1() { + SpecialPowers.clearUserPref("simpletest.cleanup.1"); + info("do_cleanup1 run!"); +} +function do_cleanup2() { + SpecialPowers.clearUserPref("simpletest.cleanup.2"); + info("do_cleanup2 run!"); +} +SpecialPowers.setBoolPref("simpletest.cleanup.1", true); +SpecialPowers.setBoolPref("simpletest.cleanup.2", true); +SimpleTest.registerCleanupFunction(do_cleanup1); +SimpleTest.registerCleanupFunction(do_cleanup2); +ok(true, "dummy check"); +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_sanity_cleanup2.html b/testing/mochitest/tests/Harness_sanity/test_sanity_cleanup2.html new file mode 100644 index 0000000000..b0b7523819 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanity_cleanup2.html @@ -0,0 +1,24 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>SimpleTest.registerCleanupFunction test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +for (const pref of [1, 2]) { + try { + SpecialPowers.getBoolPref("simpletest.cleanup." + pref); + ok(false, "Cleanup function should have unset pref"); + } + catch(ex) { + ok(true, "Pref was not set"); + } +} + +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_sanity_manifest.html b/testing/mochitest/tests/Harness_sanity/test_sanity_manifest.html new file mode 100644 index 0000000000..d07df21ef5 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanity_manifest.html @@ -0,0 +1,16 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>SimpleTest.expected = 'fail' test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +ok(false, "We expect this to fail"); + +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_sanity_manifest_pf.html b/testing/mochitest/tests/Harness_sanity/test_sanity_manifest_pf.html new file mode 100644 index 0000000000..4b83fda596 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanity_manifest_pf.html @@ -0,0 +1,17 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>SimpleTest.expected = 'fail' test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +ok(true, "We expect this to pass"); +ok(false, "We expect this to fail"); + +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_sanity_waitForCondition.html b/testing/mochitest/tests/Harness_sanity/test_sanity_waitForCondition.html new file mode 100644 index 0000000000..f059adb88d --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanity_waitForCondition.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title>SimpleTest.waitForCondition test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> +<script> + +var captureFailure = false; +var capturedFailures = []; +window.ok = function (cond, name) { + if (!captureFailure) { + SimpleTest.ok(cond, name); + } else if (cond) { + SimpleTest.ok(false, `Expect a failure with "${name}"`); + } else { + capturedFailures.push(name); + } +}; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("test behavior SimpleTest.waitForCondition"); + +addLoadEvent(testNormal); + +function testNormal() { + var condition = false; + SimpleTest.waitForCondition(() => condition, () => { + ok(condition, "Should only be called when condition is true"); + SimpleTest.executeSoon(testTimeout); + }, "Shouldn't timeout"); + setTimeout(() => { condition = true; }, 1000); +} + +function testTimeout() { + captureFailure = true; + SimpleTest.waitForCondition(() => false, () => { + captureFailure = false; + is(capturedFailures.length, 1, "Should captured one failure"); + is(capturedFailures[0], "Should timeout", + "Should capture the failure passed in"); + SimpleTest.executeSoon(() => SimpleTest.finish()); + }, "Should timeout"); +} + +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/xpcshell.toml b/testing/mochitest/tests/Harness_sanity/xpcshell.toml new file mode 100644 index 0000000000..f203c1ef9f --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/xpcshell.toml @@ -0,0 +1,5 @@ +[DEFAULT] + +["test_SpecialPowersSandbox.js"] + +["test_SpecialPowersSpawn.js"] diff --git a/testing/mochitest/tests/MochiKit-1.4.2/LICENSE.txt b/testing/mochitest/tests/MochiKit-1.4.2/LICENSE.txt new file mode 100644 index 0000000000..4d0065befd --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/LICENSE.txt @@ -0,0 +1,69 @@ +MochiKit is dual-licensed software. It is available under the terms of the +MIT License, or the Academic Free License version 2.1. The full text of +each license is included below. + +MIT License +=========== + +Copyright (c) 2005 Bob Ippolito. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Academic Free License v. 2.1 +============================ + +Copyright (c) 2005 Bob Ippolito. All rights reserved. + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following notice immediately following the copyright notice for the Original Work: + +Licensed under the Academic Free License version 2.1 + +1) Grant of Copyright License. Licensor hereby grants You a world-wide, royalty-free, non-exclusive, perpetual, sublicenseable license to do the following: + +a) to reproduce the Original Work in copies; + +b) to prepare derivative works ("Derivative Works") based upon the Original Work; + +c) to distribute copies of the Original Work and Derivative Works to the public; + +d) to perform the Original Work publicly; and + +e) to display the Original Work publicly. + +2) Grant of Patent License. Licensor hereby grants You a world-wide, royalty-free, non-exclusive, perpetual, sublicenseable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, to make, use, sell and offer for sale the Original Work and Derivative Works. + +3) Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor hereby agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work, and by publishing the address of that information repository in a notice immediately following the copyright notice that applies to the Original Work. + +4) Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior written permission of the Licensor. Nothing in this License shall be deemed to grant any rights to trademarks, copyrights, patents, trade secrets or any other intellectual property of Licensor except as expressly stated herein. No patent license is granted to make, use, sell or offer to sell embodiments of any patent claims other than the licensed claims defined in Section 2. No right is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under different terms from this License any Original Work that Licensor otherwise would have a right to license. + +5) This section intentionally omitted. + +6) Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + +7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately proceeding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to Original Work is granted hereunder except under this disclaimer. + +8) Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to any person for any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to liability for death or personal injury resulting from Licensor's negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. + +9) Acceptance and Termination. If You distribute copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. Nothing else but this License (or another written agreement between Licensor and You) grants You permission to create Derivative Works based upon the Original Work or to exercise any of the rights granted in Section 1 herein, and any attempt to do so except under the terms of this License (or another written agreement between Licensor and You) is expressly prohibited by U.S. copyright law, the equivalent laws of other countries, and by international treaty. Therefore, by exercising any of the rights granted to You in Section 1 herein, You indicate Your acceptance of this License and all of its terms and conditions. + +10) Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + +11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et seq., the equivalent laws of other countries, and international treaty. This section shall survive the termination of this License. + +12) Attorneys Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + +13) Miscellaneous. This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + +14) Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +15) Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + +This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved. Permission is hereby granted to copy and distribute this license without modification. This license may not be modified without the express written permission of its copyright owner. + + + diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Async.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Async.js new file mode 100644 index 0000000000..6c5da15925 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Async.js @@ -0,0 +1,682 @@ +/*** + +MochiKit.Async 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +MochiKit.Base._deps('Async', ['Base']); + +MochiKit.Async.NAME = "MochiKit.Async"; +MochiKit.Async.VERSION = "1.4.2"; +MochiKit.Async.__repr__ = function () { + return "[" + this.NAME + " " + this.VERSION + "]"; +}; +MochiKit.Async.toString = function () { + return this.__repr__(); +}; + +/** @id MochiKit.Async.Deferred */ +MochiKit.Async.Deferred = function (/* optional */ canceller) { + this.chain = []; + this.id = this._nextId(); + this.fired = -1; + this.paused = 0; + this.results = [null, null]; + this.canceller = canceller; + this.silentlyCancelled = false; + this.chained = false; +}; + +MochiKit.Async.Deferred.prototype = { + /** @id MochiKit.Async.Deferred.prototype.repr */ + repr: function () { + var state; + if (this.fired == -1) { + state = 'unfired'; + } else if (this.fired === 0) { + state = 'success'; + } else { + state = 'error'; + } + return 'Deferred(' + this.id + ', ' + state + ')'; + }, + + toString: MochiKit.Base.forwardCall("repr"), + + _nextId: MochiKit.Base.counter(), + + /** @id MochiKit.Async.Deferred.prototype.cancel */ + cancel: function () { + var self = MochiKit.Async; + if (this.fired == -1) { + if (this.canceller) { + this.canceller(this); + } else { + this.silentlyCancelled = true; + } + if (this.fired == -1) { + this.errback(new self.CancelledError(this)); + } + } else if ((this.fired === 0) && (this.results[0] instanceof self.Deferred)) { + this.results[0].cancel(); + } + }, + + _resback: function (res) { + /*** + + The primitive that means either callback or errback + + ***/ + this.fired = ((res instanceof Error) ? 1 : 0); + this.results[this.fired] = res; + this._fire(); + }, + + _check: function () { + if (this.fired != -1) { + if (!this.silentlyCancelled) { + throw new MochiKit.Async.AlreadyCalledError(this); + } + this.silentlyCancelled = false; + return; + } + }, + + /** @id MochiKit.Async.Deferred.prototype.callback */ + callback: function (res) { + this._check(); + if (res instanceof MochiKit.Async.Deferred) { + throw new Error("Deferred instances can only be chained if they are the result of a callback"); + } + this._resback(res); + }, + + /** @id MochiKit.Async.Deferred.prototype.errback */ + errback: function (res) { + this._check(); + var self = MochiKit.Async; + if (res instanceof self.Deferred) { + throw new Error("Deferred instances can only be chained if they are the result of a callback"); + } + if (!(res instanceof Error)) { + res = new self.GenericError(res); + } + this._resback(res); + }, + + /** @id MochiKit.Async.Deferred.prototype.addBoth */ + addBoth: function (fn) { + if (arguments.length > 1) { + fn = MochiKit.Base.partial.apply(null, arguments); + } + return this.addCallbacks(fn, fn); + }, + + /** @id MochiKit.Async.Deferred.prototype.addCallback */ + addCallback: function (fn) { + if (arguments.length > 1) { + fn = MochiKit.Base.partial.apply(null, arguments); + } + return this.addCallbacks(fn, null); + }, + + /** @id MochiKit.Async.Deferred.prototype.addErrback */ + addErrback: function (fn) { + if (arguments.length > 1) { + fn = MochiKit.Base.partial.apply(null, arguments); + } + return this.addCallbacks(null, fn); + }, + + /** @id MochiKit.Async.Deferred.prototype.addCallbacks */ + addCallbacks: function (cb, eb) { + if (this.chained) { + throw new Error("Chained Deferreds can not be re-used"); + } + this.chain.push([cb, eb]); + if (this.fired >= 0) { + this._fire(); + } + return this; + }, + + _fire: function () { + /*** + + Used internally to exhaust the callback sequence when a result + is available. + + ***/ + var chain = this.chain; + var fired = this.fired; + var res = this.results[fired]; + var self = this; + var cb = null; + while (chain.length > 0 && this.paused === 0) { + // Array + var pair = chain.shift(); + var f = pair[fired]; + if (f === null) { + continue; + } + try { + res = f(res); + fired = ((res instanceof Error) ? 1 : 0); + if (res instanceof MochiKit.Async.Deferred) { + cb = function (res) { + self._resback(res); + self.paused--; + if ((self.paused === 0) && (self.fired >= 0)) { + self._fire(); + } + }; + this.paused++; + } + } catch (err) { + fired = 1; + if (!(err instanceof Error)) { + err = new MochiKit.Async.GenericError(err); + } + res = err; + } + } + this.fired = fired; + this.results[fired] = res; + if (cb && this.paused) { + // this is for "tail recursion" in case the dependent deferred + // is already fired + res.addBoth(cb); + res.chained = true; + } + } +}; + +MochiKit.Base.update(MochiKit.Async, { + /** @id MochiKit.Async.evalJSONRequest */ + evalJSONRequest: function (req) { + return MochiKit.Base.evalJSON(req.responseText); + }, + + /** @id MochiKit.Async.succeed */ + succeed: function (/* optional */result) { + var d = new MochiKit.Async.Deferred(); + d.callback.apply(d, arguments); + return d; + }, + + /** @id MochiKit.Async.fail */ + fail: function (/* optional */result) { + var d = new MochiKit.Async.Deferred(); + d.errback.apply(d, arguments); + return d; + }, + + /** @id MochiKit.Async.getXMLHttpRequest */ + getXMLHttpRequest: function () { + var self = arguments.callee; + if (!self.XMLHttpRequest) { + var tryThese = [ + function () { return new XMLHttpRequest(); }, + function () { return new ActiveXObject('Msxml2.XMLHTTP'); }, + function () { return new ActiveXObject('Microsoft.XMLHTTP'); }, + function () { return new ActiveXObject('Msxml2.XMLHTTP.4.0'); }, + function () { + throw new MochiKit.Async.BrowserComplianceError("Browser does not support XMLHttpRequest"); + } + ]; + for (var i = 0; i < tryThese.length; i++) { + var func = tryThese[i]; + try { + self.XMLHttpRequest = func; + return func(); + } catch (e) { + // pass + } + } + } + return self.XMLHttpRequest(); + }, + + _xhr_onreadystatechange: function (d) { + // MochiKit.Logging.logDebug('this.readyState', this.readyState); + var m = MochiKit.Base; + if (this.readyState == 4) { + // IE SUCKS + try { + this.onreadystatechange = null; + } catch (e) { + try { + this.onreadystatechange = m.noop; + } catch (e) { + } + } + var status = null; + try { + status = this.status; + if (!status && m.isNotEmpty(this.responseText)) { + // 0 or undefined seems to mean cached or local + status = 304; + } + } catch (e) { + // pass + // MochiKit.Logging.logDebug('error getting status?', repr(items(e))); + } + // 200 is OK, 201 is CREATED, 204 is NO CONTENT + // 304 is NOT MODIFIED, 1223 is apparently a bug in IE + if (status == 200 || status == 201 || status == 204 || + status == 304 || status == 1223) { + d.callback(this); + } else { + var err = new MochiKit.Async.XMLHttpRequestError(this, "Request failed"); + if (err.number) { + // XXX: This seems to happen on page change + d.errback(err); + } else { + // XXX: this seems to happen when the server is unreachable + d.errback(err); + } + } + } + }, + + _xhr_canceller: function (req) { + // IE SUCKS + try { + req.onreadystatechange = null; + } catch (e) { + try { + req.onreadystatechange = MochiKit.Base.noop; + } catch (e) { + } + } + req.abort(); + }, + + + /** @id MochiKit.Async.sendXMLHttpRequest */ + sendXMLHttpRequest: function (req, /* optional */ sendContent) { + if (typeof(sendContent) == "undefined" || sendContent === null) { + sendContent = ""; + } + + var m = MochiKit.Base; + var self = MochiKit.Async; + var d = new self.Deferred(m.partial(self._xhr_canceller, req)); + + try { + req.onreadystatechange = m.bind(self._xhr_onreadystatechange, + req, d); + req.send(sendContent); + } catch (e) { + try { + req.onreadystatechange = null; + } catch (ignore) { + // pass + } + d.errback(e); + } + + return d; + + }, + + /** @id MochiKit.Async.doXHR */ + doXHR: function (url, opts) { + /* + Work around a Firefox bug by dealing with XHR during + the next event loop iteration. Maybe it's this one: + https://bugzilla.mozilla.org/show_bug.cgi?id=249843 + */ + var self = MochiKit.Async; + return self.callLater(0, self._doXHR, url, opts); + }, + + _doXHR: function (url, opts) { + var m = MochiKit.Base; + opts = m.update({ + method: 'GET', + sendContent: '' + /* + queryString: undefined, + username: undefined, + password: undefined, + headers: undefined, + mimeType: undefined + */ + }, opts); + var self = MochiKit.Async; + var req = self.getXMLHttpRequest(); + if (opts.queryString) { + var qs = m.queryString(opts.queryString); + if (qs) { + url += "?" + qs; + } + } + // Safari will send undefined:undefined, so we have to check. + // We can't use apply, since the function is native. + if ('username' in opts) { + req.open(opts.method, url, true, opts.username, opts.password); + } else { + req.open(opts.method, url, true); + } + if (req.overrideMimeType && opts.mimeType) { + req.overrideMimeType(opts.mimeType); + } + req.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + if (opts.headers) { + var headers = opts.headers; + if (!m.isArrayLike(headers)) { + headers = m.items(headers); + } + for (var i = 0; i < headers.length; i++) { + var header = headers[i]; + var name = header[0]; + var value = header[1]; + req.setRequestHeader(name, value); + } + } + return self.sendXMLHttpRequest(req, opts.sendContent); + }, + + _buildURL: function (url/*, ...*/) { + if (arguments.length > 1) { + var m = MochiKit.Base; + var qs = m.queryString.apply(null, m.extend(null, arguments, 1)); + if (qs) { + return url + "?" + qs; + } + } + return url; + }, + + /** @id MochiKit.Async.doSimpleXMLHttpRequest */ + doSimpleXMLHttpRequest: function (url/*, ...*/) { + var self = MochiKit.Async; + url = self._buildURL.apply(self, arguments); + return self.doXHR(url); + }, + + /** @id MochiKit.Async.loadJSONDoc */ + loadJSONDoc: function (url/*, ...*/) { + var self = MochiKit.Async; + url = self._buildURL.apply(self, arguments); + var d = self.doXHR(url, { + 'mimeType': 'text/plain', + 'headers': [['Accept', 'application/json']] + }); + d = d.addCallback(self.evalJSONRequest); + return d; + }, + + /** @id MochiKit.Async.wait */ + wait: function (seconds, /* optional */value) { + var d = new MochiKit.Async.Deferred(); + var m = MochiKit.Base; + if (typeof(value) != 'undefined') { + d.addCallback(function () { return value; }); + } + var timeout = setTimeout( + m.bind("callback", d), + Math.floor(seconds * 1000)); + d.canceller = function () { + try { + clearTimeout(timeout); + } catch (e) { + // pass + } + }; + return d; + }, + + /** @id MochiKit.Async.callLater */ + callLater: function (seconds, func) { + var m = MochiKit.Base; + var pfunc = m.partial.apply(m, m.extend(null, arguments, 1)); + return MochiKit.Async.wait(seconds).addCallback( + function (res) { return pfunc(); } + ); + } +}); + + +/** @id MochiKit.Async.DeferredLock */ +MochiKit.Async.DeferredLock = function () { + this.waiting = []; + this.locked = false; + this.id = this._nextId(); +}; + +MochiKit.Async.DeferredLock.prototype = { + __class__: MochiKit.Async.DeferredLock, + /** @id MochiKit.Async.DeferredLock.prototype.acquire */ + acquire: function () { + var d = new MochiKit.Async.Deferred(); + if (this.locked) { + this.waiting.push(d); + } else { + this.locked = true; + d.callback(this); + } + return d; + }, + /** @id MochiKit.Async.DeferredLock.prototype.release */ + release: function () { + if (!this.locked) { + throw TypeError("Tried to release an unlocked DeferredLock"); + } + this.locked = false; + if (this.waiting.length > 0) { + this.locked = true; + this.waiting.shift().callback(this); + } + }, + _nextId: MochiKit.Base.counter(), + repr: function () { + var state; + if (this.locked) { + state = 'locked, ' + this.waiting.length + ' waiting'; + } else { + state = 'unlocked'; + } + return 'DeferredLock(' + this.id + ', ' + state + ')'; + }, + toString: MochiKit.Base.forwardCall("repr") + +}; + +/** @id MochiKit.Async.DeferredList */ +MochiKit.Async.DeferredList = function (list, /* optional */fireOnOneCallback, fireOnOneErrback, consumeErrors, canceller) { + + // call parent constructor + MochiKit.Async.Deferred.apply(this, [canceller]); + + this.list = list; + var resultList = []; + this.resultList = resultList; + + this.finishedCount = 0; + this.fireOnOneCallback = fireOnOneCallback; + this.fireOnOneErrback = fireOnOneErrback; + this.consumeErrors = consumeErrors; + + var cb = MochiKit.Base.bind(this._cbDeferred, this); + for (var i = 0; i < list.length; i++) { + var d = list[i]; + resultList.push(undefined); + d.addCallback(cb, i, true); + d.addErrback(cb, i, false); + } + + if (list.length === 0 && !fireOnOneCallback) { + this.callback(this.resultList); + } + +}; + +MochiKit.Async.DeferredList.prototype = new MochiKit.Async.Deferred(); + +MochiKit.Async.DeferredList.prototype._cbDeferred = function (index, succeeded, result) { + this.resultList[index] = [succeeded, result]; + this.finishedCount += 1; + if (this.fired == -1) { + if (succeeded && this.fireOnOneCallback) { + this.callback([index, result]); + } else if (!succeeded && this.fireOnOneErrback) { + this.errback(result); + } else if (this.finishedCount == this.list.length) { + this.callback(this.resultList); + } + } + if (!succeeded && this.consumeErrors) { + result = null; + } + return result; +}; + +/** @id MochiKit.Async.gatherResults */ +MochiKit.Async.gatherResults = function (deferredList) { + var d = new MochiKit.Async.DeferredList(deferredList, false, true, false); + d.addCallback(function (results) { + var ret = []; + for (var i = 0; i < results.length; i++) { + ret.push(results[i][1]); + } + return ret; + }); + return d; +}; + +/** @id MochiKit.Async.maybeDeferred */ +MochiKit.Async.maybeDeferred = function (func) { + var self = MochiKit.Async; + var result; + try { + var r = func.apply(null, MochiKit.Base.extend([], arguments, 1)); + if (r instanceof self.Deferred) { + result = r; + } else if (r instanceof Error) { + result = self.fail(r); + } else { + result = self.succeed(r); + } + } catch (e) { + result = self.fail(e); + } + return result; +}; + + +MochiKit.Async.EXPORT = [ + "AlreadyCalledError", + "CancelledError", + "BrowserComplianceError", + "GenericError", + "XMLHttpRequestError", + "Deferred", + "succeed", + "fail", + "getXMLHttpRequest", + "doSimpleXMLHttpRequest", + "loadJSONDoc", + "wait", + "callLater", + "sendXMLHttpRequest", + "DeferredLock", + "DeferredList", + "gatherResults", + "maybeDeferred", + "doXHR" +]; + +MochiKit.Async.EXPORT_OK = [ + "evalJSONRequest" +]; + +MochiKit.Async.__new__ = function () { + var m = MochiKit.Base; + var ne = m.partial(m._newNamedError, this); + + ne("AlreadyCalledError", + /** @id MochiKit.Async.AlreadyCalledError */ + function (deferred) { + /*** + + Raised by the Deferred if callback or errback happens + after it was already fired. + + ***/ + this.deferred = deferred; + } + ); + + ne("CancelledError", + /** @id MochiKit.Async.CancelledError */ + function (deferred) { + /*** + + Raised by the Deferred cancellation mechanism. + + ***/ + this.deferred = deferred; + } + ); + + ne("BrowserComplianceError", + /** @id MochiKit.Async.BrowserComplianceError */ + function (msg) { + /*** + + Raised when the JavaScript runtime is not capable of performing + the given function. Technically, this should really never be + raised because a non-conforming JavaScript runtime probably + isn't going to support exceptions in the first place. + + ***/ + this.message = msg; + } + ); + + ne("GenericError", + /** @id MochiKit.Async.GenericError */ + function (msg) { + this.message = msg; + } + ); + + ne("XMLHttpRequestError", + /** @id MochiKit.Async.XMLHttpRequestError */ + function (req, msg) { + /*** + + Raised when an XMLHttpRequest does not complete for any reason. + + ***/ + this.req = req; + this.message = msg; + try { + // Strange but true that this can raise in some cases. + this.number = req.status; + } catch (e) { + // pass + } + } + ); + + + this.EXPORT_TAGS = { + ":common": this.EXPORT, + ":all": m.concat(this.EXPORT, this.EXPORT_OK) + }; + + m.nameFunctions(this); + +}; + +MochiKit.Async.__new__(); + +MochiKit.Base._exportSymbols(this, MochiKit.Async); diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Base.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Base.js new file mode 100644 index 0000000000..39b151c589 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Base.js @@ -0,0 +1,1489 @@ +/*** + +MochiKit.Base 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide("MochiKit.Base"); +} +if (typeof(MochiKit) == 'undefined') { + MochiKit = {}; +} +if (typeof(MochiKit.Base) == 'undefined') { + MochiKit.Base = {}; +} +if (typeof(MochiKit.__export__) == "undefined") { + MochiKit.__export__ = (MochiKit.__compat__ || + (typeof(JSAN) == 'undefined' && typeof(dojo) == 'undefined') + ); +} + +MochiKit.Base.VERSION = "1.4.2"; +MochiKit.Base.NAME = "MochiKit.Base"; +/** @id MochiKit.Base.update */ +MochiKit.Base.update = function (self, obj/*, ... */) { + if (self === null || self === undefined) { + self = {}; + } + for (var i = 1; i < arguments.length; i++) { + var o = arguments[i]; + if (typeof(o) != 'undefined' && o !== null) { + for (var k in o) { + self[k] = o[k]; + } + } + } + return self; +}; + +MochiKit.Base.update(MochiKit.Base, { + __repr__: function () { + return "[" + this.NAME + " " + this.VERSION + "]"; + }, + + toString: function () { + return this.__repr__(); + }, + + /** @id MochiKit.Base.camelize */ + camelize: function (selector) { + /* from dojo.style.toCamelCase */ + var arr = selector.split('-'); + var cc = arr[0]; + for (var i = 1; i < arr.length; i++) { + cc += arr[i].charAt(0).toUpperCase() + arr[i].substring(1); + } + return cc; + }, + + /** @id MochiKit.Base.counter */ + counter: function (n/* = 1 */) { + if (arguments.length === 0) { + n = 1; + } + return function () { + return n++; + }; + }, + + /** @id MochiKit.Base.clone */ + clone: function (obj) { + var me = arguments.callee; + if (arguments.length == 1) { + me.prototype = obj; + return new me(); + } + }, + + _deps: function(module, deps) { + if (!(module in MochiKit)) { + MochiKit[module] = {}; + } + + if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.' + module); + } + for (var i = 0; i < deps.length; i++) { + if (typeof(dojo) != 'undefined') { + dojo.require('MochiKit.' + deps[i]); + } + if (typeof(JSAN) != 'undefined') { + JSAN.use('MochiKit.' + deps[i], []); + } + if (!(deps[i] in MochiKit)) { + throw 'MochiKit.' + module + ' depends on MochiKit.' + deps[i] + '!' + } + } + }, + + _flattenArray: function (res, lst) { + for (var i = 0; i < lst.length; i++) { + var o = lst[i]; + if (o instanceof Array) { + arguments.callee(res, o); + } else { + res.push(o); + } + } + return res; + }, + + /** @id MochiKit.Base.flattenArray */ + flattenArray: function (lst) { + return MochiKit.Base._flattenArray([], lst); + }, + + /** @id MochiKit.Base.flattenArguments */ + flattenArguments: function (lst/* ...*/) { + var res = []; + var m = MochiKit.Base; + var args = m.extend(null, arguments); + while (args.length) { + var o = args.shift(); + if (o && typeof(o) == "object" && typeof(o.length) == "number") { + for (var i = o.length - 1; i >= 0; i--) { + args.unshift(o[i]); + } + } else { + res.push(o); + } + } + return res; + }, + + /** @id MochiKit.Base.extend */ + extend: function (self, obj, /* optional */skip) { + // Extend an array with an array-like object starting + // from the skip index + if (!skip) { + skip = 0; + } + if (obj) { + // allow iterable fall-through, but skip the full isArrayLike + // check for speed, this is called often. + var l = obj.length; + if (typeof(l) != 'number' /* !isArrayLike(obj) */) { + if (typeof(MochiKit.Iter) != "undefined") { + obj = MochiKit.Iter.list(obj); + l = obj.length; + } else { + throw new TypeError("Argument not an array-like and MochiKit.Iter not present"); + } + } + if (!self) { + self = []; + } + for (var i = skip; i < l; i++) { + self.push(obj[i]); + } + } + // This mutates, but it's convenient to return because + // it's often used like a constructor when turning some + // ghetto array-like to a real array + return self; + }, + + + /** @id MochiKit.Base.updatetree */ + updatetree: function (self, obj/*, ...*/) { + if (self === null || self === undefined) { + self = {}; + } + for (var i = 1; i < arguments.length; i++) { + var o = arguments[i]; + if (typeof(o) != 'undefined' && o !== null) { + for (var k in o) { + var v = o[k]; + if (typeof(self[k]) == 'object' && typeof(v) == 'object') { + arguments.callee(self[k], v); + } else { + self[k] = v; + } + } + } + } + return self; + }, + + /** @id MochiKit.Base.setdefault */ + setdefault: function (self, obj/*, ...*/) { + if (self === null || self === undefined) { + self = {}; + } + for (var i = 1; i < arguments.length; i++) { + var o = arguments[i]; + for (var k in o) { + if (!(k in self)) { + self[k] = o[k]; + } + } + } + return self; + }, + + /** @id MochiKit.Base.keys */ + keys: function (obj) { + var rval = []; + for (var prop in obj) { + rval.push(prop); + } + return rval; + }, + + /** @id MochiKit.Base.values */ + values: function (obj) { + var rval = []; + for (var prop in obj) { + rval.push(obj[prop]); + } + return rval; + }, + + /** @id MochiKit.Base.items */ + items: function (obj) { + var rval = []; + var e; + for (var prop in obj) { + var v; + try { + v = obj[prop]; + } catch (e) { + continue; + } + rval.push([prop, v]); + } + return rval; + }, + + + _newNamedError: function (module, name, func) { + func.prototype = new MochiKit.Base.NamedError(module.NAME + "." + name); + module[name] = func; + }, + + + /** @id MochiKit.Base.operator */ + operator: { + // unary logic operators + /** @id MochiKit.Base.truth */ + truth: function (a) { return !!a; }, + /** @id MochiKit.Base.lognot */ + lognot: function (a) { return !a; }, + /** @id MochiKit.Base.identity */ + identity: function (a) { return a; }, + + // bitwise unary operators + /** @id MochiKit.Base.not */ + not: function (a) { return ~a; }, + /** @id MochiKit.Base.neg */ + neg: function (a) { return -a; }, + + // binary operators + /** @id MochiKit.Base.add */ + add: function (a, b) { return a + b; }, + /** @id MochiKit.Base.sub */ + sub: function (a, b) { return a - b; }, + /** @id MochiKit.Base.div */ + div: function (a, b) { return a / b; }, + /** @id MochiKit.Base.mod */ + mod: function (a, b) { return a % b; }, + /** @id MochiKit.Base.mul */ + mul: function (a, b) { return a * b; }, + + // bitwise binary operators + /** @id MochiKit.Base.and */ + and: function (a, b) { return a & b; }, + /** @id MochiKit.Base.or */ + or: function (a, b) { return a | b; }, + /** @id MochiKit.Base.xor */ + xor: function (a, b) { return a ^ b; }, + /** @id MochiKit.Base.lshift */ + lshift: function (a, b) { return a << b; }, + /** @id MochiKit.Base.rshift */ + rshift: function (a, b) { return a >> b; }, + /** @id MochiKit.Base.zrshift */ + zrshift: function (a, b) { return a >>> b; }, + + // near-worthless built-in comparators + /** @id MochiKit.Base.eq */ + eq: function (a, b) { return a == b; }, + /** @id MochiKit.Base.ne */ + ne: function (a, b) { return a != b; }, + /** @id MochiKit.Base.gt */ + gt: function (a, b) { return a > b; }, + /** @id MochiKit.Base.ge */ + ge: function (a, b) { return a >= b; }, + /** @id MochiKit.Base.lt */ + lt: function (a, b) { return a < b; }, + /** @id MochiKit.Base.le */ + le: function (a, b) { return a <= b; }, + + // strict built-in comparators + seq: function (a, b) { return a === b; }, + sne: function (a, b) { return a !== b; }, + + // compare comparators + /** @id MochiKit.Base.ceq */ + ceq: function (a, b) { return MochiKit.Base.compare(a, b) === 0; }, + /** @id MochiKit.Base.cne */ + cne: function (a, b) { return MochiKit.Base.compare(a, b) !== 0; }, + /** @id MochiKit.Base.cgt */ + cgt: function (a, b) { return MochiKit.Base.compare(a, b) == 1; }, + /** @id MochiKit.Base.cge */ + cge: function (a, b) { return MochiKit.Base.compare(a, b) != -1; }, + /** @id MochiKit.Base.clt */ + clt: function (a, b) { return MochiKit.Base.compare(a, b) == -1; }, + /** @id MochiKit.Base.cle */ + cle: function (a, b) { return MochiKit.Base.compare(a, b) != 1; }, + + // binary logical operators + /** @id MochiKit.Base.logand */ + logand: function (a, b) { return a && b; }, + /** @id MochiKit.Base.logor */ + logor: function (a, b) { return a || b; }, + /** @id MochiKit.Base.contains */ + contains: function (a, b) { return b in a; } + }, + + /** @id MochiKit.Base.forwardCall */ + forwardCall: function (func) { + return function () { + return this[func].apply(this, arguments); + }; + }, + + /** @id MochiKit.Base.itemgetter */ + itemgetter: function (func) { + return function (arg) { + return arg[func]; + }; + }, + + /** @id MochiKit.Base.typeMatcher */ + typeMatcher: function (/* typ */) { + var types = {}; + for (var i = 0; i < arguments.length; i++) { + var typ = arguments[i]; + types[typ] = typ; + } + return function () { + for (var i = 0; i < arguments.length; i++) { + if (!(typeof(arguments[i]) in types)) { + return false; + } + } + return true; + }; + }, + + /** @id MochiKit.Base.isNull */ + isNull: function (/* ... */) { + for (var i = 0; i < arguments.length; i++) { + if (arguments[i] !== null) { + return false; + } + } + return true; + }, + + /** @id MochiKit.Base.isUndefinedOrNull */ + isUndefinedOrNull: function (/* ... */) { + for (var i = 0; i < arguments.length; i++) { + var o = arguments[i]; + if (!(typeof(o) == 'undefined' || o === null)) { + return false; + } + } + return true; + }, + + /** @id MochiKit.Base.isEmpty */ + isEmpty: function (obj) { + return !MochiKit.Base.isNotEmpty.apply(this, arguments); + }, + + /** @id MochiKit.Base.isNotEmpty */ + isNotEmpty: function (obj) { + for (var i = 0; i < arguments.length; i++) { + var o = arguments[i]; + if (!(o && o.length)) { + return false; + } + } + return true; + }, + + /** @id MochiKit.Base.isArrayLike */ + isArrayLike: function () { + for (var i = 0; i < arguments.length; i++) { + var o = arguments[i]; + var typ = typeof(o); + if ( + (typ != 'object' && !(typ == 'function' && typeof(o.item) == 'function')) || + o === null || + typeof(o.length) != 'number' || + o.nodeType === 3 || + o.nodeType === 4 + ) { + return false; + } + } + return true; + }, + + /** @id MochiKit.Base.isDateLike */ + isDateLike: function () { + for (var i = 0; i < arguments.length; i++) { + var o = arguments[i]; + if (typeof(o) != "object" || o === null + || typeof(o.getTime) != 'function') { + return false; + } + } + return true; + }, + + + /** @id MochiKit.Base.xmap */ + xmap: function (fn/*, obj... */) { + if (fn === null) { + return MochiKit.Base.extend(null, arguments, 1); + } + var rval = []; + for (var i = 1; i < arguments.length; i++) { + rval.push(fn(arguments[i])); + } + return rval; + }, + + /** @id MochiKit.Base.map */ + map: function (fn, lst/*, lst... */) { + var m = MochiKit.Base; + var itr = MochiKit.Iter; + var isArrayLike = m.isArrayLike; + if (arguments.length <= 2) { + // allow an iterable to be passed + if (!isArrayLike(lst)) { + if (itr) { + // fast path for map(null, iterable) + lst = itr.list(lst); + if (fn === null) { + return lst; + } + } else { + throw new TypeError("Argument not an array-like and MochiKit.Iter not present"); + } + } + // fast path for map(null, lst) + if (fn === null) { + return m.extend(null, lst); + } + // disabled fast path for map(fn, lst) + /* + if (false && typeof(Array.prototype.map) == 'function') { + // Mozilla fast-path + return Array.prototype.map.call(lst, fn); + } + */ + var rval = []; + for (var i = 0; i < lst.length; i++) { + rval.push(fn(lst[i])); + } + return rval; + } else { + // default for map(null, ...) is zip(...) + if (fn === null) { + fn = Array; + } + var length = null; + for (i = 1; i < arguments.length; i++) { + // allow iterables to be passed + if (!isArrayLike(arguments[i])) { + if (itr) { + return itr.list(itr.imap.apply(null, arguments)); + } else { + throw new TypeError("Argument not an array-like and MochiKit.Iter not present"); + } + } + // find the minimum length + var l = arguments[i].length; + if (length === null || length > l) { + length = l; + } + } + rval = []; + for (i = 0; i < length; i++) { + var args = []; + for (var j = 1; j < arguments.length; j++) { + args.push(arguments[j][i]); + } + rval.push(fn.apply(this, args)); + } + return rval; + } + }, + + /** @id MochiKit.Base.xfilter */ + xfilter: function (fn/*, obj... */) { + var rval = []; + if (fn === null) { + fn = MochiKit.Base.operator.truth; + } + for (var i = 1; i < arguments.length; i++) { + var o = arguments[i]; + if (fn(o)) { + rval.push(o); + } + } + return rval; + }, + + /** @id MochiKit.Base.filter */ + filter: function (fn, lst, self) { + var rval = []; + // allow an iterable to be passed + var m = MochiKit.Base; + if (!m.isArrayLike(lst)) { + if (MochiKit.Iter) { + lst = MochiKit.Iter.list(lst); + } else { + throw new TypeError("Argument not an array-like and MochiKit.Iter not present"); + } + } + if (fn === null) { + fn = m.operator.truth; + } + if (typeof(Array.prototype.filter) == 'function') { + // Mozilla fast-path + return Array.prototype.filter.call(lst, fn, self); + } else if (typeof(self) == 'undefined' || self === null) { + for (var i = 0; i < lst.length; i++) { + var o = lst[i]; + if (fn(o)) { + rval.push(o); + } + } + } else { + for (i = 0; i < lst.length; i++) { + o = lst[i]; + if (fn.call(self, o)) { + rval.push(o); + } + } + } + return rval; + }, + + + _wrapDumbFunction: function (func) { + return function () { + // fast path! + switch (arguments.length) { + case 0: return func(); + case 1: return func(arguments[0]); + case 2: return func(arguments[0], arguments[1]); + case 3: return func(arguments[0], arguments[1], arguments[2]); + } + var args = []; + for (var i = 0; i < arguments.length; i++) { + args.push("arguments[" + i + "]"); + } + return eval("(func(" + args.join(",") + "))"); + }; + }, + + /** @id MochiKit.Base.methodcaller */ + methodcaller: function (func/*, args... */) { + var args = MochiKit.Base.extend(null, arguments, 1); + if (typeof(func) == "function") { + return function (obj) { + return func.apply(obj, args); + }; + } else { + return function (obj) { + return obj[func].apply(obj, args); + }; + } + }, + + /** @id MochiKit.Base.method */ + method: function (self, func) { + var m = MochiKit.Base; + return m.bind.apply(this, m.extend([func, self], arguments, 2)); + }, + + /** @id MochiKit.Base.compose */ + compose: function (f1, f2/*, f3, ... fN */) { + var fnlist = []; + var m = MochiKit.Base; + if (arguments.length === 0) { + throw new TypeError("compose() requires at least one argument"); + } + for (var i = 0; i < arguments.length; i++) { + var fn = arguments[i]; + if (typeof(fn) != "function") { + throw new TypeError(m.repr(fn) + " is not a function"); + } + fnlist.push(fn); + } + return function () { + var args = arguments; + for (var i = fnlist.length - 1; i >= 0; i--) { + args = [fnlist[i].apply(this, args)]; + } + return args[0]; + }; + }, + + /** @id MochiKit.Base.bind */ + bind: function (func, self/* args... */) { + if (typeof(func) == "string") { + func = self[func]; + } + var im_func = func.im_func; + var im_preargs = func.im_preargs; + var im_self = func.im_self; + var m = MochiKit.Base; + if (typeof(func) == "function" && typeof(func.apply) == "undefined") { + // this is for cases where JavaScript sucks ass and gives you a + // really dumb built-in function like alert() that doesn't have + // an apply + func = m._wrapDumbFunction(func); + } + if (typeof(im_func) != 'function') { + im_func = func; + } + if (typeof(self) != 'undefined') { + im_self = self; + } + if (typeof(im_preargs) == 'undefined') { + im_preargs = []; + } else { + im_preargs = im_preargs.slice(); + } + m.extend(im_preargs, arguments, 2); + var newfunc = function () { + var args = arguments; + var me = arguments.callee; + if (me.im_preargs.length > 0) { + args = m.concat(me.im_preargs, args); + } + var self = me.im_self; + if (!self) { + self = this; + } + return me.im_func.apply(self, args); + }; + newfunc.im_self = im_self; + newfunc.im_func = im_func; + newfunc.im_preargs = im_preargs; + return newfunc; + }, + + /** @id MochiKit.Base.bindLate */ + bindLate: function (func, self/* args... */) { + var m = MochiKit.Base; + if (typeof(func) != "string") { + return m.bind.apply(this, arguments); + } + var im_preargs = m.extend([], arguments, 2); + var newfunc = function () { + var args = arguments; + var me = arguments.callee; + if (me.im_preargs.length > 0) { + args = m.concat(me.im_preargs, args); + } + var self = me.im_self; + if (!self) { + self = this; + } + return self[me.im_func].apply(self, args); + }; + newfunc.im_self = self; + newfunc.im_func = func; + newfunc.im_preargs = im_preargs; + return newfunc; + }, + + /** @id MochiKit.Base.bindMethods */ + bindMethods: function (self) { + var bind = MochiKit.Base.bind; + for (var k in self) { + var func = self[k]; + if (typeof(func) == 'function') { + self[k] = bind(func, self); + } + } + }, + + /** @id MochiKit.Base.registerComparator */ + registerComparator: function (name, check, comparator, /* optional */ override) { + MochiKit.Base.comparatorRegistry.register(name, check, comparator, override); + }, + + _primitives: {'boolean': true, 'string': true, 'number': true}, + + /** @id MochiKit.Base.compare */ + compare: function (a, b) { + if (a == b) { + return 0; + } + var aIsNull = (typeof(a) == 'undefined' || a === null); + var bIsNull = (typeof(b) == 'undefined' || b === null); + if (aIsNull && bIsNull) { + return 0; + } else if (aIsNull) { + return -1; + } else if (bIsNull) { + return 1; + } + var m = MochiKit.Base; + // bool, number, string have meaningful comparisons + var prim = m._primitives; + if (!(typeof(a) in prim && typeof(b) in prim)) { + try { + return m.comparatorRegistry.match(a, b); + } catch (e) { + if (e != m.NotFound) { + throw e; + } + } + } + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } + // These types can't be compared + var repr = m.repr; + throw new TypeError(repr(a) + " and " + repr(b) + " can not be compared"); + }, + + /** @id MochiKit.Base.compareDateLike */ + compareDateLike: function (a, b) { + return MochiKit.Base.compare(a.getTime(), b.getTime()); + }, + + /** @id MochiKit.Base.compareArrayLike */ + compareArrayLike: function (a, b) { + var compare = MochiKit.Base.compare; + var count = a.length; + var rval = 0; + if (count > b.length) { + rval = 1; + count = b.length; + } else if (count < b.length) { + rval = -1; + } + for (var i = 0; i < count; i++) { + var cmp = compare(a[i], b[i]); + if (cmp) { + return cmp; + } + } + return rval; + }, + + /** @id MochiKit.Base.registerRepr */ + registerRepr: function (name, check, wrap, /* optional */override) { + MochiKit.Base.reprRegistry.register(name, check, wrap, override); + }, + + /** @id MochiKit.Base.repr */ + repr: function (o) { + if (typeof(o) == "undefined") { + return "undefined"; + } else if (o === null) { + return "null"; + } + try { + if (typeof(o.__repr__) == 'function') { + return o.__repr__(); + } else if (typeof(o.repr) == 'function' && o.repr != arguments.callee) { + return o.repr(); + } + return MochiKit.Base.reprRegistry.match(o); + } catch (e) { + if (typeof(o.NAME) == 'string' && (
+ o.toString == Function.prototype.toString ||
+ o.toString == Object.prototype.toString
+ )) {
+ return o.NAME;
+ } + } + try { + var ostring = (o + ""); + } catch (e) { + return "[" + typeof(o) + "]"; + } + if (typeof(o) == "function") { + ostring = ostring.replace(/^\s+/, "").replace(/\s+/g, " "); + ostring = ostring.replace(/,(\S)/, ", $1"); + var idx = ostring.indexOf("{"); + if (idx != -1) { + ostring = ostring.substr(0, idx) + "{...}"; + } + } + return ostring; + }, + + /** @id MochiKit.Base.reprArrayLike */ + reprArrayLike: function (o) { + var m = MochiKit.Base; + return "[" + m.map(m.repr, o).join(", ") + "]"; + }, + + /** @id MochiKit.Base.reprString */ + reprString: function (o) { + return ('"' + o.replace(/(["\\])/g, '\\$1') + '"' + ).replace(/[\f]/g, "\\f" + ).replace(/[\b]/g, "\\b" + ).replace(/[\n]/g, "\\n" + ).replace(/[\t]/g, "\\t" + ).replace(/[\v]/g, "\\v" + ).replace(/[\r]/g, "\\r"); + }, + + /** @id MochiKit.Base.reprNumber */ + reprNumber: function (o) { + return o + ""; + }, + + /** @id MochiKit.Base.registerJSON */ + registerJSON: function (name, check, wrap, /* optional */override) { + MochiKit.Base.jsonRegistry.register(name, check, wrap, override); + }, + + + /** @id MochiKit.Base.evalJSON */ + evalJSON: function () { + return eval("(" + MochiKit.Base._filterJSON(arguments[0]) + ")"); + }, + + _filterJSON: function (s) { + var m = s.match(/^\s*\/\*(.*)\*\/\s*$/); + if (m) { + return m[1]; + } + return s; + }, + + /** @id MochiKit.Base.serializeJSON */ + serializeJSON: function (o) { + var objtype = typeof(o); + if (objtype == "number" || objtype == "boolean") { + return o + ""; + } else if (o === null) { + return "null"; + } else if (objtype == "string") { + var res = ""; + for (var i = 0; i < o.length; i++) { + var c = o.charAt(i); + if (c == '\"') { + res += '\\"'; + } else if (c == '\\') { + res += '\\\\'; + } else if (c == '\b') { + res += '\\b'; + } else if (c == '\f') { + res += '\\f'; + } else if (c == '\n') { + res += '\\n'; + } else if (c == '\r') { + res += '\\r'; + } else if (c == '\t') { + res += '\\t'; + } else if (o.charCodeAt(i) <= 0x1F) { + var hex = o.charCodeAt(i).toString(16); + if (hex.length < 2) { + hex = '0' + hex; + } + res += '\\u00' + hex.toUpperCase(); + } else { + res += c; + } + } + return '"' + res + '"'; + } + // recurse + var me = arguments.callee; + // short-circuit for objects that support "json" serialization + // if they return "self" then just pass-through... + var newObj; + if (typeof(o.__json__) == "function") { + newObj = o.__json__(); + if (o !== newObj) { + return me(newObj); + } + } + if (typeof(o.json) == "function") { + newObj = o.json(); + if (o !== newObj) { + return me(newObj); + } + } + // array + if (objtype != "function" && typeof(o.length) == "number") { + var res = []; + for (var i = 0; i < o.length; i++) { + var val = me(o[i]); + if (typeof(val) != "string") { + // skip non-serializable values + continue; + } + res.push(val); + } + return "[" + res.join(", ") + "]"; + } + // look in the registry + var m = MochiKit.Base; + try { + newObj = m.jsonRegistry.match(o); + if (o !== newObj) { + return me(newObj); + } + } catch (e) { + if (e != m.NotFound) { + // something really bad happened + throw e; + } + } + // undefined is outside of the spec + if (objtype == "undefined") { + throw new TypeError("undefined can not be serialized as JSON"); + } + // it's a function with no adapter, bad + if (objtype == "function") { + return null; + } + // generic object code path + res = []; + for (var k in o) { + var useKey; + if (typeof(k) == "number") { + useKey = '"' + k + '"'; + } else if (typeof(k) == "string") { + useKey = me(k); + } else { + // skip non-string or number keys + continue; + } + val = me(o[k]); + if (typeof(val) != "string") { + // skip non-serializable values + continue; + } + res.push(useKey + ":" + val); + } + return "{" + res.join(", ") + "}"; + }, + + + /** @id MochiKit.Base.objEqual */ + objEqual: function (a, b) { + return (MochiKit.Base.compare(a, b) === 0); + }, + + /** @id MochiKit.Base.arrayEqual */ + arrayEqual: function (self, arr) { + if (self.length != arr.length) { + return false; + } + return (MochiKit.Base.compare(self, arr) === 0); + }, + + /** @id MochiKit.Base.concat */ + concat: function (/* lst... */) { + var rval = []; + var extend = MochiKit.Base.extend; + for (var i = 0; i < arguments.length; i++) { + extend(rval, arguments[i]); + } + return rval; + }, + + /** @id MochiKit.Base.keyComparator */ + keyComparator: function (key/* ... */) { + // fast-path for single key comparisons + var m = MochiKit.Base; + var compare = m.compare; + if (arguments.length == 1) { + return function (a, b) { + return compare(a[key], b[key]); + }; + } + var compareKeys = m.extend(null, arguments); + return function (a, b) { + var rval = 0; + // keep comparing until something is inequal or we run out of + // keys to compare + for (var i = 0; (rval === 0) && (i < compareKeys.length); i++) { + var key = compareKeys[i]; + rval = compare(a[key], b[key]); + } + return rval; + }; + }, + + /** @id MochiKit.Base.reverseKeyComparator */ + reverseKeyComparator: function (key) { + var comparator = MochiKit.Base.keyComparator.apply(this, arguments); + return function (a, b) { + return comparator(b, a); + }; + }, + + /** @id MochiKit.Base.partial */ + partial: function (func) { + var m = MochiKit.Base; + return m.bind.apply(this, m.extend([func, undefined], arguments, 1)); + }, + + /** @id MochiKit.Base.listMinMax */ + listMinMax: function (which, lst) { + if (lst.length === 0) { + return null; + } + var cur = lst[0]; + var compare = MochiKit.Base.compare; + for (var i = 1; i < lst.length; i++) { + var o = lst[i]; + if (compare(o, cur) == which) { + cur = o; + } + } + return cur; + }, + + /** @id MochiKit.Base.objMax */ + objMax: function (/* obj... */) { + return MochiKit.Base.listMinMax(1, arguments); + }, + + /** @id MochiKit.Base.objMin */ + objMin: function (/* obj... */) { + return MochiKit.Base.listMinMax(-1, arguments); + }, + + /** @id MochiKit.Base.findIdentical */ + findIdentical: function (lst, value, start/* = 0 */, /* optional */end) { + if (typeof(end) == "undefined" || end === null) { + end = lst.length; + } + if (typeof(start) == "undefined" || start === null) { + start = 0; + } + for (var i = start; i < end; i++) { + if (lst[i] === value) { + return i; + } + } + return -1; + }, + + /** @id MochiKit.Base.mean */ + mean: function(/* lst... */) { + /* http://www.nist.gov/dads/HTML/mean.html */ + var sum = 0; + + var m = MochiKit.Base; + var args = m.extend(null, arguments); + var count = args.length; + + while (args.length) { + var o = args.shift(); + if (o && typeof(o) == "object" && typeof(o.length) == "number") { + count += o.length - 1; + for (var i = o.length - 1; i >= 0; i--) { + sum += o[i]; + } + } else { + sum += o; + } + } + + if (count <= 0) { + throw new TypeError('mean() requires at least one argument'); + } + + return sum/count; + }, + + /** @id MochiKit.Base.median */ + median: function(/* lst... */) { + /* http://www.nist.gov/dads/HTML/median.html */ + var data = MochiKit.Base.flattenArguments(arguments); + if (data.length === 0) { + throw new TypeError('median() requires at least one argument'); + } + data.sort(compare); + if (data.length % 2 == 0) { + var upper = data.length / 2; + return (data[upper] + data[upper - 1]) / 2; + } else { + return data[(data.length - 1) / 2]; + } + }, + + /** @id MochiKit.Base.findValue */ + findValue: function (lst, value, start/* = 0 */, /* optional */end) { + if (typeof(end) == "undefined" || end === null) { + end = lst.length; + } + if (typeof(start) == "undefined" || start === null) { + start = 0; + } + var cmp = MochiKit.Base.compare; + for (var i = start; i < end; i++) { + if (cmp(lst[i], value) === 0) { + return i; + } + } + return -1; + }, + + /** @id MochiKit.Base.nodeWalk */ + nodeWalk: function (node, visitor) { + var nodes = [node]; + var extend = MochiKit.Base.extend; + while (nodes.length) { + var res = visitor(nodes.shift()); + if (res) { + extend(nodes, res); + } + } + }, + + + /** @id MochiKit.Base.nameFunctions */ + nameFunctions: function (namespace) { + var base = namespace.NAME; + if (typeof(base) == 'undefined') { + base = ''; + } else { + base = base + '.'; + } + for (var name in namespace) { + var o = namespace[name]; + if (typeof(o) == 'function' && typeof(o.NAME) == 'undefined') { + try { + o.NAME = base + name; + } catch (e) { + // pass + } + } + } + }, + + + /** @id MochiKit.Base.queryString */ + queryString: function (names, values) { + // check to see if names is a string or a DOM element, and if + // MochiKit.DOM is available. If so, drop it like it's a form + // Ugliest conditional in MochiKit? Probably! + if (typeof(MochiKit.DOM) != "undefined" && arguments.length == 1 + && (typeof(names) == "string" || ( + typeof(names.nodeType) != "undefined" && names.nodeType > 0 + )) + ) { + var kv = MochiKit.DOM.formContents(names); + names = kv[0]; + values = kv[1]; + } else if (arguments.length == 1) { + // Allow the return value of formContents to be passed directly + if (typeof(names.length) == "number" && names.length == 2) { + return arguments.callee(names[0], names[1]); + } + var o = names; + names = []; + values = []; + for (var k in o) { + var v = o[k]; + if (typeof(v) == "function") { + continue; + } else if (MochiKit.Base.isArrayLike(v)){ + for (var i = 0; i < v.length; i++) { + names.push(k); + values.push(v[i]); + } + } else { + names.push(k); + values.push(v); + } + } + } + var rval = []; + var len = Math.min(names.length, values.length); + var urlEncode = MochiKit.Base.urlEncode; + for (var i = 0; i < len; i++) { + v = values[i]; + if (typeof(v) != 'undefined' && v !== null) { + rval.push(urlEncode(names[i]) + "=" + urlEncode(v)); + } + } + return rval.join("&"); + }, + + + /** @id MochiKit.Base.parseQueryString */ + parseQueryString: function (encodedString, useArrays) { + // strip a leading '?' from the encoded string + var qstr = (encodedString.charAt(0) == "?") + ? encodedString.substring(1) + : encodedString; + var pairs = qstr.replace(/\+/g, "%20").split(/\&\;|\&\#38\;|\&|\&/); + var o = {}; + var decode; + if (typeof(decodeURIComponent) != "undefined") { + decode = decodeURIComponent; + } else { + decode = unescape; + } + if (useArrays) { + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i].split("="); + var name = decode(pair.shift()); + if (!name) { + continue; + } + var arr = o[name]; + if (!(arr instanceof Array)) { + arr = []; + o[name] = arr; + } + arr.push(decode(pair.join("="))); + } + } else { + for (i = 0; i < pairs.length; i++) { + pair = pairs[i].split("="); + var name = pair.shift(); + if (!name) { + continue; + } + o[decode(name)] = decode(pair.join("=")); + } + } + return o; + } +}); + +/** @id MochiKit.Base.AdapterRegistry */ +MochiKit.Base.AdapterRegistry = function () { + this.pairs = []; +}; + +MochiKit.Base.AdapterRegistry.prototype = { + /** @id MochiKit.Base.AdapterRegistry.prototype.register */ + register: function (name, check, wrap, /* optional */ override) { + if (override) { + this.pairs.unshift([name, check, wrap]); + } else { + this.pairs.push([name, check, wrap]); + } + }, + + /** @id MochiKit.Base.AdapterRegistry.prototype.match */ + match: function (/* ... */) { + for (var i = 0; i < this.pairs.length; i++) { + var pair = this.pairs[i]; + if (pair[1].apply(this, arguments)) { + return pair[2].apply(this, arguments); + } + } + throw MochiKit.Base.NotFound; + }, + + /** @id MochiKit.Base.AdapterRegistry.prototype.unregister */ + unregister: function (name) { + for (var i = 0; i < this.pairs.length; i++) { + var pair = this.pairs[i]; + if (pair[0] == name) { + this.pairs.splice(i, 1); + return true; + } + } + return false; + } +}; + + +MochiKit.Base.EXPORT = [ + "flattenArray", + "noop", + "camelize", + "counter", + "clone", + "extend", + "update", + "updatetree", + "setdefault", + "keys", + "values", + "items", + "NamedError", + "operator", + "forwardCall", + "itemgetter", + "typeMatcher", + "isCallable", + "isUndefined", + "isUndefinedOrNull", + "isNull", + "isEmpty", + "isNotEmpty", + "isArrayLike", + "isDateLike", + "xmap", + "map", + "xfilter", + "filter", + "methodcaller", + "compose", + "bind", + "bindLate", + "bindMethods", + "NotFound", + "AdapterRegistry", + "registerComparator", + "compare", + "registerRepr", + "repr", + "objEqual", + "arrayEqual", + "concat", + "keyComparator", + "reverseKeyComparator", + "partial", + "merge", + "listMinMax", + "listMax", + "listMin", + "objMax", + "objMin", + "nodeWalk", + "zip", + "urlEncode", + "queryString", + "serializeJSON", + "registerJSON", + "evalJSON", + "parseQueryString", + "findValue", + "findIdentical", + "flattenArguments", + "method", + "average", + "mean", + "median" +]; + +MochiKit.Base.EXPORT_OK = [ + "nameFunctions", + "comparatorRegistry", + "reprRegistry", + "jsonRegistry", + "compareDateLike", + "compareArrayLike", + "reprArrayLike", + "reprString", + "reprNumber" +]; + +MochiKit.Base._exportSymbols = function (globals, module) { + if (!MochiKit.__export__) { + return; + } + var all = module.EXPORT_TAGS[":all"]; + for (var i = 0; i < all.length; i++) { + globals[all[i]] = module[all[i]]; + } +}; + +MochiKit.Base.__new__ = function () { + // A singleton raised when no suitable adapter is found + var m = this; + + // convenience + /** @id MochiKit.Base.noop */ + m.noop = m.operator.identity; + + // Backwards compat + m.forward = m.forwardCall; + m.find = m.findValue; + + if (typeof(encodeURIComponent) != "undefined") { + /** @id MochiKit.Base.urlEncode */ + m.urlEncode = function (unencoded) { + return encodeURIComponent(unencoded).replace(/\'/g, '%27'); + }; + } else { + m.urlEncode = function (unencoded) { + return escape(unencoded + ).replace(/\+/g, '%2B' + ).replace(/\"/g,'%22' + ).rval.replace(/\'/g, '%27'); + }; + } + + /** @id MochiKit.Base.NamedError */ + m.NamedError = function (name) { + this.message = name; + this.name = name; + }; + m.NamedError.prototype = new Error(); + m.update(m.NamedError.prototype, { + repr: function () { + if (this.message && this.message != this.name) { + return this.name + "(" + m.repr(this.message) + ")"; + } else { + return this.name + "()"; + } + }, + toString: m.forwardCall("repr") + }); + + /** @id MochiKit.Base.NotFound */ + m.NotFound = new m.NamedError("MochiKit.Base.NotFound"); + + + /** @id MochiKit.Base.listMax */ + m.listMax = m.partial(m.listMinMax, 1); + /** @id MochiKit.Base.listMin */ + m.listMin = m.partial(m.listMinMax, -1); + + /** @id MochiKit.Base.isCallable */ + m.isCallable = m.typeMatcher('function'); + /** @id MochiKit.Base.isUndefined */ + m.isUndefined = m.typeMatcher('undefined'); + + /** @id MochiKit.Base.merge */ + m.merge = m.partial(m.update, null); + /** @id MochiKit.Base.zip */ + m.zip = m.partial(m.map, null); + + /** @id MochiKit.Base.average */ + m.average = m.mean; + + /** @id MochiKit.Base.comparatorRegistry */ + m.comparatorRegistry = new m.AdapterRegistry(); + m.registerComparator("dateLike", m.isDateLike, m.compareDateLike); + m.registerComparator("arrayLike", m.isArrayLike, m.compareArrayLike); + + /** @id MochiKit.Base.reprRegistry */ + m.reprRegistry = new m.AdapterRegistry(); + m.registerRepr("arrayLike", m.isArrayLike, m.reprArrayLike); + m.registerRepr("string", m.typeMatcher("string"), m.reprString); + m.registerRepr("numbers", m.typeMatcher("number", "boolean"), m.reprNumber); + + /** @id MochiKit.Base.jsonRegistry */ + m.jsonRegistry = new m.AdapterRegistry(); + + var all = m.concat(m.EXPORT, m.EXPORT_OK); + m.EXPORT_TAGS = { + ":common": m.concat(m.EXPORT_OK), + ":all": all + }; + + m.nameFunctions(this); + +}; + +MochiKit.Base.__new__(); + +// +// XXX: Internet Explorer blows +// +if (MochiKit.__export__) { + compare = MochiKit.Base.compare; + compose = MochiKit.Base.compose; + serializeJSON = MochiKit.Base.serializeJSON; + mean = MochiKit.Base.mean; + median = MochiKit.Base.median; +} + +MochiKit.Base._exportSymbols(this, MochiKit.Base); diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Color.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Color.js new file mode 100644 index 0000000000..a0d5bd8c34 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Color.js @@ -0,0 +1,863 @@ +/*** + +MochiKit.Color 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito and others. All rights Reserved. + +***/ + +MochiKit.Base._deps('Color', ['Base', 'DOM', 'Style']); + +MochiKit.Color.NAME = "MochiKit.Color"; +MochiKit.Color.VERSION = "1.4.2"; + +MochiKit.Color.__repr__ = function () { + return "[" + this.NAME + " " + this.VERSION + "]"; +}; + +MochiKit.Color.toString = function () { + return this.__repr__(); +}; + + +/** @id MochiKit.Color.Color */ +MochiKit.Color.Color = function (red, green, blue, alpha) { + if (typeof(alpha) == 'undefined' || alpha === null) { + alpha = 1.0; + } + this.rgb = { + r: red, + g: green, + b: blue, + a: alpha + }; +}; + + +// Prototype methods + +MochiKit.Color.Color.prototype = { + + __class__: MochiKit.Color.Color, + + /** @id MochiKit.Color.Color.prototype.colorWithAlpha */ + colorWithAlpha: function (alpha) { + var rgb = this.rgb; + var m = MochiKit.Color; + return m.Color.fromRGB(rgb.r, rgb.g, rgb.b, alpha); + }, + + /** @id MochiKit.Color.Color.prototype.colorWithHue */ + colorWithHue: function (hue) { + // get an HSL model, and set the new hue... + var hsl = this.asHSL(); + hsl.h = hue; + var m = MochiKit.Color; + // convert back to RGB... + return m.Color.fromHSL(hsl); + }, + + /** @id MochiKit.Color.Color.prototype.colorWithSaturation */ + colorWithSaturation: function (saturation) { + // get an HSL model, and set the new hue... + var hsl = this.asHSL(); + hsl.s = saturation; + var m = MochiKit.Color; + // convert back to RGB... + return m.Color.fromHSL(hsl); + }, + + /** @id MochiKit.Color.Color.prototype.colorWithLightness */ + colorWithLightness: function (lightness) { + // get an HSL model, and set the new hue... + var hsl = this.asHSL(); + hsl.l = lightness; + var m = MochiKit.Color; + // convert back to RGB... + return m.Color.fromHSL(hsl); + }, + + /** @id MochiKit.Color.Color.prototype.darkerColorWithLevel */ + darkerColorWithLevel: function (level) { + var hsl = this.asHSL(); + hsl.l = Math.max(hsl.l - level, 0); + var m = MochiKit.Color; + return m.Color.fromHSL(hsl); + }, + + /** @id MochiKit.Color.Color.prototype.lighterColorWithLevel */ + lighterColorWithLevel: function (level) { + var hsl = this.asHSL(); + hsl.l = Math.min(hsl.l + level, 1); + var m = MochiKit.Color; + return m.Color.fromHSL(hsl); + }, + + /** @id MochiKit.Color.Color.prototype.blendedColor */ + blendedColor: function (other, /* optional */ fraction) { + if (typeof(fraction) == 'undefined' || fraction === null) { + fraction = 0.5; + } + var sf = 1.0 - fraction; + var s = this.rgb; + var d = other.rgb; + var df = fraction; + return MochiKit.Color.Color.fromRGB( + (s.r * sf) + (d.r * df), + (s.g * sf) + (d.g * df), + (s.b * sf) + (d.b * df), + (s.a * sf) + (d.a * df) + ); + }, + + /** @id MochiKit.Color.Color.prototype.compareRGB */ + compareRGB: function (other) { + var a = this.asRGB(); + var b = other.asRGB(); + return MochiKit.Base.compare( + [a.r, a.g, a.b, a.a], + [b.r, b.g, b.b, b.a] + ); + }, + + /** @id MochiKit.Color.Color.prototype.isLight */ + isLight: function () { + return this.asHSL().b > 0.5; + }, + + /** @id MochiKit.Color.Color.prototype.isDark */ + isDark: function () { + return (!this.isLight()); + }, + + /** @id MochiKit.Color.Color.prototype.toHSLString */ + toHSLString: function () { + var c = this.asHSL(); + var ccc = MochiKit.Color.clampColorComponent; + var rval = this._hslString; + if (!rval) { + var mid = ( + ccc(c.h, 360).toFixed(0) + + "," + ccc(c.s, 100).toPrecision(4) + "%" + + "," + ccc(c.l, 100).toPrecision(4) + "%" + ); + var a = c.a; + if (a >= 1) { + a = 1; + rval = "hsl(" + mid + ")"; + } else { + if (a <= 0) { + a = 0; + } + rval = "hsla(" + mid + "," + a + ")"; + } + this._hslString = rval; + } + return rval; + }, + + /** @id MochiKit.Color.Color.prototype.toRGBString */ + toRGBString: function () { + var c = this.rgb; + var ccc = MochiKit.Color.clampColorComponent; + var rval = this._rgbString; + if (!rval) { + var mid = ( + ccc(c.r, 255).toFixed(0) + + "," + ccc(c.g, 255).toFixed(0) + + "," + ccc(c.b, 255).toFixed(0) + ); + if (c.a != 1) { + rval = "rgba(" + mid + "," + c.a + ")"; + } else { + rval = "rgb(" + mid + ")"; + } + this._rgbString = rval; + } + return rval; + }, + + /** @id MochiKit.Color.Color.prototype.asRGB */ + asRGB: function () { + return MochiKit.Base.clone(this.rgb); + }, + + /** @id MochiKit.Color.Color.prototype.toHexString */ + toHexString: function () { + var m = MochiKit.Color; + var c = this.rgb; + var ccc = MochiKit.Color.clampColorComponent; + var rval = this._hexString; + if (!rval) { + rval = ("#" + + m.toColorPart(ccc(c.r, 255)) + + m.toColorPart(ccc(c.g, 255)) + + m.toColorPart(ccc(c.b, 255)) + ); + this._hexString = rval; + } + return rval; + }, + + /** @id MochiKit.Color.Color.prototype.asHSV */ + asHSV: function () { + var hsv = this.hsv; + var c = this.rgb; + if (typeof(hsv) == 'undefined' || hsv === null) { + hsv = MochiKit.Color.rgbToHSV(this.rgb); + this.hsv = hsv; + } + return MochiKit.Base.clone(hsv); + }, + + /** @id MochiKit.Color.Color.prototype.asHSL */ + asHSL: function () { + var hsl = this.hsl; + var c = this.rgb; + if (typeof(hsl) == 'undefined' || hsl === null) { + hsl = MochiKit.Color.rgbToHSL(this.rgb); + this.hsl = hsl; + } + return MochiKit.Base.clone(hsl); + }, + + /** @id MochiKit.Color.Color.prototype.toString */ + toString: function () { + return this.toRGBString(); + }, + + /** @id MochiKit.Color.Color.prototype.repr */ + repr: function () { + var c = this.rgb; + var col = [c.r, c.g, c.b, c.a]; + return this.__class__.NAME + "(" + col.join(", ") + ")"; + } + +}; + +// Constructor methods + +MochiKit.Base.update(MochiKit.Color.Color, { + /** @id MochiKit.Color.Color.fromRGB */ + fromRGB: function (red, green, blue, alpha) { + // designated initializer + var Color = MochiKit.Color.Color; + if (arguments.length == 1) { + var rgb = red; + red = rgb.r; + green = rgb.g; + blue = rgb.b; + if (typeof(rgb.a) == 'undefined') { + alpha = undefined; + } else { + alpha = rgb.a; + } + } + return new Color(red, green, blue, alpha); + }, + + /** @id MochiKit.Color.Color.fromHSL */ + fromHSL: function (hue, saturation, lightness, alpha) { + var m = MochiKit.Color; + return m.Color.fromRGB(m.hslToRGB.apply(m, arguments)); + }, + + /** @id MochiKit.Color.Color.fromHSV */ + fromHSV: function (hue, saturation, value, alpha) { + var m = MochiKit.Color; + return m.Color.fromRGB(m.hsvToRGB.apply(m, arguments)); + }, + + /** @id MochiKit.Color.Color.fromName */ + fromName: function (name) { + var Color = MochiKit.Color.Color; + // Opera 9 seems to "quote" named colors(?!) + if (name.charAt(0) == '"') { + name = name.substr(1, name.length - 2); + } + var htmlColor = Color._namedColors[name.toLowerCase()]; + if (typeof(htmlColor) == 'string') { + return Color.fromHexString(htmlColor); + } else if (name == "transparent") { + return Color.transparentColor(); + } + return null; + }, + + /** @id MochiKit.Color.Color.fromString */ + fromString: function (colorString) { + var self = MochiKit.Color.Color; + var three = colorString.substr(0, 3); + if (three == "rgb") { + return self.fromRGBString(colorString); + } else if (three == "hsl") { + return self.fromHSLString(colorString); + } else if (colorString.charAt(0) == "#") { + return self.fromHexString(colorString); + } + return self.fromName(colorString); + }, + + + /** @id MochiKit.Color.Color.fromHexString */ + fromHexString: function (hexCode) { + if (hexCode.charAt(0) == '#') { + hexCode = hexCode.substring(1); + } + var components = []; + var i, hex; + if (hexCode.length == 3) { + for (i = 0; i < 3; i++) { + hex = hexCode.substr(i, 1); + components.push(parseInt(hex + hex, 16) / 255.0); + } + } else { + for (i = 0; i < 6; i += 2) { + hex = hexCode.substr(i, 2); + components.push(parseInt(hex, 16) / 255.0); + } + } + var Color = MochiKit.Color.Color; + return Color.fromRGB.apply(Color, components); + }, + + + _fromColorString: function (pre, method, scales, colorCode) { + // parses either HSL or RGB + if (colorCode.indexOf(pre) === 0) { + colorCode = colorCode.substring(colorCode.indexOf("(", 3) + 1, colorCode.length - 1); + } + var colorChunks = colorCode.split(/\s*,\s*/); + var colorFloats = []; + for (var i = 0; i < colorChunks.length; i++) { + var c = colorChunks[i]; + var val; + var three = c.substring(c.length - 3); + if (c.charAt(c.length - 1) == '%') { + val = 0.01 * parseFloat(c.substring(0, c.length - 1)); + } else if (three == "deg") { + val = parseFloat(c) / 360.0; + } else if (three == "rad") { + val = parseFloat(c) / (Math.PI * 2); + } else { + val = scales[i] * parseFloat(c); + } + colorFloats.push(val); + } + return this[method].apply(this, colorFloats); + }, + + /** @id MochiKit.Color.Color.fromComputedStyle */ + fromComputedStyle: function (elem, style) { + var d = MochiKit.DOM; + var cls = MochiKit.Color.Color; + for (elem = d.getElement(elem); elem; elem = elem.parentNode) { + var actualColor = MochiKit.Style.getStyle.apply(d, arguments); + if (!actualColor) { + continue; + } + var color = cls.fromString(actualColor); + if (!color) { + break; + } + if (color.asRGB().a > 0) { + return color; + } + } + return null; + }, + + /** @id MochiKit.Color.Color.fromBackground */ + fromBackground: function (elem) { + var cls = MochiKit.Color.Color; + return cls.fromComputedStyle( + elem, "backgroundColor", "background-color") || cls.whiteColor(); + }, + + /** @id MochiKit.Color.Color.fromText */ + fromText: function (elem) { + var cls = MochiKit.Color.Color; + return cls.fromComputedStyle( + elem, "color", "color") || cls.blackColor(); + }, + + /** @id MochiKit.Color.Color.namedColors */ + namedColors: function () { + return MochiKit.Base.clone(MochiKit.Color.Color._namedColors); + } +}); + + +// Module level functions + +MochiKit.Base.update(MochiKit.Color, { + /** @id MochiKit.Color.clampColorComponent */ + clampColorComponent: function (v, scale) { + v *= scale; + if (v < 0) { + return 0; + } else if (v > scale) { + return scale; + } else { + return v; + } + }, + + _hslValue: function (n1, n2, hue) { + if (hue > 6.0) { + hue -= 6.0; + } else if (hue < 0.0) { + hue += 6.0; + } + var val; + if (hue < 1.0) { + val = n1 + (n2 - n1) * hue; + } else if (hue < 3.0) { + val = n2; + } else if (hue < 4.0) { + val = n1 + (n2 - n1) * (4.0 - hue); + } else { + val = n1; + } + return val; + }, + + /** @id MochiKit.Color.hsvToRGB */ + hsvToRGB: function (hue, saturation, value, alpha) { + if (arguments.length == 1) { + var hsv = hue; + hue = hsv.h; + saturation = hsv.s; + value = hsv.v; + alpha = hsv.a; + } + var red; + var green; + var blue; + if (saturation === 0) { + red = value; + green = value; + blue = value; + } else { + var i = Math.floor(hue * 6); + var f = (hue * 6) - i; + var p = value * (1 - saturation); + var q = value * (1 - (saturation * f)); + var t = value * (1 - (saturation * (1 - f))); + switch (i) { + case 1: red = q; green = value; blue = p; break; + case 2: red = p; green = value; blue = t; break; + case 3: red = p; green = q; blue = value; break; + case 4: red = t; green = p; blue = value; break; + case 5: red = value; green = p; blue = q; break; + case 6: // fall through + case 0: red = value; green = t; blue = p; break; + } + } + return { + r: red, + g: green, + b: blue, + a: alpha + }; + }, + + /** @id MochiKit.Color.hslToRGB */ + hslToRGB: function (hue, saturation, lightness, alpha) { + if (arguments.length == 1) { + var hsl = hue; + hue = hsl.h; + saturation = hsl.s; + lightness = hsl.l; + alpha = hsl.a; + } + var red; + var green; + var blue; + if (saturation === 0) { + red = lightness; + green = lightness; + blue = lightness; + } else { + var m2; + if (lightness <= 0.5) { + m2 = lightness * (1.0 + saturation); + } else { + m2 = lightness + saturation - (lightness * saturation); + } + var m1 = (2.0 * lightness) - m2; + var f = MochiKit.Color._hslValue; + var h6 = hue * 6.0; + red = f(m1, m2, h6 + 2); + green = f(m1, m2, h6); + blue = f(m1, m2, h6 - 2); + } + return { + r: red, + g: green, + b: blue, + a: alpha + }; + }, + + /** @id MochiKit.Color.rgbToHSV */ + rgbToHSV: function (red, green, blue, alpha) { + if (arguments.length == 1) { + var rgb = red; + red = rgb.r; + green = rgb.g; + blue = rgb.b; + alpha = rgb.a; + } + var max = Math.max(Math.max(red, green), blue); + var min = Math.min(Math.min(red, green), blue); + var hue; + var saturation; + var value = max; + if (min == max) { + hue = 0; + saturation = 0; + } else { + var delta = (max - min); + saturation = delta / max; + + if (red == max) { + hue = (green - blue) / delta; + } else if (green == max) { + hue = 2 + ((blue - red) / delta); + } else { + hue = 4 + ((red - green) / delta); + } + hue /= 6; + if (hue < 0) { + hue += 1; + } + if (hue > 1) { + hue -= 1; + } + } + return { + h: hue, + s: saturation, + v: value, + a: alpha + }; + }, + + /** @id MochiKit.Color.rgbToHSL */ + rgbToHSL: function (red, green, blue, alpha) { + if (arguments.length == 1) { + var rgb = red; + red = rgb.r; + green = rgb.g; + blue = rgb.b; + alpha = rgb.a; + } + var max = Math.max(red, Math.max(green, blue)); + var min = Math.min(red, Math.min(green, blue)); + var hue; + var saturation; + var lightness = (max + min) / 2.0; + var delta = max - min; + if (delta === 0) { + hue = 0; + saturation = 0; + } else { + if (lightness <= 0.5) { + saturation = delta / (max + min); + } else { + saturation = delta / (2 - max - min); + } + if (red == max) { + hue = (green - blue) / delta; + } else if (green == max) { + hue = 2 + ((blue - red) / delta); + } else { + hue = 4 + ((red - green) / delta); + } + hue /= 6; + if (hue < 0) { + hue += 1; + } + if (hue > 1) { + hue -= 1; + } + + } + return { + h: hue, + s: saturation, + l: lightness, + a: alpha + }; + }, + + /** @id MochiKit.Color.toColorPart */ + toColorPart: function (num) { + num = Math.round(num); + var digits = num.toString(16); + if (num < 16) { + return '0' + digits; + } + return digits; + }, + + __new__: function () { + var m = MochiKit.Base; + /** @id MochiKit.Color.fromRGBString */ + this.Color.fromRGBString = m.bind( + this.Color._fromColorString, this.Color, "rgb", "fromRGB", + [1.0/255.0, 1.0/255.0, 1.0/255.0, 1] + ); + /** @id MochiKit.Color.fromHSLString */ + this.Color.fromHSLString = m.bind( + this.Color._fromColorString, this.Color, "hsl", "fromHSL", + [1.0/360.0, 0.01, 0.01, 1] + ); + + var third = 1.0 / 3.0; + /** @id MochiKit.Color.colors */ + var colors = { + // NSColor colors plus transparent + /** @id MochiKit.Color.blackColor */ + black: [0, 0, 0], + /** @id MochiKit.Color.blueColor */ + blue: [0, 0, 1], + /** @id MochiKit.Color.brownColor */ + brown: [0.6, 0.4, 0.2], + /** @id MochiKit.Color.cyanColor */ + cyan: [0, 1, 1], + /** @id MochiKit.Color.darkGrayColor */ + darkGray: [third, third, third], + /** @id MochiKit.Color.grayColor */ + gray: [0.5, 0.5, 0.5], + /** @id MochiKit.Color.greenColor */ + green: [0, 1, 0], + /** @id MochiKit.Color.lightGrayColor */ + lightGray: [2 * third, 2 * third, 2 * third], + /** @id MochiKit.Color.magentaColor */ + magenta: [1, 0, 1], + /** @id MochiKit.Color.orangeColor */ + orange: [1, 0.5, 0], + /** @id MochiKit.Color.purpleColor */ + purple: [0.5, 0, 0.5], + /** @id MochiKit.Color.redColor */ + red: [1, 0, 0], + /** @id MochiKit.Color.transparentColor */ + transparent: [0, 0, 0, 0], + /** @id MochiKit.Color.whiteColor */ + white: [1, 1, 1], + /** @id MochiKit.Color.yellowColor */ + yellow: [1, 1, 0] + }; + + var makeColor = function (name, r, g, b, a) { + var rval = this.fromRGB(r, g, b, a); + this[name] = function () { return rval; }; + return rval; + }; + + for (var k in colors) { + var name = k + "Color"; + var bindArgs = m.concat( + [makeColor, this.Color, name], + colors[k] + ); + this.Color[name] = m.bind.apply(null, bindArgs); + } + + var isColor = function () { + for (var i = 0; i < arguments.length; i++) { + if (!(arguments[i] instanceof MochiKit.Color.Color)) { + return false; + } + } + return true; + }; + + var compareColor = function (a, b) { + return a.compareRGB(b); + }; + + m.nameFunctions(this); + + m.registerComparator(this.Color.NAME, isColor, compareColor); + + this.EXPORT_TAGS = { + ":common": this.EXPORT, + ":all": m.concat(this.EXPORT, this.EXPORT_OK) + }; + + } +}); + +MochiKit.Color.EXPORT = [ + "Color" +]; + +MochiKit.Color.EXPORT_OK = [ + "clampColorComponent", + "rgbToHSL", + "hslToRGB", + "rgbToHSV", + "hsvToRGB", + "toColorPart" +]; + +MochiKit.Color.__new__(); + +MochiKit.Base._exportSymbols(this, MochiKit.Color); + +// Full table of css3 X11 colors <http://www.w3.org/TR/css3-color/#X11COLORS> + +MochiKit.Color.Color._namedColors = { + aliceblue: "#f0f8ff", + antiquewhite: "#faebd7", + aqua: "#00ffff", + aquamarine: "#7fffd4", + azure: "#f0ffff", + beige: "#f5f5dc", + bisque: "#ffe4c4", + black: "#000000", + blanchedalmond: "#ffebcd", + blue: "#0000ff", + blueviolet: "#8a2be2", + brown: "#a52a2a", + burlywood: "#deb887", + cadetblue: "#5f9ea0", + chartreuse: "#7fff00", + chocolate: "#d2691e", + coral: "#ff7f50", + cornflowerblue: "#6495ed", + cornsilk: "#fff8dc", + crimson: "#dc143c", + cyan: "#00ffff", + darkblue: "#00008b", + darkcyan: "#008b8b", + darkgoldenrod: "#b8860b", + darkgray: "#a9a9a9", + darkgreen: "#006400", + darkgrey: "#a9a9a9", + darkkhaki: "#bdb76b", + darkmagenta: "#8b008b", + darkolivegreen: "#556b2f", + darkorange: "#ff8c00", + darkorchid: "#9932cc", + darkred: "#8b0000", + darksalmon: "#e9967a", + darkseagreen: "#8fbc8f", + darkslateblue: "#483d8b", + darkslategray: "#2f4f4f", + darkslategrey: "#2f4f4f", + darkturquoise: "#00ced1", + darkviolet: "#9400d3", + deeppink: "#ff1493", + deepskyblue: "#00bfff", + dimgray: "#696969", + dimgrey: "#696969", + dodgerblue: "#1e90ff", + firebrick: "#b22222", + floralwhite: "#fffaf0", + forestgreen: "#228b22", + fuchsia: "#ff00ff", + gainsboro: "#dcdcdc", + ghostwhite: "#f8f8ff", + gold: "#ffd700", + goldenrod: "#daa520", + gray: "#808080", + green: "#008000", + greenyellow: "#adff2f", + grey: "#808080", + honeydew: "#f0fff0", + hotpink: "#ff69b4", + indianred: "#cd5c5c", + indigo: "#4b0082", + ivory: "#fffff0", + khaki: "#f0e68c", + lavender: "#e6e6fa", + lavenderblush: "#fff0f5", + lawngreen: "#7cfc00", + lemonchiffon: "#fffacd", + lightblue: "#add8e6", + lightcoral: "#f08080", + lightcyan: "#e0ffff", + lightgoldenrodyellow: "#fafad2", + lightgray: "#d3d3d3", + lightgreen: "#90ee90", + lightgrey: "#d3d3d3", + lightpink: "#ffb6c1", + lightsalmon: "#ffa07a", + lightseagreen: "#20b2aa", + lightskyblue: "#87cefa", + lightslategray: "#778899", + lightslategrey: "#778899", + lightsteelblue: "#b0c4de", + lightyellow: "#ffffe0", + lime: "#00ff00", + limegreen: "#32cd32", + linen: "#faf0e6", + magenta: "#ff00ff", + maroon: "#800000", + mediumaquamarine: "#66cdaa", + mediumblue: "#0000cd", + mediumorchid: "#ba55d3", + mediumpurple: "#9370db", + mediumseagreen: "#3cb371", + mediumslateblue: "#7b68ee", + mediumspringgreen: "#00fa9a", + mediumturquoise: "#48d1cc", + mediumvioletred: "#c71585", + midnightblue: "#191970", + mintcream: "#f5fffa", + mistyrose: "#ffe4e1", + moccasin: "#ffe4b5", + navajowhite: "#ffdead", + navy: "#000080", + oldlace: "#fdf5e6", + olive: "#808000", + olivedrab: "#6b8e23", + orange: "#ffa500", + orangered: "#ff4500", + orchid: "#da70d6", + palegoldenrod: "#eee8aa", + palegreen: "#98fb98", + paleturquoise: "#afeeee", + palevioletred: "#db7093", + papayawhip: "#ffefd5", + peachpuff: "#ffdab9", + peru: "#cd853f", + pink: "#ffc0cb", + plum: "#dda0dd", + powderblue: "#b0e0e6", + purple: "#800080", + red: "#ff0000", + rosybrown: "#bc8f8f", + royalblue: "#4169e1", + saddlebrown: "#8b4513", + salmon: "#fa8072", + sandybrown: "#f4a460", + seagreen: "#2e8b57", + seashell: "#fff5ee", + sienna: "#a0522d", + silver: "#c0c0c0", + skyblue: "#87ceeb", + slateblue: "#6a5acd", + slategray: "#708090", + slategrey: "#708090", + snow: "#fffafa", + springgreen: "#00ff7f", + steelblue: "#4682b4", + tan: "#d2b48c", + teal: "#008080", + thistle: "#d8bfd8", + tomato: "#ff6347", + turquoise: "#40e0d0", + violet: "#ee82ee", + wheat: "#f5deb3", + white: "#ffffff", + whitesmoke: "#f5f5f5", + yellow: "#ffff00", + yellowgreen: "#9acd32" +}; diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/DOM.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/DOM.js new file mode 100644 index 0000000000..b43c7baa40 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/DOM.js @@ -0,0 +1,1256 @@ +/*** + +MochiKit.DOM 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +MochiKit.Base._deps('DOM', ['Base']); + +MochiKit.DOM.NAME = "MochiKit.DOM"; +MochiKit.DOM.VERSION = "1.4.2"; +MochiKit.DOM.__repr__ = function () { + return "[" + this.NAME + " " + this.VERSION + "]"; +}; +MochiKit.DOM.toString = function () { + return this.__repr__(); +}; + +MochiKit.DOM.EXPORT = [ + "removeEmptyTextNodes", + "formContents", + "currentWindow", + "currentDocument", + "withWindow", + "withDocument", + "registerDOMConverter", + "coerceToDOM", + "createDOM", + "createDOMFunc", + "isChildNode", + "getNodeAttribute", + "removeNodeAttribute", + "setNodeAttribute", + "updateNodeAttributes", + "appendChildNodes", + "insertSiblingNodesAfter", + "insertSiblingNodesBefore", + "replaceChildNodes", + "removeElement", + "swapDOM", + "BUTTON", + "TT", + "PRE", + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "BR", + "CANVAS", + "HR", + "LABEL", + "TEXTAREA", + "FORM", + "STRONG", + "SELECT", + "OPTION", + "OPTGROUP", + "LEGEND", + "FIELDSET", + "P", + "UL", + "OL", + "LI", + "DL", + "DT", + "DD", + "TD", + "TR", + "THEAD", + "TBODY", + "TFOOT", + "TABLE", + "TH", + "INPUT", + "SPAN", + "A", + "DIV", + "IMG", + "getElement", + "$", + "getElementsByTagAndClassName", + "addToCallStack", + "addLoadEvent", + "focusOnLoad", + "setElementClass", + "toggleElementClass", + "addElementClass", + "removeElementClass", + "swapElementClass", + "hasElementClass", + "computedStyle", // deprecated in 1.4 + "escapeHTML", + "toHTML", + "emitHTML", + "scrapeText", + "getFirstParentByTagAndClassName", + "getFirstElementByTagAndClassName" +]; + +MochiKit.DOM.EXPORT_OK = [ + "domConverters" +]; + +MochiKit.DOM.DEPRECATED = [ + /** @id MochiKit.DOM.computedStyle */ + ['computedStyle', 'MochiKit.Style.getStyle', '1.4'], + /** @id MochiKit.DOM.elementDimensions */ + ['elementDimensions', 'MochiKit.Style.getElementDimensions', '1.4'], + /** @id MochiKit.DOM.elementPosition */ + ['elementPosition', 'MochiKit.Style.getElementPosition', '1.4'], + /** @id MochiKit.DOM.getViewportDimensions */ + ['getViewportDimensions', 'MochiKit.Style.getViewportDimensions', '1.4'], + /** @id MochiKit.DOM.hideElement */ + ['hideElement', 'MochiKit.Style.hideElement', '1.4'], + /** @id MochiKit.DOM.makeClipping */ + ['makeClipping', 'MochiKit.Style.makeClipping', '1.4.1'], + /** @id MochiKit.DOM.makePositioned */ + ['makePositioned', 'MochiKit.Style.makePositioned', '1.4.1'], + /** @id MochiKit.DOM.setElementDimensions */ + ['setElementDimensions', 'MochiKit.Style.setElementDimensions', '1.4'], + /** @id MochiKit.DOM.setElementPosition */ + ['setElementPosition', 'MochiKit.Style.setElementPosition', '1.4'], + /** @id MochiKit.DOM.setDisplayForElement */ + ['setDisplayForElement', 'MochiKit.Style.setDisplayForElement', '1.4'], + /** @id MochiKit.DOM.setOpacity */ + ['setOpacity', 'MochiKit.Style.setOpacity', '1.4'], + /** @id MochiKit.DOM.showElement */ + ['showElement', 'MochiKit.Style.showElement', '1.4'], + /** @id MochiKit.DOM.undoClipping */ + ['undoClipping', 'MochiKit.Style.undoClipping', '1.4.1'], + /** @id MochiKit.DOM.undoPositioned */ + ['undoPositioned', 'MochiKit.Style.undoPositioned', '1.4.1'], + /** @id MochiKit.DOM.Coordinates */ + ['Coordinates', 'MochiKit.Style.Coordinates', '1.4'], // FIXME: broken + /** @id MochiKit.DOM.Dimensions */ + ['Dimensions', 'MochiKit.Style.Dimensions', '1.4'] // FIXME: broken +]; + +MochiKit.Base.update(MochiKit.DOM, { + + /** @id MochiKit.DOM.currentWindow */ + currentWindow: function () { + return MochiKit.DOM._window; + }, + + /** @id MochiKit.DOM.currentDocument */ + currentDocument: function () { + return MochiKit.DOM._document; + }, + + /** @id MochiKit.DOM.withWindow */ + withWindow: function (win, func) { + var self = MochiKit.DOM; + var oldDoc = self._document; + var oldWin = self._window; + var rval; + try { + self._window = win; + self._document = win.document; + rval = func(); + } catch (e) { + self._window = oldWin; + self._document = oldDoc; + throw e; + } + self._window = oldWin; + self._document = oldDoc; + return rval; + }, + + /** @id MochiKit.DOM.formContents */ + formContents: function (elem/* = document.body */) { + var names = []; + var values = []; + var m = MochiKit.Base; + var self = MochiKit.DOM; + if (typeof(elem) == "undefined" || elem === null) { + elem = self._document.body; + } else { + elem = self.getElement(elem); + } + m.nodeWalk(elem, function (elem) { + var name = elem.name; + if (m.isNotEmpty(name)) { + var tagName = elem.tagName.toUpperCase(); + if (tagName === "INPUT" + && (elem.type == "radio" || elem.type == "checkbox") + && !elem.checked + ) { + return null; + } + if (tagName === "SELECT") { + if (elem.type == "select-one") { + if (elem.selectedIndex >= 0) { + var opt = elem.options[elem.selectedIndex]; + var v = opt.value; + if (!v) { + var h = opt.outerHTML; + // internet explorer sure does suck. + if (h && !h.match(/^[^>]+\svalue\s*=/i)) { + v = opt.text; + } + } + names.push(name); + values.push(v); + return null; + } + // no form elements? + names.push(name); + values.push(""); + return null; + } else { + var opts = elem.options; + if (!opts.length) { + names.push(name); + values.push(""); + return null; + } + for (var i = 0; i < opts.length; i++) { + var opt = opts[i]; + if (!opt.selected) { + continue; + } + var v = opt.value; + if (!v) { + var h = opt.outerHTML; + // internet explorer sure does suck. + if (h && !h.match(/^[^>]+\svalue\s*=/i)) { + v = opt.text; + } + } + names.push(name); + values.push(v); + } + return null; + } + } + if (tagName === "FORM" || tagName === "P" || tagName === "SPAN" + || tagName === "DIV" + ) { + return elem.childNodes; + } + names.push(name); + values.push(elem.value || ''); + return null; + } + return elem.childNodes; + }); + return [names, values]; + }, + + /** @id MochiKit.DOM.withDocument */ + withDocument: function (doc, func) { + var self = MochiKit.DOM; + var oldDoc = self._document; + var rval; + try { + self._document = doc; + rval = func(); + } catch (e) { + self._document = oldDoc; + throw e; + } + self._document = oldDoc; + return rval; + }, + + /** @id MochiKit.DOM.registerDOMConverter */ + registerDOMConverter: function (name, check, wrap, /* optional */override) { + MochiKit.DOM.domConverters.register(name, check, wrap, override); + }, + + /** @id MochiKit.DOM.coerceToDOM */ + coerceToDOM: function (node, ctx) { + var m = MochiKit.Base; + var im = MochiKit.Iter; + var self = MochiKit.DOM; + if (im) { + var iter = im.iter; + var repeat = im.repeat; + } + var map = m.map; + var domConverters = self.domConverters; + var coerceToDOM = arguments.callee; + var NotFound = m.NotFound; + while (true) { + if (typeof(node) == 'undefined' || node === null) { + return null; + } + // this is a safari childNodes object, avoiding crashes w/ attr + // lookup + if (typeof(node) == "function" && + typeof(node.length) == "number" && + !(node instanceof Function)) { + node = im ? im.list(node) : m.extend(null, node); + } + if (typeof(node.nodeType) != 'undefined' && node.nodeType > 0) { + return node; + } + if (typeof(node) == 'number' || typeof(node) == 'boolean') { + node = node.toString(); + // FALL THROUGH + } + if (typeof(node) == 'string') { + return self._document.createTextNode(node); + } + if (typeof(node.__dom__) == 'function') { + node = node.__dom__(ctx); + continue; + } + if (typeof(node.dom) == 'function') { + node = node.dom(ctx); + continue; + } + if (typeof(node) == 'function') { + node = node.apply(ctx, [ctx]); + continue; + } + + if (im) { + // iterable + var iterNodes = null; + try { + iterNodes = iter(node); + } catch (e) { + // pass + } + if (iterNodes) { + return map(coerceToDOM, iterNodes, repeat(ctx)); + } + } else if (m.isArrayLike(node)) { + var func = function (n) { return coerceToDOM(n, ctx); }; + return map(func, node); + } + + // adapter + try { + node = domConverters.match(node, ctx); + continue; + } catch (e) { + if (e != NotFound) { + throw e; + } + } + + // fallback + return self._document.createTextNode(node.toString()); + } + // mozilla warnings aren't too bright + return undefined; + }, + + /** @id MochiKit.DOM.isChildNode */ + isChildNode: function (node, maybeparent) { + var self = MochiKit.DOM; + if (typeof(node) == "string") { + node = self.getElement(node); + } + if (typeof(maybeparent) == "string") { + maybeparent = self.getElement(maybeparent); + } + if (typeof(node) == 'undefined' || node === null) { + return false; + } + while (node != null && node !== self._document) { + if (node === maybeparent) { + return true; + } + node = node.parentNode; + } + return false; + }, + + /** @id MochiKit.DOM.setNodeAttribute */ + setNodeAttribute: function (node, attr, value) { + var o = {}; + o[attr] = value; + try { + return MochiKit.DOM.updateNodeAttributes(node, o); + } catch (e) { + // pass + } + return null; + }, + + /** @id MochiKit.DOM.getNodeAttribute */ + getNodeAttribute: function (node, attr) { + var self = MochiKit.DOM; + var rename = self.attributeArray.renames[attr]; + var ignoreValue = self.attributeArray.ignoreAttr[attr]; + node = self.getElement(node); + try { + if (rename) { + return node[rename]; + } + var value = node.getAttribute(attr); + if (value != ignoreValue) { + return value; + } + } catch (e) { + // pass + } + return null; + }, + + /** @id MochiKit.DOM.removeNodeAttribute */ + removeNodeAttribute: function (node, attr) { + var self = MochiKit.DOM; + var rename = self.attributeArray.renames[attr]; + node = self.getElement(node); + try { + if (rename) { + return node[rename]; + } + return node.removeAttribute(attr); + } catch (e) { + // pass + } + return null; + }, + + /** @id MochiKit.DOM.updateNodeAttributes */ + updateNodeAttributes: function (node, attrs) { + var elem = node; + var self = MochiKit.DOM; + if (typeof(node) == 'string') { + elem = self.getElement(node); + } + if (attrs) { + var updatetree = MochiKit.Base.updatetree; + if (self.attributeArray.compliant) { + // not IE, good. + for (var k in attrs) { + var v = attrs[k]; + if (typeof(v) == 'object' && typeof(elem[k]) == 'object') { + if (k == "style" && MochiKit.Style) { + MochiKit.Style.setStyle(elem, v); + } else { + updatetree(elem[k], v); + } + } else if (k.substring(0, 2) == "on") { + if (typeof(v) == "string") { + v = new Function(v); + } + elem[k] = v; + } else { + elem.setAttribute(k, v); + } + if (typeof(elem[k]) == "string" && elem[k] != v) { + // Also set property for weird attributes (see #302) + elem[k] = v; + } + } + } else { + // IE is insane in the membrane + var renames = self.attributeArray.renames; + for (var k in attrs) { + v = attrs[k]; + var renamed = renames[k]; + if (k == "style" && typeof(v) == "string") { + elem.style.cssText = v; + } else if (typeof(renamed) == "string") { + elem[renamed] = v; + } else if (typeof(elem[k]) == 'object' + && typeof(v) == 'object') { + if (k == "style" && MochiKit.Style) { + MochiKit.Style.setStyle(elem, v); + } else { + updatetree(elem[k], v); + } + } else if (k.substring(0, 2) == "on") { + if (typeof(v) == "string") { + v = new Function(v); + } + elem[k] = v; + } else { + elem.setAttribute(k, v); + } + if (typeof(elem[k]) == "string" && elem[k] != v) { + // Also set property for weird attributes (see #302) + elem[k] = v; + } + } + } + } + return elem; + }, + + /** @id MochiKit.DOM.appendChildNodes */ + appendChildNodes: function (node/*, nodes...*/) { + var elem = node; + var self = MochiKit.DOM; + if (typeof(node) == 'string') { + elem = self.getElement(node); + } + var nodeStack = [ + self.coerceToDOM( + MochiKit.Base.extend(null, arguments, 1), + elem + ) + ]; + var concat = MochiKit.Base.concat; + while (nodeStack.length) { + var n = nodeStack.shift(); + if (typeof(n) == 'undefined' || n === null) { + // pass + } else if (typeof(n.nodeType) == 'number') { + elem.appendChild(n); + } else { + nodeStack = concat(n, nodeStack); + } + } + return elem; + }, + + + /** @id MochiKit.DOM.insertSiblingNodesBefore */ + insertSiblingNodesBefore: function (node/*, nodes...*/) { + var elem = node; + var self = MochiKit.DOM; + if (typeof(node) == 'string') { + elem = self.getElement(node); + } + var nodeStack = [ + self.coerceToDOM( + MochiKit.Base.extend(null, arguments, 1), + elem + ) + ]; + var parentnode = elem.parentNode; + var concat = MochiKit.Base.concat; + while (nodeStack.length) { + var n = nodeStack.shift(); + if (typeof(n) == 'undefined' || n === null) { + // pass + } else if (typeof(n.nodeType) == 'number') { + parentnode.insertBefore(n, elem); + } else { + nodeStack = concat(n, nodeStack); + } + } + return parentnode; + }, + + /** @id MochiKit.DOM.insertSiblingNodesAfter */ + insertSiblingNodesAfter: function (node/*, nodes...*/) { + var elem = node; + var self = MochiKit.DOM; + + if (typeof(node) == 'string') { + elem = self.getElement(node); + } + var nodeStack = [ + self.coerceToDOM( + MochiKit.Base.extend(null, arguments, 1), + elem + ) + ]; + + if (elem.nextSibling) { + return self.insertSiblingNodesBefore(elem.nextSibling, nodeStack); + } + else { + return self.appendChildNodes(elem.parentNode, nodeStack); + } + }, + + /** @id MochiKit.DOM.replaceChildNodes */ + replaceChildNodes: function (node/*, nodes...*/) { + var elem = node; + var self = MochiKit.DOM; + if (typeof(node) == 'string') { + elem = self.getElement(node); + arguments[0] = elem; + } + var child; + while ((child = elem.firstChild)) { + elem.removeChild(child); + } + if (arguments.length < 2) { + return elem; + } else { + return self.appendChildNodes.apply(this, arguments); + } + }, + + /** @id MochiKit.DOM.createDOM */ + createDOM: function (name, attrs/*, nodes... */) { + var elem; + var self = MochiKit.DOM; + var m = MochiKit.Base; + if (typeof(attrs) == "string" || typeof(attrs) == "number") { + var args = m.extend([name, null], arguments, 1); + return arguments.callee.apply(this, args); + } + if (typeof(name) == 'string') { + // Internet Explorer is dumb + var xhtml = self._xhtml; + if (attrs && !self.attributeArray.compliant) { + // http://msdn.microsoft.com/workshop/author/dhtml/reference/properties/name_2.asp + var contents = ""; + if ('name' in attrs) { + contents += ' name="' + self.escapeHTML(attrs.name) + '"'; + } + if (name == 'input' && 'type' in attrs) { + contents += ' type="' + self.escapeHTML(attrs.type) + '"'; + } + if (contents) { + name = "<" + name + contents + ">"; + xhtml = false; + } + } + var d = self._document; + if (xhtml && d === document) { + elem = d.createElementNS("http://www.w3.org/1999/xhtml", name); + } else { + elem = d.createElement(name); + } + } else { + elem = name; + } + if (attrs) { + self.updateNodeAttributes(elem, attrs); + } + if (arguments.length <= 2) { + return elem; + } else { + var args = m.extend([elem], arguments, 2); + return self.appendChildNodes.apply(this, args); + } + }, + + /** @id MochiKit.DOM.createDOMFunc */ + createDOMFunc: function (/* tag, attrs, *nodes */) { + var m = MochiKit.Base; + return m.partial.apply( + this, + m.extend([MochiKit.DOM.createDOM], arguments) + ); + }, + + /** @id MochiKit.DOM.removeElement */ + removeElement: function (elem) { + var self = MochiKit.DOM; + var e = self.coerceToDOM(self.getElement(elem)); + e.parentNode.removeChild(e); + return e; + }, + + /** @id MochiKit.DOM.swapDOM */ + swapDOM: function (dest, src) { + var self = MochiKit.DOM; + dest = self.getElement(dest); + var parent = dest.parentNode; + if (src) { + src = self.coerceToDOM(self.getElement(src), parent); + parent.replaceChild(src, dest); + } else { + parent.removeChild(dest); + } + return src; + }, + + /** @id MochiKit.DOM.getElement */ + getElement: function (id) { + var self = MochiKit.DOM; + if (arguments.length == 1) { + return ((typeof(id) == "string") ? + self._document.getElementById(id) : id); + } else { + return MochiKit.Base.map(self.getElement, arguments); + } + }, + + /** @id MochiKit.DOM.getElementsByTagAndClassName */ + getElementsByTagAndClassName: function (tagName, className, + /* optional */parent) { + var self = MochiKit.DOM; + if (typeof(tagName) == 'undefined' || tagName === null) { + tagName = '*'; + } + if (typeof(parent) == 'undefined' || parent === null) { + parent = self._document; + } + parent = self.getElement(parent); + if (parent == null) { + return []; + } + var children = (parent.getElementsByTagName(tagName) + || self._document.all); + if (typeof(className) == 'undefined' || className === null) { + return MochiKit.Base.extend(null, children); + } + + var elements = []; + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var cls = child.className; + if (typeof(cls) != "string") { + cls = child.getAttribute("class"); + } + if (typeof(cls) == "string") { + var classNames = cls.split(' '); + for (var j = 0; j < classNames.length; j++) { + if (classNames[j] == className) { + elements.push(child); + break; + } + } + } + } + + return elements; + }, + + _newCallStack: function (path, once) { + var rval = function () { + var callStack = arguments.callee.callStack; + for (var i = 0; i < callStack.length; i++) { + if (callStack[i].apply(this, arguments) === false) { + break; + } + } + if (once) { + try { + this[path] = null; + } catch (e) { + // pass + } + } + }; + rval.callStack = []; + return rval; + }, + + /** @id MochiKit.DOM.addToCallStack */ + addToCallStack: function (target, path, func, once) { + var self = MochiKit.DOM; + var existing = target[path]; + var regfunc = existing; + if (!(typeof(existing) == 'function' + && typeof(existing.callStack) == "object" + && existing.callStack !== null)) { + regfunc = self._newCallStack(path, once); + if (typeof(existing) == 'function') { + regfunc.callStack.push(existing); + } + target[path] = regfunc; + } + regfunc.callStack.push(func); + }, + + /** @id MochiKit.DOM.addLoadEvent */ + addLoadEvent: function (func) { + var self = MochiKit.DOM; + self.addToCallStack(self._window, "onload", func, true); + + }, + + /** @id MochiKit.DOM.focusOnLoad */ + focusOnLoad: function (element) { + var self = MochiKit.DOM; + self.addLoadEvent(function () { + element = self.getElement(element); + if (element) { + element.focus(); + } + }); + }, + + /** @id MochiKit.DOM.setElementClass */ + setElementClass: function (element, className) { + var self = MochiKit.DOM; + var obj = self.getElement(element); + if (self.attributeArray.compliant) { + obj.setAttribute("class", className); + } else { + obj.setAttribute("className", className); + } + }, + + /** @id MochiKit.DOM.toggleElementClass */ + toggleElementClass: function (className/*, element... */) { + var self = MochiKit.DOM; + for (var i = 1; i < arguments.length; i++) { + var obj = self.getElement(arguments[i]); + if (!self.addElementClass(obj, className)) { + self.removeElementClass(obj, className); + } + } + }, + + /** @id MochiKit.DOM.addElementClass */ + addElementClass: function (element, className) { + var self = MochiKit.DOM; + var obj = self.getElement(element); + var cls = obj.className; + if (typeof(cls) != "string") { + cls = obj.getAttribute("class"); + } + // trivial case, no className yet + if (typeof(cls) != "string" || cls.length === 0) { + self.setElementClass(obj, className); + return true; + } + // the other trivial case, already set as the only class + if (cls == className) { + return false; + } + var classes = cls.split(" "); + for (var i = 0; i < classes.length; i++) { + // already present + if (classes[i] == className) { + return false; + } + } + // append class + self.setElementClass(obj, cls + " " + className); + return true; + }, + + /** @id MochiKit.DOM.removeElementClass */ + removeElementClass: function (element, className) { + var self = MochiKit.DOM; + var obj = self.getElement(element); + var cls = obj.className; + if (typeof(cls) != "string") { + cls = obj.getAttribute("class"); + } + // trivial case, no className yet + if (typeof(cls) != "string" || cls.length === 0) { + return false; + } + // other trivial case, set only to className + if (cls == className) { + self.setElementClass(obj, ""); + return true; + } + var classes = cls.split(" "); + for (var i = 0; i < classes.length; i++) { + // already present + if (classes[i] == className) { + // only check sane case where the class is used once + classes.splice(i, 1); + self.setElementClass(obj, classes.join(" ")); + return true; + } + } + // not found + return false; + }, + + /** @id MochiKit.DOM.swapElementClass */ + swapElementClass: function (element, fromClass, toClass) { + var obj = MochiKit.DOM.getElement(element); + var res = MochiKit.DOM.removeElementClass(obj, fromClass); + if (res) { + MochiKit.DOM.addElementClass(obj, toClass); + } + return res; + }, + + /** @id MochiKit.DOM.hasElementClass */ + hasElementClass: function (element, className/*...*/) { + var obj = MochiKit.DOM.getElement(element); + if (obj == null) { + return false; + } + var cls = obj.className; + if (typeof(cls) != "string") { + cls = obj.getAttribute("class"); + } + if (typeof(cls) != "string") { + return false; + } + var classes = cls.split(" "); + for (var i = 1; i < arguments.length; i++) { + var good = false; + for (var j = 0; j < classes.length; j++) { + if (classes[j] == arguments[i]) { + good = true; + break; + } + } + if (!good) { + return false; + } + } + return true; + }, + + /** @id MochiKit.DOM.escapeHTML */ + escapeHTML: function (s) { + return s.replace(/&/g, "&" + ).replace(/"/g, """ + ).replace(/</g, "<" + ).replace(/>/g, ">"); + }, + + /** @id MochiKit.DOM.toHTML */ + toHTML: function (dom) { + return MochiKit.DOM.emitHTML(dom).join(""); + }, + + /** @id MochiKit.DOM.emitHTML */ + emitHTML: function (dom, /* optional */lst) { + if (typeof(lst) == 'undefined' || lst === null) { + lst = []; + } + // queue is the call stack, we're doing this non-recursively + var queue = [dom]; + var self = MochiKit.DOM; + var escapeHTML = self.escapeHTML; + var attributeArray = self.attributeArray; + while (queue.length) { + dom = queue.pop(); + if (typeof(dom) == 'string') { + lst.push(dom); + } else if (dom.nodeType == 1) { + // we're not using higher order stuff here + // because safari has heisenbugs.. argh. + // + // I think it might have something to do with + // garbage collection and function calls. + lst.push('<' + dom.tagName.toLowerCase()); + var attributes = []; + var domAttr = attributeArray(dom); + for (var i = 0; i < domAttr.length; i++) { + var a = domAttr[i]; + attributes.push([ + " ", + a.name, + '="', + escapeHTML(a.value), + '"' + ]); + } + attributes.sort(); + for (i = 0; i < attributes.length; i++) { + var attrs = attributes[i]; + for (var j = 0; j < attrs.length; j++) { + lst.push(attrs[j]); + } + } + if (dom.hasChildNodes()) { + lst.push(">"); + // queue is the FILO call stack, so we put the close tag + // on first + queue.push("</" + dom.tagName.toLowerCase() + ">"); + var cnodes = dom.childNodes; + for (i = cnodes.length - 1; i >= 0; i--) { + queue.push(cnodes[i]); + } + } else { + lst.push('/>'); + } + } else if (dom.nodeType == 3) { + lst.push(escapeHTML(dom.nodeValue)); + } + } + return lst; + }, + + /** @id MochiKit.DOM.scrapeText */ + scrapeText: function (node, /* optional */asArray) { + var rval = []; + (function (node) { + var cn = node.childNodes; + if (cn) { + for (var i = 0; i < cn.length; i++) { + arguments.callee.call(this, cn[i]); + } + } + var nodeValue = node.nodeValue; + if (typeof(nodeValue) == 'string') { + rval.push(nodeValue); + } + })(MochiKit.DOM.getElement(node)); + if (asArray) { + return rval; + } else { + return rval.join(""); + } + }, + + /** @id MochiKit.DOM.removeEmptyTextNodes */ + removeEmptyTextNodes: function (element) { + element = MochiKit.DOM.getElement(element); + for (var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) { + node.parentNode.removeChild(node); + } + } + }, + + /** @id MochiKit.DOM.getFirstElementByTagAndClassName */ + getFirstElementByTagAndClassName: function (tagName, className, + /* optional */parent) { + var self = MochiKit.DOM; + if (typeof(tagName) == 'undefined' || tagName === null) { + tagName = '*'; + } + if (typeof(parent) == 'undefined' || parent === null) { + parent = self._document; + } + parent = self.getElement(parent); + if (parent == null) { + return null; + } + var children = (parent.getElementsByTagName(tagName) + || self._document.all); + if (children.length <= 0) { + return null; + } else if (typeof(className) == 'undefined' || className === null) { + return children[0]; + } + + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var cls = child.className; + if (typeof(cls) != "string") { + cls = child.getAttribute("class"); + } + if (typeof(cls) == "string") { + var classNames = cls.split(' '); + for (var j = 0; j < classNames.length; j++) { + if (classNames[j] == className) { + return child; + } + } + } + } + return null; + }, + + /** @id MochiKit.DOM.getFirstParentByTagAndClassName */ + getFirstParentByTagAndClassName: function (elem, tagName, className) { + var self = MochiKit.DOM; + elem = self.getElement(elem); + if (typeof(tagName) == 'undefined' || tagName === null) { + tagName = '*'; + } else { + tagName = tagName.toUpperCase(); + } + if (typeof(className) == 'undefined' || className === null) { + className = null; + } + if (elem) { + elem = elem.parentNode; + } + while (elem && elem.tagName) { + var curTagName = elem.tagName.toUpperCase(); + if ((tagName === '*' || tagName == curTagName) && + (className === null || self.hasElementClass(elem, className))) { + return elem; + } + elem = elem.parentNode; + } + return null; + }, + + __new__: function (win) { + + var m = MochiKit.Base; + if (typeof(document) != "undefined") { + this._document = document; + var kXULNSURI = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ this._xhtml = (document.documentElement &&
+ document.createElementNS && + document.documentElement.namespaceURI === kXULNSURI);
+ } else if (MochiKit.MockDOM) { + this._document = MochiKit.MockDOM.document; + } + this._window = win; + + this.domConverters = new m.AdapterRegistry(); + + var __tmpElement = this._document.createElement("span"); + var attributeArray; + if (__tmpElement && __tmpElement.attributes && + __tmpElement.attributes.length > 0) { + // for braindead browsers (IE) that insert extra junk + var filter = m.filter; + attributeArray = function (node) { + /*** + + Return an array of attributes for a given node, + filtering out attributes that don't belong for + that are inserted by "Certain Browsers". + + ***/ + return filter(attributeArray.ignoreAttrFilter, node.attributes); + }; + attributeArray.ignoreAttr = {}; + var attrs = __tmpElement.attributes; + var ignoreAttr = attributeArray.ignoreAttr; + for (var i = 0; i < attrs.length; i++) { + var a = attrs[i]; + ignoreAttr[a.name] = a.value; + } + attributeArray.ignoreAttrFilter = function (a) { + return (attributeArray.ignoreAttr[a.name] != a.value); + }; + attributeArray.compliant = false; + attributeArray.renames = { + "class": "className", + "checked": "defaultChecked", + "usemap": "useMap", + "for": "htmlFor", + "readonly": "readOnly", + "colspan": "colSpan", + "bgcolor": "bgColor", + "cellspacing": "cellSpacing", + "cellpadding": "cellPadding" + }; + } else { + attributeArray = function (node) { + return node.attributes; + }; + attributeArray.compliant = true; + attributeArray.ignoreAttr = {}; + attributeArray.renames = {}; + } + this.attributeArray = attributeArray; + + // FIXME: this really belongs in Base, and could probably be cleaner + var _deprecated = function(fromModule, arr) { + var fromName = arr[0]; + var toName = arr[1]; + var toModule = toName.split('.')[1]; + var str = ''; + + str += 'if (!MochiKit.' + toModule + ') { throw new Error("'; + str += 'This function has been deprecated and depends on MochiKit.'; + str += toModule + '.");}'; + str += 'return ' + toName + '.apply(this, arguments);'; + MochiKit[fromModule][fromName] = new Function(str); + } + for (var i = 0; i < MochiKit.DOM.DEPRECATED.length; i++) { + _deprecated('DOM', MochiKit.DOM.DEPRECATED[i]); + } + + // shorthand for createDOM syntax + var createDOMFunc = this.createDOMFunc; + /** @id MochiKit.DOM.UL */ + this.UL = createDOMFunc("ul"); + /** @id MochiKit.DOM.OL */ + this.OL = createDOMFunc("ol"); + /** @id MochiKit.DOM.LI */ + this.LI = createDOMFunc("li"); + /** @id MochiKit.DOM.DL */ + this.DL = createDOMFunc("dl"); + /** @id MochiKit.DOM.DT */ + this.DT = createDOMFunc("dt"); + /** @id MochiKit.DOM.DD */ + this.DD = createDOMFunc("dd"); + /** @id MochiKit.DOM.TD */ + this.TD = createDOMFunc("td"); + /** @id MochiKit.DOM.TR */ + this.TR = createDOMFunc("tr"); + /** @id MochiKit.DOM.TBODY */ + this.TBODY = createDOMFunc("tbody"); + /** @id MochiKit.DOM.THEAD */ + this.THEAD = createDOMFunc("thead"); + /** @id MochiKit.DOM.TFOOT */ + this.TFOOT = createDOMFunc("tfoot"); + /** @id MochiKit.DOM.TABLE */ + this.TABLE = createDOMFunc("table"); + /** @id MochiKit.DOM.TH */ + this.TH = createDOMFunc("th"); + /** @id MochiKit.DOM.INPUT */ + this.INPUT = createDOMFunc("input"); + /** @id MochiKit.DOM.SPAN */ + this.SPAN = createDOMFunc("span"); + /** @id MochiKit.DOM.A */ + this.A = createDOMFunc("a"); + /** @id MochiKit.DOM.DIV */ + this.DIV = createDOMFunc("div"); + /** @id MochiKit.DOM.IMG */ + this.IMG = createDOMFunc("img"); + /** @id MochiKit.DOM.BUTTON */ + this.BUTTON = createDOMFunc("button"); + /** @id MochiKit.DOM.TT */ + this.TT = createDOMFunc("tt"); + /** @id MochiKit.DOM.PRE */ + this.PRE = createDOMFunc("pre"); + /** @id MochiKit.DOM.H1 */ + this.H1 = createDOMFunc("h1"); + /** @id MochiKit.DOM.H2 */ + this.H2 = createDOMFunc("h2"); + /** @id MochiKit.DOM.H3 */ + this.H3 = createDOMFunc("h3"); + /** @id MochiKit.DOM.H4 */ + this.H4 = createDOMFunc("h4"); + /** @id MochiKit.DOM.H5 */ + this.H5 = createDOMFunc("h5"); + /** @id MochiKit.DOM.H6 */ + this.H6 = createDOMFunc("h6"); + /** @id MochiKit.DOM.BR */ + this.BR = createDOMFunc("br"); + /** @id MochiKit.DOM.HR */ + this.HR = createDOMFunc("hr"); + /** @id MochiKit.DOM.LABEL */ + this.LABEL = createDOMFunc("label"); + /** @id MochiKit.DOM.TEXTAREA */ + this.TEXTAREA = createDOMFunc("textarea"); + /** @id MochiKit.DOM.FORM */ + this.FORM = createDOMFunc("form"); + /** @id MochiKit.DOM.P */ + this.P = createDOMFunc("p"); + /** @id MochiKit.DOM.SELECT */ + this.SELECT = createDOMFunc("select"); + /** @id MochiKit.DOM.OPTION */ + this.OPTION = createDOMFunc("option"); + /** @id MochiKit.DOM.OPTGROUP */ + this.OPTGROUP = createDOMFunc("optgroup"); + /** @id MochiKit.DOM.LEGEND */ + this.LEGEND = createDOMFunc("legend"); + /** @id MochiKit.DOM.FIELDSET */ + this.FIELDSET = createDOMFunc("fieldset"); + /** @id MochiKit.DOM.STRONG */ + this.STRONG = createDOMFunc("strong"); + /** @id MochiKit.DOM.CANVAS */ + this.CANVAS = createDOMFunc("canvas"); + + /** @id MochiKit.DOM.$ */ + this.$ = this.getElement; + + this.EXPORT_TAGS = { + ":common": this.EXPORT, + ":all": m.concat(this.EXPORT, this.EXPORT_OK) + }; + + m.nameFunctions(this); + + } +}); + + +MochiKit.DOM.__new__(((typeof(window) == "undefined") ? this : window)); + +// +// XXX: Internet Explorer blows +// +if (MochiKit.__export__) { + withWindow = MochiKit.DOM.withWindow; + withDocument = MochiKit.DOM.withDocument; +} + +MochiKit.Base._exportSymbols(this, MochiKit.DOM); diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/DateTime.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/DateTime.js new file mode 100644 index 0000000000..cbb3c91b66 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/DateTime.js @@ -0,0 +1,222 @@ +/*** + +MochiKit.DateTime 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +MochiKit.Base._deps('DateTime', ['Base']); + +MochiKit.DateTime.NAME = "MochiKit.DateTime"; +MochiKit.DateTime.VERSION = "1.4.2"; +MochiKit.DateTime.__repr__ = function () { + return "[" + this.NAME + " " + this.VERSION + "]"; +}; +MochiKit.DateTime.toString = function () { + return this.__repr__(); +}; + +/** @id MochiKit.DateTime.isoDate */ +MochiKit.DateTime.isoDate = function (str) { + str = str + ""; + if (typeof(str) != "string" || str.length === 0) { + return null; + } + var iso = str.split('-'); + if (iso.length === 0) { + return null; + } + var date = new Date(iso[0], iso[1] - 1, iso[2]); + date.setFullYear(iso[0]); + date.setMonth(iso[1] - 1); + date.setDate(iso[2]); + return date; +}; + +MochiKit.DateTime._isoRegexp = /(\d{4,})(?:-(\d{1,2})(?:-(\d{1,2})(?:[T ](\d{1,2}):(\d{1,2})(?::(\d{1,2})(?:\.(\d+))?)?(?:(Z)|([+-])(\d{1,2})(?::(\d{1,2}))?)?)?)?)?/; + +/** @id MochiKit.DateTime.isoTimestamp */ +MochiKit.DateTime.isoTimestamp = function (str) { + str = str + ""; + if (typeof(str) != "string" || str.length === 0) { + return null; + } + var res = str.match(MochiKit.DateTime._isoRegexp); + if (typeof(res) == "undefined" || res === null) { + return null; + } + var year, month, day, hour, min, sec, msec; + year = parseInt(res[1], 10); + if (typeof(res[2]) == "undefined" || res[2] === '') { + return new Date(year); + } + month = parseInt(res[2], 10) - 1; + day = parseInt(res[3], 10); + if (typeof(res[4]) == "undefined" || res[4] === '') { + return new Date(year, month, day); + } + hour = parseInt(res[4], 10); + min = parseInt(res[5], 10); + sec = (typeof(res[6]) != "undefined" && res[6] !== '') ? parseInt(res[6], 10) : 0; + if (typeof(res[7]) != "undefined" && res[7] !== '') { + msec = Math.round(1000.0 * parseFloat("0." + res[7])); + } else { + msec = 0; + } + if ((typeof(res[8]) == "undefined" || res[8] === '') && (typeof(res[9]) == "undefined" || res[9] === '')) { + return new Date(year, month, day, hour, min, sec, msec); + } + var ofs; + if (typeof(res[9]) != "undefined" && res[9] !== '') { + ofs = parseInt(res[10], 10) * 3600000; + if (typeof(res[11]) != "undefined" && res[11] !== '') { + ofs += parseInt(res[11], 10) * 60000; + } + if (res[9] == "-") { + ofs = -ofs; + } + } else { + ofs = 0; + } + return new Date(Date.UTC(year, month, day, hour, min, sec, msec) - ofs); +}; + +/** @id MochiKit.DateTime.toISOTime */ +MochiKit.DateTime.toISOTime = function (date, realISO/* = false */) { + if (typeof(date) == "undefined" || date === null) { + return null; + } + var hh = date.getHours(); + var mm = date.getMinutes(); + var ss = date.getSeconds(); + var lst = [ + ((realISO && (hh < 10)) ? "0" + hh : hh), + ((mm < 10) ? "0" + mm : mm), + ((ss < 10) ? "0" + ss : ss) + ]; + return lst.join(":"); +}; + +/** @id MochiKit.DateTime.toISOTimeStamp */ +MochiKit.DateTime.toISOTimestamp = function (date, realISO/* = false*/) { + if (typeof(date) == "undefined" || date === null) { + return null; + } + var sep = realISO ? "T" : " "; + var foot = realISO ? "Z" : ""; + if (realISO) { + date = new Date(date.getTime() + (date.getTimezoneOffset() * 60000)); + } + return MochiKit.DateTime.toISODate(date) + sep + MochiKit.DateTime.toISOTime(date, realISO) + foot; +}; + +/** @id MochiKit.DateTime.toISODate */ +MochiKit.DateTime.toISODate = function (date) { + if (typeof(date) == "undefined" || date === null) { + return null; + } + var _padTwo = MochiKit.DateTime._padTwo; + var _padFour = MochiKit.DateTime._padFour; + return [ + _padFour(date.getFullYear()), + _padTwo(date.getMonth() + 1), + _padTwo(date.getDate()) + ].join("-"); +}; + +/** @id MochiKit.DateTime.americanDate */ +MochiKit.DateTime.americanDate = function (d) { + d = d + ""; + if (typeof(d) != "string" || d.length === 0) { + return null; + } + var a = d.split('/'); + return new Date(a[2], a[0] - 1, a[1]); +}; + +MochiKit.DateTime._padTwo = function (n) { + return (n > 9) ? n : "0" + n; +}; + +MochiKit.DateTime._padFour = function(n) { + switch(n.toString().length) { + case 1: return "000" + n; break; + case 2: return "00" + n; break; + case 3: return "0" + n; break; + case 4: + default: + return n; + } +}; + +/** @id MochiKit.DateTime.toPaddedAmericanDate */ +MochiKit.DateTime.toPaddedAmericanDate = function (d) { + if (typeof(d) == "undefined" || d === null) { + return null; + } + var _padTwo = MochiKit.DateTime._padTwo; + return [ + _padTwo(d.getMonth() + 1), + _padTwo(d.getDate()), + d.getFullYear() + ].join('/'); +}; + +/** @id MochiKit.DateTime.toAmericanDate */ +MochiKit.DateTime.toAmericanDate = function (d) { + if (typeof(d) == "undefined" || d === null) { + return null; + } + return [d.getMonth() + 1, d.getDate(), d.getFullYear()].join('/'); +}; + +MochiKit.DateTime.EXPORT = [ + "isoDate", + "isoTimestamp", + "toISOTime", + "toISOTimestamp", + "toISODate", + "americanDate", + "toPaddedAmericanDate", + "toAmericanDate" +]; + +MochiKit.DateTime.EXPORT_OK = []; +MochiKit.DateTime.EXPORT_TAGS = { + ":common": MochiKit.DateTime.EXPORT, + ":all": MochiKit.DateTime.EXPORT +}; + +MochiKit.DateTime.__new__ = function () { + // MochiKit.Base.nameFunctions(this); + var base = this.NAME + "."; + for (var k in this) { + var o = this[k]; + if (typeof(o) == 'function' && typeof(o.NAME) == 'undefined') { + try { + o.NAME = base + k; + } catch (e) { + // pass + } + } + } +}; + +MochiKit.DateTime.__new__(); + +if (typeof(MochiKit.Base) != "undefined") { + MochiKit.Base._exportSymbols(this, MochiKit.DateTime); +} else { + (function (globals, module) { + if ((typeof(JSAN) == 'undefined' && typeof(dojo) == 'undefined') + || (MochiKit.__export__ === false)) { + var all = module.EXPORT_TAGS[":all"]; + for (var i = 0; i < all.length; i++) { + globals[all[i]] = module[all[i]]; + } + } + })(this, MochiKit.DateTime); +} diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/DragAndDrop.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/DragAndDrop.js new file mode 100644 index 0000000000..b23b102080 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/DragAndDrop.js @@ -0,0 +1,793 @@ +/*** +MochiKit.DragAndDrop 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) + Mochi-ized By Thomas Herve (_firstname_@nimail.org) + +***/ + +MochiKit.Base._deps('DragAndDrop', ['Base', 'Iter', 'DOM', 'Signal', 'Visual', 'Position']); + +MochiKit.DragAndDrop.NAME = 'MochiKit.DragAndDrop'; +MochiKit.DragAndDrop.VERSION = '1.4.2'; + +MochiKit.DragAndDrop.__repr__ = function () { + return '[' + this.NAME + ' ' + this.VERSION + ']'; +}; + +MochiKit.DragAndDrop.toString = function () { + return this.__repr__(); +}; + +MochiKit.DragAndDrop.EXPORT = [ + "Droppable", + "Draggable" +]; + +MochiKit.DragAndDrop.EXPORT_OK = [ + "Droppables", + "Draggables" +]; + +MochiKit.DragAndDrop.Droppables = { + /*** + + Manage all droppables. Shouldn't be used, use the Droppable object instead. + + ***/ + drops: [], + + remove: function (element) { + this.drops = MochiKit.Base.filter(function (d) { + return d.element != MochiKit.DOM.getElement(element); + }, this.drops); + }, + + register: function (drop) { + this.drops.push(drop); + }, + + unregister: function (drop) { + this.drops = MochiKit.Base.filter(function (d) { + return d != drop; + }, this.drops); + }, + + prepare: function (element) { + MochiKit.Base.map(function (drop) { + if (drop.isAccepted(element)) { + if (drop.options.activeclass) { + MochiKit.DOM.addElementClass(drop.element, + drop.options.activeclass); + } + drop.options.onactive(drop.element, element); + } + }, this.drops); + }, + + findDeepestChild: function (drops) { + deepest = drops[0]; + + for (i = 1; i < drops.length; ++i) { + if (MochiKit.DOM.isChildNode(drops[i].element, deepest.element)) { + deepest = drops[i]; + } + } + return deepest; + }, + + show: function (point, element) { + if (!this.drops.length) { + return; + } + var affected = []; + + if (this.last_active) { + this.last_active.deactivate(); + } + MochiKit.Iter.forEach(this.drops, function (drop) { + if (drop.isAffected(point, element)) { + affected.push(drop); + } + }); + if (affected.length > 0) { + drop = this.findDeepestChild(affected); + MochiKit.Position.within(drop.element, point.page.x, point.page.y); + drop.options.onhover(element, drop.element, + MochiKit.Position.overlap(drop.options.overlap, drop.element)); + drop.activate(); + } + }, + + fire: function (event, element) { + if (!this.last_active) { + return; + } + MochiKit.Position.prepare(); + + if (this.last_active.isAffected(event.mouse(), element)) { + this.last_active.options.ondrop(element, + this.last_active.element, event); + } + }, + + reset: function (element) { + MochiKit.Base.map(function (drop) { + if (drop.options.activeclass) { + MochiKit.DOM.removeElementClass(drop.element, + drop.options.activeclass); + } + drop.options.ondesactive(drop.element, element); + }, this.drops); + if (this.last_active) { + this.last_active.deactivate(); + } + } +}; + +/** @id MochiKit.DragAndDrop.Droppable */ +MochiKit.DragAndDrop.Droppable = function (element, options) { + var cls = arguments.callee; + if (!(this instanceof cls)) { + return new cls(element, options); + } + this.__init__(element, options); +}; + +MochiKit.DragAndDrop.Droppable.prototype = { + /*** + + A droppable object. Simple use is to create giving an element: + + new MochiKit.DragAndDrop.Droppable('myelement'); + + Generally you'll want to define the 'ondrop' function and maybe the + 'accept' option to filter draggables. + + ***/ + __class__: MochiKit.DragAndDrop.Droppable, + + __init__: function (element, /* optional */options) { + var d = MochiKit.DOM; + var b = MochiKit.Base; + this.element = d.getElement(element); + this.options = b.update({ + + /** @id MochiKit.DragAndDrop.greedy */ + greedy: true, + + /** @id MochiKit.DragAndDrop.hoverclass */ + hoverclass: null, + + /** @id MochiKit.DragAndDrop.activeclass */ + activeclass: null, + + /** @id MochiKit.DragAndDrop.hoverfunc */ + hoverfunc: b.noop, + + /** @id MochiKit.DragAndDrop.accept */ + accept: null, + + /** @id MochiKit.DragAndDrop.onactive */ + onactive: b.noop, + + /** @id MochiKit.DragAndDrop.ondesactive */ + ondesactive: b.noop, + + /** @id MochiKit.DragAndDrop.onhover */ + onhover: b.noop, + + /** @id MochiKit.DragAndDrop.ondrop */ + ondrop: b.noop, + + /** @id MochiKit.DragAndDrop.containment */ + containment: [], + tree: false + }, options); + + // cache containers + this.options._containers = []; + b.map(MochiKit.Base.bind(function (c) { + this.options._containers.push(d.getElement(c)); + }, this), this.options.containment); + + MochiKit.Style.makePositioned(this.element); // fix IE + + MochiKit.DragAndDrop.Droppables.register(this); + }, + + /** @id MochiKit.DragAndDrop.isContained */ + isContained: function (element) { + if (this.options._containers.length) { + var containmentNode; + if (this.options.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return MochiKit.Iter.some(this.options._containers, function (c) { + return containmentNode == c; + }); + } else { + return true; + } + }, + + /** @id MochiKit.DragAndDrop.isAccepted */ + isAccepted: function (element) { + return ((!this.options.accept) || MochiKit.Iter.some( + this.options.accept, function (c) { + return MochiKit.DOM.hasElementClass(element, c); + })); + }, + + /** @id MochiKit.DragAndDrop.isAffected */ + isAffected: function (point, element) { + return ((this.element != element) && + this.isContained(element) && + this.isAccepted(element) && + MochiKit.Position.within(this.element, point.page.x, + point.page.y)); + }, + + /** @id MochiKit.DragAndDrop.deactivate */ + deactivate: function () { + /*** + + A droppable is deactivate when a draggable has been over it and left. + + ***/ + if (this.options.hoverclass) { + MochiKit.DOM.removeElementClass(this.element, + this.options.hoverclass); + } + this.options.hoverfunc(this.element, false); + MochiKit.DragAndDrop.Droppables.last_active = null; + }, + + /** @id MochiKit.DragAndDrop.activate */ + activate: function () { + /*** + + A droppable is active when a draggable is over it. + + ***/ + if (this.options.hoverclass) { + MochiKit.DOM.addElementClass(this.element, this.options.hoverclass); + } + this.options.hoverfunc(this.element, true); + MochiKit.DragAndDrop.Droppables.last_active = this; + }, + + /** @id MochiKit.DragAndDrop.destroy */ + destroy: function () { + /*** + + Delete this droppable. + + ***/ + MochiKit.DragAndDrop.Droppables.unregister(this); + }, + + /** @id MochiKit.DragAndDrop.repr */ + repr: function () { + return '[' + this.__class__.NAME + ", options:" + MochiKit.Base.repr(this.options) + "]"; + } +}; + +MochiKit.DragAndDrop.Draggables = { + /*** + + Manage draggables elements. Not intended to direct use. + + ***/ + drags: [], + + register: function (draggable) { + if (this.drags.length === 0) { + var conn = MochiKit.Signal.connect; + this.eventMouseUp = conn(document, 'onmouseup', this, this.endDrag); + this.eventMouseMove = conn(document, 'onmousemove', this, + this.updateDrag); + this.eventKeypress = conn(document, 'onkeypress', this, + this.keyPress); + } + this.drags.push(draggable); + }, + + unregister: function (draggable) { + this.drags = MochiKit.Base.filter(function (d) { + return d != draggable; + }, this.drags); + if (this.drags.length === 0) { + var disc = MochiKit.Signal.disconnect; + disc(this.eventMouseUp); + disc(this.eventMouseMove); + disc(this.eventKeypress); + } + }, + + activate: function (draggable) { + // allows keypress events if window is not currently focused + // fails for Safari + window.focus(); + this.activeDraggable = draggable; + }, + + deactivate: function () { + this.activeDraggable = null; + }, + + updateDrag: function (event) { + if (!this.activeDraggable) { + return; + } + var pointer = event.mouse(); + // Mozilla-based browsers fire successive mousemove events with + // the same coordinates, prevent needless redrawing (moz bug?) + if (this._lastPointer && (MochiKit.Base.repr(this._lastPointer.page) == + MochiKit.Base.repr(pointer.page))) { + return; + } + this._lastPointer = pointer; + this.activeDraggable.updateDrag(event, pointer); + }, + + endDrag: function (event) { + if (!this.activeDraggable) { + return; + } + this._lastPointer = null; + this.activeDraggable.endDrag(event); + this.activeDraggable = null; + }, + + keyPress: function (event) { + if (this.activeDraggable) { + this.activeDraggable.keyPress(event); + } + }, + + notify: function (eventName, draggable, event) { + MochiKit.Signal.signal(this, eventName, draggable, event); + } +}; + +/** @id MochiKit.DragAndDrop.Draggable */ +MochiKit.DragAndDrop.Draggable = function (element, options) { + var cls = arguments.callee; + if (!(this instanceof cls)) { + return new cls(element, options); + } + this.__init__(element, options); +}; + +MochiKit.DragAndDrop.Draggable.prototype = { + /*** + + A draggable object. Simple instantiate : + + new MochiKit.DragAndDrop.Draggable('myelement'); + + ***/ + __class__ : MochiKit.DragAndDrop.Draggable, + + __init__: function (element, /* optional */options) { + var v = MochiKit.Visual; + var b = MochiKit.Base; + options = b.update({ + + /** @id MochiKit.DragAndDrop.handle */ + handle: false, + + /** @id MochiKit.DragAndDrop.starteffect */ + starteffect: function (innerelement) { + this._savedOpacity = MochiKit.Style.getStyle(innerelement, 'opacity') || 1.0; + new v.Opacity(innerelement, {duration:0.2, from:this._savedOpacity, to:0.7}); + }, + /** @id MochiKit.DragAndDrop.reverteffect */ + reverteffect: function (innerelement, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2) + + Math.abs(left_offset^2))*0.02; + return new v.Move(innerelement, + {x: -left_offset, y: -top_offset, duration: dur}); + }, + + /** @id MochiKit.DragAndDrop.endeffect */ + endeffect: function (innerelement) { + new v.Opacity(innerelement, {duration:0.2, from:0.7, to:this._savedOpacity}); + }, + + /** @id MochiKit.DragAndDrop.onchange */ + onchange: b.noop, + + /** @id MochiKit.DragAndDrop.zindex */ + zindex: 1000, + + /** @id MochiKit.DragAndDrop.revert */ + revert: false, + + /** @id MochiKit.DragAndDrop.scroll */ + scroll: false, + + /** @id MochiKit.DragAndDrop.scrollSensitivity */ + scrollSensitivity: 20, + + /** @id MochiKit.DragAndDrop.scrollSpeed */ + scrollSpeed: 15, + // false, or xy or [x, y] or function (x, y){return [x, y];} + + /** @id MochiKit.DragAndDrop.snap */ + snap: false + }, options); + + var d = MochiKit.DOM; + this.element = d.getElement(element); + + if (options.handle && (typeof(options.handle) == 'string')) { + this.handle = d.getFirstElementByTagAndClassName(null, + options.handle, this.element); + } + if (!this.handle) { + this.handle = d.getElement(options.handle); + } + if (!this.handle) { + this.handle = this.element; + } + + if (options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { + options.scroll = d.getElement(options.scroll); + this._isScrollChild = MochiKit.DOM.isChildNode(this.element, options.scroll); + } + + MochiKit.Style.makePositioned(this.element); // fix IE + + this.delta = this.currentDelta(); + this.options = options; + this.dragging = false; + + this.eventMouseDown = MochiKit.Signal.connect(this.handle, + 'onmousedown', this, this.initDrag); + MochiKit.DragAndDrop.Draggables.register(this); + }, + + /** @id MochiKit.DragAndDrop.destroy */ + destroy: function () { + MochiKit.Signal.disconnect(this.eventMouseDown); + MochiKit.DragAndDrop.Draggables.unregister(this); + }, + + /** @id MochiKit.DragAndDrop.currentDelta */ + currentDelta: function () { + var s = MochiKit.Style.getStyle; + return [ + parseInt(s(this.element, 'left') || '0'), + parseInt(s(this.element, 'top') || '0')]; + }, + + /** @id MochiKit.DragAndDrop.initDrag */ + initDrag: function (event) { + if (!event.mouse().button.left) { + return; + } + // abort on form elements, fixes a Firefox issue + var src = event.target(); + var tagName = (src.tagName || '').toUpperCase(); + if (tagName === 'INPUT' || tagName === 'SELECT' || + tagName === 'OPTION' || tagName === 'BUTTON' || + tagName === 'TEXTAREA') { + return; + } + + if (this._revert) { + this._revert.cancel(); + this._revert = null; + } + + var pointer = event.mouse(); + var pos = MochiKit.Position.cumulativeOffset(this.element); + this.offset = [pointer.page.x - pos.x, pointer.page.y - pos.y]; + + MochiKit.DragAndDrop.Draggables.activate(this); + event.stop(); + }, + + /** @id MochiKit.DragAndDrop.startDrag */ + startDrag: function (event) { + this.dragging = true; + if (this.options.selectclass) { + MochiKit.DOM.addElementClass(this.element, + this.options.selectclass); + } + if (this.options.zindex) { + this.originalZ = parseInt(MochiKit.Style.getStyle(this.element, + 'z-index') || '0'); + this.element.style.zIndex = this.options.zindex; + } + + if (this.options.ghosting) { + this._clone = this.element.cloneNode(true); + this.ghostPosition = MochiKit.Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + if (this.options.scroll) { + if (this.options.scroll == window) { + var where = this._getWindowScroll(this.options.scroll); + this.originalScrollLeft = where.left; + this.originalScrollTop = where.top; + } else { + this.originalScrollLeft = this.options.scroll.scrollLeft; + this.originalScrollTop = this.options.scroll.scrollTop; + } + } + + MochiKit.DragAndDrop.Droppables.prepare(this.element); + MochiKit.DragAndDrop.Draggables.notify('start', this, event); + if (this.options.starteffect) { + this.options.starteffect(this.element); + } + }, + + /** @id MochiKit.DragAndDrop.updateDrag */ + updateDrag: function (event, pointer) { + if (!this.dragging) { + this.startDrag(event); + } + MochiKit.Position.prepare(); + MochiKit.DragAndDrop.Droppables.show(pointer, this.element); + MochiKit.DragAndDrop.Draggables.notify('drag', this, event); + this.draw(pointer); + this.options.onchange(this); + + if (this.options.scroll) { + this.stopScrolling(); + var p, q; + if (this.options.scroll == window) { + var s = this._getWindowScroll(this.options.scroll); + p = new MochiKit.Style.Coordinates(s.left, s.top); + q = new MochiKit.Style.Coordinates(s.left + s.width, + s.top + s.height); + } else { + p = MochiKit.Position.page(this.options.scroll); + p.x += this.options.scroll.scrollLeft; + p.y += this.options.scroll.scrollTop; + p.x += (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0); + p.y += (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0); + q = new MochiKit.Style.Coordinates(p.x + this.options.scroll.offsetWidth, + p.y + this.options.scroll.offsetHeight); + } + var speed = [0, 0]; + if (pointer.page.x > (q.x - this.options.scrollSensitivity)) { + speed[0] = pointer.page.x - (q.x - this.options.scrollSensitivity); + } else if (pointer.page.x < (p.x + this.options.scrollSensitivity)) { + speed[0] = pointer.page.x - (p.x + this.options.scrollSensitivity); + } + if (pointer.page.y > (q.y - this.options.scrollSensitivity)) { + speed[1] = pointer.page.y - (q.y - this.options.scrollSensitivity); + } else if (pointer.page.y < (p.y + this.options.scrollSensitivity)) { + speed[1] = pointer.page.y - (p.y + this.options.scrollSensitivity); + } + this.startScrolling(speed); + } + + // fix AppleWebKit rendering + if (/AppleWebKit/.test(navigator.appVersion)) { + window.scrollBy(0, 0); + } + event.stop(); + }, + + /** @id MochiKit.DragAndDrop.finishDrag */ + finishDrag: function (event, success) { + var dr = MochiKit.DragAndDrop; + this.dragging = false; + if (this.options.selectclass) { + MochiKit.DOM.removeElementClass(this.element, + this.options.selectclass); + } + + if (this.options.ghosting) { + // XXX: from a user point of view, it would be better to remove + // the node only *after* the MochiKit.Visual.Move end when used + // with revert. + MochiKit.Position.relativize(this.element, this.ghostPosition); + MochiKit.DOM.removeElement(this._clone); + this._clone = null; + } + + if (success) { + dr.Droppables.fire(event, this.element); + } + dr.Draggables.notify('end', this, event); + + var revert = this.options.revert; + if (revert && typeof(revert) == 'function') { + revert = revert(this.element); + } + + var d = this.currentDelta(); + if (revert && this.options.reverteffect) { + this._revert = this.options.reverteffect(this.element, + d[1] - this.delta[1], d[0] - this.delta[0]); + } else { + this.delta = d; + } + + if (this.options.zindex) { + this.element.style.zIndex = this.originalZ; + } + + if (this.options.endeffect) { + this.options.endeffect(this.element); + } + + dr.Draggables.deactivate(); + dr.Droppables.reset(this.element); + }, + + /** @id MochiKit.DragAndDrop.keyPress */ + keyPress: function (event) { + if (event.key().string != "KEY_ESCAPE") { + return; + } + this.finishDrag(event, false); + event.stop(); + }, + + /** @id MochiKit.DragAndDrop.endDrag */ + endDrag: function (event) { + if (!this.dragging) { + return; + } + this.stopScrolling(); + this.finishDrag(event, true); + event.stop(); + }, + + /** @id MochiKit.DragAndDrop.draw */ + draw: function (point) { + var pos = MochiKit.Position.cumulativeOffset(this.element); + var d = this.currentDelta(); + pos.x -= d[0]; + pos.y -= d[1]; + + if (this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { + pos.x -= this.options.scroll.scrollLeft - this.originalScrollLeft; + pos.y -= this.options.scroll.scrollTop - this.originalScrollTop; + } + + var p = [point.page.x - pos.x - this.offset[0], + point.page.y - pos.y - this.offset[1]]; + + if (this.options.snap) { + if (typeof(this.options.snap) == 'function') { + p = this.options.snap(p[0], p[1]); + } else { + if (this.options.snap instanceof Array) { + var i = -1; + p = MochiKit.Base.map(MochiKit.Base.bind(function (v) { + i += 1; + return Math.round(v/this.options.snap[i]) * + this.options.snap[i]; + }, this), p); + } else { + p = MochiKit.Base.map(MochiKit.Base.bind(function (v) { + return Math.round(v/this.options.snap) * + this.options.snap; + }, this), p); + } + } + } + var style = this.element.style; + if ((!this.options.constraint) || + (this.options.constraint == 'horizontal')) { + style.left = p[0] + 'px'; + } + if ((!this.options.constraint) || + (this.options.constraint == 'vertical')) { + style.top = p[1] + 'px'; + } + if (style.visibility == 'hidden') { + style.visibility = ''; // fix gecko rendering + } + }, + + /** @id MochiKit.DragAndDrop.stopScrolling */ + stopScrolling: function () { + if (this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + MochiKit.DragAndDrop.Draggables._lastScrollPointer = null; + } + }, + + /** @id MochiKit.DragAndDrop.startScrolling */ + startScrolling: function (speed) { + if (!speed[0] && !speed[1]) { + return; + } + this.scrollSpeed = [speed[0] * this.options.scrollSpeed, + speed[1] * this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(MochiKit.Base.bind(this.scroll, this), 10); + }, + + /** @id MochiKit.DragAndDrop.scroll */ + scroll: function () { + var current = new Date(); + var delta = current - this.lastScrolled; + this.lastScrolled = current; + + if (this.options.scroll == window) { + var s = this._getWindowScroll(this.options.scroll); + if (this.scrollSpeed[0] || this.scrollSpeed[1]) { + var dm = delta / 1000; + this.options.scroll.scrollTo(s.left + dm * this.scrollSpeed[0], + s.top + dm * this.scrollSpeed[1]); + } + } else { + this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; + this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; + } + + var d = MochiKit.DragAndDrop; + + MochiKit.Position.prepare(); + d.Droppables.show(d.Draggables._lastPointer, this.element); + d.Draggables.notify('drag', this); + if (this._isScrollChild) { + d.Draggables._lastScrollPointer = d.Draggables._lastScrollPointer || d.Draggables._lastPointer; + d.Draggables._lastScrollPointer.x += this.scrollSpeed[0] * delta / 1000; + d.Draggables._lastScrollPointer.y += this.scrollSpeed[1] * delta / 1000; + if (d.Draggables._lastScrollPointer.x < 0) { + d.Draggables._lastScrollPointer.x = 0; + } + if (d.Draggables._lastScrollPointer.y < 0) { + d.Draggables._lastScrollPointer.y = 0; + } + this.draw(d.Draggables._lastScrollPointer); + } + + this.options.onchange(this); + }, + + _getWindowScroll: function (win) { + var vp, w, h; + MochiKit.DOM.withWindow(win, function () { + vp = MochiKit.Style.getViewportPosition(win.document); + }); + if (win.innerWidth) { + w = win.innerWidth; + h = win.innerHeight; + } else if (win.document.documentElement && win.document.documentElement.clientWidth) { + w = win.document.documentElement.clientWidth; + h = win.document.documentElement.clientHeight; + } else { + w = win.document.body.offsetWidth; + h = win.document.body.offsetHeight; + } + return {top: vp.y, left: vp.x, width: w, height: h}; + }, + + /** @id MochiKit.DragAndDrop.repr */ + repr: function () { + return '[' + this.__class__.NAME + ", options:" + MochiKit.Base.repr(this.options) + "]"; + } +}; + +MochiKit.DragAndDrop.__new__ = function () { + MochiKit.Base.nameFunctions(this); + + this.EXPORT_TAGS = { + ":common": this.EXPORT, + ":all": MochiKit.Base.concat(this.EXPORT, this.EXPORT_OK) + }; +}; + +MochiKit.DragAndDrop.__new__(); + +MochiKit.Base._exportSymbols(this, MochiKit.DragAndDrop); + diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Format.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Format.js new file mode 100644 index 0000000000..36b71537c2 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Format.js @@ -0,0 +1,304 @@ +/*** + +MochiKit.Format 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +MochiKit.Base._deps('Format', ['Base']); + +MochiKit.Format.NAME = "MochiKit.Format"; +MochiKit.Format.VERSION = "1.4.2"; +MochiKit.Format.__repr__ = function () { + return "[" + this.NAME + " " + this.VERSION + "]"; +}; +MochiKit.Format.toString = function () { + return this.__repr__(); +}; + +MochiKit.Format._numberFormatter = function (placeholder, header, footer, locale, isPercent, precision, leadingZeros, separatorAt, trailingZeros) { + return function (num) { + num = parseFloat(num); + if (typeof(num) == "undefined" || num === null || isNaN(num)) { + return placeholder; + } + var curheader = header; + var curfooter = footer; + if (num < 0) { + num = -num; + } else { + curheader = curheader.replace(/-/, ""); + } + var me = arguments.callee; + var fmt = MochiKit.Format.formatLocale(locale); + if (isPercent) { + num = num * 100.0; + curfooter = fmt.percent + curfooter; + } + num = MochiKit.Format.roundToFixed(num, precision); + var parts = num.split(/\./); + var whole = parts[0]; + var frac = (parts.length == 1) ? "" : parts[1]; + var res = ""; + while (whole.length < leadingZeros) { + whole = "0" + whole; + } + if (separatorAt) { + while (whole.length > separatorAt) { + var i = whole.length - separatorAt; + //res = res + fmt.separator + whole.substring(i, whole.length); + res = fmt.separator + whole.substring(i, whole.length) + res; + whole = whole.substring(0, i); + } + } + res = whole + res; + if (precision > 0) { + while (frac.length < trailingZeros) { + frac = frac + "0"; + } + res = res + fmt.decimal + frac; + } + return curheader + res + curfooter; + }; +}; + +/** @id MochiKit.Format.numberFormatter */ +MochiKit.Format.numberFormatter = function (pattern, placeholder/* = "" */, locale/* = "default" */) { + // http://java.sun.com/docs/books/tutorial/i18n/format/numberpattern.html + // | 0 | leading or trailing zeros + // | # | just the number + // | , | separator + // | . | decimal separator + // | % | Multiply by 100 and format as percent + if (typeof(placeholder) == "undefined") { + placeholder = ""; + } + var match = pattern.match(/((?:[0#]+,)?[0#]+)(?:\.([0#]+))?(%)?/); + if (!match) { + throw TypeError("Invalid pattern"); + } + var header = pattern.substr(0, match.index); + var footer = pattern.substr(match.index + match[0].length); + if (header.search(/-/) == -1) { + header = header + "-"; + } + var whole = match[1]; + var frac = (typeof(match[2]) == "string" && match[2] != "") ? match[2] : ""; + var isPercent = (typeof(match[3]) == "string" && match[3] != ""); + var tmp = whole.split(/,/); + var separatorAt; + if (typeof(locale) == "undefined") { + locale = "default"; + } + if (tmp.length == 1) { + separatorAt = null; + } else { + separatorAt = tmp[1].length; + } + var leadingZeros = whole.length - whole.replace(/0/g, "").length; + var trailingZeros = frac.length - frac.replace(/0/g, "").length; + var precision = frac.length; + var rval = MochiKit.Format._numberFormatter( + placeholder, header, footer, locale, isPercent, precision, + leadingZeros, separatorAt, trailingZeros + ); + var m = MochiKit.Base; + if (m) { + var fn = arguments.callee; + var args = m.concat(arguments); + rval.repr = function () { + return [ + self.NAME, + "(", + map(m.repr, args).join(", "), + ")" + ].join(""); + }; + } + return rval; +}; + +/** @id MochiKit.Format.formatLocale */ +MochiKit.Format.formatLocale = function (locale) { + if (typeof(locale) == "undefined" || locale === null) { + locale = "default"; + } + if (typeof(locale) == "string") { + var rval = MochiKit.Format.LOCALE[locale]; + if (typeof(rval) == "string") { + rval = arguments.callee(rval); + MochiKit.Format.LOCALE[locale] = rval; + } + return rval; + } else { + return locale; + } +}; + +/** @id MochiKit.Format.twoDigitAverage */ +MochiKit.Format.twoDigitAverage = function (numerator, denominator) { + if (denominator) { + var res = numerator / denominator; + if (!isNaN(res)) { + return MochiKit.Format.twoDigitFloat(res); + } + } + return "0"; +}; + +/** @id MochiKit.Format.twoDigitFloat */ +MochiKit.Format.twoDigitFloat = function (aNumber) { + var res = roundToFixed(aNumber, 2); + if (res.indexOf(".00") > 0) { + return res.substring(0, res.length - 3); + } else if (res.charAt(res.length - 1) == "0") { + return res.substring(0, res.length - 1); + } else { + return res; + } +}; + +/** @id MochiKit.Format.lstrip */ +MochiKit.Format.lstrip = function (str, /* optional */chars) { + str = str + ""; + if (typeof(str) != "string") { + return null; + } + if (!chars) { + return str.replace(/^\s+/, ""); + } else { + return str.replace(new RegExp("^[" + chars + "]+"), ""); + } +}; + +/** @id MochiKit.Format.rstrip */ +MochiKit.Format.rstrip = function (str, /* optional */chars) { + str = str + ""; + if (typeof(str) != "string") { + return null; + } + if (!chars) { + return str.replace(/\s+$/, ""); + } else { + return str.replace(new RegExp("[" + chars + "]+$"), ""); + } +}; + +/** @id MochiKit.Format.strip */ +MochiKit.Format.strip = function (str, /* optional */chars) { + var self = MochiKit.Format; + return self.rstrip(self.lstrip(str, chars), chars); +}; + +/** @id MochiKit.Format.truncToFixed */ +MochiKit.Format.truncToFixed = function (aNumber, precision) { + var res = Math.floor(aNumber).toFixed(0); + if (aNumber < 0) { + res = Math.ceil(aNumber).toFixed(0); + if (res.charAt(0) != "-" && precision > 0) { + res = "-" + res; + } + } + if (res.indexOf("e") < 0 && precision > 0) { + var tail = aNumber.toString(); + if (tail.indexOf("e") > 0) { + tail = "."; + } else if (tail.indexOf(".") < 0) { + tail = "."; + } else { + tail = tail.substring(tail.indexOf(".")); + } + if (tail.length - 1 > precision) { + tail = tail.substring(0, precision + 1); + } + while (tail.length - 1 < precision) { + tail += "0"; + } + res += tail; + } + return res; +}; + +/** @id MochiKit.Format.roundToFixed */ +MochiKit.Format.roundToFixed = function (aNumber, precision) { + var upper = Math.abs(aNumber) + 0.5 * Math.pow(10, -precision); + var res = MochiKit.Format.truncToFixed(upper, precision); + if (aNumber < 0) { + res = "-" + res; + } + return res; +}; + +/** @id MochiKit.Format.percentFormat */ +MochiKit.Format.percentFormat = function (aNumber) { + return MochiKit.Format.twoDigitFloat(100 * aNumber) + '%'; +}; + +MochiKit.Format.EXPORT = [ + "truncToFixed", + "roundToFixed", + "numberFormatter", + "formatLocale", + "twoDigitAverage", + "twoDigitFloat", + "percentFormat", + "lstrip", + "rstrip", + "strip" +]; + +MochiKit.Format.LOCALE = { + en_US: {separator: ",", decimal: ".", percent: "%"}, + de_DE: {separator: ".", decimal: ",", percent: "%"}, + pt_BR: {separator: ".", decimal: ",", percent: "%"}, + fr_FR: {separator: " ", decimal: ",", percent: "%"}, + "default": "en_US" +}; + +MochiKit.Format.EXPORT_OK = []; +MochiKit.Format.EXPORT_TAGS = { + ':all': MochiKit.Format.EXPORT, + ':common': MochiKit.Format.EXPORT +}; + +MochiKit.Format.__new__ = function () { + // MochiKit.Base.nameFunctions(this); + var base = this.NAME + "."; + var k, v, o; + for (k in this.LOCALE) { + o = this.LOCALE[k]; + if (typeof(o) == "object") { + o.repr = function () { return this.NAME; }; + o.NAME = base + "LOCALE." + k; + } + } + for (k in this) { + o = this[k]; + if (typeof(o) == 'function' && typeof(o.NAME) == 'undefined') { + try { + o.NAME = base + k; + } catch (e) { + // pass + } + } + } +}; + +MochiKit.Format.__new__(); + +if (typeof(MochiKit.Base) != "undefined") { + MochiKit.Base._exportSymbols(this, MochiKit.Format); +} else { + (function (globals, module) { + if ((typeof(JSAN) == 'undefined' && typeof(dojo) == 'undefined') + || (MochiKit.__export__ === false)) { + var all = module.EXPORT_TAGS[":all"]; + for (var i = 0; i < all.length; i++) { + globals[all[i]] = module[all[i]]; + } + } + })(this, MochiKit.Format); +} diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Iter.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Iter.js new file mode 100644 index 0000000000..99f3155b89 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Iter.js @@ -0,0 +1,844 @@ +/*** + +MochiKit.Iter 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +MochiKit.Base._deps('Iter', ['Base']); + +MochiKit.Iter.NAME = "MochiKit.Iter"; +MochiKit.Iter.VERSION = "1.4.2"; +MochiKit.Base.update(MochiKit.Iter, { + __repr__: function () { + return "[" + this.NAME + " " + this.VERSION + "]"; + }, + toString: function () { + return this.__repr__(); + }, + + /** @id MochiKit.Iter.registerIteratorFactory */ + registerIteratorFactory: function (name, check, iterfactory, /* optional */ override) { + MochiKit.Iter.iteratorRegistry.register(name, check, iterfactory, override); + }, + + /** @id MochiKit.Iter.isIterable */ + isIterable: function(o) { + return o != null && + (typeof(o.next) == "function" || typeof(o.iter) == "function"); + }, + + /** @id MochiKit.Iter.iter */ + iter: function (iterable, /* optional */ sentinel) { + var self = MochiKit.Iter; + if (arguments.length == 2) { + return self.takewhile( + function (a) { return a != sentinel; }, + iterable + ); + } + if (typeof(iterable.next) == 'function') { + return iterable; + } else if (typeof(iterable.iter) == 'function') { + return iterable.iter(); + /* + } else if (typeof(iterable.__iterator__) == 'function') { + // + // XXX: We can't support JavaScript 1.7 __iterator__ directly + // because of Object.prototype.__iterator__ + // + return iterable.__iterator__(); + */ + } + + try { + return self.iteratorRegistry.match(iterable); + } catch (e) { + var m = MochiKit.Base; + if (e == m.NotFound) { + e = new TypeError(typeof(iterable) + ": " + m.repr(iterable) + " is not iterable"); + } + throw e; + } + }, + + /** @id MochiKit.Iter.count */ + count: function (n) { + if (!n) { + n = 0; + } + var m = MochiKit.Base; + return { + repr: function () { return "count(" + n + ")"; }, + toString: m.forwardCall("repr"), + next: m.counter(n) + }; + }, + + /** @id MochiKit.Iter.cycle */ + cycle: function (p) { + var self = MochiKit.Iter; + var m = MochiKit.Base; + var lst = []; + var iterator = self.iter(p); + return { + repr: function () { return "cycle(...)"; }, + toString: m.forwardCall("repr"), + next: function () { + try { + var rval = iterator.next(); + lst.push(rval); + return rval; + } catch (e) { + if (e != self.StopIteration) { + throw e; + } + if (lst.length === 0) { + this.next = function () { + throw self.StopIteration; + }; + } else { + var i = -1; + this.next = function () { + i = (i + 1) % lst.length; + return lst[i]; + }; + } + return this.next(); + } + } + }; + }, + + /** @id MochiKit.Iter.repeat */ + repeat: function (elem, /* optional */n) { + var m = MochiKit.Base; + if (typeof(n) == 'undefined') { + return { + repr: function () { + return "repeat(" + m.repr(elem) + ")"; + }, + toString: m.forwardCall("repr"), + next: function () { + return elem; + } + }; + } + return { + repr: function () { + return "repeat(" + m.repr(elem) + ", " + n + ")"; + }, + toString: m.forwardCall("repr"), + next: function () { + if (n <= 0) { + throw MochiKit.Iter.StopIteration; + } + n -= 1; + return elem; + } + }; + }, + + /** @id MochiKit.Iter.next */ + next: function (iterator) { + return iterator.next(); + }, + + /** @id MochiKit.Iter.izip */ + izip: function (p, q/*, ...*/) { + var m = MochiKit.Base; + var self = MochiKit.Iter; + var next = self.next; + var iterables = m.map(self.iter, arguments); + return { + repr: function () { return "izip(...)"; }, + toString: m.forwardCall("repr"), + next: function () { return m.map(next, iterables); } + }; + }, + + /** @id MochiKit.Iter.ifilter */ + ifilter: function (pred, seq) { + var m = MochiKit.Base; + seq = MochiKit.Iter.iter(seq); + if (pred === null) { + pred = m.operator.truth; + } + return { + repr: function () { return "ifilter(...)"; }, + toString: m.forwardCall("repr"), + next: function () { + while (true) { + var rval = seq.next(); + if (pred(rval)) { + return rval; + } + } + // mozilla warnings aren't too bright + return undefined; + } + }; + }, + + /** @id MochiKit.Iter.ifilterfalse */ + ifilterfalse: function (pred, seq) { + var m = MochiKit.Base; + seq = MochiKit.Iter.iter(seq); + if (pred === null) { + pred = m.operator.truth; + } + return { + repr: function () { return "ifilterfalse(...)"; }, + toString: m.forwardCall("repr"), + next: function () { + while (true) { + var rval = seq.next(); + if (!pred(rval)) { + return rval; + } + } + // mozilla warnings aren't too bright + return undefined; + } + }; + }, + + /** @id MochiKit.Iter.islice */ + islice: function (seq/*, [start,] stop[, step] */) { + var self = MochiKit.Iter; + var m = MochiKit.Base; + seq = self.iter(seq); + var start = 0; + var stop = 0; + var step = 1; + var i = -1; + if (arguments.length == 2) { + stop = arguments[1]; + } else if (arguments.length == 3) { + start = arguments[1]; + stop = arguments[2]; + } else { + start = arguments[1]; + stop = arguments[2]; + step = arguments[3]; + } + return { + repr: function () { + return "islice(" + ["...", start, stop, step].join(", ") + ")"; + }, + toString: m.forwardCall("repr"), + next: function () { + var rval; + while (i < start) { + rval = seq.next(); + i++; + } + if (start >= stop) { + throw self.StopIteration; + } + start += step; + return rval; + } + }; + }, + + /** @id MochiKit.Iter.imap */ + imap: function (fun, p, q/*, ...*/) { + var m = MochiKit.Base; + var self = MochiKit.Iter; + var iterables = m.map(self.iter, m.extend(null, arguments, 1)); + var map = m.map; + var next = self.next; + return { + repr: function () { return "imap(...)"; }, + toString: m.forwardCall("repr"), + next: function () { + return fun.apply(this, map(next, iterables)); + } + }; + }, + + /** @id MochiKit.Iter.applymap */ + applymap: function (fun, seq, self) { + seq = MochiKit.Iter.iter(seq); + var m = MochiKit.Base; + return { + repr: function () { return "applymap(...)"; }, + toString: m.forwardCall("repr"), + next: function () { + return fun.apply(self, seq.next()); + } + }; + }, + + /** @id MochiKit.Iter.chain */ + chain: function (p, q/*, ...*/) { + // dumb fast path + var self = MochiKit.Iter; + var m = MochiKit.Base; + if (arguments.length == 1) { + return self.iter(arguments[0]); + } + var argiter = m.map(self.iter, arguments); + return { + repr: function () { return "chain(...)"; }, + toString: m.forwardCall("repr"), + next: function () { + while (argiter.length > 1) { + try { + var result = argiter[0].next(); + return result; + } catch (e) { + if (e != self.StopIteration) { + throw e; + } + argiter.shift(); + var result = argiter[0].next(); + return result; + } + } + if (argiter.length == 1) { + // optimize last element + var arg = argiter.shift(); + this.next = m.bind("next", arg); + return this.next(); + } + throw self.StopIteration; + } + }; + }, + + /** @id MochiKit.Iter.takewhile */ + takewhile: function (pred, seq) { + var self = MochiKit.Iter; + seq = self.iter(seq); + return { + repr: function () { return "takewhile(...)"; }, + toString: MochiKit.Base.forwardCall("repr"), + next: function () { + var rval = seq.next(); + if (!pred(rval)) { + this.next = function () { + throw self.StopIteration; + }; + this.next(); + } + return rval; + } + }; + }, + + /** @id MochiKit.Iter.dropwhile */ + dropwhile: function (pred, seq) { + seq = MochiKit.Iter.iter(seq); + var m = MochiKit.Base; + var bind = m.bind; + return { + "repr": function () { return "dropwhile(...)"; }, + "toString": m.forwardCall("repr"), + "next": function () { + while (true) { + var rval = seq.next(); + if (!pred(rval)) { + break; + } + } + this.next = bind("next", seq); + return rval; + } + }; + }, + + _tee: function (ident, sync, iterable) { + sync.pos[ident] = -1; + var m = MochiKit.Base; + var listMin = m.listMin; + return { + repr: function () { return "tee(" + ident + ", ...)"; }, + toString: m.forwardCall("repr"), + next: function () { + var rval; + var i = sync.pos[ident]; + + if (i == sync.max) { + rval = iterable.next(); + sync.deque.push(rval); + sync.max += 1; + sync.pos[ident] += 1; + } else { + rval = sync.deque[i - sync.min]; + sync.pos[ident] += 1; + if (i == sync.min && listMin(sync.pos) != sync.min) { + sync.min += 1; + sync.deque.shift(); + } + } + return rval; + } + }; + }, + + /** @id MochiKit.Iter.tee */ + tee: function (iterable, n/* = 2 */) { + var rval = []; + var sync = { + "pos": [], + "deque": [], + "max": -1, + "min": -1 + }; + if (arguments.length == 1 || typeof(n) == "undefined" || n === null) { + n = 2; + } + var self = MochiKit.Iter; + iterable = self.iter(iterable); + var _tee = self._tee; + for (var i = 0; i < n; i++) { + rval.push(_tee(i, sync, iterable)); + } + return rval; + }, + + /** @id MochiKit.Iter.list */ + list: function (iterable) { + // Fast-path for Array and Array-like + var rval; + if (iterable instanceof Array) { + return iterable.slice(); + } + // this is necessary to avoid a Safari crash + if (typeof(iterable) == "function" && + !(iterable instanceof Function) && + typeof(iterable.length) == 'number') { + rval = []; + for (var i = 0; i < iterable.length; i++) { + rval.push(iterable[i]); + } + return rval; + } + + var self = MochiKit.Iter; + iterable = self.iter(iterable); + var rval = []; + var a_val; + try { + while (true) { + a_val = iterable.next(); + rval.push(a_val); + } + } catch (e) { + if (e != self.StopIteration) { + throw e; + } + return rval; + } + // mozilla warnings aren't too bright + return undefined; + }, + + + /** @id MochiKit.Iter.reduce */ + reduce: function (fn, iterable, /* optional */initial) { + var i = 0; + var x = initial; + var self = MochiKit.Iter; + iterable = self.iter(iterable); + if (arguments.length < 3) { + try { + x = iterable.next(); + } catch (e) { + if (e == self.StopIteration) { + e = new TypeError("reduce() of empty sequence with no initial value"); + } + throw e; + } + i++; + } + try { + while (true) { + x = fn(x, iterable.next()); + } + } catch (e) { + if (e != self.StopIteration) { + throw e; + } + } + return x; + }, + + /** @id MochiKit.Iter.range */ + range: function (/* [start,] stop[, step] */) { + var start = 0; + var stop = 0; + var step = 1; + if (arguments.length == 1) { + stop = arguments[0]; + } else if (arguments.length == 2) { + start = arguments[0]; + stop = arguments[1]; + } else if (arguments.length == 3) { + start = arguments[0]; + stop = arguments[1]; + step = arguments[2]; + } else { + throw new TypeError("range() takes 1, 2, or 3 arguments!"); + } + if (step === 0) { + throw new TypeError("range() step must not be 0"); + } + return { + next: function () { + if ((step > 0 && start >= stop) || (step < 0 && start <= stop)) { + throw MochiKit.Iter.StopIteration; + } + var rval = start; + start += step; + return rval; + }, + repr: function () { + return "range(" + [start, stop, step].join(", ") + ")"; + }, + toString: MochiKit.Base.forwardCall("repr") + }; + }, + + /** @id MochiKit.Iter.sum */ + sum: function (iterable, start/* = 0 */) { + if (typeof(start) == "undefined" || start === null) { + start = 0; + } + var x = start; + var self = MochiKit.Iter; + iterable = self.iter(iterable); + try { + while (true) { + x += iterable.next(); + } + } catch (e) { + if (e != self.StopIteration) { + throw e; + } + } + return x; + }, + + /** @id MochiKit.Iter.exhaust */ + exhaust: function (iterable) { + var self = MochiKit.Iter; + iterable = self.iter(iterable); + try { + while (true) { + iterable.next(); + } + } catch (e) { + if (e != self.StopIteration) { + throw e; + } + } + }, + + /** @id MochiKit.Iter.forEach */ + forEach: function (iterable, func, /* optional */obj) { + var m = MochiKit.Base; + var self = MochiKit.Iter; + if (arguments.length > 2) { + func = m.bind(func, obj); + } + // fast path for array + if (m.isArrayLike(iterable) && !self.isIterable(iterable)) { + try { + for (var i = 0; i < iterable.length; i++) { + func(iterable[i]); + } + } catch (e) { + if (e != self.StopIteration) { + throw e; + } + } + } else { + self.exhaust(self.imap(func, iterable)); + } + }, + + /** @id MochiKit.Iter.every */ + every: function (iterable, func) { + var self = MochiKit.Iter; + try { + self.ifilterfalse(func, iterable).next(); + return false; + } catch (e) { + if (e != self.StopIteration) { + throw e; + } + return true; + } + }, + + /** @id MochiKit.Iter.sorted */ + sorted: function (iterable, /* optional */cmp) { + var rval = MochiKit.Iter.list(iterable); + if (arguments.length == 1) { + cmp = MochiKit.Base.compare; + } + rval.sort(cmp); + return rval; + }, + + /** @id MochiKit.Iter.reversed */ + reversed: function (iterable) { + var rval = MochiKit.Iter.list(iterable); + rval.reverse(); + return rval; + }, + + /** @id MochiKit.Iter.some */ + some: function (iterable, func) { + var self = MochiKit.Iter; + try { + self.ifilter(func, iterable).next(); + return true; + } catch (e) { + if (e != self.StopIteration) { + throw e; + } + return false; + } + }, + + /** @id MochiKit.Iter.iextend */ + iextend: function (lst, iterable) { + var m = MochiKit.Base; + var self = MochiKit.Iter; + if (m.isArrayLike(iterable) && !self.isIterable(iterable)) { + // fast-path for array-like + for (var i = 0; i < iterable.length; i++) { + lst.push(iterable[i]); + } + } else { + iterable = self.iter(iterable); + try { + while (true) { + lst.push(iterable.next()); + } + } catch (e) { + if (e != self.StopIteration) { + throw e; + } + } + } + return lst; + }, + + /** @id MochiKit.Iter.groupby */ + groupby: function(iterable, /* optional */ keyfunc) { + var m = MochiKit.Base; + var self = MochiKit.Iter; + if (arguments.length < 2) { + keyfunc = m.operator.identity; + } + iterable = self.iter(iterable); + + // shared + var pk = undefined; + var k = undefined; + var v; + + function fetch() { + v = iterable.next(); + k = keyfunc(v); + }; + + function eat() { + var ret = v; + v = undefined; + return ret; + }; + + var first = true; + var compare = m.compare; + return { + repr: function () { return "groupby(...)"; }, + next: function() { + // iterator-next + + // iterate until meet next group + while (compare(k, pk) === 0) { + fetch(); + if (first) { + first = false; + break; + } + } + pk = k; + return [k, { + next: function() { + // subiterator-next + if (v == undefined) { // Is there something to eat? + fetch(); + } + if (compare(k, pk) !== 0) { + throw self.StopIteration; + } + return eat(); + } + }]; + } + }; + }, + + /** @id MochiKit.Iter.groupby_as_array */ + groupby_as_array: function (iterable, /* optional */ keyfunc) { + var m = MochiKit.Base; + var self = MochiKit.Iter; + if (arguments.length < 2) { + keyfunc = m.operator.identity; + } + + iterable = self.iter(iterable); + var result = []; + var first = true; + var prev_key; + var compare = m.compare; + while (true) { + try { + var value = iterable.next(); + var key = keyfunc(value); + } catch (e) { + if (e == self.StopIteration) { + break; + } + throw e; + } + if (first || compare(key, prev_key) !== 0) { + var values = []; + result.push([key, values]); + } + values.push(value); + first = false; + prev_key = key; + } + return result; + }, + + /** @id MochiKit.Iter.arrayLikeIter */ + arrayLikeIter: function (iterable) { + var i = 0; + return { + repr: function () { return "arrayLikeIter(...)"; }, + toString: MochiKit.Base.forwardCall("repr"), + next: function () { + if (i >= iterable.length) { + throw MochiKit.Iter.StopIteration; + } + return iterable[i++]; + } + }; + }, + + /** @id MochiKit.Iter.hasIterateNext */ + hasIterateNext: function (iterable) { + return (iterable && typeof(iterable.iterateNext) == "function"); + }, + + /** @id MochiKit.Iter.iterateNextIter */ + iterateNextIter: function (iterable) { + return { + repr: function () { return "iterateNextIter(...)"; }, + toString: MochiKit.Base.forwardCall("repr"), + next: function () { + var rval = iterable.iterateNext(); + if (rval === null || rval === undefined) { + throw MochiKit.Iter.StopIteration; + } + return rval; + } + }; + } +}); + + +MochiKit.Iter.EXPORT_OK = [ + "iteratorRegistry", + "arrayLikeIter", + "hasIterateNext", + "iterateNextIter" +]; + +MochiKit.Iter.EXPORT = [ + "StopIteration", + "registerIteratorFactory", + "iter", + "count", + "cycle", + "repeat", + "next", + "izip", + "ifilter", + "ifilterfalse", + "islice", + "imap", + "applymap", + "chain", + "takewhile", + "dropwhile", + "tee", + "list", + "reduce", + "range", + "sum", + "exhaust", + "forEach", + "every", + "sorted", + "reversed", + "some", + "iextend", + "groupby", + "groupby_as_array" +]; + +MochiKit.Iter.__new__ = function () { + var m = MochiKit.Base; + // Re-use StopIteration if exists (e.g. SpiderMonkey) + if (typeof(StopIteration) != "undefined") { + this.StopIteration = StopIteration; + } else { + /** @id MochiKit.Iter.StopIteration */ + this.StopIteration = new m.NamedError("StopIteration"); + } + this.iteratorRegistry = new m.AdapterRegistry(); + // Register the iterator factory for arrays + this.registerIteratorFactory( + "arrayLike", + m.isArrayLike, + this.arrayLikeIter + ); + + this.registerIteratorFactory( + "iterateNext", + this.hasIterateNext, + this.iterateNextIter + ); + + this.EXPORT_TAGS = { + ":common": this.EXPORT, + ":all": m.concat(this.EXPORT, this.EXPORT_OK) + }; + + m.nameFunctions(this); + +}; + +MochiKit.Iter.__new__(); + +// +// XXX: Internet Explorer blows +// +if (MochiKit.__export__) { + reduce = MochiKit.Iter.reduce; +} + +MochiKit.Base._exportSymbols(this, MochiKit.Iter); diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Logging.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Logging.js new file mode 100644 index 0000000000..463ccd0b76 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Logging.js @@ -0,0 +1,315 @@ +/*** + +MochiKit.Logging 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +MochiKit.Base._deps('Logging', ['Base']); + +MochiKit.Logging.NAME = "MochiKit.Logging"; +MochiKit.Logging.VERSION = "1.4.2"; +MochiKit.Logging.__repr__ = function () { + return "[" + this.NAME + " " + this.VERSION + "]"; +}; + +MochiKit.Logging.toString = function () { + return this.__repr__(); +}; + + +MochiKit.Logging.EXPORT = [ + "LogLevel", + "LogMessage", + "Logger", + "alertListener", + "logger", + "log", + "logError", + "logDebug", + "logFatal", + "logWarning" +]; + + +MochiKit.Logging.EXPORT_OK = [ + "logLevelAtLeast", + "isLogMessage", + "compareLogMessage" +]; + + +/** @id MochiKit.Logging.LogMessage */ +MochiKit.Logging.LogMessage = function (num, level, info) { + this.num = num; + this.level = level; + this.info = info; + this.timestamp = new Date(); +}; + +MochiKit.Logging.LogMessage.prototype = { + /** @id MochiKit.Logging.LogMessage.prototype.repr */ + repr: function () { + var m = MochiKit.Base; + return 'LogMessage(' + + m.map( + m.repr, + [this.num, this.level, this.info] + ).join(', ') + ')'; + }, + /** @id MochiKit.Logging.LogMessage.prototype.toString */ + toString: MochiKit.Base.forwardCall("repr") +}; + +MochiKit.Base.update(MochiKit.Logging, { + /** @id MochiKit.Logging.logLevelAtLeast */ + logLevelAtLeast: function (minLevel) { + var self = MochiKit.Logging; + if (typeof(minLevel) == 'string') { + minLevel = self.LogLevel[minLevel]; + } + return function (msg) { + var msgLevel = msg.level; + if (typeof(msgLevel) == 'string') { + msgLevel = self.LogLevel[msgLevel]; + } + return msgLevel >= minLevel; + }; + }, + + /** @id MochiKit.Logging.isLogMessage */ + isLogMessage: function (/* ... */) { + var LogMessage = MochiKit.Logging.LogMessage; + for (var i = 0; i < arguments.length; i++) { + if (!(arguments[i] instanceof LogMessage)) { + return false; + } + } + return true; + }, + + /** @id MochiKit.Logging.compareLogMessage */ + compareLogMessage: function (a, b) { + return MochiKit.Base.compare([a.level, a.info], [b.level, b.info]); + }, + + /** @id MochiKit.Logging.alertListener */ + alertListener: function (msg) { + alert( + "num: " + msg.num + + "\nlevel: " + msg.level + + "\ninfo: " + msg.info.join(" ") + ); + } + +}); + +/** @id MochiKit.Logging.Logger */ +MochiKit.Logging.Logger = function (/* optional */maxSize) { + this.counter = 0; + if (typeof(maxSize) == 'undefined' || maxSize === null) { + maxSize = -1; + } + this.maxSize = maxSize; + this._messages = []; + this.listeners = {}; + this.useNativeConsole = false; +}; + +MochiKit.Logging.Logger.prototype = { + /** @id MochiKit.Logging.Logger.prototype.clear */ + clear: function () { + this._messages.splice(0, this._messages.length); + }, + + /** @id MochiKit.Logging.Logger.prototype.logToConsole */ + logToConsole: function (msg) { + if (typeof(window) != "undefined" && window.console + && window.console.log) { + // Safari and FireBug 0.4 + // Percent replacement is a workaround for cute Safari crashing bug + window.console.log(msg.replace(/%/g, '\uFF05')); + } else if (typeof(opera) != "undefined" && opera.postError) { + // Opera + opera.postError(msg); + } else if (typeof(printfire) == "function") { + // FireBug 0.3 and earlier + printfire(msg); + } else if (typeof(Debug) != "undefined" && Debug.writeln) { + // IE Web Development Helper (?) + // http://www.nikhilk.net/Entry.aspx?id=93 + Debug.writeln(msg); + } else if (typeof(debug) != "undefined" && debug.trace) { + // Atlas framework (?) + // http://www.nikhilk.net/Entry.aspx?id=93 + debug.trace(msg); + } + }, + + /** @id MochiKit.Logging.Logger.prototype.dispatchListeners */ + dispatchListeners: function (msg) { + for (var k in this.listeners) { + var pair = this.listeners[k]; + if (pair.ident != k || (pair[0] && !pair[0](msg))) { + continue; + } + pair[1](msg); + } + }, + + /** @id MochiKit.Logging.Logger.prototype.addListener */ + addListener: function (ident, filter, listener) { + if (typeof(filter) == 'string') { + filter = MochiKit.Logging.logLevelAtLeast(filter); + } + var entry = [filter, listener]; + entry.ident = ident; + this.listeners[ident] = entry; + }, + + /** @id MochiKit.Logging.Logger.prototype.removeListener */ + removeListener: function (ident) { + delete this.listeners[ident]; + }, + + /** @id MochiKit.Logging.Logger.prototype.baseLog */ + baseLog: function (level, message/*, ...*/) { + if (typeof(level) == "number") { + if (level >= MochiKit.Logging.LogLevel.FATAL) { + level = 'FATAL'; + } else if (level >= MochiKit.Logging.LogLevel.ERROR) { + level = 'ERROR'; + } else if (level >= MochiKit.Logging.LogLevel.WARNING) { + level = 'WARNING'; + } else if (level >= MochiKit.Logging.LogLevel.INFO) { + level = 'INFO'; + } else { + level = 'DEBUG'; + } + } + var msg = new MochiKit.Logging.LogMessage( + this.counter, + level, + MochiKit.Base.extend(null, arguments, 1) + ); + this._messages.push(msg); + this.dispatchListeners(msg); + if (this.useNativeConsole) { + this.logToConsole(msg.level + ": " + msg.info.join(" ")); + } + this.counter += 1; + while (this.maxSize >= 0 && this._messages.length > this.maxSize) { + this._messages.shift(); + } + }, + + /** @id MochiKit.Logging.Logger.prototype.getMessages */ + getMessages: function (howMany) { + var firstMsg = 0; + if (!(typeof(howMany) == 'undefined' || howMany === null)) { + firstMsg = Math.max(0, this._messages.length - howMany); + } + return this._messages.slice(firstMsg); + }, + + /** @id MochiKit.Logging.Logger.prototype.getMessageText */ + getMessageText: function (howMany) { + if (typeof(howMany) == 'undefined' || howMany === null) { + howMany = 30; + } + var messages = this.getMessages(howMany); + if (messages.length) { + var lst = map(function (m) { + return '\n [' + m.num + '] ' + m.level + ': ' + m.info.join(' '); + }, messages); + lst.unshift('LAST ' + messages.length + ' MESSAGES:'); + return lst.join(''); + } + return ''; + }, + + /** @id MochiKit.Logging.Logger.prototype.debuggingBookmarklet */ + debuggingBookmarklet: function (inline) { + if (typeof(MochiKit.LoggingPane) == "undefined") { + alert(this.getMessageText()); + } else { + MochiKit.LoggingPane.createLoggingPane(inline || false); + } + } +}; + +MochiKit.Logging.__new__ = function () { + this.LogLevel = { + ERROR: 40, + FATAL: 50, + WARNING: 30, + INFO: 20, + DEBUG: 10 + }; + + var m = MochiKit.Base; + m.registerComparator("LogMessage", + this.isLogMessage, + this.compareLogMessage + ); + + var partial = m.partial; + + var Logger = this.Logger; + var baseLog = Logger.prototype.baseLog; + m.update(this.Logger.prototype, { + debug: partial(baseLog, 'DEBUG'), + log: partial(baseLog, 'INFO'), + error: partial(baseLog, 'ERROR'), + fatal: partial(baseLog, 'FATAL'), + warning: partial(baseLog, 'WARNING') + }); + + // indirectly find logger so it can be replaced + var self = this; + var connectLog = function (name) { + return function () { + self.logger[name].apply(self.logger, arguments); + }; + }; + + /** @id MochiKit.Logging.log */ + this.log = connectLog('log'); + /** @id MochiKit.Logging.logError */ + this.logError = connectLog('error'); + /** @id MochiKit.Logging.logDebug */ + this.logDebug = connectLog('debug'); + /** @id MochiKit.Logging.logFatal */ + this.logFatal = connectLog('fatal'); + /** @id MochiKit.Logging.logWarning */ + this.logWarning = connectLog('warning'); + this.logger = new Logger(); + this.logger.useNativeConsole = true; + + this.EXPORT_TAGS = { + ":common": this.EXPORT, + ":all": m.concat(this.EXPORT, this.EXPORT_OK) + }; + + m.nameFunctions(this); + +}; + +if (typeof(printfire) == "undefined" && + typeof(document) != "undefined" && document.createEvent && + typeof(dispatchEvent) != "undefined") { + // FireBug really should be less lame about this global function + printfire = function () { + printfire.args = arguments; + var ev = document.createEvent("Events"); + ev.initEvent("printfire", false, true); + dispatchEvent(ev); + }; +} + +MochiKit.Logging.__new__(); + +MochiKit.Base._exportSymbols(this, MochiKit.Logging); diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/LoggingPane.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/LoggingPane.js new file mode 100644 index 0000000000..95c4fe5902 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/LoggingPane.js @@ -0,0 +1,353 @@ +/*** + +MochiKit.LoggingPane 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +MochiKit.Base._deps('LoggingPane', ['Base', 'Logging']); + +MochiKit.LoggingPane.NAME = "MochiKit.LoggingPane"; +MochiKit.LoggingPane.VERSION = "1.4.2"; +MochiKit.LoggingPane.__repr__ = function () { + return "[" + this.NAME + " " + this.VERSION + "]"; +}; + +MochiKit.LoggingPane.toString = function () { + return this.__repr__(); +}; + +/** @id MochiKit.LoggingPane.createLoggingPane */ +MochiKit.LoggingPane.createLoggingPane = function (inline/* = false */) { + var m = MochiKit.LoggingPane; + inline = !(!inline); + if (m._loggingPane && m._loggingPane.inline != inline) { + m._loggingPane.closePane(); + m._loggingPane = null; + } + if (!m._loggingPane || m._loggingPane.closed) { + m._loggingPane = new m.LoggingPane(inline, MochiKit.Logging.logger); + } + return m._loggingPane; +}; + +/** @id MochiKit.LoggingPane.LoggingPane */ +MochiKit.LoggingPane.LoggingPane = function (inline/* = false */, logger/* = MochiKit.Logging.logger */) { + + /* Use a div if inline, pop up a window if not */ + /* Create the elements */ + if (typeof(logger) == "undefined" || logger === null) { + logger = MochiKit.Logging.logger; + } + this.logger = logger; + var update = MochiKit.Base.update; + var updatetree = MochiKit.Base.updatetree; + var bind = MochiKit.Base.bind; + var clone = MochiKit.Base.clone; + var win = window; + var uid = "_MochiKit_LoggingPane"; + if (typeof(MochiKit.DOM) != "undefined") { + win = MochiKit.DOM.currentWindow(); + } + if (!inline) { + // name the popup with the base URL for uniqueness + var url = win.location.href.split("?")[0].replace(/[#:\/.><&%-]/g, "_"); + var name = uid + "_" + url; + var nwin = win.open("", name, "dependent,resizable,height=200"); + if (!nwin) { + alert("Not able to open debugging window due to pop-up blocking."); + return undefined; + } + nwin.document.write( + '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" ' + + '"http://www.w3.org/TR/html4/loose.dtd">' + + '<html><head><title>[MochiKit.LoggingPane]</title></head>' + + '<body></body></html>' + ); + nwin.document.close(); + nwin.document.title += ' ' + win.document.title; + win = nwin; + } + var doc = win.document; + this.doc = doc; + + // Connect to the debug pane if it already exists (i.e. in a window orphaned by the page being refreshed) + var debugPane = doc.getElementById(uid); + var existing_pane = !!debugPane; + if (debugPane && typeof(debugPane.loggingPane) != "undefined") { + debugPane.loggingPane.logger = this.logger; + debugPane.loggingPane.buildAndApplyFilter(); + return debugPane.loggingPane; + } + + if (existing_pane) { + // clear any existing contents + var child; + while ((child = debugPane.firstChild)) { + debugPane.removeChild(child); + } + } else { + debugPane = doc.createElement("div"); + debugPane.id = uid; + } + debugPane.loggingPane = this; + var levelFilterField = doc.createElement("input"); + var infoFilterField = doc.createElement("input"); + var filterButton = doc.createElement("button"); + var loadButton = doc.createElement("button"); + var clearButton = doc.createElement("button"); + var closeButton = doc.createElement("button"); + var logPaneArea = doc.createElement("div"); + var logPane = doc.createElement("div"); + + /* Set up the functions */ + var listenerId = uid + "_Listener"; + this.colorTable = clone(this.colorTable); + var messages = []; + var messageFilter = null; + + /** @id MochiKit.LoggingPane.messageLevel */ + var messageLevel = function (msg) { + var level = msg.level; + if (typeof(level) == "number") { + level = MochiKit.Logging.LogLevel[level]; + } + return level; + }; + + /** @id MochiKit.LoggingPane.messageText */ + var messageText = function (msg) { + return msg.info.join(" "); + }; + + /** @id MochiKit.LoggingPane.addMessageText */ + var addMessageText = bind(function (msg) { + var level = messageLevel(msg); + var text = messageText(msg); + var c = this.colorTable[level]; + var p = doc.createElement("span"); + p.className = "MochiKit-LogMessage MochiKit-LogLevel-" + level; + p.style.cssText = "margin: 0px; white-space: -moz-pre-wrap; white-space: -o-pre-wrap; white-space: pre-wrap; white-space: pre-line; word-wrap: break-word; wrap-option: emergency; color: " + c; + p.appendChild(doc.createTextNode(level + ": " + text)); + logPane.appendChild(p); + logPane.appendChild(doc.createElement("br")); + if (logPaneArea.offsetHeight > logPaneArea.scrollHeight) { + logPaneArea.scrollTop = 0; + } else { + logPaneArea.scrollTop = logPaneArea.scrollHeight; + } + }, this); + + /** @id MochiKit.LoggingPane.addMessage */ + var addMessage = function (msg) { + messages[messages.length] = msg; + addMessageText(msg); + }; + + /** @id MochiKit.LoggingPane.buildMessageFilter */ + var buildMessageFilter = function () { + var levelre, infore; + try { + /* Catch any exceptions that might arise due to invalid regexes */ + levelre = new RegExp(levelFilterField.value); + infore = new RegExp(infoFilterField.value); + } catch(e) { + /* If there was an error with the regexes, do no filtering */ + logDebug("Error in filter regex: " + e.message); + return null; + } + + return function (msg) { + return ( + levelre.test(messageLevel(msg)) && + infore.test(messageText(msg)) + ); + }; + }; + + /** @id MochiKit.LoggingPane.clearMessagePane */ + var clearMessagePane = function () { + while (logPane.firstChild) { + logPane.removeChild(logPane.firstChild); + } + }; + + /** @id MochiKit.LoggingPane.clearMessages */ + var clearMessages = function () { + messages = []; + clearMessagePane(); + }; + + /** @id MochiKit.LoggingPane.closePane */ + var closePane = bind(function () { + if (this.closed) { + return; + } + this.closed = true; + if (MochiKit.LoggingPane._loggingPane == this) { + MochiKit.LoggingPane._loggingPane = null; + } + this.logger.removeListener(listenerId); + try { + try { + debugPane.loggingPane = null; + } catch(e) { logFatal("Bookmarklet was closed incorrectly."); } + if (inline) { + debugPane.parentNode.removeChild(debugPane); + } else { + this.win.close(); + } + } catch(e) {} + }, this); + + /** @id MochiKit.LoggingPane.filterMessages */ + var filterMessages = function () { + clearMessagePane(); + + for (var i = 0; i < messages.length; i++) { + var msg = messages[i]; + if (messageFilter === null || messageFilter(msg)) { + addMessageText(msg); + } + } + }; + + this.buildAndApplyFilter = function () { + messageFilter = buildMessageFilter(); + + filterMessages(); + + this.logger.removeListener(listenerId); + this.logger.addListener(listenerId, messageFilter, addMessage); + }; + + + /** @id MochiKit.LoggingPane.loadMessages */ + var loadMessages = bind(function () { + messages = this.logger.getMessages(); + filterMessages(); + }, this); + + /** @id MochiKit.LoggingPane.filterOnEnter */ + var filterOnEnter = bind(function (event) { + event = event || window.event; + key = event.which || event.keyCode; + if (key == 13) { + this.buildAndApplyFilter(); + } + }, this); + + /* Create the debug pane */ + var style = "display: block; z-index: 1000; left: 0px; bottom: 0px; position: fixed; width: 100%; background-color: white; font: " + this.logFont; + if (inline) { + style += "; height: 10em; border-top: 2px solid black"; + } else { + style += "; height: 100%;"; + } + debugPane.style.cssText = style; + + if (!existing_pane) { + doc.body.appendChild(debugPane); + } + + /* Create the filter fields */ + style = {"cssText": "width: 33%; display: inline; font: " + this.logFont}; + + updatetree(levelFilterField, { + "value": "FATAL|ERROR|WARNING|INFO|DEBUG", + "onkeypress": filterOnEnter, + "style": style + }); + debugPane.appendChild(levelFilterField); + + updatetree(infoFilterField, { + "value": ".*", + "onkeypress": filterOnEnter, + "style": style + }); + debugPane.appendChild(infoFilterField); + + /* Create the buttons */ + style = "width: 8%; display:inline; font: " + this.logFont; + + filterButton.appendChild(doc.createTextNode("Filter")); + filterButton.onclick = bind("buildAndApplyFilter", this); + filterButton.style.cssText = style; + debugPane.appendChild(filterButton); + + loadButton.appendChild(doc.createTextNode("Load")); + loadButton.onclick = loadMessages; + loadButton.style.cssText = style; + debugPane.appendChild(loadButton); + + clearButton.appendChild(doc.createTextNode("Clear")); + clearButton.onclick = clearMessages; + clearButton.style.cssText = style; + debugPane.appendChild(clearButton); + + closeButton.appendChild(doc.createTextNode("Close")); + closeButton.onclick = closePane; + closeButton.style.cssText = style; + debugPane.appendChild(closeButton); + + /* Create the logging pane */ + logPaneArea.style.cssText = "overflow: auto; width: 100%"; + logPane.style.cssText = "width: 100%; height: " + (inline ? "8em" : "100%"); + + logPaneArea.appendChild(logPane); + debugPane.appendChild(logPaneArea); + + this.buildAndApplyFilter(); + loadMessages(); + + if (inline) { + this.win = undefined; + } else { + this.win = win; + } + this.inline = inline; + this.closePane = closePane; + this.closed = false; + + + return this; +}; + +MochiKit.LoggingPane.LoggingPane.prototype = { + "logFont": "8pt Verdana,sans-serif", + "colorTable": { + "ERROR": "red", + "FATAL": "darkred", + "WARNING": "blue", + "INFO": "black", + "DEBUG": "green" + } +}; + + +MochiKit.LoggingPane.EXPORT_OK = [ + "LoggingPane" +]; + +MochiKit.LoggingPane.EXPORT = [ + "createLoggingPane" +]; + +MochiKit.LoggingPane.__new__ = function () { + this.EXPORT_TAGS = { + ":common": this.EXPORT, + ":all": MochiKit.Base.concat(this.EXPORT, this.EXPORT_OK) + }; + + MochiKit.Base.nameFunctions(this); + + MochiKit.LoggingPane._loggingPane = null; + +}; + +MochiKit.LoggingPane.__new__(); + +MochiKit.Base._exportSymbols(this, MochiKit.LoggingPane); diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/MochiKit.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/MochiKit.js new file mode 100644 index 0000000000..1ec157ed2b --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/MochiKit.js @@ -0,0 +1,188 @@ +/*** + +MochiKit.MochiKit 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +if (typeof(MochiKit) == 'undefined') { + MochiKit = {}; +} + +if (typeof(MochiKit.MochiKit) == 'undefined') { + /** @id MochiKit.MochiKit */ + MochiKit.MochiKit = {}; +} + +MochiKit.MochiKit.NAME = "MochiKit.MochiKit"; +MochiKit.MochiKit.VERSION = "1.4.2"; +MochiKit.MochiKit.__repr__ = function () { + return "[" + this.NAME + " " + this.VERSION + "]"; +}; + +/** @id MochiKit.MochiKit.toString */ +MochiKit.MochiKit.toString = function () { + return this.__repr__(); +}; + +/** @id MochiKit.MochiKit.SUBMODULES */ +MochiKit.MochiKit.SUBMODULES = [ + "Base", + "Iter", + "Logging", + "DateTime", + "Format", + "Async", + "DOM", + "Selector", + "Style", + "LoggingPane", + "Color", + "Signal", + "Position", + "Visual", + "DragAndDrop", + "Sortable" +]; + +if (typeof(JSAN) != 'undefined' || typeof(dojo) != 'undefined') { + if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.MochiKit'); + (function (lst) { + for (var i = 0; i < lst.length; i++) { + dojo.require("MochiKit." + lst[i]); + } + })(MochiKit.MochiKit.SUBMODULES); + } + if (typeof(JSAN) != 'undefined') { + (function (lst) { + for (var i = 0; i < lst.length; i++) { + JSAN.use("MochiKit." + lst[i], []); + } + })(MochiKit.MochiKit.SUBMODULES); + } + (function () { + var extend = MochiKit.Base.extend; + var self = MochiKit.MochiKit; + var modules = self.SUBMODULES; + var EXPORT = []; + var EXPORT_OK = []; + var EXPORT_TAGS = {}; + var i, k, m, all; + for (i = 0; i < modules.length; i++) { + m = MochiKit[modules[i]]; + extend(EXPORT, m.EXPORT); + extend(EXPORT_OK, m.EXPORT_OK); + for (k in m.EXPORT_TAGS) { + EXPORT_TAGS[k] = extend(EXPORT_TAGS[k], m.EXPORT_TAGS[k]); + } + all = m.EXPORT_TAGS[":all"]; + if (!all) { + all = extend(null, m.EXPORT, m.EXPORT_OK); + } + var j; + for (j = 0; j < all.length; j++) { + k = all[j]; + self[k] = m[k]; + } + } + self.EXPORT = EXPORT; + self.EXPORT_OK = EXPORT_OK; + self.EXPORT_TAGS = EXPORT_TAGS; + }()); + +} else { + if (typeof(MochiKit.__compat__) == 'undefined') { + MochiKit.__compat__ = true; + } + (function () { + if (typeof(document) == "undefined") { + return; + } + var scripts = document.getElementsByTagName("script"); + var kXHTMLNSURI = "http://www.w3.org/1999/xhtml"; + var kSVGNSURI = "http://www.w3.org/2000/svg"; + var kXLINKNSURI = "http://www.w3.org/1999/xlink"; + var kXULNSURI = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var base = null; + var baseElem = null; + var allScripts = {}; + var i; + var src; + for (i = 0; i < scripts.length; i++) { + src = null; + switch (scripts[i].namespaceURI) { + case kSVGNSURI: + src = scripts[i].getAttributeNS(kXLINKNSURI, "href"); + break; + /* + case null: // HTML + case '': // HTML + case kXHTMLNSURI: + case kXULNSURI: + */ + default: + src = scripts[i].getAttribute("src"); + break; + } + if (!src) { + continue; + } + allScripts[src] = true; + if (src.match(/MochiKit.js(\?.*)?$/)) { + base = src.substring(0, src.lastIndexOf('MochiKit.js')); + baseElem = scripts[i]; + } + } + if (base === null) { + return; + } + var modules = MochiKit.MochiKit.SUBMODULES; + for (var i = 0; i < modules.length; i++) { + if (MochiKit[modules[i]]) { + continue; + } + var uri = base + modules[i] + '.js'; + if (uri in allScripts) { + continue; + } + if (baseElem.namespaceURI == kSVGNSURI || + baseElem.namespaceURI == kXULNSURI) { + // SVG, XUL + /* + SVG does not support document.write, so if Safari wants to + support SVG tests it should fix its deferred loading bug + (see following below). + + */ + var s = document.createElementNS(baseElem.namespaceURI, 'script'); + s.setAttribute("id", "MochiKit_" + base + modules[i]); + if (baseElem.namespaceURI == kSVGNSURI) { + s.setAttributeNS(kXLINKNSURI, 'href', uri); + } else { + s.setAttribute('src', uri); + } + s.setAttribute("type", "application/x-javascript"); + baseElem.parentNode.appendChild(s); + } else { + // HTML, XHTML + /* + DOM can not be used here because Safari does + deferred loading of scripts unless they are + in the document or inserted with document.write + + This is not XHTML compliant. If you want XHTML + compliance then you must use the packed version of MochiKit + or include each script individually (basically unroll + these document.write calls into your XHTML source) + + */ + document.write('<' + baseElem.nodeName + ' src="' + uri + + '" type="text/javascript"></script>'); + } + }; + })(); +} diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/MockDOM.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/MockDOM.js new file mode 100644 index 0000000000..250d12eede --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/MockDOM.js @@ -0,0 +1,115 @@ +/*** + +MochiKit.MockDOM 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +if (typeof(MochiKit) == "undefined") { + MochiKit = {}; +} + +if (typeof(MochiKit.MockDOM) == "undefined") { + MochiKit.MockDOM = {}; +} + +MochiKit.MockDOM.NAME = "MochiKit.MockDOM"; +MochiKit.MockDOM.VERSION = "1.4.2"; + +MochiKit.MockDOM.__repr__ = function () { + return "[" + this.NAME + " " + this.VERSION + "]"; +}; + +/** @id MochiKit.MockDOM.toString */ +MochiKit.MockDOM.toString = function () { + return this.__repr__(); +}; + +/** @id MochiKit.MockDOM.createDocument */ +MochiKit.MockDOM.createDocument = function () { + var doc = new MochiKit.MockDOM.MockElement("DOCUMENT"); + doc.body = doc.createElement("BODY"); + doc.appendChild(doc.body); + return doc; +}; + +/** @id MochiKit.MockDOM.MockElement */ +MochiKit.MockDOM.MockElement = function (name, data, ownerDocument) { + this.tagName = this.nodeName = name.toUpperCase(); + this.ownerDocument = ownerDocument || null; + if (name == "DOCUMENT") { + this.nodeType = 9; + this.childNodes = []; + } else if (typeof(data) == "string") { + this.nodeValue = data; + this.nodeType = 3; + } else { + this.nodeType = 1; + this.childNodes = []; + } + if (name.substring(0, 1) == "<") { + var nameattr = name.substring( + name.indexOf('"') + 1, name.lastIndexOf('"')); + name = name.substring(1, name.indexOf(" ")); + this.tagName = this.nodeName = name.toUpperCase(); + this.setAttribute("name", nameattr); + } +}; + +MochiKit.MockDOM.MockElement.prototype = { + /** @id MochiKit.MockDOM.MockElement.prototype.createElement */ + createElement: function (tagName) { + return new MochiKit.MockDOM.MockElement(tagName, null, this.nodeType == 9 ? this : this.ownerDocument); + }, + /** @id MochiKit.MockDOM.MockElement.prototype.createTextNode */ + createTextNode: function (text) { + return new MochiKit.MockDOM.MockElement("text", text, this.nodeType == 9 ? this : this.ownerDocument); + }, + /** @id MochiKit.MockDOM.MockElement.prototype.setAttribute */ + setAttribute: function (name, value) { + this[name] = value; + }, + /** @id MochiKit.MockDOM.MockElement.prototype.getAttribute */ + getAttribute: function (name) { + return this[name]; + }, + /** @id MochiKit.MockDOM.MockElement.prototype.appendChild */ + appendChild: function (child) { + this.childNodes.push(child); + }, + /** @id MochiKit.MockDOM.MockElement.prototype.toString */ + toString: function () { + return "MockElement(" + this.tagName + ")"; + }, + /** @id MochiKit.MockDOM.MockElement.prototype.getElementsByTagName */ + getElementsByTagName: function (tagName) { + var foundElements = []; + MochiKit.Base.nodeWalk(this, function(node){ + if (tagName == '*' || tagName == node.tagName) { + foundElements.push(node); + return node.childNodes; + } + }); + return foundElements; + } +}; + + /** @id MochiKit.MockDOM.EXPORT_OK */ +MochiKit.MockDOM.EXPORT_OK = [ + "mockElement", + "createDocument" +]; + + /** @id MochiKit.MockDOM.EXPORT */ +MochiKit.MockDOM.EXPORT = [ + "document" +]; + +MochiKit.MockDOM.__new__ = function () { + this.document = this.createDocument(); +}; + +MochiKit.MockDOM.__new__(); diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Position.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Position.js new file mode 100644 index 0000000000..70288c7c15 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Position.js @@ -0,0 +1,236 @@ +/*** + +MochiKit.Position 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005-2006 Bob Ippolito and others. All rights Reserved. + +***/ + +MochiKit.Base._deps('Position', ['Base', 'DOM', 'Style']); + +MochiKit.Position.NAME = 'MochiKit.Position'; +MochiKit.Position.VERSION = '1.4.2'; +MochiKit.Position.__repr__ = function () { + return '[' + this.NAME + ' ' + this.VERSION + ']'; +}; +MochiKit.Position.toString = function () { + return this.__repr__(); +}; + +MochiKit.Position.EXPORT_OK = []; + +MochiKit.Position.EXPORT = [ +]; + + +MochiKit.Base.update(MochiKit.Position, { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + /** @id MochiKit.Position.prepare */ + prepare: function () { + var deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + var deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + this.windowOffset = new MochiKit.Style.Coordinates(deltaX, deltaY); + }, + + /** @id MochiKit.Position.cumulativeOffset */ + cumulativeOffset: function (element) { + var valueT = 0; + var valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return new MochiKit.Style.Coordinates(valueL, valueT); + }, + + /** @id MochiKit.Position.realOffset */ + realOffset: function (element) { + var valueT = 0; + var valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return new MochiKit.Style.Coordinates(valueL, valueT); + }, + + /** @id MochiKit.Position.within */ + within: function (element, x, y) { + if (this.includeScrollOffsets) { + return this.withinIncludingScrolloffsets(element, x, y); + } + this.xcomp = x; + this.ycomp = y; + this.offset = this.cumulativeOffset(element); + if (element.style.position == "fixed") { + this.offset.x += this.windowOffset.x; + this.offset.y += this.windowOffset.y; + } + + return (y >= this.offset.y && + y < this.offset.y + element.offsetHeight && + x >= this.offset.x && + x < this.offset.x + element.offsetWidth); + }, + + /** @id MochiKit.Position.withinIncludingScrolloffsets */ + withinIncludingScrolloffsets: function (element, x, y) { + var offsetcache = this.realOffset(element); + + this.xcomp = x + offsetcache.x - this.windowOffset.x; + this.ycomp = y + offsetcache.y - this.windowOffset.y; + this.offset = this.cumulativeOffset(element); + + return (this.ycomp >= this.offset.y && + this.ycomp < this.offset.y + element.offsetHeight && + this.xcomp >= this.offset.x && + this.xcomp < this.offset.x + element.offsetWidth); + }, + + // within must be called directly before + /** @id MochiKit.Position.overlap */ + overlap: function (mode, element) { + if (!mode) { + return 0; + } + if (mode == 'vertical') { + return ((this.offset.y + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + } + if (mode == 'horizontal') { + return ((this.offset.x + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + } + }, + + /** @id MochiKit.Position.absolutize */ + absolutize: function (element) { + element = MochiKit.DOM.getElement(element); + if (element.style.position == 'absolute') { + return; + } + MochiKit.Position.prepare(); + + var offsets = MochiKit.Position.positionedOffset(element); + var width = element.clientWidth; + var height = element.clientHeight; + + var oldStyle = { + 'position': element.style.position, + 'left': offsets.x - parseFloat(element.style.left || 0), + 'top': offsets.y - parseFloat(element.style.top || 0), + 'width': element.style.width, + 'height': element.style.height + }; + + element.style.position = 'absolute'; + element.style.top = offsets.y + 'px'; + element.style.left = offsets.x + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + + return oldStyle; + }, + + /** @id MochiKit.Position.positionedOffset */ + positionedOffset: function (element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + p = MochiKit.Style.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') { + break; + } + } + } while (element); + return new MochiKit.Style.Coordinates(valueL, valueT); + }, + + /** @id MochiKit.Position.relativize */ + relativize: function (element, oldPos) { + element = MochiKit.DOM.getElement(element); + if (element.style.position == 'relative') { + return; + } + MochiKit.Position.prepare(); + + var top = parseFloat(element.style.top || 0) - + (oldPos['top'] || 0); + var left = parseFloat(element.style.left || 0) - + (oldPos['left'] || 0); + + element.style.position = oldPos['position']; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = oldPos['width']; + element.style.height = oldPos['height']; + }, + + /** @id MochiKit.Position.clone */ + clone: function (source, target) { + source = MochiKit.DOM.getElement(source); + target = MochiKit.DOM.getElement(target); + target.style.position = 'absolute'; + var offsets = this.cumulativeOffset(source); + target.style.top = offsets.y + 'px'; + target.style.left = offsets.x + 'px'; + target.style.width = source.offsetWidth + 'px'; + target.style.height = source.offsetHeight + 'px'; + }, + + /** @id MochiKit.Position.page */ + page: function (forElement) { + var valueT = 0; + var valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent == document.body && MochiKit.Style.getStyle(element, 'position') == 'absolute') { + break; + } + } while (element = element.offsetParent); + + element = forElement; + do { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } while (element = element.parentNode); + + return new MochiKit.Style.Coordinates(valueL, valueT); + } +}); + +MochiKit.Position.__new__ = function (win) { + var m = MochiKit.Base; + this.EXPORT_TAGS = { + ':common': this.EXPORT, + ':all': m.concat(this.EXPORT, this.EXPORT_OK) + }; + + m.nameFunctions(this); +}; + +MochiKit.Position.__new__(this); + +MochiKit.Base._exportSymbols(this, MochiKit.Position);
\ No newline at end of file diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Selector.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Selector.js new file mode 100644 index 0000000000..e428cad16e --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Selector.js @@ -0,0 +1,415 @@ +/*** + +MochiKit.Selector 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito and others. All rights Reserved. + +***/ + +MochiKit.Base._deps('Selector', ['Base', 'DOM', 'Iter']); + +MochiKit.Selector.NAME = "MochiKit.Selector"; +MochiKit.Selector.VERSION = "1.4.2"; + +MochiKit.Selector.__repr__ = function () { + return "[" + this.NAME + " " + this.VERSION + "]"; +}; + +MochiKit.Selector.toString = function () { + return this.__repr__(); +}; + +MochiKit.Selector.EXPORT = [ + "Selector", + "findChildElements", + "findDocElements", + "$$" +]; + +MochiKit.Selector.EXPORT_OK = [ +]; + +MochiKit.Selector.Selector = function (expression) { + this.params = {classNames: [], pseudoClassNames: []}; + this.expression = expression.toString().replace(/(^\s+|\s+$)/g, ''); + this.parseExpression(); + this.compileMatcher(); +}; + +MochiKit.Selector.Selector.prototype = { + /*** + + Selector class: convenient object to make CSS selections. + + ***/ + __class__: MochiKit.Selector.Selector, + + /** @id MochiKit.Selector.Selector.prototype.parseExpression */ + parseExpression: function () { + function abort(message) { + throw 'Parse error in selector: ' + message; + } + + if (this.expression == '') { + abort('empty expression'); + } + + var repr = MochiKit.Base.repr; + var params = this.params; + var expr = this.expression; + var match, modifier, clause, rest; + while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!^$*]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) { + params.attributes = params.attributes || []; + params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''}); + expr = match[1]; + } + + if (expr == '*') { + return this.params.wildcard = true; + } + + while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+(?:\([^)]*\))?)(.*)/i)) { + modifier = match[1]; + clause = match[2]; + rest = match[3]; + switch (modifier) { + case '#': + params.id = clause; + break; + case '.': + params.classNames.push(clause); + break; + case ':': + params.pseudoClassNames.push(clause); + break; + case '': + case undefined: + params.tagName = clause.toUpperCase(); + break; + default: + abort(repr(expr)); + } + expr = rest; + } + + if (expr.length > 0) { + abort(repr(expr)); + } + }, + + /** @id MochiKit.Selector.Selector.prototype.buildMatchExpression */ + buildMatchExpression: function () { + var repr = MochiKit.Base.repr; + var params = this.params; + var conditions = []; + var clause, i; + + function childElements(element) { + return "MochiKit.Base.filter(function (node) { return node.nodeType == 1; }, " + element + ".childNodes)"; + } + + if (params.wildcard) { + conditions.push('true'); + } + if (clause = params.id) { + conditions.push('element.id == ' + repr(clause)); + } + if (clause = params.tagName) { + conditions.push('element.tagName.toUpperCase() == ' + repr(clause)); + } + if ((clause = params.classNames).length > 0) { + for (i = 0; i < clause.length; i++) { + conditions.push('MochiKit.DOM.hasElementClass(element, ' + repr(clause[i]) + ')'); + } + } + if ((clause = params.pseudoClassNames).length > 0) { + for (i = 0; i < clause.length; i++) { + var match = clause[i].match(/^([^(]+)(?:\((.*)\))?$/); + var pseudoClass = match[1]; + var pseudoClassArgument = match[2]; + switch (pseudoClass) { + case 'root': + conditions.push('element.nodeType == 9 || element === element.ownerDocument.documentElement'); break; + case 'nth-child': + case 'nth-last-child': + case 'nth-of-type': + case 'nth-last-of-type': + match = pseudoClassArgument.match(/^((?:(\d+)n\+)?(\d+)|odd|even)$/); + if (!match) { + throw "Invalid argument to pseudo element nth-child: " + pseudoClassArgument; + } + var a, b; + if (match[0] == 'odd') { + a = 2; + b = 1; + } else if (match[0] == 'even') { + a = 2; + b = 0; + } else { + a = match[2] && parseInt(match) || null; + b = parseInt(match[3]); + } + conditions.push('this.nthChild(element,' + a + ',' + b + + ',' + !!pseudoClass.match('^nth-last') // Reverse + + ',' + !!pseudoClass.match('of-type$') // Restrict to same tagName + + ')'); + break; + case 'first-child': + conditions.push('this.nthChild(element, null, 1)'); + break; + case 'last-child': + conditions.push('this.nthChild(element, null, 1, true)'); + break; + case 'first-of-type': + conditions.push('this.nthChild(element, null, 1, false, true)'); + break; + case 'last-of-type': + conditions.push('this.nthChild(element, null, 1, true, true)'); + break; + case 'only-child': + conditions.push(childElements('element.parentNode') + '.length == 1'); + break; + case 'only-of-type': + conditions.push('MochiKit.Base.filter(function (node) { return node.tagName == element.tagName; }, ' + childElements('element.parentNode') + ').length == 1'); + break; + case 'empty': + conditions.push('element.childNodes.length == 0'); + break; + case 'enabled': + conditions.push('(this.isUIElement(element) && element.disabled === false)'); + break; + case 'disabled': + conditions.push('(this.isUIElement(element) && element.disabled === true)'); + break; + case 'checked': + conditions.push('(this.isUIElement(element) && element.checked === true)'); + break; + case 'not': + var subselector = new MochiKit.Selector.Selector(pseudoClassArgument); + conditions.push('!( ' + subselector.buildMatchExpression() + ')') + break; + } + } + } + if (clause = params.attributes) { + MochiKit.Base.map(function (attribute) { + var value = 'MochiKit.DOM.getNodeAttribute(element, ' + repr(attribute.name) + ')'; + var splitValueBy = function (delimiter) { + return value + '.split(' + repr(delimiter) + ')'; + } + conditions.push(value + ' != null'); + switch (attribute.operator) { + case '=': + conditions.push(value + ' == ' + repr(attribute.value)); + break; + case '~=': + conditions.push('MochiKit.Base.findValue(' + splitValueBy(' ') + ', ' + repr(attribute.value) + ') > -1'); + break; + case '^=': + conditions.push(value + '.substring(0, ' + attribute.value.length + ') == ' + repr(attribute.value)); + break; + case '$=': + conditions.push(value + '.substring(' + value + '.length - ' + attribute.value.length + ') == ' + repr(attribute.value)); + break; + case '*=': + conditions.push(value + '.match(' + repr(attribute.value) + ')'); + break; + case '|=': + conditions.push(splitValueBy('-') + '[0].toUpperCase() == ' + repr(attribute.value.toUpperCase())); + break; + case '!=': + conditions.push(value + ' != ' + repr(attribute.value)); + break; + case '': + case undefined: + // Condition already added above + break; + default: + throw 'Unknown operator ' + attribute.operator + ' in selector'; + } + }, clause); + } + + return conditions.join(' && '); + }, + + /** @id MochiKit.Selector.Selector.prototype.compileMatcher */ + compileMatcher: function () { + var code = 'return (!element.tagName) ? false : ' + + this.buildMatchExpression() + ';'; + this.match = new Function('element', code); + }, + + /** @id MochiKit.Selector.Selector.prototype.nthChild */ + nthChild: function (element, a, b, reverse, sametag){ + var siblings = MochiKit.Base.filter(function (node) { + return node.nodeType == 1; + }, element.parentNode.childNodes); + if (sametag) { + siblings = MochiKit.Base.filter(function (node) { + return node.tagName == element.tagName; + }, siblings); + } + if (reverse) { + siblings = MochiKit.Iter.reversed(siblings); + } + if (a) { + var actualIndex = MochiKit.Base.findIdentical(siblings, element); + return ((actualIndex + 1 - b) / a) % 1 == 0; + } else { + return b == MochiKit.Base.findIdentical(siblings, element) + 1; + } + }, + + /** @id MochiKit.Selector.Selector.prototype.isUIElement */ + isUIElement: function (element) { + return MochiKit.Base.findValue(['input', 'button', 'select', 'option', 'textarea', 'object'], + element.tagName.toLowerCase()) > -1; + }, + + /** @id MochiKit.Selector.Selector.prototype.findElements */ + findElements: function (scope, axis) { + var element; + + if (axis == undefined) { + axis = ""; + } + + function inScope(element, scope) { + if (axis == "") { + return MochiKit.DOM.isChildNode(element, scope); + } else if (axis == ">") { + return element.parentNode === scope; + } else if (axis == "+") { + return element === nextSiblingElement(scope); + } else if (axis == "~") { + var sibling = scope; + while (sibling = nextSiblingElement(sibling)) { + if (element === sibling) { + return true; + } + } + return false; + } else { + throw "Invalid axis: " + axis; + } + } + + if (element = MochiKit.DOM.getElement(this.params.id)) { + if (this.match(element)) { + if (!scope || inScope(element, scope)) { + return [element]; + } + } + } + + function nextSiblingElement(node) { + node = node.nextSibling; + while (node && node.nodeType != 1) { + node = node.nextSibling; + } + return node; + } + + if (axis == "") { + scope = (scope || MochiKit.DOM.currentDocument()).getElementsByTagName(this.params.tagName || '*'); + } else if (axis == ">") { + if (!scope) { + throw "> combinator not allowed without preceding expression"; + } + scope = MochiKit.Base.filter(function (node) { + return node.nodeType == 1; + }, scope.childNodes); + } else if (axis == "+") { + if (!scope) { + throw "+ combinator not allowed without preceding expression"; + } + scope = nextSiblingElement(scope) && [nextSiblingElement(scope)]; + } else if (axis == "~") { + if (!scope) { + throw "~ combinator not allowed without preceding expression"; + } + var newscope = []; + while (nextSiblingElement(scope)) { + scope = nextSiblingElement(scope); + newscope.push(scope); + } + scope = newscope; + } + + if (!scope) { + return []; + } + + var results = MochiKit.Base.filter(MochiKit.Base.bind(function (scopeElt) { + return this.match(scopeElt); + }, this), scope); + + return results; + }, + + /** @id MochiKit.Selector.Selector.prototype.repr */ + repr: function () { + return 'Selector(' + this.expression + ')'; + }, + + toString: MochiKit.Base.forwardCall("repr") +}; + +MochiKit.Base.update(MochiKit.Selector, { + + /** @id MochiKit.Selector.findChildElements */ + findChildElements: function (element, expressions) { + var uniq = function(arr) { + var res = []; + for (var i = 0; i < arr.length; i++) { + if (MochiKit.Base.findIdentical(res, arr[i]) < 0) { + res.push(arr[i]); + } + } + return res; + }; + return MochiKit.Base.flattenArray(MochiKit.Base.map(function (expression) { + var nextScope = ""; + var reducer = function (results, expr) { + if (match = expr.match(/^[>+~]$/)) { + nextScope = match[0]; + return results; + } else { + var selector = new MochiKit.Selector.Selector(expr); + var elements = MochiKit.Iter.reduce(function (elements, result) { + return MochiKit.Base.extend(elements, selector.findElements(result || element, nextScope)); + }, results, []); + nextScope = ""; + return elements; + } + }; + var exprs = expression.replace(/(^\s+|\s+$)/g, '').split(/\s+/); + return uniq(MochiKit.Iter.reduce(reducer, exprs, [null])); + }, expressions)); + }, + + findDocElements: function () { + return MochiKit.Selector.findChildElements(MochiKit.DOM.currentDocument(), arguments); + }, + + __new__: function () { + var m = MochiKit.Base; + + this.$$ = this.findDocElements; + + this.EXPORT_TAGS = { + ":common": this.EXPORT, + ":all": m.concat(this.EXPORT, this.EXPORT_OK) + }; + + m.nameFunctions(this); + } +}); + +MochiKit.Selector.__new__(); + +MochiKit.Base._exportSymbols(this, MochiKit.Selector); + diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Signal.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Signal.js new file mode 100644 index 0000000000..d49513c459 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Signal.js @@ -0,0 +1,897 @@ +/*** + +MochiKit.Signal 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2006 Jonathan Gardner, Beau Hartshorne, Bob Ippolito. All rights Reserved. + +***/ + +MochiKit.Base._deps('Signal', ['Base', 'DOM', 'Style']); + +MochiKit.Signal.NAME = 'MochiKit.Signal'; +MochiKit.Signal.VERSION = '1.4.2'; + +MochiKit.Signal._observers = []; + +/** @id MochiKit.Signal.Event */ +MochiKit.Signal.Event = function (src, e) { + this._event = e || window.event; + this._src = src; +}; + +MochiKit.Base.update(MochiKit.Signal.Event.prototype, { + + __repr__: function () { + var repr = MochiKit.Base.repr; + var str = '{event(): ' + repr(this.event()) + + ', src(): ' + repr(this.src()) + + ', type(): ' + repr(this.type()) + + ', target(): ' + repr(this.target()); + + if (this.type() && + this.type().indexOf('key') === 0 || + this.type().indexOf('mouse') === 0 || + this.type().indexOf('click') != -1 || + this.type() == 'contextmenu') { + str += ', modifier(): ' + '{alt: ' + repr(this.modifier().alt) + + ', ctrl: ' + repr(this.modifier().ctrl) + + ', meta: ' + repr(this.modifier().meta) + + ', shift: ' + repr(this.modifier().shift) + + ', any: ' + repr(this.modifier().any) + '}'; + } + + if (this.type() && this.type().indexOf('key') === 0) { + str += ', key(): {code: ' + repr(this.key().code) + + ', string: ' + repr(this.key().string) + '}'; + } + + if (this.type() && ( + this.type().indexOf('mouse') === 0 || + this.type().indexOf('click') != -1 || + this.type() == 'contextmenu')) { + + str += ', mouse(): {page: ' + repr(this.mouse().page) + + ', client: ' + repr(this.mouse().client); + + if (this.type() != 'mousemove' && this.type() != 'mousewheel') { + str += ', button: {left: ' + repr(this.mouse().button.left) + + ', middle: ' + repr(this.mouse().button.middle) + + ', right: ' + repr(this.mouse().button.right) + '}'; + } + if (this.type() == 'mousewheel') { + str += ', wheel: ' + repr(this.mouse().wheel); + } + str += '}'; + } + if (this.type() == 'mouseover' || this.type() == 'mouseout' || + this.type() == 'mouseenter' || this.type() == 'mouseleave') { + str += ', relatedTarget(): ' + repr(this.relatedTarget()); + } + str += '}'; + return str; + }, + + /** @id MochiKit.Signal.Event.prototype.toString */ + toString: function () { + return this.__repr__(); + }, + + /** @id MochiKit.Signal.Event.prototype.src */ + src: function () { + return this._src; + }, + + /** @id MochiKit.Signal.Event.prototype.event */ + event: function () { + return this._event; + }, + + /** @id MochiKit.Signal.Event.prototype.type */ + type: function () { + if (this._event.type === "DOMMouseScroll") { + return "mousewheel"; + } else { + return this._event.type || undefined; + } + }, + + /** @id MochiKit.Signal.Event.prototype.target */ + target: function () { + return this._event.target || this._event.srcElement; + }, + + _relatedTarget: null, + /** @id MochiKit.Signal.Event.prototype.relatedTarget */ + relatedTarget: function () { + if (this._relatedTarget !== null) { + return this._relatedTarget; + } + + var elem = null; + if (this.type() == 'mouseover' || this.type() == 'mouseenter') { + elem = (this._event.relatedTarget || + this._event.fromElement); + } else if (this.type() == 'mouseout' || this.type() == 'mouseleave') { + elem = (this._event.relatedTarget || + this._event.toElement); + } + try { + if (elem !== null && elem.nodeType !== null) { + this._relatedTarget = elem; + return elem; + } + } catch (ignore) { + // Firefox 3 throws a permission denied error when accessing + // any property on XUL elements (e.g. scrollbars)... + } + + return undefined; + }, + + _modifier: null, + /** @id MochiKit.Signal.Event.prototype.modifier */ + modifier: function () { + if (this._modifier !== null) { + return this._modifier; + } + var m = {}; + m.alt = this._event.altKey; + m.ctrl = this._event.ctrlKey; + m.meta = this._event.metaKey || false; // IE and Opera punt here + m.shift = this._event.shiftKey; + m.any = m.alt || m.ctrl || m.shift || m.meta; + this._modifier = m; + return m; + }, + + _key: null, + /** @id MochiKit.Signal.Event.prototype.key */ + key: function () { + if (this._key !== null) { + return this._key; + } + var k = {}; + if (this.type() && this.type().indexOf('key') === 0) { + + /* + + If you're looking for a special key, look for it in keydown or + keyup, but never keypress. If you're looking for a Unicode + chracter, look for it with keypress, but never keyup or + keydown. + + Notes: + + FF key event behavior: + key event charCode keyCode + DOWN ku,kd 0 40 + DOWN kp 0 40 + ESC ku,kd 0 27 + ESC kp 0 27 + a ku,kd 0 65 + a kp 97 0 + shift+a ku,kd 0 65 + shift+a kp 65 0 + 1 ku,kd 0 49 + 1 kp 49 0 + shift+1 ku,kd 0 0 + shift+1 kp 33 0 + + IE key event behavior: + (IE doesn't fire keypress events for special keys.) + key event keyCode + DOWN ku,kd 40 + DOWN kp undefined + ESC ku,kd 27 + ESC kp 27 + a ku,kd 65 + a kp 97 + shift+a ku,kd 65 + shift+a kp 65 + 1 ku,kd 49 + 1 kp 49 + shift+1 ku,kd 49 + shift+1 kp 33 + + Safari key event behavior: + (Safari sets charCode and keyCode to something crazy for + special keys.) + key event charCode keyCode + DOWN ku,kd 63233 40 + DOWN kp 63233 63233 + ESC ku,kd 27 27 + ESC kp 27 27 + a ku,kd 97 65 + a kp 97 97 + shift+a ku,kd 65 65 + shift+a kp 65 65 + 1 ku,kd 49 49 + 1 kp 49 49 + shift+1 ku,kd 33 49 + shift+1 kp 33 33 + + */ + + /* look for special keys here */ + if (this.type() == 'keydown' || this.type() == 'keyup') { + k.code = this._event.keyCode; + k.string = (MochiKit.Signal._specialKeys[k.code] || + 'KEY_UNKNOWN'); + this._key = k; + return k; + + /* look for characters here */ + } else if (this.type() == 'keypress') { + + /* + + Special key behavior: + + IE: does not fire keypress events for special keys + FF: sets charCode to 0, and sets the correct keyCode + Safari: sets keyCode and charCode to something stupid + + */ + + k.code = 0; + k.string = ''; + + if (typeof(this._event.charCode) != 'undefined' && + this._event.charCode !== 0 && + !MochiKit.Signal._specialMacKeys[this._event.charCode]) { + k.code = this._event.charCode; + k.string = String.fromCharCode(k.code); + } else if (this._event.keyCode && + typeof(this._event.charCode) == 'undefined') { // IE + k.code = this._event.keyCode; + k.string = String.fromCharCode(k.code); + } + + this._key = k; + return k; + } + } + return undefined; + }, + + _mouse: null, + /** @id MochiKit.Signal.Event.prototype.mouse */ + mouse: function () { + if (this._mouse !== null) { + return this._mouse; + } + + var m = {}; + var e = this._event; + + if (this.type() && ( + this.type().indexOf('mouse') === 0 || + this.type().indexOf('click') != -1 || + this.type() == 'contextmenu')) { + + m.client = new MochiKit.Style.Coordinates(0, 0); + if (e.clientX || e.clientY) { + m.client.x = (!e.clientX || e.clientX < 0) ? 0 : e.clientX; + m.client.y = (!e.clientY || e.clientY < 0) ? 0 : e.clientY; + } + + m.page = new MochiKit.Style.Coordinates(0, 0); + if (e.pageX || e.pageY) { + m.page.x = (!e.pageX || e.pageX < 0) ? 0 : e.pageX; + m.page.y = (!e.pageY || e.pageY < 0) ? 0 : e.pageY; + } else { + /* + + The IE shortcut can be off by two. We fix it. See: + http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/getboundingclientrect.asp + + This is similar to the method used in + MochiKit.Style.getElementPosition(). + + */ + var de = MochiKit.DOM._document.documentElement; + var b = MochiKit.DOM._document.body; + + m.page.x = e.clientX + + (de.scrollLeft || b.scrollLeft) - + (de.clientLeft || 0); + + m.page.y = e.clientY + + (de.scrollTop || b.scrollTop) - + (de.clientTop || 0); + + } + if (this.type() != 'mousemove' && this.type() != 'mousewheel') { + m.button = {}; + m.button.left = false; + m.button.right = false; + m.button.middle = false; + + /* we could check e.button, but which is more consistent */ + if (e.which) { + m.button.left = (e.which == 1); + m.button.middle = (e.which == 2); + m.button.right = (e.which == 3); + + /* + + Mac browsers and right click: + + - Safari doesn't fire any click events on a right + click: + http://bugs.webkit.org/show_bug.cgi?id=6595 + + - Firefox fires the event, and sets ctrlKey = true + + - Opera fires the event, and sets metaKey = true + + oncontextmenu is fired on right clicks between + browsers and across platforms. + + */ + + } else { + m.button.left = !!(e.button & 1); + m.button.right = !!(e.button & 2); + m.button.middle = !!(e.button & 4); + } + } + if (this.type() == 'mousewheel') { + m.wheel = new MochiKit.Style.Coordinates(0, 0); + if (e.wheelDeltaX || e.wheelDeltaY) { + m.wheel.x = e.wheelDeltaX / -40 || 0; + m.wheel.y = e.wheelDeltaY / -40 || 0; + } else if (e.wheelDelta) { + m.wheel.y = e.wheelDelta / -40; + } else { + m.wheel.y = e.detail || 0; + } + } + this._mouse = m; + return m; + } + return undefined; + }, + + /** @id MochiKit.Signal.Event.prototype.stop */ + stop: function () { + this.stopPropagation(); + this.preventDefault(); + }, + + /** @id MochiKit.Signal.Event.prototype.stopPropagation */ + stopPropagation: function () { + if (this._event.stopPropagation) { + this._event.stopPropagation(); + } else { + this._event.cancelBubble = true; + } + }, + + /** @id MochiKit.Signal.Event.prototype.preventDefault */ + preventDefault: function () { + if (this._event.preventDefault) { + this._event.preventDefault(); + } else if (this._confirmUnload === null) { + this._event.returnValue = false; + } + }, + + _confirmUnload: null, + + /** @id MochiKit.Signal.Event.prototype.confirmUnload */ + confirmUnload: function (msg) { + if (this.type() == 'beforeunload') { + this._confirmUnload = msg; + this._event.returnValue = msg; + } + } +}); + +/* Safari sets keyCode to these special values onkeypress. */ +MochiKit.Signal._specialMacKeys = { + 3: 'KEY_ENTER', + 63289: 'KEY_NUM_PAD_CLEAR', + 63276: 'KEY_PAGE_UP', + 63277: 'KEY_PAGE_DOWN', + 63275: 'KEY_END', + 63273: 'KEY_HOME', + 63234: 'KEY_ARROW_LEFT', + 63232: 'KEY_ARROW_UP', + 63235: 'KEY_ARROW_RIGHT', + 63233: 'KEY_ARROW_DOWN', + 63302: 'KEY_INSERT', + 63272: 'KEY_DELETE' +}; + +/* for KEY_F1 - KEY_F12 */ +(function () { + var _specialMacKeys = MochiKit.Signal._specialMacKeys; + for (i = 63236; i <= 63242; i++) { + // no F0 + _specialMacKeys[i] = 'KEY_F' + (i - 63236 + 1); + } +})(); + +/* Standard keyboard key codes. */ +MochiKit.Signal._specialKeys = { + 8: 'KEY_BACKSPACE', + 9: 'KEY_TAB', + 12: 'KEY_NUM_PAD_CLEAR', // weird, for Safari and Mac FF only + 13: 'KEY_ENTER', + 16: 'KEY_SHIFT', + 17: 'KEY_CTRL', + 18: 'KEY_ALT', + 19: 'KEY_PAUSE', + 20: 'KEY_CAPS_LOCK', + 27: 'KEY_ESCAPE', + 32: 'KEY_SPACEBAR', + 33: 'KEY_PAGE_UP', + 34: 'KEY_PAGE_DOWN', + 35: 'KEY_END', + 36: 'KEY_HOME', + 37: 'KEY_ARROW_LEFT', + 38: 'KEY_ARROW_UP', + 39: 'KEY_ARROW_RIGHT', + 40: 'KEY_ARROW_DOWN', + 44: 'KEY_PRINT_SCREEN', + 45: 'KEY_INSERT', + 46: 'KEY_DELETE', + 59: 'KEY_SEMICOLON', // weird, for Safari and IE only + 91: 'KEY_WINDOWS_LEFT', + 92: 'KEY_WINDOWS_RIGHT', + 93: 'KEY_SELECT', + 106: 'KEY_NUM_PAD_ASTERISK', + 107: 'KEY_NUM_PAD_PLUS_SIGN', + 109: 'KEY_NUM_PAD_HYPHEN-MINUS', + 110: 'KEY_NUM_PAD_FULL_STOP', + 111: 'KEY_NUM_PAD_SOLIDUS', + 144: 'KEY_NUM_LOCK', + 145: 'KEY_SCROLL_LOCK', + 186: 'KEY_SEMICOLON', + 187: 'KEY_EQUALS_SIGN', + 188: 'KEY_COMMA', + 189: 'KEY_HYPHEN-MINUS', + 190: 'KEY_FULL_STOP', + 191: 'KEY_SOLIDUS', + 192: 'KEY_GRAVE_ACCENT', + 219: 'KEY_LEFT_SQUARE_BRACKET', + 220: 'KEY_REVERSE_SOLIDUS', + 221: 'KEY_RIGHT_SQUARE_BRACKET', + 222: 'KEY_APOSTROPHE' + // undefined: 'KEY_UNKNOWN' +}; + +(function () { + /* for KEY_0 - KEY_9 */ + var _specialKeys = MochiKit.Signal._specialKeys; + for (var i = 48; i <= 57; i++) { + _specialKeys[i] = 'KEY_' + (i - 48); + } + + /* for KEY_A - KEY_Z */ + for (i = 65; i <= 90; i++) { + _specialKeys[i] = 'KEY_' + String.fromCharCode(i); + } + + /* for KEY_NUM_PAD_0 - KEY_NUM_PAD_9 */ + for (i = 96; i <= 105; i++) { + _specialKeys[i] = 'KEY_NUM_PAD_' + (i - 96); + } + + /* for KEY_F1 - KEY_F12 */ + for (i = 112; i <= 123; i++) { + // no F0 + _specialKeys[i] = 'KEY_F' + (i - 112 + 1); + } +})(); + +/* Internal object to keep track of created signals. */ +MochiKit.Signal.Ident = function (ident) { + this.source = ident.source; + this.signal = ident.signal; + this.listener = ident.listener; + this.isDOM = ident.isDOM; + this.objOrFunc = ident.objOrFunc; + this.funcOrStr = ident.funcOrStr; + this.connected = ident.connected; +}; + +MochiKit.Signal.Ident.prototype = {}; + +MochiKit.Base.update(MochiKit.Signal, { + + __repr__: function () { + return '[' + this.NAME + ' ' + this.VERSION + ']'; + }, + + toString: function () { + return this.__repr__(); + }, + + _unloadCache: function () { + var self = MochiKit.Signal; + var observers = self._observers; + + for (var i = 0; i < observers.length; i++) { + if (observers[i].signal !== 'onload' && observers[i].signal !== 'onunload') { + self._disconnect(observers[i]); + } + } + }, + + _listener: function (src, sig, func, obj, isDOM) { + var self = MochiKit.Signal; + var E = self.Event; + if (!isDOM) { + /* We don't want to re-bind already bound methods */ + if (typeof(func.im_self) == 'undefined') { + return MochiKit.Base.bindLate(func, obj); + } else { + return func; + } + } + obj = obj || src; + if (typeof(func) == "string") { + if (sig === 'onload' || sig === 'onunload') { + return function (nativeEvent) { + obj[func].apply(obj, [new E(src, nativeEvent)]); + + var ident = new MochiKit.Signal.Ident({ + source: src, signal: sig, objOrFunc: obj, funcOrStr: func}); + + MochiKit.Signal._disconnect(ident); + }; + } else { + return function (nativeEvent) { + obj[func].apply(obj, [new E(src, nativeEvent)]); + }; + } + } else { + if (sig === 'onload' || sig === 'onunload') { + return function (nativeEvent) { + func.apply(obj, [new E(src, nativeEvent)]); + + var ident = new MochiKit.Signal.Ident({ + source: src, signal: sig, objOrFunc: func}); + + MochiKit.Signal._disconnect(ident); + }; + } else { + return function (nativeEvent) { + func.apply(obj, [new E(src, nativeEvent)]); + }; + } + } + }, + + _browserAlreadyHasMouseEnterAndLeave: function () { + return /MSIE/.test(navigator.userAgent); + }, + + _browserLacksMouseWheelEvent: function () { + return /Gecko\//.test(navigator.userAgent); + }, + + _mouseEnterListener: function (src, sig, func, obj) { + var E = MochiKit.Signal.Event; + return function (nativeEvent) { + var e = new E(src, nativeEvent); + try { + e.relatedTarget().nodeName; + } catch (err) { + /* probably hit a permission denied error; possibly one of + * firefox's screwy anonymous DIVs inside an input element. + * Allow this event to propogate up. + */ + return; + } + e.stop(); + if (MochiKit.DOM.isChildNode(e.relatedTarget(), src)) { + /* We've moved between our node and a child. Ignore. */ + return; + } + e.type = function () { return sig; }; + if (typeof(func) == "string") { + return obj[func].apply(obj, [e]); + } else { + return func.apply(obj, [e]); + } + }; + }, + + _getDestPair: function (objOrFunc, funcOrStr) { + var obj = null; + var func = null; + if (typeof(funcOrStr) != 'undefined') { + obj = objOrFunc; + func = funcOrStr; + if (typeof(funcOrStr) == 'string') { + if (typeof(objOrFunc[funcOrStr]) != "function") { + throw new Error("'funcOrStr' must be a function on 'objOrFunc'"); + } + } else if (typeof(funcOrStr) != 'function') { + throw new Error("'funcOrStr' must be a function or string"); + } + } else if (typeof(objOrFunc) != "function") { + throw new Error("'objOrFunc' must be a function if 'funcOrStr' is not given"); + } else { + func = objOrFunc; + } + return [obj, func]; + }, + + /** @id MochiKit.Signal.connect */ + connect: function (src, sig, objOrFunc/* optional */, funcOrStr) { + src = MochiKit.DOM.getElement(src); + var self = MochiKit.Signal; + + if (typeof(sig) != 'string') { + throw new Error("'sig' must be a string"); + } + + var destPair = self._getDestPair(objOrFunc, funcOrStr); + var obj = destPair[0]; + var func = destPair[1]; + if (typeof(obj) == 'undefined' || obj === null) { + obj = src; + } + + var isDOM = !!(src.addEventListener || src.attachEvent); + if (isDOM && (sig === "onmouseenter" || sig === "onmouseleave") + && !self._browserAlreadyHasMouseEnterAndLeave()) { + var listener = self._mouseEnterListener(src, sig.substr(2), func, obj); + if (sig === "onmouseenter") { + sig = "onmouseover"; + } else { + sig = "onmouseout"; + } + } else if (isDOM && sig == "onmousewheel" && self._browserLacksMouseWheelEvent()) { + var listener = self._listener(src, sig, func, obj, isDOM); + sig = "onDOMMouseScroll"; + } else { + var listener = self._listener(src, sig, func, obj, isDOM); + } + + if (src.addEventListener) { + src.addEventListener(sig.substr(2), listener, false); + } else if (src.attachEvent) { + src.attachEvent(sig, listener); // useCapture unsupported + } + + var ident = new MochiKit.Signal.Ident({ + source: src, + signal: sig, + listener: listener, + isDOM: isDOM, + objOrFunc: objOrFunc, + funcOrStr: funcOrStr, + connected: true + }); + self._observers.push(ident); + + if (!isDOM && typeof(src.__connect__) == 'function') { + var args = MochiKit.Base.extend([ident], arguments, 1); + src.__connect__.apply(src, args); + } + + return ident; + }, + + _disconnect: function (ident) { + // already disconnected + if (!ident.connected) { + return; + } + ident.connected = false; + var src = ident.source; + var sig = ident.signal; + var listener = ident.listener; + // check isDOM + if (!ident.isDOM) { + if (typeof(src.__disconnect__) == 'function') { + src.__disconnect__(ident, sig, ident.objOrFunc, ident.funcOrStr); + } + return; + } + if (src.removeEventListener) { + src.removeEventListener(sig.substr(2), listener, false); + } else if (src.detachEvent) { + src.detachEvent(sig, listener); // useCapture unsupported + } else { + throw new Error("'src' must be a DOM element"); + } + }, + + /** @id MochiKit.Signal.disconnect */ + disconnect: function (ident) { + var self = MochiKit.Signal; + var observers = self._observers; + var m = MochiKit.Base; + if (arguments.length > 1) { + // compatibility API + var src = MochiKit.DOM.getElement(arguments[0]); + var sig = arguments[1]; + var obj = arguments[2]; + var func = arguments[3]; + for (var i = observers.length - 1; i >= 0; i--) { + var o = observers[i]; + if (o.source === src && o.signal === sig && o.objOrFunc === obj && o.funcOrStr === func) { + self._disconnect(o); + if (!self._lock) { + observers.splice(i, 1); + } else { + self._dirty = true; + } + return true; + } + } + } else { + var idx = m.findIdentical(observers, ident); + if (idx >= 0) { + self._disconnect(ident); + if (!self._lock) { + observers.splice(idx, 1); + } else { + self._dirty = true; + } + return true; + } + } + return false; + }, + + /** @id MochiKit.Signal.disconnectAllTo */ + disconnectAllTo: function (objOrFunc, /* optional */funcOrStr) { + var self = MochiKit.Signal; + var observers = self._observers; + var disconnect = self._disconnect; + var locked = self._lock; + var dirty = self._dirty; + if (typeof(funcOrStr) === 'undefined') { + funcOrStr = null; + } + for (var i = observers.length - 1; i >= 0; i--) { + var ident = observers[i]; + if (ident.objOrFunc === objOrFunc && + (funcOrStr === null || ident.funcOrStr === funcOrStr)) { + disconnect(ident); + if (locked) { + dirty = true; + } else { + observers.splice(i, 1); + } + } + } + self._dirty = dirty; + }, + + /** @id MochiKit.Signal.disconnectAll */ + disconnectAll: function (src/* optional */, sig) { + src = MochiKit.DOM.getElement(src); + var m = MochiKit.Base; + var signals = m.flattenArguments(m.extend(null, arguments, 1)); + var self = MochiKit.Signal; + var disconnect = self._disconnect; + var observers = self._observers; + var i, ident; + var locked = self._lock; + var dirty = self._dirty; + if (signals.length === 0) { + // disconnect all + for (i = observers.length - 1; i >= 0; i--) { + ident = observers[i]; + if (ident.source === src) { + disconnect(ident); + if (!locked) { + observers.splice(i, 1); + } else { + dirty = true; + } + } + } + } else { + var sigs = {}; + for (i = 0; i < signals.length; i++) { + sigs[signals[i]] = true; + } + for (i = observers.length - 1; i >= 0; i--) { + ident = observers[i]; + if (ident.source === src && ident.signal in sigs) { + disconnect(ident); + if (!locked) { + observers.splice(i, 1); + } else { + dirty = true; + } + } + } + } + self._dirty = dirty; + }, + + /** @id MochiKit.Signal.signal */ + signal: function (src, sig) { + var self = MochiKit.Signal; + var observers = self._observers; + src = MochiKit.DOM.getElement(src); + var args = MochiKit.Base.extend(null, arguments, 2); + var errors = []; + self._lock = true; + for (var i = 0; i < observers.length; i++) { + var ident = observers[i]; + if (ident.source === src && ident.signal === sig && + ident.connected) { + try { + ident.listener.apply(src, args); + } catch (e) { + errors.push(e); + } + } + } + self._lock = false; + if (self._dirty) { + self._dirty = false; + for (var i = observers.length - 1; i >= 0; i--) { + if (!observers[i].connected) { + observers.splice(i, 1); + } + } + } + if (errors.length == 1) { + throw errors[0]; + } else if (errors.length > 1) { + var e = new Error("Multiple errors thrown in handling 'sig', see errors property"); + e.errors = errors; + throw e; + } + } + +}); + +MochiKit.Signal.EXPORT_OK = []; + +MochiKit.Signal.EXPORT = [ + 'connect', + 'disconnect', + 'signal', + 'disconnectAll', + 'disconnectAllTo' +]; + +MochiKit.Signal.__new__ = function (win) { + var m = MochiKit.Base; + this._document = document; + this._window = win; + this._lock = false; + this._dirty = false; + + try { + this.connect(window, 'onunload', this._unloadCache); + } catch (e) { + // pass: might not be a browser + } + + this.EXPORT_TAGS = { + ':common': this.EXPORT, + ':all': m.concat(this.EXPORT, this.EXPORT_OK) + }; + + m.nameFunctions(this); +}; + +MochiKit.Signal.__new__(this); + +// +// XXX: Internet Explorer blows +// +if (MochiKit.__export__) { + connect = MochiKit.Signal.connect; + disconnect = MochiKit.Signal.disconnect; + disconnectAll = MochiKit.Signal.disconnectAll; + signal = MochiKit.Signal.signal; +} + +MochiKit.Base._exportSymbols(this, MochiKit.Signal); diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Sortable.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Sortable.js new file mode 100644 index 0000000000..463cce2fd3 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Sortable.js @@ -0,0 +1,589 @@ +/*** +Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) + Mochi-ized By Thomas Herve (_firstname_@nimail.org) + +See scriptaculous.js for full license. + +***/ + +MochiKit.Base._deps('Sortable', ['Base', 'Iter', 'DOM', 'Position', 'DragAndDrop']); + +MochiKit.Sortable.NAME = 'MochiKit.Sortable'; +MochiKit.Sortable.VERSION = '1.4.2'; + +MochiKit.Sortable.__repr__ = function () { + return '[' + this.NAME + ' ' + this.VERSION + ']'; +}; + +MochiKit.Sortable.toString = function () { + return this.__repr__(); +}; + +MochiKit.Sortable.EXPORT = [ +]; + +MochiKit.Sortable.EXPORT_OK = [ +]; + +MochiKit.Base.update(MochiKit.Sortable, { + /*** + + Manage sortables. Mainly use the create function to add a sortable. + + ***/ + sortables: {}, + + _findRootElement: function (element) { + while (element.tagName.toUpperCase() != "BODY") { + if (element.id && MochiKit.Sortable.sortables[element.id]) { + return element; + } + element = element.parentNode; + } + }, + + _createElementId: function(element) { + if (element.id == null || element.id == "") { + var d = MochiKit.DOM; + var id; + var count = 1; + while (d.getElement(id = "sortable" + count) != null) { + count += 1; + } + d.setNodeAttribute(element, "id", id); + } + }, + + /** @id MochiKit.Sortable.options */ + options: function (element) { + element = MochiKit.Sortable._findRootElement(MochiKit.DOM.getElement(element)); + if (!element) { + return; + } + return MochiKit.Sortable.sortables[element.id]; + }, + + /** @id MochiKit.Sortable.destroy */ + destroy: function (element){ + var s = MochiKit.Sortable.options(element); + var b = MochiKit.Base; + var d = MochiKit.DragAndDrop; + + if (s) { + MochiKit.Signal.disconnect(s.startHandle); + MochiKit.Signal.disconnect(s.endHandle); + b.map(function (dr) { + d.Droppables.remove(dr); + }, s.droppables); + b.map(function (dr) { + dr.destroy(); + }, s.draggables); + + delete MochiKit.Sortable.sortables[s.element.id]; + } + }, + + /** @id MochiKit.Sortable.create */ + create: function (element, options) { + element = MochiKit.DOM.getElement(element); + var self = MochiKit.Sortable; + self._createElementId(element); + + /** @id MochiKit.Sortable.options */ + options = MochiKit.Base.update({ + + /** @id MochiKit.Sortable.element */ + element: element, + + /** @id MochiKit.Sortable.tag */ + tag: 'li', // assumes li children, override with tag: 'tagname' + + /** @id MochiKit.Sortable.dropOnEmpty */ + dropOnEmpty: false, + + /** @id MochiKit.Sortable.tree */ + tree: false, + + /** @id MochiKit.Sortable.treeTag */ + treeTag: 'ul', + + /** @id MochiKit.Sortable.overlap */ + overlap: 'vertical', // one of 'vertical', 'horizontal' + + /** @id MochiKit.Sortable.constraint */ + constraint: 'vertical', // one of 'vertical', 'horizontal', false + // also takes array of elements (or ids); or false + + /** @id MochiKit.Sortable.containment */ + containment: [element], + + /** @id MochiKit.Sortable.handle */ + handle: false, // or a CSS class + + /** @id MochiKit.Sortable.only */ + only: false, + + /** @id MochiKit.Sortable.hoverclass */ + hoverclass: null, + + /** @id MochiKit.Sortable.ghosting */ + ghosting: false, + + /** @id MochiKit.Sortable.scroll */ + scroll: false, + + /** @id MochiKit.Sortable.scrollSensitivity */ + scrollSensitivity: 20, + + /** @id MochiKit.Sortable.scrollSpeed */ + scrollSpeed: 15, + + /** @id MochiKit.Sortable.format */ + format: /^[^_]*_(.*)$/, + + /** @id MochiKit.Sortable.onChange */ + onChange: MochiKit.Base.noop, + + /** @id MochiKit.Sortable.onUpdate */ + onUpdate: MochiKit.Base.noop, + + /** @id MochiKit.Sortable.accept */ + accept: null + }, options); + + // clear any old sortable with same element + self.destroy(element); + + // build options for the draggables + var options_for_draggable = { + revert: true, + ghosting: options.ghosting, + scroll: options.scroll, + scrollSensitivity: options.scrollSensitivity, + scrollSpeed: options.scrollSpeed, + constraint: options.constraint, + handle: options.handle + }; + + if (options.starteffect) { + options_for_draggable.starteffect = options.starteffect; + } + + if (options.reverteffect) { + options_for_draggable.reverteffect = options.reverteffect; + } else if (options.ghosting) { + options_for_draggable.reverteffect = function (innerelement) { + innerelement.style.top = 0; + innerelement.style.left = 0; + }; + } + + if (options.endeffect) { + options_for_draggable.endeffect = options.endeffect; + } + + if (options.zindex) { + options_for_draggable.zindex = options.zindex; + } + + // build options for the droppables + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass, + onhover: self.onHover, + tree: options.tree, + accept: options.accept + } + + var options_for_tree = { + onhover: self.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass, + accept: options.accept + } + + // fix for gecko engine + MochiKit.DOM.removeEmptyTextNodes(element); + + options.draggables = []; + options.droppables = []; + + // drop on empty handling + if (options.dropOnEmpty || options.tree) { + new MochiKit.DragAndDrop.Droppable(element, options_for_tree); + options.droppables.push(element); + } + MochiKit.Base.map(function (e) { + // handles are per-draggable + var handle = options.handle ? + MochiKit.DOM.getFirstElementByTagAndClassName(null, + options.handle, e) : e; + options.draggables.push( + new MochiKit.DragAndDrop.Draggable(e, + MochiKit.Base.update(options_for_draggable, + {handle: handle}))); + new MochiKit.DragAndDrop.Droppable(e, options_for_droppable); + if (options.tree) { + e.treeNode = element; + } + options.droppables.push(e); + }, (self.findElements(element, options) || [])); + + if (options.tree) { + MochiKit.Base.map(function (e) { + new MochiKit.DragAndDrop.Droppable(e, options_for_tree); + e.treeNode = element; + options.droppables.push(e); + }, (self.findTreeElements(element, options) || [])); + } + + // keep reference + self.sortables[element.id] = options; + + options.lastValue = self.serialize(element); + options.startHandle = MochiKit.Signal.connect(MochiKit.DragAndDrop.Draggables, 'start', + MochiKit.Base.partial(self.onStart, element)); + options.endHandle = MochiKit.Signal.connect(MochiKit.DragAndDrop.Draggables, 'end', + MochiKit.Base.partial(self.onEnd, element)); + }, + + /** @id MochiKit.Sortable.onStart */ + onStart: function (element, draggable) { + var self = MochiKit.Sortable; + var options = self.options(element); + options.lastValue = self.serialize(options.element); + }, + + /** @id MochiKit.Sortable.onEnd */ + onEnd: function (element, draggable) { + var self = MochiKit.Sortable; + self.unmark(); + var options = self.options(element); + if (options.lastValue != self.serialize(options.element)) { + options.onUpdate(options.element); + } + }, + + // return all suitable-for-sortable elements in a guaranteed order + + /** @id MochiKit.Sortable.findElements */ + findElements: function (element, options) { + return MochiKit.Sortable.findChildren(element, options.only, options.tree, options.tag); + }, + + /** @id MochiKit.Sortable.findTreeElements */ + findTreeElements: function (element, options) { + return MochiKit.Sortable.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); + }, + + /** @id MochiKit.Sortable.findChildren */ + findChildren: function (element, only, recursive, tagName) { + if (!element.hasChildNodes()) { + return null; + } + tagName = tagName.toUpperCase(); + if (only) { + only = MochiKit.Base.flattenArray([only]); + } + var elements = []; + MochiKit.Base.map(function (e) { + if (e.tagName && + e.tagName.toUpperCase() == tagName && + (!only || + MochiKit.Iter.some(only, function (c) { + return MochiKit.DOM.hasElementClass(e, c); + }))) { + elements.push(e); + } + if (recursive) { + var grandchildren = MochiKit.Sortable.findChildren(e, only, recursive, tagName); + if (grandchildren && grandchildren.length > 0) { + elements = elements.concat(grandchildren); + } + } + }, element.childNodes); + return elements; + }, + + /** @id MochiKit.Sortable.onHover */ + onHover: function (element, dropon, overlap) { + if (MochiKit.DOM.isChildNode(dropon, element)) { + return; + } + var self = MochiKit.Sortable; + + if (overlap > .33 && overlap < .66 && self.options(dropon).tree) { + return; + } else if (overlap > 0.5) { + self.mark(dropon, 'before'); + if (dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = 'hidden'; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if (dropon.parentNode != oldParentNode) { + self.options(oldParentNode).onChange(element); + } + self.options(dropon.parentNode).onChange(element); + } + } else { + self.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if (nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = 'hidden'; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if (dropon.parentNode != oldParentNode) { + self.options(oldParentNode).onChange(element); + } + self.options(dropon.parentNode).onChange(element); + } + } + }, + + _offsetSize: function (element, type) { + if (type == 'vertical' || type == 'height') { + return element.offsetHeight; + } else { + return element.offsetWidth; + } + }, + + /** @id MochiKit.Sortable.onEmptyHover */ + onEmptyHover: function (element, dropon, overlap) { + var oldParentNode = element.parentNode; + var self = MochiKit.Sortable; + var droponOptions = self.options(dropon); + + if (!MochiKit.DOM.isChildNode(dropon, element)) { + var index; + + var children = self.findElements(dropon, {tag: droponOptions.tag, + only: droponOptions.only}); + var child = null; + + if (children) { + var offset = self._offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); + + for (index = 0; index < children.length; index += 1) { + if (offset - self._offsetSize(children[index], droponOptions.overlap) >= 0) { + offset -= self._offsetSize(children[index], droponOptions.overlap); + } else if (offset - (self._offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { + child = index + 1 < children.length ? children[index + 1] : null; + break; + } else { + child = children[index]; + break; + } + } + } + + dropon.insertBefore(element, child); + + self.options(oldParentNode).onChange(element); + droponOptions.onChange(element); + } + }, + + /** @id MochiKit.Sortable.unmark */ + unmark: function () { + var m = MochiKit.Sortable._marker; + if (m) { + MochiKit.Style.hideElement(m); + } + }, + + /** @id MochiKit.Sortable.mark */ + mark: function (dropon, position) { + // mark on ghosting only + var d = MochiKit.DOM; + var self = MochiKit.Sortable; + var sortable = self.options(dropon.parentNode); + if (sortable && !sortable.ghosting) { + return; + } + + if (!self._marker) { + self._marker = d.getElement('dropmarker') || + document.createElement('DIV'); + MochiKit.Style.hideElement(self._marker); + d.addElementClass(self._marker, 'dropmarker'); + self._marker.style.position = 'absolute'; + document.getElementsByTagName('body').item(0).appendChild(self._marker); + } + var offsets = MochiKit.Position.cumulativeOffset(dropon); + self._marker.style.left = offsets.x + 'px'; + self._marker.style.top = offsets.y + 'px'; + + if (position == 'after') { + if (sortable.overlap == 'horizontal') { + self._marker.style.left = (offsets.x + dropon.clientWidth) + 'px'; + } else { + self._marker.style.top = (offsets.y + dropon.clientHeight) + 'px'; + } + } + MochiKit.Style.showElement(self._marker); + }, + + _tree: function (element, options, parent) { + var self = MochiKit.Sortable; + var children = self.findElements(element, options) || []; + + for (var i = 0; i < children.length; ++i) { + var match = children[i].id.match(options.format); + + if (!match) { + continue; + } + + var child = { + id: encodeURIComponent(match ? match[1] : null), + element: element, + parent: parent, + children: [], + position: parent.children.length, + container: self._findChildrenElement(children[i], options.treeTag.toUpperCase()) + } + + /* Get the element containing the children and recurse over it */ + if (child.container) { + self._tree(child.container, options, child) + } + + parent.children.push (child); + } + + return parent; + }, + + /* Finds the first element of the given tag type within a parent element. + Used for finding the first LI[ST] within a L[IST]I[TEM].*/ + _findChildrenElement: function (element, containerTag) { + if (element && element.hasChildNodes) { + containerTag = containerTag.toUpperCase(); + for (var i = 0; i < element.childNodes.length; ++i) { + if (element.childNodes[i].tagName.toUpperCase() == containerTag) { + return element.childNodes[i]; + } + } + } + return null; + }, + + /** @id MochiKit.Sortable.tree */ + tree: function (element, options) { + element = MochiKit.DOM.getElement(element); + var sortableOptions = MochiKit.Sortable.options(element); + options = MochiKit.Base.update({ + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format + }, options || {}); + + var root = { + id: null, + parent: null, + children: new Array, + container: element, + position: 0 + } + + return MochiKit.Sortable._tree(element, options, root); + }, + + /** + * Specifies the sequence for the Sortable. + * @param {Node} element Element to use as the Sortable. + * @param {Object} newSequence New sequence to use. + * @param {Object} options Options to use fro the Sortable. + */ + setSequence: function (element, newSequence, options) { + var self = MochiKit.Sortable; + var b = MochiKit.Base; + element = MochiKit.DOM.getElement(element); + options = b.update(self.options(element), options || {}); + + var nodeMap = {}; + b.map(function (n) { + var m = n.id.match(options.format); + if (m) { + nodeMap[m[1]] = [n, n.parentNode]; + } + n.parentNode.removeChild(n); + }, self.findElements(element, options)); + + b.map(function (ident) { + var n = nodeMap[ident]; + if (n) { + n[1].appendChild(n[0]); + delete nodeMap[ident]; + } + }, newSequence); + }, + + /* Construct a [i] index for a particular node */ + _constructIndex: function (node) { + var index = ''; + do { + if (node.id) { + index = '[' + node.position + ']' + index; + } + } while ((node = node.parent) != null); + return index; + }, + + /** @id MochiKit.Sortable.sequence */ + sequence: function (element, options) { + element = MochiKit.DOM.getElement(element); + var self = MochiKit.Sortable; + var options = MochiKit.Base.update(self.options(element), options || {}); + + return MochiKit.Base.map(function (item) { + return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; + }, MochiKit.DOM.getElement(self.findElements(element, options) || [])); + }, + + /** + * Serializes the content of a Sortable. Useful to send this content through a XMLHTTPRequest. + * These options override the Sortable options for the serialization only. + * @param {Node} element Element to serialize. + * @param {Object} options Serialization options. + */ + serialize: function (element, options) { + element = MochiKit.DOM.getElement(element); + var self = MochiKit.Sortable; + options = MochiKit.Base.update(self.options(element), options || {}); + var name = encodeURIComponent(options.name || element.id); + + if (options.tree) { + return MochiKit.Base.flattenArray(MochiKit.Base.map(function (item) { + return [name + self._constructIndex(item) + "[id]=" + + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); + }, self.tree(element, options).children)).join('&'); + } else { + return MochiKit.Base.map(function (item) { + return name + "[]=" + encodeURIComponent(item); + }, self.sequence(element, options)).join('&'); + } + } +}); + +// trunk compatibility +MochiKit.Sortable.Sortable = MochiKit.Sortable; + +MochiKit.Sortable.__new__ = function () { + MochiKit.Base.nameFunctions(this); + + this.EXPORT_TAGS = { + ":common": this.EXPORT, + ":all": MochiKit.Base.concat(this.EXPORT, this.EXPORT_OK) + }; +}; + +MochiKit.Sortable.__new__(); + +MochiKit.Base._exportSymbols(this, MochiKit.Sortable); diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Style.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Style.js new file mode 100644 index 0000000000..1488110c40 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Style.js @@ -0,0 +1,594 @@ +/*** + +MochiKit.Style 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005-2006 Bob Ippolito, Beau Hartshorne. All rights Reserved. + +***/ + +MochiKit.Base._deps('Style', ['Base', 'DOM']); + +MochiKit.Style.NAME = 'MochiKit.Style'; +MochiKit.Style.VERSION = '1.4.2'; +MochiKit.Style.__repr__ = function () { + return '[' + this.NAME + ' ' + this.VERSION + ']'; +}; +MochiKit.Style.toString = function () { + return this.__repr__(); +}; + +MochiKit.Style.EXPORT_OK = []; + +MochiKit.Style.EXPORT = [ + 'setStyle', + 'setOpacity', + 'getStyle', + 'getElementDimensions', + 'elementDimensions', // deprecated + 'setElementDimensions', + 'getElementPosition', + 'elementPosition', // deprecated + 'setElementPosition', + "makePositioned", + "undoPositioned", + "makeClipping", + "undoClipping", + 'setDisplayForElement', + 'hideElement', + 'showElement', + 'getViewportDimensions', + 'getViewportPosition', + 'Dimensions', + 'Coordinates' +]; + + +/* + + Dimensions + +*/ +/** @id MochiKit.Style.Dimensions */ +MochiKit.Style.Dimensions = function (w, h) { + this.w = w; + this.h = h; +}; + +MochiKit.Style.Dimensions.prototype.__repr__ = function () { + var repr = MochiKit.Base.repr; + return '{w: ' + repr(this.w) + ', h: ' + repr(this.h) + '}'; +}; + +MochiKit.Style.Dimensions.prototype.toString = function () { + return this.__repr__(); +}; + + +/* + + Coordinates + +*/ +/** @id MochiKit.Style.Coordinates */ +MochiKit.Style.Coordinates = function (x, y) { + this.x = x; + this.y = y; +}; + +MochiKit.Style.Coordinates.prototype.__repr__ = function () { + var repr = MochiKit.Base.repr; + return '{x: ' + repr(this.x) + ', y: ' + repr(this.y) + '}'; +}; + +MochiKit.Style.Coordinates.prototype.toString = function () { + return this.__repr__(); +}; + + +MochiKit.Base.update(MochiKit.Style, { + + /** @id MochiKit.Style.getStyle */ + getStyle: function (elem, cssProperty) { + var dom = MochiKit.DOM; + var d = dom._document; + + elem = dom.getElement(elem); + cssProperty = MochiKit.Base.camelize(cssProperty); + + if (!elem || elem == d) { + return undefined; + } + if (cssProperty == 'opacity' && typeof(elem.filters) != 'undefined') { + var opacity = (MochiKit.Style.getStyle(elem, 'filter') || '').match(/alpha\(opacity=(.*)\)/); + if (opacity && opacity[1]) { + return parseFloat(opacity[1]) / 100; + } + return 1.0; + } + if (cssProperty == 'float' || cssProperty == 'cssFloat' || cssProperty == 'styleFloat') { + if (elem.style["float"]) { + return elem.style["float"]; + } else if (elem.style.cssFloat) { + return elem.style.cssFloat; + } else if (elem.style.styleFloat) { + return elem.style.styleFloat; + } else { + return "none"; + } + } + var value = elem.style ? elem.style[cssProperty] : null; + if (!value) { + if (d.defaultView && d.defaultView.getComputedStyle) { + var css = d.defaultView.getComputedStyle(elem, null); + cssProperty = cssProperty.replace(/([A-Z])/g, '-$1' + ).toLowerCase(); // from dojo.style.toSelectorCase + value = css ? css.getPropertyValue(cssProperty) : null; + } else if (elem.currentStyle) { + value = elem.currentStyle[cssProperty]; + if (/^\d/.test(value) && !/px$/.test(value) && cssProperty != 'fontWeight') { + /* Convert to px using an hack from Dean Edwards */ + var left = elem.style.left; + var rsLeft = elem.runtimeStyle.left; + elem.runtimeStyle.left = elem.currentStyle.left; + elem.style.left = value || 0; + value = elem.style.pixelLeft + "px"; + elem.style.left = left; + elem.runtimeStyle.left = rsLeft; + } + } + } + if (cssProperty == 'opacity') { + value = parseFloat(value); + } + + if (/Opera/.test(navigator.userAgent) && (MochiKit.Base.findValue(['left', 'top', 'right', 'bottom'], cssProperty) != -1)) { + if (MochiKit.Style.getStyle(elem, 'position') == 'static') { + value = 'auto'; + } + } + + return value == 'auto' ? null : value; + }, + + /** @id MochiKit.Style.setStyle */ + setStyle: function (elem, style) { + elem = MochiKit.DOM.getElement(elem); + for (var name in style) { + switch (name) { + case 'opacity': + MochiKit.Style.setOpacity(elem, style[name]); + break; + case 'float': + case 'cssFloat': + case 'styleFloat': + if (typeof(elem.style["float"]) != "undefined") { + elem.style["float"] = style[name]; + } else if (typeof(elem.style.cssFloat) != "undefined") { + elem.style.cssFloat = style[name]; + } else { + elem.style.styleFloat = style[name]; + } + break; + default: + elem.style[MochiKit.Base.camelize(name)] = style[name]; + } + } + }, + + /** @id MochiKit.Style.setOpacity */ + setOpacity: function (elem, o) { + elem = MochiKit.DOM.getElement(elem); + var self = MochiKit.Style; + if (o == 1) { + var toSet = /Gecko/.test(navigator.userAgent) && !(/Konqueror|AppleWebKit|KHTML/.test(navigator.userAgent)); + elem.style["opacity"] = toSet ? 0.999999 : 1.0; + if (/MSIE/.test(navigator.userAgent)) { + elem.style['filter'] = + self.getStyle(elem, 'filter').replace(/alpha\([^\)]*\)/gi, ''); + } + } else { + if (o < 0.00001) { + o = 0; + } + elem.style["opacity"] = o; + if (/MSIE/.test(navigator.userAgent)) { + elem.style['filter'] = + self.getStyle(elem, 'filter').replace(/alpha\([^\)]*\)/gi, '') + 'alpha(opacity=' + o * 100 + ')'; + } + } + }, + + /* + + getElementPosition is adapted from YAHOO.util.Dom.getXY v0.9.0. + Copyright: Copyright (c) 2006, Yahoo! Inc. All rights reserved. + License: BSD, http://developer.yahoo.net/yui/license.txt + + */ + + /** @id MochiKit.Style.getElementPosition */ + getElementPosition: function (elem, /* optional */relativeTo) { + var self = MochiKit.Style; + var dom = MochiKit.DOM; + elem = dom.getElement(elem); + + if (!elem || + (!(elem.x && elem.y) && + (!elem.parentNode === null || + self.getStyle(elem, 'display') == 'none'))) { + return undefined; + } + + var c = new self.Coordinates(0, 0); + var box = null; + var parent = null; + + var d = MochiKit.DOM._document; + var de = d.documentElement; + var b = d.body; + + if (!elem.parentNode && elem.x && elem.y) { + /* it's just a MochiKit.Style.Coordinates object */ + c.x += elem.x || 0; + c.y += elem.y || 0; + } else if (elem.getBoundingClientRect) { // IE shortcut + /* + + The IE shortcut can be off by two. We fix it. See: + http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/getboundingclientrect.asp + + This is similar to the method used in + MochiKit.Signal.Event.mouse(). + + */ + box = elem.getBoundingClientRect(); + + c.x += box.left + + (de.scrollLeft || b.scrollLeft) - + (de.clientLeft || 0); + + c.y += box.top + + (de.scrollTop || b.scrollTop) - + (de.clientTop || 0); + + } else if (elem.offsetParent) { + c.x += elem.offsetLeft; + c.y += elem.offsetTop; + parent = elem.offsetParent; + + if (parent != elem) { + while (parent) { + c.x += parseInt(parent.style.borderLeftWidth) || 0; + c.y += parseInt(parent.style.borderTopWidth) || 0; + c.x += parent.offsetLeft; + c.y += parent.offsetTop; + parent = parent.offsetParent; + } + } + + /* + + Opera < 9 and old Safari (absolute) incorrectly account for + body offsetTop and offsetLeft. + + */ + var ua = navigator.userAgent.toLowerCase(); + if ((typeof(opera) != 'undefined' && + parseFloat(opera.version()) < 9) || + (ua.indexOf('AppleWebKit') != -1 && + self.getStyle(elem, 'position') == 'absolute')) { + + c.x -= b.offsetLeft; + c.y -= b.offsetTop; + + } + + // Adjust position for strange Opera scroll bug + if (elem.parentNode) { + parent = elem.parentNode; + } else { + parent = null; + } + while (parent) { + var tagName = parent.tagName.toUpperCase(); + if (tagName === 'BODY' || tagName === 'HTML') { + break; + } + var disp = self.getStyle(parent, 'display'); + // Handle strange Opera bug for some display + if (disp.search(/^inline|table-row.*$/i)) { + c.x -= parent.scrollLeft; + c.y -= parent.scrollTop; + } + if (parent.parentNode) { + parent = parent.parentNode; + } else { + parent = null; + } + } + } + + if (typeof(relativeTo) != 'undefined') { + relativeTo = arguments.callee(relativeTo); + if (relativeTo) { + c.x -= (relativeTo.x || 0); + c.y -= (relativeTo.y || 0); + } + } + + return c; + }, + + /** @id MochiKit.Style.setElementPosition */ + setElementPosition: function (elem, newPos/* optional */, units) { + elem = MochiKit.DOM.getElement(elem); + if (typeof(units) == 'undefined') { + units = 'px'; + } + var newStyle = {}; + var isUndefNull = MochiKit.Base.isUndefinedOrNull; + if (!isUndefNull(newPos.x)) { + newStyle['left'] = newPos.x + units; + } + if (!isUndefNull(newPos.y)) { + newStyle['top'] = newPos.y + units; + } + MochiKit.DOM.updateNodeAttributes(elem, {'style': newStyle}); + }, + + /** @id MochiKit.Style.makePositioned */ + makePositioned: function (element) { + element = MochiKit.DOM.getElement(element); + var pos = MochiKit.Style.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, + // when an element is position relative but top and left have + // not been defined + if (/Opera/.test(navigator.userAgent)) { + element.style.top = 0; + element.style.left = 0; + } + } + }, + + /** @id MochiKit.Style.undoPositioned */ + undoPositioned: function (element) { + element = MochiKit.DOM.getElement(element); + if (element.style.position == 'relative') { + element.style.position = element.style.top = element.style.left = element.style.bottom = element.style.right = ''; + } + }, + + /** @id MochiKit.Style.makeClipping */ + makeClipping: function (element) { + element = MochiKit.DOM.getElement(element); + var s = element.style; + var oldOverflow = { 'overflow': s.overflow, + 'overflow-x': s.overflowX, + 'overflow-y': s.overflowY }; + if ((MochiKit.Style.getStyle(element, 'overflow') || 'visible') != 'hidden') { + element.style.overflow = 'hidden'; + element.style.overflowX = 'hidden'; + element.style.overflowY = 'hidden'; + } + return oldOverflow; + }, + + /** @id MochiKit.Style.undoClipping */ + undoClipping: function (element, overflow) { + element = MochiKit.DOM.getElement(element); + if (typeof(overflow) == 'string') { + element.style.overflow = overflow; + } else if (overflow != null) { + element.style.overflow = overflow['overflow']; + element.style.overflowX = overflow['overflow-x']; + element.style.overflowY = overflow['overflow-y']; + } + }, + + /** @id MochiKit.Style.getElementDimensions */ + getElementDimensions: function (elem, contentSize/*optional*/) { + var self = MochiKit.Style; + var dom = MochiKit.DOM; + if (typeof(elem.w) == 'number' || typeof(elem.h) == 'number') { + return new self.Dimensions(elem.w || 0, elem.h || 0); + } + elem = dom.getElement(elem); + if (!elem) { + return undefined; + } + var disp = self.getStyle(elem, 'display'); + // display can be empty/undefined on WebKit/KHTML + if (disp == 'none' || disp == '' || typeof(disp) == 'undefined') { + var s = elem.style; + var originalVisibility = s.visibility; + var originalPosition = s.position; + var originalDisplay = s.display; + s.visibility = 'hidden'; + s.position = 'absolute'; + s.display = self._getDefaultDisplay(elem); + var originalWidth = elem.offsetWidth; + var originalHeight = elem.offsetHeight; + s.display = originalDisplay; + s.position = originalPosition; + s.visibility = originalVisibility; + } else { + originalWidth = elem.offsetWidth || 0; + originalHeight = elem.offsetHeight || 0; + } + if (contentSize) { + var tableCell = 'colSpan' in elem && 'rowSpan' in elem; + var collapse = (tableCell && elem.parentNode && self.getStyle( + elem.parentNode, 'borderCollapse') == 'collapse') + if (collapse) { + if (/MSIE/.test(navigator.userAgent)) { + var borderLeftQuota = elem.previousSibling? 0.5 : 1; + var borderRightQuota = elem.nextSibling? 0.5 : 1; + } + else { + var borderLeftQuota = 0.5; + var borderRightQuota = 0.5; + } + } else { + var borderLeftQuota = 1; + var borderRightQuota = 1; + } + originalWidth -= Math.round( + (parseFloat(self.getStyle(elem, 'paddingLeft')) || 0) + + (parseFloat(self.getStyle(elem, 'paddingRight')) || 0) + + borderLeftQuota * + (parseFloat(self.getStyle(elem, 'borderLeftWidth')) || 0) + + borderRightQuota * + (parseFloat(self.getStyle(elem, 'borderRightWidth')) || 0) + ); + if (tableCell) { + if (/Opera/.test(navigator.userAgent) + && !/Konqueror|AppleWebKit|KHTML/.test(navigator.userAgent)) { + var borderHeightQuota = 0; + } else if (/MSIE/.test(navigator.userAgent)) { + var borderHeightQuota = 1; + } else { + var borderHeightQuota = collapse? 0.5 : 1; + } + } else { + var borderHeightQuota = 1; + } + originalHeight -= Math.round( + (parseFloat(self.getStyle(elem, 'paddingTop')) || 0) + + (parseFloat(self.getStyle(elem, 'paddingBottom')) || 0) + + borderHeightQuota * ( + (parseFloat(self.getStyle(elem, 'borderTopWidth')) || 0) + + (parseFloat(self.getStyle(elem, 'borderBottomWidth')) || 0)) + ); + } + return new self.Dimensions(originalWidth, originalHeight); + }, + + /** @id MochiKit.Style.setElementDimensions */ + setElementDimensions: function (elem, newSize/* optional */, units) { + elem = MochiKit.DOM.getElement(elem); + if (typeof(units) == 'undefined') { + units = 'px'; + } + var newStyle = {}; + var isUndefNull = MochiKit.Base.isUndefinedOrNull; + if (!isUndefNull(newSize.w)) { + newStyle['width'] = newSize.w + units; + } + if (!isUndefNull(newSize.h)) { + newStyle['height'] = newSize.h + units; + } + MochiKit.DOM.updateNodeAttributes(elem, {'style': newStyle}); + }, + + _getDefaultDisplay: function (elem) { + var self = MochiKit.Style; + var dom = MochiKit.DOM; + elem = dom.getElement(elem); + if (!elem) { + return undefined; + } + var tagName = elem.tagName.toUpperCase(); + return self._defaultDisplay[tagName] || 'block'; + }, + + /** @id MochiKit.Style.setDisplayForElement */ + setDisplayForElement: function (display, element/*, ...*/) { + var elements = MochiKit.Base.extend(null, arguments, 1); + var getElement = MochiKit.DOM.getElement; + for (var i = 0; i < elements.length; i++) { + element = getElement(elements[i]); + if (element) { + element.style.display = display; + } + } + }, + + /** @id MochiKit.Style.getViewportDimensions */ + getViewportDimensions: function () { + var d = new MochiKit.Style.Dimensions(); + var w = MochiKit.DOM._window; + var b = MochiKit.DOM._document.body; + if (w.innerWidth) { + d.w = w.innerWidth; + d.h = w.innerHeight; + } else if (b && b.parentElement && b.parentElement.clientWidth) { + d.w = b.parentElement.clientWidth; + d.h = b.parentElement.clientHeight; + } else if (b && b.clientWidth) { + d.w = b.clientWidth; + d.h = b.clientHeight; + } + return d; + }, + + /** @id MochiKit.Style.getViewportPosition */ + getViewportPosition: function () { + var c = new MochiKit.Style.Coordinates(0, 0); + var d = MochiKit.DOM._document; + var de = d.documentElement; + var db = d.body; + if (de && (de.scrollTop || de.scrollLeft)) { + c.x = de.scrollLeft; + c.y = de.scrollTop; + } else if (db) { + c.x = db.scrollLeft; + c.y = db.scrollTop; + } + return c; + }, + + __new__: function () { + var m = MochiKit.Base; + + var inlines = ['A','ABBR','ACRONYM','B','BASEFONT','BDO','BIG','BR', + 'CITE','CODE','DFN','EM','FONT','I','IMG','KBD','LABEL', + 'Q','S','SAMP','SMALL','SPAN','STRIKE','STRONG','SUB', + 'SUP','TEXTAREA','TT','U','VAR']; + this._defaultDisplay = { 'TABLE': 'table', + 'THEAD': 'table-header-group', + 'TBODY': 'table-row-group', + 'TFOOT': 'table-footer-group', + 'COLGROUP': 'table-column-group', + 'COL': 'table-column', + 'TR': 'table-row', + 'TD': 'table-cell', + 'TH': 'table-cell', + 'CAPTION': 'table-caption', + 'LI': 'list-item', + 'INPUT': 'inline-block', + 'SELECT': 'inline-block' }; + // CSS 'display' support in IE6/7 is just broken... + if (/MSIE/.test(navigator.userAgent)) { + for (var k in this._defaultDisplay) { + var v = this._defaultDisplay[k]; + if (v.indexOf('table') == 0) { + this._defaultDisplay[k] = 'block'; + } + } + } + for (var i = 0; i < inlines.length; i++) { + this._defaultDisplay[inlines[i]] = 'inline'; + } + + this.elementPosition = this.getElementPosition; + this.elementDimensions = this.getElementDimensions; + + this.hideElement = m.partial(this.setDisplayForElement, 'none'); + // TODO: showElement could be improved by using getDefaultDisplay. + this.showElement = m.partial(this.setDisplayForElement, 'block'); + + this.EXPORT_TAGS = { + ':common': this.EXPORT, + ':all': m.concat(this.EXPORT, this.EXPORT_OK) + }; + + m.nameFunctions(this); + } +}); + +MochiKit.Style.__new__(); +MochiKit.Base._exportSymbols(this, MochiKit.Style); diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Test.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Test.js new file mode 100644 index 0000000000..30bfb4c88f --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Test.js @@ -0,0 +1,162 @@ +/*** + +MochiKit.Test 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +MochiKit.Base._deps('Test', ['Base']); + +MochiKit.Test.NAME = "MochiKit.Test"; +MochiKit.Test.VERSION = "1.4.2"; +MochiKit.Test.__repr__ = function () { + return "[" + this.NAME + " " + this.VERSION + "]"; +}; + +MochiKit.Test.toString = function () { + return this.__repr__(); +}; + + +MochiKit.Test.EXPORT = ["runTests"]; +MochiKit.Test.EXPORT_OK = []; + +MochiKit.Test.runTests = function (obj) { + if (typeof(obj) == "string") { + obj = JSAN.use(obj); + } + var suite = new MochiKit.Test.Suite(); + suite.run(obj); +}; + +MochiKit.Test.Suite = function () { + this.testIndex = 0; + MochiKit.Base.bindMethods(this); +}; + +MochiKit.Test.Suite.prototype = { + run: function (obj) { + try { + obj(this); + } catch (e) { + this.traceback(e); + } + }, + traceback: function (e) { + var items = MochiKit.Iter.sorted(MochiKit.Base.items(e)); + print("not ok " + this.testIndex + " - Error thrown"); + for (var i = 0; i < items.length; i++) { + var kv = items[i]; + if (kv[0] == "stack") { + kv[1] = kv[1].split(/\n/)[0]; + } + this.print("# " + kv.join(": ")); + } + }, + print: function (s) { + print(s); + }, + is: function (got, expected, /* optional */message) { + var res = 1; + var msg = null; + try { + res = MochiKit.Base.compare(got, expected); + } catch (e) { + msg = "Can not compare " + typeof(got) + ":" + typeof(expected); + } + if (res) { + msg = "Expected value did not compare equal"; + } + if (!res) { + return this.testResult(true, message); + } + return this.testResult(false, message, + [[msg], ["got:", got], ["expected:", expected]]); + }, + + testResult: function (pass, msg, failures) { + this.testIndex += 1; + if (pass) { + this.print("ok " + this.testIndex + " - " + msg); + return; + } + this.print("not ok " + this.testIndex + " - " + msg); + if (failures) { + for (var i = 0; i < failures.length; i++) { + this.print("# " + failures[i].join(" ")); + } + } + }, + + isDeeply: function (got, expected, /* optional */message) { + var m = MochiKit.Base; + var res = 1; + try { + res = m.compare(got, expected); + } catch (e) { + // pass + } + if (res === 0) { + return this.ok(true, message); + } + var gk = m.keys(got); + var ek = m.keys(expected); + gk.sort(); + ek.sort(); + if (m.compare(gk, ek)) { + // differing keys + var cmp = {}; + var i; + for (i = 0; i < gk.length; i++) { + cmp[gk[i]] = "got"; + } + for (i = 0; i < ek.length; i++) { + if (ek[i] in cmp) { + delete cmp[ek[i]]; + } else { + cmp[ek[i]] = "expected"; + } + } + var diffkeys = m.keys(cmp); + diffkeys.sort(); + var gotkeys = []; + var expkeys = []; + while (diffkeys.length) { + var k = diffkeys.shift(); + if (k in Object.prototype) { + continue; + } + (cmp[k] == "got" ? gotkeys : expkeys).push(k); + } + + + } + + return this.testResult((!res), msg, + (msg ? [["got:", got], ["expected:", expected]] : undefined) + ); + }, + + ok: function (res, message) { + return this.testResult(res, message); + } +}; + +MochiKit.Test.__new__ = function () { + var m = MochiKit.Base; + + this.EXPORT_TAGS = { + ":common": this.EXPORT, + ":all": m.concat(this.EXPORT, this.EXPORT_OK) + }; + + m.nameFunctions(this); + +}; + +MochiKit.Test.__new__(); + +MochiKit.Base._exportSymbols(this, MochiKit.Test); diff --git a/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Visual.js b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Visual.js new file mode 100644 index 0000000000..df975544d2 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Visual.js @@ -0,0 +1,2026 @@ +/*** + +MochiKit.Visual 1.4.2 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito and others. All rights Reserved. + +***/ + +MochiKit.Base._deps('Visual', ['Base', 'DOM', 'Style', 'Color', 'Position']); + +MochiKit.Visual.NAME = "MochiKit.Visual"; +MochiKit.Visual.VERSION = "1.4.2"; + +MochiKit.Visual.__repr__ = function () { + return "[" + this.NAME + " " + this.VERSION + "]"; +}; + +MochiKit.Visual.toString = function () { + return this.__repr__(); +}; + +MochiKit.Visual._RoundCorners = function (e, options) { + e = MochiKit.DOM.getElement(e); + this._setOptions(options); + if (this.options.__unstable__wrapElement) { + e = this._doWrap(e); + } + + var color = this.options.color; + var C = MochiKit.Color.Color; + if (this.options.color === "fromElement") { + color = C.fromBackground(e); + } else if (!(color instanceof C)) { + color = C.fromString(color); + } + this.isTransparent = (color.asRGB().a <= 0); + + var bgColor = this.options.bgColor; + if (this.options.bgColor === "fromParent") { + bgColor = C.fromBackground(e.offsetParent); + } else if (!(bgColor instanceof C)) { + bgColor = C.fromString(bgColor); + } + + this._roundCornersImpl(e, color, bgColor); +}; + +MochiKit.Visual._RoundCorners.prototype = { + _doWrap: function (e) { + var parent = e.parentNode; + var doc = MochiKit.DOM.currentDocument(); + if (typeof(doc.defaultView) === "undefined" + || doc.defaultView === null) { + return e; + } + var style = doc.defaultView.getComputedStyle(e, null); + if (typeof(style) === "undefined" || style === null) { + return e; + } + var wrapper = MochiKit.DOM.DIV({"style": { + display: "block", + // convert padding to margin + marginTop: style.getPropertyValue("padding-top"), + marginRight: style.getPropertyValue("padding-right"), + marginBottom: style.getPropertyValue("padding-bottom"), + marginLeft: style.getPropertyValue("padding-left"), + // remove padding so the rounding looks right + padding: "0px" + /* + paddingRight: "0px", + paddingLeft: "0px" + */ + }}); + wrapper.innerHTML = e.innerHTML; + e.innerHTML = ""; + e.appendChild(wrapper); + return e; + }, + + _roundCornersImpl: function (e, color, bgColor) { + if (this.options.border) { + this._renderBorder(e, bgColor); + } + if (this._isTopRounded()) { + this._roundTopCorners(e, color, bgColor); + } + if (this._isBottomRounded()) { + this._roundBottomCorners(e, color, bgColor); + } + }, + + _renderBorder: function (el, bgColor) { + var borderValue = "1px solid " + this._borderColor(bgColor); + var borderL = "border-left: " + borderValue; + var borderR = "border-right: " + borderValue; + var style = "style='" + borderL + ";" + borderR + "'"; + el.innerHTML = "<div " + style + ">" + el.innerHTML + "</div>"; + }, + + _roundTopCorners: function (el, color, bgColor) { + var corner = this._createCorner(bgColor); + for (var i = 0; i < this.options.numSlices; i++) { + corner.appendChild( + this._createCornerSlice(color, bgColor, i, "top") + ); + } + el.style.paddingTop = 0; + el.insertBefore(corner, el.firstChild); + }, + + _roundBottomCorners: function (el, color, bgColor) { + var corner = this._createCorner(bgColor); + for (var i = (this.options.numSlices - 1); i >= 0; i--) { + corner.appendChild( + this._createCornerSlice(color, bgColor, i, "bottom") + ); + } + el.style.paddingBottom = 0; + el.appendChild(corner); + }, + + _createCorner: function (bgColor) { + var dom = MochiKit.DOM; + return dom.DIV({style: {backgroundColor: bgColor.toString()}}); + }, + + _createCornerSlice: function (color, bgColor, n, position) { + var slice = MochiKit.DOM.SPAN(); + + var inStyle = slice.style; + inStyle.backgroundColor = color.toString(); + inStyle.display = "block"; + inStyle.height = "1px"; + inStyle.overflow = "hidden"; + inStyle.fontSize = "1px"; + + var borderColor = this._borderColor(color, bgColor); + if (this.options.border && n === 0) { + inStyle.borderTopStyle = "solid"; + inStyle.borderTopWidth = "1px"; + inStyle.borderLeftWidth = "0px"; + inStyle.borderRightWidth = "0px"; + inStyle.borderBottomWidth = "0px"; + // assumes css compliant box model + inStyle.height = "0px"; + inStyle.borderColor = borderColor.toString(); + } else if (borderColor) { + inStyle.borderColor = borderColor.toString(); + inStyle.borderStyle = "solid"; + inStyle.borderWidth = "0px 1px"; + } + + if (!this.options.compact && (n == (this.options.numSlices - 1))) { + inStyle.height = "2px"; + } + + this._setMargin(slice, n, position); + this._setBorder(slice, n, position); + + return slice; + }, + + _setOptions: function (options) { + this.options = { + corners: "all", + color: "fromElement", + bgColor: "fromParent", + blend: true, + border: false, + compact: false, + __unstable__wrapElement: false + }; + MochiKit.Base.update(this.options, options); + + this.options.numSlices = (this.options.compact ? 2 : 4); + }, + + _whichSideTop: function () { + var corners = this.options.corners; + if (this._hasString(corners, "all", "top")) { + return ""; + } + + var has_tl = (corners.indexOf("tl") != -1); + var has_tr = (corners.indexOf("tr") != -1); + if (has_tl && has_tr) { + return ""; + } + if (has_tl) { + return "left"; + } + if (has_tr) { + return "right"; + } + return ""; + }, + + _whichSideBottom: function () { + var corners = this.options.corners; + if (this._hasString(corners, "all", "bottom")) { + return ""; + } + + var has_bl = (corners.indexOf('bl') != -1); + var has_br = (corners.indexOf('br') != -1); + if (has_bl && has_br) { + return ""; + } + if (has_bl) { + return "left"; + } + if (has_br) { + return "right"; + } + return ""; + }, + + _borderColor: function (color, bgColor) { + if (color == "transparent") { + return bgColor; + } else if (this.options.border) { + return this.options.border; + } else if (this.options.blend) { + return bgColor.blendedColor(color); + } + return ""; + }, + + + _setMargin: function (el, n, corners) { + var marginSize = this._marginSize(n) + "px"; + var whichSide = ( + corners == "top" ? this._whichSideTop() : this._whichSideBottom() + ); + var style = el.style; + + if (whichSide == "left") { + style.marginLeft = marginSize; + style.marginRight = "0px"; + } else if (whichSide == "right") { + style.marginRight = marginSize; + style.marginLeft = "0px"; + } else { + style.marginLeft = marginSize; + style.marginRight = marginSize; + } + }, + + _setBorder: function (el, n, corners) { + var borderSize = this._borderSize(n) + "px"; + var whichSide = ( + corners == "top" ? this._whichSideTop() : this._whichSideBottom() + ); + + var style = el.style; + if (whichSide == "left") { + style.borderLeftWidth = borderSize; + style.borderRightWidth = "0px"; + } else if (whichSide == "right") { + style.borderRightWidth = borderSize; + style.borderLeftWidth = "0px"; + } else { + style.borderLeftWidth = borderSize; + style.borderRightWidth = borderSize; + } + }, + + _marginSize: function (n) { + if (this.isTransparent) { + return 0; + } + + var o = this.options; + if (o.compact && o.blend) { + var smBlendedMarginSizes = [1, 0]; + return smBlendedMarginSizes[n]; + } else if (o.compact) { + var compactMarginSizes = [2, 1]; + return compactMarginSizes[n]; + } else if (o.blend) { + var blendedMarginSizes = [3, 2, 1, 0]; + return blendedMarginSizes[n]; + } else { + var marginSizes = [5, 3, 2, 1]; + return marginSizes[n]; + } + }, + + _borderSize: function (n) { + var o = this.options; + var borderSizes; + if (o.compact && (o.blend || this.isTransparent)) { + return 1; + } else if (o.compact) { + borderSizes = [1, 0]; + } else if (o.blend) { + borderSizes = [2, 1, 1, 1]; + } else if (o.border) { + borderSizes = [0, 2, 0, 0]; + } else if (this.isTransparent) { + borderSizes = [5, 3, 2, 1]; + } else { + return 0; + } + return borderSizes[n]; + }, + + _hasString: function (str) { + for (var i = 1; i< arguments.length; i++) { + if (str.indexOf(arguments[i]) != -1) { + return true; + } + } + return false; + }, + + _isTopRounded: function () { + return this._hasString(this.options.corners, + "all", "top", "tl", "tr" + ); + }, + + _isBottomRounded: function () { + return this._hasString(this.options.corners, + "all", "bottom", "bl", "br" + ); + }, + + _hasSingleTextChild: function (el) { + return (el.childNodes.length == 1 && el.childNodes[0].nodeType == 3); + } +}; + +/** @id MochiKit.Visual.roundElement */ +MochiKit.Visual.roundElement = function (e, options) { + new MochiKit.Visual._RoundCorners(e, options); +}; + +/** @id MochiKit.Visual.roundClass */ +MochiKit.Visual.roundClass = function (tagName, className, options) { + var elements = MochiKit.DOM.getElementsByTagAndClassName( + tagName, className + ); + for (var i = 0; i < elements.length; i++) { + MochiKit.Visual.roundElement(elements[i], options); + } +}; + +/** @id MochiKit.Visual.tagifyText */ +MochiKit.Visual.tagifyText = function (element, /* optional */tagifyStyle) { + /*** + + Change a node text to character in tags. + + @param tagifyStyle: the style to apply to character nodes, default to + 'position: relative'. + + ***/ + tagifyStyle = tagifyStyle || 'position:relative'; + if (/MSIE/.test(navigator.userAgent)) { + tagifyStyle += ';zoom:1'; + } + element = MochiKit.DOM.getElement(element); + var ma = MochiKit.Base.map; + ma(function (child) { + if (child.nodeType == 3) { + ma(function (character) { + element.insertBefore( + MochiKit.DOM.SPAN({style: tagifyStyle}, + character == ' ' ? String.fromCharCode(160) : character), child); + }, child.nodeValue.split('')); + MochiKit.DOM.removeElement(child); + } + }, element.childNodes); +}; + +/** @id MochiKit.Visual.forceRerendering */ +MochiKit.Visual.forceRerendering = function (element) { + try { + element = MochiKit.DOM.getElement(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { + } +}; + +/** @id MochiKit.Visual.multiple */ +MochiKit.Visual.multiple = function (elements, effect, /* optional */options) { + /*** + + Launch the same effect subsequently on given elements. + + ***/ + options = MochiKit.Base.update({ + speed: 0.1, delay: 0.0 + }, options); + var masterDelay = options.delay; + var index = 0; + MochiKit.Base.map(function (innerelement) { + options.delay = index * options.speed + masterDelay; + new effect(innerelement, options); + index += 1; + }, elements); +}; + +MochiKit.Visual.PAIRS = { + 'slide': ['slideDown', 'slideUp'], + 'blind': ['blindDown', 'blindUp'], + 'appear': ['appear', 'fade'], + 'size': ['grow', 'shrink'] +}; + +/** @id MochiKit.Visual.toggle */ +MochiKit.Visual.toggle = function (element, /* optional */effect, /* optional */options) { + /*** + + Toggle an item between two state depending of its visibility, making + a effect between these states. Default effect is 'appear', can be + 'slide' or 'blind'. + + ***/ + element = MochiKit.DOM.getElement(element); + effect = (effect || 'appear').toLowerCase(); + options = MochiKit.Base.update({ + queue: {position: 'end', scope: (element.id || 'global'), limit: 1} + }, options); + var v = MochiKit.Visual; + v[MochiKit.Style.getStyle(element, 'display') != 'none' ? + v.PAIRS[effect][1] : v.PAIRS[effect][0]](element, options); +}; + +/*** + +Transitions: define functions calculating variations depending of a position. + +***/ + +MochiKit.Visual.Transitions = {}; + +/** @id MochiKit.Visual.Transitions.linear */ +MochiKit.Visual.Transitions.linear = function (pos) { + return pos; +}; + +/** @id MochiKit.Visual.Transitions.sinoidal */ +MochiKit.Visual.Transitions.sinoidal = function (pos) { + return 0.5 - Math.cos(pos*Math.PI)/2; +}; + +/** @id MochiKit.Visual.Transitions.reverse */ +MochiKit.Visual.Transitions.reverse = function (pos) { + return 1 - pos; +}; + +/** @id MochiKit.Visual.Transitions.flicker */ +MochiKit.Visual.Transitions.flicker = function (pos) { + return 0.25 - Math.cos(pos*Math.PI)/4 + Math.random()/2; +}; + +/** @id MochiKit.Visual.Transitions.wobble */ +MochiKit.Visual.Transitions.wobble = function (pos) { + return 0.5 - Math.cos(9*pos*Math.PI)/2; +}; + +/** @id MochiKit.Visual.Transitions.pulse */ +MochiKit.Visual.Transitions.pulse = function (pos, pulses) { + if (pulses) { + pos *= 2 * pulses; + } else { + pos *= 10; + } + var decimals = pos - Math.floor(pos); + return (Math.floor(pos) % 2 == 0) ? decimals : 1 - decimals; +}; + +/** @id MochiKit.Visual.Transitions.parabolic */ +MochiKit.Visual.Transitions.parabolic = function (pos) { + return pos * pos; +}; + +/** @id MochiKit.Visual.Transitions.none */ +MochiKit.Visual.Transitions.none = function (pos) { + return 0; +}; + +/** @id MochiKit.Visual.Transitions.full */ +MochiKit.Visual.Transitions.full = function (pos) { + return 1; +}; + +/*** + +Core effects + +***/ + +MochiKit.Visual.ScopedQueue = function () { + var cls = arguments.callee; + if (!(this instanceof cls)) { + return new cls(); + } + this.__init__(); +}; + +MochiKit.Base.update(MochiKit.Visual.ScopedQueue.prototype, { + __init__: function () { + this.effects = []; + this.interval = null; + }, + + /** @id MochiKit.Visual.ScopedQueue.prototype.add */ + add: function (effect) { + var timestamp = new Date().getTime(); + + var position = (typeof(effect.options.queue) == 'string') ? + effect.options.queue : effect.options.queue.position; + + var ma = MochiKit.Base.map; + switch (position) { + case 'front': + // move unstarted effects after this effect + ma(function (e) { + if (e.state == 'idle') { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + } + }, this.effects); + break; + case 'end': + var finish; + // start effect after last queued effect has finished + ma(function (e) { + var i = e.finishOn; + if (i >= (finish || i)) { + finish = i; + } + }, this.effects); + timestamp = finish || timestamp; + break; + case 'break': + ma(function (e) { + e.finalize(); + }, this.effects); + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + if (!effect.options.queue.limit || + this.effects.length < effect.options.queue.limit) { + this.effects.push(effect); + } + + if (!this.interval) { + this.interval = this.startLoop(MochiKit.Base.bind(this.loop, this), + 40); + } + }, + + /** @id MochiKit.Visual.ScopedQueue.prototype.startLoop */ + startLoop: function (func, interval) { + return setInterval(func, interval); + }, + + /** @id MochiKit.Visual.ScopedQueue.prototype.remove */ + remove: function (effect) { + this.effects = MochiKit.Base.filter(function (e) { + return e != effect; + }, this.effects); + if (!this.effects.length) { + this.stopLoop(this.interval); + this.interval = null; + } + }, + + /** @id MochiKit.Visual.ScopedQueue.prototype.stopLoop */ + stopLoop: function (interval) { + clearInterval(interval); + }, + + /** @id MochiKit.Visual.ScopedQueue.prototype.loop */ + loop: function () { + var timePos = new Date().getTime(); + MochiKit.Base.map(function (effect) { + effect.loop(timePos); + }, this.effects); + } +}); + +MochiKit.Visual.Queues = { + instances: {}, + + get: function (queueName) { + if (typeof(queueName) != 'string') { + return queueName; + } + + if (!this.instances[queueName]) { + this.instances[queueName] = new MochiKit.Visual.ScopedQueue(); + } + return this.instances[queueName]; + } +}; + +MochiKit.Visual.Queue = MochiKit.Visual.Queues.get('global'); + +MochiKit.Visual.DefaultOptions = { + transition: MochiKit.Visual.Transitions.sinoidal, + duration: 1.0, // seconds + fps: 25.0, // max. 25fps due to MochiKit.Visual.Queue implementation + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' +}; + +MochiKit.Visual.Base = function () {}; + +MochiKit.Visual.Base.prototype = { + /*** + + Basic class for all Effects. Define a looping mechanism called for each step + of an effect. Don't instantiate it, only subclass it. + + ***/ + + __class__ : MochiKit.Visual.Base, + + /** @id MochiKit.Visual.Base.prototype.start */ + start: function (options) { + var v = MochiKit.Visual; + this.options = MochiKit.Base.setdefault(options, + v.DefaultOptions); + this.currentFrame = 0; + this.state = 'idle'; + this.startOn = this.options.delay*1000; + this.finishOn = this.startOn + (this.options.duration*1000); + this.event('beforeStart'); + if (!this.options.sync) { + v.Queues.get(typeof(this.options.queue) == 'string' ? + 'global' : this.options.queue.scope).add(this); + } + }, + + /** @id MochiKit.Visual.Base.prototype.loop */ + loop: function (timePos) { + if (timePos >= this.startOn) { + if (timePos >= this.finishOn) { + return this.finalize(); + } + var pos = (timePos - this.startOn) / (this.finishOn - this.startOn); + var frame = + Math.round(pos * this.options.fps * this.options.duration); + if (frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + + /** @id MochiKit.Visual.Base.prototype.render */ + render: function (pos) { + if (this.state == 'idle') { + this.state = 'running'; + this.event('beforeSetup'); + this.setup(); + this.event('afterSetup'); + } + if (this.state == 'running') { + if (this.options.transition) { + pos = this.options.transition(pos); + } + pos *= (this.options.to - this.options.from); + pos += this.options.from; + this.event('beforeUpdate'); + this.update(pos); + this.event('afterUpdate'); + } + }, + + /** @id MochiKit.Visual.Base.prototype.cancel */ + cancel: function () { + if (!this.options.sync) { + MochiKit.Visual.Queues.get(typeof(this.options.queue) == 'string' ? + 'global' : this.options.queue.scope).remove(this); + } + this.state = 'finished'; + }, + + /** @id MochiKit.Visual.Base.prototype.finalize */ + finalize: function () { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + this.finish(); + this.event('afterFinish'); + }, + + setup: function () { + }, + + finish: function () { + }, + + update: function (position) { + }, + + /** @id MochiKit.Visual.Base.prototype.event */ + event: function (eventName) { + if (this.options[eventName + 'Internal']) { + this.options[eventName + 'Internal'](this); + } + if (this.options[eventName]) { + this.options[eventName](this); + } + }, + + /** @id MochiKit.Visual.Base.prototype.repr */ + repr: function () { + return '[' + this.__class__.NAME + ', options:' + + MochiKit.Base.repr(this.options) + ']'; + } +}; + +/** @id MochiKit.Visual.Parallel */ +MochiKit.Visual.Parallel = function (effects, options) { + var cls = arguments.callee; + if (!(this instanceof cls)) { + return new cls(effects, options); + } + + this.__init__(effects, options); +}; + +MochiKit.Visual.Parallel.prototype = new MochiKit.Visual.Base(); + +MochiKit.Base.update(MochiKit.Visual.Parallel.prototype, { + /*** + + Run multiple effects at the same time. + + ***/ + + __class__ : MochiKit.Visual.Parallel, + + __init__: function (effects, options) { + this.effects = effects || []; + this.start(options); + }, + + /** @id MochiKit.Visual.Parallel.prototype.update */ + update: function (position) { + MochiKit.Base.map(function (effect) { + effect.render(position); + }, this.effects); + }, + + /** @id MochiKit.Visual.Parallel.prototype.finish */ + finish: function () { + MochiKit.Base.map(function (effect) { + effect.finalize(); + }, this.effects); + } +}); + +/** @id MochiKit.Visual.Sequence */ +MochiKit.Visual.Sequence = function (effects, options) { + var cls = arguments.callee; + if (!(this instanceof cls)) { + return new cls(effects, options); + } + this.__init__(effects, options); +}; + +MochiKit.Visual.Sequence.prototype = new MochiKit.Visual.Base(); + +MochiKit.Base.update(MochiKit.Visual.Sequence.prototype, { + + __class__ : MochiKit.Visual.Sequence, + + __init__: function (effects, options) { + var defs = { transition: MochiKit.Visual.Transitions.linear, + duration: 0 }; + this.effects = effects || []; + MochiKit.Base.map(function (effect) { + defs.duration += effect.options.duration; + }, this.effects); + MochiKit.Base.setdefault(options, defs); + this.start(options); + }, + + /** @id MochiKit.Visual.Sequence.prototype.update */ + update: function (position) { + var time = position * this.options.duration; + for (var i = 0; i < this.effects.length; i++) { + var effect = this.effects[i]; + if (time <= effect.options.duration) { + effect.render(time / effect.options.duration); + break; + } else { + time -= effect.options.duration; + } + } + }, + + /** @id MochiKit.Visual.Sequence.prototype.finish */ + finish: function () { + MochiKit.Base.map(function (effect) { + effect.finalize(); + }, this.effects); + } +}); + +/** @id MochiKit.Visual.Opacity */ +MochiKit.Visual.Opacity = function (element, options) { + var cls = arguments.callee; + if (!(this instanceof cls)) { + return new cls(element, options); + } + this.__init__(element, options); +}; + +MochiKit.Visual.Opacity.prototype = new MochiKit.Visual.Base(); + +MochiKit.Base.update(MochiKit.Visual.Opacity.prototype, { + /*** + + Change the opacity of an element. + + @param options: 'from' and 'to' change the starting and ending opacities. + Must be between 0.0 and 1.0. Default to current opacity and 1.0. + + ***/ + + __class__ : MochiKit.Visual.Opacity, + + __init__: function (element, /* optional */options) { + var b = MochiKit.Base; + var s = MochiKit.Style; + this.element = MochiKit.DOM.getElement(element); + // make this work on IE on elements without 'layout' + if (this.element.currentStyle && + (!this.element.currentStyle.hasLayout)) { + s.setStyle(this.element, {zoom: 1}); + } + options = b.update({ + from: s.getStyle(this.element, 'opacity') || 0.0, + to: 1.0 + }, options); + this.start(options); + }, + + /** @id MochiKit.Visual.Opacity.prototype.update */ + update: function (position) { + MochiKit.Style.setStyle(this.element, {'opacity': position}); + } +}); + +/** @id MochiKit.Visual.Move.prototype */ +MochiKit.Visual.Move = function (element, options) { + var cls = arguments.callee; + if (!(this instanceof cls)) { + return new cls(element, options); + } + this.__init__(element, options); +}; + +MochiKit.Visual.Move.prototype = new MochiKit.Visual.Base(); + +MochiKit.Base.update(MochiKit.Visual.Move.prototype, { + /*** + + Move an element between its current position to a defined position + + @param options: 'x' and 'y' for final positions, default to 0, 0. + + ***/ + + __class__ : MochiKit.Visual.Move, + + __init__: function (element, /* optional */options) { + this.element = MochiKit.DOM.getElement(element); + options = MochiKit.Base.update({ + x: 0, + y: 0, + mode: 'relative' + }, options); + this.start(options); + }, + + /** @id MochiKit.Visual.Move.prototype.setup */ + setup: function () { + // Bug in Opera: Opera returns the 'real' position of a static element + // or relative element that does not have top/left explicitly set. + // ==> Always set top and left for position relative elements in your + // stylesheets (to 0 if you do not need them) + MochiKit.Style.makePositioned(this.element); + + var s = this.element.style; + var originalVisibility = s.visibility; + var originalDisplay = s.display; + if (originalDisplay == 'none') { + s.visibility = 'hidden'; + s.display = ''; + } + + this.originalLeft = parseFloat(MochiKit.Style.getStyle(this.element, 'left') || '0'); + this.originalTop = parseFloat(MochiKit.Style.getStyle(this.element, 'top') || '0'); + + if (this.options.mode == 'absolute') { + // absolute movement, so we need to calc deltaX and deltaY + this.options.x -= this.originalLeft; + this.options.y -= this.originalTop; + } + if (originalDisplay == 'none') { + s.visibility = originalVisibility; + s.display = originalDisplay; + } + }, + + /** @id MochiKit.Visual.Move.prototype.update */ + update: function (position) { + MochiKit.Style.setStyle(this.element, { + left: Math.round(this.options.x * position + this.originalLeft) + 'px', + top: Math.round(this.options.y * position + this.originalTop) + 'px' + }); + } +}); + +/** @id MochiKit.Visual.Scale */ +MochiKit.Visual.Scale = function (element, percent, options) { + var cls = arguments.callee; + if (!(this instanceof cls)) { + return new cls(element, percent, options); + } + this.__init__(element, percent, options); +}; + +MochiKit.Visual.Scale.prototype = new MochiKit.Visual.Base(); + +MochiKit.Base.update(MochiKit.Visual.Scale.prototype, { + /*** + + Change the size of an element. + + @param percent: final_size = percent*original_size + + @param options: several options changing scale behaviour + + ***/ + + __class__ : MochiKit.Visual.Scale, + + __init__: function (element, percent, /* optional */options) { + this.element = MochiKit.DOM.getElement(element); + options = MochiKit.Base.update({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or {} with provided values + scaleFrom: 100.0, + scaleTo: percent + }, options); + this.start(options); + }, + + /** @id MochiKit.Visual.Scale.prototype.setup */ + setup: function () { + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = MochiKit.Style.getStyle(this.element, + 'position'); + + var ma = MochiKit.Base.map; + var b = MochiKit.Base.bind; + this.originalStyle = {}; + ma(b(function (k) { + this.originalStyle[k] = this.element.style[k]; + }, this), ['top', 'left', 'width', 'height', 'fontSize']); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = MochiKit.Style.getStyle(this.element, + 'font-size') || '100%'; + ma(b(function (fontSizeType) { + if (fontSize.indexOf(fontSizeType) > 0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = fontSizeType; + } + }, this), ['em', 'px', '%']); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + if (/^content/.test(this.options.scaleMode)) { + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + } else if (this.options.scaleMode == 'box') { + this.dims = [this.element.offsetHeight, this.element.offsetWidth]; + } else { + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + } + }, + + /** @id MochiKit.Visual.Scale.prototype.update */ + update: function (position) { + var currentScale = (this.options.scaleFrom/100.0) + + (this.factor * position); + if (this.options.scaleContent && this.fontSize) { + MochiKit.Style.setStyle(this.element, { + fontSize: this.fontSize * currentScale + this.fontSizeType + }); + } + this.setDimensions(this.dims[0] * currentScale, + this.dims[1] * currentScale); + }, + + /** @id MochiKit.Visual.Scale.prototype.finish */ + finish: function () { + if (this.restoreAfterFinish) { + MochiKit.Style.setStyle(this.element, this.originalStyle); + } + }, + + /** @id MochiKit.Visual.Scale.prototype.setDimensions */ + setDimensions: function (height, width) { + var d = {}; + var r = Math.round; + if (/MSIE/.test(navigator.userAgent)) { + r = Math.ceil; + } + if (this.options.scaleX) { + d.width = r(width) + 'px'; + } + if (this.options.scaleY) { + d.height = r(height) + 'px'; + } + if (this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if (this.elementPositioning == 'absolute') { + if (this.options.scaleY) { + d.top = this.originalTop - topd + 'px'; + } + if (this.options.scaleX) { + d.left = this.originalLeft - leftd + 'px'; + } + } else { + if (this.options.scaleY) { + d.top = -topd + 'px'; + } + if (this.options.scaleX) { + d.left = -leftd + 'px'; + } + } + } + MochiKit.Style.setStyle(this.element, d); + } +}); + +/** @id MochiKit.Visual.Highlight */ +MochiKit.Visual.Highlight = function (element, options) { + var cls = arguments.callee; + if (!(this instanceof cls)) { + return new cls(element, options); + } + this.__init__(element, options); +}; + +MochiKit.Visual.Highlight.prototype = new MochiKit.Visual.Base(); + +MochiKit.Base.update(MochiKit.Visual.Highlight.prototype, { + /*** + + Highlight an item of the page. + + @param options: 'startcolor' for choosing highlighting color, default + to '#ffff99'. + + ***/ + + __class__ : MochiKit.Visual.Highlight, + + __init__: function (element, /* optional */options) { + this.element = MochiKit.DOM.getElement(element); + options = MochiKit.Base.update({ + startcolor: '#ffff99' + }, options); + this.start(options); + }, + + /** @id MochiKit.Visual.Highlight.prototype.setup */ + setup: function () { + var b = MochiKit.Base; + var s = MochiKit.Style; + // Prevent executing on elements not in the layout flow + if (s.getStyle(this.element, 'display') == 'none') { + this.cancel(); + return; + } + // Disable background image during the effect + this.oldStyle = { + backgroundImage: s.getStyle(this.element, 'background-image') + }; + s.setStyle(this.element, { + backgroundImage: 'none' + }); + + if (!this.options.endcolor) { + this.options.endcolor = + MochiKit.Color.Color.fromBackground(this.element).toHexString(); + } + if (b.isUndefinedOrNull(this.options.restorecolor)) { + this.options.restorecolor = s.getStyle(this.element, + 'background-color'); + } + // init color calculations + this._base = b.map(b.bind(function (i) { + return parseInt( + this.options.startcolor.slice(i*2 + 1, i*2 + 3), 16); + }, this), [0, 1, 2]); + this._delta = b.map(b.bind(function (i) { + return parseInt(this.options.endcolor.slice(i*2 + 1, i*2 + 3), 16) + - this._base[i]; + }, this), [0, 1, 2]); + }, + + /** @id MochiKit.Visual.Highlight.prototype.update */ + update: function (position) { + var m = '#'; + MochiKit.Base.map(MochiKit.Base.bind(function (i) { + m += MochiKit.Color.toColorPart(Math.round(this._base[i] + + this._delta[i]*position)); + }, this), [0, 1, 2]); + MochiKit.Style.setStyle(this.element, { + backgroundColor: m + }); + }, + + /** @id MochiKit.Visual.Highlight.prototype.finish */ + finish: function () { + MochiKit.Style.setStyle(this.element, + MochiKit.Base.update(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +/** @id MochiKit.Visual.ScrollTo */ +MochiKit.Visual.ScrollTo = function (element, options) { + var cls = arguments.callee; + if (!(this instanceof cls)) { + return new cls(element, options); + } + this.__init__(element, options); +}; + +MochiKit.Visual.ScrollTo.prototype = new MochiKit.Visual.Base(); + +MochiKit.Base.update(MochiKit.Visual.ScrollTo.prototype, { + /*** + + Scroll to an element in the page. + + ***/ + + __class__ : MochiKit.Visual.ScrollTo, + + __init__: function (element, /* optional */options) { + this.element = MochiKit.DOM.getElement(element); + this.start(options); + }, + + /** @id MochiKit.Visual.ScrollTo.prototype.setup */ + setup: function () { + var p = MochiKit.Position; + p.prepare(); + var offsets = p.cumulativeOffset(this.element); + if (this.options.offset) { + offsets.y += this.options.offset; + } + var max; + if (window.innerHeight) { + max = window.innerHeight - window.height; + } else if (document.documentElement && + document.documentElement.clientHeight) { + max = document.documentElement.clientHeight - + document.body.scrollHeight; + } else if (document.body) { + max = document.body.clientHeight - document.body.scrollHeight; + } + this.scrollStart = p.windowOffset.y; + this.delta = (offsets.y > max ? max : offsets.y) - this.scrollStart; + }, + + /** @id MochiKit.Visual.ScrollTo.prototype.update */ + update: function (position) { + var p = MochiKit.Position; + p.prepare(); + window.scrollTo(p.windowOffset.x, this.scrollStart + (position * this.delta)); + } +}); + +MochiKit.Visual.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; + +MochiKit.Visual.Morph = function (element, options) { + var cls = arguments.callee; + if (!(this instanceof cls)) { + return new cls(element, options); + } + this.__init__(element, options); +}; + +MochiKit.Visual.Morph.prototype = new MochiKit.Visual.Base(); + +MochiKit.Base.update(MochiKit.Visual.Morph.prototype, { + /*** + + Morph effect: make a transformation from current style to the given style, + automatically making a transition between the two. + + ***/ + + __class__ : MochiKit.Visual.Morph, + + __init__: function (element, /* optional */options) { + this.element = MochiKit.DOM.getElement(element); + this.start(options); + }, + + /** @id MochiKit.Visual.Morph.prototype.setup */ + setup: function () { + var b = MochiKit.Base; + var style = this.options.style; + this.styleStart = {}; + this.styleEnd = {}; + this.units = {}; + var value, unit; + for (var s in style) { + value = style[s]; + s = b.camelize(s); + if (MochiKit.Visual.CSS_LENGTH.test(value)) { + var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); + value = parseFloat(components[1]); + unit = (components.length == 3) ? components[2] : null; + this.styleEnd[s] = value; + this.units[s] = unit; + value = MochiKit.Style.getStyle(this.element, s); + components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); + value = parseFloat(components[1]); + this.styleStart[s] = value; + } else if (/[Cc]olor$/.test(s)) { + var c = MochiKit.Color.Color; + value = c.fromString(value); + if (value) { + this.units[s] = "color"; + this.styleEnd[s] = value.toHexString(); + value = MochiKit.Style.getStyle(this.element, s); + this.styleStart[s] = c.fromString(value).toHexString(); + + this.styleStart[s] = b.map(b.bind(function (i) { + return parseInt( + this.styleStart[s].slice(i*2 + 1, i*2 + 3), 16); + }, this), [0, 1, 2]); + this.styleEnd[s] = b.map(b.bind(function (i) { + return parseInt( + this.styleEnd[s].slice(i*2 + 1, i*2 + 3), 16); + }, this), [0, 1, 2]); + } + } else { + // For non-length & non-color properties, we just set the value + this.element.style[s] = value; + } + } + }, + + /** @id MochiKit.Visual.Morph.prototype.update */ + update: function (position) { + var value; + for (var s in this.styleStart) { + if (this.units[s] == "color") { + var m = '#'; + var start = this.styleStart[s]; + var end = this.styleEnd[s]; + MochiKit.Base.map(MochiKit.Base.bind(function (i) { + m += MochiKit.Color.toColorPart(Math.round(start[i] + + (end[i] - start[i])*position)); + }, this), [0, 1, 2]); + this.element.style[s] = m; + } else { + value = this.styleStart[s] + Math.round((this.styleEnd[s] - this.styleStart[s]) * position * 1000) / 1000 + this.units[s]; + this.element.style[s] = value; + } + } + } +}); + +/*** + +Combination effects. + +***/ + +/** @id MochiKit.Visual.fade */ +MochiKit.Visual.fade = function (element, /* optional */ options) { + /*** + + Fade a given element: change its opacity and hide it in the end. + + @param options: 'to' and 'from' to change opacity. + + ***/ + var s = MochiKit.Style; + var oldOpacity = s.getStyle(element, 'opacity'); + options = MochiKit.Base.update({ + from: s.getStyle(element, 'opacity') || 1.0, + to: 0.0, + afterFinishInternal: function (effect) { + if (effect.options.to !== 0) { + return; + } + s.hideElement(effect.element); + s.setStyle(effect.element, {'opacity': oldOpacity}); + } + }, options); + return new MochiKit.Visual.Opacity(element, options); +}; + +/** @id MochiKit.Visual.appear */ +MochiKit.Visual.appear = function (element, /* optional */ options) { + /*** + + Make an element appear. + + @param options: 'to' and 'from' to change opacity. + + ***/ + var s = MochiKit.Style; + var v = MochiKit.Visual; + options = MochiKit.Base.update({ + from: (s.getStyle(element, 'display') == 'none' ? 0.0 : + s.getStyle(element, 'opacity') || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function (effect) { + v.forceRerendering(effect.element); + }, + beforeSetupInternal: function (effect) { + s.setStyle(effect.element, {'opacity': effect.options.from}); + s.showElement(effect.element); + } + }, options); + return new v.Opacity(element, options); +}; + +/** @id MochiKit.Visual.puff */ +MochiKit.Visual.puff = function (element, /* optional */ options) { + /*** + + 'Puff' an element: grow it to double size, fading it and make it hidden. + + ***/ + var s = MochiKit.Style; + var v = MochiKit.Visual; + element = MochiKit.DOM.getElement(element); + var elementDimensions = MochiKit.Style.getElementDimensions(element, true); + var oldStyle = { + position: s.getStyle(element, 'position'), + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height, + opacity: s.getStyle(element, 'opacity') + }; + options = MochiKit.Base.update({ + beforeSetupInternal: function (effect) { + MochiKit.Position.absolutize(effect.effects[0].element); + }, + afterFinishInternal: function (effect) { + s.hideElement(effect.effects[0].element); + s.setStyle(effect.effects[0].element, oldStyle); + }, + scaleContent: true, + scaleFromCenter: true + }, options); + return new v.Parallel( + [new v.Scale(element, 200, + {sync: true, scaleFromCenter: options.scaleFromCenter, + scaleMode: {originalHeight: elementDimensions.h, + originalWidth: elementDimensions.w}, + scaleContent: options.scaleContent, restoreAfterFinish: true}), + new v.Opacity(element, {sync: true, to: 0.0 })], + options); +}; + +/** @id MochiKit.Visual.blindUp */ +MochiKit.Visual.blindUp = function (element, /* optional */ options) { + /*** + + Blind an element up: change its vertical size to 0. + + ***/ + var d = MochiKit.DOM; + var s = MochiKit.Style; + element = d.getElement(element); + var elementDimensions = s.getElementDimensions(element, true); + var elemClip = s.makeClipping(element); + options = MochiKit.Base.update({ + scaleContent: false, + scaleX: false, + scaleMode: {originalHeight: elementDimensions.h, + originalWidth: elementDimensions.w}, + restoreAfterFinish: true, + afterFinishInternal: function (effect) { + s.hideElement(effect.element); + s.undoClipping(effect.element, elemClip); + } + }, options); + return new MochiKit.Visual.Scale(element, 0, options); +}; + +/** @id MochiKit.Visual.blindDown */ +MochiKit.Visual.blindDown = function (element, /* optional */ options) { + /*** + + Blind an element down: restore its vertical size. + + ***/ + var d = MochiKit.DOM; + var s = MochiKit.Style; + element = d.getElement(element); + var elementDimensions = s.getElementDimensions(element, true); + var elemClip; + options = MochiKit.Base.update({ + scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.h, + originalWidth: elementDimensions.w}, + restoreAfterFinish: true, + afterSetupInternal: function (effect) { + elemClip = s.makeClipping(effect.element); + s.setStyle(effect.element, {height: '0px'}); + s.showElement(effect.element); + }, + afterFinishInternal: function (effect) { + s.undoClipping(effect.element, elemClip); + } + }, options); + return new MochiKit.Visual.Scale(element, 100, options); +}; + +/** @id MochiKit.Visual.switchOff */ +MochiKit.Visual.switchOff = function (element, /* optional */ options) { + /*** + + Apply a switch-off-like effect. + + ***/ + var d = MochiKit.DOM; + var s = MochiKit.Style; + element = d.getElement(element); + var elementDimensions = s.getElementDimensions(element, true); + var oldOpacity = s.getStyle(element, 'opacity'); + var elemClip; + options = MochiKit.Base.update({ + duration: 0.7, + restoreAfterFinish: true, + beforeSetupInternal: function (effect) { + s.makePositioned(element); + elemClip = s.makeClipping(element); + }, + afterFinishInternal: function (effect) { + s.hideElement(element); + s.undoClipping(element, elemClip); + s.undoPositioned(element); + s.setStyle(element, {'opacity': oldOpacity}); + } + }, options); + var v = MochiKit.Visual; + return new v.Sequence( + [new v.appear(element, + { sync: true, duration: 0.57 * options.duration, + from: 0, transition: v.Transitions.flicker }), + new v.Scale(element, 1, + { sync: true, duration: 0.43 * options.duration, + scaleFromCenter: true, scaleX: false, + scaleMode: {originalHeight: elementDimensions.h, + originalWidth: elementDimensions.w}, + scaleContent: false, restoreAfterFinish: true })], + options); +}; + +/** @id MochiKit.Visual.dropOut */ +MochiKit.Visual.dropOut = function (element, /* optional */ options) { + /*** + + Make an element fall and disappear. + + ***/ + var d = MochiKit.DOM; + var s = MochiKit.Style; + element = d.getElement(element); + var oldStyle = { + top: s.getStyle(element, 'top'), + left: s.getStyle(element, 'left'), + opacity: s.getStyle(element, 'opacity') + }; + + options = MochiKit.Base.update({ + duration: 0.5, + distance: 100, + beforeSetupInternal: function (effect) { + s.makePositioned(effect.effects[0].element); + }, + afterFinishInternal: function (effect) { + s.hideElement(effect.effects[0].element); + s.undoPositioned(effect.effects[0].element); + s.setStyle(effect.effects[0].element, oldStyle); + } + }, options); + var v = MochiKit.Visual; + return new v.Parallel( + [new v.Move(element, {x: 0, y: options.distance, sync: true}), + new v.Opacity(element, {sync: true, to: 0.0})], + options); +}; + +/** @id MochiKit.Visual.shake */ +MochiKit.Visual.shake = function (element, /* optional */ options) { + /*** + + Move an element from left to right several times. + + ***/ + var d = MochiKit.DOM; + var v = MochiKit.Visual; + var s = MochiKit.Style; + element = d.getElement(element); + var oldStyle = { + top: s.getStyle(element, 'top'), + left: s.getStyle(element, 'left') + }; + options = MochiKit.Base.update({ + duration: 0.5, + afterFinishInternal: function (effect) { + s.undoPositioned(element); + s.setStyle(element, oldStyle); + } + }, options); + return new v.Sequence( + [new v.Move(element, { sync: true, duration: 0.1 * options.duration, + x: 20, y: 0 }), + new v.Move(element, { sync: true, duration: 0.2 * options.duration, + x: -40, y: 0 }), + new v.Move(element, { sync: true, duration: 0.2 * options.duration, + x: 40, y: 0 }), + new v.Move(element, { sync: true, duration: 0.2 * options.duration, + x: -40, y: 0 }), + new v.Move(element, { sync: true, duration: 0.2 * options.duration, + x: 40, y: 0 }), + new v.Move(element, { sync: true, duration: 0.1 * options.duration, + x: -20, y: 0 })], + options); +}; + +/** @id MochiKit.Visual.slideDown */ +MochiKit.Visual.slideDown = function (element, /* optional */ options) { + /*** + + Slide an element down. + It needs to have the content of the element wrapped in a container + element with fixed height. + + ***/ + var d = MochiKit.DOM; + var b = MochiKit.Base; + var s = MochiKit.Style; + element = d.getElement(element); + if (!element.firstChild) { + throw new Error("MochiKit.Visual.slideDown must be used on a element with a child"); + } + d.removeEmptyTextNodes(element); + var oldInnerBottom = s.getStyle(element.firstChild, 'bottom') || 0; + var elementDimensions = s.getElementDimensions(element, true); + var elemClip; + options = b.update({ + scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.h, + originalWidth: elementDimensions.w}, + restoreAfterFinish: true, + afterSetupInternal: function (effect) { + s.makePositioned(effect.element); + s.makePositioned(effect.element.firstChild); + if (/Opera/.test(navigator.userAgent)) { + s.setStyle(effect.element, {top: ''}); + } + elemClip = s.makeClipping(effect.element); + s.setStyle(effect.element, {height: '0px'}); + s.showElement(effect.element); + }, + afterUpdateInternal: function (effect) { + var elementDimensions = s.getElementDimensions(effect.element, true); + s.setStyle(effect.element.firstChild, + {bottom: (effect.dims[0] - elementDimensions.h) + 'px'}); + }, + afterFinishInternal: function (effect) { + s.undoClipping(effect.element, elemClip); + // IE will crash if child is undoPositioned first + if (/MSIE/.test(navigator.userAgent)) { + s.undoPositioned(effect.element); + s.undoPositioned(effect.element.firstChild); + } else { + s.undoPositioned(effect.element.firstChild); + s.undoPositioned(effect.element); + } + s.setStyle(effect.element.firstChild, {bottom: oldInnerBottom}); + } + }, options); + + return new MochiKit.Visual.Scale(element, 100, options); +}; + +/** @id MochiKit.Visual.slideUp */ +MochiKit.Visual.slideUp = function (element, /* optional */ options) { + /*** + + Slide an element up. + It needs to have the content of the element wrapped in a container + element with fixed height. + + ***/ + var d = MochiKit.DOM; + var b = MochiKit.Base; + var s = MochiKit.Style; + element = d.getElement(element); + if (!element.firstChild) { + throw new Error("MochiKit.Visual.slideUp must be used on a element with a child"); + } + d.removeEmptyTextNodes(element); + var oldInnerBottom = s.getStyle(element.firstChild, 'bottom'); + var elementDimensions = s.getElementDimensions(element, true); + var elemClip; + options = b.update({ + scaleContent: false, + scaleX: false, + scaleMode: {originalHeight: elementDimensions.h, + originalWidth: elementDimensions.w}, + scaleFrom: 100, + restoreAfterFinish: true, + beforeStartInternal: function (effect) { + s.makePositioned(effect.element); + s.makePositioned(effect.element.firstChild); + if (/Opera/.test(navigator.userAgent)) { + s.setStyle(effect.element, {top: ''}); + } + elemClip = s.makeClipping(effect.element); + s.showElement(effect.element); + }, + afterUpdateInternal: function (effect) { + var elementDimensions = s.getElementDimensions(effect.element, true); + s.setStyle(effect.element.firstChild, + {bottom: (effect.dims[0] - elementDimensions.h) + 'px'}); + }, + afterFinishInternal: function (effect) { + s.hideElement(effect.element); + s.undoClipping(effect.element, elemClip); + s.undoPositioned(effect.element.firstChild); + s.undoPositioned(effect.element); + s.setStyle(effect.element.firstChild, {bottom: oldInnerBottom}); + } + }, options); + return new MochiKit.Visual.Scale(element, 0, options); +}; + +// Bug in opera makes the TD containing this element expand for a instance +// after finish +/** @id MochiKit.Visual.squish */ +MochiKit.Visual.squish = function (element, /* optional */ options) { + /*** + + Reduce an element and make it disappear. + + ***/ + var d = MochiKit.DOM; + var b = MochiKit.Base; + var s = MochiKit.Style; + var elementDimensions = s.getElementDimensions(element, true); + var elemClip; + options = b.update({ + restoreAfterFinish: true, + scaleMode: {originalHeight: elementDimensions.w, + originalWidth: elementDimensions.h}, + beforeSetupInternal: function (effect) { + elemClip = s.makeClipping(effect.element); + }, + afterFinishInternal: function (effect) { + s.hideElement(effect.element); + s.undoClipping(effect.element, elemClip); + } + }, options); + + return new MochiKit.Visual.Scale(element, /Opera/.test(navigator.userAgent) ? 1 : 0, options); +}; + +/** @id MochiKit.Visual.grow */ +MochiKit.Visual.grow = function (element, /* optional */ options) { + /*** + + Grow an element to its original size. Make it zero-sized before + if necessary. + + ***/ + var d = MochiKit.DOM; + var v = MochiKit.Visual; + var s = MochiKit.Style; + element = d.getElement(element); + options = MochiKit.Base.update({ + direction: 'center', + moveTransition: v.Transitions.sinoidal, + scaleTransition: v.Transitions.sinoidal, + opacityTransition: v.Transitions.full, + scaleContent: true, + scaleFromCenter: false + }, options); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: s.getStyle(element, 'opacity') + }; + var dims = s.getElementDimensions(element, true); + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = dims.w; + initialMoveY = moveY = 0; + moveX = -dims.w; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = dims.h; + moveY = -dims.h; + break; + case 'bottom-right': + initialMoveX = dims.w; + initialMoveY = dims.h; + moveX = -dims.w; + moveY = -dims.h; + break; + case 'center': + initialMoveX = dims.w / 2; + initialMoveY = dims.h / 2; + moveX = -dims.w / 2; + moveY = -dims.h / 2; + break; + } + + var optionsParallel = MochiKit.Base.update({ + beforeSetupInternal: function (effect) { + s.setStyle(effect.effects[0].element, {height: '0px'}); + s.showElement(effect.effects[0].element); + }, + afterFinishInternal: function (effect) { + s.undoClipping(effect.effects[0].element); + s.undoPositioned(effect.effects[0].element); + s.setStyle(effect.effects[0].element, oldStyle); + } + }, options); + + return new v.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetupInternal: function (effect) { + s.hideElement(effect.element); + s.makeClipping(effect.element); + s.makePositioned(effect.element); + }, + afterFinishInternal: function (effect) { + new v.Parallel( + [new v.Opacity(effect.element, { + sync: true, to: 1.0, from: 0.0, + transition: options.opacityTransition + }), + new v.Move(effect.element, { + x: moveX, y: moveY, sync: true, + transition: options.moveTransition + }), + new v.Scale(effect.element, 100, { + scaleMode: {originalHeight: dims.h, + originalWidth: dims.w}, + sync: true, + scaleFrom: /Opera/.test(navigator.userAgent) ? 1 : 0, + transition: options.scaleTransition, + scaleContent: options.scaleContent, + scaleFromCenter: options.scaleFromCenter, + restoreAfterFinish: true + }) + ], optionsParallel + ); + } + }); +}; + +/** @id MochiKit.Visual.shrink */ +MochiKit.Visual.shrink = function (element, /* optional */ options) { + /*** + + Shrink an element and make it disappear. + + ***/ + var d = MochiKit.DOM; + var v = MochiKit.Visual; + var s = MochiKit.Style; + element = d.getElement(element); + options = MochiKit.Base.update({ + direction: 'center', + moveTransition: v.Transitions.sinoidal, + scaleTransition: v.Transitions.sinoidal, + opacityTransition: v.Transitions.none, + scaleContent: true, + scaleFromCenter: false + }, options); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: s.getStyle(element, 'opacity') + }; + + var dims = s.getElementDimensions(element, true); + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = dims.w; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = dims.h; + break; + case 'bottom-right': + moveX = dims.w; + moveY = dims.h; + break; + case 'center': + moveX = dims.w / 2; + moveY = dims.h / 2; + break; + } + var elemClip; + + var optionsParallel = MochiKit.Base.update({ + beforeStartInternal: function (effect) { + s.makePositioned(effect.effects[0].element); + elemClip = s.makeClipping(effect.effects[0].element); + }, + afterFinishInternal: function (effect) { + s.hideElement(effect.effects[0].element); + s.undoClipping(effect.effects[0].element, elemClip); + s.undoPositioned(effect.effects[0].element); + s.setStyle(effect.effects[0].element, oldStyle); + } + }, options); + + return new v.Parallel( + [new v.Opacity(element, { + sync: true, to: 0.0, from: 1.0, + transition: options.opacityTransition + }), + new v.Scale(element, /Opera/.test(navigator.userAgent) ? 1 : 0, { + scaleMode: {originalHeight: dims.h, originalWidth: dims.w}, + sync: true, transition: options.scaleTransition, + scaleContent: options.scaleContent, + scaleFromCenter: options.scaleFromCenter, + restoreAfterFinish: true + }), + new v.Move(element, { + x: moveX, y: moveY, sync: true, transition: options.moveTransition + }) + ], optionsParallel + ); +}; + +/** @id MochiKit.Visual.pulsate */ +MochiKit.Visual.pulsate = function (element, /* optional */ options) { + /*** + + Pulse an element between appear/fade. + + ***/ + var d = MochiKit.DOM; + var v = MochiKit.Visual; + var b = MochiKit.Base; + var oldOpacity = MochiKit.Style.getStyle(element, 'opacity'); + options = b.update({ + duration: 3.0, + from: 0, + afterFinishInternal: function (effect) { + MochiKit.Style.setStyle(effect.element, {'opacity': oldOpacity}); + } + }, options); + var transition = options.transition || v.Transitions.sinoidal; + options.transition = function (pos) { + return transition(1 - v.Transitions.pulse(pos, options.pulses)); + }; + return new v.Opacity(element, options); +}; + +/** @id MochiKit.Visual.fold */ +MochiKit.Visual.fold = function (element, /* optional */ options) { + /*** + + Fold an element, first vertically, then horizontally. + + ***/ + var d = MochiKit.DOM; + var v = MochiKit.Visual; + var s = MochiKit.Style; + element = d.getElement(element); + var elementDimensions = s.getElementDimensions(element, true); + var oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height + }; + var elemClip = s.makeClipping(element); + options = MochiKit.Base.update({ + scaleContent: false, + scaleX: false, + scaleMode: {originalHeight: elementDimensions.h, + originalWidth: elementDimensions.w}, + afterFinishInternal: function (effect) { + new v.Scale(element, 1, { + scaleContent: false, + scaleY: false, + scaleMode: {originalHeight: elementDimensions.h, + originalWidth: elementDimensions.w}, + afterFinishInternal: function (effect) { + s.hideElement(effect.element); + s.undoClipping(effect.element, elemClip); + s.setStyle(effect.element, oldStyle); + } + }); + } + }, options); + return new v.Scale(element, 5, options); +}; + + +// Compatibility with MochiKit 1.0 +MochiKit.Visual.Color = MochiKit.Color.Color; +MochiKit.Visual.getElementsComputedStyle = MochiKit.DOM.computedStyle; + +/* end of Rico adaptation */ + +MochiKit.Visual.__new__ = function () { + var m = MochiKit.Base; + + m.nameFunctions(this); + + this.EXPORT_TAGS = { + ":common": this.EXPORT, + ":all": m.concat(this.EXPORT, this.EXPORT_OK) + }; + +}; + +MochiKit.Visual.EXPORT = [ + "roundElement", + "roundClass", + "tagifyText", + "multiple", + "toggle", + "Parallel", + "Sequence", + "Opacity", + "Move", + "Scale", + "Highlight", + "ScrollTo", + "Morph", + "fade", + "appear", + "puff", + "blindUp", + "blindDown", + "switchOff", + "dropOut", + "shake", + "slideDown", + "slideUp", + "squish", + "grow", + "shrink", + "pulsate", + "fold" +]; + +MochiKit.Visual.EXPORT_OK = [ + "Base", + "PAIRS" +]; + +MochiKit.Visual.__new__(); + +MochiKit.Base._exportSymbols(this, MochiKit.Visual); diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/SimpleTest/test.css b/testing/mochitest/tests/MochiKit-1.4.2/tests/SimpleTest/test.css new file mode 100644 index 0000000000..9fc0a3f8d6 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/SimpleTest/test.css @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +@import url("../../../SimpleTest/test.css"); diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/mochitest.toml b/testing/mochitest/tests/MochiKit-1.4.2/tests/mochitest.toml new file mode 100644 index 0000000000..5869906263 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/mochitest.toml @@ -0,0 +1,45 @@ +[DEFAULT] +support-files = [ + "SimpleTest/test.css", + "test_Base.js", + "test_Color.js", + "test_DateTime.js", + "test_DragAndDrop.js", + "test_Format.js", + "test_Iter.js", + "test_Logging.js", + "test_MochiKit-Async.json", + "test_Signal.js"] + +['test_MochiKit-Async.html'] + +['test_MochiKit-Base.html'] + +['test_MochiKit-Color.html'] + +['test_MochiKit-DOM-Safari.html'] + +['test_MochiKit-DOM.html'] + +['test_MochiKit-DateTime.html'] + +['test_MochiKit-DragAndDrop.html'] + +['test_MochiKit-Format.html'] + +['test_MochiKit-Iter.html'] + +['test_MochiKit-JSAN.html'] +disabled='This test is broken: "Error: JSAN is not defined ... Line: 10" (And is removed in future MochiKit v1.5)' + +['test_MochiKit-Logging.html'] + +['test_MochiKit-MochiKit.html'] + +['test_MochiKit-Selector.html'] + +['test_MochiKit-Signal.html'] + +['test_MochiKit-Style.html'] + +['test_MochiKit-Visual.html'] diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Base.js b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Base.js new file mode 100644 index 0000000000..eb0c5f5779 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Base.js @@ -0,0 +1,577 @@ +if (typeof(dojo) != 'undefined') { dojo.require('MochiKit.Base'); } +if (typeof(JSAN) != 'undefined') { JSAN.use('MochiKit.Base'); } +if (typeof(tests) == 'undefined') { tests = {}; } + +tests.test_Base = function (t) { + // test bind + var not_self = {"toString": function () { return "not self"; } }; + var self = {"toString": function () { return "self"; } }; + var func = function (arg) { return this.toString() + " " + arg; }; + var boundFunc = bind(func, self); + not_self.boundFunc = boundFunc; + + t.is( boundFunc("foo"), "self foo", "boundFunc bound to self properly" ); + t.is( not_self.boundFunc("foo"), "self foo", "boundFunc bound to self on another obj" ); + t.is( bind(boundFunc, not_self)("foo"), "not self foo", "boundFunc successfully rebound!" ); + t.is( bind(boundFunc, undefined, "foo")(), "self foo", "boundFunc partial no self change" ); + t.is( bind(boundFunc, not_self, "foo")(), "not self foo", "boundFunc partial self change" ); + + // test method + not_self = {"toString": function () { return "not self"; } }; + self = {"toString": function () { return "self"; } }; + func = function (arg) { return this.toString() + " " + arg; }; + var boundMethod = method(self, func); + not_self.boundMethod = boundMethod; + + t.is( boundMethod("foo"), "self foo", "boundMethod bound to self properly" ); + t.is( not_self.boundMethod("foo"), "self foo", "boundMethod bound to self on another obj" ); + t.is( method(not_self, boundMethod)("foo"), "not self foo", "boundMethod successfully rebound!" ); + t.is( method(undefined, boundMethod, "foo")(), "self foo", "boundMethod partial no self change" ); + t.is( method(not_self, boundMethod, "foo")(), "not self foo", "boundMethod partial self change" ); + + // test bindLate + self = {"toString": function () { return "self"; } }; + boundFunc = bindLate("toString", self); + t.is( boundFunc(), "self", "bindLate binds properly" ); + self.toString = function () { return "not self"; }; + t.is( boundFunc(), "not self", "bindLate late function lookup" ); + func = function (arg) { return this.toString() + " " + arg; }; + boundFunc = bindLate(func, self); + t.is( boundFunc("foo"), "not self foo", "bindLate fallback to standard bind" ); + + // test bindMethods + + var O = function (value) { + bindMethods(this); + this.value = value; + }; + O.prototype.func = function () { + return this.value; + }; + + var o = new O("boring"); + var p = {}; + p.func = o.func; + var func = o.func; + t.is( o.func(), "boring", "bindMethods doesn't break shit" ); + t.is( p.func(), "boring", "bindMethods works on other objects" ); + t.is( func(), "boring", "bindMethods works on functions" ); + + var p = clone(o); + t.ok( p instanceof O, "cloned correct inheritance" ); + var q = clone(p); + t.ok( q instanceof O, "clone-cloned correct inheritance" ); + q.foo = "bar"; + t.is( p.foo, undefined, "clone-clone is copy-on-write" ); + p.bar = "foo"; + t.is( o.bar, undefined, "clone is copy-on-write" ); + t.is( q.bar, "foo", "clone-clone has proper delegation" ); + // unbind + p.func = bind(p.func, null); + t.is( p.func(), "boring", "clone function calls correct" ); + q.value = "awesome"; + t.is( q.func(), "awesome", "clone really does work" ); + + // test boring boolean funcs + t.is( isNull(null), true, "isNull matches null" ); + t.is( isNull(undefined), false, "isNull doesn't match undefined" ); + t.is( isNull({}), false, "isNull doesn't match objects" ); + + t.is( isCallable(isCallable), true, "isCallable returns true on itself" ); + t.is( isCallable(1), false, "isCallable returns false on numbers" ); + + t.is( isUndefined(null), false, "null is not undefined" ); + t.is( isUndefined(""), false, "empty string is not undefined" ); + t.is( isUndefined(undefined), true, "undefined is undefined" ); + t.is( isUndefined({}.foo), true, "missing property is undefined" ); + + t.is( isUndefinedOrNull(null), true, "null is undefined or null" ); + t.is( isUndefinedOrNull(""), false, "empty string is not undefined or null" ); + t.is( isUndefinedOrNull(undefined), true, "undefined is undefined or null" ); + t.is( isUndefinedOrNull({}.foo), true, "missing property is undefined or null" ); + + t.is( isEmpty(null), true, "isEmpty null" ); + t.is( isEmpty([], [], ""), true, "isEmpty true" ); + t.is( isEmpty([], [1], ""), true, "isEmpty true" ); + t.is( isEmpty([1], [1], "1"), false, "isEmpty false" ); + t.is( isEmpty([1], [1], "1"), false, "isEmpty false" ); + + t.is( isNotEmpty(null), false, "isNotEmpty null" ); + t.is( isNotEmpty([], [], ""), false, "isNotEmpty false" ); + t.is( isNotEmpty([], [1], ""), false, "isNotEmpty false" ); + t.is( isNotEmpty([1], [1], "1"), true, "isNotEmpty true" ); + t.is( isNotEmpty([1], [1], "1"), true, "isNotEmpty true" ); + + t.is( isArrayLike(undefined), false, "isArrayLike(undefined)" ); + t.is( isArrayLike(null), false, "isArrayLike(null)" ); + t.is( isArrayLike([]), true, "isArrayLike([])" ); + + // test extension of arrays + var a = []; + var b = []; + var three = [1, 2, 3]; + + extend(a, three, 1); + t.ok( objEqual(a, [2, 3]), "extend to an empty array" ); + extend(a, three, 1) + t.ok( objEqual(a, [2, 3, 2, 3]), "extend to a non-empty array" ); + + extend(b, three); + t.ok( objEqual(b, three), "extend of an empty array" ); + + var c1 = extend(null, three); + t.ok( objEqual(c1, three), "extend null" ); + var c2 = extend(undefined, three); + t.ok( objEqual(c2, three), "extend undefined" ); + + + t.is( compare(1, 2), -1, "numbers compare lt" ); + t.is( compare(2, 1), 1, "numbers compare gt" ); + t.is( compare(1, 1), 0, "numbers compare eq" ); + t.is( compare([1], [1]), 0, "arrays compare eq" ); + t.is( compare([1], [1, 2]), -1, "arrays compare lt (length)" ); + t.is( compare([1, 2], [2, 1]), -1, "arrays compare lt (contents)" ); + t.is( compare([1, 2], [1]), 1, "arrays compare gt (length)" ); + t.is( compare([2, 1], [1, 1]), 1, "arrays compare gt (contents)" ); + + // test partial application + var a = []; + var func = function (a, b) { + if (arguments.length != 2) { + return "bad args"; + } else { + return this.value + a + b; + } + }; + var self = {"value": 1, "func": func}; + var self2 = {"value": 2}; + t.is( self.func(2, 3), 6, "setup for test is correct" ); + self.funcTwo = partial(self.func, 2); + t.is( self.funcTwo(3), 6, "partial application works" ); + t.is( self.funcTwo(3), 6, "partial application works still" ); + t.is( bind(self.funcTwo, self2)(3), 7, "rebinding partial works" ); + self.funcTwo = bind(bind(self.funcTwo, self2), null); + t.is( self.funcTwo(3), 6, "re-unbinding partial application works" ); + + + // nodeWalk test + // ... looks a lot like a DOM tree on purpose + var tree = { + "id": "nodeWalkTestTree", + "test:int": "1", + "childNodes": [ + { + "test:int": "2", + "childNodes": [ + {"test:int": "5"}, + "ignored string", + {"ignored": "object"}, + ["ignored", "list"], + { + "test:skipchildren": "1", + "childNodes": [{"test:int": 6}] + } + ] + }, + {"test:int": "3"}, + {"test:int": "4"} + ] + } + + var visitedNodes = []; + nodeWalk(tree, function (node) { + var attr = node["test:int"]; + if (attr) { + visitedNodes.push(attr); + } + if (node["test:skipchildren"]) { + return; + } + return node.childNodes; + }); + + t.ok( objEqual(visitedNodes, ["1", "2", "3", "4", "5"]), "nodeWalk looks like it works"); + + // test map + var minusOne = function (x) { return x - 1; }; + var res = map(minusOne, [1, 2, 3]); + t.ok( objEqual(res, [0, 1, 2]), "map works" ); + + var res2 = xmap(minusOne, 1, 2, 3); + t.ok( objEqual(res2, res), "xmap works" ); + + res = map(operator.add, [1, 2, 3], [2, 4, 6]); + t.ok( objEqual(res, [3, 6, 9]), "map(fn, p, q) works" ); + + res = map(operator.add, [1, 2, 3], [2, 4, 6, 8]); + t.ok( objEqual(res, [3, 6, 9]), "map(fn, p, q) works (q long)" ); + + res = map(operator.add, [1, 2, 3, 4], [2, 4, 6]); + t.ok( objEqual(res, [3, 6, 9]), "map(fn, p, q) works (p long)" ); + + res = map(null, [1, 2, 3], [2, 4, 6]); + t.ok( objEqual(res, [[1, 2], [2, 4], [3, 6]]), "map(null, p, q) works" ); + + res = zip([1, 2, 3], [2, 4, 6]); + t.ok( objEqual(res, [[1, 2], [2, 4], [3, 6]]), "zip(p, q) works" ); + + res = map(null, [1, 2, 3]); + t.ok( objEqual(res, [1, 2, 3]), "map(null, lst) works" ); + + + + + t.is( isNotEmpty("foo"), true, "3 char string is not empty" ); + t.is( isNotEmpty(""), false, "0 char string is empty" ); + t.is( isNotEmpty([1, 2, 3]), true, "3 element list is not empty" ); + t.is( isNotEmpty([]), false, "0 element list is empty" ); + + // test filter + var greaterThanThis = function (x) { return x > this; }; + var greaterThanOne = function (x) { return x > 1; }; + var res = filter(greaterThanOne, [-1, 0, 1, 2, 3]); + t.ok( objEqual(res, [2, 3]), "filter works" ); + var res = filter(greaterThanThis, [-1, 0, 1, 2, 3], 1); + t.ok( objEqual(res, [2, 3]), "filter self works" ); + var res2 = xfilter(greaterThanOne, -1, 0, 1, 2, 3); + t.ok( objEqual(res2, res), "xfilter works" ); + + t.is(objMax(1, 2, 9, 12, 42, -16, 16), 42, "objMax works (with numbers)"); + t.is(objMin(1, 2, 9, 12, 42, -16, 16), -16, "objMin works (with numbers)"); + + // test adapter registry + + var R = new AdapterRegistry(); + R.register("callable", isCallable, function () { return "callable"; }); + R.register("arrayLike", isArrayLike, function () { return "arrayLike"; }); + t.is( R.match(function () {}), "callable", "registry found callable" ); + t.is( R.match([]), "arrayLike", "registry found ArrayLike" ); + try { + R.match(null); + t.ok( false, "non-matching didn't raise!" ); + } catch (e) { + t.is( e, NotFound, "non-matching raised correctly" ); + } + R.register("undefinedOrNull", isUndefinedOrNull, function () { return "undefinedOrNull" }); + R.register("undefined", isUndefined, function () { return "undefined" }); + t.is( R.match(undefined), "undefinedOrNull", "priorities are as documented" ); + t.ok( R.unregister("undefinedOrNull"), "removed adapter" ); + t.is( R.match(undefined), "undefined", "adapter was removed" ); + R.register("undefinedOrNull", isUndefinedOrNull, function () { return "undefinedOrNull" }, true); + t.is( R.match(undefined), "undefinedOrNull", "override works" ); + + var a1 = {"a": 1, "b": 2, "c": 2}; + var a2 = {"a": 2, "b": 1, "c": 2}; + t.is( keyComparator("a")(a1, a2), -1, "keyComparator 1 lt" ); + t.is( keyComparator("c")(a1, a2), 0, "keyComparator 1 eq" ); + t.is( keyComparator("c", "b")(a1, a2), 1, "keyComparator 2 eq gt" ); + t.is( keyComparator("c", "a")(a1, a2), -1, "keyComparator 2 eq lt" ); + t.is( reverseKeyComparator("a")(a1, a2), 1, "reverseKeyComparator" ); + t.is( compare(concat([1], [2], [3]), [1, 2, 3]), 0, "concat" ); + t.is( repr("foo"), '"foo"', "string repr" ); + t.is( repr(1), '1', "number repr" ); + t.is( listMin([1, 3, 5, 3, -1]), -1, "listMin" ); + t.is( objMin(1, 3, 5, 3, -1), -1, "objMin" ); + t.is( listMax([1, 3, 5, 3, -1]), 5, "listMax" ); + t.is( objMax(1, 3, 5, 3, -1), 5, "objMax" ); + + var v = keys(a1); + v.sort(); + t.is( compare(v, ["a", "b", "c"]), 0, "keys" ); + v = items(a1); + v.sort(); + t.is( compare(v, [["a", 1], ["b", 2], ["c", 2]]), 0, "items" ); + + var StringMap = function() {}; + a = new StringMap(); + a.foo = "bar"; + b = new StringMap(); + b.foo = "bar"; + try { + compare(a, b); + t.ok( false, "bad comparison registered!?" ); + } catch (e) { + t.ok( e instanceof TypeError, "bad comparison raised TypeError" ); + } + + t.is( repr(a), "[object Object]", "default repr for StringMap" ); + var isStringMap = function () { + for (var i = 0; i < arguments.length; i++) { + if (!(arguments[i] instanceof StringMap)) { + return false; + } + } + return true; + }; + + registerRepr("stringMap", + isStringMap, + function (obj) { + return "StringMap(" + repr(items(obj)) + ")"; + } + ); + + t.is( repr(a), 'StringMap([["foo", "bar"]])', "repr worked" ); + + // not public API + MochiKit.Base.reprRegistry.unregister("stringMap"); + + t.is( repr(a), "[object Object]", "default repr for StringMap" ); + + registerComparator("stringMap", + isStringMap, + function (a, b) { + // no sorted(...) in base + a = items(a); + b = items(b); + a.sort(compare); + b.sort(compare); + return compare(a, b); + } + ); + + t.is( compare(a, b), 0, "registerComparator" ); + + update(a, {"foo": "bar"}, {"wibble": "baz"}, undefined, null, {"grr": 1}); + t.is( a.foo, "bar", "update worked (first obj)" ); + t.is( a.wibble, "baz", "update worked (second obj)" ); + t.is( a.grr, 1, "update worked (skipped undefined and null)" ); + t.is( compare(a, b), 1, "update worked (comparison)" ); + + + setdefault(a, {"foo": "unf"}, {"bar": "web taco"} ); + t.is( a.foo, "bar", "setdefault worked (skipped existing)" ); + t.is( a.bar, "web taco", "setdefault worked (set non-existing)" ); + + a = null; + a = setdefault(null, {"foo": "bar"}); + t.is( a.foo, "bar", "setdefault worked (self is null)" ); + + a = null; + a = setdefault(undefined, {"foo": "bar"}); + t.is( a.foo, "bar", "setdefault worked (self is undefined)" ); + + a = null; + a = update(null, {"foo": "bar"}, {"wibble": "baz"}, undefined, null, {"grr": 1}); + t.is( a.foo, "bar", "update worked (self is null, first obj)" ); + t.is( a.wibble, "baz", "update worked (self is null, second obj)" ); + t.is( a.grr, 1, "update worked (self is null, skipped undefined and null)" ); + + a = null; + a = update(undefined, {"foo": "bar"}, {"wibble": "baz"}, undefined, null, {"grr": 1}); + t.is( a.foo, "bar", "update worked (self is undefined, first obj)" ); + t.is( a.wibble, "baz", "update worked (self is undefined, second obj)" ); + t.is( a.grr, 1, "update worked (self is undefined, skipped undefined and null)" ); + + + var c = items(merge({"foo": "bar"}, {"wibble": "baz"})); + c.sort(compare); + t.is( compare(c, [["foo", "bar"], ["wibble", "baz"]]), 0, "merge worked" ); + + // not public API + MochiKit.Base.comparatorRegistry.unregister("stringMap"); + + try { + compare(a, b); + t.ok( false, "bad comparison registered!?" ); + } catch (e) { + t.ok( e instanceof TypeError, "bad comparison raised TypeError" ); + } + + var o = {"__repr__": function () { return "__repr__"; }}; + t.is( repr(o), "__repr__", "__repr__ protocol" ); + t.is( repr(MochiKit.Base), MochiKit.Base.__repr__(), "__repr__ protocol when repr is defined" ); + var o = {"NAME": "NAME"}; + t.is( repr(o), "NAME", "NAME protocol (obj)" ); + o = function () { return "TACO" }; + o.NAME = "NAME"; + t.is( repr(o), "NAME", "NAME protocol (func)" ); + + t.is( repr(MochiKit.Base.nameFunctions), "MochiKit.Base.nameFunctions", "test nameFunctions" ); + // Done! + + t.is( urlEncode("1+2=2").toUpperCase(), "1%2B2%3D2", "urlEncode" ); + t.is( queryString(["a", "b"], [1, "two"]), "a=1&b=two", "queryString"); + t.is( queryString({"a": 1}), "a=1", "one item alternate form queryString" ); + var o = {"a": 1, "b": 2, "c": function() {}}; + var res = queryString(o).split("&"); + res.sort(); + t.is( res.join("&"), "a=1&b=2", "two item alternate form queryString, function skip" ); + var res = parseQueryString("1+1=2&b=3%3D2"); + t.is( res["1 1"], "2", "parseQueryString pathological name" ); + t.is( res.b, "3=2", "parseQueryString second name:value pair" ); + var res = parseQueryString("foo=one&foo=two", true); + t.is( res["foo"].join(" "), "one two", "parseQueryString useArrays" ); + var res = parseQueryString("?foo=2&bar=1"); + t.is( res["foo"], "2", "parseQueryString strip leading question mark"); + + var res = parseQueryString("x=1&y=2"); + t.is( typeof(res['&']), "undefined", "extra cruft in parseQueryString output"); + + t.is( serializeJSON("foo\n\r\b\f\t\u000B\u001B"), "\"foo\\n\\r\\b\\f\\t\\u000B\\u001B\"", "string JSON" ); + t.is( serializeJSON(null), "null", "null JSON"); + try { + serializeJSON(undefined); + t.ok(false, "undefined should not be serializable"); + } catch (e) { + t.ok(e instanceof TypeError, "undefined not serializable"); + } + t.is( serializeJSON(1), "1", "1 JSON"); + t.is( serializeJSON(1.23), "1.23", "1.23 JSON"); + t.is( serializeJSON(serializeJSON), null, "function JSON (null, not string)" ); + t.is( serializeJSON([1, "2", 3.3]), "[1, \"2\", 3.3]", "array JSON" ); + var res = evalJSON(serializeJSON({"a":1, "b":2})); + t.is( res.a, 1, "evalJSON on an object (1)" ); + t.is( res.b, 2, "evalJSON on an object (2)" ); + var res = {"a": 1, "b": 2, "json": function () { return this; }}; + var res = evalJSON(serializeJSON(res)); + t.is( res.a, 1, "evalJSON on an object that jsons self (1)" ); + t.is( res.b, 2, "evalJSON on an object that jsons self (2)" ); + var strJSON = {"a": 1, "b": 2, "json": function () { return "json"; }}; + t.is( serializeJSON(strJSON), "\"json\"", "json serialization calling" ); + t.is( serializeJSON([strJSON]), "[\"json\"]", "json serialization calling in a structure" ); + t.is( evalJSON('/* {"result": 1} */').result, 1, "json comment stripping" ); + t.is( evalJSON('/* {"*/ /*": 1} */')['*/ /*'], 1, "json comment stripping" ); + registerJSON("isDateLike", + isDateLike, + function (d) { + return "this was a date"; + } + ); + t.is( serializeJSON(new Date()), "\"this was a date\"", "json registry" ); + MochiKit.Base.jsonRegistry.unregister("isDateLike"); + + var a = {"foo": {"bar": 12, "wibble": 13}}; + var b = {"foo": {"baz": 4, "bar": 16}, "bar": 4}; + updatetree(a, b); + var expect = [["bar", 16], ["baz", 4], ["wibble", 13]]; + var got = items(a.foo); + got.sort(compare); + t.is( repr(got), repr(expect), "updatetree merge" ); + t.is( a.bar, 4, "updatetree insert" ); + + var aa = {"foo": {"bar": 12, "wibble": 13}}; + var bb = {"foo": {"baz": 4, "bar": 16}, "bar": 4}; + + cc = updatetree(null, aa, bb); + got = items(cc.foo); + got.sort(compare); + t.is( repr(got), repr(expect), "updatetree merge (self is null)" ); + t.is( cc.bar, 4, "updatetree insert (self is null)" ); + + cc = updatetree(undefined, aa, bb); + got = items(cc.foo); + got.sort(compare); + t.is( repr(got), repr(expect), "updatetree merge (self is undefined)" ); + t.is( cc.bar, 4, "updatetree insert (self is undefined)" ); + + var c = counter(); + t.is( c(), 1, "counter starts at 1" ); + t.is( c(), 2, "counter increases" ); + c = counter(2); + t.is( c(), 2, "counter starts at 2" ); + t.is( c(), 3, "counter increases" ); + + t.is( findValue([1, 2, 3], 4), -1, "findValue returns -1 on not found"); + t.is( findValue([1, 2, 3], 1), 0, "findValue returns correct index"); + t.is( findValue([1, 2, 3], 1, 1), -1, "findValue honors start"); + t.is( findValue([1, 2, 3], 2, 0, 1), -1, "findValue honors end"); + t.is( findIdentical([1, 2, 3], 4), -1, "findIdentical returns -1"); + t.is( findIdentical([1, 2, 3], 1), 0, "findIdentical returns correct index"); + t.is( findIdentical([1, 2, 3], 1, 1), -1, "findIdentical honors start"); + t.is( findIdentical([1, 2, 3], 2, 0, 1), -1, "findIdentical honors end"); + + var flat = flattenArguments(1, "2", 3, [4, [5, [6, 7], 8, [], 9]]); + var expect = [1, "2", 3, 4, 5, 6, 7, 8, 9]; + t.is( repr(flat), repr(expect), "flattenArguments" ); + + var fn = function () { + return [this, concat(arguments)]; + } + t.is( methodcaller("toLowerCase")("FOO"), "foo", "methodcaller with a method name" ); + t.is( repr(methodcaller(fn, 2, 3)(1)), "[1, [2, 3]]", "methodcaller with a function" ); + + var f1 = function (x) { return [1, x]; }; + var f2 = function (x) { return [2, x]; }; + var f3 = function (x) { return [3, x]; }; + t.is( repr(f1(f2(f3(4)))), "[1, [2, [3, 4]]]", "test the compose test" ); + t.is( repr(compose(f1,f2,f3)(4)), "[1, [2, [3, 4]]]", "three fn composition works" ); + t.is( repr(compose(compose(f1,f2),f3)(4)), "[1, [2, [3, 4]]]", "associative left" ); + t.is( repr(compose(f1,compose(f2,f3))(4)), "[1, [2, [3, 4]]]", "associative right" ); + + try { + compose(f1, "foo"); + t.ok( false, "wrong compose argument not raised!" ); + } catch (e) { + t.is( e.name, 'TypeError', "wrong compose argument raised correctly" ); + } + + t.is(camelize('one'), 'one', 'one word'); + t.is(camelize('one-two'), 'oneTwo', 'two words'); + t.is(camelize('one-two-three'), 'oneTwoThree', 'three words'); + t.is(camelize('1-one'), '1One', 'letter and word'); + t.is(camelize('one-'), 'one', 'trailing hyphen'); + t.is(camelize('-one'), 'One', 'starting hyphen'); + t.is(camelize('o-two'), 'oTwo', 'one character and word'); + + var flat = flattenArray([1, "2", 3, [4, [5, [6, 7], 8, [], 9]]]); + var expect = [1, "2", 3, 4, 5, 6, 7, 8, 9]; + t.is( repr(flat), repr(expect), "flattenArray" ); + + /* mean */ + try { + mean(); + t.ok( false, "mean no arguments didn't raise!" ); + } catch (e) { + t.is( e.name, 'TypeError', "no arguments raised correctly" ); + } + t.is( mean(1), 1, 'single argument (arg list)'); + t.is( mean([1]), 1, 'single argument (array)'); + t.is( mean(1,2,3), 2, 'three arguments (arg list)'); + t.is( mean([1,2,3]), 2, 'three arguments (array)'); + t.is( average(1), 1, 'test the average alias'); + + /* median */ + try { + median(); + t.ok( false, "median no arguments didn't raise!" ); + } catch (e) { + t.is( e.name, 'TypeError', "no arguments raised correctly" ); + } + t.is( median(1), 1, 'single argument (arg list)'); + t.is( median([1]), 1, 'single argument (array)'); + t.is( median(3,1,2), 2, 'three arguments (arg list)'); + t.is( median([3,1,2]), 2, 'three arguments (array)'); + t.is( median(3,1,2,4), 2.5, 'four arguments (arg list)'); + t.is( median([3,1,2,4]), 2.5, 'four arguments (array)'); + + /* #185 */ + t.is( serializeJSON(parseQueryString("")), "{}", "parseQueryString('')" ); + t.is( serializeJSON(parseQueryString("", true)), "{}", "parseQueryString('', true)" ); + + /* #109 */ + t.is( queryString({ids: [1,2,3]}), "ids=1&ids=2&ids=3", "queryString array value" ); + t.is( queryString({ids: "123"}), "ids=123", "queryString string value" ); + + /* test values */ + var o = {a: 1, b: 2, c: 4, d: -1}; + var got = values(o); + got.sort(); + t.is( repr(got), repr([-1, 1, 2, 4]), "values()" ); + + t.is( queryString([["foo", "bar"], ["baz", "wibble"]]), "foo=baz&bar=wibble" ); + o = parseQueryString("foo=1=1=1&bar=2&baz&wibble="); + t.is( o.foo, "1=1=1", "parseQueryString multiple = first" ); + t.is( o.bar, "2", "parseQueryString multiple = second" ); + t.is( o.baz, "", "parseQueryString multiple = third" ); + t.is( o.wibble, "", "parseQueryString multiple = fourth" ); + + /* queryString with null values */ + t.is( queryString(["a", "b"], [1, null]), "a=1", "queryString with null value" ); + t.is( queryString({"a": 1, "b": null}), "a=1", "queryString with null value" ); + + var reprFunc = function (a, b) { + return; + } + t.is( repr(reprFunc), "function (a, b) {...}", "repr of function" ); +}; diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Color.js b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Color.js new file mode 100644 index 0000000000..c736c8db34 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Color.js @@ -0,0 +1,137 @@ +if (typeof(dojo) != 'undefined') { dojo.require('MochiKit.Color'); } +if (typeof(JSAN) != 'undefined') { JSAN.use('MochiKit.Color'); } +if (typeof(tests) == 'undefined') { tests = {}; } + +tests.test_Color = function (t) { + var approx = function (a, b, msg) { + return t.is(a.toPrecision(4), b.toPrecision(4), msg); + }; + + t.is( Color.whiteColor().toHexString(), "#ffffff", "whiteColor has right hex" ); + t.is( Color.blackColor().toHexString(), "#000000", "blackColor has right hex" ); + t.is( Color.blueColor().toHexString(), "#0000ff", "blueColor has right hex" ); + t.is( Color.redColor().toHexString(), "#ff0000", "redColor has right hex" ); + t.is( Color.greenColor().toHexString(), "#00ff00", "greenColor has right hex" ); + t.is( compare(Color.whiteColor(), Color.whiteColor()), 0, "default colors compare right" ); + t.ok( Color.whiteColor() == Color.whiteColor(), "default colors are interned" ); + t.is( Color.whiteColor().toRGBString(), "rgb(255,255,255)", "toRGBString white" ); + t.is( Color.blueColor().toRGBString(), "rgb(0,0,255)", "toRGBString blue" ); + t.is( Color.fromRGB(190/255, 222/255, 173/255).toHexString(), "#bedead", "fromRGB works" ); + t.is( Color.fromRGB(226/255, 15.9/255, 182/255).toHexString(), "#e210b6", "fromRGB < 16 works" ); + t.is( Color.fromRGB({r:190/255,g:222/255,b:173/255}).toHexString(), "#bedead", "alt fromRGB works" ); + t.is( Color.fromHexString("#bedead").toHexString(), "#bedead", "round-trip hex" ); + t.is( Color.fromString("#bedead").toHexString(), "#bedead", "round-trip string(hex)" ); + t.is( Color.fromRGBString("rgb(190,222,173)").toHexString(), "#bedead", "round-trip rgb" ); + t.is( Color.fromString("rgb(190,222,173)").toHexString(), "#bedead", "round-trip rgb" ); + + var hsl = Color.redColor().asHSL(); + approx( hsl.h, 0.0, "red hsl.h" ); + approx( hsl.s, 1.0, "red hsl.s" ); + approx( hsl.l, 0.5, "red hsl.l" ); + hsl = Color.fromRGB(0, 0, 0.5).asHSL(); + approx( hsl.h, 2/3, "darkblue hsl.h" ); + approx( hsl.s, 1.0, "darkblue hsl.s" ); + approx( hsl.l, 0.25, "darkblue hsl.l" ); + hsl = Color.fromString("#4169E1").asHSL(); + approx( hsl.h, (5/8), "4169e1 h"); + approx( hsl.s, (8/11), "4169e1 s"); + approx( hsl.l, (29/51), "4169e1 l"); + hsl = Color.fromString("#555544").asHSL(); + approx( hsl.h, (1/6), "555544 h" ); + approx( hsl.s, (1/9), "555544 s" ); + approx( hsl.l, (3/10), "555544 l" ); + hsl = Color.fromRGB(0.5, 1, 0.5).asHSL(); + approx( hsl.h, 1/3, "aqua hsl.h" ); + approx( hsl.s, 1.0, "aqua hsl.s" ); + approx( hsl.l, 0.75, "aqua hsl.l" ); + t.is( + Color.fromHSL(hsl.h, hsl.s, hsl.l).toHexString(), + Color.fromRGB(0.5, 1, 0.5).toHexString(), + "fromHSL works with components" + ); + t.is( + Color.fromHSL(hsl).toHexString(), + Color.fromRGB(0.5, 1, 0.5).toHexString(), + "fromHSL alt form" + ); + t.is( + Color.fromString("hsl(120,100%,75%)").toHexString(), + "#80ff80", + "fromHSLString" + ); + t.is( + Color.fromRGB(0.5, 1, 0.5).toHSLString(), + "hsl(120,100.0%,75.00%)", + "toHSLString" + ); + t.is( Color.fromHSL(0, 0, 0).toHexString(), "#000000", "fromHSL to black" ); + hsl = Color.blackColor().asHSL(); + approx( hsl.h, 0.0, "black hsl.h" ); + approx( hsl.s, 0.0, "black hsl.s" ); + approx( hsl.l, 0.0, "black hsl.l" ); + hsl.h = 1.0; + hsl = Color.blackColor().asHSL(); + approx( hsl.h, 0.0, "asHSL returns copy" ); + var rgb = Color.brownColor().asRGB(); + approx( rgb.r, 153/255, "brown rgb.r" ); + approx( rgb.g, 102/255, "brown rgb.g" ); + approx( rgb.b, 51/255, "brown rgb.b" ); + rgb.r = 0; + rgb = Color.brownColor().asRGB(); + approx( rgb.r, 153/255, "asRGB returns copy" ); + + t.is( Color.fromName("aqua").toHexString(), "#00ffff", "aqua fromName" ); + t.is( Color.fromString("aqua").toHexString(), "#00ffff", "aqua fromString" ); + t.is( Color.fromName("transparent"), Color.transparentColor(), "transparent fromName" ); + t.is( Color.fromString("transparent"), Color.transparentColor(), "transparent fromString" ); + t.is( Color.transparentColor().toRGBString(), "rgba(0,0,0,0)", "transparent toRGBString" ); + t.is( Color.fromRGBString("rgba( 0, 255, 255, 50%)").asRGB().a, 0.5, "rgba parsing alpha correctly" ); + t.is( Color.fromRGBString("rgba( 0, 255, 255, 50%)").toRGBString(), "rgba(0,255,255,0.5)", "rgba output correctly" ); + t.is( Color.fromRGBString("rgba( 0, 255, 255, 1)").toHexString(), "#00ffff", "fromRGBString with spaces and alpha" ); + t.is( Color.fromRGBString("rgb( 0, 255, 255)").toHexString(), "#00ffff", "fromRGBString with spaces" ); + t.is( Color.fromRGBString("rgb( 0, 100%, 255)").toHexString(), "#00ffff", "fromRGBString with percents" ); + + var hsv = Color.redColor().asHSV(); + approx( hsv.h, 0.0, "red hsv.h" ); + approx( hsv.s, 1.0, "red hsv.s" ); + approx( hsv.v, 1.0, "red hsv.v" ); + t.is( Color.fromHSV(hsv).toHexString(), Color.redColor().toHexString(), "red hexstring" ); + hsv = Color.fromRGB(0, 0, 0.5).asHSV(); + approx( hsv.h, 2/3, "darkblue hsv.h" ); + approx( hsv.s, 1.0, "darkblue hsv.s" ); + approx( hsv.v, 0.5, "darkblue hsv.v" ); + t.is( Color.fromHSV(hsv).toHexString(), Color.fromRGB(0, 0, 0.5).toHexString(), "darkblue hexstring" ); + hsv = Color.fromString("#4169E1").asHSV(); + approx( hsv.h, 5/8, "4169e1 h"); + approx( hsv.s, 32/45, "4169e1 s"); + approx( hsv.v, 15/17, "4169e1 l"); + t.is( Color.fromHSV(hsv).toHexString(), "#4169e1", "4169e1 hexstring" ); + hsv = Color.fromString("#555544").asHSV(); + approx( hsv.h, 1/6, "555544 h" ); + approx( hsv.s, 1/5, "555544 s" ); + approx( hsv.v, 1/3, "555544 l" ); + t.is( Color.fromHSV(hsv).toHexString(), "#555544", "555544 hexstring" ); + hsv = Color.fromRGB(0.5, 1, 0.5).asHSV(); + approx( hsv.h, 1/3, "aqua hsv.h" ); + approx( hsv.s, 0.5, "aqua hsv.s" ); + approx( hsv.v, 1, "aqua hsv.v" ); + t.is( + Color.fromHSV(hsv.h, hsv.s, hsv.v).toHexString(), + Color.fromRGB(0.5, 1, 0.5).toHexString(), + "fromHSV works with components" + ); + t.is( + Color.fromHSV(hsv).toHexString(), + Color.fromRGB(0.5, 1, 0.5).toHexString(), + "fromHSV alt form" + ); + hsv = Color.fromRGB(1, 1, 1).asHSV() + approx( hsv.h, 0, 'white hsv.h' ); + approx( hsv.s, 0, 'white hsv.s' ); + approx( hsv.v, 1, 'white hsv.v' ); + t.is( + Color.fromHSV(0, 0, 1).toHexString(), + '#ffffff', + 'HSV saturation' + ); +}; diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_DateTime.js b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_DateTime.js new file mode 100644 index 0000000000..a7178086d1 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_DateTime.js @@ -0,0 +1,52 @@ +if (typeof(dojo) != 'undefined') { dojo.require('MochiKit.DateTime'); } +if (typeof(JSAN) != 'undefined') { JSAN.use('MochiKit.DateTime'); } +if (typeof(tests) == 'undefined') { tests = {}; } + +tests.test_DateTime = function (t) { + var testDate = isoDate('2005-2-3'); + t.is(testDate.getFullYear(), 2005, "isoDate year ok"); + t.is(testDate.getDate(), 3, "isoDate day ok"); + t.is(testDate.getMonth(), 1, "isoDate month ok"); + t.ok(objEqual(testDate, new Date("February 3, 2005")), "matches string date"); + t.is(toISODate(testDate), '2005-02-03', 'toISODate ok'); + + var testDate = isoDate('2005-06-08'); + t.is(testDate.getFullYear(), 2005, "isoDate year ok"); + t.is(testDate.getDate(), 8, "isoDate day ok"); + t.is(testDate.getMonth(), 5, "isoDate month ok"); + t.ok(objEqual(testDate, new Date("June 8, 2005")), "matches string date"); + t.is(toISODate(testDate), '2005-06-08', 'toISODate ok'); + + var testDate = isoDate('0500-12-12'); + t.is(testDate.getFullYear(), 500, 'isoDate year ok for year < 1000'); + t.is(testDate.getDate(), 12, 'isoDate day ok for year < 1000'); + t.is(testDate.getMonth(), 11, 'isoDate month ok for year < 1000'); + t.ok(objEqual(testDate, new Date("December 12, 0500")), "matches string date for year < 1000"); + t.is(toISODate(testDate), '0500-12-12', 'toISODate ok for year < 1000'); + + t.is(compare(new Date("February 3, 2005"), new Date(2005, 1, 3)), 0, "dates compare eq"); + t.is(compare(new Date("February 3, 2005"), new Date(2005, 2, 3)), -1, "dates compare lt"); + t.is(compare(new Date("February 3, 2005"), new Date(2005, 0, 3)), 1, "dates compare gt"); + + var testDate = isoDate('2005-2-3'); + t.is(compare(americanDate('2/3/2005'), testDate), 0, "americanDate eq"); + t.is(compare('2/3/2005', toAmericanDate(testDate)), 0, "toAmericanDate eq"); + + var testTimestamp = isoTimestamp('2005-2-3 22:01:03'); + t.is(compare(testTimestamp, new Date(2005,1,3,22,1,3)), 0, "isoTimestamp eq"); + t.is(compare(testTimestamp, isoTimestamp('2005-2-3T22:01:03')), 0, "isoTimestamp (real ISO) eq"); + t.is(compare(toISOTimestamp(testTimestamp), '2005-02-03 22:01:03'), 0, "toISOTimestamp eq"); + testTimestamp = isoTimestamp('2005-2-3T22:01:03Z'); + t.is(toISOTimestamp(testTimestamp, true), '2005-02-03T22:01:03Z', "toISOTimestamp (real ISO) eq"); + + var localTZ = Math.round((new Date(2005,1,3,22,1,3)).getTimezoneOffset()/60) + var direction = (localTZ < 0) ? "+" : "-"; + localTZ = Math.abs(localTZ); + localTZ = direction + ((localTZ < 10) ? "0" : "") + localTZ; + testTimestamp = isoTimestamp("2005-2-3T22:01:03" + localTZ); + var testDateTimestamp = new Date(2005,1,3,22,1,3); + t.is(compare(testTimestamp, testDateTimestamp), 0, "equal with local tz"); + testTimestamp = isoTimestamp("2005-2-3T17:01:03-05"); + var testDateTimestamp = new Date(Date.UTC(2005,1,3,22,1,3)); + t.is(compare(testTimestamp, testDateTimestamp), 0, "equal with specific tz"); +}; diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_DragAndDrop.js b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_DragAndDrop.js new file mode 100644 index 0000000000..d3a3c58379 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_DragAndDrop.js @@ -0,0 +1,30 @@ +if (typeof(dojo) != 'undefined') { dojo.require('MochiKit.Signal'); } +if (typeof(JSAN) != 'undefined') { JSAN.use('MochiKit.Signal'); } +if (typeof(tests) == 'undefined') { tests = {}; } + +tests.test_DragAndDrop = function (t) { + + var drag1 = new MochiKit.DragAndDrop.Draggable('drag1', {'revert': true, 'ghosting': true}); + + var drop1 = new MochiKit.DragAndDrop.Droppable('drop1', {'hoverclass': 'drop-hover'}); + drop1.activate(); + t.is(hasElementClass('drop1', 'drop-hover'), true, "hoverclass ok"); + drop1.deactivate(); + t.is(hasElementClass('drop1', 'drop-hover'), false, "remove hoverclass ok"); + drop1.destroy(); + + t.is( isEmpty(MochiKit.DragAndDrop.Droppables.drops), true, "Unregister droppable ok"); + + var onhover = function (element) { + t.is(element, getElement('drag1'), 'onhover ok'); + }; + var drop2 = new MochiKit.DragAndDrop.Droppable('drop1', {'onhover': onhover}); + var pos = getElementPosition('drop1'); + pos = {"x": pos.x + 5, "y": pos.y + 5}; + MochiKit.DragAndDrop.Droppables.show({"page": pos}, getElement('drag1')); + + drag1.destroy(); + t.is( isEmpty(MochiKit.DragAndDrop.Draggables.drops), true, "Unregister draggable ok"); + +}; + diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Format.js b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Format.js new file mode 100644 index 0000000000..dd18a8ff76 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Format.js @@ -0,0 +1,89 @@ +if (typeof(dojo) != 'undefined') { dojo.require('MochiKit.Format'); } +if (typeof(JSAN) != 'undefined') { JSAN.use('MochiKit.Format'); } +if (typeof(tests) == 'undefined') { tests = {}; } + +tests.test_Format = function (t) { + t.is( truncToFixed(0.1234, 3), "0.123", "truncToFixed truncate" ); + t.is( truncToFixed(0.12, 3), "0.120", "truncToFixed trailing zeros" ); + t.is( truncToFixed(0.15, 1), "0.1", "truncToFixed no round" ); + t.is( truncToFixed(0.15, 0), "0", "truncToFixed zero (edge case)" ); + t.is( truncToFixed(568.80, 2), "568.80", "truncToFixed 568.80, floating-point error" ); + t.is( truncToFixed(1.23e+20, 2), "123000000000000000000.00", "truncToFixed 1.23e+20" ); + t.is( truncToFixed(-1.23e+20, 2), "-123000000000000000000.00", "truncToFixed -1.23e+20" ); + t.is( truncToFixed(1.23e-10, 2), "0.00", "truncToFixed 1.23e-10" ); + + t.is( roundToFixed(0.1234, 3), "0.123", "roundToFixed truncate" ); + t.is( roundToFixed(0.12, 3), "0.120", "roundToFixed trailing zeros" ); + t.is( roundToFixed(0.15, 1), "0.2", "roundToFixed round" ); + t.is( roundToFixed(0.15, 0), "0", "roundToFixed zero (edge case)" ); + t.is( roundToFixed(568.80, 2), "568.80", "roundToFixed 568.80, floating-point error" ); + + t.is( twoDigitFloat(-0.1234), "-0.12", "twoDigitFloat -0.1234 correct"); + t.is( twoDigitFloat(-0.1), "-0.1", "twoDigitFloat -0.1 correct"); + t.is( twoDigitFloat(-0), "0", "twoDigitFloat -0 correct"); + t.is( twoDigitFloat(0), "0", "twoDigitFloat 0 correct"); + t.is( twoDigitFloat(1), "1", "twoDigitFloat 1 correct"); + t.is( twoDigitFloat(1.0), "1", "twoDigitFloat 1.0 correct"); + t.is( twoDigitFloat(1.2), "1.2", "twoDigitFloat 1.2 correct"); + t.is( twoDigitFloat(1.234), "1.23", "twoDigitFloat 1.234 correct"); + t.is( twoDigitFloat(0.23), "0.23", "twoDigitFloat 0.23 correct"); + t.is( twoDigitFloat(0.01), "0.01", "twoDigitFloat 0.01 correct"); + t.is( twoDigitFloat(568.80), "568.8", "twoDigitFloat, floating-point error"); + + t.is( percentFormat(123), "12300%", "percentFormat 123 correct"); + t.is( percentFormat(1.23), "123%", "percentFormat 123 correct"); + t.is( twoDigitAverage(1, 0), "0", "twoDigitAverage dbz correct"); + t.is( twoDigitAverage(1, 1), "1", "twoDigitAverage 1 correct"); + t.is( twoDigitAverage(1, 10), "0.1", "twoDigitAverage .1 correct"); + function reprIs(a, b) { + arguments[0] = repr(a); + arguments[1] = repr(b); + t.is.apply(this, arguments); + } + reprIs( lstrip("\r\t\n foo \n\t\r"), "foo \n\t\r", "lstrip whitespace chars" ); + reprIs( rstrip("\r\t\n foo \n\t\r"), "\r\t\n foo", "rstrip whitespace chars" ); + reprIs( strip("\r\t\n foo \n\t\r"), "foo", "strip whitespace chars" ); + reprIs( lstrip("\r\n\t \r", "\r"), "\n\t \r", "lstrip custom chars" ); + reprIs( rstrip("\r\n\t \r", "\r"), "\r\n\t ", "rstrip custom chars" ); + reprIs( strip("\r\n\t \r", "\r"), "\n\t ", "strip custom chars" ); + + var nf = numberFormatter("$###,###.00 footer"); + t.is( nf(1000.1), "$1,000.10 footer", "trailing zeros" ); + t.is( nf(1000000.1), "$1,000,000.10 footer", "two seps" ); + t.is( nf(100), "$100.00 footer", "shorter than sep" ); + t.is( nf(100.555), "$100.56 footer", "rounding" ); + t.is( nf(-100.555), "$-100.56 footer", "default neg" ); + nf = numberFormatter("-$###,###.00"); + t.is( nf(-100.555), "-$100.56", "custom neg" ); + nf = numberFormatter("0000.0000"); + t.is( nf(0), "0000.0000", "leading and trailing" ); + t.is( nf(1.1), "0001.1000", "leading and trailing" ); + t.is( nf(12345.12345), "12345.1235", "no need for leading/trailing" ); + nf = numberFormatter("0000.0000"); + t.is( nf("taco"), "", "default placeholder" ); + nf = numberFormatter("###,###.00", "foo", "de_DE"); + t.is( nf("taco"), "foo", "custom placeholder" ); + t.is( nf(12345.12345), "12.345,12", "de_DE locale" ); + nf = numberFormatter("#%"); + t.is( nf(1), "100%", "trivial percent" ); + t.is( nf(0.55), "55%", "percent" ); + + var customLocale = { + separator: " apples and ", + decimal: " bagels at ", + percent: "am for breakfast"}; + var customFormatter = numberFormatter("###,###.0%", "No breakfast", customLocale); + t.is( customFormatter(23.458), "2 apples and 345 bagels at 8am for breakfast", "custom locale" ); + + nf = numberFormatter("###,###"); + t.is( nf(123), "123", "large number format" ); + t.is( nf(1234), "1,234", "large number format" ); + t.is( nf(12345), "12,345", "large number format" ); + t.is( nf(123456), "123,456", "large number format" ); + t.is( nf(1234567), "1,234,567", "large number format" ); + t.is( nf(12345678), "12,345,678", "large number format" ); + t.is( nf(123456789), "123,456,789", "large number format" ); + t.is( nf(1234567890), "1,234,567,890", "large number format" ); + t.is( nf(12345678901), "12,345,678,901", "large number format" ); + t.is( nf(123456789012), "123,456,789,012", "large number format" ); +}; diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Iter.js b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Iter.js new file mode 100644 index 0000000000..15b7b8932e --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Iter.js @@ -0,0 +1,186 @@ +if (typeof(dojo) != 'undefined') { dojo.require('MochiKit.Iter'); } +if (typeof(JSAN) != 'undefined') { JSAN.use('MochiKit.Iter'); } +if (typeof(tests) == 'undefined') { tests = {}; } + +tests.test_Iter = function (t) { + t.is( sum([1, 2, 3, 4, 5]), 15, "sum works on Arrays" ); + t.is( compare(list([1, 2, 3]), [1, 2, 3]), 0, "list([x]) == [x]" ); + t.is( compare(list(range(6, 0, -1)), [6, 5, 4, 3, 2, 1]), 0, "list(range(6, 0, -1)"); + t.is( compare(list(range(6)), [0, 1, 2, 3, 4, 5]), 0, "list(range(6))" ); + var moreThanTwo = partial(operator.lt, 2); + t.is( sum(ifilter(moreThanTwo, range(6))), 12, "sum(ifilter(, range()))" ); + t.is( sum(ifilterfalse(moreThanTwo, range(6))), 3, "sum(ifilterfalse(, range()))" ); + + var c = count(10); + t.is( compare([c.next(), c.next(), c.next()], [10, 11, 12]), 0, "count()" ); + c = cycle([1, 2]); + t.is( compare([c.next(), c.next(), c.next()], [1, 2, 1]), 0, "cycle()" ); + c = repeat("foo", 3); + t.is( compare(list(c), ["foo", "foo", "foo"]), 0, "repeat()" ); + c = izip([1, 2], [3, 4, 5], repeat("foo")); + t.is( compare(list(c), [[1, 3, "foo"], [2, 4, "foo"]]), 0, "izip()" ); + + t.is( compare(list(range(5)), [0, 1, 2, 3, 4]), 0, "range(x)" ); + c = islice(range(10), 0, 10, 2); + t.is( compare(list(c), [0, 2, 4, 6, 8]), 0, "islice(x, y, z)" ); + + c = imap(operator.add, [1, 2, 3], [2, 4, 6]); + t.is( compare(list(c), [3, 6, 9]), 0, "imap(fn, p, q)" ); + + c = filter(partial(operator.lt, 1), iter([1, 2, 3])); + t.is( compare(c, [2, 3]), 0, "filter(fn, iterable)" ); + + c = map(partial(operator.add, -1), iter([1, 2, 3])); + t.is( compare(c, [0, 1, 2]), 0, "map(fn, iterable)" ); + + c = map(operator.add, iter([1, 2, 3]), [2, 4, 6]); + t.is( compare(c, [3, 6, 9]), 0, "map(fn, iterable, q)" ); + + c = map(operator.add, iter([1, 2, 3]), iter([2, 4, 6])); + t.is( compare(c, [3, 6, 9]), 0, "map(fn, iterable, iterable)" ); + + c = applymap(operator.add, [[1, 2], [2, 4], [3, 6]]); + t.is( compare(list(c), [3, 6, 9]), 0, "applymap()" ); + + c = applymap(function (a) { return [this, a]; }, [[1], [2]], 1); + t.is( compare(list(c), [[1, 1], [1, 2]]), 0, "applymap(self)" ); + + c = chain(range(2), range(3)); + t.is( compare(list(c), [0, 1, 0, 1, 2]), 0, "chain(p, q)" ); + + var lessThanFive = partial(operator.gt, 5); + c = takewhile(lessThanFive, count()); + t.is( compare(list(c), [0, 1, 2, 3, 4]), 0, "takewhile()" ); + + c = dropwhile(lessThanFive, range(10)); + t.is( compare(list(c), [5, 6, 7, 8, 9]), 0, "dropwhile()" ); + + c = tee(range(5), 3); + t.is( compare(list(c[0]), list(c[1])), 0, "tee(..., 3) p0 == p1" ); + t.is( compare(list(c[2]), [0, 1, 2, 3, 4]), 0, "tee(..., 3) p2 == fixed" ); + + t.is( compare(reduce(operator.add, range(10)), 45), 0, "reduce(op.add)" ); + + try { + reduce(operator.add, []); + t.ok( false, "reduce didn't raise anything with empty list and no start?!" ); + } catch (e) { + if (e instanceof TypeError) { + t.ok( true, "reduce raised TypeError correctly" ); + } else { + t.ok( false, "reduce raised the wrong exception?" ); + } + } + + t.is( reduce(operator.add, [], 10), 10, "range initial value OK empty" ); + t.is( reduce(operator.add, [1], 10), 11, "range initial value OK populated" ); + + t.is( compare(iextend([1], range(2)), [1, 0, 1]), 0, "iextend(...)" ); + var rval = []; + var o = [0, 1, 2, 3]; + o.next = range(2).next; + t.is( iextend([], o).length, 2, "iextend handles array-like iterables" ); + + var x = []; + exhaust(imap(bind(x.push, x), range(5))); + t.is( compare(x, [0, 1, 2, 3, 4]), 0, "exhaust(...)" ); + + t.is( every([1, 2, 3, 4, 5, 4], lessThanFive), false, "every false" ); + t.is( every([1, 2, 3, 4, 4], lessThanFive), true, "every true" ); + t.is( some([1, 2, 3, 4, 4], lessThanFive), true, "some true" ); + t.is( some([5, 6, 7, 8, 9], lessThanFive), false, "some false" ); + t.is( some([5, 6, 7, 8, 4], lessThanFive), true, "some true" ); + + var rval = []; + forEach(range(2), rval.push, rval); + t.is( compare(rval, [0, 1]), 0, "forEach works bound" ); + + function foo(o) { + rval.push(o); + } + forEach(range(2), foo); + t.is( compare(rval, [0, 1, 0, 1]), 0, "forEach works unbound" ); + + var rval = []; + var o = [0, 1, 2, 3]; + o.next = range(2).next; + forEach(o, rval.push, rval); + t.is( rval.length, 2, "forEach handles array-like iterables" ); + + t.is( compare(sorted([3, 2, 1]), [1, 2, 3]), 0, "sorted default" ); + rval = sorted(["aaa", "bb", "c"], keyComparator("length")); + t.is(compare(rval, ["c", "bb", "aaa"]), 0, "sorted custom"); + + t.is( compare(reversed(range(4)), [3, 2, 1, 0]), 0, "reversed iterator" ); + t.is( compare(reversed([5, 6, 7]), [7, 6, 5]), 0, "reversed list" ); + + var o = {lst: [1, 2, 3], iterateNext: function () { return this.lst.shift(); }}; + t.is( compare(list(o), [1, 2, 3]), 0, "iterateNext" ); + + + function except(exc, func) { + try { + func(); + t.ok(false, exc.name + " was not raised."); + } catch (e) { + if (e == exc) { + t.ok( true, "raised " + exc.name + " correctly" ); + } else { + t.ok( false, "raised the wrong exception?" ); + } + } + } + + odd = partial(operator.and, 1) + + // empty + grouped = groupby([]); + except(StopIteration, grouped.next); + + // exhaust sub-iterator + grouped = groupby([2,4,6,7], odd); + kv = grouped.next(); k = kv[0], subiter = kv[1]; + t.is(k, 0, "odd(2) = odd(4) = odd(6) == 0"); + t.is(subiter.next(), 2, "sub-iterator.next() == 2"); + t.is(subiter.next(), 4, "sub-iterator.next() == 4"); + t.is(subiter.next(), 6, "sub-iterator.next() == 6"); + except(StopIteration, subiter.next); + kv = grouped.next(); key = kv[0], subiter = kv[1]; + t.is(key, 1, "odd(7) == 1"); + t.is(subiter.next(), 7, "sub-iterator.next() == 7"); + except(StopIteration, subiter.next); + + // not consume sub-iterator + grouped = groupby([2,4,6,7], odd); + kv = grouped.next(); key = kv[0], subiter = kv[1]; + t.is(key, 0, "0 = odd(2) = odd(4) = odd(6)"); + kv = grouped.next(); key = kv[0], subiter = kv[1]; + t.is(key, 1, "1 = odd(7)"); + except(StopIteration, grouped.next); + + // consume sub-iterator partially + grouped = groupby([3,1,1,2], odd); + kv = grouped.next(); key = kv[0], subiter = kv[1]; + t.is(key, 1, "odd(1) == 1"); + t.is(subiter.next(), 3, "sub-iterator.next() == 3"); + kv = grouped.next(); key = kv[0], v = kv[1]; + t.is(key, 0, "skip (1,1), odd(2) == 0"); + except(StopIteration, grouped.next); + + // null + grouped = groupby([null,null]); + kv = grouped.next(); k = kv[0], v = kv[1]; + t.is(k, null, "null ok"); + + // groupby - array version + isEqual = (t.isDeeply || function (a, b, msg) { + return t.ok(compare(a, b) == 0, msg); + }); + isEqual(groupby_as_array([ ] ), [ ], "empty"); + isEqual(groupby_as_array([1,1,1]), [ [1,[1,1,1]] ], "[1,1,1]: [1,1,1]"); + isEqual(groupby_as_array([1,2,2]), [ [1,[1] ], [2,[2,2]] ], "[1,2,2]: [1], [2,2]"); + isEqual(groupby_as_array([1,1,2]), [ [1,[1,1] ], [2,[2 ]] ], "[1,1,2]: [1,1], [2]"); + isEqual(groupby_as_array([null,null] ), [ [null,[null,null]] ], "[null,null]: [null,null]"); + grouped = groupby_as_array([1,1,3,2,4,6,8], odd); + isEqual(grouped, [[1, [1,1,3]], [0,[2,4,6,8]]], "[1,1,3,2,4,6,7] odd: [1,1,3], [2,4,6,8]"); +}; diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Logging.js b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Logging.js new file mode 100644 index 0000000000..b368e58b47 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Logging.js @@ -0,0 +1,88 @@ +if (typeof(dojo) != 'undefined') { dojo.require('MochiKit.Logging'); } +if (typeof(JSAN) != 'undefined') { JSAN.use('MochiKit.Logging'); } +if (typeof(tests) == 'undefined') { tests = {}; } + +tests.test_Logging = function (t) { + + // just in case + logger.clear(); + + t.is( logLevelAtLeast('DEBUG')('INFO'), false, 'logLevelAtLeast false' ); + t.is( logLevelAtLeast('WARNING')('INFO'), false, 'logLevelAtLeast true' ); + t.ok( logger instanceof Logger, "global logger installed" ); + + var allMessages = []; + logger.addListener("allMessages", null, + bind(allMessages.push, allMessages)); + + var fatalMessages = []; + logger.addListener("fatalMessages", "FATAL", + bind(fatalMessages.push, fatalMessages)); + + var firstTwo = []; + logger.addListener("firstTwo", null, + bind(firstTwo.push, firstTwo)); + + + log("foo"); + var msgs = logger.getMessages(); + t.is( msgs.length, 1, 'global log() put one message in queue' ); + t.is( compare(allMessages, msgs), 0, "allMessages listener" ); + var msg = msgs.pop(); + t.is( compare(msg.info, ["foo"]), 0, "info matches" ); + t.is( msg.level, "INFO", "level matches" ); + + logDebug("debugFoo"); + t.is( msgs.length, 0, 'getMessages() returns copy' ); + msgs = logger.getMessages(); + t.is( compare(allMessages, msgs), 0, "allMessages listener" ); + t.is( msgs.length, 2, 'logDebug()' ); + msg = msgs.pop(); + t.is( compare(msg.info, ["debugFoo"]), 0, "info matches" ); + t.is( msg.level, "DEBUG", "level matches" ); + + logger.removeListener("firstTwo"); + + logError("errorFoo"); + msgs = logger.getMessages(); + t.is( compare(allMessages, msgs), 0, "allMessages listener" ); + t.is( msgs.length, 3, 'logError()' ); + msg = msgs.pop(); + t.is( compare(msg.info, ["errorFoo"]), 0, "info matches" ); + t.is( msg.level, "ERROR", "level matches" ); + + logWarning("warningFoo"); + msgs = logger.getMessages(); + t.is( compare(allMessages, msgs), 0, "allMessages listener" ); + t.is( msgs.length, 4, 'logWarning()' ); + msg = msgs.pop(); + t.is( compare(msg.info, ["warningFoo"]), 0, "info matches" ); + t.is( msg.level, "WARNING", "level matches" ); + + logFatal("fatalFoo"); + msgs = logger.getMessages(); + t.is( compare(allMessages, msgs), 0, "allMessages listener" ); + t.is( msgs.length, 5, 'logFatal()' ); + msg = msgs.pop(); + t.is( compare(fatalMessages, [msg]), 0, "fatalMessages listener" ); + t.is( compare(msg.info, ["fatalFoo"]), 0, "info matches" ); + t.is( msg.level, "FATAL", "level matches" ); + msgs = logger.getMessages(1); + t.is( compare(fatalMessages, msgs), 0, "getMessages with limit returns latest" ); + + logger.removeListener("allMessages"); + logger.removeListener("fatalMessages"); + + t.is( compare(firstTwo, logger.getMessages().slice(0, 2)), 0, "firstTwo" ); + + logger.clear(); + msgs = logger.getMessages(); + t.is(msgs.length, 0, "clear removes existing messages"); + + logger.baseLog(LogLevel.INFO, 'infoFoo'); + msg = logger.getMessages().pop(); + t.is(msg.level, "INFO", "baseLog converts level") + logger.baseLog(45, 'errorFoo'); + msg = logger.getMessages().pop(); + t.is(msg.level, "ERROR", "baseLog converts ad-hoc level") +}; diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Async.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Async.html new file mode 100644 index 0000000000..82bf3b3b35 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Async.html @@ -0,0 +1,408 @@ +<html> +<head> + <script type="text/javascript" src="../MochiKit/Base.js"></script> + <script type="text/javascript" src="../MochiKit/Async.js"></script> + <script type="text/javascript" src="../MochiKit/Iter.js"></script> + <script type="text/javascript" src="../MochiKit/DOM.js"></script> + <script type="text/javascript" src="../MochiKit/Style.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> +</head> +<body> + +<pre id="test"> +<script type="text/javascript"> +try { + + var increment = function (res) { + return res + 1; + } + + var throwStuff = function (res) { + throw new GenericError(res); + } + + var catchStuff = function (res) { + return res.message; + } + + var returnError = function (res) { + return new GenericError(res); + } + + var anythingOkCallback = function (msg) { + return function (res) { + ok(true, msg); + return res; + } + } + + var testEqCallback = function () { + /* + sort of emulate how deferreds work in Twisted + for "convenient" testing + */ + var args = []; + for (var i = 0; i < arguments.length; i++) { + args.push(arguments[i]); + } + return function (res) { + var nargs = args.slice(); + nargs.unshift(res); + is.apply(this, nargs); + return res; + } + } + + var neverHappen = function (d) { + ok(false, "this should never happen"); + } + + /* + Test normal Deferred operation + */ + var d = new Deferred(); + d.addCallback(testEqCallback(1, "pre-deferred callback")); + d.callback(1); + d.addCallback(increment); + d.addCallback(testEqCallback(2, "post-deferred callback")); + d.addCallback(throwStuff); + d.addCallback(neverHappen); + d.addErrback(catchStuff); + d.addCallback(testEqCallback(2, "throw -> err, catch -> success")); + d.addCallback(returnError); + d.addCallback(neverHappen); + d.addErrback(catchStuff); + d.addCallback(testEqCallback(2, "return -> err, catch -> succcess")); + + /* + Test Deferred cancellation + */ + var cancelled = function (d) { + ok(true, "canceller called!"); + } + + var cancelledError = function (res) { + ok(res instanceof CancelledError, "CancelledError here"); + } + + d = new Deferred(cancelled); + d.addCallback(neverHappen); + d.addErrback(cancelledError); + d.cancel(); + + /* + Test succeed / fail + */ + + d = succeed(1).addCallback(testEqCallback(1, "succeed")); + + // default error + d = fail().addCallback(neverHappen); + d = d.addErrback(anythingOkCallback("default fail")); + + // default wrapped error + d = fail("web taco").addCallback(neverHappen).addErrback(catchStuff); + d = d.addCallback(testEqCallback("web taco", "wrapped fail")); + + // default unwrapped error + d = fail(new GenericError("ugh")).addCallback(neverHappen).addErrback(catchStuff); + d = d.addCallback(testEqCallback("ugh", "unwrapped fail")); + + /* + Test deferred dependencies + */ + + var deferredIncrement = function (res) { + var rval = succeed(res); + rval.addCallback(increment); + return rval; + } + + d = succeed(1).addCallback(deferredIncrement); + d = d.addCallback(testEqCallback(2, "dependent deferred succeed")); + + var deferredFailure = function (res) { + return fail(res); + } + + d = succeed("ugh").addCallback(deferredFailure).addErrback(catchStuff); + d = d.addCallback(testEqCallback("ugh", "dependent deferred fail")); + + /* + Test double-calling, double-failing, etc. + */ + try { + succeed(1).callback(2); + neverHappen(); + } catch (e) { + ok(e instanceof AlreadyCalledError, "double-call"); + } + try { + fail(1).errback(2); + neverHappen(); + } catch (e) { + ok(e instanceof AlreadyCalledError, "double-fail"); + } + try { + d = succeed(1); + d.cancel(); + d = d.callback(2); + ok(true, "swallowed one callback, no canceller"); + d.callback(3); + neverHappen(); + } catch (e) { + ok(e instanceof AlreadyCalledError, "swallow cancel"); + } + try { + d = new Deferred(cancelled); + d.cancel(); + d = d.callback(1); + neverHappen(); + } catch (e) { + ok(e instanceof AlreadyCalledError, "non-swallowed cancel"); + } + + /* Test incorrect Deferred usage */ + + d = new Deferred(); + try { + d.callback(new Deferred()); + neverHappen(); + } catch (e) { + ok (e instanceof Error, "deferred not allowed for callback"); + } + d = new Deferred(); + try { + d.errback(new Deferred()); + neverHappen(); + } catch (e) { + ok (e instanceof Error, "deferred not allowed for errback"); + } + + d = new Deferred(); + (new Deferred()).addCallback(function () { return d; }).callback(1); + try { + d.addCallback(function () {}); + neverHappen(); + } catch (e) { + ok (e instanceof Error, "chained deferred not allowed to be re-used"); + } + + /* + evalJSONRequest test + */ + var fakeReq = {"responseText":'[1,2,3,4,"asdf",{"a":["b", "c"]}]'}; + var obj = [1,2,3,4,"asdf",{"a":["b", "c"]}]; + isDeeply(obj, evalJSONRequest(fakeReq), "evalJSONRequest"); + + try { + MochiKit.Async.getXMLHttpRequest(); + ok(true, "getXMLHttpRequest"); + } catch (e) { + ok(false, "no love from getXMLHttpRequest"); + } + + var lock = new DeferredLock(); + var lst = []; + var pushNumber = function (x) { + return function (res) { lst.push(x); } + }; + lock.acquire().addCallback(pushNumber(1)); + is( compare(lst, [1]), 0, "lock acquired" ); + lock.acquire().addCallback(pushNumber(2)); + is( compare(lst, [1]), 0, "lock waiting for release" ); + lock.acquire().addCallback(pushNumber(3)); + is( compare(lst, [1]), 0, "lock waiting for release" ); + lock.release(); + is( compare(lst, [1, 2]), 0, "lock passed on" ); + lock.release(); + is( compare(lst, [1, 2, 3]), 0, "lock passed on" ); + lock.release(); + try { + lock.release(); + ok( false, "over-release didn't raise" ); + } catch (e) { + ok( true, "over-release raised" ); + } + lock.acquire().addCallback(pushNumber(1)); + is( compare(lst, [1, 2, 3, 1]), 0, "lock acquired" ); + lock.release(); + is( compare(lst, [1, 2, 3, 1]), 0, "lock released" ); + + var d = new Deferred(); + lst = []; + d.addCallback(operator.add, 2); + d.addBoth(operator.add, 4); + d.addCallback(bind(lst.push, lst)); + d.callback(1); + is( lst[0], 7, "auto-partial addCallback addBoth" ); + d.addCallback(function () { throw new Error(); }); + ebTest = function(a, b) { + map(bind(lst.push, lst), arguments); + }; + d.addErrback(ebTest, "foo"); + is( lst[1], "foo", "auto-partial errback" ); + is( lst.length, 3, "auto-partial errback" ); + + /* + Test DeferredList + */ + + var callList = [new Deferred(), new Deferred(), new Deferred()]; + callList[0].addCallback(increment); + callList[1].addCallback(increment); + callList[2].addCallback(increment); + var defList = new DeferredList(callList); + ok(defList instanceof Deferred, "DeferredList looks like a Deferred"); + + callList[0].callback(3); + callList[1].callback(5); + callList[2].callback(4); + + defList.addCallback(function (lst) { + is( arrayEqual(lst, [[true, 4], [true, 6], [true, 5]]), true, + "deferredlist result ok" ); + }); + + /* + Test fireOnOneCallback + */ + + var callList2 = [new Deferred(), new Deferred(), new Deferred()]; + callList2[0].addCallback(increment); + callList2[1].addCallback(increment); + callList2[2].addCallback(increment); + var defList2 = new DeferredList(callList2, true); + callList2[1].callback(5); + callList2[0].callback(3); + callList2[2].callback(4); + + defList2.addCallback(function (lst) { + is( arrayEqual(lst, [1, 6]), true, "deferredlist fireOnOneCallback ok" ); + }); + + /* + Test fireOnOneErrback + */ + + var callList3 = [new Deferred(), new Deferred(), new Deferred()]; + callList3[0].addCallback(increment); + callList3[1].addCallback(throwStuff); + callList3[2].addCallback(increment); + var defList3 = new DeferredList(callList3, false, true); + defList3.callback = neverHappen; + callList3[0].callback(3); + callList3[1].callback("foo"); + callList3[2].callback(4); + + defList3.addErrback(function (err) { + is( err.message, "foo", "deferredlist fireOnOneErrback ok" ); + }); + + /* + Test consumeErrors + */ + + var callList4 = [new Deferred(), new Deferred(), new Deferred()]; + callList4[0].addCallback(increment); + callList4[1].addCallback(throwStuff); + callList4[2].addCallback(increment); + var defList4 = new DeferredList(callList4, false, false, true); + defList4.addErrback(neverHappen); + callList4[1].addCallback(function (arg) { + is(arg, null, "deferredlist consumeErrors ok" ); + }); + callList4[0].callback(3); + callList4[1].callback("foo"); + callList4[2].callback(4); + + /* + Test gatherResults + */ + + var callList5 = [new Deferred(), new Deferred(), new Deferred()]; + callList5[0].addCallback(increment); + callList5[1].addCallback(increment); + callList5[2].addCallback(increment); + var gatherRet = gatherResults(callList5); + callList5[0].callback(3); + callList5[1].callback(5); + callList5[2].callback(4); + + gatherRet.addCallback(function (lst) { + is( arrayEqual(lst, [4, 6, 5]), true, + "gatherResults result ok" ); + }); + + /* + Test maybeDeferred + */ + + var maybeDef = maybeDeferred(increment, 4); + maybeDef.addCallback(testEqCallback(5, "maybeDeferred sync ok")); + + var maybeDef2 = deferredIncrement(8); + maybeDef2.addCallback(testEqCallback(9, "maybeDeferred async ok")); + + ok( true, "synchronous test suite finished!"); + + var t = (new Date().getTime()); + SimpleTest.waitForExplicitFinish(); + checkCallLater = function (originalTime) { + is(originalTime, t, "argument passed in OK"); + is(arguments.length, 1, "argument count right"); + }; + var lock = new DeferredLock(); + withLock = function (msg) { + var cb = partial.apply(null, extend(null, arguments, 1)); + var d = lock.acquire().addCallback(cb); + d.addErrback(ok, false, msg); + d.addCallback(function () { + ok(true, msg); + lock.release(); + }); + return d; + } + withLock("callLater", function () { + return callLater(0, checkCallLater, t); + }); + withLock("wait", function () { + return wait(0, t).addCallback(checkCallLater); + }); + withLock("loadJSONDoc", function () { + var d = loadJSONDoc("test_MochiKit-Async.json"); + d.addCallback(function (doc) { + is(doc.passed, true, "loadJSONDoc passed"); + }); + d.addErrback(function (doc) { + ok(false, "loadJSONDoc failed"); + }); + return d; + }); + lock.acquire().addCallback(function () { + ok(true, "async suite finished"); + SimpleTest.finish(); + }); + + +} catch (err) { + + var s = "test suite failure!\n"; + var o = {}; + var k = null; + for (k in err) { + // ensure unique keys?! + if (!o[k]) { + s += k + ": " + err[k] + "\n"; + o[k] = err[k]; + } + } + ok ( false, s ); + SimpleTest.finish(); + +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Async.json b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Async.json new file mode 100644 index 0000000000..037e18c818 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Async.json @@ -0,0 +1 @@ +{"passed": true} diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Base.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Base.html new file mode 100644 index 0000000000..ae5f8413eb --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Base.html @@ -0,0 +1,34 @@ +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../MochiKit/Base.js"></script> + <script type="text/javascript" src="../MochiKit/Iter.js"></script> + <script type="text/javascript" src="../MochiKit/DOM.js"></script> + <script type="text/javascript" src="../MochiKit/Style.js"></script> + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> +</head> +<body> + +<pre id="test"> +<script type="text/javascript" src="test_Base.js"></script> +<script type="text/javascript"> +try { + tests.test_Base({ok:ok,is:is}); + ok( true, "test suite finished!"); +} catch (err) { + var s = "test suite failure!\n"; + var o = {}; + var k = null; + for (k in err) { + // ensure unique keys?! + if (!o[k]) { + s += k + ": " + err[k] + "\n"; + o[k] = err[k]; + } + } + ok ( false, s ); +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Color.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Color.html new file mode 100644 index 0000000000..f0e73a4856 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Color.html @@ -0,0 +1,84 @@ +<html> +<head> + <script type="text/javascript" src="../MochiKit/Base.js"></script> + <script type="text/javascript" src="../MochiKit/Iter.js"></script> + <script type="text/javascript" src="../MochiKit/DOM.js"></script> + <script type="text/javascript" src="../MochiKit/Style.js"></script> + <script type="text/javascript" src="../MochiKit/Logging.js"></script> + <script type="text/javascript" src="../MochiKit/Color.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> + <style type="text/css">.redtext {color: red}</style> +</head> +<body> +<div style="position:absolute; top: 0px; left:0px; width:0px; height:0px"> + <span style="color: red" id="c_direct"></span> + <span class="redtext" id="c_indirect"></span> +</div> +<pre id="test"> +<script type="text/javascript" src="test_Color.js"></script> +<script type="text/javascript"> +try { + + var t = {ok:ok, is:is}; + tests.test_Color({ok:ok, is:is}); + is( + Color.fromText(SPAN()).toHexString(), + "#000000", + "fromText no style" + ); + + is( + Color.fromText("c_direct").toHexString(), + Color.fromName("red").toHexString(), + "fromText direct style" + ); + + is( + Color.fromText("c_indirect").toHexString(), + Color.fromName("red").toHexString(), + "fromText indirect style" + ); + + is( + Color.fromComputedStyle("c_direct", "color").toHexString(), + Color.fromName("red").toHexString(), + "fromComputedStyle direct style" + ); + + is( + Color.fromComputedStyle("c_indirect", "color").toHexString(), + Color.fromName("red").toHexString(), + "fromComputedStyle indirect style" + ); + + is( + Color.fromBackground((SPAN(null, 'test'))).toHexString(), + Color.fromName("white").toHexString(), + "fromBackground with DOM" + ); + + + // Done! + + ok( true, "test suite finished!"); + +} catch (err) { + + var s = "test suite failure!\n"; + var o = {}; + var k = null; + for (k in err) { + // ensure unique keys?! + if (!o[k]) { + s += k + ": " + err[k] + "\n"; + o[k] = err[k]; + } + } + ok ( false, s ); + +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-DOM-Safari.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-DOM-Safari.html new file mode 100644 index 0000000000..84a7441d5d --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-DOM-Safari.html @@ -0,0 +1,48 @@ +<html> +<head> + <script type="text/javascript" src="../MochiKit/MockDOM.js"></script> + <script type="text/javascript" src="../MochiKit/Base.js"></script> + <script type="text/javascript" src="../MochiKit/Iter.js"></script> + <script type="text/javascript" src="../MochiKit/DOM.js"></script> + <script type="text/javascript" src="../MochiKit/Style.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> +</head> +<body> + +<pre id="test"> +<script type="text/javascript"> +try { + + for (var i = 0; i < 10000; i++) { + var n = document.createElement("DIV"); + n.appendChild(document.createTextNode("")); + var list = MochiKit.Iter.list(n.childNodes); + var n2 = document.createElement("DIV"); + appendChildNodes(n2, n.childNodes); + var n3 = document.createElement("DIV"); + replaceChildNodes(n3, n2.childNodes); + } + ok( true, "Safari didn't crash! #213" ); + ok( true, "test suite finished!"); + + +} catch (err) { + + var s = "test suite failure!\n"; + var o = {}; + var k = null; + for (k in err) { + // ensure unique keys?! + if (!o[k]) { + s += k + ": " + err[k] + "\n"; + o[k] = err[k]; + } + } + ok ( false, s ); + +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-DOM.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-DOM.html new file mode 100644 index 0000000000..752a37cde0 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-DOM.html @@ -0,0 +1,363 @@ +<html> +<head> + <script type="text/javascript" src="../MochiKit/MockDOM.js"></script> + <script type="text/javascript" src="../MochiKit/Base.js"></script> + <script type="text/javascript" src="../MochiKit/Iter.js"></script> + <script type="text/javascript" src="../MochiKit/DOM.js"></script> + <script type="text/javascript" src="../MochiKit/Style.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> +</head> +<body> + +<div style="display: none;"> + <form id="form_test"> + <select name="select"> + <option value="foo" selected="selected">foo</option> + <option value="bar">bar</option> + <option value="baz">baz</option> + </select> + <select name="selmultiple" multiple="multiple"> + <option value="bar" selected="selected">bar</option> + <option value="baz" selected="selected">baz</option> + <option value="foo">foo</option> + </select> + <input type="hidden" name="hidden" value="test" /> + <input type="radio" name="radio_off" value="1" /> + <input type="radio" name="radio_off" value="2" /> + <input type="radio" name="radio_off" value="3" /> + <input type="radio" name="radio_on" value="1" /> + <input type="radio" name="radio_on" value="2" checked="checked" /> + <input type="radio" name="radio_on" value="3" /> + </form> + <form id="form_test2"> + <select name="selempty"> + <option value="" selected="selected">foo</option> + </select> + <select name="selempty2"> + <option selected="selected">foo</option> + </select> + </form> + <div id="parentTwo" class="two"> + <div id="parentOne" class="one"> + <div id="parentZero" class="zero"> + <span id="child">child</span> + </div> + </div> + </div> +</div> + +<pre id="test"> +<script type="text/javascript"> +try { + + lst = []; + o = {"blah": function () { lst.push("original"); }}; + addToCallStack(o, "blah", function () { lst.push("new"); }, true); + addToCallStack(o, "blah", function () { lst.push("stuff"); }, true); + is( typeof(o.blah), 'function', 'addToCallStack has a function' ); + is( o.blah.callStack.length, 3, 'callStack length 3' ); + o.blah(); + is( lst.join(" "), "original new stuff", "callStack in correct order" ); + is( o.blah, null, "set to null" ); + lst = []; + o = {"blah": function () { lst.push("original"); }}; + addToCallStack(o, "blah", + function () { lst.push("new"); return false;}, false); + addToCallStack(o, "blah", function () { lst.push("stuff"); }, false); + o.blah(); + is( lst.join(" "), "original new", "callStack in correct order (abort)" ); + o.blah(); + is( lst.join(" "), "original new original new", "callStack in correct order (again)" ); + + + is( escapeHTML("<>\"&bar"), "<>"&bar", "escapeHTML" ); // for emacs highlighting: " + + var isDOM = function (value, expected, message) { + is( escapeHTML(toHTML(value)), escapeHTML(expected), message ); + }; + + var d = document.createElement('span'); + updateNodeAttributes(d, {"foo": "bar", "baz": "wibble"}); + isDOM( d, '<span baz="wibble" foo="bar"/>', "updateNodeAttributes" ); + var d = document.createElement('input'); + d.value = "foo"; + updateNodeAttributes(d, { value: "bar" }); + is( d.value, 'bar', "updateNodeAttributes updates value property" ); + is( d.getAttribute("value"), 'bar', "updateNodeAttributes updates value attribute" ); + + var d = document.createElement('span'); + var o = { elem: SPAN(null, "foo"), __dom__: function() { return this.elem; } }; + appendChildNodes(d, 'word up', [document.createElement('span')], o); + isDOM( d, '<span>word up<span/><span>foo</span></span>', 'appendChildNodes' ); + removeElement(o); + isDOM( d, '<span>word up<span/></span>', 'removeElement using DOM Coercion Rules' ); + + replaceChildNodes(d, 'Think Different'); + isDOM( d, '<span>Think Different</span>', 'replaceChildNodes' ); + + + insertSiblingNodesBefore(d.childNodes[0], 'word up', document.createElement('span')); + isDOM( d, '<span>word up<span/>Think Different</span>', 'insertSiblingNodesBefore' ); + + insertSiblingNodesAfter(d.childNodes[0], 'purple monkey', document.createElement('span')); + isDOM( d, '<span>word uppurple monkey<span/><span/>Think Different</span>', 'insertSiblingNodesAfter' ); + + d = createDOM("span"); + isDOM( d, "<span/>", "createDOM empty" ); + + + d = createDOM("span", {"foo": "bar", "baz": "wibble"}); + isDOM( d, '<span baz="wibble" foo="bar"/>', "createDOM attributes" ); + + d = createDOM("span", {"foo": "bar", "baz": "wibble", "spam": "egg"}, "one", "two", "three"); + is( getNodeAttribute(d, 'foo'), "bar", "createDOM attribute" ); + is( getNodeAttribute(d, 'baz'), "wibble", "createDOM attribute" ); + is( getNodeAttribute(d, 'lang'), null, "getNodeAttribute on IE added attribute" ); + is( getNodeAttribute("donotexist", 'foo'), null, "getNodeAttribute invalid node id" ); + removeNodeAttribute(d, "spam"); + is( scrapeText(d), "onetwothree", "createDOM contents" ); + + isDOM( d, '<span baz="wibble" foo="bar">onetwothree</span>', "createDOM contents" ); + + d = createDOM("span", null, function (f) { + return this.nodeName.toLowerCase() + "hi" + f.nodeName.toLowerCase();}); + isDOM( d, '<span>spanhispan</span>', 'createDOM function call' ); + + d = createDOM("span", null, {msg: "hi", dom: function (f) { + return f.nodeName.toLowerCase() + this.msg; }}); + isDOM( d, '<span>spanhi</span>', 'createDOM this.dom() call' ); + + d = createDOM("span", null, {msg: "hi", __dom__: function (f) { + return f.nodeName.toLowerCase() + this.msg; }}); + isDOM( d, '<span>spanhi</span>', 'createDOM this.__dom__() call' ); + + d = createDOM("span", null, range(4)); + isDOM( d, '<span>0123</span>', 'createDOM iterable' ); + + + var d = {"taco": "pork"}; + registerDOMConverter("taco", + function (o) { return !isUndefinedOrNull(o.taco); }, + function (o) { return "Goddamn, I like " + o.taco + " tacos"; } + ); + d = createDOM("span", null, d); + // not yet public API + domConverters.unregister("taco"); + + isDOM( d, "<span>Goddamn, I like pork tacos</span>", "createDOM with custom converter" ); + + is( + escapeHTML(toHTML(SPAN(null))), + escapeHTML(toHTML(createDOM("span", null))), + "createDOMFunc vs createDOM" + ); + + is( scrapeText(d), "Goddamn, I like pork tacos", "scrape OK" ); + is( scrapeText(d, true).join(""), "Goddamn, I like pork tacos", "scrape Array OK" ); + + var st = DIV(null, STRONG(null, "d"), "oor ", STRONG(null, "f", SPAN(null, "r"), "a"), "me"); + is( scrapeText(st), "door frame", "scrape in-order" ); + + + ok( !isUndefinedOrNull(getElement("test")), "getElement might work" ); + ok( !isUndefinedOrNull($("test")), "$ alias might work" ); + ok( getElement("donotexist") === null, "getElement invalid id" ); + + d = createDOM("span", null, "one", "two"); + swapDOM(d.childNodes[0], document.createTextNode("uno")); + isDOM( d, "<span>unotwo</span>", "swapDOM" ); + var o = { elem: SPAN(null, "foo"), __dom__: function() { return this.elem; } }; + swapDOM(d.childNodes[0], o); + isDOM( d, "<span><span>foo</span>two</span>", "swapDOM using DOM Coercion Rules" ); + + is( scrapeText(d, true).join(" "), "foo two", "multi-node scrapeText" ); + /* + + TODO: + addLoadEvent (async test?) + + */ + + d = createDOM("span", {"class": "foo"}); + setElementClass(d, "bar baz"); + ok( d.className == "bar baz", "setElementClass"); + toggleElementClass("bar", d); + ok( d.className == "baz", "toggleElementClass: " + d.className); + toggleElementClass("bar", d); + ok( hasElementClass(d, "baz", "bar"), + "toggleElementClass 2: " + d.className); + addElementClass(d, "bar"); + ok( hasElementClass(d, "baz", "bar"), + "toggleElementClass 3: " + d.className); + ok( addElementClass(d, "blah"), "addElementClass return"); + ok( hasElementClass(d, "baz", "bar", "blah"), "addElementClass action"); + ok( !hasElementClass(d, "not"), "hasElementClass single"); + ok( !hasElementClass(d, "baz", "not"), "hasElementClass multiple"); + ok( removeElementClass(d, "blah"), "removeElementClass" ); + ok( !removeElementClass(d, "blah"), "removeElementClass again" ); + ok( !hasElementClass(d, "blah"), "removeElementClass again (hasElement)" ); + removeElementClass(d, "baz"); + ok( !swapElementClass(d, "blah", "baz"), "false swapElementClass" ); + ok( !hasElementClass(d, "baz"), "false swapElementClass from" ); + ok( !hasElementClass(d, "blah"), "false swapElementClass to" ); + addElementClass(d, "blah"); + ok( swapElementClass(d, "blah", "baz"), "swapElementClass" ); + ok( hasElementClass(d, "baz"), "swapElementClass has toClass" ); + ok( !hasElementClass(d, "blah"), "swapElementClass !has fromClass" ); + ok( !swapElementClass(d, "blah", "baz"), "swapElementClass twice" ); + ok( hasElementClass(d, "baz"), "swapElementClass has toClass" ); + ok( !hasElementClass(d, "blah"), "swapElementClass !has fromClass" ); + ok( !hasElementClass("donotexist", "foo"), "hasElementClass invalid node id" ); + + TABLE; + TBODY; + TR; + var t = TABLE(null, + TBODY({"class": "foo bar", "id":"tbody0"}, + TR({"class": "foo", "id":"tr0"}), + TR({"class": "bar", "id":"tr1"}) + ) + ); + + var matchElements = getElementsByTagAndClassName; + is( + map(itemgetter("id"), matchElements(null, "foo", t)).join(" "), + "tbody0 tr0", + "getElementsByTagAndClassName found all tags with foo class" + ); + is( + map(itemgetter("id"), matchElements("tr", "foo", t)).join(" "), + "tr0", + "getElementsByTagAndClassName found all tr tags with foo class" + ); + is( + map(itemgetter("id"), matchElements("tr", null, t)).join(" "), + "tr0 tr1", + "getElementsByTagAndClassName found all tr tags" + ); + is( getElementsByTagAndClassName("td", null, t).length, 0, "getElementsByTagAndClassName no match found"); + is( getElementsByTagAndClassName("p", [], "donotexist").length, 0, "getElementsByTagAndClassName invalid parent id"); + + is( getFirstElementByTagAndClassName(null, "foo", t).id, "tbody0", "getFirstElementByTagAndClassName class name" ); + is( getFirstElementByTagAndClassName("tr", "foo", t).id, "tr0", "getFirstElementByTagAndClassName tag and class name" ); + is( getFirstElementByTagAndClassName("tr", null, t).id, "tr0", "getFirstElementByTagAndClassName tag name" ); + ok( getFirstElementByTagAndClassName("td", null, t) === null, "getFirstElementByTagAndClassName no matching tag" ); + ok( getFirstElementByTagAndClassName("tr", "donotexist", t) === null, "getFirstElementByTagAndClassName no matching class" ); + ok( getFirstElementByTagAndClassName('*', null, 'donotexist') === null, "getFirstElementByTagAndClassName invalid parent id" ); + + var oldDoc = document; + var doc = MochiKit.MockDOM.createDocument(); + is( currentDocument(), document, "currentDocument() correct" ); + withDocument(doc, function () { + ok( document != doc, "global doc unchanged" ); + is( currentDocument(), doc, "currentDocument() correct" ); + var h1 = H1(); + var span = SPAN(null, "foo", h1); + appendChildNodes(currentDocument().body, span); + }); + is( document, oldDoc, "doc restored" ); + is( doc.childNodes.length, 1, "doc has one child" ); + is( doc.body.childNodes.length, 1, "body has one child" ); + var sp = doc.body.childNodes[0]; + is( sp.nodeName, "SPAN", "only child is SPAN" ); + is( sp.childNodes.length, 2, "SPAN has two childNodes" ); + is( sp.childNodes[0].nodeValue, "foo", "first node is text" ); + is( sp.childNodes[1].nodeName, "H1", "second child is H1" ); + + is( currentDocument(), document, "currentDocument() correct" ); + try { + withDocument(doc, function () { + ok( document != doc, "global doc unchanged" ); + is( currentDocument(), doc, "currentDocument() correct" ); + throw new Error("foo"); + }); + ok( false, "didn't throw" ); + } catch (e) { + ok( true, "threw" ); + } + + var mockWindow = {"foo": "bar"}; + is (currentWindow(), window, "currentWindow ok"); + withWindow(mockWindow, function () { + is(currentWindow(), mockWindow, "withWindow ok"); + }); + is (currentWindow(), window, "currentWindow ok"); + + doc = MochiKit.MockDOM.createDocument(); + var frm; + withDocument(doc, function () { + frm = FORM({name: "ignore"}, + INPUT({name:"foo", value:"bar"}), + INPUT({name:"foo", value:"bar"}), + INPUT({name:"baz", value:"bar"}) + ); + }); + var kv = formContents(frm); + is( kv[0].join(","), "foo,foo,baz", "mock formContents names" ); + is( kv[1].join(","), "bar,bar,bar", "mock formContents values" ); + is( queryString(frm), "foo=bar&foo=bar&baz=bar", "mock queryString hook" ); + + var kv = formContents("form_test"); + is( kv[0].join(","), "select,selmultiple,selmultiple,hidden,radio_on", "formContents names" ); + is( kv[1].join(","), "foo,bar,baz,test,2", "formContents values" ); + is( queryString("form_test"), "select=foo&selmultiple=bar&selmultiple=baz&hidden=test&radio_on=2", "queryString hook" ); + kv = formContents("form_test2"); + is( kv[0].join(","), "selempty,selempty2", "formContents names empty option values" ); + is( kv[1].join(","), ",foo", "formContents empty option values" ); + is( queryString("form_test2"), "selempty=&selempty2=foo", "queryString empty option values" ); + + var d = DIV(null, SPAN(), " \n\t", SPAN(), "foo", SPAN(), " "); + is( d.childNodes.length, 6, "removeEmptyNodes test conditions correct" ); + removeEmptyTextNodes(d); + is( d.childNodes.length, 4, "removeEmptyNodes" ); + + is( getFirstParentByTagAndClassName('child', 'div', 'two'), getElement("parentTwo"), "getFirstParentByTagAndClassName found parent" ); + is( getFirstParentByTagAndClassName('child', 'div'), getElement("parentZero"), "getFirstParentByTagAndClassName found parent (any class)" ); + is( getFirstParentByTagAndClassName('child', '*', 'two'), getElement("parentTwo"), "getFirstParentByTagAndClassName found parent (any tag)" ); + is( getFirstParentByTagAndClassName('child', '*'), getElement("parentZero"), "getFirstParentByTagAndClassName found parent (any tag + any class)" ); + ok( getFirstParentByTagAndClassName('child', 'form') === null, "getFirstParentByTagAndClassName found null parent (no match)" ); + ok( getFirstParentByTagAndClassName('donotexist', '*') === null, "getFirstParentByTagAndClassName invalid elem id" ); + + ok( isChildNode('child', 'child'), "isChildNode of itself"); + ok( isChildNode('child', 'parentZero'), "isChildNode direct child"); + ok( isChildNode('child', 'parentTwo'), "isChildNode sub child"); + ok( !isChildNode('child', 'form_test'), "isChildNode wrong child"); + ok( !isChildNode('child', 'donotexist'), "isChildNode no parent"); + ok( !isChildNode('donotexist', 'child'), "isChildNode no parent"); + ok( isChildNode('child', document.body), "isChildNode of body"); + ok( isChildNode($('child').firstChild, 'parentTwo'), "isChildNode text node"); + ok( !isChildNode( SPAN(), document.body), "isChildNode child not in DOM"); + ok( !isChildNode( SPAN(), 'child'), "isChildNode child not in DOM"); + ok( !isChildNode( 'child', SPAN()), "isChildNode parent not in DOM"); + + // Test optional dependency on Iter + var Iter = MochiKit.Iter; + delete MochiKit["Iter"]; + d = DIV({"foo": "bar"}, SPAN({}, "one")); + is( getNodeAttribute(d, 'foo'), "bar", "createDOM attribute without Iter" ); + is( scrapeText(d), "one", "node contents without Iter" ); + MochiKit.Iter = Iter; + + ok( true, "test suite finished!"); + + +} catch (err) { + + var s = "test suite failure!\n"; + var o = {}; + var k = null; + for (k in err) { + // ensure unique keys?! + if (!o[k]) { + s += k + ": " + err[k] + "\n"; + o[k] = err[k]; + } + } + ok ( false, s ); + +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-DateTime.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-DateTime.html new file mode 100644 index 0000000000..5e91271bca --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-DateTime.html @@ -0,0 +1,39 @@ +<html> +<head> + <script type="text/javascript" src="../MochiKit/Base.js"></script> + <script type="text/javascript" src="../MochiKit/DateTime.js"></script> + <script type="text/javascript" src="../MochiKit/Iter.js"></script> + <script type="text/javascript" src="../MochiKit/DOM.js"></script> + <script type="text/javascript" src="../MochiKit/Style.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> +</head> +<body> + +<pre id="test"> +<script type="text/javascript" src="test_DateTime.js"></script> +<script type="text/javascript"> +try { + + tests.test_DateTime({ok:ok, is:is}); + ok( true, "test suite finished!"); + +} catch (err) { + + var s = "test suite failure!\n"; + var o = {}; + var k = null; + for (k in err) { + // ensure unique keys?! + if (!o[k]) { + s += k + ": " + err[k] + "\n"; + o[k] = err[k]; + } + } + ok ( false, s ); + +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-DragAndDrop.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-DragAndDrop.html new file mode 100644 index 0000000000..8e4a43682b --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-DragAndDrop.html @@ -0,0 +1,60 @@ +<html> +<head> + <script type="text/javascript" src="../MochiKit/Base.js"></script> + <script type="text/javascript" src="../MochiKit/Iter.js"></script> + <script type="text/javascript" src="../MochiKit/DOM.js"></script> + <script type="text/javascript" src="../MochiKit/Style.js"></script> + <script type="text/javascript" src="../MochiKit/Color.js"></script> + <script type="text/javascript" src="../MochiKit/Signal.js"></script> + <!-- + Include MochiKit/Position.js to fix the 2 following errors: + "Error: uncaught exception: MochiKit.Visual depends on MochiKit.Position!" + "Error: uncaught exception: MochiKit.DragAndDrop depends on MochiKit.Position!" + --> + <script type="text/javascript" src="../MochiKit/Position.js"></script> + <script type="text/javascript" src="../MochiKit/Visual.js"></script> + <script type="text/javascript" src="../MochiKit/DragAndDrop.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> + <style type="text/css"> + .drop-hover { + } + #drag1 { + visibility: hidden; + } + #drop1 { + visibility: hidden; + } + </style> +</head> +<body> +<div id='drag1'>drag1</div> +<div id='drop1'>drop1</div> +<pre id="test"> +<script type="text/javascript" src="test_DragAndDrop.js"></script> +<script type="text/javascript"> +try { + + // Counting the number of tests is really lame + tests.test_DragAndDrop({ok:ok, is:is}); + ok( true, "test suite finished!"); + +} catch (err) { + + var s = "test suite failure!\n"; + var o = {}; + var k = null; + for (k in err) { + // ensure unique keys?! + if (!o[k]) { + s += k + ": " + err[k] + "\n"; + o[k] = err[k]; + } + } + ok ( false, s ); + +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Format.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Format.html new file mode 100644 index 0000000000..337f27b81a --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Format.html @@ -0,0 +1,39 @@ +<html> +<head> + <script type="text/javascript" src="../MochiKit/Base.js"></script> + <script type="text/javascript" src="../MochiKit/Format.js"></script> + <script type="text/javascript" src="../MochiKit/Iter.js"></script> + <script type="text/javascript" src="../MochiKit/DOM.js"></script> + <script type="text/javascript" src="../MochiKit/Style.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> +</head> +<body> + +<pre id="test"> +<script type="text/javascript" src="test_Format.js"></script> +<script type="text/javascript"> +try { + + tests.test_Format({ok:ok, is:is}); + ok( true, "test suite finished!"); + +} catch (err) { + + var s = "test suite failure!\n"; + var o = {}; + var k = null; + for (k in err) { + // ensure unique keys?! + if (!o[k]) { + s += k + ": " + err[k] + "\n"; + o[k] = err[k]; + } + } + ok ( false, s ); + +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Iter.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Iter.html new file mode 100644 index 0000000000..42f2cfa847 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Iter.html @@ -0,0 +1,38 @@ +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../MochiKit/Base.js"></script> + <script type="text/javascript" src="../MochiKit/Iter.js"></script> + <script type="text/javascript" src="../MochiKit/DOM.js"></script> + <script type="text/javascript" src="../MochiKit/Style.js"></script> + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> +</head> +<body> + +<pre id="test"> +<script type="text/javascript" src="test_Iter.js"></script> +<script type="text/javascript"> +try { + + tests.test_Iter({ok:ok, is:is}); + ok( true, "test suite finished!"); + +} catch (err) { + + var s = "test suite failure!\n"; + var o = {}; + var k = null; + for (k in err) { + // ensure unique keys?! + if (!o[k]) { + s += k + ": " + err[k] + "\n"; + o[k] = err[k]; + } + } + ok ( false, s ); + +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-JSAN.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-JSAN.html new file mode 100644 index 0000000000..53a0e0ed04 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-JSAN.html @@ -0,0 +1,32 @@ +<html> +<head> + <script type="text/javascript" src="JSAN.js"></script> +</head> +<body> + +<pre id="test"> +<script type="text/javascript"> + // TODO: Make this a harness for the other tests + JSAN.use('Test.More'); + JSAN.addRepository('..'); + var lst = []; + plan({"tests": 1}); + var wc = {}; + wc['MochiKit'] = true; + for (var k in window) { wc[k] = true; } + for (var k in window) { wc[k] = true; } + JSAN.use('MochiKit.MochiKit', []); + for (var k in window) { + if (!(k in wc) && !(k.charAt(0) == '[')) { + lst.push(k); + } + } + lst.sort(); + pollution = lst.join(" "); + is(pollution, "compare reduce", "namespace pollution?"); + JSAN.use('MochiKit.MochiKit'); + +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Logging.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Logging.html new file mode 100644 index 0000000000..a0c6dad368 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Logging.html @@ -0,0 +1,40 @@ +<html> +<head> + <script type="text/javascript" src="../MochiKit/Base.js"></script> + <script type="text/javascript" src="../MochiKit/Logging.js"></script> + <script type="text/javascript" src="../MochiKit/Iter.js"></script> + <script type="text/javascript" src="../MochiKit/DOM.js"></script> + <script type="text/javascript" src="../MochiKit/Style.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> + +</head> +<body> + +<pre id="test"> +<script type="text/javascript" src="test_Logging.js"></script> +<script type="text/javascript"> +try { + + tests.test_Logging({ok:ok, is:is}); + ok( true, "test suite finished!"); + +} catch (err) { + + var s = "test suite failure!\n"; + var o = {}; + var k = null; + for (k in err) { + // ensure unique keys?! + if (!o[k]) { + s += k + ": " + err[k] + "\n"; + o[k] = err[k]; + } + } + ok ( false, s ); + +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-MochiKit.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-MochiKit.html new file mode 100644 index 0000000000..88947a0115 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-MochiKit.html @@ -0,0 +1,18 @@ +<html> +<head> + <script type="text/javascript" src="../MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> +</head> +<body> + +<pre id="test"> +<script type="text/javascript"> + is( isUndefined(null), false, "null is not undefined" ); + is( isUndefined(""), false, "empty string is not undefined" ); + is( isUndefined(undefined), true, "undefined is undefined" ); + is( isUndefined({}.foo), true, "missing property is undefined" ); +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Selector.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Selector.html new file mode 100644 index 0000000000..456be56352 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Selector.html @@ -0,0 +1,295 @@ +<html> +<head> + <script type="text/javascript" src="../MochiKit/MockDOM.js"></script> + <script type="text/javascript" src="../MochiKit/Base.js"></script> + <script type="text/javascript" src="../MochiKit/Iter.js"></script> + <script type="text/javascript" src="../MochiKit/DOM.js"></script> + <script type="text/javascript" src="../MochiKit/Style.js"></script> + <script type="text/javascript" src="../MochiKit/Selector.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> + <style type="text/css"> + p, #sequence { + display: none; + } + </style> +</head> +<body> + <p>Test originally from <a href="http://simon.incutio.com/archive/2003/03/25/#getElementsBySelector" rel="bookmark">this blog entry</a>.</p> + + <p>Here are some links in a normal paragraph: <a href="http://www.google.com/" title="Google!">Google</a>, <a href="http://groups.google.com/">Google Groups</a>. This link has <code>class="blog"</code>: <a href="http://diveintomark.org/" class="blog" fakeattribute="bla">diveintomark</a></p> + <div id="foo"> + <p>Everything inside the red border is inside a div with <code>id="foo"</code>.</p> + <p>This is a normal link: <a href="http://www.yahoo.com/">Yahoo</a></p> + + <a style="display: none" href="http://www.example.com/outsidep">This a is not inside a p</a> + + <p>This link has <code>class="blog"</code>: <a href="http://simon.incutio.com/" class="blog">Simon Willison's Weblog</a></p> + <p>This <span><a href="http://www.example.com/insidespan">link</a></span> is inside a span, not directly child of p</p> + <p lang="en-us">Nonninn</p> + <p lang="is-IS">Sniðugt</p> + <p> + <input type="button" name="enabled" value="enabled" id="enabled"> + <input type="button" name="disabled" value="disabled" id="disabled" disabled="1" /> + <input type="checkbox" name="checked" value="checked" id="checked" checked="1" /> + </p> + </div> + + <div id="sequence"> + <a href="http://www.example.com/link1">Link 1</a> + <a href="http://www.example.com/link2">Link 2</a> + <a href="http://www.example.com/link3">Link 3</a> + <a href="http://www.example.com/link4">Link 4</a> + <p>Something else</p> + <a href="http://www.example.com/link5">Link 5</a> + <a href="http://www.example.com/link6">Link 6</a> + <a href="http://www.example.com/link7">Link 7</a> + <a href="http://www.example.com/link8">Link 8</a> + </div> + + <div id="multiclass" class="multiple classnames here"></div> +<pre id="test"> +<script type="text/javascript"> +try { + + var testExpected = function (res, exp, lbl) { + for (var i=0; i < res.length; i ++) { + is( res[i].href, exp[i], lbl + ' (' + i + ')'); + } + }; + + var expected = ['http://simon.incutio.com/archive/2003/03/25/#getElementsBySelector', + 'http://www.google.com/', + 'http://groups.google.com/', + 'http://diveintomark.org/', + 'http://www.yahoo.com/', + 'http://www.example.com/outsidep', + 'http://simon.incutio.com/', + 'http://www.example.com/insidespan', + 'http://www.example.com/link1', + 'http://www.example.com/link2', + 'http://www.example.com/link3', + 'http://www.example.com/link4', + 'http://www.example.com/link5', + 'http://www.example.com/link6', + 'http://www.example.com/link7', + 'http://www.example.com/link8']; + var results = $$('a'); + testExpected(results, expected, "'a' selector"); + + expected = ['http://diveintomark.org/', 'http://simon.incutio.com/']; + results = $$('p a.blog'); + testExpected(results, expected, "'p a.blog' selector"); + + expected = ['http://www.yahoo.com/', + 'http://www.example.com/outsidep', + 'http://simon.incutio.com/', + 'http://www.example.com/insidespan', + 'http://www.example.com/link1', + 'http://www.example.com/link2', + 'http://www.example.com/link3', + 'http://www.example.com/link4', + 'http://www.example.com/link5', + 'http://www.example.com/link6', + 'http://www.example.com/link7', + 'http://www.example.com/link8']; + results = $$('div a'); + testExpected(results, expected, "'div a' selector"); + + expected = ['http://www.yahoo.com/', + 'http://www.example.com/outsidep', + 'http://simon.incutio.com/', + 'http://www.example.com/insidespan']; + results = $$('div#foo a'); + testExpected(results, expected, "'div#foo a' selector"); + + expected = ['http://simon.incutio.com/', + 'http://www.example.com/insidespan']; + results = $$('#foo a.blog'); + testExpected(results, expected, "'#foo a.blog' selector"); + + expected = ['http://diveintomark.org/', + 'http://simon.incutio.com/', + 'http://www.example.com/insidespan']; + results = $$('.blog'); + testExpected(results, expected, "'.blog' selector"); + + expected = ['http://www.google.com/', + 'http://www.yahoo.com/', + 'http://www.example.com/outsidep', + 'http://www.example.com/insidespan', + 'http://www.example.com/link1', + 'http://www.example.com/link2', + 'http://www.example.com/link3', + 'http://www.example.com/link4', + 'http://www.example.com/link5', + 'http://www.example.com/link6', + 'http://www.example.com/link7', + 'http://www.example.com/link8']; + results = $$('a[href^="http://www"]'); + testExpected(results, expected, "'a[href^=http://www]' selector"); + + expected = ['http://diveintomark.org/']; + results = $$('a[href$="org/"]'); + testExpected(results, expected, "'a[href$=org/]' selector"); + + expected = ['http://www.google.com/', + 'http://groups.google.com/']; + results = $$('a[href*="google"]'); + testExpected(results, expected, "'a[href*=google]' selector"); + + expected = ['http://simon.incutio.com/archive/2003/03/25/#getElementsBySelector']; + results = $$('a[rel="bookmark"]'); + testExpected(results, expected, "'a[rel=bookmark]' selector"); + + expected = ['http://diveintomark.org/']; + results = $$('a[fakeattribute]'); + testExpected(results, expected, "'a[fakeattribute]' selector"); + + /* This doesn't work in IE due to silly DOM implementation + expected = ['http://www.google.com/']; + results = $$('a[title]'); + testExpected(results, expected, "'a[title]' selector"); + */ + + // Test attribute operators (also for elements not having the attribute) + results = $$('p[lang="en-us"]'); + is( results[0].firstChild.nodeValue, 'Nonninn', "'p[lang=en-us]' selector"); + results = $$('p[lang!="is-IS"]'); + is( results[0].firstChild.nodeValue, 'Nonninn', "'p[lang!=is-IS]' selector"); + results = $$('p[lang~="en-us"]'); + is( results[0].firstChild.nodeValue, 'Nonninn', "'p[lang~=en-us]' selector"); + results = $$('p[lang^="en"]'); + is( results[0].firstChild.nodeValue, 'Nonninn', "'p[lang^=en]' selector"); + results = $$('p[lang$="us"]'); + is( results[0].firstChild.nodeValue, 'Nonninn', "'p[lang$=us]' selector"); + results = $$('p[lang*="-u"]'); + is( results[0].firstChild.nodeValue, 'Nonninn', "'p[lang*=-u]' selector"); + results = $$('p[lang|="en"]'); + is( results[0].firstChild.nodeValue, 'Nonninn', "'p[lang|=en]' selector"); + + expected = ['http://simon.incutio.com/archive/2003/03/25/#getElementsBySelector', + 'http://www.google.com/', + 'http://groups.google.com/', + 'http://diveintomark.org/', + 'http://www.yahoo.com/', + 'http://simon.incutio.com/', + 'http://www.example.com/insidespan']; + results = $$('p > a'); + testExpected(results, expected, "'p > a' selector"); + + expected = ['http://www.example.com/insidespan']; + results = $$('span > a'); + testExpected(results, expected, "'span > a' selector"); + + expected = ['http://groups.google.com/', + 'http://www.example.com/link2', + 'http://www.example.com/link3', + 'http://www.example.com/link4', + 'http://www.example.com/link6', + 'http://www.example.com/link7', + 'http://www.example.com/link8']; + results = $$('a + a'); + testExpected(results, expected, "'a + a' selector"); + + expected = ['http://www.example.com/outsidep', + 'http://www.example.com/link5', + 'http://www.example.com/link6', + 'http://www.example.com/link7', + 'http://www.example.com/link8']; + results = $$('p ~ a'); + testExpected(results, expected, "'p ~ a' selector"); + + expected = ['http://www.example.com/link1', + 'http://www.example.com/link3', + 'http://www.example.com/link6', + 'http://www.example.com/link8']; + results = $$('#sequence a:nth-child(odd)'); + testExpected(results, expected, "'#sequence a:nth-child(odd)' selector"); + + expected = ['http://www.example.com/link1', + 'http://www.example.com/link3', + 'http://www.example.com/link5', + 'http://www.example.com/link7']; + results = $$('#sequence a:nth-of-type(odd)'); + testExpected(results, expected, "'#sequence a:nth-of-type(odd)' selector"); + + expected = ['http://www.example.com/link1', + 'http://www.example.com/link4', + 'http://www.example.com/link7']; + results = $$('#sequence a:nth-of-type(3n+1)'); + testExpected(results, expected, "'#sequence a:nth-of-type(3n+1)' selector"); + + expected = ['http://www.example.com/link5']; + results = $$('#sequence a:nth-child(6)'); + testExpected(results, expected, "'#sequence a:nth-child(6)' selector"); + + expected = ['http://www.example.com/link5']; + results = $$('#sequence a:nth-of-type(5)'); + testExpected(results, expected, "'#sequence a:nth-of-type(5)' selector"); + + expected = [$('enabled'), $('checked')]; + results = $$('body :enabled'); + for (var i=0; i < results.length; i ++) { + is( results[i], expected[i], "'body :enabled" + ' (' + i + ')'); + } + + expected = [$('disabled')]; + results = $$('body :disabled'); + for (var i=0; i < results.length; i ++) { + is( results[i], expected[i], "'body :disabled" + ' (' + i + ')'); + } + + expected = [$('checked')]; + results = $$('body :checked'); + for (var i=0; i < results.length; i ++) { + is( results[i], expected[i], "'body :checked" + ' (' + i + ')'); + } + + expected = document.getElementsByTagName('p'); + results = $$('a[href$=outsidep] ~ *'); + for (var i=0; i < results.length; i ++) { + is( results[i], expected[i+4], "'a[href$=outsidep] ~ *' selector" + ' (' + i + ')'); + } + + expected = [document.documentElement]; + results = $$(':root'); + for (var i=0; i < results.length; i ++) { + is( results[i], expected[i], "':root' selector" + ' (' + i + ')'); + } + + expected = [$('multiclass')]; + results = $$('[class~=classnames]'); + for (var i=0; i < results.length; i ++) { + is( results[i], expected[i], "'~=' attribute test" + ' (' + i + ')'); + } + + var doc = MochiKit.MockDOM.createDocument(); + appendChildNodes(doc.body, A({"href": "http://www.example.com/insideAnotherDocument"}, "Inside a document")); + withDocument(doc, function(){ + is( $$(":root")[0], doc, ":root on a different document" ); + is( $$("a")[0], doc.body.firstChild, "a inside a different document" ); + }); + + ok( true, "test suite finished!"); + + +} catch (err) { + + var s = "test suite failure!\n"; + var o = {}; + var k = null; + for (k in err) { + // ensure unique keys?! + if (!o[k]) { + s += k + ": " + err[k] + "\n"; + o[k] = err[k]; + } + } + ok ( false, s ); + +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Signal.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Signal.html new file mode 100644 index 0000000000..127b534cca --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Signal.html @@ -0,0 +1,43 @@ +<html> +<head> + <script type="text/javascript" src="../MochiKit/Base.js"></script> + <script type="text/javascript" src="../MochiKit/Iter.js"></script> + <script type="text/javascript" src="../MochiKit/DOM.js"></script> + <script type="text/javascript" src="../MochiKit/Style.js"></script> + <script type="text/javascript" src="../MochiKit/Signal.js"></script> + <script type="text/javascript" src="../MochiKit/Logging.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> + +</head> +<body> + +Please ignore this button: <input type="submit" id="submit" /><br /> + +<pre id="test"> +<script type="text/javascript" src="test_Signal.js"></script> +<script type="text/javascript"> +try { + + tests.test_Signal({ok:ok, is:is}); + ok(true, "test suite finished!"); + +} catch (err) { + + var s = "test suite failure!\n"; + var o = {}; + var k = null; + for (k in err) { + // ensure unique keys?! + if (!o[k]) { + s += k + ": " + err[k] + "\n"; + o[k] = err[k]; + } + } + ok(false, s); + +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Style.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Style.html new file mode 100644 index 0000000000..a490bdf8f8 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Style.html @@ -0,0 +1,231 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> +<html> +<head> + <script type="text/javascript" src="../MochiKit/MockDOM.js"></script> + <script type="text/javascript" src="../MochiKit/Base.js"></script> + <script type="text/javascript" src="../MochiKit/DOM.js"></script> + <script type="text/javascript" src="../MochiKit/Iter.js"></script> + <script type="text/javascript" src="../MochiKit/Style.js"></script> + <script type="text/javascript" src="../MochiKit/Color.js"></script> + <script type="text/javascript" src="../MochiKit/Logging.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> + <style type="text/css"> + #hideTest { + display: none; + } + </style> +</head> +<body style="border: 0; margin: 0; padding: 0;"> + +<div id="styleTest" style="position: absolute; left: 400px; top: 100px; width: 100px; height: 80px; padding: 10px 20px 30px 40px; border-width: 1px 2px 3px 4px; border-style: solid; border-color: blue; background: red; opacity: 0.5; filter: alpha(opacity=50); font-size: 10px; overflow-x: visible; overflow-y: hidden;"><div id="innerDiv"></div>TEST<span id="styleSubTest">SUB</span><div id="floatTest" style="float: left;">Float</div></div> + +<div id="hideTest" style="width: 100px; height: 100px;"><div id="innerHideTest" style="width: 10px; height: 10px;"></div></div> + +<table id="testTable" border="0" cellspacing="0" cellpadding="0" + style="position:absolute;left: 400px; top:300px;line-height:20px;"><tr align="center"> +<td id="testCell1" style="width: 80px; height: 30px; border:2px solid blue">1</td> +<td id="testCell2" style="width: 80px; height: 30px; border:2px solid blue">2</td> +<td id="testCell3" style="width: 80px; height: 30px; border:2px solid blue">3</td> +</tr></table> + +<pre id="test"> +<script type="text/javascript"> + +try { + + // initial + var pos = getElementPosition('styleTest'); + is(pos.x, 400, 'initial x position'); + is(pos.y, 100, 'initial y position'); + + // Coordinates including border and padding + pos = getElementPosition('innerDiv'); + is(pos.x, 444, 'x position with offsetParent border'); + is(pos.y, 111, 'y position with offsetParent border'); + + // moved + var newPos = new MochiKit.Style.Coordinates(500, 200); + setElementPosition('styleTest', newPos); + pos = getElementPosition('styleTest'); + is(pos.x, 500, 'updated x position'); + is(pos.y, 200, 'updated y position'); + + // moved with relativeTo + anotherPos = new MochiKit.Style.Coordinates(100, 100); + pos = getElementPosition('styleTest', anotherPos); + is(pos.x, 400, 'updated x position (using relativeTo parameter)'); + is(pos.y, 100, 'updated y position (using relativeTo parameter)'); + + // Coordinates object + pos = getElementPosition({x: 123, y: 321}); + is(pos.x, 123, 'passthrough x position'); + is(pos.y, 321, 'passthrough y position'); + + // Coordinates object with relativeTo + pos = getElementPosition({x: 123, y: 321}, {x: 100, y: 50}); + is(pos.x, 23, 'passthrough x position (using relativeTo parameter)'); + is(pos.y, 271, 'passthrough y position (using relativeTo parameter)'); + + pos = getElementPosition('garbage'); + is(typeof(pos), 'undefined', + 'invalid element should return an undefined position'); + + // Only set one coordinate + setElementPosition('styleTest', {'x': 300}); + pos = getElementPosition('styleTest'); + is(pos.x, 300, 'updated only x position'); + is(pos.y, 200, 'not updated y position'); + + var mc = MochiKit.Color.Color; + var red = mc.fromString('rgb(255,0,0)'); + var color = null; + + color = mc.fromString(getStyle('styleTest', 'background-color')); + is(color.toHexString(), red.toHexString(), + 'test getStyle selector case'); + + color = mc.fromString(getStyle('styleTest', 'backgroundColor')); + is(color.toHexString(), red.toHexString(), + 'test getStyle camel case'); + + is(getStyle('styleSubTest', 'font-size'), '10px', + 'test computed getStyle selector case'); + + is(getStyle('styleSubTest', 'fontSize'), '10px', + 'test computed getStyle camel case'); + + is(eval(getStyle('styleTest', 'opacity')), 0.5, + 'test getStyle opacity'); + + is(getStyle('styleTest', 'opacity'), 0.5, 'test getOpacity'); + + setStyle('styleTest', {'opacity': 0.2}); + is(getStyle('styleTest', 'opacity'), 0.2, 'test setOpacity'); + + setStyle('styleTest', {'opacity': 0}); + is(getStyle('styleTest', 'opacity'), 0, 'test setOpacity'); + + setStyle('styleTest', {'opacity': 1}); + var t = getStyle('styleTest', 'opacity'); + ok(t > 0.999 && t <= 1, 'test setOpacity'); + + is(getStyle('floatTest', 'float'), "left", 'getStyle of float'); + is(getStyle('floatTest', 'cssFloat'), "left", 'getStyle of cssFloat'); + is(getStyle('floatTest', 'styleFloat'), "left", 'getStyle of styleFloat'); + is(getStyle('styleTest', 'float'), "none", 'getStyle of float when unset'); + + setStyle('floatTest', { "float": "right" }); + is(getStyle('floatTest', 'float'), "right", 'setStyle of CSS float'); + is(getStyle('floatTest', 'cssFloat'), "right", 'setStyle of CSS cssFloat'); + is(getStyle('floatTest', 'styleFloat'), "right", 'setStyle of CSS styleFloat'); + + var dims = getElementDimensions('styleTest'); + is(dims.w, 166, 'getElementDimensions w ok'); + is(dims.h, 124, 'getElementDimensions h ok'); + + dims = getElementDimensions('styleTest', true); + is(dims.w, 100, 'getElementDimensions content w ok'); + is(dims.h, 80, 'getElementDimensions content h ok'); + + setElementDimensions('styleTest', {'w': 200, 'h': 150}); + dims = getElementDimensions('styleTest', true); + is(dims.w, 200, 'setElementDimensions w ok'); + is(dims.h, 150, 'setElementDimensions h ok'); + + setElementDimensions('styleTest', {'w': 150}); + dims = getElementDimensions('styleTest', true); + is(dims.w, 150, 'setElementDimensions only w ok'); + is(dims.h, 150, 'setElementDimensions h not updated ok'); + + hideElement('styleTest'); + dims = getElementDimensions('styleTest', true); + is(dims.w, 150, 'getElementDimensions w ok when display none'); + is(dims.h, 150, 'getElementDimensions h ok when display none'); + + dims = getElementDimensions('hideTest', true); + is(dims.w, 100, 'getElementDimensions w ok when CSS display none'); + is(dims.h, 100, 'getElementDimensions h ok when CSS display none'); + + /* TODO: can we create a work-around for this case? + dims = getElementDimensions('innerHideTest', true); + is(dims.w, 10, 'getElementDimensions w ok when parent CSS display none'); + is(dims.h, 10, 'getElementDimensions h ok when parent CSS display none'); + */ + + var elem = DIV(); + appendChildNodes('styleTest', elem); + var before = elem.style.display; + getElementDimensions(elem); + var after = elem.style.display; + is(after, before, 'getElementDimensions modified element display'); + + dims = getViewportDimensions(); + is(dims.w > 0, true, 'test getViewportDimensions w'); + is(dims.h > 0, true, 'test getViewportDimensions h'); + + pos = getViewportPosition(); + is(pos.x, 0, 'test getViewportPosition x'); + is(pos.y, 0, 'test getViewportPosition y'); + + // The 3(+3) following |is(dims.w, 80, ...);| need a width of at least 652px to pass. + // Our SimpleTest/TestRunner.js runs tests inside an |iframe.width = "500";| only. + // Work around that. + // NB: This test already passes without this workaround when run alone. + setElementPosition('testTable', {'x': dims.w - (3 * (2 + 80 + 2))}); + pos = getElementPosition('testTable'); + is(dims.w - pos.x >= (3 * (2 + 80 + 2)), true, 'Is viewport width enough to display \'testTable\' at expected size?'); + + dims = getElementDimensions('testCell1', true); + is(dims.w, 80, 'default left table cell content w ok'); + is(dims.h, 30, 'default left table cell content h ok'); + dims = getElementDimensions('testCell2', true); + is(dims.w, 80, 'default middle table cell content w ok'); + is(dims.h, 30, 'default middle table cell content h ok'); + dims = getElementDimensions('testCell3', true); + is(dims.w, 80, 'default right table cell content w ok'); + is(dims.h, 30, 'default right table cell content h ok'); + + setStyle('testTable', {'borderCollapse': 'collapse'}); + dims = getElementDimensions('testCell1', true); + is(dims.w, 80, 'collapsed left table cell content w ok'); + is(dims.h, 30, 'collapsed left table cell content h ok'); + dims = getElementDimensions('testCell2', true); + is(dims.w, 80, 'collapsed middle table cell content w ok'); + is(dims.h, 30, 'collapsed middle table cell content h ok'); + dims = getElementDimensions('testCell3', true); + is(dims.w, 80, 'collapsed right table cell content w ok'); + is(dims.h, 30, 'collapsed right table cell content h ok'); + + hideElement('testTable'); + + var overflow = makeClipping('styleTest'); + is(getStyle('styleTest', 'overflow-x'), 'hidden', 'make clipping on overflow-x'); + is(getStyle('styleTest', 'overflow-y'), 'hidden', 'make clipping on overflow-y'); + + undoClipping('styleTest', overflow); + is(getStyle('styleTest', 'overflow-x'), 'visible', 'undo clipping on overflow-x'); + is(getStyle('styleTest', 'overflow-y'), 'hidden', 'undo clipping on overflow-y'); + + ok( true, "test suite finished!"); + + +} catch (err) { + + var s = "test suite failure!\n"; + var o = {}; + var k = null; + for (k in err) { + // ensure unique keys?! + if (!o[k]) { + s += k + ": " + err[k] + "\n"; + o[k] = err[k]; + } + } + ok ( false, s ); + +} +</script> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Visual.html b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Visual.html new file mode 100644 index 0000000000..fc12599e84 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Visual.html @@ -0,0 +1,197 @@ +<html> +<head> + <script type="text/javascript" src="../MochiKit/Base.js"></script> + <script type="text/javascript" src="../MochiKit/Iter.js"></script> + <script type="text/javascript" src="../MochiKit/DOM.js"></script> + <script type="text/javascript" src="../MochiKit/Async.js"></script> + <script type="text/javascript" src="../MochiKit/Style.js"></script> + <script type="text/javascript" src="../MochiKit/Color.js"></script> + <script type="text/javascript" src="../MochiKit/Signal.js"></script> + <script type="text/javascript" src="../MochiKit/Position.js"></script> + <script type="text/javascript" src="../MochiKit/Visual.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + + <link rel="stylesheet" type="text/css" href="SimpleTest/test.css"> + <style type="text/css"> + #elt1, #elt2, #ctn1 { + visibility: hidden; + font-size: 1em; + margin: 2px; + } + #elt3 { + display: none; + } + #ctn1 { + height: 2px; + } + </style> +</head> +<body> + +<div id='elt1'>elt1</div> +<div id='ctn1'><div id='elt2'></div></div> +<div id='elt3'>elt3</div> +<pre id="test"> +<script type="text/javascript"> +try { + var TestQueue = function () { + }; + + TestQueue.prototype = new MochiKit.Visual.ScopedQueue(); + + MochiKit.Base.update(TestQueue.prototype, { + startLoop: function (func, interval) { + this.started = true; + var timePos = new Date().getTime(); + while (this.started) { + timePos += interval; + MochiKit.Base.map(function (effect) { + effect.loop(timePos); + }, this.effects); + } + }, + stopLoop: function () { + this.started = false; + } + }); + + var gl = new TestQueue(); + MochiKit.Visual.Queues.instances['global'] = gl; + MochiKit.Visual.Queues.instances['elt1'] = gl; + MochiKit.Visual.Queues.instances['elt2'] = gl; + MochiKit.Visual.Queues.instances['elt3'] = gl; + MochiKit.Visual.Queues.instances['ctn1'] = gl; + MochiKit.Visual.Queue = gl; + + pulsate("elt1", {afterFinish: function () { + is(getElement('elt1').style.display != 'none', true, "pulsate ok"); + }}); + + pulsate("elt1", {pulses: 2, afterFinish: function () { + is(getElement('elt1').style.display != 'none', true, "pulsate with numbered pulses ok"); + }}); + + shake("elt1", {afterFinish: function () { + is(getElement('elt1').style.display != 'none', true, "shake ok"); + }}); + + fade("elt1", {afterFinish: function () { + is(getElement('elt1').style.display, 'none', "fade ok"); + }}); + + appear("elt1", {afterFinish: function () { + is(getElement('elt1').style.display != 'none', true, "appear ok"); + }}); + + toggle("elt1", "size", {afterFinish: function () { + is(getElement('elt1').style.display, 'none', "toggle size ok"); + }}); + + toggle("elt1", "size", {afterFinish: function () { + is(getElement('elt1').style.display != 'none', true, "toggle size reverse ok"); + }}); + + Morph("elt1", {"style": {"font-size": "2em"}, afterFinish: function () { + is(getStyle("elt1", "font-size"), "2em", "Morph OK"); + }}); + + Morph("elt1", {"style": {"font-size": "1em", "margin-left": "4px"}, afterFinish: function () { + is(getStyle("elt1", "font-size"), "1em", "Morph multiple (font) OK"); + is(getStyle("elt1", "margin-left"), "4px", "Morph multiple (margin) OK"); + }}); + + Morph("elt1", {"style": {"font-style": "italic"}, afterFinish: function () { + is(getStyle("elt1", "font-style"), "italic", "Morph generic property OK"); + }}); + + switchOff("elt1", {afterFinish: function () { + is(getElement('elt1').style.display, 'none', "switchOff ok"); + }}); + + grow("elt1", {afterFinish: function () { + is(getElement('elt1').style.display != 'none', true, "grow ok"); + }}); + + shrink("elt1", {afterFinish: function () { + is(getElement('elt1').style.display, 'none', "shrink ok"); + }}); + + showElement('elt1'); + dropOut("elt1", {afterFinish: function () { + is(getElement('elt1').style.display, 'none', "dropOut ok"); + }}); + + showElement('elt1'); + puff("elt1", {afterFinish: function () { + is(getElement('elt1').style.display, 'none', "puff ok"); + }}); + + showElement('elt1'); + fold("elt1", {afterFinish: function () { + is(getElement('elt1').style.display, 'none', "fold ok"); + }}); + + showElement('elt1'); + squish("elt1", {afterFinish: function () { + is(getElement('elt1').style.display, 'none', "squish ok"); + }}); + + slideUp("ctn1", {afterFinish: function () { + is(getElement('ctn1').style.display, 'none', "slideUp ok"); + }}); + + slideDown("ctn1", {afterFinish: function () { + is(getElement('ctn1').style.display != 'none', true, "slideDown ok"); + }}); + + blindDown("ctn1", {afterFinish: function () { + is(getElement('ctn1').style.display != 'none', true, "blindDown ok"); + }}); + + blindUp("ctn1", {afterFinish: function () { + is(getElement('ctn1').style.display, 'none', "blindUp ok"); + }}); + + multiple(["elt1", "ctn1"], appear, {afterFinish: function (effect) { + is(effect.element.style.display != 'none', true, "multiple ok"); + }}); + + toggle("elt3", "size", {afterFinish: function () { + is(getElement('elt3').style.display != 'none', true, "toggle with css ok"); + }}); + + toggle("elt3", "size", {afterFinish: function () { + is(getElement('elt3').style.display, 'none', "toggle with css ok"); + }}); + + var toTests = [roundElement, roundClass, tagifyText, Opacity, Move, Highlight, ScrollTo, Morph]; + for (var m in toTests) { + toTests[m]("elt1"); + ok(true, toTests[m].NAME + " doesn't need 'new' keyword"); + } + Scale("elt1", 1); + ok(true, "Scale doesn't need 'new' keyword"); + + ok(true, "visual suite finished"); + +} catch (err) { + + var s = "test suite failure!\n"; + var o = {}; + var k = null; + for (k in err) { + // ensure unique keys?! + if (!o[k]) { + s += k + ": " + err[k] + "\n"; + o[k] = err[k]; + } + } + ok ( false, s ); + SimpleTest.finish(); + +} +</script> +</pre> +</body> +</html> + diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Signal.js b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Signal.js new file mode 100644 index 0000000000..cbee78575f --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Signal.js @@ -0,0 +1,481 @@ +if (typeof(dojo) != 'undefined') { dojo.require('MochiKit.Signal'); } +if (typeof(JSAN) != 'undefined') { JSAN.use('MochiKit.Signal'); } +if (typeof(tests) == 'undefined') { tests = {}; } + +tests.test_Signal = function (t) { + + var submit = MochiKit.DOM.getElement('submit'); + var ident = null; + var i = 0; + var aFunction = function() { + t.ok(this === submit, "aFunction should have 'this' as submit"); + i++; + if (typeof(this.someVar) != 'undefined') { + i += this.someVar; + } + }; + + var aObject = {}; + aObject.aMethod = function() { + t.ok(this === aObject, "aMethod should have 'this' as aObject"); + i++; + }; + + ident = connect('submit', 'onclick', aFunction); + MochiKit.DOM.getElement('submit').click(); + t.is(i, 1, 'HTML onclick event can be connected to a function'); + + disconnect(ident); + MochiKit.DOM.getElement('submit').click(); + t.is(i, 1, 'HTML onclick can be disconnected from a function'); + + var submit = MochiKit.DOM.getElement('submit'); + + ident = connect(submit, 'onclick', aFunction); + submit.click(); + t.is(i, 2, 'Checking that a DOM element can be connected to a function'); + + disconnect(ident); + submit.click(); + t.is(i, 2, '...and then disconnected'); + + if (MochiKit.DOM.getElement('submit').fireEvent || + (document.createEvent && + typeof(document.createEvent('MouseEvents').initMouseEvent) == 'function')) { + + /* + + Adapted from: + http://www.devdaily.com/java/jwarehouse/jforum/tests/selenium/javascript/htmlutils.js.shtml + License: Apache + Copyright: Copyright 2004 ThoughtWorks, Inc + + */ + var triggerMouseEvent = function(element, eventType, canBubble) { + element = MochiKit.DOM.getElement(element); + canBubble = (typeof(canBubble) == 'undefined') ? true : canBubble; + if (element.fireEvent) { + var newEvt = document.createEventObject(); + newEvt.clientX = 1; + newEvt.clientY = 1; + newEvt.button = 1; + newEvt.detail = 3; + element.fireEvent('on' + eventType, newEvt); + } else if (document.createEvent && (typeof(document.createEvent('MouseEvents').initMouseEvent) == 'function')) { + var evt = document.createEvent('MouseEvents'); + evt.initMouseEvent(eventType, canBubble, true, // event, bubbles, cancelable + document.defaultView, 3, // view, detail (either scroll or # of clicks) + 1, 0, 0, 0, // screenX, screenY, clientX, clientY + false, false, false, false, // ctrlKey, altKey, shiftKey, metaKey + 0, null); // buttonCode, relatedTarget + element.dispatchEvent(evt); + } + }; + + var eventTest = function(e) { + i++; + t.ok((typeof(e.event()) === 'object'), 'checking that event() is an object'); + t.ok((typeof(e.type()) === 'string'), 'checking that type() is a string'); + t.ok((e.target() === MochiKit.DOM.getElement('submit')), 'checking that target is "submit"'); + t.ok((typeof(e.modifier()) === 'object'), 'checking that modifier() is an object'); + t.ok(e.modifier().alt === false, 'checking that modifier().alt is defined, but false'); + t.ok(e.modifier().ctrl === false, 'checking that modifier().ctrl is defined, but false'); + t.ok(e.modifier().meta === false, 'checking that modifier().meta is defined, but false'); + t.ok(e.modifier().shift === false, 'checking that modifier().shift is defined, but false'); + t.ok((typeof(e.mouse()) === 'object'), 'checking that mouse() is an object'); + t.ok((typeof(e.mouse().button) === 'object'), 'checking that mouse().button is an object'); + t.ok(e.mouse().button.left === true, 'checking that mouse().button.left is true'); + t.ok(e.mouse().button.middle === false, 'checking that mouse().button.middle is false'); + t.ok(e.mouse().button.right === false, 'checking that mouse().button.right is false'); + t.ok((typeof(e.mouse().page) === 'object'), 'checking that mouse().page is an object'); + t.ok((typeof(e.mouse().page.x) === 'number'), 'checking that mouse().page.x is a number'); + t.ok((typeof(e.mouse().page.y) === 'number'), 'checking that mouse().page.y is a number'); + t.ok((typeof(e.mouse().client) === 'object'), 'checking that mouse().client is an object'); + t.ok((typeof(e.mouse().client.x) === 'number'), 'checking that mouse().client.x is a number'); + t.ok((typeof(e.mouse().client.y) === 'number'), 'checking that mouse().client.y is a number'); + + /* these should not be defined */ + t.ok((typeof(e.relatedTarget()) === 'undefined'), 'checking that relatedTarget() is undefined'); + t.ok((typeof(e.key()) === 'undefined'), 'checking that key() is undefined'); + t.ok((typeof(e.mouse().wheel) === 'undefined'), 'checking that mouse().wheel is undefined'); + }; + + + ident = connect('submit', 'onmousedown', eventTest); + triggerMouseEvent('submit', 'mousedown', false); + t.is(i, 3, 'Connecting an event to an HTML object and firing a synthetic event'); + + disconnect(ident); + triggerMouseEvent('submit', 'mousedown', false); + t.is(i, 3, 'Disconnecting an event to an HTML object and firing a synthetic event'); + + ident = connect('submit', 'onmousewheel', function(e) { + i++; + t.ok((typeof(e.mouse()) === 'object'), 'checking that mouse() is an object'); + t.ok((typeof(e.mouse().wheel) === 'object'), 'checking that mouse().wheel is an object'); + t.ok((typeof(e.mouse().wheel.x) === 'number'), 'checking that mouse().wheel.x is a number'); + t.ok((typeof(e.mouse().wheel.y) === 'number'), 'checking that mouse().wheel.y is a number'); + }); + var nativeSignal = 'mousewheel'; + if (MochiKit.Signal._browserLacksMouseWheelEvent()) { + nativeSignal = 'DOMMouseScroll'; + } + triggerMouseEvent('submit', nativeSignal, false); + t.is(i, 4, 'Connecting a mousewheel event to an HTML object and firing a synthetic event'); + disconnect(ident); + triggerMouseEvent('submit', nativeSignal, false); + t.is(i, 4, 'Disconnecting a mousewheel event to an HTML object and firing a synthetic event'); + } + + // non-DOM tests + + var hasNoSignals = {}; + + var hasSignals = {someVar: 1}; + + var i = 0; + + var aFunction = function() { + i++; + if (typeof(this.someVar) != 'undefined') { + i += this.someVar; + } + }; + + var bFunction = function(someArg, someOtherArg) { + i += someArg + someOtherArg; + }; + + + var aObject = {}; + aObject.aMethod = function() { + i++; + }; + + aObject.bMethod = function() { + i++; + }; + + var bObject = {}; + bObject.bMethod = function() { + i++; + }; + + + ident = connect(hasSignals, 'signalOne', aFunction); + signal(hasSignals, 'signalOne'); + t.is(i, 2, 'Connecting function'); + i = 0; + + disconnect(ident); + signal(hasSignals, 'signalOne'); + t.is(i, 0, 'New style disconnecting function'); + i = 0; + + + ident = connect(hasSignals, 'signalOne', bFunction); + signal(hasSignals, 'signalOne', 1, 2); + t.is(i, 3, 'Connecting function'); + i = 0; + + disconnect(ident); + signal(hasSignals, 'signalOne', 1, 2); + t.is(i, 0, 'New style disconnecting function'); + i = 0; + + + connect(hasSignals, 'signalOne', aFunction); + signal(hasSignals, 'signalOne'); + t.is(i, 2, 'Connecting function'); + i = 0; + + disconnect(hasSignals, 'signalOne', aFunction); + signal(hasSignals, 'signalOne'); + t.is(i, 0, 'Old style disconnecting function'); + i = 0; + + + ident = connect(hasSignals, 'signalOne', aObject, aObject.aMethod); + signal(hasSignals, 'signalOne'); + t.is(i, 1, 'Connecting obj-function'); + i = 0; + + disconnect(ident); + signal(hasSignals, 'signalOne'); + t.is(i, 0, 'New style disconnecting obj-function'); + i = 0; + + connect(hasSignals, 'signalOne', aObject, aObject.aMethod); + signal(hasSignals, 'signalOne'); + t.is(i, 1, 'Connecting obj-function'); + i = 0; + + disconnect(hasSignals, 'signalOne', aObject, aObject.aMethod); + signal(hasSignals, 'signalOne'); + t.is(i, 0, 'Disconnecting obj-function'); + i = 0; + + + ident = connect(hasSignals, 'signalTwo', aObject, 'aMethod'); + signal(hasSignals, 'signalTwo'); + t.is(i, 1, 'Connecting obj-string'); + i = 0; + + disconnect(ident); + signal(hasSignals, 'signalTwo'); + t.is(i, 0, 'New style disconnecting obj-string'); + i = 0; + + + connect(hasSignals, 'signalTwo', aObject, 'aMethod'); + signal(hasSignals, 'signalTwo'); + t.is(i, 1, 'Connecting obj-string'); + i = 0; + + disconnect(hasSignals, 'signalTwo', aObject, 'aMethod'); + signal(hasSignals, 'signalTwo'); + t.is(i, 0, 'Old style disconnecting obj-string'); + i = 0; + + + var shouldRaise = function() { return undefined.attr; }; + + try { + connect(hasSignals, 'signalOne', shouldRaise); + signal(hasSignals, 'signalOne'); + t.ok(false, 'An exception was not raised'); + } catch (e) { + t.ok(true, 'An exception was raised'); + } + disconnect(hasSignals, 'signalOne', shouldRaise); + t.is(i, 0, 'Exception raised, signal should not have fired'); + i = 0; + + + connect(hasSignals, 'signalOne', aObject, 'aMethod'); + connect(hasSignals, 'signalOne', aObject, 'bMethod'); + signal(hasSignals, 'signalOne'); + t.is(i, 2, 'Connecting one signal to two slots in one object'); + i = 0; + + disconnect(hasSignals, 'signalOne', aObject, 'aMethod'); + disconnect(hasSignals, 'signalOne', aObject, 'bMethod'); + signal(hasSignals, 'signalOne'); + t.is(i, 0, 'Disconnecting one signal from two slots in one object'); + i = 0; + + + connect(hasSignals, 'signalOne', aObject, 'aMethod'); + connect(hasSignals, 'signalOne', bObject, 'bMethod'); + signal(hasSignals, 'signalOne'); + t.is(i, 2, 'Connecting one signal to two slots in two objects'); + i = 0; + + disconnect(hasSignals, 'signalOne', aObject, 'aMethod'); + disconnect(hasSignals, 'signalOne', bObject, 'bMethod'); + signal(hasSignals, 'signalOne'); + t.is(i, 0, 'Disconnecting one signal from two slots in two objects'); + i = 0; + + + try { + connect(nothing, 'signalOne', aObject, 'aMethod'); + signal(nothing, 'signalOne'); + t.ok(false, 'An exception was not raised when connecting undefined'); + } catch (e) { + t.ok(true, 'An exception was raised when connecting undefined'); + } + + try { + disconnect(nothing, 'signalOne', aObject, 'aMethod'); + t.ok(false, 'An exception was not raised when disconnecting undefined'); + } catch (e) { + t.ok(true, 'An exception was raised when disconnecting undefined'); + } + + + try { + connect(hasSignals, 'signalOne', nothing); + signal(hasSignals, 'signalOne'); + t.ok(false, 'An exception was not raised when connecting an undefined function'); + } catch (e) { + t.ok(true, 'An exception was raised when connecting an undefined function'); + } + + try { + disconnect(hasSignals, 'signalOne', nothing); + t.ok(false, 'An exception was not raised when disconnecting an undefined function'); + } catch (e) { + t.ok(true, 'An exception was raised when disconnecting an undefined function'); + } + + + try { + connect(hasSignals, 'signalOne', aObject, aObject.nothing); + signal(hasSignals, 'signalOne'); + t.ok(false, 'An exception was not raised when connecting an undefined method'); + } catch (e) { + t.ok(true, 'An exception was raised when connecting an undefined method'); + } + + try { + connect(hasSignals, 'signalOne', aObject, 'nothing'); + signal(hasSignals, 'signalOne'); + t.ok(false, 'An exception was not raised when connecting an undefined method (as string)'); + } catch (e) { + t.ok(true, 'An exception was raised when connecting an undefined method (as string)'); + } + + t.is(i, 0, 'Signals should not have fired'); + + connect(hasSignals, 'signalOne', aFunction); + connect(hasSignals, 'signalOne', aObject, 'aMethod'); + disconnectAll(hasSignals, 'signalOne'); + signal(hasSignals, 'signalOne'); + t.is(i, 0, 'disconnectAll works with single explicit signal'); + i = 0; + + connect(hasSignals, 'signalOne', aFunction); + connect(hasSignals, 'signalOne', aObject, 'aMethod'); + connect(hasSignals, 'signalTwo', aFunction); + connect(hasSignals, 'signalTwo', aObject, 'aMethod'); + disconnectAll(hasSignals, 'signalOne'); + signal(hasSignals, 'signalOne'); + t.is(i, 0, 'disconnectAll works with single explicit signal'); + signal(hasSignals, 'signalTwo'); + t.is(i, 3, 'disconnectAll does not disconnect unrelated signals'); + i = 0; + + connect(hasSignals, 'signalOne', aFunction); + connect(hasSignals, 'signalOne', aObject, 'aMethod'); + connect(hasSignals, 'signalTwo', aFunction); + connect(hasSignals, 'signalTwo', aObject, 'aMethod'); + disconnectAll(hasSignals, 'signalOne', 'signalTwo'); + signal(hasSignals, 'signalOne'); + signal(hasSignals, 'signalTwo'); + t.is(i, 0, 'disconnectAll works with two explicit signals'); + i = 0; + + connect(hasSignals, 'signalOne', aFunction); + connect(hasSignals, 'signalOne', aObject, 'aMethod'); + connect(hasSignals, 'signalTwo', aFunction); + connect(hasSignals, 'signalTwo', aObject, 'aMethod'); + disconnectAll(hasSignals, ['signalOne', 'signalTwo']); + signal(hasSignals, 'signalOne'); + signal(hasSignals, 'signalTwo'); + t.is(i, 0, 'disconnectAll works with two explicit signals as a list'); + i = 0; + + connect(hasSignals, 'signalOne', aFunction); + connect(hasSignals, 'signalOne', aObject, 'aMethod'); + connect(hasSignals, 'signalTwo', aFunction); + connect(hasSignals, 'signalTwo', aObject, 'aMethod'); + disconnectAll(hasSignals); + signal(hasSignals, 'signalOne'); + signal(hasSignals, 'signalTwo'); + t.is(i, 0, 'disconnectAll works with implicit signals'); + i = 0; + + var toggle = function() { + disconnectAll(hasSignals, 'signalOne'); + connect(hasSignals, 'signalOne', aFunction); + i++; + }; + + connect(hasSignals, 'signalOne', aFunction); + connect(hasSignals, 'signalTwo', function() { i++; }); + connect(hasSignals, 'signalTwo', toggle); + connect(hasSignals, 'signalTwo', function() { i++; }); // #147 + connect(hasSignals, 'signalTwo', function() { i++; }); + signal(hasSignals, 'signalTwo'); + t.is(i, 4, 'disconnectAll fired in a signal loop works'); + i = 0; + disconnectAll('signalOne'); + disconnectAll('signalTwo'); + + var testfunc = function () { arguments.callee.count++; }; + testfunc.count = 0; + var testObj = { + methOne: function () { this.countOne++; }, countOne: 0, + methTwo: function () { this.countTwo++; }, countTwo: 0 + }; + connect(hasSignals, 'signalOne', testfunc); + connect(hasSignals, 'signalTwo', testfunc); + signal(hasSignals, 'signalOne'); + signal(hasSignals, 'signalTwo'); + t.is(testfunc.count, 2, 'disconnectAllTo func precondition'); + disconnectAllTo(testfunc); + signal(hasSignals, 'signalOne'); + signal(hasSignals, 'signalTwo'); + t.is(testfunc.count, 2, 'disconnectAllTo func'); + + connect(hasSignals, 'signalOne', testObj, 'methOne'); + connect(hasSignals, 'signalTwo', testObj, 'methTwo'); + signal(hasSignals, 'signalOne'); + signal(hasSignals, 'signalTwo'); + t.is(testObj.countOne, 1, 'disconnectAllTo obj precondition'); + t.is(testObj.countTwo, 1, 'disconnectAllTo obj precondition'); + disconnectAllTo(testObj); + signal(hasSignals, 'signalOne'); + signal(hasSignals, 'signalTwo'); + t.is(testObj.countOne, 1, 'disconnectAllTo obj'); + t.is(testObj.countTwo, 1, 'disconnectAllTo obj'); + + testObj.countOne = testObj.countTwo = 0; + connect(hasSignals, 'signalOne', testObj, 'methOne'); + connect(hasSignals, 'signalTwo', testObj, 'methTwo'); + disconnectAllTo(testObj, 'methOne'); + signal(hasSignals, 'signalOne'); + signal(hasSignals, 'signalTwo'); + t.is(testObj.countOne, 0, 'disconnectAllTo obj+str'); + t.is(testObj.countTwo, 1, 'disconnectAllTo obj+str'); + + has__Connect = { + count: 0, + __connect__: function (ident) { + this.count += arguments.length; + disconnect(ident); + } + }; + connect(has__Connect, 'signalOne', function() { + t.fail("__connect__ should have disconnected signal"); + }); + t.is(has__Connect.count, 3, '__connect__ is called when it exists'); + signal(has__Connect, 'signalOne'); + + has__Disconnect = { + count: 0, + __disconnect__: function (ident) { + this.count += arguments.length; + } + }; + connect(has__Disconnect, 'signalOne', aFunction); + connect(has__Disconnect, 'signalTwo', aFunction); + disconnectAll(has__Disconnect); + t.is(has__Disconnect.count, 8, '__disconnect__ is called when it exists'); + + var events = {}; + var test_ident = connect(events, "test", function() { + var fail_ident = connect(events, "fail", function () { + events.failed = true; + }); + disconnect(fail_ident); + signal(events, "fail"); + }); + signal(events, "test"); + t.is(events.failed, undefined, 'disconnected slots do not fire'); + + var sink = {f: function (ev) { this.ev = ev; }}; + var src = {}; + bindMethods(sink); + connect(src, 'signal', sink.f); + signal(src, 'signal', 'worked'); + t.is(sink.ev, 'worked', 'custom signal does not re-bind methods'); + + var lateObj = { fun: function() { this.value = 1; } }; + connect(src, 'signal', lateObj, "fun"); + signal(src, 'signal'); + lateObj.fun = function() { this.value = 2; }; + signal(src, 'signal'); + t.is(lateObj.value, 2, 'connect uses late function binding'); +}; diff --git a/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js b/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js new file mode 100644 index 0000000000..0e95565019 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js @@ -0,0 +1,1073 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Accessible states used to check node's state from the accessiblity API + * perspective. + * + * Note: if gecko is built with --disable-accessibility, the interfaces + * are not defined. This is why we use getters instead to be able to use + * these statically. + */ + +this.AccessibilityUtils = (function () { + const FORCE_DISABLE_ACCESSIBILITY_PREF = "accessibility.force_disabled"; + + // Accessible states. + const { STATE_FOCUSABLE, STATE_INVISIBLE, STATE_LINKED, STATE_UNAVAILABLE } = + Ci.nsIAccessibleStates; + + // Accessible action for showing long description. + const CLICK_ACTION = "click"; + + // Roles that are considered focusable with the keyboard. + const KEYBOARD_FOCUSABLE_ROLES = new Set([ + Ci.nsIAccessibleRole.ROLE_BUTTONMENU, + Ci.nsIAccessibleRole.ROLE_CHECKBUTTON, + Ci.nsIAccessibleRole.ROLE_COMBOBOX, + Ci.nsIAccessibleRole.ROLE_EDITCOMBOBOX, + Ci.nsIAccessibleRole.ROLE_ENTRY, + Ci.nsIAccessibleRole.ROLE_LINK, + Ci.nsIAccessibleRole.ROLE_LISTBOX, + Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT, + Ci.nsIAccessibleRole.ROLE_PUSHBUTTON, + Ci.nsIAccessibleRole.ROLE_RADIOBUTTON, + Ci.nsIAccessibleRole.ROLE_SLIDER, + Ci.nsIAccessibleRole.ROLE_SPINBUTTON, + Ci.nsIAccessibleRole.ROLE_SUMMARY, + Ci.nsIAccessibleRole.ROLE_SWITCH, + Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON, + ]); + + // Roles that are user interactive. + const INTERACTIVE_ROLES = new Set([ + ...KEYBOARD_FOCUSABLE_ROLES, + Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM, + Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION, + Ci.nsIAccessibleRole.ROLE_COMBOBOX_OPTION, + Ci.nsIAccessibleRole.ROLE_MENUITEM, + Ci.nsIAccessibleRole.ROLE_OPTION, + Ci.nsIAccessibleRole.ROLE_OUTLINE, + Ci.nsIAccessibleRole.ROLE_OUTLINEITEM, + Ci.nsIAccessibleRole.ROLE_PAGETAB, + Ci.nsIAccessibleRole.ROLE_PARENT_MENUITEM, + Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM, + Ci.nsIAccessibleRole.ROLE_RICH_OPTION, + ]); + + // Roles that are considered interactive when they are focusable. + const INTERACTIVE_IF_FOCUSABLE_ROLES = new Set([ + // If article is focusable, we can assume it is inside a feed. + Ci.nsIAccessibleRole.ROLE_ARTICLE, + // Column header can be focusable. + Ci.nsIAccessibleRole.ROLE_COLUMNHEADER, + Ci.nsIAccessibleRole.ROLE_GRID_CELL, + Ci.nsIAccessibleRole.ROLE_MENUBAR, + Ci.nsIAccessibleRole.ROLE_MENUPOPUP, + Ci.nsIAccessibleRole.ROLE_PAGETABLIST, + // Row header can be focusable. + Ci.nsIAccessibleRole.ROLE_ROWHEADER, + Ci.nsIAccessibleRole.ROLE_SCROLLBAR, + Ci.nsIAccessibleRole.ROLE_SEPARATOR, + Ci.nsIAccessibleRole.ROLE_TOOLBAR, + ]); + + // Roles that are considered form controls. + const FORM_ROLES = new Set([ + Ci.nsIAccessibleRole.ROLE_CHECKBUTTON, + Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION, + Ci.nsIAccessibleRole.ROLE_COMBOBOX, + Ci.nsIAccessibleRole.ROLE_EDITCOMBOBOX, + Ci.nsIAccessibleRole.ROLE_ENTRY, + Ci.nsIAccessibleRole.ROLE_LISTBOX, + Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT, + Ci.nsIAccessibleRole.ROLE_PROGRESSBAR, + Ci.nsIAccessibleRole.ROLE_RADIOBUTTON, + Ci.nsIAccessibleRole.ROLE_SLIDER, + Ci.nsIAccessibleRole.ROLE_SPINBUTTON, + Ci.nsIAccessibleRole.ROLE_SWITCH, + ]); + + const DEFAULT_ENV = Object.freeze({ + // Checks that accessible object has at least one accessible action. + actionCountRule: true, + // Checks that accessible object (and its corresponding node) is focusable + // (has focusable state and its node's tabindex is not set to -1). + focusableRule: true, + // Checks that clickable accessible object (and its corresponding node) has + // appropriate interactive semantics. + ifClickableThenInteractiveRule: true, + // Checks that accessible object has a role that is considered to be + // interactive. + interactiveRule: true, + // Checks that accessible object has a non-empty label. + labelRule: true, + // Checks that a node is enabled and is expected to be enabled via + // the accessibility API. + mustBeEnabled: true, + // Checks that a node has a corresponding accessible object. + mustHaveAccessibleRule: true, + // Checks that accessible object (and its corresponding node) have a non- + // negative tabindex. Platform accessibility API still sets focusable state + // on tabindex=-1 nodes. + nonNegativeTabIndexRule: true, + }); + + let gA11YChecks = false; + + let gEnv = { + ...DEFAULT_ENV, + }; + + /** + * Get role attribute for an accessible object if specified for its + * corresponding {@code DOMNode}. + * + * @param {nsIAccessible} accessible + * Accessible for which to determine its role attribute value. + * + * @returns {String} + * Role attribute value if specified. + */ + function getAriaRoles(accessible) { + try { + return accessible.attributes.getStringProperty("xml-roles"); + } catch (e) { + // No xml-roles. nsPersistentProperties throws if the attribute for a key + // is not found. + } + + return ""; + } + + /** + * Get related accessible objects that are targets of labelled by relation e.g. + * labels. + * @param {nsIAccessible} accessible + * Accessible objects to get labels for. + * + * @returns {Array} + * A list of accessible objects that are labels for a given accessible. + */ + function getLabels(accessible) { + const relation = accessible.getRelationByType( + Ci.nsIAccessibleRelation.RELATION_LABELLED_BY + ); + return [...relation.getTargets().enumerate(Ci.nsIAccessible)]; + } + + /** + * Test if an accessible has a {@code hidden} attribute. + * + * @param {nsIAccessible} accessible + * Accessible object. + * + * @return {boolean} + * True if the accessible object has a {@code hidden} attribute, false + * otherwise. + */ + function hasHiddenAttribute(accessible) { + let hidden = false; + try { + hidden = accessible.attributes.getStringProperty("hidden"); + } catch (e) {} + // If the property is missing, error will be thrown + return hidden && hidden === "true"; + } + + /** + * Test if an accessible is hidden from the user. + * + * @param {nsIAccessible} accessible + * Accessible object. + * + * @return {boolean} + * True if accessible is hidden from user, false otherwise. + */ + function isHidden(accessible) { + if (!accessible) { + return true; + } + + while (accessible) { + if (hasHiddenAttribute(accessible)) { + return true; + } + + accessible = accessible.parent; + } + + return false; + } + + /** + * Check if an accessible has a given state. + * + * @param {nsIAccessible} accessible + * Accessible object to test. + * @param {number} stateToMatch + * State to match. + * + * @return {boolean} + * True if |accessible| has |stateToMatch|, false otherwise. + */ + function matchState(accessible, stateToMatch) { + const state = {}; + accessible.getState(state, {}); + + return !!(state.value & stateToMatch); + } + + /** + * Determine if an accessible is a keyboard focusable browser toolbar button. + * Browser toolbar buttons aren't keyboard focusable in the usual way. + * Instead, focus is managed by JS code which sets tabindex on a single + * button at a time. Thus, we need to special case the focusable check for + * these buttons. + */ + function isKeyboardFocusableBrowserToolbarButton(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + const toolbar = + node.closest("toolbar") || + node.flattenedTreeParentNode.closest("toolbar"); + if (!toolbar || toolbar.getAttribute("keyNav") != "true") { + return false; + } + // The Go button in the Url Bar is an example of a purposefully + // non-focusable image toolbar button that provides an mouse/touch-only + // control for the search query submission, while a keyboard user could + // press `Enter` to do it. Similarly, two scroll buttons that appear when + // toolbar is overflowing, and keyboard-only users would actually scroll + // tabs in the toolbar while trying to navigate to these controls. When + // toolbarbuttons are redundant for keyboard users, we do not want to + // create an extra tab stop for such controls, thus we are expecting the + // button markup to include `keyNav="false"` attribute to flag it. + if (node.getAttribute("keyNav") == "false") { + const ariaRoles = getAriaRoles(accessible); + return ( + ariaRoles.includes("button") || + accessible.role == Ci.nsIAccessibleRole.ROLE_PUSHBUTTON + ); + } + return node.ownerGlobal.ToolbarKeyboardNavigator._isButton(node); + } + + /** + * Determine if an accessible is a keyboard focusable control within a Firefox + * View list. The main landmark of the Firefox View has role="application" for + * users to expect a custom keyboard navigation pattern. Controls within this + * area aren't keyboard focusable in the usual way. Instead, focus is managed + * by JS code which sets tabindex on a single control within each list at a + * time. Thus, we need to special case the focusable check for these controls. + */ + function isKeyboardFocusableFxviewControlInApplication(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + // Firefox View application rows currently include only buttons and links: + if ( + !node.className.includes("fxview-tab-row-") || + (accessible.role != Ci.nsIAccessibleRole.ROLE_PUSHBUTTON && + accessible.role != Ci.nsIAccessibleRole.ROLE_LINK) + ) { + return false; // Not a button or a link in a Firefox View app. + } + // ToDo: We may eventually need to support intervening generics between + // a list and its listitem here and/or aria-owns lists. + const listitemAcc = accessible.parent; + const listAcc = listitemAcc.parent; + if ( + (!listAcc || listAcc.role != Ci.nsIAccessibleRole.ROLE_LIST) && + (!listitemAcc || listitemAcc.role != Ci.nsIAccessibleRole.ROLE_LISTITEM) + ) { + return false; // This button/link isn't inside a listitem within a list. + } + // All listitems should be not focusable while both a button and a link + // within each list item might have tabindex="-1". + if ( + node.tabIndex && + matchState(accessible, STATE_FOCUSABLE) && + !matchState(listitemAcc, STATE_FOCUSABLE) + ) { + // ToDo: We may eventually need to support lists which use aria-owns here. + // Check that there is only one keyboard reachable control within the list. + const childCount = listAcc.childCount; + let foundFocusable = false; + for (let c = 0; c < childCount; c++) { + const listitem = listAcc.getChildAt(c); + const listitemChildCount = listitem.childCount; + for (let i = 0; i < listitemChildCount; i++) { + const listitemControl = listitem.getChildAt(i); + // Use tabIndex rather than a11y focusable state because all controls + // within the listitem might have tabindex="-1". + if (listitemControl.DOMNode.tabIndex == 0) { + if (foundFocusable) { + // Only one control within a list should be focusable. + // ToDo: Fine-tune the a11y-check error message generated in this case. + // Strictly speaking, it's not ideal that we're performing an action + // from an is function, which normally only queries something without + // any externally observable behaviour. That said, fixing that would + // involve different return values for different cases (not a list, + // too many focusable listitem controls, etc) so we could move the + // a11yFail call to the caller. + a11yFail( + "Only one control should be focusable in a list", + accessible + ); + return false; + } + foundFocusable = true; + } + } + } + return foundFocusable; + } + return false; + } + + /** + * Determine if an accessible is a keyboard focusable option within a listbox. + * We use it in the Url bar results - these controls are't keyboard focusable + * in the usual way. Instead, focus is managed by JS code which sets tabindex + * on a single option at a time. Thus, we need to special case the focusable + * check for these option items. + */ + function isKeyboardFocusableOption(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + const urlbarListbox = node.closest(".urlbarView-results"); + if (!urlbarListbox || urlbarListbox.getAttribute("role") != "listbox") { + return false; + } + return node.getAttribute("role") == "option"; + } + + /** + * Determine if an accessible is a keyboard focusable PanelMultiView control. + * These controls aren't keyboard focusable in the usual way. Instead, focus + * is managed by JS code which sets tabindex dynamically. Thus, we need to + * special case the focusable check for these controls. + */ + function isKeyboardFocusablePanelMultiViewControl(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + const panelview = node.closest("panelview"); + if (!panelview || panelview.hasAttribute("disablekeynav")) { + return false; + } + return ( + node.ownerGlobal.PanelView.forNode(panelview)._tabNavigableWalker.filter( + node + ) == NodeFilter.FILTER_ACCEPT + ); + } + + /** + * Determine if an accessible is a keyboard focusable tab within a tablist. + * Per the ARIA design pattern, these controls aren't keyboard focusable in + * the usual way. Instead, focus is managed by JS code which sets tabindex on + * a single tab at a time. Thus, we need to special case the focusable check + * for these tab controls. + */ + function isKeyboardFocusableTabInTablist(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + if (accessible.role != Ci.nsIAccessibleRole.ROLE_PAGETAB) { + return false; // Not a tab. + } + // ToDo: We may eventually need to support intervening generics between + // a tab and its tablist here. + const tablist = accessible.parent; + if (!tablist || tablist.role != Ci.nsIAccessibleRole.ROLE_PAGETABLIST) { + return false; // The tab isn't inside a tablist. + } + // ToDo: We may eventually need to support tablists which use + // aria-activedescendant here. + // Check that there is only one keyboard reachable tab. + const childCount = tablist.childCount; + let foundFocusable = false; + for (let c = 0; c < childCount; c++) { + const tab = tablist.getChildAt(c); + // Use tabIndex rather than a11y focusable state because all tabs might + // have tabindex="-1". + if (tab.DOMNode.tabIndex == 0) { + if (foundFocusable) { + // Only one tab within a tablist should be focusable. + // ToDo: Fine-tune the a11y-check error message generated in this case. + // Strictly speaking, it's not ideal that we're performing an action + // from an is function, which normally only queries something without + // any externally observable behaviour. That said, fixing that would + // involve different return values for different cases (not a tab, + // too many focusable tabs, etc) so we could move the a11yFail call + // to the caller. + a11yFail("Only one tab should be focusable in a tablist", accessible); + return false; + } + foundFocusable = true; + } + } + return foundFocusable; + } + + /** + * Determine if an accessible is a keyboard focusable button in the url bar. + * Url bar buttons aren't keyboard focusable in the usual way. Instead, + * focus is managed by JS code which sets tabindex on a single button at a + * time. Thus, we need to special case the focusable check for these buttons. + * This also applies to the search bar buttons that reuse the same pattern. + */ + function isKeyboardFocusableUrlbarButton(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + const isUrlBar = + node + .closest(".urlbarView > .search-one-offs") + ?.getAttribute("disabletab") == "true"; + const isSearchBar = + node + .closest("#PopupSearchAutoComplete > .search-one-offs") + ?.getAttribute("is_searchbar") == "true"; + return ( + (isUrlBar || isSearchBar) && + node.getAttribute("tabindex") == "-1" && + node.tagName == "button" && + node.classList.contains("searchbar-engine-one-off-item") + ); + } + + /** + * Determine if an accessible is a keyboard focusable XUL tab. + * Only one tab is focusable at a time, but after focusing it, you can use + * the keyboard to focus other tabs. + */ + function isKeyboardFocusableXULTab(accessible) { + const node = accessible.DOMNode; + return node && XULElement.isInstance(node) && node.tagName == "tab"; + } + + /** + * XUL treecol elements currently aren't focusable, making them inaccessible. + * For now, we don't flag these as a failure to avoid breaking multiple tests. + * ToDo: We should remove this exception after this is fixed in bug 1848397. + */ + function isInaccessibleXulTreecol(node) { + if (!node || !node.ownerGlobal) { + return false; + } + const listheader = node.flattenedTreeParentNode; + if (listheader.tagName !== "listheader" || node.tagName !== "treecol") { + return false; + } + return true; + } + + /** + * Determine if an accessible is a combobox container of the url bar. We + * intentionally leave this element unlabeled, because its child is a search + * input that is the target and main control of this component. In general, we + * want to avoid duplication in the label announcement when a user focuses the + * input. Both NVDA and VO ignore the label on at least one of these controls + * if both have a label. But the bigger concern here is that it's very + * difficult to keep the accessible name synchronized between the combobox and + * the input. Thus, we need to special case the label check for this control. + */ + function isUnlabeledUrlBarCombobox(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + const ariaRoles = getAriaRoles(accessible); + // There are only two cases of this pattern: <moz-input-box> and <searchbar> + const isMozInputBox = + node.tagName == "moz-input-box" && + node.classList.contains("urlbar-input-box"); + const isSearchbar = node.tagName == "searchbar" && node.id == "searchbar"; + return (isMozInputBox || isSearchbar) && ariaRoles.includes("combobox"); + } + + /** + * Determine if an accessible is an option within the url bar. We know each + * url bar option is accessible, but it disappears as soon as it is clicked + * during tests and the a11y-checks do not have time to test the label, + * because the Fluent localization is not yet completed by then. Thus, we + * need to special case the label check for these controls. + */ + function isUnlabeledUrlBarOption(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + const ariaRoles = getAriaRoles(accessible); + return ( + node.tagName == "span" && + ariaRoles.includes("option") && + node.classList.contains("urlbarView-row-inner") && + node.hasAttribute("data-l10n-id") + ); + } + + /** + * Determine if an accessible is a menuitem within the XUL menu. We know each + * menuitem is accessible, but it disappears as soon as it is clicked during + * tests and the a11y-checks do not have time to test the label, because the + * Fluent localization is not yet completed by then. Thus, we need to special + * case the label check for these controls. + */ + function isUnlabeledMenuitem(accessible) { + const node = accessible.DOMNode; + if (!node || !node.ownerGlobal) { + return false; + } + let hasLabel = false; + for (const child of node.childNodes) { + if (child.tagName == "label") { + hasLabel = true; + } + } + return ( + accessible.role == Ci.nsIAccessibleRole.ROLE_MENUITEM && + accessible.parent.role == Ci.nsIAccessibleRole.ROLE_MENUPOPUP && + hasLabel && + node.hasAttribute("data-l10n-id") + ); + } + + /** + * Determine if a node is a XUL element for which tabIndex should be ignored. + * Some XUL elements report -1 for the .tabIndex property, even though they + * are in fact keyboard focusable. + */ + function shouldIgnoreTabIndex(node) { + if (!XULElement.isInstance(node)) { + return false; + } + return node.tagName == "label" && node.getAttribute("is") == "text-link"; + } + + /** + * Determine if accessible is focusable with the keyboard. + * + * @param {nsIAccessible} accessible + * Accessible for which to determine if it is keyboard focusable. + * + * @returns {Boolean} + * True if focusable with the keyboard. + */ + function isKeyboardFocusable(accessible) { + if ( + isKeyboardFocusableBrowserToolbarButton(accessible) || + isKeyboardFocusableOption(accessible) || + isKeyboardFocusablePanelMultiViewControl(accessible) || + isKeyboardFocusableUrlbarButton(accessible) || + isKeyboardFocusableXULTab(accessible) || + isKeyboardFocusableTabInTablist(accessible) || + isKeyboardFocusableFxviewControlInApplication(accessible) + ) { + return true; + } + // State will be focusable even if the tabindex is negative. + const node = accessible.DOMNode; + const role = accessible.role; + return ( + matchState(accessible, STATE_FOCUSABLE) && + // Platform accessibility will still report STATE_FOCUSABLE even with the + // tabindex="-1" so we need to check that it is >= 0 to be considered + // keyboard focusable. + (!gEnv.nonNegativeTabIndexRule || + node.tabIndex > -1 || + node.closest('[aria-activedescendant][tabindex="0"]') || + // If an ARIA toolbar uses a roving tabindex, some controls on the + // toolbar might not currently be focusable even though they can be + // reached with arrow keys and become focusable at that point. + ((role == Ci.nsIAccessibleRole.ROLE_PUSHBUTTON || + role == Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON) && + node.closest('[role="toolbar"]')) || + shouldIgnoreTabIndex(node)) + ); + } + + function buildMessage(message, DOMNode) { + if (DOMNode) { + const { id, tagName, className } = DOMNode; + message += `: id: ${id}, tagName: ${tagName}, className: ${className}`; + } + + return message; + } + + /** + * Fail a test with a given message because of an issue with a given + * accessible object. This is used for cases where there's an actual + * accessibility failure that prevents UI from being accessible to keyboard/AT + * users. + * + * @param {String} message + * @param {nsIAccessible} accessible + * Accessible to log along with the failure message. + */ + function a11yFail(message, { DOMNode }) { + SpecialPowers.SimpleTest.ok(false, buildMessage(message, DOMNode)); + } + + /** + * Log a todo statement with a given message because of an issue with a given + * accessible object. This is used for cases where accessibility best + * practices are not followed or for something that is not as severe to be + * considered a failure. + * @param {String} message + * @param {nsIAccessible} accessible + * Accessible to log along with the todo message. + */ + function a11yWarn(message, { DOMNode }) { + SpecialPowers.SimpleTest.todo(false, buildMessage(message, DOMNode)); + } + + /** + * Test if the node's unavailable via the accessibility API. + * + * @param {nsIAccessible} accessible + * Accessible object. + */ + function assertEnabled(accessible) { + if (gEnv.mustBeEnabled && matchState(accessible, STATE_UNAVAILABLE)) { + a11yFail( + "Node expected to be enabled but is disabled via the accessibility API", + accessible + ); + } + } + + /** + * Test if it is possible to focus on a node with the keyboard. This method + * also checks for additional keyboard focus issues that might arise. + * + * @param {nsIAccessible} accessible + * Accessible object for a node. + */ + function assertFocusable(accessible) { + if ( + gEnv.mustBeEnabled && + gEnv.focusableRule && + !isKeyboardFocusable(accessible) + ) { + const ariaRoles = getAriaRoles(accessible); + // Do not force ARIA combobox or listbox to be focusable. + if (!ariaRoles.includes("combobox") && !ariaRoles.includes("listbox")) { + a11yFail("Node is not focusable via the accessibility API", accessible); + } + + return; + } + + if (!INTERACTIVE_IF_FOCUSABLE_ROLES.has(accessible.role)) { + // ROLE_TABLE is used for grids too which are considered interactive. + if ( + accessible.role === Ci.nsIAccessibleRole.ROLE_TABLE && + !getAriaRoles(accessible).includes("grid") + ) { + a11yWarn( + "Focusable nodes should have interactive semantics", + accessible + ); + + return; + } + } + + if (accessible.DOMNode.tabIndex > 0) { + a11yWarn("Avoid using tabindex attribute greater than zero", accessible); + } + } + + /** + * Test if it is possible to interact with a node via the accessibility API. + * + * @param {nsIAccessible} accessible + * Accessible object for a node. + */ + function assertInteractive(accessible) { + if ( + gEnv.mustBeEnabled && + gEnv.actionCountRule && + accessible.actionCount === 0 + ) { + a11yFail("Node does not support any accessible actions", accessible); + + return; + } + + if ( + gEnv.mustBeEnabled && + gEnv.interactiveRule && + !INTERACTIVE_ROLES.has(accessible.role) + ) { + if ( + // Labels that have a label for relation with their target are clickable. + (accessible.role !== Ci.nsIAccessibleRole.ROLE_LABEL || + accessible.getRelationByType( + Ci.nsIAccessibleRelation.RELATION_LABEL_FOR + ).targetsCount === 0) && + // Images that are inside an anchor (have linked state). + (accessible.role !== Ci.nsIAccessibleRole.ROLE_GRAPHIC || + !matchState(accessible, STATE_LINKED)) + ) { + // Look for click action in the list of actions. + for (let i = 0; i < accessible.actionCount; i++) { + if ( + gEnv.ifClickableThenInteractiveRule && + accessible.getActionName(i) === CLICK_ACTION + ) { + a11yFail( + "Clickable nodes must have interactive semantics", + accessible + ); + } + } + } + + a11yFail( + "Node does not have a correct interactive role and may not be " + + "manipulated via the accessibility API", + accessible + ); + } + } + + /** + * Test if the node is labelled appropriately for accessibility API. + * + * @param {nsIAccessible} accessible + * Accessible object for a node. + */ + function assertLabelled(accessible, allowRecurse = true) { + const { DOMNode } = accessible; + let name = accessible.name; + if (!name) { + if ( + isUnlabeledUrlBarCombobox(accessible) || + isUnlabeledUrlBarOption(accessible) || + isUnlabeledMenuitem(accessible) + ) { + return; + } + // If text has just been inserted into the tree, the a11y engine might not + // have picked it up yet. + forceRefreshDriverTick(DOMNode); + try { + name = accessible.name; + } catch (e) { + // The Accessible died because the DOM node was removed or hidden. + if (gEnv.labelRule) { + a11yWarn("Unlabeled element removed before l10n finished", { + DOMNode, + }); + } + return; + } + const doc = DOMNode.ownerDocument; + if ( + !name && + allowRecurse && + gEnv.labelRule && + doc.hasPendingL10nMutations + ) { + // There are pending async l10n mutations which might result in a valid + // accessible name. Try this check again once l10n is finished. + doc.addEventListener( + "L10nMutationsFinished", + () => { + try { + accessible.name; + } catch (e) { + // The Accessible died because the DOM node was removed or hidden. + a11yWarn("Unlabeled element removed before l10n finished", { + DOMNode, + }); + return; + } + assertLabelled(accessible, false); + }, + { once: true } + ); + return; + } + } + if (name) { + name = name.trim(); + } + if (gEnv.labelRule && !name) { + a11yFail("Interactive elements must be labeled", accessible); + + return; + } + + if (FORM_ROLES.has(accessible.role)) { + const labels = getLabels(accessible); + const hasNameFromVisibleLabel = labels.some( + label => !matchState(label, STATE_INVISIBLE) + ); + + if (!hasNameFromVisibleLabel) { + a11yWarn("Form elements should have a visible text label", accessible); + } + } else if ( + accessible.role === Ci.nsIAccessibleRole.ROLE_LINK && + DOMNode.nodeName === "AREA" && + DOMNode.hasAttribute("href") + ) { + const alt = DOMNode.getAttribute("alt"); + if (alt && alt.trim() !== name) { + a11yFail( + "Use alt attribute to label area elements that have the href attribute", + accessible + ); + } + } + } + + /** + * Test if the node's visible via accessibility API. + * + * @param {nsIAccessible} accessible + * Accessible object for a node. + */ + function assertVisible(accessible) { + if (isHidden(accessible)) { + a11yFail( + "Node is not currently visible via the accessibility API and may not " + + "be manipulated by it", + accessible + ); + } + } + + /** + * Walk node ancestry and force refresh driver tick in every document. + * @param {DOMNode} node + * Node for traversing the ancestry. + */ + function forceRefreshDriverTick(node) { + const wins = []; + let bc = BrowsingContext.getFromWindow(node.ownerDocument.defaultView); // eslint-disable-line + while (bc) { + wins.push(bc.associatedWindow); + bc = bc.embedderWindowGlobal?.browsingContext; + } + + let win = wins.pop(); + while (win) { + // Stop the refresh driver from doing its regular ticks and force two + // refresh driver ticks: first to let layout update and notify a11y, and + // the second to let a11y process updates. + win.windowUtils.advanceTimeAndRefresh(100); + win.windowUtils.advanceTimeAndRefresh(100); + // Go back to normal refresh driver ticks. + win.windowUtils.restoreNormalRefresh(); + win = wins.pop(); + } + } + + /** + * Get an accessible object for a node. + * Note: this method will not resolve if accessible object does not become + * available for a given node. + * + * @param {DOMNode} node + * Node to get the accessible object for. + * + * @return {nsIAccessible} + * Accessibility object for a given node. + */ + function getAccessible(node) { + const accessibilityService = Cc[ + "@mozilla.org/accessibilityService;1" + ].getService(Ci.nsIAccessibilityService); + if (!accessibilityService) { + // This is likely a build with --disable-accessibility + return null; + } + + let acc = accessibilityService.getAccessibleFor(node); + if (acc) { + return acc; + } + + // Force refresh tick throughout document hierarchy + forceRefreshDriverTick(node); + return accessibilityService.getAccessibleFor(node); + } + + /** + * Find the nearest interactive accessible ancestor for a node. + */ + function findInteractiveAccessible(node) { + let acc; + // Walk DOM ancestors until we find one with an accessible. + for (; node && !acc; node = node.flattenedTreeParentNode) { + acc = getAccessible(node); + } + if (!acc) { + // No accessible ancestor. + return acc; + } + // Walk a11y ancestors until we find one which is interactive. + for (; acc; acc = acc.parent) { + if (INTERACTIVE_ROLES.has(acc.role)) { + return acc; + } + } + // No interactive ancestor. + return null; + } + + function runIfA11YChecks(task) { + return (...args) => (gA11YChecks ? task(...args) : null); + } + + /** + * AccessibilityUtils provides utility methods for retrieving accessible objects + * and performing accessibility related checks. + * Current methods: + * assertCanBeClicked + * setEnv + * resetEnv + * + */ + const AccessibilityUtils = { + assertCanBeClicked(node) { + // Click events might fire on an inaccessible or non-interactive + // descendant, even if the test author targeted them at an interactive + // element. For example, if there's a button with an image inside it, + // node might be the image. + const acc = findInteractiveAccessible(node); + if (!acc) { + if (isInaccessibleXulTreecol(node)) { + return; + } + if (gEnv.mustHaveAccessibleRule) { + a11yFail("Node is not accessible via accessibility API", { + DOMNode: node, + }); + } + + return; + } + + assertInteractive(acc); + assertFocusable(acc); + assertVisible(acc); + assertEnabled(acc); + assertLabelled(acc); + }, + + setEnv(env = DEFAULT_ENV) { + gEnv = { + ...DEFAULT_ENV, + ...env, + }; + }, + + resetEnv() { + gEnv = { ...DEFAULT_ENV }; + }, + + reset(a11yChecks = false, testPath = "") { + gA11YChecks = a11yChecks; + + const { Services } = SpecialPowers; + // Disable accessibility service if it is running and if a11y checks are + // disabled. However, don't do this for accessibility engine tests. + if ( + !gA11YChecks && + Services.appinfo.accessibilityEnabled && + !testPath.startsWith("chrome://mochitests/content/browser/accessible/") + ) { + Services.prefs.setIntPref(FORCE_DISABLE_ACCESSIBILITY_PREF, 1); + Services.prefs.clearUserPref(FORCE_DISABLE_ACCESSIBILITY_PREF); + } + + // Reset accessibility environment flags that might've been set within the + // test. + this.resetEnv(); + }, + + init() { + this._shouldHandleClicks = true; + // A top level xul window's DocShell doesn't have a chromeEventHandler + // attribute. In that case, the chrome event handler is just the global + // window object. + this._handler ??= + window.docShell.chromeEventHandler ?? window.docShell.domWindow; + this._handler.addEventListener("click", this, true, true); + }, + + uninit() { + this._handler?.removeEventListener("click", this, true); + this._handler = null; + }, + + /** + * Suppress (or disable suppression of) handling of captured click events. + * This should only be called by EventUtils, etc. when a click event will + * be generated but we know it is not actually a click intended to activate + * a control; e.g. drag/drop. Tests that wish to disable specific checks + * should use setEnv instead. + */ + suppressClickHandling(shouldSuppress) { + this._shouldHandleClicks = !shouldSuppress; + }, + + handleEvent({ composedTarget }) { + if (!this._shouldHandleClicks) { + return; + } + if (composedTarget.tagName.toLowerCase() == "slot") { + // The click occurred on a text node inside a slot. Since events don't + // target text nodes, the event was retargeted to the slot. However, a + // slot isn't itself rendered. To deal with this, use the slot's parent + // instead. + composedTarget = composedTarget.flattenedTreeParentNode; + } + const bounds = + composedTarget.ownerGlobal?.windowUtils?.getBoundsWithoutFlushing( + composedTarget + ); + if (bounds && (bounds.width == 0 || bounds.height == 0)) { + // Some tests click hidden nodes. These clearly aren't testing the UI + // for the node itself (and presumably there is a test somewhere else + // that does). Therefore, we can't (and shouldn't) do a11y checks. + return; + } + this.assertCanBeClicked(composedTarget); + }, + }; + + AccessibilityUtils.assertCanBeClicked = runIfA11YChecks( + AccessibilityUtils.assertCanBeClicked.bind(AccessibilityUtils) + ); + + AccessibilityUtils.setEnv = runIfA11YChecks( + AccessibilityUtils.setEnv.bind(AccessibilityUtils) + ); + + AccessibilityUtils.resetEnv = runIfA11YChecks( + AccessibilityUtils.resetEnv.bind(AccessibilityUtils) + ); + + return AccessibilityUtils; +})(); diff --git a/testing/mochitest/tests/SimpleTest/ChromeTask.js b/testing/mochitest/tests/SimpleTest/ChromeTask.js new file mode 100644 index 0000000000..289f6f2eb2 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/ChromeTask.js @@ -0,0 +1,174 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function ChromeTask_ChromeScript() { + /* eslint-env mozilla/chrome-script */ + + "use strict"; + + const { Assert: AssertCls } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + addMessageListener("chrome-task:spawn", async function (aData) { + let id = aData.id; + let source = aData.runnable || "()=>{}"; + + function getStack(aStack) { + let frames = []; + for (let frame = aStack; frame; frame = frame.caller) { + frames.push(frame.filename + ":" + frame.name + ":" + frame.lineNumber); + } + return frames.join("\n"); + } + + /* eslint-disable no-unused-vars */ + var Assert = new AssertCls((err, message, stack) => { + sendAsyncMessage("chrome-task:test-result", { + id, + condition: !err, + name: err ? err.message : message, + stack: getStack(err ? err.stack : stack), + }); + }); + + var ok = Assert.ok.bind(Assert); + var is = Assert.equal.bind(Assert); + var isnot = Assert.notEqual.bind(Assert); + + function todo(expr, name) { + sendAsyncMessage("chrome-task:test-todo", { id, expr, name }); + } + + function todo_is(a, b, name) { + sendAsyncMessage("chrome-task:test-todo_is", { id, a, b, name }); + } + + function info(name) { + sendAsyncMessage("chrome-task:test-info", { id, name }); + } + /* eslint-enable no-unused-vars */ + + try { + let runnablestr = ` + (() => { + return (${source}); + })();`; + + // eslint-disable-next-line no-eval + let runnable = eval(runnablestr); + let result = await runnable.call(this, aData.arg); + sendAsyncMessage("chrome-task:complete", { + id, + result, + }); + } catch (ex) { + sendAsyncMessage("chrome-task:complete", { + id, + error: ex.toString(), + }); + } + }); +} + +/** + * This object provides the public module functions. + */ +var ChromeTask = { + /** + * the ChromeScript if it has already been loaded. + */ + _chromeScript: null, + + /** + * Mapping from message id to associated promise. + */ + _promises: new Map(), + + /** + * Incrementing integer to generate unique message id. + */ + _messageID: 1, + + /** + * Creates and starts a new task in the chrome process. + * + * @param arg A single serializable argument that will be passed to the + * task when executed on the content process. + * @param task + * - A generator or function which will be serialized and sent to + * the remote browser to be executed. Unlike Task.spawn, this + * argument may not be an iterator as it will be serialized and + * sent to the remote browser. + * @return A promise object where you can register completion callbacks to be + * called when the task terminates. + * @resolves With the final returned value of the task if it executes + * successfully. + * @rejects An error message if execution fails. + */ + spawn: function ChromeTask_spawn(arg, task) { + // Load the frame script if needed. + let handle = ChromeTask._chromeScript; + if (!handle) { + handle = SpecialPowers.loadChromeScript(ChromeTask_ChromeScript); + handle.addMessageListener("chrome-task:complete", ChromeTask.onComplete); + handle.addMessageListener("chrome-task:test-result", ChromeTask.onResult); + handle.addMessageListener("chrome-task:test-info", ChromeTask.onInfo); + handle.addMessageListener("chrome-task:test-todo", ChromeTask.onTodo); + handle.addMessageListener( + "chrome-task:test-todo_is", + ChromeTask.onTodoIs + ); + ChromeTask._chromeScript = handle; + } + + let deferred = {}; + deferred.promise = new Promise((resolve, reject) => { + deferred.resolve = resolve; + deferred.reject = reject; + }); + + let id = ChromeTask._messageID++; + ChromeTask._promises.set(id, deferred); + + handle.sendAsyncMessage("chrome-task:spawn", { + id, + runnable: task.toString(), + arg, + }); + + return deferred.promise; + }, + + onComplete(aData) { + let deferred = ChromeTask._promises.get(aData.id); + ChromeTask._promises.delete(aData.id); + + if (aData.error) { + deferred.reject(aData.error); + } else { + deferred.resolve(aData.result); + } + }, + + onResult(aData) { + SimpleTest.record(aData.condition, aData.name); + }, + + onInfo(aData) { + SimpleTest.info(aData.name); + }, + + onTodo(aData) { + SimpleTest.todo(aData.expr, aData.name); + }, + + onTodoIs(aData) { + SimpleTest.todo_is(aData.a, aData.b, aData.name); + }, +}; diff --git a/testing/mochitest/tests/SimpleTest/EventUtils.js b/testing/mochitest/tests/SimpleTest/EventUtils.js new file mode 100644 index 0000000000..739c7052ea --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/EventUtils.js @@ -0,0 +1,3919 @@ +/** + * EventUtils provides some utility methods for creating and sending DOM events. + * + * When adding methods to this file, please add a performance test for it. + */ + +// Certain functions assume this is loaded into browser window scope. +// This is modifiable because certain chrome tests create their own gBrowser. +/* global gBrowser:true */ + +// This file is used both in privileged and unprivileged contexts, so we have to +// be careful about our access to Components.interfaces. We also want to avoid +// naming collisions with anything that might be defined in the scope that imports +// this script. +// +// Even if the real |Components| doesn't exist, we might shim in a simple JS +// placebo for compat. An easy way to differentiate this from the real thing +// is whether the property is read-only or not. The real |Components| property +// is read-only. +/* global _EU_Ci, _EU_Cc, _EU_Cu, _EU_ChromeUtils, _EU_OS */ +window.__defineGetter__("_EU_Ci", function () { + var c = Object.getOwnPropertyDescriptor(window, "Components"); + return c && c.value && !c.writable ? Ci : SpecialPowers.Ci; +}); + +window.__defineGetter__("_EU_Cc", function () { + var c = Object.getOwnPropertyDescriptor(window, "Components"); + return c && c.value && !c.writable ? Cc : SpecialPowers.Cc; +}); + +window.__defineGetter__("_EU_Cu", function () { + var c = Object.getOwnPropertyDescriptor(window, "Components"); + return c && c.value && !c.writable ? Cu : SpecialPowers.Cu; +}); + +window.__defineGetter__("_EU_ChromeUtils", function () { + var c = Object.getOwnPropertyDescriptor(window, "ChromeUtils"); + return c && c.value && !c.writable ? ChromeUtils : SpecialPowers.ChromeUtils; +}); + +window.__defineGetter__("_EU_OS", function () { + delete this._EU_OS; + try { + this._EU_OS = _EU_ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ).platform; + } catch (ex) { + this._EU_OS = null; + } + return this._EU_OS; +}); + +function _EU_isMac(aWindow = window) { + if (window._EU_OS) { + return window._EU_OS == "macosx"; + } + if (aWindow) { + try { + return aWindow.navigator.platform.indexOf("Mac") > -1; + } catch (ex) {} + } + return navigator.platform.indexOf("Mac") > -1; +} + +function _EU_isWin(aWindow = window) { + if (window._EU_OS) { + return window._EU_OS == "win"; + } + if (aWindow) { + try { + return aWindow.navigator.platform.indexOf("Win") > -1; + } catch (ex) {} + } + return navigator.platform.indexOf("Win") > -1; +} + +function _EU_isLinux(aWindow = window) { + if (window._EU_OS) { + return window._EU_OS == "linux"; + } + if (aWindow) { + try { + return aWindow.navigator.platform.startsWith("Linux"); + } catch (ex) {} + } + return navigator.platform.startsWith("Linux"); +} + +function _EU_isAndroid(aWindow = window) { + if (window._EU_OS) { + return window._EU_OS == "android"; + } + if (aWindow) { + try { + return aWindow.navigator.userAgent.includes("Android"); + } catch (ex) {} + } + return navigator.userAgent.includes("Android"); +} + +function _EU_maybeWrap(o) { + // We're used in some contexts where there is no SpecialPowers and also in + // some where it exists but has no wrap() method. And this is somewhat + // independent of whether window.Components is a thing... + var haveWrap = false; + try { + haveWrap = SpecialPowers.wrap != undefined; + } catch (e) { + // Just leave it false. + } + if (!haveWrap) { + // Not much we can do here. + return o; + } + var c = Object.getOwnPropertyDescriptor(window, "Components"); + return c && c.value && !c.writable ? o : SpecialPowers.wrap(o); +} + +function _EU_maybeUnwrap(o) { + var haveWrap = false; + try { + haveWrap = SpecialPowers.unwrap != undefined; + } catch (e) { + // Just leave it false. + } + if (!haveWrap) { + // Not much we can do here. + return o; + } + var c = Object.getOwnPropertyDescriptor(window, "Components"); + return c && c.value && !c.writable ? o : SpecialPowers.unwrap(o); +} + +function _EU_getPlatform() { + if (_EU_isWin()) { + return "windows"; + } + if (_EU_isMac()) { + return "mac"; + } + if (_EU_isAndroid()) { + return "android"; + } + if (_EU_isLinux()) { + return "linux"; + } + return "unknown"; +} + +/** + * promiseElementReadyForUserInput() dispatches mousemove events to aElement + * and waits one of them for a while. Then, returns "resolved" state when it's + * successfully received. Otherwise, if it couldn't receive mousemove event on + * it, this throws an exception. So, aElement must be an element which is + * assumed non-collapsed visible element in the window. + * + * This is useful if you need to synthesize mouse events via the main process + * but your test cannot check whether the element is now in APZ to deliver + * a user input event. + */ +async function promiseElementReadyForUserInput( + aElement, + aWindow = window, + aLogFunc = null +) { + if (typeof aElement == "string") { + aElement = aWindow.document.getElementById(aElement); + } + + function waitForMouseMoveForHittest() { + return new Promise(resolve => { + let timeout; + const onHit = () => { + if (aLogFunc) { + aLogFunc("mousemove received"); + } + aWindow.clearInterval(timeout); + resolve(true); + }; + aElement.addEventListener("mousemove", onHit, { + capture: true, + once: true, + }); + timeout = aWindow.setInterval(() => { + if (aLogFunc) { + aLogFunc("mousemove not received in this 300ms"); + } + aElement.removeEventListener("mousemove", onHit, { + capture: true, + }); + resolve(false); + }, 300); + synthesizeMouseAtCenter(aElement, { type: "mousemove" }, aWindow); + }); + } + for (let i = 0; i < 20; i++) { + if (await waitForMouseMoveForHittest()) { + return Promise.resolve(); + } + } + throw new Error("The element or the window did not become interactive"); +} + +function getElement(id) { + return typeof id == "string" ? document.getElementById(id) : id; +} + +this.$ = this.getElement; + +function computeButton(aEvent) { + if (typeof aEvent.button != "undefined") { + return aEvent.button; + } + return aEvent.type == "contextmenu" ? 2 : 0; +} + +function computeButtons(aEvent, utils) { + if (typeof aEvent.buttons != "undefined") { + return aEvent.buttons; + } + + if (typeof aEvent.button != "undefined") { + return utils.MOUSE_BUTTONS_NOT_SPECIFIED; + } + + if (typeof aEvent.type != "undefined" && aEvent.type != "mousedown") { + return utils.MOUSE_BUTTONS_NO_BUTTON; + } + + return utils.MOUSE_BUTTONS_NOT_SPECIFIED; +} + +/** + * Send a mouse event to the node aTarget (aTarget can be an id, or an + * actual node) . The "event" passed in to aEvent is just a JavaScript + * object with the properties set that the real mouse event object should + * have. This includes the type of the mouse event. Pretty much all those + * properties are optional. + * E.g. to send an click event to the node with id 'node' you might do this: + * + * ``sendMouseEvent({type:'click'}, 'node');`` + */ +function sendMouseEvent(aEvent, aTarget, aWindow) { + if ( + ![ + "click", + "contextmenu", + "dblclick", + "mousedown", + "mouseup", + "mouseover", + "mouseout", + ].includes(aEvent.type) + ) { + throw new Error( + "sendMouseEvent doesn't know about event type '" + aEvent.type + "'" + ); + } + + if (!aWindow) { + aWindow = window; + } + + if (typeof aTarget == "string") { + aTarget = aWindow.document.getElementById(aTarget); + } + + var event = aWindow.document.createEvent("MouseEvent"); + + var typeArg = aEvent.type; + var canBubbleArg = true; + var cancelableArg = true; + var viewArg = aWindow; + var detailArg = + aEvent.detail || + // eslint-disable-next-line no-nested-ternary + (aEvent.type == "click" || + aEvent.type == "mousedown" || + aEvent.type == "mouseup" + ? 1 + : aEvent.type == "dblclick" + ? 2 + : 0); + var screenXArg = aEvent.screenX || 0; + var screenYArg = aEvent.screenY || 0; + var clientXArg = aEvent.clientX || 0; + var clientYArg = aEvent.clientY || 0; + var ctrlKeyArg = aEvent.ctrlKey || false; + var altKeyArg = aEvent.altKey || false; + var shiftKeyArg = aEvent.shiftKey || false; + var metaKeyArg = aEvent.metaKey || false; + var buttonArg = computeButton(aEvent); + var relatedTargetArg = aEvent.relatedTarget || null; + + event.initMouseEvent( + typeArg, + canBubbleArg, + cancelableArg, + viewArg, + detailArg, + screenXArg, + screenYArg, + clientXArg, + clientYArg, + ctrlKeyArg, + altKeyArg, + shiftKeyArg, + metaKeyArg, + buttonArg, + relatedTargetArg + ); + + // If documentURIObject exists or `window` is a stub object, we're in + // a chrome scope, so don't bother trying to go through SpecialPowers. + if (!window.document || window.document.documentURIObject) { + return aTarget.dispatchEvent(event); + } + return SpecialPowers.dispatchEvent(aWindow, aTarget, event); +} + +function isHidden(aElement) { + var box = aElement.getBoundingClientRect(); + return box.width == 0 && box.height == 0; +} + +/** + * Send a drag event to the node aTarget (aTarget can be an id, or an + * actual node) . The "event" passed in to aEvent is just a JavaScript + * object with the properties set that the real drag event object should + * have. This includes the type of the drag event. + */ +function sendDragEvent(aEvent, aTarget, aWindow = window) { + if ( + ![ + "drag", + "dragstart", + "dragend", + "dragover", + "dragenter", + "dragleave", + "drop", + ].includes(aEvent.type) + ) { + throw new Error( + "sendDragEvent doesn't know about event type '" + aEvent.type + "'" + ); + } + + if (typeof aTarget == "string") { + aTarget = aWindow.document.getElementById(aTarget); + } + + /* + * Drag event cannot be performed if the element is hidden, except 'dragend' + * event where the element can becomes hidden after start dragging. + */ + if (aEvent.type != "dragend" && isHidden(aTarget)) { + var targetName = aTarget.nodeName; + if ("id" in aTarget && aTarget.id) { + targetName += "#" + aTarget.id; + } + throw new Error(`${aEvent.type} event target ${targetName} is hidden`); + } + + var event = aWindow.document.createEvent("DragEvent"); + + var typeArg = aEvent.type; + var canBubbleArg = true; + var cancelableArg = true; + var viewArg = aWindow; + var detailArg = aEvent.detail || 0; + var screenXArg = aEvent.screenX || 0; + var screenYArg = aEvent.screenY || 0; + var clientXArg = aEvent.clientX || 0; + var clientYArg = aEvent.clientY || 0; + var ctrlKeyArg = aEvent.ctrlKey || false; + var altKeyArg = aEvent.altKey || false; + var shiftKeyArg = aEvent.shiftKey || false; + var metaKeyArg = aEvent.metaKey || false; + var buttonArg = computeButton(aEvent); + var relatedTargetArg = aEvent.relatedTarget || null; + var dataTransfer = aEvent.dataTransfer || null; + + event.initDragEvent( + typeArg, + canBubbleArg, + cancelableArg, + viewArg, + detailArg, + screenXArg, + screenYArg, + clientXArg, + clientYArg, + ctrlKeyArg, + altKeyArg, + shiftKeyArg, + metaKeyArg, + buttonArg, + relatedTargetArg, + dataTransfer + ); + + if (aEvent._domDispatchOnly) { + return aTarget.dispatchEvent(event); + } + + var utils = _getDOMWindowUtils(aWindow); + return utils.dispatchDOMEventViaPresShellForTesting(aTarget, event); +} + +/** + * Send the char aChar to the focused element. This method handles casing of + * chars (sends the right charcode, and sends a shift key for uppercase chars). + * No other modifiers are handled at this point. + * + * For now this method only works for ASCII characters and emulates the shift + * key state on US keyboard layout. + */ +function sendChar(aChar, aWindow) { + var hasShift; + // Emulate US keyboard layout for the shiftKey state. + switch (aChar) { + case "!": + case "@": + case "#": + case "$": + case "%": + case "^": + case "&": + case "*": + case "(": + case ")": + case "_": + case "+": + case "{": + case "}": + case ":": + case '"': + case "|": + case "<": + case ">": + case "?": + hasShift = true; + break; + default: + hasShift = + aChar.toLowerCase() != aChar.toUpperCase() && + aChar == aChar.toUpperCase(); + break; + } + synthesizeKey(aChar, { shiftKey: hasShift }, aWindow); +} + +/** + * Send the string aStr to the focused element. + * + * For now this method only works for ASCII characters and emulates the shift + * key state on US keyboard layout. + */ +function sendString(aStr, aWindow) { + for (let i = 0; i < aStr.length; ++i) { + // Do not split a surrogate pair to call synthesizeKey. Dispatching two + // sets of keydown and keyup caused by two calls of synthesizeKey is not + // good behavior. It could happen due to a bug, but a surrogate pair should + // be introduced with one key press operation. Therefore, calling it with + // a surrogate pair is the right thing. + // Note that TextEventDispatcher will consider whether a surrogate pair + // should cause one or two keypress events automatically. Therefore, we + // don't need to check the related prefs here. + if ( + (aStr.charCodeAt(i) & 0xfc00) == 0xd800 && + i + 1 < aStr.length && + (aStr.charCodeAt(i + 1) & 0xfc00) == 0xdc00 + ) { + sendChar(aStr.substring(i, i + 2), aWindow); + i++; + } else { + sendChar(aStr.charAt(i), aWindow); + } + } +} + +/** + * Send the non-character key aKey to the focused node. + * The name of the key should be the part that comes after ``DOM_VK_`` in the + * KeyEvent constant name for this key. + * No modifiers are handled at this point. + */ +function sendKey(aKey, aWindow) { + var keyName = "VK_" + aKey.toUpperCase(); + synthesizeKey(keyName, { shiftKey: false }, aWindow); +} + +/** + * Parse the key modifier flags from aEvent. Used to share code between + * synthesizeMouse and synthesizeKey. + */ +function _parseModifiers(aEvent, aWindow = window) { + var nsIDOMWindowUtils = _EU_Ci.nsIDOMWindowUtils; + var mval = 0; + if (aEvent.shiftKey) { + mval |= nsIDOMWindowUtils.MODIFIER_SHIFT; + } + if (aEvent.ctrlKey) { + mval |= nsIDOMWindowUtils.MODIFIER_CONTROL; + } + if (aEvent.altKey) { + mval |= nsIDOMWindowUtils.MODIFIER_ALT; + } + if (aEvent.metaKey) { + mval |= nsIDOMWindowUtils.MODIFIER_META; + } + if (aEvent.accelKey) { + mval |= _EU_isMac(aWindow) + ? nsIDOMWindowUtils.MODIFIER_META + : nsIDOMWindowUtils.MODIFIER_CONTROL; + } + if (aEvent.altGrKey) { + mval |= nsIDOMWindowUtils.MODIFIER_ALTGRAPH; + } + if (aEvent.capsLockKey) { + mval |= nsIDOMWindowUtils.MODIFIER_CAPSLOCK; + } + if (aEvent.fnKey) { + mval |= nsIDOMWindowUtils.MODIFIER_FN; + } + if (aEvent.fnLockKey) { + mval |= nsIDOMWindowUtils.MODIFIER_FNLOCK; + } + if (aEvent.numLockKey) { + mval |= nsIDOMWindowUtils.MODIFIER_NUMLOCK; + } + if (aEvent.scrollLockKey) { + mval |= nsIDOMWindowUtils.MODIFIER_SCROLLLOCK; + } + if (aEvent.symbolKey) { + mval |= nsIDOMWindowUtils.MODIFIER_SYMBOL; + } + if (aEvent.symbolLockKey) { + mval |= nsIDOMWindowUtils.MODIFIER_SYMBOLLOCK; + } + + return mval; +} + +/** + * Synthesize a mouse event on a target. The actual client point is determined + * by taking the aTarget's client box and offseting it by aOffsetX and + * aOffsetY. This allows mouse clicks to be simulated by calling this method. + * + * aEvent is an object which may contain the properties: + * `shiftKey`, `ctrlKey`, `altKey`, `metaKey`, `accessKey`, `clickCount`, + * `button`, `type`. + * For valid `type`s see nsIDOMWindowUtils' `sendMouseEvent`. + * + * If the type is specified, an mouse event of that type is fired. Otherwise, + * a mousedown followed by a mouseup is performed. + * + * aWindow is optional, and defaults to the current window object. + * + * Returns whether the event had preventDefault() called on it. + */ +function synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) { + var rect = aTarget.getBoundingClientRect(); + return synthesizeMouseAtPoint( + rect.left + aOffsetX, + rect.top + aOffsetY, + aEvent, + aWindow + ); +} +function synthesizeTouch(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) { + var rect = aTarget.getBoundingClientRect(); + return synthesizeTouchAtPoint( + rect.left + aOffsetX, + rect.top + aOffsetY, + aEvent, + aWindow + ); +} + +/** + * Return the drag service. Note that if we're in the headless mode, this + * may return null because the service may be never instantiated (e.g., on + * Linux). + */ +function getDragService() { + try { + return _EU_Cc["@mozilla.org/widget/dragservice;1"].getService( + _EU_Ci.nsIDragService + ); + } catch (e) { + // If we're in the headless mode, the drag service may be never + // instantiated. In this case, an exception is thrown. Let's ignore + // any exceptions since without the drag service, nobody can create a + // drag session. + return null; + } +} + +/** + * End drag session if there is. + * + * TODO: This should synthesize "drop" if necessary. + * + * @param left X offset in the viewport + * @param top Y offset in the viewport + * @param aEvent The event data, the modifiers are applied to the + * "dragend" event. + * @param aWindow The window. + * @return true if handled. In this case, the caller should not + * synthesize DOM events basically. + */ +function _maybeEndDragSession(left, top, aEvent, aWindow) { + const dragService = getDragService(); + const dragSession = dragService?.getCurrentSession(); + if (!dragSession) { + return false; + } + // FIXME: If dragSession.dragAction is not + // nsIDragService.DRAGDROP_ACTION_NONE nor aEvent.type is not `keydown`, we + // need to synthesize a "drop" event or call setDragEndPointForTests here to + // set proper left/top to `dragend` event. + try { + dragService.endDragSession(false, _parseModifiers(aEvent, aWindow)); + } catch (e) {} + return true; +} + +function _maybeSynthesizeDragOver(left, top, aEvent, aWindow) { + const dragSession = getDragService()?.getCurrentSession(); + if (!dragSession) { + return false; + } + const target = aWindow.document.elementFromPoint(left, top); + if (target) { + sendDragEvent( + createDragEventObject( + "dragover", + target, + aWindow, + dragSession.dataTransfer, + { + accelKey: aEvent.accelKey, + altKey: aEvent.altKey, + altGrKey: aEvent.altGrKey, + ctrlKey: aEvent.ctrlKey, + metaKey: aEvent.metaKey, + shiftKey: aEvent.shiftKey, + capsLockKey: aEvent.capsLockKey, + fnKey: aEvent.fnKey, + fnLockKey: aEvent.fnLockKey, + numLockKey: aEvent.numLockKey, + scrollLockKey: aEvent.scrollLockKey, + symbolKey: aEvent.symbolKey, + symbolLockKey: aEvent.symbolLockKey, + } + ), + target, + aWindow + ); + } + return true; +} + +/* + * Synthesize a mouse event at a particular point in aWindow. + * + * aEvent is an object which may contain the properties: + * `shiftKey`, `ctrlKey`, `altKey`, `metaKey`, `accessKey`, `clickCount`, + * `button`, `type`. + * For valid `type`s see nsIDOMWindowUtils' `sendMouseEvent`. + * + * If the type is specified, an mouse event of that type is fired. Otherwise, + * a mousedown followed by a mouseup is performed. + * + * aWindow is optional, and defaults to the current window object. + */ +function synthesizeMouseAtPoint(left, top, aEvent, aWindow = window) { + if (aEvent.allowToHandleDragDrop) { + if (aEvent.type == "mouseup" || !aEvent.type) { + if (_maybeEndDragSession(left, top, aEvent, aWindow)) { + return false; + } + } else if (aEvent.type == "mousemove") { + if (_maybeSynthesizeDragOver(left, top, aEvent, aWindow)) { + return false; + } + } + } + + var utils = _getDOMWindowUtils(aWindow); + var defaultPrevented = false; + + if (utils) { + var button = computeButton(aEvent); + var clickCount = aEvent.clickCount || 1; + var modifiers = _parseModifiers(aEvent, aWindow); + var pressure = "pressure" in aEvent ? aEvent.pressure : 0; + + // aWindow might be cross-origin from us. + var MouseEvent = _EU_maybeWrap(aWindow).MouseEvent; + + // Default source to mouse. + var inputSource = + "inputSource" in aEvent + ? aEvent.inputSource + : MouseEvent.MOZ_SOURCE_MOUSE; + // Compute a pointerId if needed. + var id; + if ("id" in aEvent) { + id = aEvent.id; + } else { + var isFromPen = inputSource === MouseEvent.MOZ_SOURCE_PEN; + id = isFromPen + ? utils.DEFAULT_PEN_POINTER_ID + : utils.DEFAULT_MOUSE_POINTER_ID; + } + + var isDOMEventSynthesized = + "isSynthesized" in aEvent ? aEvent.isSynthesized : true; + var isWidgetEventSynthesized = + "isWidgetEventSynthesized" in aEvent + ? aEvent.isWidgetEventSynthesized + : false; + if ("type" in aEvent && aEvent.type) { + defaultPrevented = utils.sendMouseEvent( + aEvent.type, + left, + top, + button, + clickCount, + modifiers, + false, + pressure, + inputSource, + isDOMEventSynthesized, + isWidgetEventSynthesized, + computeButtons(aEvent, utils), + id + ); + } else { + utils.sendMouseEvent( + "mousedown", + left, + top, + button, + clickCount, + modifiers, + false, + pressure, + inputSource, + isDOMEventSynthesized, + isWidgetEventSynthesized, + computeButtons(Object.assign({ type: "mousedown" }, aEvent), utils), + id + ); + utils.sendMouseEvent( + "mouseup", + left, + top, + button, + clickCount, + modifiers, + false, + pressure, + inputSource, + isDOMEventSynthesized, + isWidgetEventSynthesized, + computeButtons(Object.assign({ type: "mouseup" }, aEvent), utils), + id + ); + } + } + + return defaultPrevented; +} + +function synthesizeTouchAtPoint(left, top, aEvent, aWindow = window) { + var utils = _getDOMWindowUtils(aWindow); + let defaultPrevented = false; + + if (utils) { + var id = aEvent.id || utils.DEFAULT_TOUCH_POINTER_ID; + var rx = aEvent.rx || 1; + var ry = aEvent.ry || 1; + var angle = aEvent.angle || 0; + var force = aEvent.force || (aEvent.type === "touchend" ? 0 : 1); + var tiltX = aEvent.tiltX || 0; + var tiltY = aEvent.tiltY || 0; + var twist = aEvent.twist || 0; + var modifiers = _parseModifiers(aEvent, aWindow); + + if ("type" in aEvent && aEvent.type) { + defaultPrevented = utils.sendTouchEvent( + aEvent.type, + [id], + [left], + [top], + [rx], + [ry], + [angle], + [force], + [tiltX], + [tiltY], + [twist], + modifiers + ); + } else { + utils.sendTouchEvent( + "touchstart", + [id], + [left], + [top], + [rx], + [ry], + [angle], + [force], + [tiltX], + [tiltY], + [twist], + modifiers + ); + utils.sendTouchEvent( + "touchend", + [id], + [left], + [top], + [rx], + [ry], + [angle], + [force], + [tiltX], + [tiltY], + [twist], + modifiers + ); + } + } + return defaultPrevented; +} + +// Call synthesizeMouse with coordinates at the center of aTarget. +function synthesizeMouseAtCenter(aTarget, aEvent, aWindow) { + var rect = aTarget.getBoundingClientRect(); + return synthesizeMouse( + aTarget, + rect.width / 2, + rect.height / 2, + aEvent, + aWindow + ); +} +function synthesizeTouchAtCenter(aTarget, aEvent, aWindow) { + var rect = aTarget.getBoundingClientRect(); + synthesizeTouchAtPoint( + rect.left + rect.width / 2, + rect.top + rect.height / 2, + aEvent, + aWindow + ); +} + +/** + * Synthesize a wheel event without flush layout at a particular point in + * aWindow. + * + * aEvent is an object which may contain the properties: + * shiftKey, ctrlKey, altKey, metaKey, accessKey, deltaX, deltaY, deltaZ, + * deltaMode, lineOrPageDeltaX, lineOrPageDeltaY, isMomentum, + * isNoLineOrPageDelta, isCustomizedByPrefs, expectedOverflowDeltaX, + * expectedOverflowDeltaY + * + * deltaMode must be defined, others are ok even if undefined. + * + * expectedOverflowDeltaX and expectedOverflowDeltaY take integer value. The + * value is just checked as 0 or positive or negative. + * + * aWindow is optional, and defaults to the current window object. + */ +function synthesizeWheelAtPoint(aLeft, aTop, aEvent, aWindow = window) { + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return; + } + + var modifiers = _parseModifiers(aEvent, aWindow); + var options = 0; + if (aEvent.isNoLineOrPageDelta) { + options |= utils.WHEEL_EVENT_CAUSED_BY_NO_LINE_OR_PAGE_DELTA_DEVICE; + } + if (aEvent.isMomentum) { + options |= utils.WHEEL_EVENT_CAUSED_BY_MOMENTUM; + } + if (aEvent.isCustomizedByPrefs) { + options |= utils.WHEEL_EVENT_CUSTOMIZED_BY_USER_PREFS; + } + if (typeof aEvent.expectedOverflowDeltaX !== "undefined") { + if (aEvent.expectedOverflowDeltaX === 0) { + options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_ZERO; + } else if (aEvent.expectedOverflowDeltaX > 0) { + options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_POSITIVE; + } else { + options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_NEGATIVE; + } + } + if (typeof aEvent.expectedOverflowDeltaY !== "undefined") { + if (aEvent.expectedOverflowDeltaY === 0) { + options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_ZERO; + } else if (aEvent.expectedOverflowDeltaY > 0) { + options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_POSITIVE; + } else { + options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_NEGATIVE; + } + } + + // Avoid the JS warnings "reference to undefined property" + if (!aEvent.deltaX) { + aEvent.deltaX = 0; + } + if (!aEvent.deltaY) { + aEvent.deltaY = 0; + } + if (!aEvent.deltaZ) { + aEvent.deltaZ = 0; + } + + var lineOrPageDeltaX = + // eslint-disable-next-line no-nested-ternary + aEvent.lineOrPageDeltaX != null + ? aEvent.lineOrPageDeltaX + : aEvent.deltaX > 0 + ? Math.floor(aEvent.deltaX) + : Math.ceil(aEvent.deltaX); + var lineOrPageDeltaY = + // eslint-disable-next-line no-nested-ternary + aEvent.lineOrPageDeltaY != null + ? aEvent.lineOrPageDeltaY + : aEvent.deltaY > 0 + ? Math.floor(aEvent.deltaY) + : Math.ceil(aEvent.deltaY); + utils.sendWheelEvent( + aLeft, + aTop, + aEvent.deltaX, + aEvent.deltaY, + aEvent.deltaZ, + aEvent.deltaMode, + modifiers, + lineOrPageDeltaX, + lineOrPageDeltaY, + options + ); +} + +/** + * Synthesize a wheel event on a target. The actual client point is determined + * by taking the aTarget's client box and offseting it by aOffsetX and + * aOffsetY. + * + * aEvent is an object which may contain the properties: + * shiftKey, ctrlKey, altKey, metaKey, accessKey, deltaX, deltaY, deltaZ, + * deltaMode, lineOrPageDeltaX, lineOrPageDeltaY, isMomentum, + * isNoLineOrPageDelta, isCustomizedByPrefs, expectedOverflowDeltaX, + * expectedOverflowDeltaY + * + * deltaMode must be defined, others are ok even if undefined. + * + * expectedOverflowDeltaX and expectedOverflowDeltaY take integer value. The + * value is just checked as 0 or positive or negative. + * + * aWindow is optional, and defaults to the current window object. + */ +function synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) { + var rect = aTarget.getBoundingClientRect(); + synthesizeWheelAtPoint( + rect.left + aOffsetX, + rect.top + aOffsetY, + aEvent, + aWindow + ); +} + +const _FlushModes = { + FLUSH: 0, + NOFLUSH: 1, +}; + +function _sendWheelAndPaint( + aTarget, + aOffsetX, + aOffsetY, + aEvent, + aCallback, + aFlushMode = _FlushModes.FLUSH, + aWindow = window +) { + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return; + } + + if (utils.isMozAfterPaintPending) { + // If a paint is pending, then APZ may be waiting for a scroll acknowledgement + // from the content thread. If we send a wheel event now, it could be ignored + // by APZ (or its scroll offset could be overridden). To avoid problems we + // just wait for the paint to complete. + aWindow.waitForAllPaintsFlushed(function () { + _sendWheelAndPaint( + aTarget, + aOffsetX, + aOffsetY, + aEvent, + aCallback, + aFlushMode, + aWindow + ); + }); + return; + } + + var onwheel = function () { + SpecialPowers.removeSystemEventListener(window, "wheel", onwheel); + + // Wait one frame since the wheel event has not caused a refresh observer + // to be added yet. + setTimeout(function () { + utils.advanceTimeAndRefresh(1000); + + if (!aCallback) { + utils.advanceTimeAndRefresh(0); + return; + } + + var waitForPaints = function () { + SpecialPowers.Services.obs.removeObserver( + waitForPaints, + "apz-repaints-flushed" + ); + aWindow.waitForAllPaintsFlushed(function () { + utils.restoreNormalRefresh(); + aCallback(); + }); + }; + + SpecialPowers.Services.obs.addObserver( + waitForPaints, + "apz-repaints-flushed" + ); + if (!utils.flushApzRepaints(aWindow)) { + waitForPaints(); + } + }, 0); + }; + + // Listen for the system wheel event, because it happens after all of + // the other wheel events, including legacy events. + SpecialPowers.addSystemEventListener(aWindow, "wheel", onwheel); + if (aFlushMode === _FlushModes.FLUSH) { + synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow); + } else { + synthesizeWheelAtPoint(aOffsetX, aOffsetY, aEvent, aWindow); + } +} + +/** + * This is a wrapper around synthesizeWheel that waits for the wheel event + * to be dispatched and for the subsequent layout/paints to be flushed. + * + * This requires including paint_listener.js. Tests must call + * DOMWindowUtils.restoreNormalRefresh() before finishing, if they use this + * function. + * + * If no callback is provided, the caller is assumed to have its own method of + * determining scroll completion and the refresh driver is not automatically + * restored. + */ +function sendWheelAndPaint( + aTarget, + aOffsetX, + aOffsetY, + aEvent, + aCallback, + aWindow = window +) { + _sendWheelAndPaint( + aTarget, + aOffsetX, + aOffsetY, + aEvent, + aCallback, + _FlushModes.FLUSH, + aWindow + ); +} + +/** + * Similar to sendWheelAndPaint but without flushing layout for obtaining + * ``aTarget`` position in ``aWindow`` before sending the wheel event. + * ``aOffsetX`` and ``aOffsetY`` should be offsets against aWindow. + */ +function sendWheelAndPaintNoFlush( + aTarget, + aOffsetX, + aOffsetY, + aEvent, + aCallback, + aWindow = window +) { + _sendWheelAndPaint( + aTarget, + aOffsetX, + aOffsetY, + aEvent, + aCallback, + _FlushModes.NOFLUSH, + aWindow + ); +} + +function synthesizeNativeTapAtCenter( + aTarget, + aLongTap = false, + aCallback = null, + aWindow = window +) { + let rect = aTarget.getBoundingClientRect(); + return synthesizeNativeTap( + aTarget, + rect.width / 2, + rect.height / 2, + aLongTap, + aCallback, + aWindow + ); +} + +function synthesizeNativeTap( + aTarget, + aOffsetX, + aOffsetY, + aLongTap = false, + aCallback = null, + aWindow = window +) { + let utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return; + } + + let scale = aWindow.devicePixelRatio; + let rect = aTarget.getBoundingClientRect(); + let x = (aWindow.mozInnerScreenX + rect.left + aOffsetX) * scale; + let y = (aWindow.mozInnerScreenY + rect.top + aOffsetY) * scale; + + let observer = { + observe: (subject, topic, data) => { + if (aCallback && topic == "mouseevent") { + aCallback(data); + } + }, + }; + utils.sendNativeTouchTap(x, y, aLongTap, observer); +} + +/** + * Similar to synthesizeMouse but generates a native widget level event + * (so will actually move the "real" mouse cursor etc. Be careful because + * this can impact later code as well! (e.g. with hover states etc.) + * + * @description There are 3 mutually exclusive ways of indicating the location of the + * mouse event: set ``atCenter``, or pass ``offsetX`` and ``offsetY``, + * or pass ``screenX`` and ``screenY``. Do not attempt to mix these. + * + * @param {object} aParams + * @param {string} aParams.type "click", "mousedown", "mouseup" or "mousemove" + * @param {Element} aParams.target Origin of offsetX and offsetY, must be an element + * @param {Boolean} [aParams.atCenter] + * Instead of offsetX/Y, synthesize the event at center of `target`. + * @param {Number} [aParams.offsetX] + * X offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel") + * @param {Number} [aParams.offsetY] + * Y offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel") + * @param {Number} [aParams.screenX] + * X offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"), + * Neither offsetX/Y nor atCenter must be set if this is set. + * @param {Number} [aParams.screenY] + * Y offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"), + * Neither offsetX/Y nor atCenter must be set if this is set. + * @param {String} [aParams.scale="screenPixelsPerCSSPixel"] + * If scale is "screenPixelsPerCSSPixel", devicePixelRatio will be used. + * If scale is "inScreenPixels", clientX/Y nor scaleX/Y are not adjusted with screenPixelsPerCSSPixel. + * @param {Number} [aParams.button=0] + * Defaults to 0, if "click", "mousedown", "mouseup", set same value as DOM MouseEvent.button + * @param {Object} [aParams.modifiers={}] + * Active modifiers, see `_parseNativeModifiers` + * @param {Window} [aParams.win=window] + * The window to use its utils. Defaults to the window in which EventUtils.js is running. + * @param {Element} [aParams.elementOnWidget=target] + * Defaults to target. If element under the point is in another widget from target's widget, + * e.g., when it's in a XUL <panel>, specify this. + */ +function synthesizeNativeMouseEvent(aParams, aCallback = null) { + const { + type, + target, + offsetX, + offsetY, + atCenter, + screenX, + screenY, + scale = "screenPixelsPerCSSPixel", + button = 0, + modifiers = {}, + win = window, + elementOnWidget = target, + } = aParams; + if (atCenter) { + if (offsetX != undefined || offsetY != undefined) { + throw Error( + `atCenter is specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified` + ); + } + if (screenX != undefined || screenY != undefined) { + throw Error( + `atCenter is specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified` + ); + } + if (!target) { + throw Error("atCenter is specified, but target is not specified"); + } + } else if (offsetX != undefined && offsetY != undefined) { + if (screenX != undefined || screenY != undefined) { + throw Error( + `offsetX/Y are specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified` + ); + } + if (!target) { + throw Error( + "offsetX and offsetY are specified, but target is not specified" + ); + } + } else if (screenX != undefined && screenY != undefined) { + if (offsetX != undefined || offsetY != undefined) { + throw Error( + `screenX/Y are specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified` + ); + } + } + const utils = _getDOMWindowUtils(win); + if (!utils) { + return; + } + + const rect = target?.getBoundingClientRect(); + let resolution = 1.0; + try { + resolution = _getDOMWindowUtils(win.top).getResolution(); + } catch (e) { + // XXX How to get mobile viewport scale on Fission+xorigin since + // window.top access isn't allowed due to cross-origin? + } + const scaleValue = (() => { + if (scale === "inScreenPixels") { + return 1.0; + } + if (scale === "screenPixelsPerCSSPixel") { + return win.devicePixelRatio; + } + throw Error(`invalid scale value (${scale}) is specified`); + })(); + // XXX mozInnerScreen might be invalid value on mobile viewport (Bug 1701546), + // so use window.top's mozInnerScreen. But this won't work fission+xorigin + // with mobile viewport until mozInnerScreen returns valid value with + // scale. + const x = (() => { + if (screenX != undefined) { + return screenX * scaleValue; + } + let winInnerOffsetX = win.mozInnerScreenX; + try { + winInnerOffsetX = + win.top.mozInnerScreenX + + (win.mozInnerScreenX - win.top.mozInnerScreenX) * resolution; + } catch (e) { + // XXX fission+xorigin test throws permission denied since win.top is + // cross-origin. + } + return ( + (((atCenter ? rect.width / 2 : offsetX) + rect.left) * resolution + + winInnerOffsetX) * + scaleValue + ); + })(); + const y = (() => { + if (screenY != undefined) { + return screenY * scaleValue; + } + let winInnerOffsetY = win.mozInnerScreenY; + try { + winInnerOffsetY = + win.top.mozInnerScreenY + + (win.mozInnerScreenY - win.top.mozInnerScreenY) * resolution; + } catch (e) { + // XXX fission+xorigin test throws permission denied since win.top is + // cross-origin. + } + return ( + (((atCenter ? rect.height / 2 : offsetY) + rect.top) * resolution + + winInnerOffsetY) * + scaleValue + ); + })(); + const modifierFlags = _parseNativeModifiers(modifiers); + + const observer = { + observe: (subject, topic, data) => { + if (aCallback && topic == "mouseevent") { + aCallback(data); + } + }, + }; + if (type === "click") { + utils.sendNativeMouseEvent( + x, + y, + utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN, + button, + modifierFlags, + elementOnWidget, + function () { + utils.sendNativeMouseEvent( + x, + y, + utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP, + button, + modifierFlags, + elementOnWidget, + observer + ); + } + ); + return; + } + utils.sendNativeMouseEvent( + x, + y, + (() => { + switch (type) { + case "mousedown": + return utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN; + case "mouseup": + return utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP; + case "mousemove": + return utils.NATIVE_MOUSE_MESSAGE_MOVE; + default: + throw Error(`Invalid type is specified: ${type}`); + } + })(), + button, + modifierFlags, + elementOnWidget, + observer + ); +} + +function promiseNativeMouseEvent(aParams) { + return new Promise(resolve => synthesizeNativeMouseEvent(aParams, resolve)); +} + +function synthesizeNativeMouseEventAndWaitForEvent(aParams, aCallback) { + const listener = aParams.eventTargetToListen || aParams.target; + const eventType = aParams.eventTypeToWait || aParams.type; + listener.addEventListener(eventType, aCallback, { + capture: true, + once: true, + }); + synthesizeNativeMouseEvent(aParams); +} + +function promiseNativeMouseEventAndWaitForEvent(aParams) { + return new Promise(resolve => + synthesizeNativeMouseEventAndWaitForEvent(aParams, resolve) + ); +} + +/** + * This is a wrapper around synthesizeNativeMouseEvent that waits for the mouse + * event to be dispatched to the target content. + * + * This API is supposed to be used in those test cases that synthesize some + * input events to chrome process and have some checks in content. + */ +function synthesizeAndWaitNativeMouseMove( + aTarget, + aOffsetX, + aOffsetY, + aCallback, + aWindow = window +) { + let browser = gBrowser.selectedTab.linkedBrowser; + let mm = browser.messageManager; + let { ContentTask } = _EU_ChromeUtils.importESModule( + "resource://testing-common/ContentTask.sys.mjs" + ); + + let eventRegisteredPromise = new Promise(resolve => { + mm.addMessageListener( + "Test:MouseMoveRegistered", + function processed(message) { + mm.removeMessageListener("Test:MouseMoveRegistered", processed); + resolve(); + } + ); + }); + let eventReceivedPromise = ContentTask.spawn( + browser, + [aOffsetX, aOffsetY], + ([clientX, clientY]) => { + return new Promise(resolve => { + addEventListener("mousemove", function onMouseMoveEvent(e) { + if (e.clientX == clientX && e.clientY == clientY) { + removeEventListener("mousemove", onMouseMoveEvent); + resolve(); + } + }); + sendAsyncMessage("Test:MouseMoveRegistered"); + }); + } + ); + eventRegisteredPromise.then(() => { + synthesizeNativeMouseEvent({ + type: "mousemove", + target: aTarget, + offsetX: aOffsetX, + offsetY: aOffsetY, + win: aWindow, + }); + }); + return eventReceivedPromise; +} + +/** + * Synthesize a key event. It is targeted at whatever would be targeted by an + * actual keypress by the user, typically the focused element. + * + * @param {String} aKey + * Should be either: + * + * - key value (recommended). If you specify a non-printable key name, + * prepend the ``KEY_`` prefix. Otherwise, specifying a printable key, the + * key value should be specified. + * + * - keyCode name starting with ``VK_`` (e.g., ``VK_RETURN``). This is available + * only for compatibility with legacy API. Don't use this with new tests. + * + * @param {Object} [aEvent] + * Optional event object with more specifics about the key event to + * synthesize. + * @param {String} [aEvent.code] + * If you don't specify this explicitly, it'll be guessed from aKey + * of US keyboard layout. Note that this value may be different + * between browsers. For example, "Insert" is never set only on + * macOS since actual key operation won't cause this code value. + * In such case, the value becomes empty string. + * If you need to emulate non-US keyboard layout or virtual keyboard + * which doesn't emulate hardware key input, you should set this value + * to empty string explicitly. + * @param {Number} [aEvent.repeat] + * If you emulate auto-repeat, you should set the count of repeat. + * This method will automatically synthesize keydown (and keypress). + * @param {*} aEvent.location + * If you want to specify this, you can specify this explicitly. + * However, if you don't specify this value, it will be computed + * from code value. + * @param {String} aEvent.type + * Basically, you shouldn't specify this. Then, this function will + * synthesize keydown (, keypress) and keyup. + * If keydown is specified, this only fires keydown (and keypress if + * it should be fired). + * If keyup is specified, this only fires keyup. + * @param {Number} aEvent.keyCode + * Must be 0 - 255 (0xFF). If this is specified explicitly, + * .keyCode value is initialized with this value. + * @param {Window} aWindow + * Is optional and defaults to the current window object. + * @param {Function} aCallback + * Is optional and can be used to receive notifications from TIP. + * + * @description + * ``accelKey``, ``altKey``, ``altGraphKey``, ``ctrlKey``, ``capsLockKey``, + * ``fnKey``, ``fnLockKey``, ``numLockKey``, ``metaKey``, ``scrollLockKey``, + * ``shiftKey``, ``symbolKey``, ``symbolLockKey`` + * Basically, you shouldn't use these attributes. nsITextInputProcessor + * manages modifier key state when you synthesize modifier key events. + * However, if some of these attributes are true, this function activates + * the modifiers only during dispatching the key events. + * Note that if some of these values are false, they are ignored (i.e., + * not inactivated with this function). + * + */ +function synthesizeKey(aKey, aEvent = undefined, aWindow = window, aCallback) { + const event = aEvent === undefined || aEvent === null ? {} : aEvent; + let dispatchKeydown = + !("type" in event) || event.type === "keydown" || !event.type; + const dispatchKeyup = + !("type" in event) || event.type === "keyup" || !event.type; + + if (dispatchKeydown && aKey == "KEY_Escape") { + let eventForKeydown = Object.assign({}, JSON.parse(JSON.stringify(event))); + eventForKeydown.type = "keydown"; + if ( + _maybeEndDragSession( + // TODO: We should set the last dragover point instead + 0, + 0, + eventForKeydown, + aWindow + ) + ) { + if (!dispatchKeyup) { + return; + } + // We don't need to dispatch only keydown event because it's consumed by + // the drag session. + dispatchKeydown = false; + } + } + + var TIP = _getTIP(aWindow, aCallback); + if (!TIP) { + return; + } + var KeyboardEvent = _getKeyboardEvent(aWindow); + var modifiers = _emulateToActivateModifiers(TIP, event, aWindow); + var keyEventDict = _createKeyboardEventDictionary(aKey, event, TIP, aWindow); + var keyEvent = new KeyboardEvent("", keyEventDict.dictionary); + + try { + if (dispatchKeydown) { + TIP.keydown(keyEvent, keyEventDict.flags); + if ("repeat" in event && event.repeat > 1) { + keyEventDict.dictionary.repeat = true; + var repeatedKeyEvent = new KeyboardEvent("", keyEventDict.dictionary); + for (var i = 1; i < event.repeat; i++) { + TIP.keydown(repeatedKeyEvent, keyEventDict.flags); + } + } + } + if (dispatchKeyup) { + TIP.keyup(keyEvent, keyEventDict.flags); + } + } finally { + _emulateToInactivateModifiers(TIP, modifiers, aWindow); + } +} + +/** + * This is a wrapper around synthesizeKey that waits for the key event to be + * dispatched to the target content. It returns a promise which is resolved + * when the content receives the key event. + * + * This API is supposed to be used in those test cases that synthesize some + * input events to chrome process and have some checks in content. + */ +function synthesizeAndWaitKey( + aKey, + aEvent, + aWindow = window, + checkBeforeSynthesize, + checkAfterSynthesize +) { + let browser = gBrowser.selectedTab.linkedBrowser; + let mm = browser.messageManager; + let keyCode = _createKeyboardEventDictionary(aKey, aEvent, null, aWindow) + .dictionary.keyCode; + let { ContentTask } = _EU_ChromeUtils.importESModule( + "resource://testing-common/ContentTask.sys.mjs" + ); + + let keyRegisteredPromise = new Promise(resolve => { + mm.addMessageListener("Test:KeyRegistered", function processed(message) { + mm.removeMessageListener("Test:KeyRegistered", processed); + resolve(); + }); + }); + // eslint-disable-next-line no-shadow + let keyReceivedPromise = ContentTask.spawn(browser, keyCode, keyCode => { + return new Promise(resolve => { + addEventListener("keyup", function onKeyEvent(e) { + if (e.keyCode == keyCode) { + removeEventListener("keyup", onKeyEvent); + resolve(); + } + }); + sendAsyncMessage("Test:KeyRegistered"); + }); + }); + keyRegisteredPromise.then(() => { + if (checkBeforeSynthesize) { + checkBeforeSynthesize(); + } + synthesizeKey(aKey, aEvent, aWindow); + if (checkAfterSynthesize) { + checkAfterSynthesize(); + } + }); + return keyReceivedPromise; +} + +function _parseNativeModifiers(aModifiers, aWindow = window) { + let modifiers = 0; + if (aModifiers.capsLockKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CAPS_LOCK; + } + if (aModifiers.numLockKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUM_LOCK; + } + if (aModifiers.shiftKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_LEFT; + } + if (aModifiers.shiftRightKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_RIGHT; + } + if (aModifiers.ctrlKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT; + } + if (aModifiers.ctrlRightKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT; + } + if (aModifiers.altKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT; + } + if (aModifiers.altRightKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_RIGHT; + } + if (aModifiers.metaKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT; + } + if (aModifiers.metaRightKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT; + } + if (aModifiers.helpKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_HELP; + } + if (aModifiers.fnKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_FUNCTION; + } + if (aModifiers.numericKeyPadKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUMERIC_KEY_PAD; + } + + if (aModifiers.accelKey) { + modifiers |= _EU_isMac(aWindow) + ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT + : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT; + } + if (aModifiers.accelRightKey) { + modifiers |= _EU_isMac(aWindow) + ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT + : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT; + } + if (aModifiers.altGrKey) { + modifiers |= _EU_isMac(aWindow) + ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT + : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_GRAPH; + } + return modifiers; +} + +// Mac: Any unused number is okay for adding new keyboard layout. +// When you add new keyboard layout here, you need to modify +// TISInputSourceWrapper::InitByLayoutID(). +// Win: These constants can be found by inspecting registry keys under +// HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Keyboard Layouts + +const KEYBOARD_LAYOUT_ARABIC = { + name: "Arabic", + Mac: 6, + Win: 0x00000401, + hasAltGrOnWin: false, +}; +_defineConstant("KEYBOARD_LAYOUT_ARABIC", KEYBOARD_LAYOUT_ARABIC); +const KEYBOARD_LAYOUT_ARABIC_PC = { + name: "Arabic - PC", + Mac: 7, + Win: null, + hasAltGrOnWin: false, +}; +_defineConstant("KEYBOARD_LAYOUT_ARABIC_PC", KEYBOARD_LAYOUT_ARABIC_PC); +const KEYBOARD_LAYOUT_BRAZILIAN_ABNT = { + name: "Brazilian ABNT", + Mac: null, + Win: 0x00000416, + hasAltGrOnWin: true, +}; +_defineConstant( + "KEYBOARD_LAYOUT_BRAZILIAN_ABNT", + KEYBOARD_LAYOUT_BRAZILIAN_ABNT +); +const KEYBOARD_LAYOUT_DVORAK_QWERTY = { + name: "Dvorak-QWERTY", + Mac: 4, + Win: null, + hasAltGrOnWin: false, +}; +_defineConstant("KEYBOARD_LAYOUT_DVORAK_QWERTY", KEYBOARD_LAYOUT_DVORAK_QWERTY); +const KEYBOARD_LAYOUT_EN_US = { + name: "US", + Mac: 0, + Win: 0x00000409, + hasAltGrOnWin: false, +}; +_defineConstant("KEYBOARD_LAYOUT_EN_US", KEYBOARD_LAYOUT_EN_US); +const KEYBOARD_LAYOUT_FRENCH = { + name: "French", + Mac: 8, // Some keys mapped different from PC, e.g., Digit6, Digit8, Equal, Slash and Backslash + Win: 0x0000040c, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_FRENCH", KEYBOARD_LAYOUT_FRENCH); +const KEYBOARD_LAYOUT_FRENCH_PC = { + name: "French-PC", + Mac: 13, // Compatible with Windows + Win: 0x0000040c, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_FRENCH_PC", KEYBOARD_LAYOUT_FRENCH_PC); +const KEYBOARD_LAYOUT_GREEK = { + name: "Greek", + Mac: 1, + Win: 0x00000408, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_GREEK", KEYBOARD_LAYOUT_GREEK); +const KEYBOARD_LAYOUT_GERMAN = { + name: "German", + Mac: 2, + Win: 0x00000407, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_GERMAN", KEYBOARD_LAYOUT_GERMAN); +const KEYBOARD_LAYOUT_HEBREW = { + name: "Hebrew", + Mac: 9, + Win: 0x0000040d, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_HEBREW", KEYBOARD_LAYOUT_HEBREW); +const KEYBOARD_LAYOUT_JAPANESE = { + name: "Japanese", + Mac: null, + Win: 0x00000411, + hasAltGrOnWin: false, +}; +_defineConstant("KEYBOARD_LAYOUT_JAPANESE", KEYBOARD_LAYOUT_JAPANESE); +const KEYBOARD_LAYOUT_KHMER = { + name: "Khmer", + Mac: null, + Win: 0x00000453, + hasAltGrOnWin: true, +}; // available on Win7 or later. +_defineConstant("KEYBOARD_LAYOUT_KHMER", KEYBOARD_LAYOUT_KHMER); +const KEYBOARD_LAYOUT_LITHUANIAN = { + name: "Lithuanian", + Mac: 10, + Win: 0x00010427, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_LITHUANIAN", KEYBOARD_LAYOUT_LITHUANIAN); +const KEYBOARD_LAYOUT_NORWEGIAN = { + name: "Norwegian", + Mac: 11, + Win: 0x00000414, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_NORWEGIAN", KEYBOARD_LAYOUT_NORWEGIAN); +const KEYBOARD_LAYOUT_RUSSIAN = { + name: "Russian", + Mac: null, + Win: 0x00000419, + hasAltGrOnWin: true, // No AltGr, but Ctrl + Alt + Digit8 introduces a char +}; +_defineConstant("KEYBOARD_LAYOUT_RUSSIAN", KEYBOARD_LAYOUT_RUSSIAN); +const KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC = { + name: "Russian - Mnemonic", + Mac: null, + Win: 0x00020419, + hasAltGrOnWin: true, +}; // available on Win8 or later. +_defineConstant( + "KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC", + KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC +); +const KEYBOARD_LAYOUT_SPANISH = { + name: "Spanish", + Mac: 12, + Win: 0x0000040a, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_SPANISH", KEYBOARD_LAYOUT_SPANISH); +const KEYBOARD_LAYOUT_SWEDISH = { + name: "Swedish", + Mac: 3, + Win: 0x0000041d, + hasAltGrOnWin: true, +}; +_defineConstant("KEYBOARD_LAYOUT_SWEDISH", KEYBOARD_LAYOUT_SWEDISH); +const KEYBOARD_LAYOUT_THAI = { + name: "Thai", + Mac: 5, + Win: 0x0002041e, + hasAltGrOnWin: false, +}; +_defineConstant("KEYBOARD_LAYOUT_THAI", KEYBOARD_LAYOUT_THAI); + +/** + * synthesizeNativeKey() dispatches native key event on active window. + * This is implemented only on Windows and Mac. Note that this function + * dispatches the key event asynchronously and returns immediately. If a + * callback function is provided, the callback will be called upon + * completion of the key dispatch. + * + * @param aKeyboardLayout One of KEYBOARD_LAYOUT_* defined above. + * @param aNativeKeyCode A native keycode value defined in + * NativeKeyCodes.js. + * @param aModifiers Modifier keys. If no modifire key is pressed, + * this must be {}. Otherwise, one or more items + * referred in _parseNativeModifiers() must be + * true. + * @param aChars Specify characters which should be generated + * by the key event. + * @param aUnmodifiedChars Specify characters of unmodified (except Shift) + * aChar value. + * @param aCallback If provided, this callback will be invoked + * once the native keys have been processed + * by Gecko. Will never be called if this + * function returns false. + * @return True if this function succeed dispatching + * native key event. Otherwise, false. + */ + +function synthesizeNativeKey( + aKeyboardLayout, + aNativeKeyCode, + aModifiers, + aChars, + aUnmodifiedChars, + aCallback, + aWindow = window +) { + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return false; + } + var nativeKeyboardLayout = null; + if (_EU_isMac(aWindow)) { + nativeKeyboardLayout = aKeyboardLayout.Mac; + } else if (_EU_isWin(aWindow)) { + nativeKeyboardLayout = aKeyboardLayout.Win; + } + if (nativeKeyboardLayout === null) { + return false; + } + + var observer = { + observe(aSubject, aTopic, aData) { + if (aCallback && aTopic == "keyevent") { + aCallback(aData); + } + }, + }; + utils.sendNativeKeyEvent( + nativeKeyboardLayout, + aNativeKeyCode, + _parseNativeModifiers(aModifiers, aWindow), + aChars, + aUnmodifiedChars, + observer + ); + return true; +} + +var _gSeenEvent = false; + +/** + * Indicate that an event with an original target of aExpectedTarget and + * a type of aExpectedEvent is expected to be fired, or not expected to + * be fired. + */ +function _expectEvent(aExpectedTarget, aExpectedEvent, aTestName) { + if (!aExpectedTarget || !aExpectedEvent) { + return null; + } + + _gSeenEvent = false; + + var type = + aExpectedEvent.charAt(0) == "!" + ? aExpectedEvent.substring(1) + : aExpectedEvent; + var eventHandler = function (event) { + var epassed = + !_gSeenEvent && + event.originalTarget == aExpectedTarget && + event.type == type; + is( + epassed, + true, + aTestName + " " + type + " event target " + (_gSeenEvent ? "twice" : "") + ); + _gSeenEvent = true; + }; + + aExpectedTarget.addEventListener(type, eventHandler); + return eventHandler; +} + +/** + * Check if the event was fired or not. The event handler aEventHandler + * will be removed. + */ +function _checkExpectedEvent( + aExpectedTarget, + aExpectedEvent, + aEventHandler, + aTestName +) { + if (aEventHandler) { + var expectEvent = aExpectedEvent.charAt(0) != "!"; + var type = expectEvent ? aExpectedEvent : aExpectedEvent.substring(1); + aExpectedTarget.removeEventListener(type, aEventHandler); + var desc = type + " event"; + if (!expectEvent) { + desc += " not"; + } + is(_gSeenEvent, expectEvent, aTestName + " " + desc + " fired"); + } + + _gSeenEvent = false; +} + +/** + * Similar to synthesizeMouse except that a test is performed to see if an + * event is fired at the right target as a result. + * + * aExpectedTarget - the expected originalTarget of the event. + * aExpectedEvent - the expected type of the event, such as 'select'. + * aTestName - the test name when outputing results + * + * To test that an event is not fired, use an expected type preceded by an + * exclamation mark, such as '!select'. This might be used to test that a + * click on a disabled element doesn't fire certain events for instance. + * + * aWindow is optional, and defaults to the current window object. + */ +function synthesizeMouseExpectEvent( + aTarget, + aOffsetX, + aOffsetY, + aEvent, + aExpectedTarget, + aExpectedEvent, + aTestName, + aWindow +) { + var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName); + synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow); + _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName); +} + +/** + * Similar to synthesizeKey except that a test is performed to see if an + * event is fired at the right target as a result. + * + * aExpectedTarget - the expected originalTarget of the event. + * aExpectedEvent - the expected type of the event, such as 'select'. + * aTestName - the test name when outputing results + * + * To test that an event is not fired, use an expected type preceded by an + * exclamation mark, such as '!select'. + * + * aWindow is optional, and defaults to the current window object. + */ +function synthesizeKeyExpectEvent( + key, + aEvent, + aExpectedTarget, + aExpectedEvent, + aTestName, + aWindow +) { + var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName); + synthesizeKey(key, aEvent, aWindow); + _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName); +} + +function disableNonTestMouseEvents(aDisable) { + var domutils = _getDOMWindowUtils(); + domutils.disableNonTestMouseEvents(aDisable); +} + +function _getDOMWindowUtils(aWindow = window) { + // Leave this here as something, somewhere, passes a falsy argument + // to this, causing the |window| default argument not to get picked up. + if (!aWindow) { + aWindow = window; + } + + // If documentURIObject exists or `window` is a stub object, we're in + // a chrome scope, so don't bother trying to go through SpecialPowers. + if (!window.document || window.document.documentURIObject) { + return aWindow.windowUtils; + } + + // we need parent.SpecialPowers for: + // layout/base/tests/test_reftests_with_caret.html + // chrome: toolkit/content/tests/chrome/test_findbar.xul + // chrome: toolkit/content/tests/chrome/test_popup_anchor.xul + if ("SpecialPowers" in window && window.SpecialPowers != undefined) { + return SpecialPowers.getDOMWindowUtils(aWindow); + } + if ("SpecialPowers" in parent && parent.SpecialPowers != undefined) { + return parent.SpecialPowers.getDOMWindowUtils(aWindow); + } + + // TODO: this is assuming we are in chrome space + return aWindow.windowUtils; +} + +function _defineConstant(name, value) { + Object.defineProperty(this, name, { + value, + enumerable: true, + writable: false, + }); +} + +const COMPOSITION_ATTR_RAW_CLAUSE = + _EU_Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE; +_defineConstant("COMPOSITION_ATTR_RAW_CLAUSE", COMPOSITION_ATTR_RAW_CLAUSE); +const COMPOSITION_ATTR_SELECTED_RAW_CLAUSE = + _EU_Ci.nsITextInputProcessor.ATTR_SELECTED_RAW_CLAUSE; +_defineConstant( + "COMPOSITION_ATTR_SELECTED_RAW_CLAUSE", + COMPOSITION_ATTR_SELECTED_RAW_CLAUSE +); +const COMPOSITION_ATTR_CONVERTED_CLAUSE = + _EU_Ci.nsITextInputProcessor.ATTR_CONVERTED_CLAUSE; +_defineConstant( + "COMPOSITION_ATTR_CONVERTED_CLAUSE", + COMPOSITION_ATTR_CONVERTED_CLAUSE +); +const COMPOSITION_ATTR_SELECTED_CLAUSE = + _EU_Ci.nsITextInputProcessor.ATTR_SELECTED_CLAUSE; +_defineConstant( + "COMPOSITION_ATTR_SELECTED_CLAUSE", + COMPOSITION_ATTR_SELECTED_CLAUSE +); + +var TIPMap = new WeakMap(); + +function _getTIP(aWindow, aCallback) { + if (!aWindow) { + aWindow = window; + } + var tip; + if (TIPMap.has(aWindow)) { + tip = TIPMap.get(aWindow); + } else { + tip = _EU_Cc["@mozilla.org/text-input-processor;1"].createInstance( + _EU_Ci.nsITextInputProcessor + ); + TIPMap.set(aWindow, tip); + } + if (!tip.beginInputTransactionForTests(aWindow, aCallback)) { + tip = null; + TIPMap.delete(aWindow); + } + return tip; +} + +function _getKeyboardEvent(aWindow = window) { + if (typeof KeyboardEvent != "undefined") { + try { + // See if the object can be instantiated; sometimes this yields + // 'TypeError: can't access dead object' or 'KeyboardEvent is not a constructor'. + new KeyboardEvent("", {}); + return KeyboardEvent; + } catch (ex) {} + } + if (typeof content != "undefined" && "KeyboardEvent" in content) { + return content.KeyboardEvent; + } + return aWindow.KeyboardEvent; +} + +// eslint-disable-next-line complexity +function _guessKeyNameFromKeyCode(aKeyCode, aWindow = window) { + var KeyboardEvent = _getKeyboardEvent(aWindow); + switch (aKeyCode) { + case KeyboardEvent.DOM_VK_CANCEL: + return "Cancel"; + case KeyboardEvent.DOM_VK_HELP: + return "Help"; + case KeyboardEvent.DOM_VK_BACK_SPACE: + return "Backspace"; + case KeyboardEvent.DOM_VK_TAB: + return "Tab"; + case KeyboardEvent.DOM_VK_CLEAR: + return "Clear"; + case KeyboardEvent.DOM_VK_RETURN: + return "Enter"; + case KeyboardEvent.DOM_VK_SHIFT: + return "Shift"; + case KeyboardEvent.DOM_VK_CONTROL: + return "Control"; + case KeyboardEvent.DOM_VK_ALT: + return "Alt"; + case KeyboardEvent.DOM_VK_PAUSE: + return "Pause"; + case KeyboardEvent.DOM_VK_EISU: + return "Eisu"; + case KeyboardEvent.DOM_VK_ESCAPE: + return "Escape"; + case KeyboardEvent.DOM_VK_CONVERT: + return "Convert"; + case KeyboardEvent.DOM_VK_NONCONVERT: + return "NonConvert"; + case KeyboardEvent.DOM_VK_ACCEPT: + return "Accept"; + case KeyboardEvent.DOM_VK_MODECHANGE: + return "ModeChange"; + case KeyboardEvent.DOM_VK_PAGE_UP: + return "PageUp"; + case KeyboardEvent.DOM_VK_PAGE_DOWN: + return "PageDown"; + case KeyboardEvent.DOM_VK_END: + return "End"; + case KeyboardEvent.DOM_VK_HOME: + return "Home"; + case KeyboardEvent.DOM_VK_LEFT: + return "ArrowLeft"; + case KeyboardEvent.DOM_VK_UP: + return "ArrowUp"; + case KeyboardEvent.DOM_VK_RIGHT: + return "ArrowRight"; + case KeyboardEvent.DOM_VK_DOWN: + return "ArrowDown"; + case KeyboardEvent.DOM_VK_SELECT: + return "Select"; + case KeyboardEvent.DOM_VK_PRINT: + return "Print"; + case KeyboardEvent.DOM_VK_EXECUTE: + return "Execute"; + case KeyboardEvent.DOM_VK_PRINTSCREEN: + return "PrintScreen"; + case KeyboardEvent.DOM_VK_INSERT: + return "Insert"; + case KeyboardEvent.DOM_VK_DELETE: + return "Delete"; + case KeyboardEvent.DOM_VK_WIN: + return "OS"; + case KeyboardEvent.DOM_VK_CONTEXT_MENU: + return "ContextMenu"; + case KeyboardEvent.DOM_VK_SLEEP: + return "Standby"; + case KeyboardEvent.DOM_VK_F1: + return "F1"; + case KeyboardEvent.DOM_VK_F2: + return "F2"; + case KeyboardEvent.DOM_VK_F3: + return "F3"; + case KeyboardEvent.DOM_VK_F4: + return "F4"; + case KeyboardEvent.DOM_VK_F5: + return "F5"; + case KeyboardEvent.DOM_VK_F6: + return "F6"; + case KeyboardEvent.DOM_VK_F7: + return "F7"; + case KeyboardEvent.DOM_VK_F8: + return "F8"; + case KeyboardEvent.DOM_VK_F9: + return "F9"; + case KeyboardEvent.DOM_VK_F10: + return "F10"; + case KeyboardEvent.DOM_VK_F11: + return "F11"; + case KeyboardEvent.DOM_VK_F12: + return "F12"; + case KeyboardEvent.DOM_VK_F13: + return "F13"; + case KeyboardEvent.DOM_VK_F14: + return "F14"; + case KeyboardEvent.DOM_VK_F15: + return "F15"; + case KeyboardEvent.DOM_VK_F16: + return "F16"; + case KeyboardEvent.DOM_VK_F17: + return "F17"; + case KeyboardEvent.DOM_VK_F18: + return "F18"; + case KeyboardEvent.DOM_VK_F19: + return "F19"; + case KeyboardEvent.DOM_VK_F20: + return "F20"; + case KeyboardEvent.DOM_VK_F21: + return "F21"; + case KeyboardEvent.DOM_VK_F22: + return "F22"; + case KeyboardEvent.DOM_VK_F23: + return "F23"; + case KeyboardEvent.DOM_VK_F24: + return "F24"; + case KeyboardEvent.DOM_VK_NUM_LOCK: + return "NumLock"; + case KeyboardEvent.DOM_VK_SCROLL_LOCK: + return "ScrollLock"; + case KeyboardEvent.DOM_VK_VOLUME_MUTE: + return "AudioVolumeMute"; + case KeyboardEvent.DOM_VK_VOLUME_DOWN: + return "AudioVolumeDown"; + case KeyboardEvent.DOM_VK_VOLUME_UP: + return "AudioVolumeUp"; + case KeyboardEvent.DOM_VK_META: + return "Meta"; + case KeyboardEvent.DOM_VK_ALTGR: + return "AltGraph"; + case KeyboardEvent.DOM_VK_PROCESSKEY: + return "Process"; + case KeyboardEvent.DOM_VK_ATTN: + return "Attn"; + case KeyboardEvent.DOM_VK_CRSEL: + return "CrSel"; + case KeyboardEvent.DOM_VK_EXSEL: + return "ExSel"; + case KeyboardEvent.DOM_VK_EREOF: + return "EraseEof"; + case KeyboardEvent.DOM_VK_PLAY: + return "Play"; + default: + return "Unidentified"; + } +} + +function _createKeyboardEventDictionary( + aKey, + aKeyEvent, + aTIP = null, + aWindow = window +) { + var result = { dictionary: null, flags: 0 }; + var keyCodeIsDefined = "keyCode" in aKeyEvent; + var keyCode = + keyCodeIsDefined && aKeyEvent.keyCode >= 0 && aKeyEvent.keyCode <= 255 + ? aKeyEvent.keyCode + : 0; + var keyName = "Unidentified"; + var code = aKeyEvent.code; + if (!aTIP) { + aTIP = _getTIP(aWindow); + } + if (aKey.indexOf("KEY_") == 0) { + keyName = aKey.substr("KEY_".length); + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY; + if (code === undefined) { + code = aTIP.computeCodeValueOfNonPrintableKey( + keyName, + aKeyEvent.location + ); + } + } else if (aKey.indexOf("VK_") == 0) { + keyCode = _getKeyboardEvent(aWindow)["DOM_" + aKey]; + if (!keyCode) { + throw new Error("Unknown key: " + aKey); + } + keyName = _guessKeyNameFromKeyCode(keyCode, aWindow); + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY; + if (code === undefined) { + code = aTIP.computeCodeValueOfNonPrintableKey( + keyName, + aKeyEvent.location + ); + } + } else if (aKey != "") { + keyName = aKey; + if (!keyCodeIsDefined) { + keyCode = aTIP.guessKeyCodeValueOfPrintableKeyInUSEnglishKeyboardLayout( + aKey, + aKeyEvent.location + ); + } + if (!keyCode) { + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO; + } + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY; + if (code === undefined) { + code = aTIP.guessCodeValueOfPrintableKeyInUSEnglishKeyboardLayout( + keyName, + aKeyEvent.location + ); + } + } + var locationIsDefined = "location" in aKeyEvent; + if (locationIsDefined && aKeyEvent.location === 0) { + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEY_LOCATION_STANDARD; + } + if (aKeyEvent.doNotMarkKeydownAsProcessed) { + result.flags |= + _EU_Ci.nsITextInputProcessor.KEY_DONT_MARK_KEYDOWN_AS_PROCESSED; + } + if (aKeyEvent.markKeyupAsProcessed) { + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_MARK_KEYUP_AS_PROCESSED; + } + result.dictionary = { + key: keyName, + code, + location: locationIsDefined ? aKeyEvent.location : 0, + repeat: "repeat" in aKeyEvent ? aKeyEvent.repeat === true : false, + keyCode, + }; + return result; +} + +function _emulateToActivateModifiers(aTIP, aKeyEvent, aWindow = window) { + if (!aKeyEvent) { + return null; + } + var KeyboardEvent = _getKeyboardEvent(aWindow); + + var modifiers = { + normal: [ + { key: "Alt", attr: "altKey" }, + { key: "AltGraph", attr: "altGraphKey" }, + { key: "Control", attr: "ctrlKey" }, + { key: "Fn", attr: "fnKey" }, + { key: "Meta", attr: "metaKey" }, + { key: "Shift", attr: "shiftKey" }, + { key: "Symbol", attr: "symbolKey" }, + { key: _EU_isMac(aWindow) ? "Meta" : "Control", attr: "accelKey" }, + ], + lockable: [ + { key: "CapsLock", attr: "capsLockKey" }, + { key: "FnLock", attr: "fnLockKey" }, + { key: "NumLock", attr: "numLockKey" }, + { key: "ScrollLock", attr: "scrollLockKey" }, + { key: "SymbolLock", attr: "symbolLockKey" }, + ], + }; + + for (let i = 0; i < modifiers.normal.length; i++) { + if (!aKeyEvent[modifiers.normal[i].attr]) { + continue; + } + if (aTIP.getModifierState(modifiers.normal[i].key)) { + continue; // already activated. + } + let event = new KeyboardEvent("", { key: modifiers.normal[i].key }); + aTIP.keydown( + event, + aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT + ); + modifiers.normal[i].activated = true; + } + for (let i = 0; i < modifiers.lockable.length; i++) { + if (!aKeyEvent[modifiers.lockable[i].attr]) { + continue; + } + if (aTIP.getModifierState(modifiers.lockable[i].key)) { + continue; // already activated. + } + let event = new KeyboardEvent("", { key: modifiers.lockable[i].key }); + aTIP.keydown( + event, + aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT + ); + aTIP.keyup( + event, + aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT + ); + modifiers.lockable[i].activated = true; + } + return modifiers; +} + +function _emulateToInactivateModifiers(aTIP, aModifiers, aWindow = window) { + if (!aModifiers) { + return; + } + var KeyboardEvent = _getKeyboardEvent(aWindow); + for (let i = 0; i < aModifiers.normal.length; i++) { + if (!aModifiers.normal[i].activated) { + continue; + } + let event = new KeyboardEvent("", { key: aModifiers.normal[i].key }); + aTIP.keyup( + event, + aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT + ); + } + for (let i = 0; i < aModifiers.lockable.length; i++) { + if (!aModifiers.lockable[i].activated) { + continue; + } + if (!aTIP.getModifierState(aModifiers.lockable[i].key)) { + continue; // who already inactivated this? + } + let event = new KeyboardEvent("", { key: aModifiers.lockable[i].key }); + aTIP.keydown( + event, + aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT + ); + aTIP.keyup( + event, + aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT + ); + } +} + +/** + * Synthesize a composition event and keydown event and keyup events unless + * you prevent to dispatch them explicitly (see aEvent.key's explanation). + * + * Note that you shouldn't call this with "compositionstart" unless you need to + * test compositionstart event which is NOT followed by compositionupdate + * event immediately. Typically, native IME starts composition with + * a pair of keydown and keyup event and dispatch compositionstart and + * compositionupdate (and non-standard text event) between them. So, in most + * cases, you should call synthesizeCompositionChange() directly. + * If you call this with compositionstart, keyup event will be fired + * immediately after compositionstart. In other words, you should use + * "compositionstart" only when you need to emulate IME which just starts + * composition with compositionstart event but does not send composing text to + * us until committing the composition. This is behavior of some Chinese IMEs. + * + * @param aEvent The composition event information. This must + * have |type| member. The value must be + * "compositionstart", "compositionend", + * "compositioncommitasis" or "compositioncommit". + * + * And also this may have |data| and |locale| which + * would be used for the value of each property of + * the composition event. Note that the |data| is + * ignored if the event type is "compositionstart" + * or "compositioncommitasis". + * + * If |key| is undefined, "keydown" and "keyup" + * events which are marked as "processed by IME" + * are dispatched. If |key| is not null, "keydown" + * and/or "keyup" events are dispatched (if the + * |key.type| is specified as "keydown", only + * "keydown" event is dispatched). Otherwise, + * i.e., if |key| is null, neither "keydown" nor + * "keyup" event is dispatched. + * + * If |key.doNotMarkKeydownAsProcessed| is not true, + * key value and keyCode value of "keydown" event + * will be set to "Process" and DOM_VK_PROCESSKEY. + * If |key.markKeyupAsProcessed| is true, + * key value and keyCode value of "keyup" event + * will be set to "Process" and DOM_VK_PROCESSKEY. + * @param aWindow Optional (If null, current |window| will be used) + * @param aCallback Optional (If non-null, use the callback for + * receiving notifications to IME) + */ +function synthesizeComposition(aEvent, aWindow = window, aCallback) { + var TIP = _getTIP(aWindow, aCallback); + if (!TIP) { + return; + } + var KeyboardEvent = _getKeyboardEvent(aWindow); + var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow); + var keyEventDict = { dictionary: null, flags: 0 }; + var keyEvent = null; + if (aEvent.key && typeof aEvent.key.key === "string") { + keyEventDict = _createKeyboardEventDictionary( + aEvent.key.key, + aEvent.key, + TIP, + aWindow + ); + keyEvent = new KeyboardEvent( + // eslint-disable-next-line no-nested-ternary + aEvent.key.type === "keydown" + ? "keydown" + : aEvent.key.type === "keyup" + ? "keyup" + : "", + keyEventDict.dictionary + ); + } else if (aEvent.key === undefined) { + keyEventDict = _createKeyboardEventDictionary( + "KEY_Process", + {}, + TIP, + aWindow + ); + keyEvent = new KeyboardEvent("", keyEventDict.dictionary); + } + try { + switch (aEvent.type) { + case "compositionstart": + TIP.startComposition(keyEvent, keyEventDict.flags); + break; + case "compositioncommitasis": + TIP.commitComposition(keyEvent, keyEventDict.flags); + break; + case "compositioncommit": + TIP.commitCompositionWith(aEvent.data, keyEvent, keyEventDict.flags); + break; + } + } finally { + _emulateToInactivateModifiers(TIP, modifiers, aWindow); + } +} +/** + * Synthesize eCompositionChange event which causes a DOM text event, may + * cause compositionupdate event, and causes keydown event and keyup event + * unless you prevent to dispatch them explicitly (see aEvent.key's + * explanation). + * + * Note that if you call this when there is no composition, compositionstart + * event will be fired automatically. This is better than you use + * synthesizeComposition("compositionstart") in most cases. See the + * explanation of synthesizeComposition(). + * + * @param aEvent The compositionchange event's information, this has + * |composition| and |caret| members. |composition| has + * |string| and |clauses| members. |clauses| must be array + * object. Each object has |length| and |attr|. And |caret| + * has |start| and |length|. See the following tree image. + * + * aEvent + * +-- composition + * | +-- string + * | +-- clauses[] + * | +-- length + * | +-- attr + * +-- caret + * | +-- start + * | +-- length + * +-- key + * + * Set the composition string to |composition.string|. Set its + * clauses information to the |clauses| array. + * + * When it's composing, set the each clauses' length to the + * |composition.clauses[n].length|. The sum of the all length + * values must be same as the length of |composition.string|. + * Set nsICompositionStringSynthesizer.ATTR_* to the + * |composition.clauses[n].attr|. + * + * When it's not composing, set 0 to the + * |composition.clauses[0].length| and + * |composition.clauses[0].attr|. + * + * Set caret position to the |caret.start|. It's offset from + * the start of the composition string. Set caret length to + * |caret.length|. If it's larger than 0, it should be wide + * caret. However, current nsEditor doesn't support wide + * caret, therefore, you should always set 0 now. + * + * If |key| is undefined, "keydown" and "keyup" events which + * are marked as "processed by IME" are dispatched. If |key| + * is not null, "keydown" and/or "keyup" events are dispatched + * (if the |key.type| is specified as "keydown", only "keydown" + * event is dispatched). Otherwise, i.e., if |key| is null, + * neither "keydown" nor "keyup" event is dispatched. + * If |key.doNotMarkKeydownAsProcessed| is not true, key value + * and keyCode value of "keydown" event will be set to + * "Process" and DOM_VK_PROCESSKEY. + * If |key.markKeyupAsProcessed| is true key value and keyCode + * value of "keyup" event will be set to "Process" and + * DOM_VK_PROCESSKEY. + * + * @param aWindow Optional (If null, current |window| will be used) + * @param aCallback Optional (If non-null, use the callback for receiving + * notifications to IME) + */ +function synthesizeCompositionChange(aEvent, aWindow = window, aCallback) { + var TIP = _getTIP(aWindow, aCallback); + if (!TIP) { + return; + } + var KeyboardEvent = _getKeyboardEvent(aWindow); + + if ( + !aEvent.composition || + !aEvent.composition.clauses || + !aEvent.composition.clauses[0] + ) { + return; + } + + TIP.setPendingCompositionString(aEvent.composition.string); + if (aEvent.composition.clauses[0].length) { + for (var i = 0; i < aEvent.composition.clauses.length; i++) { + switch (aEvent.composition.clauses[i].attr) { + case TIP.ATTR_RAW_CLAUSE: + case TIP.ATTR_SELECTED_RAW_CLAUSE: + case TIP.ATTR_CONVERTED_CLAUSE: + case TIP.ATTR_SELECTED_CLAUSE: + TIP.appendClauseToPendingComposition( + aEvent.composition.clauses[i].length, + aEvent.composition.clauses[i].attr + ); + break; + case 0: + // Ignore dummy clause for the argument. + break; + default: + throw new Error("invalid clause attribute specified"); + } + } + } + + if (aEvent.caret) { + TIP.setCaretInPendingComposition(aEvent.caret.start); + } + + var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow); + try { + var keyEventDict = { dictionary: null, flags: 0 }; + var keyEvent = null; + if (aEvent.key && typeof aEvent.key.key === "string") { + keyEventDict = _createKeyboardEventDictionary( + aEvent.key.key, + aEvent.key, + TIP, + aWindow + ); + keyEvent = new KeyboardEvent( + // eslint-disable-next-line no-nested-ternary + aEvent.key.type === "keydown" + ? "keydown" + : aEvent.key.type === "keyup" + ? "keyup" + : "", + keyEventDict.dictionary + ); + } else if (aEvent.key === undefined) { + keyEventDict = _createKeyboardEventDictionary( + "KEY_Process", + {}, + TIP, + aWindow + ); + keyEvent = new KeyboardEvent("", keyEventDict.dictionary); + } + TIP.flushPendingComposition(keyEvent, keyEventDict.flags); + } finally { + _emulateToInactivateModifiers(TIP, modifiers, aWindow); + } +} + +// Must be synchronized with nsIDOMWindowUtils. +const QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK = 0x0000; +const QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK = 0x0001; + +const QUERY_CONTENT_FLAG_SELECTION_NORMAL = 0x0000; +const QUERY_CONTENT_FLAG_SELECTION_SPELLCHECK = 0x0002; +const QUERY_CONTENT_FLAG_SELECTION_IME_RAWINPUT = 0x0004; +const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDRAWTEXT = 0x0008; +const QUERY_CONTENT_FLAG_SELECTION_IME_CONVERTEDTEXT = 0x0010; +const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDCONVERTEDTEXT = 0x0020; +const QUERY_CONTENT_FLAG_SELECTION_ACCESSIBILITY = 0x0040; +const QUERY_CONTENT_FLAG_SELECTION_FIND = 0x0080; +const QUERY_CONTENT_FLAG_SELECTION_URLSECONDARY = 0x0100; +const QUERY_CONTENT_FLAG_SELECTION_URLSTRIKEOUT = 0x0200; + +const QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT = 0x0400; + +const SELECTION_SET_FLAG_USE_NATIVE_LINE_BREAK = 0x0000; +const SELECTION_SET_FLAG_USE_XP_LINE_BREAK = 0x0001; +const SELECTION_SET_FLAG_REVERSE = 0x0002; + +/** + * Synthesize a query text content event. + * + * @param aOffset The character offset. 0 means the first character in the + * selection root. + * @param aLength The length of getting text. If the length is too long, + * the extra length is ignored. + * @param aIsRelative Optional (If true, aOffset is relative to start of + * composition if there is, or start of selection.) + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeQueryTextContent(aOffset, aLength, aIsRelative, aWindow) { + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return null; + } + var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK; + if (aIsRelative === true) { + flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT; + } + return utils.sendQueryContentEvent( + utils.QUERY_TEXT_CONTENT, + aOffset, + aLength, + 0, + 0, + flags + ); +} + +/** + * Synthesize a query selected text event. + * + * @param aSelectionType Optional, one of QUERY_CONTENT_FLAG_SELECTION_*. + * If null, QUERY_CONTENT_FLAG_SELECTION_NORMAL will + * be used. + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeQuerySelectedText(aSelectionType, aWindow) { + var utils = _getDOMWindowUtils(aWindow); + var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK; + if (aSelectionType) { + flags |= aSelectionType; + } + + return utils.sendQueryContentEvent( + utils.QUERY_SELECTED_TEXT, + 0, + 0, + 0, + 0, + flags + ); +} + +/** + * Synthesize a query caret rect event. + * + * @param aOffset The caret offset. 0 means left side of the first character + * in the selection root. + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeQueryCaretRect(aOffset, aWindow) { + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return null; + } + return utils.sendQueryContentEvent( + utils.QUERY_CARET_RECT, + aOffset, + 0, + 0, + 0, + QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK + ); +} + +/** + * Synthesize a selection set event. + * + * @param aOffset The character offset. 0 means the first character in the + * selection root. + * @param aLength The length of the text. If the length is too long, + * the extra length is ignored. + * @param aReverse If true, the selection is from |aOffset + aLength| to + * |aOffset|. Otherwise, from |aOffset| to |aOffset + aLength|. + * @param aWindow Optional (If null, current |window| will be used) + * @return True, if succeeded. Otherwise false. + */ +async function synthesizeSelectionSet( + aOffset, + aLength, + aReverse, + aWindow = window +) { + const utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return false; + } + // eSetSelection event will be compared with selection cache in + // IMEContentObserver, but it may have not been updated yet. Therefore, we + // need to flush pending things of IMEContentObserver. + await new Promise(resolve => + aWindow.requestAnimationFrame(() => aWindow.requestAnimationFrame(resolve)) + ); + const flags = aReverse ? SELECTION_SET_FLAG_REVERSE : 0; + return utils.sendSelectionSetEvent(aOffset, aLength, flags); +} + +/** + * Synthesize a query text rect event. + * + * @param aOffset The character offset. 0 means the first character in the + * selection root. + * @param aLength The length of the text. If the length is too long, + * the extra length is ignored. + * @param aIsRelative Optional (If true, aOffset is relative to start of + * composition if there is, or start of selection.) + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeQueryTextRect(aOffset, aLength, aIsRelative, aWindow) { + if (aIsRelative !== undefined && typeof aIsRelative !== "boolean") { + throw new Error( + "Maybe, you set Window object to the 3rd argument, but it should be a boolean value" + ); + } + var utils = _getDOMWindowUtils(aWindow); + let flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK; + if (aIsRelative === true) { + flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT; + } + return utils.sendQueryContentEvent( + utils.QUERY_TEXT_RECT, + aOffset, + aLength, + 0, + 0, + flags + ); +} + +/** + * Synthesize a query text rect array event. + * + * @param aOffset The character offset. 0 means the first character in the + * selection root. + * @param aLength The length of the text. If the length is too long, + * the extra length is ignored. + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeQueryTextRectArray(aOffset, aLength, aWindow) { + var utils = _getDOMWindowUtils(aWindow); + return utils.sendQueryContentEvent( + utils.QUERY_TEXT_RECT_ARRAY, + aOffset, + aLength, + 0, + 0, + QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK + ); +} + +/** + * Synthesize a query editor rect event. + * + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeQueryEditorRect(aWindow) { + var utils = _getDOMWindowUtils(aWindow); + return utils.sendQueryContentEvent( + utils.QUERY_EDITOR_RECT, + 0, + 0, + 0, + 0, + QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK + ); +} + +/** + * Synthesize a character at point event. + * + * @param aX, aY The offset in the client area of the DOM window. + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeCharAtPoint(aX, aY, aWindow) { + var utils = _getDOMWindowUtils(aWindow); + return utils.sendQueryContentEvent( + utils.QUERY_CHARACTER_AT_POINT, + 0, + 0, + aX, + aY, + QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK + ); +} + +/** + * INTERNAL USE ONLY + * Create an event object to pass to sendDragEvent. + * + * @param aType The string represents drag event type. + * @param aDestElement The element to fire the drag event, used to calculate + * screenX/Y and clientX/Y. + * @param aDestWindow Optional; Defaults to the current window object. + * @param aDataTransfer dataTransfer for current drag session. + * @param aDragEvent The object contains properties to override the event + * object + * @return An object to pass to sendDragEvent. + */ +function createDragEventObject( + aType, + aDestElement, + aDestWindow, + aDataTransfer, + aDragEvent +) { + var destRect = aDestElement.getBoundingClientRect(); + var destClientX = destRect.left + destRect.width / 2; + var destClientY = destRect.top + destRect.height / 2; + var destScreenX = aDestWindow.mozInnerScreenX + destClientX; + var destScreenY = aDestWindow.mozInnerScreenY + destClientY; + if ("clientX" in aDragEvent && !("screenX" in aDragEvent)) { + aDragEvent.screenX = aDestWindow.mozInnerScreenX + aDragEvent.clientX; + } + if ("clientY" in aDragEvent && !("screenY" in aDragEvent)) { + aDragEvent.screenY = aDestWindow.mozInnerScreenY + aDragEvent.clientY; + } + + // Wrap only in plain mochitests + let dataTransfer; + if (aDataTransfer) { + dataTransfer = _EU_maybeUnwrap( + _EU_maybeWrap(aDataTransfer).mozCloneForEvent(aType) + ); + + // Copy over the drop effect. This isn't copied over by Clone, as it uses + // more complex logic in the actual implementation (see + // nsContentUtils::SetDataTransferInEvent for actual impl). + dataTransfer.dropEffect = aDataTransfer.dropEffect; + } + + return Object.assign( + { + type: aType, + screenX: destScreenX, + screenY: destScreenY, + clientX: destClientX, + clientY: destClientY, + dataTransfer, + _domDispatchOnly: aDragEvent._domDispatchOnly, + }, + aDragEvent + ); +} + +/** + * Emulate a event sequence of dragstart, dragenter, and dragover. + * + * @param {Element} aSrcElement + * The element to use to start the drag. + * @param {Element} aDestElement + * The element to fire the dragover, dragenter events + * @param {Array} aDragData + * The data to supply for the data transfer. + * This data is in the format: + * + * [ + * [ + * {"type": value, "data": value }, + * ..., + * ], + * ... + * ] + * + * Pass null to avoid modifying dataTransfer. + * @param {String} [aDropEffect="move"] + * The drop effect to set during the dragstart event, or 'move' if omitted. + * @param {Window} [aWindow=window] + * The window in which the drag happens. Defaults to the window in which + * EventUtils.js is loaded. + * @param {Window} [aDestWindow=aWindow] + * Used when aDestElement is in a different window than aSrcElement. + * Default is to match ``aWindow``. + * @param {Object} [aDragEvent={}] + * Defaults to empty object. Overwrites an object passed to sendDragEvent. + * @return {Array} + * A two element array, where the first element is the value returned + * from sendDragEvent for dragover event, and the second element is the + * dataTransfer for the current drag session. + */ +function synthesizeDragOver( + aSrcElement, + aDestElement, + aDragData, + aDropEffect, + aWindow, + aDestWindow, + aDragEvent = {} +) { + if (!aWindow) { + aWindow = window; + } + if (!aDestWindow) { + aDestWindow = aWindow; + } + + // eslint-disable-next-line mozilla/use-services + const obs = _EU_Cc["@mozilla.org/observer-service;1"].getService( + _EU_Ci.nsIObserverService + ); + const ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService( + _EU_Ci.nsIDragService + ); + var sess = ds.getCurrentSession(); + + // This method runs before other callbacks, and acts as a way to inject the + // initial drag data into the DataTransfer. + function fillDrag(event) { + if (aDragData) { + for (var i = 0; i < aDragData.length; i++) { + var item = aDragData[i]; + for (var j = 0; j < item.length; j++) { + _EU_maybeWrap(event.dataTransfer).mozSetDataAt( + item[j].type, + item[j].data, + i + ); + } + } + } + event.dataTransfer.dropEffect = aDropEffect || "move"; + event.preventDefault(); + } + + function trapDrag(subject, topic) { + if (topic == "on-datatransfer-available") { + sess.dataTransfer = _EU_maybeUnwrap( + _EU_maybeWrap(subject).mozCloneForEvent("drop") + ); + sess.dataTransfer.dropEffect = subject.dropEffect; + } + } + + // need to use real mouse action + aWindow.addEventListener("dragstart", fillDrag, true); + obs.addObserver(trapDrag, "on-datatransfer-available"); + synthesizeMouseAtCenter(aSrcElement, { type: "mousedown" }, aWindow); + + var rect = aSrcElement.getBoundingClientRect(); + var x = rect.width / 2; + var y = rect.height / 2; + synthesizeMouse(aSrcElement, x, y, { type: "mousemove" }, aWindow); + synthesizeMouse(aSrcElement, x + 10, y + 10, { type: "mousemove" }, aWindow); + aWindow.removeEventListener("dragstart", fillDrag, true); + obs.removeObserver(trapDrag, "on-datatransfer-available"); + + var dataTransfer = sess.dataTransfer; + if (!dataTransfer) { + throw new Error("No data transfer object after synthesizing the mouse!"); + } + + // The EventStateManager will fire our dragenter event if it needs to. + var event = createDragEventObject( + "dragover", + aDestElement, + aDestWindow, + dataTransfer, + aDragEvent + ); + var result = sendDragEvent(event, aDestElement, aDestWindow); + + return [result, dataTransfer]; +} + +/** + * Emulate the drop event and mouseup event. + * This should be called after synthesizeDragOver. + * + * @param {*} aResult + * The first element of the array returned from ``synthesizeDragOver``. + * @param {DataTransfer} aDataTransfer + * The second element of the array returned from ``synthesizeDragOver``. + * @param {Element} aDestElement + * The element on which to fire the drop event. + * @param {Window} [aDestWindow=window] + * The window in which the drop happens. Defaults to the window in which + * EventUtils.js is loaded. + * @param {Object} [aDragEvent={}] + * Defaults to empty object. Overwrites an object passed to sendDragEvent. + * @return {String} + * "none" if aResult is true, ``aDataTransfer.dropEffect`` otherwise. + */ +function synthesizeDropAfterDragOver( + aResult, + aDataTransfer, + aDestElement, + aDestWindow, + aDragEvent = {} +) { + if (!aDestWindow) { + aDestWindow = window; + } + + var effect = aDataTransfer.dropEffect; + var event; + + if (aResult) { + effect = "none"; + } else if (effect != "none") { + event = createDragEventObject( + "drop", + aDestElement, + aDestWindow, + aDataTransfer, + aDragEvent + ); + sendDragEvent(event, aDestElement, aDestWindow); + } + // Don't run accessibility checks for this click, since we're not actually + // clicking. It's just generated as part of the drop. + // this.AccessibilityUtils might not be set if this isn't a browser test or + // if a browser test has loaded its own copy of EventUtils for some reason. + // In the latter case, the test probably shouldn't do that. + this.AccessibilityUtils?.suppressClickHandling(true); + synthesizeMouse(aDestElement, 2, 2, { type: "mouseup" }, aDestWindow); + this.AccessibilityUtils?.suppressClickHandling(false); + + return effect; +} + +/** + * Emulate a drag and drop by emulating a dragstart and firing events dragenter, + * dragover, and drop. + * + * @param {Element} aSrcElement + * The element to use to start the drag. + * @param {Element} aDestElement + * The element to fire the dragover, dragenter events + * @param {Array} aDragData + * The data to supply for the data transfer. + * This data is in the format: + * + * [ + * [ + * {"type": value, "data": value }, + * ..., + * ], + * ... + * ] + * + * Pass null to avoid modifying dataTransfer. + * @param {String} [aDropEffect="move"] + * The drop effect to set during the dragstart event, or 'move' if omitted.. + * @param {Window} [aWindow=window] + * The window in which the drag happens. Defaults to the window in which + * EventUtils.js is loaded. + * @param {Window} [aDestWindow=aWindow] + * Used when aDestElement is in a different window than aSrcElement. + * Default is to match ``aWindow``. + * @param {Object} [aDragEvent={}] + * Defaults to empty object. Overwrites an object passed to sendDragEvent. + * @return {String} + * The drop effect that was desired. + */ +function synthesizeDrop( + aSrcElement, + aDestElement, + aDragData, + aDropEffect, + aWindow, + aDestWindow, + aDragEvent = {} +) { + if (!aWindow) { + aWindow = window; + } + if (!aDestWindow) { + aDestWindow = aWindow; + } + + var ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService( + _EU_Ci.nsIDragService + ); + + let dropAction; + switch (aDropEffect) { + case null: + case undefined: + case "move": + dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_MOVE; + break; + case "copy": + dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_COPY; + break; + case "link": + dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_LINK; + break; + default: + throw new Error(`${aDropEffect} is an invalid drop effect value`); + } + + ds.startDragSessionForTests(dropAction); + + try { + var [result, dataTransfer] = synthesizeDragOver( + aSrcElement, + aDestElement, + aDragData, + aDropEffect, + aWindow, + aDestWindow, + aDragEvent + ); + return synthesizeDropAfterDragOver( + result, + dataTransfer, + aDestElement, + aDestWindow, + aDragEvent + ); + } finally { + ds.endDragSession(true, _parseModifiers(aDragEvent)); + } +} + +function _getFlattenedTreeParentNode(aNode) { + return _EU_maybeUnwrap(_EU_maybeWrap(aNode).flattenedTreeParentNode); +} + +function _getInclusiveFlattenedTreeParentElement(aNode) { + for ( + let inclusiveAncestor = aNode; + inclusiveAncestor; + inclusiveAncestor = _getFlattenedTreeParentNode(inclusiveAncestor) + ) { + if (inclusiveAncestor.nodeType == Node.ELEMENT_NODE) { + return inclusiveAncestor; + } + } + return null; +} + +function _nodeIsFlattenedTreeDescendantOf( + aPossibleDescendant, + aPossibleAncestor +) { + do { + if (aPossibleDescendant == aPossibleAncestor) { + return true; + } + aPossibleDescendant = _getFlattenedTreeParentNode(aPossibleDescendant); + } while (aPossibleDescendant); + return false; +} + +function _computeSrcElementFromSrcSelection(aSrcSelection) { + let srcElement = aSrcSelection.focusNode; + while (_EU_maybeWrap(srcElement).isNativeAnonymous) { + srcElement = _getFlattenedTreeParentNode(srcElement); + } + if (srcElement.nodeType !== Node.ELEMENT_NODE) { + srcElement = _getInclusiveFlattenedTreeParentElement(srcElement); + } + return srcElement; +} + +/** + * Emulate a drag and drop by emulating a dragstart by mousedown and mousemove, + * and firing events dragenter, dragover, drop, and dragend. + * This does not modify dataTransfer and tries to emulate the plain drag and + * drop as much as possible, compared to synthesizeDrop. + * Note that if synthesized dragstart is canceled, this throws an exception + * because in such case, Gecko does not start drag session. + * + * @param {Object} aParams + * @param {Event} aParams.dragEvent + * The DnD events will be generated with modifiers specified with this. + * @param {Element} aParams.srcElement + * The element to start dragging. If srcSelection is + * set, this is computed for element at focus node. + * @param {Selection|nil} aParams.srcSelection + * The selection to start to drag, set null if srcElement is set. + * @param {Element|nil} aParams.destElement + * The element to drop on. Pass null to emulate a drop on an invalid target. + * @param {Number} aParams.srcX + * The initial x coordinate inside srcElement or ignored if srcSelection is set. + * @param {Number} aParams.srcY + * The initial y coordinate inside srcElement or ignored if srcSelection is set. + * @param {Number} aParams.stepX + * The x-axis step for mousemove inside srcElement + * @param {Number} aParams.stepY + * The y-axis step for mousemove inside srcElement + * @param {Number} aParams.finalX + * The final x coordinate inside srcElement + * @param {Number} aParams.finalY + * The final x coordinate inside srcElement + * @param {Any} aParams.id + * The pointer event id + * @param {Window} aParams.srcWindow + * The window for dispatching event on srcElement, defaults to the current window object. + * @param {Window} aParams.destWindow + * The window for dispatching event on destElement, defaults to the current window object. + * @param {Boolean} aParams.expectCancelDragStart + * Set to true if the test cancels "dragstart" + * @param {Boolean} aParams.expectSrcElementDisconnected + * Set to true if srcElement will be disconnected and + * "dragend" event won't be fired. + * @param {Function} aParams.logFunc + * Set function which takes one argument if you need to log rect of target. E.g., `console.log`. + */ +// eslint-disable-next-line complexity +async function synthesizePlainDragAndDrop(aParams) { + let { + dragEvent = {}, + srcElement, + srcSelection, + destElement, + srcX = 2, + srcY = 2, + stepX = 9, + stepY = 9, + finalX = srcX + stepX * 2, + finalY = srcY + stepY * 2, + id = _getDOMWindowUtils(window).DEFAULT_MOUSE_POINTER_ID, + srcWindow = window, + destWindow = window, + expectCancelDragStart = false, + expectSrcElementDisconnected = false, + logFunc, + } = aParams; + // Don't modify given dragEvent object because we modify dragEvent below and + // callers may use the object multiple times so that callers must not assume + // that it'll be modified. + if (aParams.dragEvent !== undefined) { + dragEvent = Object.assign({}, aParams.dragEvent); + } + + function rectToString(aRect) { + return `left: ${aRect.left}, top: ${aRect.top}, right: ${aRect.right}, bottom: ${aRect.bottom}`; + } + + if (logFunc) { + logFunc("synthesizePlainDragAndDrop() -- START"); + } + + if (srcSelection) { + srcElement = _computeSrcElementFromSrcSelection(srcSelection); + let srcElementRect = srcElement.getBoundingClientRect(); + if (logFunc) { + logFunc( + `srcElement.getBoundingClientRect(): ${rectToString(srcElementRect)}` + ); + } + // Use last selection client rect because nsIDragSession.sourceNode is + // initialized from focus node which is usually in last rect. + let selectionRectList = srcSelection.getRangeAt(0).getClientRects(); + let lastSelectionRect = selectionRectList[selectionRectList.length - 1]; + if (logFunc) { + logFunc( + `srcSelection.getRangeAt(0).getClientRects()[${ + selectionRectList.length - 1 + }]: ${rectToString(lastSelectionRect)}` + ); + } + // Click at center of last selection rect. + srcX = Math.floor(lastSelectionRect.left + lastSelectionRect.width / 2); + srcY = Math.floor(lastSelectionRect.top + lastSelectionRect.height / 2); + // Then, adjust srcX and srcY for making them offset relative to + // srcElementRect because they will be used when we call synthesizeMouse() + // with srcElement. + srcX = Math.floor(srcX - srcElementRect.left); + srcY = Math.floor(srcY - srcElementRect.top); + // Finally, recalculate finalX and finalY with new srcX and srcY if they + // are not specified by the caller. + if (aParams.finalX === undefined) { + finalX = srcX + stepX * 2; + } + if (aParams.finalY === undefined) { + finalY = srcY + stepY * 2; + } + } else if (logFunc) { + logFunc( + `srcElement.getBoundingClientRect(): ${rectToString( + srcElement.getBoundingClientRect() + )}` + ); + } + + const ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService( + _EU_Ci.nsIDragService + ); + + const editingHost = (() => { + if (!srcElement.matches(":read-write")) { + return null; + } + let lastEditableElement = srcElement; + for ( + let inclusiveAncestor = + _getInclusiveFlattenedTreeParentElement(srcElement); + inclusiveAncestor; + inclusiveAncestor = _getInclusiveFlattenedTreeParentElement( + _getFlattenedTreeParentNode(inclusiveAncestor) + ) + ) { + if (inclusiveAncestor.matches(":read-write")) { + lastEditableElement = inclusiveAncestor; + if (lastEditableElement == srcElement.ownerDocument.body) { + break; + } + } + } + return lastEditableElement; + })(); + try { + _getDOMWindowUtils(srcWindow).disableNonTestMouseEvents(true); + + await new Promise(r => setTimeout(r, 0)); + + let mouseDownEvent; + function onMouseDown(aEvent) { + mouseDownEvent = aEvent; + if (logFunc) { + logFunc( + `"${aEvent.type}" event is fired on ${ + aEvent.target + } (composedTarget: ${_EU_maybeUnwrap( + _EU_maybeWrap(aEvent).composedTarget + )}` + ); + } + if ( + !_nodeIsFlattenedTreeDescendantOf( + _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget), + srcElement + ) + ) { + // If srcX and srcY does not point in one of rects in srcElement, + // "mousedown" target is not in srcElement. Such case must not + // be expected by this API users so that we should throw an exception + // for making debugging easier. + throw new Error( + 'event target of "mousedown" is not srcElement nor its descendant' + ); + } + } + try { + srcWindow.addEventListener("mousedown", onMouseDown, { capture: true }); + synthesizeMouse( + srcElement, + srcX, + srcY, + { type: "mousedown", id }, + srcWindow + ); + if (logFunc) { + logFunc(`mousedown at ${srcX}, ${srcY}`); + } + if (!mouseDownEvent) { + throw new Error('"mousedown" event is not fired'); + } + } finally { + srcWindow.removeEventListener("mousedown", onMouseDown, { + capture: true, + }); + } + + let dragStartEvent; + function onDragStart(aEvent) { + dragStartEvent = aEvent; + if (logFunc) { + logFunc(`"${aEvent.type}" event is fired`); + } + if ( + !_nodeIsFlattenedTreeDescendantOf( + _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget), + srcElement + ) + ) { + // If srcX and srcY does not point in one of rects in srcElement, + // "dragstart" target is not in srcElement. Such case must not + // be expected by this API users so that we should throw an exception + // for making debugging easier. + throw new Error( + 'event target of "dragstart" is not srcElement nor its descendant' + ); + } + } + let dragEnterEvent; + function onDragEnterGenerated(aEvent) { + dragEnterEvent = aEvent; + } + srcWindow.addEventListener("dragstart", onDragStart, { capture: true }); + srcWindow.addEventListener("dragenter", onDragEnterGenerated, { + capture: true, + }); + try { + // Wait for the next event tick after each event dispatch, so that UI + // elements (e.g. menu) work like the real user input. + await new Promise(r => setTimeout(r, 0)); + + srcX += stepX; + srcY += stepY; + synthesizeMouse( + srcElement, + srcX, + srcY, + { type: "mousemove", id }, + srcWindow + ); + if (logFunc) { + logFunc(`first mousemove at ${srcX}, ${srcY}`); + } + + await new Promise(r => setTimeout(r, 0)); + + srcX += stepX; + srcY += stepY; + synthesizeMouse( + srcElement, + srcX, + srcY, + { type: "mousemove", id }, + srcWindow + ); + if (logFunc) { + logFunc(`second mousemove at ${srcX}, ${srcY}`); + } + + await new Promise(r => setTimeout(r, 0)); + + if (!dragStartEvent) { + throw new Error('"dragstart" event is not fired'); + } + } finally { + srcWindow.removeEventListener("dragstart", onDragStart, { + capture: true, + }); + srcWindow.removeEventListener("dragenter", onDragEnterGenerated, { + capture: true, + }); + } + + let session = ds.getCurrentSession(); + if (!session) { + if (expectCancelDragStart) { + synthesizeMouse( + srcElement, + finalX, + finalY, + { type: "mouseup", id }, + srcWindow + ); + return; + } + throw new Error("drag hasn't been started by the operation"); + } else if (expectCancelDragStart) { + throw new Error("drag has been started by the operation"); + } + + if (destElement) { + if ( + (srcElement != destElement && !dragEnterEvent) || + destElement != dragEnterEvent.target + ) { + if (logFunc) { + logFunc( + `destElement.getBoundingClientRect(): ${rectToString( + destElement.getBoundingClientRect() + )}` + ); + } + + function onDragEnter(aEvent) { + dragEnterEvent = aEvent; + if (logFunc) { + logFunc(`"${aEvent.type}" event is fired`); + } + if (aEvent.target != destElement) { + throw new Error('event target of "dragenter" is not destElement'); + } + } + destWindow.addEventListener("dragenter", onDragEnter, { + capture: true, + }); + try { + let event = createDragEventObject( + "dragenter", + destElement, + destWindow, + null, + dragEvent + ); + sendDragEvent(event, destElement, destWindow); + if (!dragEnterEvent && !destElement.disabled) { + throw new Error('"dragenter" event is not fired'); + } + if (dragEnterEvent && destElement.disabled) { + throw new Error( + '"dragenter" event should not be fired on disable element' + ); + } + } finally { + destWindow.removeEventListener("dragenter", onDragEnter, { + capture: true, + }); + } + } + + let dragOverEvent; + function onDragOver(aEvent) { + dragOverEvent = aEvent; + if (logFunc) { + logFunc(`"${aEvent.type}" event is fired`); + } + if (aEvent.target != destElement) { + throw new Error('event target of "dragover" is not destElement'); + } + } + destWindow.addEventListener("dragover", onDragOver, { capture: true }); + try { + // dragover and drop are only fired to a valid drop target. If the + // destElement parameter is null, this function is being used to + // simulate a drag'n'drop over an invalid drop target. + let event = createDragEventObject( + "dragover", + destElement, + destWindow, + null, + dragEvent + ); + sendDragEvent(event, destElement, destWindow); + if (!dragOverEvent && !destElement.disabled) { + throw new Error('"dragover" event is not fired'); + } + if (dragEnterEvent && destElement.disabled) { + throw new Error( + '"dragover" event should not be fired on disable element' + ); + } + } finally { + destWindow.removeEventListener("dragover", onDragOver, { + capture: true, + }); + } + + await new Promise(r => setTimeout(r, 0)); + + // If there is not accept to drop the data, "drop" event shouldn't be + // fired. + // XXX nsIDragSession.canDrop is different only on Linux. It must be + // a bug of gtk/nsDragService since it manages `mCanDrop` by itself. + // Thus, we should use nsIDragSession.dragAction instead. + if (session.dragAction != _EU_Ci.nsIDragService.DRAGDROP_ACTION_NONE) { + let dropEvent; + function onDrop(aEvent) { + dropEvent = aEvent; + if (logFunc) { + logFunc(`"${aEvent.type}" event is fired`); + } + if ( + !_nodeIsFlattenedTreeDescendantOf( + _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget), + destElement + ) + ) { + throw new Error( + 'event target of "drop" is not destElement nor its descendant' + ); + } + } + destWindow.addEventListener("drop", onDrop, { capture: true }); + try { + let event = createDragEventObject( + "drop", + destElement, + destWindow, + null, + dragEvent + ); + sendDragEvent(event, destElement, destWindow); + if (!dropEvent && session.canDrop) { + throw new Error('"drop" event is not fired'); + } + } finally { + destWindow.removeEventListener("drop", onDrop, { capture: true }); + } + return; + } + } + + // Since we don't synthesize drop event, we need to set drag end point + // explicitly for "dragEnd" event which will be fired by + // endDragSession(). + dragEvent.clientX = finalX; + dragEvent.clientY = finalY; + let event = createDragEventObject( + "dragend", + destElement || srcElement, + destElement ? srcWindow : destWindow, + null, + dragEvent + ); + session.setDragEndPointForTests(event.screenX, event.screenY); + } finally { + await new Promise(r => setTimeout(r, 0)); + + if (ds.getCurrentSession()) { + const sourceNode = ds.sourceNode; + let dragEndEvent; + function onDragEnd(aEvent) { + dragEndEvent = aEvent; + if (logFunc) { + logFunc(`"${aEvent.type}" event is fired`); + } + if ( + !_nodeIsFlattenedTreeDescendantOf( + _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget), + srcElement + ) && + _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget) != editingHost + ) { + throw new Error( + 'event target of "dragend" is not srcElement nor its descendant' + ); + } + if (expectSrcElementDisconnected) { + throw new Error( + `"dragend" event shouldn't be fired when the source node is disconnected (the source node is ${ + sourceNode?.isConnected ? "connected" : "null or disconnected" + })` + ); + } + } + srcWindow.addEventListener("dragend", onDragEnd, { capture: true }); + try { + ds.endDragSession(true, _parseModifiers(dragEvent)); + if (!expectSrcElementDisconnected && !dragEndEvent) { + // eslint-disable-next-line no-unsafe-finally + throw new Error( + `"dragend" event is not fired by nsIDragService.endDragSession()${ + ds.sourceNode && !ds.sourceNode.isConnected + ? "(sourceNode was disconnected)" + : "" + }` + ); + } + } finally { + srcWindow.removeEventListener("dragend", onDragEnd, { capture: true }); + } + } + _getDOMWindowUtils(srcWindow).disableNonTestMouseEvents(false); + if (logFunc) { + logFunc("synthesizePlainDragAndDrop() -- END"); + } + } +} + +function _checkDataTransferItems(aDataTransfer, aExpectedDragData) { + try { + // We must wrap only in plain mochitests, not chrome + let dataTransfer = _EU_maybeWrap(aDataTransfer); + if (!dataTransfer) { + return null; + } + if ( + aExpectedDragData == null || + dataTransfer.mozItemCount != aExpectedDragData.length + ) { + return dataTransfer; + } + for (let i = 0; i < dataTransfer.mozItemCount; i++) { + let dtTypes = dataTransfer.mozTypesAt(i); + if (dtTypes.length != aExpectedDragData[i].length) { + return dataTransfer; + } + for (let j = 0; j < dtTypes.length; j++) { + if (dtTypes[j] != aExpectedDragData[i][j].type) { + return dataTransfer; + } + let dtData = dataTransfer.mozGetDataAt(dtTypes[j], i); + if (aExpectedDragData[i][j].eqTest) { + if ( + !aExpectedDragData[i][j].eqTest( + dtData, + aExpectedDragData[i][j].data + ) + ) { + return dataTransfer; + } + } else if (aExpectedDragData[i][j].data != dtData) { + return dataTransfer; + } + } + } + } catch (ex) { + return ex; + } + return true; +} + +/** + * This callback type is used with ``synthesizePlainDragAndCancel()``. + * It should compare ``actualData`` and ``expectedData`` and return + * true if the two should be considered equal, false otherwise. + * + * @callback eqTest + * @param {*} actualData + * @param {*} expectedData + * @return {boolean} + */ + +/** + * synthesizePlainDragAndCancel() synthesizes drag start with + * synthesizePlainDragAndDrop(), but always cancel it with preventing default + * of "dragstart". Additionally, this checks whether the dataTransfer of + * "dragstart" event has only expected items. + * + * @param {Object} aParams + * The params which is set to the argument of ``synthesizePlainDragAndDrop()``. + * @param {Array} aExpectedDataTransferItems + * All expected dataTransfer items. + * This data is in the format: + * + * [ + * [ + * {"type": value, "data": value, eqTest: function} + * ..., + * ], + * ... + * ] + * + * This can also be null. + * You can optionally provide ``eqTest`` {@type eqTest} if the + * comparison to the expected data transfer items can't be done + * with x == y; + * @return {boolean} + * true if aExpectedDataTransferItems matches with + * DragEvent.dataTransfer of "dragstart" event. + * Otherwise, the dataTransfer object (may be null) or + * thrown exception, NOT false. Therefore, you shouldn't + * use. + */ +async function synthesizePlainDragAndCancel( + aParams, + aExpectedDataTransferItems +) { + let srcElement = aParams.srcSelection + ? _computeSrcElementFromSrcSelection(aParams.srcSelection) + : aParams.srcElement; + let result; + function onDragStart(aEvent) { + aEvent.preventDefault(); + result = _checkDataTransferItems( + aEvent.dataTransfer, + aExpectedDataTransferItems + ); + } + SpecialPowers.addSystemEventListener( + srcElement.ownerDocument, + "dragstart", + onDragStart, + { capture: true } + ); + try { + aParams.expectCancelDragStart = true; + await synthesizePlainDragAndDrop(aParams); + } finally { + SpecialPowers.removeSystemEventListener( + srcElement.ownerDocument, + "dragstart", + onDragStart, + { capture: true } + ); + } + return result; +} + +class EventCounter { + constructor(aTarget, aType, aOptions = {}) { + this.target = aTarget; + this.type = aType; + this.options = aOptions; + + this.eventCount = 0; + // Bug 1512817: + // SpecialPowers is picky and needs to be passed an explicit reference to + // the function to be called. To avoid having to bind "this", we therefore + // define the method this way, via a property. + this.handleEvent = aEvent => { + this.eventCount++; + }; + + if (aOptions.mozSystemGroup) { + SpecialPowers.addSystemEventListener( + aTarget, + aType, + this.handleEvent, + aOptions.capture + ); + } else { + aTarget.addEventListener(aType, this, aOptions); + } + } + + unregister() { + if (this.options.mozSystemGroup) { + SpecialPowers.removeSystemEventListener( + this.target, + this.type, + this.handleEvent, + this.options.capture + ); + } else { + this.target.removeEventListener(this.type, this, this.options); + } + } + + get count() { + return this.eventCount; + } +} diff --git a/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js new file mode 100644 index 0000000000..fa86116c92 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js @@ -0,0 +1,180 @@ +const { ExtensionTestCommon } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/ExtensionTestCommon.sys.mjs" +); + +var ExtensionTestUtils = { + // Shortcut to more easily access WebExtensionPolicy.backgroundServiceWorkerEnabled + // from mochitest-plain tests. + getBackgroundServiceWorkerEnabled() { + return ExtensionTestCommon.getBackgroundServiceWorkerEnabled(); + }, + + // A test helper used to check if the pref "extension.backgroundServiceWorker.forceInTestExtension" + // is set to true. + isInBackgroundServiceWorkerTests() { + return ExtensionTestCommon.isInBackgroundServiceWorkerTests(); + }, + + get testAssertions() { + return ExtensionTestCommon.testAssertions; + }, +}; + +ExtensionTestUtils.loadExtension = function (ext) { + // Cleanup functions need to be registered differently depending on + // whether we're in browser chrome or plain mochitests. + var registerCleanup; + /* global registerCleanupFunction */ + if (typeof registerCleanupFunction != "undefined") { + registerCleanup = registerCleanupFunction; + } else { + registerCleanup = SimpleTest.registerCleanupFunction.bind(SimpleTest); + } + + var testResolve; + var testDone = new Promise(resolve => { + testResolve = resolve; + }); + + var messageHandler = new Map(); + var messageAwaiter = new Map(); + + var messageQueue = new Set(); + + registerCleanup(() => { + if (messageQueue.size) { + let names = Array.from(messageQueue, ([msg]) => msg); + SimpleTest.is(JSON.stringify(names), "[]", "message queue is empty"); + } + if (messageAwaiter.size) { + let names = Array.from(messageAwaiter.keys()); + SimpleTest.is( + JSON.stringify(names), + "[]", + "no tasks awaiting on messages" + ); + } + }); + + function checkMessages() { + for (let message of messageQueue) { + let [msg, ...args] = message; + + let listener = messageAwaiter.get(msg); + if (listener) { + messageQueue.delete(message); + messageAwaiter.delete(msg); + + listener.resolve(...args); + return; + } + } + } + + function checkDuplicateListeners(msg) { + if (messageHandler.has(msg) || messageAwaiter.has(msg)) { + throw new Error("only one message handler allowed"); + } + } + + function testHandler(kind, pass, msg, ...args) { + if (kind == "test-eq") { + let [expected, actual] = args; + SimpleTest.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`); + } else if (kind == "test-log") { + SimpleTest.info(msg); + } else if (kind == "test-result") { + SimpleTest.ok(pass, msg); + } + } + + var handler = { + async testResult(kind, pass, msg, ...args) { + if (kind == "test-done") { + SimpleTest.ok(pass, msg); + await testResolve(msg); + } + testHandler(kind, pass, msg, ...args); + }, + + testMessage(msg, ...args) { + var msgHandler = messageHandler.get(msg); + if (msgHandler) { + msgHandler(...args); + } else { + messageQueue.add([msg, ...args]); + checkMessages(); + } + }, + }; + + // Mimic serialization of functions as done in `Extension.generateXPI` and + // `Extension.generateZipFile` because functions are dropped when `ext` object + // is sent to the main process via the message manager. + ext = Object.assign({}, ext); + if (ext.files) { + ext.files = Object.assign({}, ext.files); + for (let filename of Object.keys(ext.files)) { + let file = ext.files[filename]; + if (typeof file === "function" || Array.isArray(file)) { + ext.files[filename] = ExtensionTestCommon.serializeScript(file); + } + } + } + if ("background" in ext) { + ext.background = ExtensionTestCommon.serializeScript(ext.background); + } + + var extension = SpecialPowers.loadExtension(ext, handler); + + registerCleanup(async () => { + if (extension.state == "pending" || extension.state == "running") { + SimpleTest.ok(false, "Extension left running at test shutdown"); + await extension.unload(); + } else if (extension.state == "unloading") { + SimpleTest.ok(false, "Extension not fully unloaded at test shutdown"); + } + }); + + extension.awaitMessage = msg => { + return new Promise(resolve => { + checkDuplicateListeners(msg); + + messageAwaiter.set(msg, { resolve }); + checkMessages(); + }); + }; + + extension.onMessage = (msg, callback) => { + checkDuplicateListeners(msg); + messageHandler.set(msg, callback); + }; + + extension.awaitFinish = msg => { + return testDone.then(actual => { + if (msg) { + SimpleTest.is(actual, msg, "test result correct"); + } + return actual; + }); + }; + + SimpleTest.info(`Extension loaded`); + return extension; +}; + +ExtensionTestUtils.failOnSchemaWarnings = (warningsAsErrors = true) => { + let prefName = "extensions.webextensions.warnings-as-errors"; + let prefPromise = SpecialPowers.setBoolPref(prefName, warningsAsErrors); + if (!warningsAsErrors) { + let registerCleanup; + if (typeof registerCleanupFunction != "undefined") { + registerCleanup = registerCleanupFunction; + } else { + registerCleanup = SimpleTest.registerCleanupFunction.bind(SimpleTest); + } + registerCleanup(() => SpecialPowers.setBoolPref(prefName, true)); + } + // In mochitests, setBoolPref is async. + return prefPromise.then(() => {}); +}; diff --git a/testing/mochitest/tests/SimpleTest/LogController.js b/testing/mochitest/tests/SimpleTest/LogController.js new file mode 100644 index 0000000000..29580022f8 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/LogController.js @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var LogController = {}; //create the logger object + +LogController.counter = 0; //current log message number +LogController.listeners = []; +LogController.logLevel = { + FATAL: 50, + ERROR: 40, + WARNING: 30, + INFO: 20, + DEBUG: 10, +}; + +/* set minimum logging level */ +LogController.logLevelAtLeast = function (minLevel) { + if (typeof minLevel == "string") { + minLevel = LogController.logLevel[minLevel]; + } + return function (msg) { + var msgLevel = msg.level; + if (typeof msgLevel == "string") { + msgLevel = LogController.logLevel[msgLevel]; + } + return msgLevel >= minLevel; + }; +}; + +/* creates the log message with the given level and info */ +LogController.createLogMessage = function (level, info) { + var msg = {}; + msg.num = LogController.counter; + msg.level = level; + msg.info = info; + msg.timestamp = new Date(); + return msg; +}; + +/* helper method to return a sub-array */ +LogController.extend = function (args, skip) { + var ret = []; + for (var i = skip; i < args.length; i++) { + ret.push(args[i]); + } + return ret; +}; + +/* logs message with given level. Currently used locally by log() and error() */ +LogController.logWithLevel = function (level, message /*, ...*/) { + var msg = LogController.createLogMessage( + level, + LogController.extend(arguments, 1) + ); + LogController.dispatchListeners(msg); + LogController.counter += 1; +}; + +/* log with level INFO */ +LogController.log = function (message /*, ...*/) { + LogController.logWithLevel("INFO", message); +}; + +/* log with level ERROR */ +LogController.error = function (message /*, ...*/) { + LogController.logWithLevel("ERROR", message); +}; + +/* send log message to listeners */ +LogController.dispatchListeners = function (msg) { + for (var k in LogController.listeners) { + var pair = LogController.listeners[k]; + if (pair.ident != k || (pair[0] && !pair[0](msg))) { + continue; + } + pair[1](msg); + } +}; + +/* add a listener to this log given an identifier, a filter (can be null) and the listener object */ +LogController.addListener = function (ident, filter, listener) { + if (typeof filter == "string") { + filter = LogController.logLevelAtLeast(filter); + } else if (filter !== null && typeof filter !== "function") { + throw new Error("Filter must be a string, a function, or null"); + } + var entry = [filter, listener]; + entry.ident = ident; + LogController.listeners[ident] = entry; +}; + +/* remove a listener from this log */ +LogController.removeListener = function (ident) { + delete LogController.listeners[ident]; +}; diff --git a/testing/mochitest/tests/SimpleTest/MemoryStats.js b/testing/mochitest/tests/SimpleTest/MemoryStats.js new file mode 100644 index 0000000000..40548697ea --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/MemoryStats.js @@ -0,0 +1,131 @@ +/* -*- js-indent-level: 4; indent-tabs-mode: nil -*- */ +/* vim:set ts=4 sw=4 sts=4 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var MemoryStats = {}; + +/** + * Statistics that we want to retrieve and display after every test is + * done. The keys of this table are intended to be identical to the + * relevant attributes of nsIMemoryReporterManager. However, since + * nsIMemoryReporterManager doesn't necessarily support all these + * statistics in all build configurations, we also use this table to + * tell us whether statistics are supported or not. + */ +var MEM_STAT_UNKNOWN = 0; +var MEM_STAT_UNSUPPORTED = 1; +var MEM_STAT_SUPPORTED = 2; + +MemoryStats._hasMemoryStatistics = {}; +MemoryStats._hasMemoryStatistics.vsize = MEM_STAT_UNKNOWN; +MemoryStats._hasMemoryStatistics.vsizeMaxContiguous = MEM_STAT_UNKNOWN; +MemoryStats._hasMemoryStatistics.residentFast = MEM_STAT_UNKNOWN; +MemoryStats._hasMemoryStatistics.heapAllocated = MEM_STAT_UNKNOWN; + +MemoryStats._getService = function (className, interfaceName) { + var service; + try { + service = Cc[className].getService(Ci[interfaceName]); + } catch (e) { + service = SpecialPowers.Cc[className].getService( + SpecialPowers.Ci[interfaceName] + ); + } + return service; +}; + +MemoryStats._nsIFile = function (pathname) { + var f; + var contractID = "@mozilla.org/file/local;1"; + try { + f = Cc[contractID].createInstance(Ci.nsIFile); + } catch (e) { + f = SpecialPowers.Cc[contractID].createInstance(SpecialPowers.Ci.nsIFile); + } + f.initWithPath(pathname); + return f; +}; + +MemoryStats.constructPathname = function (directory, basename) { + var d = MemoryStats._nsIFile(directory); + d.append(basename); + return d.path; +}; + +MemoryStats.dump = function ( + testNumber, + testURL, + dumpOutputDirectory, + dumpAboutMemory, + dumpDMD +) { + // Use dump because treeherder uses --quiet, which drops 'info' + // from the structured logger. + var info = function (message) { + dump(message + "\n"); + }; + + var mrm = MemoryStats._getService( + "@mozilla.org/memory-reporter-manager;1", + "nsIMemoryReporterManager" + ); + var statMessage = ""; + for (var stat in MemoryStats._hasMemoryStatistics) { + var supported = MemoryStats._hasMemoryStatistics[stat]; + var firstAccess = false; + if (supported == MEM_STAT_UNKNOWN) { + firstAccess = true; + try { + void mrm[stat]; + supported = MEM_STAT_SUPPORTED; + } catch (e) { + supported = MEM_STAT_UNSUPPORTED; + } + MemoryStats._hasMemoryStatistics[stat] = supported; + } + if (supported == MEM_STAT_SUPPORTED) { + var sizeInMB = Math.round(mrm[stat] / (1024 * 1024)); + statMessage += " | " + stat + " " + sizeInMB + "MB"; + } else if (firstAccess) { + info( + "MEMORY STAT " + stat + " not supported in this build configuration." + ); + } + } + if (statMessage.length) { + info("MEMORY STAT" + statMessage); + } + + if (dumpAboutMemory) { + var basename = "about-memory-" + testNumber + ".json.gz"; + var dumpfile = MemoryStats.constructPathname(dumpOutputDirectory, basename); + info(testURL + " | MEMDUMP-START " + dumpfile); + let md = MemoryStats._getService( + "@mozilla.org/memory-info-dumper;1", + "nsIMemoryInfoDumper" + ); + md.dumpMemoryReportsToNamedFile( + dumpfile, + function () { + info("TEST-INFO | " + testURL + " | MEMDUMP-END"); + }, + null, + /* anonymize = */ false, + /* minimize memory usage = */ false + ); + } + + if (dumpDMD) { + let md = MemoryStats._getService( + "@mozilla.org/memory-info-dumper;1", + "nsIMemoryInfoDumper" + ); + md.dumpMemoryInfoToTempDir( + String(testNumber), + /* anonymize = */ false, + /* minimize memory usage = */ false + ); + } +}; diff --git a/testing/mochitest/tests/SimpleTest/MockObjects.js b/testing/mochitest/tests/SimpleTest/MockObjects.js new file mode 100644 index 0000000000..5782707c04 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/MockObjects.js @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Allows registering a mock XPCOM component, that temporarily replaces the + * original one when an object implementing a given ContractID is requested + * using createInstance. + * + * @param aContractID + * The ContractID of the component to replace, for example + * "@mozilla.org/filepicker;1". + * + * @param aReplacementCtor + * The constructor function for the JavaScript object that will be + * created every time createInstance is called. This object must + * implement QueryInterface and provide the XPCOM interfaces required by + * the specified ContractID (for example + * Components.interfaces.nsIFilePicker). + */ + +function MockObjectRegisterer(aContractID, aReplacementCtor) { + this._contractID = aContractID; + this._replacementCtor = aReplacementCtor; +} + +MockObjectRegisterer.prototype = { + /** + * Replaces the current factory with one that returns a new mock object. + * + * After register() has been called, it is mandatory to call unregister() to + * restore the original component. Usually, you should use a try-catch block + * to ensure that unregister() is called. + */ + register: function MOR_register() { + if (this._originalCID) { + throw new Error("Invalid object state when calling register()"); + } + + // Define a factory that creates a new object using the given constructor. + var isChrome = location.protocol == "chrome:"; + var providedConstructor = this._replacementCtor; + this._mockFactory = { + createInstance: function MF_createInstance(aIid) { + var inst = new providedConstructor().QueryInterface(aIid); + if (!isChrome) { + inst = SpecialPowers.wrapCallbackObject(inst); + } + return inst; + }, + }; + if (!isChrome) { + this._mockFactory = SpecialPowers.wrapCallbackObject(this._mockFactory); + } + + var retVal = SpecialPowers.swapFactoryRegistration( + null, + this._contractID, + this._mockFactory + ); + if ("error" in retVal) { + throw new Error("ERROR: " + retVal.error); + } else { + this._originalCID = retVal.originalCID; + } + }, + + /** + * Restores the original factory. + */ + unregister: function MOR_unregister() { + if (!this._originalCID) { + throw new Error("Invalid object state when calling unregister()"); + } + + // Free references to the mock factory. + SpecialPowers.swapFactoryRegistration(this._originalCID, this._contractID); + + // Allow registering a mock factory again later. + this._originalCID = null; + this._mockFactory = null; + }, + + // --- Private methods and properties --- + + /** + * The factory of the component being replaced. + */ + _originalCID: null, + + /** + * The nsIFactory that was automatically generated by this object. + */ + _mockFactory: null, +}; diff --git a/testing/mochitest/tests/SimpleTest/MozillaLogger.js b/testing/mochitest/tests/SimpleTest/MozillaLogger.js new file mode 100644 index 0000000000..13ed5bd8f5 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/MozillaLogger.js @@ -0,0 +1,102 @@ +/** + * MozillaLogger, a base class logger that just logs to stdout. + */ + +"use strict"; + +function formatLogMessage(msg) { + return msg.info.join(" ") + "\n"; +} + +function importMJS(mjs) { + if (typeof ChromeUtils === "object") { + return ChromeUtils.importESModule(mjs); + } + /* globals SpecialPowers */ + return SpecialPowers.ChromeUtils.importESModule(mjs); +} + +// When running in release builds, we get a fake Components object in +// web contexts, so we need to check that the Components object is sane, +// too, not just that it exists. +let haveComponents = + typeof Components === "object" && + typeof Components.Constructor === "function"; + +let CC = ( + haveComponents ? Components : SpecialPowers.wrap(SpecialPowers.Components) +).Constructor; + +let ConverterOutputStream = CC( + "@mozilla.org/intl/converter-output-stream;1", + "nsIConverterOutputStream", + "init" +); + +class MozillaLogger { + get logCallback() { + return msg => { + this.log(formatLogMessage(msg)); + }; + } + + log(msg) { + dump(msg); + } + + close() {} +} + +/** + * MozillaFileLogger, a log listener that can write to a local file. + * intended to be run from chrome space + */ + +/** Init the file logger with the absolute path to the file. + It will create and append if the file already exists **/ +class MozillaFileLogger extends MozillaLogger { + constructor(aPath) { + super(); + + const { FileUtils } = importMJS("resource://gre/modules/FileUtils.sys.mjs"); + + this._file = FileUtils.File(aPath); + this._foStream = FileUtils.openFileOutputStream( + this._file, + FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_APPEND + ); + + this._converter = ConverterOutputStream(this._foStream, "UTF-8"); + } + + get logCallback() { + return msg => { + if (this._converter) { + var data = formatLogMessage(msg); + this.log(data); + + if (data.includes("SimpleTest FINISH")) { + this.close(); + } + } + }; + } + + log(msg) { + if (this._converter) { + this._converter.writeString(msg); + } + } + + close() { + this._converter.flush(); + this._converter.close(); + + this._foStream = null; + this._converter = null; + this._file = null; + } +} + +this.MozillaLogger = MozillaLogger; +this.MozillaFileLogger = MozillaFileLogger; diff --git a/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js b/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js new file mode 100644 index 0000000000..80ff8bf07c --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js @@ -0,0 +1,722 @@ +/** + * This file defines all virtual keycodes for synthesizeNativeKey() of + * EventUtils.js and nsIDOMWindowUtils.sendNativeKeyEvent(). + * These values are defined in each platform's SDK or documents. + */ + +function _defineConstant(name, value) { + Object.defineProperty(this, name, { + value, + enumerable: true, + writable: false, + }); +} + +// Windows +// Windows' native key code values may include scan code value which can be +// retrieved with |((code & 0xFFFF0000 >> 16)|. If the value is 0, it will +// be computed with active keyboard layout automatically. +// FYI: Don't define scan code here for printable keys, numeric keys and +// IME keys because they depend on active keyboard layout. +// XXX: Although, ABNT C1 key depends on keyboard layout in strictly speaking. +// However, computing its scan code from the virtual keycode, +// WIN_VK_ABNT_C1, doesn't work fine (computed as 0x0073, "IntlRo"). +// Therefore, we should specify it here explicitly (it should be 0x0056, +// "IntlBackslash"). Fortunately, the key always generates 0x0056 with +// any keyboard layouts as far as I've tested. So, this must be safe to +// test new regressions. + +const WIN_VK_LBUTTON = 0x00000001; +_defineConstant("WIN_VK_LBUTTON", WIN_VK_LBUTTON); +const WIN_VK_RBUTTON = 0x00000002; +_defineConstant("WIN_VK_RBUTTON", WIN_VK_RBUTTON); +const WIN_VK_CANCEL = 0xe0460003; +_defineConstant("WIN_VK_CANCEL", WIN_VK_CANCEL); +const WIN_VK_MBUTTON = 0x00000004; +_defineConstant("WIN_VK_MBUTTON", WIN_VK_MBUTTON); +const WIN_VK_XBUTTON1 = 0x00000005; +_defineConstant("WIN_VK_XBUTTON1", WIN_VK_XBUTTON1); +const WIN_VK_XBUTTON2 = 0x00000006; +_defineConstant("WIN_VK_XBUTTON2", WIN_VK_XBUTTON2); +const WIN_VK_BACK = 0x000e0008; +_defineConstant("WIN_VK_BACK", WIN_VK_BACK); +const WIN_VK_TAB = 0x000f0009; +_defineConstant("WIN_VK_TAB", WIN_VK_TAB); +const WIN_VK_CLEAR = 0x004c000c; +_defineConstant("WIN_VK_CLEAR", WIN_VK_CLEAR); +const WIN_VK_RETURN = 0x001c000d; +_defineConstant("WIN_VK_RETURN", WIN_VK_RETURN); +const WIN_VK_SHIFT = 0x002a0010; +_defineConstant("WIN_VK_SHIFT", WIN_VK_SHIFT); +const WIN_VK_CONTROL = 0x001d0011; +_defineConstant("WIN_VK_CONTROL", WIN_VK_CONTROL); +const WIN_VK_MENU = 0x00380012; +_defineConstant("WIN_VK_MENU", WIN_VK_MENU); +const WIN_VK_PAUSE = 0x00450013; +_defineConstant("WIN_VK_PAUSE", WIN_VK_PAUSE); +const WIN_VK_CAPITAL = 0x003a0014; +_defineConstant("WIN_VK_CAPITAL", WIN_VK_CAPITAL); +const WIN_VK_KANA = 0x00000015; +_defineConstant("WIN_VK_KANA", WIN_VK_KANA); +const WIN_VK_HANGUEL = 0x00000015; +_defineConstant("WIN_VK_HANGUEL", WIN_VK_HANGUEL); +const WIN_VK_HANGUL = 0x00000015; +_defineConstant("WIN_VK_HANGUL", WIN_VK_HANGUL); +const WIN_VK_JUNJA = 0x00000017; +_defineConstant("WIN_VK_JUNJA", WIN_VK_JUNJA); +const WIN_VK_FINAL = 0x00000018; +_defineConstant("WIN_VK_FINAL", WIN_VK_FINAL); +const WIN_VK_HANJA = 0x00000019; +_defineConstant("WIN_VK_HANJA", WIN_VK_HANJA); +const WIN_VK_KANJI = 0x00000019; +_defineConstant("WIN_VK_KANJI", WIN_VK_KANJI); +const WIN_VK_ESCAPE = 0x0001001b; +_defineConstant("WIN_VK_ESCAPE", WIN_VK_ESCAPE); +const WIN_VK_CONVERT = 0x0000001c; +_defineConstant("WIN_VK_CONVERT", WIN_VK_CONVERT); +const WIN_VK_NONCONVERT = 0x0000001d; +_defineConstant("WIN_VK_NONCONVERT", WIN_VK_NONCONVERT); +const WIN_VK_ACCEPT = 0x0000001e; +_defineConstant("WIN_VK_ACCEPT", WIN_VK_ACCEPT); +const WIN_VK_MODECHANGE = 0x0000001f; +_defineConstant("WIN_VK_MODECHANGE", WIN_VK_MODECHANGE); +const WIN_VK_SPACE = 0x00390020; +_defineConstant("WIN_VK_SPACE", WIN_VK_SPACE); +const WIN_VK_PRIOR = 0xe0490021; +_defineConstant("WIN_VK_PRIOR", WIN_VK_PRIOR); +const WIN_VK_NEXT = 0xe0510022; +_defineConstant("WIN_VK_NEXT", WIN_VK_NEXT); +const WIN_VK_END = 0xe04f0023; +_defineConstant("WIN_VK_END", WIN_VK_END); +const WIN_VK_HOME = 0xe0470024; +_defineConstant("WIN_VK_HOME", WIN_VK_HOME); +const WIN_VK_LEFT = 0xe04b0025; +_defineConstant("WIN_VK_LEFT", WIN_VK_LEFT); +const WIN_VK_UP = 0xe0480026; +_defineConstant("WIN_VK_UP", WIN_VK_UP); +const WIN_VK_RIGHT = 0xe04d0027; +_defineConstant("WIN_VK_RIGHT", WIN_VK_RIGHT); +const WIN_VK_DOWN = 0xe0500028; +_defineConstant("WIN_VK_DOWN", WIN_VK_DOWN); +const WIN_VK_SELECT = 0x00000029; +_defineConstant("WIN_VK_SELECT", WIN_VK_SELECT); +const WIN_VK_PRINT = 0x0000002a; +_defineConstant("WIN_VK_PRINT", WIN_VK_PRINT); +const WIN_VK_EXECUTE = 0x0000002b; +_defineConstant("WIN_VK_EXECUTE", WIN_VK_EXECUTE); +const WIN_VK_SNAPSHOT = 0xe037002c; +_defineConstant("WIN_VK_SNAPSHOT", WIN_VK_SNAPSHOT); +const WIN_VK_INSERT = 0xe052002d; +_defineConstant("WIN_VK_INSERT", WIN_VK_INSERT); +const WIN_VK_DELETE = 0xe053002e; +_defineConstant("WIN_VK_DELETE", WIN_VK_DELETE); +const WIN_VK_HELP = 0x0000002f; +_defineConstant("WIN_VK_HELP", WIN_VK_HELP); +const WIN_VK_0 = 0x00000030; +_defineConstant("WIN_VK_0", WIN_VK_0); +const WIN_VK_1 = 0x00000031; +_defineConstant("WIN_VK_1", WIN_VK_1); +const WIN_VK_2 = 0x00000032; +_defineConstant("WIN_VK_2", WIN_VK_2); +const WIN_VK_3 = 0x00000033; +_defineConstant("WIN_VK_3", WIN_VK_3); +const WIN_VK_4 = 0x00000034; +_defineConstant("WIN_VK_4", WIN_VK_4); +const WIN_VK_5 = 0x00000035; +_defineConstant("WIN_VK_5", WIN_VK_5); +const WIN_VK_6 = 0x00000036; +_defineConstant("WIN_VK_6", WIN_VK_6); +const WIN_VK_7 = 0x00000037; +_defineConstant("WIN_VK_7", WIN_VK_7); +const WIN_VK_8 = 0x00000038; +_defineConstant("WIN_VK_8", WIN_VK_8); +const WIN_VK_9 = 0x00000039; +_defineConstant("WIN_VK_9", WIN_VK_9); +const WIN_VK_A = 0x00000041; +_defineConstant("WIN_VK_A", WIN_VK_A); +const WIN_VK_B = 0x00000042; +_defineConstant("WIN_VK_B", WIN_VK_B); +const WIN_VK_C = 0x00000043; +_defineConstant("WIN_VK_C", WIN_VK_C); +const WIN_VK_D = 0x00000044; +_defineConstant("WIN_VK_D", WIN_VK_D); +const WIN_VK_E = 0x00000045; +_defineConstant("WIN_VK_E", WIN_VK_E); +const WIN_VK_F = 0x00000046; +_defineConstant("WIN_VK_F", WIN_VK_F); +const WIN_VK_G = 0x00000047; +_defineConstant("WIN_VK_G", WIN_VK_G); +const WIN_VK_H = 0x00000048; +_defineConstant("WIN_VK_H", WIN_VK_H); +const WIN_VK_I = 0x00000049; +_defineConstant("WIN_VK_I", WIN_VK_I); +const WIN_VK_J = 0x0000004a; +_defineConstant("WIN_VK_J", WIN_VK_J); +const WIN_VK_K = 0x0000004b; +_defineConstant("WIN_VK_K", WIN_VK_K); +const WIN_VK_L = 0x0000004c; +_defineConstant("WIN_VK_L", WIN_VK_L); +const WIN_VK_M = 0x0000004d; +_defineConstant("WIN_VK_M", WIN_VK_M); +const WIN_VK_N = 0x0000004e; +_defineConstant("WIN_VK_N", WIN_VK_N); +const WIN_VK_O = 0x0000004f; +_defineConstant("WIN_VK_O", WIN_VK_O); +const WIN_VK_P = 0x00000050; +_defineConstant("WIN_VK_P", WIN_VK_P); +const WIN_VK_Q = 0x00000051; +_defineConstant("WIN_VK_Q", WIN_VK_Q); +const WIN_VK_R = 0x00000052; +_defineConstant("WIN_VK_R", WIN_VK_R); +const WIN_VK_S = 0x00000053; +_defineConstant("WIN_VK_S", WIN_VK_S); +const WIN_VK_T = 0x00000054; +_defineConstant("WIN_VK_T", WIN_VK_T); +const WIN_VK_U = 0x00000055; +_defineConstant("WIN_VK_U", WIN_VK_U); +const WIN_VK_V = 0x00000056; +_defineConstant("WIN_VK_V", WIN_VK_V); +const WIN_VK_W = 0x00000057; +_defineConstant("WIN_VK_W", WIN_VK_W); +const WIN_VK_X = 0x00000058; +_defineConstant("WIN_VK_X", WIN_VK_X); +const WIN_VK_Y = 0x00000059; +_defineConstant("WIN_VK_Y", WIN_VK_Y); +const WIN_VK_Z = 0x0000005a; +_defineConstant("WIN_VK_Z", WIN_VK_Z); +const WIN_VK_LWIN = 0xe05b005b; +_defineConstant("WIN_VK_LWIN", WIN_VK_LWIN); +const WIN_VK_RWIN = 0xe05c005c; +_defineConstant("WIN_VK_RWIN", WIN_VK_RWIN); +const WIN_VK_APPS = 0xe05d005d; +_defineConstant("WIN_VK_APPS", WIN_VK_APPS); +const WIN_VK_SLEEP = 0x0000005f; +_defineConstant("WIN_VK_SLEEP", WIN_VK_SLEEP); +const WIN_VK_NUMPAD0 = 0x00520060; +_defineConstant("WIN_VK_NUMPAD0", WIN_VK_NUMPAD0); +const WIN_VK_NUMPAD1 = 0x004f0061; +_defineConstant("WIN_VK_NUMPAD1", WIN_VK_NUMPAD1); +const WIN_VK_NUMPAD2 = 0x00500062; +_defineConstant("WIN_VK_NUMPAD2", WIN_VK_NUMPAD2); +const WIN_VK_NUMPAD3 = 0x00510063; +_defineConstant("WIN_VK_NUMPAD3", WIN_VK_NUMPAD3); +const WIN_VK_NUMPAD4 = 0x004b0064; +_defineConstant("WIN_VK_NUMPAD4", WIN_VK_NUMPAD4); +const WIN_VK_NUMPAD5 = 0x004c0065; +_defineConstant("WIN_VK_NUMPAD5", WIN_VK_NUMPAD5); +const WIN_VK_NUMPAD6 = 0x004d0066; +_defineConstant("WIN_VK_NUMPAD6", WIN_VK_NUMPAD6); +const WIN_VK_NUMPAD7 = 0x00470067; +_defineConstant("WIN_VK_NUMPAD7", WIN_VK_NUMPAD7); +const WIN_VK_NUMPAD8 = 0x00480068; +_defineConstant("WIN_VK_NUMPAD8", WIN_VK_NUMPAD8); +const WIN_VK_NUMPAD9 = 0x00490069; +_defineConstant("WIN_VK_NUMPAD9", WIN_VK_NUMPAD9); +const WIN_VK_MULTIPLY = 0x0037006a; +_defineConstant("WIN_VK_MULTIPLY", WIN_VK_MULTIPLY); +const WIN_VK_ADD = 0x004e006b; +_defineConstant("WIN_VK_ADD", WIN_VK_ADD); +const WIN_VK_SEPARATOR = 0x0000006c; +_defineConstant("WIN_VK_SEPARATOR", WIN_VK_SEPARATOR); +const WIN_VK_OEM_NEC_SEPARATE = 0x0000006c; +_defineConstant("WIN_VK_OEM_NEC_SEPARATE", WIN_VK_OEM_NEC_SEPARATE); +const WIN_VK_SUBTRACT = 0x004a006d; +_defineConstant("WIN_VK_SUBTRACT", WIN_VK_SUBTRACT); +const WIN_VK_DECIMAL = 0x0053006e; +_defineConstant("WIN_VK_DECIMAL", WIN_VK_DECIMAL); +const WIN_VK_DIVIDE = 0xe035006f; +_defineConstant("WIN_VK_DIVIDE", WIN_VK_DIVIDE); +const WIN_VK_F1 = 0x003b0070; +_defineConstant("WIN_VK_F1", WIN_VK_F1); +const WIN_VK_F2 = 0x003c0071; +_defineConstant("WIN_VK_F2", WIN_VK_F2); +const WIN_VK_F3 = 0x003d0072; +_defineConstant("WIN_VK_F3", WIN_VK_F3); +const WIN_VK_F4 = 0x003e0073; +_defineConstant("WIN_VK_F4", WIN_VK_F4); +const WIN_VK_F5 = 0x003f0074; +_defineConstant("WIN_VK_F5", WIN_VK_F5); +const WIN_VK_F6 = 0x00400075; +_defineConstant("WIN_VK_F6", WIN_VK_F6); +const WIN_VK_F7 = 0x00410076; +_defineConstant("WIN_VK_F7", WIN_VK_F7); +const WIN_VK_F8 = 0x00420077; +_defineConstant("WIN_VK_F8", WIN_VK_F8); +const WIN_VK_F9 = 0x00430078; +_defineConstant("WIN_VK_F9", WIN_VK_F9); +const WIN_VK_F10 = 0x00440079; +_defineConstant("WIN_VK_F10", WIN_VK_F10); +const WIN_VK_F11 = 0x0057007a; +_defineConstant("WIN_VK_F11", WIN_VK_F11); +const WIN_VK_F12 = 0x0058007b; +_defineConstant("WIN_VK_F12", WIN_VK_F12); +const WIN_VK_F13 = 0x0064007c; +_defineConstant("WIN_VK_F13", WIN_VK_F13); +const WIN_VK_F14 = 0x0065007d; +_defineConstant("WIN_VK_F14", WIN_VK_F14); +const WIN_VK_F15 = 0x0066007e; +_defineConstant("WIN_VK_F15", WIN_VK_F15); +const WIN_VK_F16 = 0x0067007f; +_defineConstant("WIN_VK_F16", WIN_VK_F16); +const WIN_VK_F17 = 0x00680080; +_defineConstant("WIN_VK_F17", WIN_VK_F17); +const WIN_VK_F18 = 0x00690081; +_defineConstant("WIN_VK_F18", WIN_VK_F18); +const WIN_VK_F19 = 0x006a0082; +_defineConstant("WIN_VK_F19", WIN_VK_F19); +const WIN_VK_F20 = 0x006b0083; +_defineConstant("WIN_VK_F20", WIN_VK_F20); +const WIN_VK_F21 = 0x006c0084; +_defineConstant("WIN_VK_F21", WIN_VK_F21); +const WIN_VK_F22 = 0x006d0085; +_defineConstant("WIN_VK_F22", WIN_VK_F22); +const WIN_VK_F23 = 0x006e0086; +_defineConstant("WIN_VK_F23", WIN_VK_F23); +const WIN_VK_F24 = 0x00760087; +_defineConstant("WIN_VK_F24", WIN_VK_F24); +const WIN_VK_NUMLOCK = 0xe0450090; +_defineConstant("WIN_VK_NUMLOCK", WIN_VK_NUMLOCK); +const WIN_VK_SCROLL = 0x00460091; +_defineConstant("WIN_VK_SCROLL", WIN_VK_SCROLL); +const WIN_VK_OEM_FJ_JISHO = 0x00000092; +_defineConstant("WIN_VK_OEM_FJ_JISHO", WIN_VK_OEM_FJ_JISHO); +const WIN_VK_OEM_NEC_EQUAL = 0x00000092; +_defineConstant("WIN_VK_OEM_NEC_EQUAL", WIN_VK_OEM_NEC_EQUAL); +const WIN_VK_OEM_FJ_MASSHOU = 0x00000093; +_defineConstant("WIN_VK_OEM_FJ_MASSHOU", WIN_VK_OEM_FJ_MASSHOU); +const WIN_VK_OEM_FJ_TOUROKU = 0x00000094; +_defineConstant("WIN_VK_OEM_FJ_TOUROKU", WIN_VK_OEM_FJ_TOUROKU); +const WIN_VK_OEM_FJ_LOYA = 0x00000095; +_defineConstant("WIN_VK_OEM_FJ_LOYA", WIN_VK_OEM_FJ_LOYA); +const WIN_VK_OEM_FJ_ROYA = 0x00000096; +_defineConstant("WIN_VK_OEM_FJ_ROYA", WIN_VK_OEM_FJ_ROYA); +const WIN_VK_LSHIFT = 0x002a00a0; +_defineConstant("WIN_VK_LSHIFT", WIN_VK_LSHIFT); +const WIN_VK_RSHIFT = 0x003600a1; +_defineConstant("WIN_VK_RSHIFT", WIN_VK_RSHIFT); +const WIN_VK_LCONTROL = 0x001d00a2; +_defineConstant("WIN_VK_LCONTROL", WIN_VK_LCONTROL); +const WIN_VK_RCONTROL = 0xe01d00a3; +_defineConstant("WIN_VK_RCONTROL", WIN_VK_RCONTROL); +const WIN_VK_LMENU = 0x003800a4; +_defineConstant("WIN_VK_LMENU", WIN_VK_LMENU); +const WIN_VK_RMENU = 0xe03800a5; +_defineConstant("WIN_VK_RMENU", WIN_VK_RMENU); +const WIN_VK_BROWSER_BACK = 0xe06a00a6; +_defineConstant("WIN_VK_BROWSER_BACK", WIN_VK_BROWSER_BACK); +const WIN_VK_BROWSER_FORWARD = 0xe06900a7; +_defineConstant("WIN_VK_BROWSER_FORWARD", WIN_VK_BROWSER_FORWARD); +const WIN_VK_BROWSER_REFRESH = 0xe06700a8; +_defineConstant("WIN_VK_BROWSER_REFRESH", WIN_VK_BROWSER_REFRESH); +const WIN_VK_BROWSER_STOP = 0xe06800a9; +_defineConstant("WIN_VK_BROWSER_STOP", WIN_VK_BROWSER_STOP); +const WIN_VK_BROWSER_SEARCH = 0x000000aa; +_defineConstant("WIN_VK_BROWSER_SEARCH", WIN_VK_BROWSER_SEARCH); +const WIN_VK_BROWSER_FAVORITES = 0xe06600ab; +_defineConstant("WIN_VK_BROWSER_FAVORITES", WIN_VK_BROWSER_FAVORITES); +const WIN_VK_BROWSER_HOME = 0xe03200ac; +_defineConstant("WIN_VK_BROWSER_HOME", WIN_VK_BROWSER_HOME); +const WIN_VK_VOLUME_MUTE = 0xe02000ad; +_defineConstant("WIN_VK_VOLUME_MUTE", WIN_VK_VOLUME_MUTE); +const WIN_VK_VOLUME_DOWN = 0xe02e00ae; +_defineConstant("WIN_VK_VOLUME_DOWN", WIN_VK_VOLUME_DOWN); +const WIN_VK_VOLUME_UP = 0xe03000af; +_defineConstant("WIN_VK_VOLUME_UP", WIN_VK_VOLUME_UP); +const WIN_VK_MEDIA_NEXT_TRACK = 0xe01900b0; +_defineConstant("WIN_VK_MEDIA_NEXT_TRACK", WIN_VK_MEDIA_NEXT_TRACK); +const WIN_VK_OEM_FJ_000 = 0x000000b0; +_defineConstant("WIN_VK_OEM_FJ_000", WIN_VK_OEM_FJ_000); +const WIN_VK_MEDIA_PREV_TRACK = 0xe01000b1; +_defineConstant("WIN_VK_MEDIA_PREV_TRACK", WIN_VK_MEDIA_PREV_TRACK); +const WIN_VK_OEM_FJ_EUQAL = 0x000000b1; +_defineConstant("WIN_VK_OEM_FJ_EUQAL", WIN_VK_OEM_FJ_EUQAL); +const WIN_VK_MEDIA_STOP = 0xe02400b2; +_defineConstant("WIN_VK_MEDIA_STOP", WIN_VK_MEDIA_STOP); +const WIN_VK_MEDIA_PLAY_PAUSE = 0xe02200b3; +_defineConstant("WIN_VK_MEDIA_PLAY_PAUSE", WIN_VK_MEDIA_PLAY_PAUSE); +const WIN_VK_OEM_FJ_00 = 0x000000b3; +_defineConstant("WIN_VK_OEM_FJ_00", WIN_VK_OEM_FJ_00); +const WIN_VK_LAUNCH_MAIL = 0xe06c00b4; +_defineConstant("WIN_VK_LAUNCH_MAIL", WIN_VK_LAUNCH_MAIL); +const WIN_VK_LAUNCH_MEDIA_SELECT = 0xe06d00b5; +_defineConstant("WIN_VK_LAUNCH_MEDIA_SELECT", WIN_VK_LAUNCH_MEDIA_SELECT); +const WIN_VK_LAUNCH_APP1 = 0xe06b00b6; +_defineConstant("WIN_VK_LAUNCH_APP1", WIN_VK_LAUNCH_APP1); +const WIN_VK_LAUNCH_APP2 = 0xe02100b7; +_defineConstant("WIN_VK_LAUNCH_APP2", WIN_VK_LAUNCH_APP2); +const WIN_VK_OEM_1 = 0x000000ba; +_defineConstant("WIN_VK_OEM_1", WIN_VK_OEM_1); +const WIN_VK_OEM_PLUS = 0x000000bb; +_defineConstant("WIN_VK_OEM_PLUS", WIN_VK_OEM_PLUS); +const WIN_VK_OEM_COMMA = 0x000000bc; +_defineConstant("WIN_VK_OEM_COMMA", WIN_VK_OEM_COMMA); +const WIN_VK_OEM_MINUS = 0x000000bd; +_defineConstant("WIN_VK_OEM_MINUS", WIN_VK_OEM_MINUS); +const WIN_VK_OEM_PERIOD = 0x000000be; +_defineConstant("WIN_VK_OEM_PERIOD", WIN_VK_OEM_PERIOD); +const WIN_VK_OEM_2 = 0x000000bf; +_defineConstant("WIN_VK_OEM_2", WIN_VK_OEM_2); +const WIN_VK_OEM_3 = 0x000000c0; +_defineConstant("WIN_VK_OEM_3", WIN_VK_OEM_3); +const WIN_VK_ABNT_C1 = 0x005600c1; +_defineConstant("WIN_VK_ABNT_C1", WIN_VK_ABNT_C1); +const WIN_VK_ABNT_C2 = 0x000000c2; +_defineConstant("WIN_VK_ABNT_C2", WIN_VK_ABNT_C2); +const WIN_VK_OEM_4 = 0x000000db; +_defineConstant("WIN_VK_OEM_4", WIN_VK_OEM_4); +const WIN_VK_OEM_5 = 0x000000dc; +_defineConstant("WIN_VK_OEM_5", WIN_VK_OEM_5); +const WIN_VK_OEM_6 = 0x000000dd; +_defineConstant("WIN_VK_OEM_6", WIN_VK_OEM_6); +const WIN_VK_OEM_7 = 0x000000de; +_defineConstant("WIN_VK_OEM_7", WIN_VK_OEM_7); +const WIN_VK_OEM_8 = 0x000000df; +_defineConstant("WIN_VK_OEM_8", WIN_VK_OEM_8); +const WIN_VK_OEM_NEC_DP1 = 0x000000e0; +_defineConstant("WIN_VK_OEM_NEC_DP1", WIN_VK_OEM_NEC_DP1); +const WIN_VK_OEM_AX = 0x000000e1; +_defineConstant("WIN_VK_OEM_AX", WIN_VK_OEM_AX); +const WIN_VK_OEM_NEC_DP2 = 0x000000e1; +_defineConstant("WIN_VK_OEM_NEC_DP2", WIN_VK_OEM_NEC_DP2); +const WIN_VK_OEM_102 = 0x000000e2; +_defineConstant("WIN_VK_OEM_102", WIN_VK_OEM_102); +const WIN_VK_OEM_NEC_DP3 = 0x000000e2; +_defineConstant("WIN_VK_OEM_NEC_DP3", WIN_VK_OEM_NEC_DP3); +const WIN_VK_ICO_HELP = 0x000000e3; +_defineConstant("WIN_VK_ICO_HELP", WIN_VK_ICO_HELP); +const WIN_VK_OEM_NEC_DP4 = 0x000000e3; +_defineConstant("WIN_VK_OEM_NEC_DP4", WIN_VK_OEM_NEC_DP4); +const WIN_VK_ICO_00 = 0x000000e4; +_defineConstant("WIN_VK_ICO_00", WIN_VK_ICO_00); +const WIN_VK_PROCESSKEY = 0x000000e5; +_defineConstant("WIN_VK_PROCESSKEY", WIN_VK_PROCESSKEY); +const WIN_VK_ICO_CLEAR = 0x000000e6; +_defineConstant("WIN_VK_ICO_CLEAR", WIN_VK_ICO_CLEAR); +const WIN_VK_PACKET = 0x000000e7; +_defineConstant("WIN_VK_PACKET", WIN_VK_PACKET); +const WIN_VK_ERICSSON_BASE = 0x000000e8; +_defineConstant("WIN_VK_ERICSSON_BASE", WIN_VK_ERICSSON_BASE); +const WIN_VK_OEM_RESET = 0x000000e9; +_defineConstant("WIN_VK_OEM_RESET", WIN_VK_OEM_RESET); +const WIN_VK_OEM_JUMP = 0x000000ea; +_defineConstant("WIN_VK_OEM_JUMP", WIN_VK_OEM_JUMP); +const WIN_VK_OEM_PA1 = 0x000000eb; +_defineConstant("WIN_VK_OEM_PA1", WIN_VK_OEM_PA1); +const WIN_VK_OEM_PA2 = 0x000000ec; +_defineConstant("WIN_VK_OEM_PA2", WIN_VK_OEM_PA2); +const WIN_VK_OEM_PA3 = 0x000000ed; +_defineConstant("WIN_VK_OEM_PA3", WIN_VK_OEM_PA3); +const WIN_VK_OEM_WSCTRL = 0x000000ee; +_defineConstant("WIN_VK_OEM_WSCTRL", WIN_VK_OEM_WSCTRL); +const WIN_VK_OEM_CUSEL = 0x000000ef; +_defineConstant("WIN_VK_OEM_CUSEL", WIN_VK_OEM_CUSEL); +const WIN_VK_OEM_ATTN = 0x000000f0; +_defineConstant("WIN_VK_OEM_ATTN", WIN_VK_OEM_ATTN); +const WIN_VK_OEM_FINISH = 0x000000f1; +_defineConstant("WIN_VK_OEM_FINISH", WIN_VK_OEM_FINISH); +const WIN_VK_OEM_COPY = 0x000000f2; +_defineConstant("WIN_VK_OEM_COPY", WIN_VK_OEM_COPY); +const WIN_VK_OEM_AUTO = 0x000000f3; +_defineConstant("WIN_VK_OEM_AUTO", WIN_VK_OEM_AUTO); +const WIN_VK_OEM_ENLW = 0x000000f4; +_defineConstant("WIN_VK_OEM_ENLW", WIN_VK_OEM_ENLW); +const WIN_VK_OEM_BACKTAB = 0x000000f5; +_defineConstant("WIN_VK_OEM_BACKTAB", WIN_VK_OEM_BACKTAB); +const WIN_VK_ATTN = 0x000000f6; +_defineConstant("WIN_VK_ATTN", WIN_VK_ATTN); +const WIN_VK_CRSEL = 0x000000f7; +_defineConstant("WIN_VK_CRSEL", WIN_VK_CRSEL); +const WIN_VK_EXSEL = 0x000000f8; +_defineConstant("WIN_VK_EXSEL", WIN_VK_EXSEL); +const WIN_VK_EREOF = 0x000000f9; +_defineConstant("WIN_VK_EREOF", WIN_VK_EREOF); +const WIN_VK_PLAY = 0x000000fa; +_defineConstant("WIN_VK_PLAY", WIN_VK_PLAY); +const WIN_VK_ZOOM = 0x000000fb; +_defineConstant("WIN_VK_ZOOM", WIN_VK_ZOOM); +const WIN_VK_NONAME = 0x000000fc; +_defineConstant("WIN_VK_NONAME", WIN_VK_NONAME); +const WIN_VK_PA1 = 0x000000fd; +_defineConstant("WIN_VK_PA1", WIN_VK_PA1); +const WIN_VK_OEM_CLEAR = 0x000000fe; +_defineConstant("WIN_VK_OEM_CLEAR", WIN_VK_OEM_CLEAR); + +const WIN_VK_NUMPAD_RETURN = 0xe01c000d; +_defineConstant("WIN_VK_NUMPAD_RETURN", WIN_VK_NUMPAD_RETURN); +const WIN_VK_NUMPAD_PRIOR = 0x00490021; +_defineConstant("WIN_VK_NUMPAD_PRIOR", WIN_VK_NUMPAD_PRIOR); +const WIN_VK_NUMPAD_NEXT = 0x00510022; +_defineConstant("WIN_VK_NUMPAD_NEXT", WIN_VK_NUMPAD_NEXT); +const WIN_VK_NUMPAD_END = 0x004f0023; +_defineConstant("WIN_VK_NUMPAD_END", WIN_VK_NUMPAD_END); +const WIN_VK_NUMPAD_HOME = 0x00470024; +_defineConstant("WIN_VK_NUMPAD_HOME", WIN_VK_NUMPAD_HOME); +const WIN_VK_NUMPAD_LEFT = 0x004b0025; +_defineConstant("WIN_VK_NUMPAD_LEFT", WIN_VK_NUMPAD_LEFT); +const WIN_VK_NUMPAD_UP = 0x00480026; +_defineConstant("WIN_VK_NUMPAD_UP", WIN_VK_NUMPAD_UP); +const WIN_VK_NUMPAD_RIGHT = 0x004d0027; +_defineConstant("WIN_VK_NUMPAD_RIGHT", WIN_VK_NUMPAD_RIGHT); +const WIN_VK_NUMPAD_DOWN = 0x00500028; +_defineConstant("WIN_VK_NUMPAD_DOWN", WIN_VK_NUMPAD_DOWN); +const WIN_VK_NUMPAD_INSERT = 0x0052002d; +_defineConstant("WIN_VK_NUMPAD_INSERT", WIN_VK_NUMPAD_INSERT); +const WIN_VK_NUMPAD_DELETE = 0x0053002e; +_defineConstant("WIN_VK_NUMPAD_DELETE", WIN_VK_NUMPAD_DELETE); + +// Mac + +const MAC_VK_ANSI_A = 0x00; +_defineConstant("MAC_VK_ANSI_A", MAC_VK_ANSI_A); +const MAC_VK_ANSI_S = 0x01; +_defineConstant("MAC_VK_ANSI_S", MAC_VK_ANSI_S); +const MAC_VK_ANSI_D = 0x02; +_defineConstant("MAC_VK_ANSI_D", MAC_VK_ANSI_D); +const MAC_VK_ANSI_F = 0x03; +_defineConstant("MAC_VK_ANSI_F", MAC_VK_ANSI_F); +const MAC_VK_ANSI_H = 0x04; +_defineConstant("MAC_VK_ANSI_H", MAC_VK_ANSI_H); +const MAC_VK_ANSI_G = 0x05; +_defineConstant("MAC_VK_ANSI_G", MAC_VK_ANSI_G); +const MAC_VK_ANSI_Z = 0x06; +_defineConstant("MAC_VK_ANSI_Z", MAC_VK_ANSI_Z); +const MAC_VK_ANSI_X = 0x07; +_defineConstant("MAC_VK_ANSI_X", MAC_VK_ANSI_X); +const MAC_VK_ANSI_C = 0x08; +_defineConstant("MAC_VK_ANSI_C", MAC_VK_ANSI_C); +const MAC_VK_ANSI_V = 0x09; +_defineConstant("MAC_VK_ANSI_V", MAC_VK_ANSI_V); +const MAC_VK_ISO_Section = 0x0a; +_defineConstant("MAC_VK_ISO_Section", MAC_VK_ISO_Section); +const MAC_VK_ANSI_B = 0x0b; +_defineConstant("MAC_VK_ANSI_B", MAC_VK_ANSI_B); +const MAC_VK_ANSI_Q = 0x0c; +_defineConstant("MAC_VK_ANSI_Q", MAC_VK_ANSI_Q); +const MAC_VK_ANSI_W = 0x0d; +_defineConstant("MAC_VK_ANSI_W", MAC_VK_ANSI_W); +const MAC_VK_ANSI_E = 0x0e; +_defineConstant("MAC_VK_ANSI_E", MAC_VK_ANSI_E); +const MAC_VK_ANSI_R = 0x0f; +_defineConstant("MAC_VK_ANSI_R", MAC_VK_ANSI_R); +const MAC_VK_ANSI_Y = 0x10; +_defineConstant("MAC_VK_ANSI_Y", MAC_VK_ANSI_Y); +const MAC_VK_ANSI_T = 0x11; +_defineConstant("MAC_VK_ANSI_T", MAC_VK_ANSI_T); +const MAC_VK_ANSI_1 = 0x12; +_defineConstant("MAC_VK_ANSI_1", MAC_VK_ANSI_1); +const MAC_VK_ANSI_2 = 0x13; +_defineConstant("MAC_VK_ANSI_2", MAC_VK_ANSI_2); +const MAC_VK_ANSI_3 = 0x14; +_defineConstant("MAC_VK_ANSI_3", MAC_VK_ANSI_3); +const MAC_VK_ANSI_4 = 0x15; +_defineConstant("MAC_VK_ANSI_4", MAC_VK_ANSI_4); +const MAC_VK_ANSI_6 = 0x16; +_defineConstant("MAC_VK_ANSI_6", MAC_VK_ANSI_6); +const MAC_VK_ANSI_5 = 0x17; +_defineConstant("MAC_VK_ANSI_5", MAC_VK_ANSI_5); +const MAC_VK_ANSI_Equal = 0x18; +_defineConstant("MAC_VK_ANSI_Equal", MAC_VK_ANSI_Equal); +const MAC_VK_ANSI_9 = 0x19; +_defineConstant("MAC_VK_ANSI_9", MAC_VK_ANSI_9); +const MAC_VK_ANSI_7 = 0x1a; +_defineConstant("MAC_VK_ANSI_7", MAC_VK_ANSI_7); +const MAC_VK_ANSI_Minus = 0x1b; +_defineConstant("MAC_VK_ANSI_Minus", MAC_VK_ANSI_Minus); +const MAC_VK_ANSI_8 = 0x1c; +_defineConstant("MAC_VK_ANSI_8", MAC_VK_ANSI_8); +const MAC_VK_ANSI_0 = 0x1d; +_defineConstant("MAC_VK_ANSI_0", MAC_VK_ANSI_0); +const MAC_VK_ANSI_RightBracket = 0x1e; +_defineConstant("MAC_VK_ANSI_RightBracket", MAC_VK_ANSI_RightBracket); +const MAC_VK_ANSI_O = 0x1f; +_defineConstant("MAC_VK_ANSI_O", MAC_VK_ANSI_O); +const MAC_VK_ANSI_U = 0x20; +_defineConstant("MAC_VK_ANSI_U", MAC_VK_ANSI_U); +const MAC_VK_ANSI_LeftBracket = 0x21; +_defineConstant("MAC_VK_ANSI_LeftBracket", MAC_VK_ANSI_LeftBracket); +const MAC_VK_ANSI_I = 0x22; +_defineConstant("MAC_VK_ANSI_I", MAC_VK_ANSI_I); +const MAC_VK_ANSI_P = 0x23; +_defineConstant("MAC_VK_ANSI_P", MAC_VK_ANSI_P); +const MAC_VK_Return = 0x24; +_defineConstant("MAC_VK_Return", MAC_VK_Return); +const MAC_VK_ANSI_L = 0x25; +_defineConstant("MAC_VK_ANSI_L", MAC_VK_ANSI_L); +const MAC_VK_ANSI_J = 0x26; +_defineConstant("MAC_VK_ANSI_J", MAC_VK_ANSI_J); +const MAC_VK_ANSI_Quote = 0x27; +_defineConstant("MAC_VK_ANSI_Quote", MAC_VK_ANSI_Quote); +const MAC_VK_ANSI_K = 0x28; +_defineConstant("MAC_VK_ANSI_K", MAC_VK_ANSI_K); +const MAC_VK_ANSI_Semicolon = 0x29; +_defineConstant("MAC_VK_ANSI_Semicolon", MAC_VK_ANSI_Semicolon); +const MAC_VK_ANSI_Backslash = 0x2a; +_defineConstant("MAC_VK_ANSI_Backslash", MAC_VK_ANSI_Backslash); +const MAC_VK_ANSI_Comma = 0x2b; +_defineConstant("MAC_VK_ANSI_Comma", MAC_VK_ANSI_Comma); +const MAC_VK_ANSI_Slash = 0x2c; +_defineConstant("MAC_VK_ANSI_Slash", MAC_VK_ANSI_Slash); +const MAC_VK_ANSI_N = 0x2d; +_defineConstant("MAC_VK_ANSI_N", MAC_VK_ANSI_N); +const MAC_VK_ANSI_M = 0x2e; +_defineConstant("MAC_VK_ANSI_M", MAC_VK_ANSI_M); +const MAC_VK_ANSI_Period = 0x2f; +_defineConstant("MAC_VK_ANSI_Period", MAC_VK_ANSI_Period); +const MAC_VK_Tab = 0x30; +_defineConstant("MAC_VK_Tab", MAC_VK_Tab); +const MAC_VK_Space = 0x31; +_defineConstant("MAC_VK_Space", MAC_VK_Space); +const MAC_VK_ANSI_Grave = 0x32; +_defineConstant("MAC_VK_ANSI_Grave", MAC_VK_ANSI_Grave); +const MAC_VK_Delete = 0x33; +_defineConstant("MAC_VK_Delete", MAC_VK_Delete); +const MAC_VK_PC_Backspace = 0x33; +_defineConstant("MAC_VK_PC_Backspace", MAC_VK_PC_Backspace); +const MAC_VK_Powerbook_KeypadEnter = 0x34; +_defineConstant("MAC_VK_Powerbook_KeypadEnter", MAC_VK_Powerbook_KeypadEnter); +const MAC_VK_Escape = 0x35; +_defineConstant("MAC_VK_Escape", MAC_VK_Escape); +const MAC_VK_RightCommand = 0x36; +_defineConstant("MAC_VK_RightCommand", MAC_VK_RightCommand); +const MAC_VK_Command = 0x37; +_defineConstant("MAC_VK_Command", MAC_VK_Command); +const MAC_VK_Shift = 0x38; +_defineConstant("MAC_VK_Shift", MAC_VK_Shift); +const MAC_VK_CapsLock = 0x39; +_defineConstant("MAC_VK_CapsLock", MAC_VK_CapsLock); +const MAC_VK_Option = 0x3a; +_defineConstant("MAC_VK_Option", MAC_VK_Option); +const MAC_VK_Control = 0x3b; +_defineConstant("MAC_VK_Control", MAC_VK_Control); +const MAC_VK_RightShift = 0x3c; +_defineConstant("MAC_VK_RightShift", MAC_VK_RightShift); +const MAC_VK_RightOption = 0x3d; +_defineConstant("MAC_VK_RightOption", MAC_VK_RightOption); +const MAC_VK_RightControl = 0x3e; +_defineConstant("MAC_VK_RightControl", MAC_VK_RightControl); +const MAC_VK_Function = 0x3f; +_defineConstant("MAC_VK_Function", MAC_VK_Function); +const MAC_VK_F17 = 0x40; +_defineConstant("MAC_VK_F17", MAC_VK_F17); +const MAC_VK_ANSI_KeypadDecimal = 0x41; +_defineConstant("MAC_VK_ANSI_KeypadDecimal", MAC_VK_ANSI_KeypadDecimal); +const MAC_VK_ANSI_KeypadMultiply = 0x43; +_defineConstant("MAC_VK_ANSI_KeypadMultiply", MAC_VK_ANSI_KeypadMultiply); +const MAC_VK_ANSI_KeypadPlus = 0x45; +_defineConstant("MAC_VK_ANSI_KeypadPlus", MAC_VK_ANSI_KeypadPlus); +const MAC_VK_ANSI_KeypadClear = 0x47; +_defineConstant("MAC_VK_ANSI_KeypadClear", MAC_VK_ANSI_KeypadClear); +const MAC_VK_VolumeUp = 0x48; +_defineConstant("MAC_VK_VolumeUp", MAC_VK_VolumeUp); +const MAC_VK_VolumeDown = 0x49; +_defineConstant("MAC_VK_VolumeDown", MAC_VK_VolumeDown); +const MAC_VK_Mute = 0x4a; +_defineConstant("MAC_VK_Mute", MAC_VK_Mute); +const MAC_VK_ANSI_KeypadDivide = 0x4b; +_defineConstant("MAC_VK_ANSI_KeypadDivide", MAC_VK_ANSI_KeypadDivide); +const MAC_VK_ANSI_KeypadEnter = 0x4c; +_defineConstant("MAC_VK_ANSI_KeypadEnter", MAC_VK_ANSI_KeypadEnter); +const MAC_VK_ANSI_KeypadMinus = 0x4e; +_defineConstant("MAC_VK_ANSI_KeypadMinus", MAC_VK_ANSI_KeypadMinus); +const MAC_VK_F18 = 0x4f; +_defineConstant("MAC_VK_F18", MAC_VK_F18); +const MAC_VK_F19 = 0x50; +_defineConstant("MAC_VK_F19", MAC_VK_F19); +const MAC_VK_ANSI_KeypadEquals = 0x51; +_defineConstant("MAC_VK_ANSI_KeypadEquals", MAC_VK_ANSI_KeypadEquals); +const MAC_VK_ANSI_Keypad0 = 0x52; +_defineConstant("MAC_VK_ANSI_Keypad0", MAC_VK_ANSI_Keypad0); +const MAC_VK_ANSI_Keypad1 = 0x53; +_defineConstant("MAC_VK_ANSI_Keypad1", MAC_VK_ANSI_Keypad1); +const MAC_VK_ANSI_Keypad2 = 0x54; +_defineConstant("MAC_VK_ANSI_Keypad2", MAC_VK_ANSI_Keypad2); +const MAC_VK_ANSI_Keypad3 = 0x55; +_defineConstant("MAC_VK_ANSI_Keypad3", MAC_VK_ANSI_Keypad3); +const MAC_VK_ANSI_Keypad4 = 0x56; +_defineConstant("MAC_VK_ANSI_Keypad4", MAC_VK_ANSI_Keypad4); +const MAC_VK_ANSI_Keypad5 = 0x57; +_defineConstant("MAC_VK_ANSI_Keypad5", MAC_VK_ANSI_Keypad5); +const MAC_VK_ANSI_Keypad6 = 0x58; +_defineConstant("MAC_VK_ANSI_Keypad6", MAC_VK_ANSI_Keypad6); +const MAC_VK_ANSI_Keypad7 = 0x59; +_defineConstant("MAC_VK_ANSI_Keypad7", MAC_VK_ANSI_Keypad7); +const MAC_VK_F20 = 0x5a; +_defineConstant("MAC_VK_F20", MAC_VK_F20); +const MAC_VK_ANSI_Keypad8 = 0x5b; +_defineConstant("MAC_VK_ANSI_Keypad8", MAC_VK_ANSI_Keypad8); +const MAC_VK_ANSI_Keypad9 = 0x5c; +_defineConstant("MAC_VK_ANSI_Keypad9", MAC_VK_ANSI_Keypad9); +const MAC_VK_JIS_Yen = 0x5d; +_defineConstant("MAC_VK_JIS_Yen", MAC_VK_JIS_Yen); +const MAC_VK_JIS_Underscore = 0x5e; +_defineConstant("MAC_VK_JIS_Underscore", MAC_VK_JIS_Underscore); +const MAC_VK_JIS_KeypadComma = 0x5f; +_defineConstant("MAC_VK_JIS_KeypadComma", MAC_VK_JIS_KeypadComma); +const MAC_VK_F5 = 0x60; +_defineConstant("MAC_VK_F5", MAC_VK_F5); +const MAC_VK_F6 = 0x61; +_defineConstant("MAC_VK_F6", MAC_VK_F6); +const MAC_VK_F7 = 0x62; +_defineConstant("MAC_VK_F7", MAC_VK_F7); +const MAC_VK_F3 = 0x63; +_defineConstant("MAC_VK_F3", MAC_VK_F3); +const MAC_VK_F8 = 0x64; +_defineConstant("MAC_VK_F8", MAC_VK_F8); +const MAC_VK_F9 = 0x65; +_defineConstant("MAC_VK_F9", MAC_VK_F9); +const MAC_VK_JIS_Eisu = 0x66; +_defineConstant("MAC_VK_JIS_Eisu", MAC_VK_JIS_Eisu); +const MAC_VK_F11 = 0x67; +_defineConstant("MAC_VK_F11", MAC_VK_F11); +const MAC_VK_JIS_Kana = 0x68; +_defineConstant("MAC_VK_JIS_Kana", MAC_VK_JIS_Kana); +const MAC_VK_F13 = 0x69; +_defineConstant("MAC_VK_F13", MAC_VK_F13); +const MAC_VK_PC_PrintScreen = 0x69; +_defineConstant("MAC_VK_PC_PrintScreen", MAC_VK_PC_PrintScreen); +const MAC_VK_F16 = 0x6a; +_defineConstant("MAC_VK_F16", MAC_VK_F16); +const MAC_VK_F14 = 0x6b; +_defineConstant("MAC_VK_F14", MAC_VK_F14); +const MAC_VK_PC_ScrollLock = 0x6b; +_defineConstant("MAC_VK_PC_ScrollLock", MAC_VK_PC_ScrollLock); +const MAC_VK_F10 = 0x6d; +_defineConstant("MAC_VK_F10", MAC_VK_F10); +const MAC_VK_PC_ContextMenu = 0x6e; +_defineConstant("MAC_VK_PC_ContextMenu", MAC_VK_PC_ContextMenu); +const MAC_VK_F12 = 0x6f; +_defineConstant("MAC_VK_F12", MAC_VK_F12); +const MAC_VK_F15 = 0x71; +_defineConstant("MAC_VK_F15", MAC_VK_F15); +const MAC_VK_PC_Pause = 0x71; +_defineConstant("MAC_VK_PC_Pause", MAC_VK_PC_Pause); +const MAC_VK_Help = 0x72; +_defineConstant("MAC_VK_Help", MAC_VK_Help); +const MAC_VK_PC_Insert = 0x72; +_defineConstant("MAC_VK_PC_Insert", MAC_VK_PC_Insert); +const MAC_VK_Home = 0x73; +_defineConstant("MAC_VK_Home", MAC_VK_Home); +const MAC_VK_PageUp = 0x74; +_defineConstant("MAC_VK_PageUp", MAC_VK_PageUp); +const MAC_VK_ForwardDelete = 0x75; +_defineConstant("MAC_VK_ForwardDelete", MAC_VK_ForwardDelete); +const MAC_VK_PC_Delete = 0x75; +_defineConstant("MAC_VK_PC_Delete", MAC_VK_PC_Delete); +const MAC_VK_F4 = 0x76; +_defineConstant("MAC_VK_F4", MAC_VK_F4); +const MAC_VK_End = 0x77; +_defineConstant("MAC_VK_End", MAC_VK_End); +const MAC_VK_F2 = 0x78; +_defineConstant("MAC_VK_F2", MAC_VK_F2); +const MAC_VK_PageDown = 0x79; +_defineConstant("MAC_VK_PageDown", MAC_VK_PageDown); +const MAC_VK_F1 = 0x7a; +_defineConstant("MAC_VK_F1", MAC_VK_F1); +const MAC_VK_LeftArrow = 0x7b; +_defineConstant("MAC_VK_LeftArrow", MAC_VK_LeftArrow); +const MAC_VK_RightArrow = 0x7c; +_defineConstant("MAC_VK_RightArrow", MAC_VK_RightArrow); +const MAC_VK_DownArrow = 0x7d; +_defineConstant("MAC_VK_DownArrow", MAC_VK_DownArrow); +const MAC_VK_UpArrow = 0x7e; +_defineConstant("MAC_VK_UpArrow", MAC_VK_UpArrow); diff --git a/testing/mochitest/tests/SimpleTest/SimpleTest.js b/testing/mochitest/tests/SimpleTest/SimpleTest.js new file mode 100644 index 0000000000..a237c25103 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js @@ -0,0 +1,2267 @@ +/* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ + +// Generally gTestPath should be set by the harness. +/* global gTestPath */ + +/** + * SimpleTest framework object. + * @class + */ +var SimpleTest = {}; +var parentRunner = null; + +// Using a try/catch rather than SpecialPowers.Cu.isRemoteProxy() because +// it doesn't cover the case where an iframe is xorigin but fission is +// not enabled. +let isSameOrigin = function (w) { + try { + w.top.TestRunner; + } catch (e) { + if (e instanceof DOMException) { + return false; + } + } + return true; +}; +let isXOrigin = !isSameOrigin(window); + +// Note: duplicated in browser-test.js . See also bug 1820150. +function isErrorOrException(err) { + // It'd be nice if we had either `Error.isError(err)` or `Error.isInstance(err)` + // but we don't, so do it ourselves: + if (!err) { + return false; + } + if (err instanceof SpecialPowers.Ci.nsIException) { + return true; + } + try { + let glob = SpecialPowers.Cu.getGlobalForObject(err); + return err instanceof glob.Error; + } catch { + // getGlobalForObject can be upset if it doesn't get passed an object. + // Just do a standard instanceof check using this global and cross fingers: + } + return err instanceof Error; +} + +// In normal test runs, the window that has a TestRunner in its parent is +// the primary window. In single test runs, if there is no parent and there +// is no opener then it is the primary window. +var isSingleTestRun = + parent == window && + !(opener || (window.arguments && window.arguments[0].SimpleTest)); +try { + var isPrimaryTestWindow = + (isXOrigin && parent != window && parent == top) || + (!isXOrigin && (!!parent.TestRunner || isSingleTestRun)); +} catch (e) { + dump( + "TEST-UNEXPECTED-FAIL, Exception caught: " + + e.message + + ", at: " + + e.fileName + + " (" + + e.lineNumber + + "), location: " + + window.location.href + + "\n" + ); +} + +let xOriginRunner = { + init(harnessWindow) { + this.harnessWindow = harnessWindow; + let url = new URL(document.URL); + this.testFile = url.pathname; + this.showTestReport = url.searchParams.get("showTestReport") == "true"; + this.expected = url.searchParams.get("expected"); + }, + callHarnessMethod(applyOn, command, ...params) { + // Message handled by xOriginTestRunnerHandler in TestRunner.js + this.harnessWindow.postMessage( + { + harnessType: "SimpleTest", + applyOn, + command, + params, + }, + "*" + ); + }, + getParameterInfo() { + let url = new URL(document.URL); + return { + currentTestURL: url.searchParams.get("currentTestURL"), + testRoot: url.searchParams.get("testRoot"), + }; + }, + addFailedTest(test) { + this.callHarnessMethod("runner", "addFailedTest", test); + }, + expectAssertions(min, max) { + this.callHarnessMethod("runner", "expectAssertions", min, max); + }, + expectChildProcessCrash() { + this.callHarnessMethod("runner", "expectChildProcessCrash"); + }, + requestLongerTimeout(factor) { + this.callHarnessMethod("runner", "requestLongerTimeout", factor); + }, + _lastAssertionCount: 0, + testFinished(tests) { + var newAssertionCount = SpecialPowers.assertionCount(); + var numAsserts = newAssertionCount - this._lastAssertionCount; + this._lastAssertionCount = newAssertionCount; + this.callHarnessMethod("runner", "addAssertionCount", numAsserts); + this.callHarnessMethod("runner", "testFinished", tests); + }, + structuredLogger: { + info(msg) { + xOriginRunner.callHarnessMethod("logger", "structuredLogger.info", msg); + }, + warning(msg) { + xOriginRunner.callHarnessMethod( + "logger", + "structuredLogger.warning", + msg + ); + }, + error(msg) { + xOriginRunner.callHarnessMethod("logger", "structuredLogger.error", msg); + }, + activateBuffering() { + xOriginRunner.callHarnessMethod( + "logger", + "structuredLogger.activateBuffering" + ); + }, + deactivateBuffering() { + xOriginRunner.callHarnessMethod( + "logger", + "structuredLogger.deactivateBuffering" + ); + }, + testStatus(url, subtest, status, expected, diagnostic, stack) { + xOriginRunner.callHarnessMethod( + "logger", + "structuredLogger.testStatus", + url, + subtest, + status, + expected, + diagnostic, + stack + ); + }, + }, +}; + +// Finds the TestRunner for this test run and the SpecialPowers object (in +// case it is not defined) from a parent/opener window. +// +// Finding the SpecialPowers object is needed when we have ChromePowers in +// harness.xhtml and we need SpecialPowers in the iframe, and also for tests +// like test_focus.xhtml where we open a window which opens another window which +// includes SimpleTest.js. +(function () { + function ancestor(w) { + return w.parent != w + ? w.parent + : w.opener || + (!isXOrigin && + w.arguments && + SpecialPowers.wrap(Window).isInstance(w.arguments[0]) && + w.arguments[0]); + } + + var w = ancestor(window); + while (w && !parentRunner) { + isXOrigin = !isSameOrigin(w); + + if (isXOrigin) { + if (w.parent != w) { + w = w.top; + } + xOriginRunner.init(w); + parentRunner = xOriginRunner; + } + + if (!parentRunner) { + parentRunner = w.TestRunner; + if (!parentRunner && w.wrappedJSObject) { + parentRunner = w.wrappedJSObject.TestRunner; + } + } + w = ancestor(w); + } + + if (parentRunner) { + SimpleTest.harnessParameters = parentRunner.getParameterInfo(); + } +})(); + +/* Helper functions pulled out of various MochiKit modules */ +if (typeof repr == "undefined") { + this.repr = function repr(o) { + if (typeof o == "undefined") { + return "undefined"; + } else if (o === null) { + return "null"; + } + try { + if (typeof o.__repr__ == "function") { + return o.__repr__(); + } else if (typeof o.repr == "function" && o.repr != repr) { + return o.repr(); + } + } catch (e) {} + try { + if ( + typeof o.NAME == "string" && + (o.toString == Function.prototype.toString || + o.toString == Object.prototype.toString) + ) { + return o.NAME; + } + } catch (e) {} + var ostring; + try { + if (o === 0) { + ostring = 1 / o > 0 ? "+0" : "-0"; + } else if (typeof o === "string") { + ostring = JSON.stringify(o); + } else if (Array.isArray(o)) { + ostring = "[" + o.map(val => repr(val)).join(", ") + "]"; + } else { + ostring = o + ""; + } + } catch (e) { + return "[" + typeof o + "]"; + } + if (typeof o == "function") { + o = ostring.replace(/^\s+/, ""); + var idx = o.indexOf("{"); + if (idx != -1) { + o = o.substr(0, idx) + "{...}"; + } + } + return ostring; + }; +} + +/* This returns a function that applies the previously given parameters. + * This is used by SimpleTest.showReport + */ +if (typeof partial == "undefined") { + this.partial = function (func) { + var args = []; + for (let i = 1; i < arguments.length; i++) { + args.push(arguments[i]); + } + return function () { + if (arguments.length) { + for (let i = 1; i < arguments.length; i++) { + args.push(arguments[i]); + } + } + func(args); + }; + }; +} + +if (typeof getElement == "undefined") { + this.getElement = function (id) { + return typeof id == "string" ? document.getElementById(id) : id; + }; + this.$ = this.getElement; +} + +SimpleTest._newCallStack = function (path) { + var rval = function callStackHandler() { + var callStack = callStackHandler.callStack; + for (var i = 0; i < callStack.length; i++) { + if (callStack[i].apply(this, arguments) === false) { + break; + } + } + try { + this[path] = null; + } catch (e) { + // pass + } + }; + rval.callStack = []; + return rval; +}; + +if (typeof addLoadEvent == "undefined") { + this.addLoadEvent = function (func) { + var existing = window.onload; + var regfunc = existing; + if ( + !( + typeof existing == "function" && + typeof existing.callStack == "object" && + existing.callStack !== null + ) + ) { + regfunc = SimpleTest._newCallStack("onload"); + if (typeof existing == "function") { + regfunc.callStack.push(existing); + } + window.onload = regfunc; + } + regfunc.callStack.push(func); + }; +} + +function createEl(type, attrs, html) { + //use createElementNS so the xul/xhtml tests have no issues + var el; + if (!document.body) { + el = document.createElementNS("http://www.w3.org/1999/xhtml", type); + } else { + el = document.createElement(type); + } + if (attrs !== null && attrs !== undefined) { + for (var k in attrs) { + el.setAttribute(k, attrs[k]); + } + } + if (html !== null && html !== undefined) { + el.appendChild(document.createTextNode(html)); + } + return el; +} + +/* lots of tests use this as a helper to get css properties */ +if (typeof computedStyle == "undefined") { + this.computedStyle = function (elem, cssProperty) { + elem = getElement(elem); + if (elem.currentStyle) { + return elem.currentStyle[cssProperty]; + } + if (typeof document.defaultView == "undefined" || document === null) { + return undefined; + } + var style = document.defaultView.getComputedStyle(elem); + if (typeof style == "undefined" || style === null) { + return undefined; + } + + var selectorCase = cssProperty.replace(/([A-Z])/g, "-$1").toLowerCase(); + + return style.getPropertyValue(selectorCase); + }; +} + +SimpleTest._tests = []; +SimpleTest._stopOnLoad = true; +SimpleTest._cleanupFunctions = []; +SimpleTest._taskCleanupFunctions = []; +SimpleTest._currentTask = null; +SimpleTest._timeoutFunctions = []; +SimpleTest._inChaosMode = false; +// When using failure pattern file to filter unexpected issues, +// SimpleTest.expected would be an array of [pattern, expected count], +// and SimpleTest.num_failed would be an array of actual counts which +// has the same length as SimpleTest.expected. +SimpleTest.expected = "pass"; +SimpleTest.num_failed = 0; + +SpecialPowers.setAsDefaultAssertHandler(); + +function usesFailurePatterns() { + return Array.isArray(SimpleTest.expected); +} + +/** + * Checks whether there is any failure pattern matches the given error + * message, and if found, bumps the counter of the failure pattern. + * + * @return {boolean} Whether a matched failure pattern is found. + */ +function recordIfMatchesFailurePattern(name, diag) { + let index = SimpleTest.expected.findIndex(([pat, count]) => { + return ( + pat == null || + (typeof name == "string" && name.includes(pat)) || + (typeof diag == "string" && diag.includes(pat)) + ); + }); + if (index >= 0) { + SimpleTest.num_failed[index]++; + return true; + } + return false; +} + +SimpleTest.setExpected = function () { + if (!parentRunner) { + return; + } + if (!Array.isArray(parentRunner.expected)) { + SimpleTest.expected = parentRunner.expected; + } else { + // Assertions are checked by the runner. + SimpleTest.expected = parentRunner.expected.filter( + ([pat]) => pat != "ASSERTION" + ); + SimpleTest.num_failed = new Array(SimpleTest.expected.length); + SimpleTest.num_failed.fill(0); + } +}; +SimpleTest.setExpected(); + +/** + * Something like assert. + **/ +SimpleTest.ok = function (condition, name) { + if (arguments.length > 2) { + const diag = "Too many arguments passed to `ok(condition, name)`"; + SimpleTest.record(false, name, diag); + } else { + SimpleTest.record(condition, name); + } +}; + +SimpleTest.record = function (condition, name, diag, stack, expected) { + var test = { result: !!condition, name, diag }; + let successInfo; + let failureInfo; + if (SimpleTest.expected == "fail") { + if (!test.result) { + SimpleTest.num_failed++; + test.result = true; + } + successInfo = { + status: "PASS", + expected: "PASS", + message: "TEST-PASS", + }; + failureInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + } else if (!test.result && usesFailurePatterns()) { + if (recordIfMatchesFailurePattern(name, diag)) { + test.result = true; + // Add a mark for unexpected failures suppressed by failure pattern. + name = "[suppressed] " + name; + } + successInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + failureInfo = { + status: "FAIL", + expected: "PASS", + message: "TEST-UNEXPECTED-FAIL", + }; + } else if (expected == "fail") { + successInfo = { + status: "PASS", + expected: "FAIL", + message: "TEST-UNEXPECTED-PASS", + }; + failureInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + } else { + successInfo = { + status: "PASS", + expected: "PASS", + message: "TEST-PASS", + }; + failureInfo = { + status: "FAIL", + expected: "PASS", + message: "TEST-UNEXPECTED-FAIL", + }; + } + + if (condition) { + stack = null; + } else if (!stack) { + stack = new Error().stack + .replace(/^(.*@)http:\/\/mochi.test:8888\/tests\//gm, " $1") + .split("\n"); + stack.splice(0, 1); + stack = stack.join("\n"); + } + SimpleTest._logResult(test, successInfo, failureInfo, stack); + SimpleTest._tests.push(test); +}; + +/** + * Roughly equivalent to ok(Object.is(a, b), name) + **/ +SimpleTest.is = function (a, b, name) { + // Be lazy and use Object.is til we want to test a browser without it. + var pass = Object.is(a, b); + var diag = pass ? "" : "got " + repr(a) + ", expected " + repr(b); + SimpleTest.record(pass, name, diag); +}; + +SimpleTest.isfuzzy = function (a, b, epsilon, name) { + var pass = a >= b - epsilon && a <= b + epsilon; + var diag = pass + ? "" + : "got " + + repr(a) + + ", expected " + + repr(b) + + " epsilon: +/- " + + repr(epsilon); + SimpleTest.record(pass, name, diag); +}; + +SimpleTest.isnot = function (a, b, name) { + var pass = !Object.is(a, b); + var diag = pass ? "" : "didn't expect " + repr(a) + ", but got it"; + SimpleTest.record(pass, name, diag); +}; + +/** + * Check that the function call throws an exception. + */ +SimpleTest.doesThrow = function (fn, name) { + var gotException = false; + try { + fn(); + } catch (ex) { + gotException = true; + } + ok(gotException, name); +}; + +// --------------- Test.Builder/Test.More todo() ----------------- + +SimpleTest.todo = function (condition, name, diag) { + var test = { result: !!condition, name, diag, todo: true }; + if ( + test.result && + usesFailurePatterns() && + recordIfMatchesFailurePattern(name, diag) + ) { + // Flipping the result to false so we don't get unexpected result. There + // is no perfect way here. A known failure can trigger unexpected pass, + // in which case, tagging it as KNOWN-FAIL probably makes more sense than + // marking it PASS. + test.result = false; + // Add a mark for unexpected failures suppressed by failure pattern. + name = "[suppressed] " + name; + } + var successInfo = { + status: "PASS", + expected: "FAIL", + message: "TEST-UNEXPECTED-PASS", + }; + var failureInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + SimpleTest._logResult(test, successInfo, failureInfo); + SimpleTest._tests.push(test); +}; + +/* + * Returns the absolute URL to a test data file from where tests + * are served. i.e. the file doesn't necessarely exists where tests + * are executed. + * + * (For android, mochitest are executed on the device, while + * all mochitest html (and others) files are served from the test runner + * slave) + */ +SimpleTest.getTestFileURL = function (path) { + var location = window.location; + // Remove mochitest html file name from the path + var remotePath = location.pathname.replace(/\/[^\/]+?$/, ""); + var url = location.origin + remotePath + "/" + path; + return url; +}; + +SimpleTest._getCurrentTestURL = function () { + return ( + (SimpleTest.harnessParameters && + SimpleTest.harnessParameters.currentTestURL) || + (parentRunner && parentRunner.currentTestURL) || + (typeof gTestPath == "string" && gTestPath) || + "unknown test url" + ); +}; + +SimpleTest._forceLogMessageOutput = false; + +/** + * Force all test messages to be displayed. Only applies for the current test. + */ +SimpleTest.requestCompleteLog = function () { + if (!parentRunner || SimpleTest._forceLogMessageOutput) { + return; + } + + parentRunner.structuredLogger.deactivateBuffering(); + SimpleTest._forceLogMessageOutput = true; + + SimpleTest.registerCleanupFunction(function () { + parentRunner.structuredLogger.activateBuffering(); + SimpleTest._forceLogMessageOutput = false; + }); +}; + +SimpleTest._logResult = function (test, passInfo, failInfo, stack) { + var url = SimpleTest._getCurrentTestURL(); + var result = test.result ? passInfo : failInfo; + var diagnostic = test.diag || null; + // BUGFIX : coercing test.name to a string, because some a11y tests pass an xpconnect object + var subtest = test.name ? String(test.name) : null; + var isError = !test.result == !test.todo; + + if (parentRunner) { + if (!result.status || !result.expected) { + if (diagnostic) { + parentRunner.structuredLogger.info(diagnostic); + } + return; + } + + if (isError) { + parentRunner.addFailedTest(url); + } + + parentRunner.structuredLogger.testStatus( + url, + subtest, + result.status, + result.expected, + diagnostic, + stack + ); + } else if (typeof dump === "function") { + var diagMessage = test.name + (test.diag ? " - " + test.diag : ""); + var debugMsg = [result.message, url, diagMessage].join(" | "); + dump(debugMsg + "\n"); + } else { + // Non-Mozilla browser? Just do nothing. + } +}; + +SimpleTest.info = function (name, message) { + var log = message ? name + " | " + message : name; + if (parentRunner) { + parentRunner.structuredLogger.info(log); + } else { + dump(log + "\n"); + } +}; + +/** + * Copies of is and isnot with the call to ok replaced by a call to todo. + **/ + +SimpleTest.todo_is = function (a, b, name) { + var pass = Object.is(a, b); + var diag = pass + ? repr(a) + " should equal " + repr(b) + : "got " + repr(a) + ", expected " + repr(b); + SimpleTest.todo(pass, name, diag); +}; + +SimpleTest.todo_isnot = function (a, b, name) { + var pass = !Object.is(a, b); + var diag = pass + ? repr(a) + " should not equal " + repr(b) + : "didn't expect " + repr(a) + ", but got it"; + SimpleTest.todo(pass, name, diag); +}; + +/** + * Makes a test report, returns it as a DIV element. + **/ +SimpleTest.report = function () { + var passed = 0; + var failed = 0; + var todo = 0; + + var tallyAndCreateDiv = function (test) { + var cls, msg, div; + var diag = test.diag ? " - " + test.diag : ""; + if (test.todo && !test.result) { + todo++; + cls = "test_todo"; + msg = "todo | " + test.name + diag; + } else if (test.result && !test.todo) { + passed++; + cls = "test_ok"; + msg = "passed | " + test.name + diag; + } else { + failed++; + cls = "test_not_ok"; + msg = "failed | " + test.name + diag; + } + div = createEl("div", { class: cls }, msg); + return div; + }; + var results = []; + for (var d = 0; d < SimpleTest._tests.length; d++) { + results.push(tallyAndCreateDiv(SimpleTest._tests[d])); + } + + var summary_class = + // eslint-disable-next-line no-nested-ternary + failed != 0 ? "some_fail" : passed == 0 ? "todo_only" : "all_pass"; + + var div1 = createEl("div", { class: "tests_report" }); + var div2 = createEl("div", { class: "tests_summary " + summary_class }); + var div3 = createEl("div", { class: "tests_passed" }, "Passed: " + passed); + var div4 = createEl("div", { class: "tests_failed" }, "Failed: " + failed); + var div5 = createEl("div", { class: "tests_todo" }, "Todo: " + todo); + div2.appendChild(div3); + div2.appendChild(div4); + div2.appendChild(div5); + div1.appendChild(div2); + for (var t = 0; t < results.length; t++) { + //iterate in order + div1.appendChild(results[t]); + } + return div1; +}; + +/** + * Toggle element visibility + **/ +SimpleTest.toggle = function (el) { + if (computedStyle(el, "display") == "block") { + el.style.display = "none"; + } else { + el.style.display = "block"; + } +}; + +/** + * Toggle visibility for divs with a specific class. + **/ +SimpleTest.toggleByClass = function (cls, evt) { + var children = document.getElementsByTagName("div"); + var elements = []; + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var clsName = child.className; + if (!clsName) { + continue; + } + var classNames = clsName.split(" "); + for (var j = 0; j < classNames.length; j++) { + if (classNames[j] == cls) { + elements.push(child); + break; + } + } + } + for (var t = 0; t < elements.length; t++) { + //TODO: again, for-in loop over elems seems to break this + SimpleTest.toggle(elements[t]); + } + if (evt) { + evt.preventDefault(); + } +}; + +/** + * Shows the report in the browser + **/ +SimpleTest.showReport = function () { + var togglePassed = createEl("a", { href: "#" }, "Toggle passed checks"); + var toggleFailed = createEl("a", { href: "#" }, "Toggle failed checks"); + var toggleTodo = createEl("a", { href: "#" }, "Toggle todo checks"); + togglePassed.onclick = partial(SimpleTest.toggleByClass, "test_ok"); + toggleFailed.onclick = partial(SimpleTest.toggleByClass, "test_not_ok"); + toggleTodo.onclick = partial(SimpleTest.toggleByClass, "test_todo"); + var body = document.body; // Handles HTML documents + if (!body) { + // Do the XML thing. + body = document.getElementsByTagNameNS( + "http://www.w3.org/1999/xhtml", + "body" + )[0]; + } + var firstChild = body.childNodes[0]; + var addNode; + if (firstChild) { + addNode = function (el) { + body.insertBefore(el, firstChild); + }; + } else { + addNode = function (el) { + body.appendChild(el); + }; + } + addNode(togglePassed); + addNode(createEl("span", null, " ")); + addNode(toggleFailed); + addNode(createEl("span", null, " ")); + addNode(toggleTodo); + addNode(SimpleTest.report()); + // Add a separator from the test content. + addNode(createEl("hr")); +}; + +/** + * Tells SimpleTest to don't finish the test when the document is loaded, + * useful for asynchronous tests. + * + * When SimpleTest.waitForExplicitFinish is called, + * explicit SimpleTest.finish() is required. + **/ +SimpleTest.waitForExplicitFinish = function () { + SimpleTest._stopOnLoad = false; +}; + +/** + * Multiply the timeout the parent runner uses for this test by the + * given factor. + * + * For example, in a test that may take a long time to complete, using + * "SimpleTest.requestLongerTimeout(5)" will give it 5 times as long to + * finish. + * + * @param {Number} factor + * The multiplication factor to use on the timeout for this test. + */ +SimpleTest.requestLongerTimeout = function (factor) { + if (parentRunner) { + parentRunner.requestLongerTimeout(factor); + } else { + dump( + "[SimpleTest.requestLongerTimeout()] ignoring request, maybe you meant to call the global `requestLongerTimeout` instead?\n" + ); + } +}; + +/** + * Note that the given range of assertions is to be expected. When + * this function is not called, 0 assertions are expected. When only + * one argument is given, that number of assertions are expected. + * + * A test where we expect to have assertions (which should largely be a + * transitional mechanism to get assertion counts down from our current + * situation) can call the SimpleTest.expectAssertions() function, with + * either one or two arguments: one argument gives an exact number + * expected, and two arguments give a range. For example, a test might do + * one of the following: + * + * @example + * + * // Currently triggers two assertions (bug NNNNNN). + * SimpleTest.expectAssertions(2); + * + * // Currently triggers one assertion on Mac (bug NNNNNN). + * if (navigator.platform.indexOf("Mac") == 0) { + * SimpleTest.expectAssertions(1); + * } + * + * // Currently triggers two assertions on all platforms (bug NNNNNN), + * // but intermittently triggers two additional assertions (bug NNNNNN) + * // on Windows. + * if (navigator.platform.indexOf("Win") == 0) { + * SimpleTest.expectAssertions(2, 4); + * } else { + * SimpleTest.expectAssertions(2); + * } + * + * // Intermittently triggers up to three assertions (bug NNNNNN). + * SimpleTest.expectAssertions(0, 3); + */ +SimpleTest.expectAssertions = function (min, max) { + if (parentRunner) { + parentRunner.expectAssertions(min, max); + } +}; + +SimpleTest._flakyTimeoutIsOK = false; +SimpleTest._originalSetTimeout = window.setTimeout; +window.setTimeout = function SimpleTest_setTimeoutShim() { + // Don't break tests that are loaded without a parent runner. + if (parentRunner) { + // Right now, we only enable these checks for mochitest-plain. + switch (SimpleTest.harnessParameters.testRoot) { + case "browser": + case "chrome": + case "a11y": + break; + default: + if ( + !SimpleTest._alreadyFinished && + arguments.length > 1 && + arguments[1] > 0 + ) { + if (SimpleTest._flakyTimeoutIsOK) { + SimpleTest.todo( + false, + "The author of the test has indicated that flaky timeouts are expected. Reason: " + + SimpleTest._flakyTimeoutReason + ); + } else { + SimpleTest.ok( + false, + "Test attempted to use a flaky timeout value " + arguments[1] + ); + } + } + } + } + return SimpleTest._originalSetTimeout.apply(window, arguments); +}; + +/** + * Request the framework to allow usage of setTimeout(func, timeout) + * where ``timeout > 0``. This is required to note that the author of + * the test is aware of the inherent flakiness in the test caused by + * that, and asserts that there is no way around using the magic timeout + * value number for some reason. + * + * Use of this function is **STRONGLY** discouraged. Think twice before + * using it. Such magic timeout values could result in intermittent + * failures in your test, and are almost never necessary! + * + * @param {String} reason + * A string representation of the reason why the test needs timeouts. + * + */ +SimpleTest.requestFlakyTimeout = function (reason) { + SimpleTest.is(typeof reason, "string", "A valid string reason is expected"); + SimpleTest.isnot(reason, "", "Reason cannot be empty"); + SimpleTest._flakyTimeoutIsOK = true; + SimpleTest._flakyTimeoutReason = reason; +}; + +/** + * If the page is not yet loaded, waits for the load event. If the page is + * not yet focused, focuses and waits for the window to be focused. + * If the current page is 'about:blank', then the page is assumed to not + * yet be loaded. Pass true for expectBlankPage to not make this assumption + * if you expect a blank page to be present. + * + * The target object should be specified if it is different than 'window'. The + * actual focused window may be a descendant window of aObject. + * + * @param {Window|browser|BrowsingContext} [aObject] + * Optional object to be focused, and may be any of: + * window - a window object to focus + * browser - a <browser>/<iframe> element. The top-level window + * within the frame will be focused. + * browsing context - a browsing context containing a window to focus + * If not specified, defaults to the global 'window'. + * @param {boolean} [expectBlankPage=false] + * True if targetWindow.location is 'about:blank'. + * @param {boolean} [aBlurSubframe=false] + * If true, and a subframe within the window to focus is focused, blur + * it so that the specified window or browsing context will receive + * focus events. + * + * @returns The browsing context that was focused. + */ +SimpleTest.promiseFocus = async function ( + aObject, + aExpectBlankPage = false, + aBlurSubframe = false +) { + let browser; + let browsingContext; + let windowToFocus; + + if (!aObject) { + aObject = window; + } + + async function waitForEvent(aTarget, aEventName) { + return new Promise(resolve => { + aTarget.addEventListener(aEventName, resolve, { + capture: true, + once: true, + }); + }); + } + + if (SpecialPowers.wrap(Window).isInstance(aObject)) { + windowToFocus = aObject; + + let isBlank = windowToFocus.location.href == "about:blank"; + if ( + aExpectBlankPage != isBlank || + windowToFocus.document.readyState != "complete" + ) { + info("must wait for load"); + await waitForEvent(windowToFocus, "load"); + } + } else { + if (SpecialPowers.wrap(Element).isInstance(aObject)) { + // assume this is a browser/iframe element + browsingContext = aObject.browsingContext; + } else { + browsingContext = aObject; + } + + browser = + browsingContext == aObject ? aObject.top.embedderElement : aObject; + windowToFocus = browser.ownerGlobal; + } + + if (!windowToFocus.document.hasFocus()) { + info("must wait for focus"); + let focusPromise = waitForEvent(windowToFocus.document, "focus"); + SpecialPowers.focus(windowToFocus); + await focusPromise; + } + + if (browser) { + if (windowToFocus.document.activeElement != browser) { + browser.focus(); + } + + info("must wait for focus in content"); + + // Make sure that the child process thinks it is focused as well. + await SpecialPowers.ensureFocus(browsingContext, aBlurSubframe); + } else { + if (aBlurSubframe) { + SpecialPowers.clearFocus(windowToFocus); + } + + browsingContext = windowToFocus.browsingContext; + } + + // Some tests rely on this delay, likely expecting layout or paint to occur. + await new Promise(resolve => { + SimpleTest.executeSoon(resolve); + }); + + return browsingContext; +}; + +/** + * Version of promiseFocus that uses a callback. For compatibility, + * the callback is passed one argument, the window that was focused. + * If the focused window is not in the same process, null is supplied. + */ +SimpleTest.waitForFocus = function (callback, aObject, expectBlankPage) { + SimpleTest.promiseFocus(aObject, expectBlankPage).then(focusedBC => { + callback(focusedBC?.window); + }); +}; +/* eslint-enable mozilla/use-services */ + +SimpleTest.stripLinebreaksAndWhitespaceAfterTags = function (aString) { + return aString.replace(/(>\s*(\r\n|\n|\r)*\s*)/gm, ">"); +}; + +/* + * `navigator.platform` should include this, when the platform is Windows. + */ +const kPlatformWindows = "Win"; + +/* + * See `SimpleTest.waitForClipboard`. + */ +const kTextHtmlPrefixClipboardDataWindows = + "<html><body>\n<!--StartFragment-->"; + +/* + * See `SimpleTest.waitForClipboard`. + */ +const kTextHtmlSuffixClipboardDataWindows = + "<!--EndFragment-->\n</body>\n</html>"; + +/* + * Polls the clipboard waiting for the expected value. A known value different than + * the expected value is put on the clipboard first (and also polled for) so we + * can be sure the value we get isn't just the expected value because it was already + * on the clipboard. This only uses the global clipboard and only for text/plain + * values. + * + * @param {String|Function} aExpectedStringOrValidatorFn + * The string value that is expected to be on the clipboard, or a + * validator function getting expected clipboard data and returning a bool. + * If you specify string value, line breakers in clipboard are treated + * as LineFeed. Therefore, you cannot include CarriageReturn to the + * string. + * If you specify string value and expect "text/html" data, this wraps + * the expected value with `kTextHtmlPrefixClipboardDataWindows` and + * `kTextHtmlSuffixClipboardDataWindows` only when it runs on Windows + * because they are appended only by nsDataObj.cpp for Windows. + * https://searchfox.org/mozilla-central/rev/8f7b017a31326515cb467e69eef1f6c965b4f00e/widget/windows/nsDataObj.cpp#1798-1805,1839-1840,1842 + * Therefore, you can specify selected (copied) HTML data simply on any + * platforms. + * @param {Function} aSetupFn + * A function responsible for setting the clipboard to the expected value, + * called after the known value setting succeeds. + * @param {Function} aSuccessFn + * A function called when the expected value is found on the clipboard. + * @param {Function} aFailureFn + * A function called if the expected value isn't found on the clipboard + * within 5s. It can also be called if the known value can't be found. + * @param {String} [aFlavor="text/plain"] + * The flavor to look for. + * @param {Number} [aTimeout=5000] + * The timeout (in milliseconds) to wait for a clipboard change. + * @param {boolean} [aExpectFailure=false] + * If true, fail if the clipboard contents are modified within the timeout + * interval defined by aTimeout. When aExpectFailure is true, the argument + * aExpectedStringOrValidatorFn must be null, as it won't be used. + * @param {boolean} [aDontInitializeClipboardIfExpectFailure=false] + * If aExpectFailure and this is set to true, this does NOT initialize + * clipboard with random data before running aSetupFn. + */ +SimpleTest.waitForClipboard = function ( + aExpectedStringOrValidatorFn, + aSetupFn, + aSuccessFn, + aFailureFn, + aFlavor, + aTimeout, + aExpectFailure, + aDontInitializeClipboardIfExpectFailure +) { + let promise = SimpleTest.promiseClipboardChange( + aExpectedStringOrValidatorFn, + aSetupFn, + aFlavor, + aTimeout, + aExpectFailure, + aDontInitializeClipboardIfExpectFailure + ); + promise.then(aSuccessFn).catch(aFailureFn); +}; + +/** + * Promise-oriented version of waitForClipboard. + */ +SimpleTest.promiseClipboardChange = async function ( + aExpectedStringOrValidatorFn, + aSetupFn, + aFlavor, + aTimeout, + aExpectFailure, + aDontInitializeClipboardIfExpectFailure +) { + let requestedFlavor = aFlavor || "text/plain"; + + // The known value we put on the clipboard before running aSetupFn + let initialVal = "waitForClipboard-known-value-" + Math.random(); + let preExpectedVal = initialVal; + + let inputValidatorFn; + if (aExpectFailure) { + // If we expect failure, the aExpectedStringOrValidatorFn should be null + if (aExpectedStringOrValidatorFn !== null) { + SimpleTest.ok( + false, + "When expecting failure, aExpectedStringOrValidatorFn must be null" + ); + } + + inputValidatorFn = function (aData) { + return aData != initialVal; + }; + // Build a default validator function for common string input. + } else if (typeof aExpectedStringOrValidatorFn == "string") { + if (aExpectedStringOrValidatorFn.includes("\r")) { + throw new Error( + "Use function instead of string to compare raw line breakers in clipboard" + ); + } + if (requestedFlavor === "text/html" && navigator.platform.includes("Win")) { + inputValidatorFn = function (aData) { + return ( + aData.replace(/\r\n?/g, "\n") === + kTextHtmlPrefixClipboardDataWindows + + aExpectedStringOrValidatorFn + + kTextHtmlSuffixClipboardDataWindows + ); + }; + } else { + inputValidatorFn = function (aData) { + return aData.replace(/\r\n?/g, "\n") === aExpectedStringOrValidatorFn; + }; + } + } else { + inputValidatorFn = aExpectedStringOrValidatorFn; + } + + let maxPolls = aTimeout ? aTimeout / 100 : 50; + + async function putAndVerify(operationFn, validatorFn, flavor, expectFailure) { + await operationFn(); + + let data; + for (let i = 0; i < maxPolls; i++) { + data = SpecialPowers.getClipboardData(flavor); + if (validatorFn(data)) { + // Don't show the success message when waiting for preExpectedVal + if (preExpectedVal) { + preExpectedVal = null; + } else { + SimpleTest.ok( + !expectFailure, + "Clipboard has the given value: '" + data + "'" + ); + } + + return data; + } + + // Wait 100ms and check again. + await new Promise(resolve => { + SimpleTest._originalSetTimeout.apply(window, [resolve, 100]); + }); + } + + let errorMsg = `Timed out while polling clipboard for ${ + preExpectedVal ? "initialized" : "requested" + } data, got: ${data}`; + SimpleTest.ok(expectFailure, errorMsg); + if (!expectFailure) { + throw new Error(errorMsg); + } + return data; + } + + if (!aExpectFailure || !aDontInitializeClipboardIfExpectFailure) { + // First we wait for a known value different from the expected one. + SimpleTest.info(`Initializing clipboard with "${preExpectedVal}"...`); + await putAndVerify( + function () { + SpecialPowers.clipboardCopyString(preExpectedVal); + }, + function (aData) { + return aData == preExpectedVal; + }, + "text/plain", + false + ); + + SimpleTest.info( + "Succeeded initializing clipboard, start requested things..." + ); + } else { + preExpectedVal = null; + } + + return putAndVerify( + aSetupFn, + inputValidatorFn, + requestedFlavor, + aExpectFailure + ); +}; + +/** + * Wait for a condition for a while (actually up to 3s here). + * + * @param {Function} aCond + * A function returns the result of the condition + * @param {Function} aCallback + * A function called after the condition is passed or timeout. + * @param {String} aErrorMsg + * The message displayed when the condition failed to pass + * before timeout. + */ +SimpleTest.waitForCondition = function (aCond, aCallback, aErrorMsg) { + this.promiseWaitForCondition(aCond, aErrorMsg).then(() => aCallback()); +}; +SimpleTest.promiseWaitForCondition = async function (aCond, aErrorMsg) { + for (let tries = 0; tries < 30; ++tries) { + // Wait 100ms between checks. + await new Promise(resolve => { + SimpleTest._originalSetTimeout.apply(window, [resolve, 100]); + }); + + let conditionPassed; + try { + conditionPassed = await aCond(); + } catch (e) { + ok(false, `${e}\n${e.stack}`); + conditionPassed = false; + } + if (conditionPassed) { + return; + } + } + ok(false, aErrorMsg); +}; + +/** + * Executes a function shortly after the call, but lets the caller continue + * working (or finish). + * + * @param {Function} aFunc + * Function to execute soon. + */ +SimpleTest.executeSoon = function (aFunc) { + if ("SpecialPowers" in window) { + return SpecialPowers.executeSoon(aFunc, window); + } + setTimeout(aFunc, 0); + return null; // Avoid warning. +}; + +/** + * Register a cleanup/teardown function (which may be async) to run after all + * tasks have finished, before running the next test. If async (or the function + * returns a promise), the framework will wait for the promise/async function + * to resolve. + * + * @param {Function} aFunc + * The cleanup/teardown function to run. + */ +SimpleTest.registerCleanupFunction = function (aFunc) { + SimpleTest._cleanupFunctions.push(aFunc); +}; + +/** + * Register a cleanup/teardown function (which may be async) to run after the + * current task has finished, before running the next task. If async (or the + * function returns a promise), the framework will wait for the promise/async + * function to resolve. + * + * @param {Function} aFunc + * The cleanup/teardown function to run. + */ +SimpleTest.registerCurrentTaskCleanupFunction = function (aFunc) { + if (!SimpleTest._currentTask) { + return; + } + SimpleTest._currentTask._cleanupFunctions ||= []; + SimpleTest._currentTask._cleanupFunctions.push(aFunc); +}; + +/** + * Register a cleanup/teardown function (which may be async) to run after each + * task has finished, before running the next task. If async (or the + * function returns a promise), the framework will wait for the promise/async + * function to resolve. + * + * @param {Function} aFunc + * The cleanup/teardown function to run. + */ +SimpleTest.registerTaskCleanupFunction = function (aFunc) { + SimpleTest._taskCleanupFunctions.push(aFunc); +}; + +SimpleTest.registerTimeoutFunction = function (aFunc) { + SimpleTest._timeoutFunctions.push(aFunc); +}; + +SimpleTest.testInChaosMode = function () { + if (SimpleTest._inChaosMode) { + // It's already enabled for this test, don't enter twice + return; + } + SpecialPowers.DOMWindowUtils.enterChaosMode(); + SimpleTest._inChaosMode = true; + // increase timeout here as chaosmode is very slow (i.e. 10x) + // doing 20x as this overwrites anything the tests set + SimpleTest.requestLongerTimeout(20); +}; + +SimpleTest.timeout = async function () { + for (const func of SimpleTest._timeoutFunctions) { + await func(); + } + SimpleTest._timeoutFunctions = []; +}; + +SimpleTest.finishWithFailure = function (msg) { + SimpleTest.ok(false, msg); + SimpleTest.finish(); +}; + +/** + * Finishes the tests. This is automatically called, except when + * SimpleTest.waitForExplicitFinish() has been invoked. + **/ +SimpleTest.finish = function () { + if (SimpleTest._alreadyFinished) { + var err = + "TEST-UNEXPECTED-FAIL | SimpleTest | this test already called finish!"; + if (parentRunner) { + parentRunner.structuredLogger.error(err); + } else { + dump(err + "\n"); + } + } + + if (SimpleTest.expected == "fail" && SimpleTest.num_failed <= 0) { + let msg = "We expected at least one failure"; + let test = { + result: false, + name: "fail-if condition in manifest", + diag: msg, + }; + let successInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + let failureInfo = { + status: "PASS", + expected: "FAIL", + message: "TEST-UNEXPECTED-PASS", + }; + SimpleTest._logResult(test, successInfo, failureInfo); + SimpleTest._tests.push(test); + } else if (usesFailurePatterns()) { + SimpleTest.expected.forEach(([pat, expected_count], i) => { + let count = SimpleTest.num_failed[i]; + let diag; + if (expected_count === null && count == 0) { + diag = "expected some failures but got none"; + } else if (expected_count !== null && expected_count != count) { + diag = `expected ${expected_count} failures but got ${count}`; + } else { + return; + } + let name = pat + ? `failure pattern \`${pat}\` in this test` + : "failures in this test"; + let test = { result: false, name, diag }; + let successInfo = { + status: "PASS", + expected: "PASS", + message: "TEST-PASS", + }; + let failureInfo = { + status: "FAIL", + expected: "PASS", + message: "TEST-UNEXPECTED-FAIL", + }; + SimpleTest._logResult(test, successInfo, failureInfo); + SimpleTest._tests.push(test); + }); + } + + SimpleTest._timeoutFunctions = []; + + SimpleTest.testsLength = SimpleTest._tests.length; + + SimpleTest._alreadyFinished = true; + + if (SimpleTest._inChaosMode) { + SpecialPowers.DOMWindowUtils.leaveChaosMode(); + SimpleTest._inChaosMode = false; + } + + var afterCleanup = async function () { + SpecialPowers.removeFiles(); + + if (SpecialPowers.DOMWindowUtils.isTestControllingRefreshes) { + SimpleTest.ok(false, "test left refresh driver under test control"); + SpecialPowers.DOMWindowUtils.restoreNormalRefresh(); + } + if (SimpleTest._expectingUncaughtException) { + SimpleTest.ok( + false, + "expectUncaughtException was called but no uncaught exception was detected!" + ); + } + if (!SimpleTest._tests.length) { + SimpleTest.ok( + false, + "[SimpleTest.finish()] No checks actually run. " + + "(You need to call ok(), is(), or similar " + + "functions at least once. Make sure you use " + + "SimpleTest.waitForExplicitFinish() if you need " + + "it.)" + ); + } + + let workers = await SpecialPowers.registeredServiceWorkers(); + let promise = null; + if (SimpleTest._expectingRegisteredServiceWorker) { + if (workers.length === 0) { + SimpleTest.ok( + false, + "This test is expected to leave a service worker registered" + ); + } + } else if (workers.length) { + let FULL_PROFILE_WORKERS_TO_IGNORE = []; + if (parentRunner.conditionedProfile) { + // Full profile has service workers in the profile, without clearing the + // profile service workers will be leftover. We perform a startsWith + // check below because some origins (s.0cf.io) use a cache-busting query + // parameter. + FULL_PROFILE_WORKERS_TO_IGNORE = [ + "https://www.youtube.com/sw.js", + "https://s.0cf.io/sw.js", + ]; + } else { + SimpleTest.ok( + false, + "This test left a service worker registered without cleaning it up" + ); + } + + for (let worker of workers) { + if ( + FULL_PROFILE_WORKERS_TO_IGNORE.some(ignoreBase => + worker.scriptSpec.startsWith(ignoreBase) + ) + ) { + continue; + } + SimpleTest.ok( + false, + `Left over worker: ${worker.scriptSpec} (scope: ${worker.scope})` + ); + } + promise = SpecialPowers.removeAllServiceWorkerData(); + } + + // If we want to wait for removeAllServiceWorkerData to finish, above, + // there's a small chance that spinning the event loop could cause + // SpecialPowers and SimpleTest to go away (e.g. if the test did + // document.open). promise being non-null should be rare (a test would + // have had to already fail by leaving a service worker around), so + // limit the chances of the async wait happening to that case. + function finish() { + if (parentRunner) { + /* We're running in an iframe, and the parent has a TestRunner */ + parentRunner.testFinished(SimpleTest._tests); + } + + if (!parentRunner || parentRunner.showTestReport) { + SpecialPowers.flushPermissions(function () { + SpecialPowers.flushPrefEnv(function () { + SimpleTest.showReport(); + }); + }); + } + } + + if (promise) { + promise.then(finish); + } else { + finish(); + } + }; + + var executeCleanupFunction = function () { + var func = SimpleTest._cleanupFunctions.pop(); + + if (!func) { + afterCleanup(); + return; + } + + var ret; + try { + ret = func(); + } catch (ex) { + SimpleTest.ok(false, "Cleanup function threw exception: " + ex); + } + + if (ret && ret.constructor.name == "Promise") { + ret.then(executeCleanupFunction, ex => + SimpleTest.ok(false, "Cleanup promise rejected: " + ex) + ); + } else { + executeCleanupFunction(); + } + }; + + executeCleanupFunction(); + + SpecialPowers.notifyObservers(null, "test-complete"); +}; + +/** + * Monitor console output from now until endMonitorConsole is called. + * + * Expect to receive all console messages described by the elements of + * ``msgs``, an array, in the order listed in ``msgs``; each element is an + * object which may have any number of the following properties: + * + * message, errorMessage, sourceName, sourceLine, category: string or regexp + * lineNumber, columnNumber: number + * isScriptError, isWarning: boolean + * + * Strings, numbers, and booleans must compare equal to the named + * property of the Nth console message. Regexps must match. Any + * fields present in the message but not in the pattern object are ignored. + * + * In addition to the above properties, elements in ``msgs`` may have a ``forbid`` + * boolean property. When ``forbid`` is true, a failure is logged each time a + * matching message is received. + * + * If ``forbidUnexpectedMsgs`` is true, then the messages received in the console + * must exactly match the non-forbidden messages in ``msgs``; for each received + * message not described by the next element in ``msgs``, a failure is logged. If + * false, then other non-forbidden messages are ignored, but all expected + * messages must still be received. + * + * After endMonitorConsole is called, ``continuation`` will be called + * asynchronously. (Normally, you will want to pass ``SimpleTest.finish`` here.) + * + * It is incorrect to use this function in a test which has not called + * SimpleTest.waitForExplicitFinish. + */ +SimpleTest.monitorConsole = function ( + continuation, + msgs, + forbidUnexpectedMsgs +) { + if (SimpleTest._stopOnLoad) { + ok(false, "Console monitoring requires use of waitForExplicitFinish."); + } + + function msgMatches(msg, pat) { + for (var k in pat) { + if (!(k in msg)) { + return false; + } + if (pat[k] instanceof RegExp && typeof msg[k] === "string") { + if (!pat[k].test(msg[k])) { + return false; + } + } else if (msg[k] !== pat[k]) { + return false; + } + } + return true; + } + + var forbiddenMsgs = []; + var i = 0; + while (i < msgs.length) { + let pat = msgs[i]; + if ("forbid" in pat) { + var forbid = pat.forbid; + delete pat.forbid; + if (forbid) { + forbiddenMsgs.push(pat); + msgs.splice(i, 1); + continue; + } + } + i++; + } + + var counter = 0; + var assertionLabel = JSON.stringify(msgs); + function listener(msg) { + if (msg.message === "SENTINEL" && !msg.isScriptError) { + is( + counter, + msgs.length, + "monitorConsole | number of messages " + assertionLabel + ); + SimpleTest.executeSoon(continuation); + return; + } + for (let pat of forbiddenMsgs) { + if (msgMatches(msg, pat)) { + ok( + false, + "monitorConsole | observed forbidden message " + JSON.stringify(msg) + ); + return; + } + } + if (counter >= msgs.length) { + var str = "monitorConsole | extra message | " + JSON.stringify(msg); + if (forbidUnexpectedMsgs) { + ok(false, str); + } else { + info(str); + } + return; + } + var matches = msgMatches(msg, msgs[counter]); + if (forbidUnexpectedMsgs) { + ok( + matches, + "monitorConsole | [" + counter + "] must match " + JSON.stringify(msg) + ); + } else { + info( + "monitorConsole | [" + + counter + + "] " + + (matches ? "matched " : "did not match ") + + JSON.stringify(msg) + ); + } + if (matches) { + counter++; + } + } + SpecialPowers.registerConsoleListener(listener); +}; + +/** + * Stop monitoring console output. + */ +SimpleTest.endMonitorConsole = function () { + SpecialPowers.postConsoleSentinel(); +}; + +/** + * Run ``testfn`` synchronously, and monitor its console output. + * + * ``msgs`` is handled as described above for monitorConsole. + * + * After ``testfn`` returns, console monitoring will stop, and ``continuation`` + * will be called asynchronously. + * + */ +SimpleTest.expectConsoleMessages = function (testfn, msgs, continuation) { + SimpleTest.monitorConsole(continuation, msgs); + testfn(); + SimpleTest.executeSoon(SimpleTest.endMonitorConsole); +}; + +/** + * Wrapper around ``expectConsoleMessages`` for the case where the test has + * only one ``testfn`` to run. + */ +SimpleTest.runTestExpectingConsoleMessages = function (testfn, msgs) { + SimpleTest.waitForExplicitFinish(); + SimpleTest.expectConsoleMessages(testfn, msgs, SimpleTest.finish); +}; + +/** + * Indicates to the test framework that the current test expects one or + * more crashes (from plugins or IPC documents), and that the minidumps from + * those crashes should be removed. + */ +SimpleTest.expectChildProcessCrash = function () { + if (parentRunner) { + parentRunner.expectChildProcessCrash(); + } +}; + +/** + * Indicates to the test framework that the next uncaught exception during + * the test is expected, and should not cause a test failure. + */ +SimpleTest.expectUncaughtException = function (aExpecting) { + SimpleTest._expectingUncaughtException = + aExpecting === void 0 || !!aExpecting; +}; + +/** + * Returns whether the test has indicated that it expects an uncaught exception + * to occur. + */ +SimpleTest.isExpectingUncaughtException = function () { + return SimpleTest._expectingUncaughtException; +}; + +/** + * Indicates to the test framework that all of the uncaught exceptions + * during the test are known problems that should be fixed in the future, + * but which should not cause the test to fail currently. + */ +SimpleTest.ignoreAllUncaughtExceptions = function (aIgnoring) { + SimpleTest._ignoringAllUncaughtExceptions = + aIgnoring === void 0 || !!aIgnoring; +}; + +/** + * Returns whether the test has indicated that all uncaught exceptions should be + * ignored. + */ +SimpleTest.isIgnoringAllUncaughtExceptions = function () { + return SimpleTest._ignoringAllUncaughtExceptions; +}; + +/** + * Indicates to the test framework that this test is expected to leave a + * service worker registered when it finishes. + */ +SimpleTest.expectRegisteredServiceWorker = function () { + SimpleTest._expectingRegisteredServiceWorker = true; +}; + +/** + * Resets any state this SimpleTest object has. This is important for + * browser chrome mochitests, which reuse the same SimpleTest object + * across a run. + */ +SimpleTest.reset = function () { + SimpleTest._ignoringAllUncaughtExceptions = false; + SimpleTest._expectingUncaughtException = false; + SimpleTest._expectingRegisteredServiceWorker = false; + SimpleTest._bufferedMessages = []; +}; + +if (isPrimaryTestWindow) { + addLoadEvent(function () { + if (SimpleTest._stopOnLoad) { + SimpleTest.finish(); + } + }); +} + +// --------------- Test.Builder/Test.More isDeeply() ----------------- + +SimpleTest.DNE = { dne: "Does not exist" }; +SimpleTest.LF = "\r\n"; + +SimpleTest._deepCheck = function (e1, e2, stack, seen) { + var ok = false; + if (Object.is(e1, e2)) { + // Handles identical primitives and references. + ok = true; + } else if ( + typeof e1 != "object" || + typeof e2 != "object" || + e1 === null || + e2 === null + ) { + // If either argument is a primitive or function, don't consider the arguments the same. + ok = false; + } else if (e1 == SimpleTest.DNE || e2 == SimpleTest.DNE) { + ok = false; + } else if (SimpleTest.isa(e1, "Array") && SimpleTest.isa(e2, "Array")) { + ok = SimpleTest._eqArray(e1, e2, stack, seen); + } else { + ok = SimpleTest._eqAssoc(e1, e2, stack, seen); + } + return ok; +}; + +SimpleTest._eqArray = function (a1, a2, stack, seen) { + // Return if they're the same object. + if (a1 == a2) { + return true; + } + + // JavaScript objects have no unique identifiers, so we have to store + // references to them all in an array, and then compare the references + // directly. It's slow, but probably won't be much of an issue in + // practice. Start by making a local copy of the array to as to avoid + // confusing a reference seen more than once (such as [a, a]) for a + // circular reference. + for (var j = 0; j < seen.length; j++) { + if (seen[j][0] == a1) { + return seen[j][1] == a2; + } + } + + // If we get here, we haven't seen a1 before, so store it with reference + // to a2. + seen.push([a1, a2]); + + var ok = true; + // Only examines enumerable attributes. Only works for numeric arrays! + // Associative arrays return 0. So call _eqAssoc() for them, instead. + var max = Math.max(a1.length, a2.length); + if (max == 0) { + return SimpleTest._eqAssoc(a1, a2, stack, seen); + } + for (var i = 0; i < max; i++) { + var e1 = i < a1.length ? a1[i] : SimpleTest.DNE; + var e2 = i < a2.length ? a2[i] : SimpleTest.DNE; + stack.push({ type: "Array", idx: i, vals: [e1, e2] }); + ok = SimpleTest._deepCheck(e1, e2, stack, seen); + if (ok) { + stack.pop(); + } else { + break; + } + } + return ok; +}; + +SimpleTest._eqAssoc = function (o1, o2, stack, seen) { + // Return if they're the same object. + if (o1 == o2) { + return true; + } + + // JavaScript objects have no unique identifiers, so we have to store + // references to them all in an array, and then compare the references + // directly. It's slow, but probably won't be much of an issue in + // practice. Start by making a local copy of the array to as to avoid + // confusing a reference seen more than once (such as [a, a]) for a + // circular reference. + seen = seen.slice(0); + for (let j = 0; j < seen.length; j++) { + if (seen[j][0] == o1) { + return seen[j][1] == o2; + } + } + + // If we get here, we haven't seen o1 before, so store it with reference + // to o2. + seen.push([o1, o2]); + + // They should be of the same class. + + var ok = true; + // Only examines enumerable attributes. + var o1Size = 0; + // eslint-disable-next-line no-unused-vars + for (let i in o1) { + o1Size++; + } + var o2Size = 0; + // eslint-disable-next-line no-unused-vars + for (let i in o2) { + o2Size++; + } + var bigger = o1Size > o2Size ? o1 : o2; + for (let i in bigger) { + var e1 = i in o1 ? o1[i] : SimpleTest.DNE; + var e2 = i in o2 ? o2[i] : SimpleTest.DNE; + stack.push({ type: "Object", idx: i, vals: [e1, e2] }); + ok = SimpleTest._deepCheck(e1, e2, stack, seen); + if (ok) { + stack.pop(); + } else { + break; + } + } + return ok; +}; + +SimpleTest._formatStack = function (stack) { + var variable = "$Foo"; + for (let i = 0; i < stack.length; i++) { + var entry = stack[i]; + var type = entry.type; + var idx = entry.idx; + if (idx != null) { + if (type == "Array") { + // Numeric array index. + variable += "[" + idx + "]"; + } else { + // Associative array index. + idx = idx.replace("'", "\\'"); + variable += "['" + idx + "']"; + } + } + } + + var vals = stack[stack.length - 1].vals.slice(0, 2); + var vars = [ + variable.replace("$Foo", "got"), + variable.replace("$Foo", "expected"), + ]; + + var out = "Structures begin differing at:" + SimpleTest.LF; + for (let i = 0; i < vals.length; i++) { + var val = vals[i]; + if (val === SimpleTest.DNE) { + val = "Does not exist"; + } else { + val = repr(val); + } + out += vars[i] + " = " + val + SimpleTest.LF; + } + + return " " + out; +}; + +SimpleTest.isDeeply = function (it, as, name) { + var stack = [{ vals: [it, as] }]; + var seen = []; + if (SimpleTest._deepCheck(it, as, stack, seen)) { + SimpleTest.record(true, name); + } else { + SimpleTest.record(false, name, SimpleTest._formatStack(stack)); + } +}; + +SimpleTest.typeOf = function (object) { + var c = Object.prototype.toString.apply(object); + var name = c.substring(8, c.length - 1); + if (name != "Object") { + return name; + } + // It may be a non-core class. Try to extract the class name from + // the constructor function. This may not work in all implementations. + if (/function ([^(\s]+)/.test(Function.toString.call(object.constructor))) { + return RegExp.$1; + } + // No idea. :-( + return name; +}; + +SimpleTest.isa = function (object, clas) { + return SimpleTest.typeOf(object) == clas; +}; + +// Global symbols: +var ok = SimpleTest.ok; +var record = SimpleTest.record; +var is = SimpleTest.is; +var isfuzzy = SimpleTest.isfuzzy; +var isnot = SimpleTest.isnot; +var todo = SimpleTest.todo; +var todo_is = SimpleTest.todo_is; +var todo_isnot = SimpleTest.todo_isnot; +var isDeeply = SimpleTest.isDeeply; +var info = SimpleTest.info; + +var gOldOnError = window.onerror; +window.onerror = function simpletestOnerror( + errorMsg, + url, + lineNumber, + columnNumber, + originalException +) { + // Log the message. + // XXX Chrome mochitests sometimes trigger this window.onerror handler, + // but there are a number of uncaught JS exceptions from those tests. + // For now, for tests that self identify as having unintentional uncaught + // exceptions, just dump it so that the error is visible but doesn't cause + // a test failure. See bug 652494. + var isExpected = !!SimpleTest._expectingUncaughtException; + var message = (isExpected ? "expected " : "") + "uncaught exception"; + var error = errorMsg + " at "; + try { + error += originalException.stack; + } catch (e) { + // At least use the url+line+column we were given + error += url + ":" + lineNumber + ":" + columnNumber; + } + if (!SimpleTest._ignoringAllUncaughtExceptions) { + // Don't log if SimpleTest.finish() is already called, it would cause failures + if (!SimpleTest._alreadyFinished) { + SimpleTest.record(isExpected, message, error); + } + SimpleTest._expectingUncaughtException = false; + } else { + SimpleTest.todo(false, message + ": " + error); + } + // There is no Components.stack.caller to log. (See bug 511888.) + + // Call previous handler. + if (gOldOnError) { + try { + // Ignore return value: always run default handler. + gOldOnError(errorMsg, url, lineNumber); + } catch (e) { + // Log the error. + SimpleTest.info("Exception thrown by gOldOnError(): " + e); + // Log its stack. + if (e.stack) { + SimpleTest.info("JavaScript error stack:\n" + e.stack); + } + } + } + + if (!SimpleTest._stopOnLoad && !isExpected && !SimpleTest._alreadyFinished) { + // Need to finish() manually here, yet let the test actually end first. + SimpleTest.executeSoon(SimpleTest.finish); + } +}; + +// Lifted from dom/media/test/manifest.js +// Make sure to not touch navigator in here, since we want to push prefs that +// will affect the APIs it exposes, but the set of exposed APIs is determined +// when Navigator.prototype is created. So if we touch navigator before pushing +// the prefs, the APIs it exposes will not take those prefs into account. We +// work around this by using a navigator object from a different global for our +// UA string testing. +var gAndroidSdk = null; +function getAndroidSdk() { + if (gAndroidSdk === null) { + var iframe = document.documentElement.appendChild( + document.createElement("iframe") + ); + iframe.style.display = "none"; + var nav = iframe.contentWindow.navigator; + if ( + !nav.userAgent.includes("Mobile") && + !nav.userAgent.includes("Tablet") + ) { + gAndroidSdk = -1; + } else { + // See nsSystemInfo.cpp, the getProperty('version') returns different value + // on each platforms, so we need to distinguish the android platform. + var versionString = nav.userAgent.includes("Android") + ? "version" + : "sdk_version"; + gAndroidSdk = SpecialPowers.Services.sysinfo.getProperty(versionString); + } + document.documentElement.removeChild(iframe); + } + return gAndroidSdk; +} + +// add_task(generatorFunction): +// Call `add_task(generatorFunction)` for each separate +// asynchronous task in a mochitest. Tasks are run consecutively. +// Before the first task, `SimpleTest.waitForExplicitFinish()` +// will be called automatically, and after the last task, +// `SimpleTest.finish()` will be called. +var add_task = (function () { + // The list of tasks to run. + var task_list = []; + var run_only_this_task = null; + + function isGenerator(value) { + return ( + value && typeof value === "object" && typeof value.next === "function" + ); + } + + // The "add_task" function + return function (generatorFunction, options = { isSetup: false }) { + if (task_list.length === 0) { + // This is the first time add_task has been called. + // First, confirm that SimpleTest is available. + if (!SimpleTest) { + throw new Error("SimpleTest not available."); + } + // Don't stop tests until asynchronous tasks are finished. + SimpleTest.waitForExplicitFinish(); + // Because the client is using add_task for this set of tests, + // we need to spawn a "master task" that calls each task in succesion. + // Use setTimeout to ensure the master task runs after the client + // script finishes. + setTimeout(function nextTick() { + // If we are in a HTML document, we should wait for the document + // to be fully loaded. + // These checks ensure that we are in an HTML document without + // throwing TypeError; also I am told that readyState in XUL documents + // are totally bogus so we don't try to do this there. + if ( + typeof window !== "undefined" && + typeof HTMLDocument !== "undefined" && + window.document instanceof HTMLDocument && + window.document.readyState !== "complete" + ) { + setTimeout(nextTick); + return; + } + + (async () => { + // Allow for a task to be skipped; we need only use the structured logger + // for this, whilst deactivating log buffering to ensure that messages + // are always printed to stdout. + function skipTask(name) { + let logger = parentRunner && parentRunner.structuredLogger; + if (!logger) { + info("add_task | Skipping test " + name); + return; + } + logger.deactivateBuffering(); + logger.testStatus(SimpleTest._getCurrentTestURL(), name, "SKIP"); + logger.warning("add_task | Skipping test " + name); + logger.activateBuffering(); + } + + // We stop the entire test file at the first exception because this + // may mean that the state of subsequent tests may be corrupt. + try { + for (var task of task_list) { + SimpleTest._currentTask = task; + var name = task.name || ""; + if ( + task.__skipMe || + (run_only_this_task && task != run_only_this_task) + ) { + skipTask(name); + continue; + } + const taskInfo = action => + info( + `${ + task.isSetup ? "add_setup" : "add_task" + } | ${action} ${name}` + ); + taskInfo("Entering"); + let result = await task(); + if (isGenerator(result)) { + ok(false, "Task returned a generator"); + } + if (task._cleanupFunctions) { + for (const fn of task._cleanupFunctions) { + await fn(); + } + } + for (const fn of SimpleTest._taskCleanupFunctions) { + await fn(); + } + taskInfo("Leaving"); + delete SimpleTest._currentTask; + } + } catch (ex) { + try { + let serializedEx; + if ( + typeof ex == "string" || + (typeof ex == "object" && isErrorOrException(ex)) + ) { + serializedEx = `${ex}`; + } else { + serializedEx = JSON.stringify(ex); + } + + SimpleTest.record( + false, + serializedEx, + "Should not throw any errors", + ex.stack + ); + } catch (ex2) { + SimpleTest.record( + false, + "(The exception cannot be converted to string.)", + "Should not throw any errors", + ex.stack + ); + } + } + // All tasks are finished. + SimpleTest.finish(); + })(); + }); + } + generatorFunction.skip = () => (generatorFunction.__skipMe = true); + generatorFunction.only = () => (run_only_this_task = generatorFunction); + // Add the task to the list of tasks to run after + // the main thread is finished. + if (options.isSetup) { + generatorFunction.isSetup = true; + let lastSetupIndex = task_list.findLastIndex(t => t.isSetup) + 1; + task_list.splice(lastSetupIndex, 0, generatorFunction); + } else { + task_list.push(generatorFunction); + } + return generatorFunction; + }; +})(); + +// Like add_task, but setup tasks are executed first. +function add_setup(generatorFunction) { + return add_task(generatorFunction, { isSetup: true }); +} + +// Request complete log when using failure patterns so that failure info +// from infra can be useful. +if (usesFailurePatterns()) { + SimpleTest.requestCompleteLog(); +} + +addEventListener("message", async event => { + if (event.data == "SimpleTest:timeout") { + await SimpleTest.timeout(); + SimpleTest.finish(); + } +}); diff --git a/testing/mochitest/tests/SimpleTest/TestRunner.js b/testing/mochitest/tests/SimpleTest/TestRunner.js new file mode 100644 index 0000000000..5f305a176b --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/TestRunner.js @@ -0,0 +1,1168 @@ +/* -*- js-indent-level: 4; indent-tabs-mode: nil -*- */ +/* + * e10s event dispatcher from content->chrome + * + * type = eventName (QuitApplication) + * data = json object {"filename":filename} <- for LoggerInit + */ + +// This file expects the following files to be loaded. +/* import-globals-from LogController.js */ +/* import-globals-from MemoryStats.js */ +/* import-globals-from MozillaLogger.js */ + +/* eslint-disable no-unsanitized/property */ + +"use strict"; + +const { StructuredLogger, StructuredFormatter } = + SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/StructuredLog.sys.mjs" + ); + +function getElement(id) { + return typeof id == "string" ? document.getElementById(id) : id; +} + +this.$ = this.getElement; + +function contentDispatchEvent(type, data, sync) { + if (typeof data == "undefined") { + data = {}; + } + + var event = new CustomEvent("contentEvent", { + bubbles: true, + detail: { + sync, + type, + data: JSON.stringify(data), + }, + }); + document.dispatchEvent(event); +} + +function contentAsyncEvent(type, data) { + contentDispatchEvent(type, data, 0); +} + +/* Helper Function */ +function extend(obj, /* optional */ skip) { + // Extend an array with an array-like object starting + // from the skip index + if (!skip) { + skip = 0; + } + if (obj) { + var l = obj.length; + var ret = []; + for (var i = skip; i < l; i++) { + ret.push(obj[i]); + } + } + return ret; +} + +function flattenArguments(lst /* ...*/) { + var res = []; + var args = extend(arguments); + while (args.length) { + var o = args.shift(); + if (o && typeof o == "object" && typeof o.length == "number") { + for (var i = o.length - 1; i >= 0; i--) { + args.unshift(o[i]); + } + } else { + res.push(o); + } + } + return res; +} + +function testInXOriginFrame() { + // Check if the test running in an iframe is a cross origin test. + try { + $("testframe").contentWindow.origin; + return false; + } catch (e) { + return true; + } +} + +function testInDifferentProcess() { + // Check if the test running in an iframe that is loaded in a different process. + return SpecialPowers.Cu.isRemoteProxy($("testframe").contentWindow); +} + +/** + * TestRunner: A test runner for SimpleTest + * TODO: + * + * * Avoid moving iframes: That causes reloads on mozilla and opera. + * + * + **/ +var TestRunner = {}; +TestRunner.logEnabled = false; +TestRunner._currentTest = 0; +TestRunner._lastTestFinished = -1; +TestRunner._loopIsRestarting = false; +TestRunner.currentTestURL = ""; +TestRunner.originalTestURL = ""; +TestRunner._urls = []; +TestRunner._lastAssertionCount = 0; +TestRunner._expectedMinAsserts = 0; +TestRunner._expectedMaxAsserts = 0; + +TestRunner.timeout = 300 * 1000; // 5 minutes. +TestRunner.maxTimeouts = 4; // halt testing after too many timeouts +TestRunner.runSlower = false; +TestRunner.dumpOutputDirectory = ""; +TestRunner.dumpAboutMemoryAfterTest = false; +TestRunner.dumpDMDAfterTest = false; +TestRunner.slowestTestTime = 0; +TestRunner.slowestTestURL = ""; +TestRunner.interactiveDebugger = false; +TestRunner.cleanupCrashes = false; +TestRunner.timeoutAsPass = false; +TestRunner.conditionedProfile = false; +TestRunner.comparePrefs = false; + +TestRunner._expectingProcessCrash = false; +TestRunner._structuredFormatter = new StructuredFormatter(); + +/** + * Make sure the tests don't hang indefinitely. + **/ +TestRunner._numTimeouts = 0; +TestRunner._currentTestStartTime = new Date().valueOf(); +TestRunner._timeoutFactor = 1; + +/** + * Used to collect code coverage with the js debugger. + */ +TestRunner.jscovDirPrefix = ""; +var coverageCollector = {}; + +function record(succeeded, expectedFail, msg) { + let successInfo; + let failureInfo; + if (expectedFail) { + successInfo = { + status: "PASS", + expected: "FAIL", + message: "TEST-UNEXPECTED-PASS", + }; + failureInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + } else { + successInfo = { + status: "PASS", + expected: "PASS", + message: "TEST-PASS", + }; + failureInfo = { + status: "FAIL", + expected: "PASS", + message: "TEST-UNEXPECTED-FAIL", + }; + } + + let result = succeeded ? successInfo : failureInfo; + + TestRunner.structuredLogger.testStatus( + TestRunner.currentTestURL, + msg, + result.status, + result.expected, + "", + "" + ); +} + +TestRunner._checkForHangs = function () { + function reportError(win, msg) { + if (testInXOriginFrame() || "SimpleTest" in win) { + record(false, TestRunner.timeoutAsPass, msg); + } else if ("W3CTest" in win) { + win.W3CTest.logFailure(msg); + } + } + + async function killTest(win) { + if (testInXOriginFrame()) { + win.postMessage("SimpleTest:timeout", "*"); + } else if ("SimpleTest" in win) { + await win.SimpleTest.timeout(); + win.SimpleTest.finish(); + } else if ("W3CTest" in win) { + await win.W3CTest.timeout(); + } + } + + if (TestRunner._currentTest < TestRunner._urls.length) { + var runtime = new Date().valueOf() - TestRunner._currentTestStartTime; + if ( + !TestRunner.interactiveDebugger && + runtime >= TestRunner.timeout * TestRunner._timeoutFactor + ) { + let testIframe = $("testframe"); + var frameWindow = + (!testInXOriginFrame() && testIframe.contentWindow.wrappedJSObject) || + testIframe.contentWindow; + reportError(frameWindow, "Test timed out."); + TestRunner.updateUI([{ result: false }]); + + // If we have too many timeouts, give up. We don't want to wait hours + // for results if some bug causes lots of tests to time out. + if ( + ++TestRunner._numTimeouts >= TestRunner.maxTimeouts || + TestRunner.runUntilFailure + ) { + TestRunner._haltTests = true; + + TestRunner.currentTestURL = "(SimpleTest/TestRunner.js)"; + reportError( + frameWindow, + TestRunner.maxTimeouts + " test timeouts, giving up." + ); + var skippedTests = TestRunner._urls.length - TestRunner._currentTest; + reportError( + frameWindow, + "Skipping " + skippedTests + " remaining tests." + ); + } + + // Add a little (1 second) delay to ensure automation.py has time to notice + // "Test timed out" log and process it (= take a screenshot). + setTimeout(async function delayedKillTest() { + try { + await killTest(frameWindow); + } catch (e) { + reportError(frameWindow, "Test error: " + e); + TestRunner.updateUI([{ result: false }]); + } + }, 1000); + + if (TestRunner._haltTests) { + return; + } + } + + setTimeout(TestRunner._checkForHangs, 30000); + } +}; + +TestRunner.requestLongerTimeout = function (factor) { + TestRunner._timeoutFactor = factor; +}; + +/** + * This is used to loop tests + **/ +TestRunner.repeat = 0; +TestRunner._currentLoop = 1; + +TestRunner.expectAssertions = function (min, max) { + if (typeof max == "undefined") { + max = min; + } + if ( + typeof min != "number" || + typeof max != "number" || + min < 0 || + max < min + ) { + throw new Error("bad parameter to expectAssertions"); + } + TestRunner._expectedMinAsserts = min; + TestRunner._expectedMaxAsserts = max; +}; + +/** + * This function is called after generating the summary. + **/ +TestRunner.onComplete = null; + +/** + * Adds a failed test case to a list so we can rerun only the failed tests + **/ +TestRunner._failedTests = {}; +TestRunner._failureFile = ""; + +TestRunner.addFailedTest = function (testName) { + if (TestRunner._failedTests[testName] == undefined) { + TestRunner._failedTests[testName] = ""; + } +}; + +TestRunner.setFailureFile = function (fileName) { + TestRunner._failureFile = fileName; +}; + +TestRunner.generateFailureList = function () { + if (TestRunner._failureFile) { + var failures = new MozillaFileLogger(TestRunner._failureFile); + failures.log(JSON.stringify(TestRunner._failedTests)); + failures.close(); + } +}; + +/** + * If logEnabled is true, this is the logger that will be used. + **/ + +// This delimiter is used to avoid interleaving Mochitest/Gecko logs. +var LOG_DELIMITER = "\ue175\uee31\u2c32\uacbf"; + +// A log callback for StructuredLog.sys.mjs +TestRunner._dumpMessage = function (message) { + var str; + + // This is a directive to python to format these messages + // for compatibility with mozharness. This can be removed + // with the MochitestFormatter (see bug 1045525). + message.js_source = "TestRunner.js"; + if ( + TestRunner.interactiveDebugger && + message.action in TestRunner._structuredFormatter + ) { + str = TestRunner._structuredFormatter[message.action](message); + } else { + str = LOG_DELIMITER + JSON.stringify(message) + LOG_DELIMITER; + } + // BUGFIX: browser-chrome tests don't use LogController + if (Object.keys(LogController.listeners).length !== 0) { + LogController.log(str); + } else { + dump("\n" + str + "\n"); + } + // Checking for error messages + if (message.expected || message.level === "ERROR") { + TestRunner.failureHandler(); + } +}; + +// From https://searchfox.org/mozilla-central/source/testing/modules/StructuredLog.sys.mjs +TestRunner.structuredLogger = new StructuredLogger( + "mochitest", + TestRunner._dumpMessage, + [], + TestRunner +); +TestRunner.structuredLogger.deactivateBuffering = function () { + TestRunner.structuredLogger.logData("buffering_off"); +}; +TestRunner.structuredLogger.activateBuffering = function () { + TestRunner.structuredLogger.logData("buffering_on"); +}; + +TestRunner.log = function (msg) { + if (TestRunner.logEnabled) { + TestRunner.structuredLogger.info(msg); + } else { + dump(msg + "\n"); + } +}; + +TestRunner.error = function (msg) { + if (TestRunner.logEnabled) { + TestRunner.structuredLogger.error(msg); + } else { + dump(msg + "\n"); + TestRunner.failureHandler(); + } +}; + +TestRunner.failureHandler = function () { + if (TestRunner.runUntilFailure) { + TestRunner._haltTests = true; + } + + if (TestRunner.debugOnFailure) { + // You've hit this line because you requested to break into the + // debugger upon a testcase failure on your test run. + // eslint-disable-next-line no-debugger + debugger; + } +}; + +/** + * Toggle element visibility + **/ +TestRunner._toggle = function (el) { + if (el.className == "noshow") { + el.className = ""; + el.style.cssText = ""; + } else { + el.className = "noshow"; + el.style.cssText = "width:0px; height:0px; border:0px;"; + } +}; + +/** + * Creates the iframe that contains a test + **/ +TestRunner._makeIframe = function (url, retry) { + var iframe = $("testframe"); + if ( + url != "about:blank" && + (("hasFocus" in document && !document.hasFocus()) || + ("activeElement" in document && document.activeElement != iframe)) + ) { + contentAsyncEvent("Focus"); + window.focus(); + SpecialPowers.focus(); + iframe.focus(); + if (retry < 3) { + window.setTimeout(function () { + TestRunner._makeIframe(url, retry + 1); + }, 1000); + return; + } + + TestRunner.structuredLogger.info( + "Error: Unable to restore focus, expect failures and timeouts." + ); + } + window.scrollTo(0, $("indicator").offsetTop); + try { + let urlObj = new URL(url); + if (TestRunner.xOriginTests) { + // The test will run in a xorigin iframe, so we pass in additional test params in the + // URL since the content process won't be able to access them from the parentRunner + // directly. + let params = TestRunner.getParameterInfo(); + urlObj.searchParams.append( + "currentTestURL", + urlObj.pathname.replace("/tests/", "") + ); + urlObj.searchParams.append("closeWhenDone", params.closeWhenDone); + urlObj.searchParams.append("showTestReport", TestRunner.showTestReport); + urlObj.searchParams.append("expected", TestRunner.expected); + iframe.src = urlObj.href; + } else { + iframe.src = url; + } + } catch { + // If the provided `url` is not a valid URL (i.e. doesn't include a protocol) + // then the new URL() constructor will raise a TypeError. This is expected in the + // usual case (i.e. non-xorigin iFrame tests) so set the URL in the usual way. + iframe.src = url; + } + iframe.name = url; + iframe.width = "500"; +}; + +/** + * Returns the current test URL. + * We use this to tell whether the test has navigated to another test without + * being finished first. + */ +TestRunner.getLoadedTestURL = function () { + if (!testInXOriginFrame()) { + var prefix = ""; + // handle mochitest-chrome URIs + if ($("testframe").contentWindow.location.protocol == "chrome:") { + prefix = "chrome://mochitests"; + } + return prefix + $("testframe").contentWindow.location.pathname; + } + return TestRunner.currentTestURL; +}; + +TestRunner.setParameterInfo = function (params) { + this._params = params; +}; + +TestRunner.getParameterInfo = function () { + return this._params; +}; + +/** + * Print information about which prefs are set. + * This is used to help validate that the tests are actually + * running in the expected context. + */ +TestRunner.dumpPrefContext = function () { + let prefs = ["fission.autostart"]; + + let message = ["Dumping test context:"]; + prefs.forEach(function formatPref(pref) { + let val = SpecialPowers.getBoolPref(pref); + message.push(pref + "=" + val); + }); + TestRunner.structuredLogger.info(message.join("\n ")); +}; + +/** + * TestRunner entry point. + * + * The arguments are the URLs of the test to be ran. + * + **/ +TestRunner.runTests = function (/*url...*/) { + TestRunner.structuredLogger.info("SimpleTest START"); + TestRunner.dumpPrefContext(); + TestRunner.originalTestURL = $("current-test").innerHTML; + + SpecialPowers.registerProcessCrashObservers(); + + // Initialize code coverage + if (TestRunner.jscovDirPrefix != "") { + var { CoverageCollector } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/CoverageUtils.sys.mjs" + ); + coverageCollector = new CoverageCollector(TestRunner.jscovDirPrefix); + } + + SpecialPowers.requestResetCoverageCounters().then(() => { + TestRunner._urls = flattenArguments(arguments); + + var singleTestRun = this._urls.length <= 1 && TestRunner.repeat <= 1; + TestRunner.showTestReport = singleTestRun; + var frame = $("testframe"); + frame.src = ""; + if (singleTestRun) { + // Can't use document.body because this runs in a XUL doc as well... + var body = document.getElementsByTagName("body")[0]; + body.setAttribute("singletest", "true"); + frame.removeAttribute("scrolling"); + } + TestRunner._checkForHangs(); + TestRunner.runNextTest(); + }); +}; + +/** + * Used for running a set of tests in a loop for debugging purposes + * Takes an array of URLs + **/ +TestRunner.resetTests = function (listURLs) { + TestRunner._currentTest = 0; + // Reset our "Current-test" line - functionality depends on it + $("current-test").innerHTML = TestRunner.originalTestURL; + if (TestRunner.logEnabled) { + TestRunner.structuredLogger.info( + "SimpleTest START Loop " + TestRunner._currentLoop + ); + } + + TestRunner._urls = listURLs; + $("testframe").src = ""; + TestRunner._checkForHangs(); + TestRunner.runNextTest(); +}; + +TestRunner.getNextUrl = function () { + var url = ""; + // sometimes we have a subtest/harness which doesn't use a manifest + if ( + TestRunner._urls[TestRunner._currentTest] instanceof Object && + "test" in TestRunner._urls[TestRunner._currentTest] + ) { + url = TestRunner._urls[TestRunner._currentTest].test.url; + TestRunner.expected = + TestRunner._urls[TestRunner._currentTest].test.expected; + } else { + url = TestRunner._urls[TestRunner._currentTest]; + TestRunner.expected = "pass"; + } + return url; +}; + +/** + * Run the next test. If no test remains, calls onComplete(). + **/ +TestRunner._haltTests = false; +async function _runNextTest() { + if ( + TestRunner._currentTest < TestRunner._urls.length && + !TestRunner._haltTests + ) { + var url = TestRunner.getNextUrl(); + TestRunner.currentTestURL = url; + + $("current-test-path").innerHTML = url; + + TestRunner._currentTestStartTimestamp = SpecialPowers.Cu.now(); + TestRunner._currentTestStartTime = new Date().valueOf(); + TestRunner._timeoutFactor = 1; + TestRunner._expectedMinAsserts = 0; + TestRunner._expectedMaxAsserts = 0; + + TestRunner.structuredLogger.testStart(url); + + if (TestRunner._urls[TestRunner._currentTest].test.allow_xul_xbl) { + await SpecialPowers.pushPermissions([ + { type: "allowXULXBL", allow: true, context: "http://mochi.test:8888" }, + { type: "allowXULXBL", allow: true, context: "http://example.org" }, + ]); + } + TestRunner._makeIframe(url, 0); + } else { + $("current-test").innerHTML = "<b>Finished</b>"; + // Only unload the last test to run if we're running more than one test. + if (TestRunner._urls.length > 1) { + TestRunner._makeIframe("about:blank", 0); + } + + var passCount = parseInt($("pass-count").innerHTML, 10); + var failCount = parseInt($("fail-count").innerHTML, 10); + var todoCount = parseInt($("todo-count").innerHTML, 10); + + if (passCount === 0 && failCount === 0 && todoCount === 0) { + // No |$('testframe').contentWindow|, so manually update: ... + // ... the log, + TestRunner.structuredLogger.error( + "TEST-UNEXPECTED-FAIL | SimpleTest/TestRunner.js | No checks actually run" + ); + // ... the count, + $("fail-count").innerHTML = 1; + // ... the indicator. + var indicator = $("indicator"); + indicator.innerHTML = "Status: Fail (No checks actually run)"; + indicator.style.backgroundColor = "red"; + } + + let e10sMode = SpecialPowers.isMainProcess() ? "non-e10s" : "e10s"; + + TestRunner.structuredLogger.info("TEST-START | Shutdown"); + TestRunner.structuredLogger.info("Passed: " + passCount); + TestRunner.structuredLogger.info("Failed: " + failCount); + TestRunner.structuredLogger.info("Todo: " + todoCount); + TestRunner.structuredLogger.info("Mode: " + e10sMode); + TestRunner.structuredLogger.info( + "Slowest: " + + TestRunner.slowestTestTime + + "ms - " + + TestRunner.slowestTestURL + ); + + // If we are looping, don't send this cause it closes the log file, + // also don't unregister the crash observers until we're done. + if (TestRunner.repeat === 0) { + SpecialPowers.unregisterProcessCrashObservers(); + TestRunner.structuredLogger.info("SimpleTest FINISHED"); + } + + if (TestRunner.repeat === 0 && TestRunner.onComplete) { + TestRunner.onComplete(); + } + + if ( + TestRunner._currentLoop <= TestRunner.repeat && + !TestRunner._haltTests + ) { + TestRunner._currentLoop++; + TestRunner.resetTests(TestRunner._urls); + TestRunner._loopIsRestarting = true; + } else { + // Loops are finished + if (TestRunner.logEnabled) { + TestRunner.structuredLogger.info( + "TEST-INFO | Ran " + TestRunner._currentLoop + " Loops" + ); + TestRunner.structuredLogger.info("SimpleTest FINISHED"); + } + + if (TestRunner.onComplete) { + TestRunner.onComplete(); + } + } + TestRunner.generateFailureList(); + + if (TestRunner.jscovDirPrefix != "") { + coverageCollector.finalize(); + } + } +} +TestRunner.runNextTest = _runNextTest; + +TestRunner.expectChildProcessCrash = function () { + TestRunner._expectingProcessCrash = true; +}; + +/** + * This stub is called by SimpleTest when a test is finished. + **/ +TestRunner.testFinished = function (tests) { + // Need to track subtests recorded here separately or else they'll + // trigger the `result after SimpleTest.finish()` error. + var extraTests = []; + var result = "OK"; + + // Prevent a test from calling finish() multiple times before we + // have a chance to unload it. + if ( + TestRunner._currentTest == TestRunner._lastTestFinished && + !TestRunner._loopIsRestarting + ) { + TestRunner.structuredLogger.testEnd( + TestRunner.currentTestURL, + "ERROR", + "OK", + "called finish() multiple times" + ); + TestRunner.updateUI([{ result: false }]); + return; + } + + if (TestRunner.jscovDirPrefix != "") { + coverageCollector.recordTestCoverage(TestRunner.currentTestURL); + } + + SpecialPowers.requestDumpCoverageCounters().then(() => { + TestRunner._lastTestFinished = TestRunner._currentTest; + TestRunner._loopIsRestarting = false; + + // TODO : replace this by a function that returns the mem data as an object + // that's dumped later with the test_end message + MemoryStats.dump( + TestRunner._currentTest, + TestRunner.currentTestURL, + TestRunner.dumpOutputDirectory, + TestRunner.dumpAboutMemoryAfterTest, + TestRunner.dumpDMDAfterTest + ); + + async function cleanUpCrashDumpFiles() { + if ( + !(await SpecialPowers.removeExpectedCrashDumpFiles( + TestRunner._expectingProcessCrash + )) + ) { + let subtest = "expected-crash-dump-missing"; + TestRunner.structuredLogger.testStatus( + TestRunner.currentTestURL, + subtest, + "ERROR", + "PASS", + "This test did not leave any crash dumps behind, but we were expecting some!" + ); + extraTests.push({ name: subtest, result: false }); + result = "ERROR"; + } + + var unexpectedCrashDumpFiles = + await SpecialPowers.findUnexpectedCrashDumpFiles(); + TestRunner._expectingProcessCrash = false; + if (unexpectedCrashDumpFiles.length) { + let subtest = "unexpected-crash-dump-found"; + TestRunner.structuredLogger.testStatus( + TestRunner.currentTestURL, + subtest, + "ERROR", + "PASS", + "This test left crash dumps behind, but we " + + "weren't expecting it to!", + null, + { unexpected_crashdump_files: unexpectedCrashDumpFiles } + ); + extraTests.push({ name: subtest, result: false }); + result = "CRASH"; + unexpectedCrashDumpFiles.sort().forEach(function (aFilename) { + TestRunner.structuredLogger.info( + "Found unexpected crash dump file " + aFilename + "." + ); + }); + } + + if (TestRunner.cleanupCrashes) { + if (await SpecialPowers.removePendingCrashDumpFiles()) { + TestRunner.structuredLogger.info( + "This test left pending crash dumps" + ); + } + } + } + + function runNextTest() { + if (TestRunner.currentTestURL != TestRunner.getLoadedTestURL()) { + TestRunner.structuredLogger.testStatus( + TestRunner.currentTestURL, + TestRunner.getLoadedTestURL(), + "FAIL", + "PASS", + "finished in a non-clean fashion, probably" + + " because it didn't call SimpleTest.finish()", + { loaded_test_url: TestRunner.getLoadedTestURL() } + ); + extraTests.push({ name: "clean-finish", result: false }); + result = result != "CRASH" ? "ERROR" : result; + } + + SpecialPowers.addProfilerMarker( + "TestRunner", + { category: "Test", startTime: TestRunner._currentTestStartTimestamp }, + TestRunner.currentTestURL + ); + var runtime = new Date().valueOf() - TestRunner._currentTestStartTime; + + if ( + TestRunner.slowestTestTime < runtime && + TestRunner._timeoutFactor >= 1 + ) { + TestRunner.slowestTestTime = runtime; + TestRunner.slowestTestURL = TestRunner.currentTestURL; + } + + TestRunner.updateUI(tests.concat(extraTests)); + + // Don't show the interstitial if we just run one test with no repeats: + if (TestRunner._urls.length == 1 && TestRunner.repeat <= 1) { + TestRunner.testUnloaded(result, runtime); + return; + } + + var interstitialURL; + if ( + !testInXOriginFrame() && + $("testframe").contentWindow.location.protocol == "chrome:" + ) { + interstitialURL = + "tests/SimpleTest/iframe-between-tests.html?result=" + + result + + "&runtime=" + + runtime; + } else { + interstitialURL = + "/tests/SimpleTest/iframe-between-tests.html?result=" + + result + + "&runtime=" + + runtime; + } + // check if there were test run after SimpleTest.finish, which should never happen + if (!testInXOriginFrame()) { + $("testframe").contentWindow.addEventListener("unload", function () { + var testwin = $("testframe").contentWindow; + if (testwin.SimpleTest) { + if (typeof testwin.SimpleTest.testsLength === "undefined") { + TestRunner.structuredLogger.error( + "TEST-UNEXPECTED-FAIL | " + + TestRunner.currentTestURL + + " fired an unload callback with missing test data," + + " possibly due to the test navigating or reloading" + ); + TestRunner.updateUI([{ result: false }]); + } else if ( + testwin.SimpleTest._tests.length != testwin.SimpleTest.testsLength + ) { + var didReportError = false; + var wrongtestlength = + testwin.SimpleTest._tests.length - + testwin.SimpleTest.testsLength; + var wrongtestname = ""; + for (var i = 0; i < wrongtestlength; i++) { + wrongtestname = + testwin.SimpleTest._tests[testwin.SimpleTest.testsLength + i] + .name; + TestRunner.structuredLogger.error( + "TEST-UNEXPECTED-FAIL | " + + TestRunner.currentTestURL + + " logged result after SimpleTest.finish(): " + + wrongtestname + ); + didReportError = true; + } + if (!didReportError) { + // This clause shouldn't be reachable, but if we somehow get + // here (e.g. if wrongtestlength is somehow negative), it's + // important that we log *something* for the { result: false } + // test-failure that we're about to post. + TestRunner.structuredLogger.error( + "TEST-UNEXPECTED-FAIL | " + + TestRunner.currentTestURL + + " hit an unexpected condition when checking for" + + " logged results after SimpleTest.finish()" + ); + } + TestRunner.updateUI([{ result: false }]); + } + } + }); + } + TestRunner._makeIframe(interstitialURL, 0); + } + + SpecialPowers.executeAfterFlushingMessageQueue(async function () { + await SpecialPowers.waitForCrashes(TestRunner._expectingProcessCrash); + await cleanUpCrashDumpFiles(); + await SpecialPowers.flushPermissions(); + await SpecialPowers.flushPrefEnv(); + runNextTest(); + }); + }); +}; + +/** + * This stub is called by XOrigin Tests to report assertion count. + **/ +TestRunner._xoriginAssertionCount = 0; +TestRunner.addAssertionCount = function (count) { + if (!testInXOriginFrame()) { + TestRunner.error( + `addAssertionCount should only be called by a cross origin test` + ); + return; + } + + if (testInDifferentProcess()) { + TestRunner._xoriginAssertionCount += count; + } +}; + +TestRunner.testUnloaded = function (result, runtime) { + // If we're in a debug build, check assertion counts. This code is + // similar to the code in Tester_nextTest in browser-test.js used + // for browser-chrome mochitests. + if (SpecialPowers.isDebugBuild) { + var newAssertionCount = + SpecialPowers.assertionCount() + TestRunner._xoriginAssertionCount; + var numAsserts = newAssertionCount - TestRunner._lastAssertionCount; + TestRunner._lastAssertionCount = newAssertionCount; + + var max = TestRunner._expectedMaxAsserts; + var min = TestRunner._expectedMinAsserts; + if (Array.isArray(TestRunner.expected)) { + // Accumulate all assertion counts recorded in the failure pattern file. + let additionalAsserts = TestRunner.expected.reduce( + (acc, [pat, count]) => { + return pat == "ASSERTION" ? acc + count : acc; + }, + 0 + ); + min += additionalAsserts; + max += additionalAsserts; + } + + TestRunner.structuredLogger.assertionCount( + TestRunner.currentTestURL, + numAsserts, + min, + max + ); + + if (numAsserts < min || numAsserts > max) { + result = "ERROR"; + + var direction = "more"; + var target = max; + if (numAsserts < min) { + direction = "less"; + target = min; + } + TestRunner.structuredLogger.testStatus( + TestRunner.currentTestURL, + "Assertion Count", + "ERROR", + "PASS", + numAsserts + + " is " + + direction + + " than expected " + + target + + " assertions" + ); + + // reset result so we don't print a second error on test-end + result = "OK"; + } + } + + TestRunner.structuredLogger.testEnd( + TestRunner.currentTestURL, + result, + "OK", + "Finished in " + runtime + "ms", + { runtime } + ); + + // Always do this, so we can "reset" preferences between tests + SpecialPowers.comparePrefsToBaseline( + TestRunner.ignorePrefs, + TestRunner.verifyPrefsNextTest + ); +}; + +TestRunner.verifyPrefsNextTest = function (p) { + if (TestRunner.comparePrefs) { + let prefs = Array.from(SpecialPowers.Cu.waiveXrays(p), x => + SpecialPowers.unwrapIfWrapped(SpecialPowers.Cu.unwaiveXrays(x)) + ); + prefs.forEach(pr => + TestRunner.structuredLogger.error( + "TEST-UNEXPECTED-FAIL | " + + TestRunner.currentTestURL + + " | changed preference: " + + pr + ) + ); + } + TestRunner.doNextTest(); +}; + +TestRunner.doNextTest = function () { + TestRunner._currentTest++; + if (TestRunner.runSlower) { + setTimeout(TestRunner.runNextTest, 1000); + } else { + TestRunner.runNextTest(); + } +}; + +/** + * Get the results. + */ +TestRunner.countResults = function (tests) { + var nOK = 0; + var nNotOK = 0; + var nTodo = 0; + for (var i = 0; i < tests.length; ++i) { + var test = tests[i]; + if (test.todo && !test.result) { + nTodo++; + } else if (test.result && !test.todo) { + nOK++; + } else { + nNotOK++; + } + } + return { OK: nOK, notOK: nNotOK, todo: nTodo }; +}; + +/** + * Print out table of any error messages found during looped run + */ +TestRunner.displayLoopErrors = function (tableName, tests) { + if (TestRunner.countResults(tests).notOK > 0) { + var table = $(tableName); + var curtest; + if (!table.rows.length) { + //if table headers are not yet generated, make them + var row = table.insertRow(table.rows.length); + var cell = row.insertCell(0); + var textNode = document.createTextNode("Test File Name:"); + cell.appendChild(textNode); + cell = row.insertCell(1); + textNode = document.createTextNode("Test:"); + cell.appendChild(textNode); + cell = row.insertCell(2); + textNode = document.createTextNode("Error message:"); + cell.appendChild(textNode); + } + + //find the broken test + for (var testnum in tests) { + curtest = tests[testnum]; + if ( + !( + (curtest.todo && !curtest.result) || + (curtest.result && !curtest.todo) + ) + ) { + //this is a failed test or the result of todo test. Display the related message + row = table.insertRow(table.rows.length); + cell = row.insertCell(0); + textNode = document.createTextNode(TestRunner.currentTestURL); + cell.appendChild(textNode); + cell = row.insertCell(1); + textNode = document.createTextNode(curtest.name); + cell.appendChild(textNode); + cell = row.insertCell(2); + textNode = document.createTextNode(curtest.diag ? curtest.diag : ""); + cell.appendChild(textNode); + } + } + } +}; + +TestRunner.updateUI = function (tests) { + var results = TestRunner.countResults(tests); + var passCount = parseInt($("pass-count").innerHTML) + results.OK; + var failCount = parseInt($("fail-count").innerHTML) + results.notOK; + var todoCount = parseInt($("todo-count").innerHTML) + results.todo; + $("pass-count").innerHTML = passCount; + $("fail-count").innerHTML = failCount; + $("todo-count").innerHTML = todoCount; + + // Set the top Green/Red bar + var indicator = $("indicator"); + if (failCount > 0) { + indicator.innerHTML = "Status: Fail"; + indicator.style.backgroundColor = "red"; + } else if (passCount > 0) { + indicator.innerHTML = "Status: Pass"; + indicator.style.backgroundColor = "#0d0"; + } else { + indicator.innerHTML = "Status: ToDo"; + indicator.style.backgroundColor = "orange"; + } + + // Set the table values + var trID = "tr-" + $("current-test-path").innerHTML; + var row = $(trID); + + // Only update the row if it actually exists (autoUI) + if (row != null) { + var tds = row.getElementsByTagName("td"); + tds[0].style.backgroundColor = "#0d0"; + tds[0].innerHTML = parseInt(tds[0].innerHTML) + parseInt(results.OK); + tds[1].style.backgroundColor = results.notOK > 0 ? "red" : "#0d0"; + tds[1].innerHTML = parseInt(tds[1].innerHTML) + parseInt(results.notOK); + tds[2].style.backgroundColor = results.todo > 0 ? "orange" : "#0d0"; + tds[2].innerHTML = parseInt(tds[2].innerHTML) + parseInt(results.todo); + } + + //if we ran in a loop, display any found errors + if (TestRunner.repeat > 0) { + TestRunner.displayLoopErrors("fail-table", tests); + } +}; + +// XOrigin Tests +// If "--enable-xorigin-tests" is set, mochitests are run in a cross origin iframe. +// The parent process will run at http://mochi.xorigin-test:8888", and individual +// mochitests will be launched in a cross-origin iframe at http://mochi.test:8888. + +var xOriginDispatchMap = { + runner: TestRunner, + logger: TestRunner.structuredLogger, + addFailedTest: TestRunner.addFailedTest, + expectAssertions: TestRunner.expectAssertions, + expectChildProcessCrash: TestRunner.expectChildProcessCrash, + requestLongerTimeout: TestRunner.requestLongerTimeout, + "structuredLogger.deactivateBuffering": + TestRunner.structuredLogger.deactivateBuffering, + "structuredLogger.activateBuffering": + TestRunner.structuredLogger.activateBuffering, + "structuredLogger.testStatus": TestRunner.structuredLogger.testStatus, + "structuredLogger.info": TestRunner.structuredLogger.info, + "structuredLogger.warning": TestRunner.structuredLogger.warning, + "structuredLogger.error": TestRunner.structuredLogger.error, + testFinished: TestRunner.testFinished, + addAssertionCount: TestRunner.addAssertionCount, +}; + +function xOriginTestRunnerHandler(event) { + if (event.data.harnessType != "SimpleTest") { + return; + } + // Handles messages from xOriginRunner in SimpleTest.js. + if (event.data.command in xOriginDispatchMap) { + xOriginDispatchMap[event.data.command].apply( + xOriginDispatchMap[event.data.applyOn], + event.data.params + ); + } else { + TestRunner.error(`Command ${event.data.command} not found + in xOriginDispatchMap`); + } +} + +TestRunner.setXOriginEventHandler = function () { + window.addEventListener("message", xOriginTestRunnerHandler); +}; diff --git a/testing/mochitest/tests/SimpleTest/WindowSnapshot.js b/testing/mochitest/tests/SimpleTest/WindowSnapshot.js new file mode 100644 index 0000000000..d1925f36e6 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/WindowSnapshot.js @@ -0,0 +1,122 @@ +var gWindowUtils; + +try { + gWindowUtils = SpecialPowers.getDOMWindowUtils(window); + if (gWindowUtils && !gWindowUtils.compareCanvases) { + gWindowUtils = null; + } +} catch (e) { + gWindowUtils = null; +} + +function snapshotWindow(win, withCaret) { + return SpecialPowers.snapshotWindow(win, withCaret); +} + +function snapshotRect(win, rect) { + return SpecialPowers.snapshotRect(win, rect); +} + +// If the two snapshots don't compare as expected (true for equal, false for +// unequal), returns their serializations as data URIs. In all cases, returns +// whether the comparison was as expected. +function compareSnapshots(s1, s2, expectEqual, fuzz) { + if (s1.width != s2.width || s1.height != s2.height) { + ok( + false, + "Snapshot canvases are not the same size: " + + s1.width + + "x" + + s1.height + + " vs. " + + s2.width + + "x" + + s2.height + ); + return [false]; + } + var passed = false; + var numDifferentPixels; + var maxDifference = { value: undefined }; + if (gWindowUtils) { + var equal; + try { + numDifferentPixels = gWindowUtils.compareCanvases(s1, s2, maxDifference); + if (!fuzz) { + equal = numDifferentPixels == 0; + } else { + equal = + numDifferentPixels <= fuzz.numDifferentPixels && + maxDifference.value <= fuzz.maxDifference; + } + passed = equal == expectEqual; + } catch (e) { + ok(false, "Exception thrown from compareCanvases: " + e); + } + } + + var s1DataURI, s2DataURI; + if (!passed) { + s1DataURI = s1.toDataURL(); + s2DataURI = s2.toDataURL(); + + if (!gWindowUtils) { + passed = (s1DataURI == s2DataURI) == expectEqual; + } + } + + return [ + passed, + s1DataURI, + s2DataURI, + numDifferentPixels, + maxDifference.value, + ]; +} + +function assertSnapshots(s1, s2, expectEqual, fuzz, s1name, s2name) { + var [passed, s1DataURI, s2DataURI, numDifferentPixels, maxDifference] = + compareSnapshots(s1, s2, expectEqual, fuzz); + var sym = expectEqual ? "==" : "!="; + ok(passed, "reftest comparison: " + sym + " " + s1name + " " + s2name); + if (!passed) { + let status = "TEST-UNEXPECTED-FAIL"; + if (usesFailurePatterns() && recordIfMatchesFailurePattern(s1name)) { + status = "TEST-KNOWN-FAIL"; + } + // The language / format in this message should match the failure messages + // displayed by reftest.js's "RecordResult()" method so that log output + // can be parsed by reftest-analyzer.xhtml + var report = + "REFTEST " + + status + + " | " + + s1name + + " | image comparison (" + + sym + + "), max difference: " + + maxDifference + + ", number of differing pixels: " + + numDifferentPixels + + "\n"; + if (expectEqual) { + report += "REFTEST IMAGE 1 (TEST): " + s1DataURI + "\n"; + report += "REFTEST IMAGE 2 (REFERENCE): " + s2DataURI + "\n"; + } else { + report += "REFTEST IMAGE: " + s1DataURI + "\n"; + } + (info || dump)(report); + } + return passed; +} + +function assertWindowPureColor(win, color) { + const snapshot = SpecialPowers.snapshotRect(win); + const canvas = document.createElement("canvas"); + canvas.width = snapshot.width; + canvas.height = snapshot.height; + const context = canvas.getContext("2d"); + context.fillStyle = color; + context.fillRect(0, 0, canvas.width, canvas.height); + assertSnapshots(snapshot, canvas, true, null, "snapshot", color); +} diff --git a/testing/mochitest/tests/SimpleTest/WorkerHandler.js b/testing/mochitest/tests/SimpleTest/WorkerHandler.js new file mode 100644 index 0000000000..7794087805 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/WorkerHandler.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Sets the worker message and error event handlers to respond to SimpleTest + * style messages. + */ +function listenForTests(worker, opts = { verbose: true }) { + worker.onerror = function (error) { + error.preventDefault(); + ok(false, "Worker error " + error.message); + }; + worker.onmessage = function (msg) { + if (opts && opts.verbose) { + ok(true, "MAIN: onmessage " + JSON.stringify(msg.data)); + } + switch (msg.data.kind) { + case "is": + SimpleTest.ok( + msg.data.outcome, + msg.data.description + "( " + msg.data.a + " ==? " + msg.data.b + ")" + ); + return; + case "isnot": + SimpleTest.ok( + msg.data.outcome, + msg.data.description + "( " + msg.data.a + " !=? " + msg.data.b + ")" + ); + return; + case "ok": + SimpleTest.ok(msg.data.condition, msg.data.description); + return; + case "info": + SimpleTest.info(msg.data.description); + return; + case "finish": + SimpleTest.finish(); + return; + default: + SimpleTest.ok( + false, + "test_osfile.xul: wrong message " + JSON.stringify(msg.data) + ); + } + }; +} diff --git a/testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js b/testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js new file mode 100644 index 0000000000..ce4848d7af --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function log(text) { + dump("WORKER " + text + "\n"); +} + +function send(message) { + self.postMessage(message); +} + +function finish() { + send({ kind: "finish" }); +} + +function ok(condition, description) { + send({ kind: "ok", condition: !!condition, description: "" + description }); +} + +function is(a, b, description) { + let outcome = a == b; // Need to decide outcome here, as not everything can be serialized + send({ + kind: "is", + outcome, + description: "" + description, + a: "" + a, + b: "" + b, + }); +} + +function isnot(a, b, description) { + let outcome = a != b; // Need to decide outcome here, as not everything can be serialized + send({ + kind: "isnot", + outcome, + description: "" + description, + a: "" + a, + b: "" + b, + }); +} + +function info(description) { + send({ kind: "info", description: "" + description }); +} diff --git a/testing/mochitest/tests/SimpleTest/iframe-between-tests.html b/testing/mochitest/tests/SimpleTest/iframe-between-tests.html new file mode 100644 index 0000000000..b5efaf6170 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/iframe-between-tests.html @@ -0,0 +1,20 @@ +<title>iframe for between tests</title> +<!-- + This page exists so that our accounting for assertions correctly + counts assertions that happen while leaving a page. We load this page + after a test finishes, check the assertion counts, and then go on to + load the next. +--> +<script> +window.addEventListener("load", function() { + var runner = (parent.TestRunner || parent.wrappedJSObject.TestRunner); + const urlParams = new URLSearchParams(window.location.search); + var result = urlParams.get("result", "ERROR"); + var runtime = urlParams.get("runtime", "-1"); + runner.testUnloaded(result, runtime); + + if (SpecialPowers) { + SpecialPowers.DOMWindowUtils.runNextCollectorTimer(); + } +}); +</script> diff --git a/testing/mochitest/tests/SimpleTest/moz.build b/testing/mochitest/tests/SimpleTest/moz.build new file mode 100644 index 0000000000..55c54e6255 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/moz.build @@ -0,0 +1,27 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +TEST_HARNESS_FILES.testing.mochitest.tests.SimpleTest += [ + "/docshell/test/chrome/docshell_helpers.js", + "AccessibilityUtils.js", + "ChromeTask.js", + "EventUtils.js", + "ExtensionTestUtils.js", + "iframe-between-tests.html", + "LogController.js", + "MemoryStats.js", + "MockObjects.js", + "MozillaLogger.js", + "NativeKeyCodes.js", + "paint_listener.js", + "setup.js", + "SimpleTest.js", + "test.css", + "TestRunner.js", + "WindowSnapshot.js", + "WorkerHandler.js", + "WorkerSimpleTest.js", +] diff --git a/testing/mochitest/tests/SimpleTest/paint_listener.js b/testing/mochitest/tests/SimpleTest/paint_listener.js new file mode 100644 index 0000000000..2fc6ab425a --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/paint_listener.js @@ -0,0 +1,109 @@ +(function () { + var accumulatedRect = null; + var onpaint = []; + var debug = SpecialPowers.getBoolPref("testing.paint_listener.debug", false); + const FlushModes = { + FLUSH: 0, + NOFLUSH: 1, + }; + + function paintListener(event) { + if (event.target != window) { + if (debug) { + dump("got MozAfterPaint for wrong window\n"); + } + return; + } + var clientRect = event.boundingClientRect; + var eventRect; + if (clientRect) { + eventRect = [ + clientRect.left, + clientRect.top, + clientRect.right, + clientRect.bottom, + ]; + } else { + eventRect = [0, 0, 0, 0]; + } + if (debug) { + dump("got MozAfterPaint: " + eventRect.join(",") + "\n"); + } + accumulatedRect = accumulatedRect + ? [ + Math.min(accumulatedRect[0], eventRect[0]), + Math.min(accumulatedRect[1], eventRect[1]), + Math.max(accumulatedRect[2], eventRect[2]), + Math.max(accumulatedRect[3], eventRect[3]), + ] + : eventRect; + if (debug) { + dump("Dispatching " + onpaint.length + " onpaint listeners\n"); + } + while (onpaint.length) { + window.setTimeout(onpaint.pop(), 0); + } + } + window.addEventListener("MozAfterPaint", paintListener); + + function waitForPaints(callback, subdoc, flushMode) { + // Wait until paint suppression has ended + var utils = SpecialPowers.getDOMWindowUtils(window); + if (utils.paintingSuppressed) { + if (debug) { + dump("waiting for paint suppression to end...\n"); + } + window.setTimeout(function () { + waitForPaints(callback, subdoc, flushMode); + }, 0); + return; + } + + // The call to getBoundingClientRect will flush pending layout + // notifications. Sometimes, however, this is undesirable since it can mask + // bugs where the code under test should be performing the flush. + if (flushMode === FlushModes.FLUSH) { + document.documentElement.getBoundingClientRect(); + if (subdoc) { + subdoc.documentElement.getBoundingClientRect(); + } + } + + if (utils.isMozAfterPaintPending) { + if (debug) { + dump("waiting for paint...\n"); + } + onpaint.push(function () { + waitForPaints(callback, subdoc, FlushModes.NOFLUSH); + }); + if (utils.isTestControllingRefreshes) { + utils.advanceTimeAndRefresh(0); + } + return; + } + + if (debug) { + dump("done...\n"); + } + var result = accumulatedRect || [0, 0, 0, 0]; + accumulatedRect = null; + callback.apply(null, result); + } + + window.waitForAllPaintsFlushed = function (callback, subdoc) { + waitForPaints(callback, subdoc, FlushModes.FLUSH); + }; + + window.waitForAllPaints = function (callback) { + waitForPaints(callback, null, FlushModes.NOFLUSH); + }; + + window.promiseAllPaintsDone = function (subdoc = null, flush = false) { + var flushmode = flush ? FlushModes.FLUSH : FlushModes.NOFLUSH; + return new Promise(function (resolve, reject) { + // The callback is given the components of the rect, but resolve() can + // only be given one arg, so we turn it back into an array. + waitForPaints((l, r, t, b) => resolve([l, r, t, b]), subdoc, flushmode); + }); + }; +})(); diff --git a/testing/mochitest/tests/SimpleTest/setup.js b/testing/mochitest/tests/SimpleTest/setup.js new file mode 100644 index 0000000000..05ba8066a1 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/setup.js @@ -0,0 +1,386 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This file expects the following files to be loaded. +/* import-globals-from TestRunner.js */ + +// From the harness: +/* import-globals-from ../../chrome-harness.js */ +/* import-globals-from ../../chunkifyTests.js */ + +// It appears we expect these from one of the MochiKit scripts. +/* global toggleElementClass, removeElementClass, addElementClass, + hasElementClass */ + +TestRunner.logEnabled = true; +TestRunner.logger = LogController; + +if (!("SpecialPowers" in window)) { + dump("SimpleTest setup.js found SpecialPowers unavailable: reloading...\n"); + setTimeout(() => { + window.location.reload(); + }, 1000); +} + +/* Helper function */ +function parseQueryString(encodedString, useArrays) { + // strip a leading '?' from the encoded string + var qstr = + encodedString.length && encodedString[0] == "?" + ? encodedString.substring(1) + : encodedString; + var pairs = qstr.replace(/\+/g, "%20").split(/(\&\;|\&\#38\;|\&|\&)/); + var o = {}; + var decode; + if (typeof decodeURIComponent != "undefined") { + decode = decodeURIComponent; + } else { + decode = unescape; + } + if (useArrays) { + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i].split("="); + if (pair.length !== 2) { + continue; + } + var name = decode(pair[0]); + var arr = o[name]; + if (!(arr instanceof Array)) { + arr = []; + o[name] = arr; + } + arr.push(decode(pair[1])); + } + } else { + for (i = 0; i < pairs.length; i++) { + pair = pairs[i].split("="); + if (pair.length !== 2) { + continue; + } + o[decode(pair[0])] = decode(pair[1]); + } + } + return o; +} + +/* helper function, specifically for prefs to ignore */ +function loadFile(url, callback) { + let req = new XMLHttpRequest(); + req.open("GET", url); + req.onload = function () { + if (req.readyState == 4) { + if (req.status == 200) { + try { + let prefs = JSON.parse(req.responseText); + callback(prefs); + } catch (e) { + dump( + "TEST-UNEXPECTED-FAIL: setup.js | error parsing " + + url + + " (" + + e + + ")\n" + ); + throw e; + } + } else { + dump( + "TEST-UNEXPECTED-FAIL: setup.js | error loading " + + url + + " (HTTP " + + req.status + + ")\n" + ); + callback({}); + } + } + }; + req.send(); +} + +// Check the query string for arguments +var params = parseQueryString(location.search.substring(1), true); + +var config = {}; +if (window.readConfig) { + config = readConfig(); +} + +if (config.testRoot == "chrome" || config.testRoot == "a11y") { + for (var p in params) { + // Compare with arrays to find boolean equivalents, since that's what + // |parseQueryString| with useArrays returns. + if (params[p] == [1]) { + config[p] = true; + } else if (params[p] == [0]) { + config[p] = false; + } else { + config[p] = params[p]; + } + } + params = config; + params.baseurl = "chrome://mochitests/content"; +} else if (params.xOriginTests) { + params.baseurl = "http://mochi.test:8888/tests/"; +} else { + params.baseurl = ""; +} + +if (params.testRoot == "browser") { + params.testPrefix = "chrome://mochitests/content/browser/"; +} else if (params.testRoot == "chrome") { + params.testPrefix = "chrome://mochitests/content/chrome/"; +} else if (params.testRoot == "a11y") { + params.testPrefix = "chrome://mochitests/content/a11y/"; +} else if (params.xOriginTests) { + params.testPrefix = "http://mochi.test:8888/tests/"; + params.httpsBaseUrl = "https://example.org:443/tests/"; +} else { + params.testPrefix = "/tests/"; +} + +// set the per-test timeout if specified in the query string +if (params.timeout) { + TestRunner.timeout = parseInt(params.timeout) * 1000; +} + +// log levels for console and logfile +var fileLevel = params.fileLevel || null; +var consoleLevel = params.consoleLevel || null; + +// repeat tells us how many times to repeat the tests +if (params.repeat) { + TestRunner.repeat = params.repeat; +} + +if (params.runUntilFailure) { + TestRunner.runUntilFailure = true; +} + +// closeWhenDone tells us to close the browser when complete +if (params.closeWhenDone) { + TestRunner.onComplete = SpecialPowers.quit.bind(SpecialPowers); +} + +if (params.failureFile) { + TestRunner.setFailureFile(params.failureFile); +} + +// Breaks execution and enters the JS debugger on a test failure +if (params.debugOnFailure) { + TestRunner.debugOnFailure = true; +} + +// logFile to write our results +if (params.logFile) { + var mfl = new MozillaFileLogger(params.logFile); + TestRunner.logger.addListener("mozLogger", fileLevel + "", mfl.logCallback); +} + +// A temporary hack for android 4.0 where Fennec utilizes the pandaboard so much it reboots +if (params.runSlower) { + TestRunner.runSlower = true; +} + +if (params.dumpOutputDirectory) { + TestRunner.dumpOutputDirectory = params.dumpOutputDirectory; +} + +if (params.dumpAboutMemoryAfterTest) { + TestRunner.dumpAboutMemoryAfterTest = true; +} + +if (params.dumpDMDAfterTest) { + TestRunner.dumpDMDAfterTest = true; +} + +// We need to check several things here because mochitest-chrome passes +// `jsdebugger` and `debugger` directly, but in other tests we're reliant +// on the `interactiveDebugger` flag being passed along. +if (params.interactiveDebugger || params.jsdebugger || params.debugger) { + TestRunner.interactiveDebugger = true; +} + +if (params.jscovDirPrefix) { + TestRunner.jscovDirPrefix = params.jscovDirPrefix; +} + +if (params.maxTimeouts) { + TestRunner.maxTimeouts = params.maxTimeouts; +} + +if (params.cleanupCrashes) { + TestRunner.cleanupCrashes = true; +} + +if (params.xOriginTests) { + TestRunner.xOriginTests = true; + TestRunner.setXOriginEventHandler(); +} + +if (params.timeoutAsPass) { + TestRunner.timeoutAsPass = true; +} + +if (params.conditionedProfile) { + TestRunner.conditionedProfile = true; +} + +if (params.comparePrefs) { + TestRunner.comparePrefs = true; +} + +// Log things to the console if appropriate. +TestRunner.logger.addListener( + "dumpListener", + consoleLevel + "", + function (msg) { + dump(msg.info.join(" ") + "\n"); + } +); + +var gTestList = []; +var RunSet = {}; + +RunSet.runall = function (e) { + // Filter tests to include|exclude tests based on data in params.filter. + // This allows for including or excluding tests from the gTestList + // TODO Only used by ipc tests, remove once those are implemented sanely + if (params.testManifest) { + getTestManifest( + getTestManifestURL(params.testManifest), + params, + function (filter) { + gTestList = filterTests(filter, gTestList, params.runOnly); + RunSet.runtests(); + } + ); + } else { + RunSet.runtests(); + } +}; + +RunSet.runtests = function (e) { + // Which tests we're going to run + var my_tests = gTestList; + + if (params.startAt || params.endAt) { + my_tests = skipTests(my_tests, params.startAt, params.endAt); + } + + if (params.shuffle) { + for (var i = my_tests.length - 1; i > 0; --i) { + var j = Math.floor(Math.random() * i); + var tmp = my_tests[j]; + my_tests[j] = my_tests[i]; + my_tests[i] = tmp; + } + } + TestRunner.setParameterInfo(params); + TestRunner.runTests(my_tests); +}; + +RunSet.reloadAndRunAll = function (e) { + e.preventDefault(); + //window.location.hash = ""; + if (params.autorun) { + window.location.search += ""; + // eslint-disable-next-line no-self-assign + window.location.href = window.location.href; + } else if (window.location.search) { + window.location.href += "&autorun=1"; + } else { + window.location.href += "?autorun=1"; + } +}; + +// UI Stuff +function toggleVisible(elem) { + toggleElementClass("invisible", elem); +} + +function makeVisible(elem) { + removeElementClass(elem, "invisible"); +} + +function makeInvisible(elem) { + addElementClass(elem, "invisible"); +} + +function isVisible(elem) { + // you may also want to check for + // getElement(elem).style.display == "none" + return !hasElementClass(elem, "invisible"); +} + +function toggleNonTests(e) { + e.preventDefault(); + var elems = document.getElementsByClassName("non-test"); + for (var i = "0"; i < elems.length; i++) { + toggleVisible(elems[i]); + } + if (isVisible(elems[0])) { + $("toggleNonTests").innerHTML = "Hide Non-Tests"; + } else { + $("toggleNonTests").innerHTML = "Show Non-Tests"; + } +} + +// hook up our buttons +function hookup() { + if (params.manifestFile) { + getTestManifest( + getTestManifestURL(params.manifestFile), + params, + hookupTests + ); + } else { + hookupTests(gTestList); + } +} + +function getPrefList() { + if (params.ignorePrefsFile) { + loadFile(getTestManifestURL(params.ignorePrefsFile), function (prefs) { + TestRunner.ignorePrefs = prefs; + RunSet.runall(); + }); + } else { + RunSet.runall(); + } +} + +function hookupTests(testList) { + if (testList.length) { + gTestList = testList; + } else { + gTestList = []; + for (var obj in testList) { + gTestList.push(testList[obj]); + } + } + + document.getElementById("runtests").onclick = RunSet.reloadAndRunAll; + document.getElementById("toggleNonTests").onclick = toggleNonTests; + // run automatically if autorun specified + if (params.autorun) { + getPrefList(); + } +} + +function getTestManifestURL(path) { + // The test manifest url scheme should be the same protocol as the containing + // window... unless it's not http(s) + if ( + window.location.protocol == "http:" || + window.location.protocol == "https:" + ) { + return window.location.protocol + "//" + window.location.host + "/" + path; + } + return "http://mochi.test:8888/" + path; +} diff --git a/testing/mochitest/tests/SimpleTest/test.css b/testing/mochitest/tests/SimpleTest/test.css new file mode 100644 index 0000000000..357070e8ae --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/test.css @@ -0,0 +1,39 @@ +.test_ok { + color: #0d0; + display: none; +} + +.test_not_ok { + color: red; + display: block; +} + +.test_todo { + /* color: orange; */ + display: block; +} + +.test_ok, .test_not_ok, .test_todo { + border-bottom-width: 2px; + border-bottom-style: solid; + border-bottom-color: black; +} + +.all_pass { + background-color: #0d0; +} + +.some_fail { + background-color: red; +} + +.todo_only { + background-color: orange; +} + +.tests_report { + border-width: 2px; + border-style: solid; + width: 20em; + display: table; +} diff --git a/testing/mochitest/tests/browser/browser.toml b/testing/mochitest/tests/browser/browser.toml new file mode 100644 index 0000000000..9367630370 --- /dev/null +++ b/testing/mochitest/tests/browser/browser.toml @@ -0,0 +1,102 @@ +[DEFAULT] +support-files = [ + "head.js", + "dummy.html"] + +['browser_BrowserTestUtils.js'] +skip-if = [ + "verify", +] + +['browser_add_task.js'] + +['browser_async.js'] + +['browser_browserLoaded_content_loaded.js'] +https_first_disabled = true + +['browser_document_builder_sjs.js'] + +['browser_fail.js'] +skip-if = [ + "verify", +] + +['browser_fail_add_task.js'] +skip-if = [ + "verify", +] + +['browser_fail_add_task_uncaught_rejection.js'] +skip-if = [ + "verify", +] + +['browser_fail_async.js'] +skip-if = [ + "verify", +] + +['browser_fail_if.js'] +fail-if = true + +['browser_fail_throw.js'] +skip-if = [ + "verify", +] + +['browser_fail_timeout.js'] +skip-if = true # Disabled beacuse it takes too long (bug 1178959) + +['browser_fail_uncaught_rejection.js'] +skip-if = [ + "verify", +] + +['browser_fail_uncaught_rejection_expected.js'] +skip-if = [ + "verify", +] + +['browser_fail_uncaught_rejection_expected_multi.js'] +skip-if = [ + "verify", +] + +['browser_fail_unexpectedTimeout.js'] +skip-if = true # Disabled beacuse it takes too long (bug 1178959) + +['browser_getTestFile.js'] +support-files = [ + "test-dir/*", + "waitForFocusPage.html"] + +['browser_head.js'] + +['browser_parameters.js'] + +['browser_pass.js'] + +['browser_privileges.js'] + +['browser_requestLongerTimeout.js'] +skip-if = true # Disabled beacuse it takes too long (bug 1178959) + +['browser_sanityException.js'] + +['browser_sanityException2.js'] + +['browser_setup_runs_first.js'] + +['browser_setup_runs_for_only_tests.js'] + +['browser_tasks_skip.js'] + +['browser_tasks_skipall.js'] + +['browser_uncaught_rejection_expected.js'] + +['browser_waitForFocus.js'] + +['browser_zz_fail_openwindow.js'] +skip-if = true # this catches outside of the main loop to find an extra window diff --git a/testing/mochitest/tests/browser/browser_BrowserTestUtils.js b/testing/mochitest/tests/browser/browser_BrowserTestUtils.js new file mode 100644 index 0000000000..cde2b8b927 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_BrowserTestUtils.js @@ -0,0 +1,312 @@ +function getLastEventDetails(browser) { + return SpecialPowers.spawn(browser, [], async function () { + return content.document.getElementById("out").textContent; + }); +} + +add_task(async function () { + let onClickEvt = + 'document.getElementById("out").textContent = event.target.localName + "," + event.clientX + "," + event.clientY;'; + const url = + "<body onclick='" + + onClickEvt + + "' style='margin: 0'>" + + "<button id='one' style='margin: 0; margin-left: 16px; margin-top: 14px; width: 80px; height: 40px;'>Test</button>" + + "<div onmousedown='event.preventDefault()' style='margin: 0; width: 80px; height: 60px;'>Other</div>" + + "<span id='out'></span></body>"; + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html," + url + ); + + let browser = tab.linkedBrowser; + await BrowserTestUtils.synthesizeMouseAtCenter("#one", {}, browser); + let details = await getLastEventDetails(browser); + + is(details, "button,56,34", "synthesizeMouseAtCenter"); + + await BrowserTestUtils.synthesizeMouse("#one", 4, 9, {}, browser); + details = await getLastEventDetails(browser); + is(details, "button,20,23", "synthesizeMouse"); + + await BrowserTestUtils.synthesizeMouseAtPoint(15, 6, {}, browser); + details = await getLastEventDetails(browser); + is(details, "body,15,6", "synthesizeMouseAtPoint on body"); + + await BrowserTestUtils.synthesizeMouseAtPoint( + 20, + 22, + {}, + browser.browsingContext + ); + details = await getLastEventDetails(browser); + is(details, "button,20,22", "synthesizeMouseAtPoint on button"); + + await BrowserTestUtils.synthesizeMouseAtCenter("body > div", {}, browser); + details = await getLastEventDetails(browser); + is(details, "div,40,84", "synthesizeMouseAtCenter with complex selector"); + + let cancelled = await BrowserTestUtils.synthesizeMouseAtCenter( + "body > div", + { type: "mousedown" }, + browser + ); + details = await getLastEventDetails(browser); + is( + details, + "div,40,84", + "synthesizeMouseAtCenter mousedown with complex selector" + ); + ok( + cancelled, + "synthesizeMouseAtCenter mousedown with complex selector not cancelled" + ); + + cancelled = await BrowserTestUtils.synthesizeMouseAtCenter( + "body > div", + { type: "mouseup" }, + browser + ); + details = await getLastEventDetails(browser); + is( + details, + "div,40,84", + "synthesizeMouseAtCenter mouseup with complex selector" + ); + ok( + !cancelled, + "synthesizeMouseAtCenter mouseup with complex selector cancelled" + ); + + gBrowser.removeTab(tab); +}); + +add_task(async function testSynthesizeMouseAtPointsButtons() { + let onMouseEvt = + 'document.getElementById("mouselog").textContent += "/" + [event.type,event.clientX,event.clientY,event.button,event.buttons].join(",");'; + + let getLastMouseEventDetails = browser => { + return SpecialPowers.spawn(browser, [], async () => { + let log = content.document.getElementById("mouselog").textContent; + content.document.getElementById("mouselog").textContent = ""; + return log; + }); + }; + + const url = + "<body" + + "' onmousedown='" + + onMouseEvt + + "' onmousemove='" + + onMouseEvt + + "' onmouseup='" + + onMouseEvt + + "' style='margin: 0'>" + + "<div style='margin: 0; width: 80px; height: 60px;'>Mouse area</div>" + + "<span id='mouselog'></span>" + + "</body>"; + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html," + url + ); + + let browser = tab.linkedBrowser; + let details; + + await BrowserTestUtils.synthesizeMouseAtPoint( + 21, + 22, + { + type: "mousemove", + }, + browser.browsingContext + ); + details = await getLastMouseEventDetails(browser); + is(details, "/mousemove,21,22,0,0", "synthesizeMouseAtPoint mousemove"); + + await BrowserTestUtils.synthesizeMouseAtPoint( + 22, + 23, + {}, + browser.browsingContext + ); + details = await getLastMouseEventDetails(browser); + is( + details, + "/mousedown,22,23,0,1/mouseup,22,23,0,0", + "synthesizeMouseAtPoint default action includes buttons on mousedown only" + ); + + await BrowserTestUtils.synthesizeMouseAtPoint( + 20, + 22, + { + type: "mousedown", + }, + browser.browsingContext + ); + details = await getLastMouseEventDetails(browser); + is( + details, + "/mousedown,20,22,0,1", + "synthesizeMouseAtPoint mousedown includes buttons" + ); + + await BrowserTestUtils.synthesizeMouseAtPoint( + 21, + 20, + { + type: "mouseup", + }, + browser.browsingContext + ); + details = await getLastMouseEventDetails(browser); + is(details, "/mouseup,21,20,0,0", "synthesizeMouseAtPoint mouseup"); + + await BrowserTestUtils.synthesizeMouseAtPoint( + 20, + 22, + { + type: "mousedown", + button: 2, + }, + browser.browsingContext + ); + details = await getLastMouseEventDetails(browser); + is( + details, + "/mousedown,20,22,2,2", + + "synthesizeMouseAtPoint mousedown respects specified button 2" + ); + + await BrowserTestUtils.synthesizeMouseAtPoint( + 21, + 20, + { + type: "mouseup", + button: 2, + }, + browser.browsingContext + ); + details = await getLastMouseEventDetails(browser); + is( + details, + "/mouseup,21,20,2,0", + "synthesizeMouseAtPoint mouseup with button 2" + ); + + gBrowser.removeTab(tab); +}); + +add_task(async function mouse_in_iframe() { + let onClickEvt = "document.body.lastChild.textContent = event.target.id;"; + const url = `<iframe style='margin: 30px;' src='data:text/html,<body onclick="${onClickEvt}"> + <p><button>One</button></p><p><button id="two">Two</button></p><p id="out"></p></body>'></iframe> + <iframe style='margin: 10px;' src='data:text/html,<body onclick="${onClickEvt}"> + <p><button>Three</button></p><p><button id="four">Four</button></p><p id="out"></p></body>'></iframe>`; + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html," + url + ); + + let browser = tab.linkedBrowser; + + await BrowserTestUtils.synthesizeMouse( + "#two", + 5, + 10, + {}, + browser.browsingContext.children[0] + ); + + let details = await getLastEventDetails(browser.browsingContext.children[0]); + is(details, "two", "synthesizeMouse"); + + await BrowserTestUtils.synthesizeMouse( + "#four", + 5, + 10, + {}, + browser.browsingContext.children[1] + ); + details = await getLastEventDetails(browser.browsingContext.children[1]); + is(details, "four", "synthesizeMouse"); + + gBrowser.removeTab(tab); +}); + +add_task(async function () { + await BrowserTestUtils.registerAboutPage( + registerCleanupFunction, + "about-pages-are-cool", + getRootDirectory(gTestPath) + "dummy.html", + 0 + ); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:about-pages-are-cool", + true + ); + ok(tab, "Successfully created an about: page and loaded it."); + BrowserTestUtils.removeTab(tab); + try { + await BrowserTestUtils.unregisterAboutPage("about-pages-are-cool"); + ok(true, "Successfully unregistered the about page."); + } catch (ex) { + ok(false, "Should not throw unregistering a known about: page"); + } + await BrowserTestUtils.unregisterAboutPage("random-other-about-page").then( + () => { + ok( + false, + "Should not have succeeded unregistering an unknown about: page." + ); + }, + () => { + ok( + true, + "Should have returned a rejected promise trying to unregister an unknown about page" + ); + } + ); +}); + +add_task(async function testWaitForEvent() { + // A promise returned by BrowserTestUtils.waitForEvent should not be resolved + // in the same event tick as the event listener is called. + let eventListenerCalled = false; + let waitForEventResolved = false; + // Use capturing phase to make sure the event listener added by + // BrowserTestUtils.waitForEvent is called before the normal event listener + // below. + let eventPromise = BrowserTestUtils.waitForEvent( + gBrowser, + "dummyevent", + true + ); + eventPromise.then(() => { + waitForEventResolved = true; + }); + // Add normal event listener that is called after the event listener added by + // BrowserTestUtils.waitForEvent. + gBrowser.addEventListener( + "dummyevent", + () => { + eventListenerCalled = true; + is( + waitForEventResolved, + false, + "BrowserTestUtils.waitForEvent promise resolution handler shouldn't be called at this point." + ); + }, + { once: true } + ); + + var event = new CustomEvent("dummyevent"); + gBrowser.dispatchEvent(event); + + await eventPromise; + + is(eventListenerCalled, true, "dummyevent listener should be called"); +}); diff --git a/testing/mochitest/tests/browser/browser_add_task.js b/testing/mochitest/tests/browser/browser_add_task.js new file mode 100644 index 0000000000..2466112834 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_add_task.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var test1Complete = false; +var test2Complete = false; + +function executeWithTimeout() { + return new Promise(resolve => + executeSoon(function () { + ok(true, "we get here after a timeout"); + resolve(); + }) + ); +} + +add_task(async function asyncTest_no1() { + await executeWithTimeout(); + test1Complete = true; +}); + +add_task(async function asyncTest_no2() { + await executeWithTimeout(); + test2Complete = true; +}); + +add_task(function () { + ok(test1Complete, "We have been through test 1"); + ok(test2Complete, "We have been through test 2"); +}); diff --git a/testing/mochitest/tests/browser/browser_async.js b/testing/mochitest/tests/browser/browser_async.js new file mode 100644 index 0000000000..d07cb21740 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_async.js @@ -0,0 +1,9 @@ +function test() { + waitForExplicitFinish(); + function done() { + ok(true, "timeout ran"); + finish(); + } + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(done, 500); +} diff --git a/testing/mochitest/tests/browser/browser_browserLoaded_content_loaded.js b/testing/mochitest/tests/browser/browser_browserLoaded_content_loaded.js new file mode 100644 index 0000000000..fa29e03d23 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_browserLoaded_content_loaded.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +function isDOMLoaded(browser) { + return SpecialPowers.spawn(browser, [], async function () { + Assert.equal( + content.document.readyState, + "complete", + "Browser should be loaded." + ); + }); +} + +// It checks if calling BrowserTestUtils.browserLoaded() yields +// browser object. +add_task(async function () { + let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com"); + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + await isDOMLoaded(browser); + gBrowser.removeTab(tab); +}); + +// It checks that BrowserTestUtils.browserLoaded() works well with +// promise.all(). +add_task(async function () { + let tabURLs = [ + `http://example.org`, + `http://mochi.test:8888`, + `http://test:80`, + ]; + // Add tabs, get the respective browsers + let browsers = tabURLs.map( + u => BrowserTestUtils.addTab(gBrowser, u).linkedBrowser + ); + + // wait for promises to settle + await Promise.all( + (function* () { + for (let b of browsers) { + yield BrowserTestUtils.browserLoaded(b); + } + })() + ); + for (const browser of browsers) { + await isDOMLoaded(browser); + } + // cleanup + browsers + .map(browser => gBrowser.getTabForBrowser(browser)) + .forEach(tab => gBrowser.removeTab(tab)); +}); diff --git a/testing/mochitest/tests/browser/browser_document_builder_sjs.js b/testing/mochitest/tests/browser/browser_document_builder_sjs.js new file mode 100644 index 0000000000..4b653a792d --- /dev/null +++ b/testing/mochitest/tests/browser/browser_document_builder_sjs.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Checks that document-builder.sjs works as expected +add_task(async function assertHtmlParam() { + const html = "<main><h1>I'm built different</h1></main>"; + const delay = 5000; + + const params = new URLSearchParams({ + delay, + html, + }); + params.append("headers", "x-header-1:a"); + params.append("headers", "x-header-2:b"); + + const startTime = performance.now(); + const request = new Request( + `https://example.com/document-builder.sjs?${params}` + ); + info("Do a fetch request to document-builder.sjs"); + const response = await fetch(request); + const duration = performance.now() - startTime; + + is(response.status, 200, "Response is a 200"); + ok( + duration > delay, + `The delay parameter works as expected (took ${duration}ms)` + ); + + const responseText = await response.text(); + is(responseText, html, "The response has the expected content"); + + is( + response.headers.get("content-type"), + "text/html", + "response has the expected content-type" + ); + is( + response.headers.get("x-header-1"), + "a", + "first header was set as expected" + ); + is( + response.headers.get("x-header-2"), + "b", + "second header was set as expected" + ); +}); + +add_task(async function assertFileParam() { + const file = `browser/testing/mochitest/tests/browser/dummy.html`; + const request = new Request( + `https://example.com/document-builder.sjs?file=${file}` + ); + + info("Do a fetch request to document-builder.sjs with a `file` parameter"); + const response = await fetch(request); + is(response.status, 200, "Response is a 200"); + is( + response.headers.get("content-type"), + "text/html", + "response has the expected content-type" + ); + + const responseText = await response.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(responseText, "text/html"); + is( + doc.body.innerHTML.trim(), + "This is a dummy page", + "The response has the file content" + ); +}); diff --git a/testing/mochitest/tests/browser/browser_fail.js b/testing/mochitest/tests/browser/browser_fail.js new file mode 100644 index 0000000000..f6b439fb0d --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail.js @@ -0,0 +1,10 @@ +setExpectedFailuresForSelfTest(6); + +function test() { + ok(false, "fail ok"); + is(true, false, "fail is"); + isnot(true, true, "fail isnot"); + todo(true, "fail todo"); + todo_is(true, true, "fail todo_is"); + todo_isnot(true, false, "fail todo_isnot"); +} diff --git a/testing/mochitest/tests/browser/browser_fail_add_task.js b/testing/mochitest/tests/browser/browser_fail_add_task.js new file mode 100644 index 0000000000..aeb1129943 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_add_task.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +setExpectedFailuresForSelfTest(4); + +function rejectOnNextTick(error) { + return new Promise((resolve, reject) => executeSoon(() => reject(error))); +} + +add_task(async function failWithoutError() { + await rejectOnNextTick(undefined); +}); + +add_task(async function failWithString() { + await rejectOnNextTick("This is a string"); +}); + +add_task(async function failWithInt() { + await rejectOnNextTick(42); +}); + +// This one should display a stack trace +add_task(async function failWithError() { + await rejectOnNextTick(new Error("This is an error")); +}); diff --git a/testing/mochitest/tests/browser/browser_fail_add_task_uncaught_rejection.js b/testing/mochitest/tests/browser/browser_fail_add_task_uncaught_rejection.js new file mode 100644 index 0000000000..8a42740acb --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_add_task_uncaught_rejection.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +setExpectedFailuresForSelfTest(4); + +async function rejectOnNextTick(error) { + await Promise.resolve(); + + Promise.reject(error); +} + +add_task(async function failWithoutError() { + await rejectOnNextTick(undefined); +}); + +add_task(async function failWithString() { + await rejectOnNextTick("This is a string"); +}); + +add_task(async function failWithInt() { + await rejectOnNextTick(42); +}); + +// This one should display a stack trace +add_task(async function failWithError() { + await rejectOnNextTick(new Error("This is an error")); +}); diff --git a/testing/mochitest/tests/browser/browser_fail_async.js b/testing/mochitest/tests/browser/browser_fail_async.js new file mode 100644 index 0000000000..6f284bf2aa --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_async.js @@ -0,0 +1,9 @@ +setExpectedFailuresForSelfTest(1); + +function test() { + waitForExplicitFinish(); + executeSoon(() => { + ok(false, "fail"); + finish(); + }); +} diff --git a/testing/mochitest/tests/browser/browser_fail_if.js b/testing/mochitest/tests/browser/browser_fail_if.js new file mode 100644 index 0000000000..dad56e7dac --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_if.js @@ -0,0 +1,4 @@ +// We expect this test to fail because it is marked as fail-if in the manifest. +function test() { + ok(false, "fail ok"); +} diff --git a/testing/mochitest/tests/browser/browser_fail_throw.js b/testing/mochitest/tests/browser/browser_fail_throw.js new file mode 100644 index 0000000000..585b47561d --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_throw.js @@ -0,0 +1,5 @@ +setExpectedFailuresForSelfTest(1); + +function test() { + throw new Error("thrown exception"); +} diff --git a/testing/mochitest/tests/browser/browser_fail_timeout.js b/testing/mochitest/tests/browser/browser_fail_timeout.js new file mode 100644 index 0000000000..44030a00f0 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_timeout.js @@ -0,0 +1,9 @@ +function test() { + function end() { + ok(false, "should have timed out"); + finish(); + } + waitForExplicitFinish(); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(end, 40000); +} diff --git a/testing/mochitest/tests/browser/browser_fail_uncaught_rejection.js b/testing/mochitest/tests/browser/browser_fail_uncaught_rejection.js new file mode 100644 index 0000000000..86d3e77b7f --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_uncaught_rejection.js @@ -0,0 +1,14 @@ +setExpectedFailuresForSelfTest(2); + +function test() { + Promise.reject(new Error("Promise rejection.")); + (async () => { + throw new Error("Synchronous rejection from async function."); + })(); + + // The following rejections are caught, so they won't result in failures. + Promise.reject(new Error("Promise rejection.")).catch(() => {}); + (async () => { + throw new Error("Synchronous rejection from async function."); + })().catch(() => {}); +} diff --git a/testing/mochitest/tests/browser/browser_fail_uncaught_rejection_expected.js b/testing/mochitest/tests/browser/browser_fail_uncaught_rejection_expected.js new file mode 100644 index 0000000000..6c30d4eb3d --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_uncaught_rejection_expected.js @@ -0,0 +1,12 @@ +setExpectedFailuresForSelfTest(1); + +// The test will fail because there is only one of two expected rejections. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.expectUncaughtRejection(/Promise rejection./); +PromiseTestUtils.expectUncaughtRejection(/Promise rejection./); + +function test() { + Promise.reject(new Error("Promise rejection.")); +} diff --git a/testing/mochitest/tests/browser/browser_fail_uncaught_rejection_expected_multi.js b/testing/mochitest/tests/browser/browser_fail_uncaught_rejection_expected_multi.js new file mode 100644 index 0000000000..cea5a870aa --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_uncaught_rejection_expected_multi.js @@ -0,0 +1,11 @@ +setExpectedFailuresForSelfTest(1); + +// The test will fail because an expected uncaught rejection is actually caught. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.expectUncaughtRejection(/Promise rejection./); + +function test() { + Promise.reject(new Error("Promise rejection.")).catch(() => {}); +} diff --git a/testing/mochitest/tests/browser/browser_fail_unexpectedTimeout.js b/testing/mochitest/tests/browser/browser_fail_unexpectedTimeout.js new file mode 100644 index 0000000000..d0aef231bd --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_unexpectedTimeout.js @@ -0,0 +1,14 @@ +function test() { + function message() { + info("This should delay timeout"); + } + function end() { + ok(true, "Should have not timed out, but notified long running test"); + finish(); + } + waitForExplicitFinish(); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(message, 20000); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(end, 40000); +} diff --git a/testing/mochitest/tests/browser/browser_getTestFile.js b/testing/mochitest/tests/browser/browser_getTestFile.js new file mode 100644 index 0000000000..154bf716da --- /dev/null +++ b/testing/mochitest/tests/browser/browser_getTestFile.js @@ -0,0 +1,37 @@ +add_task(async function test() { + SimpleTest.doesThrow(function () { + getTestFilePath("/browser_getTestFile.js"); + }, "getTestFilePath rejects absolute paths"); + + await Promise.all([ + IOUtils.exists(getTestFilePath("browser_getTestFile.js")).then(function ( + exists + ) { + ok(exists, "getTestFilePath consider the path as being relative"); + }), + + IOUtils.exists(getTestFilePath("./browser_getTestFile.js")).then(function ( + exists + ) { + ok(exists, "getTestFilePath also accepts explicit relative path"); + }), + + IOUtils.exists(getTestFilePath("./browser_getTestFileTypo.xul")).then( + function (exists) { + ok(!exists, "getTestFilePath do not throw if the file doesn't exists"); + } + ), + + IOUtils.readUTF8(getTestFilePath("test-dir/test-file")).then(function ( + content + ) { + is(content, "foo\n", "getTestFilePath can reach sub-folder files 1/2"); + }), + + IOUtils.readUTF8(getTestFilePath("./test-dir/test-file")).then(function ( + content + ) { + is(content, "foo\n", "getTestFilePath can reach sub-folder files 2/2"); + }), + ]); +}); diff --git a/testing/mochitest/tests/browser/browser_head.js b/testing/mochitest/tests/browser/browser_head.js new file mode 100644 index 0000000000..3e3893f2d4 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_head.js @@ -0,0 +1,16 @@ +var testVar; + +registerCleanupFunction(function () { + ok(true, "I'm a cleanup function in test file"); + is( + this.testVar, + "I'm a var in test file", + "Test cleanup function scope is correct" + ); +}); + +function test() { + is(headVar, "I'm a var in head file", "Head variables are set"); + ok(headMethod(), "Head methods are imported"); + testVar = "I'm a var in test file"; +} diff --git a/testing/mochitest/tests/browser/browser_parameters.js b/testing/mochitest/tests/browser/browser_parameters.js new file mode 100644 index 0000000000..813cd662ba --- /dev/null +++ b/testing/mochitest/tests/browser/browser_parameters.js @@ -0,0 +1,3 @@ +function test() { + ok(SimpleTest.harnessParameters, "Should have parameters"); +} diff --git a/testing/mochitest/tests/browser/browser_pass.js b/testing/mochitest/tests/browser/browser_pass.js new file mode 100644 index 0000000000..e512ff374a --- /dev/null +++ b/testing/mochitest/tests/browser/browser_pass.js @@ -0,0 +1,13 @@ +function test() { + SimpleTest.requestCompleteLog(); + ok(true, "pass ok"); + is(true, true, "pass is"); + isnot(false, true, "pass isnot"); + todo(false, "pass todo"); + todo_is(false, true, "pass todo_is"); + todo_isnot(true, true, "pass todo_isnot"); + info("info message"); + + var func = is; + func(true, true, "pass indirect is"); +} diff --git a/testing/mochitest/tests/browser/browser_privileges.js b/testing/mochitest/tests/browser/browser_privileges.js new file mode 100644 index 0000000000..042a928b9c --- /dev/null +++ b/testing/mochitest/tests/browser/browser_privileges.js @@ -0,0 +1,17 @@ +function test() { + // simple test to confirm we have chrome privileges + let hasPrivileges = true; + + // this will throw an exception if we are not running with privileges + try { + // eslint-disable-next-line no-unused-vars, mozilla/use-services + var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + } catch (e) { + hasPrivileges = false; + } + + // if we get here, we must have chrome privileges + ok(hasPrivileges, "running with chrome privileges"); +} diff --git a/testing/mochitest/tests/browser/browser_requestLongerTimeout.js b/testing/mochitest/tests/browser/browser_requestLongerTimeout.js new file mode 100644 index 0000000000..4107e11fd0 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_requestLongerTimeout.js @@ -0,0 +1,10 @@ +function test() { + requestLongerTimeout(2); + function end() { + ok(true, "should not time out"); + finish(); + } + waitForExplicitFinish(); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(end, 40000); +} diff --git a/testing/mochitest/tests/browser/browser_sanityException.js b/testing/mochitest/tests/browser/browser_sanityException.js new file mode 100644 index 0000000000..2678dd80ad --- /dev/null +++ b/testing/mochitest/tests/browser/browser_sanityException.js @@ -0,0 +1,5 @@ +function test() { + ok(true, "ok called"); + expectUncaughtException(); + throw new Error("this is a deliberately thrown exception"); +} diff --git a/testing/mochitest/tests/browser/browser_sanityException2.js b/testing/mochitest/tests/browser/browser_sanityException2.js new file mode 100644 index 0000000000..4512ed982b --- /dev/null +++ b/testing/mochitest/tests/browser/browser_sanityException2.js @@ -0,0 +1,11 @@ +function test() { + waitForExplicitFinish(); + ok(true, "ok called"); + executeSoon(function () { + expectUncaughtException(); + throw new Error("this is a deliberately thrown exception"); + }); + executeSoon(function () { + finish(); + }); +} diff --git a/testing/mochitest/tests/browser/browser_setup_runs_first.js b/testing/mochitest/tests/browser/browser_setup_runs_first.js new file mode 100644 index 0000000000..6b9ce145da --- /dev/null +++ b/testing/mochitest/tests/browser/browser_setup_runs_first.js @@ -0,0 +1,14 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let someVar = 1; + +add_task(() => { + is(someVar, 2, "Should get updated by setup which runs first."); +}); + +add_setup(() => { + someVar = 2; +}); diff --git a/testing/mochitest/tests/browser/browser_setup_runs_for_only_tests.js b/testing/mochitest/tests/browser/browser_setup_runs_for_only_tests.js new file mode 100644 index 0000000000..d1b811b2d1 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_setup_runs_for_only_tests.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let someVar = 1; + +add_setup(() => { + someVar = 2; +}); + +/* eslint-disable mozilla/reject-addtask-only */ +add_task(() => { + is(someVar, 2, "Setup should have run, even though this is the only test."); +}).only(); diff --git a/testing/mochitest/tests/browser/browser_tasks_skip.js b/testing/mochitest/tests/browser/browser_tasks_skip.js new file mode 100644 index 0000000000..99f3e8d2c2 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_tasks_skip.js @@ -0,0 +1,21 @@ +"use strict"; + +add_task(async function skipMeNot1() { + Assert.ok(true, "Well well well."); +}); + +add_task(async function skipMe1() { + Assert.ok(false, "Not skipped after all."); +}).skip(); + +add_task(async function skipMeNot2() { + Assert.ok(true, "Well well well."); +}); + +add_task(async function skipMeNot3() { + Assert.ok(true, "Well well well."); +}); + +add_task(async function skipMe2() { + Assert.ok(false, "Not skipped after all."); +}).skip(); diff --git a/testing/mochitest/tests/browser/browser_tasks_skipall.js b/testing/mochitest/tests/browser/browser_tasks_skipall.js new file mode 100644 index 0000000000..13290e58ba --- /dev/null +++ b/testing/mochitest/tests/browser/browser_tasks_skipall.js @@ -0,0 +1,23 @@ +"use strict"; + +/* eslint-disable mozilla/reject-addtask-only */ + +add_task(async function skipMe1() { + Assert.ok(false, "Not skipped after all."); +}); + +add_task(async function skipMe2() { + Assert.ok(false, "Not skipped after all."); +}).skip(); + +add_task(async function skipMe3() { + Assert.ok(false, "Not skipped after all."); +}).only(); + +add_task(async function skipMeNot() { + Assert.ok(true, "Well well well."); +}).only(); + +add_task(async function skipMe4() { + Assert.ok(false, "Not skipped after all."); +}); diff --git a/testing/mochitest/tests/browser/browser_uncaught_rejection_expected.js b/testing/mochitest/tests/browser/browser_uncaught_rejection_expected.js new file mode 100644 index 0000000000..2ee9f23d79 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_uncaught_rejection_expected.js @@ -0,0 +1,12 @@ +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally(/Allowed rejection./); +PromiseTestUtils.expectUncaughtRejection(/Promise rejection./); +PromiseTestUtils.expectUncaughtRejection(/Promise rejection./); + +function test() { + Promise.reject(new Error("Promise rejection.")); + Promise.reject(new Error("Promise rejection.")); + Promise.reject(new Error("Allowed rejection.")); +} diff --git a/testing/mochitest/tests/browser/browser_waitForFocus.js b/testing/mochitest/tests/browser/browser_waitForFocus.js new file mode 100644 index 0000000000..b41b07f423 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_waitForFocus.js @@ -0,0 +1,160 @@ +const gBaseURL = "https://example.com/browser/testing/mochitest/tests/browser/"; + +function promiseTabLoadEvent(tab, url) { + let promise = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url); + if (url) { + tab.linkedBrowser.loadURI(Services.io.newURI(url)); + } + return promise; +} + +// Load a new blank tab +add_task(async function () { + await BrowserTestUtils.openNewForegroundTab(gBrowser); + + gURLBar.focus(); + + let browser = gBrowser.selectedBrowser; + await SimpleTest.promiseFocus(browser, true); + + is( + document.activeElement, + browser, + "Browser is focused when about:blank is loaded" + ); + + gBrowser.removeCurrentTab(); + gURLBar.focus(); +}); + +add_task(async function () { + await BrowserTestUtils.openNewForegroundTab(gBrowser); + + gURLBar.focus(); + + let browser = gBrowser.selectedBrowser; + // If we're running in e10s, we don't have access to the content + // window, so only test window arguments in non-e10s mode. + if (browser.contentWindow) { + await SimpleTest.promiseFocus(browser.contentWindow, true); + + is( + document.activeElement, + browser, + "Browser is focused when about:blank is loaded" + ); + } + + gBrowser.removeCurrentTab(); + gURLBar.focus(); +}); + +// Load a tab with a subframe inside it and wait until the subframe is focused +add_task(async function () { + let tab = BrowserTestUtils.addTab(gBrowser); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + // If we're running in e10s, we don't have access to the content + // window, so only test <iframe> arguments in non-e10s mode. + if (browser.contentWindow) { + await promiseTabLoadEvent(tab, gBaseURL + "waitForFocusPage.html"); + + await SimpleTest.promiseFocus(browser.contentWindow); + + is( + document.activeElement, + browser, + "Browser is focused when page is loaded" + ); + + await SimpleTest.promiseFocus(browser.contentWindow.frames[0]); + + is( + browser.contentWindow.document.activeElement.localName, + "iframe", + "Child iframe is focused" + ); + } + + gBrowser.removeCurrentTab(); +}); + +// Pass a browser to promiseFocus +add_task(async function () { + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + gBaseURL + "waitForFocusPage.html" + ); + + gURLBar.focus(); + + await SimpleTest.promiseFocus(gBrowser.selectedBrowser); + + is( + document.activeElement, + gBrowser.selectedBrowser, + "Browser is focused when promiseFocus is passed a browser" + ); + + gBrowser.removeCurrentTab(); +}); + +// Tests focusing the sidebar, which is in a parent process subframe +// and then switching the focus to another window. +add_task(async function () { + await SidebarUI.show("viewBookmarksSidebar"); + + gURLBar.focus(); + + // Focus the sidebar. + await SimpleTest.promiseFocus(SidebarUI.browser); + is( + document.activeElement, + document.getElementById("sidebar"), + "sidebar focused" + ); + ok( + document.activeElement.contentDocument.hasFocus(), + "sidebar document hasFocus" + ); + + // Focus the sidebar again, which should cause no change. + await SimpleTest.promiseFocus(SidebarUI.browser); + is( + document.activeElement, + document.getElementById("sidebar"), + "sidebar focused" + ); + ok( + document.activeElement.contentDocument.hasFocus(), + "sidebar document hasFocus" + ); + + // Focus another window. The sidebar should no longer be focused. + let window2 = await BrowserTestUtils.openNewBrowserWindow(); + is( + document.activeElement, + document.getElementById("sidebar"), + "sidebar focused after window 2 opened" + ); + ok( + !document.activeElement.contentDocument.hasFocus(), + "sidebar document hasFocus after window 2 opened" + ); + + // Focus the first window again and the sidebar should be focused again. + await SimpleTest.promiseFocus(window); + is( + document.activeElement, + document.getElementById("sidebar"), + "sidebar focused after window1 refocused" + ); + ok( + document.activeElement.contentDocument.hasFocus(), + "sidebar document hasFocus after window1 refocused" + ); + + await BrowserTestUtils.closeWindow(window2); + await SidebarUI.hide(); +}); diff --git a/testing/mochitest/tests/browser/browser_zz_fail_openwindow.js b/testing/mochitest/tests/browser/browser_zz_fail_openwindow.js new file mode 100644 index 0000000000..2f7fb04d78 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_zz_fail_openwindow.js @@ -0,0 +1,13 @@ +function test() { + waitForExplicitFinish(); + function done() { + ok(true, "timeout ran"); + finish(); + } + + ok(OpenBrowserWindow(), "opened browser window"); + // and didn't close it! + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(done, 10000); +} diff --git a/testing/mochitest/tests/browser/dummy.html b/testing/mochitest/tests/browser/dummy.html new file mode 100644 index 0000000000..7954185285 --- /dev/null +++ b/testing/mochitest/tests/browser/dummy.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> + <meta http-equiv="Content-Security-Policy" content="default-src 'none'; object-src 'none'"></meta> + <title>This is a dummy page</title> + <meta charset="utf-8"> + <body>This is a dummy page</body> +</html> diff --git a/testing/mochitest/tests/browser/head.js b/testing/mochitest/tests/browser/head.js new file mode 100644 index 0000000000..fdfd097176 --- /dev/null +++ b/testing/mochitest/tests/browser/head.js @@ -0,0 +1,16 @@ +var headVar = "I'm a var in head file"; + +function headMethod() { + return true; +} + +ok(true, "I'm a test in head file"); + +registerCleanupFunction(function () { + ok(true, "I'm a cleanup function in head file"); + is( + this.headVar, + "I'm a var in head file", + "Head cleanup function scope is correct" + ); +}); diff --git a/testing/mochitest/tests/browser/test-dir/test-file b/testing/mochitest/tests/browser/test-dir/test-file new file mode 100644 index 0000000000..257cc5642c --- /dev/null +++ b/testing/mochitest/tests/browser/test-dir/test-file @@ -0,0 +1 @@ +foo diff --git a/testing/mochitest/tests/browser/waitForFocusPage.html b/testing/mochitest/tests/browser/waitForFocusPage.html new file mode 100644 index 0000000000..286ad7849c --- /dev/null +++ b/testing/mochitest/tests/browser/waitForFocusPage.html @@ -0,0 +1,4 @@ +<body> + <input> + <iframe id="f" src="data:text/plain,Test" width=80 height=80></iframe> +</body> diff --git a/testing/mochitest/tests/moz.build b/testing/mochitest/tests/moz.build new file mode 100644 index 0000000000..e265d56c14 --- /dev/null +++ b/testing/mochitest/tests/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += ["SimpleTest"] + +MOCHITEST_MANIFESTS += ["Harness_sanity/mochitest.toml"] +XPCSHELL_TESTS_MANIFESTS += ["Harness_sanity/xpcshell.toml"] +BROWSER_CHROME_MANIFESTS += ["browser/browser.toml"] diff --git a/testing/mochitest/tests/python/conftest.py b/testing/mochitest/tests/python/conftest.py new file mode 100644 index 0000000000..308b9d99cb --- /dev/null +++ b/testing/mochitest/tests/python/conftest.py @@ -0,0 +1,167 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import json +import os +from argparse import Namespace + +import mozinfo +import pytest +import six +from manifestparser import TestManifest, expression +from moztest.selftest.fixtures import binary_fixture, setup_test_harness # noqa + +here = os.path.abspath(os.path.dirname(__file__)) +setup_args = [os.path.join(here, "files"), "mochitest", "testing/mochitest"] + + +@pytest.fixture +def create_manifest(tmpdir, build_obj): + def inner(string, name="manifest.ini"): + manifest = tmpdir.join(name) + manifest.write(string, ensure=True) + # pylint --py3k: W1612 + path = six.text_type(manifest) + return TestManifest(manifests=(path,), strict=False, rootdir=tmpdir.strpath) + + return inner + + +@pytest.fixture(scope="function") +def parser(request): + parser = pytest.importorskip("mochitest_options") + + app = getattr(request.module, "APP", "generic") + return parser.MochitestArgumentParser(app=app) + + +@pytest.fixture(scope="function") +def runtests(setup_test_harness, binary, parser, request): + """Creates an easy to use entry point into the mochitest harness. + + :returns: A function with the signature `*tests, **opts`. Each test is a file name + (relative to the `files` dir). At least one is required. The opts are + used to override the default mochitest options, they are optional. + """ + flavor = "plain" + if "flavor" in request.fixturenames: + flavor = request.getfixturevalue("flavor") + + runFailures = "" + if "runFailures" in request.fixturenames: + runFailures = request.getfixturevalue("runFailures") + + restartAfterFailure = False + if "restartAfterFailure" in request.fixturenames: + restartAfterFailure = request.getfixturevalue("restartAfterFailure") + + setup_test_harness(*setup_args, flavor=flavor) + + runtests = pytest.importorskip("runtests") + + mochitest_root = runtests.SCRIPT_DIR + if flavor == "plain": + test_root = os.path.join(mochitest_root, "tests", "selftests") + manifest_name = "mochitest.ini" + elif flavor == "browser-chrome": + test_root = os.path.join(mochitest_root, "browser", "tests", "selftests") + manifest_name = "browser.ini" + else: + raise Exception(f"Invalid flavor {flavor}!") + + # pylint --py3k: W1648 + buf = six.StringIO() + options = vars(parser.parse_args([])) + options.update( + { + "app": binary, + "flavor": flavor, + "runFailures": runFailures, + "restartAfterFailure": restartAfterFailure, + "keep_open": False, + "log_raw": [buf], + } + ) + + if runFailures == "selftest": + options["crashAsPass"] = True + options["timeoutAsPass"] = True + runtests.mozinfo.update({"selftest": True}) + + if not os.path.isdir(runtests.build_obj.bindir): + package_root = os.path.dirname(mochitest_root) + options.update( + { + "certPath": os.path.join(package_root, "certs"), + "utilityPath": os.path.join(package_root, "bin"), + } + ) + options["extraProfileFiles"].append( + os.path.join(package_root, "bin", "plugins") + ) + + options.update(getattr(request.module, "OPTIONS", {})) + + def normalize(test): + if isinstance(test, str): + test = [test] + return [ + { + "name": t, + "relpath": t, + "path": os.path.join(test_root, t), + # add a dummy manifest file because mochitest expects it + "manifest": os.path.join(test_root, manifest_name), + "manifest_relpath": manifest_name, + "skip-if": runFailures, + } + for t in test + ] + + def inner(*tests, **opts): + assert len(tests) > 0 + + # Inject a TestManifest in the runtests option if one + # has not been already included by the caller. + if not isinstance(options["manifestFile"], TestManifest): + manifest = TestManifest() + options["manifestFile"] = manifest + # pylint --py3k: W1636 + manifest.tests.extend(list(map(normalize, tests))[0]) + options.update(opts) + + result = runtests.run_test_harness(parser, Namespace(**options)) + out = json.loads("[" + ",".join(buf.getvalue().splitlines()) + "]") + buf.close() + return result, out + + return inner + + +@pytest.fixture +def build_obj(setup_test_harness): + setup_test_harness(*setup_args) + mochitest_options = pytest.importorskip("mochitest_options") + return mochitest_options.build_obj + + +@pytest.fixture(autouse=True) +def skip_using_mozinfo(request, setup_test_harness): + """Gives tests the ability to skip based on values from mozinfo. + + Example: + @pytest.mark.skip_mozinfo("!e10s || os == 'linux'") + def test_foo(): + pass + """ + + setup_test_harness(*setup_args) + runtests = pytest.importorskip("runtests") + runtests.update_mozinfo() + + skip_mozinfo = request.node.get_closest_marker("skip_mozinfo") + if skip_mozinfo: + value = skip_mozinfo.args[0] + if expression.parse(value, **mozinfo.info): + pytest.skip("skipped due to mozinfo match: \n{}".format(value)) diff --git a/testing/mochitest/tests/python/files/browser-args.toml b/testing/mochitest/tests/python/files/browser-args.toml new file mode 100644 index 0000000000..7490bea3c8 --- /dev/null +++ b/testing/mochitest/tests/python/files/browser-args.toml @@ -0,0 +1,8 @@ +[DEFAULT] +args = [ + "--headless", + "--window-size=800,600", + "--new-tab http://example.org", +] + +["browser_pass.js"] diff --git a/testing/mochitest/tests/python/files/browser_assertion.js b/testing/mochitest/tests/python/files/browser_assertion.js new file mode 100644 index 0000000000..243703206e --- /dev/null +++ b/testing/mochitest/tests/python/files/browser_assertion.js @@ -0,0 +1,7 @@ +function test() { + const Cc = SpecialPowers.Cc; + const Ci = SpecialPowers.Ci; + let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); + debug.assertion("failed assertion check", "false", "test_assertion.js", 15); + ok(true, "Should pass"); +}
\ No newline at end of file diff --git a/testing/mochitest/tests/python/files/browser_crash.js b/testing/mochitest/tests/python/files/browser_crash.js new file mode 100644 index 0000000000..54e431ed7f --- /dev/null +++ b/testing/mochitest/tests/python/files/browser_crash.js @@ -0,0 +1,7 @@ +function test() { + const Cc = SpecialPowers.Cc; + const Ci = SpecialPowers.Ci; + let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); + debug.abort("test_crash.js", 5); + ok(false, "Should pass"); +}
\ No newline at end of file diff --git a/testing/mochitest/tests/python/files/browser_fail.js b/testing/mochitest/tests/python/files/browser_fail.js new file mode 100644 index 0000000000..abcb6dae60 --- /dev/null +++ b/testing/mochitest/tests/python/files/browser_fail.js @@ -0,0 +1,3 @@ +function test() { + ok(false, "Test is ok"); +} diff --git a/testing/mochitest/tests/python/files/browser_fail2.js b/testing/mochitest/tests/python/files/browser_fail2.js new file mode 100644 index 0000000000..abcb6dae60 --- /dev/null +++ b/testing/mochitest/tests/python/files/browser_fail2.js @@ -0,0 +1,3 @@ +function test() { + ok(false, "Test is ok"); +} diff --git a/testing/mochitest/tests/python/files/browser_leak.js b/testing/mochitest/tests/python/files/browser_leak.js new file mode 100644 index 0000000000..ded8dd8b56 --- /dev/null +++ b/testing/mochitest/tests/python/files/browser_leak.js @@ -0,0 +1,4 @@ +function test() { + SpecialPowers.Cu.intentionallyLeak(); + ok(true, "Test is ok"); +} diff --git a/testing/mochitest/tests/python/files/browser_pass.js b/testing/mochitest/tests/python/files/browser_pass.js new file mode 100644 index 0000000000..5e5c567f13 --- /dev/null +++ b/testing/mochitest/tests/python/files/browser_pass.js @@ -0,0 +1,3 @@ +function test() { + ok(true, "Test is OK"); +}
\ No newline at end of file diff --git a/testing/mochitest/tests/python/files/mochitest-args.ini b/testing/mochitest/tests/python/files/mochitest-args.ini new file mode 100644 index 0000000000..9c3d44d05f --- /dev/null +++ b/testing/mochitest/tests/python/files/mochitest-args.ini @@ -0,0 +1,7 @@ +[DEFAULT] +args = + --headless + --window-size=800,600 + --new-tab http://example.org + +[test_pass.html] diff --git a/testing/mochitest/tests/python/files/mochitest-dupemanifest-1.ini b/testing/mochitest/tests/python/files/mochitest-dupemanifest-1.ini new file mode 100644 index 0000000000..35d66d765c --- /dev/null +++ b/testing/mochitest/tests/python/files/mochitest-dupemanifest-1.ini @@ -0,0 +1 @@ +[test_pass.html] diff --git a/testing/mochitest/tests/python/files/mochitest-dupemanifest-2.ini b/testing/mochitest/tests/python/files/mochitest-dupemanifest-2.ini new file mode 100644 index 0000000000..35d66d765c --- /dev/null +++ b/testing/mochitest/tests/python/files/mochitest-dupemanifest-2.ini @@ -0,0 +1 @@ +[test_pass.html] diff --git a/testing/mochitest/tests/python/files/test_assertion.html b/testing/mochitest/tests/python/files/test_assertion.html new file mode 100644 index 0000000000..7740064107 --- /dev/null +++ b/testing/mochitest/tests/python/files/test_assertion.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1343659 +--> +<head> + <meta charset="utf-8"> + <title>Test Assertion</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + const Cc = SpecialPowers.Cc; + const Ci = SpecialPowers.Ci; + let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); + debug.assertion("failed assertion check", "false", "test_assertion.html", 15); + ok(true, "Should pass"); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1343659">Mozilla Bug 1343659</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/python/files/test_crash.html b/testing/mochitest/tests/python/files/test_crash.html new file mode 100644 index 0000000000..09ea2faf01 --- /dev/null +++ b/testing/mochitest/tests/python/files/test_crash.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1343659 +--> +<head> + <meta charset="utf-8"> + <title>Test Crash</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + const Cc = SpecialPowers.Cc; + const Ci = SpecialPowers.Ci; + let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); + debug.abort("test_crash.html", 15); + ok(true, "Should pass"); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1343659">Mozilla Bug 1343659</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/python/files/test_fail.html b/testing/mochitest/tests/python/files/test_fail.html new file mode 100644 index 0000000000..3d0555a5a0 --- /dev/null +++ b/testing/mochitest/tests/python/files/test_fail.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1343659 +--> +<head> + <meta charset="utf-8"> + <title>Test Fail</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + ok(false, "Test is ok"); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1343659">Mozilla Bug 1343659</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/python/files/test_fail2.html b/testing/mochitest/tests/python/files/test_fail2.html new file mode 100644 index 0000000000..3d0555a5a0 --- /dev/null +++ b/testing/mochitest/tests/python/files/test_fail2.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1343659 +--> +<head> + <meta charset="utf-8"> + <title>Test Fail</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + ok(false, "Test is ok"); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1343659">Mozilla Bug 1343659</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/python/files/test_leak.html b/testing/mochitest/tests/python/files/test_leak.html new file mode 100644 index 0000000000..4609e368de --- /dev/null +++ b/testing/mochitest/tests/python/files/test_leak.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1521223 +--> +<head> + <meta charset="utf-8"> + <title>Test Pass</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + SpecialPowers.Cu.intentionallyLeak(); + ok(true, "Test is ok"); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1521223">Mozilla Bug 1521223</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/python/files/test_pass.html b/testing/mochitest/tests/python/files/test_pass.html new file mode 100644 index 0000000000..9dacafaaa3 --- /dev/null +++ b/testing/mochitest/tests/python/files/test_pass.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1343659 +--> +<head> + <meta charset="utf-8"> + <title>Test Pass</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + ok(true, "Test is ok"); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1343659">Mozilla Bug 1343659</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/testing/mochitest/tests/python/python.toml b/testing/mochitest/tests/python/python.toml new file mode 100644 index 0000000000..d82844144e --- /dev/null +++ b/testing/mochitest/tests/python/python.toml @@ -0,0 +1,13 @@ +[DEFAULT] +subsuite = "mochitest" + +["test_build_profile.py"] + +["test_create_directories.py"] + +["test_get_active_tests.py"] + +["test_message_logger.py"] + +["test_mochitest_integration.py"] +sequential = true diff --git a/testing/mochitest/tests/python/test_build_profile.py b/testing/mochitest/tests/python/test_build_profile.py new file mode 100644 index 0000000000..32fc938d73 --- /dev/null +++ b/testing/mochitest/tests/python/test_build_profile.py @@ -0,0 +1,79 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import json +import os +from argparse import Namespace + +import mozunit +import pytest +from conftest import setup_args +from mozbuild.base import MozbuildObject +from mozprofile import Profile +from mozprofile.prefs import Preferences +from six import string_types + +here = os.path.abspath(os.path.dirname(__file__)) + + +@pytest.fixture +def build_profile(monkeypatch, setup_test_harness, parser): + setup_test_harness(*setup_args) + runtests = pytest.importorskip("runtests") + md = runtests.MochitestDesktop("plain", {"log_tbpl": "-"}) + monkeypatch.setattr(md, "fillCertificateDB", lambda *args, **kwargs: None) + + options = parser.parse_args([]) + options = vars(options) + + def inner(**kwargs): + opts = options.copy() + opts.update(kwargs) + + return md, md.buildProfile(Namespace(**opts)) + + return inner + + +@pytest.fixture +def profile_data_dir(): + build = MozbuildObject.from_environment(cwd=here) + return os.path.join(build.topsrcdir, "testing", "profiles") + + +def test_common_prefs_are_all_set(build_profile, profile_data_dir): + md, result = build_profile() + + with open(os.path.join(profile_data_dir, "profiles.json"), "r") as fh: + base_profiles = json.load(fh)["mochitest"] + + # build the expected prefs + expected_prefs = {} + for profile in base_profiles: + for name in Profile.preference_file_names: + path = os.path.join(profile_data_dir, profile, name) + if os.path.isfile(path): + expected_prefs.update(Preferences.read_prefs(path)) + + # read the actual prefs + actual_prefs = {} + for name in Profile.preference_file_names: + path = os.path.join(md.profile.profile, name) + if os.path.isfile(path): + actual_prefs.update(Preferences.read_prefs(path)) + + # keep this in sync with the values in MochitestDesktop.merge_base_profiles + interpolation = { + "server": "127.0.0.1:8888", + } + for k, v in expected_prefs.items(): + if isinstance(v, string_types): + v = v.format(**interpolation) + + assert k in actual_prefs + assert k and actual_prefs[k] == v + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mochitest/tests/python/test_create_directories.py b/testing/mochitest/tests/python/test_create_directories.py new file mode 100644 index 0000000000..ffbed625a5 --- /dev/null +++ b/testing/mochitest/tests/python/test_create_directories.py @@ -0,0 +1,221 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import unittest.mock as mock +from argparse import Namespace +from collections import defaultdict +from textwrap import dedent + +import mozunit +import pytest +import six +from conftest import setup_args +from manifestparser import TestManifest + + +# Directly running runTests() is likely not working nor a good idea +# So at least we try to minimize with just: +# - getActiveTests() +# - create manifests list +# - parseAndCreateTestsDirs() +# +# Hopefully, breaking the runTests() calls to parseAndCreateTestsDirs() will +# anyway trigger other tests failures so it would be spotted, and we at least +# ensure some coverage of handling the manifest content, creation of the +# directories and cleanup +@pytest.fixture +def prepareRunTests(setup_test_harness, parser): + setup_test_harness(*setup_args) + runtests = pytest.importorskip("runtests") + md = runtests.MochitestDesktop("plain", {"log_tbpl": "-"}) + + options = vars(parser.parse_args([])) + + def inner(**kwargs): + opts = options.copy() + opts.update(kwargs) + + manifest = opts.get("manifestFile") + if isinstance(manifest, six.string_types): + md.testRootAbs = os.path.dirname(manifest) + elif isinstance(manifest, TestManifest): + md.testRootAbs = manifest.rootdir + + md._active_tests = None + md.prefs_by_manifest = defaultdict(set) + tests = md.getActiveTests(Namespace(**opts)) + manifests = set(t["manifest"] for t in tests) + for m in sorted(manifests): + md.parseAndCreateTestsDirs(m) + return md + + return inner + + +@pytest.fixture +def create_manifest(tmpdir, build_obj): + def inner(string, name="manifest.ini"): + manifest = tmpdir.join(name) + manifest.write(string, ensure=True) + # pylint --py3k: W1612 + path = six.text_type(manifest) + return TestManifest(manifests=(path,), strict=False, rootdir=tmpdir.strpath) + + return inner + + +def create_manifest_empty(create_manifest): + manifest = create_manifest( + dedent( + """ + [DEFAULT] + [files/test_pass.html] + [files/test_fail.html] + """ + ) + ) + + return { + "runByManifest": True, + "manifestFile": manifest, + } + + +def create_manifest_one(create_manifest): + manifest = create_manifest( + dedent( + """ + [DEFAULT] + test-directories = + .snap_firefox_current_real + [files/test_pass.html] + [files/test_fail.html] + """ + ) + ) + + return { + "runByManifest": True, + "manifestFile": manifest, + } + + +def create_manifest_mult(create_manifest): + manifest = create_manifest( + dedent( + """ + [DEFAULT] + test-directories = + .snap_firefox_current_real + .snap_firefox_current_real2 + [files/test_pass.html] + [files/test_fail.html] + """ + ) + ) + + return { + "runByManifest": True, + "manifestFile": manifest, + } + + +def test_no_entry(prepareRunTests, create_manifest): + options = create_manifest_empty(create_manifest) + with mock.patch("os.makedirs") as mock_os_makedirs: + _ = prepareRunTests(**options) + mock_os_makedirs.assert_not_called() + + +def test_one_entry(prepareRunTests, create_manifest): + options = create_manifest_one(create_manifest) + with mock.patch("os.makedirs") as mock_os_makedirs: + md = prepareRunTests(**options) + mock_os_makedirs.assert_called_once_with(".snap_firefox_current_real") + + opts = mock.Mock(pidFile="") # so cleanup() does not check it + with mock.patch("os.path.exists") as mock_os_path_exists, mock.patch( + "shutil.rmtree" + ) as mock_shutil_rmtree: + md.cleanup(opts, False) + mock_os_path_exists.assert_called_once_with(".snap_firefox_current_real") + mock_shutil_rmtree.assert_called_once_with(".snap_firefox_current_real") + + +def test_one_entry_already_exists(prepareRunTests, create_manifest): + options = create_manifest_one(create_manifest) + with mock.patch( + "os.path.exists", return_value=True + ) as mock_os_path_exists, mock.patch("os.makedirs") as mock_os_makedirs: + with pytest.raises(FileExistsError): + _ = prepareRunTests(**options) + mock_os_path_exists.assert_called_once_with(".snap_firefox_current_real") + mock_os_makedirs.assert_not_called() + + +def test_mult_entry(prepareRunTests, create_manifest): + options = create_manifest_mult(create_manifest) + with mock.patch("os.makedirs") as mock_os_makedirs: + md = prepareRunTests(**options) + assert mock_os_makedirs.call_count == 2 + mock_os_makedirs.assert_has_calls( + [ + mock.call(".snap_firefox_current_real"), + mock.call(".snap_firefox_current_real2"), + ] + ) + + opts = mock.Mock(pidFile="") # so cleanup() does not check it + with mock.patch("os.path.exists") as mock_os_path_exists, mock.patch( + "shutil.rmtree" + ) as mock_shutil_rmtree: + md.cleanup(opts, False) + + assert mock_os_path_exists.call_count == 2 + mock_os_path_exists.assert_has_calls( + [ + mock.call(".snap_firefox_current_real"), + mock.call().__bool__(), + mock.call(".snap_firefox_current_real2"), + mock.call().__bool__(), + ] + ) + + assert mock_os_makedirs.call_count == 2 + mock_shutil_rmtree.assert_has_calls( + [ + mock.call(".snap_firefox_current_real"), + mock.call(".snap_firefox_current_real2"), + ] + ) + + +def test_mult_entry_one_already_exists(prepareRunTests, create_manifest): + options = create_manifest_mult(create_manifest) + with mock.patch( + "os.path.exists", side_effect=[True, False] + ) as mock_os_path_exists, mock.patch("os.makedirs") as mock_os_makedirs: + with pytest.raises(FileExistsError): + _ = prepareRunTests(**options) + mock_os_path_exists.assert_called_once_with(".snap_firefox_current_real") + mock_os_makedirs.assert_not_called() + + with mock.patch( + "os.path.exists", side_effect=[False, True] + ) as mock_os_path_exists, mock.patch("os.makedirs") as mock_os_makedirs: + with pytest.raises(FileExistsError): + _ = prepareRunTests(**options) + assert mock_os_path_exists.call_count == 2 + mock_os_path_exists.assert_has_calls( + [ + mock.call(".snap_firefox_current_real"), + mock.call(".snap_firefox_current_real2"), + ] + ) + mock_os_makedirs.assert_not_called() + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mochitest/tests/python/test_get_active_tests.py b/testing/mochitest/tests/python/test_get_active_tests.py new file mode 100644 index 0000000000..f91bc64a5f --- /dev/null +++ b/testing/mochitest/tests/python/test_get_active_tests.py @@ -0,0 +1,269 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +from argparse import Namespace +from collections import defaultdict +from textwrap import dedent + +import mozunit +import pytest +import six +from conftest import setup_args +from manifestparser import TestManifest + + +@pytest.fixture +def get_active_tests(setup_test_harness, parser): + setup_test_harness(*setup_args) + runtests = pytest.importorskip("runtests") + md = runtests.MochitestDesktop("plain", {"log_tbpl": "-"}) + + options = vars(parser.parse_args([])) + + def inner(**kwargs): + opts = options.copy() + opts.update(kwargs) + + manifest = opts.get("manifestFile") + if isinstance(manifest, six.string_types): + md.testRootAbs = os.path.dirname(manifest) + elif isinstance(manifest, TestManifest): + md.testRootAbs = manifest.rootdir + + md._active_tests = None + md.prefs_by_manifest = defaultdict(set) + return md, md.getActiveTests(Namespace(**opts)) + + return inner + + +@pytest.fixture +def create_manifest(tmpdir, build_obj): + def inner(string, name="manifest.ini"): + manifest = tmpdir.join(name) + manifest.write(string, ensure=True) + # pylint --py3k: W1612 + path = six.text_type(manifest) + return TestManifest(manifests=(path,), strict=False, rootdir=tmpdir.strpath) + + return inner + + +def test_args_validation(get_active_tests, create_manifest): + # Test args set in a single manifest. + manifest_relpath = "manifest.ini" + manifest = create_manifest( + dedent( + """ + [DEFAULT] + args= + --cheese + --foo=bar + --foo1 bar1 + + [files/test_pass.html] + [files/test_fail.html] + """ + ) + ) + + options = { + "runByManifest": True, + "manifestFile": manifest, + } + md, tests = get_active_tests(**options) + + assert len(tests) == 2 + assert manifest_relpath in md.args_by_manifest + + args = md.args_by_manifest[manifest_relpath] + assert len(args) == 1 + assert args.pop() == "\n--cheese\n--foo=bar\n--foo1 bar1" + + # Test args set with runByManifest disabled. + options["runByManifest"] = False + with pytest.raises(SystemExit): + get_active_tests(**options) + + # Test args set in non-default section. + options["runByManifest"] = True + options["manifestFile"] = create_manifest( + dedent( + """ + [files/test_pass.html] + args=--foo2=bar2 + [files/test_fail.html] + """ + ) + ) + with pytest.raises(SystemExit): + get_active_tests(**options) + + +def test_args_validation_with_ancestor_manifest(get_active_tests, create_manifest): + # Test args set by an ancestor manifest. + create_manifest( + dedent( + """ + [DEFAULT] + args= + --cheese + + [files/test_pass.html] + [files/test_fail.html] + """ + ), + name="subdir/manifest.ini", + ) + + manifest = create_manifest( + dedent( + """ + [DEFAULT] + args = + --foo=bar + + [include:manifest.ini] + [test_foo.html] + """ + ), + name="subdir/ancestor-manifest.ini", + ) + + options = { + "runByManifest": True, + "manifestFile": manifest, + } + + md, tests = get_active_tests(**options) + assert len(tests) == 3 + + key = os.path.join("subdir", "ancestor-manifest.ini") + assert key in md.args_by_manifest + args = md.args_by_manifest[key] + assert len(args) == 1 + assert args.pop() == "\n--foo=bar" + + key = "{}:{}".format( + os.path.join("subdir", "ancestor-manifest.ini"), + os.path.join("subdir", "manifest.ini"), + ) + assert key in md.args_by_manifest + args = md.args_by_manifest[key] + assert len(args) == 1 + assert args.pop() == "\n--foo=bar \n--cheese" + + +def test_prefs_validation(get_active_tests, create_manifest): + # Test prefs set in a single manifest. + manifest_relpath = "manifest.ini" + manifest = create_manifest( + dedent( + """ + [DEFAULT] + prefs= + foo=bar + browser.dom.foo=baz + + [files/test_pass.html] + [files/test_fail.html] + """ + ) + ) + + options = { + "runByManifest": True, + "manifestFile": manifest, + } + md, tests = get_active_tests(**options) + + assert len(tests) == 2 + assert manifest_relpath in md.prefs_by_manifest + + prefs = md.prefs_by_manifest[manifest_relpath] + assert len(prefs) == 1 + assert prefs.pop() == "\nfoo=bar\nbrowser.dom.foo=baz" + + # Test prefs set with runByManifest disabled. + options["runByManifest"] = False + with pytest.raises(SystemExit): + get_active_tests(**options) + + # Test prefs set in non-default section. + options["runByManifest"] = True + options["manifestFile"] = create_manifest( + dedent( + """ + [files/test_pass.html] + prefs=foo=bar + [files/test_fail.html] + """ + ) + ) + with pytest.raises(SystemExit): + get_active_tests(**options) + + +def test_prefs_validation_with_ancestor_manifest(get_active_tests, create_manifest): + # Test prefs set by an ancestor manifest. + create_manifest( + dedent( + """ + [DEFAULT] + prefs= + foo=bar + browser.dom.foo=baz + + [files/test_pass.html] + [files/test_fail.html] + """ + ), + name="subdir/manifest.ini", + ) + + manifest = create_manifest( + dedent( + """ + [DEFAULT] + prefs = + browser.dom.foo=fleem + flower=rose + + [include:manifest.ini] + [test_foo.html] + """ + ), + name="subdir/ancestor-manifest.ini", + ) + + options = { + "runByManifest": True, + "manifestFile": manifest, + } + + md, tests = get_active_tests(**options) + assert len(tests) == 3 + + key = os.path.join("subdir", "ancestor-manifest.ini") + assert key in md.prefs_by_manifest + prefs = md.prefs_by_manifest[key] + assert len(prefs) == 1 + assert prefs.pop() == "\nbrowser.dom.foo=fleem\nflower=rose" + + key = "{}:{}".format( + os.path.join("subdir", "ancestor-manifest.ini"), + os.path.join("subdir", "manifest.ini"), + ) + assert key in md.prefs_by_manifest + prefs = md.prefs_by_manifest[key] + assert len(prefs) == 1 + assert ( + prefs.pop() + == "\nbrowser.dom.foo=fleem\nflower=rose \nfoo=bar\nbrowser.dom.foo=baz" + ) + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mochitest/tests/python/test_message_logger.py b/testing/mochitest/tests/python/test_message_logger.py new file mode 100644 index 0000000000..60bf6f9dc9 --- /dev/null +++ b/testing/mochitest/tests/python/test_message_logger.py @@ -0,0 +1,191 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +import json +import time +import types + +import mozunit +import pytest +import six +from conftest import setup_args +from mozlog.formatters import JSONFormatter +from mozlog.handlers.base import StreamHandler +from mozlog.structuredlog import StructuredLogger +from six import string_types + + +@pytest.fixture +def logger(): + logger = StructuredLogger("mochitest_message_logger") + + buf = six.StringIO() + handler = StreamHandler(buf, JSONFormatter()) + logger.add_handler(handler) + return logger + + +@pytest.fixture +def get_message_logger(setup_test_harness, logger): + setup_test_harness(*setup_args) + runtests = pytest.importorskip("runtests") + + def fake_message(self, action, **extra): + message = { + "action": action, + "time": time.time(), + } + if action in ("test_start", "test_end", "test_status"): + message["test"] = "test_foo.html" + + if action == "test_end": + message["status"] = "PASS" + message["expected"] = "PASS" + + elif action == "test_status": + message["subtest"] = "bar" + message["status"] = "PASS" + + elif action == "log": + message["level"] = "INFO" + message["message"] = "foobar" + + message.update(**extra) + return self.write(json.dumps(message)) + + def inner(**kwargs): + ml = runtests.MessageLogger(logger, **kwargs) + + # Create a convenience function for faking incoming log messages. + ml.fake_message = types.MethodType(fake_message, ml) + return ml + + return inner + + +@pytest.fixture +def get_lines(logger): + buf = logger.handlers[0].stream + + def inner(): + lines = buf.getvalue().splitlines() + buf.truncate(0) + # Python3 will not reposition the buffer position after + # truncate and will extend the buffer with null bytes. + # Force the buffer position to the start of the buffer + # to prevent null bytes from creeping in. + buf.seek(0) + return lines + + return inner + + +@pytest.fixture +def assert_actions(get_lines): + def inner(expected): + if isinstance(expected, string_types): + expected = [expected] + + lines = get_lines() + actions = [json.loads(l)["action"] for l in lines] + assert actions == expected + + return inner + + +def test_buffering_on(get_message_logger, assert_actions): + ml = get_message_logger(buffering=True) + + # no buffering initially (outside of test) + ml.fake_message("log") + assert_actions(["log"]) + + # inside a test buffering is enabled, only 'test_start' logged + ml.fake_message("test_start") + ml.fake_message("test_status") + ml.fake_message("log") + assert_actions(["test_start"]) + + # buffering turned off manually within a test + ml.fake_message("buffering_off") + ml.fake_message("test_status") + ml.fake_message("log") + assert_actions(["test_status", "log"]) + + # buffering turned back on again + ml.fake_message("buffering_on") + ml.fake_message("test_status") + ml.fake_message("log") + assert_actions([]) + + # test end, it failed! All previsouly buffered messages are now logged. + ml.fake_message("test_end", status="FAIL") + assert_actions( + [ + "log", # "Buffered messages logged" + "test_status", + "log", + "test_status", + "log", + "log", # "Buffered messages finished" + "test_end", + ] + ) + + # enabling buffering outside of a test has no affect + ml.fake_message("buffering_on") + ml.fake_message("log") + ml.fake_message("test_status") + assert_actions(["log", "test_status"]) + + +def test_buffering_off(get_message_logger, assert_actions): + ml = get_message_logger(buffering=False) + + ml.fake_message("test_start") + assert_actions(["test_start"]) + + # messages logged no matter what the state + ml.fake_message("test_status") + ml.fake_message("buffering_off") + ml.fake_message("log") + assert_actions(["test_status", "log"]) + + # even after a 'buffering_on' action + ml.fake_message("buffering_on") + ml.fake_message("test_status") + ml.fake_message("log") + assert_actions(["test_status", "log"]) + + # no buffer to empty on test fail + ml.fake_message("test_end", status="FAIL") + assert_actions(["test_end"]) + + +@pytest.mark.parametrize( + "name,expected", + ( + ("/tests/test_foo.html", "test_foo.html"), + ("chrome://mochitests/content/a11y/test_foo.html", "test_foo.html"), + ("chrome://mochitests/content/browser/test_foo.html", "test_foo.html"), + ("chrome://mochitests/content/chrome/test_foo.html", "test_foo.html"), + ( + "https://example.org:443/tests/netwerk/test_foo.html", + "netwerk/test_foo.html", + ), + ("http://mochi.test:8888/tests/test_foo.html", "test_foo.html"), + ("http://mochi.test:8888/content/dom/browser/test_foo.html", None), + ), +) +def test_test_names_fixed_to_be_relative(name, expected, get_message_logger, get_lines): + ml = get_message_logger(buffering=False) + ml.fake_message("test_start", test=name) + lines = get_lines() + + if expected is None: + expected = name + assert json.loads(lines[0])["test"] == expected + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mochitest/tests/python/test_mochitest_integration.py b/testing/mochitest/tests/python/test_mochitest_integration.py new file mode 100644 index 0000000000..f881ea3f30 --- /dev/null +++ b/testing/mochitest/tests/python/test_mochitest_integration.py @@ -0,0 +1,348 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +from functools import partial + +import mozunit +import pytest +from conftest import setup_args +from manifestparser import TestManifest +from mozharness.base.log import ERROR, INFO, WARNING +from mozharness.mozilla.automation import TBPL_FAILURE, TBPL_SUCCESS, TBPL_WARNING +from moztest.selftest.output import filter_action, get_mozharness_status + +here = os.path.abspath(os.path.dirname(__file__)) +get_mozharness_status = partial(get_mozharness_status, "mochitest") + + +@pytest.fixture +def test_name(request): + flavor = request.getfixturevalue("flavor") + + def inner(name): + if flavor == "plain": + return f"test_{name}.html" + elif flavor == "browser-chrome": + return f"browser_{name}.js" + + return inner + + +@pytest.fixture +def test_manifest(setup_test_harness, request): + flavor = request.getfixturevalue("flavor") + test_root = setup_test_harness(*setup_args, flavor=flavor) + assert test_root + + def inner(manifestFileNames): + return TestManifest( + manifests=[os.path.join(test_root, name) for name in manifestFileNames], + strict=False, + rootdir=test_root, + ) + + return inner + + +@pytest.mark.parametrize( + "flavor,manifest", + [ + ("plain", "mochitest-args.ini"), + ("browser-chrome", "browser-args.toml"), + ], +) +def test_output_extra_args(flavor, manifest, runtests, test_manifest, test_name): + # Explicitly provide a manifestFile property that includes the + # manifest file that contains command line arguments. + extra_opts = { + "manifestFile": test_manifest([manifest]), + "runByManifest": True, + } + + results = { + "status": 0, + "tbpl_status": TBPL_SUCCESS, + "log_level": (INFO, WARNING), + } + + status, lines = runtests(test_name("pass"), **extra_opts) + assert status == results["status"] + + tbpl_status, log_level, _ = get_mozharness_status(lines, status) + assert tbpl_status == results["tbpl_status"] + assert log_level in results["log_level"] + + # Filter log entries for the application command including the used + # command line arguments. + lines = filter_action("log", lines) + command = next( + l["message"] for l in lines if l["message"].startswith("Application command") + ) + assert "--headless --window-size 800,600 --new-tab http://example.org" in command + + +@pytest.mark.parametrize("runFailures", ["selftest", ""]) +@pytest.mark.parametrize("flavor", ["plain", "browser-chrome"]) +def test_output_pass(flavor, runFailures, runtests, test_name): + extra_opts = {} + results = { + "status": 1 if runFailures else 0, + "tbpl_status": TBPL_WARNING if runFailures else TBPL_SUCCESS, + "log_level": (INFO, WARNING), + "lines": 2 if runFailures else 1, + "line_status": "PASS", + } + if runFailures: + extra_opts["runFailures"] = runFailures + extra_opts["crashAsPass"] = True + extra_opts["timeoutAsPass"] = True + + status, lines = runtests(test_name("pass"), **extra_opts) + assert status == results["status"] + + tbpl_status, log_level, summary = get_mozharness_status(lines, status) + assert tbpl_status == results["tbpl_status"] + assert log_level in results["log_level"] + + lines = filter_action("test_status", lines) + assert len(lines) == results["lines"] + assert lines[0]["status"] == results["line_status"] + + +@pytest.mark.parametrize("runFailures", ["selftest", ""]) +@pytest.mark.parametrize("flavor", ["plain", "browser-chrome"]) +def test_output_fail(flavor, runFailures, runtests, test_name): + extra_opts = {} + results = { + "status": 0 if runFailures else 1, + "tbpl_status": TBPL_SUCCESS if runFailures else TBPL_WARNING, + "log_level": (INFO, WARNING), + "lines": 1, + "line_status": "PASS" if runFailures else "FAIL", + } + if runFailures: + extra_opts["runFailures"] = runFailures + extra_opts["crashAsPass"] = True + extra_opts["timeoutAsPass"] = True + + status, lines = runtests(test_name("fail"), **extra_opts) + assert status == results["status"] + + tbpl_status, log_level, summary = get_mozharness_status(lines, status) + assert tbpl_status == results["tbpl_status"] + assert log_level in results["log_level"] + + lines = filter_action("test_status", lines) + assert len(lines) == results["lines"] + assert lines[0]["status"] == results["line_status"] + + +@pytest.mark.parametrize("runFailures", ["selftest", ""]) +@pytest.mark.parametrize("flavor", ["plain", "browser-chrome"]) +def test_output_restart_after_failure(flavor, runFailures, runtests, test_name): + extra_opts = {} + results = { + "status": 0 if runFailures else 1, + "tbpl_status": TBPL_SUCCESS if runFailures else TBPL_WARNING, + "log_level": (INFO, WARNING), + "lines": 2, + "line_status": "PASS" if runFailures else "FAIL", + } + extra_opts["restartAfterFailure"] = True + if runFailures: + extra_opts["runFailures"] = runFailures + extra_opts["crashAsPass"] = True + extra_opts["timeoutAsPass"] = True + + tests = [test_name("fail"), test_name("fail2")] + status, lines = runtests(tests, **extra_opts) + assert status == results["status"] + + tbpl_status, log_level, summary = get_mozharness_status(lines, status) + assert tbpl_status == results["tbpl_status"] + assert log_level in results["log_level"] + + # Ensure the harness cycled when failing (look for launching browser) + start_lines = [ + line for line in lines if "Application command:" in line.get("message", "") + ] + if not runFailures: + assert len(start_lines) == results["lines"] + else: + assert len(start_lines) == 1 + + +@pytest.mark.skip_mozinfo("!crashreporter") +@pytest.mark.parametrize("runFailures", ["selftest", ""]) +@pytest.mark.parametrize("flavor", ["plain", "browser-chrome"]) +def test_output_crash(flavor, runFailures, runtests, test_name): + extra_opts = {} + results = { + "status": 0 if runFailures else 1, + "tbpl_status": TBPL_FAILURE, + "log_level": ERROR, + "lines": 1, + } + if runFailures: + extra_opts["runFailures"] = runFailures + extra_opts["crashAsPass"] = True + extra_opts["timeoutAsPass"] = True + # bug 1443327 - we do not set MOZ_CRASHREPORTER_SHUTDOWN for browser-chrome + # the error regex's don't pick this up as a failure + if flavor == "browser-chrome": + results["tbpl_status"] = TBPL_SUCCESS + results["log_level"] = (INFO, WARNING) + + status, lines = runtests( + test_name("crash"), environment=["MOZ_CRASHREPORTER_SHUTDOWN=1"], **extra_opts + ) + assert status == results["status"] + + tbpl_status, log_level, summary = get_mozharness_status(lines, status) + assert tbpl_status == results["tbpl_status"] + assert log_level in results["log_level"] + + if not runFailures: + crash = filter_action("crash", lines) + assert len(crash) == 1 + assert crash[0]["action"] == "crash" + assert crash[0]["signature"] + assert crash[0]["minidump_path"] + + lines = filter_action("test_end", lines) + assert len(lines) == results["lines"] + + +@pytest.mark.skip_mozinfo("!asan") +@pytest.mark.parametrize("runFailures", [""]) +@pytest.mark.parametrize("flavor", ["plain"]) +def test_output_asan(flavor, runFailures, runtests, test_name): + extra_opts = {} + results = { + "status": 1, + "tbpl_status": TBPL_FAILURE, + "log_level": ERROR, + "lines": 0, + } + + status, lines = runtests( + test_name("crash"), environment=["MOZ_CRASHREPORTER_SHUTDOWN=1"], **extra_opts + ) + assert status == results["status"] + + tbpl_status, log_level, summary = get_mozharness_status(lines, status) + assert tbpl_status == results["tbpl_status"] + assert log_level == results["log_level"] + + crash = filter_action("crash", lines) + assert len(crash) == results["lines"] + + process_output = filter_action("process_output", lines) + assert any("ERROR: AddressSanitizer" in l["data"] for l in process_output) + + +@pytest.mark.skip_mozinfo("!debug") +@pytest.mark.parametrize("runFailures", [""]) +@pytest.mark.parametrize("flavor", ["plain"]) +def test_output_assertion(flavor, runFailures, runtests, test_name): + extra_opts = {} + results = { + "status": 0, + "tbpl_status": TBPL_WARNING, + "log_level": WARNING, + "lines": 1, + "assertions": 1, + } + + status, lines = runtests(test_name("assertion"), **extra_opts) + # TODO: mochitest should return non-zero here + assert status == results["status"] + + tbpl_status, log_level, summary = get_mozharness_status(lines, status) + assert tbpl_status == results["tbpl_status"] + assert log_level == results["log_level"] + + # assertion failure prints error, but still emits test-ok + test_end = filter_action("test_end", lines) + assert len(test_end) == results["lines"] + assert test_end[0]["status"] == "OK" + + assertions = filter_action("assertion_count", lines) + assert len(assertions) == results["assertions"] + assert assertions[0]["count"] == results["assertions"] + + +@pytest.mark.skip_mozinfo("!debug") +@pytest.mark.parametrize("runFailures", [""]) +@pytest.mark.parametrize("flavor", ["plain"]) +def test_output_leak(flavor, runFailures, runtests, test_name): + extra_opts = {} + results = {"status": 0, "tbpl_status": TBPL_WARNING, "log_level": WARNING} + + status, lines = runtests(test_name("leak"), **extra_opts) + # TODO: mochitest should return non-zero here + assert status == results["status"] + + tbpl_status, log_level, summary = get_mozharness_status(lines, status) + assert tbpl_status == results["tbpl_status"] + assert log_level == results["log_level"] + + leak_totals = filter_action("mozleak_total", lines) + found_leaks = False + for lt in leak_totals: + if lt["bytes"] == 0: + # No leaks in this process. + assert len(lt["objects"]) == 0 + continue + + assert not found_leaks, "Only one process should have leaked" + found_leaks = True + assert lt["process"] == "tab" + assert lt["bytes"] == 1 + assert lt["objects"] == ["IntentionallyLeakedObject"] + + assert found_leaks, "At least one process should have leaked" + + +@pytest.mark.parametrize("flavor", ["plain"]) +def test_output_testfile_in_dupe_manifests(flavor, runtests, test_name, test_manifest): + results = { + "status": 0, + "tbpl_status": TBPL_SUCCESS, + "log_level": (INFO, WARNING), + "line_status": "PASS", + # We expect the test to be executed exactly 2 times, + # once for each manifest where the test file has been included. + "lines": 2, + } + + # Explicitly provide a manifestFile property that includes the + # two manifest files that share the same test file. + extra_opts = { + "manifestFile": test_manifest( + [ + "mochitest-dupemanifest-1.ini", + "mochitest-dupemanifest-2.ini", + ] + ), + "runByManifest": True, + } + + # Execute mochitest by explicitly request the test file listed + # in two manifest files to be executed. + status, lines = runtests(test_name("pass"), **extra_opts) + assert status == results["status"] + + tbpl_status, log_level, summary = get_mozharness_status(lines, status) + assert tbpl_status == results["tbpl_status"] + assert log_level in results["log_level"] + + lines = filter_action("test_status", lines) + assert len(lines) == results["lines"] + assert lines[0]["status"] == results["line_status"] + + +if __name__ == "__main__": + mozunit.main() |