diff options
Diffstat (limited to '')
278 files changed, 67573 insertions, 0 deletions
diff --git a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs new file mode 100644 index 0000000000..e91adf6a31 --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs @@ -0,0 +1,2721 @@ +/* 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/. */ + +/* + * This module implements a number of utilities useful for browser tests. + * + * All asynchronous helper methods should return promises, rather than being + * callback based. + */ + +// This file uses ContentTask & frame scripts, where these are available. +/* global ContentTaskUtils */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const { ComponentUtils } = ChromeUtils.import( + "resource://gre/modules/ComponentUtils.jsm" +); + +import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ContentTask: "resource://testing-common/ContentTask.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", +}); + +XPCOMUtils.defineLazyServiceGetters(lazy, { + ProtocolProxyService: [ + "@mozilla.org/network/protocol-proxy-service;1", + "nsIProtocolProxyService", + ], +}); + +const PROCESSSELECTOR_CONTRACTID = "@mozilla.org/ipc/processselector;1"; +const OUR_PROCESSSELECTOR_CID = Components.ID( + "{f9746211-3d53-4465-9aeb-ca0d96de0253}" +); +const EXISTING_JSID = Cc[PROCESSSELECTOR_CONTRACTID]; +const DEFAULT_PROCESSSELECTOR_CID = EXISTING_JSID + ? Components.ID(EXISTING_JSID.number) + : null; + +let gListenerId = 0; + +// A process selector that always asks for a new process. +function NewProcessSelector() {} + +NewProcessSelector.prototype = { + classID: OUR_PROCESSSELECTOR_CID, + QueryInterface: ChromeUtils.generateQI(["nsIContentProcessProvider"]), + + provideProcess() { + return Ci.nsIContentProcessProvider.NEW_PROCESS; + }, +}; + +let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +let selectorFactory = ComponentUtils.generateSingletonFactory( + NewProcessSelector +); +registrar.registerFactory(OUR_PROCESSSELECTOR_CID, "", null, selectorFactory); + +const kAboutPageRegistrationContentScript = + "chrome://mochikit/content/tests/BrowserTestUtils/content-about-page-utils.js"; + +/** + * Create and register the BrowserTestUtils and ContentEventListener window + * actors. + */ +function registerActors() { + ChromeUtils.registerWindowActor("BrowserTestUtils", { + parent: { + esModuleURI: "resource://testing-common/BrowserTestUtilsParent.sys.mjs", + }, + child: { + esModuleURI: "resource://testing-common/BrowserTestUtilsChild.sys.mjs", + events: { + DOMContentLoaded: { capture: true }, + load: { capture: true }, + }, + }, + allFrames: true, + includeChrome: true, + }); + + ChromeUtils.registerWindowActor("ContentEventListener", { + parent: { + esModuleURI: + "resource://testing-common/ContentEventListenerParent.sys.mjs", + }, + child: { + esModuleURI: + "resource://testing-common/ContentEventListenerChild.sys.mjs", + events: { + // We need to see the creation of all new windows, in case they have + // a browsing context we are interested in. + DOMWindowCreated: { capture: true }, + }, + }, + allFrames: true, + }); +} + +registerActors(); + +/** + * BrowserTestUtils provides useful test utilities for working with the browser + * in browser mochitests. Most common operations (opening, closing and switching + * between tabs and windows, loading URLs, waiting for events in the parent or + * content process, clicking things in the content process, registering about + * pages, etc.) have dedicated helpers on this object. + * + * @class + */ +export var BrowserTestUtils = { + /** + * Loads a page in a new tab, executes a Task and closes the tab. + * + * @param {Object|String} options + * If this is a string it is the url to open and will be opened in the + * currently active browser window. + * @param {tabbrowser} [options.gBrowser + * A reference to the ``tabbrowser`` element where the new tab should + * be opened, + * @param {string} options.url + * The URL of the page to load. + * @param {Function} taskFn + * Async function representing that will be executed while + * the tab is loaded. The first argument passed to the function is a + * reference to the browser object for the new tab. + * + * @return {Any} Returns the value that is returned from taskFn. + * @resolves When the tab has been closed. + * @rejects Any exception from taskFn is propagated. + */ + async withNewTab(options, taskFn) { + if (typeof options == "string") { + options = { + gBrowser: Services.wm.getMostRecentWindow("navigator:browser").gBrowser, + url: options, + }; + } + let tab = await BrowserTestUtils.openNewForegroundTab(options); + let originalWindow = tab.ownerGlobal; + let result; + try { + result = await taskFn(tab.linkedBrowser); + } finally { + let finalWindow = tab.ownerGlobal; + if (originalWindow == finalWindow && !tab.closing && tab.linkedBrowser) { + // taskFn may resolve within a tick after opening a new tab. + // We shouldn't remove the newly opened tab in the same tick. + // Wait for the next tick here. + await TestUtils.waitForTick(); + BrowserTestUtils.removeTab(tab); + } else { + Services.console.logStringMessage( + "BrowserTestUtils.withNewTab: Tab was already closed before " + + "removeTab would have been called" + ); + } + } + + return Promise.resolve(result); + }, + + /** + * Opens a new tab in the foreground. + * + * This function takes an options object (which is preferred) or actual + * parameters. The names of the options must correspond to the names below. + * gBrowser is required and all other options are optional. + * + * @param {tabbrowser} gBrowser + * The tabbrowser to open the tab new in. + * @param {string} opening (or url) + * May be either a string URL to load in the tab, or a function that + * will be called to open a foreground tab. Defaults to "about:blank". + * @param {boolean} waitForLoad + * True to wait for the page in the new tab to load. Defaults to true. + * @param {boolean} waitForStateStop + * True to wait for the web progress listener to send STATE_STOP for the + * document in the tab. Defaults to false. + * @param {boolean} forceNewProcess + * True to force the new tab to load in a new process. Defaults to + * false. + * + * @return {Promise} + * Resolves when the tab is ready and loaded as necessary. + * @resolves The new tab. + */ + openNewForegroundTab(tabbrowser, ...args) { + let startTime = Cu.now(); + let options; + if ( + tabbrowser.ownerGlobal && + tabbrowser === tabbrowser.ownerGlobal.gBrowser + ) { + // tabbrowser is a tabbrowser, read the rest of the arguments from args. + let [ + opening = "about:blank", + waitForLoad = true, + waitForStateStop = false, + forceNewProcess = false, + ] = args; + + options = { opening, waitForLoad, waitForStateStop, forceNewProcess }; + } else { + if ("url" in tabbrowser && !("opening" in tabbrowser)) { + tabbrowser.opening = tabbrowser.url; + } + + let { + opening = "about:blank", + waitForLoad = true, + waitForStateStop = false, + forceNewProcess = false, + } = tabbrowser; + + tabbrowser = tabbrowser.gBrowser; + options = { opening, waitForLoad, waitForStateStop, forceNewProcess }; + } + + let { + opening: opening, + waitForLoad: aWaitForLoad, + waitForStateStop: aWaitForStateStop, + } = options; + + let promises, tab; + try { + // If we're asked to force a new process, replace the normal process + // selector with one that always asks for a new process. + // If DEFAULT_PROCESSSELECTOR_CID is null, we're in non-e10s mode and we + // should skip this. + if (options.forceNewProcess && DEFAULT_PROCESSSELECTOR_CID) { + Services.ppmm.releaseCachedProcesses(); + registrar.registerFactory( + OUR_PROCESSSELECTOR_CID, + "", + PROCESSSELECTOR_CONTRACTID, + null + ); + } + + promises = [ + BrowserTestUtils.switchTab(tabbrowser, function() { + if (typeof opening == "function") { + opening(); + tab = tabbrowser.selectedTab; + } else { + tabbrowser.selectedTab = tab = BrowserTestUtils.addTab( + tabbrowser, + opening + ); + } + }), + ]; + + if (aWaitForLoad) { + promises.push(BrowserTestUtils.browserLoaded(tab.linkedBrowser)); + } + if (aWaitForStateStop) { + promises.push(BrowserTestUtils.browserStopped(tab.linkedBrowser)); + } + } finally { + // Restore the original process selector, if needed. + if (options.forceNewProcess && DEFAULT_PROCESSSELECTOR_CID) { + registrar.registerFactory( + DEFAULT_PROCESSSELECTOR_CID, + "", + PROCESSSELECTOR_CONTRACTID, + null + ); + } + } + return Promise.all(promises).then(() => { + let { innerWindowId } = tabbrowser.ownerGlobal.windowGlobalChild; + ChromeUtils.addProfilerMarker( + "BrowserTestUtils", + { startTime, category: "Test", innerWindowId }, + "openNewForegroundTab" + ); + return tab; + }); + }, + + /** + * Checks if a DOM element is hidden. + * + * @param {Element} element + * The element which is to be checked. + * + * @return {boolean} + */ + is_hidden(element) { + let win = element.ownerGlobal; + let style = win.getComputedStyle(element); + if (style.display == "none") { + return true; + } + if (style.visibility != "visible") { + return true; + } + if (win.XULPopupElement.isInstance(element)) { + return ["hiding", "closed"].includes(element.state); + } + + // Hiding a parent element will hide all its children + if (element.parentNode != element.ownerDocument) { + return BrowserTestUtils.is_hidden(element.parentNode); + } + + return false; + }, + + /** + * Checks if a DOM element is visible. + * + * @param {Element} element + * The element which is to be checked. + * + * @return {boolean} + */ + is_visible(element) { + let win = element.ownerGlobal; + let style = win.getComputedStyle(element); + if (style.display == "none") { + return false; + } + if (style.visibility != "visible") { + return false; + } + if (win.XULPopupElement.isInstance(element) && element.state != "open") { + return false; + } + + // Hiding a parent element will hide all its children + if (element.parentNode != element.ownerDocument) { + return BrowserTestUtils.is_visible(element.parentNode); + } + + return true; + }, + + /** + * If the argument is a browsingContext, return it. If the + * argument is a browser/frame, returns the browsing context for it. + */ + getBrowsingContextFrom(browser) { + if (Element.isInstance(browser)) { + return browser.browsingContext; + } + + return browser; + }, + + /** + * Switches to a tab and resolves when it is ready. + * + * @param {tabbrowser} tabbrowser + * The tabbrowser. + * @param {tab} tab + * Either a tab element to switch to or a function to perform the switch. + * + * @return {Promise} + * Resolves when the tab has been switched to. + * @resolves The tab switched to. + */ + switchTab(tabbrowser, tab) { + let startTime = Cu.now(); + let { innerWindowId } = tabbrowser.ownerGlobal.windowGlobalChild; + + let promise = new Promise(resolve => { + tabbrowser.addEventListener( + "TabSwitchDone", + function() { + TestUtils.executeSoon(() => { + ChromeUtils.addProfilerMarker( + "BrowserTestUtils", + { category: "Test", startTime, innerWindowId }, + "switchTab" + ); + resolve(tabbrowser.selectedTab); + }); + }, + { once: true } + ); + }); + + if (typeof tab == "function") { + tab(); + } else { + tabbrowser.selectedTab = tab; + } + return promise; + }, + + /** + * Waits for an ongoing page load in a browser window to complete. + * + * This can be used in conjunction with any synchronous method for starting a + * load, like the "addTab" method on "tabbrowser", and must be called before + * yielding control to the event loop. Note that calling this after multiple + * successive load operations can be racy, so ``wantLoad`` should be specified + * in these cases. + * + * This function works by listening for custom load events on ``browser``. These + * are sent by a BrowserTestUtils window actor in response to "load" and + * "DOMContentLoaded" content events. + * + * @param {xul:browser} browser + * A xul:browser. + * @param {Boolean} [includeSubFrames = false] + * A boolean indicating if loads from subframes should be included. + * @param {string|function} [wantLoad = null] + * If a function, takes a URL and returns true if that's the load we're + * interested in. If a string, gives the URL of the load we're interested + * in. If not present, the first load resolves the promise. + * @param {boolean} [maybeErrorPage = false] + * If true, this uses DOMContentLoaded event instead of load event. + * Also wantLoad will be called with visible URL, instead of + * 'about:neterror?...' for error page. + * + * @return {Promise} + * @resolves When a load event is triggered for the browser. + */ + browserLoaded( + browser, + includeSubFrames = false, + wantLoad = null, + maybeErrorPage = false + ) { + let startTime = Cu.now(); + let { innerWindowId } = browser.ownerGlobal.windowGlobalChild; + + // Passing a url as second argument is a common mistake we should prevent. + if (includeSubFrames && typeof includeSubFrames != "boolean") { + throw new Error( + "The second argument to browserLoaded should be a boolean." + ); + } + + // If browser belongs to tabbrowser-tab, ensure it has been + // inserted into the document. + let tabbrowser = browser.ownerGlobal.gBrowser; + if (tabbrowser && tabbrowser.getTabForBrowser) { + let tab = tabbrowser.getTabForBrowser(browser); + if (tab) { + tabbrowser._insertBrowser(tab); + } + } + + function isWanted(url) { + if (!wantLoad) { + return true; + } else if (typeof wantLoad == "function") { + return wantLoad(url); + } + + // HTTPS-First (Bug 1704453) TODO: In case we are waiting + // for an http:// URL to be loaded and https-first is enabled, + // then we also return true in case the backend upgraded + // the load to https://. + if ( + BrowserTestUtils._httpsFirstEnabled && + typeof wantLoad == "string" && + wantLoad.startsWith("http://") + ) { + let wantLoadHttps = wantLoad.replace("http://", "https://"); + if (wantLoadHttps == url) { + return true; + } + } + + // It's a string. + return wantLoad == url; + } + + // Error pages are loaded slightly differently, so listen for the + // DOMContentLoaded event for those instead. + let loadEvent = maybeErrorPage ? "DOMContentLoaded" : "load"; + let eventName = `BrowserTestUtils:ContentEvent:${loadEvent}`; + + return new Promise((resolve, reject) => { + function listener(event) { + switch (event.type) { + case eventName: { + let { browsingContext, internalURL, visibleURL } = event.detail; + + // Sometimes we arrive here without an internalURL. If that's the + // case, just keep waiting until we get one. + if (!internalURL) { + return; + } + + // Ignore subframes if we only care about the top-level load. + let subframe = browsingContext !== browsingContext.top; + if (subframe && !includeSubFrames) { + return; + } + + // See testing/mochitest/BrowserTestUtils/content/BrowserTestUtilsChild.sys.mjs + // for the difference between visibleURL and internalURL. + if (!isWanted(maybeErrorPage ? visibleURL : internalURL)) { + return; + } + + ChromeUtils.addProfilerMarker( + "BrowserTestUtils", + { startTime, category: "Test", innerWindowId }, + "browserLoaded: " + internalURL + ); + resolve(internalURL); + break; + } + + case "unload": + reject( + new Error( + "The window unloaded while we were waiting for the browser to load - this should never happen." + ) + ); + break; + + default: + return; + } + + browser.removeEventListener(eventName, listener, true); + browser.ownerGlobal.removeEventListener("unload", listener); + } + + browser.addEventListener(eventName, listener, true); + browser.ownerGlobal.addEventListener("unload", listener); + }); + }, + + /** + * Waits for the selected browser to load in a new window. This + * is most useful when you've got a window that might not have + * loaded its DOM yet, and where you can't easily use browserLoaded + * on gBrowser.selectedBrowser since gBrowser doesn't yet exist. + * + * @param {xul:window} window + * A newly opened window for which we're waiting for the + * first browser load. + * @param {Boolean} aboutBlank [optional] + * If false, about:blank loads are ignored and we continue + * to wait. + * @param {function|null} checkFn [optional] + * If checkFn(browser) returns false, the load is ignored + * and we continue to wait. + * + * @return {Promise} + * @resolves Once the selected browser fires its load event. + */ + firstBrowserLoaded(win, aboutBlank = true, checkFn = null) { + return this.waitForEvent( + win, + "BrowserTestUtils:ContentEvent:load", + true, + event => { + if (checkFn) { + return checkFn(event.target); + } + return ( + win.gBrowser.selectedBrowser.currentURI.spec !== "about:blank" || + aboutBlank + ); + } + ); + }, + + _webProgressListeners: new Set(), + + _contentEventListenerSharedState: new Map(), + + _contentEventListeners: new Map(), + + /** + * Waits for the web progress listener associated with this tab to fire a + * state change that matches checkFn for the toplevel document. + * + * @param {xul:browser} browser + * A xul:browser. + * @param {String} expectedURI (optional) + * A specific URL to check the channel load against + * @param {Function} checkFn + * If checkFn(aStateFlags, aStatus) returns false, the state change + * is ignored and we continue to wait. + * + * @return {Promise} + * @resolves When the desired state change reaches the tab's progress listener + */ + waitForBrowserStateChange(browser, expectedURI, checkFn) { + return new Promise(resolve => { + let wpl = { + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + dump( + "Saw state " + + aStateFlags.toString(16) + + " and status " + + aStatus.toString(16) + + "\n" + ); + if (checkFn(aStateFlags, aStatus) && aWebProgress.isTopLevel) { + let chan = aRequest.QueryInterface(Ci.nsIChannel); + dump( + "Browser got expected state change " + + chan.originalURI.spec + + "\n" + ); + if (!expectedURI || chan.originalURI.spec == expectedURI) { + browser.removeProgressListener(wpl); + BrowserTestUtils._webProgressListeners.delete(wpl); + resolve(); + } + } + }, + onSecurityChange() {}, + onStatusChange() {}, + onLocationChange() {}, + onContentBlockingEvent() {}, + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsIWebProgressListener2", + "nsISupportsWeakReference", + ]), + }; + browser.addProgressListener(wpl); + this._webProgressListeners.add(wpl); + dump( + "Waiting for browser state change" + + (expectedURI ? " of " + expectedURI : "") + + "\n" + ); + }); + }, + + /** + * Waits for the web progress listener associated with this tab to fire a + * STATE_STOP for the toplevel document. + * + * @param {xul:browser} browser + * A xul:browser. + * @param {String} expectedURI (optional) + * A specific URL to check the channel load against + * @param {Boolean} checkAborts (optional, defaults to false) + * Whether NS_BINDING_ABORTED stops 'count' as 'real' stops + * (e.g. caused by the stop button or equivalent APIs) + * + * @return {Promise} + * @resolves When STATE_STOP reaches the tab's progress listener + */ + browserStopped(browser, expectedURI, checkAborts = false) { + let testFn = function(aStateFlags, aStatus) { + return ( + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + (checkAborts || aStatus != Cr.NS_BINDING_ABORTED) + ); + }; + dump( + "Waiting for browser load" + + (expectedURI ? " of " + expectedURI : "") + + "\n" + ); + return BrowserTestUtils.waitForBrowserStateChange( + browser, + expectedURI, + testFn + ); + }, + + /** + * Waits for the web progress listener associated with this tab to fire a + * STATE_START for the toplevel document. + * + * @param {xul:browser} browser + * A xul:browser. + * @param {String} expectedURI (optional) + * A specific URL to check the channel load against + * + * @return {Promise} + * @resolves When STATE_START reaches the tab's progress listener + */ + browserStarted(browser, expectedURI) { + let testFn = function(aStateFlags, aStatus) { + return ( + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && + aStateFlags & Ci.nsIWebProgressListener.STATE_START + ); + }; + dump( + "Waiting for browser to start load" + + (expectedURI ? " of " + expectedURI : "") + + "\n" + ); + return BrowserTestUtils.waitForBrowserStateChange( + browser, + expectedURI, + testFn + ); + }, + + /** + * Waits for a tab to open and load a given URL. + * + * By default, the method doesn't wait for the tab contents to load. + * + * @param {tabbrowser} tabbrowser + * The tabbrowser to look for the next new tab in. + * @param {string|function} [wantLoad = null] + * If a function, takes a URL and returns true if that's the load we're + * interested in. If a string, gives the URL of the load we're interested + * in. If not present, the first non-about:blank load is used. + * @param {boolean} [waitForLoad = false] + * True to wait for the page in the new tab to load. Defaults to false. + * @param {boolean} [waitForAnyTab = false] + * True to wait for the url to be loaded in any new tab, not just the next + * one opened. + * + * @return {Promise} + * @resolves With the {xul:tab} when a tab is opened and its location changes + * to the given URL and optionally that browser has loaded. + * + * NB: this method will not work if you open a new tab with e.g. BrowserOpenTab + * and the tab does not load a URL, because no onLocationChange will fire. + */ + waitForNewTab( + tabbrowser, + wantLoad = null, + waitForLoad = false, + waitForAnyTab = false + ) { + let urlMatches; + if (wantLoad && typeof wantLoad == "function") { + urlMatches = wantLoad; + } else if (wantLoad) { + urlMatches = urlToMatch => urlToMatch == wantLoad; + } else { + urlMatches = urlToMatch => urlToMatch != "about:blank"; + } + return new Promise((resolve, reject) => { + tabbrowser.tabContainer.addEventListener( + "TabOpen", + function tabOpenListener(openEvent) { + if (!waitForAnyTab) { + tabbrowser.tabContainer.removeEventListener( + "TabOpen", + tabOpenListener + ); + } + let newTab = openEvent.target; + let newBrowser = newTab.linkedBrowser; + let result; + if (waitForLoad) { + // If waiting for load, resolve with promise for that, which when load + // completes resolves to the new tab. + result = BrowserTestUtils.browserLoaded( + newBrowser, + false, + urlMatches + ).then(() => newTab); + } else { + // If not waiting for load, just resolve with the new tab. + result = newTab; + } + + let progressListener = { + onLocationChange(aBrowser) { + // Only interested in location changes on our browser. + if (aBrowser != newBrowser) { + return; + } + + // Check that new location is the URL we want. + if (!urlMatches(aBrowser.currentURI.spec)) { + return; + } + if (waitForAnyTab) { + tabbrowser.tabContainer.removeEventListener( + "TabOpen", + tabOpenListener + ); + } + tabbrowser.removeTabsProgressListener(progressListener); + TestUtils.executeSoon(() => resolve(result)); + }, + }; + tabbrowser.addTabsProgressListener(progressListener); + } + ); + }); + }, + + /** + * Waits for onLocationChange. + * + * @param {tabbrowser} tabbrowser + * The tabbrowser to wait for the location change on. + * @param {string} url + * The string URL to look for. The URL must match the URL in the + * location bar exactly. + * @return {Promise} + * @resolves When onLocationChange fires. + */ + waitForLocationChange(tabbrowser, url) { + return new Promise((resolve, reject) => { + let progressListener = { + onLocationChange( + aBrowser, + aWebProgress, + aRequest, + aLocationURI, + aFlags + ) { + if ( + (url && aLocationURI.spec != url) || + (!url && aLocationURI.spec == "about:blank") + ) { + return; + } + + tabbrowser.removeTabsProgressListener(progressListener); + resolve(); + }, + }; + tabbrowser.addTabsProgressListener(progressListener); + }); + }, + + /** + * Waits for the next browser window to open and be fully loaded. + * + * @param {Object} aParams + * @param {string} [aParams.url] + * If set, we will wait until the initial browser in the new window + * has loaded a particular page. + * If unset, the initial browser may or may not have finished + * loading its first page when the resulting Promise resolves. + * @param {bool} [aParams.anyWindow] + * True to wait for the url to be loaded in any new + * window, not just the next one opened. + * @param {bool} [aParams.maybeErrorPage] + * See ``browserLoaded`` function. + * @return {Promise} + * A Promise which resolves the next time that a DOM window + * opens and the delayed startup observer notification fires. + */ + waitForNewWindow(aParams = {}) { + let { url = null, anyWindow = false, maybeErrorPage = false } = aParams; + + if (anyWindow && !url) { + throw new Error("url should be specified if anyWindow is true"); + } + + return new Promise((resolve, reject) => { + let observe = async (win, topic, data) => { + if (topic != "domwindowopened") { + return; + } + + try { + if (!anyWindow) { + Services.ww.unregisterNotification(observe); + } + + // Add these event listeners now since they may fire before the + // DOMContentLoaded event down below. + let promises = [ + this.waitForEvent(win, "focus", true), + this.waitForEvent(win, "activate"), + ]; + + if (url) { + await this.waitForEvent(win, "DOMContentLoaded"); + + if (win.document.documentURI != AppConstants.BROWSER_CHROME_URL) { + return; + } + } + + promises.push( + TestUtils.topicObserved( + "browser-delayed-startup-finished", + subject => subject == win + ) + ); + + if (url) { + let loadPromise = this.browserLoaded( + win.gBrowser.selectedBrowser, + false, + url, + maybeErrorPage + ); + promises.push(loadPromise); + } + + await Promise.all(promises); + + if (anyWindow) { + Services.ww.unregisterNotification(observe); + } + resolve(win); + } catch (err) { + // We failed to wait for the load in this URI. This is only an error + // if `anyWindow` is not set, as if it is we can just wait for another + // window. + if (!anyWindow) { + reject(err); + } + } + }; + Services.ww.registerNotification(observe); + }); + }, + + /** + * Loads a new URI in the given browser, triggered by the system principal. + * + * @param {xul:browser} browser + * A xul:browser. + * @param {string} uri + * The URI to load. + */ + loadURI(browser, uri) { + browser.loadURI(uri, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + }, + + /** + * Maybe create a preloaded browser and ensure it's finished loading. + * + * @param gBrowser (<xul:tabbrowser>) + * The tabbrowser in which to preload a browser. + */ + async maybeCreatePreloadedBrowser(gBrowser) { + let win = gBrowser.ownerGlobal; + win.NewTabPagePreloading.maybeCreatePreloadedBrowser(win); + + // We cannot use the regular BrowserTestUtils helper for waiting here, since that + // would try to insert the preloaded browser, which would only break things. + await lazy.ContentTask.spawn(gBrowser.preloadedBrowser, [], async () => { + await ContentTaskUtils.waitForCondition(() => { + return ( + this.content.document && + this.content.document.readyState == "complete" + ); + }); + }); + }, + + /** + * @param win (optional) + * The window we should wait to have "domwindowopened" sent through + * the observer service for. If this is not supplied, we'll just + * resolve when the first "domwindowopened" notification is seen. + * @param {function} checkFn [optional] + * Called with the nsIDOMWindow object as argument, should return true + * if the event is the expected one, or false if it should be ignored + * and observing should continue. If not specified, the first window + * resolves the returned promise. + * @return {Promise} + * A Promise which resolves when a "domwindowopened" notification + * has been fired by the window watcher. + */ + domWindowOpened(win, checkFn) { + return new Promise(resolve => { + async function observer(subject, topic, data) { + if (topic == "domwindowopened" && (!win || subject === win)) { + let observedWindow = subject; + if (checkFn && !(await checkFn(observedWindow))) { + return; + } + Services.ww.unregisterNotification(observer); + resolve(observedWindow); + } + } + Services.ww.registerNotification(observer); + }); + }, + + /** + * @param win (optional) + * The window we should wait to have "domwindowopened" sent through + * the observer service for. If this is not supplied, we'll just + * resolve when the first "domwindowopened" notification is seen. + * The promise will be resolved once the new window's document has been + * loaded. + * + * @param {function} checkFn (optional) + * Called with the nsIDOMWindow object as argument, should return true + * if the event is the expected one, or false if it should be ignored + * and observing should continue. If not specified, the first window + * resolves the returned promise. + * + * @return {Promise} + * A Promise which resolves when a "domwindowopened" notification + * has been fired by the window watcher. + */ + domWindowOpenedAndLoaded(win, checkFn) { + return this.domWindowOpened(win, async observedWin => { + await this.waitForEvent(observedWin, "load"); + if (checkFn && !(await checkFn(observedWin))) { + return false; + } + return true; + }); + }, + + /** + * @param win (optional) + * The window we should wait to have "domwindowclosed" sent through + * the observer service for. If this is not supplied, we'll just + * resolve when the first "domwindowclosed" notification is seen. + * @return {Promise} + * A Promise which resolves when a "domwindowclosed" notification + * has been fired by the window watcher. + */ + domWindowClosed(win) { + return new Promise(resolve => { + function observer(subject, topic, data) { + if (topic == "domwindowclosed" && (!win || subject === win)) { + Services.ww.unregisterNotification(observer); + resolve(subject); + } + } + Services.ww.registerNotification(observer); + }); + }, + + /** + * Clear the stylesheet cache and open a new window to ensure + * CSS @supports -moz-bool-pref(...) {} rules are correctly + * applied to the browser chrome. + * + * @param {Object} options See BrowserTestUtils.openNewBrowserWindow + * @returns {Promise} Resolves with the new window once it is loaded. + */ + async openNewWindowWithFlushedCacheForMozSupports(options) { + ChromeUtils.clearStyleSheetCache(); + return BrowserTestUtils.openNewBrowserWindow(options); + }, + + /** + * Open a new browser window from an existing one. + * This relies on OpenBrowserWindow in browser.js, and waits for the window + * to be completely loaded before resolving. + * + * @param {Object} options + * Options to pass to OpenBrowserWindow. Additionally, supports: + * @param {bool} options.waitForTabURL + * Forces the initial browserLoaded check to wait for the tab to + * load the given URL (instead of about:blank) + * + * @return {Promise} + * Resolves with the new window once it is loaded. + */ + async openNewBrowserWindow(options = {}) { + let startTime = Cu.now(); + + let currentWin = lazy.BrowserWindowTracker.getTopWindow({ private: false }); + if (!currentWin) { + throw new Error( + "Can't open a new browser window from this helper if no non-private window is open." + ); + } + let win = currentWin.OpenBrowserWindow(options); + + let promises = [ + this.waitForEvent(win, "focus", true), + this.waitForEvent(win, "activate"), + ]; + + // Wait for browser-delayed-startup-finished notification, it indicates + // that the window has loaded completely and is ready to be used for + // testing. + promises.push( + TestUtils.topicObserved( + "browser-delayed-startup-finished", + subject => subject == win + ).then(() => win) + ); + + promises.push( + this.firstBrowserLoaded(win, !options.waitForTabURL, browser => { + return ( + !options.waitForTabURL || + options.waitForTabURL == browser.currentURI.spec + ); + }) + ); + + await Promise.all(promises); + ChromeUtils.addProfilerMarker( + "BrowserTestUtils", + { startTime, category: "Test" }, + "openNewBrowserWindow" + ); + + return win; + }, + + /** + * Closes a window. + * + * @param {Window} win + * A window to close. + * + * @return {Promise} + * Resolves when the provided window has been closed. For browser + * windows, the Promise will also wait until all final SessionStore + * messages have been sent up from all browser tabs. + */ + closeWindow(win) { + let closedPromise = BrowserTestUtils.windowClosed(win); + win.close(); + return closedPromise; + }, + + /** + * Returns a Promise that resolves when a window has finished closing. + * + * @param {Window} win + * The closing window. + * + * @return {Promise} + * Resolves when the provided window has been fully closed. For + * browser windows, the Promise will also wait until all final + * SessionStore messages have been sent up from all browser tabs. + */ + windowClosed(win) { + let domWinClosedPromise = BrowserTestUtils.domWindowClosed(win); + let promises = [domWinClosedPromise]; + let winType = win.document.documentElement.getAttribute("windowtype"); + let flushTopic = "sessionstore-browser-shutdown-flush"; + + if (winType == "navigator:browser") { + let finalMsgsPromise = new Promise(resolve => { + let browserSet = new Set(win.gBrowser.browsers); + // Ensure all browsers have been inserted or we won't get + // messages back from them. + browserSet.forEach(browser => { + win.gBrowser._insertBrowser(win.gBrowser.getTabForBrowser(browser)); + }); + + let observer = (subject, topic, data) => { + if (browserSet.has(subject)) { + browserSet.delete(subject); + } + if (!browserSet.size) { + Services.obs.removeObserver(observer, flushTopic); + // Give the TabStateFlusher a chance to react to this final + // update and for the TabStateFlusher.flushWindow promise + // to resolve before we resolve. + TestUtils.executeSoon(resolve); + } + }; + + Services.obs.addObserver(observer, flushTopic); + }); + + promises.push(finalMsgsPromise); + } + + return Promise.all(promises); + }, + + /** + * Returns a Promise that resolves once the SessionStore information for the + * given tab is updated and all listeners are called. + * + * @param {xul:tab} tab + * The tab that will be removed. + * @returns {Promise} + * @resolves When the SessionStore information is updated. + */ + waitForSessionStoreUpdate(tab) { + return new Promise(resolve => { + let browser = tab.linkedBrowser; + let flushTopic = "sessionstore-browser-shutdown-flush"; + let observer = (subject, topic, data) => { + if (subject === browser) { + Services.obs.removeObserver(observer, flushTopic); + // Wait for the next event tick to make sure other listeners are + // called. + TestUtils.executeSoon(() => resolve()); + } + }; + Services.obs.addObserver(observer, flushTopic); + }); + }, + + /** + * Waits for an event to be fired on a specified element. + * + * @example + * + * let promiseEvent = BrowserTestUtils.waitForEvent(element, "eventName"); + * // Do some processing here that will cause the event to be fired + * // ... + * // Now wait until the Promise is fulfilled + * let receivedEvent = await promiseEvent; + * + * @example + * // The promise resolution/rejection handler for the returned promise is + * // guaranteed not to be called until the next event tick after the event + * // listener gets called, so that all other event listeners for the element + * // are executed before the handler is executed. + * + * let promiseEvent = BrowserTestUtils.waitForEvent(element, "eventName"); + * // Same event tick here. + * await promiseEvent; + * // Next event tick here. + * + * @example + * // If some code, such like adding yet another event listener, needs to be + * // executed in the same event tick, use raw addEventListener instead and + * // place the code inside the event listener. + * + * element.addEventListener("load", () => { + * // Add yet another event listener in the same event tick as the load + * // event listener. + * p = BrowserTestUtils.waitForEvent(element, "ready"); + * }, { once: true }); + * + * @param {Element} subject + * The element that should receive the event. + * @param {string} eventName + * Name of the event to listen to. + * @param {bool} [capture] + * True to use a capturing listener. + * @param {function} [checkFn] + * Called with the Event object as argument, should return true if the + * event is the expected one, or false if it should be ignored and + * listening should continue. If not specified, the first event with + * the specified name resolves the returned promise. + * @param {bool} [wantsUntrusted=false] + * True to receive synthetic events dispatched by web content. + * + * @note Because this function is intended for testing, any error in checkFn + * will cause the returned promise to be rejected instead of waiting for + * the next event, since this is probably a bug in the test. + * + * @returns {Promise} + * @resolves The Event object. + */ + waitForEvent(subject, eventName, capture, checkFn, wantsUntrusted) { + let startTime = Cu.now(); + let innerWindowId = subject.ownerGlobal?.windowGlobalChild.innerWindowId; + + return new Promise((resolve, reject) => { + let removed = false; + function listener(event) { + function cleanup() { + removed = true; + // Avoid keeping references to objects after the promise resolves. + subject = null; + checkFn = null; + } + try { + if (checkFn && !checkFn(event)) { + return; + } + subject.removeEventListener(eventName, listener, capture); + cleanup(); + TestUtils.executeSoon(() => { + ChromeUtils.addProfilerMarker( + "BrowserTestUtils", + { startTime, category: "Test", innerWindowId }, + "waitForEvent: " + eventName + ); + resolve(event); + }); + } catch (ex) { + try { + subject.removeEventListener(eventName, listener, capture); + } catch (ex2) { + // Maybe the provided object does not support removeEventListener. + } + cleanup(); + TestUtils.executeSoon(() => reject(ex)); + } + } + + subject.addEventListener(eventName, listener, capture, wantsUntrusted); + + TestUtils.promiseTestFinished?.then(() => { + if (removed) { + return; + } + + subject.removeEventListener(eventName, listener, capture); + let text = eventName + " listener"; + if (subject.id) { + text += ` on #${subject.id}`; + } + text += " not removed before the end of test"; + reject(text); + ChromeUtils.addProfilerMarker( + "BrowserTestUtils", + { startTime, category: "Test", innerWindowId }, + "waitForEvent: " + text + ); + }); + }); + }, + + /** + * Like waitForEvent, but adds the event listener to the message manager + * global for browser. + * + * @param {string} eventName + * Name of the event to listen to. + * @param {bool} capture [optional] + * Whether to use a capturing listener. + * @param {function} checkFn [optional] + * Called with the Event object as argument, should return true if the + * event is the expected one, or false if it should be ignored and + * listening should continue. If not specified, the first event with + * the specified name resolves the returned promise. + * @param {bool} wantUntrusted [optional] + * Whether to accept untrusted events + * + * @note As of bug 1588193, this function no longer rejects the returned + * promise in the case of a checkFn error. Instead, since checkFn is now + * called through eval in the content process, the error is thrown in + * the listener created by ContentEventListenerChild. Work to improve + * error handling (eg. to reject the promise as before and to preserve + * the filename/stack) is being tracked in bug 1593811. + * + * @returns {Promise} + */ + waitForContentEvent( + browser, + eventName, + capture = false, + checkFn, + wantUntrusted = false + ) { + return new Promise(resolve => { + let removeEventListener = this.addContentEventListener( + browser, + eventName, + () => { + removeEventListener(); + resolve(eventName); + }, + { capture, wantUntrusted }, + checkFn + ); + }); + }, + + /** + * Like waitForEvent, but acts on a popup. It ensures the popup is not already + * in the expected state. + * + * @param {Element} popup + * The popup element that should receive the event. + * @param {string} eventSuffix + * The event suffix expected to be received, one of "shown" or "hidden". + * @returns {Promise} + */ + waitForPopupEvent(popup, eventSuffix) { + let endState = { shown: "open", hidden: "closed" }[eventSuffix]; + + if (popup.state == endState) { + return Promise.resolve(); + } + return this.waitForEvent(popup, "popup" + eventSuffix); + }, + + /** + * Waits for the select popup to be shown. This is needed because the select + * dropdown is created lazily. + * + * @param {Window} win + * A window to expect the popup in. + * + * @return {Promise} + * Resolves when the popup has been fully opened. The resolution value + * is the select popup. + */ + async waitForSelectPopupShown(win) { + let getMenulist = () => + win.document.getElementById("ContentSelectDropdown"); + let menulist = getMenulist(); + if (!menulist) { + await this.waitForMutationCondition( + win.document, + { childList: true, subtree: true }, + getMenulist + ); + menulist = getMenulist(); + if (menulist.menupopup.state == "open") { + return menulist.menupopup; + } + } + await this.waitForEvent(menulist.menupopup, "popupshown"); + return menulist.menupopup; + }, + + /** + * Adds a content event listener on the given browser + * element. Similar to waitForContentEvent, but the listener will + * fire until it is removed. A callable object is returned that, + * when called, removes the event listener. Note that this function + * works even if the browser's frameloader is swapped. + * + * @param {xul:browser} browser + * The browser element to listen for events in. + * @param {string} eventName + * Name of the event to listen to. + * @param {function} listener + * Function to call in parent process when event fires. + * Not passed any arguments. + * @param {object} listenerOptions [optional] + * Options to pass to the event listener. + * @param {function} checkFn [optional] + * Called with the Event object as argument, should return true if the + * event is the expected one, or false if it should be ignored and + * listening should continue. If not specified, the first event with + * the specified name resolves the returned promise. This is called + * within the content process and can have no closure environment. + * + * @returns function + * If called, the return value will remove the event listener. + */ + addContentEventListener( + browser, + eventName, + listener, + listenerOptions = {}, + checkFn + ) { + let id = gListenerId++; + let contentEventListeners = this._contentEventListeners; + contentEventListeners.set(id, { + listener, + browserId: browser.browserId, + }); + + let eventListenerState = this._contentEventListenerSharedState; + eventListenerState.set(id, { + eventName, + listenerOptions, + checkFnSource: checkFn ? checkFn.toSource() : "", + }); + + Services.ppmm.sharedData.set( + "BrowserTestUtils:ContentEventListener", + eventListenerState + ); + Services.ppmm.sharedData.flush(); + + let unregisterFunction = function() { + if (!eventListenerState.has(id)) { + return; + } + eventListenerState.delete(id); + contentEventListeners.delete(id); + Services.ppmm.sharedData.set( + "BrowserTestUtils:ContentEventListener", + eventListenerState + ); + Services.ppmm.sharedData.flush(); + }; + return unregisterFunction; + }, + + /** + * This is an internal method to be invoked by + * BrowserTestUtilsParent.sys.mjs when a content event we were listening for + * happens. + * + * @private + */ + _receivedContentEventListener(listenerId, browserId) { + let listenerData = this._contentEventListeners.get(listenerId); + if (!listenerData) { + return; + } + if (listenerData.browserId != browserId) { + return; + } + listenerData.listener(); + }, + + /** + * This is an internal method that cleans up any state from content event + * listeners. + * + * @private + */ + _cleanupContentEventListeners() { + this._contentEventListeners.clear(); + + if (this._contentEventListenerSharedState.size != 0) { + this._contentEventListenerSharedState.clear(); + Services.ppmm.sharedData.set( + "BrowserTestUtils:ContentEventListener", + this._contentEventListenerSharedState + ); + Services.ppmm.sharedData.flush(); + } + + if (this._contentEventListenerActorRegistered) { + this._contentEventListenerActorRegistered = false; + ChromeUtils.unregisterWindowActor("ContentEventListener"); + } + }, + + observe(subject, topic, data) { + switch (topic) { + case "test-complete": + this._cleanupContentEventListeners(); + break; + } + }, + + /** + * Wait until DOM mutations cause the condition expressed in checkFn + * to pass. + * + * Intended as an easy-to-use alternative to waitForCondition. + * + * @param {Element} target The target in which to observe mutations. + * @param {Object} options The options to pass to MutationObserver.observe(); + * @param {function} checkFn Function that returns true when it wants the promise to be + * resolved. + */ + waitForMutationCondition(target, options, checkFn) { + if (checkFn()) { + return Promise.resolve(); + } + return new Promise(resolve => { + let obs = new target.ownerGlobal.MutationObserver(function() { + if (checkFn()) { + obs.disconnect(); + resolve(); + } + }); + obs.observe(target, options); + }); + }, + + /** + * Like browserLoaded, but waits for an error page to appear. + * + * @param {xul:browser} browser + * A xul:browser. + * + * @return {Promise} + * @resolves When an error page has been loaded in the browser. + */ + waitForErrorPage(browser) { + return this.waitForContentEvent( + browser, + "AboutNetErrorLoad", + false, + null, + true + ); + }, + + /** + * Waits for the next top-level document load in the current browser. The URI + * of the document is compared against expectedURL. The load is then stopped + * before it actually starts. + * + * @param {string} expectedURL + * The URL of the document that is expected to load. + * @param {object} browser + * The browser to wait for. + * @param {function} checkFn (optional) + * Function to run on the channel before stopping it. + * @returns {Promise} + */ + waitForDocLoadAndStopIt(expectedURL, browser, checkFn) { + let isHttp = url => /^https?:/.test(url); + + return new Promise(resolve => { + // Redirect non-http URIs to http://mochi.test:8888/, so we can still + // use http-on-before-connect to listen for loads. Since we're + // aborting the load as early as possible, it doesn't matter whether the + // server handles it sensibly or not. However, this also means that this + // helper shouldn't be used to load local URIs (about pages, chrome:// + // URIs, etc). + let proxyFilter; + if (!isHttp(expectedURL)) { + proxyFilter = { + proxyInfo: lazy.ProtocolProxyService.newProxyInfo( + "http", + "mochi.test", + 8888, + "", + "", + 0, + 4096, + null + ), + + applyFilter(channel, defaultProxyInfo, callback) { + callback.onProxyFilterResult( + isHttp(channel.URI.spec) ? defaultProxyInfo : this.proxyInfo + ); + }, + }; + + lazy.ProtocolProxyService.registerChannelFilter(proxyFilter, 0); + } + + function observer(chan) { + chan.QueryInterface(Ci.nsIHttpChannel); + if (!chan.originalURI || chan.originalURI.spec !== expectedURL) { + return; + } + if (checkFn && !checkFn(chan)) { + return; + } + + // TODO: We should check that the channel's BrowsingContext matches + // the browser's. See bug 1587114. + + try { + chan.cancel(Cr.NS_BINDING_ABORTED); + } finally { + if (proxyFilter) { + lazy.ProtocolProxyService.unregisterChannelFilter(proxyFilter); + } + Services.obs.removeObserver(observer, "http-on-before-connect"); + resolve(); + } + } + + Services.obs.addObserver(observer, "http-on-before-connect"); + }); + }, + + /** + * Versions of EventUtils.jsm synthesizeMouse functions that synthesize a + * mouse event in a child process and return promises that resolve when the + * event has fired and completed. Instead of a window, a browser or + * browsing context is required to be passed to this function. + * + * @param target + * One of the following: + * - a selector string that identifies the element to target. The syntax is as + * for querySelector. + * - a function to be run in the content process that returns the element to + * target + * - null, in which case the offset is from the content document's edge. + * @param {integer} offsetX + * x offset from target's left bounding edge + * @param {integer} offsetY + * y offset from target's top bounding edge + * @param {Object} event object + * Additional arguments, similar to the EventUtils.jsm version + * @param {BrowserContext|MozFrameLoaderOwner} browsingContext + * Browsing context or browser element, must not be null + * @param {boolean} handlingUserInput + * Whether the synthesize should be perfomed while simulating + * user interaction (making windowUtils.isHandlingUserInput be true). + * + * @returns {Promise} + * @resolves True if the mouse event was cancelled. + */ + synthesizeMouse( + target, + offsetX, + offsetY, + event, + browsingContext, + handlingUserInput + ) { + let targetFn = null; + if (typeof target == "function") { + targetFn = target.toString(); + target = null; + } else if (typeof target != "string" && !Array.isArray(target)) { + target = null; + } + + browsingContext = this.getBrowsingContextFrom(browsingContext); + return this.sendQuery(browsingContext, "Test:SynthesizeMouse", { + target, + targetFn, + x: offsetX, + y: offsetY, + event, + handlingUserInput, + }); + }, + + /** + * Versions of EventUtils.jsm synthesizeTouch functions that synthesize a + * touch event in a child process and return promises that resolve when the + * event has fired and completed. Instead of a window, a browser or + * browsing context is required to be passed to this function. + * + * @param target + * One of the following: + * - a selector string that identifies the element to target. The syntax is as + * for querySelector. + * - a function to be run in the content process that returns the element to + * target + * - null, in which case the offset is from the content document's edge. + * @param {integer} offsetX + * x offset from target's left bounding edge + * @param {integer} offsetY + * y offset from target's top bounding edge + * @param {Object} event object + * Additional arguments, similar to the EventUtils.jsm version + * @param {BrowserContext|MozFrameLoaderOwner} browsingContext + * Browsing context or browser element, must not be null + * + * @returns {Promise} + * @resolves True if the touch event was cancelled. + */ + synthesizeTouch(target, offsetX, offsetY, event, browsingContext) { + let targetFn = null; + if (typeof target == "function") { + targetFn = target.toString(); + target = null; + } else if (typeof target != "string" && !Array.isArray(target)) { + target = null; + } + + browsingContext = this.getBrowsingContextFrom(browsingContext); + return this.sendQuery(browsingContext, "Test:SynthesizeTouch", { + target, + targetFn, + x: offsetX, + y: offsetY, + event, + }); + }, + + /** + * Wait for a message to be fired from a particular message manager + * + * @param {nsIMessageManager} messageManager + * The message manager that should be used. + * @param {String} message + * The message we're waiting for. + * @param {Function} checkFn (optional) + * Optional function to invoke to check the message. + */ + waitForMessage(messageManager, message, checkFn) { + return new Promise(resolve => { + messageManager.addMessageListener(message, function onMessage(msg) { + if (!checkFn || checkFn(msg)) { + messageManager.removeMessageListener(message, onMessage); + resolve(msg.data); + } + }); + }); + }, + + /** + * Version of synthesizeMouse that uses the center of the target as the mouse + * location. Arguments and the return value are the same. + */ + synthesizeMouseAtCenter(target, event, browsingContext) { + // Use a flag to indicate to center rather than having a separate message. + event.centered = true; + return BrowserTestUtils.synthesizeMouse( + target, + 0, + 0, + event, + browsingContext + ); + }, + + /** + * Version of synthesizeMouse that uses a client point within the child + * window instead of a target as the offset. Otherwise, the arguments and + * return value are the same as synthesizeMouse. + */ + synthesizeMouseAtPoint(offsetX, offsetY, event, browsingContext) { + return BrowserTestUtils.synthesizeMouse( + null, + offsetX, + offsetY, + event, + browsingContext + ); + }, + + /** + * Removes the given tab from its parent tabbrowser. + * This method doesn't SessionStore etc. + * + * @param (tab) tab + * The tab to remove. + * @param (Object) options + * Extra options to pass to tabbrowser's removeTab method. + */ + removeTab(tab, options = {}) { + tab.ownerGlobal.gBrowser.removeTab(tab, options); + }, + + /** + * Returns a Promise that resolves once the tab starts closing. + * + * @param (tab) tab + * The tab that will be removed. + * @returns (Promise) + * @resolves When the tab starts closing. Does not get passed a value. + */ + waitForTabClosing(tab) { + return this.waitForEvent(tab, "TabClose"); + }, + + /** + * + * @param {tab} tab + * The tab that will be reloaded. + * @param {Boolean} [includeSubFrames = false] + * A boolean indicating if loads from subframes should be included + * when waiting for the frame to reload. + * @returns {Promise} + * @resolves When the tab finishes reloading. + */ + reloadTab(tab, includeSubFrames = false) { + const finished = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + includeSubFrames + ); + tab.ownerGlobal.gBrowser.reloadTab(tab); + return finished; + }, + + /** + * Create enough tabs to cause a tab overflow in the given window. + * @param {Function} registerCleanupFunction + * The test framework doesn't keep its cleanup stuff anywhere accessible, + * so the first argument is a reference to your cleanup registration + * function, allowing us to clean up after you if necessary. + * @param {Window} win + * The window where the tabs need to be overflowed. + * @param {boolean} overflowAtStart + * Determines whether the new tabs are added at the beginning of the + * URL bar or at the end of it. + */ + async overflowTabs(registerCleanupFunction, win, overflowAtStart = true) { + let index = overflowAtStart ? 0 : undefined; + let { gBrowser } = win; + let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox; + const originalSmoothScroll = arrowScrollbox.smoothScroll; + arrowScrollbox.smoothScroll = false; + registerCleanupFunction(() => { + arrowScrollbox.smoothScroll = originalSmoothScroll; + }); + + let width = ele => ele.getBoundingClientRect().width; + let tabMinWidth = parseInt( + win.getComputedStyle(gBrowser.selectedTab).minWidth + ); + let tabCountForOverflow = Math.ceil( + (width(arrowScrollbox) / tabMinWidth) * 1.1 + ); + while (gBrowser.tabs.length < tabCountForOverflow) { + BrowserTestUtils.addTab(gBrowser, "about:blank", { + skipAnimation: true, + index, + }); + } + }, + + /** + * Crashes a remote frame tab and cleans up the generated minidumps. + * Resolves with the data from the .extra file (the crash annotations). + * + * @param (Browser) browser + * A remote <xul:browser> element. Must not be null. + * @param (bool) shouldShowTabCrashPage + * True if it is expected that the tab crashed page will be shown + * for this browser. If so, the Promise will only resolve once the + * tab crash page has loaded. + * @param (bool) shouldClearMinidumps + * True if the minidumps left behind by the crash should be removed. + * @param (BrowsingContext) browsingContext + * The context where the frame leaves. Default to + * top level context if not supplied. + * @param (object?) options + * An object with any of the following fields: + * crashType: "CRASH_INVALID_POINTER_DEREF" | "CRASH_OOM" + * The type of crash. If unspecified, default to "CRASH_INVALID_POINTER_DEREF" + * asyncCrash: bool + * If specified and `true`, cause the crash asynchronously. + * + * @returns (Promise) + * @resolves An Object with key-value pairs representing the data from the + * crash report's extra file (if applicable). + */ + async crashFrame( + browser, + shouldShowTabCrashPage = true, + shouldClearMinidumps = true, + browsingContext, + options = {} + ) { + let extra = {}; + + if (!browser.isRemoteBrowser) { + throw new Error("<xul:browser> needs to be remote in order to crash"); + } + + /** + * Returns the directory where crash dumps are stored. + * + * @return nsIFile + */ + function getMinidumpDirectory() { + let dir = Services.dirsvc.get("ProfD", Ci.nsIFile); + dir.append("minidumps"); + return dir; + } + + /** + * Removes a file from a directory. This is a no-op if the file does not + * exist. + * + * @param directory + * The nsIFile representing the directory to remove from. + * @param filename + * A string for the file to remove from the directory. + */ + function removeFile(directory, filename) { + let file = directory.clone(); + file.append(filename); + if (file.exists()) { + file.remove(false); + } + } + + let expectedPromises = []; + + let crashCleanupPromise = new Promise((resolve, reject) => { + let observer = (subject, topic, data) => { + if (topic != "ipc:content-shutdown") { + reject("Received incorrect observer topic: " + topic); + return; + } + if (!(subject instanceof Ci.nsIPropertyBag2)) { + reject("Subject did not implement nsIPropertyBag2"); + return; + } + // we might see this called as the process terminates due to previous tests. + // We are only looking for "abnormal" exits... + if (!subject.hasKey("abnormal")) { + dump( + "\nThis is a normal termination and isn't the one we are looking for...\n" + ); + return; + } + + Services.obs.removeObserver(observer, "ipc:content-shutdown"); + + let dumpID; + if (AppConstants.MOZ_CRASHREPORTER) { + dumpID = subject.getPropertyAsAString("dumpID"); + if (!dumpID) { + reject( + "dumpID was not present despite crash reporting being enabled" + ); + return; + } + } + + let removalPromise = Promise.resolve(); + + if (dumpID) { + removalPromise = Services.crashmanager + .ensureCrashIsPresent(dumpID) + .then(async () => { + let minidumpDirectory = getMinidumpDirectory(); + let extrafile = minidumpDirectory.clone(); + extrafile.append(dumpID + ".extra"); + if (extrafile.exists()) { + if (AppConstants.MOZ_CRASHREPORTER) { + extra = await IOUtils.readJSON(extrafile.path); + } else { + dump( + "\nCrashReporter not enabled - will not return any extra data\n" + ); + } + } else { + dump(`\nNo .extra file for dumpID: ${dumpID}\n`); + } + + if (shouldClearMinidumps) { + removeFile(minidumpDirectory, dumpID + ".dmp"); + removeFile(minidumpDirectory, dumpID + ".extra"); + } + }); + } + + removalPromise.then(() => { + dump("\nCrash cleaned up\n"); + // There might be other ipc:content-shutdown handlers that need to + // run before we want to continue, so we'll resolve on the next tick + // of the event loop. + TestUtils.executeSoon(() => resolve()); + }); + }; + + Services.obs.addObserver(observer, "ipc:content-shutdown"); + }); + + expectedPromises.push(crashCleanupPromise); + + if (shouldShowTabCrashPage) { + expectedPromises.push( + new Promise((resolve, reject) => { + browser.addEventListener( + "AboutTabCrashedReady", + function onCrash() { + browser.removeEventListener("AboutTabCrashedReady", onCrash); + dump("\nabout:tabcrashed loaded and ready\n"); + resolve(); + }, + false, + true + ); + }) + ); + } + + // Trigger crash by sending a message to BrowserTestUtils actor. + this.sendAsyncMessage( + browsingContext || browser.browsingContext, + "BrowserTestUtils:CrashFrame", + { + crashType: options.crashType || "", + asyncCrash: options.asyncCrash || false, + } + ); + + await Promise.all(expectedPromises); + + if (shouldShowTabCrashPage) { + let gBrowser = browser.ownerGlobal.gBrowser; + let tab = gBrowser.getTabForBrowser(browser); + if (tab.getAttribute("crashed") != "true") { + throw new Error("Tab should be marked as crashed"); + } + } + + return extra; + }, + + /** + * Attempts to simulate a launch fail by crashing a browser, but + * stripping the browser of its childID so that the TabCrashHandler + * thinks it was a launch fail. + * + * @param browser (<xul:browser>) + * The browser to simulate a content process launch failure on. + * @return Promise + * @resolves undefined + * Resolves when the TabCrashHandler should be done handling the + * simulated crash. + */ + simulateProcessLaunchFail(browser, dueToBuildIDMismatch = false) { + const NORMAL_CRASH_TOPIC = "ipc:content-shutdown"; + + Object.defineProperty(browser.frameLoader, "childID", { + get: () => 0, + }); + + let sawNormalCrash = false; + let observer = (subject, topic, data) => { + sawNormalCrash = true; + }; + + Services.obs.addObserver(observer, NORMAL_CRASH_TOPIC); + + Services.obs.notifyObservers( + browser.frameLoader, + "oop-frameloader-crashed" + ); + + let eventType = dueToBuildIDMismatch + ? "oop-browser-buildid-mismatch" + : "oop-browser-crashed"; + + let event = new browser.ownerGlobal.CustomEvent(eventType, { + bubbles: true, + }); + event.isTopFrame = true; + browser.dispatchEvent(event); + + Services.obs.removeObserver(observer, NORMAL_CRASH_TOPIC); + + if (sawNormalCrash) { + throw new Error(`Unexpectedly saw ${NORMAL_CRASH_TOPIC}`); + } + + return new Promise(resolve => TestUtils.executeSoon(resolve)); + }, + + /** + * Returns a promise that is resolved when element gains attribute (or, + * optionally, when it is set to value). + * @param {String} attr + * The attribute to wait for + * @param {Element} element + * The element which should gain the attribute + * @param {String} value (optional) + * Optional, the value the attribute should have. + * + * @returns {Promise} + */ + waitForAttribute(attr, element, value) { + let MutationObserver = element.ownerGlobal.MutationObserver; + return new Promise(resolve => { + let mut = new MutationObserver(mutations => { + if ( + (!value && element.hasAttribute(attr)) || + (value && element.getAttribute(attr) === value) + ) { + resolve(); + mut.disconnect(); + } + }); + + mut.observe(element, { attributeFilter: [attr] }); + }); + }, + + /** + * Returns a promise that is resolved when element loses an attribute. + * @param {String} attr + * The attribute to wait for + * @param {Element} element + * The element which should lose the attribute + * + * @returns {Promise} + */ + waitForAttributeRemoval(attr, element) { + if (!element.hasAttribute(attr)) { + return Promise.resolve(); + } + + let MutationObserver = element.ownerGlobal.MutationObserver; + return new Promise(resolve => { + dump("Waiting for removal\n"); + let mut = new MutationObserver(mutations => { + if (!element.hasAttribute(attr)) { + resolve(); + mut.disconnect(); + } + }); + + mut.observe(element, { attributeFilter: [attr] }); + }); + }, + + /** + * Version of EventUtils' `sendChar` function; it will synthesize a keypress + * event in a child process and returns a Promise that will resolve when the + * event was fired. Instead of a Window, a Browser or Browsing Context + * is required to be passed to this function. + * + * @param {String} char + * A character for the keypress event that is sent to the browser. + * @param {BrowserContext|MozFrameLoaderOwner} browsingContext + * Browsing context or browser element, must not be null + * + * @returns {Promise} + * @resolves True if the keypress event was synthesized. + */ + sendChar(char, browsingContext) { + browsingContext = this.getBrowsingContextFrom(browsingContext); + return this.sendQuery(browsingContext, "Test:SendChar", { char }); + }, + + /** + * Version of EventUtils' `synthesizeKey` function; it will synthesize a key + * event in a child process and returns a Promise that will resolve when the + * event was fired. Instead of a Window, a Browser or Browsing Context + * is required to be passed to this function. + * + * @param {String} key + * See the documentation available for EventUtils#synthesizeKey. + * @param {Object} event + * See the documentation available for EventUtils#synthesizeKey. + * @param {BrowserContext|MozFrameLoaderOwner} browsingContext + * Browsing context or browser element, must not be null + * + * @returns {Promise} + */ + synthesizeKey(key, event, browsingContext) { + browsingContext = this.getBrowsingContextFrom(browsingContext); + return this.sendQuery(browsingContext, "Test:SynthesizeKey", { + key, + event, + }); + }, + + /** + * Version of EventUtils' `synthesizeComposition` function; it will synthesize + * a composition event in a child process and returns a Promise that will + * resolve when the event was fired. Instead of a Window, a Browser or + * Browsing Context is required to be passed to this function. + * + * @param {Object} event + * See the documentation available for EventUtils#synthesizeComposition. + * @param {BrowserContext|MozFrameLoaderOwner} browsingContext + * Browsing context or browser element, must not be null + * + * @returns {Promise} + * @resolves False if the composition event could not be synthesized. + */ + synthesizeComposition(event, browsingContext) { + browsingContext = this.getBrowsingContextFrom(browsingContext); + return this.sendQuery(browsingContext, "Test:SynthesizeComposition", { + event, + }); + }, + + /** + * Version of EventUtils' `synthesizeCompositionChange` function; it will + * synthesize a compositionchange event in a child process and returns a + * Promise that will resolve when the event was fired. Instead of a Window, a + * Browser or Browsing Context object is required to be passed to this function. + * + * @param {Object} event + * See the documentation available for EventUtils#synthesizeCompositionChange. + * @param {BrowserContext|MozFrameLoaderOwner} browsingContext + * Browsing context or browser element, must not be null + * + * @returns {Promise} + */ + synthesizeCompositionChange(event, browsingContext) { + browsingContext = this.getBrowsingContextFrom(browsingContext); + return this.sendQuery(browsingContext, "Test:SynthesizeCompositionChange", { + event, + }); + }, + + // TODO: Fix consumers and remove me. + waitForCondition: TestUtils.waitForCondition, + + /** + * Waits for a <xul:notification> with a particular value to appear + * for the <xul:notificationbox> of the passed in browser. + * + * @param {xul:tabbrowser} tabbrowser + * The gBrowser that hosts the browser that should show + * the notification. For most tests, this will probably be + * gBrowser. + * @param {xul:browser} browser + * The browser that should be showing the notification. + * @param {String} notificationValue + * The "value" of the notification, which is often used as + * a unique identifier. Example: "plugin-crashed". + * + * @return {Promise} + * Resolves to the <xul:notification> that is being shown. + */ + waitForNotificationBar(tabbrowser, browser, notificationValue) { + let notificationBox = tabbrowser.getNotificationBox(browser); + return this.waitForNotificationInNotificationBox( + notificationBox, + notificationValue + ); + }, + + /** + * Waits for a <xul:notification> with a particular value to appear + * in the global <xul:notificationbox> of the given browser window. + * + * @param {Window} win + * The browser window in whose global notificationbox the + * notification is expected to appear. + * @param {String} notificationValue + * The "value" of the notification, which is often used as + * a unique identifier. Example: "captive-portal-detected". + * + * @return {Promise} + * Resolves to the <xul:notification> that is being shown. + */ + waitForGlobalNotificationBar(win, notificationValue) { + return this.waitForNotificationInNotificationBox( + win.gNotificationBox, + notificationValue + ); + }, + + waitForNotificationInNotificationBox(notificationBox, notificationValue) { + return new Promise(resolve => { + let check = event => { + return event.target.getAttribute("value") == notificationValue; + }; + + BrowserTestUtils.waitForEvent( + notificationBox.stack, + "AlertActive", + false, + check + ).then(event => { + // The originalTarget of the AlertActive on a notificationbox + // will be the notification itself. + resolve(event.originalTarget); + }); + }); + }, + + /** + * Waits for CSS transitions to complete for an element. Tracks any + * transitions that start after this function is called and resolves once all + * started transitions complete. + * + * @param {Element} element + * The element that will transition. + * @param {Number} timeout + * The maximum time to wait in milliseconds. Defaults to 5 seconds. + * @return {Promise} + * Resolves when transitions complete or rejects if the timeout is hit. + */ + waitForTransition(element, timeout = 5000) { + return new Promise((resolve, reject) => { + let cleanup = () => { + element.removeEventListener("transitionrun", listener); + element.removeEventListener("transitionend", listener); + }; + + let timer = element.ownerGlobal.setTimeout(() => { + cleanup(); + reject(); + }, timeout); + + let transitionCount = 0; + + let listener = event => { + if (event.type == "transitionrun") { + transitionCount++; + } else { + transitionCount--; + if (transitionCount == 0) { + cleanup(); + element.ownerGlobal.clearTimeout(timer); + resolve(); + } + } + }; + + element.addEventListener("transitionrun", listener); + element.addEventListener("transitionend", listener); + element.addEventListener("transitioncancel", listener); + }); + }, + + _knownAboutPages: new Set(), + _loadedAboutContentScript: false, + + /** + * Registers an about: page with particular flags in both the parent + * and any content processes. Returns a promise that resolves when + * registration is complete. + * + * @param {Function} registerCleanupFunction + * The test framework doesn't keep its cleanup stuff anywhere accessible, + * so the first argument is a reference to your cleanup registration + * function, allowing us to clean up after you if necessary. + * @param {String} aboutModule + * The name of the about page. + * @param {String} pageURI + * The URI the about: page should point to. + * @param {Number} flags + * The nsIAboutModule flags to use for registration. + * + * @returns {Promise} + * Promise that resolves when registration has finished. + */ + registerAboutPage(registerCleanupFunction, aboutModule, pageURI, flags) { + // Return a promise that resolves when registration finished. + const kRegistrationMsgId = + "browser-test-utils:about-registration:registered"; + let rv = this.waitForMessage(Services.ppmm, kRegistrationMsgId, msg => { + return msg.data == aboutModule; + }); + // Load a script that registers our page, then send it a message to execute the registration. + if (!this._loadedAboutContentScript) { + Services.ppmm.loadProcessScript( + kAboutPageRegistrationContentScript, + true + ); + this._loadedAboutContentScript = true; + registerCleanupFunction(this._removeAboutPageRegistrations.bind(this)); + } + Services.ppmm.broadcastAsyncMessage( + "browser-test-utils:about-registration:register", + { aboutModule, pageURI, flags } + ); + return rv.then(() => { + this._knownAboutPages.add(aboutModule); + }); + }, + + unregisterAboutPage(aboutModule) { + if (!this._knownAboutPages.has(aboutModule)) { + return Promise.reject( + new Error("We don't think this about page exists!") + ); + } + const kUnregistrationMsgId = + "browser-test-utils:about-registration:unregistered"; + let rv = this.waitForMessage(Services.ppmm, kUnregistrationMsgId, msg => { + return msg.data == aboutModule; + }); + Services.ppmm.broadcastAsyncMessage( + "browser-test-utils:about-registration:unregister", + aboutModule + ); + return rv.then(() => this._knownAboutPages.delete(aboutModule)); + }, + + async _removeAboutPageRegistrations() { + for (let aboutModule of this._knownAboutPages) { + await this.unregisterAboutPage(aboutModule); + } + Services.ppmm.removeDelayedProcessScript( + kAboutPageRegistrationContentScript + ); + }, + + /** + * Waits for the dialog to open, and clicks the specified button. + * + * @param {string} buttonNameOrElementID + * The name of the button ("accept", "cancel", etc) or element ID to + * click. + * @param {string} uri + * The URI of the dialog to wait for. Defaults to the common dialog. + * @return {Promise} + * A Promise which resolves when a "domwindowopened" notification + * for a dialog has been fired by the window watcher and the + * specified button is clicked. + */ + async promiseAlertDialogOpen( + buttonNameOrElementID, + uri = "chrome://global/content/commonDialog.xhtml", + options = { callback: null, isSubDialog: false } + ) { + let win; + if (uri == "chrome://global/content/commonDialog.xhtml") { + [win] = await TestUtils.topicObserved("common-dialog-loaded"); + } else if (options.isSubDialog) { + [win] = await TestUtils.topicObserved("subdialog-loaded"); + } else { + // The test listens for the "load" event which guarantees that the alert + // class has already been added (it is added when "DOMContentLoaded" is + // fired). + win = await this.domWindowOpenedAndLoaded(null, win => { + return win.document.documentURI === uri; + }); + } + + if (options.callback) { + await options.callback(win); + return win; + } + + if (buttonNameOrElementID) { + let dialog = win.document.querySelector("dialog"); + let element = + dialog.getButton(buttonNameOrElementID) || + win.document.getElementById(buttonNameOrElementID); + element.click(); + } + + return win; + }, + + /** + * Wait for the containing dialog with the id `window-modal-dialog` to become + * empty and close. + * + * @param {HTMLDialogElement} dialog + * The dialog to wait on. + * @return {Promise} + * Resolves once the the dialog has closed + */ + async waitForDialogClose(dialog) { + return this.waitForEvent(dialog, "close").then(() => { + return this.waitForMutationCondition( + dialog, + { childList: true, attributes: true }, + () => !dialog.hasChildNodes() && !dialog.open + ); + }); + }, + + /** + * Waits for the dialog to open, and clicks the specified button, and waits + * for the dialog to close. + * + * @param {string} buttonNameOrElementID + * The name of the button ("accept", "cancel", etc) or element ID to + * click. + * @param {string} uri + * The URI of the dialog to wait for. Defaults to the common dialog. + * + * @return {Promise} + * A Promise which resolves when a "domwindowopened" notification + * for a dialog has been fired by the window watcher and the + * specified button is clicked, and the dialog has been fully closed. + */ + async promiseAlertDialog( + buttonNameOrElementID, + uri = "chrome://global/content/commonDialog.xhtml", + options = { callback: null, isSubDialog: false } + ) { + let win = await this.promiseAlertDialogOpen( + buttonNameOrElementID, + uri, + options + ); + if (!win.docShell.browsingContext.embedderElement) { + return this.windowClosed(win); + } + const dialog = win.top.document.getElementById("window-modal-dialog"); + return this.waitForDialogClose(dialog); + }, + + /** + * Opens a tab with a given uri and params object. If the params object is not set + * or the params parameter does not include a triggeringPrincipal then this function + * provides a params object using the systemPrincipal as the default triggeringPrincipal. + * + * @param {xul:tabbrowser} tabbrowser + * The gBrowser object to open the tab with. + * @param {string} uri + * The URI to open in the new tab. + * @param {object} params [optional] + * Parameters object for gBrowser.addTab. + * @param {function} beforeLoadFunc [optional] + * A function to run after that xul:browser has been created but before the URL is + * loaded. Can spawn a content task in the tab, for example. + */ + addTab(tabbrowser, uri, params = {}, beforeLoadFunc = null) { + if (!params.triggeringPrincipal) { + params.triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + } + if (!params.allowInheritPrincipal) { + params.allowInheritPrincipal = true; + } + if (beforeLoadFunc) { + let window = tabbrowser.ownerGlobal; + window.addEventListener( + "TabOpen", + function(e) { + beforeLoadFunc(e.target); + }, + { once: true } + ); + } + return tabbrowser.addTab(uri, params); + }, + + /** + * There are two ways to listen for observers in a content process: + * 1. Call contentTopicObserved which will watch for an observer notification + * in a content process to occur, and will return a promise which resolves + * when that notification occurs. + * 2. Enclose calls to contentTopicObserved inside a pair of calls to + * startObservingTopics and stopObservingTopics. Usually this pair will be + * placed at the start and end of a test or set of tests. Any observer + * notification that happens between the start and stop that doesn't match + * any explicitly expected by using contentTopicObserved will cause + * stopObservingTopics to reject with an error. + * For example: + * + * await BrowserTestUtils.startObservingTopics(bc, ["a", "b", "c"]); + * await BrowserTestUtils contentTopicObserved(bc, "a", 2); + * await BrowserTestUtils.stopObservingTopics(bc, ["a", "b", "c"]); + * + * This will expect two "a" notifications to occur, but will fail if more + * than two occur, or if any "b" or "c" notifications occur. + * + * Note that this function doesn't handle adding a listener for the same topic + * more than once. To do that, use the aCount argument. + * + * @param aBrowsingContext + * The browsing context associated with the content process to listen to. + * @param {string} aTopic + * Observer topic to listen to. May be null to listen to any topic. + * @param {number} aCount + * Number of such matching topics to listen to, defaults to 1. A match + * occurs when the topic and filter function match. + * @param {function} aFilterFn + * Function to be evaluated in the content process which should + * return true if the notification matches. This function is passed + * the same arguments as nsIObserver.observe(). May be null to + * always match. + * @returns {Promise} resolves when the notification occurs. + */ + contentTopicObserved(aBrowsingContext, aTopic, aCount = 1, aFilterFn = null) { + return this.sendQuery(aBrowsingContext, "BrowserTestUtils:ObserveTopic", { + topic: aTopic, + count: aCount, + filterFunctionSource: aFilterFn ? aFilterFn.toSource() : null, + }); + }, + + /** + * Starts observing a list of topics in a content process. Use contentTopicObserved + * to allow an observer notification. Any other observer notification that occurs that + * matches one of the specified topics will cause the promise to reject. + * + * Calling this function more than once adds additional topics to be observed without + * replacing the existing ones. + * + * @param {BrowsingContext} aBrowsingContext + * The browsing context associated with the content process to listen to. + * @param {String[]} aTopics array of observer topics + * @returns {Promise} resolves when the listeners have been added. + */ + startObservingTopics(aBrowsingContext, aTopics) { + return this.sendQuery( + aBrowsingContext, + "BrowserTestUtils:StartObservingTopics", + { + topics: aTopics, + } + ); + }, + + /** + * Stop listening to a set of observer topics. + * + * @param {BrowsingContext} aBrowsingContext + * The browsing context associated with the content process to listen to. + * @param {String[]} aTopics array of observer topics. If empty, then all + * current topics being listened to are removed. + * @returns {Promise} promise that fails if an unexpected observer occurs. + */ + stopObservingTopics(aBrowsingContext, aTopics) { + return this.sendQuery( + aBrowsingContext, + "BrowserTestUtils:StopObservingTopics", + { + topics: aTopics, + } + ); + }, + + /** + * Sends a message to a specific BrowserTestUtils window actor. + * @param {BrowsingContext} aBrowsingContext + * The browsing context where the actor lives. + * @param {string} aMessageName + * Name of the message to be sent to the actor. + * @param {object} aMessageData + * Extra information to pass to the actor. + */ + async sendAsyncMessage(aBrowsingContext, aMessageName, aMessageData) { + if (!aBrowsingContext.currentWindowGlobal) { + await this.waitForCondition(() => aBrowsingContext.currentWindowGlobal); + } + + let actor = aBrowsingContext.currentWindowGlobal.getActor( + "BrowserTestUtils" + ); + actor.sendAsyncMessage(aMessageName, aMessageData); + }, + + /** + * Sends a query to a specific BrowserTestUtils window actor. + * @param {BrowsingContext} aBrowsingContext + * The browsing context where the actor lives. + * @param {string} aMessageName + * Name of the message to be sent to the actor. + * @param {object} aMessageData + * Extra information to pass to the actor. + */ + async sendQuery(aBrowsingContext, aMessageName, aMessageData) { + let startTime = Cu.now(); + if (!aBrowsingContext.currentWindowGlobal) { + await this.waitForCondition(() => aBrowsingContext.currentWindowGlobal); + } + + let actor = aBrowsingContext.currentWindowGlobal.getActor( + "BrowserTestUtils" + ); + return actor.sendQuery(aMessageName, aMessageData).then(val => { + ChromeUtils.addProfilerMarker( + "BrowserTestUtils", + { startTime, category: "Test" }, + aMessageName + ); + return val; + }); + }, +}; + +XPCOMUtils.defineLazyPreferenceGetter( + BrowserTestUtils, + "_httpsFirstEnabled", + "dom.security.https_first", + false +); + +Services.obs.addObserver(BrowserTestUtils, "test-complete"); diff --git a/testing/mochitest/BrowserTestUtils/BrowserTestUtilsChild.sys.mjs b/testing/mochitest/BrowserTestUtils/BrowserTestUtilsChild.sys.mjs new file mode 100644 index 0000000000..5172c7f17f --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtilsChild.sys.mjs @@ -0,0 +1,378 @@ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +/* 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/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", +}); + +class BrowserTestUtilsChildObserver { + constructor() { + this.currentObserverStatus = ""; + this.observerItems = []; + } + + startObservingTopics(aTopics) { + for (let topic of aTopics) { + Services.obs.addObserver(this, topic); + this.observerItems.push({ topic }); + } + } + + stopObservingTopics(aTopics) { + if (aTopics) { + for (let topic of aTopics) { + let index = this.observerItems.findIndex(item => item.topic == topic); + if (index >= 0) { + Services.obs.removeObserver(this, topic); + this.observerItems.splice(index, 1); + } + } + } else { + for (let topic of this.observerItems) { + Services.obs.removeObserver(this, topic); + } + this.observerItems = []; + } + + if (this.currentObserverStatus) { + let error = new Error(this.currentObserverStatus); + this.currentObserverStatus = ""; + throw error; + } + } + + observeTopic(topic, count, filterFn, callbackResolver) { + // If the topic is in the list already, assume that it came from a + // startObservingTopics call. If it isn't in the list already, assume + // that it isn't within a start/stop set and the observer has to be + // removed afterwards. + let removeObserver = false; + let index = this.observerItems.findIndex(item => item.topic == topic); + if (index == -1) { + removeObserver = true; + this.startObservingTopics([topic]); + } + + for (let item of this.observerItems) { + if (item.topic == topic) { + item.count = count || 1; + item.filterFn = filterFn; + item.promiseResolver = () => { + if (removeObserver) { + this.stopObservingTopics([topic]); + } + callbackResolver(); + }; + break; + } + } + } + + observe(aSubject, aTopic, aData) { + for (let item of this.observerItems) { + if (item.topic != aTopic) { + continue; + } + if (item.filterFn && !item.filterFn(aSubject, aTopic, aData)) { + break; + } + + if (--item.count >= 0) { + if (item.count == 0 && item.promiseResolver) { + item.promiseResolver(); + } + return; + } + } + + // Otherwise, if the observer doesn't match, fail. + console.log( + "Failed: Observer topic " + aTopic + " not expected in content process" + ); + this.currentObserverStatus += + "Topic " + aTopic + " not expected in content process\n"; + } +} + +BrowserTestUtilsChildObserver.prototype.QueryInterface = ChromeUtils.generateQI( + ["nsIObserver", "nsISupportsWeakReference"] +); + +export class BrowserTestUtilsChild extends JSWindowActorChild { + actorCreated() { + this._EventUtils = null; + } + + get EventUtils() { + if (!this._EventUtils) { + // Set up a dummy environment so that EventUtils works. We need to be careful to + // pass a window object into each EventUtils method we call rather than having + // it rely on the |window| global. + let win = this.contentWindow; + let EventUtils = { + get KeyboardEvent() { + return win.KeyboardEvent; + }, + // EventUtils' `sendChar` function relies on the navigator to synthetize events. + get navigator() { + return win.navigator; + }, + }; + + EventUtils.window = {}; + EventUtils.parent = EventUtils.window; + EventUtils._EU_Ci = Ci; + EventUtils._EU_Cc = Cc; + + Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", + EventUtils + ); + + this._EventUtils = EventUtils; + } + + return this._EventUtils; + } + + receiveMessage(aMessage) { + switch (aMessage.name) { + case "Test:SynthesizeMouse": { + return this.synthesizeMouse(aMessage.data, this.contentWindow); + } + + case "Test:SynthesizeTouch": { + return this.synthesizeTouch(aMessage.data, this.contentWindow); + } + + case "Test:SendChar": { + return this.EventUtils.sendChar(aMessage.data.char, this.contentWindow); + } + + case "Test:SynthesizeKey": + this.EventUtils.synthesizeKey( + aMessage.data.key, + aMessage.data.event || {}, + this.contentWindow + ); + break; + + case "Test:SynthesizeComposition": { + return this.EventUtils.synthesizeComposition( + aMessage.data.event, + this.contentWindow + ); + } + + case "Test:SynthesizeCompositionChange": + this.EventUtils.synthesizeCompositionChange( + aMessage.data.event, + this.contentWindow + ); + break; + + case "BrowserTestUtils:StartObservingTopics": { + this.observer = new BrowserTestUtilsChildObserver(); + this.observer.startObservingTopics(aMessage.data.topics); + break; + } + + case "BrowserTestUtils:StopObservingTopics": { + if (this.observer) { + this.observer.stopObservingTopics(aMessage.data.topics); + this.observer = null; + } + break; + } + + case "BrowserTestUtils:ObserveTopic": { + return new Promise(resolve => { + let filterFn; + if (aMessage.data.filterFunctionSource) { + /* eslint-disable-next-line no-eval */ + filterFn = eval( + `(() => (${aMessage.data.filterFunctionSource}))()` + ); + } + + let observer = this.observer || new BrowserTestUtilsChildObserver(); + observer.observeTopic( + aMessage.data.topic, + aMessage.data.count, + filterFn, + resolve + ); + }); + } + + case "BrowserTestUtils:CrashFrame": { + // This is to intentionally crash the frame. + // We crash by using js-ctypes. The crash + // should happen immediately + // upon loading this frame script. + + const { ctypes } = ChromeUtils.import( + "resource://gre/modules/ctypes.jsm" + ); + + let dies = function() { + dump("\nEt tu, Brute?\n"); + ChromeUtils.privateNoteIntentionalCrash(); + + switch (aMessage.data.crashType) { + case "CRASH_OOM": { + let debug = Cc["@mozilla.org/xpcom/debug;1"].getService( + Ci.nsIDebug2 + ); + debug.crashWithOOM(); + break; + } + case "CRASH_INVALID_POINTER_DEREF": // Fallthrough + default: { + // Dereference a bad pointer. + let zero = new ctypes.intptr_t(8); + let badptr = ctypes.cast( + zero, + ctypes.PointerType(ctypes.int32_t) + ); + badptr.contents; + } + } + }; + + if (aMessage.data.asyncCrash) { + let { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" + ); + // Get out of the stack. + setTimeout(dies, 0); + } else { + dies(); + } + } + } + + return undefined; + } + + handleEvent(aEvent) { + switch (aEvent.type) { + case "DOMContentLoaded": + case "load": { + this.sendAsyncMessage(aEvent.type, { + internalURL: aEvent.target.documentURI, + visibleURL: aEvent.target.location + ? aEvent.target.location.href + : null, + }); + break; + } + } + } + + synthesizeMouse(data, window) { + let target = data.target; + if (typeof target == "string") { + target = this.document.querySelector(target); + } else if (typeof data.targetFn == "string") { + let runnablestr = ` + (() => { + return (${data.targetFn}); + })();`; + /* eslint-disable no-eval */ + target = eval(runnablestr)(); + /* eslint-enable no-eval */ + } + + let left = data.x; + let top = data.y; + if (target) { + if (target.ownerDocument !== this.document) { + // Account for nodes found in iframes. + let cur = target; + do { + // eslint-disable-next-line mozilla/use-ownerGlobal + let frame = cur.ownerDocument.defaultView.frameElement; + let rect = frame.getBoundingClientRect(); + + left += rect.left; + top += rect.top; + + cur = frame; + } while (cur && cur.ownerDocument !== this.document); + + // node must be in this document tree. + if (!cur) { + throw new Error("target must be in the main document tree"); + } + } + + let rect = target.getBoundingClientRect(); + left += rect.left; + top += rect.top; + + if (data.event.centered) { + left += rect.width / 2; + top += rect.height / 2; + } + } + + let result; + + lazy.E10SUtils.wrapHandlingUserInput(window, data.handlingUserInput, () => { + if (data.event && data.event.wheel) { + this.EventUtils.synthesizeWheelAtPoint(left, top, data.event, window); + } else { + result = this.EventUtils.synthesizeMouseAtPoint( + left, + top, + data.event, + window + ); + } + }); + + return result; + } + + synthesizeTouch(data, window) { + let target = data.target; + if (typeof target == "string") { + target = this.document.querySelector(target); + } else if (typeof data.targetFn == "string") { + let runnablestr = ` + (() => { + return (${data.targetFn}); + })();`; + /* eslint-disable no-eval */ + target = eval(runnablestr)(); + /* eslint-enable no-eval */ + } + + if (target) { + if (target.ownerDocument !== this.document) { + // Account for nodes found in iframes. + let cur = target; + do { + cur = cur.ownerGlobal.frameElement; + } while (cur && cur.ownerDocument !== this.document); + + // node must be in this document tree. + if (!cur) { + throw new Error("target must be in the main document tree"); + } + } + } + + return this.EventUtils.synthesizeTouch( + target, + data.x, + data.y, + data.event, + window + ); + } +} diff --git a/testing/mochitest/BrowserTestUtils/BrowserTestUtilsParent.sys.mjs b/testing/mochitest/BrowserTestUtils/BrowserTestUtilsParent.sys.mjs new file mode 100644 index 0000000000..ae8321233a --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtilsParent.sys.mjs @@ -0,0 +1,39 @@ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +/* 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/. */ + +export class BrowserTestUtilsParent extends JSWindowActorParent { + receiveMessage(aMessage) { + switch (aMessage.name) { + case "DOMContentLoaded": + case "load": { + // Don't dispatch events that came from stale actors. + let bc = this.browsingContext; + if ( + (bc.embedderElement && bc.embedderElement.browsingContext != bc) || + !(this.manager && this.manager.isCurrentGlobal) + ) { + return; + } + + let event = new CustomEvent( + `BrowserTestUtils:ContentEvent:${aMessage.name}`, + { + detail: { + browsingContext: this.browsingContext, + ...aMessage.data, + }, + } + ); + + let browser = this.browsingContext.top.embedderElement; + if (browser) { + browser.dispatchEvent(event); + } + + break; + } + } + } +} diff --git a/testing/mochitest/BrowserTestUtils/ContentEventListenerChild.sys.mjs b/testing/mochitest/BrowserTestUtils/ContentEventListenerChild.sys.mjs new file mode 100644 index 0000000000..937a9d35cc --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/ContentEventListenerChild.sys.mjs @@ -0,0 +1,178 @@ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +/* 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/. */ + +export class ContentEventListenerChild extends JSWindowActorChild { + actorCreated() { + this._contentEvents = new Map(); + this._shutdown = false; + this._chromeEventHandler = null; + Services.cpmm.sharedData.addEventListener("change", this); + } + + didDestroy() { + this._shutdown = true; + Services.cpmm.sharedData.removeEventListener("change", this); + this._updateContentEventListeners(/* clearListeners = */ true); + if (this._contentEvents.size != 0) { + throw new Error(`Didn't expect content events after willDestroy`); + } + } + + handleEvent(event) { + switch (event.type) { + case "DOMWindowCreated": { + this._updateContentEventListeners(); + break; + } + + case "change": { + if ( + !event.changedKeys.includes("BrowserTestUtils:ContentEventListener") + ) { + return; + } + this._updateContentEventListeners(); + break; + } + } + } + + /** + * This method first determines the desired set of content event listeners + * for the window. This is either the empty set, if clearListeners is true, + * or is retrieved from the message manager's shared data. It then compares + * this event listener data to the existing set of listeners that we have + * registered, as recorded in this._contentEvents. Each content event listener + * has been assigned a unique id by the parent process. If a listener was + * added, but is not in the new event data, it is removed. If a listener was + * not present, but is in the new event data, it is added. If it is in both, + * then a basic check is done to see if they are the same. + * + * @param {bool} clearListeners [optional] + * If this is true, then instead of checking shared data to decide + * what the desired set of listeners is, just use the empty set. This + * will result in any existing listeners being cleared, and is used + * when the window is going away. + */ + _updateContentEventListeners(clearListeners = false) { + // If we've already begun the destruction process, any new event + // listeners for our bc id can't possibly really be for us, so ignore them. + if (this._shutdown && !clearListeners) { + throw new Error( + "Tried to update after we shut down content event listening" + ); + } + + let newEventData; + if (!clearListeners) { + newEventData = Services.cpmm.sharedData.get( + "BrowserTestUtils:ContentEventListener" + ); + } + if (!newEventData) { + newEventData = new Map(); + } + + // Check that entries that continue to exist are the same and remove entries + // that no longer exist. + for (let [ + listenerId, + { eventName, listener, listenerOptions }, + ] of this._contentEvents.entries()) { + let newData = newEventData.get(listenerId); + if (newData) { + if (newData.eventName !== eventName) { + // Could potentially check if listenerOptions are the same, but + // checkFnSource can't be checked unless we store it, and this is + // just a smoke test anyways, so don't bother. + throw new Error( + "Got new content event listener that disagreed with existing data" + ); + } + continue; + } + if (!this._chromeEventHandler) { + throw new Error( + "Trying to remove an event listener for waitForContentEvent without a cached event handler" + ); + } + this._chromeEventHandler.removeEventListener( + eventName, + listener, + listenerOptions + ); + this._contentEvents.delete(listenerId); + } + + let actorChild = this; + + // Add in new entries. + for (let [ + listenerId, + { eventName, listenerOptions, checkFnSource }, + ] of newEventData.entries()) { + let oldData = this._contentEvents.get(listenerId); + if (oldData) { + // We checked that the data is the same in the previous loop. + continue; + } + + /* eslint-disable no-eval */ + let checkFn; + if (checkFnSource) { + checkFn = eval(`(() => (${unescape(checkFnSource)}))()`); + } + /* eslint-enable no-eval */ + + function listener(event) { + if (checkFn && !checkFn(event)) { + return; + } + actorChild.sendAsyncMessage("ContentEventListener:Run", { + listenerId, + }); + } + + // Cache the chrome event handler because this.docShell won't be + // available during shut down. + if (!this._chromeEventHandler) { + try { + this._chromeEventHandler = this.docShell.chromeEventHandler; + } catch (error) { + if (error.name === "InvalidStateError") { + // We'll arrive here if we no longer have our manager, so we can + // just swallow this error. + continue; + } + throw error; + } + } + + // Some windows, like top-level browser windows, maybe not have a chrome + // event handler set up as this point, but we don't actually care about + // events on those windows, so ignore them. + if (!this._chromeEventHandler) { + continue; + } + + this._chromeEventHandler.addEventListener( + eventName, + listener, + listenerOptions + ); + this._contentEvents.set(listenerId, { + eventName, + listener, + listenerOptions, + }); + } + + // If there are no active content events, clear our reference to the chrome + // event handler to prevent leaks. + if (this._contentEvents.size == 0) { + this._chromeEventHandler = null; + } + } +} diff --git a/testing/mochitest/BrowserTestUtils/ContentEventListenerParent.sys.mjs b/testing/mochitest/BrowserTestUtils/ContentEventListenerParent.sys.mjs new file mode 100644 index 0000000000..1568287070 --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/ContentEventListenerParent.sys.mjs @@ -0,0 +1,20 @@ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +/* 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 { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs"; + +export class ContentEventListenerParent extends JSWindowActorParent { + receiveMessage(aMessage) { + switch (aMessage.name) { + case "ContentEventListener:Run": { + BrowserTestUtils._receivedContentEventListener( + aMessage.data.listenerId, + this.browsingContext.browserId + ); + break; + } + } + } +} diff --git a/testing/mochitest/BrowserTestUtils/ContentTask.sys.mjs b/testing/mochitest/BrowserTestUtils/ContentTask.sys.mjs new file mode 100644 index 0000000000..6c3df200c7 --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/ContentTask.sys.mjs @@ -0,0 +1,141 @@ +/* -*- 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/. */ + +const FRAME_SCRIPT = "resource://testing-common/content-task.js"; + +/** + * Keeps track of whether the frame script was already loaded. + */ +var gFrameScriptLoaded = false; + +/** + * Mapping from message id to associated promise. + */ +var gPromises = new Map(); + +/** + * Incrementing integer to generate unique message id. + */ +var gMessageID = 1; + +/** + * This object provides the public module functions. + */ +export var ContentTask = { + /** + * _testScope saves the current testScope from + * browser-test.js. This is used to implement SimpleTest functions + * like ok() and is() in the content process. The scope is only + * valid for tasks spawned in the current test, so we keep track of + * the ID of the first task spawned in this test (_scopeValidId). + */ + _testScope: null, + _scopeValidId: 0, + + /** + * Creates and starts a new task in a browser's content. + * + * @param browser A xul:browser + * @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 ContentTask_spawn(browser, arg, task) { + // Load the frame script if needed. + if (!gFrameScriptLoaded) { + Services.mm.loadFrameScript(FRAME_SCRIPT, true); + gFrameScriptLoaded = true; + } + + let deferred = {}; + deferred.promise = new Promise((resolve, reject) => { + deferred.resolve = resolve; + deferred.reject = reject; + }); + + let id = gMessageID++; + gPromises.set(id, deferred); + + browser.messageManager.sendAsyncMessage("content-task:spawn", { + id, + runnable: task.toString(), + arg, + }); + + return deferred.promise; + }, + + setTestScope(scope) { + this._testScope = scope; + this._scopeValidId = gMessageID; + }, +}; + +var ContentMessageListener = { + receiveMessage(aMessage) { + let id = aMessage.data.id; + + if (id < ContentTask._scopeValidId) { + throw new Error("test result returned after test finished"); + } + + if (aMessage.name == "content-task:complete") { + let deferred = gPromises.get(id); + gPromises.delete(id); + + if (aMessage.data.error) { + deferred.reject(aMessage.data.error); + } else { + deferred.resolve(aMessage.data.result); + } + } else if (aMessage.name == "content-task:test-result") { + let data = aMessage.data; + ContentTask._testScope.record( + data.condition, + data.name, + null, + data.stack + ); + } else if (aMessage.name == "content-task:test-info") { + ContentTask._testScope.info(aMessage.data.name); + } else if (aMessage.name == "content-task:test-todo") { + ContentTask._testScope.todo(aMessage.data.expr, aMessage.data.name); + } else if (aMessage.name == "content-task:test-todo_is") { + ContentTask._testScope.todo_is( + aMessage.data.a, + aMessage.data.b, + aMessage.data.name + ); + } + }, +}; + +Services.mm.addMessageListener("content-task:complete", ContentMessageListener); +Services.mm.addMessageListener( + "content-task:test-result", + ContentMessageListener +); +Services.mm.addMessageListener( + "content-task:test-info", + ContentMessageListener +); +Services.mm.addMessageListener( + "content-task:test-todo", + ContentMessageListener +); +Services.mm.addMessageListener( + "content-task:test-todo_is", + ContentMessageListener +); diff --git a/testing/mochitest/BrowserTestUtils/ContentTaskUtils.sys.mjs b/testing/mochitest/BrowserTestUtils/ContentTaskUtils.sys.mjs new file mode 100644 index 0000000000..f34074168e --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/ContentTaskUtils.sys.mjs @@ -0,0 +1,249 @@ +/* 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/. */ + +/* + * This module implements a number of utility functions that can be loaded + * into content scope. + * + * All asynchronous helper methods should return promises, rather than being + * callback based. + */ + +// Disable ownerGlobal use since that's not available on content-privileged elements. + +/* eslint-disable mozilla/use-ownerGlobal */ + +import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; + +export var ContentTaskUtils = { + /** + * Checks if a DOM element is hidden. + * + * @param {Element} element + * The element which is to be checked. + * + * @return {boolean} + */ + is_hidden(element) { + let style = element.ownerDocument.defaultView.getComputedStyle(element); + if (style.display == "none") { + return true; + } + if (style.visibility != "visible") { + return true; + } + + // Hiding a parent element will hide all its children + if ( + element.parentNode != element.ownerDocument && + element.parentNode.nodeType != Node.DOCUMENT_FRAGMENT_NODE + ) { + return ContentTaskUtils.is_hidden(element.parentNode); + } + + // Walk up the shadow DOM if we've reached the top of the shadow root + if (element.parentNode.host) { + return ContentTaskUtils.is_hidden(element.parentNode.host); + } + + return false; + }, + + /** + * Checks if a DOM element is visible. + * + * @param {Element} element + * The element which is to be checked. + * + * @return {boolean} + */ + is_visible(element) { + return !this.is_hidden(element); + }, + + /** + * Will poll a condition function until it returns true. + * + * @param condition + * A condition function that must return true or false. If the + * condition ever throws, this is also treated as a false. + * @param msg + * The message to use when the returned promise is rejected. + * This message will be extended with additional information + * about the number of tries or the thrown exception. + * @param interval + * The time interval to poll the condition function. Defaults + * to 100ms. + * @param maxTries + * The number of times to poll before giving up and rejecting + * if the condition has not yet returned true. Defaults to 50 + * (~5 seconds for 100ms intervals) + * @return Promise + * Resolves when condition is true. + * Rejects if timeout is exceeded or condition ever throws. + */ + async waitForCondition(condition, msg, interval = 100, maxTries = 50) { + let startTime = Cu.now(); + for (let tries = 0; tries < maxTries; ++tries) { + await new Promise(resolve => setTimeout(resolve, interval)); + + let conditionPassed = false; + try { + conditionPassed = await condition(); + } catch (e) { + msg += ` - threw exception: ${e}`; + ChromeUtils.addProfilerMarker( + "ContentTaskUtils", + { startTime, category: "Test" }, + `waitForCondition - ${msg}` + ); + throw msg; + } + if (conditionPassed) { + ChromeUtils.addProfilerMarker( + "ContentTaskUtils", + { startTime, category: "Test" }, + `waitForCondition succeeded after ${tries} retries - ${msg}` + ); + return conditionPassed; + } + } + + msg += ` - timed out after ${maxTries} tries.`; + ChromeUtils.addProfilerMarker( + "ContentTaskUtils", + { startTime, category: "Test" }, + `waitForCondition - ${msg}` + ); + throw msg; + }, + + /** + * Waits for an event to be fired on a specified element. + * + * Usage: + * let promiseEvent = ContentTasKUtils.waitForEvent(element, "eventName"); + * // Do some processing here that will cause the event to be fired + * // ... + * // Now yield until the Promise is fulfilled + * let receivedEvent = yield promiseEvent; + * + * @param {Element} subject + * The element that should receive the event. + * @param {string} eventName + * Name of the event to listen to. + * @param {bool} capture [optional] + * True to use a capturing listener. + * @param {function} checkFn [optional] + * Called with the Event object as argument, should return true if the + * event is the expected one, or false if it should be ignored and + * listening should continue. If not specified, the first event with + * the specified name resolves the returned promise. + * + * @note Because this function is intended for testing, any error in checkFn + * will cause the returned promise to be rejected instead of waiting for + * the next event, since this is probably a bug in the test. + * + * @returns {Promise} + * @resolves The Event object. + */ + waitForEvent(subject, eventName, capture, checkFn, wantsUntrusted = false) { + return new Promise((resolve, reject) => { + let startTime = Cu.now(); + subject.addEventListener( + eventName, + function listener(event) { + try { + if (checkFn && !checkFn(event)) { + return; + } + subject.removeEventListener(eventName, listener, capture); + setTimeout(() => { + ChromeUtils.addProfilerMarker( + "ContentTaskUtils", + { category: "Test", startTime }, + "waitForEvent - " + eventName + ); + resolve(event); + }, 0); + } catch (ex) { + try { + subject.removeEventListener(eventName, listener, capture); + } catch (ex2) { + // Maybe the provided object does not support removeEventListener. + } + setTimeout(() => reject(ex), 0); + } + }, + capture, + wantsUntrusted + ); + }); + }, + + /** + * Wait until DOM mutations cause the condition expressed in checkFn to pass. + * Intended as an easy-to-use alternative to waitForCondition. + * + * @param {Element} subject + * The element on which to observe mutations. + * @param {Object} options + * The options to pass to MutationObserver.observe(); + * @param {function} checkFn [optional] + * Function that returns true when it wants the promise to be resolved. + * If not specified, the first mutation will resolve the promise. + * + * @returns {Promise<void>} + */ + waitForMutationCondition(subject, options, checkFn) { + if (checkFn?.()) { + return Promise.resolve(); + } + return new Promise(resolve => { + let obs = new subject.ownerGlobal.MutationObserver(function() { + if (checkFn && !checkFn()) { + return; + } + obs.disconnect(); + resolve(); + }); + obs.observe(subject, options); + }); + }, + + /** + * Gets an instance of the `EventUtils` helper module for usage in + * content tasks. See https://searchfox.org/mozilla-central/source/testing/mochitest/tests/SimpleTest/EventUtils.js + * + * @param content + * The `content` global object from your content task. + * + * @returns an EventUtils instance. + */ + getEventUtils(content) { + if (content._EventUtils) { + return content._EventUtils; + } + + let EventUtils = (content._EventUtils = {}); + + EventUtils.window = {}; + EventUtils.setTimeout = setTimeout; + EventUtils.parent = EventUtils.window; + /* eslint-disable camelcase */ + EventUtils._EU_Ci = Ci; + EventUtils._EU_Cc = Cc; + /* eslint-enable camelcase */ + // EventUtils' `sendChar` function relies on the navigator to synthetize events. + EventUtils.navigator = content.navigator; + EventUtils.KeyboardEvent = content.KeyboardEvent; + + Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", + EventUtils + ); + + return EventUtils; + }, +}; diff --git a/testing/mochitest/BrowserTestUtils/content/content-about-page-utils.js b/testing/mochitest/BrowserTestUtils/content/content-about-page-utils.js new file mode 100644 index 0000000000..e20100db4b --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/content/content-about-page-utils.js @@ -0,0 +1,81 @@ +/* eslint-env mozilla/process-script */ + +"use strict"; + +var Cm = Components.manager; + +function AboutPage(aboutHost, chromeURL, uriFlags) { + this.chromeURL = chromeURL; + this.aboutHost = aboutHost; + this.classID = Components.ID(Services.uuid.generateUUID().number); + this.description = "BrowserTestUtils: " + aboutHost; + this.uriFlags = uriFlags; +} + +AboutPage.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]), + getURIFlags(aURI) { + // eslint-disable-line no-unused-vars + return this.uriFlags; + }, + + newChannel(aURI, aLoadInfo) { + let newURI = Services.io.newURI(this.chromeURL); + let channel = Services.io.newChannelFromURIWithLoadInfo(newURI, aLoadInfo); + channel.originalURI = aURI; + + if (this.uriFlags & Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT) { + channel.owner = null; + } + return channel; + }, + + createInstance(iid) { + return this.QueryInterface(iid); + }, + + register() { + Cm.QueryInterface(Ci.nsIComponentRegistrar).registerFactory( + this.classID, + this.description, + "@mozilla.org/network/protocol/about;1?what=" + this.aboutHost, + this + ); + }, + + unregister() { + Cm.QueryInterface(Ci.nsIComponentRegistrar).unregisterFactory( + this.classID, + this + ); + }, +}; + +const gRegisteredPages = new Map(); + +addMessageListener("browser-test-utils:about-registration:register", msg => { + let { aboutModule, pageURI, flags } = msg.data; + if (gRegisteredPages.has(aboutModule)) { + gRegisteredPages.get(aboutModule).unregister(); + } + let moduleObj = new AboutPage(aboutModule, pageURI, flags); + moduleObj.register(); + gRegisteredPages.set(aboutModule, moduleObj); + sendAsyncMessage( + "browser-test-utils:about-registration:registered", + aboutModule + ); +}); + +addMessageListener("browser-test-utils:about-registration:unregister", msg => { + let aboutModule = msg.data; + let moduleObj = gRegisteredPages.get(aboutModule); + if (moduleObj) { + moduleObj.unregister(); + gRegisteredPages.delete(aboutModule); + } + sendAsyncMessage( + "browser-test-utils:about-registration:unregistered", + aboutModule + ); +}); diff --git a/testing/mochitest/BrowserTestUtils/content/content-task.js b/testing/mochitest/BrowserTestUtils/content/content-task.js new file mode 100644 index 0000000000..8a07a22714 --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/content/content-task.js @@ -0,0 +1,124 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +"use strict"; + +let { ContentTaskUtils } = ChromeUtils.importESModule( + "resource://testing-common/ContentTaskUtils.sys.mjs" +); +const { Assert: AssertCls } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +// Injects EventUtils into ContentTask scope. To avoid leaks, this does not hold on +// to the window global. This means you **need** to pass the window as an argument to +// the individual EventUtils functions. +// See SimpleTest/EventUtils.js for documentation. +var EventUtils = { + _EU_Ci: Ci, + _EU_Cc: Cc, + KeyboardEvent: content.KeyboardEvent, + navigator: content.navigator, + setTimeout, + window: {}, +}; + +EventUtils.synthesizeClick = element => + new Promise(resolve => { + element.addEventListener( + "click", + function() { + resolve(); + }, + { once: true } + ); + + EventUtils.synthesizeMouseAtCenter( + element, + { type: "mousedown", isSynthesized: false }, + content + ); + EventUtils.synthesizeMouseAtCenter( + element, + { type: "mouseup", isSynthesized: false }, + content + ); + }); + +try { + Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", + EventUtils + ); +} catch (e) { + // There are some xpcshell tests which may use ContentTask. + // Just ignore if loading EventUtils fails. Tests that need it + // will fail anyway. + EventUtils = null; +} + +addMessageListener("content-task:spawn", async function(msg) { + let id = msg.data.id; + let source = msg.data.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"); + } + + var Assert = new AssertCls((err, message, stack) => { + sendAsyncMessage("content-task:test-result", { + id, + condition: !err, + name: err ? err.message : message, + stack: getStack(err ? err.stack : stack), + }); + }); + + /* eslint-disable no-unused-vars */ + var ok = Assert.ok.bind(Assert); + var is = Assert.equal.bind(Assert); + var isnot = Assert.notEqual.bind(Assert); + + function todo(expr, name) { + sendAsyncMessage("content-task:test-todo", { id, expr, name }); + } + + function todo_is(a, b, name) { + sendAsyncMessage("content-task:test-todo_is", { id, a, b, name }); + } + + function info(name) { + sendAsyncMessage("content-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, msg.data.arg); + sendAsyncMessage("content-task:complete", { + id, + result, + }); + } catch (ex) { + sendAsyncMessage("content-task:complete", { + id, + error: ex.toString(), + }); + } +}); diff --git a/testing/mochitest/BrowserTestUtils/moz.build b/testing/mochitest/BrowserTestUtils/moz.build new file mode 100644 index 0000000000..91675cdfbf --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/moz.build @@ -0,0 +1,16 @@ +# -*- 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/. + +TESTING_JS_MODULES += [ + "BrowserTestUtils.sys.mjs", + "BrowserTestUtilsChild.sys.mjs", + "BrowserTestUtilsParent.sys.mjs", + "content/content-task.js", + "ContentEventListenerChild.sys.mjs", + "ContentEventListenerParent.sys.mjs", + "ContentTask.sys.mjs", + "ContentTaskUtils.sys.mjs", +] diff --git a/testing/mochitest/MochiKit/Async.js b/testing/mochitest/MochiKit/Async.js new file mode 100644 index 0000000000..8ffaad2594 --- /dev/null +++ b/testing/mochitest/MochiKit/Async.js @@ -0,0 +1,681 @@ +/*** + +MochiKit.Async 1.4 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide("MochiKit.Async"); + dojo.require("MochiKit.Base"); +} +if (typeof(JSAN) != 'undefined') { + JSAN.use("MochiKit.Base", []); +} + +try { + if (typeof(MochiKit.Base) == 'undefined') { + throw ""; + } +} catch (e) { + throw "MochiKit.Async depends on MochiKit.Base!"; +} + +if (typeof(MochiKit.Async) == 'undefined') { + MochiKit.Async = {}; +} + +MochiKit.Async.NAME = "MochiKit.Async"; +MochiKit.Async.VERSION = "1.4"; +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 eval('(' + arguments[0].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, 304 is NOT_MODIFIED + if (status == 200 || status == 304) { // OK + 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) { + 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; + } + } + req.open(opts.method, url, true, opts.username, opts.password); + if (req.overrideMimeType && opts.mimeType) { + req.overrideMimeType(opts.mimeType); + } + 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/MochiKit/Base.js b/testing/mochitest/MochiKit/Base.js new file mode 100644 index 0000000000..3a91e31e30 --- /dev/null +++ b/testing/mochitest/MochiKit/Base.js @@ -0,0 +1,1401 @@ +/*** + +MochiKit.Base 1.4 + +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"; +MochiKit.Base.NAME = "MochiKit.Base"; +/** @id MochiKit.Base.update */ +MochiKit.Base.update = function (self, obj/*, ... */) { + if (self === null) { + 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(); + } + }, + + _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 = {}; + } + 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 = {}; + } + 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 + ) { + 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.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) { + try { + if (typeof(o.NAME) == 'string' && ( + o.toString == Function.prototype.toString || + o.toString == Object.prototype.toString + )) { + return o.NAME; + } + } catch (e) { + } + } + try { + var 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; + }, + + /** @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(/[\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("(" + arguments[0] + ")"); + }, + + /** @id MochiKit.Base.serializeJSON */ + serializeJSON: function (o) { + var objtype = typeof(o); + if (objtype == "number" || objtype == "boolean") { + return o + ""; + } else if (o === null) { + return "null"; + } + var m = MochiKit.Base; + var reprString = m.reprString; + if (objtype == "string") { + return reprString(o); + } + // 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") { + val = "undefined"; + } + res.push(val); + } + return "[" + res.join(", ") + "]"; + } + // look in the registry + 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 = reprString(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) { + var o = names; + names = []; + values = []; + for (var k in o) { + var v = o[k]; + if (typeof(v) == "function") { + continue; + } else if (typeof(v) != "string" && + typeof(v.length) == "number") { + 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[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; + } +}); + +/** @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", + "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; +} + +MochiKit.Base._exportSymbols(this, MochiKit.Base); diff --git a/testing/mochitest/MochiKit/Color.js b/testing/mochitest/MochiKit/Color.js new file mode 100644 index 0000000000..ac9e4151a8 --- /dev/null +++ b/testing/mochitest/MochiKit/Color.js @@ -0,0 +1,903 @@ +/*** + +MochiKit.Color 1.4 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito and others. All rights Reserved. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.Color'); + dojo.require('MochiKit.Base'); + dojo.require('MochiKit.DOM'); + dojo.require('MochiKit.Style'); +} + +if (typeof(JSAN) != 'undefined') { + JSAN.use("MochiKit.Base", []); + JSAN.use("MochiKit.DOM", []); + JSAN.use("MochiKit.Style", []); +} + +try { + if (typeof(MochiKit.Base) == 'undefined') { + throw ""; + } +} catch (e) { + throw "MochiKit.Color depends on MochiKit.Base"; +} + +try { + if (typeof(MochiKit.Base) == 'undefined') { + throw ""; + } +} catch (e) { + throw "MochiKit.Color depends on MochiKit.DOM"; +} + +try { + if (typeof(MochiKit.Base) == 'undefined') { + throw ""; + } +} catch (e) { + throw "MochiKit.Color depends on MochiKit.Style"; +} + +if (typeof(MochiKit.Color) == "undefined") { + MochiKit.Color = {}; +} + +MochiKit.Color.NAME = "MochiKit.Color"; +MochiKit.Color.VERSION = "1.4"; + +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.computedStyle.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 = 0; + green = 0; + blue = 0; + } 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 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", + rebeccapurple: "#663399", + 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/MochiKit/Controls.js b/testing/mochitest/MochiKit/Controls.js new file mode 100644 index 0000000000..539b09d374 --- /dev/null +++ b/testing/mochitest/MochiKit/Controls.js @@ -0,0 +1,1388 @@ +/*** +Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) + (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) + (c) 2005 Jon Tirsen (http://www.tirsen.com) +Contributors: + Richard Livsey + Rahul Bhargava + Rob Wills + Mochi-ized By Thomas Herve (_firstname_@nimail.org) + +See scriptaculous.js for full license. + +Autocompleter.Base handles all the autocompletion functionality +that's independent of the data source for autocompletion. This +includes drawing the autocompletion menu, observing keyboard +and mouse events, and similar. + +Specific autocompleters need to provide, at the very least, +a getUpdatedChoices function that will be invoked every time +the text inside the monitored textbox changes. This method +should get the text for which to provide autocompletion by +invoking this.getToken(), NOT by directly accessing +this.element.value. This is to allow incremental tokenized +autocompletion. Specific auto-completion logic (AJAX, etc) +belongs in getUpdatedChoices. + +Tokenized incremental autocompletion is enabled automatically +when an autocompleter is instantiated with the 'tokens' option +in the options parameter, e.g.: +new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); +will incrementally autocomplete with a comma as the token. +Additionally, ',' in the above example can be replaced with +a token array, e.g. { tokens: [',', '\n'] } which +enables autocompletion on multiple tokens. This is most +useful when one of the tokens is \n (a newline), as it +allows smart autocompletion after linebreaks. + +***/ + +MochiKit.Base.update(MochiKit.Base, { + ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)', + +/** @id MochiKit.Base.stripScripts */ + stripScripts: function (str) { + return str.replace(new RegExp(MochiKit.Base.ScriptFragment, 'img'), ''); + }, + +/** @id MochiKit.Base.stripTags */ + stripTags: function(str) { + return str.replace(/<\/?[^>]+>/gi, ''); + }, + +/** @id MochiKit.Base.extractScripts */ + extractScripts: function (str) { + var matchAll = new RegExp(MochiKit.Base.ScriptFragment, 'img'); + var matchOne = new RegExp(MochiKit.Base.ScriptFragment, 'im'); + return MochiKit.Base.map(function (scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }, str.match(matchAll) || []); + }, + +/** @id MochiKit.Base.evalScripts */ + evalScripts: function (str) { + return MochiKit.Base.map(function (scr) { + eval(scr); + }, MochiKit.Base.extractScripts(str)); + } +}); + +MochiKit.Form = { + +/** @id MochiKit.Form.serialize */ + serialize: function (form) { + var elements = MochiKit.Form.getElements(form); + var queryComponents = []; + + for (var i = 0; i < elements.length; i++) { + var queryComponent = MochiKit.Form.serializeElement(elements[i]); + if (queryComponent) { + queryComponents.push(queryComponent); + } + } + + return queryComponents.join('&'); + }, + +/** @id MochiKit.Form.getElements */ + getElements: function (form) { + form = MochiKit.DOM.getElement(form); + var elements = []; + + for (tagName in MochiKit.Form.Serializers) { + var tagElements = form.getElementsByTagName(tagName); + for (var j = 0; j < tagElements.length; j++) { + elements.push(tagElements[j]); + } + } + return elements; + }, + +/** @id MochiKit.Form.serializeElement */ + serializeElement: function (element) { + element = MochiKit.DOM.getElement(element); + var method = element.tagName.toLowerCase(); + var parameter = MochiKit.Form.Serializers[method](element); + + if (parameter) { + var key = encodeURIComponent(parameter[0]); + if (key.length === 0) { + return; + } + + if (!(parameter[1] instanceof Array)) { + parameter[1] = [parameter[1]]; + } + + return parameter[1].map(function (value) { + return key + '=' + encodeURIComponent(value); + }).join('&'); + } + } +}; + +MochiKit.Form.Serializers = { + +/** @id MochiKit.Form.Serializers.input */ + input: function (element) { + switch (element.type.toLowerCase()) { + case 'submit': + case 'hidden': + case 'password': + case 'text': + return MochiKit.Form.Serializers.textarea(element); + case 'checkbox': + case 'radio': + return MochiKit.Form.Serializers.inputSelector(element); + } + return false; + }, + +/** @id MochiKit.Form.Serializers.inputSelector */ + inputSelector: function (element) { + if (element.checked) { + return [element.name, element.value]; + } + }, + +/** @id MochiKit.Form.Serializers.textarea */ + textarea: function (element) { + return [element.name, element.value]; + }, + +/** @id MochiKit.Form.Serializers.select */ + select: function (element) { + return MochiKit.Form.Serializers[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + +/** @id MochiKit.Form.Serializers.selectOne */ + selectOne: function (element) { + var value = '', opt, index = element.selectedIndex; + if (index >= 0) { + opt = element.options[index]; + value = opt.value; + if (!value && !('value' in opt)) { + value = opt.text; + } + } + return [element.name, value]; + }, + +/** @id MochiKit.Form.Serializers.selectMany */ + selectMany: function (element) { + var value = []; + for (var i = 0; i < element.length; i++) { + var opt = element.options[i]; + if (opt.selected) { + var optValue = opt.value; + if (!optValue && !('value' in opt)) { + optValue = opt.text; + } + value.push(optValue); + } + } + return [element.name, value]; + } +}; + +/** @id Ajax */ +var Ajax = { + activeRequestCount: 0 +}; + +Ajax.Responders = { + responders: [], + +/** @id Ajax.Responders.register */ + register: function (responderToAdd) { + if (MochiKit.Base.find(this.responders, responderToAdd) == -1) { + this.responders.push(responderToAdd); + } + }, + +/** @id Ajax.Responders.unregister */ + unregister: function (responderToRemove) { + this.responders = this.responders.without(responderToRemove); + }, + +/** @id Ajax.Responders.dispatch */ + dispatch: function (callback, request, transport, json) { + MochiKit.Iter.forEach(this.responders, function (responder) { + if (responder[callback] && + typeof(responder[callback]) == 'function') { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) {} + } + }); + } +}; + +Ajax.Responders.register({ + +/** @id Ajax.Responders.onCreate */ + onCreate: function () { + Ajax.activeRequestCount++; + }, + +/** @id Ajax.Responders.onComplete */ + onComplete: function () { + Ajax.activeRequestCount--; + } +}); + +/** @id Ajax.Base */ +Ajax.Base = function () {}; + +Ajax.Base.prototype = { + +/** @id Ajax.Base.prototype.setOptions */ + setOptions: function (options) { + this.options = { + method: 'post', + asynchronous: true, + parameters: '' + } + MochiKit.Base.update(this.options, options || {}); + }, + +/** @id Ajax.Base.prototype.responseIsSuccess */ + responseIsSuccess: function () { + return this.transport.status == undefined + || this.transport.status === 0 + || (this.transport.status >= 200 && this.transport.status < 300); + }, + +/** @id Ajax.Base.prototype.responseIsFailure */ + responseIsFailure: function () { + return !this.responseIsSuccess(); + } +}; + +/** @id Ajax.Request */ +Ajax.Request = function (url, options) { + this.__init__(url, options); +}; + +/** @id Ajax.Events */ +Ajax.Request.Events = ['Uninitialized', 'Loading', 'Loaded', + 'Interactive', 'Complete']; + +MochiKit.Base.update(Ajax.Request.prototype, Ajax.Base.prototype); + +MochiKit.Base.update(Ajax.Request.prototype, { + __init__: function (url, options) { + this.transport = MochiKit.Async.getXMLHttpRequest(); + this.setOptions(options); + this.request(url); + }, + +/** @id Ajax.Request.prototype.request */ + request: function (url) { + var parameters = this.options.parameters || ''; + if (parameters.length > 0){ + parameters += '&_='; + } + + try { + this.url = url; + if (this.options.method == 'get' && parameters.length > 0) { + this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; + } + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.options.method, this.url, + this.options.asynchronous); + + if (this.options.asynchronous) { + this.transport.onreadystatechange = MochiKit.Base.bind(this.onStateChange, this); + setTimeout(MochiKit.Base.bind(function () { + this.respondToReadyState(1); + }, this), 10); + } + + this.setRequestHeaders(); + + var body = this.options.postBody ? this.options.postBody : parameters; + this.transport.send(this.options.method == 'post' ? body : null); + + } catch (e) { + this.dispatchException(e); + } + }, + +/** @id Ajax.Request.prototype.setRequestHeaders */ + setRequestHeaders: function () { + var requestHeaders = ['X-Requested-With', 'XMLHttpRequest']; + + if (this.options.method == 'post') { + requestHeaders.push('Content-type', + 'application/x-www-form-urlencoded'); + + /* Force 'Connection: close' for Mozilla browsers to work around + * a bug where XMLHttpRequest sends an incorrect Content-length + * header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType) { + requestHeaders.push('Connection', 'close'); + } + } + + if (this.options.requestHeaders) { + requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); + } + + for (var i = 0; i < requestHeaders.length; i += 2) { + this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); + } + }, + +/** @id Ajax.Request.prototype.onStateChange */ + onStateChange: function () { + var readyState = this.transport.readyState; + if (readyState != 1) { + this.respondToReadyState(this.transport.readyState); + } + }, + +/** @id Ajax.Request.prototype.header */ + header: function (name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) {} + }, + +/** @id Ajax.Request.prototype.evalJSON */ + evalJSON: function () { + try { + return eval(this.header('X-JSON')); + } catch (e) {} + }, + +/** @id Ajax.Request.prototype.evalResponse */ + evalResponse: function () { + try { + return eval(this.transport.responseText); + } catch (e) { + this.dispatchException(e); + } + }, + +/** @id Ajax.Request.prototype.respondToReadyState */ + respondToReadyState: function (readyState) { + var event = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (event == 'Complete') { + try { + (this.options['on' + this.transport.status] + || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] + || MochiKit.Base.noop)(transport, json); + } catch (e) { + this.dispatchException(e); + } + + if ((this.header('Content-type') || '').match(/^text\/javascript/i)) { + this.evalResponse(); + } + } + + try { + (this.options['on' + event] || MochiKit.Base.noop)(transport, json); + Ajax.Responders.dispatch('on' + event, this, transport, json); + } catch (e) { + this.dispatchException(e); + } + + /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ + if (event == 'Complete') { + this.transport.onreadystatechange = MochiKit.Base.noop; + } + }, + +/** @id Ajax.Request.prototype.dispatchException */ + dispatchException: function (exception) { + (this.options.onException || MochiKit.Base.noop)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +/** @id Ajax.Updater */ +Ajax.Updater = function (container, url, options) { + this.__init__(container, url, options); +}; + +MochiKit.Base.update(Ajax.Updater.prototype, Ajax.Request.prototype); + +MochiKit.Base.update(Ajax.Updater.prototype, { + __init__: function (container, url, options) { + this.containers = { + success: container.success ? MochiKit.DOM.getElement(container.success) : MochiKit.DOM.getElement(container), + failure: container.failure ? MochiKit.DOM.getElement(container.failure) : + (container.success ? null : MochiKit.DOM.getElement(container)) + } + this.transport = MochiKit.Async.getXMLHttpRequest(); + this.setOptions(options); + + var onComplete = this.options.onComplete || MochiKit.Base.noop; + this.options.onComplete = MochiKit.Base.bind(function (transport, object) { + this.updateContent(); + onComplete(transport, object); + }, this); + + this.request(url); + }, + +/** @id Ajax.Updater.prototype.updateContent */ + updateContent: function () { + var receiver = this.responseIsSuccess() ? + this.containers.success : this.containers.failure; + var response = this.transport.responseText; + + if (!this.options.evalScripts) { + response = MochiKit.Base.stripScripts(response); + } + + if (receiver) { + if (this.options.insertion) { + new this.options.insertion(receiver, response); + } else { + MochiKit.DOM.getElement(receiver).innerHTML = + MochiKit.Base.stripScripts(response); + setTimeout(function () { + MochiKit.Base.evalScripts(response); + }, 10); + } + } + + if (this.responseIsSuccess()) { + if (this.onComplete) { + setTimeout(MochiKit.Base.bind(this.onComplete, this), 10); + } + } + } +}); + +/** @id Field */ +var Field = { + +/** @id clear */ + clear: function () { + for (var i = 0; i < arguments.length; i++) { + MochiKit.DOM.getElement(arguments[i]).value = ''; + } + }, + +/** @id focus */ + focus: function (element) { + MochiKit.DOM.getElement(element).focus(); + }, + +/** @id present */ + present: function () { + for (var i = 0; i < arguments.length; i++) { + if (MochiKit.DOM.getElement(arguments[i]).value == '') { + return false; + } + } + return true; + }, + +/** @id select */ + select: function (element) { + MochiKit.DOM.getElement(element).select(); + }, + +/** @id activate */ + activate: function (element) { + element = MochiKit.DOM.getElement(element); + element.focus(); + if (element.select) { + element.select(); + } + }, + +/** @id scrollFreeActivate */ + scrollFreeActivate: function (field) { + setTimeout(function () { + Field.activate(field); + }, 1); + } +}; + + +/** @id Autocompleter */ +var Autocompleter = {}; + +/** @id Autocompleter.Base */ +Autocompleter.Base = function () {}; + +Autocompleter.Base.prototype = { + +/** @id Autocompleter.Base.prototype.baseInitialize */ + baseInitialize: function (element, update, options) { + this.element = MochiKit.DOM.getElement(element); + this.update = MochiKit.DOM.getElement(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + + if (this.setOptions) { + this.setOptions(options); + } + else { + this.options = options || {}; + } + + this.options.paramName = this.options.paramName || this.element.name; + this.options.tokens = this.options.tokens || []; + this.options.frequency = this.options.frequency || 0.4; + this.options.minChars = this.options.minChars || 1; + this.options.onShow = this.options.onShow || function (element, update) { + if (!update.style.position || update.style.position == 'absolute') { + update.style.position = 'absolute'; + MochiKit.Position.clone(element, update, { + setHeight: false, + offsetTop: element.offsetHeight + }); + } + MochiKit.Visual.appear(update, {duration:0.15}); + }; + this.options.onHide = this.options.onHide || function (element, update) { + MochiKit.Visual.fade(update, {duration: 0.15}); + }; + + if (typeof(this.options.tokens) == 'string') { + this.options.tokens = new Array(this.options.tokens); + } + + this.observer = null; + + this.element.setAttribute('autocomplete', 'off'); + + MochiKit.Style.hideElement(this.update); + + MochiKit.Signal.connect(this.element, 'onblur', this, this.onBlur); + MochiKit.Signal.connect(this.element, 'onkeypress', this, this.onKeyPress, this); + }, + +/** @id Autocompleter.Base.prototype.show */ + show: function () { + if (MochiKit.Style.getStyle(this.update, 'display') == 'none') { + this.options.onShow(this.element, this.update); + } + if (!this.iefix && /MSIE/.test(navigator.userAgent && + (MochiKit.Style.getStyle(this.update, 'position') == 'absolute')) { + new Insertion.After(this.update, + '<iframe id="' + this.update.id + '_iefix" '+ + 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' + + 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>'); + this.iefix = MochiKit.DOM.getElement(this.update.id + '_iefix'); + } + if (this.iefix) { + setTimeout(MochiKit.Base.bind(this.fixIEOverlapping, this), 50); + } + }, + +/** @id Autocompleter.Base.prototype.fixIEOverlapping */ + fixIEOverlapping: function () { + MochiKit.Position.clone(this.update, this.iefix); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + MochiKit.Style.showElement(this.iefix); + }, + +/** @id Autocompleter.Base.prototype.hide */ + hide: function () { + this.stopIndicator(); + if (MochiKit.Style.getStyle(this.update, 'display') != 'none') { + this.options.onHide(this.element, this.update); + } + if (this.iefix) { + MochiKit.Style.hideElement(this.iefix); + } + }, + +/** @id Autocompleter.Base.prototype.startIndicator */ + startIndicator: function () { + if (this.options.indicator) { + MochiKit.Style.showElement(this.options.indicator); + } + }, + +/** @id Autocompleter.Base.prototype.stopIndicator */ + stopIndicator: function () { + if (this.options.indicator) { + MochiKit.Style.hideElement(this.options.indicator); + } + }, + +/** @id Autocompleter.Base.prototype.onKeyPress */ + onKeyPress: function (event) { + if (this.active) { + if (event.key().string == "KEY_TAB" || event.key().string == "KEY_RETURN") { + this.selectEntry(); + MochiKit.Event.stop(event); + } else if (event.key().string == "KEY_ESCAPE") { + this.hide(); + this.active = false; + MochiKit.Event.stop(event); + return; + } else if (event.key().string == "KEY_LEFT" || event.key().string == "KEY_RIGHT") { + return; + } else if (event.key().string == "KEY_UP") { + this.markPrevious(); + this.render(); + if (/AppleWebKit'/.test(navigator.appVersion)) { + event.stop(); + } + return; + } else if (event.key().string == "KEY_DOWN") { + this.markNext(); + this.render(); + if (/AppleWebKit'/.test(navigator.appVersion)) { + event.stop(); + } + return; + } + } else { + if (event.key().string == "KEY_TAB" || event.key().string == "KEY_RETURN") { + return; + } + } + + this.changed = true; + this.hasFocus = true; + + if (this.observer) { + clearTimeout(this.observer); + } + this.observer = setTimeout(MochiKit.Base.bind(this.onObserverEvent, this), + this.options.frequency*1000); + }, + +/** @id Autocompleter.Base.prototype.findElement */ + findElement: function (event, tagName) { + var element = event.target; + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) { + element = element.parentNode; + } + return element; + }, + +/** @id Autocompleter.Base.prototype.hover */ + onHover: function (event) { + var element = this.findElement(event, 'LI'); + if (this.index != element.autocompleteIndex) { + this.index = element.autocompleteIndex; + this.render(); + } + event.stop(); + }, + +/** @id Autocompleter.Base.prototype.onClick */ + onClick: function (event) { + var element = this.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.selectEntry(); + this.hide(); + }, + +/** @id Autocompleter.Base.prototype.onBlur */ + onBlur: function (event) { + // needed to make click events working + setTimeout(MochiKit.Base.bind(this.hide, this), 250); + this.hasFocus = false; + this.active = false; + }, + +/** @id Autocompleter.Base.prototype.render */ + render: function () { + if (this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) { + this.index == i ? + MochiKit.DOM.addElementClass(this.getEntry(i), 'selected') : + MochiKit.DOM.removeElementClass(this.getEntry(i), 'selected'); + } + if (this.hasFocus) { + this.show(); + this.active = true; + } + } else { + this.active = false; + this.hide(); + } + }, + +/** @id Autocompleter.Base.prototype.markPrevious */ + markPrevious: function () { + if (this.index > 0) { + this.index-- + } else { + this.index = this.entryCount-1; + } + }, + +/** @id Autocompleter.Base.prototype.markNext */ + markNext: function () { + if (this.index < this.entryCount-1) { + this.index++ + } else { + this.index = 0; + } + }, + +/** @id Autocompleter.Base.prototype.getEntry */ + getEntry: function (index) { + return this.update.firstChild.childNodes[index]; + }, + +/** @id Autocompleter.Base.prototype.getCurrentEntry */ + getCurrentEntry: function () { + return this.getEntry(this.index); + }, + +/** @id Autocompleter.Base.prototype.selectEntry */ + selectEntry: function () { + this.active = false; + this.updateElement(this.getCurrentEntry()); + }, + +/** @id Autocompleter.Base.prototype.collectTextNodesIgnoreClass */ + collectTextNodesIgnoreClass: function (element, className) { + return MochiKit.Base.flattenArray(MochiKit.Base.map(function (node) { + if (node.nodeType == 3) { + return node.nodeValue; + } else if (node.hasChildNodes() && !MochiKit.DOM.hasElementClass(node, className)) { + return this.collectTextNodesIgnoreClass(node, className); + } + return ''; + }, MochiKit.DOM.getElement(element).childNodes)).join(''); + }, + +/** @id Autocompleter.Base.prototype.updateElement */ + updateElement: function (selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + var value = ''; + if (this.options.select) { + var nodes = document.getElementsByClassName(this.options.select, selectedElement) || []; + if (nodes.length > 0) { + value = MochiKit.DOM.scrapeText(nodes[0]); + } + } else { + value = this.collectTextNodesIgnoreClass(selectedElement, 'informal'); + } + var lastTokenPos = this.findLastToken(); + if (lastTokenPos != -1) { + var newValue = this.element.value.substr(0, lastTokenPos + 1); + var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); + if (whitespace) { + newValue += whitespace[0]; + } + this.element.value = newValue + value; + } else { + this.element.value = value; + } + this.element.focus(); + + if (this.options.afterUpdateElement) { + this.options.afterUpdateElement(this.element, selectedElement); + } + }, + +/** @id Autocompleter.Base.prototype.updateChoices */ + updateChoices: function (choices) { + if (!this.changed && this.hasFocus) { + this.update.innerHTML = choices; + var d = MochiKit.DOM; + d.removeEmptyTextNodes(this.update); + d.removeEmptyTextNodes(this.update.firstChild); + + if (this.update.firstChild && this.update.firstChild.childNodes) { + this.entryCount = this.update.firstChild.childNodes.length; + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entryCount = 0; + } + + this.stopIndicator(); + + this.index = 0; + this.render(); + } + }, + +/** @id Autocompleter.Base.prototype.addObservers */ + addObservers: function (element) { + MochiKit.Signal.connect(element, 'onmouseover', this, this.onHover); + MochiKit.Signal.connect(element, 'onclick', this, this.onClick); + }, + +/** @id Autocompleter.Base.prototype.onObserverEvent */ + onObserverEvent: function () { + this.changed = false; + if (this.getToken().length >= this.options.minChars) { + this.startIndicator(); + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + }, + +/** @id Autocompleter.Base.prototype.getToken */ + getToken: function () { + var tokenPos = this.findLastToken(); + if (tokenPos != -1) { + var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); + } else { + var ret = this.element.value; + } + return /\n/.test(ret) ? '' : ret; + }, + +/** @id Autocompleter.Base.prototype.findLastToken */ + findLastToken: function () { + var lastTokenPos = -1; + + for (var i = 0; i < this.options.tokens.length; i++) { + var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]); + if (thisTokenPos > lastTokenPos) { + lastTokenPos = thisTokenPos; + } + } + return lastTokenPos; + } +} + +/** @id Ajax.Autocompleter */ +Ajax.Autocompleter = function (element, update, url, options) { + this.__init__(element, update, url, options); +}; + +MochiKit.Base.update(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype); + +MochiKit.Base.update(Ajax.Autocompleter.prototype, { + __init__: function (element, update, url, options) { + this.baseInitialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = MochiKit.Base.bind(this.onComplete, this); + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + +/** @id Ajax.Autocompleter.prototype.getUpdatedChoices */ + getUpdatedChoices: function () { + var entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + + this.options.parameters = this.options.callback ? + this.options.callback(this.element, entry) : entry; + + if (this.options.defaultParams) { + this.options.parameters += '&' + this.options.defaultParams; + } + new Ajax.Request(this.url, this.options); + }, + +/** @id Ajax.Autocompleter.prototype.onComplete */ + onComplete: function (request) { + this.updateChoices(request.responseText); + } +}); + +/*** + +The local array autocompleter. Used when you'd prefer to +inject an array of autocompletion options into the page, rather +than sending out Ajax queries, which can be quite slow sometimes. + +The constructor takes four parameters. The first two are, as usual, +the id of the monitored textbox, and id of the autocompletion menu. +The third is the array you want to autocomplete from, and the fourth +is the options block. + +Extra local autocompletion options: +- choices - How many autocompletion choices to offer + +- partialSearch - If false, the autocompleter will match entered + text only at the beginning of strings in the + autocomplete array. Defaults to true, which will + match text at the beginning of any *word* in the + strings in the autocomplete array. If you want to + search anywhere in the string, additionally set + the option fullSearch to true (default: off). + +- fullSsearch - Search anywhere in autocomplete array strings. + +- partialChars - How many characters to enter before triggering + a partial match (unlike minChars, which defines + how many characters are required to do any match + at all). Defaults to 2. + +- ignoreCase - Whether to ignore case when autocompleting. + Defaults to true. + +It's possible to pass in a custom function as the 'selector' +option, if you prefer to write your own autocompletion logic. +In that case, the other options above will not apply unless +you support them. + +***/ + +/** @id Autocompleter.Local */ +Autocompleter.Local = function (element, update, array, options) { + this.__init__(element, update, array, options); +}; + +MochiKit.Base.update(Autocompleter.Local.prototype, Autocompleter.Base.prototype); + +MochiKit.Base.update(Autocompleter.Local.prototype, { + __init__: function (element, update, array, options) { + this.baseInitialize(element, update, options); + this.options.array = array; + }, + +/** @id Autocompleter.Local.prototype.getUpdatedChoices */ + getUpdatedChoices: function () { + this.updateChoices(this.options.selector(this)); + }, + +/** @id Autocompleter.Local.prototype.setOptions */ + setOptions: function (options) { + this.options = MochiKit.Base.update({ + choices: 10, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, + selector: function (instance) { + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); + var count = 0; + + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { + + var elem = instance.options.array[i]; + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (foundPos != -1) { + if (foundPos === 0 && elem.length != entry.length) { + ret.push('<li><strong>' + elem.substr(0, entry.length) + '</strong>' + + elem.substr(entry.length) + '</li>'); + break; + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos - 1, 1))) { + partial.push('<li>' + elem.substr(0, foundPos) + '<strong>' + + elem.substr(foundPos, entry.length) + '</strong>' + elem.substr( + foundPos + entry.length) + '</li>'); + break; + } + } + + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); + + } + } + if (partial.length) { + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) + } + return '<ul>' + ret.join('') + '</ul>'; + } + }, options || {}); + } +}); + +/*** + +AJAX in-place editor + +see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor + +Use this if you notice weird scrolling problems on some browsers, +the DOM might be a bit confused when this gets called so do this +waits 1 ms (with setTimeout) until it does the activation + +***/ + +/** @id Ajax.InPlaceEditor */ +Ajax.InPlaceEditor = function (element, url, options) { + this.__init__(element, url, options); +}; + +/** @id Ajax.InPlaceEditor.defaultHighlightColor */ +Ajax.InPlaceEditor.defaultHighlightColor = '#FFFF99'; + +Ajax.InPlaceEditor.prototype = { + __init__: function (element, url, options) { + this.url = url; + this.element = MochiKit.DOM.getElement(element); + + this.options = MochiKit.Base.update({ + okButton: true, + okText: 'ok', + cancelLink: true, + cancelText: 'cancel', + savingText: 'Saving...', + clickToEditText: 'Click to edit', + okText: 'ok', + rows: 1, + onComplete: function (transport, element) { + new MochiKit.Visual.Highlight(element, {startcolor: this.options.highlightcolor}); + }, + onFailure: function (transport) { + alert('Error communicating with the server: ' + MochiKit.Base.stripTags(transport.responseText)); + }, + callback: function (form) { + return MochiKit.DOM.formContents(form); + }, + handleLineBreaks: true, + loadingText: 'Loading...', + savingClassName: 'inplaceeditor-saving', + loadingClassName: 'inplaceeditor-loading', + formClassName: 'inplaceeditor-form', + highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, + highlightendcolor: '#FFFFFF', + externalControl: null, + submitOnBlur: false, + ajaxOptions: {} + }, options || {}); + + if (!this.options.formId && this.element.id) { + this.options.formId = this.element.id + '-inplaceeditor'; + if (MochiKit.DOM.getElement(this.options.formId)) { + // there's already a form with that name, don't specify an id + this.options.formId = null; + } + } + + if (this.options.externalControl) { + this.options.externalControl = MochiKit.DOM.getElement(this.options.externalControl); + } + + this.originalBackground = MochiKit.Style.getStyle(this.element, 'background-color'); + if (!this.originalBackground) { + this.originalBackground = 'transparent'; + } + + this.element.title = this.options.clickToEditText; + + this.onclickListener = MochiKit.Signal.connect(this.element, 'onclick', this, this.enterEditMode); + this.mouseoverListener = MochiKit.Signal.connect(this.element, 'onmouseover', this, this.enterHover); + this.mouseoutListener = MochiKit.Signal.connect(this.element, 'onmouseout', this, this.leaveHover); + if (this.options.externalControl) { + this.onclickListenerExternal = MochiKit.Signal.connect(this.options.externalControl, + 'onclick', this, this.enterEditMode); + this.mouseoverListenerExternal = MochiKit.Signal.connect(this.options.externalControl, + 'onmouseover', this, this.enterHover); + this.mouseoutListenerExternal = MochiKit.Signal.connect(this.options.externalControl, + 'onmouseout', this, this.leaveHover); + } + }, + +/** @id Ajax.InPlaceEditor.prototype.enterEditMode */ + enterEditMode: function (evt) { + if (this.saving) { + return; + } + if (this.editing) { + return; + } + this.editing = true; + this.onEnterEditMode(); + if (this.options.externalControl) { + MochiKit.Style.hideElement(this.options.externalControl); + } + MochiKit.Style.hideElement(this.element); + this.createForm(); + this.element.parentNode.insertBefore(this.form, this.element); + Field.scrollFreeActivate(this.editField); + // stop the event to avoid a page refresh in Safari + if (evt) { + evt.stop(); + } + return false; + }, + +/** @id Ajax.InPlaceEditor.prototype.createForm */ + createForm: function () { + this.form = document.createElement('form'); + this.form.id = this.options.formId; + MochiKit.DOM.addElementClass(this.form, this.options.formClassName) + this.form.onsubmit = MochiKit.Base.bind(this.onSubmit, this); + + this.createEditField(); + + if (this.options.textarea) { + var br = document.createElement('br'); + this.form.appendChild(br); + } + + if (this.options.okButton) { + okButton = document.createElement('input'); + okButton.type = 'submit'; + okButton.value = this.options.okText; + this.form.appendChild(okButton); + } + + if (this.options.cancelLink) { + cancelLink = document.createElement('a'); + cancelLink.href = '#'; + cancelLink.appendChild(document.createTextNode(this.options.cancelText)); + cancelLink.onclick = MochiKit.Base.bind(this.onclickCancel, this); + this.form.appendChild(cancelLink); + } + }, + +/** @id Ajax.InPlaceEditor.prototype.hasHTMLLineBreaks */ + hasHTMLLineBreaks: function (string) { + if (!this.options.handleLineBreaks) { + return false; + } + return string.match(/<br/i) || string.match(/<p>/i); + }, + +/** @id Ajax.InPlaceEditor.prototype.convertHTMLLineBreaks */ + convertHTMLLineBreaks: function (string) { + return string.replace(/<br>/gi, '\n').replace(/<br\/>/gi, '\n').replace(/<\/p>/gi, '\n').replace(/<p>/gi, ''); + }, + +/** @id Ajax.InPlaceEditor.prototype.createEditField */ + createEditField: function () { + var text; + if (this.options.loadTextURL) { + text = this.options.loadingText; + } else { + text = this.getText(); + } + + var obj = this; + + if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { + this.options.textarea = false; + var textField = document.createElement('input'); + textField.obj = this; + textField.type = 'text'; + textField.name = 'value'; + textField.value = text; + textField.style.backgroundColor = this.options.highlightcolor; + var size = this.options.size || this.options.cols || 0; + if (size !== 0) { + textField.size = size; + } + if (this.options.submitOnBlur) { + textField.onblur = MochiKit.Base.bind(this.onSubmit, this); + } + this.editField = textField; + } else { + this.options.textarea = true; + var textArea = document.createElement('textarea'); + textArea.obj = this; + textArea.name = 'value'; + textArea.value = this.convertHTMLLineBreaks(text); + textArea.rows = this.options.rows; + textArea.cols = this.options.cols || 40; + if (this.options.submitOnBlur) { + textArea.onblur = MochiKit.Base.bind(this.onSubmit, this); + } + this.editField = textArea; + } + + if (this.options.loadTextURL) { + this.loadExternalText(); + } + this.form.appendChild(this.editField); + }, + +/** @id Ajax.InPlaceEditor.prototype.getText */ + getText: function () { + return this.element.innerHTML; + }, + +/** @id Ajax.InPlaceEditor.prototype.loadExternalText */ + loadExternalText: function () { + MochiKit.DOM.addElementClass(this.form, this.options.loadingClassName); + this.editField.disabled = true; + new Ajax.Request( + this.options.loadTextURL, + MochiKit.Base.update({ + asynchronous: true, + onComplete: MochiKit.Base.bind(this.onLoadedExternalText, this) + }, this.options.ajaxOptions) + ); + }, + +/** @id Ajax.InPlaceEditor.prototype.onLoadedExternalText */ + onLoadedExternalText: function (transport) { + MochiKit.DOM.removeElementClass(this.form, this.options.loadingClassName); + this.editField.disabled = false; + this.editField.value = MochiKit.Base.stripTags(transport); + }, + +/** @id Ajax.InPlaceEditor.prototype.onclickCancel */ + onclickCancel: function () { + this.onComplete(); + this.leaveEditMode(); + return false; + }, + +/** @id Ajax.InPlaceEditor.prototype.onFailure */ + onFailure: function (transport) { + this.options.onFailure(transport); + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + this.oldInnerHTML = null; + } + return false; + }, + +/** @id Ajax.InPlaceEditor.prototype.onSubmit */ + onSubmit: function () { + // onLoading resets these so we need to save them away for the Ajax call + var form = this.form; + var value = this.editField.value; + + // do this first, sometimes the ajax call returns before we get a + // chance to switch on Saving which means this will actually switch on + // Saving *after* we have left edit mode causing Saving to be + // displayed indefinitely + this.onLoading(); + + new Ajax.Updater( + { + success: this.element, + // dont update on failure (this could be an option) + failure: null + }, + this.url, + MochiKit.Base.update({ + parameters: this.options.callback(form, value), + onComplete: MochiKit.Base.bind(this.onComplete, this), + onFailure: MochiKit.Base.bind(this.onFailure, this) + }, this.options.ajaxOptions) + ); + // stop the event to avoid a page refresh in Safari + if (arguments.length > 1) { + arguments[0].stop(); + } + return false; + }, + +/** @id Ajax.InPlaceEditor.prototype.onLoading */ + onLoading: function () { + this.saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + +/** @id Ajax.InPlaceEditor.prototype.onSaving */ + showSaving: function () { + this.oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + MochiKit.DOM.addElementClass(this.element, this.options.savingClassName); + this.element.style.backgroundColor = this.originalBackground; + MochiKit.Style.showElement(this.element); + }, + +/** @id Ajax.InPlaceEditor.prototype.removeForm */ + removeForm: function () { + if (this.form) { + if (this.form.parentNode) { + MochiKit.DOM.removeElement(this.form); + } + this.form = null; + } + }, + +/** @id Ajax.InPlaceEditor.prototype.enterHover */ + enterHover: function () { + if (this.saving) { + return; + } + this.element.style.backgroundColor = this.options.highlightcolor; + if (this.effect) { + this.effect.cancel(); + } + MochiKit.DOM.addElementClass(this.element, this.options.hoverClassName) + }, + +/** @id Ajax.InPlaceEditor.prototype.leaveHover */ + leaveHover: function () { + if (this.options.backgroundColor) { + this.element.style.backgroundColor = this.oldBackground; + } + MochiKit.DOM.removeElementClass(this.element, this.options.hoverClassName) + if (this.saving) { + return; + } + this.effect = new MochiKit.Visual.Highlight(this.element, { + startcolor: this.options.highlightcolor, + endcolor: this.options.highlightendcolor, + restorecolor: this.originalBackground + }); + }, + +/** @id Ajax.InPlaceEditor.prototype.leaveEditMode */ + leaveEditMode: function () { + MochiKit.DOM.removeElementClass(this.element, this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this.originalBackground; + MochiKit.Style.showElement(this.element); + if (this.options.externalControl) { + MochiKit.Style.showElement(this.options.externalControl); + } + this.editing = false; + this.saving = false; + this.oldInnerHTML = null; + this.onLeaveEditMode(); + }, + +/** @id Ajax.InPlaceEditor.prototype.onComplete */ + onComplete: function (transport) { + this.leaveEditMode(); + MochiKit.Base.bind(this.options.onComplete, this)(transport, this.element); + }, + +/** @id Ajax.InPlaceEditor.prototype.onEnterEditMode */ + onEnterEditMode: function () {}, + +/** @id Ajax.InPlaceEditor.prototype.onLeaveEditMode */ + onLeaveEditMode: function () {}, + + /** @id Ajax.InPlaceEditor.prototype.dispose */ + dispose: function () { + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + } + this.leaveEditMode(); + MochiKit.Signal.disconnect(this.onclickListener); + MochiKit.Signal.disconnect(this.mouseoverListener); + MochiKit.Signal.disconnect(this.mouseoutListener); + if (this.options.externalControl) { + MochiKit.Signal.disconnect(this.onclickListenerExternal); + MochiKit.Signal.disconnect(this.mouseoverListenerExternal); + MochiKit.Signal.disconnect(this.mouseoutListenerExternal); + } + } +}; + diff --git a/testing/mochitest/MochiKit/DOM.js b/testing/mochitest/MochiKit/DOM.js new file mode 100644 index 0000000000..caff07ec2a --- /dev/null +++ b/testing/mochitest/MochiKit/DOM.js @@ -0,0 +1,1043 @@ +/*** + +MochiKit.DOM 1.4 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide("MochiKit.DOM"); + dojo.require("MochiKit.Base"); +} +if (typeof(JSAN) != 'undefined') { + JSAN.use("MochiKit.Base", []); +} + +try { + if (typeof(MochiKit.Base) == 'undefined') { + throw ""; + } +} catch (e) { + throw "MochiKit.DOM depends on MochiKit.Base!"; +} + +if (typeof(MochiKit.DOM) == 'undefined') { + MochiKit.DOM = {}; +} + +MochiKit.DOM.NAME = "MochiKit.DOM"; +MochiKit.DOM.VERSION = "1.4"; +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", + "setNodeAttribute", + "updateNodeAttributes", + "appendChildNodes", + "replaceChildNodes", + "removeElement", + "swapDOM", + "BUTTON", + "TT", + "PRE", + "H1", + "H2", + "H3", + "BR", + "CANVAS", + "HR", + "LABEL", + "TEXTAREA", + "FORM", + "STRONG", + "SELECT", + "OPTION", + "OPTGROUP", + "LEGEND", + "FIELDSET", + "P", + "UL", + "OL", + "LI", + "TD", + "TR", + "THEAD", + "TBODY", + "TFOOT", + "TABLE", + "TH", + "INPUT", + "SPAN", + "A", + "DIV", + "IMG", + "getElement", + "$", + "getElementsByTagAndClassName", + "addToCallStack", + "addLoadEvent", + "focusOnLoad", + "setElementClass", + "toggleElementClass", + "addElementClass", + "removeElementClass", + "swapElementClass", + "hasElementClass", + "escapeHTML", + "toHTML", + "emitHTML", + "scrapeText" +]; + +MochiKit.DOM.EXPORT_OK = [ + "domConverters" +]; + +MochiKit.DOM.DEPRECATED = [ + ['computedStyle', 'MochiKit.Style.computedStyle', '1.4'], + /** @id MochiKit.DOM.elementDimensions */ + ['elementDimensions', 'MochiKit.Style.getElementDimensions', '1.4'], + /** @id MochiKit.DOM.elementPosition */ + ['elementPosition', 'MochiKit.Style.getElementPosition', '1.4'], + ['hideElement', 'MochiKit.Style.hideElement', '1.4'], + /** @id MochiKit.DOM.setElementDimensions */ + ['setElementDimensions', 'MochiKit.Style.setElementDimensions', '1.4'], + /** @id MochiKit.DOM.setElementPosition */ + ['setElementPosition', 'MochiKit.Style.setElementPosition', '1.4'], + ['setDisplayForElement', 'MochiKit.Style.setDisplayForElement', '1.4'], + /** @id MochiKit.DOM.setOpacity */ + ['setOpacity', 'MochiKit.Style.setOpacity', '1.4'], + ['showElement', 'MochiKit.Style.showElement', '1.4'], + /** @id MochiKit.DOM.Coordinates */ + ['Coordinates', 'MochiKit.Style.Coordinates', '1.4'], // FIXME: broken + /** @id MochiKit.DOM.Dimensions */ + ['Dimensions', 'MochiKit.Style.Dimensions', '1.4'] // FIXME: broken +]; + +/** @id MochiKit.DOM.getViewportDimensions */ +MochiKit.DOM.getViewportDimensions = new Function('' + + 'if (!MochiKit["Style"]) {' + + ' throw new Error("This function has been deprecated and depends on MochiKit.Style.");' + + '}' + + 'return MochiKit.Style.getViewportDimensions.apply(this, arguments);'); + +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._win; + 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 */) { + var names = []; + var values = []; + var m = MochiKit.Base; + var self = MochiKit.DOM; + if (typeof(elem) == "undefined" || elem === null) { + elem = self._document; + } 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]; + names.push(name); + values.push(opt.value); + 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; + } + names.push(name); + values.push(opt.value); + } + 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; + } + 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)); + } + } + + // 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 (node === maybeparent) { + return true; + } + while (node && node.tagName.toUpperCase() != "BODY") { + node = node.parentNode; + if (node === maybeparent) { + return true; + } + } + 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]; + node = self.getElement(node); + try { + if (rename) { + return node[rename]; + } + return node.getAttribute(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') { + 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); + } + } + } else { + // IE is insane in the membrane + var renames = self.attributeArray.renames; + for (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') { + 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); + } + } + } + } + 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.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 e = MochiKit.DOM.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.getElement(src); + 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); + 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 (!cls) { + continue; + } + 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; + // trivial case, no className yet + if (cls == undefined || 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; + // trivial case, no className yet + if (cls == undefined || 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); + var cls = obj.className; + if (!cls) { + 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); + } + } + }, + + __new__: function (win) { + + var m = MochiKit.Base; + if (typeof(document) != "undefined") { + this._document = document; + this._xhtml = + document.createElementNS && + document.createElement("testname").localName == "testname"; + } 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 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" + }; + } else { + 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 node.attributes; + }; + attributeArray.compliant = true; + attributeArray.renames = {}; + } + this.attributeArray = attributeArray; + + // FIXME: this really belongs in Base, and could probably be cleaner + var _deprecated = function(fromModule, arr) { + var modules = arr[1].split('.'); + var str = ''; + var obj = {}; + + str += 'if (!MochiKit.' + modules[1] + ') { throw new Error("'; + str += 'This function has been deprecated and depends on MochiKit.'; + str += modules[1] + '.");}'; + str += 'return MochiKit.' + modules[1] + '.' + arr[0]; + str += '.apply(this, arguments);'; + + obj[modules[2]] = new Function(str); + MochiKit.Base.update(MochiKit[fromModule], obj); + } + for (var i; 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.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.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/MochiKit/DateTime.js b/testing/mochitest/MochiKit/DateTime.js new file mode 100644 index 0000000000..1b517b3a5b --- /dev/null +++ b/testing/mochitest/MochiKit/DateTime.js @@ -0,0 +1,216 @@ +/*** + +MochiKit.DateTime 1.4 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.DateTime'); +} + +if (typeof(MochiKit) == 'undefined') { + MochiKit = {}; +} + +if (typeof(MochiKit.DateTime) == 'undefined') { + MochiKit.DateTime = {}; +} + +MochiKit.DateTime.NAME = "MochiKit.DateTime"; +MochiKit.DateTime.VERSION = "1.4"; +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; + } + return new Date(iso[0], iso[1] - 1, iso[2]); +}; + +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; + return [ + 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; +}; + +/** @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/MochiKit/DragAndDrop.js b/testing/mochitest/MochiKit/DragAndDrop.js new file mode 100644 index 0000000000..b23a5ecd33 --- /dev/null +++ b/testing/mochitest/MochiKit/DragAndDrop.js @@ -0,0 +1,821 @@ +/*** +MochiKit.DragAndDrop 1.4 + +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) + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.DragAndDrop'); + dojo.require('MochiKit.Base'); + dojo.require('MochiKit.DOM'); + dojo.require('MochiKit.Iter'); + dojo.require('MochiKit.Visual'); + dojo.require('MochiKit.Signal'); +} + +if (typeof(JSAN) != 'undefined') { + JSAN.use("MochiKit.Base", []); + JSAN.use("MochiKit.DOM", []); + JSAN.use("MochiKit.Visual", []); + JSAN.use("MochiKit.Iter", []); + JSAN.use("MochiKit.Signal", []); +} + +try { + if (typeof(MochiKit.Base) == 'undefined' || + typeof(MochiKit.DOM) == 'undefined' || + typeof(MochiKit.Visual) == 'undefined' || + typeof(MochiKit.Signal) == 'undefined' || + typeof(MochiKit.Iter) == 'undefined') { + throw ""; + } +} catch (e) { + throw "MochiKit.DragAndDrop depends on MochiKit.Base, MochiKit.DOM, MochiKit.Visual, MochiKit.Signal and MochiKit.Iter!"; +} + +if (typeof(MochiKit.DragAndDrop) == 'undefined') { + MochiKit.DragAndDrop = {}; +} + +MochiKit.DragAndDrop.NAME = 'MochiKit.DragAndDrop'; +MochiKit.DragAndDrop.VERSION = '1.4'; + +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.isParent(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) { + 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); + + d.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) { + 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.getOpacity(innerelement) || 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); + } + + d.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); + if (this.options.ghosting) { + var r = MochiKit.Position.realOffset(this.element); + pos.x += r.x - MochiKit.Position.windowOffset.x; + pos.y += r.y - MochiKit.Position.windowOffset.y; + } + 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 d = delta / 1000; + this.options.scroll.scrollTo(s.left + d * this.scrollSpeed[0], + s.top + d * 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 (w) { + var vp, w, h; + MochiKit.DOM.withWindow(w, function () { + vp = MochiKit.Style.getViewportPosition(w.document); + }); + if (w.innerWidth) { + w = w.innerWidth; + h = w.innerHeight; + } else if (w.document.documentElement && w.document.documentElement.clientWidth) { + w = w.document.documentElement.clientWidth; + h = w.document.documentElement.clientHeight; + } else { + w = w.document.body.offsetWidth; + h = w.document.body.offsetHeight + } + return {top: vp.x, left: vp.y, 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/MochiKit/Format.js b/testing/mochitest/MochiKit/Format.js new file mode 100644 index 0000000000..8890bd573a --- /dev/null +++ b/testing/mochitest/MochiKit/Format.js @@ -0,0 +1,304 @@ +/*** + +MochiKit.Format 1.4 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.Format'); +} + +if (typeof(MochiKit) == 'undefined') { + MochiKit = {}; +} + +if (typeof(MochiKit.Format) == 'undefined') { + MochiKit.Format = {}; +} + +MochiKit.Format.NAME = "MochiKit.Format"; +MochiKit.Format.VERSION = "1.4"; +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(numerator / denominator); + } + } + return "0"; +}; + +/** @id MochiKit.Format.twoDigitFloat */ +MochiKit.Format.twoDigitFloat = function (someFloat) { + var sign = (someFloat < 0 ? '-' : ''); + var s = Math.floor(Math.abs(someFloat) * 100).toString(); + if (s == '0') { + return s; + } + if (s.length < 3) { + while (s.charAt(s.length - 1) == '0') { + s = s.substring(0, s.length - 1); + } + return sign + '0.' + s; + } + var head = sign + s.substring(0, s.length - 2); + var tail = s.substring(s.length - 2, s.length); + if (tail == '00') { + return head; + } else if (tail.charAt(1) == '0') { + return head + '.' + tail.charAt(0); + } else { + return head + '.' + tail; + } +}; + +/** @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) { + aNumber = Math.floor(aNumber * Math.pow(10, precision)); + var res = (aNumber * Math.pow(10, -precision)).toFixed(precision); + if (res.charAt(0) == ".") { + res = "0" + res; + } + return res; +}; + +/** @id MochiKit.Format.roundToFixed */ +MochiKit.Format.roundToFixed = function (aNumber, precision) { + return MochiKit.Format.truncToFixed( + aNumber + 0.5 * Math.pow(10, -precision), + precision + ); +}; + +/** @id MochiKit.Format.percentFormat */ +MochiKit.Format.percentFormat = function (someFloat) { + return MochiKit.Format.twoDigitFloat(100 * someFloat) + '%'; +}; + +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: "%"}, + 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/MochiKit/Iter.js b/testing/mochitest/MochiKit/Iter.js new file mode 100644 index 0000000000..bb3767de94 --- /dev/null +++ b/testing/mochitest/MochiKit/Iter.js @@ -0,0 +1,843 @@ +/*** + +MochiKit.Iter 1.4 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.Iter'); + dojo.require('MochiKit.Base'); +} + +if (typeof(JSAN) != 'undefined') { + JSAN.use("MochiKit.Base", []); +} + +try { + if (typeof(MochiKit.Base) == 'undefined') { + throw ""; + } +} catch (e) { + throw "MochiKit.Iter depends on MochiKit.Base!"; +} + +if (typeof(MochiKit.Iter) == 'undefined') { + MochiKit.Iter = {}; +} + +MochiKit.Iter.NAME = "MochiKit.Iter"; +MochiKit.Iter.VERSION = "1.4"; +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.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 { + return argiter[0].next(); + } catch (e) { + if (e != self.StopIteration) { + throw e; + } + argiter.shift(); + } + } + 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 m = MochiKit.Base; + if (typeof(iterable.slice) == 'function') { + return iterable.slice(); + } else if (m.isArrayLike(iterable)) { + return m.concat(iterable); + } + + var self = MochiKit.Iter; + iterable = self.iter(iterable); + var rval = []; + try { + while (true) { + rval.push(iterable.next()); + } + } 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 */self) { + var m = MochiKit.Base; + if (arguments.length > 2) { + func = m.bind(func, self); + } + // fast path for array + if (m.isArrayLike(iterable)) { + try { + for (var i = 0; i < iterable.length; i++) { + func(iterable[i]); + } + } catch (e) { + if (e != MochiKit.Iter.StopIteration) { + throw e; + } + } + } else { + self = MochiKit.Iter; + 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) { + if (MochiKit.Base.isArrayLike(iterable)) { + // fast-path for array-like + for (var i = 0; i < iterable.length; i++) { + lst.push(iterable[i]); + } + } else { + var self = MochiKit.Iter; + 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/MochiKit/LICENSE.txt b/testing/mochitest/MochiKit/LICENSE.txt new file mode 100644 index 0000000000..4d0065befd --- /dev/null +++ b/testing/mochitest/MochiKit/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/MochiKit/Logging.js b/testing/mochitest/MochiKit/Logging.js new file mode 100644 index 0000000000..687cd2383a --- /dev/null +++ b/testing/mochitest/MochiKit/Logging.js @@ -0,0 +1,321 @@ +/*** + +MochiKit.Logging 1.4 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.Logging'); + dojo.require('MochiKit.Base'); +} + +if (typeof(JSAN) != 'undefined') { + JSAN.use("MochiKit.Base", []); +} + +try { + if (typeof(MochiKit.Base) == 'undefined') { + throw ""; + } +} catch (e) { + throw "MochiKit.Logging depends on MochiKit.Base!"; +} + +if (typeof(MochiKit.Logging) == 'undefined') { + MochiKit.Logging = {}; +} + +MochiKit.Logging.NAME = "MochiKit.Logging"; +MochiKit.Logging.VERSION = "1.4"; +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/*, ...*/) { + 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 stupid 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/MochiKit/LoggingPane.js b/testing/mochitest/MochiKit/LoggingPane.js new file mode 100644 index 0000000000..8ecf410a7e --- /dev/null +++ b/testing/mochitest/MochiKit/LoggingPane.js @@ -0,0 +1,371 @@ +/*** + +MochiKit.LoggingPane 1.4 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.LoggingPane'); + dojo.require('MochiKit.Logging'); + dojo.require('MochiKit.Base'); +} + +if (typeof(JSAN) != 'undefined') { + JSAN.use("MochiKit.Logging", []); + JSAN.use("MochiKit.Base", []); +} + +try { + if (typeof(MochiKit.Base) == 'undefined' || typeof(MochiKit.Logging) == 'undefined') { + throw ""; + } +} catch (e) { + throw "MochiKit.LoggingPane depends on MochiKit.Base and MochiKit.Logging!"; +} + +if (typeof(MochiKit.LoggingPane) == 'undefined') { + MochiKit.LoggingPane = {}; +} + +MochiKit.LoggingPane.NAME = "MochiKit.LoggingPane"; +MochiKit.LoggingPane.VERSION = "1.4"; +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); + + debugPane.loggingPane = null; + + if (inline) { + debugPane.parentNode.removeChild(debugPane); + } else { + this.win.close(); + } + }, 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/MochiKit/MochiKit.js b/testing/mochitest/MochiKit/MochiKit.js new file mode 100644 index 0000000000..d0935e3655 --- /dev/null +++ b/testing/mochitest/MochiKit/MochiKit.js @@ -0,0 +1,152 @@ +/*** + +MochiKit.MochiKit 1.4 + +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"; +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", + "Style", + "LoggingPane", + "Color", + "Signal", + "Visual" +]; + +if (typeof(JSAN) != 'undefined' || typeof(dojo) != 'undefined') { + if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.MochiKit'); + dojo.require("MochiKit.*"); + } + 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 kXULNSURI = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var base = null; + var baseElem = null; + var allScripts = {}; + var i; + for (i = 0; i < scripts.length; i++) { + var src = scripts[i].getAttribute("src"); + 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 (document.documentElement && + document.documentElement.namespaceURI == kXULNSURI) { + // XUL + var s = document.createElementNS(kXULNSURI, 'script'); + s.setAttribute("id", "MochiKit_" + base + modules[i]); + s.setAttribute("src", uri); + s.setAttribute("type", "application/x-javascript"); + baseElem.parentNode.appendChild(s); + } else { + // HTML + /* + 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('<script src="' + uri + + '" type="text/javascript"></script>'); + } + }; + })(); +} diff --git a/testing/mochitest/MochiKit/MockDOM.js b/testing/mochitest/MochiKit/MockDOM.js new file mode 100644 index 0000000000..3f8654a987 --- /dev/null +++ b/testing/mochitest/MochiKit/MockDOM.js @@ -0,0 +1,100 @@ +/*** + +MochiKit.MockDOM 1.4 + +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"; + +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) { + this.tagName = this.nodeName = name.toUpperCase(); + 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); + }, + /** @id MochiKit.MockDOM.MockElement.prototype.createTextNode */ + createTextNode: function (text) { + return new MochiKit.MockDOM.MockElement("text", text); + }, + /** @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.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/MochiKit/New.js b/testing/mochitest/MochiKit/New.js new file mode 100644 index 0000000000..529f1e7223 --- /dev/null +++ b/testing/mochitest/MochiKit/New.js @@ -0,0 +1,283 @@ + +MochiKit.Base.update(MochiKit.DOM, { + /** @id MochiKit.DOM.makeClipping */ + makeClipping: function (element) { + element = MochiKit.DOM.getElement(element); + var oldOverflow = element.style.overflow; + if ((MochiKit.Style.getStyle(element, 'overflow') || 'visible') != 'hidden') { + element.style.overflow = 'hidden'; + } + return oldOverflow; + }, + + /** @id MochiKit.DOM.undoClipping */ + undoClipping: function (element, overflow) { + element = MochiKit.DOM.getElement(element); + if (!overflow) { + return; + } + element.style.overflow = overflow; + }, + + /** @id MochiKit.DOM.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.DOM.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.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); + var children = (parent.getElementsByTagName(tagName) + || self._document.all); + if (typeof(className) == 'undefined' || className === null) { + return children[0]; + } + + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var classNames = child.className.split(' '); + for (var j = 0; j < classNames.length; j++) { + if (classNames[j] == className) { + return child; + } + } + } + }, + + /** @id MochiKit.DOM.isParent */ + isParent: function (child, element) { + if (!child.parentNode || child == element) { + return false; + } + + if (child.parentNode == element) { + return true; + } + + return MochiKit.DOM.isParent(child.parentNode, element); + } +}); + +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); + } +}; + diff --git a/testing/mochitest/MochiKit/Signal.js b/testing/mochitest/MochiKit/Signal.js new file mode 100644 index 0000000000..74199c170e --- /dev/null +++ b/testing/mochitest/MochiKit/Signal.js @@ -0,0 +1,857 @@ +/*** + +MochiKit.Signal 1.4 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2006 Jonathan Gardner, Beau Hartshorne, Bob Ippolito. All rights Reserved. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.Signal'); + dojo.require('MochiKit.Base'); + dojo.require('MochiKit.DOM'); + dojo.require('MochiKit.Style'); +} +if (typeof(JSAN) != 'undefined') { + JSAN.use('MochiKit.Base', []); + JSAN.use('MochiKit.DOM', []); + JSAN.use('MochiKit.Style', []); +} + +try { + if (typeof(MochiKit.Base) == 'undefined') { + throw ''; + } +} catch (e) { + throw 'MochiKit.Signal depends on MochiKit.Base!'; +} + +try { + if (typeof(MochiKit.DOM) == 'undefined') { + throw ''; + } +} catch (e) { + throw 'MochiKit.Signal depends on MochiKit.DOM!'; +} + +try { + if (typeof(MochiKit.Style) == 'undefined') { + throw ''; + } +} catch (e) { + throw 'MochiKit.Signal depends on MochiKit.Style!'; +} + +if (typeof(MochiKit.Signal) == 'undefined') { + MochiKit.Signal = {}; +} + +MochiKit.Signal.NAME = 'MochiKit.Signal'; +MochiKit.Signal.VERSION = '1.4'; + +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()) + + ', 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') { + str += ', button: {left: ' + repr(this.mouse().button.left) + + ', middle: ' + repr(this.mouse().button.middle) + + ', right: ' + repr(this.mouse().button.right) + '}}'; + } else { + str += '}'; + } + } + if (this.type() == 'mouseover' || this.type() == 'mouseout') { + 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 () { + 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') { + elem = (this._event.relatedTarget || + this._event.fromElement); + } else if (this.type() == 'mouseout') { + elem = (this._event.relatedTarget || + this._event.toElement); + } + if (elem !== null) { + this._relatedTarget = elem; + return elem; + } + + 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') { + 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://bugzilla.opendarwin.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); + } + } + 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); + } +})(); + +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++) { + self._disconnect(observers[i]); + } + + delete self._observers; + + try { + window.onload = undefined; + } catch(e) { + // pass + } + + try { + window.onunload = undefined; + } catch(e) { + // pass + } + }, + + _listener: function (src, func, obj, isDOM) { + var self = MochiKit.Signal; + var E = self.Event; + if (!isDOM) { + return MochiKit.Base.bind(func, obj); + } + obj = obj || src; + if (typeof(func) == "string") { + return function (nativeEvent) { + obj[func].apply(obj, [new E(src, nativeEvent)]); + }; + } else { + return function (nativeEvent) { + func.apply(obj, [new E(src, nativeEvent)]); + }; + } + }, + + _browserAlreadyHasMouseEnterAndLeave: function () { + return /MSIE/.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 { + var listener = self._listener(src, 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 = [src, sig, listener, isDOM, objOrFunc, funcOrStr, 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[6]) { return; } + ident[6] = false; + // check isDOM + if (!ident[3]) { return; } + var src = ident[0]; + var sig = ident[1]; + var listener = ident[2]; + 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[0] === src && o[1] === sig && o[4] === obj && o[5] === 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[4] === objOrFunc && + (funcOrStr === null || ident[5] === 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[0] === 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[0] === src && ident[1] 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[0] === src && ident[1] === sig) { + try { + ident[2].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][6]) { + 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/MochiKit/Sortable.js b/testing/mochitest/MochiKit/Sortable.js new file mode 100644 index 0000000000..2bee90b5d8 --- /dev/null +++ b/testing/mochitest/MochiKit/Sortable.js @@ -0,0 +1,588 @@ +/*** +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. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.DragAndDrop'); + dojo.require('MochiKit.Base'); + dojo.require('MochiKit.DOM'); + dojo.require('MochiKit.Iter'); +} + +if (typeof(JSAN) != 'undefined') { + JSAN.use("MochiKit.Base", []); + JSAN.use("MochiKit.DOM", []); + JSAN.use("MochiKit.Iter", []); +} + +try { + if (typeof(MochiKit.Base) == 'undefined' || + typeof(MochiKit.DOM) == 'undefined' || + typeof(MochiKit.Iter) == 'undefined') { + throw ""; + } +} catch (e) { + throw "MochiKit.DragAndDrop depends on MochiKit.Base, MochiKit.DOM and MochiKit.Iter!"; +} + +if (typeof(MochiKit.Sortable) == 'undefined') { + MochiKit.Sortable = {}; +} + +MochiKit.Sortable.NAME = 'MochiKit.Sortable'; +MochiKit.Sortable.VERSION = '1.4'; + +MochiKit.Sortable.__repr__ = function () { + return '[' + this.NAME + ' ' + this.VERSION + ']'; +}; + +MochiKit.Sortable.toString = function () { + return this.__repr__(); +}; + +MochiKit.Sortable.EXPORT = [ +]; + +MochiKit.DragAndDrop.EXPORT_OK = [ + "Sortable" +]; + +MochiKit.Sortable.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.Sortable.sortables[element.id]) { + return element; + } + element = element.parentNode; + } + }, + + /** @id MochiKit.Sortable.Sortable.options */ + options: function (element) { + element = MochiKit.Sortable.Sortable._findRootElement(MochiKit.DOM.getElement(element)); + if (!element) { + return; + } + return MochiKit.Sortable.Sortable.sortables[element.id]; + }, + + /** @id MochiKit.Sortable.Sortable.destroy */ + destroy: function (element){ + var s = MochiKit.Sortable.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.Sortable.sortables[s.element.id]; + } + }, + + /** @id MochiKit.Sortable.Sortable.create */ + create: function (element, options) { + element = MochiKit.DOM.getElement(element); + var self = MochiKit.Sortable.Sortable; + + /** @id MochiKit.Sortable.Sortable.options */ + options = MochiKit.Base.update({ + + /** @id MochiKit.Sortable.Sortable.element */ + element: element, + + /** @id MochiKit.Sortable.Sortable.tag */ + tag: 'li', // assumes li children, override with tag: 'tagname' + + /** @id MochiKit.Sortable.Sortable.dropOnEmpty */ + dropOnEmpty: false, + + /** @id MochiKit.Sortable.Sortable.tree */ + tree: false, + + /** @id MochiKit.Sortable.Sortable.treeTag */ + treeTag: 'ul', + + /** @id MochiKit.Sortable.Sortable.overlap */ + overlap: 'vertical', // one of 'vertical', 'horizontal' + + /** @id MochiKit.Sortable.Sortable.constraint */ + constraint: 'vertical', // one of 'vertical', 'horizontal', false + // also takes array of elements (or ids); or false + + /** @id MochiKit.Sortable.Sortable.containment */ + containment: [element], + + /** @id MochiKit.Sortable.Sortable.handle */ + handle: false, // or a CSS class + + /** @id MochiKit.Sortable.Sortable.only */ + only: false, + + /** @id MochiKit.Sortable.Sortable.hoverclass */ + hoverclass: null, + + /** @id MochiKit.Sortable.Sortable.ghosting */ + ghosting: false, + + /** @id MochiKit.Sortable.Sortable.scroll */ + scroll: false, + + /** @id MochiKit.Sortable.Sortable.scrollSensitivity */ + scrollSensitivity: 20, + + /** @id MochiKit.Sortable.Sortable.scrollSpeed */ + scrollSpeed: 15, + + /** @id MochiKit.Sortable.Sortable.format */ + format: /^[^_]*_(.*)$/, + + /** @id MochiKit.Sortable.Sortable.onChange */ + onChange: MochiKit.Base.noop, + + /** @id MochiKit.Sortable.Sortable.onUpdate */ + onUpdate: MochiKit.Base.noop, + + /** @id MochiKit.Sortable.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.Sortable.onStart */ + onStart: function (element, draggable) { + var self = MochiKit.Sortable.Sortable; + var options = self.options(element); + options.lastValue = self.serialize(options.element); + }, + + /** @id MochiKit.Sortable.Sortable.onEnd */ + onEnd: function (element, draggable) { + var self = MochiKit.Sortable.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.Sortable.findElements */ + findElements: function (element, options) { + return MochiKit.Sortable.Sortable.findChildren( + element, options.only, options.tree ? true : false, options.tag); + }, + + /** @id MochiKit.Sortable.Sortable.findTreeElements */ + findTreeElements: function (element, options) { + return MochiKit.Sortable.Sortable.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); + }, + + /** @id MochiKit.Sortable.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.Sortable.findChildren(e, only, recursive, tagName); + if (grandchildren && grandchildren.length > 0) { + elements = elements.concat(grandchildren); + } + } + }, element.childNodes); + return elements; + }, + + /** @id MochiKit.Sortable.Sortable.onHover */ + onHover: function (element, dropon, overlap) { + if (MochiKit.DOM.isParent(dropon, element)) { + return; + } + var self = MochiKit.Sortable.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.Sortable.onEmptyHover */ + onEmptyHover: function (element, dropon, overlap) { + var oldParentNode = element.parentNode; + var self = MochiKit.Sortable.Sortable; + var droponOptions = self.options(dropon); + + if (!MochiKit.DOM.isParent(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.Sortable.unmark */ + unmark: function () { + var m = MochiKit.Sortable.Sortable._marker; + if (m) { + MochiKit.Style.hideElement(m); + } + }, + + /** @id MochiKit.Sortable.Sortable.mark */ + mark: function (dropon, position) { + // mark on ghosting only + var d = MochiKit.DOM; + var self = MochiKit.Sortable.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.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.Sortable.tree */ + tree: function (element, options) { + element = MochiKit.DOM.getElement(element); + var sortableOptions = MochiKit.Sortable.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.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.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.Sortable.sequence */ + sequence: function (element, options) { + element = MochiKit.DOM.getElement(element); + var self = MochiKit.Sortable.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.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('&'); + } + } +}; + diff --git a/testing/mochitest/MochiKit/Style.js b/testing/mochitest/MochiKit/Style.js new file mode 100644 index 0000000000..6abf6d7172 --- /dev/null +++ b/testing/mochitest/MochiKit/Style.js @@ -0,0 +1,475 @@ +/*** + +MochiKit.Style 1.4 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005-2006 Bob Ippolito, Beau Hartshorne. All rights Reserved. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.Style'); + dojo.require('MochiKit.Base'); + dojo.require('MochiKit.DOM'); +} +if (typeof(JSAN) != 'undefined') { + JSAN.use('MochiKit.Base', []); +} + +try { + if (typeof(MochiKit.Base) == 'undefined') { + throw ''; + } +} catch (e) { + throw 'MochiKit.Style depends on MochiKit.Base!'; +} + +try { + if (typeof(MochiKit.DOM) == 'undefined') { + throw ''; + } +} catch (e) { + throw 'MochiKit.Style depends on MochiKit.DOM!'; +} + + +if (typeof(MochiKit.Style) == 'undefined') { + MochiKit.Style = {}; +} + +MochiKit.Style.NAME = 'MochiKit.Style'; +MochiKit.Style.VERSION = '1.4'; +MochiKit.Style.__repr__ = function () { + return '[' + this.NAME + ' ' + this.VERSION + ']'; +}; +MochiKit.Style.toString = function () { + return this.__repr__(); +}; + +MochiKit.Style.EXPORT_OK = []; + +MochiKit.Style.EXPORT = [ + 'setOpacity', + 'getOpacity', + 'setStyle', + 'getStyle', // temporary + 'computedStyle', + 'getElementDimensions', + 'elementDimensions', // deprecated + 'setElementDimensions', + 'getElementPosition', + 'elementPosition', // deprecated + 'setElementPosition', + '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.computedStyle */ + computedStyle: 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; + } + + /* from YUI 0.10.0 */ + if (cssProperty == 'opacity' && elem.filters) { // IE opacity + try { + return elem.filters.item('DXImageTransform.Microsoft.Alpha' + ).opacity / 100; + } catch(e) { + try { + return elem.filters.item('alpha').opacity / 100; + } catch(e) {} + } + } + + if (elem.currentStyle) { + return elem.currentStyle[cssProperty]; + } + if (typeof(d.defaultView) == 'undefined') { + return undefined; + } + if (d.defaultView === null) { + return undefined; + } + var style = d.defaultView.getComputedStyle(elem, null); + if (typeof(style) == 'undefined' || style === null) { + return undefined; + } + + var selectorCase = cssProperty.replace(/([A-Z])/g, '-$1' + ).toLowerCase(); // from dojo.style.toSelectorCase + + return style.getPropertyValue(selectorCase); + }, + + /** @id MochiKit.Style.getStyle */ + getStyle: function (elem, style) { + elem = MochiKit.DOM.getElement(elem); + var value = elem.style[MochiKit.Base.camelize(style)]; + if (!value) { + if (document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(elem, null); + value = css ? css.getPropertyValue(style) : null; + } else if (elem.currentStyle) { + value = elem.currentStyle[MochiKit.Base.camelize(style)]; + } + } + + if (/Opera/.test(navigator.userAgent) && (MochiKit.Base.find(['left', 'top', 'right', 'bottom'], style) != -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 (name in style) { + elem.style[MochiKit.Base.camelize(name)] = style[name]; + } + }, + + /** @id MochiKit.Style.getOpacity */ + getOpacity: function (elem) { + var opacity; + if (opacity = MochiKit.Style.getStyle(elem, 'opacity')) { + return parseFloat(opacity); + } + if (opacity = (MochiKit.Style.getStyle(elem, 'filter') || '').match(/alpha\(opacity=(.*)\)/)) { + if (opacity[1]) { + return parseFloat(opacity[1]) / 100; + } + } + return 1.0; + }, + /** @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|Safari|KHTML/.test(navigator.userAgent)); + self.setStyle(elem, {opacity: toSet ? 0.999999 : 1.0}); + if (/MSIE/.test(navigator.userAgent)) { + self.setStyle(elem, {filter: + self.getStyle(elem, 'filter').replace(/alpha\([^\)]*\)/gi, '')}); + } + } else { + if (o < 0.00001) { + o = 0; + } + self.setStyle(elem, {opacity: o}); + if (/MSIE/.test(navigator.userAgent)) { + self.setStyle(elem, + {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.computedStyle(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 += 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('safari') != -1 && + self.computedStyle(elem, 'position') == 'absolute')) { + + c.x -= b.offsetLeft; + c.y -= b.offsetTop; + + } + } + + if (typeof(relativeTo) != 'undefined') { + relativeTo = arguments.callee(relativeTo); + if (relativeTo) { + c.x -= (relativeTo.x || 0); + c.y -= (relativeTo.y || 0); + } + } + + if (elem.parentNode) { + parent = elem.parentNode; + } else { + parent = null; + } + + while (parent) { + var tagName = parent.tagName.toUpperCase(); + if (tagName === 'BODY' || tagName === 'HTML') { + break; + } + c.x -= parent.scrollLeft; + c.y -= parent.scrollTop; + if (parent.parentNode) { + parent = parent.parentNode; + } else { + parent = null; + } + } + + 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.getElementDimensions */ + getElementDimensions: function (elem) { + 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.computedStyle(elem, 'display'); + // display can be empty/undefined on WebKit/KHTML + if (disp != 'none' && disp != '' && typeof(disp) != 'undefined') { + return new self.Dimensions(elem.offsetWidth || 0, + elem.offsetHeight || 0); + } + var s = elem.style; + var originalVisibility = s.visibility; + var originalPosition = s.position; + s.visibility = 'hidden'; + s.position = 'absolute'; + s.display = ''; + var originalWidth = elem.offsetWidth; + var originalHeight = elem.offsetHeight; + s.display = 'none'; + s.position = originalPosition; + s.visibility = originalVisibility; + 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}); + }, + + /** @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++) { + var 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.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; + + this.elementPosition = this.getElementPosition; + this.elementDimensions = this.getElementDimensions; + + this.hideElement = m.partial(this.setDisplayForElement, 'none'); + 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/MochiKit/Test.js b/testing/mochitest/MochiKit/Test.js new file mode 100644 index 0000000000..632356a439 --- /dev/null +++ b/testing/mochitest/MochiKit/Test.js @@ -0,0 +1,181 @@ +/*** + +MochiKit.Test 1.4 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.Test'); + dojo.require('MochiKit.Base'); +} + +if (typeof(JSAN) != 'undefined') { + JSAN.use("MochiKit.Base", []); +} + +try { + if (typeof(MochiKit.Base) == 'undefined') { + throw ""; + } +} catch (e) { + throw "MochiKit.Test depends on MochiKit.Base!"; +} + +if (typeof(MochiKit.Test) == 'undefined') { + MochiKit.Test = {}; +} + +MochiKit.Test.NAME = "MochiKit.Test"; +MochiKit.Test.VERSION = "1.4"; +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/MochiKit/Visual.js b/testing/mochitest/MochiKit/Visual.js new file mode 100644 index 0000000000..bf8b3dfc30 --- /dev/null +++ b/testing/mochitest/MochiKit/Visual.js @@ -0,0 +1,1823 @@ +/*** + +MochiKit.Visual 1.4 + +See <http://mochikit.com/> for documentation, downloads, license, etc. + +(c) 2005 Bob Ippolito and others. All rights Reserved. + +***/ + +if (typeof(dojo) != 'undefined') { + dojo.provide('MochiKit.Visual'); + dojo.require('MochiKit.Base'); + dojo.require('MochiKit.DOM'); + dojo.require('MochiKit.Style'); + dojo.require('MochiKit.Color'); +} + +if (typeof(JSAN) != 'undefined') { + JSAN.use("MochiKit.Base", []); + JSAN.use("MochiKit.DOM", []); + JSAN.use("MochiKit.Style", []); + JSAN.use("MochiKit.Color", []); +} + +try { + if (typeof(MochiKit.Base) === 'undefined' || + typeof(MochiKit.DOM) === 'undefined' || + typeof(MochiKit.Style) === 'undefined' || + typeof(MochiKit.Color) === 'undefined') { + throw ""; + } +} catch (e) { + throw "MochiKit.Visual depends on MochiKit.Base, MochiKit.DOM, MochiKit.Style and MochiKit.Color!"; +} + +if (typeof(MochiKit.Visual) == "undefined") { + MochiKit.Visual = {}; +} + +MochiKit.Visual.NAME = "MochiKit.Visual"; +MochiKit.Visual.VERSION = "1.4"; + +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'. + + ***/ + var 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[element.style.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 (-Math.cos(pos*Math.PI)/2) + 0.5; +}; + +/** @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 ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; +}; + +/** @id MochiKit.Visual.Transitions.wobble */ +MochiKit.Visual.Transitions.wobble = function (pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; +}; + +/** @id MochiKit.Visual.Transitions.pulse */ +MochiKit.Visual.Transitions.pulse = function (pos) { + return (Math.floor(pos*10) % 2 == 0 ? + (pos*10 - Math.floor(pos*10)) : 1 - (pos*10 - Math.floor(pos*10))); +}; + +/** @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 () { + 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 == 0) { + 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) { + 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. + + ***/ + __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.Opacity */ +MochiKit.Visual.Opacity = function (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. + + ***/ + __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.getOpacity(this.element) || 0.0, + to: 1.0 + }, options || {}); + this.start(options); + }, + + /** @id MochiKit.Visual.Opacity.prototype.update */ + update: function (position) { + MochiKit.Style.setOpacity(this.element, position); + } +}); + +/** @id MochiKit.Visual.Opacity.prototype.Move */ +MochiKit.Visual.Move = function (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. + + ***/ + __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.DOM.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) { + 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 + + ***/ + __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) { + 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'. + + ***/ + __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) { + 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. + + ***/ + __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)); + } +}); + +/*** + +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 = MochiKit.DOM.getElement(element).style.opacity || ''; + options = MochiKit.Base.update({ + from: s.getOpacity(element) || 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.getOpacity(element) || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function (effect) { + v.forceRerendering(effect.element); + }, + beforeSetupInternal: function (effect) { + s.setOpacity(effect.element, 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 oldStyle = { + opacity: element.style.opacity || '', + position: s.getStyle(element, 'position'), + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height + }; + 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); + } + }, options || {}); + return new v.Parallel( + [new v.Scale(element, 200, + {sync: true, scaleFromCenter: true, + scaleContent: true, 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; + element = d.getElement(element); + var elemClip = d.makeClipping(element); + options = MochiKit.Base.update({ + scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function (effect) { + MochiKit.Style.hideElement(effect.element); + d.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); + 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 = d.makeClipping(effect.element); + s.setStyle(effect.element, {height: '0px'}); + s.showElement(effect.element); + }, + afterFinishInternal: function (effect) { + d.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; + element = d.getElement(element); + var oldOpacity = element.style.opacity || ''; + var elemClip; + var options = MochiKit.Base.update({ + duration: 0.3, + scaleFromCenter: true, + scaleX: false, + scaleContent: false, + restoreAfterFinish: true, + beforeSetupInternal: function (effect) { + d.makePositioned(effect.element); + elemClip = d.makeClipping(effect.element); + }, + afterFinishInternal: function (effect) { + MochiKit.Style.hideElement(effect.element); + d.undoClipping(effect.element, elemClip); + d.undoPositioned(effect.element); + MochiKit.Style.setStyle(effect.element, {opacity: oldOpacity}); + } + }, options || {}); + var v = MochiKit.Visual; + return new v.appear(element, { + duration: 0.4, + from: 0, + transition: v.Transitions.flicker, + afterFinishInternal: function (effect) { + new v.Scale(effect.element, 1, 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: element.style.opacity || '' + }; + + options = MochiKit.Base.update({ + duration: 0.5, + beforeSetupInternal: function (effect) { + d.makePositioned(effect.effects[0].element); + }, + afterFinishInternal: function (effect) { + s.hideElement(effect.effects[0].element); + d.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: 100, 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); + options = MochiKit.Base.update({ + x: -20, + y: 0, + duration: 0.05, + afterFinishInternal: function (effect) { + d.undoPositioned(effect.element); + s.setStyle(effect.element, oldStyle); + } + }, options || {}); + var oldStyle = { + top: s.getStyle(element, 'top'), + left: s.getStyle(element, 'left') }; + return new v.Move(element, + {x: 20, y: 0, duration: 0.05, afterFinishInternal: function (effect) { + new v.Move(effect.element, + {x: -40, y: 0, duration: 0.1, afterFinishInternal: function (effect) { + new v.Move(effect.element, + {x: 40, y: 0, duration: 0.1, afterFinishInternal: function (effect) { + new v.Move(effect.element, + {x: -40, y: 0, duration: 0.1, afterFinishInternal: function (effect) { + new v.Move(effect.element, + {x: 40, y: 0, duration: 0.1, afterFinishInternal: function (effect) { + new v.Move(effect.element, 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 "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); + var elemClip; + options = b.update({ + scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.h, + originalWidth: elementDimensions.w}, + restoreAfterFinish: true, + afterSetupInternal: function (effect) { + d.makePositioned(effect.element); + d.makePositioned(effect.element.firstChild); + if (/Opera/.test(navigator.userAgent)) { + s.setStyle(effect.element, {top: ''}); + } + elemClip = d.makeClipping(effect.element); + s.setStyle(effect.element, {height: '0px'}); + s.showElement(effect.element); + }, + afterUpdateInternal: function (effect) { + s.setStyle(effect.element.firstChild, + {bottom: (effect.dims[0] - effect.element.clientHeight) + 'px'}) + }, + afterFinishInternal: function (effect) { + d.undoClipping(effect.element, elemClip); + // IE will crash if child is undoPositioned first + if (/MSIE/.test(navigator.userAgent)) { + d.undoPositioned(effect.element); + d.undoPositioned(effect.element.firstChild); + } else { + d.undoPositioned(effect.element.firstChild); + d.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 "MochiKit.Visual.slideUp must be used on a element with a child"; + } + d.removeEmptyTextNodes(element); + var oldInnerBottom = s.getStyle(element.firstChild, 'bottom'); + var elemClip; + options = b.update({ + scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + restoreAfterFinish: true, + beforeStartInternal: function (effect) { + d.makePositioned(effect.element); + d.makePositioned(effect.element.firstChild); + if (/Opera/.test(navigator.userAgent)) { + s.setStyle(effect.element, {top: ''}); + } + elemClip = d.makeClipping(effect.element); + s.showElement(effect.element); + }, + afterUpdateInternal: function (effect) { + s.setStyle(effect.element.firstChild, + {bottom: (effect.dims[0] - effect.element.clientHeight) + 'px'}); + }, + afterFinishInternal: function (effect) { + s.hideElement(effect.element); + d.undoClipping(effect.element, elemClip); + d.undoPositioned(effect.element.firstChild); + d.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 elemClip; + options = b.update({ + restoreAfterFinish: true, + beforeSetupInternal: function (effect) { + elemClip = d.makeClipping(effect.element); + }, + afterFinishInternal: function (effect) { + MochiKit.Style.hideElement(effect.element); + d.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 + }, options || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.style.opacity || '' + }; + + var dims = s.getElementDimensions(element); + 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) { + d.undoClipping(effect.effects[0].element); + d.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); + d.makeClipping(effect.element); + d.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, + 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 + }, options || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.style.opacity || '' + }; + + var dims = s.getElementDimensions(element); + 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) { + elemClip = d.makePositioned(effect.effects[0].element); + d.makeClipping(effect.effects[0].element); + }, + afterFinishInternal: function (effect) { + s.hideElement(effect.effects[0].element); + d.undoClipping(effect.effects[0].element, elemClip); + d.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, { + sync: true, transition: options.scaleTransition, + 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 = d.getElement(element).style.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; + var reverser = b.bind(function (pos) { + return transition(1 - v.Transitions.pulse(pos)); + }, transition); + b.bind(reverser, transition); + return new v.Opacity(element, b.update({ + transition: reverser}, 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 oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height + }; + var elemClip = d.makeClipping(element); + options = MochiKit.Base.update({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function (effect) { + new v.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function (effect) { + s.hideElement(effect.element); + d.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", + "Base", + "Parallel", + "Opacity", + "Move", + "Scale", + "Highlight", + "ScrollTo", + "fade", + "appear", + "puff", + "blindUp", + "blindDown", + "switchOff", + "dropOut", + "shake", + "slideDown", + "slideUp", + "squish", + "grow", + "shrink", + "pulsate", + "fold" +]; + +MochiKit.Visual.EXPORT_OK = [ + "PAIRS" +]; + +MochiKit.Visual.__new__(); + +MochiKit.Base._exportSymbols(this, MochiKit.Visual); diff --git a/testing/mochitest/MochiKit/__package__.js b/testing/mochitest/MochiKit/__package__.js new file mode 100644 index 0000000000..4c58c39476 --- /dev/null +++ b/testing/mochitest/MochiKit/__package__.js @@ -0,0 +1,19 @@ +dojo.hostenv.conditionalLoadModule({ + "common": [ + "MochiKit.Base", + "MochiKit.Iter", + "MochiKit.Logging", + "MochiKit.DateTime", + "MochiKit.Format", + "MochiKit.Async", + "MochiKit.Color" + ], + "browser": [ + "MochiKit.DOM", + "MochiKit.Style", + "MochiKit.Signal", + "MochiKit.LoggingPane", + "MochiKit.Visual" + ] +}); +dojo.hostenv.moduleLoaded("MochiKit.*"); diff --git a/testing/mochitest/README.txt b/testing/mochitest/README.txt new file mode 100644 index 0000000000..45caa674d1 --- /dev/null +++ b/testing/mochitest/README.txt @@ -0,0 +1 @@ +See https://developer.mozilla.org/en/docs/Mochitest for detailed information on running and writing mochitests. diff --git a/testing/mochitest/ShutdownLeaksCollector.sys.mjs b/testing/mochitest/ShutdownLeaksCollector.sys.mjs new file mode 100644 index 0000000000..44b2025429 --- /dev/null +++ b/testing/mochitest/ShutdownLeaksCollector.sys.mjs @@ -0,0 +1,61 @@ +/* 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 { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; + +// This listens for the message "browser-test:collect-request". When it gets it, +// it runs some GCs and CCs, then prints out a message indicating the collections +// are complete. Mochitest uses this information to determine when windows and +// docshells should be destroyed. + +export var ContentCollector = { + init() { + let processType = Services.appinfo.processType; + if (processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { + // In the main process, we handle triggering collections in browser-test.js + return; + } + + Services.cpmm.addMessageListener("browser-test:collect-request", this); + }, + + receiveMessage(aMessage) { + switch (aMessage.name) { + case "browser-test:collect-request": + Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize"); + + Cu.forceGC(); + Cu.forceCC(); + + let shutdownCleanup = aCallback => { + Cu.schedulePreciseShrinkingGC(() => { + // Run the GC and CC a few times to make sure that as much + // as possible is freed. + Cu.forceGC(); + Cu.forceCC(); + aCallback(); + }); + }; + + shutdownCleanup(() => { + setTimeout(() => { + shutdownCleanup(() => { + this.finish(); + }); + }, 1000); + }); + + break; + } + }, + + finish() { + let pid = Services.appinfo.processID; + dump("Completed ShutdownLeaks collections in process " + pid + "\n"); + + Services.cpmm.removeMessageListener("browser-test:collect-request", this); + }, +}; + +ContentCollector.init(); diff --git a/testing/mochitest/__init__.py b/testing/mochitest/__init__.py new file mode 100644 index 0000000000..6fbe8159b2 --- /dev/null +++ b/testing/mochitest/__init__.py @@ -0,0 +1,3 @@ +# 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/. diff --git a/testing/mochitest/api.js b/testing/mochitest/api.js new file mode 100644 index 0000000000..f9353acbf8 --- /dev/null +++ b/testing/mochitest/api.js @@ -0,0 +1,175 @@ +/* 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/. */ + +/* globals AppConstants, ExtensionAPI, Services */ + +function loadChromeScripts(win) { + Services.scriptloader.loadSubScript( + "chrome://mochikit/content/chrome-harness.js", + win + ); + Services.scriptloader.loadSubScript( + "chrome://mochikit/content/mochitest-e10s-utils.js", + win + ); + Services.scriptloader.loadSubScript( + "chrome://mochikit/content/browser-test.js", + win + ); +} + +// ///// Android /////// + +const windowTracker = { + init() { + Services.obs.addObserver(this, "chrome-document-global-created"); + }, + + async observe(window, topic, data) { + if (topic === "chrome-document-global-created") { + await new Promise(resolve => + window.addEventListener("DOMContentLoaded", resolve, { once: true }) + ); + + let { document } = window; + let { documentURI } = document; + + if (documentURI !== AppConstants.BROWSER_CHROME_URL) { + return; + } + loadChromeScripts(window); + } + }, +}; + +function androidStartup() { + // Only browser chrome tests need help starting. + let testRoot = Services.prefs.getStringPref("mochitest.testRoot", ""); + if (testRoot.endsWith("/chrome")) { + // The initial window is created from browser startup, which races + // against extension initialization. If it has already been created, + // inject the test scripts now, otherwise wait for the browser window + // to show up. + let win = Services.wm.getMostRecentWindow("navigator:browser"); + if (win) { + loadChromeScripts(win); + return; + } + + windowTracker.init(); + } + + Services.fog.initializeFOG(); +} + +// ///// Desktop /////// + +// Special case for Thunderbird windows. +const IS_THUNDERBIRD = + Services.appinfo.ID == "{3550f703-e582-4d05-9a08-453d09bdfdc6}"; +const WINDOW_TYPE = IS_THUNDERBIRD ? "mail:3pane" : "navigator:browser"; + +var WindowListener = { + // browser-test.js is only loaded into the first window. Setup that + // needs to happen in all navigator:browser windows should go here. + setupWindow(win) { + win.nativeConsole = win.console; + ChromeUtils.defineESModuleGetters(win, { + console: "resource://gre/modules/Console.sys.mjs", + }); + }, + + tearDownWindow(win) { + if (win.nativeConsole) { + win.console = win.nativeConsole; + win.nativeConsole = undefined; + } + }, + + onOpenWindow(xulWin) { + let win = xulWin.docShell.domWindow; + + win.addEventListener( + "load", + function() { + if ( + win.document.documentElement.getAttribute("windowtype") == WINDOW_TYPE + ) { + WindowListener.setupWindow(win); + } + }, + { once: true } + ); + }, +}; + +function loadMochitest(e) { + let flavor = e.detail[0]; + let url = e.detail[1]; + + let win = Services.wm.getMostRecentWindow(WINDOW_TYPE); + win.removeEventListener("mochitest-load", loadMochitest); + + // for mochitest-plain, navigating to the url is all we need + if (!IS_THUNDERBIRD) { + win.loadURI( + url, + null, + null, + null, + null, + null, + null, + null, + Services.scriptSecurityManager.getSystemPrincipal() + ); + } + if (flavor == "mochitest") { + return; + } + + WindowListener.setupWindow(win); + Services.wm.addListener(WindowListener); + + loadChromeScripts(win); +} + +this.mochikit = class extends ExtensionAPI { + onStartup() { + let aomStartup = Cc[ + "@mozilla.org/addons/addon-manager-startup;1" + ].getService(Ci.amIAddonManagerStartup); + const manifestURI = Services.io.newURI( + "manifest.json", + null, + this.extension.rootURI + ); + const targetURL = this.extension.rootURI.resolve("content/"); + this.chromeHandle = aomStartup.registerChrome(manifestURI, [ + ["content", "mochikit", targetURL], + ]); + + if (AppConstants.platform == "android") { + androidStartup(); + } else { + let win = Services.wm.getMostRecentWindow(WINDOW_TYPE); + // wait for event fired from start_desktop.js containing the + // suite and url to load + win.addEventListener("mochitest-load", loadMochitest); + } + } + + onShutdown() { + if (AppConstants.platform != "android") { + for (let win of Services.wm.getEnumerator(WINDOW_TYPE)) { + WindowListener.tearDownWindow(win); + } + + Services.wm.removeListener(WindowListener); + } + + this.chromeHandle.destruct(); + this.chromeHandle = null; + } +}; diff --git a/testing/mochitest/baselinecoverage/browser_chrome/browser.ini b/testing/mochitest/baselinecoverage/browser_chrome/browser.ini new file mode 100644 index 0000000000..f5a3f685ef --- /dev/null +++ b/testing/mochitest/baselinecoverage/browser_chrome/browser.ini @@ -0,0 +1,6 @@ +[DEFAULT] + +[browser_baselinecoverage.js] +run-if = ccov && verify +[browser_baselinecoverage_browser-chrome.js] +run-if = ccov && verify
\ No newline at end of file diff --git a/testing/mochitest/baselinecoverage/browser_chrome/browser_baselinecoverage.js b/testing/mochitest/baselinecoverage/browser_chrome/browser_baselinecoverage.js new file mode 100644 index 0000000000..379dc6aac3 --- /dev/null +++ b/testing/mochitest/baselinecoverage/browser_chrome/browser_baselinecoverage.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +"use strict"; + +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +add_task(async function() { + requestLongerTimeout(2); + ok(true, "Collecting baseline coverage for javascript (.js) file types."); + await new Promise(c => setTimeout(c, 30 * 1000)); +}); diff --git a/testing/mochitest/baselinecoverage/browser_chrome/browser_baselinecoverage_browser-chrome.js b/testing/mochitest/baselinecoverage/browser_chrome/browser_baselinecoverage_browser-chrome.js new file mode 100644 index 0000000000..4914e38e7a --- /dev/null +++ b/testing/mochitest/baselinecoverage/browser_chrome/browser_baselinecoverage_browser-chrome.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +"use strict"; + +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +add_task(async function() { + requestLongerTimeout(2); + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:blank", + }, + async function(browser) { + ok(true, "Collecting baseline coverage for browser-chrome tests."); + await new Promise(c => setTimeout(c, 30 * 1000)); + } + ); + + await BrowserTestUtils.closeWindow(newWin); +}); diff --git a/testing/mochitest/baselinecoverage/chrome/chrome.ini b/testing/mochitest/baselinecoverage/chrome/chrome.ini new file mode 100644 index 0000000000..4f427ca9e2 --- /dev/null +++ b/testing/mochitest/baselinecoverage/chrome/chrome.ini @@ -0,0 +1,4 @@ +[DEFAULT] + +[test_baselinecoverage.xhtml] +run-if = ccov && verify
\ No newline at end of file diff --git a/testing/mochitest/baselinecoverage/chrome/test_baselinecoverage.xhtml b/testing/mochitest/baselinecoverage/chrome/test_baselinecoverage.xhtml new file mode 100644 index 0000000000..a51da7608b --- /dev/null +++ b/testing/mochitest/baselinecoverage/chrome/test_baselinecoverage.xhtml @@ -0,0 +1,25 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("Baseline coverage stabilization delay."); + + async function delayBaseline() { + SimpleTest.ok(true, "Collecting baseline coverage for XUL (.xul) file types."); + await new Promise((c) => setTimeout(c, 30 * 1000)); + SimpleTest.finish(); + } + + delayBaseline(); + ]]></script> + + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;" /> +</window> diff --git a/testing/mochitest/baselinecoverage/plain/mochitest.ini b/testing/mochitest/baselinecoverage/plain/mochitest.ini new file mode 100644 index 0000000000..c3667e373f --- /dev/null +++ b/testing/mochitest/baselinecoverage/plain/mochitest.ini @@ -0,0 +1,4 @@ +[DEFAULT] + +[test_baselinecoverage.html] +run-if = ccov && verify
\ No newline at end of file diff --git a/testing/mochitest/baselinecoverage/plain/test_baselinecoverage.html b/testing/mochitest/baselinecoverage/plain/test_baselinecoverage.html new file mode 100644 index 0000000000..0fdc912808 --- /dev/null +++ b/testing/mochitest/baselinecoverage/plain/test_baselinecoverage.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Baseline Coverage Collection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("Baseline coverage stabilization delay."); + + async function delayBaseline() { + SimpleTest.ok(true, "Collecting baseline coverage for HTML (.html) file types."); + await new Promise((c) => setTimeout(c, 30 * 1000)); + SimpleTest.finish(); + } + + delayBaseline(); +</script> +</body> +</html> diff --git a/testing/mochitest/bisection.py b/testing/mochitest/bisection.py new file mode 100644 index 0000000000..044be23542 --- /dev/null +++ b/testing/mochitest/bisection.py @@ -0,0 +1,287 @@ +import math + +import mozinfo + + +class Bisect(object): + + "Class for creating, bisecting and summarizing for --bisect-chunk option." + + def __init__(self, harness): + super(Bisect, self).__init__() + self.summary = [] + self.contents = {} + self.repeat = 10 + self.failcount = 0 + self.max_failures = 3 + + def setup(self, tests): + """This method is used to initialize various variables that are required + for test bisection""" + status = 0 + self.contents.clear() + # We need totalTests key in contents for sanity check + self.contents["totalTests"] = tests + self.contents["tests"] = tests + self.contents["loop"] = 0 + return status + + def reset(self, expectedError, result): + """This method is used to initialize self.expectedError and self.result + for each loop in runtests.""" + self.expectedError = expectedError + self.result = result + + def get_tests_for_bisection(self, options, tests): + """Make a list of tests for bisection from a given list of tests""" + bisectlist = [] + for test in tests: + bisectlist.append(test) + if test.endswith(options.bisectChunk): + break + + return bisectlist + + def pre_test(self, options, tests, status): + """This method is used to call other methods for setting up variables and + getting the list of tests for bisection.""" + if options.bisectChunk == "default": + return tests + # The second condition in 'if' is required to verify that the failing + # test is the last one. + elif "loop" not in self.contents or not self.contents["tests"][-1].endswith( + options.bisectChunk + ): + tests = self.get_tests_for_bisection(options, tests) + status = self.setup(tests) + + return self.next_chunk_binary(options, status) + + def post_test(self, options, expectedError, result): + """This method is used to call other methods to summarize results and check whether a + sanity check is done or not.""" + self.reset(expectedError, result) + status = self.summarize_chunk(options) + # Check whether sanity check has to be done. Also it is necessary to check whether + # options.bisectChunk is present in self.expectedError as we do not want to run + # if it is "default". + if status == -1 and options.bisectChunk in self.expectedError: + # In case we have a debug build, we don't want to run a sanity + # check, will take too much time. + if mozinfo.info["debug"]: + return status + + testBleedThrough = self.contents["testsToRun"][0] + tests = self.contents["totalTests"] + tests.remove(testBleedThrough) + # To make sure that the failing test is dependent on some other + # test. + if options.bisectChunk in testBleedThrough: + return status + + status = self.setup(tests) + self.summary.append("Sanity Check:") + + return status + + def next_chunk_reverse(self, options, status): + "This method is used to bisect the tests in a reverse search fashion." + + # Base Cases. + if self.contents["loop"] <= 1: + self.contents["testsToRun"] = self.contents["tests"] + if self.contents["loop"] == 1: + self.contents["testsToRun"] = [self.contents["tests"][-1]] + self.contents["loop"] += 1 + return self.contents["testsToRun"] + + if "result" in self.contents: + if self.contents["result"] == "PASS": + chunkSize = self.contents["end"] - self.contents["start"] + self.contents["end"] = self.contents["start"] - 1 + self.contents["start"] = self.contents["end"] - chunkSize + + # self.contents['result'] will be expected error only if it fails. + elif self.contents["result"] == "FAIL": + self.contents["tests"] = self.contents["testsToRun"] + status = 1 # for initializing + + # initialize + if status: + totalTests = len(self.contents["tests"]) + chunkSize = int(math.ceil(totalTests / 10.0)) + self.contents["start"] = totalTests - chunkSize - 1 + self.contents["end"] = totalTests - 2 + + start = self.contents["start"] + end = self.contents["end"] + 1 + self.contents["testsToRun"] = self.contents["tests"][start:end] + self.contents["testsToRun"].append(self.contents["tests"][-1]) + self.contents["loop"] += 1 + + return self.contents["testsToRun"] + + def next_chunk_binary(self, options, status): + "This method is used to bisect the tests in a binary search fashion." + + # Base cases. + if self.contents["loop"] <= 1: + self.contents["testsToRun"] = self.contents["tests"] + if self.contents["loop"] == 1: + self.contents["testsToRun"] = [self.contents["tests"][-1]] + self.contents["loop"] += 1 + return self.contents["testsToRun"] + + # Initialize the contents dict. + if status: + totalTests = len(self.contents["tests"]) + self.contents["start"] = 0 + self.contents["end"] = totalTests - 2 + + # pylint --py3k W1619 + mid = (self.contents["start"] + self.contents["end"]) / 2 + if "result" in self.contents: + if self.contents["result"] == "PASS": + self.contents["end"] = mid + + elif self.contents["result"] == "FAIL": + self.contents["start"] = mid + 1 + + mid = (self.contents["start"] + self.contents["end"]) / 2 + start = mid + 1 + end = self.contents["end"] + 1 + self.contents["testsToRun"] = self.contents["tests"][start:end] + if not self.contents["testsToRun"]: + self.contents["testsToRun"].append(self.contents["tests"][mid]) + self.contents["testsToRun"].append(self.contents["tests"][-1]) + self.contents["loop"] += 1 + + return self.contents["testsToRun"] + + def summarize_chunk(self, options): + "This method is used summarize the results after the list of tests is run." + if options.bisectChunk == "default": + # if no expectedError that means all the tests have successfully + # passed. + if len(self.expectedError) == 0: + return -1 + options.bisectChunk = self.expectedError.keys()[0] + self.summary.append("\tFound Error in test: %s" % options.bisectChunk) + return 0 + + # If options.bisectChunk is not in self.result then we need to move to + # the next run. + if options.bisectChunk not in self.result: + return -1 + + self.summary.append("\tPass %d:" % self.contents["loop"]) + if len(self.contents["testsToRun"]) > 1: + self.summary.append( + "\t\t%d test files(start,end,failing). [%s, %s, %s]" + % ( + len(self.contents["testsToRun"]), + self.contents["testsToRun"][0], + self.contents["testsToRun"][-2], + self.contents["testsToRun"][-1], + ) + ) + else: + self.summary.append("\t\t1 test file [%s]" % self.contents["testsToRun"][0]) + return self.check_for_intermittent(options) + + if self.result[options.bisectChunk] == "PASS": + self.summary.append("\t\tno failures found.") + if self.contents["loop"] == 1: + status = -1 + else: + self.contents["result"] = "PASS" + status = 0 + + elif self.result[options.bisectChunk] == "FAIL": + if "expectedError" not in self.contents: + self.summary.append("\t\t%s failed." % self.contents["testsToRun"][-1]) + self.contents["expectedError"] = self.expectedError[options.bisectChunk] + status = 0 + + elif ( + self.expectedError[options.bisectChunk] + == self.contents["expectedError"] + ): + self.summary.append( + "\t\t%s failed with expected error." + % self.contents["testsToRun"][-1] + ) + self.contents["result"] = "FAIL" + status = 0 + + # This code checks for test-bleedthrough. Should work for any + # algorithm. + numberOfTests = len(self.contents["testsToRun"]) + if numberOfTests < 3: + # This means that only 2 tests are run. Since the last test + # is the failing test itself therefore the bleedthrough + # test is the first test + self.summary.append( + "TEST-UNEXPECTED-FAIL | %s | Bleedthrough detected, this test is the " + "root cause for many of the above failures" + % self.contents["testsToRun"][0] + ) + status = -1 + else: + self.summary.append( + "\t\t%s failed with different error." + % self.contents["testsToRun"][-1] + ) + status = -1 + + return status + + def check_for_intermittent(self, options): + "This method is used to check whether a test is an intermittent." + if self.result[options.bisectChunk] == "PASS": + self.summary.append( + "\t\tThe test %s passed." % self.contents["testsToRun"][0] + ) + if self.repeat > 0: + # loop is set to 1 to again run the single test. + self.contents["loop"] = 1 + self.repeat -= 1 + return 0 + else: + if self.failcount > 0: + # -1 is being returned as the test is intermittent, so no need to bisect + # further. + return -1 + # If the test does not fail even once, then proceed to next chunk for bisection. + # loop is set to 2 to proceed on bisection. + self.contents["loop"] = 2 + return 1 + elif self.result[options.bisectChunk] == "FAIL": + self.summary.append( + "\t\tThe test %s failed." % self.contents["testsToRun"][0] + ) + self.failcount += 1 + self.contents["loop"] = 1 + self.repeat -= 1 + # self.max_failures is the maximum number of times a test is allowed + # to fail to be called an intermittent. If a test fails more than + # limit set, it is a perma-fail. + if self.failcount < self.max_failures: + if self.repeat == 0: + # -1 is being returned as the test is intermittent, so no need to bisect + # further. + return -1 + return 0 + else: + self.summary.append( + "TEST-UNEXPECTED-FAIL | %s | Bleedthrough detected, this test is the " + "root cause for many of the above failures" + % self.contents["testsToRun"][0] + ) + return -1 + + def print_summary(self): + "This method is used to print the recorded summary." + print("Bisection summary:") + for line in self.summary: + print(line) diff --git a/testing/mochitest/browser-harness.xhtml b/testing/mochitest/browser-harness.xhtml new file mode 100644 index 0000000000..f543582e42 --- /dev/null +++ b/testing/mochitest/browser-harness.xhtml @@ -0,0 +1,371 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- 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/. --> + +<window id="browserTestHarness" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="TestStart();" + title="Browser chrome tests" + width="1024"> + <script src="chrome://mochikit/content/tests/SimpleTest/MozillaLogger.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/LogController.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/TestRunner.js"/> + <script src="chrome://mochikit/content/chrome-harness.js"/> + <script src="chrome://mochikit/content/manifestLibrary.js" /> + <script src="chrome://mochikit/content/chunkifyTests.js"/> + <style xmlns="http://www.w3.org/1999/xhtml"><![CDATA[ + #results { + margin: 5px; + background-color: window; + user-select: text; + } + + #summary { + color: white; + border: 2px solid black; + } + + #summary.success { + background-color: #0d0; + } + + #summary.failure { + background-color: red; + } + + #summary.todo { + background-color: orange; + } + + .info { + color: grey; + } + + .failed { + color: red; + font-weight: bold; + } + + .testHeader { + margin-top: 1em; + } + + p { + margin: 0.1em; + } + + a { + color: blue; + text-decoration: underline; + } + ]]></style> + <script type="application/javascript"><![CDATA[ + var gConfig; + + var gDumper = { + get fileLogger() { + let logger = null; + if (gConfig.logFile) { + try { + logger = new MozillaFileLogger(gConfig.logFile) + } catch (ex) { + dump("TEST-UNEXPECTED-FAIL | (browser-harness.xhtml) | " + + "Error trying to log to " + gConfig.logFile + ": " + ex + "\n"); + } + } + delete this.fileLogger; + return this.fileLogger = logger; + }, + structuredLogger: TestRunner.structuredLogger, + dump(str) { + this.structuredLogger.info(str); + + if (this.fileLogger) + this.fileLogger.log(str); + }, + + done() { + if (this.fileLogger) + this.fileLogger.close(); + } + } + + function TestStart() { + gConfig = readConfig(); + + // Update the title for --start-at and --end-at. + if (gConfig.startAt || gConfig.endAt) + document.getElementById("runTestsButton").label = + "Run subset of tests"; + + if (gConfig.autorun) + setTimeout(runTests, 0); + } + + var gErrorCount = 0; + + function browserTest(aTestFile) { + this.path = aTestFile.url; + this.expected = aTestFile.expected; + this.https_first_disabled = aTestFile.https_first_disabled || false; + this.allow_xul_xbl = aTestFile.allow_xul_xbl || false; + this.dumper = gDumper; + this.results = []; + this.scope = null; + this.duration = 0; + this.unexpectedTimeouts = 0; + this.lastOutputTime = 0; + } + browserTest.prototype = { + get passCount() { + return this.results.filter(t => !t.info && !t.todo && t.pass).length; + }, + get todoCount() { + return this.results.filter(t => !t.info && t.todo && t.pass).length; + }, + get failCount() { + return this.results.filter(t => !t.info && !t.pass).length; + }, + get allowedFailureCount() { + return this.results.filter(t => t.allowedFailure).length; + }, + + addResult: function addResult(result) { + this.lastOutputTime = Date.now(); + this.results.push(result); + + if (result.info) { + if (result.msg) { + ChromeUtils.addProfilerMarker("TEST-INFO", {category: "Test"}, + result.msg); + this.dumper.structuredLogger.info(result.msg); + } + return; + } + + this.dumper.structuredLogger.testStatus(this.path, + result.name, + result.status, + result.expected, + result.msg); + let markerName = "TEST-"; + if (result.pass) { + markerName += result.todo ? "KNOWN-FAIL" : "PASS"; + } + else { + markerName += "UNEXPECTED-" + result.status; + } + let markerText = result.name; + if (result.msg) { + markerText += " - " + result.msg; + } + ChromeUtils.addProfilerMarker(markerName, {category: "Test"}, markerText); + }, + + setDuration: function setDuration(duration) { + this.duration = duration; + }, + + get htmlLog() { + let txtToHTML = Cc["@mozilla.org/txttohtmlconv;1"]. + getService(Ci.mozITXTToHTMLConv); + function _entityEncode(str) { + return txtToHTML.scanTXT(str, Ci.mozITXTToHTMLConv.kEntities); + } + var path = _entityEncode(this.path); + var html = this.results.map(function (t) { + var classname = "result "; + var result = "TEST-"; + if (t.info) { + classname = "info"; + result += "INFO"; + } + else if (t.pass) { + classname += "passed"; + if (t.todo) + result += "KNOWN-FAIL"; + else + result += "PASS"; + } + else { + classname += "failed"; + result += "UNEXPECTED-" + t.status; + } + var message = t.name + (t.msg ? " - " + t.msg : ""); + var text = result + " | " + path + " | " + _entityEncode(message); + if (!t.info && !t.pass) { + return '<p class="' + classname + '" id=\"ERROR' + (gErrorCount++) + '">' + + text + " <a href=\"javascript:scrollTo('ERROR" + gErrorCount + "')\">NEXT ERROR</a></p>"; + } + return '<p class="' + classname + '">' + text + "</p>"; + }).join("\n"); + if (this.duration) { + html += "<p class=\"info\">TEST-END | " + path + " | finished in " + + this.duration + " ms</p>"; + } + return html; + } + }; + + // Returns an array of browserTest objects for all the selected tests + function runTests() { + gConfig.baseurl = "chrome://mochitests/content"; + getTestList(gConfig, loadTestList); + } + + function loadTestList(links) { + if (!links) { + createTester({}); + return; + } + + // load server.js in so we can share template functions + var srvScope = {}; + Services.scriptloader.loadSubScript( + 'chrome://mochikit/content/server.js', + srvScope + ); + + var fileNames = []; + var fileNameRegexp = /browser_.+\.js$/; + srvScope.arrayOfTestFiles(links, fileNames, fileNameRegexp); + + if (gConfig.startAt || gConfig.endAt) { + fileNames = skipTests(fileNames, gConfig.startAt, gConfig.endAt); + } + + createTester(fileNames.map(function (f) { return new browserTest(f); })); + } + + function setStatus(aStatusString) { + document.getElementById("status").value = aStatusString; + } + + function createTester(links) { + var winType = null; + if (gConfig.testRoot == "browser") { + const IS_THUNDERBIRD = Services.appinfo.ID == "{3550f703-e582-4d05-9a08-453d09bdfdc6}"; + winType = IS_THUNDERBIRD ? "mail:3pane" : "navigator:browser"; + } + if (!winType) { + throw new Error("Unrecognized gConfig.testRoot: " + gConfig.testRoot); + } + var testWin = Services.wm.getMostRecentWindow(winType); + + setStatus("Running..."); + + // It's possible that the test harness window is not yet focused when this + // function runs (in which case testWin is already focused, and focusing it + // will be a no-op, and then the test harness window will steal focus later, + // which will mess up tests). So wait for the test harness window to be + // focused before trying to focus testWin. + waitForFocus(() => { + // Focus the test window and start tests. + waitForFocus(() => { + var Tester = new testWin.Tester(links, gDumper.structuredLogger, testsFinished); + Tester.start(); + }, testWin); + }, window); + } + + function executeSoon(callback) { + Services.tm.dispatchToMainThread(callback); + } + + function waitForFocus(callback, win) { + // If "win" is already focused, just call the callback. + if (Services.focus.focusedWindow == win) { + executeSoon(callback); + return; + } + + // Otherwise focus it, and wait for the focus event. + win.addEventListener("focus", function listener() { + executeSoon(callback); + }, { capture: true, once: true}); + win.focus(); + } + + function sum(a, b) { + return a + b; + } + + function getHTMLLogFromTests(aTests) { + if (!aTests.length) + return "<div id=\"summary\" class=\"failure\">No tests to run." + + " Did you pass an invalid --test-path?</div>"; + + var log = ""; + + var passCount = aTests.map(f => f.passCount).reduce(sum); + var failCount = aTests.map(f => f.failCount).reduce(sum); + var todoCount = aTests.map(f => f.todoCount).reduce(sum); + log += "<div id=\"summary\" class=\""; + if (failCount != 0) { + log += "failure"; + } else { + log += passCount == 0 ? "todo" : "success"; + } + log += "\">\n<p>Passed: " + passCount + "</p>\n" + + "<p>Failed: " + failCount; + if (failCount > 0) + log += " <a href=\"javascript:scrollTo('ERROR0')\">NEXT ERROR</a>"; + log += "</p>\n" + + "<p>Todo: " + todoCount + "</p>\n</div>\n<div id=\"log\">\n"; + + return log + aTests.map(function (f) { + return "<p class=\"testHeader\">Running " + f.path + "...</p>\n" + f.htmlLog; + }).join("\n") + "</div>"; + } + + function testsFinished(aTests) { + if (gConfig.closeWhenDone) { + const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); + if ( + !AppConstants.RELEASE_OR_BETA && + !AppConstants.DEBUG && + !AppConstants.MOZ_CODE_COVERAGE && + !AppConstants.ASAN && + !AppConstants.TSAN + ) { + let filename = + Services.profiler.IsActive() && + Services.env.get("MOZ_PROFILER_SHUTDOWN"); + if (!filename) { + Cu.exitIfInAutomation(); + } + Services.profiler + .dumpProfileToFileAsync(filename) + .then(() => Services.profiler.StopProfiler()) + .then(() => Cu.exitIfInAutomation()); + } else { + Services.startup.quit(Ci.nsIAppStartup.eForceQuit); + } + return; + } + + // Focus our window, to display the results + window.focus(); + + // UI + // eslint-disable-next-line no-unsanitized/property + document.getElementById("results").innerHTML = getHTMLLogFromTests(aTests); + setStatus("Done."); + } + + function scrollTo(id) { + var line = document.getElementById(id); + if (!line) + return; + + line.scrollIntoView(); + } + ]]></script> + <button id="runTestsButton" oncommand="runTests();" label="Run All Tests"/> + <label id="status"/> + <scrollbox flex="1" style="overflow: auto" align="stretch"> + <div id="results" xmlns="http://www.w3.org/1999/xhtml" flex="1"/> + </scrollbox> +</window> diff --git a/testing/mochitest/browser-test.js b/testing/mochitest/browser-test.js new file mode 100644 index 0000000000..7bc7085043 --- /dev/null +++ b/testing/mochitest/browser-test.js @@ -0,0 +1,1766 @@ +/* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */ + +/* eslint-env mozilla/browser-window */ +/* import-globals-from chrome-harness.js */ +/* import-globals-from mochitest-e10s-utils.js */ + +// Test timeout (seconds) +var gTimeoutSeconds = 45; +var gConfig; + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +ChromeUtils.defineModuleGetter( + this, + "AddonManager", + "resource://gre/modules/AddonManager.jsm" +); + +const SIMPLETEST_OVERRIDES = [ + "ok", + "record", + "is", + "isnot", + "todo", + "todo_is", + "todo_isnot", + "info", + "expectAssertions", + "requestCompleteLog", +]; + +setTimeout(testInit, 0); + +var TabDestroyObserver = { + outstanding: new Set(), + promiseResolver: null, + + init() { + Services.obs.addObserver(this, "message-manager-close"); + Services.obs.addObserver(this, "message-manager-disconnect"); + }, + + destroy() { + Services.obs.removeObserver(this, "message-manager-close"); + Services.obs.removeObserver(this, "message-manager-disconnect"); + }, + + observe(subject, topic, data) { + if (topic == "message-manager-close") { + this.outstanding.add(subject); + } else if (topic == "message-manager-disconnect") { + this.outstanding.delete(subject); + if (!this.outstanding.size && this.promiseResolver) { + this.promiseResolver(); + } + } + }, + + wait() { + if (!this.outstanding.size) { + return Promise.resolve(); + } + + return new Promise(resolve => { + this.promiseResolver = resolve; + }); + }, +}; + +function testInit() { + gConfig = readConfig(); + if (gConfig.testRoot == "browser") { + // Make sure to launch the test harness for the first opened window only + var prefs = Services.prefs; + if (prefs.prefHasUserValue("testing.browserTestHarness.running")) { + return; + } + + prefs.setBoolPref("testing.browserTestHarness.running", true); + + if (prefs.prefHasUserValue("testing.browserTestHarness.timeout")) { + gTimeoutSeconds = prefs.getIntPref("testing.browserTestHarness.timeout"); + } + + var sstring = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + sstring.data = location.search; + + Services.ww.openWindow( + window, + "chrome://mochikit/content/browser-harness.xhtml", + "browserTest", + "chrome,centerscreen,dialog=no,resizable,titlebar,toolbar=no,width=800,height=600", + sstring + ); + } else { + // This code allows us to redirect without requiring specialpowers for chrome and a11y tests. + let messageHandler = function(m) { + // eslint-disable-next-line no-undef + messageManager.removeMessageListener("chromeEvent", messageHandler); + var url = m.json.data; + + // Window is the [ChromeWindow] for messageManager, so we need content.window + // Currently chrome tests are run in a content window instead of a ChromeWindow + // eslint-disable-next-line no-undef + var webNav = content.window.docShell.QueryInterface(Ci.nsIWebNavigation); + let loadURIOptions = { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }; + webNav.loadURI(url, loadURIOptions); + }; + + var listener = + 'data:,function doLoad(e) { var data=e.detail&&e.detail.data;removeEventListener("contentEvent", function (e) { doLoad(e); }, false, true);sendAsyncMessage("chromeEvent", {"data":data}); };addEventListener("contentEvent", function (e) { doLoad(e); }, false, true);'; + // eslint-disable-next-line no-undef + messageManager.addMessageListener("chromeEvent", messageHandler); + // eslint-disable-next-line no-undef + messageManager.loadFrameScript(listener, true); + } + if (gConfig.e10s) { + e10s_init(); + + let processCount = prefs.getIntPref("dom.ipc.processCount", 1); + if (processCount > 1) { + // Currently starting a content process is slow, to aviod timeouts, let's + // keep alive content processes. + prefs.setIntPref("dom.ipc.keepProcessesAlive.web", processCount); + } + + Services.mm.loadFrameScript( + "chrome://mochikit/content/shutdown-leaks-collector.js", + true + ); + } else { + // In non-e10s, only run the ShutdownLeaksCollector in the parent process. + ChromeUtils.importESModule( + "chrome://mochikit/content/ShutdownLeaksCollector.sys.mjs" + ); + } +} + +function isGenerator(value) { + return value && typeof value === "object" && typeof value.next === "function"; +} + +function Tester(aTests, structuredLogger, aCallback) { + this.structuredLogger = structuredLogger; + this.tests = aTests; + this.callback = aCallback; + + this._scriptLoader = Services.scriptloader; + this.EventUtils = {}; + this._scriptLoader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", + this.EventUtils + ); + + this._scriptLoader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/AccessibilityUtils.js", + // AccessibilityUtils are integrated with EventUtils to perform additional + // accessibility checks for certain user interactions (clicks, etc). Load + // them into the EventUtils scope here. + this.EventUtils + ); + this.AccessibilityUtils = this.EventUtils.AccessibilityUtils; + + // Make sure our SpecialPowers actor is instantiated, in case it was + // registered after our DOMWindowCreated event was fired (which it + // most likely was). + void window.windowGlobalChild.getActor("SpecialPowers"); + + var simpleTestScope = {}; + this._scriptLoader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", + simpleTestScope + ); + this._scriptLoader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/MemoryStats.js", + simpleTestScope + ); + this._scriptLoader.loadSubScript( + "chrome://mochikit/content/chrome-harness.js", + simpleTestScope + ); + this.SimpleTest = simpleTestScope.SimpleTest; + + window.SpecialPowers.SimpleTest = this.SimpleTest; + window.SpecialPowers.setAsDefaultAssertHandler(); + + var extensionUtilsScope = { + registerCleanupFunction: fn => { + this.currentTest.scope.registerCleanupFunction(fn); + }, + }; + extensionUtilsScope.SimpleTest = this.SimpleTest; + this._scriptLoader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js", + extensionUtilsScope + ); + this.ExtensionTestUtils = extensionUtilsScope.ExtensionTestUtils; + + this.SimpleTest.harnessParameters = gConfig; + + this.MemoryStats = simpleTestScope.MemoryStats; + this.ContentTask = ChromeUtils.importESModule( + "resource://testing-common/ContentTask.sys.mjs" + ).ContentTask; + this.BrowserTestUtils = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ).BrowserTestUtils; + this.TestUtils = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" + ).TestUtils; + this.PromiseTestUtils = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" + ).PromiseTestUtils; + this.Assert = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ).Assert; + this.PerTestCoverageUtils = ChromeUtils.import( + "resource://testing-common/PerTestCoverageUtils.jsm" + ).PerTestCoverageUtils; + + this.PromiseTestUtils.init(); + + this.SimpleTestOriginal = {}; + SIMPLETEST_OVERRIDES.forEach(m => { + this.SimpleTestOriginal[m] = this.SimpleTest[m]; + }); + + this._coverageCollector = null; + + const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" + ); + + // Avoid failing tests when XPCOMUtils.defineLazyScriptGetter is used. + XPCOMUtils.overrideScriptLoaderForTests({ + loadSubScript: (url, obj) => { + let before = Object.keys(window); + try { + return this._scriptLoader.loadSubScript(url, obj); + } finally { + for (let property of Object.keys(window)) { + if ( + !before.includes(property) && + !this._globalProperties.includes(property) + ) { + this._globalProperties.push(property); + this.SimpleTest.info( + `Global property added while loading ${url}: ${property}` + ); + } + } + } + }, + loadSubScriptWithOptions: this._scriptLoader.loadSubScriptWithOptions.bind( + this._scriptLoader + ), + }); + + // ensure the mouse is reset before each test run + if (Services.env.exists("MOZ_AUTOMATION")) { + this.EventUtils.synthesizeNativeMouseEvent({ + type: "mousemove", + screenX: 1000, + screenY: 10, + }); + } +} +Tester.prototype = { + EventUtils: {}, + AccessibilityUtils: {}, + SimpleTest: {}, + ContentTask: null, + ExtensionTestUtils: null, + Assert: null, + + repeat: 0, + a11y_checks: false, + runUntilFailure: false, + checker: null, + currentTestIndex: -1, + lastStartTime: null, + lastStartTimestamp: null, + lastAssertionCount: 0, + failuresFromInitialWindowState: 0, + + get currentTest() { + return this.tests[this.currentTestIndex]; + }, + get done() { + return this.currentTestIndex == this.tests.length - 1 && this.repeat <= 0; + }, + + start: function Tester_start() { + TabDestroyObserver.init(); + + // if testOnLoad was not called, then gConfig is not defined + if (!gConfig) { + gConfig = readConfig(); + } + + if (gConfig.runUntilFailure) { + this.runUntilFailure = true; + } + + if (gConfig.a11y_checks != undefined) { + this.a11y_checks = gConfig.a11y_checks; + } + + if (gConfig.repeat) { + this.repeat = gConfig.repeat; + } + + if (gConfig.jscovDirPrefix) { + let coveragePath = gConfig.jscovDirPrefix; + let { CoverageCollector } = ChromeUtils.importESModule( + "resource://testing-common/CoverageUtils.sys.mjs" + ); + this._coverageCollector = new CoverageCollector(coveragePath); + } + + this.structuredLogger.info("*** Start BrowserChrome Test Results ***"); + Services.console.registerListener(this); + this._globalProperties = Object.keys(window); + this._globalPropertyWhitelist = [ + "navigator", + "constructor", + "top", + "Application", + "__SS_tabsToRestore", + "__SSi", + "webConsoleCommandController", + // Thunderbird + "MailMigrator", + "SearchIntegration", + ]; + + this.PerTestCoverageUtils.beforeTestSync(); + + if (this.tests.length) { + this.waitForWindowsReady().then(() => { + this.nextTest(); + }); + } else { + this.finish(); + } + }, + + async waitForWindowsReady() { + await this.setupDefaultTheme(); + await new Promise(resolve => + this.waitForGraphicsTestWindowToBeGone(resolve) + ); + await this.promiseMainWindowReady(); + }, + + async promiseMainWindowReady() { + if (window.gBrowserInit) { + await window.gBrowserInit.idleTasksFinishedPromise; + } + }, + + async setupDefaultTheme() { + // Developer Edition enables the wrong theme by default. Make sure + // the ordinary default theme is enabled. + let theme = await AddonManager.getAddonByID("default-theme@mozilla.org"); + await theme.enable(); + }, + + waitForGraphicsTestWindowToBeGone(aCallback) { + for (let win of Services.wm.getEnumerator(null)) { + if ( + win != window && + !win.closed && + win.document.documentURI == + "chrome://gfxsanity/content/sanityparent.html" + ) { + this.BrowserTestUtils.domWindowClosed(win).then(aCallback); + return; + } + } + // graphics test window is already gone, just call callback immediately + aCallback(); + }, + + waitForWindowsState: function Tester_waitForWindowsState(aCallback) { + let timedOut = this.currentTest && this.currentTest.timedOut; + // eslint-disable-next-line no-nested-ternary + let baseMsg = timedOut + ? "Found a {elt} after previous test timed out" + : this.currentTest + ? "Found an unexpected {elt} at the end of test run" + : "Found an unexpected {elt}"; + + // Remove stale tabs + if ( + this.currentTest && + window.gBrowser && + AppConstants.MOZ_APP_NAME != "thunderbird" && + gBrowser.tabs.length > 1 + ) { + let lastURI = ""; + let lastURIcount = 0; + while (gBrowser.tabs.length > 1) { + let lastTab = gBrowser.tabs[gBrowser.tabs.length - 1]; + if (!lastTab.closing) { + // Report the stale tab as an error only when they're not closing. + // Tests can finish without waiting for the closing tabs. + if (lastURI != lastTab.linkedBrowser.currentURI.spec) { + lastURI = lastTab.linkedBrowser.currentURI.spec; + } else { + lastURIcount++; + if (lastURIcount >= 3) { + this.currentTest.addResult( + new testResult({ + name: + "terminating browser early - unable to close tabs; skipping remaining tests in folder", + allowFailure: this.currentTest.allowFailure, + }) + ); + this.finish(); + } + } + this.currentTest.addResult( + new testResult({ + name: + baseMsg.replace("{elt}", "tab") + + ": " + + lastTab.linkedBrowser.currentURI.spec, + allowFailure: this.currentTest.allowFailure, + }) + ); + } + gBrowser.removeTab(lastTab); + } + } + + // Replace the last tab with a fresh one + if (window.gBrowser && AppConstants.MOZ_APP_NAME != "thunderbird") { + gBrowser.addTab("about:blank", { + skipAnimation: true, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true }); + gBrowser.stop(); + } + + // Remove stale windows + this.structuredLogger.info("checking window state"); + for (let win of Services.wm.getEnumerator(null)) { + let type = win.document.documentElement.getAttribute("windowtype"); + if ( + win != window && + !win.closed && + win.document.documentElement.getAttribute("id") != + "browserTestHarness" && + type != "devtools:webconsole" + ) { + switch (type) { + case "navigator:browser": + type = "browser window"; + break; + case "mail:3pane": + type = "mail window"; + break; + case null: + type = + "unknown window with document URI: " + + win.document.documentURI + + " and title: " + + win.document.title; + break; + } + let msg = baseMsg.replace("{elt}", type); + if (this.currentTest) { + this.currentTest.addResult( + new testResult({ + name: msg, + allowFailure: this.currentTest.allowFailure, + }) + ); + } else { + this.failuresFromInitialWindowState++; + this.structuredLogger.error("browser-test.js | " + msg); + } + + win.close(); + } + } + + // Make sure the window is raised before each test. + this.SimpleTest.waitForFocus(aCallback); + }, + + finish: function Tester_finish(aSkipSummary) { + var passCount = this.tests.reduce((a, f) => a + f.passCount, 0); + var failCount = this.tests.reduce((a, f) => a + f.failCount, 0); + var todoCount = this.tests.reduce((a, f) => a + f.todoCount, 0); + + // Include failures from window state checking prior to running the first test + failCount += this.failuresFromInitialWindowState; + + TabDestroyObserver.destroy(); + Services.console.unregisterListener(this); + + // It's important to terminate the module to avoid crashes on shutdown. + this.PromiseTestUtils.uninit(); + + // In the main process, we print the ShutdownLeaksCollector message here. + let pid = Services.appinfo.processID; + dump("Completed ShutdownLeaks collections in process " + pid + "\n"); + + this.structuredLogger.info("TEST-START | Shutdown"); + + if (this.tests.length) { + let e10sMode = window.gMultiProcessBrowser ? "e10s" : "non-e10s"; + this.structuredLogger.info("Browser Chrome Test Summary"); + this.structuredLogger.info("Passed: " + passCount); + this.structuredLogger.info("Failed: " + failCount); + this.structuredLogger.info("Todo: " + todoCount); + this.structuredLogger.info("Mode: " + e10sMode); + } else { + this.structuredLogger.error( + "browser-test.js | No tests to run. Did you pass invalid test_paths?" + ); + } + this.structuredLogger.info("*** End BrowserChrome Test Results ***"); + + // Tests complete, notify the callback and return + this.callback(this.tests); + this.accService = null; + this.callback = null; + this.tests = null; + }, + + haltTests: function Tester_haltTests() { + // Do not run any further tests + this.currentTestIndex = this.tests.length - 1; + this.repeat = 0; + }, + + observe: function Tester_observe(aSubject, aTopic, aData) { + if (!aTopic) { + this.onConsoleMessage(aSubject); + } + }, + + onConsoleMessage: function Tester_onConsoleMessage(aConsoleMessage) { + // Ignore empty messages. + if (!aConsoleMessage.message) { + return; + } + + try { + var msg = "Console message: " + aConsoleMessage.message; + if (this.currentTest) { + this.currentTest.addResult(new testMessage(msg)); + } else { + this.structuredLogger.info( + "TEST-INFO | (browser-test.js) | " + msg.replace(/\n$/, "") + "\n" + ); + } + } catch (ex) { + // Swallow exception so we don't lead to another error being reported, + // throwing us into an infinite loop + } + }, + + async ensureVsyncDisabled() { + // The WebExtension process keeps vsync enabled forever in headless mode. + // See bug 1782541. + if (Services.env.get("MOZ_HEADLESS")) { + return; + } + + try { + await this.TestUtils.waitForCondition( + () => !ChromeUtils.vsyncEnabled(), + "waiting for vsync to be disabled" + ); + } catch (e) { + this.Assert.ok(false, e); + this.Assert.ok( + false, + "vsync remained enabled at the end of the test. " + + "Is there an animation still running? " + + "Consider talking to the performance team for tips to solve this." + ); + } + }, + + async nextTest() { + if (this.currentTest) { + if (this._coverageCollector) { + this._coverageCollector.recordTestCoverage(this.currentTest.path); + } + + this.PerTestCoverageUtils.afterTestSync(); + + // Run cleanup functions for the current test before moving on to the + // next one. + let testScope = this.currentTest.scope; + while (testScope.__cleanupFunctions.length) { + let func = testScope.__cleanupFunctions.shift(); + try { + let result = await func.apply(testScope); + if (isGenerator(result)) { + this.SimpleTest.ok(false, "Cleanup function returned a generator"); + } + } catch (ex) { + this.currentTest.addResult( + new testResult({ + name: "Cleanup function threw an exception", + ex, + allowFailure: this.currentTest.allowFailure, + }) + ); + } + } + + // Spare tests cleanup work. + // Reset gReduceMotionOverride in case the test set it. + if (typeof gReduceMotionOverride == "boolean") { + gReduceMotionOverride = null; + } + + Services.obs.notifyObservers(null, "test-complete"); + + if ( + this.currentTest.passCount === 0 && + this.currentTest.failCount === 0 && + this.currentTest.todoCount === 0 + ) { + this.currentTest.addResult( + new testResult({ + name: + "This test contains no passes, no fails and no todos. Maybe" + + " it threw a silent exception? Make sure you use" + + " waitForExplicitFinish() if you need it.", + }) + ); + } + + let winUtils = window.windowUtils; + if (winUtils.isTestControllingRefreshes) { + this.currentTest.addResult( + new testResult({ + name: "test left refresh driver under test control", + }) + ); + winUtils.restoreNormalRefresh(); + } + + if (this.SimpleTest.isExpectingUncaughtException()) { + this.currentTest.addResult( + new testResult({ + name: + "expectUncaughtException was called but no uncaught" + + " exception was detected!", + allowFailure: this.currentTest.allowFailure, + }) + ); + } + + this.resolveFinishTestPromise(); + this.resolveFinishTestPromise = null; + this.TestUtils.promiseTestFinished = null; + + this.PromiseTestUtils.ensureDOMPromiseRejectionsProcessed(); + this.PromiseTestUtils.assertNoUncaughtRejections(); + this.PromiseTestUtils.assertNoMoreExpectedRejections(); + await this.ensureVsyncDisabled(); + + Object.keys(window).forEach(function(prop) { + if (parseInt(prop) == prop) { + // This is a string which when parsed as an integer and then + // stringified gives the original string. As in, this is in fact a + // string representation of an integer, so an index into + // window.frames. Skip those. + return; + } + if (!this._globalProperties.includes(prop)) { + this._globalProperties.push(prop); + if (!this._globalPropertyWhitelist.includes(prop)) { + this.currentTest.addResult( + new testResult({ + name: "test left unexpected property on window: " + prop, + allowFailure: this.currentTest.allowFailure, + }) + ); + } + } + }, this); + + // eslint-disable-next-line no-undef + await new Promise(resolve => SpecialPowers.flushPrefEnv(resolve)); + + if (gConfig.cleanupCrashes) { + let gdir = Services.dirsvc.get("UAppData", Ci.nsIFile); + gdir.append("Crash Reports"); + gdir.append("pending"); + if (gdir.exists()) { + let entries = gdir.directoryEntries; + while (entries.hasMoreElements()) { + let entry = entries.nextFile; + if (entry.isFile()) { + let msg = "this test left a pending crash report; "; + try { + entry.remove(false); + msg += "deleted " + entry.path; + } catch (e) { + msg += "could not delete " + entry.path; + } + this.structuredLogger.info(msg); + } + } + } + } + + // Notify a long running test problem if it didn't end up in a timeout. + if (this.currentTest.unexpectedTimeouts && !this.currentTest.timedOut) { + this.currentTest.addResult( + new testResult({ + name: + "This test exceeded the timeout threshold. It should be" + + " rewritten or split up. If that's not possible, use" + + " requestLongerTimeout(N), but only as a last resort.", + }) + ); + } + + // If we're in a debug build, check assertion counts. This code + // is similar to the code in TestRunner.testUnloaded in + // TestRunner.js used for all other types of mochitests. + let debugsvc = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); + if (debugsvc.isDebugBuild) { + let newAssertionCount = debugsvc.assertionCount; + let numAsserts = newAssertionCount - this.lastAssertionCount; + this.lastAssertionCount = newAssertionCount; + + let max = testScope.__expectedMaxAsserts; + let min = testScope.__expectedMinAsserts; + if (numAsserts > max) { + // TEST-UNEXPECTED-FAIL + this.currentTest.addResult( + new testResult({ + name: + "Assertion count " + + numAsserts + + " is greater than expected range " + + min + + "-" + + max + + " assertions.", + pass: true, // TEMPORARILY TEST-KNOWN-FAIL + todo: true, + allowFailure: this.currentTest.allowFailure, + }) + ); + } else if (numAsserts < min) { + // TEST-UNEXPECTED-PASS + this.currentTest.addResult( + new testResult({ + name: + "Assertion count " + + numAsserts + + " is less than expected range " + + min + + "-" + + max + + " assertions.", + todo: true, + allowFailure: this.currentTest.allowFailure, + }) + ); + } else if (numAsserts > 0) { + // TEST-KNOWN-FAIL + this.currentTest.addResult( + new testResult({ + name: + "Assertion count " + + numAsserts + + " is within expected range " + + min + + "-" + + max + + " assertions.", + pass: true, + todo: true, + allowFailure: this.currentTest.allowFailure, + }) + ); + } + } + + if (this.currentTest.allowFailure) { + if (this.currentTest.expectedAllowedFailureCount) { + this.currentTest.addResult( + new testResult({ + name: + "Expected " + + this.currentTest.expectedAllowedFailureCount + + " failures in this file, got " + + this.currentTest.allowedFailureCount + + ".", + pass: + this.currentTest.expectedAllowedFailureCount == + this.currentTest.allowedFailureCount, + }) + ); + } else if (this.currentTest.allowedFailureCount == 0) { + this.currentTest.addResult( + new testResult({ + name: + "We expect at least one assertion to fail because this" + + " test file is marked as fail-if in the manifest.", + todo: true, + knownFailure: this.currentTest.allowFailure, + }) + ); + } + } + + // Dump memory stats for main thread. + if ( + Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ) { + this.MemoryStats.dump( + this.currentTestIndex, + this.currentTest.path, + gConfig.dumpOutputDirectory, + gConfig.dumpAboutMemoryAfterTest, + gConfig.dumpDMDAfterTest + ); + } + + // Note the test run time + let name = this.currentTest.path; + name = name.slice(name.lastIndexOf("/") + 1); + ChromeUtils.addProfilerMarker( + "browser-test", + { category: "Test", startTime: this.lastStartTimestamp }, + name + ); + + // See if we should upload a profile of a failing test. + if (this.currentTest.failCount) { + // If MOZ_PROFILER_SHUTDOWN is set, the profiler got started from --profiler + // and a profile will be shown even if there's no test failure. + if ( + Services.env.exists("MOZ_UPLOAD_DIR") && + !Services.env.exists("MOZ_PROFILER_SHUTDOWN") && + Services.profiler.IsActive() + ) { + let filename = `profile_${name}.json`; + let path = Services.env.get("MOZ_UPLOAD_DIR"); + let profilePath = PathUtils.join(path, filename); + try { + let profileData = await Services.profiler.getProfileDataAsGzippedArrayBuffer(); + await IOUtils.write(profilePath, new Uint8Array(profileData)); + this.currentTest.addResult( + new testResult({ + name: + "Found unexpected failures during the test; profile uploaded in " + + filename, + }) + ); + } catch (e) { + // If the profile is large, we may encounter out of memory errors. + this.currentTest.addResult( + new testResult({ + name: + "Found unexpected failures during the test; failed to upload profile: " + + e, + }) + ); + } + } + } + + let time = Date.now() - this.lastStartTime; + + this.structuredLogger.testEnd( + this.currentTest.path, + "OK", + undefined, + "finished in " + time + "ms" + ); + this.currentTest.setDuration(time); + + if (this.runUntilFailure && this.currentTest.failCount > 0) { + this.haltTests(); + } + + // Restore original SimpleTest methods to avoid leaks. + SIMPLETEST_OVERRIDES.forEach(m => { + this.SimpleTest[m] = this.SimpleTestOriginal[m]; + }); + + this.ContentTask.setTestScope(null); + testScope.destroy(); + this.currentTest.scope = null; + } + + // Check the window state for the current test before moving to the next one. + // This also causes us to check before starting any tests, since nextTest() + // is invoked to start the tests. + this.waitForWindowsState(() => { + if (this.done) { + if (this._coverageCollector) { + this._coverageCollector.finalize(); + } else if ( + !AppConstants.RELEASE_OR_BETA && + !AppConstants.DEBUG && + !AppConstants.MOZ_CODE_COVERAGE && + !AppConstants.ASAN && + !AppConstants.TSAN + ) { + this.finish(); + return; + } + + // Uninitialize a few things explicitly so that they can clean up + // frames and browser intentionally kept alive until shutdown to + // eliminate false positives. + if (gConfig.testRoot == "browser") { + // Skip if SeaMonkey + if (AppConstants.MOZ_APP_NAME != "seamonkey") { + // Replace the document currently loaded in the browser's sidebar. + // This will prevent false positives for tests that were the last + // to touch the sidebar. They will thus not be blamed for leaking + // a document. + let sidebar = document.getElementById("sidebar"); + if (sidebar) { + sidebar.setAttribute("src", "data:text/html;charset=utf-8,"); + sidebar.docShell.createAboutBlankContentViewer(null, null); + sidebar.setAttribute("src", "about:blank"); + } + } + + // Destroy BackgroundPageThumbs resources. + let { BackgroundPageThumbs } = ChromeUtils.import( + "resource://gre/modules/BackgroundPageThumbs.jsm" + ); + BackgroundPageThumbs._destroy(); + + if (window.gBrowser) { + NewTabPagePreloading.removePreloadedBrowser(window); + } + } + + // Schedule GC and CC runs before finishing in order to detect + // DOM windows leaked by our tests or the tested code. Note that we + // use a shrinking GC so that the JS engine will discard JIT code and + // JIT caches more aggressively. + + let shutdownCleanup = aCallback => { + Cu.schedulePreciseShrinkingGC(() => { + // Run the GC and CC a few times to make sure that as much + // as possible is freed. + let numCycles = 3; + for (let i = 0; i < numCycles; i++) { + Cu.forceGC(); + Cu.forceCC(); + } + aCallback(); + }); + }; + + let { AsyncShutdown } = ChromeUtils.importESModule( + "resource://gre/modules/AsyncShutdown.sys.mjs" + ); + + let barrier = new AsyncShutdown.Barrier( + "ShutdownLeaks: Wait for cleanup to be finished before checking for leaks" + ); + Services.obs.notifyObservers( + { wrappedJSObject: barrier }, + "shutdown-leaks-before-check" + ); + + barrier.client.addBlocker( + "ShutdownLeaks: Wait for tabs to finish closing", + TabDestroyObserver.wait() + ); + + barrier.wait().then(() => { + // Simulate memory pressure so that we're forced to free more resources + // and thus get rid of more false leaks like already terminated workers. + Services.obs.notifyObservers( + null, + "memory-pressure", + "heap-minimize" + ); + + Services.ppmm.broadcastAsyncMessage("browser-test:collect-request"); + + shutdownCleanup(() => { + setTimeout(() => { + shutdownCleanup(() => { + this.finish(); + }); + }, 1000); + }); + }); + + return; + } + + if (this.repeat > 0) { + --this.repeat; + if (this.currentTestIndex < 0) { + this.currentTestIndex = 0; + } + this.execTest(); + } else { + this.currentTestIndex++; + if (gConfig.repeat) { + this.repeat = gConfig.repeat; + } + this.execTest(); + } + }); + }, + + async handleTask(task, currentTest, PromiseTestUtils, isSetup = false) { + let currentScope = currentTest.scope; + let desc = isSetup ? "setup" : "test"; + currentScope.SimpleTest.info(`Entering ${desc} ${task.name}`); + let startTimestamp = performance.now(); + try { + let result = await task(); + if (isGenerator(result)) { + currentScope.SimpleTest.ok(false, "Task returned a generator"); + } + } catch (ex) { + if (currentTest.timedOut) { + currentTest.addResult( + new testResult({ + name: `Uncaught exception received from previously timed out ${desc}`, + pass: false, + ex, + stack: typeof ex == "object" && "stack" in ex ? ex.stack : null, + allowFailure: currentTest.allowFailure, + }) + ); + // We timed out, so we've already cleaned up for this test, just get outta here. + return; + } + currentTest.addResult( + new testResult({ + name: `Uncaught exception in ${desc}`, + pass: currentScope.SimpleTest.isExpectingUncaughtException(), + ex, + stack: typeof ex == "object" && "stack" in ex ? ex.stack : null, + allowFailure: currentTest.allowFailure, + }) + ); + } + PromiseTestUtils.assertNoUncaughtRejections(); + ChromeUtils.addProfilerMarker( + isSetup ? "setup-task" : "task", + { category: "Test", startTime: startTimestamp }, + task.name.replace(/^bound /, "") || undefined + ); + currentScope.SimpleTest.info(`Leaving ${desc} ${task.name}`); + }, + + async _runTaskBasedTest(currentTest) { + let currentScope = currentTest.scope; + + // First run all the setups: + let setupFn; + while ((setupFn = currentScope.__setups.shift())) { + await this.handleTask( + setupFn, + currentTest, + this.PromiseTestUtils, + true /* is setup task */ + ); + } + + // 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. + let skipTask = task => { + let logger = this.structuredLogger; + logger.deactivateBuffering(); + logger.testStatus(this.currentTest.path, task.name, "SKIP"); + logger.warning("Skipping test " + task.name); + logger.activateBuffering(); + }; + + let task; + while ((task = currentScope.__tasks.shift())) { + if ( + task.__skipMe || + (currentScope.__runOnlyThisTask && + task != currentScope.__runOnlyThisTask) + ) { + skipTask(task); + continue; + } + await this.handleTask(task, currentTest, this.PromiseTestUtils); + } + currentScope.finish(); + }, + + execTest: function Tester_execTest() { + this.structuredLogger.testStart(this.currentTest.path); + + this.SimpleTest.reset(); + // Reset accessibility environment. + this.AccessibilityUtils.reset(this.a11y_checks); + + // Load the tests into a testscope + let currentScope = (this.currentTest.scope = new testScope( + this, + this.currentTest, + this.currentTest.expected + )); + let currentTest = this.currentTest; + + // HTTPS-First (Bug 1704453) TODO: in case a test is annoated + // with https_first_disabled then we explicitly flip the pref + // dom.security.https_first to false for the duration of the test. + if (currentTest.https_first_disabled) { + window.SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", false]], + }); + } + + if (currentTest.allow_xul_xbl) { + window.SpecialPowers.pushPermissions([ + { type: "allowXULXBL", allow: true, context: "http://mochi.test:8888" }, + { type: "allowXULXBL", allow: true, context: "http://example.org" }, + ]); + } + + // Import utils in the test scope. + let { scope } = this.currentTest; + scope.EventUtils = this.EventUtils; + scope.AccessibilityUtils = this.AccessibilityUtils; + scope.SimpleTest = this.SimpleTest; + scope.gTestPath = this.currentTest.path; + scope.ContentTask = this.ContentTask; + scope.BrowserTestUtils = this.BrowserTestUtils; + scope.TestUtils = this.TestUtils; + scope.ExtensionTestUtils = this.ExtensionTestUtils; + // Pass a custom report function for mochitest style reporting. + scope.Assert = new this.Assert(function(err, message, stack) { + currentTest.addResult( + new testResult( + err + ? { + name: err.message, + ex: err.stack, + stack: err.stack, + allowFailure: currentTest.allowFailure, + } + : { + name: message, + pass: true, + stack, + allowFailure: currentTest.allowFailure, + } + ) + ); + }, true); + + this.ContentTask.setTestScope(currentScope); + + // Allow Assert.sys.mjs methods to be tacked to the current scope. + scope.export_assertions = function() { + for (let func in this.Assert) { + this[func] = this.Assert[func].bind(this.Assert); + } + }; + + // Override SimpleTest methods with ours. + SIMPLETEST_OVERRIDES.forEach(function(m) { + this.SimpleTest[m] = this[m]; + }, scope); + + // load the tools to work with chrome .jar and remote + try { + this._scriptLoader.loadSubScript( + "chrome://mochikit/content/chrome-harness.js", + scope + ); + } catch (ex) { + /* no chrome-harness tools */ + } + + // Import head.js script if it exists. + var currentTestDirPath = this.currentTest.path.substr( + 0, + this.currentTest.path.lastIndexOf("/") + ); + var headPath = currentTestDirPath + "/head.js"; + try { + this._scriptLoader.loadSubScript(headPath, scope); + } catch (ex) { + // Bug 755558 - Ignore loadSubScript errors due to a missing head.js. + const isImportError = /^Error opening input stream/.test(ex.toString()); + + // Bug 1503169 - head.js may call loadSubScript, and generate similar errors. + // Only swallow errors that are strictly related to loading head.js. + const containsHeadPath = ex.toString().includes(headPath); + + if (!isImportError || !containsHeadPath) { + this.currentTest.addResult( + new testResult({ + name: "head.js import threw an exception", + ex, + }) + ); + } + } + + // Import the test script. + try { + this.lastStartTimestamp = performance.now(); + this.TestUtils.promiseTestFinished = new Promise(resolve => { + this.resolveFinishTestPromise = resolve; + }); + this._scriptLoader.loadSubScript(this.currentTest.path, scope); + // Run the test + this.lastStartTime = Date.now(); + if (this.currentTest.scope.__tasks) { + // This test consists of tasks, added via the `add_task()` API. + if ("test" in this.currentTest.scope) { + throw new Error( + "Cannot run both a add_task test and a normal test at the same time." + ); + } + // Spin off the async work without waiting for it to complete. + // It'll call finish() when it's done. + this._runTaskBasedTest(this.currentTest); + } else if (typeof scope.test == "function") { + scope.test(); + } else { + throw new Error( + "This test didn't call add_task, nor did it define a generatorTest() function, nor did it define a test() function, so we don't know how to run it." + ); + } + } catch (ex) { + if (!this.SimpleTest.isIgnoringAllUncaughtExceptions()) { + this.currentTest.addResult( + new testResult({ + name: "Exception thrown", + pass: this.SimpleTest.isExpectingUncaughtException(), + ex, + allowFailure: this.currentTest.allowFailure, + }) + ); + this.SimpleTest.expectUncaughtException(false); + } else { + this.currentTest.addResult(new testMessage("Exception thrown: " + ex)); + } + this.currentTest.scope.finish(); + } + + // If the test ran synchronously, move to the next test, otherwise the test + // will trigger the next test when it is done. + if (this.currentTest.scope.__done) { + this.nextTest(); + } else { + var self = this; + var timeoutExpires = Date.now() + gTimeoutSeconds * 1000; + var waitUntilAtLeast = timeoutExpires - 1000; + this.currentTest.scope.__waitTimer = this.SimpleTest._originalSetTimeout.apply( + window, + [ + function timeoutFn() { + // We sometimes get woken up long before the gTimeoutSeconds + // have elapsed (when running in chaos mode for example). This + // code ensures that we don't wrongly time out in that case. + if (Date.now() < waitUntilAtLeast) { + self.currentTest.scope.__waitTimer = setTimeout( + timeoutFn, + timeoutExpires - Date.now() + ); + return; + } + + if (--self.currentTest.scope.__timeoutFactor > 0) { + // We were asked to wait a bit longer. + self.currentTest.scope.info( + "Longer timeout required, waiting longer... Remaining timeouts: " + + self.currentTest.scope.__timeoutFactor + ); + self.currentTest.scope.__waitTimer = setTimeout( + timeoutFn, + gTimeoutSeconds * 1000 + ); + return; + } + + // If the test is taking longer than expected, but it's not hanging, + // mark the fact, but let the test continue. At the end of the test, + // if it didn't timeout, we will notify the problem through an error. + // To figure whether it's an actual hang, compare the time of the last + // result or message to half of the timeout time. + // Though, to protect against infinite loops, limit the number of times + // we allow the test to proceed. + const MAX_UNEXPECTED_TIMEOUTS = 10; + if ( + Date.now() - self.currentTest.lastOutputTime < + (gTimeoutSeconds / 2) * 1000 && + ++self.currentTest.unexpectedTimeouts <= MAX_UNEXPECTED_TIMEOUTS + ) { + self.currentTest.scope.__waitTimer = setTimeout( + timeoutFn, + gTimeoutSeconds * 1000 + ); + return; + } + + let knownFailure = false; + if (gConfig.timeoutAsPass) { + knownFailure = true; + } + self.currentTest.addResult( + new testResult({ + name: "Test timed out", + allowFailure: knownFailure, + }) + ); + self.currentTest.timedOut = true; + self.currentTest.scope.__waitTimer = null; + self.nextTest(); + }, + gTimeoutSeconds * 1000, + ] + ); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener"]), +}; + +/** + * Represents the result of one test assertion. This is described with a string + * in traditional logging, and has a "status" and "expected" property used in + * structured logging. Normally, results are mapped as follows: + * + * pass: todo: Added to: Described as: Status: Expected: + * true false passCount TEST-PASS PASS PASS + * true true todoCount TEST-KNOWN-FAIL FAIL FAIL + * false false failCount TEST-UNEXPECTED-FAIL FAIL PASS + * false true failCount TEST-UNEXPECTED-PASS PASS FAIL + * + * The "allowFailure" argument indicates that this is one of the assertions that + * should be allowed to fail, for example because "fail-if" is true for the + * current test file in the manifest. In this case, results are mapped this way: + * + * pass: todo: Added to: Described as: Status: Expected: + * true false passCount TEST-PASS PASS PASS + * true true todoCount TEST-KNOWN-FAIL FAIL FAIL + * false false todoCount TEST-KNOWN-FAIL FAIL FAIL + * false true todoCount TEST-KNOWN-FAIL FAIL FAIL + */ +function testResult({ name, pass, todo, ex, stack, allowFailure }) { + this.info = false; + this.name = name; + this.msg = ""; + + if (allowFailure && !pass) { + this.allowedFailure = true; + this.pass = true; + this.todo = false; + } else if (allowFailure && pass) { + this.pass = true; + this.todo = false; + } else { + this.pass = !!pass; + this.todo = todo; + } + + this.expected = this.todo ? "FAIL" : "PASS"; + + if (this.pass) { + this.status = this.expected; + return; + } + + this.status = this.todo ? "PASS" : "FAIL"; + + if (ex) { + if (typeof ex == "object" && "fileName" in ex) { + // we have an exception - print filename and linenumber information + this.msg += "at " + ex.fileName + ":" + ex.lineNumber + " - "; + } + this.msg += String(ex); + } + + if (stack) { + this.msg += "\nStack trace:\n"; + let normalized; + if (stack instanceof Ci.nsIStackFrame) { + let frames = []; + for ( + let frame = stack; + frame; + frame = frame.asyncCaller || frame.caller + ) { + let msg = `${frame.filename}:${frame.name}:${frame.lineNumber}`; + frames.push(frame.asyncCause ? `${frame.asyncCause}*${msg}` : msg); + } + normalized = frames.join("\n"); + } else { + normalized = "" + stack; + } + this.msg += normalized; + } + + if (gConfig.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; + } +} + +function testMessage(msg) { + this.msg = msg || ""; + this.info = true; +} + +// Need to be careful adding properties to this object, since its properties +// cannot conflict with global variables used in tests. +function testScope(aTester, aTest, expected) { + this.__tester = aTester; + + aTest.allowFailure = expected == "fail"; + + var self = this; + this.ok = function test_ok(condition, name) { + if (arguments.length > 2) { + const ex = "Too many arguments passed to ok(condition, name)`."; + self.record(false, name, ex); + } else { + self.record(condition, name); + } + }; + this.record = function test_record(condition, name, ex, stack, expected) { + if (expected == "fail") { + aTest.addResult( + new testResult({ + name, + pass: !condition, + todo: true, + ex, + stack: stack || Components.stack.caller, + allowFailure: aTest.allowFailure, + }) + ); + } else { + aTest.addResult( + new testResult({ + name, + pass: condition, + ex, + stack: stack || Components.stack.caller, + allowFailure: aTest.allowFailure, + }) + ); + } + }; + this.is = function test_is(a, b, name) { + self.record( + Object.is(a, b), + name, + `Got ${self.repr(a)}, expected ${self.repr(b)}`, + false, + Components.stack.caller + ); + }; + this.isfuzzy = function test_isfuzzy(a, b, epsilon, name) { + self.record( + a >= b - epsilon && a <= b + epsilon, + name, + `Got ${self.repr(a)}, expected ${self.repr(b)} epsilon: +/- ${self.repr( + epsilon + )}`, + false, + Components.stack.caller + ); + }; + this.isnot = function test_isnot(a, b, name) { + self.record( + !Object.is(a, b), + name, + `Didn't expect ${self.repr(a)}, but got it`, + false, + Components.stack.caller + ); + }; + this.todo = function test_todo(condition, name, ex, stack) { + aTest.addResult( + new testResult({ + name, + pass: !condition, + todo: true, + ex, + stack: stack || Components.stack.caller, + allowFailure: aTest.allowFailure, + }) + ); + }; + this.todo_is = function test_todo_is(a, b, name) { + self.todo( + Object.is(a, b), + name, + `Got ${self.repr(a)}, expected ${self.repr(b)}`, + Components.stack.caller + ); + }; + this.todo_isnot = function test_todo_isnot(a, b, name) { + self.todo( + !Object.is(a, b), + name, + `Didn't expect ${self.repr(a)}, but got it`, + Components.stack.caller + ); + }; + this.info = function test_info(name) { + aTest.addResult(new testMessage(name)); + }; + 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 (Object.is(o, +0)) { + ostring = "+0"; + } else if (Object.is(o, -0)) { + ostring = "-0"; + } else if (typeof o === "string") { + ostring = JSON.stringify(o); + } else if (Array.isArray(o)) { + ostring = "[" + o.map(val => repr(val)).join(", ") + "]"; + } else { + ostring = String(o); + } + } catch (e) { + return `[${Object.prototype.toString.call(o)}]`; + } + if (typeof o == "function") { + ostring = ostring.replace(/\) \{[^]*/, ") { ... }"); + } + return ostring; + }; + + this.executeSoon = function test_executeSoon(func) { + Services.tm.dispatchToMainThread({ + run() { + func(); + }, + }); + }; + + this.waitForExplicitFinish = function test_waitForExplicitFinish() { + self.__done = false; + }; + + this.waitForFocus = function test_waitForFocus( + callback, + targetWindow, + expectBlankPage + ) { + self.SimpleTest.waitForFocus(callback, targetWindow, expectBlankPage); + }; + + this.waitForClipboard = function test_waitForClipboard( + expected, + setup, + success, + failure, + flavor + ) { + self.SimpleTest.waitForClipboard(expected, setup, success, failure, flavor); + }; + + this.registerCleanupFunction = function test_registerCleanupFunction( + aFunction + ) { + self.__cleanupFunctions.push(aFunction); + }; + + this.requestLongerTimeout = function test_requestLongerTimeout(aFactor) { + self.__timeoutFactor = aFactor; + }; + + this.copyToProfile = function test_copyToProfile(filename) { + self.SimpleTest.copyToProfile(filename); + }; + + this.expectUncaughtException = function test_expectUncaughtException( + aExpecting + ) { + self.SimpleTest.expectUncaughtException(aExpecting); + }; + + this.ignoreAllUncaughtExceptions = function test_ignoreAllUncaughtExceptions( + aIgnoring + ) { + self.SimpleTest.ignoreAllUncaughtExceptions(aIgnoring); + }; + + this.expectAssertions = function test_expectAssertions(aMin, aMax) { + let min = aMin; + let max = aMax; + if (typeof max == "undefined") { + max = min; + } + if ( + typeof min != "number" || + typeof max != "number" || + min < 0 || + max < min + ) { + throw new Error("bad parameter to expectAssertions"); + } + self.__expectedMinAsserts = min; + self.__expectedMaxAsserts = max; + }; + + this.setExpectedFailuresForSelfTest = function test_setExpectedFailuresForSelfTest( + expectedAllowedFailureCount + ) { + aTest.allowFailure = true; + aTest.expectedAllowedFailureCount = expectedAllowedFailureCount; + }; + + this.finish = function test_finish() { + self.__done = true; + if (self.__waitTimer) { + self.executeSoon(function() { + if (self.__done && self.__waitTimer) { + clearTimeout(self.__waitTimer); + self.__waitTimer = null; + self.__tester.nextTest(); + } + }); + } + }; + + this.requestCompleteLog = function test_requestCompleteLog() { + self.__tester.structuredLogger.deactivateBuffering(); + self.registerCleanupFunction(function() { + self.__tester.structuredLogger.activateBuffering(); + }); + }; + + return this; +} + +function decorateTaskFn(fn) { + fn = fn.bind(this); + fn.skip = (val = true) => (fn.__skipMe = val); + fn.only = () => (this.__runOnlyThisTask = fn); + return fn; +} + +testScope.prototype = { + __done: true, + __tasks: null, + __setups: [], + __runOnlyThisTask: null, + __waitTimer: null, + __cleanupFunctions: [], + __timeoutFactor: 1, + __expectedMinAsserts: 0, + __expectedMaxAsserts: 0, + + EventUtils: {}, + AccessibilityUtils: {}, + SimpleTest: {}, + ContentTask: null, + BrowserTestUtils: null, + TestUtils: null, + ExtensionTestUtils: null, + Assert: null, + + /** + * Add a function which returns a promise (usually an async function) + * as a test task. + * + * The task ends when the promise returned by the function resolves or + * rejects. If the test function throws, or the promise it returns + * rejects, the test is reported as a failure. Execution continues + * with the next test function. + * + * Example usage: + * + * add_task(async function test() { + * let result = await Promise.resolve(true); + * + * ok(result); + * + * let secondary = await someFunctionThatReturnsAPromise(result); + * is(secondary, "expected value"); + * }); + * + * add_task(async function test_early_return() { + * let result = await somethingThatReturnsAPromise(); + * + * if (!result) { + * // Test is ended immediately, with success. + * return; + * } + * + * is(result, "foo"); + * }); + */ + add_task(aFunction) { + if (!this.__tasks) { + this.waitForExplicitFinish(); + this.__tasks = []; + } + let bound = decorateTaskFn.call(this, aFunction); + this.__tasks.push(bound); + return bound; + }, + + add_setup(aFunction) { + if (!this.__setups.length) { + this.waitForExplicitFinish(); + } + let bound = aFunction.bind(this); + this.__setups.push(bound); + return bound; + }, + + destroy: function test_destroy() { + for (let prop in this) { + delete this[prop]; + } + }, +}; diff --git a/testing/mochitest/chrome-harness.js b/testing/mochitest/chrome-harness.js new file mode 100644 index 0000000000..d9827fa503 --- /dev/null +++ b/testing/mochitest/chrome-harness.js @@ -0,0 +1,262 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +/* import-globals-from manifestLibrary.js */ + +// Defined in browser-test.js +/* global gTestPath */ + +/* + * getChromeURI converts a URL to a URI + * + * url: string of a URL (http://mochi.test/test.html) + * returns: a nsiURI object representing the given URL + * + */ +function getChromeURI(url) { + return Services.io.newURI(url); +} + +/* + * Convert a URL (string) into a nsIURI or NSIJARURI + * This is intended for URL's that are on a file system + * or in packaged up in an extension .jar file + * + * url: a string of a url on the local system(http://localhost/blah.html) + */ +function getResolvedURI(url) { + var chromeURI = getChromeURI(url); + var resolvedURI = Cc["@mozilla.org/chrome/chrome-registry;1"] + .getService(Ci.nsIChromeRegistry) + .convertChromeURL(chromeURI); + + try { + resolvedURI = resolvedURI.QueryInterface(Ci.nsIJARURI); + } catch (ex) {} // not a jar file + + return resolvedURI; +} + +/** + * getChromeDir is intended to be called after getResolvedURI and convert + * the input URI into a nsIFile (actually the directory containing the + * file). This can be used for copying or referencing the file or extra files + * required by the test. Usually we need to load a secondary html file or library + * and this will give us file system access to that. + * + * resolvedURI: nsIURI (from getResolvedURI) that points to a file:/// url + */ +function getChromeDir(resolvedURI) { + var fileHandler = Cc["@mozilla.org/network/protocol;1?name=file"].getService( + Ci.nsIFileProtocolHandler + ); + var chromeDir = fileHandler.getFileFromURLSpec(resolvedURI.spec); + return chromeDir.parent.QueryInterface(Ci.nsIFile); +} + +// used by tests to determine their directory based off window.location.path +function getRootDirectory(path, chromeURI) { + if (chromeURI === undefined) { + chromeURI = getChromeURI(path); + } + var myURL = chromeURI.QueryInterface(Ci.nsIURL); + var mydir = myURL.directory; + + if (mydir.match("/$") != "/") { + mydir += "/"; + } + + return chromeURI.prePath + mydir; +} + +// used by tests to determine their directory based off window.location.path +function getChromePrePath(path, chromeURI) { + if (chromeURI === undefined) { + chromeURI = getChromeURI(path); + } + + return chromeURI.prePath; +} + +/* + * Given a URI, return nsIJARURI or null + */ +function getJar(uri) { + var resolvedURI = getResolvedURI(uri); + var jar = null; + try { + if (resolvedURI.JARFile) { + jar = resolvedURI; + } + } catch (ex) {} + return jar; +} + +/* + * input: + * jar: a nsIJARURI object with the jarfile and jarentry (path in jar file) + * + * output; + * all files and subdirectories inside jarentry will be extracted to TmpD/mochikit.tmp + * we will return the location of /TmpD/mochikit.tmp* so you can reference the files locally + */ +function extractJarToTmp(jar) { + var tmpdir = Services.dirsvc.get("ProfD", Ci.nsIFile); + tmpdir.append("mochikit.tmp"); + // parseInt is used because octal escape sequences cause deprecation warnings + // in strict mode (which is turned on in debug builds) + tmpdir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0777", 8)); + + var zReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + + var fileHandler = Cc["@mozilla.org/network/protocol;1?name=file"].getService( + Ci.nsIFileProtocolHandler + ); + + var fileName = fileHandler.getFileFromURLSpec(jar.JARFile.spec); + zReader.open(fileName); + + // filepath represents the path in the jar file without the filename + var filepath = ""; + var parts = jar.JAREntry.split("/"); + for (var i = 0; i < parts.length - 1; i++) { + if (parts[i] != "") { + filepath += parts[i] + "/"; + } + } + + /* Create dir structure first, no guarantee about ordering of directories and + * files returned from findEntries. + */ + for (let dir of zReader.findEntries(filepath + "*/")) { + var targetDir = buildRelativePath(dir, tmpdir, filepath); + // parseInt is used because octal escape sequences cause deprecation warnings + // in strict mode (which is turned on in debug builds) + if (!targetDir.exists()) { + targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0777", 8)); + } + } + + // now do the files + for (var fname of zReader.findEntries(filepath + "*")) { + if (fname.substr(-1) != "/") { + var targetFile = buildRelativePath(fname, tmpdir, filepath); + zReader.extract(fname, targetFile); + } + } + return tmpdir; +} + +/* + * Take a relative path from the current mochitest file + * and returns the absolute path for the given test data file. + */ +function getTestFilePath(path) { + if (path[0] == "/") { + throw new Error("getTestFilePath only accepts relative path"); + } + // Get the chrome/jar uri for the current mochitest file + // gTestPath being defined by the test harness in browser-chrome tests + // or window is being used for mochitest-browser + var baseURI = typeof gTestPath == "string" ? gTestPath : window.location.href; + var parentURI = getResolvedURI(getRootDirectory(baseURI)); + var file; + if (parentURI.JARFile) { + // If it's a jar/zip, we have to extract it first + file = extractJarToTmp(parentURI); + } else { + // Otherwise, we can directly cast it to a file URI + var fileHandler = Cc[ + "@mozilla.org/network/protocol;1?name=file" + ].getService(Ci.nsIFileProtocolHandler); + file = fileHandler.getFileFromURLSpec(parentURI.spec); + } + // Then walk by the given relative path + path.split("/").forEach(function(p) { + if (p == "..") { + file = file.parent; + } else if (p != ".") { + file.append(p); + } + }); + return file.path; +} + +/* + * Simple utility function to take the directory structure in jarentryname and + * translate that to a path of a nsIFile. + */ +function buildRelativePath(jarentryname, destdir, basepath) { + var baseParts = basepath.split("/"); + if (baseParts[baseParts.length - 1] == "") { + baseParts.pop(); + } + + var parts = jarentryname.split("/"); + + var targetFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + targetFile.initWithFile(destdir); + + for (var i = baseParts.length; i < parts.length; i++) { + targetFile.append(parts[i]); + } + + return targetFile; +} + +function readConfig(filename) { + filename = filename || "testConfig.js"; + + var configFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + configFile.append(filename); + + if (!configFile.exists()) { + return {}; + } + + var fileInStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileInStream.init(configFile, -1, 0, 0); + + var str = NetUtil.readInputStreamToString( + fileInStream, + fileInStream.available() + ); + fileInStream.close(); + return JSON.parse(str); +} + +function getTestList(params, callback) { + var baseurl = "chrome://mochitests/content"; + if (window.parseQueryString) { + params = window.parseQueryString(location.search.substring(1), true); + } + if (!params.baseurl) { + params.baseurl = baseurl; + } + + var config = readConfig(); + for (var p in params) { + if (params[p] == 1) { + config[p] = true; + } else if (params[p] == 0) { + config[p] = false; + } else { + config[p] = params[p]; + } + } + params = config; + getTestManifest( + "http://mochi.test:8888/" + params.manifestFile, + params, + callback + ); +} diff --git a/testing/mochitest/chrome/chrome.ini b/testing/mochitest/chrome/chrome.ini new file mode 100644 index 0000000000..54fe522226 --- /dev/null +++ b/testing/mochitest/chrome/chrome.ini @@ -0,0 +1,16 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = test-dir/test-file + +[test_sample.xhtml] +[test_sanityEventUtils.xhtml] +fail-if = headless # Bug 1678303 +[test_sanityException.xhtml] +[test_sanityException2.xhtml] +[test_sanityManifest.xhtml] +fail-if = true +[test_sanityManifest_pf.xhtml] +fail-if = true +[test_chromeGetTestFile.xhtml] +[test_tasks_skip.xhtml] +[test_tasks_skipall.xhtml] diff --git a/testing/mochitest/chrome/test-dir/test-file b/testing/mochitest/chrome/test-dir/test-file new file mode 100644 index 0000000000..257cc5642c --- /dev/null +++ b/testing/mochitest/chrome/test-dir/test-file @@ -0,0 +1 @@ +foo diff --git a/testing/mochitest/chrome/test_chromeGetTestFile.xhtml b/testing/mochitest/chrome/test_chromeGetTestFile.xhtml new file mode 100644 index 0000000000..9bc0a74f3b --- /dev/null +++ b/testing/mochitest/chrome/test_chromeGetTestFile.xhtml @@ -0,0 +1,47 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<window title="Test chrome harness functions" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/chrome-harness.js"></script> + <script type="application/javascript"> + <![CDATA[ + add_task(async function test() { + SimpleTest.doesThrow(function () { + getTestFilePath("/test_chromeGetTestFile.xhtml") + }, "getTestFilePath rejects absolute paths"); + + await Promise.all([ + IOUtils.exists(getTestFilePath("test_chromeGetTestFile.xhtml")) + .then(function (exists) { + ok(exists, "getTestFilePath consider the path as being relative"); + }), + + IOUtils.exists(getTestFilePath("./test_chromeGetTestFile.xhtml")) + .then(function (exists) { + ok(exists, "getTestFilePath also accepts explicit relative path"); + }), + + IOUtils.exists(getTestFilePath("./test_chromeGetTestFileTypo.xhtml")) + .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"); + }) + ]); + }); + ]]> + </script> +</window> diff --git a/testing/mochitest/chrome/test_sample.xhtml b/testing/mochitest/chrome/test_sample.xhtml new file mode 100644 index 0000000000..041a910aa9 --- /dev/null +++ b/testing/mochitest/chrome/test_sample.xhtml @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=8675309 +--> +<window title="Mozilla Bug 8675309" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=8675309">Mozilla Bug 8675309</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 8675309 **/ + +ok(true, "sanity check"); + + + +]]> +</script> + +</window> diff --git a/testing/mochitest/chrome/test_sanityEventUtils.xhtml b/testing/mochitest/chrome/test_sanityEventUtils.xhtml new file mode 100644 index 0000000000..5eb2572d19 --- /dev/null +++ b/testing/mochitest/chrome/test_sanityEventUtils.xhtml @@ -0,0 +1,171 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<window title="Test EventUtils functions" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="text/javascript"> + var start = new Date(); + </script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript"> + var loadTime = new Date(); + </script> + <script type="application/javascript"> + <![CDATA[ + info("\nProfile::EventUtilsLoadTime: " + (loadTime - start) + "\n"); + var testFile = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + var regularDtForDrag1 = null; + var gSetDropEffect = true; + var gData; + var gEnter = false; + var gOver = false; + var dragDrop = [[ + { type : "text/plain", + data : "This is a test" } + ]]; + // this is the expected data arrays + // for testing drag of 2 items create 2 inner arrays + var drag1 = [[ + { type : "text/uri-list", + data : "http://www.mozilla.org/" } + ]]; + var drag2items = [[ + { type : "text/uri-list", + data : "http://www.mozilla.org/" } + ],[ + { type : "text/uri-list", + data : "http://www.mozilla.org/" } + ]]; + var drag1WrongFlavor = [[ + { type : "text/plain", + data : "this is text/plain" } + ]]; + var drag2 = [[ + { type : "text/plain", + data : "this is text/plain" }, + { type : "text/uri-list", + data : "http://www.mozilla.org/" } + ]]; + var drag2WrongOrder = [[ + { type : "text/uri-list", + data : "http://www.mozilla.org/" }, + { type : "text/plain", + data : "this is text/plain" } + ]]; + var dragfile = [[ + { type : "application/x-moz-file", + data : testFile, + eqTest(actualData, expectedData) {return expectedData.equals(actualData);} }, + { type : "Files", + data : null } + ]]; + + function doOnDrop(aEvent) { + gData = aEvent.dataTransfer.getData(dragDrop[0][0].type); + aEvent.preventDefault(); // cancels event and keeps dropEffect + // as was before event. + } + + function doOnDragStart(aEvent) { + var dt = aEvent.dataTransfer; + switch (aEvent.currentTarget.id) { + case "drag2" : + dt.setData("text/plain", "this is text/plain"); + // fallthrough + case "drag1" : + regularDtForDrag1 = dt; + dt.setData("text/uri-list", "http://www.mozilla.org/"); + break; + case "dragfile" : + dt.mozSetDataAt("application/x-moz-file", testFile, 0); + break; + } + dt.effectAllowed = "all"; + } + + function doOnDragEnter(aEvent) { + gEnter = true; + aEvent.dataTransfer.effectAllowed = "all"; + aEvent.preventDefault(); // sets target this element + } + + function doOnDragOver(aEvent) { + gOver = true; + if (gSetDropEffect) + aEvent.dataTransfer.dropEffect = "copy"; + aEvent.preventDefault(); + } + + SimpleTest.waitForExplicitFinish(); + function test() { + var startTime = new Date(); + var result; + + /* test synthesizeDrop */ + result = synthesizeDrop($("dragDrop"), $("dragDrop"), dragDrop, null, window); + ok(gEnter, "Fired dragenter"); + ok(gOver, "Fired dragover"); + is(result, "copy", "copy is dropEffect"); + is(gData, dragDrop[0][0].data, "Received valid drop data"); + + gSetDropEffect = false; + result = synthesizeDrop($("dragDrop"), $("dragDrop"), dragDrop, "link", window); + is(result, "link", "link is dropEffect"); + gSetDropEffect = true; + + $("textB").focus(); + var content = synthesizeQueryTextContent(0, 100); + ok(content, "synthesizeQueryTextContent should not be null"); + ok(content.succeeded, "synthesizeQueryTextContent should succeed"); + is(content.text, "I haz a content", "synthesizeQueryTextContent should be 'I haz a content': " + content.text); + + content = synthesizeQueryCaretRect(0); + ok(content, "synthesizeQueryCaretRect should not be null"); + ok(content.succeeded, "synthesizeQueryCaretRect should succeed"); + + content = synthesizeQueryTextRect(0, 100); + ok(content, "synthesizeQueryTextRect should not be null"); + ok(content.succeeded, "synthesizeQueryTextRect should succeed"); + + content = synthesizeQueryEditorRect(); + ok(content, "synthesizeQueryEditorRect should not be null"); + ok(content.succeeded, "synthesizeQueryEditorRect should succeed"); + + content = synthesizeCharAtPoint(0, 0); + ok(content, "synthesizeCharAtPoint should not be null"); + ok(content.succeeded, "synthesizeCharAtPoint should succeed"); + + content = synthesizeSelectionSet(0, 100); + ok(content, "synthesizeSelectionSet should not be null"); + is(content, true, "synthesizeSelectionSet should succeed"); + + var endTime = new Date(); + info("\nProfile::EventUtilsRunTime: " + (endTime-startTime) + "\n"); + SimpleTest.finish(); + }; + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml" onload="setTimeout(test, 0)"> + <input id="textB" value="I haz a content"/> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + <div id="drag1" ondragstart="doOnDragStart(event);">Need some space here</div> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + <div id="dragDrop" ondragover ="doOnDragOver(event);" + ondragenter ="doOnDragEnter(event);" + ondragleave ="doOnDragLeave(event);" + ondrop ="doOnDrop(event);"> + Need some depth and height to drag here + </div> + <div id="drag2" ondragstart="doOnDragStart(event);">Need more space</div> + <div id="dragfile" ondragstart="doOnDragStart(event);">Sure why not here too</div> + </body> +</window> diff --git a/testing/mochitest/chrome/test_sanityException.xhtml b/testing/mochitest/chrome/test_sanityException.xhtml new file mode 100644 index 0000000000..198e21a60c --- /dev/null +++ b/testing/mochitest/chrome/test_sanityException.xhtml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=670817 +--> +<window title="Mozilla Bug 670817" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=670817">Mozilla Bug 670817</a> +<script type="application/javascript"><![CDATA[ + +SimpleTest.expectUncaughtException(); +ok(true, "a call to ok"); +// eslint-disable-next-line no-throw-literal +throw "this is a deliberately thrown exception"; + +]]></script> +</body> + +</window> diff --git a/testing/mochitest/chrome/test_sanityException2.xhtml b/testing/mochitest/chrome/test_sanityException2.xhtml new file mode 100644 index 0000000000..78d97032aa --- /dev/null +++ b/testing/mochitest/chrome/test_sanityException2.xhtml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=670817 +--> +<window title="Mozilla Bug 670817" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=670817">Mozilla Bug 670817</a> +<script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); +ok(true, "a call to ok"); +SimpleTest.executeSoon(function() { + SimpleTest.expectUncaughtException(); + // eslint-disable-next-line no-throw-literal + throw "this is a deliberately thrown exception"; +}); +SimpleTest.executeSoon(function() { + SimpleTest.finish(); +}); + +]]></script> +</body> + +</window> diff --git a/testing/mochitest/chrome/test_sanityManifest.xhtml b/testing/mochitest/chrome/test_sanityManifest.xhtml new file mode 100644 index 0000000000..14a1ef543e --- /dev/null +++ b/testing/mochitest/chrome/test_sanityManifest.xhtml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=670817 +--> +<window title="Mozilla Bug 987849" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=987849">Mozilla Bug 987849</a> +<script type="application/javascript"><![CDATA[ +ok(false, "a call to ok"); +]]></script> +</body> + +</window> diff --git a/testing/mochitest/chrome/test_sanityManifest_pf.xhtml b/testing/mochitest/chrome/test_sanityManifest_pf.xhtml new file mode 100644 index 0000000000..0f8c5e0a48 --- /dev/null +++ b/testing/mochitest/chrome/test_sanityManifest_pf.xhtml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=670817 +--> +<window title="Mozilla Bug 987849" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=987849">Mozilla Bug 987849</a> +<script type="application/javascript"><![CDATA[ +ok(true, "a true call to ok"); +ok(false, "a false call to ok"); +]]></script> +</body> + +</window> diff --git a/testing/mochitest/chrome/test_tasks_skip.xhtml b/testing/mochitest/chrome/test_tasks_skip.xhtml new file mode 100644 index 0000000000..2d321356af --- /dev/null +++ b/testing/mochitest/chrome/test_tasks_skip.xhtml @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<window title="Test add_task.skip() function" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript"> + <![CDATA[ + + // Check that we can skip arbitrary tasks by calling `skip()`. + + add_task(async function skipMeNot1() { + ok(true, "Well well well."); + }); + + add_task(async function skipMe1() { + ok(false, "Not skipped after all."); + }).skip(); + + add_task(async function skipMeNot2() { + ok(true, "Well well well."); + }); + + add_task(async function skipMeNot3() { + ok(true, "Well well well."); + }); + + add_task(async function skipMe2() { + ok(false, "Not skipped after all."); + }).skip(); + + ]]> + </script> + <body xmlns="http://www.w3.org/1999/xhtml" > + </body> +</window> diff --git a/testing/mochitest/chrome/test_tasks_skipall.xhtml b/testing/mochitest/chrome/test_tasks_skipall.xhtml new file mode 100644 index 0000000000..40a2fe1a58 --- /dev/null +++ b/testing/mochitest/chrome/test_tasks_skipall.xhtml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<window title="Test add_task.only() function" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript"> + <![CDATA[ + + /* eslint-disable mozilla/reject-addtask-only */ + // Check that we can skip all but one task by calling `only()`. + + add_task(async function skipMe1() { + ok(false, "Not skipped after all."); + }); + + add_task(async function skipMe2() { + ok(false, "Not skipped after all."); + }).skip(); + + add_task(async function skipMe3() { + ok(false, "Not skipped after all."); + }).only(); + + add_task(async function skipMeNot() { + ok(true, "Well well well."); + }).only(); + + add_task(async function skipMe4() { + ok(false, "Not skipped after all."); + }); + + ]]> + </script> + <body xmlns="http://www.w3.org/1999/xhtml" > + </body> +</window> diff --git a/testing/mochitest/chunkifyTests.js b/testing/mochitest/chunkifyTests.js new file mode 100644 index 0000000000..21f7708b73 --- /dev/null +++ b/testing/mochitest/chunkifyTests.js @@ -0,0 +1,27 @@ +/* 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/. */ + +function skipTests(tests, startTestPattern, endTestPattern) { + var startIndex = 0, + endIndex = tests.length - 1; + for (var i = 0; i < tests.length; ++i) { + var test_path; + if (tests[i] instanceof Object && "test" in tests[i]) { + test_path = tests[i].test.url; + } else if (tests[i] instanceof Object && "url" in tests[i]) { + test_path = tests[i].url; + } else { + test_path = tests[i]; + } + if (startTestPattern && test_path.endsWith(startTestPattern)) { + startIndex = i; + } + + if (endTestPattern && test_path.endsWith(endTestPattern)) { + endIndex = i; + } + } + + return tests.slice(startIndex, endIndex + 1); +} diff --git a/testing/mochitest/document-builder.sjs b/testing/mochitest/document-builder.sjs new file mode 100644 index 0000000000..b8c9ef21e7 --- /dev/null +++ b/testing/mochitest/document-builder.sjs @@ -0,0 +1,97 @@ +/* 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"; + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +Cu.importGlobalProperties(["URLSearchParams"]); + +function loadHTMLFromFile(path) { + // Load the HTML to return in the response from file. + // Since it's relative to the cwd of the test runner, we start there and + // append to get to the actual path of the file. + const testHTMLFile = + // eslint-disable-next-line mozilla/use-services + Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("CurWorkD", Ci.nsIFile); + const dirs = path.split("/"); + for (let i = 0; i < dirs.length; i++) { + testHTMLFile.append(dirs[i]); + } + + const testHTMLFileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + testHTMLFileStream.init(testHTMLFile, -1, 0, 0); + const testHTML = NetUtil.readInputStreamToString( + testHTMLFileStream, + testHTMLFileStream.available() + ); + + return testHTML; +} + +/** + * document-builder.sjs can be used to dynamically build documents that will be used in + * mochitests. It does handle the following GET parameters: + * - file: The path to an (X)HTML file whose content will be used as a response. + * Example: document-builder.sjs?file=/tests/dom/security/test/csp/file_web_manifest_mixed_content.html + * - html: A string representation of the HTML document you want to get. + * Example: document-builder.sjs?html=<h1>Hello</h1> + * - headers: A <key:value> string representation of headers that will be set on the response + * This is only applied when the html GET parameter is passed as well + * Example: document-builder.sjs?headers=Cross-Origin-Opener-Policy:same-origin&html=<h1>Hello</h1> + * document-builder.sjs?headers=X-Header1:a&headers=X-Header2:b&html=<h1>Multiple headers</h1> + * - delay: Delay the response by X millisecond. + */ +async function handleRequest(request, response) { + response.processAsync(); + + const queryString = new URLSearchParams(request.queryString); + const html = queryString.get("html"); + const delay = queryString.get("delay"); + + if (delay) { + await new Promise(resolve => { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback( + () => { + // to avoid garbage collection + timer = null; + resolve(); + }, + delay, + Ci.nsITimer.TYPE_ONE_SHOT + ); + }); + } + + response.setHeader("Cache-Control", "no-cache", false); + if (html) { + response.setHeader("Content-Type", "text/html", false); + + if (queryString.has("headers")) { + for (const header of queryString.getAll("headers")) { + const [key, value] = header.split(":"); + response.setHeader(key, value, false); + } + } + + response.write(html); + } else { + const path = queryString.get("file"); + const doc = loadHTMLFromFile(path); + response.setHeader( + "Content-Type", + path.endsWith(".xhtml") ? "application/xhtml+xml" : "text/html", + false + ); + // This is a hack to set the correct id for the content document that is to be + // loaded in the iframe. + response.write(doc.replace(`id="body"`, `id="default-iframe-body-id"`)); + } + + response.finish(); +} diff --git a/testing/mochitest/dynamic/getMyDirectory.sjs b/testing/mochitest/dynamic/getMyDirectory.sjs new file mode 100644 index 0000000000..a51564b4d9 --- /dev/null +++ b/testing/mochitest/dynamic/getMyDirectory.sjs @@ -0,0 +1,13 @@ +function handleRequest(request, response) { + var file; + getObjectState("SERVER_ROOT", function(serverRoot) { + var ref = request.getHeader("Referer").split("?")[0]; + // 8 is "https://".length which is the longest string before the host. + var pathStart = ref.indexOf("/", 8) + 1; + var pathEnd = ref.lastIndexOf("/") + 1; + file = serverRoot.getFile(ref.substring(pathStart, pathEnd) + "x"); + }); + + response.setHeader("Content-Type", "text/plain", false); + response.write(file.path.substr(0, file.path.length - 1)); +} diff --git a/testing/mochitest/embed/Xm5i5kbIXzc b/testing/mochitest/embed/Xm5i5kbIXzc new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/mochitest/embed/Xm5i5kbIXzc diff --git a/testing/mochitest/embed/Xm5i5kbIXzc^headers^ b/testing/mochitest/embed/Xm5i5kbIXzc^headers^ new file mode 100644 index 0000000000..04fbaa08fe --- /dev/null +++ b/testing/mochitest/embed/Xm5i5kbIXzc^headers^ @@ -0,0 +1,2 @@ +HTTP 200 OK +Content-Type: text/html diff --git a/testing/mochitest/favicon.ico b/testing/mochitest/favicon.ico Binary files differnew file mode 100644 index 0000000000..d44438903b --- /dev/null +++ b/testing/mochitest/favicon.ico diff --git a/testing/mochitest/harness.xhtml b/testing/mochitest/harness.xhtml new file mode 100644 index 0000000000..e5fec05fe8 --- /dev/null +++ b/testing/mochitest/harness.xhtml @@ -0,0 +1,100 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/static/harness.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Chrome Test Harness" + directory="chrome"> + + <script src="chrome://mochikit/content/tests/SimpleTest/LogController.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/MemoryStats.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/TestRunner.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/MozillaLogger.js"/> + <script src="chrome://mochikit/content/chrome-harness.js" /> + <script src="chrome://mochikit/content/chunkifyTests.js" /> + <script src="chrome://mochikit/content/manifestLibrary.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/setup.js" /> + <script type="application/javascript"><![CDATA[ + +function loadTests() +{ + window.removeEventListener("load", loadTests); + getTestList({}, linkAndHookup); +} + +function linkAndHookup(links) { + // load server.js in so we can share template functions + var srvScope = {}; + Services.scriptloader.loadSubScript('chrome://mochikit/content/server.js', + srvScope); + + // generate our test list + srvScope.makeTags(); + var tableContent = srvScope.linksToTableRows(links, 0); + + function populate() { + // eslint-disable-next-line no-unsanitized/property + document.getElementById("test-table").innerHTML += tableContent; + } + gTestList = JSON.parse(srvScope.jsonArrayOfTestFiles(links)); + populate(); + + hookup(); +} + + window.addEventListener("load", loadTests); + ]]> + </script> + + <vbox> + <button label="Run Chrome Tests" id="runtests" flex="1"/> + + <body xmlns="http://www.w3.org/1999/xhtml" id="xulharness"> + <!--TODO: this should be separated into a file that both this file and server.js uses--> + <div class="container"> + <p style="float:right;"> + <small>Based on the MochiKit unit tests.</small> + </p> + <div class="status"> + <h1 id="indicator">Status</h1> + <h2 id="pass">Passed: <span id="pass-count">0</span></h2> + <h2 id="fail">Failed: <span id="fail-count">0</span></h2> + <h2 id="fail">Todo: <span id="todo-count">0</span></h2> + </div> + <div class="clear"></div> + <div id="current-test"> + <b>Currently Executing: <span id="current-test-path">_</span></b> + </div> + <div class="clear"></div> + <div class="frameholder"> + <iframe type="content" id="testframe" width="550" height="350"></iframe> + </div> + <div class="clear"></div> + <div class="toggle"> + <a href="#" id="toggleNonTests">Show Non-Tests</a> + <br /> + </div> + <div id="wrapper"> + <table cellpadding="0" cellspacing="0"> + <!-- tbody needed to avoid bug 494546 causing performance problems --> + <tbody id="test-table"> + <tr> + <td>Passed</td> + <td>Failed</td> + <td>Todo</td> + <td>Test Files</td> + </tr> + </tbody> + </table> + <br/> + <table cellpadding="0" cellspacing="0" border="1" bordercolor="red"> + <!-- tbody needed to avoid bug 494546 causing performance problems --> + <tbody id="fail-table"> + </tbody> + </table> + </div> + </div> + </body> + </vbox> +</window> diff --git a/testing/mochitest/leaks.py b/testing/mochitest/leaks.py new file mode 100644 index 0000000000..b3d531bd11 --- /dev/null +++ b/testing/mochitest/leaks.py @@ -0,0 +1,435 @@ +# 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/ + +# The content of this file comes orginally from automationutils.py +# and *should* be revised. + +import re +from operator import itemgetter + +RE_DOCSHELL = re.compile("I\/DocShellAndDOMWindowLeak ([+\-]{2})DOCSHELL") +RE_DOMWINDOW = re.compile("I\/DocShellAndDOMWindowLeak ([+\-]{2})DOMWINDOW") + + +class ShutdownLeaks(object): + + """ + Parses the mochitest run log when running a debug build, assigns all leaked + DOM windows (that are still around after test suite shutdown, despite running + the GC) to the tests that created them and prints leak statistics. + """ + + def __init__(self, logger): + self.logger = logger + self.tests = [] + self.leakedWindows = {} + self.hiddenWindowsCount = 0 + self.leakedDocShells = set() + self.hiddenDocShellsCount = 0 + self.numDocShellCreatedLogsSeen = 0 + self.numDocShellDestroyedLogsSeen = 0 + self.numDomWindowCreatedLogsSeen = 0 + self.numDomWindowDestroyedLogsSeen = 0 + self.currentTest = None + self.seenShutdown = set() + + def log(self, message): + action = message["action"] + + # Remove 'log' when clipboard is gone and/or structured. + if action in ("log", "process_output"): + line = message["message"] if action == "log" else message["data"] + + m = RE_DOMWINDOW.search(line) + if m: + self._logWindow(line, m.group(1) == "++") + return + + m = RE_DOCSHELL.search(line) + if m: + self._logDocShell(line, m.group(1) == "++") + return + + if line.startswith("Completed ShutdownLeaks collections in process"): + pid = int(line.split()[-1]) + self.seenShutdown.add(pid) + elif action == "test_start": + fileName = message["test"].replace( + "chrome://mochitests/content/browser/", "" + ) + self.currentTest = { + "fileName": fileName, + "windows": set(), + "docShells": set(), + } + elif action == "test_end": + # don't track a test if no windows or docShells leaked + if self.currentTest and ( + self.currentTest["windows"] or self.currentTest["docShells"] + ): + self.tests.append(self.currentTest) + self.currentTest = None + + def process(self): + failures = 0 + + if not self.seenShutdown: + self.logger.error( + "TEST-UNEXPECTED-FAIL | ShutdownLeaks | process() called before end of test suite" + ) + failures += 1 + + if ( + self.numDocShellCreatedLogsSeen == 0 + or self.numDocShellDestroyedLogsSeen == 0 + ): + self.logger.error( + "TEST-UNEXPECTED-FAIL | did not see DOCSHELL log strings." + " this occurs if the DOCSHELL logging gets disabled by" + " something. %d created seen %d destroyed seen" + % (self.numDocShellCreatedLogsSeen, self.numDocShellDestroyedLogsSeen) + ) + failures += 1 + else: + self.logger.info( + "TEST-INFO | Confirming we saw %d DOCSHELL created and %d destroyed log" + " strings." + % (self.numDocShellCreatedLogsSeen, self.numDocShellDestroyedLogsSeen) + ) + + if ( + self.numDomWindowCreatedLogsSeen == 0 + or self.numDomWindowDestroyedLogsSeen == 0 + ): + self.logger.error( + "TEST-UNEXPECTED-FAIL | did not see DOMWINDOW log strings." + " this occurs if the DOMWINDOW logging gets disabled by" + " something%d created seen %d destroyed seen" + % (self.numDomWindowCreatedLogsSeen, self.numDomWindowDestroyedLogsSeen) + ) + failures += 1 + else: + self.logger.info( + "TEST-INFO | Confirming we saw %d DOMWINDOW created and %d destroyed log" + " strings." + % (self.numDomWindowCreatedLogsSeen, self.numDomWindowDestroyedLogsSeen) + ) + + for test in self._parseLeakingTests(): + for url, count in self._zipLeakedWindows(test["leakedWindows"]): + self.logger.error( + "TEST-UNEXPECTED-FAIL | %s | leaked %d window(s) until shutdown " + "[url = %s]" % (test["fileName"], count, url) + ) + failures += 1 + + if test["leakedWindowsString"]: + self.logger.info( + "TEST-INFO | %s | windows(s) leaked: %s" + % (test["fileName"], test["leakedWindowsString"]) + ) + + if test["leakedDocShells"]: + self.logger.error( + "TEST-UNEXPECTED-FAIL | %s | leaked %d docShell(s) until " + "shutdown" % (test["fileName"], len(test["leakedDocShells"])) + ) + failures += 1 + self.logger.info( + "TEST-INFO | %s | docShell(s) leaked: %s" + % ( + test["fileName"], + ", ".join( + [ + "[pid = %s] [id = %s]" % x + for x in test["leakedDocShells"] + ] + ), + ) + ) + + if test["hiddenWindowsCount"] > 0: + # Note: to figure out how many hidden windows were created, we divide + # this number by 2, because 1 hidden window creation implies in + # 1 outer window + 1 inner window. + # pylint --py3k W1619 + self.logger.info( + "TEST-INFO | %s | This test created %d hidden window(s)" + % (test["fileName"], test["hiddenWindowsCount"] / 2) + ) + + if test["hiddenDocShellsCount"] > 0: + self.logger.info( + "TEST-INFO | %s | This test created %d hidden docshell(s)" + % (test["fileName"], test["hiddenDocShellsCount"]) + ) + + return failures + + def _logWindow(self, line, created): + pid = self._parseValue(line, "pid") + serial = self._parseValue(line, "serial") + self.numDomWindowCreatedLogsSeen += 1 if created else 0 + self.numDomWindowDestroyedLogsSeen += 0 if created else 1 + + # log line has invalid format + if not pid or not serial: + self.logger.error( + "TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse line" + ) + self.logger.error("TEST-INFO | ShutdownLeaks | Unparsable line <%s>" % line) + return + + key = (pid, serial) + + if self.currentTest: + windows = self.currentTest["windows"] + if created: + windows.add(key) + else: + windows.discard(key) + elif int(pid) in self.seenShutdown and not created: + url = self._parseValue(line, "url") + if not self._isHiddenWindowURL(url): + self.leakedWindows[key] = url + else: + self.hiddenWindowsCount += 1 + + def _logDocShell(self, line, created): + pid = self._parseValue(line, "pid") + id = self._parseValue(line, "id") + self.numDocShellCreatedLogsSeen += 1 if created else 0 + self.numDocShellDestroyedLogsSeen += 0 if created else 1 + + # log line has invalid format + if not pid or not id: + self.logger.error( + "TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse line" + ) + self.logger.error("TEST-INFO | ShutdownLeaks | Unparsable line <%s>" % line) + return + + key = (pid, id) + + if self.currentTest: + docShells = self.currentTest["docShells"] + if created: + docShells.add(key) + else: + docShells.discard(key) + elif int(pid) in self.seenShutdown and not created: + url = self._parseValue(line, "url") + if not self._isHiddenWindowURL(url): + self.leakedDocShells.add(key) + else: + self.hiddenDocShellsCount += 1 + + def _parseValue(self, line, name): + match = re.search("\[%s = (.+?)\]" % name, line) + if match: + return match.group(1) + return None + + def _parseLeakingTests(self): + leakingTests = [] + + for test in self.tests: + leakedWindows = [id for id in test["windows"] if id in self.leakedWindows] + test["leakedWindows"] = [self.leakedWindows[id] for id in leakedWindows] + test["hiddenWindowsCount"] = self.hiddenWindowsCount + test["leakedWindowsString"] = ", ".join( + ["[pid = %s] [serial = %s]" % x for x in leakedWindows] + ) + test["leakedDocShells"] = [ + id for id in test["docShells"] if id in self.leakedDocShells + ] + test["hiddenDocShellsCount"] = self.hiddenDocShellsCount + test["leakCount"] = len(test["leakedWindows"]) + len( + test["leakedDocShells"] + ) + + if ( + test["leakCount"] + or test["hiddenWindowsCount"] + or test["hiddenDocShellsCount"] + ): + leakingTests.append(test) + + return sorted(leakingTests, key=itemgetter("leakCount"), reverse=True) + + def _zipLeakedWindows(self, leakedWindows): + counts = [] + counted = set() + + for url in leakedWindows: + if url not in counted: + counts.append((url, leakedWindows.count(url))) + counted.add(url) + + return sorted(counts, key=itemgetter(1), reverse=True) + + def _isHiddenWindowURL(self, url): + return ( + url == "resource://gre-resources/hiddenWindow.html" + or url == "chrome://browser/content/hiddenWindowMac.xhtml" # Win / Linux + ) # Mac + + +class LSANLeaks(object): + + """ + Parses the log when running an LSAN build, looking for interesting stack frames + in allocation stacks, and prints out reports. + """ + + def __init__(self, logger): + self.logger = logger + self.inReport = False + self.fatalError = False + self.symbolizerError = False + self.foundFrames = set([]) + self.recordMoreFrames = None + self.currStack = None + self.maxNumRecordedFrames = 4 + + # Don't various allocation-related stack frames, as they do not help much to + # distinguish different leaks. + unescapedSkipList = [ + "malloc", + "js_malloc", + "js_arena_malloc", + "malloc_", + "__interceptor_malloc", + "moz_xmalloc", + "calloc", + "js_calloc", + "js_arena_calloc", + "calloc_", + "__interceptor_calloc", + "moz_xcalloc", + "realloc", + "js_realloc", + "js_arena_realloc", + "realloc_", + "__interceptor_realloc", + "moz_xrealloc", + "new", + "js::MallocProvider", + ] + self.skipListRegExp = re.compile( + "^" + "|".join([re.escape(f) for f in unescapedSkipList]) + "$" + ) + + self.startRegExp = re.compile( + "==\d+==ERROR: LeakSanitizer: detected memory leaks" + ) + self.fatalErrorRegExp = re.compile( + "==\d+==LeakSanitizer has encountered a fatal error." + ) + self.symbolizerOomRegExp = re.compile( + "LLVMSymbolizer: error reading file: Cannot allocate memory" + ) + self.stackFrameRegExp = re.compile(" #\d+ 0x[0-9a-f]+ in ([^(</]+)") + self.sysLibStackFrameRegExp = re.compile( + " #\d+ 0x[0-9a-f]+ \(([^+]+)\+0x[0-9a-f]+\)" + ) + + def log(self, line): + if re.match(self.startRegExp, line): + self.inReport = True + return + + if re.match(self.fatalErrorRegExp, line): + self.fatalError = True + return + + if re.match(self.symbolizerOomRegExp, line): + self.symbolizerError = True + return + + if not self.inReport: + return + + if line.startswith("Direct leak") or line.startswith("Indirect leak"): + self._finishStack() + self.recordMoreFrames = True + self.currStack = [] + return + + if line.startswith("SUMMARY: AddressSanitizer"): + self._finishStack() + self.inReport = False + return + + if not self.recordMoreFrames: + return + + stackFrame = re.match(self.stackFrameRegExp, line) + if stackFrame: + # Split the frame to remove any return types. + frame = stackFrame.group(1).split()[-1] + if not re.match(self.skipListRegExp, frame): + self._recordFrame(frame) + return + + sysLibStackFrame = re.match(self.sysLibStackFrameRegExp, line) + if sysLibStackFrame: + # System library stack frames will never match the skip list, + # so don't bother checking if they do. + self._recordFrame(sysLibStackFrame.group(1)) + + # If we don't match either of these, just ignore the frame. + # We'll end up with "unknown stack" if everything is ignored. + + def process(self): + failures = 0 + + if self.fatalError: + self.logger.error( + "TEST-UNEXPECTED-FAIL | LeakSanitizer | LeakSanitizer " + "has encountered a fatal error." + ) + failures += 1 + + if self.symbolizerError: + self.logger.error( + "TEST-UNEXPECTED-FAIL | LeakSanitizer | LLVMSymbolizer " + "was unable to allocate memory." + ) + failures += 1 + self.logger.info( + "TEST-INFO | LeakSanitizer | This will cause leaks that " + "should be ignored to instead be reported as an error" + ) + + if self.foundFrames: + self.logger.info( + "TEST-INFO | LeakSanitizer | To show the " + "addresses of leaked objects add report_objects=1 to LSAN_OPTIONS" + ) + self.logger.info( + "TEST-INFO | LeakSanitizer | This can be done " + "in testing/mozbase/mozrunner/mozrunner/utils.py" + ) + + for f in self.foundFrames: + self.logger.error("TEST-UNEXPECTED-FAIL | LeakSanitizer | leak at " + f) + failures += 1 + + return failures + + def _finishStack(self): + if self.recordMoreFrames and len(self.currStack) == 0: + self.currStack = ["unknown stack"] + if self.currStack: + self.foundFrames.add(", ".join(self.currStack)) + self.currStack = None + self.recordMoreFrames = False + self.numRecordedFrames = 0 + + def _recordFrame(self, frame): + self.currStack.append(frame) + self.numRecordedFrames += 1 + if self.numRecordedFrames >= self.maxNumRecordedFrames: + self.recordMoreFrames = False diff --git a/testing/mochitest/mach_commands.py b/testing/mochitest/mach_commands.py new file mode 100644 index 0000000000..b67346c7e3 --- /dev/null +++ b/testing/mochitest/mach_commands.py @@ -0,0 +1,565 @@ +# 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 functools +import logging +import os +import sys +import warnings +from argparse import Namespace +from collections import defaultdict + +import six +from mach.decorators import Command, CommandArgument +from mozbuild.base import MachCommandConditions as conditions +from mozbuild.base import MozbuildObject + +here = os.path.abspath(os.path.dirname(__file__)) + + +ENG_BUILD_REQUIRED = """ +The mochitest command requires an engineering build. It may be the case that +VARIANT=user or PRODUCTION=1 were set. Try re-building with VARIANT=eng: + + $ VARIANT=eng ./build.sh + +There should be an app called 'test-container.gaiamobile.org' located in +{}. +""".lstrip() + +SUPPORTED_TESTS_NOT_FOUND = """ +The mochitest command could not find any supported tests to run! The +following flavors and subsuites were found, but are either not supported on +{} builds, or were excluded on the command line: + +{} + +Double check the command line you used, and make sure you are running in +context of the proper build. To switch build contexts, either run |mach| +from the appropriate objdir, or export the correct mozconfig: + + $ export MOZCONFIG=path/to/mozconfig +""".lstrip() + +TESTS_NOT_FOUND = """ +The mochitest command could not find any mochitests under the following +test path(s): + +{} + +Please check spelling and make sure there are mochitests living there. +""".lstrip() + +SUPPORTED_APPS = ["firefox", "android", "thunderbird"] + +parser = None + + +class MochitestRunner(MozbuildObject): + + """Easily run mochitests. + + This currently contains just the basics for running mochitests. We may want + to hook up result parsing, etc. + """ + + def __init__(self, *args, **kwargs): + MozbuildObject.__init__(self, *args, **kwargs) + + # TODO Bug 794506 remove once mach integrates with virtualenv. + build_path = os.path.join(self.topobjdir, "build") + if build_path not in sys.path: + sys.path.append(build_path) + + self.tests_dir = os.path.join(self.topobjdir, "_tests") + self.mochitest_dir = os.path.join(self.tests_dir, "testing", "mochitest") + self.bin_dir = os.path.join(self.topobjdir, "dist", "bin") + + def resolve_tests(self, test_paths, test_objects=None, cwd=None): + if test_objects: + return test_objects + + from moztest.resolve import TestResolver + + resolver = self._spawn(TestResolver) + tests = list(resolver.resolve_tests(paths=test_paths, cwd=cwd)) + return tests + + def run_desktop_test(self, command_context, tests=None, **kwargs): + """Runs a mochitest.""" + # runtests.py is ambiguous, so we load the file/module manually. + if "mochitest" not in sys.modules: + import imp + + path = os.path.join(self.mochitest_dir, "runtests.py") + with open(path, "r") as fh: + imp.load_module("mochitest", fh, path, (".py", "r", imp.PY_SOURCE)) + + import mochitest + + # This is required to make other components happy. Sad, isn't it? + os.chdir(self.topobjdir) + + # Automation installs its own stream handler to stdout. Since we want + # all logging to go through us, we just remove their handler. + remove_handlers = [ + l + for l in logging.getLogger().handlers + if isinstance(l, logging.StreamHandler) + ] + for handler in remove_handlers: + logging.getLogger().removeHandler(handler) + + options = Namespace(**kwargs) + options.topsrcdir = self.topsrcdir + options.topobjdir = self.topobjdir + + from manifestparser import TestManifest + + if tests and not options.manifestFile: + manifest = TestManifest() + manifest.tests.extend(tests) + options.manifestFile = manifest + + # When developing mochitest-plain tests, it's often useful to be able to + # refresh the page to pick up modifications. Therefore leave the browser + # open if only running a single mochitest-plain test. This behaviour can + # be overridden by passing in --keep-open=false. + if ( + len(tests) == 1 + and options.keep_open is None + and not options.headless + and getattr(options, "flavor", "plain") == "plain" + ): + options.keep_open = True + + # We need this to enable colorization of output. + self.log_manager.enable_unstructured() + result = mochitest.run_test_harness(parser, options) + self.log_manager.disable_unstructured() + return result + + def run_android_test(self, command_context, tests, **kwargs): + host_ret = verify_host_bin() + if host_ret != 0: + return host_ret + + import imp + + path = os.path.join(self.mochitest_dir, "runtestsremote.py") + with open(path, "r") as fh: + imp.load_module("runtestsremote", fh, path, (".py", "r", imp.PY_SOURCE)) + import runtestsremote + + options = Namespace(**kwargs) + + from manifestparser import TestManifest + + if tests and not options.manifestFile: + manifest = TestManifest() + manifest.tests.extend(tests) + options.manifestFile = manifest + + # Firefox for Android doesn't use e10s + if options.app is not None and "geckoview" not in options.app: + options.e10s = False + print("using e10s=False for non-geckoview app") + + # Disable fission until geckoview supports fission by default. + setattr(options, "disable_fission", True) + if "fission.autostart=true" in options.extraPrefs: + setattr(options, "disable_fission", False) + + return runtestsremote.run_test_harness(parser, options) + + def run_geckoview_junit_test(self, context, **kwargs): + host_ret = verify_host_bin() + if host_ret != 0: + return host_ret + + import runjunit + + options = Namespace(**kwargs) + + # Disable fission until geckoview supports fission by default. + setattr(options, "disable_fission", True) + if "fission.autostart=true" in options.extra_prefs: + setattr(options, "disable_fission", False) + + return runjunit.run_test_harness(parser, options) + + +# parser + + +def setup_argument_parser(): + build_obj = MozbuildObject.from_environment(cwd=here) + + build_path = os.path.join(build_obj.topobjdir, "build") + if build_path not in sys.path: + sys.path.append(build_path) + + mochitest_dir = os.path.join(build_obj.topobjdir, "_tests", "testing", "mochitest") + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + import imp + + path = os.path.join(build_obj.topobjdir, mochitest_dir, "runtests.py") + if not os.path.exists(path): + path = os.path.join(here, "runtests.py") + + with open(path, "r") as fh: + imp.load_module("mochitest", fh, path, (".py", "r", imp.PY_SOURCE)) + + from mochitest_options import MochitestArgumentParser + + if conditions.is_android(build_obj): + # On Android, check for a connected device (and offer to start an + # emulator if appropriate) before running tests. This check must + # be done in this admittedly awkward place because + # MochitestArgumentParser initialization fails if no device is found. + from mozrunner.devices.android_device import ( + InstallIntent, + verify_android_device, + ) + + # verify device and xre + verify_android_device(build_obj, install=InstallIntent.NO, xre=True) + + global parser + parser = MochitestArgumentParser() + return parser + + +def setup_junit_argument_parser(): + build_obj = MozbuildObject.from_environment(cwd=here) + + build_path = os.path.join(build_obj.topobjdir, "build") + if build_path not in sys.path: + sys.path.append(build_path) + + mochitest_dir = os.path.join(build_obj.topobjdir, "_tests", "testing", "mochitest") + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # runtests.py contains MochitestDesktop, required by runjunit + import imp + + path = os.path.join(build_obj.topobjdir, mochitest_dir, "runtests.py") + if not os.path.exists(path): + path = os.path.join(here, "runtests.py") + + with open(path, "r") as fh: + imp.load_module("mochitest", fh, path, (".py", "r", imp.PY_SOURCE)) + + import runjunit + from mozrunner.devices.android_device import ( + InstallIntent, + verify_android_device, + ) + + verify_android_device( + build_obj, install=InstallIntent.NO, xre=True, network=True + ) + + global parser + parser = runjunit.JunitArgumentParser() + return parser + + +def verify_host_bin(): + # validate MOZ_HOST_BIN environment variables for Android tests + xpcshell_binary = "xpcshell" + if os.name == "nt": + xpcshell_binary = "xpcshell.exe" + MOZ_HOST_BIN = os.environ.get("MOZ_HOST_BIN") + if not MOZ_HOST_BIN: + print( + "environment variable MOZ_HOST_BIN must be set to a directory containing host " + "%s" % xpcshell_binary + ) + return 1 + elif not os.path.isdir(MOZ_HOST_BIN): + print("$MOZ_HOST_BIN does not specify a directory") + return 1 + elif not os.path.isfile(os.path.join(MOZ_HOST_BIN, xpcshell_binary)): + print("$MOZ_HOST_BIN/%s does not exist" % xpcshell_binary) + return 1 + return 0 + + +@Command( + "mochitest", + category="testing", + conditions=[functools.partial(conditions.is_buildapp_in, apps=SUPPORTED_APPS)], + description="Run any flavor of mochitest (integration test).", + parser=setup_argument_parser, +) +def run_mochitest_general( + command_context, flavor=None, test_objects=None, resolve_tests=True, **kwargs +): + from mochitest_options import ALL_FLAVORS + from mozlog.commandline import setup_logging + from mozlog.handlers import StreamHandler + from moztest.resolve import get_suite_definition + + # TODO: This is only strictly necessary while mochitest is using Python + # 2 and can be removed once the command is migrated to Python 3. + command_context.activate_virtualenv() + + buildapp = None + for app in SUPPORTED_APPS: + if conditions.is_buildapp_in(command_context, apps=[app]): + buildapp = app + break + + flavors = None + if flavor: + for fname, fobj in six.iteritems(ALL_FLAVORS): + if flavor in fobj["aliases"]: + if buildapp not in fobj["enabled_apps"]: + continue + flavors = [fname] + break + else: + flavors = [ + f for f, v in six.iteritems(ALL_FLAVORS) if buildapp in v["enabled_apps"] + ] + + from mozbuild.controller.building import BuildDriver + + command_context._ensure_state_subdir_exists(".") + + test_paths = kwargs["test_paths"] + kwargs["test_paths"] = [] + + if kwargs.get("debugger", None): + import mozdebug + + if not mozdebug.get_debugger_info(kwargs.get("debugger")): + sys.exit(1) + + mochitest = command_context._spawn(MochitestRunner) + tests = [] + if resolve_tests: + tests = mochitest.resolve_tests( + test_paths, test_objects, cwd=command_context._mach_context.cwd + ) + + if not kwargs.get("log"): + # Create shared logger + format_args = {"level": command_context._mach_context.settings["test"]["level"]} + if len(tests) == 1: + format_args["verbose"] = True + format_args["compact"] = False + + default_format = command_context._mach_context.settings["test"]["format"] + kwargs["log"] = setup_logging( + "mach-mochitest", kwargs, {default_format: sys.stdout}, format_args + ) + for handler in kwargs["log"].handlers: + if isinstance(handler, StreamHandler): + handler.formatter.inner.summary_on_shutdown = True + + driver = command_context._spawn(BuildDriver) + driver.install_tests() + + subsuite = kwargs.get("subsuite") + if subsuite == "default": + kwargs["subsuite"] = None + + suites = defaultdict(list) + is_webrtc_tag_present = False + unsupported = set() + for test in tests: + # Check if we're running a webrtc test so we can enable webrtc + # specific test logic later if needed. + if "webrtc" in test.get("tags", ""): + is_webrtc_tag_present = True + + # Filter out non-mochitests and unsupported flavors. + if test["flavor"] not in ALL_FLAVORS: + continue + + key = (test["flavor"], test.get("subsuite", "")) + if test["flavor"] not in flavors: + unsupported.add(key) + continue + + if subsuite == "default": + # "--subsuite default" means only run tests that don't have a subsuite + if test.get("subsuite"): + unsupported.add(key) + continue + elif subsuite and test.get("subsuite", "") != subsuite: + unsupported.add(key) + continue + + suites[key].append(test) + + # Only webrtc mochitests in the media suite need the websocketprocessbridge. + if ("mochitest", "media") in suites and is_webrtc_tag_present: + req = os.path.join( + "testing", + "tools", + "websocketprocessbridge", + "websocketprocessbridge_requirements_3.txt", + ) + command_context.virtualenv_manager.activate() + command_context.virtualenv_manager.install_pip_requirements( + req, require_hashes=False + ) + + # sys.executable is used to start the websocketprocessbridge, though for some + # reason it doesn't get set when calling `activate_this.py` in the virtualenv. + sys.executable = command_context.virtualenv_manager.python_path + + # This is a hack to introduce an option in mach to not send + # filtered tests to the mochitest harness. Mochitest harness will read + # the master manifest in that case. + if not resolve_tests: + for flavor in flavors: + key = (flavor, kwargs.get("subsuite")) + suites[key] = [] + + if not suites: + # Make it very clear why no tests were found + if not unsupported: + print( + TESTS_NOT_FOUND.format( + "\n".join(sorted(list(test_paths or test_objects))) + ) + ) + return 1 + + msg = [] + for f, s in unsupported: + fobj = ALL_FLAVORS[f] + apps = fobj["enabled_apps"] + name = fobj["aliases"][0] + if s: + name = "{} --subsuite {}".format(name, s) + + if buildapp not in apps: + reason = "requires {}".format(" or ".join(apps)) + else: + reason = "excluded by the command line" + msg.append(" mochitest -f {} ({})".format(name, reason)) + print(SUPPORTED_TESTS_NOT_FOUND.format(buildapp, "\n".join(sorted(msg)))) + return 1 + + if buildapp == "android": + from mozrunner.devices.android_device import ( + InstallIntent, + get_adb_path, + verify_android_device, + ) + + app = kwargs.get("app") + if not app: + app = "org.mozilla.geckoview.test_runner" + device_serial = kwargs.get("deviceSerial") + install = InstallIntent.NO if kwargs.get("no_install") else InstallIntent.YES + aab = kwargs.get("aab") + + # verify installation + verify_android_device( + command_context, + install=install, + xre=False, + network=True, + app=app, + aab=aab, + device_serial=device_serial, + ) + + if not kwargs["adbPath"]: + kwargs["adbPath"] = get_adb_path(command_context) + + run_mochitest = mochitest.run_android_test + else: + run_mochitest = mochitest.run_desktop_test + + overall = None + for (flavor, subsuite), tests in sorted(suites.items()): + suite_name, suite = get_suite_definition(flavor, subsuite) + if "test_paths" in suite["kwargs"]: + del suite["kwargs"]["test_paths"] + + harness_args = kwargs.copy() + harness_args.update(suite["kwargs"]) + # Pass in the full suite name as defined in moztest/resolve.py in case + # chunk-by-runtime is called, in which case runtime information for + # specific mochitest suite has to be loaded. See Bug 1637463. + harness_args.update({"suite_name": suite_name}) + + result = run_mochitest( + command_context._mach_context, tests=tests, **harness_args + ) + + if result: + overall = result + + # Halt tests on keyboard interrupt + if result == -1: + break + + # Only shutdown the logger if we created it + if kwargs["log"].name == "mach-mochitest": + kwargs["log"].shutdown() + + return overall + + +@Command( + "geckoview-junit", + category="testing", + conditions=[conditions.is_android], + description="Run remote geckoview junit tests.", + parser=setup_junit_argument_parser, +) +@CommandArgument( + "--no-install", + help="Do not try to install application on device before " + + "running (default: False)", + action="store_true", + default=False, +) +def run_junit(command_context, no_install, **kwargs): + command_context._ensure_state_subdir_exists(".") + + from mozrunner.devices.android_device import ( + InstallIntent, + get_adb_path, + verify_android_device, + ) + + # verify installation + app = kwargs.get("app") + device_serial = kwargs.get("deviceSerial") + verify_android_device( + command_context, + install=InstallIntent.NO if no_install else InstallIntent.YES, + xre=False, + app=app, + device_serial=device_serial, + ) + + if not kwargs.get("adbPath"): + kwargs["adbPath"] = get_adb_path(command_context) + + if not kwargs.get("log"): + from mozlog.commandline import setup_logging + + format_args = {"level": command_context._mach_context.settings["test"]["level"]} + default_format = command_context._mach_context.settings["test"]["format"] + kwargs["log"] = setup_logging( + "mach-mochitest", kwargs, {default_format: sys.stdout}, format_args + ) + + mochitest = command_context._spawn(MochitestRunner) + return mochitest.run_geckoview_junit_test(command_context._mach_context, **kwargs) diff --git a/testing/mochitest/mach_test_package_commands.py b/testing/mochitest/mach_test_package_commands.py new file mode 100644 index 0000000000..b5c4d5cdbe --- /dev/null +++ b/testing/mochitest/mach_test_package_commands.py @@ -0,0 +1,219 @@ +# 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 +import sys +from argparse import Namespace +from functools import partial + +import six +from mach.decorators import Command + +here = os.path.abspath(os.path.dirname(__file__)) +parser = None +logger = None + + +def run_test(context, is_junit, **kwargs): + from mochitest_options import ALL_FLAVORS + from mozlog.commandline import setup_logging + + if not kwargs.get("log"): + kwargs["log"] = setup_logging("mochitest", kwargs, {"mach": sys.stdout}) + global logger + logger = kwargs["log"] + + flavor = kwargs.get("flavor") or "mochitest" + if flavor not in ALL_FLAVORS: + for fname, fobj in six.iteritems(ALL_FLAVORS): + if flavor in fobj["aliases"]: + flavor = fname + break + fobj = ALL_FLAVORS[flavor] + kwargs.update(fobj.get("extra_args", {})) + + if kwargs.get("allow_software_gl_layers"): + os.environ["MOZ_LAYERS_ALLOW_SOFTWARE_GL"] = "1" + del kwargs["allow_software_gl_layers"] + if kwargs.get("mochitest_suite"): + suite = kwargs["mochitest_suite"] + del kwargs["mochitest_suite"] + elif kwargs.get("test_suite"): + suite = kwargs["test_suite"] + del kwargs["test_suite"] + if kwargs.get("no_run_tests"): + del kwargs["no_run_tests"] + + args = Namespace(**kwargs) + args.e10s = context.mozharness_config.get("e10s", args.e10s) + args.certPath = context.certs_dir + + if is_junit: + return run_geckoview_junit(context, args) + + # subsuite mapping from mozharness configs + subsuites = { + # --mochitest-suite -> --subsuite + "mochitest-chrome-gpu": "gpu", + "mochitest-plain-gpu": "gpu", + "mochitest-media": "media", + "mochitest-browser-chrome-screenshots": "screenshots", + "mochitest-webgl1-core": "webgl1-core", + "mochitest-webgl1-ext": "webgl1-ext", + "mochitest-webgl2-core": "webgl2-core", + "mochitest-webgl2-ext": "webgl2-ext", + "mochitest-webgl2-deqp": "webgl2-deqp", + "mochitest-webgpu": "webgpu", + "mochitest-devtools-chrome": "devtools", + "mochitest-browser-a11y": "a11y", + "mochitest-remote": "remote", + "mochitest-browser-media": "media-bc", + } + args.subsuite = subsuites.get(suite) + if args.subsuite == "devtools": + args.flavor = "browser" + if args.subsuite == "a11y": + args.flavor = "browser" + if args.subsuite == "media-bc": + args.flavor = "browser" + + if not args.test_paths: + mh_test_paths = json.loads(os.environ.get("MOZHARNESS_TEST_PATHS", '""')) + if mh_test_paths: + logger.info("Found MOZHARNESS_TEST_PATHS:") + logger.info(str(mh_test_paths)) + args.test_paths = [] + for k in mh_test_paths: + args.test_paths.extend(mh_test_paths[k]) + if args.test_paths: + install_subdir = fobj.get("install_subdir", fobj["suite"]) + test_root = os.path.join(context.package_root, "mochitest", install_subdir) + normalize = partial(context.normalize_test_path, test_root) + # pylint --py3k: W1636 + args.test_paths = list(map(normalize, args.test_paths)) + + import mozinfo + + if mozinfo.info.get("buildapp") == "mobile/android": + return run_mochitest_android(context, args) + return run_mochitest_desktop(context, args) + + +def run_mochitest_desktop(context, args): + args.app = args.app or context.firefox_bin + args.utilityPath = context.bin_dir + args.extraProfileFiles.append(os.path.join(context.bin_dir, "plugins")) + args.extraPrefs.append("webgl.force-enabled=true") + args.quiet = True + args.useTestMediaDevices = True + args.screenshotOnFail = True + args.cleanupCrashes = True + args.marionette_startup_timeout = "180" + args.sandboxReadWhitelist.append(context.mozharness_workdir) + if args.flavor == "browser": + args.chunkByRuntime = True + else: + args.chunkByDir = 4 + + from runtests import run_test_harness + + logger.info("mach calling runtests with args: " + str(args)) + return run_test_harness(parser, args) + + +def set_android_args(context, args): + args.app = args.app or "org.mozilla.geckoview.test_runner" + args.utilityPath = context.hostutils + args.xrePath = context.hostutils + config = context.mozharness_config + if config: + host = os.environ.get("HOST_IP", "10.0.2.2") + args.remoteWebServer = config.get("remote_webserver", host) + args.httpPort = config.get("http_port", 8854) + args.sslPort = config.get("ssl_port", 4454) + args.adbPath = config["exes"]["adb"] % { + "abs_work_dir": context.mozharness_workdir + } + args.deviceSerial = os.environ.get("DEVICE_SERIAL", "emulator-5554") + return args + + +def run_mochitest_android(context, args): + args = set_android_args(context, args) + args.extraProfileFiles.append( + os.path.join(context.package_root, "mochitest", "fonts") + ) + + from runtestsremote import run_test_harness + + logger.info("mach calling runtestsremote with args: " + str(args)) + return run_test_harness(parser, args) + + +def run_geckoview_junit(context, args): + args = set_android_args(context, args) + + from runjunit import run_test_harness + + # Force fission disabled by default for android + args["disable_fission"] = True + + logger.info("mach calling runjunit with args: " + str(args)) + return run_test_harness(parser, args) + + +def add_global_arguments(parser): + parser.add_argument("--test-suite") + parser.add_argument("--mochitest-suite") + parser.add_argument("--download-symbols") + parser.add_argument("--allow-software-gl-layers", action="store_true") + parser.add_argument("--no-run-tests", action="store_true") + + +def setup_mochitest_argument_parser(): + import mozinfo + + mozinfo.find_and_update_from_json(here) + app = "generic" + if mozinfo.info.get("buildapp") == "mobile/android": + app = "android" + + from mochitest_options import MochitestArgumentParser + + global parser + parser = MochitestArgumentParser(app=app) + add_global_arguments(parser) + return parser + + +def setup_junit_argument_parser(): + from runjunit import JunitArgumentParser + + global parser + parser = JunitArgumentParser() + add_global_arguments(parser) + return parser + + +@Command( + "mochitest", + category="testing", + description="Run the mochitest harness.", + parser=setup_mochitest_argument_parser, +) +def mochitest(command_context, **kwargs): + command_context._mach_context.activate_mozharness_venv() + return run_test(command_context._mach_context, False, **kwargs) + + +@Command( + "geckoview-junit", + category="testing", + description="Run the geckoview-junit harness.", + parser=setup_junit_argument_parser, +) +def geckoview_junit(command_context, **kwargs): + command_context._mach_context.activate_mozharness_venv() + return run_test(command_context._mach_context, True, **kwargs) diff --git a/testing/mochitest/manifest.json b/testing/mochitest/manifest.json new file mode 100644 index 0000000000..80940a3820 --- /dev/null +++ b/testing/mochitest/manifest.json @@ -0,0 +1,22 @@ +{ + "manifest_version": 2, + "name": "Mochitest", + "version": "2.0", + + "browser_specific_settings": { + "gecko": { + "id": "mochikit@mozilla.org" + } + }, + + "experiment_apis": { + "mochikit": { + "schema": "schema.json", + "parent": { + "scopes": ["addon_parent"], + "script": "api.js", + "events": ["startup"] + } + } + } +} diff --git a/testing/mochitest/manifest.webapp b/testing/mochitest/manifest.webapp new file mode 100644 index 0000000000..a557652a16 --- /dev/null +++ b/testing/mochitest/manifest.webapp @@ -0,0 +1,37 @@ +{ + "name": "Mochitest", + "type": "certified", + "description": "Mochitests", + "developer": { + "name": "The Ateam", + "url": "https://wiki.mozilla.org/Auto-tools" + }, + "permissions": { + "alarms": {}, + "browser":{}, + "power":{}, + "webapps-manage":{}, + "mobileconnection":{}, + "bluetooth":{}, + "telephony":{}, + "device-storage:pictures":{ "access": "readwrite" }, + "device-storage:sdcard":{ "access": "readwrite" }, + "settings":{ "access": "readwrite" }, + "storage":{}, + "camera":{}, + "geolocation":{}, + "wifi-manage":{}, + "desktop-notification":{}, + "idle":{}, + "network-events":{}, + "audio-channel-content":{}, + "audio-channel-alarm":{}, + }, + "locales": { + "en-US": { + "name": "Mochitest", + "description": "Mochitests" + } + }, + "default_locale": "en-US" +} diff --git a/testing/mochitest/manifestLibrary.js b/testing/mochitest/manifestLibrary.js new file mode 100644 index 0000000000..c9417f03d1 --- /dev/null +++ b/testing/mochitest/manifestLibrary.js @@ -0,0 +1,189 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +function parseTestManifest(testManifest, params, callback) { + let links = {}; + let paths = []; + + // Support --test-manifest format for mobile + if ("runtests" in testManifest || "excludetests" in testManifest) { + callback(testManifest); + return; + } + + // For mochitest-chrome and mochitest-browser-chrome harnesses, we + // define tests as links[testname] = true. + // For mochitest-plain, we define lists as an array of testnames. + for (let obj of testManifest.tests) { + let path = obj.path; + // Note that obj.disabled may be "". We still want to skip in that case. + if ("disabled" in obj) { + dump("TEST-SKIPPED | " + path + " | " + obj.disabled + "\n"); + continue; + } + if (params.testRoot != "tests" && params.testRoot !== undefined) { + let name = params.baseurl + "/" + params.testRoot + "/" + path; + links[name] = { + test: { + url: name, + expected: obj.expected, + https_first_disabled: obj.https_first_disabled, + allow_xul_xbl: obj.allow_xul_xbl, + }, + }; + } else { + let name = params.testPrefix + path; + if (params.xOriginTests && obj.scheme == "https") { + name = params.httpsBaseUrl + path; + } + paths.push({ + test: { + url: name, + expected: obj.expected, + https_first_disabled: obj.https_first_disabled, + allow_xul_xbl: obj.allow_xul_xbl, + }, + }); + } + } + if (paths.length) { + callback(paths); + } else { + callback(links); + } +} + +function getTestManifest(url, params, callback) { + let req = new XMLHttpRequest(); + req.open("GET", url); + req.onload = function() { + if (req.readyState == 4) { + if (req.status == 200) { + try { + parseTestManifest(JSON.parse(req.responseText), params, callback); + } catch (e) { + dump( + "TEST-UNEXPECTED-FAIL: manifestLibrary.js | error parsing " + + url + + " (" + + e + + ")\n" + ); + throw e; + } + } else { + dump( + "TEST-UNEXPECTED-FAIL: manifestLibrary.js | error loading " + + url + + " (HTTP " + + req.status + + ")\n" + ); + callback({}); + } + } + }; + req.send(); +} + +// Test Filtering Code +// TODO Only used by ipc tests, remove once those are implemented sanely + +/* + Open the file referenced by runOnly|exclude and use that to compare against + testList + parameters: + filter = json object of runtests | excludetests + testList = array of test names to run + runOnly = use runtests vs excludetests in case both are defined + returns: + filtered version of testList +*/ +function filterTests(filter, testList, runOnly) { + let filteredTests = []; + let runtests = {}; + let excludetests = {}; + + if (filter == null) { + return testList; + } + + if ("runtests" in filter) { + runtests = filter.runtests; + } + if ("excludetests" in filter) { + excludetests = filter.excludetests; + } + if (!("runtests" in filter) && !("excludetests" in filter)) { + if (runOnly == "true") { + runtests = filter; + } else { + excludetests = filter; + } + } + + // eslint-disable-next-line no-undef + let testRoot = config.testRoot || "tests"; + // Start with testList, and put everything that's in 'runtests' in + // filteredTests. + if (Object.keys(runtests).length) { + for (let i = 0; i < testList.length; i++) { + let testpath; + if (testList[i] instanceof Object && "test" in testList[i]) { + testpath = testList[i].test.url; + } else { + testpath = testList[i]; + } + let tmppath = testpath.replace(/^\//, ""); + for (let f in runtests) { + // Remove leading /tests/ if exists + let file = f.replace(/^\//, ""); + file = file.replace(/^tests\//, ""); + + // Match directory or filename, testList has <testroot>/<path> + if (tmppath.match(testRoot + "/" + file) != null) { + filteredTests.push(testpath); + break; + } + } + } + } else { + filteredTests = testList.slice(0); + } + + // Continue with filteredTests, and deselect everything that's in + // excludedtests. + if (!Object.keys(excludetests).length) { + return filteredTests; + } + + let refilteredTests = []; + for (let i = 0; i < filteredTests.length; i++) { + let found = false; + let testpath; + if (filteredTests[i] instanceof Object && "test" in filteredTests[i]) { + testpath = filteredTests[i].test.url; + } else { + testpath = filteredTests[i]; + } + let tmppath = testpath.replace(/^\//, ""); + for (let f in excludetests) { + // Remove leading /tests/ if exists + let file = f.replace(/^\//, ""); + file = file.replace(/^tests\//, ""); + + // Match directory or filename, testList has <testroot>/<path> + if (tmppath.match(testRoot + "/" + file) != null) { + found = true; + break; + } + } + if (!found) { + refilteredTests.push(testpath); + } + } + return refilteredTests; +} diff --git a/testing/mochitest/manifests/emulator-jb.ini b/testing/mochitest/manifests/emulator-jb.ini new file mode 100644 index 0000000000..236af51aeb --- /dev/null +++ b/testing/mochitest/manifests/emulator-jb.ini @@ -0,0 +1 @@ +[include:../tests/dom/media/test/mochitest.ini] diff --git a/testing/mochitest/manifests/moz.build b/testing/mochitest/manifests/moz.build new file mode 100644 index 0000000000..08f36d268d --- /dev/null +++ b/testing/mochitest/manifests/moz.build @@ -0,0 +1,9 @@ +# -*- 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.manifests += [ + "emulator-jb.ini", +] diff --git a/testing/mochitest/mochitest-e10s-utils.js b/testing/mochitest/mochitest-e10s-utils.js new file mode 100644 index 0000000000..09046794bb --- /dev/null +++ b/testing/mochitest/mochitest-e10s-utils.js @@ -0,0 +1,18 @@ +// Utilities for running tests in an e10s environment. + +function e10s_init() { + // Listen for an 'oop-browser-crashed' event and log it so people analysing + // test logs have a clue about what is going on. + window.addEventListener( + "oop-browser-crashed", + event => { + let uri = event.target.currentURI; + console.error( + "remote browser crashed while on " + + (uri ? uri.spec : "<unknown>") + + "\n" + ); + }, + true + ); +} diff --git a/testing/mochitest/mochitest_options.py b/testing/mochitest/mochitest_options.py new file mode 100644 index 0000000000..6128872a1f --- /dev/null +++ b/testing/mochitest/mochitest_options.py @@ -0,0 +1,1424 @@ +# 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 +import sys +import tempfile +from abc import ABCMeta, abstractmethod, abstractproperty +from argparse import SUPPRESS, ArgumentParser +from distutils import spawn +from distutils.util import strtobool +from itertools import chain + +import mozinfo +import mozlog +import moznetwork +import six +from mozprofile import DEFAULT_PORTS +from six.moves.urllib.parse import urlparse + +here = os.path.abspath(os.path.dirname(__file__)) + +try: + from mozbuild.base import MachCommandConditions as conditions + from mozbuild.base import MozbuildObject + + build_obj = MozbuildObject.from_environment(cwd=here) +except ImportError: + build_obj = None + conditions = None + + +# Maps test flavors to data needed to run them +ALL_FLAVORS = { + "mochitest": { + "suite": "plain", + "aliases": ("plain", "mochitest"), + "enabled_apps": ("firefox", "android"), + "extra_args": { + "flavor": "plain", + }, + "install_subdir": "tests", + }, + "chrome": { + "suite": "chrome", + "aliases": ("chrome", "mochitest-chrome"), + "enabled_apps": ("firefox"), + "extra_args": { + "flavor": "chrome", + }, + }, + "browser-chrome": { + "suite": "browser", + "aliases": ("browser", "browser-chrome", "mochitest-browser-chrome", "bc"), + "enabled_apps": ("firefox", "thunderbird"), + "extra_args": { + "flavor": "browser", + }, + }, + "a11y": { + "suite": "a11y", + "aliases": ("a11y", "mochitest-a11y", "accessibility"), + "enabled_apps": ("firefox",), + "extra_args": { + "flavor": "a11y", + }, + }, +} +SUPPORTED_FLAVORS = list( + chain.from_iterable([f["aliases"] for f in ALL_FLAVORS.values()]) +) +CANONICAL_FLAVORS = sorted([f["aliases"][0] for f in ALL_FLAVORS.values()]) + + +def get_default_valgrind_suppression_files(): + # We are trying to locate files in the source tree. So if we + # don't know where the source tree is, we must give up. + # + # When this is being run by |mach mochitest --valgrind ...|, it is + # expected that |build_obj| is not None, and so the logic below will + # select the correct suppression files. + # + # When this is run from mozharness, |build_obj| is None, and we expect + # that testing/mozharness/configs/unittests/linux_unittests.py will + # select the correct suppression files (and paths to them) and + # will specify them using the --valgrind-supp-files= flag. Hence this + # function will not get called when running from mozharness. + # + # Note: keep these Valgrind .sup file names consistent with those + # in testing/mozharness/configs/unittests/linux_unittest.py. + if build_obj is None or build_obj.topsrcdir is None: + return [] + + supps_path = os.path.join(build_obj.topsrcdir, "build", "valgrind") + + rv = [] + if mozinfo.os == "linux": + if mozinfo.processor == "x86_64": + rv.append(os.path.join(supps_path, "x86_64-pc-linux-gnu.sup")) + rv.append(os.path.join(supps_path, "cross-architecture.sup")) + elif mozinfo.processor == "x86": + rv.append(os.path.join(supps_path, "i386-pc-linux-gnu.sup")) + rv.append(os.path.join(supps_path, "cross-architecture.sup")) + + return rv + + +@six.add_metaclass(ABCMeta) +class ArgumentContainer: + @abstractproperty + def args(self): + pass + + @abstractproperty + def defaults(self): + pass + + @abstractmethod + def validate(self, parser, args, context): + pass + + def get_full_path(self, path, cwd): + """Get an absolute path relative to cwd.""" + return os.path.normpath(os.path.join(cwd, os.path.expanduser(path))) + + +class MochitestArguments(ArgumentContainer): + """General mochitest arguments.""" + + LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "FATAL") + + args = [ + [ + ["test_paths"], + { + "nargs": "*", + "metavar": "TEST", + "default": [], + "help": "Test to run. Can be a single test file or a directory of tests " + "(to run recursively). If omitted, the entire suite is run.", + }, + ], + [ + ["-f", "--flavor"], + { + "choices": SUPPORTED_FLAVORS, + "metavar": "{{{}}}".format(", ".join(CANONICAL_FLAVORS)), + "default": None, + "help": "Only run tests of this flavor.", + }, + ], + [ + ["--keep-open"], + { + "nargs": "?", + "type": strtobool, + "const": "true", + "default": None, + "help": "Always keep the browser open after tests complete. Or always close the " + "browser with --keep-open=false", + }, + ], + [ + ["--appname"], + { + "dest": "app", + "default": None, + "help": ( + "Override the default binary used to run tests with the path provided, e.g " + "/usr/bin/firefox. If you have run ./mach package beforehand, you can " + "specify 'dist' to run tests against the distribution bundle's binary." + ), + }, + ], + [ + ["--utility-path"], + { + "dest": "utilityPath", + "default": build_obj.bindir if build_obj is not None else None, + "help": "absolute path to directory containing utility programs " + "(xpcshell, ssltunnel, certutil)", + "suppress": True, + }, + ], + [ + ["--certificate-path"], + { + "dest": "certPath", + "default": None, + "help": "absolute path to directory containing certificate store to use testing profile", # NOQA: E501 + "suppress": True, + }, + ], + [ + ["--no-autorun"], + { + "action": "store_false", + "dest": "autorun", + "default": True, + "help": "Do not start running tests automatically.", + }, + ], + [ + ["--timeout"], + { + "type": int, + "default": None, + "help": "The per-test timeout in seconds (default: 60 seconds).", + }, + ], + [ + ["--max-timeouts"], + { + "type": int, + "dest": "maxTimeouts", + "default": None, + "help": "The maximum number of timeouts permitted before halting testing.", + }, + ], + [ + ["--total-chunks"], + { + "type": int, + "dest": "totalChunks", + "help": "Total number of chunks to split tests into.", + "default": None, + }, + ], + [ + ["--this-chunk"], + { + "type": int, + "dest": "thisChunk", + "help": "If running tests by chunks, the chunk number to run.", + "default": None, + }, + ], + [ + ["--chunk-by-runtime"], + { + "action": "store_true", + "dest": "chunkByRuntime", + "help": "Group tests such that each chunk has roughly the same runtime.", + "default": False, + }, + ], + [ + ["--chunk-by-dir"], + { + "type": int, + "dest": "chunkByDir", + "help": "Group tests together in the same chunk that are in the same top " + "chunkByDir directories.", + "default": 0, + }, + ], + [ + ["--run-by-manifest"], + { + "action": "store_true", + "dest": "runByManifest", + "help": "Run each manifest in a single browser instance with a fresh profile.", + "default": False, + "suppress": True, + }, + ], + [ + ["--shuffle"], + { + "action": "store_true", + "help": "Shuffle execution order of tests.", + "default": False, + }, + ], + [ + ["--console-level"], + { + "dest": "consoleLevel", + "choices": LOG_LEVELS, + "default": "INFO", + "help": "One of {} to determine the level of console logging.".format( + ", ".join(LOG_LEVELS) + ), + "suppress": True, + }, + ], + [ + ["--bisect-chunk"], + { + "dest": "bisectChunk", + "default": None, + "help": "Specify the failing test name to find the previous tests that may be " + "causing the failure.", + }, + ], + [ + ["--start-at"], + { + "dest": "startAt", + "default": "", + "help": "Start running the test sequence at this test.", + }, + ], + [ + ["--end-at"], + { + "dest": "endAt", + "default": "", + "help": "Stop running the test sequence at this test.", + }, + ], + [ + ["--subsuite"], + { + "default": None, + "help": "Subsuite of tests to run. Unlike tags, subsuites also remove tests from " + "the default set. Only one can be specified at once.", + }, + ], + [ + ["--setenv"], + { + "action": "append", + "dest": "environment", + "metavar": "NAME=VALUE", + "default": [], + "help": "Sets the given variable in the application's environment.", + }, + ], + [ + ["--exclude-extension"], + { + "action": "append", + "dest": "extensionsToExclude", + "default": [], + "help": "Excludes the given extension from being installed in the test profile.", + "suppress": True, + }, + ], + [ + ["--browser-arg"], + { + "action": "append", + "dest": "browserArgs", + "default": [], + "help": "Provides an argument to the test application (e.g Firefox).", + "suppress": True, + }, + ], + [ + ["--leak-threshold"], + { + "type": int, + "dest": "defaultLeakThreshold", + "default": 0, + "help": "Fail if the number of bytes leaked in default processes through " + "refcounted objects (or bytes in classes with MOZ_COUNT_CTOR and " + "MOZ_COUNT_DTOR) is greater than the given number.", + "suppress": True, + }, + ], + [ + ["--fatal-assertions"], + { + "action": "store_true", + "dest": "fatalAssertions", + "default": False, + "help": "Abort testing whenever an assertion is hit (requires a debug build to " + "be effective).", + "suppress": True, + }, + ], + [ + ["--extra-profile-file"], + { + "action": "append", + "dest": "extraProfileFiles", + "default": [], + "help": "Copy specified files/dirs to testing profile. Can be specified more " + "than once.", + "suppress": True, + }, + ], + [ + ["--install-extension"], + { + "action": "append", + "dest": "extensionsToInstall", + "default": [], + "help": "Install the specified extension in the testing profile. Can be a path " + "to a .xpi file.", + }, + ], + [ + ["--profile-path"], + { + "dest": "profilePath", + "default": None, + "help": "Directory where the profile will be stored. This directory will be " + "deleted after the tests are finished.", + "suppress": True, + }, + ], + [ + ["--conditioned-profile"], + { + "dest": "conditionedProfile", + "action": "store_true", + "default": False, + "help": "Download and run with a full conditioned profile.", + }, + ], + [ + ["--testing-modules-dir"], + { + "dest": "testingModulesDir", + "default": None, + "help": "Directory where testing-only JS modules are located.", + "suppress": True, + }, + ], + [ + ["--repeat"], + { + "type": int, + "default": 0, + "help": "Repeat the tests the given number of times.", + }, + ], + [ + ["--run-until-failure"], + { + "action": "store_true", + "dest": "runUntilFailure", + "default": False, + "help": "Run tests repeatedly but stop the first time a test fails. Default cap " + "is 30 runs, which can be overridden with the --repeat parameter.", + }, + ], + [ + ["--manifest"], + { + "dest": "manifestFile", + "default": None, + "help": "Path to a manifestparser (.ini formatted) manifest of tests to run.", + "suppress": True, + }, + ], + [ + ["--extra-mozinfo-json"], + { + "dest": "extra_mozinfo_json", + "default": None, + "help": "Filter tests based on a given mozinfo file.", + "suppress": True, + }, + ], + [ + ["--testrun-manifest-file"], + { + "dest": "testRunManifestFile", + "default": "tests.json", + "help": "Overrides the default filename of the tests.json manifest file that is " + "generated by the harness and used by SimpleTest. Only useful when running " + "multiple test runs simulatenously on the same machine.", + "suppress": True, + }, + ], + [ + ["--dump-tests"], + { + "dest": "dump_tests", + "default": None, + "help": "Specify path to a filename to dump all the tests that will be run", + "suppress": True, + }, + ], + [ + ["--failure-file"], + { + "dest": "failureFile", + "default": None, + "help": "Filename of the output file where we can store a .json list of failures " + "to be run in the future with --run-only-tests.", + "suppress": True, + }, + ], + [ + ["--run-slower"], + { + "action": "store_true", + "dest": "runSlower", + "default": False, + "help": "Delay execution between tests.", + }, + ], + [ + ["--httpd-path"], + { + "dest": "httpdPath", + "default": None, + "help": "Path to the httpd.js file.", + "suppress": True, + }, + ], + [ + ["--setpref"], + { + "action": "append", + "metavar": "PREF=VALUE", + "default": [], + "dest": "extraPrefs", + "help": "Defines an extra user preference.", + }, + ], + [ + ["--jsconsole"], + { + "action": "store_true", + "default": False, + "help": "Open the Browser Console.", + }, + ], + [ + ["--jsdebugger"], + { + "action": "store_true", + "default": False, + "help": "Start the browser JS debugger before running the test.", + }, + ], + [ + ["--jsdebugger-path"], + { + "default": None, + "dest": "jsdebuggerPath", + "help": "Path to a Firefox binary that will be used to run the toolbox. Should " + "be used together with --jsdebugger.", + }, + ], + [ + ["--debug-on-failure"], + { + "action": "store_true", + "default": False, + "dest": "debugOnFailure", + "help": "Breaks execution and enters the JS debugger on a test failure. Should " + "be used together with --jsdebugger.", + }, + ], + [ + ["--disable-e10s"], + { + "action": "store_false", + "default": True, + "dest": "e10s", + "help": "Run tests with electrolysis preferences and test filtering disabled.", + }, + ], + [ + ["--enable-a11y-checks"], + { + "action": "store_true", + "default": False, + "dest": "a11y_checks", + "help": "Run tests with accessibility checks disabled.", + }, + ], + [ + ["--disable-fission"], + { + "action": "store_true", + "default": False, + "dest": "disable_fission", + "help": "Run tests with fission (site isolation) disabled.", + }, + ], + [ + ["--enable-xorigin-tests"], + { + "action": "store_true", + "default": False, + "dest": "xOriginTests", + "help": "Run tests in a cross origin iframe.", + }, + ], + [ + ["--store-chrome-manifest"], + { + "action": "store", + "help": "Destination path to write a copy of any chrome manifest " + "written by the harness.", + "default": None, + "suppress": True, + }, + ], + [ + ["--jscov-dir-prefix"], + { + "action": "store", + "help": "Directory to store per-test line coverage data as json " + "(browser-chrome only). To emit lcov formatted data, set " + "JS_CODE_COVERAGE_OUTPUT_DIR in the environment.", + "default": None, + "suppress": True, + }, + ], + [ + ["--dmd"], + { + "action": "store_true", + "default": False, + "help": "Run tests with DMD active.", + }, + ], + [ + ["--dump-output-directory"], + { + "default": None, + "dest": "dumpOutputDirectory", + "help": "Specifies the directory in which to place dumped memory reports.", + }, + ], + [ + ["--dump-about-memory-after-test"], + { + "action": "store_true", + "default": False, + "dest": "dumpAboutMemoryAfterTest", + "help": "Dump an about:memory log after each test in the directory specified " + "by --dump-output-directory.", + }, + ], + [ + ["--dump-dmd-after-test"], + { + "action": "store_true", + "default": False, + "dest": "dumpDMDAfterTest", + "help": "Dump a DMD log (and an accompanying about:memory log) after each test. " + "These will be dumped into your default temp directory, NOT the directory " + "specified by --dump-output-directory. The logs are numbered by test, and " + "each test will include output that indicates the DMD output filename.", + }, + ], + [ + ["--screenshot-on-fail"], + { + "action": "store_true", + "default": False, + "dest": "screenshotOnFail", + "help": "Take screenshots on all test failures. Set $MOZ_UPLOAD_DIR to a directory " # NOQA: E501 + "for storing the screenshots.", + }, + ], + [ + ["--quiet"], + { + "action": "store_true", + "dest": "quiet", + "default": False, + "help": "Do not print test log lines unless a failure occurs.", + }, + ], + [ + ["--headless"], + { + "action": "store_true", + "dest": "headless", + "default": False, + "help": "Run tests in headless mode.", + }, + ], + [ + ["--pidfile"], + { + "dest": "pidFile", + "default": "", + "help": "Name of the pidfile to generate.", + "suppress": True, + }, + ], + [ + ["--use-test-media-devices"], + { + "action": "store_true", + "default": False, + "dest": "useTestMediaDevices", + "help": "Use test media device drivers for media testing.", + }, + ], + [ + ["--gmp-path"], + { + "default": None, + "help": "Path to fake GMP plugin. Will be deduced from the binary if not passed.", + "suppress": True, + }, + ], + [ + ["--xre-path"], + { + "dest": "xrePath", + "default": None, # individual scripts will set a sane default + "help": "Absolute path to directory containing XRE (probably xulrunner).", + "suppress": True, + }, + ], + [ + ["--symbols-path"], + { + "dest": "symbolsPath", + "default": None, + "help": "Absolute path to directory containing breakpad symbols, or the URL of a " + "zip file containing symbols", + "suppress": True, + }, + ], + [ + ["--debugger"], + { + "default": None, + "help": "Debugger binary to run tests in. Program name or path.", + }, + ], + [ + ["--debugger-args"], + { + "dest": "debuggerArgs", + "default": None, + "help": "Arguments to pass to the debugger.", + }, + ], + [ + ["--valgrind"], + { + "default": None, + "help": "Valgrind binary to run tests with. Program name or path.", + }, + ], + [ + ["--valgrind-args"], + { + "dest": "valgrindArgs", + "default": None, + "help": "Comma-separated list of extra arguments to pass to Valgrind.", + }, + ], + [ + ["--valgrind-supp-files"], + { + "dest": "valgrindSuppFiles", + "default": None, + "help": "Comma-separated list of suppression files to pass to Valgrind.", + }, + ], + [ + ["--debugger-interactive"], + { + "action": "store_true", + "dest": "debuggerInteractive", + "default": None, + "help": "Prevents the test harness from redirecting stdout and stderr for " + "interactive debuggers.", + "suppress": True, + }, + ], + [ + ["--tag"], + { + "action": "append", + "dest": "test_tags", + "default": None, + "help": "Filter out tests that don't have the given tag. Can be used multiple " + "times in which case the test must contain at least one of the given tags.", + }, + ], + [ + ["--marionette"], + { + "default": None, + "help": "host:port to use when connecting to Marionette", + }, + ], + [ + ["--marionette-socket-timeout"], + { + "default": None, + "help": "Timeout while waiting to receive a message from the marionette server.", + "suppress": True, + }, + ], + [ + ["--marionette-startup-timeout"], + { + "default": None, + "help": "Timeout while waiting for marionette server startup.", + "suppress": True, + }, + ], + [ + ["--cleanup-crashes"], + { + "action": "store_true", + "dest": "cleanupCrashes", + "default": False, + "help": "Delete pending crash reports before running tests.", + "suppress": True, + }, + ], + [ + ["--websocket-process-bridge-port"], + { + "default": "8191", + "dest": "websocket_process_bridge_port", + "help": "Port for websocket/process bridge. Default 8191.", + }, + ], + [ + ["--failure-pattern-file"], + { + "default": None, + "dest": "failure_pattern_file", + "help": "File describes all failure patterns of the tests.", + "suppress": True, + }, + ], + [ + ["--sandbox-read-whitelist"], + { + "default": [], + "dest": "sandboxReadWhitelist", + "action": "append", + "help": "Path to add to the sandbox whitelist.", + "suppress": True, + }, + ], + [ + ["--verify"], + { + "action": "store_true", + "default": False, + "help": "Run tests in verification mode: Run many times in different " + "ways, to see if there are intermittent failures.", + }, + ], + [ + ["--verify-fission"], + { + "action": "store_true", + "default": False, + "help": "Run tests once without Fission, once with Fission", + }, + ], + [ + ["--verify-max-time"], + { + "type": int, + "default": 3600, + "help": "Maximum time, in seconds, to run in --verify mode.", + }, + ], + [ + ["--profiler"], + { + "action": "store_true", + "dest": "profiler", + "default": False, + "help": "Run the Firefox Profiler and get a performance profile of the " + "mochitest. This is useful to find performance issues, and also " + "to see what exactly the test is doing. To get profiler options run: " + "`MOZ_PROFILER_HELP=1 ./mach run`", + }, + ], + [ + ["--profiler-save-only"], + { + "action": "store_true", + "dest": "profilerSaveOnly", + "default": False, + "help": "Run the Firefox Profiler and save it to the path specified by the " + "MOZ_UPLOAD_DIR environment variable.", + }, + ], + [ + ["--run-failures"], + { + "action": "store", + "dest": "runFailures", + "default": "", + "help": "Run fail-if/skip-if tests that match a keyword given.", + }, + ], + [ + ["--timeout-as-pass"], + { + "action": "store_true", + "dest": "timeoutAsPass", + "default": False, + "help": "treat harness level timeouts as passing (used for quarantine jobs).", + }, + ], + [ + ["--crash-as-pass"], + { + "action": "store_true", + "dest": "crashAsPass", + "default": False, + "help": "treat harness level crashes as passing (used for quarantine jobs).", + }, + ], + ] + + defaults = { + # Bug 1065098 - The gmplugin process fails to produce a leak + # log for some reason. + "ignoreMissingLeaks": ["gmplugin"], + "extensionsToExclude": ["specialpowers"], + # Set server information on the args object + "webServer": "127.0.0.1", + "httpPort": DEFAULT_PORTS["http"], + "sslPort": DEFAULT_PORTS["https"], + "webSocketPort": "9988", + # The default websocket port is incorrect in mozprofile; it is + # set to the SSL proxy setting. See: + # see https://bugzilla.mozilla.org/show_bug.cgi?id=916517 + # args.webSocketPort = DEFAULT_PORTS['ws'] + } + + def validate(self, parser, options, context): + """Validate generic options.""" + + # and android doesn't use 'app' the same way, so skip validation + if parser.app != "android": + if options.app is None: + if build_obj: + from mozbuild.base import BinaryNotFoundException + + try: + options.app = build_obj.get_binary_path() + except BinaryNotFoundException as e: + print("{}\n\n{}\n".format(e, e.help())) + sys.exit(1) + else: + parser.error( + "could not find the application path, --appname must be specified" + ) + elif options.app == "dist" and build_obj: + options.app = build_obj.get_binary_path(where="staged-package") + + options.app = self.get_full_path(options.app, parser.oldcwd) + if not os.path.exists(options.app): + parser.error( + "Error: Path {} doesn't exist. Are you executing " + "$objdir/_tests/testing/mochitest/runtests.py?".format(options.app) + ) + + if options.flavor is None: + options.flavor = "plain" + + for value in ALL_FLAVORS.values(): + if options.flavor in value["aliases"]: + options.flavor = value["suite"] + break + + if options.gmp_path is None and options.app and build_obj: + # Need to fix the location of gmp_fake which might not be shipped in the binary + gmp_modules = ( + ("gmp-fake", "1.0"), + ("gmp-clearkey", "0.1"), + ("gmp-fakeopenh264", "1.0"), + ) + options.gmp_path = os.pathsep.join( + os.path.join(build_obj.bindir, *p) for p in gmp_modules + ) + + if options.totalChunks is not None and options.thisChunk is None: + parser.error("thisChunk must be specified when totalChunks is specified") + + if options.extra_mozinfo_json: + if not os.path.isfile(options.extra_mozinfo_json): + parser.error( + "Error: couldn't find mozinfo.json at '%s'." + % options.extra_mozinfo_json + ) + + options.extra_mozinfo_json = json.load(open(options.extra_mozinfo_json)) + + if options.totalChunks: + if not 1 <= options.thisChunk <= options.totalChunks: + parser.error("thisChunk must be between 1 and totalChunks") + + if options.chunkByDir and options.chunkByRuntime: + parser.error("can only use one of --chunk-by-dir or --chunk-by-runtime") + + if options.xrePath is None: + # default xrePath to the app path if not provided + # but only if an app path was explicitly provided + if options.app != parser.get_default("app"): + options.xrePath = os.path.dirname(options.app) + if mozinfo.isMac: + options.xrePath = os.path.join( + os.path.dirname(options.xrePath), "Resources" + ) + elif build_obj is not None: + # otherwise default to dist/bin + options.xrePath = build_obj.bindir + else: + parser.error( + "could not find xre directory, --xre-path must be specified" + ) + + # allow relative paths + if options.xrePath: + options.xrePath = self.get_full_path(options.xrePath, parser.oldcwd) + + if options.profilePath: + options.profilePath = self.get_full_path(options.profilePath, parser.oldcwd) + + if options.utilityPath: + options.utilityPath = self.get_full_path(options.utilityPath, parser.oldcwd) + + if options.certPath: + options.certPath = self.get_full_path(options.certPath, parser.oldcwd) + elif build_obj: + options.certPath = os.path.join( + build_obj.topsrcdir, "build", "pgo", "certs" + ) + + if options.symbolsPath and len(urlparse(options.symbolsPath).scheme) < 2: + options.symbolsPath = self.get_full_path(options.symbolsPath, parser.oldcwd) + elif not options.symbolsPath and build_obj: + options.symbolsPath = os.path.join( + build_obj.distdir, "crashreporter-symbols" + ) + + if options.debugOnFailure and not options.jsdebugger: + parser.error("--debug-on-failure requires --jsdebugger.") + + if options.jsdebuggerPath and not options.jsdebugger: + parser.error("--jsdebugger-path requires --jsdebugger.") + + if options.debuggerArgs and not options.debugger: + parser.error("--debugger-args requires --debugger.") + + if options.valgrind or options.debugger: + # valgrind and some debuggers may cause Gecko to start slowly. Make sure + # marionette waits long enough to connect. + options.marionette_startup_timeout = 900 + options.marionette_socket_timeout = 540 + + if options.store_chrome_manifest: + options.store_chrome_manifest = os.path.abspath( + options.store_chrome_manifest + ) + if not os.path.isdir(os.path.dirname(options.store_chrome_manifest)): + parser.error( + "directory for %s does not exist as a destination to copy a " + "chrome manifest." % options.store_chrome_manifest + ) + + if options.jscov_dir_prefix: + options.jscov_dir_prefix = os.path.abspath(options.jscov_dir_prefix) + if not os.path.isdir(options.jscov_dir_prefix): + parser.error( + "directory %s does not exist as a destination for coverage " + "data." % options.jscov_dir_prefix + ) + + if options.testingModulesDir is None: + # Try to guess the testing modules directory. + possible = [os.path.join(here, os.path.pardir, "modules")] + if build_obj: + possible.insert( + 0, os.path.join(build_obj.topobjdir, "_tests", "modules") + ) + + for p in possible: + if os.path.isdir(p): + options.testingModulesDir = p + break + + # Paths to specialpowers and mochijar from the tests archive. + options.stagedAddons = [ + os.path.join(here, "extensions", "specialpowers"), + os.path.join(here, "mochijar"), + ] + if build_obj: + objdir_xpi_stage = os.path.join(build_obj.distdir, "xpi-stage") + if os.path.isdir(objdir_xpi_stage): + options.stagedAddons = [ + os.path.join(objdir_xpi_stage, "specialpowers"), + os.path.join(objdir_xpi_stage, "mochijar"), + ] + plugins_dir = os.path.join(build_obj.distdir, "plugins") + if ( + os.path.isdir(plugins_dir) + and plugins_dir not in options.extraProfileFiles + ): + options.extraProfileFiles.append(plugins_dir) + + # Even if buildbot is updated, we still want this, as the path we pass in + # to the app must be absolute and have proper slashes. + if options.testingModulesDir is not None: + options.testingModulesDir = os.path.normpath(options.testingModulesDir) + + if not os.path.isabs(options.testingModulesDir): + options.testingModulesDir = os.path.abspath(options.testingModulesDir) + + if not os.path.isdir(options.testingModulesDir): + parser.error( + "--testing-modules-dir not a directory: %s" + % options.testingModulesDir + ) + + options.testingModulesDir = options.testingModulesDir.replace("\\", "/") + if options.testingModulesDir[-1] != "/": + options.testingModulesDir += "/" + + if options.runUntilFailure: + if not options.repeat: + options.repeat = 29 + + if options.dumpOutputDirectory is None: + options.dumpOutputDirectory = tempfile.gettempdir() + + if options.dumpAboutMemoryAfterTest or options.dumpDMDAfterTest: + if not os.path.isdir(options.dumpOutputDirectory): + parser.error( + "--dump-output-directory not a directory: %s" + % options.dumpOutputDirectory + ) + + if options.useTestMediaDevices: + if not mozinfo.isLinux: + parser.error( + "--use-test-media-devices is only supported on Linux currently" + ) + + gst01 = spawn.find_executable("gst-launch-0.1") + gst010 = spawn.find_executable("gst-launch-0.10") + gst10 = spawn.find_executable("gst-launch-1.0") + pactl = spawn.find_executable("pactl") + + if not (gst01 or gst10 or gst010): + parser.error( + "Missing gst-launch-{0.1,0.10,1.0}, required for " + "--use-test-media-devices" + ) + + if not pactl: + parser.error( + "Missing binary pactl required for " "--use-test-media-devices" + ) + + # The a11y and chrome flavors can't run with e10s. + if options.flavor in ("a11y", "chrome") and options.e10s: + parser.error( + "mochitest-{} does not support e10s, try again with " + "--disable-e10s.".format(options.flavor) + ) + + # If e10s explicitly disabled and no fission option specified, disable fission + if (not options.e10s) and (not options.disable_fission): + options.disable_fission = True + + options.leakThresholds = { + "default": options.defaultLeakThreshold, + "tab": options.defaultLeakThreshold, + "forkserver": options.defaultLeakThreshold, + # GMP rarely gets a log, but when it does, it leaks a little. + "gmplugin": 20000, + } + + # See the dependencies of bug 1401764. + if mozinfo.isWin: + options.leakThresholds["tab"] = 1000 + + # XXX We can't normalize test_paths in the non build_obj case here, + # because testRoot depends on the flavor, which is determined by the + # mach command and therefore not finalized yet. Conversely, test paths + # need to be normalized here for the mach case. + if options.test_paths and build_obj: + # Normalize test paths so they are relative to test root + options.test_paths = [ + build_obj._wrap_path_argument(p).relpath() for p in options.test_paths + ] + + return options + + +class AndroidArguments(ArgumentContainer): + """Android specific arguments.""" + + args = [ + [ + ["--no-install"], + { + "action": "store_true", + "default": False, + "help": "Skip the installation of the APK.", + }, + ], + [ + ["--aab"], + { + "action": "store_true", + "default": False, + "help": "Install the test_runner app using AAB.", + }, + ], + [ + ["--deviceSerial"], + { + "dest": "deviceSerial", + "help": "adb serial number of remote device. This is required " + "when more than one device is connected to the host. " + "Use 'adb devices' to see connected devices.", + "default": None, + }, + ], + [ + ["--adbpath"], + { + "dest": "adbPath", + "default": None, + "help": "Path to adb binary.", + "suppress": True, + }, + ], + [ + ["--remote-webserver"], + { + "dest": "remoteWebServer", + "default": None, + "help": "IP address of the remote web server.", + }, + ], + [ + ["--http-port"], + { + "dest": "httpPort", + "default": DEFAULT_PORTS["http"], + "help": "http port of the remote web server.", + "suppress": True, + }, + ], + [ + ["--ssl-port"], + { + "dest": "sslPort", + "default": DEFAULT_PORTS["https"], + "help": "ssl port of the remote web server.", + "suppress": True, + }, + ], + [ + ["--remoteTestRoot"], + { + "dest": "remoteTestRoot", + "default": None, + "help": "Remote directory to use as test root " + "(eg. /data/local/tmp/test_root).", + "suppress": True, + }, + ], + [ + ["--enable-coverage"], + { + "action": "store_true", + "default": False, + "help": "Enable collecting code coverage information when running " + "junit tests.", + }, + ], + [ + ["--coverage-output-dir"], + { + "action": "store", + "default": None, + "help": "When using --enable-java-coverage, save the code coverage report " + "files to this directory.", + }, + ], + ] + + defaults = { + # we don't want to exclude specialpowers on android just yet + "extensionsToExclude": [], + # mochijar doesn't get installed via marionette on android + "extensionsToInstall": [os.path.join(here, "mochijar")], + "logFile": "mochitest.log", + "utilityPath": None, + } + + def validate(self, parser, options, context): + """Validate android options.""" + + if build_obj: + options.log_mach = "-" + + objdir_xpi_stage = os.path.join(build_obj.distdir, "xpi-stage") + if os.path.isdir(objdir_xpi_stage): + options.extensionsToInstall = [ + os.path.join(objdir_xpi_stage, "mochijar"), + os.path.join(objdir_xpi_stage, "specialpowers"), + ] + + if options.remoteWebServer is None: + options.remoteWebServer = moznetwork.get_ip() + + options.webServer = options.remoteWebServer + + if options.app is None: + options.app = "org.mozilla.geckoview.test_runner" + + if build_obj and "MOZ_HOST_BIN" in os.environ: + options.xrePath = os.environ["MOZ_HOST_BIN"] + + # Only reset the xrePath if it wasn't provided + if options.xrePath is None: + options.xrePath = options.utilityPath + + if build_obj: + options.topsrcdir = build_obj.topsrcdir + + if options.pidFile != "": + f = open(options.pidFile, "w") + f.write("%s" % os.getpid()) + f.close() + + if options.coverage_output_dir and not options.enable_coverage: + parser.error("--coverage-output-dir must be used with --enable-coverage") + if options.enable_coverage: + if not options.autorun: + parser.error("--enable-coverage cannot be used with --no-autorun") + if not options.coverage_output_dir: + parser.error( + "--coverage-output-dir must be specified when using --enable-coverage" + ) + parent_dir = os.path.dirname(options.coverage_output_dir) + if not os.path.isdir(options.coverage_output_dir): + parser.error( + "The directory for the coverage output does not exist: %s" + % parent_dir + ) + + # allow us to keep original application around for cleanup while + # running tests + options.remoteappname = options.app + return options + + +container_map = { + "generic": [MochitestArguments], + "android": [MochitestArguments, AndroidArguments], +} + + +class MochitestArgumentParser(ArgumentParser): + """%(prog)s [options] [test paths]""" + + _containers = None + context = {} + + def __init__(self, app=None, **kwargs): + ArgumentParser.__init__( + self, usage=self.__doc__, conflict_handler="resolve", **kwargs + ) + + self.oldcwd = os.getcwd() + self.app = app + if not self.app and build_obj: + if conditions.is_android(build_obj): + self.app = "android" + if not self.app: + # platform can't be determined and app wasn't specified explicitly, + # so just use generic arguments and hope for the best + self.app = "generic" + + if self.app not in container_map: + self.error( + "Unrecognized app '{}'! Must be one of: {}".format( + self.app, ", ".join(container_map.keys()) + ) + ) + + defaults = {} + for container in self.containers: + defaults.update(container.defaults) + group = self.add_argument_group( + container.__class__.__name__, container.__doc__ + ) + + for cli, kwargs in container.args: + # Allocate new lists so references to original don't get mutated. + # allowing multiple uses within a single process. + if "default" in kwargs and isinstance(kwargs["default"], list): + kwargs["default"] = [] + + if "suppress" in kwargs: + if kwargs["suppress"]: + kwargs["help"] = SUPPRESS + del kwargs["suppress"] + + group.add_argument(*cli, **kwargs) + + self.set_defaults(**defaults) + mozlog.commandline.add_logging_group(self) + + @property + def containers(self): + if self._containers: + return self._containers + + containers = container_map[self.app] + self._containers = [c() for c in containers] + return self._containers + + def validate(self, args): + for container in self.containers: + args = container.validate(self, args, self.context) + return args diff --git a/testing/mochitest/moz.build b/testing/mochitest/moz.build new file mode 100644 index 0000000000..edae41386e --- /dev/null +++ b/testing/mochitest/moz.build @@ -0,0 +1,211 @@ +# -*- 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 += [ + "manifests", + "tests", + "ssltunnel", + "BrowserTestUtils", +] + +XPI_NAME = "mochijar" + +USE_EXTENSION_MANIFEST = True + +FINAL_TARGET_FILES += [ + "api.js", + "manifest.json", + "schema.json", +] + +FINAL_TARGET_FILES.content += [ + "browser-harness.xhtml", + "browser-test.js", + "chrome-harness.js", + "chunkifyTests.js", + "harness.xhtml", + "manifestLibrary.js", + "mochitest-e10s-utils.js", + "redirect.html", + "server.js", + "shutdown-leaks-collector.js", + "ShutdownLeaksCollector.sys.mjs", +] + +FINAL_TARGET_FILES.content.dynamic += [ + "dynamic/getMyDirectory.sjs", +] + +FINAL_TARGET_FILES.content.static += [ + "static/harness.css", +] + +FINAL_TARGET_FILES.content.tests.SimpleTest += [ + "../../docshell/test/chrome/docshell_helpers.js", + "../modules/StructuredLog.sys.mjs", + "tests/SimpleTest/AccessibilityUtils.js", + "tests/SimpleTest/EventUtils.js", + "tests/SimpleTest/ExtensionTestUtils.js", + "tests/SimpleTest/iframe-between-tests.html", + "tests/SimpleTest/LogController.js", + "tests/SimpleTest/MemoryStats.js", + "tests/SimpleTest/MockObjects.js", + "tests/SimpleTest/MozillaLogger.js", + "tests/SimpleTest/NativeKeyCodes.js", + "tests/SimpleTest/paint_listener.js", + "tests/SimpleTest/setup.js", + "tests/SimpleTest/SimpleTest.js", + "tests/SimpleTest/test.css", + "tests/SimpleTest/TestRunner.js", + "tests/SimpleTest/WindowSnapshot.js", + "tests/SimpleTest/WorkerHandler.js", + "tests/SimpleTest/WorkerSimpleTest.js", +] + +FINAL_TARGET_FILES.content.tests.BrowserTestUtils += [ + "BrowserTestUtils/content/content-about-page-utils.js", + "BrowserTestUtils/content/content-task.js", +] + +MOCHITEST_MANIFESTS += [ + "baselinecoverage/plain/mochitest.ini", + "tests/MochiKit-1.4.2/tests/mochitest.ini", +] + +MOCHITEST_CHROME_MANIFESTS += [ + "baselinecoverage/chrome/chrome.ini", + "chrome/chrome.ini", +] + +BROWSER_CHROME_MANIFESTS += ["baselinecoverage/browser_chrome/browser.ini"] + +TEST_HARNESS_FILES.testing.mochitest += [ + "/build/pgo/server-locations.txt", + "/build/valgrind/cross-architecture.sup", + "/build/valgrind/i386-pc-linux-gnu.sup", + "/build/valgrind/x86_64-pc-linux-gnu.sup", + "/netwerk/test/httpserver/httpd.js", + "bisection.py", + "browser-harness.xhtml", + "browser-test.js", + "chrome-harness.js", + "chunkifyTests.js", + "document-builder.sjs", + "favicon.ico", + "harness.xhtml", + "leaks.py", + "mach_test_package_commands.py", + "manifest.webapp", + "manifestLibrary.js", + "mochitest_options.py", + "pywebsocket_wrapper.py", + "redirect.html", + "runjunit.py", + "runtests.py", + "runtestsremote.py", + "server.js", + "start_desktop.js", +] + +TEST_HARNESS_FILES.testing.mochitest.embed += [ + "embed/Xm5i5kbIXzc", + "embed/Xm5i5kbIXzc^headers^", +] + +TEST_HARNESS_FILES.testing.mochitest.pywebsocket3.mod_pywebsocket += [ + "pywebsocket3/mod_pywebsocket/__init__.py", + "pywebsocket3/mod_pywebsocket/_stream_exceptions.py", + "pywebsocket3/mod_pywebsocket/common.py", + "pywebsocket3/mod_pywebsocket/dispatch.py", + "pywebsocket3/mod_pywebsocket/extensions.py", + "pywebsocket3/mod_pywebsocket/fast_masking.i", + "pywebsocket3/mod_pywebsocket/http_header_util.py", + "pywebsocket3/mod_pywebsocket/memorizingfile.py", + "pywebsocket3/mod_pywebsocket/msgutil.py", + "pywebsocket3/mod_pywebsocket/request_handler.py", + "pywebsocket3/mod_pywebsocket/server_util.py", + "pywebsocket3/mod_pywebsocket/standalone.py", + "pywebsocket3/mod_pywebsocket/stream.py", + "pywebsocket3/mod_pywebsocket/util.py", + "pywebsocket3/mod_pywebsocket/websocket_server.py", +] + +TEST_HARNESS_FILES.testing.mochitest.pywebsocket3.mod_pywebsocket.handshake += [ + "pywebsocket3/mod_pywebsocket/handshake/__init__.py", + "pywebsocket3/mod_pywebsocket/handshake/_base.py", + "pywebsocket3/mod_pywebsocket/handshake/hybi.py", +] + +TEST_HARNESS_FILES.testing.mochitest.dynamic += [ + "dynamic/getMyDirectory.sjs", +] + +TEST_HARNESS_FILES.testing.mochitest.static += [ + "static/harness.css", +] + +TEST_HARNESS_FILES.testing.mochitest.MochiKit += [ + "MochiKit/__package__.js", + "MochiKit/Async.js", + "MochiKit/Base.js", + "MochiKit/Color.js", + "MochiKit/Controls.js", + "MochiKit/DateTime.js", + "MochiKit/DOM.js", + "MochiKit/DragAndDrop.js", + "MochiKit/Format.js", + "MochiKit/Iter.js", + "MochiKit/Logging.js", + "MochiKit/LoggingPane.js", + "MochiKit/MochiKit.js", + "MochiKit/MockDOM.js", + "MochiKit/New.js", + "MochiKit/Signal.js", + "MochiKit/Sortable.js", + "MochiKit/Style.js", + "MochiKit/Test.js", + "MochiKit/Visual.js", +] + +TEST_HARNESS_FILES.testing.mochitest.tests.testing.mochitest.tests[ + "MochiKit-1.4.2" +].MochiKit += [ + "tests/MochiKit-1.4.2/MochiKit/Async.js", + "tests/MochiKit-1.4.2/MochiKit/Base.js", + "tests/MochiKit-1.4.2/MochiKit/Color.js", + "tests/MochiKit-1.4.2/MochiKit/DateTime.js", + "tests/MochiKit-1.4.2/MochiKit/DOM.js", + "tests/MochiKit-1.4.2/MochiKit/DragAndDrop.js", + "tests/MochiKit-1.4.2/MochiKit/Format.js", + "tests/MochiKit-1.4.2/MochiKit/Iter.js", + "tests/MochiKit-1.4.2/MochiKit/Logging.js", + "tests/MochiKit-1.4.2/MochiKit/LoggingPane.js", + "tests/MochiKit-1.4.2/MochiKit/MochiKit.js", + "tests/MochiKit-1.4.2/MochiKit/MockDOM.js", + "tests/MochiKit-1.4.2/MochiKit/Position.js", + "tests/MochiKit-1.4.2/MochiKit/Selector.js", + "tests/MochiKit-1.4.2/MochiKit/Signal.js", + "tests/MochiKit-1.4.2/MochiKit/Sortable.js", + "tests/MochiKit-1.4.2/MochiKit/Style.js", + "tests/MochiKit-1.4.2/MochiKit/Test.js", + "tests/MochiKit-1.4.2/MochiKit/Visual.js", +] + +TEST_HARNESS_FILES.testing.mochitest.iceserver += [ + "/testing/tools/iceserver/iceserver.py", +] + +TEST_HARNESS_FILES.testing.mochitest.websocketprocessbridge += [ + "/testing/tools/websocketprocessbridge/websocketprocessbridge.py", + "/testing/tools/websocketprocessbridge/websocketprocessbridge_requirements_3.txt", +] + +with Files("**"): + BUG_COMPONENT = ("Testing", "Mochitest") + SCHEDULES.exclusive = ["mochitest"] + +with Files("*remote*"): + BUG_COMPONENT = ("GeckoView", "General") diff --git a/testing/mochitest/pywebsocket3/CONTRIBUTING b/testing/mochitest/pywebsocket3/CONTRIBUTING new file mode 100644 index 0000000000..f975be126f --- /dev/null +++ b/testing/mochitest/pywebsocket3/CONTRIBUTING @@ -0,0 +1,30 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution; +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to <https://cla.developers.google.com/> to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. +For instructions for contributing code, please read: +https://github.com/google/pywebsocket/wiki/CodeReviewInstruction + +## Community Guidelines + +This project follows +[Google's Open Source Community Guidelines](https://opensource.google/conduct/). diff --git a/testing/mochitest/pywebsocket3/LICENSE b/testing/mochitest/pywebsocket3/LICENSE new file mode 100644 index 0000000000..c91bea9025 --- /dev/null +++ b/testing/mochitest/pywebsocket3/LICENSE @@ -0,0 +1,28 @@ +Copyright 2020, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/testing/mochitest/pywebsocket3/README-MOZILLA b/testing/mochitest/pywebsocket3/README-MOZILLA new file mode 100644 index 0000000000..e4d3960193 --- /dev/null +++ b/testing/mochitest/pywebsocket3/README-MOZILLA @@ -0,0 +1,74 @@ +This pywebsocket code is mostly unchanged from the source at + + https://github.com/GoogleChromeLabs/pywebsocket3 + +-------------------------------------------------------------------------------- +STEPS TO UPDATE MOZILLA TO NEWER PYWEBSOCKET VERSION +-------------------------------------------------------------------------------- +- Get new pywebsocket checkout from googlecode (into, for instance, 'src') + + git clone https://github.com/GoogleChromeLabs/pywebsocket3 pywebsocket-read-only + cp -r pywebsocket-read-only/mod_pywebsocket testing/mochitest/pywebsocket3 + +- hg add/rm appropriate files, and add/remove them from + testing/mochitest/moz.build + +- We need to apply the patch to hybi.py that makes HSTS work: (attached at end + of this README) + +- Test and make sure the code works: + + mach mochitest dom/websocket/tests + +- If this doesn't take a look at the pywebsocket server log, + $OBJDIR/_tests/testing/mochitest/websock.log + +-------------------------------------------------------------------------------- +PATCH TO hybi.py for HSTS support: + + +diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/hybi.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/hybi.py +--- a/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/hybi.py ++++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/hybi.py +@@ -273,16 +273,19 @@ class Handshaker(object): + status=common.HTTP_STATUS_BAD_REQUEST) + raise VersionException('Unsupported version %r for header %s' % + (version, common.SEC_WEBSOCKET_VERSION_HEADER), + supported_versions=', '.join( + map(str, _SUPPORTED_VERSIONS))) + + def _set_protocol(self): + self._request.ws_protocol = None ++ # MOZILLA ++ self._request.sts = None ++ # /MOZILLA + + protocol_header = self._request.headers_in.get( + common.SEC_WEBSOCKET_PROTOCOL_HEADER) + + if protocol_header is None: + self._request.ws_requested_protocols = None + return + +@@ -371,16 +374,21 @@ class Handshaker(object): + format_header(common.SEC_WEBSOCKET_PROTOCOL_HEADER, + self._request.ws_protocol)) + if (self._request.ws_extensions is not None + and len(self._request.ws_extensions) != 0): + response.append( + format_header( + common.SEC_WEBSOCKET_EXTENSIONS_HEADER, + common.format_extensions(self._request.ws_extensions))) ++ # MOZILLA ++ if self._request.sts is not None: ++ response.append(format_header("Strict-Transport-Security", ++ self._request.sts)) ++ # /MOZILLA + + # Headers not specific for WebSocket + for name, value in self._request.extra_headers: + response.append(format_header(name, value)) + + response.append(u'\r\n') + + return u''.join(response) diff --git a/testing/mochitest/pywebsocket3/README.md b/testing/mochitest/pywebsocket3/README.md new file mode 100644 index 0000000000..277cbe550c --- /dev/null +++ b/testing/mochitest/pywebsocket3/README.md @@ -0,0 +1,36 @@ + +# pywebsocket3 # + +The pywebsocket project aims to provide a [WebSocket](https://tools.ietf.org/html/rfc6455) standalone server. + +pywebsocket is intended for **testing** or **experimental** purposes. + +Run this to read the general document: +``` +$ pydoc mod_pywebsocket +``` + +Please see [Wiki](../../wiki) for more details. + +# INSTALL # + +To install this package to the system, run this: +``` +$ python setup.py build +$ sudo python setup.py install +``` + +To install this package as a normal user, run this instead: + +``` +$ python setup.py build +$ python setup.py install --user +``` +# LAUNCH # + +To use pywebsocket as standalone server, run this to read the document: +``` +$ pydoc mod_pywebsocket.standalone +``` +# Disclaimer # +This is not an officially supported Google product diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/__init__.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/__init__.py new file mode 100644 index 0000000000..28d5f5950f --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/__init__.py @@ -0,0 +1,172 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" A Standalone WebSocket Server for testing purposes + +mod_pywebsocket is an API that provides WebSocket functionalities with +a standalone WebSocket server. It is intended for testing or +experimental purposes. + +Installation +============ +1. Follow standalone server documentation to start running the +standalone server. It can be read by running the following command: + + $ pydoc mod_pywebsocket.standalone + +2. Once the standalone server is launched verify it by accessing +http://localhost[:port]/console.html. Include the port number when +specified on launch. If everything is working correctly, you +will see a simple echo console. + + +Writing WebSocket handlers +========================== + +When a WebSocket request comes in, the resource name +specified in the handshake is considered as if it is a file path under +<websock_handlers> and the handler defined in +<websock_handlers>/<resource_name>_wsh.py is invoked. + +For example, if the resource name is /example/chat, the handler defined in +<websock_handlers>/example/chat_wsh.py is invoked. + +A WebSocket handler is composed of the following three functions: + + web_socket_do_extra_handshake(request) + web_socket_transfer_data(request) + web_socket_passive_closing_handshake(request) + +where: + request: mod_python request. + +web_socket_do_extra_handshake is called during the handshake after the +headers are successfully parsed and WebSocket properties (ws_origin, +and ws_resource) are added to request. A handler +can reject the request by raising an exception. + +A request object has the following properties that you can use during the +extra handshake (web_socket_do_extra_handshake): +- ws_resource +- ws_origin +- ws_version +- ws_extensions +- ws_deflate +- ws_protocol +- ws_requested_protocols + +The last two are a bit tricky. See the next subsection. + + +Subprotocol Negotiation +----------------------- + +ws_protocol is always set to None when +web_socket_do_extra_handshake is called. If ws_requested_protocols is not +None, you must choose one subprotocol from this list and set it to +ws_protocol. + +Data Transfer +------------- + +web_socket_transfer_data is called after the handshake completed +successfully. A handler can receive/send messages from/to the client +using request. mod_pywebsocket.msgutil module provides utilities +for data transfer. + +You can receive a message by the following statement. + + message = request.ws_stream.receive_message() + +This call blocks until any complete text frame arrives, and the payload data +of the incoming frame will be stored into message. When you're using IETF +HyBi 00 or later protocol, receive_message() will return None on receiving +client-initiated closing handshake. When any error occurs, receive_message() +will raise some exception. + +You can send a message by the following statement. + + request.ws_stream.send_message(message) + + +Closing Connection +------------------ + +Executing the following statement or just return-ing from +web_socket_transfer_data cause connection close. + + request.ws_stream.close_connection() + +close_connection will wait +for closing handshake acknowledgement coming from the client. When it +couldn't receive a valid acknowledgement, raises an exception. + +web_socket_passive_closing_handshake is called after the server receives +incoming closing frame from the client peer immediately. You can specify +code and reason by return values. They are sent as a outgoing closing frame +from the server. A request object has the following properties that you can +use in web_socket_passive_closing_handshake. +- ws_close_code +- ws_close_reason + + +Threading +--------- + +A WebSocket handler must be thread-safe. The standalone +server uses threads by default. + + +Configuring WebSocket Extension Processors +------------------------------------------ + +See extensions.py for supported WebSocket extensions. Note that they are +unstable and their APIs are subject to change substantially. + +A request object has these extension processing related attributes. + +- ws_requested_extensions: + + A list of common.ExtensionParameter instances representing extension + parameters received from the client in the client's opening handshake. + You shouldn't modify it manually. + +- ws_extensions: + + A list of common.ExtensionParameter instances representing extension + parameters to send back to the client in the server's opening handshake. + You shouldn't touch it directly. Instead, call methods on extension + processors. + +- ws_extension_processors: + + A list of loaded extension processors. Find the processor for the + extension you want to configure from it, and call its methods. +""" + +# vi:sts=4 sw=4 et tw=72 diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/_stream_exceptions.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/_stream_exceptions.py new file mode 100644 index 0000000000..b47878bc4a --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/_stream_exceptions.py @@ -0,0 +1,82 @@ +# Copyright 2020, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Stream Exceptions. +""" + +# Note: request.connection.write/read are used in this module, even though +# mod_python document says that they should be used only in connection +# handlers. Unfortunately, we have no other options. For example, +# request.write/read are not suitable because they don't allow direct raw bytes +# writing/reading. + + +# Exceptions +class ConnectionTerminatedException(Exception): + """This exception will be raised when a connection is terminated + unexpectedly. + """ + + pass + + +class InvalidFrameException(ConnectionTerminatedException): + """This exception will be raised when we received an invalid frame we + cannot parse. + """ + + pass + + +class BadOperationException(Exception): + """This exception will be raised when send_message() is called on + server-terminated connection or receive_message() is called on + client-terminated connection. + """ + + pass + + +class UnsupportedFrameException(Exception): + """This exception will be raised when we receive a frame with flag, opcode + we cannot handle. Handlers can just catch and ignore this exception and + call receive_message() again to continue processing the next frame. + """ + + pass + + +class InvalidUTF8Exception(Exception): + """This exception will be raised when we receive a text frame which + contains invalid UTF-8 strings. + """ + + pass + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/common.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/common.py new file mode 100644 index 0000000000..9cb11f15cb --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/common.py @@ -0,0 +1,273 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""This file must not depend on any module specific to the WebSocket protocol. +""" + +from __future__ import absolute_import +from mod_pywebsocket import http_header_util + +# Additional log level definitions. +LOGLEVEL_FINE = 9 + +# Constants indicating WebSocket protocol version. +VERSION_HYBI13 = 13 +VERSION_HYBI14 = 13 +VERSION_HYBI15 = 13 +VERSION_HYBI16 = 13 +VERSION_HYBI17 = 13 + +# Constants indicating WebSocket protocol latest version. +VERSION_HYBI_LATEST = VERSION_HYBI13 + +# Port numbers +DEFAULT_WEB_SOCKET_PORT = 80 +DEFAULT_WEB_SOCKET_SECURE_PORT = 443 + +# Schemes +WEB_SOCKET_SCHEME = 'ws' +WEB_SOCKET_SECURE_SCHEME = 'wss' + +# Frame opcodes defined in the spec. +OPCODE_CONTINUATION = 0x0 +OPCODE_TEXT = 0x1 +OPCODE_BINARY = 0x2 +OPCODE_CLOSE = 0x8 +OPCODE_PING = 0x9 +OPCODE_PONG = 0xa + +# UUID for the opening handshake and frame masking. +WEBSOCKET_ACCEPT_UUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + +# Opening handshake header names and expected values. +UPGRADE_HEADER = 'Upgrade' +WEBSOCKET_UPGRADE_TYPE = 'websocket' +CONNECTION_HEADER = 'Connection' +UPGRADE_CONNECTION_TYPE = 'Upgrade' +HOST_HEADER = 'Host' +ORIGIN_HEADER = 'Origin' +SEC_WEBSOCKET_KEY_HEADER = 'Sec-WebSocket-Key' +SEC_WEBSOCKET_ACCEPT_HEADER = 'Sec-WebSocket-Accept' +SEC_WEBSOCKET_VERSION_HEADER = 'Sec-WebSocket-Version' +SEC_WEBSOCKET_PROTOCOL_HEADER = 'Sec-WebSocket-Protocol' +SEC_WEBSOCKET_EXTENSIONS_HEADER = 'Sec-WebSocket-Extensions' + +# Extensions +PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate' + +# Status codes +# Code STATUS_NO_STATUS_RECEIVED, STATUS_ABNORMAL_CLOSURE, and +# STATUS_TLS_HANDSHAKE are pseudo codes to indicate specific error cases. +# Could not be used for codes in actual closing frames. +# Application level errors must use codes in the range +# STATUS_USER_REGISTERED_BASE to STATUS_USER_PRIVATE_MAX. The codes in the +# range STATUS_USER_REGISTERED_BASE to STATUS_USER_REGISTERED_MAX are managed +# by IANA. Usually application must define user protocol level errors in the +# range STATUS_USER_PRIVATE_BASE to STATUS_USER_PRIVATE_MAX. +STATUS_NORMAL_CLOSURE = 1000 +STATUS_GOING_AWAY = 1001 +STATUS_PROTOCOL_ERROR = 1002 +STATUS_UNSUPPORTED_DATA = 1003 +STATUS_NO_STATUS_RECEIVED = 1005 +STATUS_ABNORMAL_CLOSURE = 1006 +STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007 +STATUS_POLICY_VIOLATION = 1008 +STATUS_MESSAGE_TOO_BIG = 1009 +STATUS_MANDATORY_EXTENSION = 1010 +STATUS_INTERNAL_ENDPOINT_ERROR = 1011 +STATUS_TLS_HANDSHAKE = 1015 +STATUS_USER_REGISTERED_BASE = 3000 +STATUS_USER_REGISTERED_MAX = 3999 +STATUS_USER_PRIVATE_BASE = 4000 +STATUS_USER_PRIVATE_MAX = 4999 +# Following definitions are aliases to keep compatibility. Applications must +# not use these obsoleted definitions anymore. +STATUS_NORMAL = STATUS_NORMAL_CLOSURE +STATUS_UNSUPPORTED = STATUS_UNSUPPORTED_DATA +STATUS_CODE_NOT_AVAILABLE = STATUS_NO_STATUS_RECEIVED +STATUS_ABNORMAL_CLOSE = STATUS_ABNORMAL_CLOSURE +STATUS_INVALID_FRAME_PAYLOAD = STATUS_INVALID_FRAME_PAYLOAD_DATA +STATUS_MANDATORY_EXT = STATUS_MANDATORY_EXTENSION + +# HTTP status codes +HTTP_STATUS_BAD_REQUEST = 400 +HTTP_STATUS_FORBIDDEN = 403 +HTTP_STATUS_NOT_FOUND = 404 + + +def is_control_opcode(opcode): + return (opcode >> 3) == 1 + + +class ExtensionParameter(object): + """This is exchanged on extension negotiation in opening handshake.""" + def __init__(self, name): + self._name = name + # TODO(tyoshino): Change the data structure to more efficient one such + # as dict when the spec changes to say like + # - Parameter names must be unique + # - The order of parameters is not significant + self._parameters = [] + + def name(self): + """Return the extension name.""" + return self._name + + def add_parameter(self, name, value): + """Add a parameter.""" + self._parameters.append((name, value)) + + def get_parameters(self): + """Return the parameters.""" + return self._parameters + + def get_parameter_names(self): + """Return the names of the parameters.""" + return [name for name, unused_value in self._parameters] + + def has_parameter(self, name): + """Test if a parameter exists.""" + for param_name, param_value in self._parameters: + if param_name == name: + return True + return False + + def get_parameter_value(self, name): + """Get the value of a specific parameter.""" + for param_name, param_value in self._parameters: + if param_name == name: + return param_value + + +class ExtensionParsingException(Exception): + """Exception to handle errors in extension parsing.""" + def __init__(self, name): + super(ExtensionParsingException, self).__init__(name) + + +def _parse_extension_param(state, definition): + param_name = http_header_util.consume_token(state) + + if param_name is None: + raise ExtensionParsingException('No valid parameter name found') + + http_header_util.consume_lwses(state) + + if not http_header_util.consume_string(state, '='): + definition.add_parameter(param_name, None) + return + + http_header_util.consume_lwses(state) + + # TODO(tyoshino): Add code to validate that parsed param_value is token + param_value = http_header_util.consume_token_or_quoted_string(state) + if param_value is None: + raise ExtensionParsingException( + 'No valid parameter value found on the right-hand side of ' + 'parameter %r' % param_name) + + definition.add_parameter(param_name, param_value) + + +def _parse_extension(state): + extension_token = http_header_util.consume_token(state) + if extension_token is None: + return None + + extension = ExtensionParameter(extension_token) + + while True: + http_header_util.consume_lwses(state) + + if not http_header_util.consume_string(state, ';'): + break + + http_header_util.consume_lwses(state) + + try: + _parse_extension_param(state, extension) + except ExtensionParsingException as e: + raise ExtensionParsingException( + 'Failed to parse parameter for %r (%r)' % (extension_token, e)) + + return extension + + +def parse_extensions(data): + """Parse Sec-WebSocket-Extensions header value. + + Returns a list of ExtensionParameter objects. + Leading LWSes must be trimmed. + """ + state = http_header_util.ParsingState(data) + + extension_list = [] + while True: + extension = _parse_extension(state) + if extension is not None: + extension_list.append(extension) + + http_header_util.consume_lwses(state) + + if http_header_util.peek(state) is None: + break + + if not http_header_util.consume_string(state, ','): + raise ExtensionParsingException( + 'Failed to parse Sec-WebSocket-Extensions header: ' + 'Expected a comma but found %r' % http_header_util.peek(state)) + + http_header_util.consume_lwses(state) + + if len(extension_list) == 0: + raise ExtensionParsingException('No valid extension entry found') + + return extension_list + + +def format_extension(extension): + """Format an ExtensionParameter object.""" + formatted_params = [extension.name()] + for param_name, param_value in extension.get_parameters(): + if param_value is None: + formatted_params.append(param_name) + else: + quoted_value = http_header_util.quote_if_necessary(param_value) + formatted_params.append('%s=%s' % (param_name, quoted_value)) + return '; '.join(formatted_params) + + +def format_extensions(extension_list): + """Format a list of ExtensionParameter objects.""" + formatted_extension_list = [] + for extension in extension_list: + formatted_extension_list.append(format_extension(extension)) + return ', '.join(formatted_extension_list) + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/dispatch.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/dispatch.py new file mode 100644 index 0000000000..4ee943a5b8 --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/dispatch.py @@ -0,0 +1,385 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Dispatch WebSocket request. +""" + +from __future__ import absolute_import +import logging +import os +import re +import traceback + +from mod_pywebsocket import common +from mod_pywebsocket import handshake +from mod_pywebsocket import msgutil +from mod_pywebsocket import stream +from mod_pywebsocket import util + +_SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$') +_SOURCE_SUFFIX = '_wsh.py' +_DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake' +_TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data' +_PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME = ( + 'web_socket_passive_closing_handshake') + + +class DispatchException(Exception): + """Exception in dispatching WebSocket request.""" + def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND): + super(DispatchException, self).__init__(name) + self.status = status + + +def _default_passive_closing_handshake_handler(request): + """Default web_socket_passive_closing_handshake handler.""" + + return common.STATUS_NORMAL_CLOSURE, '' + + +def _normalize_path(path): + """Normalize path. + + Args: + path: the path to normalize. + + Path is converted to the absolute path. + The input path can use either '\\' or '/' as the separator. + The normalized path always uses '/' regardless of the platform. + """ + + path = path.replace('\\', os.path.sep) + path = os.path.realpath(path) + path = path.replace('\\', '/') + return path + + +def _create_path_to_resource_converter(base_dir): + """Returns a function that converts the path of a WebSocket handler source + file to a resource string by removing the path to the base directory from + its head, removing _SOURCE_SUFFIX from its tail, and replacing path + separators in it with '/'. + + Args: + base_dir: the path to the base directory. + """ + + base_dir = _normalize_path(base_dir) + + base_len = len(base_dir) + suffix_len = len(_SOURCE_SUFFIX) + + def converter(path): + if not path.endswith(_SOURCE_SUFFIX): + return None + # _normalize_path must not be used because resolving symlink breaks + # following path check. + path = path.replace('\\', '/') + if not path.startswith(base_dir): + return None + return path[base_len:-suffix_len] + + return converter + + +def _enumerate_handler_file_paths(directory): + """Returns a generator that enumerates WebSocket Handler source file names + in the given directory. + """ + + for root, unused_dirs, files in os.walk(directory): + for base in files: + path = os.path.join(root, base) + if _SOURCE_PATH_PATTERN.search(path): + yield path + + +class _HandlerSuite(object): + """A handler suite holder class.""" + def __init__(self, do_extra_handshake, transfer_data, + passive_closing_handshake): + self.do_extra_handshake = do_extra_handshake + self.transfer_data = transfer_data + self.passive_closing_handshake = passive_closing_handshake + + +def _source_handler_file(handler_definition): + """Source a handler definition string. + + Args: + handler_definition: a string containing Python statements that define + handler functions. + """ + + global_dic = {} + try: + # This statement is gramatically different in python 2 and 3. + # Hence, yapf will complain about this. To overcome this, we disable + # yapf for this line. + exec(handler_definition, global_dic) # yapf: disable + except Exception: + raise DispatchException('Error in sourcing handler:' + + traceback.format_exc()) + passive_closing_handshake_handler = None + try: + passive_closing_handshake_handler = _extract_handler( + global_dic, _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME) + except Exception: + passive_closing_handshake_handler = ( + _default_passive_closing_handshake_handler) + return _HandlerSuite( + _extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME), + _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME), + passive_closing_handshake_handler) + + +def _extract_handler(dic, name): + """Extracts a callable with the specified name from the given dictionary + dic. + """ + + if name not in dic: + raise DispatchException('%s is not defined.' % name) + handler = dic[name] + if not callable(handler): + raise DispatchException('%s is not callable.' % name) + return handler + + +class Dispatcher(object): + """Dispatches WebSocket requests. + + This class maintains a map from resource name to handlers. + """ + def __init__(self, + root_dir, + scan_dir=None, + allow_handlers_outside_root_dir=True): + """Construct an instance. + + Args: + root_dir: The directory where handler definition files are + placed. + scan_dir: The directory where handler definition files are + searched. scan_dir must be a directory under root_dir, + including root_dir itself. If scan_dir is None, + root_dir is used as scan_dir. scan_dir can be useful + in saving scan time when root_dir contains many + subdirectories. + allow_handlers_outside_root_dir: Scans handler files even if their + canonical path is not under root_dir. + """ + + self._logger = util.get_class_logger(self) + + self._handler_suite_map = {} + self._source_warnings = [] + if scan_dir is None: + scan_dir = root_dir + if not os.path.realpath(scan_dir).startswith( + os.path.realpath(root_dir)): + raise DispatchException('scan_dir:%s must be a directory under ' + 'root_dir:%s.' % (scan_dir, root_dir)) + self._source_handler_files_in_dir(root_dir, scan_dir, + allow_handlers_outside_root_dir) + + def add_resource_path_alias(self, alias_resource_path, + existing_resource_path): + """Add resource path alias. + + Once added, request to alias_resource_path would be handled by + handler registered for existing_resource_path. + + Args: + alias_resource_path: alias resource path + existing_resource_path: existing resource path + """ + try: + handler_suite = self._handler_suite_map[existing_resource_path] + self._handler_suite_map[alias_resource_path] = handler_suite + except KeyError: + raise DispatchException('No handler for: %r' % + existing_resource_path) + + def source_warnings(self): + """Return warnings in sourcing handlers.""" + + return self._source_warnings + + def do_extra_handshake(self, request): + """Do extra checking in WebSocket handshake. + + Select a handler based on request.uri and call its + web_socket_do_extra_handshake function. + + Args: + request: mod_python request. + + Raises: + DispatchException: when handler was not found + AbortedByUserException: when user handler abort connection + HandshakeException: when opening handshake failed + """ + + handler_suite = self.get_handler_suite(request.ws_resource) + if handler_suite is None: + raise DispatchException('No handler for: %r' % request.ws_resource) + do_extra_handshake_ = handler_suite.do_extra_handshake + try: + do_extra_handshake_(request) + except handshake.AbortedByUserException as e: + # Re-raise to tell the caller of this function to finish this + # connection without sending any error. + self._logger.debug('%s', traceback.format_exc()) + raise + except Exception as e: + util.prepend_message_to_exception( + '%s raised exception for %s: ' % + (_DO_EXTRA_HANDSHAKE_HANDLER_NAME, request.ws_resource), e) + raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN) + + def transfer_data(self, request): + """Let a handler transfer_data with a WebSocket client. + + Select a handler based on request.ws_resource and call its + web_socket_transfer_data function. + + Args: + request: mod_python request. + + Raises: + DispatchException: when handler was not found + AbortedByUserException: when user handler abort connection + """ + + # TODO(tyoshino): Terminate underlying TCP connection if possible. + try: + handler_suite = self.get_handler_suite(request.ws_resource) + if handler_suite is None: + raise DispatchException('No handler for: %r' % + request.ws_resource) + transfer_data_ = handler_suite.transfer_data + transfer_data_(request) + + if not request.server_terminated: + request.ws_stream.close_connection() + # Catch non-critical exceptions the handler didn't handle. + except handshake.AbortedByUserException as e: + self._logger.debug('%s', traceback.format_exc()) + raise + except msgutil.BadOperationException as e: + self._logger.debug('%s', e) + request.ws_stream.close_connection( + common.STATUS_INTERNAL_ENDPOINT_ERROR) + except msgutil.InvalidFrameException as e: + # InvalidFrameException must be caught before + # ConnectionTerminatedException that catches InvalidFrameException. + self._logger.debug('%s', e) + request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR) + except msgutil.UnsupportedFrameException as e: + self._logger.debug('%s', e) + request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA) + except stream.InvalidUTF8Exception as e: + self._logger.debug('%s', e) + request.ws_stream.close_connection( + common.STATUS_INVALID_FRAME_PAYLOAD_DATA) + except msgutil.ConnectionTerminatedException as e: + self._logger.debug('%s', e) + except Exception as e: + # Any other exceptions are forwarded to the caller of this + # function. + util.prepend_message_to_exception( + '%s raised exception for %s: ' % + (_TRANSFER_DATA_HANDLER_NAME, request.ws_resource), e) + raise + + def passive_closing_handshake(self, request): + """Prepare code and reason for responding client initiated closing + handshake. + """ + + handler_suite = self.get_handler_suite(request.ws_resource) + if handler_suite is None: + return _default_passive_closing_handshake_handler(request) + return handler_suite.passive_closing_handshake(request) + + def get_handler_suite(self, resource): + """Retrieves two handlers (one for extra handshake processing, and one + for data transfer) for the given request as a HandlerSuite object. + """ + + fragment = None + if '#' in resource: + resource, fragment = resource.split('#', 1) + if '?' in resource: + resource = resource.split('?', 1)[0] + handler_suite = self._handler_suite_map.get(resource) + if handler_suite and fragment: + raise DispatchException( + 'Fragment identifiers MUST NOT be used on WebSocket URIs', + common.HTTP_STATUS_BAD_REQUEST) + return handler_suite + + def _source_handler_files_in_dir(self, root_dir, scan_dir, + allow_handlers_outside_root_dir): + """Source all the handler source files in the scan_dir directory. + + The resource path is determined relative to root_dir. + """ + + # We build a map from resource to handler code assuming that there's + # only one path from root_dir to scan_dir and it can be obtained by + # comparing realpath of them. + + # Here we cannot use abspath. See + # https://bugs.webkit.org/show_bug.cgi?id=31603 + + convert = _create_path_to_resource_converter(root_dir) + scan_realpath = os.path.realpath(scan_dir) + root_realpath = os.path.realpath(root_dir) + for path in _enumerate_handler_file_paths(scan_realpath): + if (not allow_handlers_outside_root_dir and + (not os.path.realpath(path).startswith(root_realpath))): + self._logger.debug( + 'Canonical path of %s is not under root directory' % path) + continue + try: + with open(path) as handler_file: + handler_suite = _source_handler_file(handler_file.read()) + except DispatchException as e: + self._source_warnings.append('%s: %s' % (path, e)) + continue + resource = convert(path) + if resource is None: + self._logger.debug('Path to resource conversion on %s failed' % + path) + else: + self._handler_suite_map[convert(path)] = handler_suite + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/extensions.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/extensions.py new file mode 100644 index 0000000000..344539f6fc --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/extensions.py @@ -0,0 +1,473 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import +from mod_pywebsocket import common +from mod_pywebsocket import util +from mod_pywebsocket.http_header_util import quote_if_necessary + +# The list of available server side extension processor classes. +_available_processors = {} + + +class ExtensionProcessorInterface(object): + def __init__(self, request): + self._logger = util.get_class_logger(self) + + self._request = request + self._active = True + + def request(self): + return self._request + + def name(self): + return None + + def check_consistency_with_other_processors(self, processors): + pass + + def set_active(self, active): + self._active = active + + def is_active(self): + return self._active + + def _get_extension_response_internal(self): + return None + + def get_extension_response(self): + if not self._active: + self._logger.debug('Extension %s is deactivated', self.name()) + return None + + response = self._get_extension_response_internal() + if response is None: + self._active = False + return response + + def _setup_stream_options_internal(self, stream_options): + pass + + def setup_stream_options(self, stream_options): + if self._active: + self._setup_stream_options_internal(stream_options) + + +def _log_outgoing_compression_ratio(logger, original_bytes, filtered_bytes, + average_ratio): + # Print inf when ratio is not available. + ratio = float('inf') + if original_bytes != 0: + ratio = float(filtered_bytes) / original_bytes + + logger.debug('Outgoing compression ratio: %f (average: %f)' % + (ratio, average_ratio)) + + +def _log_incoming_compression_ratio(logger, received_bytes, filtered_bytes, + average_ratio): + # Print inf when ratio is not available. + ratio = float('inf') + if filtered_bytes != 0: + ratio = float(received_bytes) / filtered_bytes + + logger.debug('Incoming compression ratio: %f (average: %f)' % + (ratio, average_ratio)) + + +def _parse_window_bits(bits): + """Return parsed integer value iff the given string conforms to the + grammar of the window bits extension parameters. + """ + + if bits is None: + raise ValueError('Value is required') + + # For non integer values such as "10.0", ValueError will be raised. + int_bits = int(bits) + + # First condition is to drop leading zero case e.g. "08". + if bits != str(int_bits) or int_bits < 8 or int_bits > 15: + raise ValueError('Invalid value: %r' % bits) + + return int_bits + + +class _AverageRatioCalculator(object): + """Stores total bytes of original and result data, and calculates average + result / original ratio. + """ + def __init__(self): + self._total_original_bytes = 0 + self._total_result_bytes = 0 + + def add_original_bytes(self, value): + self._total_original_bytes += value + + def add_result_bytes(self, value): + self._total_result_bytes += value + + def get_average_ratio(self): + if self._total_original_bytes != 0: + return (float(self._total_result_bytes) / + self._total_original_bytes) + else: + return float('inf') + + +class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface): + """permessage-deflate extension processor. + + Specification: + http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-08 + """ + + _SERVER_MAX_WINDOW_BITS_PARAM = 'server_max_window_bits' + _SERVER_NO_CONTEXT_TAKEOVER_PARAM = 'server_no_context_takeover' + _CLIENT_MAX_WINDOW_BITS_PARAM = 'client_max_window_bits' + _CLIENT_NO_CONTEXT_TAKEOVER_PARAM = 'client_no_context_takeover' + + def __init__(self, request): + """Construct PerMessageDeflateExtensionProcessor.""" + + ExtensionProcessorInterface.__init__(self, request) + self._logger = util.get_class_logger(self) + + self._preferred_client_max_window_bits = None + self._client_no_context_takeover = False + + def name(self): + # This method returns "deflate" (not "permessage-deflate") for + # compatibility. + return 'deflate' + + def _get_extension_response_internal(self): + for name in self._request.get_parameter_names(): + if name not in [ + self._SERVER_MAX_WINDOW_BITS_PARAM, + self._SERVER_NO_CONTEXT_TAKEOVER_PARAM, + self._CLIENT_MAX_WINDOW_BITS_PARAM + ]: + self._logger.debug('Unknown parameter: %r', name) + return None + + server_max_window_bits = None + if self._request.has_parameter(self._SERVER_MAX_WINDOW_BITS_PARAM): + server_max_window_bits = self._request.get_parameter_value( + self._SERVER_MAX_WINDOW_BITS_PARAM) + try: + server_max_window_bits = _parse_window_bits( + server_max_window_bits) + except ValueError as e: + self._logger.debug('Bad %s parameter: %r', + self._SERVER_MAX_WINDOW_BITS_PARAM, e) + return None + + server_no_context_takeover = self._request.has_parameter( + self._SERVER_NO_CONTEXT_TAKEOVER_PARAM) + if (server_no_context_takeover and self._request.get_parameter_value( + self._SERVER_NO_CONTEXT_TAKEOVER_PARAM) is not None): + self._logger.debug('%s parameter must not have a value: %r', + self._SERVER_NO_CONTEXT_TAKEOVER_PARAM, + server_no_context_takeover) + return None + + # client_max_window_bits from a client indicates whether the client can + # accept client_max_window_bits from a server or not. + client_client_max_window_bits = self._request.has_parameter( + self._CLIENT_MAX_WINDOW_BITS_PARAM) + if (client_client_max_window_bits + and self._request.get_parameter_value( + self._CLIENT_MAX_WINDOW_BITS_PARAM) is not None): + self._logger.debug( + '%s parameter must not have a value in a ' + 'client\'s opening handshake: %r', + self._CLIENT_MAX_WINDOW_BITS_PARAM, + client_client_max_window_bits) + return None + + self._rfc1979_deflater = util._RFC1979Deflater( + server_max_window_bits, server_no_context_takeover) + + # Note that we prepare for incoming messages compressed with window + # bits upto 15 regardless of the client_max_window_bits value to be + # sent to the client. + self._rfc1979_inflater = util._RFC1979Inflater() + + self._framer = _PerMessageDeflateFramer(server_max_window_bits, + server_no_context_takeover) + self._framer.set_bfinal(False) + self._framer.set_compress_outgoing_enabled(True) + + response = common.ExtensionParameter(self._request.name()) + + if server_max_window_bits is not None: + response.add_parameter(self._SERVER_MAX_WINDOW_BITS_PARAM, + str(server_max_window_bits)) + + if server_no_context_takeover: + response.add_parameter(self._SERVER_NO_CONTEXT_TAKEOVER_PARAM, + None) + + if self._preferred_client_max_window_bits is not None: + if not client_client_max_window_bits: + self._logger.debug( + 'Processor is configured to use %s but ' + 'the client cannot accept it', + self._CLIENT_MAX_WINDOW_BITS_PARAM) + return None + response.add_parameter(self._CLIENT_MAX_WINDOW_BITS_PARAM, + str(self._preferred_client_max_window_bits)) + + if self._client_no_context_takeover: + response.add_parameter(self._CLIENT_NO_CONTEXT_TAKEOVER_PARAM, + None) + + self._logger.debug('Enable %s extension (' + 'request: server_max_window_bits=%s; ' + 'server_no_context_takeover=%r, ' + 'response: client_max_window_bits=%s; ' + 'client_no_context_takeover=%r)' % + (self._request.name(), server_max_window_bits, + server_no_context_takeover, + self._preferred_client_max_window_bits, + self._client_no_context_takeover)) + + return response + + def _setup_stream_options_internal(self, stream_options): + self._framer.setup_stream_options(stream_options) + + def set_client_max_window_bits(self, value): + """If this option is specified, this class adds the + client_max_window_bits extension parameter to the handshake response, + but doesn't reduce the LZ77 sliding window size of its inflater. + I.e., you can use this for testing client implementation but cannot + reduce memory usage of this class. + + If this method has been called with True and an offer without the + client_max_window_bits extension parameter is received, + - (When processing the permessage-deflate extension) this processor + declines the request. + - (When processing the permessage-compress extension) this processor + accepts the request. + """ + + self._preferred_client_max_window_bits = value + + def set_client_no_context_takeover(self, value): + """If this option is specified, this class adds the + client_no_context_takeover extension parameter to the handshake + response, but doesn't reset inflater for each message. I.e., you can + use this for testing client implementation but cannot reduce memory + usage of this class. + """ + + self._client_no_context_takeover = value + + def set_bfinal(self, value): + self._framer.set_bfinal(value) + + def enable_outgoing_compression(self): + self._framer.set_compress_outgoing_enabled(True) + + def disable_outgoing_compression(self): + self._framer.set_compress_outgoing_enabled(False) + + +class _PerMessageDeflateFramer(object): + """A framer for extensions with per-message DEFLATE feature.""" + def __init__(self, deflate_max_window_bits, deflate_no_context_takeover): + self._logger = util.get_class_logger(self) + + self._rfc1979_deflater = util._RFC1979Deflater( + deflate_max_window_bits, deflate_no_context_takeover) + + self._rfc1979_inflater = util._RFC1979Inflater() + + self._bfinal = False + + self._compress_outgoing_enabled = False + + # True if a message is fragmented and compression is ongoing. + self._compress_ongoing = False + + # Calculates + # (Total outgoing bytes supplied to this filter) / + # (Total bytes sent to the network after applying this filter) + self._outgoing_average_ratio_calculator = _AverageRatioCalculator() + + # Calculates + # (Total bytes received from the network) / + # (Total incoming bytes obtained after applying this filter) + self._incoming_average_ratio_calculator = _AverageRatioCalculator() + + def set_bfinal(self, value): + self._bfinal = value + + def set_compress_outgoing_enabled(self, value): + self._compress_outgoing_enabled = value + + def _process_incoming_message(self, message, decompress): + if not decompress: + return message + + received_payload_size = len(message) + self._incoming_average_ratio_calculator.add_result_bytes( + received_payload_size) + + message = self._rfc1979_inflater.filter(message) + + filtered_payload_size = len(message) + self._incoming_average_ratio_calculator.add_original_bytes( + filtered_payload_size) + + _log_incoming_compression_ratio( + self._logger, received_payload_size, filtered_payload_size, + self._incoming_average_ratio_calculator.get_average_ratio()) + + return message + + def _process_outgoing_message(self, message, end, binary): + if not binary: + message = message.encode('utf-8') + + if not self._compress_outgoing_enabled: + return message + + original_payload_size = len(message) + self._outgoing_average_ratio_calculator.add_original_bytes( + original_payload_size) + + message = self._rfc1979_deflater.filter(message, + end=end, + bfinal=self._bfinal) + + filtered_payload_size = len(message) + self._outgoing_average_ratio_calculator.add_result_bytes( + filtered_payload_size) + + _log_outgoing_compression_ratio( + self._logger, original_payload_size, filtered_payload_size, + self._outgoing_average_ratio_calculator.get_average_ratio()) + + if not self._compress_ongoing: + self._outgoing_frame_filter.set_compression_bit() + self._compress_ongoing = not end + return message + + def _process_incoming_frame(self, frame): + if frame.rsv1 == 1 and not common.is_control_opcode(frame.opcode): + self._incoming_message_filter.decompress_next_message() + frame.rsv1 = 0 + + def _process_outgoing_frame(self, frame, compression_bit): + if (not compression_bit or common.is_control_opcode(frame.opcode)): + return + + frame.rsv1 = 1 + + def setup_stream_options(self, stream_options): + """Creates filters and sets them to the StreamOptions.""" + class _OutgoingMessageFilter(object): + def __init__(self, parent): + self._parent = parent + + def filter(self, message, end=True, binary=False): + return self._parent._process_outgoing_message( + message, end, binary) + + class _IncomingMessageFilter(object): + def __init__(self, parent): + self._parent = parent + self._decompress_next_message = False + + def decompress_next_message(self): + self._decompress_next_message = True + + def filter(self, message): + message = self._parent._process_incoming_message( + message, self._decompress_next_message) + self._decompress_next_message = False + return message + + self._outgoing_message_filter = _OutgoingMessageFilter(self) + self._incoming_message_filter = _IncomingMessageFilter(self) + stream_options.outgoing_message_filters.append( + self._outgoing_message_filter) + stream_options.incoming_message_filters.append( + self._incoming_message_filter) + + class _OutgoingFrameFilter(object): + def __init__(self, parent): + self._parent = parent + self._set_compression_bit = False + + def set_compression_bit(self): + self._set_compression_bit = True + + def filter(self, frame): + self._parent._process_outgoing_frame(frame, + self._set_compression_bit) + self._set_compression_bit = False + + class _IncomingFrameFilter(object): + def __init__(self, parent): + self._parent = parent + + def filter(self, frame): + self._parent._process_incoming_frame(frame) + + self._outgoing_frame_filter = _OutgoingFrameFilter(self) + self._incoming_frame_filter = _IncomingFrameFilter(self) + stream_options.outgoing_frame_filters.append( + self._outgoing_frame_filter) + stream_options.incoming_frame_filters.append( + self._incoming_frame_filter) + + stream_options.encode_text_message_to_utf8 = False + + +_available_processors[common.PERMESSAGE_DEFLATE_EXTENSION] = ( + PerMessageDeflateExtensionProcessor) + + +def get_extension_processor(extension_request): + """Given an ExtensionParameter representing an extension offer received + from a client, configures and returns an instance of the corresponding + extension processor class. + """ + + processor_class = _available_processors.get(extension_request.name()) + if processor_class is None: + return None + return processor_class(extension_request) + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/fast_masking.i b/testing/mochitest/pywebsocket3/mod_pywebsocket/fast_masking.i new file mode 100644 index 0000000000..ddaad27f53 --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/fast_masking.i @@ -0,0 +1,98 @@ +// Copyright 2013, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +%module fast_masking + +%include "cstring.i" + +%{ +#include <cstring> + +#ifdef __SSE2__ +#include <emmintrin.h> +#endif +%} + +%apply (char *STRING, int LENGTH) { + (const char* payload, int payload_length), + (const char* masking_key, int masking_key_length) }; +%cstring_output_allocate_size( + char** result, int* result_length, delete [] *$1); + +%inline %{ + +void mask( + const char* payload, int payload_length, + const char* masking_key, int masking_key_length, + int masking_key_index, + char** result, int* result_length) { + *result = new char[payload_length]; + *result_length = payload_length; + memcpy(*result, payload, payload_length); + + char* cursor = *result; + char* cursor_end = *result + *result_length; + +#ifdef __SSE2__ + while ((cursor < cursor_end) && + (reinterpret_cast<size_t>(cursor) & 0xf)) { + *cursor ^= masking_key[masking_key_index]; + ++cursor; + masking_key_index = (masking_key_index + 1) % masking_key_length; + } + if (cursor == cursor_end) { + return; + } + + const int kBlockSize = 16; + __m128i masking_key_block; + for (int i = 0; i < kBlockSize; ++i) { + *(reinterpret_cast<char*>(&masking_key_block) + i) = + masking_key[masking_key_index]; + masking_key_index = (masking_key_index + 1) % masking_key_length; + } + + while (cursor + kBlockSize <= cursor_end) { + __m128i payload_block = + _mm_load_si128(reinterpret_cast<__m128i*>(cursor)); + _mm_stream_si128(reinterpret_cast<__m128i*>(cursor), + _mm_xor_si128(payload_block, masking_key_block)); + cursor += kBlockSize; + } +#endif + + while (cursor < cursor_end) { + *cursor ^= masking_key[masking_key_index]; + ++cursor; + masking_key_index = (masking_key_index + 1) % masking_key_length; + } +} + +%} diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/__init__.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/__init__.py new file mode 100644 index 0000000000..82be5ae9fd --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/__init__.py @@ -0,0 +1,101 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""WebSocket opening handshake processor. This class try to apply available +opening handshake processors for each protocol version until a connection is +successfully established. +""" + +from __future__ import absolute_import +import logging + +from mod_pywebsocket import common +from mod_pywebsocket.handshake import hybi +# Export AbortedByUserException, HandshakeException, and VersionException +# symbol from this module. +from mod_pywebsocket.handshake._base import AbortedByUserException +from mod_pywebsocket.handshake._base import HandshakeException +from mod_pywebsocket.handshake._base import VersionException + +_LOGGER = logging.getLogger(__name__) + + +def do_handshake(request, dispatcher): + """Performs WebSocket handshake. + + Args: + request: mod_python request. + dispatcher: Dispatcher (dispatch.Dispatcher). + + Handshaker will add attributes such as ws_resource in performing + handshake. + """ + + _LOGGER.debug('Client\'s opening handshake resource: %r', request.uri) + # To print mimetools.Message as escaped one-line string, we converts + # headers_in to dict object. Without conversion, if we use %r, it just + # prints the type and address, and if we use %s, it prints the original + # header string as multiple lines. + # + # Both mimetools.Message and MpTable_Type of mod_python can be + # converted to dict. + # + # mimetools.Message.__str__ returns the original header string. + # dict(mimetools.Message object) returns the map from header names to + # header values. While MpTable_Type doesn't have such __str__ but just + # __repr__ which formats itself as well as dictionary object. + _LOGGER.debug('Client\'s opening handshake headers: %r', + dict(request.headers_in)) + + handshakers = [] + handshakers.append(('RFC 6455', hybi.Handshaker(request, dispatcher))) + + for name, handshaker in handshakers: + _LOGGER.debug('Trying protocol version %s', name) + try: + handshaker.do_handshake() + _LOGGER.info('Established (%s protocol)', name) + return + except HandshakeException as e: + _LOGGER.debug( + 'Failed to complete opening handshake as %s protocol: %r', + name, e) + if e.status: + raise e + except AbortedByUserException as e: + raise + except VersionException as e: + raise + + # TODO(toyoshim): Add a test to cover the case all handshakers fail. + raise HandshakeException( + 'Failed to complete opening handshake for all available protocols', + status=common.HTTP_STATUS_BAD_REQUEST) + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/_base.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/_base.py new file mode 100644 index 0000000000..358dd8d497 --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/_base.py @@ -0,0 +1,179 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Common functions and exceptions used by WebSocket opening handshake +processors. +""" + +from __future__ import absolute_import +from mod_pywebsocket import common +from mod_pywebsocket import http_header_util + + +class AbortedByUserException(Exception): + """Exception for aborting a connection intentionally. + + If this exception is raised in do_extra_handshake handler, the connection + will be abandoned. No other WebSocket or HTTP(S) handler will be invoked. + + If this exception is raised in transfer_data_handler, the connection will + be closed without closing handshake. No other WebSocket or HTTP(S) handler + will be invoked. + """ + + pass + + +class HandshakeException(Exception): + """This exception will be raised when an error occurred while processing + WebSocket initial handshake. + """ + def __init__(self, name, status=None): + super(HandshakeException, self).__init__(name) + self.status = status + + +class VersionException(Exception): + """This exception will be raised when a version of client request does not + match with version the server supports. + """ + def __init__(self, name, supported_versions=''): + """Construct an instance. + + Args: + supported_version: a str object to show supported hybi versions. + (e.g. '13') + """ + super(VersionException, self).__init__(name) + self.supported_versions = supported_versions + + +def get_default_port(is_secure): + if is_secure: + return common.DEFAULT_WEB_SOCKET_SECURE_PORT + else: + return common.DEFAULT_WEB_SOCKET_PORT + + +def validate_subprotocol(subprotocol): + """Validate a value in the Sec-WebSocket-Protocol field. + + See the Section 4.1., 4.2.2., and 4.3. of RFC 6455. + """ + + if not subprotocol: + raise HandshakeException('Invalid subprotocol name: empty') + + # Parameter should be encoded HTTP token. + state = http_header_util.ParsingState(subprotocol) + token = http_header_util.consume_token(state) + rest = http_header_util.peek(state) + # If |rest| is not None, |subprotocol| is not one token or invalid. If + # |rest| is None, |token| must not be None because |subprotocol| is + # concatenation of |token| and |rest| and is not None. + if rest is not None: + raise HandshakeException('Invalid non-token string in subprotocol ' + 'name: %r' % rest) + + +def parse_host_header(request): + fields = request.headers_in[common.HOST_HEADER].split(':', 1) + if len(fields) == 1: + return fields[0], get_default_port(request.is_https()) + try: + return fields[0], int(fields[1]) + except ValueError as e: + raise HandshakeException('Invalid port number format: %r' % e) + + +def format_header(name, value): + return u'%s: %s\r\n' % (name, value) + + +def get_mandatory_header(request, key): + value = request.headers_in.get(key) + if value is None: + raise HandshakeException('Header %s is not defined' % key) + return value + + +def validate_mandatory_header(request, key, expected_value, fail_status=None): + value = get_mandatory_header(request, key) + + if value.lower() != expected_value.lower(): + raise HandshakeException( + 'Expected %r for header %s but found %r (case-insensitive)' % + (expected_value, key, value), + status=fail_status) + + +def check_request_line(request): + # 5.1 1. The three character UTF-8 string "GET". + # 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte). + if request.method != u'GET': + raise HandshakeException('Method is not GET: %r' % request.method) + + if request.protocol != u'HTTP/1.1': + raise HandshakeException('Version is not HTTP/1.1: %r' % + request.protocol) + + +def parse_token_list(data): + """Parses a header value which follows 1#token and returns parsed elements + as a list of strings. + + Leading LWSes must be trimmed. + """ + + state = http_header_util.ParsingState(data) + + token_list = [] + + while True: + token = http_header_util.consume_token(state) + if token is not None: + token_list.append(token) + + http_header_util.consume_lwses(state) + + if http_header_util.peek(state) is None: + break + + if not http_header_util.consume_string(state, ','): + raise HandshakeException('Expected a comma but found %r' % + http_header_util.peek(state)) + + http_header_util.consume_lwses(state) + + if len(token_list) == 0: + raise HandshakeException('No valid token found') + + return token_list + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/hybi.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/hybi.py new file mode 100644 index 0000000000..bddbafcca8 --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/hybi.py @@ -0,0 +1,403 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""This file provides the opening handshake processor for the WebSocket +protocol (RFC 6455). + +Specification: +http://tools.ietf.org/html/rfc6455 +""" + +from __future__ import absolute_import +import base64 +import logging +import os +import re +from hashlib import sha1 + +from mod_pywebsocket import common +from mod_pywebsocket.extensions import get_extension_processor +from mod_pywebsocket.handshake._base import check_request_line +from mod_pywebsocket.handshake._base import format_header +from mod_pywebsocket.handshake._base import get_mandatory_header +from mod_pywebsocket.handshake._base import HandshakeException +from mod_pywebsocket.handshake._base import parse_token_list +from mod_pywebsocket.handshake._base import validate_mandatory_header +from mod_pywebsocket.handshake._base import validate_subprotocol +from mod_pywebsocket.handshake._base import VersionException +from mod_pywebsocket.stream import Stream +from mod_pywebsocket.stream import StreamOptions +from mod_pywebsocket import util +from six.moves import map +from six.moves import range + +# Used to validate the value in the Sec-WebSocket-Key header strictly. RFC 4648 +# disallows non-zero padding, so the character right before == must be any of +# A, Q, g and w. +_SEC_WEBSOCKET_KEY_REGEX = re.compile('^[+/0-9A-Za-z]{21}[AQgw]==$') + +# Defining aliases for values used frequently. +_VERSION_LATEST = common.VERSION_HYBI_LATEST +_VERSION_LATEST_STRING = str(_VERSION_LATEST) +_SUPPORTED_VERSIONS = [ + _VERSION_LATEST, +] + + +def compute_accept(key): + """Computes value for the Sec-WebSocket-Accept header from value of the + Sec-WebSocket-Key header. + """ + + accept_binary = sha1(key + common.WEBSOCKET_ACCEPT_UUID).digest() + accept = base64.b64encode(accept_binary) + + return accept + + +def compute_accept_from_unicode(unicode_key): + """A wrapper function for compute_accept which takes a unicode string as an + argument, and encodes it to byte string. It then passes it on to + compute_accept. + """ + + key = unicode_key.encode('UTF-8') + return compute_accept(key) + + +class Handshaker(object): + """Opening handshake processor for the WebSocket protocol (RFC 6455).""" + def __init__(self, request, dispatcher): + """Construct an instance. + + Args: + request: mod_python request. + dispatcher: Dispatcher (dispatch.Dispatcher). + + Handshaker will add attributes such as ws_resource during handshake. + """ + + self._logger = util.get_class_logger(self) + + self._request = request + self._dispatcher = dispatcher + + def _validate_connection_header(self): + connection = get_mandatory_header(self._request, + common.CONNECTION_HEADER) + + try: + connection_tokens = parse_token_list(connection) + except HandshakeException as e: + raise HandshakeException('Failed to parse %s: %s' % + (common.CONNECTION_HEADER, e)) + + connection_is_valid = False + for token in connection_tokens: + if token.lower() == common.UPGRADE_CONNECTION_TYPE.lower(): + connection_is_valid = True + break + if not connection_is_valid: + raise HandshakeException( + '%s header doesn\'t contain "%s"' % + (common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)) + + def do_handshake(self): + self._request.ws_close_code = None + self._request.ws_close_reason = None + + # Parsing. + + check_request_line(self._request) + + validate_mandatory_header(self._request, common.UPGRADE_HEADER, + common.WEBSOCKET_UPGRADE_TYPE) + + self._validate_connection_header() + + self._request.ws_resource = self._request.uri + + unused_host = get_mandatory_header(self._request, common.HOST_HEADER) + + self._request.ws_version = self._check_version() + + try: + self._get_origin() + self._set_protocol() + self._parse_extensions() + + # Key validation, response generation. + + key = self._get_key() + accept = compute_accept(key) + self._logger.debug('%s: %r (%s)', + common.SEC_WEBSOCKET_ACCEPT_HEADER, accept, + util.hexify(base64.b64decode(accept))) + + self._logger.debug('Protocol version is RFC 6455') + + # Setup extension processors. + + processors = [] + if self._request.ws_requested_extensions is not None: + for extension_request in self._request.ws_requested_extensions: + processor = get_extension_processor(extension_request) + # Unknown extension requests are just ignored. + if processor is not None: + processors.append(processor) + self._request.ws_extension_processors = processors + + # List of extra headers. The extra handshake handler may add header + # data as name/value pairs to this list and pywebsocket appends + # them to the WebSocket handshake. + self._request.extra_headers = [] + + # Extra handshake handler may modify/remove processors. + self._dispatcher.do_extra_handshake(self._request) + processors = [ + processor + for processor in self._request.ws_extension_processors + if processor is not None + ] + + # Ask each processor if there are extensions on the request which + # cannot co-exist. When processor decided other processors cannot + # co-exist with it, the processor marks them (or itself) as + # "inactive". The first extension processor has the right to + # make the final call. + for processor in reversed(processors): + if processor.is_active(): + processor.check_consistency_with_other_processors( + processors) + processors = [ + processor for processor in processors if processor.is_active() + ] + + accepted_extensions = [] + + stream_options = StreamOptions() + + for index, processor in enumerate(processors): + if not processor.is_active(): + continue + + extension_response = processor.get_extension_response() + if extension_response is None: + # Rejected. + continue + + accepted_extensions.append(extension_response) + + processor.setup_stream_options(stream_options) + + # Inactivate all of the following compression extensions. + for j in range(index + 1, len(processors)): + processors[j].set_active(False) + + if len(accepted_extensions) > 0: + self._request.ws_extensions = accepted_extensions + self._logger.debug( + 'Extensions accepted: %r', + list( + map(common.ExtensionParameter.name, + accepted_extensions))) + else: + self._request.ws_extensions = None + + self._request.ws_stream = self._create_stream(stream_options) + + if self._request.ws_requested_protocols is not None: + if self._request.ws_protocol is None: + raise HandshakeException( + 'do_extra_handshake must choose one subprotocol from ' + 'ws_requested_protocols and set it to ws_protocol') + validate_subprotocol(self._request.ws_protocol) + + self._logger.debug('Subprotocol accepted: %r', + self._request.ws_protocol) + else: + if self._request.ws_protocol is not None: + raise HandshakeException( + 'ws_protocol must be None when the client didn\'t ' + 'request any subprotocol') + + self._send_handshake(accept) + except HandshakeException as e: + if not e.status: + # Fallback to 400 bad request by default. + e.status = common.HTTP_STATUS_BAD_REQUEST + raise e + + def _get_origin(self): + origin_header = common.ORIGIN_HEADER + origin = self._request.headers_in.get(origin_header) + if origin is None: + self._logger.debug('Client request does not have origin header') + self._request.ws_origin = origin + + def _check_version(self): + version = get_mandatory_header(self._request, + common.SEC_WEBSOCKET_VERSION_HEADER) + if version == _VERSION_LATEST_STRING: + return _VERSION_LATEST + + if version.find(',') >= 0: + raise HandshakeException( + 'Multiple versions (%r) are not allowed for header %s' % + (version, common.SEC_WEBSOCKET_VERSION_HEADER), + status=common.HTTP_STATUS_BAD_REQUEST) + raise VersionException('Unsupported version %r for header %s' % + (version, common.SEC_WEBSOCKET_VERSION_HEADER), + supported_versions=', '.join( + map(str, _SUPPORTED_VERSIONS))) + + def _set_protocol(self): + self._request.ws_protocol = None + # MOZILLA + self._request.sts = None + # /MOZILLA + + protocol_header = self._request.headers_in.get( + common.SEC_WEBSOCKET_PROTOCOL_HEADER) + + if protocol_header is None: + self._request.ws_requested_protocols = None + return + + self._request.ws_requested_protocols = parse_token_list( + protocol_header) + self._logger.debug('Subprotocols requested: %r', + self._request.ws_requested_protocols) + + def _parse_extensions(self): + extensions_header = self._request.headers_in.get( + common.SEC_WEBSOCKET_EXTENSIONS_HEADER) + if not extensions_header: + self._request.ws_requested_extensions = None + return + + try: + self._request.ws_requested_extensions = common.parse_extensions( + extensions_header) + except common.ExtensionParsingException as e: + raise HandshakeException( + 'Failed to parse Sec-WebSocket-Extensions header: %r' % e) + + self._logger.debug( + 'Extensions requested: %r', + list( + map(common.ExtensionParameter.name, + self._request.ws_requested_extensions))) + + def _validate_key(self, key): + if key.find(',') >= 0: + raise HandshakeException('Request has multiple %s header lines or ' + 'contains illegal character \',\': %r' % + (common.SEC_WEBSOCKET_KEY_HEADER, key)) + + # Validate + key_is_valid = False + try: + # Validate key by quick regex match before parsing by base64 + # module. Because base64 module skips invalid characters, we have + # to do this in advance to make this server strictly reject illegal + # keys. + if _SEC_WEBSOCKET_KEY_REGEX.match(key): + decoded_key = base64.b64decode(key) + if len(decoded_key) == 16: + key_is_valid = True + except TypeError as e: + pass + + if not key_is_valid: + raise HandshakeException('Illegal value for header %s: %r' % + (common.SEC_WEBSOCKET_KEY_HEADER, key)) + + return decoded_key + + def _get_key(self): + key = get_mandatory_header(self._request, + common.SEC_WEBSOCKET_KEY_HEADER) + + decoded_key = self._validate_key(key) + + self._logger.debug('%s: %r (%s)', common.SEC_WEBSOCKET_KEY_HEADER, key, + util.hexify(decoded_key)) + + return key.encode('UTF-8') + + def _create_stream(self, stream_options): + return Stream(self._request, stream_options) + + def _create_handshake_response(self, accept): + response = [] + + response.append(u'HTTP/1.1 101 Switching Protocols\r\n') + + # WebSocket headers + response.append( + format_header(common.UPGRADE_HEADER, + common.WEBSOCKET_UPGRADE_TYPE)) + response.append( + format_header(common.CONNECTION_HEADER, + common.UPGRADE_CONNECTION_TYPE)) + response.append( + format_header(common.SEC_WEBSOCKET_ACCEPT_HEADER, + accept.decode('UTF-8'))) + if self._request.ws_protocol is not None: + response.append( + format_header(common.SEC_WEBSOCKET_PROTOCOL_HEADER, + self._request.ws_protocol)) + if (self._request.ws_extensions is not None + and len(self._request.ws_extensions) != 0): + response.append( + format_header( + common.SEC_WEBSOCKET_EXTENSIONS_HEADER, + common.format_extensions(self._request.ws_extensions))) + # MOZILLA + if self._request.sts is not None: + response.append(format_header("Strict-Transport-Security", + self._request.sts)) + # /MOZILLA + + # Headers not specific for WebSocket + for name, value in self._request.extra_headers: + response.append(format_header(name, value)) + + response.append(u'\r\n') + + return u''.join(response) + + def _send_handshake(self, accept): + raw_response = self._create_handshake_response(accept) + self._request.connection.write(raw_response.encode('UTF-8')) + self._logger.debug('Sent server\'s opening handshake: %r', + raw_response) + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/http_header_util.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/http_header_util.py new file mode 100644 index 0000000000..ded90b247b --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/http_header_util.py @@ -0,0 +1,254 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Utilities for parsing and formatting headers that follow the grammar defined +in HTTP RFC http://www.ietf.org/rfc/rfc2616.txt. +""" + +from __future__ import absolute_import +import six.moves.urllib.parse + +_SEPARATORS = '()<>@,;:\\"/[]?={} \t' + + +def _is_char(c): + """Returns true iff c is in CHAR as specified in HTTP RFC.""" + + return ord(c) <= 127 + + +def _is_ctl(c): + """Returns true iff c is in CTL as specified in HTTP RFC.""" + + return ord(c) <= 31 or ord(c) == 127 + + +class ParsingState(object): + def __init__(self, data): + self.data = data + self.head = 0 + + +def peek(state, pos=0): + """Peeks the character at pos from the head of data.""" + + if state.head + pos >= len(state.data): + return None + + return state.data[state.head + pos] + + +def consume(state, amount=1): + """Consumes specified amount of bytes from the head and returns the + consumed bytes. If there's not enough bytes to consume, returns None. + """ + + if state.head + amount > len(state.data): + return None + + result = state.data[state.head:state.head + amount] + state.head = state.head + amount + return result + + +def consume_string(state, expected): + """Given a parsing state and a expected string, consumes the string from + the head. Returns True if consumed successfully. Otherwise, returns + False. + """ + + pos = 0 + + for c in expected: + if c != peek(state, pos): + return False + pos += 1 + + consume(state, pos) + return True + + +def consume_lws(state): + """Consumes a LWS from the head. Returns True if any LWS is consumed. + Otherwise, returns False. + + LWS = [CRLF] 1*( SP | HT ) + """ + + original_head = state.head + + consume_string(state, '\r\n') + + pos = 0 + + while True: + c = peek(state, pos) + if c == ' ' or c == '\t': + pos += 1 + else: + if pos == 0: + state.head = original_head + return False + else: + consume(state, pos) + return True + + +def consume_lwses(state): + """Consumes *LWS from the head.""" + + while consume_lws(state): + pass + + +def consume_token(state): + """Consumes a token from the head. Returns the token or None if no token + was found. + """ + + pos = 0 + + while True: + c = peek(state, pos) + if c is None or c in _SEPARATORS or _is_ctl(c) or not _is_char(c): + if pos == 0: + return None + + return consume(state, pos) + else: + pos += 1 + + +def consume_token_or_quoted_string(state): + """Consumes a token or a quoted-string, and returns the token or unquoted + string. If no token or quoted-string was found, returns None. + """ + + original_head = state.head + + if not consume_string(state, '"'): + return consume_token(state) + + result = [] + + expect_quoted_pair = False + + while True: + if not expect_quoted_pair and consume_lws(state): + result.append(' ') + continue + + c = consume(state) + if c is None: + # quoted-string is not enclosed with double quotation + state.head = original_head + return None + elif expect_quoted_pair: + expect_quoted_pair = False + if _is_char(c): + result.append(c) + else: + # Non CHAR character found in quoted-pair + state.head = original_head + return None + elif c == '\\': + expect_quoted_pair = True + elif c == '"': + return ''.join(result) + elif _is_ctl(c): + # Invalid character %r found in qdtext + state.head = original_head + return None + else: + result.append(c) + + +def quote_if_necessary(s): + """Quotes arbitrary string into quoted-string.""" + + quote = False + if s == '': + return '""' + + result = [] + for c in s: + if c == '"' or c in _SEPARATORS or _is_ctl(c) or not _is_char(c): + quote = True + + if c == '"' or _is_ctl(c): + result.append('\\' + c) + else: + result.append(c) + + if quote: + return '"' + ''.join(result) + '"' + else: + return ''.join(result) + + +def parse_uri(uri): + """Parse absolute URI then return host, port and resource.""" + + parsed = six.moves.urllib.parse.urlsplit(uri) + if parsed.scheme != 'wss' and parsed.scheme != 'ws': + # |uri| must be a relative URI. + # TODO(toyoshim): Should validate |uri|. + return None, None, uri + + if parsed.hostname is None: + return None, None, None + + port = None + try: + port = parsed.port + except ValueError: + # The port property cause ValueError on invalid null port descriptions + # like 'ws://host:INVALID_PORT/path', where the assigned port is not + # *DIGIT. For python 3.6 and later, ValueError also raises when + # assigning invalid port numbers such as 'ws://host:-1/path'. Earlier + # versions simply return None and ignore invalid port attributes. + return None, None, None + + if port is None: + if parsed.scheme == 'ws': + port = 80 + else: + port = 443 + + path = parsed.path + if not path: + path += '/' + if parsed.query: + path += '?' + parsed.query + if parsed.fragment: + path += '#' + parsed.fragment + + return parsed.hostname, port, path + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/memorizingfile.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/memorizingfile.py new file mode 100644 index 0000000000..d353967618 --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/memorizingfile.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Memorizing file. + +A memorizing file wraps a file and memorizes lines read by readline. +""" + +from __future__ import absolute_import +import sys + + +class MemorizingFile(object): + """MemorizingFile wraps a file and memorizes lines read by readline. + + Note that data read by other methods are not memorized. This behavior + is good enough for memorizing lines SimpleHTTPServer reads before + the control reaches WebSocketRequestHandler. + """ + def __init__(self, file_, max_memorized_lines=sys.maxsize): + """Construct an instance. + + Args: + file_: the file object to wrap. + max_memorized_lines: the maximum number of lines to memorize. + Only the first max_memorized_lines are memorized. + Default: sys.maxint. + """ + self._file = file_ + self._memorized_lines = [] + self._max_memorized_lines = max_memorized_lines + self._buffered = False + self._buffered_line = None + + def __getattribute__(self, name): + """Return a file attribute. + + Returns the value overridden by this class for some attributes, + and forwards the call to _file for the other attributes. + """ + if name in ('_file', '_memorized_lines', '_max_memorized_lines', + '_buffered', '_buffered_line', 'readline', + 'get_memorized_lines'): + return object.__getattribute__(self, name) + return self._file.__getattribute__(name) + + def readline(self, size=-1): + """Override file.readline and memorize the line read. + + Note that even if size is specified and smaller than actual size, + the whole line will be read out from underlying file object by + subsequent readline calls. + """ + if self._buffered: + line = self._buffered_line + self._buffered = False + else: + line = self._file.readline() + if line and len(self._memorized_lines) < self._max_memorized_lines: + self._memorized_lines.append(line) + if size >= 0 and size < len(line): + self._buffered = True + self._buffered_line = line[size:] + return line[:size] + return line + + def get_memorized_lines(self): + """Get lines memorized so far.""" + return self._memorized_lines + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/msgutil.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/msgutil.py new file mode 100644 index 0000000000..f58ca78e14 --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/msgutil.py @@ -0,0 +1,214 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Message related utilities. + +Note: request.connection.write/read are used in this module, even though +mod_python document says that they should be used only in connection +handlers. Unfortunately, we have no other options. For example, +request.write/read are not suitable because they don't allow direct raw +bytes writing/reading. +""" + +from __future__ import absolute_import +import six.moves.queue +import threading + +# Export Exception symbols from msgutil for backward compatibility +from mod_pywebsocket._stream_exceptions import ConnectionTerminatedException +from mod_pywebsocket._stream_exceptions import InvalidFrameException +from mod_pywebsocket._stream_exceptions import BadOperationException +from mod_pywebsocket._stream_exceptions import UnsupportedFrameException + + +# An API for handler to send/receive WebSocket messages. +def close_connection(request): + """Close connection. + + Args: + request: mod_python request. + """ + request.ws_stream.close_connection() + + +def send_message(request, payload_data, end=True, binary=False): + """Send a message (or part of a message). + + Args: + request: mod_python request. + payload_data: unicode text or str binary to send. + end: True to terminate a message. + False to send payload_data as part of a message that is to be + terminated by next or later send_message call with end=True. + binary: send payload_data as binary frame(s). + Raises: + BadOperationException: when server already terminated. + """ + request.ws_stream.send_message(payload_data, end, binary) + + +def receive_message(request): + """Receive a WebSocket frame and return its payload as a text in + unicode or a binary in str. + + Args: + request: mod_python request. + Raises: + InvalidFrameException: when client send invalid frame. + UnsupportedFrameException: when client send unsupported frame e.g. some + of reserved bit is set but no extension can + recognize it. + InvalidUTF8Exception: when client send a text frame containing any + invalid UTF-8 string. + ConnectionTerminatedException: when the connection is closed + unexpectedly. + BadOperationException: when client already terminated. + """ + return request.ws_stream.receive_message() + + +def send_ping(request, body): + request.ws_stream.send_ping(body) + + +class MessageReceiver(threading.Thread): + """This class receives messages from the client. + + This class provides three ways to receive messages: blocking, + non-blocking, and via callback. Callback has the highest precedence. + + Note: This class should not be used with the standalone server for wss + because pyOpenSSL used by the server raises a fatal error if the socket + is accessed from multiple threads. + """ + def __init__(self, request, onmessage=None): + """Construct an instance. + + Args: + request: mod_python request. + onmessage: a function to be called when a message is received. + May be None. If not None, the function is called on + another thread. In that case, MessageReceiver.receive + and MessageReceiver.receive_nowait are useless + because they will never return any messages. + """ + + threading.Thread.__init__(self) + self._request = request + self._queue = six.moves.queue.Queue() + self._onmessage = onmessage + self._stop_requested = False + self.setDaemon(True) + self.start() + + def run(self): + try: + while not self._stop_requested: + message = receive_message(self._request) + if self._onmessage: + self._onmessage(message) + else: + self._queue.put(message) + finally: + close_connection(self._request) + + def receive(self): + """ Receive a message from the channel, blocking. + + Returns: + message as a unicode string. + """ + return self._queue.get() + + def receive_nowait(self): + """ Receive a message from the channel, non-blocking. + + Returns: + message as a unicode string if available. None otherwise. + """ + try: + message = self._queue.get_nowait() + except six.moves.queue.Empty: + message = None + return message + + def stop(self): + """Request to stop this instance. + + The instance will be stopped after receiving the next message. + This method may not be very useful, but there is no clean way + in Python to forcefully stop a running thread. + """ + self._stop_requested = True + + +class MessageSender(threading.Thread): + """This class sends messages to the client. + + This class provides both synchronous and asynchronous ways to send + messages. + + Note: This class should not be used with the standalone server for wss + because pyOpenSSL used by the server raises a fatal error if the socket + is accessed from multiple threads. + """ + def __init__(self, request): + """Construct an instance. + + Args: + request: mod_python request. + """ + threading.Thread.__init__(self) + self._request = request + self._queue = six.moves.queue.Queue() + self.setDaemon(True) + self.start() + + def run(self): + while True: + message, condition = self._queue.get() + condition.acquire() + send_message(self._request, message) + condition.notify() + condition.release() + + def send(self, message): + """Send a message, blocking.""" + + condition = threading.Condition() + condition.acquire() + self._queue.put((message, condition)) + condition.wait() + + def send_nowait(self, message): + """Send a message, non-blocking.""" + + self._queue.put((message, threading.Condition())) + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/request_handler.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/request_handler.py new file mode 100644 index 0000000000..ea583bab3b --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/request_handler.py @@ -0,0 +1,321 @@ +# Copyright 2020, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Request Handler and Request/Connection classes for standalone server. +""" + +from __future__ import absolute_import + +import os + +from six.moves import CGIHTTPServer +from six.moves import http_client + +from mod_pywebsocket import common +from mod_pywebsocket import dispatch +from mod_pywebsocket import handshake +from mod_pywebsocket import http_header_util +from mod_pywebsocket import memorizingfile +from mod_pywebsocket import util + +# 1024 is practically large enough to contain WebSocket handshake lines. +_MAX_MEMORIZED_LINES = 1024 + + +class _StandaloneConnection(object): + """Mimic mod_python mp_conn.""" + def __init__(self, request_handler): + """Construct an instance. + + Args: + request_handler: A WebSocketRequestHandler instance. + """ + + self._request_handler = request_handler + + def get_local_addr(self): + """Getter to mimic mp_conn.local_addr.""" + + return (self._request_handler.server.server_name, + self._request_handler.server.server_port) + + local_addr = property(get_local_addr) + + def get_remote_addr(self): + """Getter to mimic mp_conn.remote_addr. + + Setting the property in __init__ won't work because the request + handler is not initialized yet there.""" + + return self._request_handler.client_address + + remote_addr = property(get_remote_addr) + + def write(self, data): + """Mimic mp_conn.write().""" + + return self._request_handler.wfile.write(data) + + def read(self, length): + """Mimic mp_conn.read().""" + + return self._request_handler.rfile.read(length) + + def get_memorized_lines(self): + """Get memorized lines.""" + + return self._request_handler.rfile.get_memorized_lines() + + +class _StandaloneRequest(object): + """Mimic mod_python request.""" + def __init__(self, request_handler, use_tls): + """Construct an instance. + + Args: + request_handler: A WebSocketRequestHandler instance. + """ + + self._logger = util.get_class_logger(self) + + self._request_handler = request_handler + self.connection = _StandaloneConnection(request_handler) + self._use_tls = use_tls + self.headers_in = request_handler.headers + + def get_uri(self): + """Getter to mimic request.uri. + + This method returns the raw data at the Request-URI part of the + Request-Line, while the uri method on the request object of mod_python + returns the path portion after parsing the raw data. This behavior is + kept for compatibility. + """ + + return self._request_handler.path + + uri = property(get_uri) + + def get_unparsed_uri(self): + """Getter to mimic request.unparsed_uri.""" + + return self._request_handler.path + + unparsed_uri = property(get_unparsed_uri) + + def get_method(self): + """Getter to mimic request.method.""" + + return self._request_handler.command + + method = property(get_method) + + def get_protocol(self): + """Getter to mimic request.protocol.""" + + return self._request_handler.request_version + + protocol = property(get_protocol) + + def is_https(self): + """Mimic request.is_https().""" + + return self._use_tls + + +class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): + """CGIHTTPRequestHandler specialized for WebSocket.""" + + # Use httplib.HTTPMessage instead of mimetools.Message. + MessageClass = http_client.HTTPMessage + + def setup(self): + """Override SocketServer.StreamRequestHandler.setup to wrap rfile + with MemorizingFile. + + This method will be called by BaseRequestHandler's constructor + before calling BaseHTTPRequestHandler.handle. + BaseHTTPRequestHandler.handle will call + BaseHTTPRequestHandler.handle_one_request and it will call + WebSocketRequestHandler.parse_request. + """ + + # Call superclass's setup to prepare rfile, wfile, etc. See setup + # definition on the root class SocketServer.StreamRequestHandler to + # understand what this does. + CGIHTTPServer.CGIHTTPRequestHandler.setup(self) + + self.rfile = memorizingfile.MemorizingFile( + self.rfile, max_memorized_lines=_MAX_MEMORIZED_LINES) + + def __init__(self, request, client_address, server): + self._logger = util.get_class_logger(self) + + self._options = server.websocket_server_options + + # Overrides CGIHTTPServerRequestHandler.cgi_directories. + self.cgi_directories = self._options.cgi_directories + # Replace CGIHTTPRequestHandler.is_executable method. + if self._options.is_executable_method is not None: + self.is_executable = self._options.is_executable_method + + # This actually calls BaseRequestHandler.__init__. + CGIHTTPServer.CGIHTTPRequestHandler.__init__(self, request, + client_address, server) + + def parse_request(self): + """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request. + + Return True to continue processing for HTTP(S), False otherwise. + + See BaseHTTPRequestHandler.handle_one_request method which calls + this method to understand how the return value will be handled. + """ + + # We hook parse_request method, but also call the original + # CGIHTTPRequestHandler.parse_request since when we return False, + # CGIHTTPRequestHandler.handle_one_request continues processing and + # it needs variables set by CGIHTTPRequestHandler.parse_request. + # + # Variables set by this method will be also used by WebSocket request + # handling (self.path, self.command, self.requestline, etc. See also + # how _StandaloneRequest's members are implemented using these + # attributes). + if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self): + return False + + if self._options.use_basic_auth: + auth = self.headers.get('Authorization') + if auth != self._options.basic_auth_credential: + self.send_response(401) + self.send_header('WWW-Authenticate', + 'Basic realm="Pywebsocket"') + self.end_headers() + self._logger.info('Request basic authentication') + return False + + host, port, resource = http_header_util.parse_uri(self.path) + if resource is None: + self._logger.info('Invalid URI: %r', self.path) + self._logger.info('Fallback to CGIHTTPRequestHandler') + return True + server_options = self.server.websocket_server_options + if host is not None: + validation_host = server_options.validation_host + if validation_host is not None and host != validation_host: + self._logger.info('Invalid host: %r (expected: %r)', host, + validation_host) + self._logger.info('Fallback to CGIHTTPRequestHandler') + return True + if port is not None: + validation_port = server_options.validation_port + if validation_port is not None and port != validation_port: + self._logger.info('Invalid port: %r (expected: %r)', port, + validation_port) + self._logger.info('Fallback to CGIHTTPRequestHandler') + return True + self.path = resource + + request = _StandaloneRequest(self, self._options.use_tls) + + try: + # Fallback to default http handler for request paths for which + # we don't have request handlers. + if not self._options.dispatcher.get_handler_suite(self.path): + self._logger.info('No handler for resource: %r', self.path) + self._logger.info('Fallback to CGIHTTPRequestHandler') + return True + except dispatch.DispatchException as e: + self._logger.info('Dispatch failed for error: %s', e) + self.send_error(e.status) + return False + + # If any Exceptions without except clause setup (including + # DispatchException) is raised below this point, it will be caught + # and logged by WebSocketServer. + + try: + try: + handshake.do_handshake(request, self._options.dispatcher) + except handshake.VersionException as e: + self._logger.info('Handshake failed for version error: %s', e) + self.send_response(common.HTTP_STATUS_BAD_REQUEST) + self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER, + e.supported_versions) + self.end_headers() + return False + except handshake.HandshakeException as e: + # Handshake for ws(s) failed. + self._logger.info('Handshake failed for error: %s', e) + self.send_error(e.status) + return False + + request._dispatcher = self._options.dispatcher + self._options.dispatcher.transfer_data(request) + except handshake.AbortedByUserException as e: + self._logger.info('Aborted: %s', e) + return False + + def log_request(self, code='-', size='-'): + """Override BaseHTTPServer.log_request.""" + + self._logger.info('"%s" %s %s', self.requestline, str(code), str(size)) + + def log_error(self, *args): + """Override BaseHTTPServer.log_error.""" + + # Despite the name, this method is for warnings than for errors. + # For example, HTTP status code is logged by this method. + self._logger.warning('%s - %s', self.address_string(), + args[0] % args[1:]) + + def is_cgi(self): + """Test whether self.path corresponds to a CGI script. + + Add extra check that self.path doesn't contains .. + Also check if the file is a executable file or not. + If the file is not executable, it is handled as static file or dir + rather than a CGI script. + """ + + if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self): + if '..' in self.path: + return False + # strip query parameter from request path + resource_name = self.path.split('?', 2)[0] + # convert resource_name into real path name in filesystem. + scriptfile = self.translate_path(resource_name) + if not os.path.isfile(scriptfile): + return False + if not self.is_executable(scriptfile): + return False + return True + return False + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/server_util.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/server_util.py new file mode 100644 index 0000000000..48c45fa444 --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/server_util.py @@ -0,0 +1,89 @@ +# Copyright 2020, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Server related utilities.""" + +from __future__ import absolute_import + +import logging +import logging.handlers +import threading +import time + +from mod_pywebsocket import common +from mod_pywebsocket import util + + +def _get_logger_from_class(c): + return logging.getLogger('%s.%s' % (c.__module__, c.__name__)) + + +def configure_logging(options): + logging.addLevelName(common.LOGLEVEL_FINE, 'FINE') + + logger = logging.getLogger() + logger.setLevel(logging.getLevelName(options.log_level.upper())) + if options.log_file: + handler = logging.handlers.RotatingFileHandler(options.log_file, 'a', + options.log_max, + options.log_count) + else: + handler = logging.StreamHandler() + formatter = logging.Formatter( + '[%(asctime)s] [%(levelname)s] %(name)s: %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + + deflate_log_level_name = logging.getLevelName( + options.deflate_log_level.upper()) + _get_logger_from_class(util._Deflater).setLevel(deflate_log_level_name) + _get_logger_from_class(util._Inflater).setLevel(deflate_log_level_name) + + +class ThreadMonitor(threading.Thread): + daemon = True + + def __init__(self, interval_in_sec): + threading.Thread.__init__(self, name='ThreadMonitor') + + self._logger = util.get_class_logger(self) + + self._interval_in_sec = interval_in_sec + + def run(self): + while True: + thread_name_list = [] + for thread in threading.enumerate(): + thread_name_list.append(thread.name) + self._logger.info("%d active threads: %s", + threading.active_count(), + ', '.join(thread_name_list)) + time.sleep(self._interval_in_sec) + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/standalone.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/standalone.py new file mode 100755 index 0000000000..b075d989f0 --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/standalone.py @@ -0,0 +1,483 @@ +#!/usr/bin/env python +# +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Standalone WebSocket server. + +Use this file to launch pywebsocket as a standalone server. + + +BASIC USAGE +=========== + +Go to the src directory and run + + $ python mod_pywebsocket/standalone.py [-p <ws_port>] + [-w <websock_handlers>] + [-d <document_root>] + +<ws_port> is the port number to use for ws:// connection. + +<document_root> is the path to the root directory of HTML files. + +<websock_handlers> is the path to the root directory of WebSocket handlers. +If not specified, <document_root> will be used. See __init__.py (or +run $ pydoc mod_pywebsocket) for how to write WebSocket handlers. + +For more detail and other options, run + + $ python mod_pywebsocket/standalone.py --help + +or see _build_option_parser method below. + +For trouble shooting, adding "--log_level debug" might help you. + + +TRY DEMO +======== + +Go to the src directory and run standalone.py with -d option to set the +document root to the directory containing example HTMLs and handlers like this: + + $ cd src + $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example + +to launch pywebsocket with the sample handler and html on port 80. Open +http://localhost/console.html, click the connect button, type something into +the text box next to the send button and click the send button. If everything +is working, you'll see the message you typed echoed by the server. + + +USING TLS +========= + +To run the standalone server with TLS support, run it with -t, -k, and -c +options. When TLS is enabled, the standalone server accepts only TLS connection. + +Note that when ssl module is used and the key/cert location is incorrect, +TLS connection silently fails while pyOpenSSL fails on startup. + +Example: + + $ PYTHONPATH=. python mod_pywebsocket/standalone.py \ + -d example \ + -p 10443 \ + -t \ + -c ../test/cert/cert.pem \ + -k ../test/cert/key.pem \ + +Note that when passing a relative path to -c and -k option, it will be resolved +using the document root directory as the base. + + +USING CLIENT AUTHENTICATION +=========================== + +To run the standalone server with TLS client authentication support, run it with +--tls-client-auth and --tls-client-ca options in addition to ones required for +TLS support. + +Example: + + $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example -p 10443 -t \ + -c ../test/cert/cert.pem -k ../test/cert/key.pem \ + --tls-client-auth \ + --tls-client-ca=../test/cert/cacert.pem + +Note that when passing a relative path to --tls-client-ca option, it will be +resolved using the document root directory as the base. + + +CONFIGURATION FILE +================== + +You can also write a configuration file and use it by specifying the path to +the configuration file by --config option. Please write a configuration file +following the documentation of the Python ConfigParser library. Name of each +entry must be the long version argument name. E.g. to set log level to debug, +add the following line: + +log_level=debug + +For options which doesn't take value, please add some fake value. E.g. for +--tls option, add the following line: + +tls=True + +Note that tls will be enabled even if you write tls=False as the value part is +fake. + +When both a command line argument and a configuration file entry are set for +the same configuration item, the command line value will override one in the +configuration file. + + +THREADING +========= + +This server is derived from SocketServer.ThreadingMixIn. Hence a thread is +used for each request. + + +SECURITY WARNING +================ + +This uses CGIHTTPServer and CGIHTTPServer is not secure. +It may execute arbitrary Python code or external programs. It should not be +used outside a firewall. +""" + +from __future__ import absolute_import +from six.moves import configparser +import base64 +import logging +import argparse +import os +import six +import sys +import traceback + +from mod_pywebsocket import common +from mod_pywebsocket import util +from mod_pywebsocket import server_util +from mod_pywebsocket.websocket_server import WebSocketServer + +_DEFAULT_LOG_MAX_BYTES = 1024 * 256 +_DEFAULT_LOG_BACKUP_COUNT = 5 + +_DEFAULT_REQUEST_QUEUE_SIZE = 128 + + +def _build_option_parser(): + parser = argparse.ArgumentParser() + + parser.add_argument( + '--config', + dest='config_file', + type=six.text_type, + default=None, + help=('Path to configuration file. See the file comment ' + 'at the top of this file for the configuration ' + 'file format')) + parser.add_argument('-H', + '--server-host', + '--server_host', + dest='server_host', + default='', + help='server hostname to listen to') + parser.add_argument('-V', + '--validation-host', + '--validation_host', + dest='validation_host', + default=None, + help='server hostname to validate in absolute path.') + parser.add_argument('-p', + '--port', + dest='port', + type=int, + default=common.DEFAULT_WEB_SOCKET_PORT, + help='port to listen to') + parser.add_argument('-P', + '--validation-port', + '--validation_port', + dest='validation_port', + type=int, + default=None, + help='server port to validate in absolute path.') + parser.add_argument( + '-w', + '--websock-handlers', + '--websock_handlers', + dest='websock_handlers', + default='.', + help=('The root directory of WebSocket handler files. ' + 'If the path is relative, --document-root is used ' + 'as the base.')) + parser.add_argument('-m', + '--websock-handlers-map-file', + '--websock_handlers_map_file', + dest='websock_handlers_map_file', + default=None, + help=('WebSocket handlers map file. ' + 'Each line consists of alias_resource_path and ' + 'existing_resource_path, separated by spaces.')) + parser.add_argument('-s', + '--scan-dir', + '--scan_dir', + dest='scan_dir', + default=None, + help=('Must be a directory under --websock-handlers. ' + 'Only handlers under this directory are scanned ' + 'and registered to the server. ' + 'Useful for saving scan time when the handler ' + 'root directory contains lots of files that are ' + 'not handler file or are handler files but you ' + 'don\'t want them to be registered. ')) + parser.add_argument( + '--allow-handlers-outside-root-dir', + '--allow_handlers_outside_root_dir', + dest='allow_handlers_outside_root_dir', + action='store_true', + default=False, + help=('Scans WebSocket handlers even if their canonical ' + 'path is not under --websock-handlers.')) + parser.add_argument('-d', + '--document-root', + '--document_root', + dest='document_root', + default='.', + help='Document root directory.') + parser.add_argument('-x', + '--cgi-paths', + '--cgi_paths', + dest='cgi_paths', + default=None, + help=('CGI paths relative to document_root.' + 'Comma-separated. (e.g -x /cgi,/htbin) ' + 'Files under document_root/cgi_path are handled ' + 'as CGI programs. Must be executable.')) + parser.add_argument('-t', + '--tls', + dest='use_tls', + action='store_true', + default=False, + help='use TLS (wss://)') + parser.add_argument('-k', + '--private-key', + '--private_key', + dest='private_key', + default='', + help='TLS private key file.') + parser.add_argument('-c', + '--certificate', + dest='certificate', + default='', + help='TLS certificate file.') + parser.add_argument('--tls-client-auth', + dest='tls_client_auth', + action='store_true', + default=False, + help='Requests TLS client auth on every connection.') + parser.add_argument('--tls-client-cert-optional', + dest='tls_client_cert_optional', + action='store_true', + default=False, + help=('Makes client certificate optional even though ' + 'TLS client auth is enabled.')) + parser.add_argument('--tls-client-ca', + dest='tls_client_ca', + default='', + help=('Specifies a pem file which contains a set of ' + 'concatenated CA certificates which are used to ' + 'validate certificates passed from clients')) + parser.add_argument('--basic-auth', + dest='use_basic_auth', + action='store_true', + default=False, + help='Requires Basic authentication.') + parser.add_argument( + '--basic-auth-credential', + dest='basic_auth_credential', + default='test:test', + help='Specifies the credential of basic authentication ' + 'by username:password pair (e.g. test:test).') + parser.add_argument('-l', + '--log-file', + '--log_file', + dest='log_file', + default='', + help='Log file.') + # Custom log level: + # - FINE: Prints status of each frame processing step + parser.add_argument('--log-level', + '--log_level', + type=six.text_type, + dest='log_level', + default='warn', + choices=[ + 'fine', 'debug', 'info', 'warning', 'warn', + 'error', 'critical' + ], + help='Log level.') + parser.add_argument( + '--deflate-log-level', + '--deflate_log_level', + type=six.text_type, + dest='deflate_log_level', + default='warn', + choices=['debug', 'info', 'warning', 'warn', 'error', 'critical'], + help='Log level for _Deflater and _Inflater.') + parser.add_argument('--thread-monitor-interval-in-sec', + '--thread_monitor_interval_in_sec', + dest='thread_monitor_interval_in_sec', + type=int, + default=-1, + help=('If positive integer is specified, run a thread ' + 'monitor to show the status of server threads ' + 'periodically in the specified inteval in ' + 'second. If non-positive integer is specified, ' + 'disable the thread monitor.')) + parser.add_argument('--log-max', + '--log_max', + dest='log_max', + type=int, + default=_DEFAULT_LOG_MAX_BYTES, + help='Log maximum bytes') + parser.add_argument('--log-count', + '--log_count', + dest='log_count', + type=int, + default=_DEFAULT_LOG_BACKUP_COUNT, + help='Log backup count') + parser.add_argument('-q', + '--queue', + dest='request_queue_size', + type=int, + default=_DEFAULT_REQUEST_QUEUE_SIZE, + help='request queue size') + + return parser + + +def _parse_args_and_config(args): + parser = _build_option_parser() + + # First, parse options without configuration file. + temporary_options, temporary_args = parser.parse_known_args(args=args) + if temporary_args: + logging.critical('Unrecognized positional arguments: %r', + temporary_args) + sys.exit(1) + + if temporary_options.config_file: + try: + config_fp = open(temporary_options.config_file, 'r') + except IOError as e: + logging.critical('Failed to open configuration file %r: %r', + temporary_options.config_file, e) + sys.exit(1) + + config_parser = configparser.SafeConfigParser() + config_parser.readfp(config_fp) + config_fp.close() + + args_from_config = [] + for name, value in config_parser.items('pywebsocket'): + args_from_config.append('--' + name) + args_from_config.append(value) + if args is None: + args = args_from_config + else: + args = args_from_config + args + return parser.parse_known_args(args=args) + else: + return temporary_options, temporary_args + + +def _main(args=None): + """You can call this function from your own program, but please note that + this function has some side-effects that might affect your program. For + example, util.wrap_popen3_for_win use in this method replaces implementation + of os.popen3. + """ + + options, args = _parse_args_and_config(args=args) + + os.chdir(options.document_root) + + server_util.configure_logging(options) + + # TODO(tyoshino): Clean up initialization of CGI related values. Move some + # of code here to WebSocketRequestHandler class if it's better. + options.cgi_directories = [] + options.is_executable_method = None + if options.cgi_paths: + options.cgi_directories = options.cgi_paths.split(',') + if sys.platform in ('cygwin', 'win32'): + cygwin_path = None + # For Win32 Python, it is expected that CYGWIN_PATH + # is set to a directory of cygwin binaries. + # For example, websocket_server.py in Chromium sets CYGWIN_PATH to + # full path of third_party/cygwin/bin. + if 'CYGWIN_PATH' in os.environ: + cygwin_path = os.environ['CYGWIN_PATH'] + util.wrap_popen3_for_win(cygwin_path) + + def __check_script(scriptpath): + return util.get_script_interp(scriptpath, cygwin_path) + + options.is_executable_method = __check_script + + if options.use_tls: + logging.debug('Using ssl module') + + if not options.private_key or not options.certificate: + logging.critical( + 'To use TLS, specify private_key and certificate.') + sys.exit(1) + + if (options.tls_client_cert_optional and not options.tls_client_auth): + logging.critical('Client authentication must be enabled to ' + 'specify tls_client_cert_optional') + sys.exit(1) + else: + if options.tls_client_auth: + logging.critical('TLS must be enabled for client authentication.') + sys.exit(1) + + if options.tls_client_cert_optional: + logging.critical('TLS must be enabled for client authentication.') + sys.exit(1) + + if not options.scan_dir: + options.scan_dir = options.websock_handlers + + if options.use_basic_auth: + options.basic_auth_credential = 'Basic ' + base64.b64encode( + options.basic_auth_credential.encode('UTF-8')).decode() + + try: + if options.thread_monitor_interval_in_sec > 0: + # Run a thread monitor to show the status of server threads for + # debugging. + server_util.ThreadMonitor( + options.thread_monitor_interval_in_sec).start() + + server = WebSocketServer(options) + server.serve_forever() + except Exception as e: + logging.critical('mod_pywebsocket: %s' % e) + logging.critical('mod_pywebsocket: %s' % traceback.format_exc()) + sys.exit(1) + + +if __name__ == '__main__': + _main(sys.argv[1:]) + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/stream.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/stream.py new file mode 100644 index 0000000000..99095224cc --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/stream.py @@ -0,0 +1,954 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""This file provides classes and helper functions for parsing/building frames +of the WebSocket protocol (RFC 6455). + +Specification: +http://tools.ietf.org/html/rfc6455 +""" + +from __future__ import absolute_import, division + +from collections import deque +import logging +import os +import struct +import time +import socket +import six + +from mod_pywebsocket import common +from mod_pywebsocket import util +from mod_pywebsocket._stream_exceptions import BadOperationException +from mod_pywebsocket._stream_exceptions import ConnectionTerminatedException +from mod_pywebsocket._stream_exceptions import InvalidFrameException +from mod_pywebsocket._stream_exceptions import InvalidUTF8Exception +from mod_pywebsocket._stream_exceptions import UnsupportedFrameException + +_NOOP_MASKER = util.NoopMasker() + + +class Frame(object): + def __init__(self, + fin=1, + rsv1=0, + rsv2=0, + rsv3=0, + opcode=None, + payload=b''): + self.fin = fin + self.rsv1 = rsv1 + self.rsv2 = rsv2 + self.rsv3 = rsv3 + self.opcode = opcode + self.payload = payload + + +# Helper functions made public to be used for writing unittests for WebSocket +# clients. + + +def create_length_header(length, mask): + """Creates a length header. + + Args: + length: Frame length. Must be less than 2^63. + mask: Mask bit. Must be boolean. + + Raises: + ValueError: when bad data is given. + """ + + if mask: + mask_bit = 1 << 7 + else: + mask_bit = 0 + + if length < 0: + raise ValueError('length must be non negative integer') + elif length <= 125: + return util.pack_byte(mask_bit | length) + elif length < (1 << 16): + return util.pack_byte(mask_bit | 126) + struct.pack('!H', length) + elif length < (1 << 63): + return util.pack_byte(mask_bit | 127) + struct.pack('!Q', length) + else: + raise ValueError('Payload is too big for one frame') + + +def create_header(opcode, payload_length, fin, rsv1, rsv2, rsv3, mask): + """Creates a frame header. + + Raises: + Exception: when bad data is given. + """ + + if opcode < 0 or 0xf < opcode: + raise ValueError('Opcode out of range') + + if payload_length < 0 or (1 << 63) <= payload_length: + raise ValueError('payload_length out of range') + + if (fin | rsv1 | rsv2 | rsv3) & ~1: + raise ValueError('FIN bit and Reserved bit parameter must be 0 or 1') + + header = b'' + + first_byte = ((fin << 7) + | (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4) + | opcode) + header += util.pack_byte(first_byte) + header += create_length_header(payload_length, mask) + + return header + + +def _build_frame(header, body, mask): + if not mask: + return header + body + + masking_nonce = os.urandom(4) + masker = util.RepeatedXorMasker(masking_nonce) + + return header + masking_nonce + masker.mask(body) + + +def _filter_and_format_frame_object(frame, mask, frame_filters): + for frame_filter in frame_filters: + frame_filter.filter(frame) + + header = create_header(frame.opcode, len(frame.payload), frame.fin, + frame.rsv1, frame.rsv2, frame.rsv3, mask) + return _build_frame(header, frame.payload, mask) + + +def create_binary_frame(message, + opcode=common.OPCODE_BINARY, + fin=1, + mask=False, + frame_filters=[]): + """Creates a simple binary frame with no extension, reserved bit.""" + + frame = Frame(fin=fin, opcode=opcode, payload=message) + return _filter_and_format_frame_object(frame, mask, frame_filters) + + +def create_text_frame(message, + opcode=common.OPCODE_TEXT, + fin=1, + mask=False, + frame_filters=[]): + """Creates a simple text frame with no extension, reserved bit.""" + + encoded_message = message.encode('utf-8') + return create_binary_frame(encoded_message, opcode, fin, mask, + frame_filters) + + +def parse_frame(receive_bytes, + logger=None, + ws_version=common.VERSION_HYBI_LATEST, + unmask_receive=True): + """Parses a frame. Returns a tuple containing each header field and + payload. + + Args: + receive_bytes: a function that reads frame data from a stream or + something similar. The function takes length of the bytes to be + read. The function must raise ConnectionTerminatedException if + there is not enough data to be read. + logger: a logging object. + ws_version: the version of WebSocket protocol. + unmask_receive: unmask received frames. When received unmasked + frame, raises InvalidFrameException. + + Raises: + ConnectionTerminatedException: when receive_bytes raises it. + InvalidFrameException: when the frame contains invalid data. + """ + + if not logger: + logger = logging.getLogger() + + logger.log(common.LOGLEVEL_FINE, 'Receive the first 2 octets of a frame') + + first_byte = ord(receive_bytes(1)) + fin = (first_byte >> 7) & 1 + rsv1 = (first_byte >> 6) & 1 + rsv2 = (first_byte >> 5) & 1 + rsv3 = (first_byte >> 4) & 1 + opcode = first_byte & 0xf + + second_byte = ord(receive_bytes(1)) + mask = (second_byte >> 7) & 1 + payload_length = second_byte & 0x7f + + logger.log( + common.LOGLEVEL_FINE, 'FIN=%s, RSV1=%s, RSV2=%s, RSV3=%s, opcode=%s, ' + 'Mask=%s, Payload_length=%s', fin, rsv1, rsv2, rsv3, opcode, mask, + payload_length) + + if (mask == 1) != unmask_receive: + raise InvalidFrameException( + 'Mask bit on the received frame did\'nt match masking ' + 'configuration for received frames') + + # The HyBi and later specs disallow putting a value in 0x0-0xFFFF + # into the 8-octet extended payload length field (or 0x0-0xFD in + # 2-octet field). + valid_length_encoding = True + length_encoding_bytes = 1 + if payload_length == 127: + logger.log(common.LOGLEVEL_FINE, + 'Receive 8-octet extended payload length') + + extended_payload_length = receive_bytes(8) + payload_length = struct.unpack('!Q', extended_payload_length)[0] + if payload_length > 0x7FFFFFFFFFFFFFFF: + raise InvalidFrameException('Extended payload length >= 2^63') + if ws_version >= 13 and payload_length < 0x10000: + valid_length_encoding = False + length_encoding_bytes = 8 + + logger.log(common.LOGLEVEL_FINE, 'Decoded_payload_length=%s', + payload_length) + elif payload_length == 126: + logger.log(common.LOGLEVEL_FINE, + 'Receive 2-octet extended payload length') + + extended_payload_length = receive_bytes(2) + payload_length = struct.unpack('!H', extended_payload_length)[0] + if ws_version >= 13 and payload_length < 126: + valid_length_encoding = False + length_encoding_bytes = 2 + + logger.log(common.LOGLEVEL_FINE, 'Decoded_payload_length=%s', + payload_length) + + if not valid_length_encoding: + logger.warning( + 'Payload length is not encoded using the minimal number of ' + 'bytes (%d is encoded using %d bytes)', payload_length, + length_encoding_bytes) + + if mask == 1: + logger.log(common.LOGLEVEL_FINE, 'Receive mask') + + masking_nonce = receive_bytes(4) + masker = util.RepeatedXorMasker(masking_nonce) + + logger.log(common.LOGLEVEL_FINE, 'Mask=%r', masking_nonce) + else: + masker = _NOOP_MASKER + + logger.log(common.LOGLEVEL_FINE, 'Receive payload data') + if logger.isEnabledFor(common.LOGLEVEL_FINE): + receive_start = time.time() + + raw_payload_bytes = receive_bytes(payload_length) + + if logger.isEnabledFor(common.LOGLEVEL_FINE): + # pylint --py3k W1619 + logger.log( + common.LOGLEVEL_FINE, 'Done receiving payload data at %s MB/s', + payload_length / (time.time() - receive_start) / 1000 / 1000) + logger.log(common.LOGLEVEL_FINE, 'Unmask payload data') + + if logger.isEnabledFor(common.LOGLEVEL_FINE): + unmask_start = time.time() + + unmasked_bytes = masker.mask(raw_payload_bytes) + + if logger.isEnabledFor(common.LOGLEVEL_FINE): + # pylint --py3k W1619 + logger.log(common.LOGLEVEL_FINE, + 'Done unmasking payload data at %s MB/s', + payload_length / (time.time() - unmask_start) / 1000 / 1000) + + return opcode, unmasked_bytes, fin, rsv1, rsv2, rsv3 + + +class FragmentedFrameBuilder(object): + """A stateful class to send a message as fragments.""" + def __init__(self, mask, frame_filters=[], encode_utf8=True): + """Constructs an instance.""" + + self._mask = mask + self._frame_filters = frame_filters + # This is for skipping UTF-8 encoding when building text type frames + # from compressed data. + self._encode_utf8 = encode_utf8 + + self._started = False + + # Hold opcode of the first frame in messages to verify types of other + # frames in the message are all the same. + self._opcode = common.OPCODE_TEXT + + def build(self, payload_data, end, binary): + if binary: + frame_type = common.OPCODE_BINARY + else: + frame_type = common.OPCODE_TEXT + if self._started: + if self._opcode != frame_type: + raise ValueError('Message types are different in frames for ' + 'the same message') + opcode = common.OPCODE_CONTINUATION + else: + opcode = frame_type + self._opcode = frame_type + + if end: + self._started = False + fin = 1 + else: + self._started = True + fin = 0 + + if binary or not self._encode_utf8: + return create_binary_frame(payload_data, opcode, fin, self._mask, + self._frame_filters) + else: + return create_text_frame(payload_data, opcode, fin, self._mask, + self._frame_filters) + + +def _create_control_frame(opcode, body, mask, frame_filters): + frame = Frame(opcode=opcode, payload=body) + + for frame_filter in frame_filters: + frame_filter.filter(frame) + + if len(frame.payload) > 125: + raise BadOperationException( + 'Payload data size of control frames must be 125 bytes or less') + + header = create_header(frame.opcode, len(frame.payload), frame.fin, + frame.rsv1, frame.rsv2, frame.rsv3, mask) + return _build_frame(header, frame.payload, mask) + + +def create_ping_frame(body, mask=False, frame_filters=[]): + return _create_control_frame(common.OPCODE_PING, body, mask, frame_filters) + + +def create_pong_frame(body, mask=False, frame_filters=[]): + return _create_control_frame(common.OPCODE_PONG, body, mask, frame_filters) + + +def create_close_frame(body, mask=False, frame_filters=[]): + return _create_control_frame(common.OPCODE_CLOSE, body, mask, + frame_filters) + + +def create_closing_handshake_body(code, reason): + body = b'' + if code is not None: + if (code > common.STATUS_USER_PRIVATE_MAX + or code < common.STATUS_NORMAL_CLOSURE): + raise BadOperationException('Status code is out of range') + if (code == common.STATUS_NO_STATUS_RECEIVED + or code == common.STATUS_ABNORMAL_CLOSURE + or code == common.STATUS_TLS_HANDSHAKE): + raise BadOperationException('Status code is reserved pseudo ' + 'code') + encoded_reason = reason.encode('utf-8') + body = struct.pack('!H', code) + encoded_reason + return body + + +class StreamOptions(object): + """Holds option values to configure Stream objects.""" + def __init__(self): + """Constructs StreamOptions.""" + + # Filters applied to frames. + self.outgoing_frame_filters = [] + self.incoming_frame_filters = [] + + # Filters applied to messages. Control frames are not affected by them. + self.outgoing_message_filters = [] + self.incoming_message_filters = [] + + self.encode_text_message_to_utf8 = True + self.mask_send = False + self.unmask_receive = True + + +class Stream(object): + """A class for parsing/building frames of the WebSocket protocol + (RFC 6455). + """ + def __init__(self, request, options): + """Constructs an instance. + + Args: + request: mod_python request. + """ + + self._logger = util.get_class_logger(self) + + self._options = options + self._request = request + + self._request.client_terminated = False + self._request.server_terminated = False + + # Holds body of received fragments. + self._received_fragments = [] + # Holds the opcode of the first fragment. + self._original_opcode = None + + self._writer = FragmentedFrameBuilder( + self._options.mask_send, self._options.outgoing_frame_filters, + self._options.encode_text_message_to_utf8) + + self._ping_queue = deque() + + def _read(self, length): + """Reads length bytes from connection. In case we catch any exception, + prepends remote address to the exception message and raise again. + + Raises: + ConnectionTerminatedException: when read returns empty string. + """ + + try: + read_bytes = self._request.connection.read(length) + if not read_bytes: + raise ConnectionTerminatedException( + 'Receiving %d byte failed. Peer (%r) closed connection' % + (length, (self._request.connection.remote_addr, ))) + return read_bytes + except IOError as e: + # Also catch an IOError because mod_python throws it. + raise ConnectionTerminatedException( + 'Receiving %d byte failed. IOError (%s) occurred' % + (length, e)) + + def _write(self, bytes_to_write): + """Writes given bytes to connection. In case we catch any exception, + prepends remote address to the exception message and raise again. + """ + + try: + self._request.connection.write(bytes_to_write) + except Exception as e: + util.prepend_message_to_exception( + 'Failed to send message to %r: ' % + (self._request.connection.remote_addr, ), e) + raise + + def receive_bytes(self, length): + """Receives multiple bytes. Retries read when we couldn't receive the + specified amount. This method returns byte strings. + + Raises: + ConnectionTerminatedException: when read returns empty string. + """ + + read_bytes = [] + while length > 0: + new_read_bytes = self._read(length) + read_bytes.append(new_read_bytes) + length -= len(new_read_bytes) + return b''.join(read_bytes) + + def _read_until(self, delim_char): + """Reads bytes until we encounter delim_char. The result will not + contain delim_char. + + Raises: + ConnectionTerminatedException: when read returns empty string. + """ + + read_bytes = [] + while True: + ch = self._read(1) + if ch == delim_char: + break + read_bytes.append(ch) + return b''.join(read_bytes) + + def _receive_frame(self): + """Receives a frame and return data in the frame as a tuple containing + each header field and payload separately. + + Raises: + ConnectionTerminatedException: when read returns empty + string. + InvalidFrameException: when the frame contains invalid data. + """ + def _receive_bytes(length): + return self.receive_bytes(length) + + return parse_frame(receive_bytes=_receive_bytes, + logger=self._logger, + ws_version=self._request.ws_version, + unmask_receive=self._options.unmask_receive) + + def _receive_frame_as_frame_object(self): + opcode, unmasked_bytes, fin, rsv1, rsv2, rsv3 = self._receive_frame() + + return Frame(fin=fin, + rsv1=rsv1, + rsv2=rsv2, + rsv3=rsv3, + opcode=opcode, + payload=unmasked_bytes) + + def receive_filtered_frame(self): + """Receives a frame and applies frame filters and message filters. + The frame to be received must satisfy following conditions: + - The frame is not fragmented. + - The opcode of the frame is TEXT or BINARY. + + DO NOT USE this method except for testing purpose. + """ + + frame = self._receive_frame_as_frame_object() + if not frame.fin: + raise InvalidFrameException( + 'Segmented frames must not be received via ' + 'receive_filtered_frame()') + if (frame.opcode != common.OPCODE_TEXT + and frame.opcode != common.OPCODE_BINARY): + raise InvalidFrameException( + 'Control frames must not be received via ' + 'receive_filtered_frame()') + + for frame_filter in self._options.incoming_frame_filters: + frame_filter.filter(frame) + for message_filter in self._options.incoming_message_filters: + frame.payload = message_filter.filter(frame.payload) + return frame + + def send_message(self, message, end=True, binary=False): + """Send message. + + Args: + message: text in unicode or binary in str to send. + binary: send message as binary frame. + + Raises: + BadOperationException: when called on a server-terminated + connection or called with inconsistent message type or + binary parameter. + """ + + if self._request.server_terminated: + raise BadOperationException( + 'Requested send_message after sending out a closing handshake') + + if binary and isinstance(message, six.text_type): + raise BadOperationException( + 'Message for binary frame must not be instance of Unicode') + + for message_filter in self._options.outgoing_message_filters: + message = message_filter.filter(message, end, binary) + + try: + # Set this to any positive integer to limit maximum size of data in + # payload data of each frame. + MAX_PAYLOAD_DATA_SIZE = -1 + + if MAX_PAYLOAD_DATA_SIZE <= 0: + self._write(self._writer.build(message, end, binary)) + return + + bytes_written = 0 + while True: + end_for_this_frame = end + bytes_to_write = len(message) - bytes_written + if (MAX_PAYLOAD_DATA_SIZE > 0 + and bytes_to_write > MAX_PAYLOAD_DATA_SIZE): + end_for_this_frame = False + bytes_to_write = MAX_PAYLOAD_DATA_SIZE + + frame = self._writer.build( + message[bytes_written:bytes_written + bytes_to_write], + end_for_this_frame, binary) + self._write(frame) + + bytes_written += bytes_to_write + + # This if must be placed here (the end of while block) so that + # at least one frame is sent. + if len(message) <= bytes_written: + break + except ValueError as e: + raise BadOperationException(e) + + def _get_message_from_frame(self, frame): + """Gets a message from frame. If the message is composed of fragmented + frames and the frame is not the last fragmented frame, this method + returns None. The whole message will be returned when the last + fragmented frame is passed to this method. + + Raises: + InvalidFrameException: when the frame doesn't match defragmentation + context, or the frame contains invalid data. + """ + + if frame.opcode == common.OPCODE_CONTINUATION: + if not self._received_fragments: + if frame.fin: + raise InvalidFrameException( + 'Received a termination frame but fragmentation ' + 'not started') + else: + raise InvalidFrameException( + 'Received an intermediate frame but ' + 'fragmentation not started') + + if frame.fin: + # End of fragmentation frame + self._received_fragments.append(frame.payload) + message = b''.join(self._received_fragments) + self._received_fragments = [] + return message + else: + # Intermediate frame + self._received_fragments.append(frame.payload) + return None + else: + if self._received_fragments: + if frame.fin: + raise InvalidFrameException( + 'Received an unfragmented frame without ' + 'terminating existing fragmentation') + else: + raise InvalidFrameException( + 'New fragmentation started without terminating ' + 'existing fragmentation') + + if frame.fin: + # Unfragmented frame + + self._original_opcode = frame.opcode + return frame.payload + else: + # Start of fragmentation frame + + if common.is_control_opcode(frame.opcode): + raise InvalidFrameException( + 'Control frames must not be fragmented') + + self._original_opcode = frame.opcode + self._received_fragments.append(frame.payload) + return None + + def _process_close_message(self, message): + """Processes close message. + + Args: + message: close message. + + Raises: + InvalidFrameException: when the message is invalid. + """ + + self._request.client_terminated = True + + # Status code is optional. We can have status reason only if we + # have status code. Status reason can be empty string. So, + # allowed cases are + # - no application data: no code no reason + # - 2 octet of application data: has code but no reason + # - 3 or more octet of application data: both code and reason + if len(message) == 0: + self._logger.debug('Received close frame (empty body)') + self._request.ws_close_code = common.STATUS_NO_STATUS_RECEIVED + elif len(message) == 1: + raise InvalidFrameException( + 'If a close frame has status code, the length of ' + 'status code must be 2 octet') + elif len(message) >= 2: + self._request.ws_close_code = struct.unpack('!H', message[0:2])[0] + self._request.ws_close_reason = message[2:].decode( + 'utf-8', 'replace') + self._logger.debug('Received close frame (code=%d, reason=%r)', + self._request.ws_close_code, + self._request.ws_close_reason) + + # As we've received a close frame, no more data is coming over the + # socket. We can now safely close the socket without worrying about + # RST sending. + + if self._request.server_terminated: + self._logger.debug( + 'Received ack for server-initiated closing handshake') + return + + self._logger.debug('Received client-initiated closing handshake') + + code = common.STATUS_NORMAL_CLOSURE + reason = '' + if hasattr(self._request, '_dispatcher'): + dispatcher = self._request._dispatcher + code, reason = dispatcher.passive_closing_handshake(self._request) + if code is None and reason is not None and len(reason) > 0: + self._logger.warning( + 'Handler specified reason despite code being None') + reason = '' + if reason is None: + reason = '' + self._send_closing_handshake(code, reason) + self._logger.debug( + 'Acknowledged closing handshake initiated by the peer ' + '(code=%r, reason=%r)', code, reason) + + def _process_ping_message(self, message): + """Processes ping message. + + Args: + message: ping message. + """ + + try: + handler = self._request.on_ping_handler + if handler: + handler(self._request, message) + return + except AttributeError: + pass + self._send_pong(message) + + def _process_pong_message(self, message): + """Processes pong message. + + Args: + message: pong message. + """ + + # TODO(tyoshino): Add ping timeout handling. + + inflight_pings = deque() + + while True: + try: + expected_body = self._ping_queue.popleft() + if expected_body == message: + # inflight_pings contains pings ignored by the + # other peer. Just forget them. + self._logger.debug( + 'Ping %r is acked (%d pings were ignored)', + expected_body, len(inflight_pings)) + break + else: + inflight_pings.append(expected_body) + except IndexError: + # The received pong was unsolicited pong. Keep the + # ping queue as is. + self._ping_queue = inflight_pings + self._logger.debug('Received a unsolicited pong') + break + + try: + handler = self._request.on_pong_handler + if handler: + handler(self._request, message) + except AttributeError: + pass + + def receive_message(self): + """Receive a WebSocket frame and return its payload as a text in + unicode or a binary in str. + + Returns: + payload data of the frame + - as unicode instance if received text frame + - as str instance if received binary frame + or None iff received closing handshake. + Raises: + BadOperationException: when called on a client-terminated + connection. + ConnectionTerminatedException: when read returns empty + string. + InvalidFrameException: when the frame contains invalid + data. + UnsupportedFrameException: when the received frame has + flags, opcode we cannot handle. You can ignore this + exception and continue receiving the next frame. + """ + + if self._request.client_terminated: + raise BadOperationException( + 'Requested receive_message after receiving a closing ' + 'handshake') + + while True: + # mp_conn.read will block if no bytes are available. + + frame = self._receive_frame_as_frame_object() + + # Check the constraint on the payload size for control frames + # before extension processes the frame. + # See also http://tools.ietf.org/html/rfc6455#section-5.5 + if (common.is_control_opcode(frame.opcode) + and len(frame.payload) > 125): + raise InvalidFrameException( + 'Payload data size of control frames must be 125 bytes or ' + 'less') + + for frame_filter in self._options.incoming_frame_filters: + frame_filter.filter(frame) + + if frame.rsv1 or frame.rsv2 or frame.rsv3: + raise UnsupportedFrameException( + 'Unsupported flag is set (rsv = %d%d%d)' % + (frame.rsv1, frame.rsv2, frame.rsv3)) + + message = self._get_message_from_frame(frame) + if message is None: + continue + + for message_filter in self._options.incoming_message_filters: + message = message_filter.filter(message) + + if self._original_opcode == common.OPCODE_TEXT: + # The WebSocket protocol section 4.4 specifies that invalid + # characters must be replaced with U+fffd REPLACEMENT + # CHARACTER. + try: + return message.decode('utf-8') + except UnicodeDecodeError as e: + raise InvalidUTF8Exception(e) + elif self._original_opcode == common.OPCODE_BINARY: + return message + elif self._original_opcode == common.OPCODE_CLOSE: + self._process_close_message(message) + return None + elif self._original_opcode == common.OPCODE_PING: + self._process_ping_message(message) + elif self._original_opcode == common.OPCODE_PONG: + self._process_pong_message(message) + else: + raise UnsupportedFrameException('Opcode %d is not supported' % + self._original_opcode) + + def _send_closing_handshake(self, code, reason): + body = create_closing_handshake_body(code, reason) + frame = create_close_frame( + body, + mask=self._options.mask_send, + frame_filters=self._options.outgoing_frame_filters) + + self._request.server_terminated = True + + self._write(frame) + + def close_connection(self, + code=common.STATUS_NORMAL_CLOSURE, + reason='', + wait_response=True): + """Closes a WebSocket connection. Note that this method blocks until + it receives acknowledgement to the closing handshake. + + Args: + code: Status code for close frame. If code is None, a close + frame with empty body will be sent. + reason: string representing close reason. + wait_response: True when caller want to wait the response. + Raises: + BadOperationException: when reason is specified with code None + or reason is not an instance of both str and unicode. + """ + + if self._request.server_terminated: + self._logger.debug( + 'Requested close_connection but server is already terminated') + return + + # When we receive a close frame, we call _process_close_message(). + # _process_close_message() immediately acknowledges to the + # server-initiated closing handshake and sets server_terminated to + # True. So, here we can assume that we haven't received any close + # frame. We're initiating a closing handshake. + + if code is None: + if reason is not None and len(reason) > 0: + raise BadOperationException( + 'close reason must not be specified if code is None') + reason = '' + else: + if not isinstance(reason, bytes) and not isinstance( + reason, six.text_type): + raise BadOperationException( + 'close reason must be an instance of bytes or unicode') + + self._send_closing_handshake(code, reason) + self._logger.debug('Initiated closing handshake (code=%r, reason=%r)', + code, reason) + + if (code == common.STATUS_GOING_AWAY + or code == common.STATUS_PROTOCOL_ERROR) or not wait_response: + # It doesn't make sense to wait for a close frame if the reason is + # protocol error or that the server is going away. For some of + # other reasons, it might not make sense to wait for a close frame, + # but it's not clear, yet. + return + + # TODO(ukai): 2. wait until the /client terminated/ flag has been set, + # or until a server-defined timeout expires. + # + # For now, we expect receiving closing handshake right after sending + # out closing handshake. + message = self.receive_message() + if message is not None: + raise ConnectionTerminatedException( + 'Didn\'t receive valid ack for closing handshake') + # TODO: 3. close the WebSocket connection. + # note: mod_python Connection (mp_conn) doesn't have close method. + + def send_ping(self, body, binary=False): + if not binary and isinstance(body, six.text_type): + body = body.encode('UTF-8') + frame = create_ping_frame(body, self._options.mask_send, + self._options.outgoing_frame_filters) + self._write(frame) + + self._ping_queue.append(body) + + def _send_pong(self, body): + frame = create_pong_frame(body, self._options.mask_send, + self._options.outgoing_frame_filters) + self._write(frame) + + def get_last_received_opcode(self): + """Returns the opcode of the WebSocket message which the last received + frame belongs to. The return value is valid iff immediately after + receive_message call. + """ + + return self._original_opcode + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/util.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/util.py new file mode 100644 index 0000000000..e164e6b8e4 --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/util.py @@ -0,0 +1,405 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""WebSocket utilities.""" + +from __future__ import absolute_import +import array +import errno +import logging +import os +import re +import six +from six.moves import map +from six.moves import range +import socket +import struct +import zlib + +try: + from mod_pywebsocket import fast_masking +except ImportError: + pass + + +def prepend_message_to_exception(message, exc): + """Prepend message to the exception.""" + exc.args = (message + str(exc), ) + return + + +def __translate_interp(interp, cygwin_path): + """Translate interp program path for Win32 python to run cygwin program + (e.g. perl). Note that it doesn't support path that contains space, + which is typically true for Unix, where #!-script is written. + For Win32 python, cygwin_path is a directory of cygwin binaries. + + Args: + interp: interp command line + cygwin_path: directory name of cygwin binary, or None + Returns: + translated interp command line. + """ + if not cygwin_path: + return interp + m = re.match('^[^ ]*/([^ ]+)( .*)?', interp) + if m: + cmd = os.path.join(cygwin_path, m.group(1)) + return cmd + m.group(2) + return interp + + +def get_script_interp(script_path, cygwin_path=None): + r"""Get #!-interpreter command line from the script. + + It also fixes command path. When Cygwin Python is used, e.g. in WebKit, + it could run "/usr/bin/perl -wT hello.pl". + When Win32 Python is used, e.g. in Chromium, it couldn't. So, fix + "/usr/bin/perl" to "<cygwin_path>\perl.exe". + + Args: + script_path: pathname of the script + cygwin_path: directory name of cygwin binary, or None + Returns: + #!-interpreter command line, or None if it is not #!-script. + """ + fp = open(script_path) + line = fp.readline() + fp.close() + m = re.match('^#!(.*)', line) + if m: + return __translate_interp(m.group(1), cygwin_path) + return None + + +def wrap_popen3_for_win(cygwin_path): + """Wrap popen3 to support #!-script on Windows. + + Args: + cygwin_path: path for cygwin binary if command path is needed to be + translated. None if no translation required. + """ + __orig_popen3 = os.popen3 + + def __wrap_popen3(cmd, mode='t', bufsize=-1): + cmdline = cmd.split(' ') + interp = get_script_interp(cmdline[0], cygwin_path) + if interp: + cmd = interp + ' ' + cmd + return __orig_popen3(cmd, mode, bufsize) + + os.popen3 = __wrap_popen3 + + +def hexify(s): + return ' '.join(['%02x' % x for x in six.iterbytes(s)]) + + +def get_class_logger(o): + """Return the logging class information.""" + return logging.getLogger('%s.%s' % + (o.__class__.__module__, o.__class__.__name__)) + + +def pack_byte(b): + """Pack an integer to network-ordered byte""" + return struct.pack('!B', b) + + +class NoopMasker(object): + """A NoOp masking object. + + This has the same interface as RepeatedXorMasker but just returns + the string passed in without making any change. + """ + def __init__(self): + """NoOp.""" + pass + + def mask(self, s): + """NoOp.""" + return s + + +class RepeatedXorMasker(object): + """A masking object that applies XOR on the string. + + Applies XOR on the byte string given to mask method with the masking bytes + given to the constructor repeatedly. This object remembers the position + in the masking bytes the last mask method call ended and resumes from + that point on the next mask method call. + """ + def __init__(self, masking_key): + self._masking_key = masking_key + self._masking_key_index = 0 + + def _mask_using_swig(self, s): + """Perform the mask via SWIG.""" + masked_data = fast_masking.mask(s, self._masking_key, + self._masking_key_index) + self._masking_key_index = ((self._masking_key_index + len(s)) % + len(self._masking_key)) + return masked_data + + def _mask_using_array(self, s): + """Perform the mask via python.""" + if isinstance(s, six.text_type): + raise Exception( + 'Masking Operation should not process unicode strings') + + result = bytearray(s) + + # Use temporary local variables to eliminate the cost to access + # attributes + masking_key = [c for c in six.iterbytes(self._masking_key)] + masking_key_size = len(masking_key) + masking_key_index = self._masking_key_index + + for i in range(len(result)): + result[i] ^= masking_key[masking_key_index] + masking_key_index = (masking_key_index + 1) % masking_key_size + + self._masking_key_index = masking_key_index + + return bytes(result) + + if 'fast_masking' in globals(): + mask = _mask_using_swig + else: + mask = _mask_using_array + + +# By making wbits option negative, we can suppress CMF/FLG (2 octet) and +# ADLER32 (4 octet) fields of zlib so that we can use zlib module just as +# deflate library. DICTID won't be added as far as we don't set dictionary. +# LZ77 window of 32K will be used for both compression and decompression. +# For decompression, we can just use 32K to cover any windows size. For +# compression, we use 32K so receivers must use 32K. +# +# Compression level is Z_DEFAULT_COMPRESSION. We don't have to match level +# to decode. +# +# See zconf.h, deflate.cc, inflate.cc of zlib library, and zlibmodule.c of +# Python. See also RFC1950 (ZLIB 3.3). + + +class _Deflater(object): + def __init__(self, window_bits): + self._logger = get_class_logger(self) + + # Using the smallest window bits of 9 for generating input frames. + # On WebSocket spec, the smallest window bit is 8. However, zlib does + # not accept window_bit = 8. + # + # Because of a zlib deflate quirk, back-references will not use the + # entire range of 1 << window_bits, but will instead use a restricted + # range of (1 << window_bits) - 262. With an increased window_bits = 9, + # back-references will be within a range of 250. These can still be + # decompressed with window_bits = 8 and the 256-byte window used there. + # + # Similar disscussions can be found in https://crbug.com/691074 + window_bits = max(window_bits, 9) + + self._compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, + zlib.DEFLATED, -window_bits) + + def compress(self, bytes): + compressed_bytes = self._compress.compress(bytes) + self._logger.debug('Compress input %r', bytes) + self._logger.debug('Compress result %r', compressed_bytes) + return compressed_bytes + + def compress_and_flush(self, bytes): + compressed_bytes = self._compress.compress(bytes) + compressed_bytes += self._compress.flush(zlib.Z_SYNC_FLUSH) + self._logger.debug('Compress input %r', bytes) + self._logger.debug('Compress result %r', compressed_bytes) + return compressed_bytes + + def compress_and_finish(self, bytes): + compressed_bytes = self._compress.compress(bytes) + compressed_bytes += self._compress.flush(zlib.Z_FINISH) + self._logger.debug('Compress input %r', bytes) + self._logger.debug('Compress result %r', compressed_bytes) + return compressed_bytes + + +class _Inflater(object): + def __init__(self, window_bits): + self._logger = get_class_logger(self) + self._window_bits = window_bits + + self._unconsumed = b'' + + self.reset() + + def decompress(self, size): + if not (size == -1 or size > 0): + raise Exception('size must be -1 or positive') + + data = b'' + + while True: + data += self._decompress.decompress(self._unconsumed, + max(0, size - len(data))) + self._unconsumed = self._decompress.unconsumed_tail + if self._decompress.unused_data: + # Encountered a last block (i.e. a block with BFINAL = 1) and + # found a new stream (unused_data). We cannot use the same + # zlib.Decompress object for the new stream. Create a new + # Decompress object to decompress the new one. + # + # It's fine to ignore unconsumed_tail if unused_data is not + # empty. + self._unconsumed = self._decompress.unused_data + self.reset() + if size >= 0 and len(data) == size: + # data is filled. Don't call decompress again. + break + else: + # Re-invoke Decompress.decompress to try to decompress all + # available bytes before invoking read which blocks until + # any new byte is available. + continue + else: + # Here, since unused_data is empty, even if unconsumed_tail is + # not empty, bytes of requested length are already in data. We + # don't have to "continue" here. + break + + if data: + self._logger.debug('Decompressed %r', data) + return data + + def append(self, data): + self._logger.debug('Appended %r', data) + self._unconsumed += data + + def reset(self): + self._logger.debug('Reset') + self._decompress = zlib.decompressobj(-self._window_bits) + + +# Compresses/decompresses given octets using the method introduced in RFC1979. + + +class _RFC1979Deflater(object): + """A compressor class that applies DEFLATE to given byte sequence and + flushes using the algorithm described in the RFC1979 section 2.1. + """ + def __init__(self, window_bits, no_context_takeover): + self._deflater = None + if window_bits is None: + window_bits = zlib.MAX_WBITS + self._window_bits = window_bits + self._no_context_takeover = no_context_takeover + + def filter(self, bytes, end=True, bfinal=False): + if self._deflater is None: + self._deflater = _Deflater(self._window_bits) + + if bfinal: + result = self._deflater.compress_and_finish(bytes) + # Add a padding block with BFINAL = 0 and BTYPE = 0. + result = result + pack_byte(0) + self._deflater = None + return result + + result = self._deflater.compress_and_flush(bytes) + if end: + # Strip last 4 octets which is LEN and NLEN field of a + # non-compressed block added for Z_SYNC_FLUSH. + result = result[:-4] + + if self._no_context_takeover and end: + self._deflater = None + + return result + + +class _RFC1979Inflater(object): + """A decompressor class a la RFC1979. + + A decompressor class for byte sequence compressed and flushed following + the algorithm described in the RFC1979 section 2.1. + """ + def __init__(self, window_bits=zlib.MAX_WBITS): + self._inflater = _Inflater(window_bits) + + def filter(self, bytes): + # Restore stripped LEN and NLEN field of a non-compressed block added + # for Z_SYNC_FLUSH. + self._inflater.append(bytes + b'\x00\x00\xff\xff') + return self._inflater.decompress(-1) + + +class DeflateSocket(object): + """A wrapper class for socket object to intercept send and recv to perform + deflate compression and decompression transparently. + """ + + # Size of the buffer passed to recv to receive compressed data. + _RECV_SIZE = 4096 + + def __init__(self, socket): + self._socket = socket + + self._logger = get_class_logger(self) + + self._deflater = _Deflater(zlib.MAX_WBITS) + self._inflater = _Inflater(zlib.MAX_WBITS) + + def recv(self, size): + """Receives data from the socket specified on the construction up + to the specified size. Once any data is available, returns it even + if it's smaller than the specified size. + """ + + # TODO(tyoshino): Allow call with size=0. It should block until any + # decompressed data is available. + if size <= 0: + raise Exception('Non-positive size passed') + while True: + data = self._inflater.decompress(size) + if len(data) != 0: + return data + + read_data = self._socket.recv(DeflateSocket._RECV_SIZE) + if not read_data: + return b'' + self._inflater.append(read_data) + + def sendall(self, bytes): + self.send(bytes) + + def send(self, bytes): + self._socket.sendall(self._deflater.compress_and_flush(bytes)) + return len(bytes) + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/websocket_server.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/websocket_server.py new file mode 100644 index 0000000000..df8cd393c7 --- /dev/null +++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/websocket_server.py @@ -0,0 +1,285 @@ +# Copyright 2020, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Standalone WebsocketServer + +This file deals with the main module of standalone server. Although it is fine +to import this file directly to use WebSocketServer, it is strongly recommended +to use standalone.py, since it is intended to act as a skeleton of this module. +""" + +from __future__ import absolute_import +from six.moves import BaseHTTPServer +from six.moves import socketserver +import logging +import re +import select +import socket +import ssl +import threading +import traceback + +from mod_pywebsocket import dispatch +from mod_pywebsocket import util +from mod_pywebsocket.request_handler import WebSocketRequestHandler + + +def _alias_handlers(dispatcher, websock_handlers_map_file): + """Set aliases specified in websock_handler_map_file in dispatcher. + + Args: + dispatcher: dispatch.Dispatcher instance + websock_handler_map_file: alias map file + """ + + with open(websock_handlers_map_file) as f: + for line in f: + if line[0] == '#' or line.isspace(): + continue + m = re.match('(\S+)\s+(\S+)$', line) + if not m: + logging.warning('Wrong format in map file:' + line) + continue + try: + dispatcher.add_resource_path_alias(m.group(1), m.group(2)) + except dispatch.DispatchException as e: + logging.error(str(e)) + + +class WebSocketServer(socketserver.ThreadingMixIn, BaseHTTPServer.HTTPServer): + """HTTPServer specialized for WebSocket.""" + + # Overrides SocketServer.ThreadingMixIn.daemon_threads + daemon_threads = True + # Overrides BaseHTTPServer.HTTPServer.allow_reuse_address + allow_reuse_address = True + + def __init__(self, options): + """Override SocketServer.TCPServer.__init__ to set SSL enabled + socket object to self.socket before server_bind and server_activate, + if necessary. + """ + + # Share a Dispatcher among request handlers to save time for + # instantiation. Dispatcher can be shared because it is thread-safe. + options.dispatcher = dispatch.Dispatcher( + options.websock_handlers, options.scan_dir, + options.allow_handlers_outside_root_dir) + if options.websock_handlers_map_file: + _alias_handlers(options.dispatcher, + options.websock_handlers_map_file) + warnings = options.dispatcher.source_warnings() + if warnings: + for warning in warnings: + logging.warning('Warning in source loading: %s' % warning) + + self._logger = util.get_class_logger(self) + + self.request_queue_size = options.request_queue_size + self.__ws_is_shut_down = threading.Event() + self.__ws_serving = False + + socketserver.BaseServer.__init__(self, + (options.server_host, options.port), + WebSocketRequestHandler) + + # Expose the options object to allow handler objects access it. We name + # it with websocket_ prefix to avoid conflict. + self.websocket_server_options = options + + self._create_sockets() + self.server_bind() + self.server_activate() + + def _create_sockets(self): + self.server_name, self.server_port = self.server_address + self._sockets = [] + if not self.server_name: + # On platforms that doesn't support IPv6, the first bind fails. + # On platforms that supports IPv6 + # - If it binds both IPv4 and IPv6 on call with AF_INET6, the + # first bind succeeds and the second fails (we'll see 'Address + # already in use' error). + # - If it binds only IPv6 on call with AF_INET6, both call are + # expected to succeed to listen both protocol. + addrinfo_array = [(socket.AF_INET6, socket.SOCK_STREAM, '', '', + ''), + (socket.AF_INET, socket.SOCK_STREAM, '', '', '')] + else: + addrinfo_array = socket.getaddrinfo(self.server_name, + self.server_port, + socket.AF_UNSPEC, + socket.SOCK_STREAM, + socket.IPPROTO_TCP) + for addrinfo in addrinfo_array: + self._logger.info('Create socket on: %r', addrinfo) + family, socktype, proto, canonname, sockaddr = addrinfo + try: + socket_ = socket.socket(family, socktype) + except Exception as e: + self._logger.info('Skip by failure: %r', e) + continue + server_options = self.websocket_server_options + if server_options.use_tls: + if server_options.tls_client_auth: + if server_options.tls_client_cert_optional: + client_cert_ = ssl.CERT_OPTIONAL + else: + client_cert_ = ssl.CERT_REQUIRED + else: + client_cert_ = ssl.CERT_NONE + socket_ = ssl.wrap_socket( + socket_, + keyfile=server_options.private_key, + certfile=server_options.certificate, + ca_certs=server_options.tls_client_ca, + cert_reqs=client_cert_) + self._sockets.append((socket_, addrinfo)) + + def server_bind(self): + """Override SocketServer.TCPServer.server_bind to enable multiple + sockets bind. + """ + + failed_sockets = [] + + for socketinfo in self._sockets: + socket_, addrinfo = socketinfo + self._logger.info('Bind on: %r', addrinfo) + if self.allow_reuse_address: + socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + socket_.bind(self.server_address) + except Exception as e: + self._logger.info('Skip by failure: %r', e) + socket_.close() + failed_sockets.append(socketinfo) + if self.server_address[1] == 0: + # The operating system assigns the actual port number for port + # number 0. This case, the second and later sockets should use + # the same port number. Also self.server_port is rewritten + # because it is exported, and will be used by external code. + self.server_address = (self.server_name, + socket_.getsockname()[1]) + self.server_port = self.server_address[1] + self._logger.info('Port %r is assigned', self.server_port) + + for socketinfo in failed_sockets: + self._sockets.remove(socketinfo) + + def server_activate(self): + """Override SocketServer.TCPServer.server_activate to enable multiple + sockets listen. + """ + + failed_sockets = [] + + for socketinfo in self._sockets: + socket_, addrinfo = socketinfo + self._logger.info('Listen on: %r', addrinfo) + try: + socket_.listen(self.request_queue_size) + except Exception as e: + self._logger.info('Skip by failure: %r', e) + socket_.close() + failed_sockets.append(socketinfo) + + for socketinfo in failed_sockets: + self._sockets.remove(socketinfo) + + if len(self._sockets) == 0: + self._logger.critical( + 'No sockets activated. Use info log level to see the reason.') + + def server_close(self): + """Override SocketServer.TCPServer.server_close to enable multiple + sockets close. + """ + + for socketinfo in self._sockets: + socket_, addrinfo = socketinfo + self._logger.info('Close on: %r', addrinfo) + socket_.close() + + def fileno(self): + """Override SocketServer.TCPServer.fileno.""" + + self._logger.critical('Not supported: fileno') + return self._sockets[0][0].fileno() + + def handle_error(self, request, client_address): + """Override SocketServer.handle_error.""" + + self._logger.error('Exception in processing request from: %r\n%s', + client_address, traceback.format_exc()) + # Note: client_address is a tuple. + + def get_request(self): + """Override TCPServer.get_request.""" + + accepted_socket, client_address = self.socket.accept() + + server_options = self.websocket_server_options + if server_options.use_tls: + # Print cipher in use. Handshake is done on accept. + self._logger.debug('Cipher: %s', accepted_socket.cipher()) + self._logger.debug('Client cert: %r', + accepted_socket.getpeercert()) + + return accepted_socket, client_address + + def serve_forever(self, poll_interval=0.5): + """Override SocketServer.BaseServer.serve_forever.""" + + self.__ws_serving = True + self.__ws_is_shut_down.clear() + handle_request = self.handle_request + if hasattr(self, '_handle_request_noblock'): + handle_request = self._handle_request_noblock + else: + self._logger.warning('Fallback to blocking request handler') + try: + while self.__ws_serving: + r, w, e = select.select( + [socket_[0] for socket_ in self._sockets], [], [], + poll_interval) + for socket_ in r: + self.socket = socket_ + handle_request() + self.socket = None + finally: + self.__ws_is_shut_down.set() + + def shutdown(self): + """Override SocketServer.BaseServer.shutdown.""" + + self.__ws_serving = False + self.__ws_is_shut_down.wait() + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket_wrapper.py b/testing/mochitest/pywebsocket_wrapper.py new file mode 100644 index 0000000000..82f087476d --- /dev/null +++ b/testing/mochitest/pywebsocket_wrapper.py @@ -0,0 +1,28 @@ +# +# 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/. +# + +"""A wrapper around pywebsocket's standalone.py which causes it to ignore +SIGINT. + +""" + +import signal +import sys + +if __name__ == "__main__": + sys.path = ["pywebsocket3"] + sys.path + from mod_pywebsocket import standalone + + # If we received --interactive as the first argument, ignore SIGINT so + # pywebsocket doesn't die on a ctrl+c meant for the debugger. Otherwise, + # die immediately on SIGINT so we don't print a messy backtrace. + if len(sys.argv) >= 2 and sys.argv[1] == "--interactive": + del sys.argv[1] + signal.signal(signal.SIGINT, signal.SIG_IGN) + else: + signal.signal(signal.SIGINT, lambda signum, frame: sys.exit(1)) + + standalone._main() diff --git a/testing/mochitest/redirect.html b/testing/mochitest/redirect.html new file mode 100644 index 0000000000..1536ed4e38 --- /dev/null +++ b/testing/mochitest/redirect.html @@ -0,0 +1,34 @@ +<html> +<head> + <title>redirecting...</title> + + <script type="text/javascript"> + function redirect(aURL) { + // We create a listener for this event in browser-test.js which will + // get picked up when specifying --flavor=chrome or --flavor=a11y + var event = new CustomEvent("contentEvent", { + bubbles: true, + detail: { + "data": aURL + location.search, + "type": "loadURI", + }, + }); + document.dispatchEvent(event); + } + + function redirectToHarness() { + redirect("chrome://mochikit/content/harness.xhtml"); + } + + function onLoad() { + setTimeout(redirectToHarness, 0); + // In case the listener is not ready, re-try periodically + setInterval(redirectToHarness, 5000); + } + </script> +</head> + +<body onload="onLoad();"> +redirecting... +</body> +</html> diff --git a/testing/mochitest/runjunit.py b/testing/mochitest/runjunit.py new file mode 100644 index 0000000000..a0faffcc84 --- /dev/null +++ b/testing/mochitest/runjunit.py @@ -0,0 +1,713 @@ +# 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 argparse +import os +import posixpath +import re +import shutil +import sys +import tempfile +import traceback + +import mozcrash +import mozinfo +import mozlog +import moznetwork +import six +from mozdevice import ADBDeviceFactory, ADBError, ADBTimeoutError +from mozprofile import DEFAULT_PORTS, Profile +from mozprofile.cli import parse_preferences +from mozprofile.permissions import ServerLocations +from runtests import MochitestDesktop, update_mozinfo + +here = os.path.abspath(os.path.dirname(__file__)) + +try: + from mach.util import UserError + from mozbuild.base import MachCommandConditions as conditions + from mozbuild.base import MozbuildObject + + build_obj = MozbuildObject.from_environment(cwd=here) +except ImportError: + build_obj = None + conditions = None + UserError = Exception + + +class JavaTestHarnessException(Exception): + pass + + +class JUnitTestRunner(MochitestDesktop): + """ + A test harness to run geckoview junit tests on a remote device. + """ + + def __init__(self, log, options): + self.log = log + self.verbose = False + if ( + options.log_tbpl_level == "debug" + or options.log_mach_level == "debug" + or options.verbose + ): + self.verbose = True + self.device = ADBDeviceFactory( + adb=options.adbPath or "adb", + device=options.deviceSerial, + test_root=options.remoteTestRoot, + verbose=self.verbose, + run_as_package=options.app, + ) + self.options = options + self.log.debug("options=%s" % vars(options)) + update_mozinfo() + self.remote_profile = posixpath.join(self.device.test_root, "junit-profile") + self.remote_filter_list = posixpath.join( + self.device.test_root, "junit-filters.list" + ) + + if self.options.coverage and not self.options.coverage_output_dir: + raise UserError( + "--coverage-output-dir is required when using --enable-coverage" + ) + if self.options.coverage: + self.remote_coverage_output_file = posixpath.join( + self.device.test_root, "junit-coverage.ec" + ) + self.coverage_output_file = os.path.join( + self.options.coverage_output_dir, "junit-coverage.ec" + ) + + self.server_init() + + self.cleanup() + self.device.clear_logcat() + self.build_profile() + self.startServers(self.options, debuggerInfo=None, public=True) + self.log.debug("Servers started") + + def collectLogcatForCurrentTest(self): + # These are unique start and end markers logged by GeckoSessionTestRule.java + START_MARKER = "1f0befec-3ff2-40ff-89cf-b127eb38b1ec" + END_MARKER = "c5ee677f-bc83-49bd-9e28-2d35f3d0f059" + logcat = self.device.get_logcat() + test_logcat = "" + started = False + for l in logcat: + if START_MARKER in l and self.test_name in l: + started = True + if started: + test_logcat += l + "\n" + if started and END_MARKER in l: + return test_logcat + + def needsWebsocketProcessBridge(self, options): + """ + Overrides MochitestDesktop.needsWebsocketProcessBridge and always + returns False as the junit tests do not use the websocket process + bridge. This is needed to satisfy MochitestDesktop.startServers. + """ + return False + + def server_init(self): + """ + Additional initialization required to satisfy MochitestDesktop.startServers + """ + self._locations = None + self.server = None + self.wsserver = None + self.websocketProcessBridge = None + self.SERVER_STARTUP_TIMEOUT = 180 if mozinfo.info.get("debug") else 90 + if self.options.remoteWebServer is None: + self.options.remoteWebServer = moznetwork.get_ip() + self.options.webServer = self.options.remoteWebServer + self.options.webSocketPort = "9988" + self.options.httpdPath = None + self.options.keep_open = False + self.options.pidFile = "" + self.options.subsuite = None + self.options.xrePath = None + if build_obj and "MOZ_HOST_BIN" in os.environ: + self.options.xrePath = os.environ["MOZ_HOST_BIN"] + if not self.options.utilityPath: + self.options.utilityPath = self.options.xrePath + if not self.options.xrePath: + self.options.xrePath = self.options.utilityPath + if build_obj: + self.options.certPath = os.path.join( + build_obj.topsrcdir, "build", "pgo", "certs" + ) + + def build_profile(self): + """ + Create a local profile with test prefs and proxy definitions and + push it to the remote device. + """ + + self.profile = Profile(locations=self.locations, proxy=self.proxy(self.options)) + self.options.profilePath = self.profile.profile + + # Set preferences + self.merge_base_profiles(self.options, "geckoview-junit") + + if self.options.web_content_isolation_strategy is not None: + self.options.extra_prefs.append( + "fission.webContentIsolationStrategy=%s" + % self.options.web_content_isolation_strategy + ) + self.options.extra_prefs.append("fission.autostart=true") + if self.options.disable_fission: + self.options.extra_prefs.pop() + self.options.extra_prefs.append("fission.autostart=false") + prefs = parse_preferences(self.options.extra_prefs) + self.profile.set_preferences(prefs) + + if self.fillCertificateDB(self.options): + self.log.error("Certificate integration failed") + + self.device.push(self.profile.profile, self.remote_profile) + self.log.debug( + "profile %s -> %s" % (str(self.profile.profile), str(self.remote_profile)) + ) + + def cleanup(self): + try: + self.stopServers() + self.log.debug("Servers stopped") + self.device.stop_application(self.options.app) + self.device.rm(self.remote_profile, force=True, recursive=True) + if hasattr(self, "profile"): + del self.profile + self.device.rm(self.remote_filter_list, force=True) + except Exception: + traceback.print_exc() + self.log.info("Caught and ignored an exception during cleanup") + + def build_command_line(self, test_filters_file, test_filters): + """ + Construct and return the 'am instrument' command line. + """ + cmd = "am instrument -w -r" + # profile location + cmd = cmd + " -e args '-profile %s'" % self.remote_profile + # chunks (shards) + shards = self.options.totalChunks + shard = self.options.thisChunk + if shards is not None and shard is not None: + shard -= 1 # shard index is 0 based + cmd = cmd + " -e numShards %d -e shardIndex %d" % (shards, shard) + + # test filters: limit run to specific test(s) + # filter can be class-name or 'class-name#method-name' (single test) + # Multiple filters must be specified as a line-separated text file + # and then pushed to the device. + filter_list_name = None + + if test_filters_file: + # We specified a pre-existing file, so use that + filter_list_name = test_filters_file + elif test_filters: + if len(test_filters) > 1: + # Generate the list file from test_filters + with tempfile.NamedTemporaryFile(delete=False, mode="w") as filter_list: + for f in test_filters: + print(f, file=filter_list) + filter_list_name = filter_list.name + else: + # A single filter may be directly appended to the command line + cmd = cmd + " -e class %s" % test_filters[0] + + if filter_list_name: + self.device.push(filter_list_name, self.remote_filter_list) + + if test_filters: + # We only remove the filter list if we generated it as a + # temporary file. + os.remove(filter_list_name) + + cmd = cmd + " -e testFile %s" % self.remote_filter_list + + # enable code coverage reports + if self.options.coverage: + cmd = cmd + " -e coverage true" + cmd = cmd + " -e coverageFile %s" % self.remote_coverage_output_file + # environment + env = {} + env["MOZ_CRASHREPORTER"] = "1" + env["MOZ_CRASHREPORTER_SHUTDOWN"] = "1" + env["XPCOM_DEBUG_BREAK"] = "stack" + env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1" + env["MOZ_IN_AUTOMATION"] = "1" + env["R_LOG_VERBOSE"] = "1" + env["R_LOG_LEVEL"] = "6" + env["R_LOG_DESTINATION"] = "stderr" + # webrender needs gfx.webrender.all=true, gtest doesn't use prefs + env["MOZ_WEBRENDER"] = "1" + # FIXME: When android switches to using Fission by default, + # MOZ_FORCE_DISABLE_FISSION will need to be configured correctly. + if self.options.disable_fission: + env["MOZ_FORCE_DISABLE_FISSION"] = "1" + else: + env["MOZ_FORCE_ENABLE_FISSION"] = "1" + + # Add additional env variables + for [key, value] in [p.split("=", 1) for p in self.options.add_env]: + env[key] = value + + for (env_count, (env_key, env_val)) in enumerate(six.iteritems(env)): + cmd = cmd + " -e env%d %s=%s" % (env_count, env_key, env_val) + # runner + cmd = cmd + " %s/%s" % (self.options.app, self.options.runner) + return cmd + + @property + def locations(self): + if self._locations is not None: + return self._locations + locations_file = os.path.join(here, "server-locations.txt") + self._locations = ServerLocations(locations_file) + return self._locations + + def need_more_runs(self): + if self.options.run_until_failure and (self.fail_count == 0): + return True + if self.runs <= self.options.repeat: + return True + return False + + def run_tests(self, test_filters_file=None, test_filters=None): + """ + Run the tests. + """ + if not self.device.is_app_installed(self.options.app): + raise UserError("%s is not installed" % self.options.app) + if self.device.process_exist(self.options.app): + raise UserError( + "%s already running before starting tests" % self.options.app + ) + # test_filters_file and test_filters must be mutually-exclusive + if test_filters_file and test_filters: + raise UserError( + "Test filters may not be specified when test-filters-file is provided" + ) + + self.test_started = False + self.pass_count = 0 + self.fail_count = 0 + self.todo_count = 0 + self.total_count = 0 + self.runs = 0 + self.seen_last_test = False + + def callback(line): + # Output callback: Parse the raw junit log messages, translating into + # treeherder-friendly test start/pass/fail messages. + + line = six.ensure_str(line) + self.log.process_output(self.options.app, str(line)) + # Expect per-test info like: "INSTRUMENTATION_STATUS: class=something" + match = re.match(r"INSTRUMENTATION_STATUS:\s*class=(.*)", line) + if match: + self.class_name = match.group(1) + # Expect per-test info like: "INSTRUMENTATION_STATUS: test=something" + match = re.match(r"INSTRUMENTATION_STATUS:\s*test=(.*)", line) + if match: + self.test_name = match.group(1) + match = re.match(r"INSTRUMENTATION_STATUS:\s*numtests=(.*)", line) + if match: + self.total_count = int(match.group(1)) + match = re.match(r"INSTRUMENTATION_STATUS:\s*current=(.*)", line) + if match: + self.current_test_id = int(match.group(1)) + match = re.match(r"INSTRUMENTATION_STATUS:\s*stack=(.*)", line) + if match: + self.exception_message = match.group(1) + if ( + "org.mozilla.geckoview.test.rule.TestHarnessException" + in self.exception_message + ): + # This is actually a problem in the test harness itself + raise JavaTestHarnessException(self.exception_message) + + # Expect per-test info like: "INSTRUMENTATION_STATUS_CODE: 0|1|..." + match = re.match(r"INSTRUMENTATION_STATUS_CODE:\s*([+-]?\d+)", line) + if match: + status = match.group(1) + full_name = "%s#%s" % (self.class_name, self.test_name) + if full_name == self.current_full_name: + # A crash in the test harness might cause us to ignore tests, + # so we double check that we've actually ran all the tests + if self.total_count == self.current_test_id: + self.seen_last_test = True + + if status == "0": + message = "" + status = "PASS" + expected = "PASS" + self.pass_count += 1 + if self.verbose: + self.log.info("Printing logcat for test:") + print(self.collectLogcatForCurrentTest()) + elif status == "-3": # ignored (skipped) + message = "" + status = "SKIP" + expected = "SKIP" + self.todo_count += 1 + elif status == "-4": # known fail + message = "" + status = "FAIL" + expected = "FAIL" + self.todo_count += 1 + else: + if self.exception_message: + message = self.exception_message + else: + message = "status %s" % status + status = "FAIL" + expected = "PASS" + self.fail_count += 1 + self.log.info("Printing logcat for test:") + print(self.collectLogcatForCurrentTest()) + self.log.test_end(full_name, status, expected, message) + self.test_started = False + else: + if self.test_started: + # next test started without reporting previous status + self.fail_count += 1 + status = "FAIL" + expected = "PASS" + self.log.test_end( + self.current_full_name, + status, + expected, + "missing test completion status", + ) + self.log.test_start(full_name) + self.test_started = True + self.current_full_name = full_name + + # Ideally all test names should be reported to suite_start, but these test + # names are not known in advance. + self.log.suite_start(["geckoview-junit"]) + try: + self.device.grant_runtime_permissions(self.options.app) + self.device.add_change_device_settings(self.options.app) + self.device.add_mock_location(self.options.app) + cmd = self.build_command_line( + test_filters_file=test_filters_file, test_filters=test_filters + ) + while self.need_more_runs(): + self.class_name = "" + self.exception_message = "" + self.test_name = "" + self.current_full_name = "" + self.current_test_id = 0 + self.runs += 1 + self.log.info("launching %s" % cmd) + p = self.device.shell( + cmd, timeout=self.options.max_time, stdout_callback=callback + ) + if p.timedout: + self.log.error( + "TEST-UNEXPECTED-TIMEOUT | runjunit.py | " + "Timed out after %d seconds" % self.options.max_time + ) + self.log.info("Passed: %d" % self.pass_count) + self.log.info("Failed: %d" % self.fail_count) + self.log.info("Todo: %d" % self.todo_count) + if not self.seen_last_test: + self.log.error( + "TEST-UNEXPECTED-FAIL | runjunit.py | " + "Some tests did not run (probably due to a crash in the harness)" + ) + finally: + self.log.suite_end() + + if self.check_for_crashes(): + self.fail_count = 1 + + if self.options.coverage: + try: + self.device.pull( + self.remote_coverage_output_file, self.coverage_output_file + ) + except ADBError: + # Avoid a task retry in case the code coverage file is not found. + self.log.error( + "No code coverage file (%s) found on remote device" + % self.remote_coverage_output_file + ) + return -1 + + return 1 if self.fail_count else 0 + + def check_for_crashes(self): + symbols_path = self.options.symbolsPath + try: + dump_dir = tempfile.mkdtemp() + remote_dir = posixpath.join(self.remote_profile, "minidumps") + if not self.device.is_dir(remote_dir): + return False + self.device.pull(remote_dir, dump_dir) + crashed = mozcrash.log_crashes( + self.log, dump_dir, symbols_path, test=self.current_full_name + ) + finally: + try: + shutil.rmtree(dump_dir) + except Exception: + self.log.warning("unable to remove directory: %s" % dump_dir) + return crashed + + +class JunitArgumentParser(argparse.ArgumentParser): + """ + An argument parser for geckoview-junit. + """ + + def __init__(self, **kwargs): + super(JunitArgumentParser, self).__init__(**kwargs) + + self.add_argument( + "--appname", + action="store", + type=str, + dest="app", + default="org.mozilla.geckoview.test", + help="Test package name.", + ) + self.add_argument( + "--adbpath", + action="store", + type=str, + dest="adbPath", + default=None, + help="Path to adb binary.", + ) + self.add_argument( + "--deviceSerial", + action="store", + type=str, + dest="deviceSerial", + help="adb serial number of remote device. This is required " + "when more than one device is connected to the host. " + "Use 'adb devices' to see connected devices. ", + ) + self.add_argument( + "--setenv", + dest="add_env", + action="append", + default=[], + help="Set target environment variable, like FOO=BAR", + ) + self.add_argument( + "--remoteTestRoot", + action="store", + type=str, + dest="remoteTestRoot", + help="Remote directory to use as test root " + "(eg. /data/local/tmp/test_root).", + ) + self.add_argument( + "--max-time", + action="store", + type=int, + dest="max_time", + default="2400", + help="Max time in seconds to wait for tests (default 2400s).", + ) + self.add_argument( + "--runner", + action="store", + type=str, + dest="runner", + default="androidx.test.runner.AndroidJUnitRunner", + help="Test runner name.", + ) + self.add_argument( + "--symbols-path", + action="store", + type=str, + dest="symbolsPath", + default=None, + help="Path to directory containing breakpad symbols, " + "or the URL of a zip file containing symbols.", + ) + self.add_argument( + "--utility-path", + action="store", + type=str, + dest="utilityPath", + default=None, + help="Path to directory containing host utility programs.", + ) + self.add_argument( + "--total-chunks", + action="store", + type=int, + dest="totalChunks", + default=None, + help="Total number of chunks to split tests into.", + ) + self.add_argument( + "--this-chunk", + action="store", + type=int, + dest="thisChunk", + default=None, + help="If running tests by chunks, the chunk number to run.", + ) + self.add_argument( + "--verbose", + "-v", + action="store_true", + dest="verbose", + default=False, + help="Verbose output - enable debug log messages", + ) + self.add_argument( + "--enable-coverage", + action="store_true", + dest="coverage", + default=False, + help="Enable code coverage collection.", + ) + self.add_argument( + "--coverage-output-dir", + action="store", + type=str, + dest="coverage_output_dir", + default=None, + help="If collecting code coverage, save the report file in this dir.", + ) + self.add_argument( + "--disable-fission", + action="store_true", + dest="disable_fission", + default=False, + help="Run the tests without Fission (site isolation) enabled.", + ) + self.add_argument( + "--web-content-isolation-strategy", + type=int, + dest="web_content_isolation_strategy", + help="Strategy used to determine whether or not a particular site should load into " + "a webIsolated content process, see fission.webContentIsolationStrategy.", + ) + self.add_argument( + "--repeat", + type=int, + default=0, + help="Repeat the tests the given number of times.", + ) + self.add_argument( + "--run-until-failure", + action="store_true", + dest="run_until_failure", + default=False, + help="Run tests repeatedly but stop the first time a test fails.", + ) + self.add_argument( + "--setpref", + action="append", + dest="extra_prefs", + default=[], + metavar="PREF=VALUE", + help="Defines an extra user preference.", + ) + # Additional options for server. + self.add_argument( + "--certificate-path", + action="store", + type=str, + dest="certPath", + default=None, + help="Path to directory containing certificate store.", + ), + self.add_argument( + "--http-port", + action="store", + type=str, + dest="httpPort", + default=DEFAULT_PORTS["http"], + help="http port of the remote web server.", + ) + self.add_argument( + "--remote-webserver", + action="store", + type=str, + dest="remoteWebServer", + help="IP address of the remote web server.", + ) + self.add_argument( + "--ssl-port", + action="store", + type=str, + dest="sslPort", + default=DEFAULT_PORTS["https"], + help="ssl port of the remote web server.", + ) + self.add_argument( + "--test-filters-file", + action="store", + type=str, + dest="test_filters_file", + default=None, + help="Line-delimited file containing test filter(s)", + ) + # Remaining arguments are test filters. + self.add_argument( + "test_filters", + nargs="*", + help="Test filter(s): class and/or method names of test(s) to run.", + ) + + mozlog.commandline.add_logging_group(self) + + +def run_test_harness(parser, options): + if hasattr(options, "log"): + log = options.log + else: + log = mozlog.commandline.setup_logging( + "runjunit", options, {"tbpl": sys.stdout} + ) + runner = JUnitTestRunner(log, options) + result = -1 + try: + device_exception = False + result = runner.run_tests( + test_filters_file=options.test_filters_file, + test_filters=options.test_filters, + ) + except KeyboardInterrupt: + log.info("runjunit.py | Received keyboard interrupt") + result = -1 + except JavaTestHarnessException as e: + log.error( + "TEST-UNEXPECTED-FAIL | runjunit.py | The previous test failed because " + "of an error in the test harness | %s" % (str(e)) + ) + except Exception as e: + traceback.print_exc() + log.error("runjunit.py | Received unexpected exception while running tests") + result = 1 + if isinstance(e, ADBTimeoutError): + device_exception = True + finally: + if not device_exception: + runner.cleanup() + return result + + +def main(args=sys.argv[1:]): + parser = JunitArgumentParser() + options = parser.parse_args() + return run_test_harness(parser, options) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/testing/mochitest/runtests.py b/testing/mochitest/runtests.py new file mode 100644 index 0000000000..58251c2f8c --- /dev/null +++ b/testing/mochitest/runtests.py @@ -0,0 +1,3932 @@ +# 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/. + +""" +Runs the Mochitest test harness. +""" + +import os +import sys + +SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) +sys.path.insert(0, SCRIPT_DIR) + +import ctypes +import glob +import json +import numbers +import platform +import re +import shlex +import shutil +import signal +import socket +import subprocess +import sys +import tempfile +import time +import traceback +import uuid +import zipfile +from argparse import Namespace +from collections import defaultdict +from contextlib import closing +from ctypes.util import find_library +from datetime import datetime, timedelta +from distutils import spawn + +import bisection +import mozcrash +import mozdebug +import mozinfo +import mozprocess +import mozrunner +from manifestparser import TestManifest +from manifestparser.filters import ( + chunk_by_dir, + chunk_by_runtime, + chunk_by_slice, + failures, + pathprefix, + subsuite, + tags, +) +from manifestparser.util import normsep +from mozgeckoprofiler import symbolicate_profile_json, view_gecko_profile + +try: + from marionette_driver.addons import Addons + from marionette_driver.marionette import Marionette +except ImportError as e: # noqa + # Defer ImportError until attempt to use Marionette + def reraise(*args, **kwargs): + raise (e) # noqa + + Marionette = reraise + +import mozleak +from leaks import LSANLeaks, ShutdownLeaks +from mochitest_options import ( + MochitestArgumentParser, + build_obj, + get_default_valgrind_suppression_files, +) +from mozlog import commandline, get_proxy_logger +from mozprofile import Profile +from mozprofile.cli import KeyValueParseError, parse_key_value, parse_preferences +from mozprofile.permissions import ServerLocations +from mozrunner.utils import get_stack_fixer_function, test_environment +from mozscreenshot import dump_screen + +HAVE_PSUTIL = False +try: + import psutil + + HAVE_PSUTIL = True +except ImportError: + pass + +import six +from six.moves.urllib.parse import quote_plus as encodeURIComponent +from six.moves.urllib_request import urlopen + +here = os.path.abspath(os.path.dirname(__file__)) + +NO_TESTS_FOUND = """ +No tests were found for flavor '{}' and the following manifest filters: +{} + +Make sure the test paths (if any) are spelt correctly and the corresponding +--flavor and --subsuite are being used. See `mach mochitest --help` for a +list of valid flavors. +""".lstrip() + + +######################################## +# Option for MOZ (former NSPR) logging # +######################################## + +# Set the desired log modules you want a log be produced +# by a try run for, or leave blank to disable the feature. +# This will be passed to MOZ_LOG environment variable. +# Try run will then put a download link for a zip archive +# of all the log files on treeherder. +MOZ_LOG = "" + +##################### +# Test log handling # +##################### + +# output processing + + +class MessageLogger(object): + + """File-like object for logging messages (structured logs)""" + + BUFFERING_THRESHOLD = 100 + # This is a delimiter used by the JS side to avoid logs interleaving + DELIMITER = "\ue175\uee31\u2c32\uacbf" + BUFFERED_ACTIONS = set(["test_status", "log"]) + VALID_ACTIONS = set( + [ + "suite_start", + "suite_end", + "test_start", + "test_end", + "test_status", + "log", + "assertion_count", + "buffering_on", + "buffering_off", + ] + ) + # Regexes that will be replaced with an empty string if found in a test + # name. We do this to normalize test names which may contain URLs and test + # package prefixes. + TEST_PATH_PREFIXES = [ + r"^/tests/", + r"^\w+://[\w\.]+(:\d+)?(/\w+)?/(tests?|a11y|chrome|browser)/", + ] + + def __init__(self, logger, buffering=True, structured=True): + self.logger = logger + self.structured = structured + self.gecko_id = "GECKO" + self.is_test_running = False + + # Even if buffering is enabled, we only want to buffer messages between + # TEST-START/TEST-END. So it is off to begin, but will be enabled after + # a TEST-START comes in. + self._buffering = False + self.restore_buffering = buffering + + # Guard to ensure we never buffer if this value was initially `False` + self._buffering_initially_enabled = buffering + + # Message buffering + self.buffered_messages = [] + + def validate(self, obj): + """Tests whether the given object is a valid structured message + (only does a superficial validation)""" + if not ( + isinstance(obj, dict) + and "action" in obj + and obj["action"] in MessageLogger.VALID_ACTIONS + ): + raise ValueError + + def _fix_subtest_name(self, message): + """Make sure subtest name is a string""" + if "subtest" in message and not isinstance( + message["subtest"], six.string_types + ): + message["subtest"] = str(message["subtest"]) + + def _fix_test_name(self, message): + """Normalize a logged test path to match the relative path from the sourcedir.""" + if message.get("test") is not None: + test = message["test"] + for pattern in MessageLogger.TEST_PATH_PREFIXES: + test = re.sub(pattern, "", test) + if test != message["test"]: + message["test"] = test + break + + def _fix_message_format(self, message): + if "message" in message: + if isinstance(message["message"], bytes): + message["message"] = message["message"].decode("utf-8", "replace") + elif not isinstance(message["message"], six.text_type): + message["message"] = six.text_type(message["message"]) + + def parse_line(self, line): + """Takes a given line of input (structured or not) and + returns a list of structured messages""" + if isinstance(line, six.binary_type): + # if line is a sequence of bytes, let's decode it + line = line.rstrip().decode("UTF-8", "replace") + else: + # line is in unicode - so let's use it as it is + line = line.rstrip() + + messages = [] + for fragment in line.split(MessageLogger.DELIMITER): + if not fragment: + continue + try: + message = json.loads(fragment) + self.validate(message) + except ValueError: + if self.structured: + message = dict( + action="process_output", + process=self.gecko_id, + data=fragment, + ) + else: + message = dict( + action="log", + level="info", + message=fragment, + ) + + self._fix_subtest_name(message) + self._fix_test_name(message) + self._fix_message_format(message) + messages.append(message) + + return messages + + @property + def buffering(self): + if not self._buffering_initially_enabled: + return False + return self._buffering + + @buffering.setter + def buffering(self, val): + self._buffering = val + + def process_message(self, message): + """Processes a structured message. Takes into account buffering, errors, ...""" + # Activation/deactivating message buffering from the JS side + if message["action"] == "buffering_on": + if self.is_test_running: + self.buffering = True + return + if message["action"] == "buffering_off": + self.buffering = False + return + + # Error detection also supports "raw" errors (in log messages) because some tests + # manually dump 'TEST-UNEXPECTED-FAIL'. + if "expected" in message or ( + message["action"] == "log" + and message.get("message", "").startswith("TEST-UNEXPECTED") + ): + self.restore_buffering = self.restore_buffering or self.buffering + self.buffering = False + if self.buffered_messages: + snipped = len(self.buffered_messages) - self.BUFFERING_THRESHOLD + if snipped > 0: + self.logger.info( + "<snipped {0} output lines - " + "if you need more context, please use " + "SimpleTest.requestCompleteLog() in your test>".format(snipped) + ) + # Dumping previously buffered messages + self.dump_buffered(limit=True) + + # Logging the error message + self.logger.log_raw(message) + # Determine if message should be buffered + elif ( + self.buffering + and self.structured + and message["action"] in self.BUFFERED_ACTIONS + ): + self.buffered_messages.append(message) + # Otherwise log the message directly + else: + self.logger.log_raw(message) + + # If a test ended, we clean the buffer + if message["action"] == "test_end": + self.is_test_running = False + self.buffered_messages = [] + self.restore_buffering = self.restore_buffering or self.buffering + self.buffering = False + + if message["action"] == "test_start": + self.is_test_running = True + if self.restore_buffering: + self.restore_buffering = False + self.buffering = True + + def write(self, line): + messages = self.parse_line(line) + for message in messages: + self.process_message(message) + return messages + + def flush(self): + sys.stdout.flush() + + def dump_buffered(self, limit=False): + if limit: + dumped_messages = self.buffered_messages[-self.BUFFERING_THRESHOLD :] + else: + dumped_messages = self.buffered_messages + + last_timestamp = None + for buf in dumped_messages: + # pylint --py3k W1619 + timestamp = datetime.fromtimestamp(buf["time"] / 1000).strftime("%H:%M:%S") + if timestamp != last_timestamp: + self.logger.info("Buffered messages logged at {}".format(timestamp)) + last_timestamp = timestamp + + self.logger.log_raw(buf) + self.logger.info("Buffered messages finished") + # Cleaning the list of buffered messages + self.buffered_messages = [] + + def finish(self): + self.dump_buffered() + self.buffering = False + self.logger.suite_end() + + +#################### +# PROCESS HANDLING # +#################### + + +def call(*args, **kwargs): + """front-end function to mozprocess.ProcessHandler""" + # TODO: upstream -> mozprocess + # https://bugzilla.mozilla.org/show_bug.cgi?id=791383 + log = get_proxy_logger("mochitest") + + def on_output(line): + log.process_output( + process=process.pid, + data=line.decode("utf8", "replace"), + command=process.commandline, + ) + + process = mozprocess.ProcessHandlerMixin( + *args, processOutputLine=on_output, **kwargs + ) + process.run() + return process.wait() + + +def killPid(pid, log): + # see also https://bugzilla.mozilla.org/show_bug.cgi?id=911249#c58 + + if HAVE_PSUTIL: + # Kill a process tree (including grandchildren) with signal.SIGTERM + if pid == os.getpid(): + raise RuntimeError("Error: trying to kill ourselves, not another process") + try: + parent = psutil.Process(pid) + children = parent.children(recursive=True) + children.append(parent) + for p in children: + p.send_signal(signal.SIGTERM) + gone, alive = psutil.wait_procs(children, timeout=30) + for p in gone: + log.info("psutil found pid %s dead" % p.pid) + for p in alive: + log.info("failed to kill pid %d after 30s" % p.pid) + except Exception as e: + log.info("Error: Failed to kill process %d: %s" % (pid, str(e))) + else: + try: + os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM)) + except Exception as e: + log.info("Failed to kill process %d: %s" % (pid, str(e))) + + +if mozinfo.isWin: + import ctypes.wintypes + + def isPidAlive(pid): + STILL_ACTIVE = 259 + PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + pHandle = ctypes.windll.kernel32.OpenProcess( + PROCESS_QUERY_LIMITED_INFORMATION, 0, pid + ) + if not pHandle: + return False + + try: + pExitCode = ctypes.wintypes.DWORD() + ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode)) + + if pExitCode.value != STILL_ACTIVE: + return False + + # We have a live process handle. But Windows aggressively + # re-uses pids, so let's attempt to verify that this is + # actually Firefox. + namesize = 1024 + pName = ctypes.create_string_buffer(namesize) + namelen = ctypes.windll.psapi.GetProcessImageFileNameA( + pHandle, pName, namesize + ) + if namelen == 0: + # Still an active process, so conservatively assume it's Firefox. + return True + + return pName.value.endswith((b"firefox.exe", b"plugin-container.exe")) + finally: + ctypes.windll.kernel32.CloseHandle(pHandle) + + +else: + import errno + + def isPidAlive(pid): + try: + # kill(pid, 0) checks for a valid PID without actually sending a signal + # The method throws OSError if the PID is invalid, which we catch + # below. + os.kill(pid, 0) + + # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if + # the process terminates before we get to this point. + wpid, wstatus = os.waitpid(pid, os.WNOHANG) + return wpid == 0 + except OSError as err: + # Catch the errors we might expect from os.kill/os.waitpid, + # and re-raise any others + if err.errno in (errno.ESRCH, errno.ECHILD, errno.EPERM): + return False + raise + + +# TODO: ^ upstream isPidAlive to mozprocess + +####################### +# HTTP SERVER SUPPORT # +####################### + + +class MochitestServer(object): + + "Web server used to serve Mochitests, for closer fidelity to the real web." + + def __init__(self, options, logger): + if isinstance(options, Namespace): + options = vars(options) + self._log = logger + self._keep_open = bool(options["keep_open"]) + self._utilityPath = options["utilityPath"] + self._xrePath = options["xrePath"] + self._profileDir = options["profilePath"] + self.webServer = options["webServer"] + self.httpPort = options["httpPort"] + if options.get("remoteWebServer") == "10.0.2.2": + # probably running an Android emulator and 10.0.2.2 will + # not be visible from host + shutdownServer = "127.0.0.1" + else: + shutdownServer = self.webServer + self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % { + "server": shutdownServer, + "port": self.httpPort, + } + self.testPrefix = "undefined" + + if options.get("httpdPath"): + self._httpdPath = options["httpdPath"] + else: + self._httpdPath = SCRIPT_DIR + self._httpdPath = os.path.abspath(self._httpdPath) + + def start(self): + "Run the Mochitest server, returning the process ID of the server." + + # get testing environment + env = test_environment(xrePath=self._xrePath, log=self._log) + env["XPCOM_DEBUG_BREAK"] = "warn" + if "LD_LIBRARY_PATH" not in env or env["LD_LIBRARY_PATH"] is None: + env["LD_LIBRARY_PATH"] = self._xrePath + else: + env["LD_LIBRARY_PATH"] = ":".join([self._xrePath, env["LD_LIBRARY_PATH"]]) + + # When running with an ASan build, our xpcshell server will also be ASan-enabled, + # thus consuming too much resources when running together with the browser on + # the test machines. Try to limit the amount of resources by disabling certain + # features. + env["ASAN_OPTIONS"] = "quarantine_size=1:redzone=32:malloc_context_size=5" + + # Likewise, when running with a TSan build, our xpcshell server will + # also be TSan-enabled. Except that in this case, we don't really + # care about races in xpcshell. So disable TSan for the server. + env["TSAN_OPTIONS"] = "report_bugs=0" + + # Don't use socket process for the xpcshell server. + env["MOZ_DISABLE_SOCKET_PROCESS"] = "1" + + if mozinfo.isWin: + env["PATH"] = env["PATH"] + ";" + str(self._xrePath) + + args = [ + "-g", + self._xrePath, + "-f", + os.path.join(self._httpdPath, "httpd.js"), + "-e", + "const _PROFILE_PATH = '%(profile)s'; const _SERVER_PORT = '%(port)s'; " + "const _SERVER_ADDR = '%(server)s'; const _TEST_PREFIX = %(testPrefix)s; " + "const _DISPLAY_RESULTS = %(displayResults)s;" + % { + "profile": self._profileDir.replace("\\", "\\\\"), + "port": self.httpPort, + "server": self.webServer, + "testPrefix": self.testPrefix, + "displayResults": str(self._keep_open).lower(), + }, + "-f", + os.path.join(SCRIPT_DIR, "server.js"), + ] + + xpcshell = os.path.join( + self._utilityPath, "xpcshell" + mozinfo.info["bin_suffix"] + ) + command = [xpcshell] + args + self._process = mozprocess.ProcessHandler(command, cwd=SCRIPT_DIR, env=env) + self._process.run() + self._log.info("%s : launching %s" % (self.__class__.__name__, command)) + pid = self._process.pid + self._log.info("runtests.py | Server pid: %d" % pid) + + def ensureReady(self, timeout): + assert timeout >= 0 + + aliveFile = os.path.join(self._profileDir, "server_alive.txt") + i = 0 + while i < timeout: + if os.path.exists(aliveFile): + break + time.sleep(0.05) + i += 0.05 + else: + self._log.error( + "TEST-UNEXPECTED-FAIL | runtests.py | Timed out while waiting for server startup." + ) + self.stop() + sys.exit(1) + + def stop(self): + try: + with closing(urlopen(self.shutdownURL)) as c: + self._log.info(six.ensure_text(c.read())) + except Exception: + self._log.info("Failed to stop web server on %s" % self.shutdownURL) + traceback.print_exc() + finally: + if self._process is not None: + # Kill the server immediately to avoid logging intermittent + # shutdown crashes, sometimes observed on Windows 10. + self._process.kill() + self._log.info("Web server killed.") + + +class WebSocketServer(object): + + "Class which encapsulates the mod_pywebsocket server" + + def __init__(self, options, scriptdir, logger, debuggerInfo=None): + self.port = options.webSocketPort + self.debuggerInfo = debuggerInfo + self._log = logger + self._scriptdir = scriptdir + + def start(self): + # Invoke pywebsocket through a wrapper which adds special SIGINT handling. + # + # If we're in an interactive debugger, the wrapper causes the server to + # ignore SIGINT so the server doesn't capture a ctrl+c meant for the + # debugger. + # + # If we're not in an interactive debugger, the wrapper causes the server to + # die silently upon receiving a SIGINT. + scriptPath = "pywebsocket_wrapper.py" + script = os.path.join(self._scriptdir, scriptPath) + + cmd = [sys.executable, script] + if self.debuggerInfo and self.debuggerInfo.interactive: + cmd += ["--interactive"] + cmd += [ + "-H", + "127.0.0.1", + "-p", + str(self.port), + "-w", + self._scriptdir, + "-l", + os.path.join(self._scriptdir, "websock.log"), + "--log-level=debug", + "--allow-handlers-outside-root-dir", + ] + env = dict(os.environ) + env["PYTHONPATH"] = os.pathsep.join(sys.path) + # Start the process. Ignore stderr so that exceptions from the server + # are not treated as failures when parsing the test log. + self._process = mozprocess.ProcessHandler( + cmd, + cwd=SCRIPT_DIR, + env=env, + processStderrLine=lambda _: None, + ) + self._process.run() + pid = self._process.pid + self._log.info("runtests.py | Websocket server pid: %d" % pid) + + def stop(self): + if self._process is not None: + self._process.kill() + + +class SSLTunnel: + def __init__(self, options, logger): + self.log = logger + self.process = None + self.utilityPath = options.utilityPath + self.xrePath = options.xrePath + self.certPath = options.certPath + self.sslPort = options.sslPort + self.httpPort = options.httpPort + self.webServer = options.webServer + self.webSocketPort = options.webSocketPort + + self.customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)") + self.clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)") + self.redirRE = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)") + + def writeLocation(self, config, loc): + for option in loc.options: + match = self.customCertRE.match(option) + if match: + customcert = match.group("nickname") + config.write( + "listen:%s:%s:%s:%s\n" + % (loc.host, loc.port, self.sslPort, customcert) + ) + + match = self.clientAuthRE.match(option) + if match: + clientauth = match.group("clientauth") + config.write( + "clientauth:%s:%s:%s:%s\n" + % (loc.host, loc.port, self.sslPort, clientauth) + ) + + match = self.redirRE.match(option) + if match: + redirhost = match.group("redirhost") + config.write( + "redirhost:%s:%s:%s:%s\n" + % (loc.host, loc.port, self.sslPort, redirhost) + ) + + if option in ( + "tls1", + "tls1_1", + "tls1_2", + "tls1_3", + "ssl3", + "3des", + "failHandshake", + ): + config.write( + "%s:%s:%s:%s\n" % (option, loc.host, loc.port, self.sslPort) + ) + + def buildConfig(self, locations, public=None): + """Create the ssltunnel configuration file""" + configFd, self.configFile = tempfile.mkstemp(prefix="ssltunnel", suffix=".cfg") + with os.fdopen(configFd, "w") as config: + config.write("httpproxy:1\n") + config.write("certdbdir:%s\n" % self.certPath) + config.write("forward:127.0.0.1:%s\n" % self.httpPort) + config.write( + "websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort) + ) + # Use "*" to tell ssltunnel to listen on the public ip + # address instead of the loopback address 127.0.0.1. This + # may have the side-effect of causing firewall warnings on + # macOS and Windows. Use "127.0.0.1" to listen on the + # loopback address. Remote tests using physical or + # emulated Android devices must use the public ip address + # in order for the sslproxy to work but Desktop tests + # which run on the same host as ssltunnel may use the + # loopback address. + listen_address = "*" if public else "127.0.0.1" + config.write("listen:%s:%s:pgoserver\n" % (listen_address, self.sslPort)) + + for loc in locations: + if loc.scheme == "https" and "nocert" not in loc.options: + self.writeLocation(config, loc) + + def start(self): + """Starts the SSL Tunnel""" + + # start ssltunnel to provide https:// URLs capability + ssltunnel = os.path.join(self.utilityPath, "ssltunnel") + if os.name == "nt": + ssltunnel += ".exe" + if not os.path.exists(ssltunnel): + self.log.error( + "INFO | runtests.py | expected to find ssltunnel at %s" % ssltunnel + ) + exit(1) + + env = test_environment(xrePath=self.xrePath, log=self.log) + env["LD_LIBRARY_PATH"] = self.xrePath + self.process = mozprocess.ProcessHandler([ssltunnel, self.configFile], env=env) + self.process.run() + self.log.info("runtests.py | SSL tunnel pid: %d" % self.process.pid) + + def stop(self): + """Stops the SSL Tunnel and cleans up""" + if self.process is not None: + self.process.kill() + if os.path.exists(self.configFile): + os.remove(self.configFile) + + +def checkAndConfigureV4l2loopback(device): + """ + Determine if a given device path is a v4l2loopback device, and if so + toggle a few settings on it via fcntl. Very linux-specific. + + Returns (status, device name) where status is a boolean. + """ + if not mozinfo.isLinux: + return False, "" + + libc = ctypes.cdll.LoadLibrary(find_library("c")) + O_RDWR = 2 + # These are from linux/videodev2.h + + class v4l2_capability(ctypes.Structure): + _fields_ = [ + ("driver", ctypes.c_char * 16), + ("card", ctypes.c_char * 32), + ("bus_info", ctypes.c_char * 32), + ("version", ctypes.c_uint32), + ("capabilities", ctypes.c_uint32), + ("device_caps", ctypes.c_uint32), + ("reserved", ctypes.c_uint32 * 3), + ] + + VIDIOC_QUERYCAP = 0x80685600 + + fd = libc.open(six.ensure_binary(device), O_RDWR) + if fd < 0: + return False, "" + + vcap = v4l2_capability() + if libc.ioctl(fd, VIDIOC_QUERYCAP, ctypes.byref(vcap)) != 0: + return False, "" + + if six.ensure_text(vcap.driver) != "v4l2 loopback": + return False, "" + + class v4l2_control(ctypes.Structure): + _fields_ = [("id", ctypes.c_uint32), ("value", ctypes.c_int32)] + + # These are private v4l2 control IDs, see: + # https://github.com/umlaeute/v4l2loopback/blob/fd822cf0faaccdf5f548cddd9a5a3dcebb6d584d/v4l2loopback.c#L131 + KEEP_FORMAT = 0x8000000 + SUSTAIN_FRAMERATE = 0x8000001 + VIDIOC_S_CTRL = 0xC008561C + + control = v4l2_control() + control.id = KEEP_FORMAT + control.value = 1 + libc.ioctl(fd, VIDIOC_S_CTRL, ctypes.byref(control)) + + control.id = SUSTAIN_FRAMERATE + control.value = 1 + libc.ioctl(fd, VIDIOC_S_CTRL, ctypes.byref(control)) + libc.close(fd) + + return True, six.ensure_text(vcap.card) + + +def findTestMediaDevices(log): + """ + Find the test media devices configured on this system, and return a dict + containing information about them. The dict will have keys for 'audio' + and 'video', each containing the name of the media device to use. + + If audio and video devices could not be found, return None. + + This method is only currently implemented for Linux. + """ + if not mozinfo.isLinux: + return None + + info = {} + # Look for a v4l2loopback device. + name = None + device = None + for dev in sorted(glob.glob("/dev/video*")): + result, name_ = checkAndConfigureV4l2loopback(dev) + if result: + name = name_ + device = dev + break + + if not (name and device): + log.error("Couldn't find a v4l2loopback video device") + return None + + # Feed it a frame of output so it has something to display + gst01 = spawn.find_executable("gst-launch-0.1") + gst010 = spawn.find_executable("gst-launch-0.10") + gst10 = spawn.find_executable("gst-launch-1.0") + if gst01: + gst = gst01 + if gst010: + gst = gst010 + else: + gst = gst10 + process = mozprocess.ProcessHandler( + [ + gst, + "--no-fault", + "videotestsrc", + "pattern=green", + "num-buffers=1", + "!", + "v4l2sink", + "device=%s" % device, + ] + ) + process.run() + info["video"] = {"name": name, "process": process} + + # check if PulseAudio module-null-sink is loaded + pactl = spawn.find_executable("pactl") + + if not pactl: + log.error("Could not find pactl on system") + return None + + try: + o = subprocess.check_output([pactl, "list", "short", "modules"]) + except subprocess.CalledProcessError: + log.error("Could not list currently loaded modules") + return None + + null_sink = [x for x in o.splitlines() if b"module-null-sink" in x] + + if not null_sink: + try: + subprocess.check_call([pactl, "load-module", "module-null-sink"]) + except subprocess.CalledProcessError: + log.error("Could not load module-null-sink") + return None + + # Hardcode the name since it's always the same. + info["audio"] = {"name": "Monitor of Null Output"} + return info + + +def create_zip(path): + """ + Takes a `path` on disk and creates a zipfile with its contents. Returns a + path to the location of the temporary zip file. + """ + with tempfile.NamedTemporaryFile() as f: + # `shutil.make_archive` writes to "{f.name}.zip", so we're really just + # using `NamedTemporaryFile` as a way to get a random path. + return shutil.make_archive(f.name, "zip", path) + + +def update_mozinfo(): + """walk up directories to find mozinfo.json update the info""" + # TODO: This should go in a more generic place, e.g. mozinfo + + path = SCRIPT_DIR + dirs = set() + while path != os.path.expanduser("~"): + if path in dirs: + break + dirs.add(path) + path = os.path.split(path)[0] + + mozinfo.find_and_update_from_json(*dirs) + + +class MochitestDesktop(object): + """ + Mochitest class for desktop firefox. + """ + + oldcwd = os.getcwd() + + # Path to the test script on the server + TEST_PATH = "tests" + CHROME_PATH = "redirect.html" + + certdbNew = False + sslTunnel = None + DEFAULT_TIMEOUT = 60.0 + mediaDevices = None + mozinfo_variables_shown = False + + patternFiles = {} + + # XXX use automation.py for test name to avoid breaking legacy + # TODO: replace this with 'runtests.py' or 'mochitest' or the like + test_name = "automation.py" + + def __init__(self, flavor, logger_options, staged_addons=None, quiet=False): + update_mozinfo() + self.flavor = flavor + self.staged_addons = staged_addons + self.server = None + self.wsserver = None + self.websocketProcessBridge = None + self.sslTunnel = None + self.manifest = None + self.tests_by_manifest = defaultdict(list) + self.args_by_manifest = defaultdict(set) + self.prefs_by_manifest = defaultdict(set) + self.env_vars_by_manifest = defaultdict(set) + self.tests_dirs_by_manifest = defaultdict(set) + self._active_tests = None + self.currentTests = None + self._locations = None + self.browserEnv = None + + self.marionette = None + self.start_script = None + self.mozLogs = None + self.start_script_kwargs = {} + self.extraArgs = [] + self.extraPrefs = {} + self.extraEnv = {} + self.extraTestsDirs = [] + self.conditioned_profile_dir = None + + if logger_options.get("log"): + self.log = logger_options["log"] + else: + self.log = commandline.setup_logging( + "mochitest", logger_options, {"tbpl": sys.stdout} + ) + + self.message_logger = MessageLogger( + logger=self.log, buffering=quiet, structured=True + ) + + # Max time in seconds to wait for server startup before tests will fail -- if + # this seems big, it's mostly for debug machines where cold startup + # (particularly after a build) takes forever. + self.SERVER_STARTUP_TIMEOUT = 180 if mozinfo.info.get("debug") else 90 + + # metro browser sub process id + self.browserProcessId = None + + self.haveDumpedScreen = False + # Create variables to count the number of passes, fails, todos. + self.countpass = 0 + self.countfail = 0 + self.counttodo = 0 + + self.expectedError = {} + self.result = {} + + self.start_script = os.path.join(here, "start_desktop.js") + + # Used to temporarily serve a performance profile + self.profiler_tempdir = None + + def environment(self, **kwargs): + kwargs["log"] = self.log + return test_environment(**kwargs) + + def getFullPath(self, path): + "Get an absolute path relative to self.oldcwd." + return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path))) + + def getLogFilePath(self, logFile): + """return the log file path relative to the device we are testing on, in most cases + it will be the full path on the local system + """ + return self.getFullPath(logFile) + + @property + def locations(self): + if self._locations is not None: + return self._locations + locations_file = os.path.join(SCRIPT_DIR, "server-locations.txt") + self._locations = ServerLocations(locations_file) + return self._locations + + def buildURLOptions(self, options, env): + """Add test control options from the command line to the url + + URL parameters to test URL: + + autorun -- kick off tests automatically + closeWhenDone -- closes the browser after the tests + hideResultsTable -- hides the table of individual test results + logFile -- logs test run to an absolute path + startAt -- name of test to start at + endAt -- name of test to end at + timeout -- per-test timeout in seconds + repeat -- How many times to repeat the test, ie: repeat=1 will run the test twice. + """ + self.urlOpts = [] + + if not hasattr(options, "logFile"): + options.logFile = "" + if not hasattr(options, "fileLevel"): + options.fileLevel = "INFO" + + # allow relative paths for logFile + if options.logFile: + options.logFile = self.getLogFilePath(options.logFile) + + if options.flavor in ("a11y", "browser", "chrome"): + self.makeTestConfig(options) + else: + if options.autorun: + self.urlOpts.append("autorun=1") + if options.timeout: + self.urlOpts.append("timeout=%d" % options.timeout) + if options.maxTimeouts: + self.urlOpts.append("maxTimeouts=%d" % options.maxTimeouts) + if not options.keep_open: + self.urlOpts.append("closeWhenDone=1") + if options.logFile: + self.urlOpts.append("logFile=" + encodeURIComponent(options.logFile)) + self.urlOpts.append( + "fileLevel=" + encodeURIComponent(options.fileLevel) + ) + if options.consoleLevel: + self.urlOpts.append( + "consoleLevel=" + encodeURIComponent(options.consoleLevel) + ) + if options.startAt: + self.urlOpts.append("startAt=%s" % options.startAt) + if options.endAt: + self.urlOpts.append("endAt=%s" % options.endAt) + if options.shuffle: + self.urlOpts.append("shuffle=1") + if "MOZ_HIDE_RESULTS_TABLE" in env and env["MOZ_HIDE_RESULTS_TABLE"] == "1": + self.urlOpts.append("hideResultsTable=1") + if options.runUntilFailure: + self.urlOpts.append("runUntilFailure=1") + if options.repeat: + self.urlOpts.append("repeat=%d" % options.repeat) + if len(options.test_paths) == 1 and os.path.isfile( + os.path.join( + self.oldcwd, + os.path.dirname(__file__), + self.TEST_PATH, + options.test_paths[0], + ) + ): + self.urlOpts.append( + "testname=%s" % "/".join([self.TEST_PATH, options.test_paths[0]]) + ) + if options.manifestFile: + self.urlOpts.append("manifestFile=%s" % options.manifestFile) + if options.failureFile: + self.urlOpts.append( + "failureFile=%s" % self.getFullPath(options.failureFile) + ) + if options.runSlower: + self.urlOpts.append("runSlower=true") + if options.debugOnFailure: + self.urlOpts.append("debugOnFailure=true") + if options.dumpOutputDirectory: + self.urlOpts.append( + "dumpOutputDirectory=%s" + % encodeURIComponent(options.dumpOutputDirectory) + ) + if options.dumpAboutMemoryAfterTest: + self.urlOpts.append("dumpAboutMemoryAfterTest=true") + if options.dumpDMDAfterTest: + self.urlOpts.append("dumpDMDAfterTest=true") + if options.debugger: + self.urlOpts.append("interactiveDebugger=true") + if options.jscov_dir_prefix: + self.urlOpts.append("jscovDirPrefix=%s" % options.jscov_dir_prefix) + if options.cleanupCrashes: + self.urlOpts.append("cleanupCrashes=true") + if "MOZ_XORIGIN_MOCHITEST" in env and env["MOZ_XORIGIN_MOCHITEST"] == "1": + options.xOriginTests = True + if options.xOriginTests: + self.urlOpts.append("xOriginTests=true") + + def normflavor(self, flavor): + """ + In some places the string 'browser-chrome' is expected instead of + 'browser' and 'mochitest' instead of 'plain'. Normalize the flavor + strings for those instances. + """ + # TODO Use consistent flavor strings everywhere and remove this + if flavor == "browser": + return "browser-chrome" + elif flavor == "plain": + return "mochitest" + return flavor + + # This check can be removed when bug 983867 is fixed. + def isTest(self, options, filename): + allow_js_css = False + if options.flavor == "browser": + allow_js_css = True + testPattern = re.compile(r"browser_.+\.js") + elif options.flavor in ("a11y", "chrome"): + testPattern = re.compile(r"(browser|test)_.+\.(xul|html|js|xhtml)") + else: + testPattern = re.compile(r"test_") + + if not allow_js_css and (".js" in filename or ".css" in filename): + return False + + pathPieces = filename.split("/") + + return testPattern.match(pathPieces[-1]) and not re.search( + r"\^headers\^$", filename + ) + + def setTestRoot(self, options): + if options.flavor != "plain": + self.testRoot = options.flavor + else: + self.testRoot = self.TEST_PATH + self.testRootAbs = os.path.join(SCRIPT_DIR, self.testRoot) + + def buildTestURL(self, options, scheme="http"): + if scheme == "https": + testHost = "https://example.com:443" + elif options.xOriginTests: + testHost = "http://mochi.xorigin-test:8888" + else: + testHost = "http://mochi.test:8888" + testURL = "/".join([testHost, self.TEST_PATH]) + + if len(options.test_paths) == 1: + if os.path.isfile( + os.path.join( + self.oldcwd, + os.path.dirname(__file__), + self.TEST_PATH, + options.test_paths[0], + ) + ): + testURL = "/".join([testURL, os.path.dirname(options.test_paths[0])]) + else: + testURL = "/".join([testURL, options.test_paths[0]]) + + if options.flavor in ("a11y", "chrome"): + testURL = "/".join([testHost, self.CHROME_PATH]) + elif options.flavor == "browser": + testURL = "about:blank" + return testURL + + def parseAndCreateTestsDirs(self, m): + testsDirs = list(self.tests_dirs_by_manifest[m])[0] + self.extraTestsDirs = [] + if testsDirs: + self.extraTestsDirs = testsDirs.strip().split() + self.log.info( + "The following extra test directories will be created:\n {}".format( + "\n ".join(self.extraTestsDirs) + ) + ) + self.createExtraTestsDirs(self.extraTestsDirs, m) + + def createExtraTestsDirs(self, extraTestsDirs=None, manifest=None): + """Take a list of directories that might be needed to exist by the test + prior to even the main process be executed, and: + - verify it does not already exists + - create it if it does + Removal of those directories is handled in cleanup() + """ + if type(extraTestsDirs) != list: + return + + for d in extraTestsDirs: + if os.path.exists(d): + raise FileExistsError( + "Directory '{}' already exists. This is a member of " + "test-directories in manifest {}.".format(d, manifest) + ) + + created = [] + for d in extraTestsDirs: + os.makedirs(d) + created += [d] + + if created != extraTestsDirs: + raise EnvironmentError( + "Not all directories were created: extraTestsDirs={} -- created={}".format( + extraTestsDirs, created + ) + ) + + def getTestsByScheme( + self, options, testsToFilter=None, disabled=True, manifestToFilter=None + ): + """Build the url path to the specific test harness and test file or directory + Build a manifest of tests to run and write out a json file for the harness to read + testsToFilter option is used to filter/keep the tests provided in the list + + disabled -- This allows to add all disabled tests on the build side + and then on the run side to only run the enabled ones + """ + + tests = self.getActiveTests(options, disabled) + paths = [] + for test in tests: + if testsToFilter and (test["path"] not in testsToFilter): + continue + # If we are running a specific manifest, the previously computed set of active + # tests should be filtered out based on the manifest that contains that entry. + # + # This is especially important when a test file is listed in multiple + # manifests (e.g. because the same test runs under a different configuration, + # and so it is being included in multiple manifests), without filtering the + # active tests based on the current manifest (configuration) that we are + # running for each of the N manifests we would be executing the active tests + # exactly N times (and so NxN runs instead of the expected N runs, one for each + # manifest). + if manifestToFilter and (test["manifest"] not in manifestToFilter): + continue + paths.append(test) + + # Generate test by schemes + for (scheme, grouped_tests) in self.groupTestsByScheme(paths).items(): + # Bug 883865 - add this functionality into manifestparser + with open( + os.path.join(SCRIPT_DIR, options.testRunManifestFile), "w" + ) as manifestFile: + manifestFile.write(json.dumps({"tests": grouped_tests})) + options.manifestFile = options.testRunManifestFile + yield (scheme, grouped_tests) + + def startWebSocketServer(self, options, debuggerInfo): + """Launch the websocket server""" + self.wsserver = WebSocketServer(options, SCRIPT_DIR, self.log, debuggerInfo) + self.wsserver.start() + + def startWebServer(self, options): + """Create the webserver and start it up""" + + self.server = MochitestServer(options, self.log) + self.server.start() + + if options.pidFile != "": + with open(options.pidFile + ".xpcshell.pid", "w") as f: + f.write("%s" % self.server._process.pid) + + def startWebsocketProcessBridge(self, options): + """Create a websocket server that can launch various processes that + JS needs (eg; ICE server for webrtc testing) + """ + + command = [ + sys.executable, + os.path.join("websocketprocessbridge", "websocketprocessbridge.py"), + "--port", + options.websocket_process_bridge_port, + ] + self.websocketProcessBridge = mozprocess.ProcessHandler(command, cwd=SCRIPT_DIR) + self.websocketProcessBridge.run() + self.log.info( + "runtests.py | websocket/process bridge pid: %d" + % self.websocketProcessBridge.pid + ) + + # ensure the server is up, wait for at most ten seconds + for i in range(1, 100): + if self.websocketProcessBridge.proc.poll() is not None: + self.log.error( + "runtests.py | websocket/process bridge failed " + "to launch. Are all the dependencies installed?" + ) + return + + try: + sock = socket.create_connection(("127.0.0.1", 8191)) + sock.close() + break + except Exception: + time.sleep(0.1) + else: + self.log.error( + "runtests.py | Timed out while waiting for " + "websocket/process bridge startup." + ) + + def needsWebsocketProcessBridge(self, options): + """ + Returns a bool indicating if the current test configuration needs + to start the websocket process bridge or not. The boils down to if + WebRTC tests that need the bridge are present. + """ + tests = self.getActiveTests(options) + is_webrtc_tag_present = False + for test in tests: + if "webrtc" in test.get("tags", ""): + is_webrtc_tag_present = True + break + return is_webrtc_tag_present and options.subsuite in ["media"] + + def startServers(self, options, debuggerInfo, public=None): + # start servers and set ports + # TODO: pass these values, don't set on `self` + self.webServer = options.webServer + self.httpPort = options.httpPort + self.sslPort = options.sslPort + self.webSocketPort = options.webSocketPort + + # httpd-path is specified by standard makefile targets and may be specified + # on the command line to select a particular version of httpd.js. If not + # specified, try to select the one from hostutils.zip, as required in + # bug 882932. + if not options.httpdPath: + options.httpdPath = os.path.join(options.utilityPath, "components") + + self.startWebServer(options) + self.startWebSocketServer(options, debuggerInfo) + + # Only webrtc mochitests in the media suite need the websocketprocessbridge. + if self.needsWebsocketProcessBridge(options): + self.startWebsocketProcessBridge(options) + + # start SSL pipe + self.sslTunnel = SSLTunnel(options, logger=self.log) + self.sslTunnel.buildConfig(self.locations, public=public) + self.sslTunnel.start() + + # If we're lucky, the server has fully started by now, and all paths are + # ready, etc. However, xpcshell cold start times suck, at least for debug + # builds. We'll try to connect to the server for awhile, and if we fail, + # we'll try to kill the server and exit with an error. + if self.server is not None: + self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT) + + def stopServers(self): + """Servers are no longer needed, and perhaps more importantly, anything they + might spew to console might confuse things.""" + if self.server is not None: + try: + self.log.info("Stopping web server") + self.server.stop() + except Exception: + self.log.critical("Exception when stopping web server") + + if self.wsserver is not None: + try: + self.log.info("Stopping web socket server") + self.wsserver.stop() + except Exception: + self.log.critical("Exception when stopping web socket server") + + if self.sslTunnel is not None: + try: + self.log.info("Stopping ssltunnel") + self.sslTunnel.stop() + except Exception: + self.log.critical("Exception stopping ssltunnel") + + if self.websocketProcessBridge is not None: + try: + self.websocketProcessBridge.kill() + self.websocketProcessBridge.wait() + self.log.info("Stopping websocket/process bridge") + except Exception: + self.log.critical("Exception stopping websocket/process bridge") + + if hasattr(self, "gstForV4l2loopbackProcess"): + try: + self.gstForV4l2loopbackProcess.kill() + self.gstForV4l2loopbackProcess.wait() + self.log.info("Stopping gst for v4l2loopback") + except Exception: + self.log.critical("Exception stopping gst for v4l2loopback") + + def copyExtraFilesToProfile(self, options): + "Copy extra files or dirs specified on the command line to the testing profile." + for f in options.extraProfileFiles: + abspath = self.getFullPath(f) + if os.path.isfile(abspath): + shutil.copy2(abspath, options.profilePath) + elif os.path.isdir(abspath): + dest = os.path.join(options.profilePath, os.path.basename(abspath)) + shutil.copytree(abspath, dest) + else: + self.log.warning("runtests.py | Failed to copy %s to profile" % abspath) + + def getChromeTestDir(self, options): + dir = os.path.join(os.path.abspath("."), SCRIPT_DIR) + "/" + if mozinfo.isWin: + dir = "file:///" + dir.replace("\\", "/") + return dir + + def writeChromeManifest(self, options): + manifest = os.path.join(options.profilePath, "tests.manifest") + with open(manifest, "w") as manifestFile: + # Register chrome directory. + chrometestDir = self.getChromeTestDir(options) + manifestFile.write( + "content mochitests %s contentaccessible=yes\n" % chrometestDir + ) + manifestFile.write( + "content mochitests-any %s contentaccessible=yes remoteenabled=yes\n" + % chrometestDir + ) + manifestFile.write( + "content mochitests-content %s contentaccessible=yes remoterequired=yes\n" + % chrometestDir + ) + + if options.testingModulesDir is not None: + manifestFile.write( + "resource testing-common file:///%s\n" % options.testingModulesDir + ) + if options.store_chrome_manifest: + shutil.copyfile(manifest, options.store_chrome_manifest) + return manifest + + def addChromeToProfile(self, options): + "Adds MochiKit chrome tests to the profile." + + # Create (empty) chrome directory. + chromedir = os.path.join(options.profilePath, "chrome") + os.mkdir(chromedir) + + # Write userChrome.css. + chrome = """ +/* set default namespace to XUL */ +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +toolbar, +toolbarpalette { + background-color: rgb(235, 235, 235) !important; +} +toolbar#nav-bar { + background-image: none !important; +} +""" + with open( + os.path.join(options.profilePath, "userChrome.css"), "a" + ) as chromeFile: + chromeFile.write(chrome) + + manifest = self.writeChromeManifest(options) + + return manifest + + def getExtensionsToInstall(self, options): + "Return a list of extensions to install in the profile" + extensions = [] + appDir = ( + options.app[: options.app.rfind(os.sep)] + if options.app + else options.utilityPath + ) + + extensionDirs = [ + # Extensions distributed with the test harness. + os.path.normpath(os.path.join(SCRIPT_DIR, "extensions")), + ] + if appDir: + # Extensions distributed with the application. + extensionDirs.append(os.path.join(appDir, "distribution", "extensions")) + + for extensionDir in extensionDirs: + if os.path.isdir(extensionDir): + for dirEntry in os.listdir(extensionDir): + if dirEntry not in options.extensionsToExclude: + path = os.path.join(extensionDir, dirEntry) + if os.path.isdir(path) or ( + os.path.isfile(path) and path.endswith(".xpi") + ): + extensions.append(path) + extensions.extend(options.extensionsToInstall) + return extensions + + def logPreamble(self, tests): + """Logs a suite_start message and test_start/test_end at the beginning of a run.""" + self.log.suite_start( + self.tests_by_manifest, name="mochitest-{}".format(self.flavor) + ) + for test in tests: + if "disabled" in test: + self.log.test_start(test["path"]) + self.log.test_end(test["path"], "SKIP", message=test["disabled"]) + + def loadFailurePatternFile(self, pat_file): + if pat_file in self.patternFiles: + return self.patternFiles[pat_file] + if not os.path.isfile(pat_file): + self.log.warning( + "runtests.py | Cannot find failure pattern file " + pat_file + ) + return None + + # Using ":error" to ensure it shows up in the failure summary. + self.log.warning( + "[runtests.py:error] Using {} to filter failures. If there " + "is any number mismatch below, you could have fixed " + "something documented in that file. Please reduce the " + "failure count appropriately.".format(pat_file) + ) + patternRE = re.compile( + r""" + ^\s*\*\s* # list bullet + (test_\S+|\.{3}) # test name + (?:\s*(`.+?`|asserts))? # failure pattern + (?::.+)? # optional description + \s*\[(\d+|\*)\] # expected count + \s*$ + """, + re.X, + ) + patterns = {} + with open(pat_file) as f: + last_name = None + for line in f: + match = patternRE.match(line) + if not match: + continue + name = match.group(1) + name = last_name if name == "..." else name + last_name = name + pat = match.group(2) + if pat is not None: + pat = "ASSERTION" if pat == "asserts" else pat[1:-1] + count = match.group(3) + count = None if count == "*" else int(count) + if name not in patterns: + patterns[name] = [] + patterns[name].append((pat, count)) + self.patternFiles[pat_file] = patterns + return patterns + + def getFailurePatterns(self, pat_file, test_name): + patterns = self.loadFailurePatternFile(pat_file) + if patterns: + return patterns.get(test_name, None) + + def getActiveTests(self, options, disabled=True): + """ + This method is used to parse the manifest and return active filtered tests. + """ + if self._active_tests: + return self._active_tests + + tests = [] + manifest = self.getTestManifest(options) + if manifest: + if options.extra_mozinfo_json: + mozinfo.update(options.extra_mozinfo_json) + + info = mozinfo.info + + filters = [ + subsuite(options.subsuite), + ] + + # Allow for only running tests/manifests which match this tag + if options.conditionedProfile: + if not options.test_tags: + options.test_tags = [] + options.test_tags.append("condprof") + + if options.test_tags: + filters.append(tags(options.test_tags)) + + if options.test_paths: + options.test_paths = self.normalize_paths(options.test_paths) + filters.append(pathprefix(options.test_paths)) + + # Add chunking filters if specified + if options.totalChunks: + if options.chunkByDir: + filters.append( + chunk_by_dir( + options.thisChunk, options.totalChunks, options.chunkByDir + ) + ) + elif options.chunkByRuntime: + if mozinfo.info["os"] == "android": + platkey = "android" + elif mozinfo.isWin: + platkey = "windows" + else: + platkey = "unix" + + runtime_file = os.path.join( + SCRIPT_DIR, + "runtimes", + "manifest-runtimes-{}.json".format(platkey), + ) + if not os.path.exists(runtime_file): + self.log.error("runtime file %s not found!" % runtime_file) + sys.exit(1) + + # Given the mochitest flavor, load the runtimes information + # for only that flavor due to manifest runtime format change in Bug 1637463. + with open(runtime_file, "r") as f: + if "suite_name" in options: + runtimes = json.load(f).get(options.suite_name, {}) + else: + runtimes = {} + + filters.append( + chunk_by_runtime( + options.thisChunk, options.totalChunks, runtimes + ) + ) + else: + filters.append( + chunk_by_slice(options.thisChunk, options.totalChunks) + ) + + noDefaultFilters = False + if options.runFailures: + filters.append(failures(options.runFailures)) + noDefaultFilters = True + + tests = manifest.active_tests( + exists=False, + disabled=disabled, + filters=filters, + noDefaultFilters=noDefaultFilters, + **info + ) + + if len(tests) == 0: + self.log.error( + NO_TESTS_FOUND.format(options.flavor, manifest.fmt_filters()) + ) + + paths = [] + for test in tests: + if len(tests) == 1 and "disabled" in test: + del test["disabled"] + + pathAbs = os.path.abspath(test["path"]) + assert os.path.normcase(pathAbs).startswith( + os.path.normcase(self.testRootAbs) + ) + tp = pathAbs[len(self.testRootAbs) :].replace("\\", "/").strip("/") + + if not self.isTest(options, tp): + self.log.warning( + "Warning: %s from manifest %s is not a valid test" + % (test["name"], test["manifest"]) + ) + continue + + manifest_key = test["manifest_relpath"] + # Ignore ancestor_manifests that live at the root (e.g, don't have a + # path separator). + if "ancestor_manifest" in test and "/" in normsep( + test["ancestor_manifest"] + ): + manifest_key = "{}:{}".format(test["ancestor_manifest"], manifest_key) + + self.tests_by_manifest[manifest_key.replace("\\", "/")].append(tp) + self.args_by_manifest[manifest_key].add(test.get("args")) + self.prefs_by_manifest[manifest_key].add(test.get("prefs")) + self.env_vars_by_manifest[manifest_key].add(test.get("environment")) + self.tests_dirs_by_manifest[manifest_key].add(test.get("test-directories")) + + for key in ["args", "prefs", "environment", "test-directories"]: + if key in test and not options.runByManifest and "disabled" not in test: + self.log.error( + "parsing {}: runByManifest mode must be enabled to " + "set the `{}` key".format(test["manifest_relpath"], key) + ) + sys.exit(1) + + testob = {"path": tp, "manifest": manifest_key} + if "disabled" in test: + testob["disabled"] = test["disabled"] + if "expected" in test: + testob["expected"] = test["expected"] + if "https_first_disabled" in test: + testob["https_first_disabled"] = test["https_first_disabled"] == "true" + if "allow_xul_xbl" in test: + testob["allow_xul_xbl"] = test["allow_xul_xbl"] == "true" + if "scheme" in test: + testob["scheme"] = test["scheme"] + if "tags" in test: + testob["tags"] = test["tags"] + if options.failure_pattern_file: + pat_file = os.path.join( + os.path.dirname(test["manifest"]), options.failure_pattern_file + ) + patterns = self.getFailurePatterns(pat_file, test["name"]) + if patterns: + testob["expected"] = patterns + paths.append(testob) + + # The 'args' key needs to be set in the DEFAULT section, unfortunately + # we can't tell what comes from DEFAULT or not. So to validate this, we + # stash all args from tests in the same manifest into a set. If the + # length of the set > 1, then we know 'args' didn't come from DEFAULT. + args_not_default = [ + m for m, p in six.iteritems(self.args_by_manifest) if len(p) > 1 + ] + if args_not_default: + self.log.error( + "The 'args' key must be set in the DEFAULT section of a " + "manifest. Fix the following manifests: {}".format( + "\n".join(args_not_default) + ) + ) + sys.exit(1) + + # The 'prefs' key needs to be set in the DEFAULT section too. + pref_not_default = [ + m for m, p in six.iteritems(self.prefs_by_manifest) if len(p) > 1 + ] + if pref_not_default: + self.log.error( + "The 'prefs' key must be set in the DEFAULT section of a " + "manifest. Fix the following manifests: {}".format( + "\n".join(pref_not_default) + ) + ) + sys.exit(1) + # The 'environment' key needs to be set in the DEFAULT section too. + env_not_default = [ + m for m, p in six.iteritems(self.env_vars_by_manifest) if len(p) > 1 + ] + if env_not_default: + self.log.error( + "The 'environment' key must be set in the DEFAULT section of a " + "manifest. Fix the following manifests: {}".format( + "\n".join(env_not_default) + ) + ) + sys.exit(1) + + paths.sort(key=lambda p: p["path"].split("/")) + if options.dump_tests: + options.dump_tests = os.path.expanduser(options.dump_tests) + assert os.path.exists(os.path.dirname(options.dump_tests)) + with open(options.dump_tests, "w") as dumpFile: + dumpFile.write(json.dumps({"active_tests": paths})) + + self.log.info("Dumping active_tests to %s file." % options.dump_tests) + sys.exit() + + # Upload a list of test manifests that were executed in this run. + if "MOZ_UPLOAD_DIR" in os.environ: + artifact = os.path.join(os.environ["MOZ_UPLOAD_DIR"], "manifests.list") + with open(artifact, "a") as fh: + fh.write("\n".join(sorted(self.tests_by_manifest.keys()))) + + self._active_tests = paths + return self._active_tests + + def getTestManifest(self, options): + if isinstance(options.manifestFile, TestManifest): + manifest = options.manifestFile + elif options.manifestFile and os.path.isfile(options.manifestFile): + manifestFileAbs = os.path.abspath(options.manifestFile) + assert manifestFileAbs.startswith(SCRIPT_DIR) + manifest = TestManifest([options.manifestFile], strict=False) + elif options.manifestFile and os.path.isfile( + os.path.join(SCRIPT_DIR, options.manifestFile) + ): + manifestFileAbs = os.path.abspath( + os.path.join(SCRIPT_DIR, options.manifestFile) + ) + assert manifestFileAbs.startswith(SCRIPT_DIR) + manifest = TestManifest([manifestFileAbs], strict=False) + else: + masterName = self.normflavor(options.flavor) + ".ini" + masterPath = os.path.join(SCRIPT_DIR, self.testRoot, masterName) + + if os.path.exists(masterPath): + manifest = TestManifest([masterPath], strict=False) + else: + manifest = None + self.log.warning( + "TestManifest masterPath %s does not exist" % masterPath + ) + + return manifest + + def makeTestConfig(self, options): + "Creates a test configuration file for customizing test execution." + options.logFile = options.logFile.replace("\\", "\\\\") + + if ( + "MOZ_HIDE_RESULTS_TABLE" in os.environ + and os.environ["MOZ_HIDE_RESULTS_TABLE"] == "1" + ): + options.hideResultsTable = True + + # strip certain unnecessary items to avoid serialization errors in json.dumps() + d = dict( + (k, v) + for k, v in options.__dict__.items() + if (v is None) or isinstance(v, (six.string_types, numbers.Number)) + ) + d["testRoot"] = self.testRoot + if options.jscov_dir_prefix: + d["jscovDirPrefix"] = options.jscov_dir_prefix + if not options.keep_open: + d["closeWhenDone"] = "1" + + d["runFailures"] = False + if options.runFailures: + d["runFailures"] = True + content = json.dumps(d) + + with open(os.path.join(options.profilePath, "testConfig.js"), "w") as config: + config.write(content) + + def buildBrowserEnv(self, options, debugger=False, env=None): + """build the environment variables for the specific test and operating system""" + if mozinfo.info["asan"] and mozinfo.isLinux and mozinfo.bits == 64: + useLSan = True + else: + useLSan = False + + browserEnv = self.environment( + xrePath=options.xrePath, env=env, debugger=debugger, useLSan=useLSan + ) + + if hasattr(options, "topsrcdir"): + browserEnv["MOZ_DEVELOPER_REPO_DIR"] = options.topsrcdir + if hasattr(options, "topobjdir"): + browserEnv["MOZ_DEVELOPER_OBJ_DIR"] = options.topobjdir + + if options.headless: + browserEnv["MOZ_HEADLESS"] = "1" + + if options.dmd: + browserEnv["DMD"] = os.environ.get("DMD", "1") + + # bug 1443327: do not set MOZ_CRASHREPORTER_SHUTDOWN during browser-chrome + # tests, since some browser-chrome tests test content process crashes; + # also exclude non-e10s since at least one non-e10s mochitest is problematic + if ( + options.flavor == "browser" or not options.e10s + ) and "MOZ_CRASHREPORTER_SHUTDOWN" in browserEnv: + del browserEnv["MOZ_CRASHREPORTER_SHUTDOWN"] + + try: + browserEnv.update( + dict( + parse_key_value( + self.extraEnv, context="environment variable in manifest" + ) + ) + ) + except KeyValueParseError as e: + self.log.error(str(e)) + return None + + # These variables are necessary for correct application startup; change + # via the commandline at your own risk. + browserEnv["XPCOM_DEBUG_BREAK"] = "stack" + + # interpolate environment passed with options + try: + browserEnv.update( + dict(parse_key_value(options.environment, context="--setenv")) + ) + except KeyValueParseError as e: + self.log.error(str(e)) + return None + + if ( + "MOZ_PROFILER_STARTUP_FEATURES" not in browserEnv + or "nativeallocations" + not in browserEnv["MOZ_PROFILER_STARTUP_FEATURES"].split(",") + ): + # Only turn on the bloat log if the profiler's native allocation feature is + # not enabled. The two are not compatible. + browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file + + # If profiling options are enabled, turn on the gecko profiler by using the + # profiler environmental variables. + if options.profiler: + # The user wants to capture a profile, and automatically view it. The + # profile will be saved to a temporary folder, then deleted after + # opening in profiler.firefox.com. + self.profiler_tempdir = tempfile.mkdtemp() + browserEnv["MOZ_PROFILER_SHUTDOWN"] = os.path.join( + self.profiler_tempdir, "mochitest-profile.json" + ) + browserEnv["MOZ_PROFILER_STARTUP"] = "1" + + if options.profilerSaveOnly: + # The user wants to capture a profile, but only to save it. This defaults + # to the MOZ_UPLOAD_DIR. + browserEnv["MOZ_PROFILER_STARTUP"] = "1" + if "MOZ_UPLOAD_DIR" in browserEnv: + browserEnv["MOZ_PROFILER_SHUTDOWN"] = os.path.join( + browserEnv["MOZ_UPLOAD_DIR"], "mochitest-profile.json" + ) + else: + self.log.error( + "--profiler-save-only was specified, but no MOZ_UPLOAD_DIR " + "environment variable was provided. Please set this " + "environment variable to a directory path in order to save " + "a performance profile." + ) + return None + + try: + gmp_path = self.getGMPPluginPath(options) + if gmp_path is not None: + browserEnv["MOZ_GMP_PATH"] = gmp_path + except EnvironmentError: + self.log.error("Could not find path to gmp-fake plugin!") + return None + + if options.fatalAssertions: + browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort" + + # Produce a mozlog, if setup (see MOZ_LOG global at the top of + # this script). + self.mozLogs = MOZ_LOG and "MOZ_UPLOAD_DIR" in os.environ + if self.mozLogs: + browserEnv["MOZ_LOG"] = MOZ_LOG + + return browserEnv + + def killNamedProc(self, pname, orphans=True): + """Kill processes matching the given command name""" + self.log.info("Checking for %s processes..." % pname) + + if HAVE_PSUTIL: + for proc in psutil.process_iter(): + try: + if proc.name() == pname: + procd = proc.as_dict(attrs=["pid", "ppid", "name", "username"]) + if proc.ppid() == 1 or not orphans: + self.log.info("killing %s" % procd) + killPid(proc.pid, self.log) + else: + self.log.info("NOT killing %s (not an orphan?)" % procd) + except Exception as e: + self.log.info( + "Warning: Unable to kill process %s: %s" % (pname, str(e)) + ) + # may not be able to access process info for all processes + continue + else: + + def _psInfo(line): + if pname in line: + self.log.info(line) + + process = mozprocess.ProcessHandler( + ["ps", "-f"], processOutputLine=_psInfo, universal_newlines=True + ) + process.run() + process.wait() + + def _psKill(line): + parts = line.split() + if len(parts) == 3 and parts[0].isdigit(): + pid = int(parts[0]) + ppid = int(parts[1]) + if parts[2] == pname: + if ppid == 1 or not orphans: + self.log.info("killing %s (pid %d)" % (pname, pid)) + killPid(pid, self.log) + else: + self.log.info( + "NOT killing %s (pid %d) (not an orphan?)" + % (pname, pid) + ) + + process = mozprocess.ProcessHandler( + ["ps", "-o", "pid,ppid,comm"], + processOutputLine=_psKill, + universal_newlines=True, + ) + process.run() + process.wait() + + def execute_start_script(self): + if not self.start_script or not self.marionette: + return + + if os.path.isfile(self.start_script): + with open(self.start_script, "r") as fh: + script = fh.read() + else: + script = self.start_script + + with self.marionette.using_context("chrome"): + return self.marionette.execute_script( + script, script_args=(self.start_script_kwargs,) + ) + + def fillCertificateDB(self, options): + # TODO: move -> mozprofile: + # https://bugzilla.mozilla.org/show_bug.cgi?id=746243#c35 + + pwfilePath = os.path.join(options.profilePath, ".crtdbpw") + with open(pwfilePath, "w") as pwfile: + pwfile.write("\n") + + # Pre-create the certification database for the profile + env = self.environment(xrePath=options.xrePath) + env["LD_LIBRARY_PATH"] = options.xrePath + bin_suffix = mozinfo.info.get("bin_suffix", "") + certutil = os.path.join(options.utilityPath, "certutil" + bin_suffix) + pk12util = os.path.join(options.utilityPath, "pk12util" + bin_suffix) + toolsEnv = env + if mozinfo.info["asan"]: + # Disable leak checking when running these tools + toolsEnv["ASAN_OPTIONS"] = "detect_leaks=0" + if mozinfo.info["tsan"]: + # Disable race checking when running these tools + toolsEnv["TSAN_OPTIONS"] = "report_bugs=0" + + if self.certdbNew: + # android uses the new DB formats exclusively + certdbPath = "sql:" + options.profilePath + else: + # desktop seems to use the old + certdbPath = options.profilePath + + # certutil.exe depends on some DLLs in the app directory + # When running tests against an MSIX-installed Firefox, these DLLs + # cannot be used out of the install directory, they must be copied + # elsewhere first. + if "WindowsApps" in options.app: + install_dir = os.path.dirname(options.app) + for f in os.listdir(install_dir): + if f.endswith(".dll"): + shutil.copy(os.path.join(install_dir, f), options.utilityPath) + + status = call( + [certutil, "-N", "-d", certdbPath, "-f", pwfilePath], env=toolsEnv + ) + if status: + return status + + # Walk the cert directory and add custom CAs and client certs + files = os.listdir(options.certPath) + for item in files: + root, ext = os.path.splitext(item) + if ext == ".ca": + trustBits = "CT,," + if root.endswith("-object"): + trustBits = "CT,,CT" + call( + [ + certutil, + "-A", + "-i", + os.path.join(options.certPath, item), + "-d", + certdbPath, + "-f", + pwfilePath, + "-n", + root, + "-t", + trustBits, + ], + env=toolsEnv, + ) + elif ext == ".client": + call( + [ + pk12util, + "-i", + os.path.join(options.certPath, item), + "-w", + pwfilePath, + "-d", + certdbPath, + ], + env=toolsEnv, + ) + + os.unlink(pwfilePath) + return 0 + + def proxy(self, options): + # proxy + # use SSL port for legacy compatibility; see + # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66 + # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221 + # - https://github.com/mozilla/mozbase/commit/43f9510e3d58bfed32790c82a57edac5f928474d + # 'ws': str(self.webSocketPort) + return { + "remote": options.webServer, + "http": options.httpPort, + "https": options.sslPort, + "ws": options.sslPort, + } + + def merge_base_profiles(self, options, category): + """Merge extra profile data from testing/profiles.""" + + # In test packages used in CI, the profile_data directory is installed + # in the SCRIPT_DIR. + profile_data_dir = os.path.join(SCRIPT_DIR, "profile_data") + # If possible, read profile data from topsrcdir. This prevents us from + # requiring a re-build to pick up newly added extensions in the + # <profile>/extensions directory. + if build_obj: + path = os.path.join(build_obj.topsrcdir, "testing", "profiles") + if os.path.isdir(path): + profile_data_dir = path + # Still not found? Look for testing/profiles relative to testing/mochitest. + if not os.path.isdir(profile_data_dir): + path = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "profiles")) + if os.path.isdir(path): + profile_data_dir = path + + with open(os.path.join(profile_data_dir, "profiles.json"), "r") as fh: + base_profiles = json.load(fh)[category] + + # values to use when interpolating preferences + interpolation = { + "server": "%s:%s" % (options.webServer, options.httpPort), + } + + for profile in base_profiles: + path = os.path.join(profile_data_dir, profile) + self.profile.merge(path, interpolation=interpolation) + + @property + def conditioned_profile_copy(self): + """Returns a copy of the original conditioned profile that was created.""" + + condprof_copy = os.path.join(tempfile.mkdtemp(), "profile") + shutil.copytree( + self.conditioned_profile_dir, + condprof_copy, + ignore=shutil.ignore_patterns("lock"), + ) + self.log.info("Created a conditioned-profile copy: %s" % condprof_copy) + return condprof_copy + + def downloadConditionedProfile(self, profile_scenario, app): + from condprof.client import get_profile + from condprof.util import get_current_platform, get_version + + if self.conditioned_profile_dir: + # We already have a directory, so provide a copy that + # will get deleted after it's done with + return self.conditioned_profile_copy + + temp_download_dir = tempfile.mkdtemp() + + # Call condprof's client API to yield our platform-specific + # conditioned-profile binary + platform = get_current_platform() + + if not profile_scenario: + profile_scenario = "settled" + + version = get_version(app) + try: + cond_prof_target_dir = get_profile( + temp_download_dir, + platform, + profile_scenario, + repo="mozilla-central", + version=version, + retries=2, # quicker failure + ) + except Exception: + if version is None: + # any other error is a showstopper + self.log.critical("Could not get the conditioned profile") + traceback.print_exc() + raise + version = None + try: + self.log.info("retrying a profile with no version specified") + cond_prof_target_dir = get_profile( + temp_download_dir, + platform, + profile_scenario, + repo="mozilla-central", + version=version, + ) + except Exception: + self.log.critical("Could not get the conditioned profile") + traceback.print_exc() + raise + + # Now get the full directory path to our fetched conditioned profile + self.conditioned_profile_dir = os.path.join( + temp_download_dir, cond_prof_target_dir + ) + if not os.path.exists(cond_prof_target_dir): + self.log.critical( + "Can't find target_dir {}, from get_profile()" + "temp_download_dir {}, platform {}, scenario {}".format( + cond_prof_target_dir, temp_download_dir, platform, profile_scenario + ) + ) + raise OSError + + self.log.info( + "Original self.conditioned_profile_dir is now set: {}".format( + self.conditioned_profile_dir + ) + ) + return self.conditioned_profile_copy + + def buildProfile(self, options): + """create the profile and add optional chrome bits and files if requested""" + # get extensions to install + extensions = self.getExtensionsToInstall(options) + + # Whitelist the _tests directory (../..) so that TESTING_JS_MODULES work + tests_dir = os.path.dirname(os.path.dirname(SCRIPT_DIR)) + sandbox_whitelist_paths = [tests_dir] + options.sandboxReadWhitelist + if platform.system() == "Linux" or platform.system() in ( + "Windows", + "Microsoft", + ): + # Trailing slashes are needed to indicate directories on Linux and Windows + sandbox_whitelist_paths = [ + os.path.join(p, "") for p in sandbox_whitelist_paths + ] + + if options.conditionedProfile: + if options.profilePath and os.path.exists(options.profilePath): + shutil.rmtree(options.profilePath, ignore_errors=True) + options.profilePath = self.downloadConditionedProfile("full", options.app) + + # This is causing `certutil -N -d -f`` to not use -f (pwd file) + try: + os.remove(os.path.join(options.profilePath, "key4.db")) + except Exception as e: + self.log.info( + "Caught exception while removing key4.db" + "during setup of conditioned profile: %s" % e + ) + + # Create the profile + self.profile = Profile( + profile=options.profilePath, + addons=extensions, + locations=self.locations, + proxy=self.proxy(options), + whitelistpaths=sandbox_whitelist_paths, + ) + + # Fix options.profilePath for legacy consumers. + options.profilePath = self.profile.profile + + manifest = self.addChromeToProfile(options) + self.copyExtraFilesToProfile(options) + + # create certificate database for the profile + # TODO: this should really be upstreamed somewhere, maybe mozprofile + certificateStatus = self.fillCertificateDB(options) + if certificateStatus: + self.log.error( + "TEST-UNEXPECTED-FAIL | runtests.py | Certificate integration failed" + ) + return None + + # Set preferences in the following order (latter overrides former): + # 1) Preferences from base profile (e.g from testing/profiles) + # 2) Prefs hardcoded in this function + # 3) Prefs from --setpref + + # Prefs from base profiles + self.merge_base_profiles(options, "mochitest") + + # Hardcoded prefs (TODO move these into a base profile) + prefs = { + "browser.tabs.remote.autostart": options.e10s, + # Enable tracing output for detailed failures in case of + # failing connection attempts, and hangs (bug 1397201) + "marionette.log.level": "Trace", + # Disable async font fallback, because the unpredictable + # extra reflow it can trigger (potentially affecting a later + # test) results in spurious intermittent failures. + "gfx.font_rendering.fallback.async": False, + } + + if options.flavor == "browser" and options.timeout: + prefs["testing.browserTestHarness.timeout"] = options.timeout + + # browser-chrome tests use a fairly short default timeout of 45 seconds; + # this is sometimes too short on asan and debug, where we expect reduced + # performance. + if ( + (mozinfo.info["asan"] or mozinfo.info["debug"]) + and options.flavor == "browser" + and options.timeout is None + ): + self.log.info("Increasing default timeout to 90 seconds") + prefs["testing.browserTestHarness.timeout"] = 90 + + # tsan builds need even more time + if ( + mozinfo.info["tsan"] + and options.flavor == "browser" + and options.timeout is None + ): + self.log.info("Increasing default timeout to 120 seconds") + prefs["testing.browserTestHarness.timeout"] = 120 + + if mozinfo.info["os"] == "win" and mozinfo.info["processor"] == "aarch64": + extended_timeout = self.DEFAULT_TIMEOUT * 4 + self.log.info( + "Increasing default timeout to {} seconds".format(extended_timeout) + ) + prefs["testing.browserTestHarness.timeout"] = extended_timeout + + if getattr(self, "testRootAbs", None): + prefs["mochitest.testRoot"] = self.testRootAbs + + # See if we should use fake media devices. + if options.useTestMediaDevices: + prefs["media.audio_loopback_dev"] = self.mediaDevices["audio"]["name"] + prefs["media.video_loopback_dev"] = self.mediaDevices["video"]["name"] + prefs["media.cubeb.output_device"] = "Null Output" + prefs["media.volume_scale"] = "1.0" + self.gstForV4l2loopbackProcess = self.mediaDevices["video"]["process"] + + self.profile.set_preferences(prefs) + + # Extra prefs from --setpref + self.profile.set_preferences(self.extraPrefs) + return manifest + + def getGMPPluginPath(self, options): + if options.gmp_path: + return options.gmp_path + + gmp_parentdirs = [ + # For local builds, GMP plugins will be under dist/bin. + options.xrePath, + # For packaged builds, GMP plugins will get copied under + # $profile/plugins. + os.path.join(self.profile.profile, "plugins"), + ] + + gmp_subdirs = [ + os.path.join("gmp-fake", "1.0"), + os.path.join("gmp-fakeopenh264", "1.0"), + os.path.join("gmp-clearkey", "0.1"), + ] + + gmp_paths = [ + os.path.join(parent, sub) + for parent in gmp_parentdirs + for sub in gmp_subdirs + if os.path.isdir(os.path.join(parent, sub)) + ] + + if not gmp_paths: + # This is fatal for desktop environments. + raise EnvironmentError("Could not find test gmp plugins") + + return os.pathsep.join(gmp_paths) + + def cleanup(self, options, final=False): + """remove temporary files, profile and virtual audio input device""" + if hasattr(self, "manifest") and self.manifest is not None: + if os.path.exists(self.manifest): + os.remove(self.manifest) + if hasattr(self, "profile"): + del self.profile + if hasattr(self, "extraTestsDirs"): + for d in self.extraTestsDirs: + if os.path.exists(d): + shutil.rmtree(d) + if options.pidFile != "" and os.path.exists(options.pidFile): + try: + os.remove(options.pidFile) + if os.path.exists(options.pidFile + ".xpcshell.pid"): + os.remove(options.pidFile + ".xpcshell.pid") + except Exception: + self.log.warning( + "cleaning up pidfile '%s' was unsuccessful from the test harness" + % options.pidFile + ) + options.manifestFile = None + + if hasattr(self, "virtualInputDeviceIdList"): + pactl = spawn.find_executable("pactl") + + if not pactl: + self.log.error("Could not find pactl on system") + return None + + for id in self.virtualInputDeviceIdList: + try: + subprocess.check_call([pactl, "unload-module", str(id)]) + except subprocess.CalledProcessError: + self.log.error( + "Could not remove pulse module with id {}".format(id) + ) + return None + + self.virtualInputDeviceIdList = [] + + def dumpScreen(self, utilityPath): + if self.haveDumpedScreen: + self.log.info( + "Not taking screenshot here: see the one that was previously logged" + ) + return + self.haveDumpedScreen = True + dump_screen(utilityPath, self.log) + + def killAndGetStack(self, processPID, utilityPath, debuggerInfo, dump_screen=False): + """ + Kill the process, preferrably in a way that gets us a stack trace. + Also attempts to obtain a screenshot before killing the process + if specified. + """ + self.log.info("Killing process: %s" % processPID) + if dump_screen: + self.dumpScreen(utilityPath) + + if mozinfo.info.get("crashreporter", True) and not debuggerInfo: + try: + minidump_path = os.path.join(self.profile.profile, "minidumps") + mozcrash.kill_and_get_minidump(processPID, minidump_path, utilityPath) + except OSError: + # https://bugzilla.mozilla.org/show_bug.cgi?id=921509 + self.log.info("Can't trigger Breakpad, process no longer exists") + return + self.log.info("Can't trigger Breakpad, just killing process") + killPid(processPID, self.log) + + def extract_child_pids(self, process_log, parent_pid=None): + """Parses the given log file for the pids of any processes launched by + the main process and returns them as a list. + If parent_pid is provided, and psutil is available, returns children of + parent_pid according to psutil. + """ + rv = [] + if parent_pid and HAVE_PSUTIL: + self.log.info("Determining child pids from psutil...") + try: + rv = [p.pid for p in psutil.Process(parent_pid).children()] + self.log.info(str(rv)) + except psutil.NoSuchProcess: + self.log.warning("Failed to lookup children of pid %d" % parent_pid) + + rv = set(rv) + pid_re = re.compile(r"==> process \d+ launched child process (\d+)") + with open(process_log) as fd: + for line in fd: + self.log.info(line.rstrip()) + m = pid_re.search(line) + if m: + rv.add(int(m.group(1))) + return rv + + def checkForZombies(self, processLog, utilityPath, debuggerInfo): + """Look for hung processes""" + + if not os.path.exists(processLog): + self.log.info("Automation Error: PID log not found: %s" % processLog) + # Whilst no hung process was found, the run should still display as + # a failure + return True + + # scan processLog for zombies + self.log.info("zombiecheck | Reading PID log: %s" % processLog) + processList = self.extract_child_pids(processLog) + # kill zombies + foundZombie = False + for processPID in processList: + self.log.info( + "zombiecheck | Checking for orphan process with PID: %d" % processPID + ) + if isPidAlive(processPID): + foundZombie = True + self.log.error( + "TEST-UNEXPECTED-FAIL | zombiecheck | child process " + "%d still alive after shutdown" % processPID + ) + self.killAndGetStack( + processPID, utilityPath, debuggerInfo, dump_screen=not debuggerInfo + ) + + return foundZombie + + def checkForRunningBrowsers(self): + firefoxes = "" + if HAVE_PSUTIL: + attrs = ["pid", "ppid", "name", "cmdline", "username"] + for proc in psutil.process_iter(): + try: + if "firefox" in proc.name(): + firefoxes = "%s%s\n" % (firefoxes, proc.as_dict(attrs=attrs)) + except Exception: + # may not be able to access process info for all processes + continue + if len(firefoxes) > 0: + # In automation, this warning is unexpected and should be investigated. + # In local testing, this is probably okay, as long as the browser is not + # running a marionette server. + self.log.warning("Found 'firefox' running before starting test browser!") + self.log.warning(firefoxes) + + def runApp( + self, + testUrl, + env, + app, + profile, + extraArgs, + utilityPath, + debuggerInfo=None, + valgrindPath=None, + valgrindArgs=None, + valgrindSuppFiles=None, + symbolsPath=None, + timeout=-1, + detectShutdownLeaks=False, + screenshotOnFail=False, + bisectChunk=None, + marionette_args=None, + e10s=True, + runFailures=False, + crashAsPass=False, + ): + """ + Run the app, log the duration it took to execute, return the status code. + Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing + for |timeout| seconds. + """ + # It can't be the case that both a with-debugger and an + # on-Valgrind run have been requested. doTests() should have + # already excluded this possibility. + assert not (valgrindPath and debuggerInfo) + + # debugger information + interactive = False + debug_args = None + if debuggerInfo: + interactive = debuggerInfo.interactive + debug_args = [debuggerInfo.path] + debuggerInfo.args + + # Set up Valgrind arguments. + if valgrindPath: + interactive = False + valgrindArgs_split = ( + [] if valgrindArgs is None else shlex.split(valgrindArgs) + ) + + valgrindSuppFiles_final = [] + if valgrindSuppFiles is not None: + valgrindSuppFiles_final = [ + "--suppressions=" + path for path in valgrindSuppFiles.split(",") + ] + + debug_args = ( + [valgrindPath] + + mozdebug.get_default_valgrind_args() + + valgrindArgs_split + + valgrindSuppFiles_final + ) + + # fix default timeout + if timeout == -1: + timeout = self.DEFAULT_TIMEOUT + + # Note in the log if running on Valgrind + if valgrindPath: + self.log.info( + "runtests.py | Running on Valgrind. " + + "Using timeout of %d seconds." % timeout + ) + + # copy env so we don't munge the caller's environment + env = env.copy() + + # Used to defer a possible IOError exception from Marionette + marionette_exception = None + + temp_file_paths = [] + + # make sure we clean up after ourselves. + try: + # set process log environment variable + tmpfd, processLog = tempfile.mkstemp(suffix="pidlog") + os.close(tmpfd) + env["MOZ_PROCESS_LOG"] = processLog + + if debuggerInfo: + # If a debugger is attached, don't use timeouts, and don't + # capture ctrl-c. + timeout = None + signal.signal(signal.SIGINT, lambda sigid, frame: None) + + # build command line + cmd = os.path.abspath(app) + args = list(extraArgs) + args.append("-marionette") + # TODO: mozrunner should use -foreground at least for mac + # https://bugzilla.mozilla.org/show_bug.cgi?id=916512 + args.append("-foreground") + self.start_script_kwargs["testUrl"] = testUrl or "about:blank" + + if detectShutdownLeaks: + env["MOZ_LOG"] = ( + env["MOZ_LOG"] + "," if env["MOZ_LOG"] else "" + ) + "DocShellAndDOMWindowLeak:3" + shutdownLeaks = ShutdownLeaks(self.log) + else: + shutdownLeaks = None + + if mozinfo.info["asan"] and mozinfo.isLinux and mozinfo.bits == 64: + lsanLeaks = LSANLeaks(self.log) + else: + lsanLeaks = None + + # create an instance to process the output + outputHandler = self.OutputHandler( + harness=self, + utilityPath=utilityPath, + symbolsPath=symbolsPath, + dump_screen_on_timeout=not debuggerInfo, + dump_screen_on_fail=screenshotOnFail, + shutdownLeaks=shutdownLeaks, + lsanLeaks=lsanLeaks, + bisectChunk=bisectChunk, + ) + + def timeoutHandler(): + browserProcessId = outputHandler.browserProcessId + self.handleTimeout( + timeout, + proc, + utilityPath, + debuggerInfo, + browserProcessId, + processLog, + ) + + kp_kwargs = { + "kill_on_timeout": False, + "cwd": SCRIPT_DIR, + "onTimeout": [timeoutHandler], + } + kp_kwargs["processOutputLine"] = [outputHandler] + + self.checkForRunningBrowsers() + + # create mozrunner instance and start the system under test process + self.lastTestSeen = self.test_name + startTime = datetime.now() + + runner_cls = mozrunner.runners.get( + mozinfo.info.get("appname", "firefox"), mozrunner.Runner + ) + runner = runner_cls( + profile=self.profile, + binary=cmd, + cmdargs=args, + env=env, + process_class=mozprocess.ProcessHandlerMixin, + process_args=kp_kwargs, + ) + + # start the runner + runner.start( + debug_args=debug_args, interactive=interactive, outputTimeout=timeout + ) + proc = runner.process_handler + self.log.info("runtests.py | Application pid: %d" % proc.pid) + + gecko_id = "GECKO(%d)" % proc.pid + self.log.process_start(gecko_id) + self.message_logger.gecko_id = gecko_id + + try: + # start marionette and kick off the tests + marionette_args = marionette_args or {} + self.marionette = Marionette(**marionette_args) + self.marionette.start_session() + + # install specialpowers and mochikit addons + addons = Addons(self.marionette) + + if self.staged_addons: + for addon_path in self.staged_addons: + if not os.path.isdir(addon_path): + self.log.error( + "TEST-UNEXPECTED-FAIL | invalid setup: missing extension at %s" + % addon_path + ) + return 1, self.lastTestSeen + temp_addon_path = create_zip(addon_path) + temp_file_paths.append(temp_addon_path) + addons.install(temp_addon_path) + + self.execute_start_script() + + # an open marionette session interacts badly with mochitest, + # delete it until we figure out why. + self.marionette.delete_session() + del self.marionette + + except IOError: + # Any IOError as thrown by Marionette means that something is + # wrong with the process, like a crash or the socket is no + # longer open. We defer raising this specific error so that + # post-test checks for leaks and crashes are performed and + # reported first. + marionette_exception = sys.exc_info() + + # wait until app is finished + # XXX copy functionality from + # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/runner.py#L61 + # until bug 913970 is fixed regarding mozrunner `wait` not returning status + # see https://bugzilla.mozilla.org/show_bug.cgi?id=913970 + self.log.info("runtests.py | Waiting for browser...") + status = proc.wait() + if status is None: + self.log.warning( + "runtests.py | Failed to get app exit code - running/crashed?" + ) + # must report an integer to process_exit() + status = 0 + self.log.process_exit("Main app process", status) + runner.process_handler = None + + # finalize output handler + outputHandler.finish() + + # record post-test information + if status: + self.message_logger.dump_buffered() + msg = "application terminated with exit code %s" % status + # self.message_logger.is_test_running indicates we need to send a test_end + if crashAsPass and self.message_logger.is_test_running: + # this works for browser-chrome, mochitest-plain has status=0 + message = { + "action": "test_end", + "status": "CRASH", + "expected": "CRASH", + "thread": None, + "pid": None, + "source": "mochitest", + "time": int(time.time()) * 1000, + "test": self.lastTestSeen, + "message": msg, + } + # need to send a test_end in order to have mozharness process messages properly + # this requires a custom message vs log.error/log.warning/etc. + self.message_logger.process_message(message) + else: + self.log.error( + "TEST-UNEXPECTED-FAIL | %s | %s" % (self.lastTestSeen, msg) + ) + else: + self.lastTestSeen = "Main app process exited normally" + + self.log.info( + "runtests.py | Application ran for: %s" + % str(datetime.now() - startTime) + ) + + # Do a final check for zombie child processes. + zombieProcesses = self.checkForZombies( + processLog, utilityPath, debuggerInfo + ) + + # check for crashes + quiet = False + if crashAsPass: + quiet = True + + minidump_path = os.path.join(self.profile.profile, "minidumps") + crash_count = mozcrash.log_crashes( + self.log, + minidump_path, + symbolsPath, + test=self.lastTestSeen, + quiet=quiet, + ) + + expected = None + if crashAsPass: + # self.message_logger.is_test_running indicates we need a test_end message + if crash_count > 0 and self.message_logger.is_test_running: + # this works for browser-chrome, mochitest-plain has status=0 + expected = "CRASH" + status = 0 + elif crash_count or zombieProcesses: + if self.message_logger.is_test_running: + expected = "PASS" + status = 1 + + if expected: + # send this out so we always wrap up the test-end message + message = { + "action": "test_end", + "status": "CRASH", + "expected": expected, + "thread": None, + "pid": None, + "source": "mochitest", + "time": int(time.time()) * 1000, + "test": self.lastTestSeen, + "message": "application terminated with exit code 0", + } + # need to send a test_end in order to have mozharness process messages properly + # this requires a custom message vs log.error/log.warning/etc. + self.message_logger.process_message(message) + finally: + # cleanup + if os.path.exists(processLog): + os.remove(processLog) + for p in temp_file_paths: + os.remove(p) + + if marionette_exception is not None: + exc, value, tb = marionette_exception + six.reraise(exc, value, tb) + + return status, self.lastTestSeen + + def initializeLooping(self, options): + """ + This method is used to clear the contents before each run of for loop. + This method is used for --run-by-dir and --bisect-chunk. + """ + if options.conditionedProfile: + if options.profilePath and os.path.exists(options.profilePath): + shutil.rmtree(options.profilePath, ignore_errors=True) + if options.manifestFile and os.path.exists(options.manifestFile): + os.remove(options.manifestFile) + + self.expectedError.clear() + self.result.clear() + options.manifestFile = None + options.profilePath = None + + def initializeVirtualInputDevices(self): + """ + Configure the system to have a number of virtual audio input devices, that + each produce a tone at a particular frequency. + + This method is only currently implemented for Linux. + """ + if not mozinfo.isLinux: + return + + pactl = spawn.find_executable("pactl") + + if not pactl: + self.log.error("Could not find pactl on system") + return + + DEVICES_COUNT = 4 + DEVICES_BASE_FREQUENCY = 110 # Hz + self.virtualInputDeviceIdList = [] + # If the device are already present, find their id and return early + o = subprocess.check_output([pactl, "list", "modules", "short"]) + found_devices = 0 + for input in o.splitlines(): + device = input.decode().split("\t") + if device[1] == "module-sine-source": + self.virtualInputDeviceIdList.append(int(device[0])) + found_devices += 1 + + if found_devices == DEVICES_COUNT: + return + elif found_devices != 0: + # Remove all devices and reinitialize them properly + for id in self.virtualInputDeviceIdList: + try: + subprocess.check_call([pactl, "unload-module", str(id)]) + except subprocess.CalledProcessError: + log.error("Could not remove pulse module with id {}".format(id)) + return None + + # We want quite a number of input devices, each with a different tone + # frequency and device name so that we can recognize them easily during + # testing. + command = [pactl, "load-module", "module-sine-source", "rate=44100"] + for i in range(1, DEVICES_COUNT + 1): + freq = i * DEVICES_BASE_FREQUENCY + complete_command = command + [ + "source_name=sine-{}".format(freq), + "frequency={}".format(freq), + ] + try: + o = subprocess.check_output(complete_command) + self.virtualInputDeviceIdList.append(o) + + except subprocess.CalledProcessError: + self.log.error( + "Could not create device with module-sine-source" + " (freq={})".format(freq) + ) + + def normalize_paths(self, paths): + # Normalize test paths so they are relative to test root + norm_paths = [] + for p in paths: + abspath = os.path.abspath(os.path.join(self.oldcwd, p)) + if abspath.startswith(self.testRootAbs): + norm_paths.append(os.path.relpath(abspath, self.testRootAbs)) + else: + norm_paths.append(p) + return norm_paths + + def runMochitests(self, options, testsToRun, manifestToFilter=None): + "This is a base method for calling other methods in this class for --bisect-chunk." + # Making an instance of bisect class for --bisect-chunk option. + bisect = bisection.Bisect(self) + finished = False + status = 0 + bisection_log = 0 + while not finished: + if options.bisectChunk: + testsToRun = bisect.pre_test(options, testsToRun, status) + # To inform that we are in the process of bisection, and to + # look for bleedthrough + if options.bisectChunk != "default" and not bisection_log: + self.log.error( + "TEST-UNEXPECTED-FAIL | Bisection | Please ignore repeats " + "and look for 'Bleedthrough' (if any) at the end of " + "the failure list" + ) + bisection_log = 1 + + result = self.doTests(options, testsToRun, manifestToFilter) + if options.bisectChunk: + status = bisect.post_test(options, self.expectedError, self.result) + else: + status = -1 + + if status == -1: + finished = True + + # We need to print the summary only if options.bisectChunk has a value. + # Also we need to make sure that we do not print the summary in between + # running tests via --run-by-dir. + if options.bisectChunk and options.bisectChunk in self.result: + bisect.print_summary() + + return result + + def groupTestsByScheme(self, tests): + """ + split tests into groups by schemes. test is classified as http if + no scheme specified + """ + httpTests = [] + httpsTests = [] + for test in tests: + if not test.get("scheme") or test.get("scheme") == "http": + httpTests.append(test) + elif test.get("scheme") == "https": + httpsTests.append(test) + return {"http": httpTests, "https": httpsTests} + + def verifyTests(self, options): + """ + Support --verify mode: Run test(s) many times in a variety of + configurations/environments in an effort to find intermittent + failures. + """ + + # Number of times to repeat test(s) when running with --repeat + VERIFY_REPEAT = 10 + # Number of times to repeat test(s) when running test in + VERIFY_REPEAT_SINGLE_BROWSER = 5 + + def step1(): + options.repeat = VERIFY_REPEAT + options.keep_open = False + options.runUntilFailure = True + options.profilePath = None + result = self.runTests(options) + result = result or (-2 if self.countfail > 0 else 0) + self.message_logger.finish() + return result + + def step2(): + options.repeat = 0 + options.keep_open = False + options.runUntilFailure = False + for i in range(VERIFY_REPEAT_SINGLE_BROWSER): + options.profilePath = None + result = self.runTests(options) + result = result or (-2 if self.countfail > 0 else 0) + self.message_logger.finish() + if result != 0: + break + return result + + def step3(): + options.repeat = VERIFY_REPEAT + options.keep_open = False + options.runUntilFailure = True + options.environment.append("MOZ_CHAOSMODE=0xfb") + options.profilePath = None + result = self.runTests(options) + options.environment.remove("MOZ_CHAOSMODE=0xfb") + result = result or (-2 if self.countfail > 0 else 0) + self.message_logger.finish() + return result + + def step4(): + options.repeat = 0 + options.keep_open = False + options.runUntilFailure = False + options.environment.append("MOZ_CHAOSMODE=0xfb") + for i in range(VERIFY_REPEAT_SINGLE_BROWSER): + options.profilePath = None + result = self.runTests(options) + result = result or (-2 if self.countfail > 0 else 0) + self.message_logger.finish() + if result != 0: + break + options.environment.remove("MOZ_CHAOSMODE=0xfb") + return result + + def fission_step(fission_pref): + if fission_pref not in options.extraPrefs: + options.extraPrefs.append(fission_pref) + options.keep_open = False + options.runUntilFailure = True + options.profilePath = None + result = self.runTests(options) + result = result or (-2 if self.countfail > 0 else 0) + self.message_logger.finish() + return result + + def fission_step1(): + return fission_step("fission.autostart=false") + + def fission_step2(): + return fission_step("fission.autostart=true") + + if options.verify_fission: + steps = [ + ("1. Run each test without fission.", fission_step1), + ("2. Run each test with fission.", fission_step2), + ] + else: + steps = [ + ("1. Run each test %d times in one browser." % VERIFY_REPEAT, step1), + ( + "2. Run each test %d times in a new browser each time." + % VERIFY_REPEAT_SINGLE_BROWSER, + step2, + ), + ( + "3. Run each test %d times in one browser, in chaos mode." + % VERIFY_REPEAT, + step3, + ), + ( + "4. Run each test %d times in a new browser each time, " + "in chaos mode." % VERIFY_REPEAT_SINGLE_BROWSER, + step4, + ), + ] + + stepResults = {} + for (descr, step) in steps: + stepResults[descr] = "not run / incomplete" + + startTime = datetime.now() + maxTime = timedelta(seconds=options.verify_max_time) + finalResult = "PASSED" + for (descr, step) in steps: + if (datetime.now() - startTime) > maxTime: + self.log.info("::: Test verification is taking too long: Giving up!") + self.log.info( + "::: So far, all checks passed, but not all checks were run." + ) + break + self.log.info(":::") + self.log.info('::: Running test verification step "%s"...' % descr) + self.log.info(":::") + result = step() + if result != 0: + stepResults[descr] = "FAIL" + finalResult = "FAILED!" + break + stepResults[descr] = "Pass" + + self.logPreamble([]) + + self.log.info(":::") + self.log.info("::: Test verification summary for:") + self.log.info(":::") + tests = self.getActiveTests(options) + for test in tests: + self.log.info("::: " + test["path"]) + self.log.info(":::") + for descr in sorted(stepResults.keys()): + self.log.info("::: %s : %s" % (descr, stepResults[descr])) + self.log.info(":::") + self.log.info("::: Test verification %s" % finalResult) + self.log.info(":::") + + return 0 + + def runTests(self, options): + """Prepare, configure, run tests and cleanup""" + self.extraPrefs = parse_preferences(options.extraPrefs) + self.extraPrefs["fission.autostart"] = not options.disable_fission + + # for test manifest parsing. + mozinfo.update( + { + "a11y_checks": options.a11y_checks, + "e10s": options.e10s, + "fission": not options.disable_fission, + "headless": options.headless, + # Until the test harness can understand default pref values, + # (https://bugzilla.mozilla.org/show_bug.cgi?id=1577912) this value + # should by synchronized with the default pref value indicated in + # StaticPrefList.yaml. + # + # Currently for automation, the pref defaults to true (but can be + # overridden with --setpref). + "serviceworker_e10s": True, + "sessionHistoryInParent": self.extraPrefs.get( + "fission.sessionHistoryInParent", False + ) + or self.extraPrefs.get("fission.autostart", True), + "socketprocess_e10s": self.extraPrefs.get( + "network.process.enabled", False + ), + "socketprocess_networking": self.extraPrefs.get( + "network.http.network_access_on_socket_process.enabled", False + ), + "swgl": self.extraPrefs.get("gfx.webrender.software", False), + "verify": options.verify, + "verify_fission": options.verify_fission, + "webgl_ipc": self.extraPrefs.get("webgl.out-of-process", False), + "wmfme": ( + self.extraPrefs.get("media.wmf.media-engine.enabled", False) + and self.extraPrefs.get( + "media.wmf.media-engine.channel-decoder.enabled", False + ) + ), + "xorigin": options.xOriginTests, + "condprof": options.conditionedProfile, + "msix": "WindowsApps" in options.app, + } + ) + + if not self.mozinfo_variables_shown: + self.mozinfo_variables_shown = True + self.log.info( + "These variables are available in the mozinfo environment and " + "can be used to skip tests conditionally:" + ) + for info in sorted(mozinfo.info.items(), key=lambda item: item[0]): + self.log.info(" {key}: {value}".format(key=info[0], value=info[1])) + self.setTestRoot(options) + + # Despite our efforts to clean up servers started by this script, in practice + # we still see infrequent cases where a process is orphaned and interferes + # with future tests, typically because the old server is keeping the port in use. + # Try to avoid those failures by checking for and killing servers before + # trying to start new ones. + self.killNamedProc("ssltunnel") + self.killNamedProc("xpcshell") + + if options.cleanupCrashes: + mozcrash.cleanup_pending_crash_reports() + + tests = self.getActiveTests(options) + self.logPreamble(tests) + + if mozinfo.info["fission"] and not mozinfo.info["e10s"]: + # Make sure this is logged *after* suite_start so it gets associated with the + # current suite in the summary formatters. + self.log.error("Fission is not supported without e10s.") + return 1 + + tests = [t for t in tests if "disabled" not in t] + + # Until we have all green, this does not run on a11y (for perf reasons) + if not options.runByManifest: + result = self.runMochitests(options, [t["path"] for t in tests]) + self.handleShutdownProfile(options) + return result + + # code for --run-by-manifest + manifests = set(t["manifest"] for t in tests) + result = 0 + + origPrefs = self.extraPrefs.copy() + for m in sorted(manifests): + self.log.info("Running manifest: {}".format(m)) + + args = list(self.args_by_manifest[m])[0] + self.extraArgs = [] + if args: + for arg in args.strip().split(): + # Split off the argument value if available so that both + # name and value will be set individually + self.extraArgs.extend(arg.split("=")) + + self.log.info( + "The following arguments will be set:\n {}".format( + "\n ".join(self.extraArgs) + ) + ) + + prefs = list(self.prefs_by_manifest[m])[0] + self.extraPrefs = origPrefs.copy() + if prefs: + prefs = prefs.strip().split() + self.log.info( + "The following extra prefs will be set:\n {}".format( + "\n ".join(prefs) + ) + ) + self.extraPrefs.update(parse_preferences(prefs)) + + envVars = list(self.env_vars_by_manifest[m])[0] + self.extraEnv = {} + if envVars: + self.extraEnv = envVars.strip().split() + self.log.info( + "The following extra environment variables will be set:\n {}".format( + "\n ".join(self.extraEnv) + ) + ) + + self.parseAndCreateTestsDirs(m) + + # If we are using --run-by-manifest, we should not use the profile path (if) provided + # by the user, since we need to create a new directory for each run. We would face + # problems if we use the directory provided by the user. + tests_in_manifest = [t["path"] for t in tests if t["manifest"] == m] + res = self.runMochitests(options, tests_in_manifest, manifestToFilter=m) + result = result or res + + # Dump the logging buffer + self.message_logger.dump_buffered() + + if res == -1: + break + + if self.manifest is not None: + self.cleanup(options, True) + + e10s_mode = "e10s" if options.e10s else "non-e10s" + + # for failure mode: where browser window has crashed and we have no reported results + if ( + self.countpass == self.countfail == self.counttodo == 0 + and options.crashAsPass + ): + self.countpass = 1 + self.result = 0 + + # printing total number of tests + if options.flavor == "browser": + print("TEST-INFO | checking window state") + print("Browser Chrome Test Summary") + print("\tPassed: %s" % self.countpass) + print("\tFailed: %s" % self.countfail) + print("\tTodo: %s" % self.counttodo) + print("\tMode: %s" % e10s_mode) + print("*** End BrowserChrome Test Results ***") + else: + print("0 INFO TEST-START | Shutdown") + print("1 INFO Passed: %s" % self.countpass) + print("2 INFO Failed: %s" % self.countfail) + print("3 INFO Todo: %s" % self.counttodo) + print("4 INFO Mode: %s" % e10s_mode) + print("5 INFO SimpleTest FINISHED") + + self.handleShutdownProfile(options) + + if not result: + if self.countfail or not (self.countpass or self.counttodo): + # at least one test failed, or + # no tests passed, and no tests failed (possibly a crash) + result = 1 + + return result + + def handleShutdownProfile(self, options): + # If shutdown profiling was enabled, then the user will want to access the + # performance profile. The following code will display helpful log messages + # and automatically open the profile if it is requested. + if self.browserEnv and "MOZ_PROFILER_SHUTDOWN" in self.browserEnv: + profile_path = self.browserEnv["MOZ_PROFILER_SHUTDOWN"] + + profiler_logger = get_proxy_logger("profiler") + profiler_logger.info("Shutdown performance profiling was enabled") + profiler_logger.info("Profile saved locally to: %s" % profile_path) + + if options.profilerSaveOnly or options.profiler: + # Only do the extra work of symbolicating and viewing the profile if + # officially requested through a command line flag. The MOZ_PROFILER_* + # flags can be set by a user. + symbolicate_profile_json(profile_path, options.topobjdir) + view_gecko_profile_from_mochitest( + profile_path, options, profiler_logger + ) + else: + profiler_logger.info( + "The profiler was enabled outside of the mochitests. " + "Use --profiler instead of MOZ_PROFILER_SHUTDOWN to " + "symbolicate and open the profile automatically." + ) + + # Clean up the temporary file if it exists. + if self.profiler_tempdir: + shutil.rmtree(self.profiler_tempdir) + + def doTests(self, options, testsToFilter=None, manifestToFilter=None): + # A call to initializeLooping method is required in case of --run-by-dir or --bisect-chunk + # since we need to initialize variables for each loop. + if options.bisectChunk or options.runByManifest: + self.initializeLooping(options) + + # get debugger info, a dict of: + # {'path': path to the debugger (string), + # 'interactive': whether the debugger is interactive or not (bool) + # 'args': arguments to the debugger (list) + # TODO: use mozrunner.local.debugger_arguments: + # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/local.py#L42 + + debuggerInfo = None + if options.debugger: + debuggerInfo = mozdebug.get_debugger_info( + options.debugger, options.debuggerArgs, options.debuggerInteractive + ) + + if options.useTestMediaDevices: + devices = findTestMediaDevices(self.log) + if not devices: + self.log.error("Could not find test media devices to use") + return 1 + self.mediaDevices = devices + self.initializeVirtualInputDevices() + + # See if we were asked to run on Valgrind + valgrindPath = None + valgrindArgs = None + valgrindSuppFiles = None + if options.valgrind: + valgrindPath = options.valgrind + if options.valgrindArgs: + valgrindArgs = options.valgrindArgs + if options.valgrindSuppFiles: + valgrindSuppFiles = options.valgrindSuppFiles + + if (valgrindArgs or valgrindSuppFiles) and not valgrindPath: + self.log.error( + "Specified --valgrind-args or --valgrind-supp-files," + " but not --valgrind" + ) + return 1 + + if valgrindPath and debuggerInfo: + self.log.error("Can't use both --debugger and --valgrind together") + return 1 + + if valgrindPath and not valgrindSuppFiles: + valgrindSuppFiles = ",".join(get_default_valgrind_suppression_files()) + + # buildProfile sets self.profile . + # This relies on sideeffects and isn't very stateful: + # https://bugzilla.mozilla.org/show_bug.cgi?id=919300 + self.manifest = self.buildProfile(options) + if self.manifest is None: + return 1 + + self.leak_report_file = os.path.join(options.profilePath, "runtests_leaks.log") + + self.browserEnv = self.buildBrowserEnv(options, debuggerInfo is not None) + + if self.browserEnv is None: + return 1 + + if self.mozLogs: + self.browserEnv["MOZ_LOG_FILE"] = "{}/moz-pid=%PID-uid={}.log".format( + self.browserEnv["MOZ_UPLOAD_DIR"], str(uuid.uuid4()) + ) + + status = 0 + try: + self.startServers(options, debuggerInfo) + + if options.jsconsole: + options.browserArgs.extend(["--jsconsole"]) + + if options.jsdebugger: + options.browserArgs.extend(["-wait-for-jsdebugger", "-jsdebugger"]) + + # -jsdebugger takes a binary path as an optional argument. + # Append jsdebuggerPath right after `-jsdebugger`. + if options.jsdebuggerPath: + options.browserArgs.extend([options.jsdebuggerPath]) + + # Remove the leak detection file so it can't "leak" to the tests run. + # The file is not there if leak logging was not enabled in the + # application build. + if os.path.exists(self.leak_report_file): + os.remove(self.leak_report_file) + + # then again to actually run mochitest + if options.timeout: + timeout = options.timeout + 30 + elif options.debugger or not options.autorun: + timeout = None + else: + # We generally want the JS harness or marionette to handle + # timeouts if they can. + # The default JS harness timeout is currently 300 seconds. + # The default Marionette socket timeout is currently 360 seconds. + # Wait a little (10 seconds) more before timing out here. + # See bug 479518 and bug 1414063. + timeout = 370.0 + + if "MOZ_CHAOSMODE=0xfb" in options.environment and timeout: + timeout *= 2 + + # Detect shutdown leaks for m-bc runs if + # code coverage is not enabled. + detectShutdownLeaks = False + if options.jscov_dir_prefix is None: + detectShutdownLeaks = ( + mozinfo.info["debug"] + and options.flavor == "browser" + and options.subsuite != "thunderbird" + and not options.crashAsPass + ) + + self.start_script_kwargs["flavor"] = self.normflavor(options.flavor) + marionette_args = { + "symbols_path": options.symbolsPath, + "socket_timeout": options.marionette_socket_timeout, + "startup_timeout": options.marionette_startup_timeout, + } + + if options.marionette: + host, port = options.marionette.split(":") + marionette_args["host"] = host + marionette_args["port"] = int(port) + + # testsToFilter parameter is used to filter out the test list that + # is sent to getTestsByScheme + for (scheme, tests) in self.getTestsByScheme( + options, testsToFilter, True, manifestToFilter + ): + # read the number of tests here, if we are not going to run any, + # terminate early + if not tests: + continue + + self.currentTests = [t["path"] for t in tests] + testURL = self.buildTestURL(options, scheme=scheme) + + self.buildURLOptions(options, self.browserEnv) + if self.urlOpts: + testURL += "?" + "&".join(self.urlOpts) + + if options.runFailures: + testURL += "&runFailures=true" + + if options.timeoutAsPass: + testURL += "&timeoutAsPass=true" + + if options.conditionedProfile: + testURL += "&conditionedProfile=true" + + self.log.info("runtests.py | Running with scheme: {}".format(scheme)) + self.log.info( + "runtests.py | Running with e10s: {}".format(options.e10s) + ) + self.log.info( + "runtests.py | Running with fission: {}".format( + mozinfo.info.get("fission", True) + ) + ) + self.log.info( + "runtests.py | Running with cross-origin iframes: {}".format( + mozinfo.info.get("xorigin", False) + ) + ) + self.log.info( + "runtests.py | Running with serviceworker_e10s: {}".format( + mozinfo.info.get("serviceworker_e10s", False) + ) + ) + self.log.info( + "runtests.py | Running with socketprocess_e10s: {}".format( + mozinfo.info.get("socketprocess_e10s", False) + ) + ) + self.log.info("runtests.py | Running tests: start.\n") + ret, _ = self.runApp( + testURL, + self.browserEnv, + options.app, + profile=self.profile, + extraArgs=options.browserArgs + self.extraArgs, + utilityPath=options.utilityPath, + debuggerInfo=debuggerInfo, + valgrindPath=valgrindPath, + valgrindArgs=valgrindArgs, + valgrindSuppFiles=valgrindSuppFiles, + symbolsPath=options.symbolsPath, + timeout=timeout, + detectShutdownLeaks=detectShutdownLeaks, + screenshotOnFail=options.screenshotOnFail, + bisectChunk=options.bisectChunk, + marionette_args=marionette_args, + e10s=options.e10s, + runFailures=options.runFailures, + crashAsPass=options.crashAsPass, + ) + status = ret or status + except KeyboardInterrupt: + self.log.info("runtests.py | Received keyboard interrupt.\n") + status = -1 + except Exception as e: + traceback.print_exc() + self.log.error( + "Automation Error: Received unexpected exception while running application\n" + ) + if "ADBTimeoutError" in repr(e): + self.log.info("runtests.py | Device disconnected. Aborting test.\n") + raise + status = 1 + finally: + self.stopServers() + + ignoreMissingLeaks = options.ignoreMissingLeaks + leakThresholds = options.leakThresholds + + if options.crashAsPass: + ignoreMissingLeaks.append("tab") + ignoreMissingLeaks.append("socket") + + # Stop leak detection if m-bc code coverage is enabled + # by maxing out the leak threshold for all processes. + if options.jscov_dir_prefix: + for processType in leakThresholds: + ignoreMissingLeaks.append(processType) + leakThresholds[processType] = sys.maxsize + + utilityPath = options.utilityPath or options.xrePath + mozleak.process_leak_log( + self.leak_report_file, + leak_thresholds=leakThresholds, + ignore_missing_leaks=ignoreMissingLeaks, + log=self.log, + stack_fixer=get_stack_fixer_function(utilityPath, options.symbolsPath), + ) + + self.log.info("runtests.py | Running tests: end.") + + if self.manifest is not None: + self.cleanup(options, False) + + return status + + def handleTimeout( + self, timeout, proc, utilityPath, debuggerInfo, browser_pid, processLog + ): + """handle process output timeout""" + # TODO: bug 913975 : _processOutput should call self.processOutputLine + # one more time one timeout (I think) + error_message = ( + "TEST-UNEXPECTED-TIMEOUT | %s | application timed out after " + "%d seconds with no output" + ) % (self.lastTestSeen, int(timeout)) + self.message_logger.dump_buffered() + self.message_logger.buffering = False + self.log.info(error_message) + self.log.error("Force-terminating active process(es).") + + browser_pid = browser_pid or proc.pid + child_pids = self.extract_child_pids(processLog, browser_pid) + self.log.info("Found child pids: %s" % child_pids) + + if HAVE_PSUTIL: + try: + browser_proc = [psutil.Process(browser_pid)] + except Exception: + self.log.info("Failed to get proc for pid %d" % browser_pid) + browser_proc = [] + try: + child_procs = [psutil.Process(pid) for pid in child_pids] + except Exception: + self.log.info("Failed to get child procs") + child_procs = [] + for pid in child_pids: + self.killAndGetStack( + pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo + ) + gone, alive = psutil.wait_procs(child_procs, timeout=30) + for p in gone: + self.log.info("psutil found pid %s dead" % p.pid) + for p in alive: + self.log.warning("failed to kill pid %d after 30s" % p.pid) + self.killAndGetStack( + browser_pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo + ) + gone, alive = psutil.wait_procs(browser_proc, timeout=30) + for p in gone: + self.log.info("psutil found pid %s dead" % p.pid) + for p in alive: + self.log.warning("failed to kill pid %d after 30s" % p.pid) + else: + self.log.error( + "psutil not available! Will wait 30s before " + "attempting to kill parent process. This should " + "not occur in mozilla automation. See bug 1143547." + ) + for pid in child_pids: + self.killAndGetStack( + pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo + ) + if child_pids: + time.sleep(30) + + self.killAndGetStack( + browser_pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo + ) + + def archiveMozLogs(self): + if self.mozLogs: + with zipfile.ZipFile( + "{}/mozLogs.zip".format(os.environ["MOZ_UPLOAD_DIR"]), + "w", + zipfile.ZIP_DEFLATED, + ) as logzip: + for logfile in glob.glob( + "{}/moz*.log*".format(os.environ["MOZ_UPLOAD_DIR"]) + ): + logzip.write(logfile, os.path.basename(logfile)) + os.remove(logfile) + logzip.close() + + class OutputHandler(object): + + """line output handler for mozrunner""" + + def __init__( + self, + harness, + utilityPath, + symbolsPath=None, + dump_screen_on_timeout=True, + dump_screen_on_fail=False, + shutdownLeaks=None, + lsanLeaks=None, + bisectChunk=None, + ): + """ + harness -- harness instance + dump_screen_on_timeout -- whether to dump the screen on timeout + """ + self.harness = harness + self.utilityPath = utilityPath + self.symbolsPath = symbolsPath + self.dump_screen_on_timeout = dump_screen_on_timeout + self.dump_screen_on_fail = dump_screen_on_fail + self.shutdownLeaks = shutdownLeaks + self.lsanLeaks = lsanLeaks + self.bisectChunk = bisectChunk + self.browserProcessId = None + self.stackFixerFunction = self.stackFixer() + + def processOutputLine(self, line): + """per line handler of output for mozprocess""" + # Parsing the line (by the structured messages logger). + messages = self.harness.message_logger.parse_line(line) + + for message in messages: + # Passing the message to the handlers + for handler in self.outputHandlers(): + message = handler(message) + + # Processing the message by the logger + self.harness.message_logger.process_message(message) + + __call__ = processOutputLine + + def outputHandlers(self): + """returns ordered list of output handlers""" + handlers = [ + self.fix_stack, + self.record_last_test, + self.dumpScreenOnTimeout, + self.dumpScreenOnFail, + self.trackShutdownLeaks, + self.trackLSANLeaks, + self.countline, + ] + if self.bisectChunk: + handlers.append(self.record_result) + handlers.append(self.first_error) + + return handlers + + def stackFixer(self): + """ + return get_stack_fixer_function, if any, to use on the output lines + """ + return get_stack_fixer_function(self.utilityPath, self.symbolsPath) + + def finish(self): + if self.shutdownLeaks: + self.harness.countfail += self.shutdownLeaks.process() + + if self.lsanLeaks: + self.harness.countfail += self.lsanLeaks.process() + + # output message handlers: + # these take a message and return a message + + def record_result(self, message): + # by default make the result key equal to pass. + if message["action"] == "test_start": + key = message["test"].split("/")[-1].strip() + self.harness.result[key] = "PASS" + elif message["action"] == "test_status": + if "expected" in message: + key = message["test"].split("/")[-1].strip() + self.harness.result[key] = "FAIL" + elif message["status"] == "FAIL": + key = message["test"].split("/")[-1].strip() + self.harness.result[key] = "TODO" + return message + + def first_error(self, message): + if ( + message["action"] == "test_status" + and "expected" in message + and message["status"] == "FAIL" + ): + key = message["test"].split("/")[-1].strip() + if key not in self.harness.expectedError: + self.harness.expectedError[key] = message.get( + "message", message["subtest"] + ).strip() + return message + + def countline(self, message): + if message["action"] == "log": + line = message.get("message", "") + elif message["action"] == "process_output": + line = message.get("data", "") + else: + return message + val = 0 + try: + val = int(line.split(":")[-1].strip()) + except (AttributeError, ValueError): + return message + + if "Passed:" in line: + self.harness.countpass += val + elif "Failed:" in line: + self.harness.countfail += val + elif "Todo:" in line: + self.harness.counttodo += val + return message + + def fix_stack(self, message): + if self.stackFixerFunction: + if message["action"] == "log": + message["message"] = self.stackFixerFunction(message["message"]) + elif message["action"] == "process_output": + message["data"] = self.stackFixerFunction(message["data"]) + return message + + def record_last_test(self, message): + """record last test on harness""" + if message["action"] == "test_start": + self.harness.lastTestSeen = message["test"] + elif message["action"] == "test_end": + if ( + self.harness.currentTests + and message["test"] == self.harness.currentTests[-1] + ): + self.harness.lastTestSeen = "Last test finished" + else: + self.harness.lastTestSeen = "{} (finished)".format(message["test"]) + return message + + def dumpScreenOnTimeout(self, message): + if ( + not self.dump_screen_on_fail + and self.dump_screen_on_timeout + and message["action"] == "test_status" + and "expected" in message + and "Test timed out" in message["subtest"] + ): + self.harness.dumpScreen(self.utilityPath) + return message + + def dumpScreenOnFail(self, message): + if ( + self.dump_screen_on_fail + and "expected" in message + and message["status"] == "FAIL" + ): + self.harness.dumpScreen(self.utilityPath) + return message + + def trackLSANLeaks(self, message): + if self.lsanLeaks and message["action"] in ("log", "process_output"): + line = ( + message.get("message", "") + if message["action"] == "log" + else message["data"] + ) + self.lsanLeaks.log(line) + return message + + def trackShutdownLeaks(self, message): + if self.shutdownLeaks: + self.shutdownLeaks.log(message) + return message + + +def view_gecko_profile_from_mochitest(profile_path, options, profiler_logger): + """Getting shutdown performance profiles from just the command line arguments is + difficult. This function makes the developer ergonomics a bit easier by taking the + generated Gecko profile, and automatically serving it to profiler.firefox.com. The + Gecko profile during shutdown is dumped to disk at: + + {objdir}/_tests/testing/mochitest/{profilename} + + This function takes that file, and launches a local webserver, and then points + a browser to profiler.firefox.com to view it. From there it's easy to publish + or save the profile. + """ + + if options.profilerSaveOnly: + # The user did not want this to automatically open, only share the location. + return + + if not os.path.exists(profile_path): + profiler_logger.error( + "No profile was found at the profile path, cannot " + "launch profiler.firefox.com." + ) + return + + profiler_logger.info("Loading this profile in the Firefox Profiler") + + view_gecko_profile(profile_path) + + +def run_test_harness(parser, options): + parser.validate(options) + + logger_options = { + key: value + for key, value in six.iteritems(vars(options)) + if key.startswith("log") or key == "valgrind" + } + + runner = MochitestDesktop( + options.flavor, logger_options, options.stagedAddons, quiet=options.quiet + ) + + options.runByManifest = False + if options.flavor in ("plain", "browser", "chrome"): + options.runByManifest = True + + if options.verify or options.verify_fission: + result = runner.verifyTests(options) + else: + result = runner.runTests(options) + + runner.archiveMozLogs() + runner.message_logger.finish() + return result + + +def cli(args=sys.argv[1:]): + # parse command line options + parser = MochitestArgumentParser(app="generic") + options = parser.parse_args(args) + if options is None: + # parsing error + sys.exit(1) + + return run_test_harness(parser, options) + + +if __name__ == "__main__": + sys.exit(cli()) diff --git a/testing/mochitest/runtestsremote.py b/testing/mochitest/runtestsremote.py new file mode 100644 index 0000000000..caeaa600db --- /dev/null +++ b/testing/mochitest/runtestsremote.py @@ -0,0 +1,464 @@ +# 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 datetime +import os +import posixpath +import shutil +import sys +import tempfile +import traceback +import uuid + +sys.path.insert(0, os.path.abspath(os.path.realpath(os.path.dirname(__file__)))) + +import mozcrash +import mozinfo +from mochitest_options import MochitestArgumentParser, build_obj +from mozdevice import ADBDeviceFactory, ADBTimeoutError, RemoteProcessMonitor +from mozscreenshot import dump_device_screen, dump_screen +from runtests import MessageLogger, MochitestDesktop + +SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) + + +class MochiRemote(MochitestDesktop): + localProfile = None + logMessages = [] + + def __init__(self, options): + MochitestDesktop.__init__(self, options.flavor, vars(options)) + + verbose = False + if ( + options.log_mach_verbose + or options.log_tbpl_level == "debug" + or options.log_mach_level == "debug" + or options.log_raw_level == "debug" + ): + verbose = True + if hasattr(options, "log"): + delattr(options, "log") + + self.certdbNew = True + self.chromePushed = False + + expected = options.app.split("/")[-1] + self.device = ADBDeviceFactory( + adb=options.adbPath or "adb", + device=options.deviceSerial, + test_root=options.remoteTestRoot, + verbose=verbose, + run_as_package=expected, + ) + + if options.remoteTestRoot is None: + options.remoteTestRoot = self.device.test_root + options.dumpOutputDirectory = options.remoteTestRoot + self.remoteLogFile = posixpath.join( + options.remoteTestRoot, "logs", "mochitest.log" + ) + logParent = posixpath.dirname(self.remoteLogFile) + self.device.rm(logParent, force=True, recursive=True) + self.device.mkdir(logParent, parents=True) + + self.remoteProfile = posixpath.join(options.remoteTestRoot, "profile") + self.device.rm(self.remoteProfile, force=True, recursive=True) + + self.message_logger = MessageLogger(logger=None) + self.message_logger.logger = self.log + + # Check that Firefox is installed + expected = options.app.split("/")[-1] + if not self.device.is_app_installed(expected): + raise Exception("%s is not installed on this device" % expected) + + self.device.clear_logcat() + + self.remoteModulesDir = posixpath.join(options.remoteTestRoot, "modules/") + + self.remoteCache = posixpath.join(options.remoteTestRoot, "cache/") + self.device.rm(self.remoteCache, force=True, recursive=True) + + # move necko cache to a location that can be cleaned up + options.extraPrefs += [ + "browser.cache.disk.parent_directory=%s" % self.remoteCache + ] + + self.remoteMozLog = posixpath.join(options.remoteTestRoot, "mozlog") + self.device.rm(self.remoteMozLog, force=True, recursive=True) + self.device.mkdir(self.remoteMozLog, parents=True) + + self.remoteChromeTestDir = posixpath.join(options.remoteTestRoot, "chrome") + self.device.rm(self.remoteChromeTestDir, force=True, recursive=True) + self.device.mkdir(self.remoteChromeTestDir, parents=True) + + self.appName = options.remoteappname + self.device.stop_application(self.appName) + if self.device.process_exist(self.appName): + self.log.warning("unable to kill %s before running tests!" % self.appName) + + # Add Android version (SDK level) to mozinfo so that manifest entries + # can be conditional on android_version. + self.log.info( + "Android sdk version '%s'; will use this to filter manifests" + % str(self.device.version) + ) + mozinfo.info["android_version"] = str(self.device.version) + mozinfo.info["is_fennec"] = not ("geckoview" in options.app) + mozinfo.info["is_emulator"] = self.device._device_serial.startswith("emulator-") + + def cleanup(self, options, final=False): + if final: + self.device.rm(self.remoteChromeTestDir, force=True, recursive=True) + self.chromePushed = False + uploadDir = os.environ.get("MOZ_UPLOAD_DIR", None) + if uploadDir and self.device.is_dir(self.remoteMozLog): + self.device.pull(self.remoteMozLog, uploadDir) + self.device.rm(self.remoteLogFile, force=True) + self.device.rm(self.remoteProfile, force=True, recursive=True) + self.device.rm(self.remoteCache, force=True, recursive=True) + MochitestDesktop.cleanup(self, options, final) + self.localProfile = None + + def dumpScreen(self, utilityPath): + if self.haveDumpedScreen: + self.log.info( + "Not taking screenshot here: see the one that was previously logged" + ) + return + self.haveDumpedScreen = True + if self.device._device_serial.startswith("emulator-"): + dump_screen(utilityPath, self.log) + else: + dump_device_screen(self.device, self.log) + + def findPath(self, paths, filename=None): + for path in paths: + p = path + if filename: + p = os.path.join(p, filename) + if os.path.exists(self.getFullPath(p)): + return path + return None + + # This seems kludgy, but this class uses paths from the remote host in the + # options, except when calling up to the base class, which doesn't + # understand the distinction. This switches out the remote values for local + # ones that the base class understands. This is necessary for the web + # server, SSL tunnel and profile building functions. + def switchToLocalPaths(self, options): + """Set local paths in the options, return a function that will restore remote values""" + remoteXrePath = options.xrePath + remoteProfilePath = options.profilePath + remoteUtilityPath = options.utilityPath + + paths = [ + options.xrePath, + ] + if build_obj: + paths.append(os.path.join(build_obj.topobjdir, "dist", "bin")) + options.xrePath = self.findPath(paths) + if options.xrePath is None: + self.log.error( + "unable to find xulrunner path for %s, please specify with --xre-path" + % os.name + ) + sys.exit(1) + + xpcshell = "xpcshell" + if os.name == "nt": + xpcshell += ".exe" + + if options.utilityPath: + paths = [options.utilityPath, options.xrePath] + else: + paths = [options.xrePath] + options.utilityPath = self.findPath(paths, xpcshell) + + if options.utilityPath is None: + self.log.error( + "unable to find utility path for %s, please specify with --utility-path" + % os.name + ) + sys.exit(1) + + xpcshell_path = os.path.join(options.utilityPath, xpcshell) + if RemoteProcessMonitor.elf_arm(xpcshell_path): + self.log.error( + "xpcshell at %s is an ARM binary; please use " + "the --utility-path argument to specify the path " + "to a desktop version." % xpcshell_path + ) + sys.exit(1) + + if self.localProfile: + options.profilePath = self.localProfile + else: + options.profilePath = None + + def fixup(): + options.xrePath = remoteXrePath + options.utilityPath = remoteUtilityPath + options.profilePath = remoteProfilePath + + return fixup + + def startServers(self, options, debuggerInfo, public=None): + """Create the servers on the host and start them up""" + restoreRemotePaths = self.switchToLocalPaths(options) + MochitestDesktop.startServers(self, options, debuggerInfo, public=True) + restoreRemotePaths() + + def buildProfile(self, options): + restoreRemotePaths = self.switchToLocalPaths(options) + if options.testingModulesDir: + try: + self.device.push(options.testingModulesDir, self.remoteModulesDir) + self.device.chmod(self.remoteModulesDir, recursive=True) + except Exception: + self.log.error( + "Automation Error: Unable to copy test modules to device." + ) + raise + savedTestingModulesDir = options.testingModulesDir + options.testingModulesDir = self.remoteModulesDir + else: + savedTestingModulesDir = None + manifest = MochitestDesktop.buildProfile(self, options) + if savedTestingModulesDir: + options.testingModulesDir = savedTestingModulesDir + self.localProfile = options.profilePath + + restoreRemotePaths() + options.profilePath = self.remoteProfile + return manifest + + def buildURLOptions(self, options, env): + saveLogFile = options.logFile + options.logFile = self.remoteLogFile + options.profilePath = self.localProfile + env["MOZ_HIDE_RESULTS_TABLE"] = "1" + retVal = MochitestDesktop.buildURLOptions(self, options, env) + + # we really need testConfig.js (for browser chrome) + try: + self.device.push(options.profilePath, self.remoteProfile) + self.device.chmod(self.remoteProfile, recursive=True) + except Exception: + self.log.error("Automation Error: Unable to copy profile to device.") + raise + + options.profilePath = self.remoteProfile + options.logFile = saveLogFile + return retVal + + def getChromeTestDir(self, options): + local = super(MochiRemote, self).getChromeTestDir(options) + remote = self.remoteChromeTestDir + if options.flavor == "chrome" and not self.chromePushed: + self.log.info("pushing %s to %s on device..." % (local, remote)) + local = os.path.join(local, "chrome") + self.device.push(local, remote) + self.chromePushed = True + return remote + + def getLogFilePath(self, logFile): + return logFile + + def getGMPPluginPath(self, options): + # TODO: bug 1149374 + return None + + def environment(self, env=None, crashreporter=True, **kwargs): + # Since running remote, do not mimic the local env: do not copy os.environ + if env is None: + env = {} + + if crashreporter: + env["MOZ_CRASHREPORTER_NO_REPORT"] = "1" + env["MOZ_CRASHREPORTER"] = "1" + env["MOZ_CRASHREPORTER_SHUTDOWN"] = "1" + else: + env["MOZ_CRASHREPORTER_DISABLE"] = "1" + + # Crash on non-local network connections by default. + # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily + # enable non-local connections for the purposes of local testing. + # Don't override the user's choice here. See bug 1049688. + env.setdefault("MOZ_DISABLE_NONLOCAL_CONNECTIONS", "1") + + # Send an env var noting that we are in automation. Passing any + # value except the empty string will declare the value to exist. + # + # This may be used to disabled network connections during testing, e.g. + # Switchboard & telemetry uploads. + env.setdefault("MOZ_IN_AUTOMATION", "1") + + # Set WebRTC logging in case it is not set yet. + env.setdefault("R_LOG_LEVEL", "6") + env.setdefault("R_LOG_DESTINATION", "stderr") + env.setdefault("R_LOG_VERBOSE", "1") + + return env + + def buildBrowserEnv(self, options, debugger=False): + browserEnv = MochitestDesktop.buildBrowserEnv(self, options, debugger=debugger) + # remove desktop environment not used on device + if "XPCOM_MEM_BLOAT_LOG" in browserEnv: + del browserEnv["XPCOM_MEM_BLOAT_LOG"] + if self.mozLogs: + browserEnv["MOZ_LOG_FILE"] = os.path.join( + self.remoteMozLog, "moz-pid=%PID-uid={}.log".format(str(uuid.uuid4())) + ) + if options.dmd: + browserEnv["DMD"] = "1" + # Contents of remoteMozLog will be pulled from device and copied to the + # host MOZ_UPLOAD_DIR, to be made available as test artifacts. Make + # MOZ_UPLOAD_DIR available to the browser environment so that tests + # can use it as though they were running on the host. + browserEnv["MOZ_UPLOAD_DIR"] = self.remoteMozLog + return browserEnv + + def runApp( + self, + testUrl, + env, + app, + profile, + extraArgs, + utilityPath, + debuggerInfo=None, + valgrindPath=None, + valgrindArgs=None, + valgrindSuppFiles=None, + symbolsPath=None, + timeout=-1, + detectShutdownLeaks=False, + screenshotOnFail=False, + bisectChunk=None, + marionette_args=None, + e10s=True, + runFailures=False, + crashAsPass=False, + ): + """ + Run the app, log the duration it took to execute, return the status code. + Kill the app if it outputs nothing for |timeout| seconds. + """ + + if timeout == -1: + timeout = self.DEFAULT_TIMEOUT + + rpm = RemoteProcessMonitor( + self.appName, + self.device, + self.log, + self.message_logger, + self.remoteLogFile, + self.remoteProfile, + ) + startTime = datetime.datetime.now() + status = 0 + profileDirectory = self.remoteProfile + "/" + args = [] + args.extend(extraArgs) + args.extend(("-no-remote", "-profile", profileDirectory)) + + pid = rpm.launch( + app, + debuggerInfo, + testUrl, + args, + env=self.environment(env=env, crashreporter=not debuggerInfo), + e10s=e10s, + ) + + # TODO: not using runFailures or crashAsPass, if we choose to use them + # we need to adjust status and check_for_crashes + self.log.info("runtestsremote.py | Application pid: %d" % pid) + if not rpm.wait(timeout): + status = 1 + self.log.info( + "runtestsremote.py | Application ran for: %s" + % str(datetime.datetime.now() - startTime) + ) + crashed = self.check_for_crashes(symbolsPath, rpm.last_test_seen) + if crashed: + status = 1 + + self.countpass += rpm.counts["pass"] + self.countfail += rpm.counts["fail"] + self.counttodo += rpm.counts["todo"] + + return status, rpm.last_test_seen + + def check_for_crashes(self, symbols_path, last_test_seen): + """ + Pull any minidumps from remote profile and log any associated crashes. + """ + try: + dump_dir = tempfile.mkdtemp() + remote_crash_dir = posixpath.join(self.remoteProfile, "minidumps") + if not self.device.is_dir(remote_crash_dir): + return False + self.device.pull(remote_crash_dir, dump_dir) + crashed = mozcrash.log_crashes( + self.log, dump_dir, symbols_path, test=last_test_seen + ) + finally: + try: + shutil.rmtree(dump_dir) + except Exception as e: + self.log.warning( + "unable to remove directory %s: %s" % (dump_dir, str(e)) + ) + return crashed + + +def run_test_harness(parser, options): + parser.validate(options) + + if options is None: + raise ValueError( + "Invalid options specified, use --help for a list of valid options" + ) + + options.runByManifest = True + + mochitest = MochiRemote(options) + + try: + if options.verify: + retVal = mochitest.verifyTests(options) + else: + retVal = mochitest.runTests(options) + except Exception as e: + mochitest.log.error("Automation Error: Exception caught while running tests") + traceback.print_exc() + if isinstance(e, ADBTimeoutError): + mochitest.log.info("Device disconnected. Will not run mochitest.cleanup().") + else: + try: + mochitest.cleanup(options) + except Exception: + # device error cleaning up... oh well! + traceback.print_exc() + retVal = 1 + + mochitest.archiveMozLogs() + mochitest.message_logger.finish() + + return retVal + + +def main(args=sys.argv[1:]): + parser = MochitestArgumentParser(app="android") + options = parser.parse_args(args) + + return run_test_harness(parser, options) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/testing/mochitest/schema.json b/testing/mochitest/schema.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/testing/mochitest/schema.json @@ -0,0 +1 @@ +[] diff --git a/testing/mochitest/server.js b/testing/mochitest/server.js new file mode 100644 index 0000000000..e8e514d344 --- /dev/null +++ b/testing/mochitest/server.js @@ -0,0 +1,873 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +// We expect these to be defined in the global scope by runtest.py. +/* global __LOCATION__, _PROFILE_PATH, _SERVER_PORT, _SERVER_ADDR, _DISPLAY_RESULTS, + _TEST_PREFIX */ +// Defined by xpcshell +/* global quit */ + +/* import-globals-from ../../netwerk/test/httpserver/httpd.js */ +/* eslint-disable mozilla/use-chromeutils-generateqi */ + +// Disable automatic network detection, so tests work correctly when +// not connected to a network. +// eslint-disable-next-line mozilla/use-services +var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); +ios.manageOfflineStatus = false; +ios.offline = false; + +var server; // for use in the shutdown handler, if necessary + +// +// HTML GENERATION +// +/* global A, ABBR, ACRONYM, ADDRESS, APPLET, AREA, B, BASE, + BASEFONT, BDO, BIG, BLOCKQUOTE, BODY, BR, BUTTON, + CAPTION, CENTER, CITE, CODE, COL, COLGROUP, DD, + DEL, DFN, DIR, DIV, DL, DT, EM, FIELDSET, FONT, + FORM, FRAME, FRAMESET, H1, H2, H3, H4, H5, H6, + HEAD, HR, HTML, I, IFRAME, IMG, INPUT, INS, + ISINDEX, KBD, LABEL, LEGEND, LI, LINK, MAP, MENU, + META, NOFRAMES, NOSCRIPT, OBJECT, OL, OPTGROUP, + OPTION, P, PARAM, PRE, Q, S, SAMP, SCRIPT, + SELECT, SMALL, SPAN, STRIKE, STRONG, STYLE, SUB, + SUP, TABLE, TBODY, TD, TEXTAREA, TFOOT, TH, THEAD, + TITLE, TR, TT, U, UL, VAR */ +var tags = [ + "A", + "ABBR", + "ACRONYM", + "ADDRESS", + "APPLET", + "AREA", + "B", + "BASE", + "BASEFONT", + "BDO", + "BIG", + "BLOCKQUOTE", + "BODY", + "BR", + "BUTTON", + "CAPTION", + "CENTER", + "CITE", + "CODE", + "COL", + "COLGROUP", + "DD", + "DEL", + "DFN", + "DIR", + "DIV", + "DL", + "DT", + "EM", + "FIELDSET", + "FONT", + "FORM", + "FRAME", + "FRAMESET", + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "HEAD", + "HR", + "HTML", + "I", + "IFRAME", + "IMG", + "INPUT", + "INS", + "ISINDEX", + "KBD", + "LABEL", + "LEGEND", + "LI", + "LINK", + "MAP", + "MENU", + "META", + "NOFRAMES", + "NOSCRIPT", + "OBJECT", + "OL", + "OPTGROUP", + "OPTION", + "P", + "PARAM", + "PRE", + "Q", + "S", + "SAMP", + "SCRIPT", + "SELECT", + "SMALL", + "SPAN", + "STRIKE", + "STRONG", + "STYLE", + "SUB", + "SUP", + "TABLE", + "TBODY", + "TD", + "TEXTAREA", + "TFOOT", + "TH", + "THEAD", + "TITLE", + "TR", + "TT", + "U", + "UL", + "VAR", +]; + +/** + * Below, we'll use makeTagFunc to create a function for each of the + * strings in 'tags'. This will allow us to use s-expression like syntax + * to create HTML. + */ +function makeTagFunc(tagName) { + return function(attrs /* rest... */) { + var startChildren = 0; + var response = ""; + + // write the start tag and attributes + response += "<" + tagName; + // if attr is an object, write attributes + if (attrs && typeof attrs == "object") { + startChildren = 1; + + for (let key in attrs) { + const value = attrs[key]; + var val = "" + value; + response += " " + key + '="' + val.replace('"', """) + '"'; + } + } + response += ">"; + + // iterate through the rest of the args + for (var i = startChildren; i < arguments.length; i++) { + if (typeof arguments[i] == "function") { + response += arguments[i](); + } else { + response += arguments[i]; + } + } + + // write the close tag + response += "</" + tagName + ">\n"; + return response; + }; +} + +function makeTags() { + // map our global HTML generation functions + for (let tag of tags) { + this[tag] = makeTagFunc(tag.toLowerCase()); + } +} + +var _quitting = false; + +/** Quit when all activity has completed. */ +function serverStopped() { + _quitting = true; +} + +// only run the "main" section if httpd.js was loaded ahead of us +if (this.nsHttpServer) { + // + // SCRIPT CODE + // + runServer(); + + // We can only have gotten here if the /server/shutdown path was requested. + if (_quitting) { + dumpn("HTTP server stopped, all pending requests complete"); + quit(0); + } + + // Impossible as the stop callback should have been called, but to be safe... + dumpn("TEST-UNEXPECTED-FAIL | failure to correctly shut down HTTP server"); + quit(1); +} + +var serverBasePath; +var displayResults = true; + +var gServerAddress; +var SERVER_PORT; + +// +// SERVER SETUP +// +function runServer() { + serverBasePath = __LOCATION__.parent; + server = createMochitestServer(serverBasePath); + + // verify server address + // if a.b.c.d or 'localhost' + if (typeof _SERVER_ADDR != "undefined") { + if (_SERVER_ADDR == "localhost") { + gServerAddress = _SERVER_ADDR; + } else { + var quads = _SERVER_ADDR.split("."); + if (quads.length == 4) { + var invalid = false; + for (var i = 0; i < 4; i++) { + if (quads[i] < 0 || quads[i] > 255) { + invalid = true; + } + } + if (!invalid) { + gServerAddress = _SERVER_ADDR; + } else { + throw new Error( + "invalid _SERVER_ADDR, please specify a valid IP Address" + ); + } + } + } + } else { + throw new Error( + "please define _SERVER_ADDR (as an ip address) before running server.js" + ); + } + + if (typeof _SERVER_PORT != "undefined") { + if (parseInt(_SERVER_PORT) > 0 && parseInt(_SERVER_PORT) < 65536) { + SERVER_PORT = _SERVER_PORT; + } + } else { + throw new Error( + "please define _SERVER_PORT (as a port number) before running server.js" + ); + } + + // If DISPLAY_RESULTS is not specified, it defaults to true + if (typeof _DISPLAY_RESULTS != "undefined") { + displayResults = _DISPLAY_RESULTS; + } + + server._start(SERVER_PORT, gServerAddress); + + // touch a file in the profile directory to indicate we're alive + var foStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + var serverAlive = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + + if (typeof _PROFILE_PATH == "undefined") { + serverAlive.initWithFile(serverBasePath); + serverAlive.append("mochitesttestingprofile"); + } else { + serverAlive.initWithPath(_PROFILE_PATH); + } + + // Create a file to inform the harness that the server is ready + if (serverAlive.exists()) { + serverAlive.append("server_alive.txt"); + foStream.init(serverAlive, 0x02 | 0x08 | 0x20, 436, 0); // write, create, truncate + var data = "It's alive!"; + foStream.write(data, data.length); + foStream.close(); + } else { + throw new Error( + "Failed to create server_alive.txt because " + + serverAlive.path + + " could not be found." + ); + } + + makeTags(); + + // + // The following is threading magic to spin an event loop -- this has to + // happen manually in xpcshell for the server to actually work. + // + var thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread; + while (!server.isStopped()) { + thread.processNextEvent(true); + } + + // Server stopped by /server/shutdown handler -- go through pending events + // and return. + + // get rid of any pending requests + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } +} + +/** Creates and returns an HTTP server configured to serve Mochitests. */ +function createMochitestServer(serverBasePath) { + var server = new nsHttpServer(); + + server.registerDirectory("/", serverBasePath); + server.registerPathHandler("/server/shutdown", serverShutdown); + server.registerPathHandler("/server/debug", serverDebug); + server.registerContentType("sjs", "sjs"); // .sjs == CGI-like functionality + server.registerContentType("jar", "application/x-jar"); + server.registerContentType("ogg", "application/ogg"); + server.registerContentType("pdf", "application/pdf"); + server.registerContentType("ogv", "video/ogg"); + server.registerContentType("oga", "audio/ogg"); + server.registerContentType("opus", "audio/ogg; codecs=opus"); + server.registerContentType("dat", "text/plain; charset=utf-8"); + server.registerContentType("frag", "text/plain"); // .frag == WebGL fragment shader + server.registerContentType("vert", "text/plain"); // .vert == WebGL vertex shader + server.registerContentType("wasm", "application/wasm"); + server.setIndexHandler(defaultDirHandler); + + var serverRoot = { + getFile: function getFile(path) { + var file = serverBasePath.clone().QueryInterface(Ci.nsIFile); + path.split("/").forEach(function(p) { + file.appendRelativePath(p); + }); + return file; + }, + QueryInterface(aIID) { + return this; + }, + }; + + server.setObjectState("SERVER_ROOT", serverRoot); + + processLocations(server); + + return server; +} + +/** + * Notifies the HTTP server about all the locations at which it might receive + * requests, so that it can properly respond to requests on any of the hosts it + * serves. + */ +function processLocations(server) { + var serverLocations = serverBasePath.clone(); + serverLocations.append("server-locations.txt"); + + const PR_RDONLY = 0x01; + var fis = new FileInputStream( + serverLocations, + PR_RDONLY, + 292 /* 0444 */, + Ci.nsIFileInputStream.CLOSE_ON_EOF + ); + + var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0); + lis.QueryInterface(Ci.nsIUnicharLineInputStream); + + const LINE_REGEXP = new RegExp( + "^([a-z][-a-z0-9+.]*)" + + "://" + + "(" + + "\\d+\\.\\d+\\.\\d+\\.\\d+" + + "|" + + "(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\\.)*" + + "[a-z](?:[-a-z0-9]*[a-z0-9])?" + + ")" + + ":" + + "(\\d+)" + + "(?:" + + "\\s+" + + "(\\S+(?:,\\S+)*)" + + ")?$" + ); + + var line = {}; + var lineno = 0; + var seenPrimary = false; + do { + var more = lis.readLine(line); + lineno++; + + var lineValue = line.value; + if (lineValue.charAt(0) == "#" || lineValue == "") { + continue; + } + + var match = LINE_REGEXP.exec(lineValue); + if (!match) { + throw new Error("Syntax error in server-locations.txt, line " + lineno); + } + + var [, scheme, host, port, options] = match; + if (options) { + if (options.split(",").includes("primary")) { + if (seenPrimary) { + throw new Error( + "Multiple primary locations in server-locations.txt, " + + "line " + + lineno + ); + } + + server.identity.setPrimary(scheme, host, port); + seenPrimary = true; + continue; + } + } + + server.identity.add(scheme, host, port); + } while (more); +} + +// PATH HANDLERS + +// /server/shutdown +function serverShutdown(metadata, response) { + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-type", "text/plain", false); + + var body = "Server shut down."; + response.bodyOutputStream.write(body, body.length); + + dumpn("Server shutting down now..."); + server.stop(serverStopped); +} + +// /server/debug?[012] +function serverDebug(metadata, response) { + response.setStatusLine(metadata.httpVersion, 400, "Bad debugging level"); + if (metadata.queryString.length !== 1) { + return; + } + + var mode; + if (metadata.queryString === "0") { + // do this now so it gets logged with the old mode + dumpn("Server debug logs disabled."); + DEBUG = false; + DEBUG_TIMESTAMP = false; + mode = "disabled"; + } else if (metadata.queryString === "1") { + DEBUG = true; + DEBUG_TIMESTAMP = false; + mode = "enabled"; + } else if (metadata.queryString === "2") { + DEBUG = true; + DEBUG_TIMESTAMP = true; + mode = "enabled, with timestamps"; + } else { + return; + } + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-type", "text/plain", false); + var body = "Server debug logs " + mode + "."; + response.bodyOutputStream.write(body, body.length); + dumpn(body); +} + +// +// DIRECTORY LISTINGS +// + +/** + * Creates a generator that iterates over the contents of + * an nsIFile directory. + */ +function* dirIter(dir) { + var en = dir.directoryEntries; + while (en.hasMoreElements()) { + yield en.nextFile; + } +} + +/** + * Builds an optionally nested object containing links to the + * files and directories within dir. + */ +function list(requestPath, directory, recurse) { + var count = 0; + var path = requestPath; + if (path.charAt(path.length - 1) != "/") { + path += "/"; + } + + var dir = directory.QueryInterface(Ci.nsIFile); + var links = {}; + + // The SimpleTest directory is hidden + let files = []; + for (let file of dirIter(dir)) { + if (file.exists() && !file.path.includes("SimpleTest")) { + files.push(file); + } + } + + // Sort files by name, so that tests can be run in a pre-defined order inside + // a given directory (see bug 384823) + function leafNameComparator(first, second) { + if (first.leafName < second.leafName) { + return -1; + } + if (first.leafName > second.leafName) { + return 1; + } + return 0; + } + files.sort(leafNameComparator); + + count = files.length; + for (let file of files) { + var key = path + file.leafName; + var childCount = 0; + if (file.isDirectory()) { + key += "/"; + } + if (recurse && file.isDirectory()) { + [links[key], childCount] = list(key, file, recurse); + count += childCount; + } else if (file.leafName.charAt(0) != ".") { + links[key] = { test: { url: key, expected: "pass" } }; + } + } + + return [links, count]; +} + +/** + * Heuristic function that determines whether a given path + * is a test case to be executed in the harness, or just + * a supporting file. + */ +function isTest(filename, pattern) { + if (pattern) { + return pattern.test(filename); + } + + // File name is a URL style path to a test file, make sure that we check for + // tests that start with the appropriate prefix. + var testPrefix = typeof _TEST_PREFIX == "string" ? _TEST_PREFIX : "test_"; + var testPattern = new RegExp("^" + testPrefix); + + var pathPieces = filename.split("/"); + + return ( + testPattern.test(pathPieces[pathPieces.length - 1]) && + !filename.includes(".js") && + !filename.includes(".css") && + !/\^headers\^$/.test(filename) + ); +} + +/** + * Transform nested hashtables of paths to nested HTML lists. + */ +function linksToListItems(links) { + var response = ""; + var children = ""; + for (let link in links) { + const value = links[link]; + var classVal = + !isTest(link) && !(value instanceof Object) + ? "non-test invisible" + : "test"; + if (value instanceof Object) { + children = UL({ class: "testdir" }, linksToListItems(value)); + } else { + children = ""; + } + + var bug_title = link.match(/test_bug\S+/); + var bug_num = null; + if (bug_title != null) { + bug_num = bug_title[0].match(/\d+/); + } + + if (bug_title == null || bug_num == null) { + response += LI({ class: classVal }, A({ href: link }, link), children); + } else { + var bug_url = "https://bugzilla.mozilla.org/show_bug.cgi?id=" + bug_num; + response += LI( + { class: classVal }, + A({ href: link }, link), + " - ", + A({ href: bug_url }, "Bug " + bug_num), + children + ); + } + } + return response; +} + +/** + * Transform nested hashtables of paths to a flat table rows. + */ +function linksToTableRows(links, recursionLevel) { + var response = ""; + for (let link in links) { + const value = links[link]; + var classVal = + !isTest(link) && value instanceof Object && "test" in value + ? "non-test invisible" + : ""; + + var spacer = "padding-left: " + 10 * recursionLevel + "px"; + + if (value instanceof Object && !("test" in value)) { + response += TR( + { class: "dir", id: "tr-" + link }, + TD({ colspan: "3" }, " "), + TD({ style: spacer }, A({ href: link }, link)) + ); + response += linksToTableRows(value, recursionLevel + 1); + } else { + var bug_title = link.match(/test_bug\S+/); + var bug_num = null; + if (bug_title != null) { + bug_num = bug_title[0].match(/\d+/); + } + if (bug_title == null || bug_num == null) { + response += TR( + { class: classVal, id: "tr-" + link }, + TD("0"), + TD("0"), + TD("0"), + TD({ style: spacer }, A({ href: link }, link)) + ); + } else { + var bug_url = "https://bugzilla.mozilla.org/show_bug.cgi?id=" + bug_num; + response += TR( + { class: classVal, id: "tr-" + link }, + TD("0"), + TD("0"), + TD("0"), + TD( + { style: spacer }, + A({ href: link }, link), + " - ", + A({ href: bug_url }, "Bug " + bug_num) + ) + ); + } + } + } + return response; +} + +function arrayOfTestFiles(linkArray, fileArray, testPattern) { + for (let link in linkArray) { + const value = linkArray[link]; + if (value instanceof Object && !("test" in value)) { + arrayOfTestFiles(value, fileArray, testPattern); + } else if (isTest(link, testPattern) && value instanceof Object) { + fileArray.push(value.test); + } + } +} +/** + * Produce a flat array of test file paths to be executed in the harness. + */ +function jsonArrayOfTestFiles(links) { + var testFiles = []; + arrayOfTestFiles(links, testFiles); + testFiles = testFiles.map(function(file) { + return '"' + file.url + '"'; + }); + + return "[" + testFiles.join(",\n") + "]"; +} + +/** + * Produce a normal directory listing. + */ +function regularListing(metadata, response) { + var [links] = list(metadata.path, metadata.getProperty("directory"), false); + response.write( + HTML( + HEAD(TITLE("mochitest index ", metadata.path)), + BODY(BR(), A({ href: ".." }, "Up a level"), UL(linksToListItems(links))) + ) + ); +} + +/** + * Read a manifestFile located at the root of the server's directory and turn + * it into an object for creating a table of clickable links for each test. + */ +function convertManifestToTestLinks(root, manifest) { + const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + + var manifestFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + manifestFile.initWithFile(serverBasePath); + manifestFile.append(manifest); + + var manifestStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + manifestStream.init(manifestFile, -1, 0, 0); + + var manifestObj = JSON.parse( + NetUtil.readInputStreamToString(manifestStream, manifestStream.available()) + ); + var paths = manifestObj.tests; + var pathPrefix = "/" + root + "/"; + return [ + paths.reduce(function(t, p) { + t[pathPrefix + p.path] = true; + return t; + }, {}), + paths.length, + ]; +} + +/** + * Produce a test harness page containing all the test cases + * below it, recursively. + */ +function testListing(metadata, response) { + var links = {}; + var count = 0; + if (!metadata.queryString.includes("manifestFile")) { + [links, count] = list( + metadata.path, + metadata.getProperty("directory"), + true + ); + } else if (typeof Components != "undefined") { + var manifest = metadata.queryString.match(/manifestFile=([^&]+)/)[1]; + + [links, count] = convertManifestToTestLinks( + metadata.path.split("/")[1], + manifest + ); + } + + var table_class = + metadata.queryString.indexOf("hideResultsTable=1") > -1 ? "invisible" : ""; + + let testname = + metadata.queryString.indexOf("testname=") > -1 + ? metadata.queryString.match(/testname=([^&]+)/)[1] + : ""; + + dumpn("count: " + count); + var tests = testname ? "['/" + testname + "']" : jsonArrayOfTestFiles(links); + response.write( + HTML( + HEAD( + TITLE("MochiTest | ", metadata.path), + LINK({ + rel: "stylesheet", + type: "text/css", + href: "/static/harness.css", + }), + SCRIPT({ + type: "text/javascript", + src: "/tests/SimpleTest/LogController.js", + }), + SCRIPT({ + type: "text/javascript", + src: "/tests/SimpleTest/MemoryStats.js", + }), + SCRIPT({ + type: "text/javascript", + src: "/tests/SimpleTest/TestRunner.js", + }), + SCRIPT({ + type: "text/javascript", + src: "/tests/SimpleTest/MozillaLogger.js", + }), + SCRIPT({ type: "text/javascript", src: "/chunkifyTests.js" }), + SCRIPT({ type: "text/javascript", src: "/manifestLibrary.js" }), + SCRIPT({ type: "text/javascript", src: "/tests/SimpleTest/setup.js" }), + SCRIPT( + { type: "text/javascript" }, + "window.onload = hookup; gTestList=" + tests + ";" + ) + ), + BODY( + DIV( + { class: "container" }, + H2("--> ", A({ href: "#", id: "runtests" }, "Run Tests"), " <--"), + P( + { style: "float: right;" }, + SMALL( + "Based on the ", + A({ href: "http://www.mochikit.com/" }, "MochiKit"), + " unit tests." + ) + ), + DIV( + { class: "status" }, + H1({ id: "indicator" }, "Status"), + H2({ id: "pass" }, "Passed: ", SPAN({ id: "pass-count" }, "0")), + H2({ id: "fail" }, "Failed: ", SPAN({ id: "fail-count" }, "0")), + H2({ id: "fail" }, "Todo: ", SPAN({ id: "todo-count" }, "0")) + ), + DIV({ class: "clear" }), + DIV( + { id: "current-test" }, + B("Currently Executing: ", SPAN({ id: "current-test-path" }, "_")) + ), + DIV({ class: "clear" }), + DIV( + { class: "frameholder" }, + IFRAME({ scrolling: "no", id: "testframe", allowfullscreen: true }) + ), + DIV({ class: "clear" }), + DIV( + { class: "toggle" }, + A({ href: "#", id: "toggleNonTests" }, "Show Non-Tests"), + BR() + ), + + displayResults + ? TABLE( + { + cellpadding: 0, + cellspacing: 0, + class: table_class, + id: "test-table", + }, + TR(TD("Passed"), TD("Failed"), TD("Todo"), TD("Test Files")), + linksToTableRows(links, 0) + ) + : "", + BR(), + TABLE({ + cellpadding: 0, + cellspacing: 0, + border: 1, + bordercolor: "red", + id: "fail-table", + }), + + DIV({ class: "clear" }) + ) + ) + ) + ); +} + +/** + * Respond to requests that match a file system directory. + * Under the tests/ directory, return a test harness page. + */ +function defaultDirHandler(metadata, response) { + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-type", "text/html;charset=utf-8", false); + try { + if (metadata.path.indexOf("/tests") != 0) { + regularListing(metadata, response); + } else { + testListing(metadata, response); + } + } catch (ex) { + response.write(ex); + } +} diff --git a/testing/mochitest/shutdown-leaks-collector.js b/testing/mochitest/shutdown-leaks-collector.js new file mode 100644 index 0000000000..2d7030f3c3 --- /dev/null +++ b/testing/mochitest/shutdown-leaks-collector.js @@ -0,0 +1,11 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +// We run this code in a .jsm rather than here to avoid keeping the current +// compartment alive. +ChromeUtils.importESModule( + "chrome://mochikit/content/ShutdownLeaksCollector.sys.mjs" +); diff --git a/testing/mochitest/ssltunnel/moz.build b/testing/mochitest/ssltunnel/moz.build new file mode 100644 index 0000000000..f9f6227153 --- /dev/null +++ b/testing/mochitest/ssltunnel/moz.build @@ -0,0 +1,20 @@ +# -*- 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/. + +GeckoProgram("ssltunnel", linkage=None) + +SOURCES += [ + "ssltunnel.cpp", +] + +USE_LIBS += [ + "nspr", + "nss", +] + +# This isn't XPCOM code, but it wants to use STL, so disable the STL +# wrappers +DisableStlWrapping() diff --git a/testing/mochitest/ssltunnel/ssltunnel.cpp b/testing/mochitest/ssltunnel/ssltunnel.cpp new file mode 100644 index 0000000000..436c712639 --- /dev/null +++ b/testing/mochitest/ssltunnel/ssltunnel.cpp @@ -0,0 +1,1676 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + * WARNING: DO NOT USE THIS CODE IN PRODUCTION SYSTEMS. It is highly likely to + * be plagued with the usual problems endemic to C (buffer overflows + * and the like). We don't especially care here (but would accept + * patches!) because this is only intended for use in our test + * harnesses in controlled situations where input is guaranteed not to + * be malicious. + */ + +#include "ScopedNSSTypes.h" +#include <assert.h> +#include <stdio.h> +#include <string> +#include <vector> +#include <algorithm> +#include <stdarg.h> +#include "prinit.h" +#include "prerror.h" +#include "prenv.h" +#include "prnetdb.h" +#include "prtpool.h" +#include "nsAlgorithm.h" +#include "nss.h" +#include "keyhi.h" +#include "ssl.h" +#include "sslproto.h" +#include "plhash.h" +#include "mozilla/Sprintf.h" +#include "mozilla/Unused.h" + +using namespace mozilla; +using namespace mozilla::psm; +using std::string; +using std::vector; + +#define IS_DELIM(m, c) ((m)[(c) >> 3] & (1 << ((c)&7))) +#define SET_DELIM(m, c) ((m)[(c) >> 3] |= (1 << ((c)&7))) +#define DELIM_TABLE_SIZE 32 + +// You can set the level of logging by env var SSLTUNNEL_LOG_LEVEL=n, where n +// is 0 through 3. The default is 1, INFO level logging. +enum LogLevel { + LEVEL_DEBUG = 0, + LEVEL_INFO = 1, + LEVEL_ERROR = 2, + LEVEL_SILENT = 3 +} gLogLevel, + gLastLogLevel; + +#define _LOG_OUTPUT(level, func, params) \ + PR_BEGIN_MACRO \ + if (level >= gLogLevel) { \ + gLastLogLevel = level; \ + func params; \ + } \ + PR_END_MACRO + +// The most verbose output +#define LOG_DEBUG(params) _LOG_OUTPUT(LEVEL_DEBUG, printf, params) + +// Top level informative messages +#define LOG_INFO(params) _LOG_OUTPUT(LEVEL_INFO, printf, params) + +// Serious errors that must be logged always until completely gag +#define LOG_ERROR(params) _LOG_OUTPUT(LEVEL_ERROR, eprintf, params) + +// Same as LOG_ERROR, but when logging is set to LEVEL_DEBUG, the message +// will be put to the stdout instead of stderr to keep continuity with other +// LOG_DEBUG message output +#define LOG_ERRORD(params) \ + PR_BEGIN_MACRO \ + if (gLogLevel == LEVEL_DEBUG) \ + _LOG_OUTPUT(LEVEL_ERROR, printf, params); \ + else \ + _LOG_OUTPUT(LEVEL_ERROR, eprintf, params); \ + PR_END_MACRO + +// If there is any output written between LOG_BEGIN_BLOCK() and +// LOG_END_BLOCK() then a new line will be put to the proper output (out/err) +#define LOG_BEGIN_BLOCK() gLastLogLevel = LEVEL_SILENT; + +#define LOG_END_BLOCK() \ + PR_BEGIN_MACRO \ + if (gLastLogLevel == LEVEL_ERROR) LOG_ERROR(("\n")); \ + if (gLastLogLevel < LEVEL_ERROR) _LOG_OUTPUT(gLastLogLevel, printf, ("\n")); \ + PR_END_MACRO + +int eprintf(const char* str, ...) { + va_list ap; + va_start(ap, str); + int result = vfprintf(stderr, str, ap); + va_end(ap); + return result; +} + +// Copied from nsCRT +char* strtok2(char* string, const char* delims, char** newStr) { + PR_ASSERT(string); + + char delimTable[DELIM_TABLE_SIZE]; + uint32_t i; + char* result; + char* str = string; + + for (i = 0; i < DELIM_TABLE_SIZE; i++) delimTable[i] = '\0'; + + for (i = 0; delims[i]; i++) { + SET_DELIM(delimTable, static_cast<uint8_t>(delims[i])); + } + + // skip to beginning + while (*str && IS_DELIM(delimTable, static_cast<uint8_t>(*str))) { + str++; + } + result = str; + + // fix up the end of the token + while (*str) { + if (IS_DELIM(delimTable, static_cast<uint8_t>(*str))) { + *str++ = '\0'; + break; + } + str++; + } + *newStr = str; + + return str == result ? nullptr : result; +} + +enum client_auth_option { caNone = 0, caRequire = 1, caRequest = 2 }; + +// Structs for passing data into jobs on the thread pool +struct server_info_t { + int32_t listen_port; + string cert_nickname; + PLHashTable* host_cert_table; + PLHashTable* host_clientauth_table; + PLHashTable* host_redir_table; + PLHashTable* host_ssl3_table; + PLHashTable* host_tls1_table; + PLHashTable* host_tls11_table; + PLHashTable* host_tls12_table; + PLHashTable* host_tls13_table; + PLHashTable* host_3des_table; + PLHashTable* host_failhandshake_table; +}; + +struct connection_info_t { + PRFileDesc* client_sock; + PRNetAddr client_addr; + server_info_t* server_info; + // the original host in the Host: header for this connection is + // stored here, for proxied connections + string original_host; + // true if no SSL should be used for this connection + bool http_proxy_only; + // true if this connection is for a WebSocket + bool iswebsocket; +}; + +struct server_match_t { + string fullHost; + bool matched; +}; + +const int32_t BUF_SIZE = 16384; +const int32_t BUF_MARGIN = 1024; +const int32_t BUF_TOTAL = BUF_SIZE + BUF_MARGIN; + +struct relayBuffer { + char *buffer, *bufferhead, *buffertail, *bufferend; + + relayBuffer() { + // Leave 1024 bytes more for request line manipulations + bufferhead = buffertail = buffer = new char[BUF_TOTAL]; + bufferend = buffer + BUF_SIZE; + } + + ~relayBuffer() { delete[] buffer; } + + void compact() { + if (buffertail == bufferhead) buffertail = bufferhead = buffer; + } + + bool empty() { return bufferhead == buffertail; } + size_t areafree() { return bufferend - buffertail; } + size_t margin() { return areafree() + BUF_MARGIN; } + size_t present() { return buffertail - bufferhead; } +}; + +// These numbers are multiplied by the number of listening ports (actual +// servers running). According the thread pool implementation there is no +// need to limit the number of threads initially, threads are allocated +// dynamically and stored in a linked list. Initial number of 2 is chosen +// to allocate a thread for socket accept and preallocate one for the first +// connection that is with high probability expected to come. +const uint32_t INITIAL_THREADS = 2; +const uint32_t MAX_THREADS = 100; +const uint32_t DEFAULT_STACKSIZE = (512 * 1024); + +// global data +string nssconfigdir; +vector<server_info_t> servers; +PRNetAddr remote_addr; +PRNetAddr websocket_server; +PRThreadPool* threads = nullptr; +PRLock* shutdown_lock = nullptr; +PRCondVar* shutdown_condvar = nullptr; +// Not really used, unless something fails to start +bool shutdown_server = false; +bool do_http_proxy = false; +bool any_host_spec_config = false; +bool listen_public = false; + +int ClientAuthValueComparator(const void* v1, const void* v2) { + int a = *static_cast<const client_auth_option*>(v1) - + *static_cast<const client_auth_option*>(v2); + if (a == 0) return 0; + if (a > 0) return 1; + // (a < 0) + return -1; +} + +static int match_hostname(PLHashEntry* he, int index, void* arg) { + server_match_t* match = (server_match_t*)arg; + if (match->fullHost.find((char*)he->key) != string::npos) + match->matched = true; + return HT_ENUMERATE_NEXT; +} + +/* + * Signal the main thread that the application should shut down. + */ +void SignalShutdown() { + PR_Lock(shutdown_lock); + PR_NotifyCondVar(shutdown_condvar); + PR_Unlock(shutdown_lock); +} + +// available flags +enum { + USE_SSL3 = 1 << 0, + USE_3DES = 1 << 1, + FAIL_HANDSHAKE = 1 << 2, + USE_TLS1 = 1 << 3, + USE_TLS1_1 = 1 << 4, + USE_TLS1_2 = 1 << 5, + USE_TLS1_3 = 1 << 6 +}; + +bool ReadConnectRequest(server_info_t* server_info, relayBuffer& buffer, + int32_t* result, string& certificate, + client_auth_option* clientauth, string& host, + string& location, int32_t* flags) { + if (buffer.present() < 4) { + LOG_DEBUG( + (" !! only %d bytes present in the buffer", (int)buffer.present())); + return false; + } + if (strncmp(buffer.buffertail - 4, "\r\n\r\n", 4)) { + LOG_ERRORD((" !! request is not tailed with CRLFCRLF but with %x %x %x %x", + *(buffer.buffertail - 4), *(buffer.buffertail - 3), + *(buffer.buffertail - 2), *(buffer.buffertail - 1))); + return false; + } + + LOG_DEBUG((" parsing initial connect request, dump:\n%.*s\n", + (int)buffer.present(), buffer.bufferhead)); + + *result = 400; + + char* token; + char* _caret; + token = strtok2(buffer.bufferhead, " ", &_caret); + if (!token) { + LOG_ERRORD((" no space found")); + return true; + } + if (strcmp(token, "CONNECT")) { + LOG_ERRORD((" not CONNECT request but %s", token)); + return true; + } + + token = strtok2(_caret, " ", &_caret); + void* c = PL_HashTableLookup(server_info->host_cert_table, token); + if (c) certificate = static_cast<char*>(c); + + host = "https://"; + host += token; + + c = PL_HashTableLookup(server_info->host_clientauth_table, token); + if (c) + *clientauth = *static_cast<client_auth_option*>(c); + else + *clientauth = caNone; + + void* redir = PL_HashTableLookup(server_info->host_redir_table, token); + if (redir) location = static_cast<char*>(redir); + + if (PL_HashTableLookup(server_info->host_ssl3_table, token)) { + *flags |= USE_SSL3; + } + + if (PL_HashTableLookup(server_info->host_3des_table, token)) { + *flags |= USE_3DES; + } + + if (PL_HashTableLookup(server_info->host_tls1_table, token)) { + *flags |= USE_TLS1; + } + + if (PL_HashTableLookup(server_info->host_tls11_table, token)) { + *flags |= USE_TLS1_1; + } + + if (PL_HashTableLookup(server_info->host_tls12_table, token)) { + *flags |= USE_TLS1_2; + } + + if (PL_HashTableLookup(server_info->host_tls13_table, token)) { + *flags |= USE_TLS1_3; + } + + if (PL_HashTableLookup(server_info->host_failhandshake_table, token)) { + *flags |= FAIL_HANDSHAKE; + } + + token = strtok2(_caret, "/", &_caret); + if (strcmp(token, "HTTP")) { + LOG_ERRORD((" not tailed with HTTP but with %s", token)); + return true; + } + + *result = (redir) ? 302 : 200; + return true; +} + +bool ConfigureSSLServerSocket(PRFileDesc* socket, server_info_t* si, + const string& certificate, + const client_auth_option clientAuth, + int32_t flags) { + const char* certnick = + certificate.empty() ? si->cert_nickname.c_str() : certificate.c_str(); + + UniqueCERTCertificate cert(PK11_FindCertFromNickname(certnick, nullptr)); + if (!cert) { + LOG_ERROR(("Failed to find cert %s\n", certnick)); + return false; + } + + UniqueSECKEYPrivateKey privKey(PK11_FindKeyByAnyCert(cert.get(), nullptr)); + if (!privKey) { + LOG_ERROR(("Failed to find private key\n")); + return false; + } + + PRFileDesc* ssl_socket = SSL_ImportFD(nullptr, socket); + if (!ssl_socket) { + LOG_ERROR(("Error importing SSL socket\n")); + return false; + } + + if (flags & FAIL_HANDSHAKE) { + // deliberately cause handshake to fail by sending the client a client hello + SSL_ResetHandshake(ssl_socket, false); + return true; + } + + SSLKEAType certKEA = NSS_FindCertKEAType(cert.get()); + if (SSL_ConfigSecureServer(ssl_socket, cert.get(), privKey.get(), certKEA) != + SECSuccess) { + LOG_ERROR(("Error configuring SSL server socket\n")); + return false; + } + + SSL_OptionSet(ssl_socket, SSL_SECURITY, true); + SSL_OptionSet(ssl_socket, SSL_HANDSHAKE_AS_CLIENT, false); + SSL_OptionSet(ssl_socket, SSL_HANDSHAKE_AS_SERVER, true); + SSL_OptionSet(ssl_socket, SSL_ENABLE_SESSION_TICKETS, true); + + if (clientAuth != caNone) { + // If we're requesting or requiring a client certificate, we should + // configure NSS to include the "certificate_authorities" field in the + // certificate request message. That way we can test that gecko properly + // takes note of it. + UniqueCERTCertificate issuer( + CERT_FindCertIssuer(cert.get(), PR_Now(), certUsageAnyCA)); + if (!issuer) { + LOG_DEBUG(("Failed to find issuer for %s\n", certnick)); + return false; + } + UniqueCERTCertList issuerList(CERT_NewCertList()); + if (!issuerList) { + LOG_ERROR(("Failed to allocate new CERTCertList\n")); + return false; + } + if (CERT_AddCertToListTail(issuerList.get(), issuer.get()) != SECSuccess) { + LOG_ERROR(("Failed to add issuer to issuerList\n")); + return false; + } + Unused << issuer.release(); // Ownership transferred to issuerList. + if (SSL_SetTrustAnchors(ssl_socket, issuerList.get()) != SECSuccess) { + LOG_ERROR( + ("Failed to set certificate_authorities list for client " + "authentication\n")); + return false; + } + SSL_OptionSet(ssl_socket, SSL_REQUEST_CERTIFICATE, true); + SSL_OptionSet(ssl_socket, SSL_REQUIRE_CERTIFICATE, clientAuth == caRequire); + } + + SSLVersionRange range = {SSL_LIBRARY_VERSION_TLS_1_3, + SSL_LIBRARY_VERSION_3_0}; + if (flags & USE_SSL3) { + range.min = PR_MIN(range.min, SSL_LIBRARY_VERSION_3_0); + range.max = PR_MAX(range.max, SSL_LIBRARY_VERSION_3_0); + } + if (flags & USE_TLS1) { + range.min = PR_MIN(range.min, SSL_LIBRARY_VERSION_TLS_1_0); + range.max = PR_MAX(range.max, SSL_LIBRARY_VERSION_TLS_1_0); + } + if (flags & USE_TLS1_1) { + range.min = PR_MIN(range.min, SSL_LIBRARY_VERSION_TLS_1_1); + range.max = PR_MAX(range.max, SSL_LIBRARY_VERSION_TLS_1_1); + } + if (flags & USE_TLS1_2) { + range.min = PR_MIN(range.min, SSL_LIBRARY_VERSION_TLS_1_2); + range.max = PR_MAX(range.max, SSL_LIBRARY_VERSION_TLS_1_2); + } + if (flags & USE_TLS1_3) { + range.min = PR_MIN(range.min, SSL_LIBRARY_VERSION_TLS_1_3); + range.max = PR_MAX(range.max, SSL_LIBRARY_VERSION_TLS_1_3); + } + // Set the valid range, if any were specified (if not, skip + // when the default range is invalid, i.e. max > min) + if (range.min <= range.max && + SSL_VersionRangeSet(ssl_socket, &range) != SECSuccess) { + LOG_ERROR(("Error configuring SSL socket version range\n")); + return false; + } + + if (flags & USE_3DES) { + for (uint16_t i = 0; i < SSL_NumImplementedCiphers; ++i) { + uint16_t cipher_id = SSL_ImplementedCiphers[i]; + if (cipher_id == TLS_RSA_WITH_3DES_EDE_CBC_SHA) { + SSL_CipherPrefSet(ssl_socket, cipher_id, true); + } else { + SSL_CipherPrefSet(ssl_socket, cipher_id, false); + } + } + } + + SSL_ResetHandshake(ssl_socket, true); + + return true; +} + +/** + * This function examines the buffer for a Sec-WebSocket-Location: field, + * and if it's present, it replaces the hostname in that field with the + * value in the server's original_host field. This function works + * in the reverse direction as AdjustWebSocketHost(), replacing the real + * hostname of a response with the potentially fake hostname that is expected + * by the browser (e.g., mochi.test). + * + * @return true if the header was adjusted successfully, or not found, false + * if the header is present but the url is not, which should indicate + * that more data needs to be read from the socket + */ +bool AdjustWebSocketLocation(relayBuffer& buffer, connection_info_t* ci) { + assert(buffer.margin()); + buffer.buffertail[1] = '\0'; + + char* wsloc = strstr(buffer.bufferhead, "Sec-WebSocket-Location:"); + if (!wsloc) return true; + // advance pointer to the start of the hostname + wsloc = strstr(wsloc, "ws://"); + if (!wsloc) return false; + wsloc += 5; + // find the end of the hostname + char* wslocend = strchr(wsloc + 1, '/'); + if (!wslocend) return false; + char* crlf = strstr(wsloc, "\r\n"); + if (!crlf) return false; + if (ci->original_host.empty()) return true; + + int diff = ci->original_host.length() - (wslocend - wsloc); + if (diff > 0) assert(size_t(diff) <= buffer.margin()); + memmove(wslocend + diff, wslocend, buffer.buffertail - wsloc - diff); + buffer.buffertail += diff; + + memcpy(wsloc, ci->original_host.c_str(), ci->original_host.length()); + return true; +} + +/** + * This function examines the buffer for a Host: field, and if it's present, + * it replaces the hostname in that field with the hostname in the server's + * remote_addr field. This is needed because proxy requests may be coming + * from mochitest with fake hosts, like mochi.test, and these need to be + * replaced with the host that the destination server is actually running + * on. + */ +bool AdjustWebSocketHost(relayBuffer& buffer, connection_info_t* ci) { + const char HEADER_UPGRADE[] = "Upgrade:"; + const char HEADER_HOST[] = "Host:"; + + PRNetAddr inet_addr = + (websocket_server.inet.port ? websocket_server : remote_addr); + + assert(buffer.margin()); + + // Cannot use strnchr so add a null char at the end. There is always some + // space left because we preserve a margin. + buffer.buffertail[1] = '\0'; + + // Verify this is a WebSocket header. + char* h1 = strstr(buffer.bufferhead, HEADER_UPGRADE); + if (!h1) return false; + h1 += strlen(HEADER_UPGRADE); + h1 += strspn(h1, " \t"); + char* h2 = strstr(h1, "WebSocket\r\n"); + if (!h2) h2 = strstr(h1, "websocket\r\n"); + if (!h2) h2 = strstr(h1, "Websocket\r\n"); + if (!h2) return false; + + char* host = strstr(buffer.bufferhead, HEADER_HOST); + if (!host) return false; + // advance pointer to beginning of hostname + host += strlen(HEADER_HOST); + host += strspn(host, " \t"); + + char* endhost = strstr(host, "\r\n"); + if (!endhost) return false; + + // Save the original host, so we can use it later on responses from the + // server. + ci->original_host.assign(host, endhost - host); + + char newhost[40]; + PR_NetAddrToString(&inet_addr, newhost, sizeof(newhost)); + assert(strlen(newhost) < sizeof(newhost) - 7); + SprintfLiteral(newhost, "%s:%d", newhost, PR_ntohs(inet_addr.inet.port)); + + int diff = strlen(newhost) - (endhost - host); + if (diff > 0) assert(size_t(diff) <= buffer.margin()); + memmove(endhost + diff, endhost, buffer.buffertail - host - diff); + buffer.buffertail += diff; + + memcpy(host, newhost, strlen(newhost)); + return true; +} + +/** + * This function prefixes Request-URI path with a full scheme-host-port + * string. + */ +bool AdjustRequestURI(relayBuffer& buffer, string* host) { + assert(buffer.margin()); + + // Cannot use strnchr so add a null char at the end. There is always some + // space left because we preserve a margin. + buffer.buffertail[1] = '\0'; + LOG_DEBUG((" incoming request to adjust:\n%s\n", buffer.bufferhead)); + + char *token, *path; + path = strchr(buffer.bufferhead, ' ') + 1; + if (!path) return false; + + // If the path doesn't start with a slash don't change it, it is probably '*' + // or a full path already. Return true, we are done with this request + // adjustment. + if (*path != '/') return true; + + token = strchr(path, ' ') + 1; + if (!token) return false; + + if (strncmp(token, "HTTP/", 5)) return false; + + size_t hostlength = host->length(); + assert(hostlength <= buffer.margin()); + + memmove(path + hostlength, path, buffer.buffertail - path); + memcpy(path, host->c_str(), hostlength); + buffer.buffertail += hostlength; + + return true; +} + +bool ConnectSocket(UniquePRFileDesc& fd, const PRNetAddr* addr, + PRIntervalTime timeout) { + PRStatus stat = PR_Connect(fd.get(), addr, timeout); + if (stat != PR_SUCCESS) return false; + + PRSocketOptionData option; + option.option = PR_SockOpt_Nonblocking; + option.value.non_blocking = true; + PR_SetSocketOption(fd.get(), &option); + + return true; +} + +/* + * Handle an incoming client connection. The server thread has already + * accepted the connection, so we just need to connect to the remote + * port and then proxy data back and forth. + * The data parameter is a connection_info_t*, and must be deleted + * by this function. + */ +void HandleConnection(void* data) { + connection_info_t* ci = static_cast<connection_info_t*>(data); + PRIntervalTime connect_timeout = PR_SecondsToInterval(30); + + UniquePRFileDesc other_sock(PR_NewTCPSocket()); + bool client_done = false; + bool client_error = false; + bool connect_accepted = !do_http_proxy; + bool ssl_updated = !do_http_proxy; + bool expect_request_start = do_http_proxy; + string certificateToUse; + string locationHeader; + client_auth_option clientAuth; + string fullHost; + int32_t flags = 0; + + LOG_DEBUG(("SSLTUNNEL(%p)): incoming connection csock(0)=%p, ssock(1)=%p\n", + static_cast<void*>(data), static_cast<void*>(ci->client_sock), + static_cast<void*>(other_sock.get()))); + if (other_sock) { + int32_t numberOfSockets = 1; + + relayBuffer buffers[2]; + + if (!do_http_proxy) { + if (!ConfigureSSLServerSocket(ci->client_sock, ci->server_info, + certificateToUse, caNone, flags)) + client_error = true; + else if (!ConnectSocket(other_sock, &remote_addr, connect_timeout)) + client_error = true; + else + numberOfSockets = 2; + } + + PRPollDesc sockets[2] = {{ci->client_sock, PR_POLL_READ, 0}, + {other_sock.get(), PR_POLL_READ, 0}}; + bool socketErrorState[2] = {false, false}; + + while (!((client_error || client_done) && buffers[0].empty() && + buffers[1].empty())) { + sockets[0].in_flags |= PR_POLL_EXCEPT; + sockets[1].in_flags |= PR_POLL_EXCEPT; + LOG_DEBUG(("SSLTUNNEL(%p)): polling flags csock(0)=%c%c, ssock(1)=%c%c\n", + static_cast<void*>(data), + sockets[0].in_flags & PR_POLL_READ ? 'R' : '-', + sockets[0].in_flags & PR_POLL_WRITE ? 'W' : '-', + sockets[1].in_flags & PR_POLL_READ ? 'R' : '-', + sockets[1].in_flags & PR_POLL_WRITE ? 'W' : '-')); + int32_t pollStatus = + PR_Poll(sockets, numberOfSockets, PR_MillisecondsToInterval(1000)); + if (pollStatus < 0) { + LOG_DEBUG(("SSLTUNNEL(%p)): pollStatus=%d, exiting\n", + static_cast<void*>(data), pollStatus)); + client_error = true; + break; + } + + if (pollStatus == 0) { + // timeout + LOG_DEBUG(("SSLTUNNEL(%p)): poll timeout, looping\n", + static_cast<void*>(data))); + continue; + } + + for (int32_t s = 0; s < numberOfSockets; ++s) { + int32_t s2 = s == 1 ? 0 : 1; + int16_t out_flags = sockets[s].out_flags; + int16_t& in_flags = sockets[s].in_flags; + int16_t& in_flags2 = sockets[s2].in_flags; + sockets[s].out_flags = 0; + + LOG_BEGIN_BLOCK(); + LOG_DEBUG(("SSLTUNNEL(%p)): %csock(%d)=%p out_flags=%d", + static_cast<void*>(data), s == 0 ? 'c' : 's', s, + static_cast<void*>(sockets[s].fd), out_flags)); + if (out_flags & (PR_POLL_EXCEPT | PR_POLL_ERR | PR_POLL_HUP)) { + LOG_DEBUG((" :exception\n")); + client_error = true; + socketErrorState[s] = true; + // We got a fatal error state on the socket. Clear the output buffer + // for this socket to break the main loop, we will never more be able + // to send those data anyway. + buffers[s2].bufferhead = buffers[s2].buffertail = buffers[s2].buffer; + continue; + } // PR_POLL_EXCEPT, PR_POLL_ERR, PR_POLL_HUP handling + + if (out_flags & PR_POLL_READ && !buffers[s].areafree()) { + LOG_DEBUG( + (" no place in read buffer but got read flag, dropping it now!")); + in_flags &= ~PR_POLL_READ; + } + + if (out_flags & PR_POLL_READ && buffers[s].areafree()) { + LOG_DEBUG((" :reading")); + int32_t bytesRead = + PR_Recv(sockets[s].fd, buffers[s].buffertail, + buffers[s].areafree(), 0, PR_INTERVAL_NO_TIMEOUT); + + if (bytesRead == 0) { + LOG_DEBUG((" socket gracefully closed")); + client_done = true; + in_flags &= ~PR_POLL_READ; + } else if (bytesRead < 0) { + if (PR_GetError() != PR_WOULD_BLOCK_ERROR) { + LOG_DEBUG((" error=%d", PR_GetError())); + // We are in error state, indicate that the connection was + // not closed gracefully + client_error = true; + socketErrorState[s] = true; + // Wipe out our send buffer, we cannot send it anyway. + buffers[s2].bufferhead = buffers[s2].buffertail = + buffers[s2].buffer; + } else + LOG_DEBUG((" would block")); + } else { + // If the other socket is in error state (unable to send/receive) + // throw this data away and continue loop + if (socketErrorState[s2]) { + LOG_DEBUG((" have read but other socket is in error state\n")); + continue; + } + + buffers[s].buffertail += bytesRead; + LOG_DEBUG((", read %d bytes", bytesRead)); + + // We have to accept and handle the initial CONNECT request here + int32_t response; + if (!connect_accepted && + ReadConnectRequest(ci->server_info, buffers[s], &response, + certificateToUse, &clientAuth, fullHost, + locationHeader, &flags)) { + // Mark this as a proxy-only connection (no SSL) if the CONNECT + // request didn't come for port 443 or from any of the server's + // cert or clientauth hostnames. + if (fullHost.find(":443") == string::npos) { + server_match_t match; + match.fullHost = fullHost; + match.matched = false; + PL_HashTableEnumerateEntries(ci->server_info->host_cert_table, + match_hostname, &match); + PL_HashTableEnumerateEntries( + ci->server_info->host_clientauth_table, match_hostname, + &match); + PL_HashTableEnumerateEntries(ci->server_info->host_ssl3_table, + match_hostname, &match); + PL_HashTableEnumerateEntries(ci->server_info->host_tls1_table, + match_hostname, &match); + PL_HashTableEnumerateEntries(ci->server_info->host_tls11_table, + match_hostname, &match); + PL_HashTableEnumerateEntries(ci->server_info->host_tls12_table, + match_hostname, &match); + PL_HashTableEnumerateEntries(ci->server_info->host_tls13_table, + match_hostname, &match); + PL_HashTableEnumerateEntries(ci->server_info->host_3des_table, + match_hostname, &match); + PL_HashTableEnumerateEntries( + ci->server_info->host_failhandshake_table, match_hostname, + &match); + ci->http_proxy_only = !match.matched; + } else { + ci->http_proxy_only = false; + } + + // Clean the request as it would be read + buffers[s].bufferhead = buffers[s].buffertail = buffers[s].buffer; + in_flags |= PR_POLL_WRITE; + connect_accepted = true; + + // Store response to the oposite buffer + if (response == 200) { + LOG_DEBUG( + (" accepted CONNECT request, connected to the server, " + "sending OK to the client\n")); + strcpy( + buffers[s2].buffer, + "HTTP/1.1 200 Connected\r\nConnection: keep-alive\r\n\r\n"); + } else if (response == 302) { + LOG_DEBUG( + (" accepted CONNECT request with redirection, " + "sending location and 302 to the client\n")); + client_done = true; + snprintf(buffers[s2].buffer, + buffers[s2].bufferend - buffers[s2].buffer, + "HTTP/1.1 302 Moved\r\n" + "Location: https://%s/\r\n" + "Connection: close\r\n\r\n", + locationHeader.c_str()); + } else { + LOG_ERRORD( + (" could not read the connect request, closing connection " + "with %d", + response)); + client_done = true; + snprintf(buffers[s2].buffer, + buffers[s2].bufferend - buffers[s2].buffer, + "HTTP/1.1 %d ERROR\r\nConnection: close\r\n\r\n", + response); + + break; + } + + buffers[s2].buffertail = + buffers[s2].buffer + strlen(buffers[s2].buffer); + + // Send the response to the client socket + break; + } // end of CONNECT handling + + if (!buffers[s].areafree()) { + // Do not poll for read when the buffer is full + LOG_DEBUG((" no place in our read buffer, stop reading")); + in_flags &= ~PR_POLL_READ; + } + + if (ssl_updated) { + if (s == 0 && expect_request_start) { + if (!strstr(buffers[s].bufferhead, "\r\n\r\n")) { + // We haven't received the complete header yet, so wait. + continue; + } + ci->iswebsocket = AdjustWebSocketHost(buffers[s], ci); + expect_request_start = !( + ci->iswebsocket || AdjustRequestURI(buffers[s], &fullHost)); + PRNetAddr* addr = &remote_addr; + if (ci->iswebsocket && websocket_server.inet.port) + addr = &websocket_server; + if (!ConnectSocket(other_sock, addr, connect_timeout)) { + LOG_ERRORD( + (" could not open connection to the real server\n")); + client_error = true; + break; + } + LOG_DEBUG(("\n connected to remote server\n")); + numberOfSockets = 2; + } else if (s == 1 && ci->iswebsocket) { + if (!AdjustWebSocketLocation(buffers[s], ci)) continue; + } + + in_flags2 |= PR_POLL_WRITE; + LOG_DEBUG((" telling the other socket to write")); + } else + LOG_DEBUG( + (" we have something for the other socket to write, but ssl " + "has not been administered on it")); + } + } // PR_POLL_READ handling + + if (out_flags & PR_POLL_WRITE) { + LOG_DEBUG((" :writing")); + int32_t bytesWrite = + PR_Send(sockets[s].fd, buffers[s2].bufferhead, + buffers[s2].present(), 0, PR_INTERVAL_NO_TIMEOUT); + + if (bytesWrite < 0) { + if (PR_GetError() != PR_WOULD_BLOCK_ERROR) { + LOG_DEBUG((" error=%d", PR_GetError())); + client_error = true; + socketErrorState[s] = true; + // We got a fatal error while writting the buffer. Clear it to + // break the main loop, we will never more be able to send it. + buffers[s2].bufferhead = buffers[s2].buffertail = + buffers[s2].buffer; + } else + LOG_DEBUG((" would block")); + } else { + LOG_DEBUG((", written %d bytes", bytesWrite)); + buffers[s2].buffertail[1] = '\0'; + LOG_DEBUG((" dump:\n%.*s\n", bytesWrite, buffers[s2].bufferhead)); + + buffers[s2].bufferhead += bytesWrite; + if (buffers[s2].present()) { + LOG_DEBUG((" still have to write %d bytes", + (int)buffers[s2].present())); + in_flags |= PR_POLL_WRITE; + } else { + if (!ssl_updated) { + LOG_DEBUG((" proxy response sent to the client")); + // Proxy response has just been writen, update to ssl + ssl_updated = true; + if (ci->http_proxy_only) { + LOG_DEBUG( + (" not updating to SSL based on http_proxy_only for this " + "socket")); + } else if (!ConfigureSSLServerSocket( + ci->client_sock, ci->server_info, + certificateToUse, clientAuth, flags)) { + LOG_ERRORD((" failed to config server socket\n")); + client_error = true; + break; + } else { + LOG_DEBUG((" client socket updated to SSL")); + } + } // sslUpdate + + LOG_DEBUG( + (" dropping our write flag and setting other socket read " + "flag")); + in_flags &= ~PR_POLL_WRITE; + in_flags2 |= PR_POLL_READ; + buffers[s2].compact(); + } + } + } // PR_POLL_WRITE handling + LOG_END_BLOCK(); // end the log + } // for... + } // while, poll + } else + client_error = true; + + LOG_DEBUG(("SSLTUNNEL(%p)): exiting root function for csock=%p, ssock=%p\n", + static_cast<void*>(data), static_cast<void*>(ci->client_sock), + static_cast<void*>(other_sock.get()))); + if (!client_error) PR_Shutdown(ci->client_sock, PR_SHUTDOWN_SEND); + PR_Close(ci->client_sock); + + delete ci; +} + +/* + * Start listening for SSL connections on a specified port, handing + * them off to client threads after accepting the connection. + * The data parameter is a server_info_t*, owned by the calling + * function. + */ +void StartServer(void* data) { + server_info_t* si = static_cast<server_info_t*>(data); + + // TODO: select ciphers? + UniquePRFileDesc listen_socket(PR_NewTCPSocket()); + if (!listen_socket) { + LOG_ERROR(("failed to create socket\n")); + SignalShutdown(); + return; + } + + // In case the socket is still open in the TIME_WAIT state from a previous + // instance of ssltunnel we ask to reuse the port. + PRSocketOptionData socket_option; + socket_option.option = PR_SockOpt_Reuseaddr; + socket_option.value.reuse_addr = true; + PR_SetSocketOption(listen_socket.get(), &socket_option); + + PRNetAddr server_addr; + PRNetAddrValue listen_addr; + if (listen_public) { + listen_addr = PR_IpAddrAny; + } else { + listen_addr = PR_IpAddrLoopback; + } + PR_InitializeNetAddr(listen_addr, si->listen_port, &server_addr); + + if (PR_Bind(listen_socket.get(), &server_addr) != PR_SUCCESS) { + LOG_ERROR(("failed to bind socket on port %d: error %d\n", si->listen_port, + PR_GetError())); + SignalShutdown(); + return; + } + + if (PR_Listen(listen_socket.get(), 1) != PR_SUCCESS) { + LOG_ERROR(("failed to listen on socket\n")); + SignalShutdown(); + return; + } + + LOG_INFO(("Server listening on port %d with cert %s\n", si->listen_port, + si->cert_nickname.c_str())); + + while (!shutdown_server) { + connection_info_t* ci = new connection_info_t(); + ci->server_info = si; + ci->http_proxy_only = do_http_proxy; + // block waiting for connections + ci->client_sock = PR_Accept(listen_socket.get(), &ci->client_addr, + PR_INTERVAL_NO_TIMEOUT); + + PRSocketOptionData option; + option.option = PR_SockOpt_Nonblocking; + option.value.non_blocking = true; + PR_SetSocketOption(ci->client_sock, &option); + + if (ci->client_sock) + // Not actually using this PRJob*... + // PRJob* job = + PR_QueueJob(threads, HandleConnection, ci, true); + else + delete ci; + } +} + +// bogus password func, just don't use passwords. :-P +char* password_func(PK11SlotInfo* slot, PRBool retry, void* arg) { + if (retry) return nullptr; + + return PL_strdup(""); +} + +server_info_t* findServerInfo(int portnumber) { + for (auto& server : servers) { + if (server.listen_port == portnumber) return &server; + } + + return nullptr; +} + +PLHashTable* get_ssl3_table(server_info_t* server) { + return server->host_ssl3_table; +} + +PLHashTable* get_tls1_table(server_info_t* server) { + return server->host_tls1_table; +} + +PLHashTable* get_tls11_table(server_info_t* server) { + return server->host_tls11_table; +} + +PLHashTable* get_tls12_table(server_info_t* server) { + return server->host_tls12_table; +} + +PLHashTable* get_tls13_table(server_info_t* server) { + return server->host_tls13_table; +} + +PLHashTable* get_3des_table(server_info_t* server) { + return server->host_3des_table; +} + +PLHashTable* get_failhandshake_table(server_info_t* server) { + return server->host_failhandshake_table; +} + +int parseWeakCryptoConfig(char* const& keyword, char*& _caret, + PLHashTable* (*get_table)(server_info_t*)) { + char* hostname = strtok2(_caret, ":", &_caret); + char* hostportstring = strtok2(_caret, ":", &_caret); + char* serverportstring = strtok2(_caret, "\n", &_caret); + + int port = atoi(serverportstring); + if (port <= 0) { + LOG_ERROR(("Invalid port specified: %s\n", serverportstring)); + return 1; + } + + if (server_info_t* existingServer = findServerInfo(port)) { + any_host_spec_config = true; + + char* hostname_copy = + new char[strlen(hostname) + strlen(hostportstring) + 2]; + if (!hostname_copy) { + LOG_ERROR(("Out of memory")); + return 1; + } + + strcpy(hostname_copy, hostname); + strcat(hostname_copy, ":"); + strcat(hostname_copy, hostportstring); + + PLHashEntry* entry = + PL_HashTableAdd(get_table(existingServer), hostname_copy, keyword); + if (!entry) { + LOG_ERROR(("Out of memory")); + return 1; + } + } else { + LOG_ERROR( + ("Server on port %d for redirhost option is not defined, use 'listen' " + "option first", + port)); + return 1; + } + + return 0; +} + +int processConfigLine(char* configLine) { + if (*configLine == 0 || *configLine == '#') return 0; + + char* _caret; + char* keyword = strtok2(configLine, ":", &_caret); + // Configure usage of http/ssl tunneling proxy behavior + if (!strcmp(keyword, "httpproxy")) { + char* value = strtok2(_caret, ":", &_caret); + if (!strcmp(value, "1")) do_http_proxy = true; + + return 0; + } + + if (!strcmp(keyword, "websocketserver")) { + char* ipstring = strtok2(_caret, ":", &_caret); + if (PR_StringToNetAddr(ipstring, &websocket_server) != PR_SUCCESS) { + LOG_ERROR(("Invalid IP address in proxy config: %s\n", ipstring)); + return 1; + } + char* remoteport = strtok2(_caret, ":", &_caret); + int port = atoi(remoteport); + if (port <= 0) { + LOG_ERROR(("Invalid remote port in proxy config: %s\n", remoteport)); + return 1; + } + websocket_server.inet.port = PR_htons(port); + return 0; + } + + // Configure the forward address of the target server + if (!strcmp(keyword, "forward")) { + char* ipstring = strtok2(_caret, ":", &_caret); + if (PR_StringToNetAddr(ipstring, &remote_addr) != PR_SUCCESS) { + LOG_ERROR(("Invalid remote IP address: %s\n", ipstring)); + return 1; + } + char* serverportstring = strtok2(_caret, ":", &_caret); + int port = atoi(serverportstring); + if (port <= 0) { + LOG_ERROR(("Invalid remote port: %s\n", serverportstring)); + return 1; + } + remote_addr.inet.port = PR_htons(port); + + return 0; + } + + // Configure all listen sockets and port+certificate bindings. + // Listen on the public address if "*" was specified as the listen + // address or listen on the loopback address if "127.0.0.1" was + // specified. Using loopback will prevent users getting errors from + // their firewalls about ssltunnel needing permission. A public + // address is required when proxying ssl traffic from a physical or + // emulated Android device since it has a different ip address from + // the host. + if (!strcmp(keyword, "listen")) { + char* hostname = strtok2(_caret, ":", &_caret); + char* hostportstring = nullptr; + if (!strcmp(hostname, "*")) { + listen_public = true; + } else if (strcmp(hostname, "127.0.0.1")) { + any_host_spec_config = true; + hostportstring = strtok2(_caret, ":", &_caret); + } + + char* serverportstring = strtok2(_caret, ":", &_caret); + char* certnick = strtok2(_caret, ":", &_caret); + + int port = atoi(serverportstring); + if (port <= 0) { + LOG_ERROR(("Invalid port specified: %s\n", serverportstring)); + return 1; + } + + if (server_info_t* existingServer = findServerInfo(port)) { + if (!hostportstring) { + LOG_ERROR( + ("Null hostportstring specified for hostname %s\n", hostname)); + return 1; + } + char* certnick_copy = new char[strlen(certnick) + 1]; + char* hostname_copy = + new char[strlen(hostname) + strlen(hostportstring) + 2]; + + strcpy(hostname_copy, hostname); + strcat(hostname_copy, ":"); + strcat(hostname_copy, hostportstring); + strcpy(certnick_copy, certnick); + + PLHashEntry* entry = PL_HashTableAdd(existingServer->host_cert_table, + hostname_copy, certnick_copy); + if (!entry) { + LOG_ERROR(("Out of memory")); + return 1; + } + } else { + server_info_t server; + server.cert_nickname = certnick; + server.listen_port = port; + server.host_cert_table = + PL_NewHashTable(0, PL_HashString, PL_CompareStrings, + PL_CompareStrings, nullptr, nullptr); + if (!server.host_cert_table) { + LOG_ERROR(("Internal, could not create hash table\n")); + return 1; + } + server.host_clientauth_table = + PL_NewHashTable(0, PL_HashString, PL_CompareStrings, + ClientAuthValueComparator, nullptr, nullptr); + if (!server.host_clientauth_table) { + LOG_ERROR(("Internal, could not create hash table\n")); + return 1; + } + server.host_redir_table = + PL_NewHashTable(0, PL_HashString, PL_CompareStrings, + PL_CompareStrings, nullptr, nullptr); + if (!server.host_redir_table) { + LOG_ERROR(("Internal, could not create hash table\n")); + return 1; + } + + server.host_ssl3_table = + PL_NewHashTable(0, PL_HashString, PL_CompareStrings, + PL_CompareStrings, nullptr, nullptr); + + if (!server.host_ssl3_table) { + LOG_ERROR(("Internal, could not create hash table\n")); + return 1; + } + + server.host_tls1_table = + PL_NewHashTable(0, PL_HashString, PL_CompareStrings, + PL_CompareStrings, nullptr, nullptr); + + if (!server.host_tls1_table) { + LOG_ERROR(("Internal, could not create hash table\n")); + return 1; + } + + server.host_tls11_table = + PL_NewHashTable(0, PL_HashString, PL_CompareStrings, + PL_CompareStrings, nullptr, nullptr); + + if (!server.host_tls11_table) { + LOG_ERROR(("Internal, could not create hash table\n")); + return 1; + } + + server.host_tls12_table = + PL_NewHashTable(0, PL_HashString, PL_CompareStrings, + PL_CompareStrings, nullptr, nullptr); + + if (!server.host_tls12_table) { + LOG_ERROR(("Internal, could not create hash table\n")); + return 1; + } + + server.host_tls13_table = + PL_NewHashTable(0, PL_HashString, PL_CompareStrings, + PL_CompareStrings, nullptr, nullptr); + + if (!server.host_tls13_table) { + LOG_ERROR(("Internal, could not create hash table\n")); + return 1; + } + + server.host_3des_table = + PL_NewHashTable(0, PL_HashString, PL_CompareStrings, + PL_CompareStrings, nullptr, nullptr); + ; + if (!server.host_3des_table) { + LOG_ERROR(("Internal, could not create hash table\n")); + return 1; + } + + server.host_failhandshake_table = + PL_NewHashTable(0, PL_HashString, PL_CompareStrings, + PL_CompareStrings, nullptr, nullptr); + ; + if (!server.host_failhandshake_table) { + LOG_ERROR(("Internal, could not create hash table\n")); + return 1; + } + + servers.push_back(server); + } + + return 0; + } + + if (!strcmp(keyword, "clientauth")) { + char* hostname = strtok2(_caret, ":", &_caret); + char* hostportstring = strtok2(_caret, ":", &_caret); + char* serverportstring = strtok2(_caret, ":", &_caret); + + int port = atoi(serverportstring); + if (port <= 0) { + LOG_ERROR(("Invalid port specified: %s\n", serverportstring)); + return 1; + } + + if (server_info_t* existingServer = findServerInfo(port)) { + char* authoptionstring = strtok2(_caret, ":", &_caret); + client_auth_option* authoption = new client_auth_option; + if (!authoption) { + LOG_ERROR(("Out of memory")); + return 1; + } + + if (!strcmp(authoptionstring, "require")) + *authoption = caRequire; + else if (!strcmp(authoptionstring, "request")) + *authoption = caRequest; + else if (!strcmp(authoptionstring, "none")) + *authoption = caNone; + else { + LOG_ERROR( + ("Incorrect client auth option modifier for host '%s'", hostname)); + delete authoption; + return 1; + } + + any_host_spec_config = true; + + char* hostname_copy = + new char[strlen(hostname) + strlen(hostportstring) + 2]; + if (!hostname_copy) { + LOG_ERROR(("Out of memory")); + delete authoption; + return 1; + } + + strcpy(hostname_copy, hostname); + strcat(hostname_copy, ":"); + strcat(hostname_copy, hostportstring); + + PLHashEntry* entry = PL_HashTableAdd( + existingServer->host_clientauth_table, hostname_copy, authoption); + if (!entry) { + LOG_ERROR(("Out of memory")); + delete authoption; + return 1; + } + } else { + LOG_ERROR( + ("Server on port %d for client authentication option is not defined, " + "use 'listen' option first", + port)); + return 1; + } + + return 0; + } + + if (!strcmp(keyword, "redirhost")) { + char* hostname = strtok2(_caret, ":", &_caret); + char* hostportstring = strtok2(_caret, ":", &_caret); + char* serverportstring = strtok2(_caret, ":", &_caret); + + int port = atoi(serverportstring); + if (port <= 0) { + LOG_ERROR(("Invalid port specified: %s\n", serverportstring)); + return 1; + } + + if (server_info_t* existingServer = findServerInfo(port)) { + char* redirhoststring = strtok2(_caret, ":", &_caret); + + any_host_spec_config = true; + + char* hostname_copy = + new char[strlen(hostname) + strlen(hostportstring) + 2]; + if (!hostname_copy) { + LOG_ERROR(("Out of memory")); + return 1; + } + + strcpy(hostname_copy, hostname); + strcat(hostname_copy, ":"); + strcat(hostname_copy, hostportstring); + + char* redir_copy = new char[strlen(redirhoststring) + 1]; + strcpy(redir_copy, redirhoststring); + PLHashEntry* entry = PL_HashTableAdd(existingServer->host_redir_table, + hostname_copy, redir_copy); + if (!entry) { + LOG_ERROR(("Out of memory")); + delete[] hostname_copy; + delete[] redir_copy; + return 1; + } + } else { + LOG_ERROR( + ("Server on port %d for redirhost option is not defined, use " + "'listen' option first", + port)); + return 1; + } + + return 0; + } + + if (!strcmp(keyword, "ssl3")) { + return parseWeakCryptoConfig(keyword, _caret, get_ssl3_table); + } + if (!strcmp(keyword, "tls1")) { + return parseWeakCryptoConfig(keyword, _caret, get_tls1_table); + } + if (!strcmp(keyword, "tls1_1")) { + return parseWeakCryptoConfig(keyword, _caret, get_tls11_table); + } + if (!strcmp(keyword, "tls1_2")) { + return parseWeakCryptoConfig(keyword, _caret, get_tls12_table); + } + if (!strcmp(keyword, "tls1_3")) { + return parseWeakCryptoConfig(keyword, _caret, get_tls13_table); + } + + if (!strcmp(keyword, "3des")) { + return parseWeakCryptoConfig(keyword, _caret, get_3des_table); + } + + if (!strcmp(keyword, "failHandshake")) { + return parseWeakCryptoConfig(keyword, _caret, get_failhandshake_table); + } + + // Configure the NSS certificate database directory + if (!strcmp(keyword, "certdbdir")) { + nssconfigdir = strtok2(_caret, "\n", &_caret); + return 0; + } + + LOG_ERROR(("Error: keyword \"%s\" unexpected\n", keyword)); + return 1; +} + +int parseConfigFile(const char* filePath) { + FILE* f = fopen(filePath, "r"); + if (!f) return 1; + + char buffer[1024], *b = buffer; + while (!feof(f)) { + char c; + + if (fscanf(f, "%c", &c) != 1) { + break; + } + + switch (c) { + case '\n': + *b++ = 0; + if (processConfigLine(buffer)) { + fclose(f); + return 1; + } + b = buffer; + continue; + + case '\r': + continue; + + default: + *b++ = c; + } + } + + fclose(f); + + // Check mandatory items + if (nssconfigdir.empty()) { + LOG_ERROR( + ("Error: missing path to NSS certification database\n,use " + "certdbdir:<path> in the config file\n")); + return 1; + } + + if (any_host_spec_config && !do_http_proxy) { + LOG_ERROR( + ("Warning: any host-specific configurations are ignored, add " + "httpproxy:1 to allow them\n")); + } + + return 0; +} + +int freeHostCertHashItems(PLHashEntry* he, int i, void* arg) { + delete[](char*) he->key; + delete[](char*) he->value; + return HT_ENUMERATE_REMOVE; +} + +int freeHostRedirHashItems(PLHashEntry* he, int i, void* arg) { + delete[](char*) he->key; + delete[](char*) he->value; + return HT_ENUMERATE_REMOVE; +} + +int freeClientAuthHashItems(PLHashEntry* he, int i, void* arg) { + delete[](char*) he->key; + delete (client_auth_option*)he->value; + return HT_ENUMERATE_REMOVE; +} + +int freeSSL3HashItems(PLHashEntry* he, int i, void* arg) { + delete[](char*) he->key; + return HT_ENUMERATE_REMOVE; +} + +int freeTLSHashItems(PLHashEntry* he, int i, void* arg) { + delete[](char*) he->key; + return HT_ENUMERATE_REMOVE; +} + +int free3DESHashItems(PLHashEntry* he, int i, void* arg) { + delete[](char*) he->key; + return HT_ENUMERATE_REMOVE; +} + +int main(int argc, char** argv) { + const char* configFilePath; + + const char* logLevelEnv = PR_GetEnv("SSLTUNNEL_LOG_LEVEL"); + gLogLevel = logLevelEnv ? (LogLevel)atoi(logLevelEnv) : LEVEL_INFO; + + if (argc == 1) + configFilePath = "ssltunnel.cfg"; + else + configFilePath = argv[1]; + + memset(&websocket_server, 0, sizeof(PRNetAddr)); + + if (parseConfigFile(configFilePath)) { + LOG_ERROR(( + "Error: config file \"%s\" missing or formating incorrect\n" + "Specify path to the config file as parameter to ssltunnel or \n" + "create ssltunnel.cfg in the working directory.\n\n" + "Example format of the config file:\n\n" + " # Enable http/ssl tunneling proxy-like behavior.\n" + " # If not specified ssltunnel simply does direct forward.\n" + " httpproxy:1\n\n" + " # Specify path to the certification database used.\n" + " certdbdir:/path/to/certdb\n\n" + " # Forward/proxy all requests in raw to 127.0.0.1:8888.\n" + " forward:127.0.0.1:8888\n\n" + " # Accept connections on port 4443 or 5678 resp. and " + "authenticate\n" + " # to any host ('*') using the 'server cert' or 'server cert 2' " + "resp.\n" + " listen:*:4443:server cert\n" + " listen:*:5678:server cert 2\n\n" + " # Accept connections on port 4443 and authenticate using\n" + " # 'a different cert' when target host is 'my.host.name:443'.\n" + " # This only works in httpproxy mode and has higher priority\n" + " # than the previous option.\n" + " listen:my.host.name:443:4443:a different cert\n\n" + " # To make a specific host require or just request a client " + "certificate\n" + " # to authenticate use the following options. This can only be " + "used\n" + " # in httpproxy mode and only after the 'listen' option has " + "been\n" + " # specified. You also have to specify the tunnel listen port.\n" + " clientauth:requesting-client-cert.host.com:443:4443:request\n" + " clientauth:requiring-client-cert.host.com:443:4443:require\n" + " # Proxy WebSocket traffic to the server at 127.0.0.1:9999,\n" + " # instead of the server specified in the 'forward' option.\n" + " websocketserver:127.0.0.1:9999\n", + configFilePath)); + return 1; + } + + // create a thread pool to handle connections + threads = + PR_CreateThreadPool(INITIAL_THREADS * servers.size(), + MAX_THREADS * servers.size(), DEFAULT_STACKSIZE); + if (!threads) { + LOG_ERROR(("Failed to create thread pool\n")); + return 1; + } + + shutdown_lock = PR_NewLock(); + if (!shutdown_lock) { + LOG_ERROR(("Failed to create lock\n")); + PR_ShutdownThreadPool(threads); + return 1; + } + shutdown_condvar = PR_NewCondVar(shutdown_lock); + if (!shutdown_condvar) { + LOG_ERROR(("Failed to create condvar\n")); + PR_ShutdownThreadPool(threads); + PR_DestroyLock(shutdown_lock); + return 1; + } + + PK11_SetPasswordFunc(password_func); + + // Initialize NSS + if (NSS_Init(nssconfigdir.c_str()) != SECSuccess) { + int32_t errorlen = PR_GetErrorTextLength(); + if (errorlen) { + auto err = mozilla::MakeUnique<char[]>(errorlen + 1); + PR_GetErrorText(err.get()); + LOG_ERROR(("Failed to init NSS: %s", err.get())); + } else { + LOG_ERROR(("Failed to init NSS: Cannot get error from NSPR.")); + } + PR_ShutdownThreadPool(threads); + PR_DestroyCondVar(shutdown_condvar); + PR_DestroyLock(shutdown_lock); + return 1; + } + + if (NSS_SetDomesticPolicy() != SECSuccess) { + LOG_ERROR(("NSS_SetDomesticPolicy failed\n")); + PR_ShutdownThreadPool(threads); + PR_DestroyCondVar(shutdown_condvar); + PR_DestroyLock(shutdown_lock); + NSS_Shutdown(); + return 1; + } + + // these values should make NSS use the defaults + if (SSL_ConfigServerSessionIDCache(0, 0, 0, nullptr) != SECSuccess) { + LOG_ERROR(("SSL_ConfigServerSessionIDCache failed\n")); + PR_ShutdownThreadPool(threads); + PR_DestroyCondVar(shutdown_condvar); + PR_DestroyLock(shutdown_lock); + NSS_Shutdown(); + return 1; + } + + for (auto& server : servers) { + // Not actually using this PRJob*... + // PRJob* server_job = + PR_QueueJob(threads, StartServer, &server, true); + } + // now wait for someone to tell us to quit + PR_Lock(shutdown_lock); + PR_WaitCondVar(shutdown_condvar, PR_INTERVAL_NO_TIMEOUT); + PR_Unlock(shutdown_lock); + shutdown_server = true; + LOG_INFO(("Shutting down...\n")); + // cleanup + PR_ShutdownThreadPool(threads); + PR_JoinThreadPool(threads); + PR_DestroyCondVar(shutdown_condvar); + PR_DestroyLock(shutdown_lock); + if (NSS_Shutdown() == SECFailure) { + LOG_DEBUG(("Leaked NSS objects!\n")); + } + + for (auto& server : servers) { + PL_HashTableEnumerateEntries(server.host_cert_table, freeHostCertHashItems, + nullptr); + PL_HashTableEnumerateEntries(server.host_clientauth_table, + freeClientAuthHashItems, nullptr); + PL_HashTableEnumerateEntries(server.host_redir_table, + freeHostRedirHashItems, nullptr); + PL_HashTableEnumerateEntries(server.host_ssl3_table, freeSSL3HashItems, + nullptr); + PL_HashTableEnumerateEntries(server.host_tls1_table, freeTLSHashItems, + nullptr); + PL_HashTableEnumerateEntries(server.host_tls11_table, freeTLSHashItems, + nullptr); + PL_HashTableEnumerateEntries(server.host_tls12_table, freeTLSHashItems, + nullptr); + PL_HashTableEnumerateEntries(server.host_tls13_table, freeTLSHashItems, + nullptr); + PL_HashTableEnumerateEntries(server.host_3des_table, free3DESHashItems, + nullptr); + PL_HashTableEnumerateEntries(server.host_failhandshake_table, + free3DESHashItems, nullptr); + PL_HashTableDestroy(server.host_cert_table); + PL_HashTableDestroy(server.host_clientauth_table); + PL_HashTableDestroy(server.host_redir_table); + PL_HashTableDestroy(server.host_ssl3_table); + PL_HashTableDestroy(server.host_tls1_table); + PL_HashTableDestroy(server.host_tls11_table); + PL_HashTableDestroy(server.host_tls12_table); + PL_HashTableDestroy(server.host_tls13_table); + PL_HashTableDestroy(server.host_3des_table); + PL_HashTableDestroy(server.host_failhandshake_table); + } + + PR_Cleanup(); + return 0; +} diff --git a/testing/mochitest/start_desktop.js b/testing/mochitest/start_desktop.js new file mode 100644 index 0000000000..93d59d935b --- /dev/null +++ b/testing/mochitest/start_desktop.js @@ -0,0 +1,22 @@ +/* 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/. */ + +// Defined by Marionette. +/* global __webDriverArguments */ +const flavor = __webDriverArguments[0].flavor; +const url = __webDriverArguments[0].testUrl; + +// eslint-disable-next-line mozilla/use-services +let wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService( + Ci.nsIWindowMediator +); +let win = wm.getMostRecentWindow("navigator:browser"); +if (!win) { + win = wm.getMostRecentWindow("mail:3pane"); +} + +// mochikit's bootstrap.js has set up a listener for this event. It's +// used so bootstrap.js knows which flavor and url to load. +let ev = new CustomEvent("mochitest-load", { detail: [flavor, url] }); +win.dispatchEvent(ev); diff --git a/testing/mochitest/static/browser.template.txt b/testing/mochitest/static/browser.template.txt new file mode 100644 index 0000000000..fe4f2080b2 --- /dev/null +++ b/testing/mochitest/static/browser.template.txt @@ -0,0 +1,8 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_TODO() { + ok(true, "TODO: implement the test"); +}); diff --git a/testing/mochitest/static/chromehtml.template.txt b/testing/mochitest/static/chromehtml.template.txt new file mode 100644 index 0000000000..2ad7a6df52 --- /dev/null +++ b/testing/mochitest/static/chromehtml.template.txt @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title><!-- TODO: insert title here --></title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script> + add_task(async function test_TODO() { + ok(true, "TODO: implement the test"); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/testing/mochitest/static/chromexhtml.template.txt b/testing/mochitest/static/chromexhtml.template.txt new file mode 100644 index 0000000000..9cd40ef308 --- /dev/null +++ b/testing/mochitest/static/chromexhtml.template.txt @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta charset="utf-8" /> + <title><!-- TODO: insert title here --></title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script><![CDATA[ + add_task(async function test_TODO() { + ok(true, "TODO: implement the test"); + }); + ]]></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/testing/mochitest/static/chromexul.template.txt b/testing/mochitest/static/chromexul.template.txt new file mode 100644 index 0000000000..6dbb611054 --- /dev/null +++ b/testing/mochitest/static/chromexul.template.txt @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<window title="TODO: Insert title here" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script><![CDATA[ + add_task(async function test_TODO() { + ok(true, "TODO: implement the test"); + }); + ]]></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + </body> +</window> diff --git a/testing/mochitest/static/harness.css b/testing/mochitest/static/harness.css new file mode 100644 index 0000000000..544a7dc954 --- /dev/null +++ b/testing/mochitest/static/harness.css @@ -0,0 +1,126 @@ +body { + margin: 0; + padding: 0; + font-family: Verdana, Helvetica, Arial, sans-serif; + font-size: 11px; + background-color: #fff; +} + +#xulharness { + position: fixed; + top: 30px; + bottom: 0; + right: 0px; + left: 0px; + overflow:auto; +} + +th, td { + font-family: Verdana, Helvetica, Arial, sans-serif; + font-size: 11px; + padding-left: .2em; + padding-right: .2em; + text-align: left; + height: 15px; + margin: 0; +} + +li, li.test, li.dir { + padding: 0; + line-height: 15px; +} + +ul { + list-style: none; + margin: 0; + margin-left: 1em; + padding: 0; + border: none; +} + +ul.top { + padding: 0; + padding-left: 1em; +} + +table#test-table { + background: #f6f6f6; + margin-left: 1em; + padding: 0; +} + +div.container { + margin: 1em; +} + +a#runtests, a { + color: #3333cc; +} + +li.non-test a { + color: #999999; +} + +small a { + color: #000; +} + +.clear { clear: both;} +.invisible { display: none;} + +div.status { + min-height: 170px; + width: 100%; + border: 1px solid #666; +} +div.frameholder { + min-height: 170px; + min-width: 500px; + background-color: #ffffff; +} + +div#current-test { + margin-top: 1em; + margin-bottom: 1em; +} + +#indicator { + color: white; + background-color: green; + padding: .5em; + margin: 0; +} + +#pass, #fail { + margin: 0; + padding: .5em; +} + +#testframe { + width: 500px; + height: 300px; +} + + +body[singletest=true] table, +body[singletest=true] h2, +body[singletest=true] p, +body[singletest=true] br, +body[singletest=true] .clear, +body[singletest=true] .toggle, +body[singletest=true] #current-test, +body[singletest=true] .status { + display: none; +} + + +body[singletest=true], +body[singletest=true] .container, +body[singletest=true] .frameholder, +body[singletest=true] #testframe { + display: flex; + flex: 1 1 auto; + height: 100%; + box-sizing: border-box; + margin: 0; +} diff --git a/testing/mochitest/static/plainhtml.template.txt b/testing/mochitest/static/plainhtml.template.txt new file mode 100644 index 0000000000..9268645fa1 --- /dev/null +++ b/testing/mochitest/static/plainhtml.template.txt @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title><!-- TODO: insert title here --></title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + ok(true, "TODO: implement the test"); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/testing/mochitest/static/plainxhtml.template.txt b/testing/mochitest/static/plainxhtml.template.txt new file mode 100644 index 0000000000..f04bdd5531 --- /dev/null +++ b/testing/mochitest/static/plainxhtml.template.txt @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta charset="utf-8" /> + <title><!-- TODO: insert title here --></title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script><![CDATA[ + ok(true, "TODO: implement the test"); + ]]></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/testing/mochitest/static/plainxul.template.txt b/testing/mochitest/static/plainxul.template.txt new file mode 100644 index 0000000000..c7fc19d46b --- /dev/null +++ b/testing/mochitest/static/plainxul.template.txt @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin"?> +<?xml-stylesheet href="/tests/SimpleTest/test.css"?> +<window title="TODO: Insert title here" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script><![CDATA[ + ok(true, "TODO: implement the test"); + ]]></script> + <body xmlns="http://www.w3.org/1999/xhtml"> + </body> +</window> diff --git a/testing/mochitest/tests/Harness_sanity/ImportTesting.jsm b/testing/mochitest/tests/Harness_sanity/ImportTesting.jsm new file mode 100644 index 0000000000..34c9652256 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/ImportTesting.jsm @@ -0,0 +1,3 @@ +var EXPORTED_SYMBOLS = []; + +// Empty module for testing via SpecialPowers.importInMainProcess. diff --git a/testing/mochitest/tests/Harness_sanity/SpecialPowersLoadChromeScript.js b/testing/mochitest/tests/Harness_sanity/SpecialPowersLoadChromeScript.js new file mode 100644 index 0000000000..e46375159f --- /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/importtesting_chromescript.js b/testing/mochitest/tests/Harness_sanity/importtesting_chromescript.js new file mode 100644 index 0000000000..a6121bff5d --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/importtesting_chromescript.js @@ -0,0 +1,5 @@ +/* eslint-env mozilla/chrome-script */ + +addMessageListener("ImportTesting:IsModuleLoaded", function(msg) { + sendAsyncMessage("ImportTesting:IsModuleLoadedReply", Cu.isModuleLoaded(msg)); +}); diff --git a/testing/mochitest/tests/Harness_sanity/mochitest.ini b/testing/mochitest/tests/Harness_sanity/mochitest.ini new file mode 100644 index 0000000000..710c4ccf64 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/mochitest.ini @@ -0,0 +1,47 @@ +[DEFAULT] +[test_TestsRunningAfterSimpleTestFinish.html] +skip-if = true #depends on fix for bug 1048446 +[test_createFiles.html] +[test_importInMainProcess.html] +skip-if = verify +support-files = importtesting_chromescript.js +[test_sanity.html] +[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_sanityWindowSnapshot.html] +[test_SpecialPowersExtension.html] +[test_SpecialPowersExtension2.html] +support-files = file_SpecialPowersFrame1.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_SimpletestGetTestFileURL.html] +[test_SpecialPowersLoadChromeScript.html] +support-files = SpecialPowersLoadChromeScript.js +[test_SpecialPowersLoadChromeScript_function.html] +[test_SpecialPowersLoadPrivilegedScript.html] +[test_bug649012.html] +[test_sanity_cleanup.html] +[test_sanity_cleanup2.html] +[test_sanityEventUtils.html] +skip-if = + verify && (os == 'win') # bug 688052 + fission && xorigin # Bug 1716411 - New fission platform triage +[test_sanitySimpletest.html] +[test_sanity_manifest.html] +fail-if = true +[test_sanity_manifest_pf.html] +fail-if = true +[test_sanity_waitForCondition.html] +[test_getweakmapkeys.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..11033d2086 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.html @@ -0,0 +1,126 @@ +<!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. + */ + +/* eslint-disable prettier/prettier */ +/* 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 ==)"], + ]; + + // 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!."); + }); + }, + "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, [], () => { + Assert.equal(1, 2, "Thing"); + Assert.equal(1, 1, "Hmm"); + Assert.ok(true, "Yay."); + Assert.ok(false, "Boo!."); + }); + }); + }, + "SpecialPowers.spawnChrome": () => { + return SpecialPowers.spawnChrome([], async () => { + Assert.equal(1, 2, "Thing"); + Assert.equal(1, 1, "Hmm"); + Assert.ok(true, "Yay."); + Assert.ok(false, "Boo!."); + }); + }, + "SpecialPowers.loadChromeScript": async () => { + let script = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + this.addMessageListener("ping", () => "pong"); + + Assert.equal(1, 2, "Thing"); + Assert.equal(1, 1, "Hmm"); + Assert.ok(true, "Yay."); + Assert.ok(false, "Boo!."); + }); + + 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"); + } +}); +</script> +</body> +</html> 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..851b919561 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSpawn.html @@ -0,0 +1,70 @@ +<!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> +/* eslint-disable prettier/prettier */ +/* 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 = 59; // 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_SpecialPowersSpawnChrome.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSpawnChrome.html new file mode 100644 index 0000000000..0281a251a7 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSpawnChrome.html @@ -0,0 +1,34 @@ +<!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> +/* eslint-disable prettier/prettier */ + + 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_importInMainProcess.html b/testing/mochitest/tests/Harness_sanity/test_importInMainProcess.html new file mode 100644 index 0000000000..10e5aa6526 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_importInMainProcess.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers.importInMainProcess</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(); + + (async () => { + var failed = false; + try { + await SpecialPowers.importInMainProcess("invalid file for import"); + } catch (e) { + ok(e.toString().indexOf("NS_ERROR_MALFORMED_URI") > -1, "Exception should be for a malformed URI"); + failed = true; + } + ok(failed, "An invalid import should throw"); + + const testingResource = "resource://testing-common/ImportTesting.jsm"; + var script = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('importtesting_chromescript.js')); + + script.addMessageListener("ImportTesting:IsModuleLoadedReply", handleFirstReply); + script.sendAsyncMessage("ImportTesting:IsModuleLoaded", testingResource); + + async function handleFirstReply(aMsg) { + ok(!aMsg, "ImportTesting.jsm shouldn't be loaded before we import it"); + + try { + await SpecialPowers.importInMainProcess(testingResource); + } catch (e) { + ok(false, "Unexpected exception when importing a valid resource: " + e.toString()); + } + + script.removeMessageListener("ImportTesting:IsModuleLoadedReply", handleFirstReply); + script.addMessageListener("ImportTesting:IsModuleLoadedReply", handleSecondReply); + script.sendAsyncMessage("ImportTesting:IsModuleLoaded", testingResource); + } + + function handleSecondReply(aMsg) { + script.removeMessageListener("ImportTesting:IsModuleLoadedReply", handleSecondReply); + + ok(aMsg, "ImportTesting.jsm should be loaded after we import it"); + + SimpleTest.finish(); + } + })(); + + </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..f2e0b0bda6 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanityEventUtils.html @@ -0,0 +1,692 @@ +<!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(); + $("testMouseEvent").addEventListener("mousedown", listener); + $("testMouseEvent").addEventListener("mouseup", listener); + // 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/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.ini b/testing/mochitest/tests/MochiKit-1.4.2/tests/mochitest.ini new file mode 100644 index 0000000000..fb85efa7d8 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/mochitest.ini @@ -0,0 +1,30 @@ +[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-DateTime.html] +[test_MochiKit-DOM.html] +[test_MochiKit-DOM-Safari.html] +[test_MochiKit-DragAndDrop.html] +[test_MochiKit-Format.html] +[test_MochiKit-Iter.html] +[test_MochiKit-Logging.html] +[test_MochiKit-MochiKit.html] +[test_MochiKit-Selector.html] +[test_MochiKit-Signal.html] +[test_MochiKit-Style.html] +[test_MochiKit-Visual.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) 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..ed38198efb --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js @@ -0,0 +1,565 @@ +/* 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 has a corresponging 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 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) { + // State will be focusable even if the tabindex is negative. + 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 || accessible.DOMNode.tabIndex > -1) + ); + } + + 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 (matchState(accessible, STATE_UNAVAILABLE)) { + a11yFail( + "Node is enabled but 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.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.actionCountRule && accessible.actionCount === 0) { + a11yFail("Node does not support any accessible actions", accessible); + + return; + } + + if (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) { + const name = accessible.name && accessible.name.trim(); + if (gEnv.labelRule && !name) { + a11yFail("Interactive elements must be labeled", accessible); + + return; + } + + const { DOMNode } = accessible; + 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); + } + + 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) { + const acc = getAccessible(node); + if (!acc) { + 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) { + gA11YChecks = a11yChecks; + + const { Services } = SpecialPowers; + // Disable accessibility service if it is running and if a11y checks are + // disabled. + if (!gA11YChecks && Services.appinfo.accessibilityEnabled) { + 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(); + }, + }; + + 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..9e6fd77a1a --- /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..d4600e3107 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/EventUtils.js @@ -0,0 +1,3665 @@ +/** + * 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 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, + }); + synthesizeMouseAtCenter(aElement, { type: "mousemove" }, aWindow); + timeout = aWindow.setInterval(() => { + if (aLogFunc) { + aLogFunc("mousemove not received in this 300ms"); + } + aElement.removeEventListener("mousemove", onHit, { + capture: true, + }); + resolve(false); + }, 300); + }); + } + 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); + } + + if (aEvent.type === "click" && this.AccessibilityUtils) { + this.AccessibilityUtils.assertCanBeClicked(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 (var i = 0; i < aStr.length; ++i) { + 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; + } + if (aEvent.osKey) { + mval |= nsIDOMWindowUtils.MODIFIER_OS; + } + + 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 + ); +} + +/* + * 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) { + 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``, ``osKey``, + * ``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) { + var event = aEvent === undefined || aEvent === null ? {} : aEvent; + + 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); + var dispatchKeydown = + !("type" in event) || event.type === "keydown" || !event.type; + var dispatchKeyup = + !("type" in event) || event.type === "keyup" || !event.type; + + 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, +}; +const KEYBOARD_LAYOUT_ARABIC_PC = { + name: "Arabic - PC", + Mac: 7, + Win: null, + hasAltGrOnWin: false, +}; +const KEYBOARD_LAYOUT_BRAZILIAN_ABNT = { + name: "Brazilian ABNT", + Mac: null, + Win: 0x00000416, + hasAltGrOnWin: true, +}; +const KEYBOARD_LAYOUT_DVORAK_QWERTY = { + name: "Dvorak-QWERTY", + Mac: 4, + Win: null, + hasAltGrOnWin: false, +}; +const KEYBOARD_LAYOUT_EN_US = { + name: "US", + Mac: 0, + Win: 0x00000409, + hasAltGrOnWin: false, +}; +const KEYBOARD_LAYOUT_FRENCH = { + name: "French", + Mac: 8, + Win: 0x0000040c, + hasAltGrOnWin: true, +}; +const KEYBOARD_LAYOUT_GREEK = { + name: "Greek", + Mac: 1, + Win: 0x00000408, + hasAltGrOnWin: true, +}; +const KEYBOARD_LAYOUT_GERMAN = { + name: "German", + Mac: 2, + Win: 0x00000407, + hasAltGrOnWin: true, +}; +const KEYBOARD_LAYOUT_HEBREW = { + name: "Hebrew", + Mac: 9, + Win: 0x0000040d, + hasAltGrOnWin: true, +}; +const KEYBOARD_LAYOUT_JAPANESE = { + name: "Japanese", + Mac: null, + Win: 0x00000411, + hasAltGrOnWin: false, +}; +const KEYBOARD_LAYOUT_KHMER = { + name: "Khmer", + Mac: null, + Win: 0x00000453, + hasAltGrOnWin: true, +}; // available on Win7 or later. +const KEYBOARD_LAYOUT_LITHUANIAN = { + name: "Lithuanian", + Mac: 10, + Win: 0x00010427, + hasAltGrOnWin: true, +}; +const KEYBOARD_LAYOUT_NORWEGIAN = { + name: "Norwegian", + Mac: 11, + Win: 0x00000414, + hasAltGrOnWin: true, +}; +const KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC = { + name: "Russian - Mnemonic", + Mac: null, + Win: 0x00020419, + hasAltGrOnWin: true, +}; // available on Win8 or later. +const KEYBOARD_LAYOUT_SPANISH = { + name: "Spanish", + Mac: 12, + Win: 0x0000040a, + hasAltGrOnWin: true, +}; +const KEYBOARD_LAYOUT_SWEDISH = { + name: "Swedish", + Mac: 3, + Win: 0x0000041d, + hasAltGrOnWin: true, +}; +const KEYBOARD_LAYOUT_THAI = { + name: "Thai", + Mac: 5, + Win: 0x0002041e, + hasAltGrOnWin: false, +}; + +/** + * 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: "OS", attr: "osKey" }, + { 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. + */ +function synthesizeSelectionSet(aOffset, aLength, aReverse, aWindow) { + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return false; + } + var 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); + } + synthesizeMouse(aDestElement, 2, 2, { type: "mouseup" }, aDestWindow); + + 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 _computeSrcElementFromSrcSelection(aSrcSelection) { + let srcElement = aSrcSelection.focusNode; + while (_EU_maybeWrap(srcElement).isNativeAnonymous) { + srcElement = _EU_maybeUnwrap( + _EU_maybeWrap(srcElement).flattenedTreeParentNode + ); + } + if (srcElement.nodeType !== Node.NODE_TYPE_ELEMENT) { + srcElement = srcElement.parentElement; + } + 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 + ); + + 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`); + } + if ( + !srcElement.contains( + _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget) + ) + ) { + // 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 ( + !srcElement.contains( + _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget) + ) + ) { + // 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 ( + !destElement.contains( + _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget) + ) + ) { + 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 ( + !srcElement.contains( + _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget) + ) + ) { + 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..9b96bc8265 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js @@ -0,0 +1,180 @@ +const { ExtensionTestCommon } = SpecialPowers.ChromeUtils.import( + "resource://testing-common/ExtensionTestCommon.jsm" +); + +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..059abdcd9b --- /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..eb319b2e76 --- /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..07d50572b0 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/MozillaLogger.js @@ -0,0 +1,103 @@ +/** + * 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..b256269e71 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js @@ -0,0 +1,369 @@ +/** + * 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. + */ + +// 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; +const WIN_VK_RBUTTON = 0x00000002; +const WIN_VK_CANCEL = 0xe0460003; +const WIN_VK_MBUTTON = 0x00000004; +const WIN_VK_XBUTTON1 = 0x00000005; +const WIN_VK_XBUTTON2 = 0x00000006; +const WIN_VK_BACK = 0x000e0008; +const WIN_VK_TAB = 0x000f0009; +const WIN_VK_CLEAR = 0x004c000c; +const WIN_VK_RETURN = 0x001c000d; +const WIN_VK_SHIFT = 0x002a0010; +const WIN_VK_CONTROL = 0x001d0011; +const WIN_VK_MENU = 0x00380012; +const WIN_VK_PAUSE = 0x00450013; +const WIN_VK_CAPITAL = 0x003a0014; +const WIN_VK_KANA = 0x00000015; +const WIN_VK_HANGUEL = 0x00000015; +const WIN_VK_HANGUL = 0x00000015; +const WIN_VK_JUNJA = 0x00000017; +const WIN_VK_FINAL = 0x00000018; +const WIN_VK_HANJA = 0x00000019; +const WIN_VK_KANJI = 0x00000019; +const WIN_VK_ESCAPE = 0x0001001b; +const WIN_VK_CONVERT = 0x0000001c; +const WIN_VK_NONCONVERT = 0x0000001d; +const WIN_VK_ACCEPT = 0x0000001e; +const WIN_VK_MODECHANGE = 0x0000001f; +const WIN_VK_SPACE = 0x00390020; +const WIN_VK_PRIOR = 0xe0490021; +const WIN_VK_NEXT = 0xe0510022; +const WIN_VK_END = 0xe04f0023; +const WIN_VK_HOME = 0xe0470024; +const WIN_VK_LEFT = 0xe04b0025; +const WIN_VK_UP = 0xe0480026; +const WIN_VK_RIGHT = 0xe04d0027; +const WIN_VK_DOWN = 0xe0500028; +const WIN_VK_SELECT = 0x00000029; +const WIN_VK_PRINT = 0x0000002a; +const WIN_VK_EXECUTE = 0x0000002b; +const WIN_VK_SNAPSHOT = 0xe037002c; +const WIN_VK_INSERT = 0xe052002d; +const WIN_VK_DELETE = 0xe053002e; +const WIN_VK_HELP = 0x0000002f; +const WIN_VK_0 = 0x00000030; +const WIN_VK_1 = 0x00000031; +const WIN_VK_2 = 0x00000032; +const WIN_VK_3 = 0x00000033; +const WIN_VK_4 = 0x00000034; +const WIN_VK_5 = 0x00000035; +const WIN_VK_6 = 0x00000036; +const WIN_VK_7 = 0x00000037; +const WIN_VK_8 = 0x00000038; +const WIN_VK_9 = 0x00000039; +const WIN_VK_A = 0x00000041; +const WIN_VK_B = 0x00000042; +const WIN_VK_C = 0x00000043; +const WIN_VK_D = 0x00000044; +const WIN_VK_E = 0x00000045; +const WIN_VK_F = 0x00000046; +const WIN_VK_G = 0x00000047; +const WIN_VK_H = 0x00000048; +const WIN_VK_I = 0x00000049; +const WIN_VK_J = 0x0000004a; +const WIN_VK_K = 0x0000004b; +const WIN_VK_L = 0x0000004c; +const WIN_VK_M = 0x0000004d; +const WIN_VK_N = 0x0000004e; +const WIN_VK_O = 0x0000004f; +const WIN_VK_P = 0x00000050; +const WIN_VK_Q = 0x00000051; +const WIN_VK_R = 0x00000052; +const WIN_VK_S = 0x00000053; +const WIN_VK_T = 0x00000054; +const WIN_VK_U = 0x00000055; +const WIN_VK_V = 0x00000056; +const WIN_VK_W = 0x00000057; +const WIN_VK_X = 0x00000058; +const WIN_VK_Y = 0x00000059; +const WIN_VK_Z = 0x0000005a; +const WIN_VK_LWIN = 0xe05b005b; +const WIN_VK_RWIN = 0xe05c005c; +const WIN_VK_APPS = 0xe05d005d; +const WIN_VK_SLEEP = 0x0000005f; +const WIN_VK_NUMPAD0 = 0x00520060; +const WIN_VK_NUMPAD1 = 0x004f0061; +const WIN_VK_NUMPAD2 = 0x00500062; +const WIN_VK_NUMPAD3 = 0x00510063; +const WIN_VK_NUMPAD4 = 0x004b0064; +const WIN_VK_NUMPAD5 = 0x004c0065; +const WIN_VK_NUMPAD6 = 0x004d0066; +const WIN_VK_NUMPAD7 = 0x00470067; +const WIN_VK_NUMPAD8 = 0x00480068; +const WIN_VK_NUMPAD9 = 0x00490069; +const WIN_VK_MULTIPLY = 0x0037006a; +const WIN_VK_ADD = 0x004e006b; +const WIN_VK_SEPARATOR = 0x0000006c; +const WIN_VK_OEM_NEC_SEPARATE = 0x0000006c; +const WIN_VK_SUBTRACT = 0x004a006d; +const WIN_VK_DECIMAL = 0x0053006e; +const WIN_VK_DIVIDE = 0xe035006f; +const WIN_VK_F1 = 0x003b0070; +const WIN_VK_F2 = 0x003c0071; +const WIN_VK_F3 = 0x003d0072; +const WIN_VK_F4 = 0x003e0073; +const WIN_VK_F5 = 0x003f0074; +const WIN_VK_F6 = 0x00400075; +const WIN_VK_F7 = 0x00410076; +const WIN_VK_F8 = 0x00420077; +const WIN_VK_F9 = 0x00430078; +const WIN_VK_F10 = 0x00440079; +const WIN_VK_F11 = 0x0057007a; +const WIN_VK_F12 = 0x0058007b; +const WIN_VK_F13 = 0x0064007c; +const WIN_VK_F14 = 0x0065007d; +const WIN_VK_F15 = 0x0066007e; +const WIN_VK_F16 = 0x0067007f; +const WIN_VK_F17 = 0x00680080; +const WIN_VK_F18 = 0x00690081; +const WIN_VK_F19 = 0x006a0082; +const WIN_VK_F20 = 0x006b0083; +const WIN_VK_F21 = 0x006c0084; +const WIN_VK_F22 = 0x006d0085; +const WIN_VK_F23 = 0x006e0086; +const WIN_VK_F24 = 0x00760087; +const WIN_VK_NUMLOCK = 0xe0450090; +const WIN_VK_SCROLL = 0x00460091; +const WIN_VK_OEM_FJ_JISHO = 0x00000092; +const WIN_VK_OEM_NEC_EQUAL = 0x00000092; +const WIN_VK_OEM_FJ_MASSHOU = 0x00000093; +const WIN_VK_OEM_FJ_TOUROKU = 0x00000094; +const WIN_VK_OEM_FJ_LOYA = 0x00000095; +const WIN_VK_OEM_FJ_ROYA = 0x00000096; +const WIN_VK_LSHIFT = 0x002a00a0; +const WIN_VK_RSHIFT = 0x003600a1; +const WIN_VK_LCONTROL = 0x001d00a2; +const WIN_VK_RCONTROL = 0xe01d00a3; +const WIN_VK_LMENU = 0x003800a4; +const WIN_VK_RMENU = 0xe03800a5; +const WIN_VK_BROWSER_BACK = 0xe06a00a6; +const WIN_VK_BROWSER_FORWARD = 0xe06900a7; +const WIN_VK_BROWSER_REFRESH = 0xe06700a8; +const WIN_VK_BROWSER_STOP = 0xe06800a9; +const WIN_VK_BROWSER_SEARCH = 0x000000aa; +const WIN_VK_BROWSER_FAVORITES = 0xe06600ab; +const WIN_VK_BROWSER_HOME = 0xe03200ac; +const WIN_VK_VOLUME_MUTE = 0xe02000ad; +const WIN_VK_VOLUME_DOWN = 0xe02e00ae; +const WIN_VK_VOLUME_UP = 0xe03000af; +const WIN_VK_MEDIA_NEXT_TRACK = 0xe01900b0; +const WIN_VK_OEM_FJ_000 = 0x000000b0; +const WIN_VK_MEDIA_PREV_TRACK = 0xe01000b1; +const WIN_VK_OEM_FJ_EUQAL = 0x000000b1; +const WIN_VK_MEDIA_STOP = 0xe02400b2; +const WIN_VK_MEDIA_PLAY_PAUSE = 0xe02200b3; +const WIN_VK_OEM_FJ_00 = 0x000000b3; +const WIN_VK_LAUNCH_MAIL = 0xe06c00b4; +const WIN_VK_LAUNCH_MEDIA_SELECT = 0xe06d00b5; +const WIN_VK_LAUNCH_APP1 = 0xe06b00b6; +const WIN_VK_LAUNCH_APP2 = 0xe02100b7; +const WIN_VK_OEM_1 = 0x000000ba; +const WIN_VK_OEM_PLUS = 0x000000bb; +const WIN_VK_OEM_COMMA = 0x000000bc; +const WIN_VK_OEM_MINUS = 0x000000bd; +const WIN_VK_OEM_PERIOD = 0x000000be; +const WIN_VK_OEM_2 = 0x000000bf; +const WIN_VK_OEM_3 = 0x000000c0; +const WIN_VK_ABNT_C1 = 0x005600c1; +const WIN_VK_ABNT_C2 = 0x000000c2; +const WIN_VK_OEM_4 = 0x000000db; +const WIN_VK_OEM_5 = 0x000000dc; +const WIN_VK_OEM_6 = 0x000000dd; +const WIN_VK_OEM_7 = 0x000000de; +const WIN_VK_OEM_8 = 0x000000df; +const WIN_VK_OEM_NEC_DP1 = 0x000000e0; +const WIN_VK_OEM_AX = 0x000000e1; +const WIN_VK_OEM_NEC_DP2 = 0x000000e1; +const WIN_VK_OEM_102 = 0x000000e2; +const WIN_VK_OEM_NEC_DP3 = 0x000000e2; +const WIN_VK_ICO_HELP = 0x000000e3; +const WIN_VK_OEM_NEC_DP4 = 0x000000e3; +const WIN_VK_ICO_00 = 0x000000e4; +const WIN_VK_PROCESSKEY = 0x000000e5; +const WIN_VK_ICO_CLEAR = 0x000000e6; +const WIN_VK_PACKET = 0x000000e7; +const WIN_VK_ERICSSON_BASE = 0x000000e8; +const WIN_VK_OEM_RESET = 0x000000e9; +const WIN_VK_OEM_JUMP = 0x000000ea; +const WIN_VK_OEM_PA1 = 0x000000eb; +const WIN_VK_OEM_PA2 = 0x000000ec; +const WIN_VK_OEM_PA3 = 0x000000ed; +const WIN_VK_OEM_WSCTRL = 0x000000ee; +const WIN_VK_OEM_CUSEL = 0x000000ef; +const WIN_VK_OEM_ATTN = 0x000000f0; +const WIN_VK_OEM_FINISH = 0x000000f1; +const WIN_VK_OEM_COPY = 0x000000f2; +const WIN_VK_OEM_AUTO = 0x000000f3; +const WIN_VK_OEM_ENLW = 0x000000f4; +const WIN_VK_OEM_BACKTAB = 0x000000f5; +const WIN_VK_ATTN = 0x000000f6; +const WIN_VK_CRSEL = 0x000000f7; +const WIN_VK_EXSEL = 0x000000f8; +const WIN_VK_EREOF = 0x000000f9; +const WIN_VK_PLAY = 0x000000fa; +const WIN_VK_ZOOM = 0x000000fb; +const WIN_VK_NONAME = 0x000000fc; +const WIN_VK_PA1 = 0x000000fd; +const WIN_VK_OEM_CLEAR = 0x000000fe; + +const WIN_VK_NUMPAD_RETURN = 0xe01c000d; +const WIN_VK_NUMPAD_PRIOR = 0x00490021; +const WIN_VK_NUMPAD_NEXT = 0x00510022; +const WIN_VK_NUMPAD_END = 0x004f0023; +const WIN_VK_NUMPAD_HOME = 0x00470024; +const WIN_VK_NUMPAD_LEFT = 0x004b0025; +const WIN_VK_NUMPAD_UP = 0x00480026; +const WIN_VK_NUMPAD_RIGHT = 0x004d0027; +const WIN_VK_NUMPAD_DOWN = 0x00500028; +const WIN_VK_NUMPAD_INSERT = 0x0052002d; +const WIN_VK_NUMPAD_DELETE = 0x0053002e; + +// Mac + +const MAC_VK_ANSI_A = 0x00; +const MAC_VK_ANSI_S = 0x01; +const MAC_VK_ANSI_D = 0x02; +const MAC_VK_ANSI_F = 0x03; +const MAC_VK_ANSI_H = 0x04; +const MAC_VK_ANSI_G = 0x05; +const MAC_VK_ANSI_Z = 0x06; +const MAC_VK_ANSI_X = 0x07; +const MAC_VK_ANSI_C = 0x08; +const MAC_VK_ANSI_V = 0x09; +const MAC_VK_ISO_Section = 0x0a; +const MAC_VK_ANSI_B = 0x0b; +const MAC_VK_ANSI_Q = 0x0c; +const MAC_VK_ANSI_W = 0x0d; +const MAC_VK_ANSI_E = 0x0e; +const MAC_VK_ANSI_R = 0x0f; +const MAC_VK_ANSI_Y = 0x10; +const MAC_VK_ANSI_T = 0x11; +const MAC_VK_ANSI_1 = 0x12; +const MAC_VK_ANSI_2 = 0x13; +const MAC_VK_ANSI_3 = 0x14; +const MAC_VK_ANSI_4 = 0x15; +const MAC_VK_ANSI_6 = 0x16; +const MAC_VK_ANSI_5 = 0x17; +const MAC_VK_ANSI_Equal = 0x18; +const MAC_VK_ANSI_9 = 0x19; +const MAC_VK_ANSI_7 = 0x1a; +const MAC_VK_ANSI_Minus = 0x1b; +const MAC_VK_ANSI_8 = 0x1c; +const MAC_VK_ANSI_0 = 0x1d; +const MAC_VK_ANSI_RightBracket = 0x1e; +const MAC_VK_ANSI_O = 0x1f; +const MAC_VK_ANSI_U = 0x20; +const MAC_VK_ANSI_LeftBracket = 0x21; +const MAC_VK_ANSI_I = 0x22; +const MAC_VK_ANSI_P = 0x23; +const MAC_VK_Return = 0x24; +const MAC_VK_ANSI_L = 0x25; +const MAC_VK_ANSI_J = 0x26; +const MAC_VK_ANSI_Quote = 0x27; +const MAC_VK_ANSI_K = 0x28; +const MAC_VK_ANSI_Semicolon = 0x29; +const MAC_VK_ANSI_Backslash = 0x2a; +const MAC_VK_ANSI_Comma = 0x2b; +const MAC_VK_ANSI_Slash = 0x2c; +const MAC_VK_ANSI_N = 0x2d; +const MAC_VK_ANSI_M = 0x2e; +const MAC_VK_ANSI_Period = 0x2f; +const MAC_VK_Tab = 0x30; +const MAC_VK_Space = 0x31; +const MAC_VK_ANSI_Grave = 0x32; +const MAC_VK_Delete = 0x33; +const MAC_VK_PC_Backspace = 0x33; +const MAC_VK_Powerbook_KeypadEnter = 0x34; +const MAC_VK_Escape = 0x35; +const MAC_VK_RightCommand = 0x36; +const MAC_VK_Command = 0x37; +const MAC_VK_Shift = 0x38; +const MAC_VK_CapsLock = 0x39; +const MAC_VK_Option = 0x3a; +const MAC_VK_Control = 0x3b; +const MAC_VK_RightShift = 0x3c; +const MAC_VK_RightOption = 0x3d; +const MAC_VK_RightControl = 0x3e; +const MAC_VK_Function = 0x3f; +const MAC_VK_F17 = 0x40; +const MAC_VK_ANSI_KeypadDecimal = 0x41; +const MAC_VK_ANSI_KeypadMultiply = 0x43; +const MAC_VK_ANSI_KeypadPlus = 0x45; +const MAC_VK_ANSI_KeypadClear = 0x47; +const MAC_VK_VolumeUp = 0x48; +const MAC_VK_VolumeDown = 0x49; +const MAC_VK_Mute = 0x4a; +const MAC_VK_ANSI_KeypadDivide = 0x4b; +const MAC_VK_ANSI_KeypadEnter = 0x4c; +const MAC_VK_ANSI_KeypadMinus = 0x4e; +const MAC_VK_F18 = 0x4f; +const MAC_VK_F19 = 0x50; +const MAC_VK_ANSI_KeypadEquals = 0x51; +const MAC_VK_ANSI_Keypad0 = 0x52; +const MAC_VK_ANSI_Keypad1 = 0x53; +const MAC_VK_ANSI_Keypad2 = 0x54; +const MAC_VK_ANSI_Keypad3 = 0x55; +const MAC_VK_ANSI_Keypad4 = 0x56; +const MAC_VK_ANSI_Keypad5 = 0x57; +const MAC_VK_ANSI_Keypad6 = 0x58; +const MAC_VK_ANSI_Keypad7 = 0x59; +const MAC_VK_F20 = 0x5a; +const MAC_VK_ANSI_Keypad8 = 0x5b; +const MAC_VK_ANSI_Keypad9 = 0x5c; +const MAC_VK_JIS_Yen = 0x5d; +const MAC_VK_JIS_Underscore = 0x5e; +const MAC_VK_JIS_KeypadComma = 0x5f; +const MAC_VK_F5 = 0x60; +const MAC_VK_F6 = 0x61; +const MAC_VK_F7 = 0x62; +const MAC_VK_F3 = 0x63; +const MAC_VK_F8 = 0x64; +const MAC_VK_F9 = 0x65; +const MAC_VK_JIS_Eisu = 0x66; +const MAC_VK_F11 = 0x67; +const MAC_VK_JIS_Kana = 0x68; +const MAC_VK_F13 = 0x69; +const MAC_VK_PC_PrintScreen = 0x69; +const MAC_VK_F16 = 0x6a; +const MAC_VK_F14 = 0x6b; +const MAC_VK_PC_ScrollLock = 0x6b; +const MAC_VK_F10 = 0x6d; +const MAC_VK_PC_ContextMenu = 0x6e; +const MAC_VK_F12 = 0x6f; +const MAC_VK_F15 = 0x71; +const MAC_VK_PC_Pause = 0x71; +const MAC_VK_Help = 0x72; +const MAC_VK_PC_Insert = 0x72; +const MAC_VK_Home = 0x73; +const MAC_VK_PageUp = 0x74; +const MAC_VK_ForwardDelete = 0x75; +const MAC_VK_PC_Delete = 0x75; +const MAC_VK_F4 = 0x76; +const MAC_VK_End = 0x77; +const MAC_VK_F2 = 0x78; +const MAC_VK_PageDown = 0x79; +const MAC_VK_F1 = 0x7a; +const MAC_VK_LeftArrow = 0x7b; +const MAC_VK_RightArrow = 0x7c; +const MAC_VK_DownArrow = 0x7d; +const MAC_VK_UpArrow = 0x7e; diff --git a/testing/mochitest/tests/SimpleTest/SimpleTest.js b/testing/mochitest/tests/SimpleTest/SimpleTest.js new file mode 100644 index 0000000000..12c8b3f6ea --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js @@ -0,0 +1,2189 @@ +/* -*- js-indent-level: 4; tab-width: 4; indent-tabs-mode: nil -*- */ +/* vim:set ts=4 sw=4 sts=4 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); + +// 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._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/unicode + * 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/unicode"] + * 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/unicode"; + + // 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/unicode", + 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); +}; + +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, in all my testing youtube is the only one. + FULL_PROFILE_WORKERS_TO_IGNORE = ["https://www.youtube.com/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.includes(worker.scriptSpec)) { + 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) { + 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"); + } + taskInfo("Leaving"); + } + } catch (ex) { + try { + let serializedEx; + if (ex instanceof Error) { + 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..915ac6cbad --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/TestRunner.js @@ -0,0 +1,1077 @@ +/* -*- 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._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 (runtime >= TestRunner.timeout * TestRunner._timeoutFactor) { + let testIframe = $("testframe"); + var frameWindow = + (!testInXOriginFrame() && testIframe.contentWindow.wrappedJSObject) || + testIframe.contentWindow; + // TODO : Do this in a way that reports that the test ended with a status "TIMEOUT" + reportError(frameWindow, "Test timed out."); + + // 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._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); + } + }, 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; + + TestRunner.structuredLogger.testEnd( + TestRunner.currentTestURL, + result, + "OK", + "Finished in " + runtime + "ms", + { runtime } + ); + + 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(); + return; + } + + var interstitialURL; + if ( + !testInXOriginFrame() && + $("testframe").contentWindow.location.protocol == "chrome:" + ) { + interstitialURL = "tests/SimpleTest/iframe-between-tests.html"; + } else { + interstitialURL = "/tests/SimpleTest/iframe-between-tests.html"; + } + // 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 && + testwin.SimpleTest._tests.length != testwin.SimpleTest.testsLength + ) { + 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 + ); + } + 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() { + // 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 + ); + } + + 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..62f1062b36 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/WindowSnapshot.js @@ -0,0 +1,127 @@ +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..1fbe55ab0d --- /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..8de879f205 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/iframe-between-tests.html @@ -0,0 +1,17 @@ +<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); + runner.testUnloaded(); + + 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..475ac28fc9 --- /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..6d8b3bf9dc --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/setup.js @@ -0,0 +1,326 @@ +/* -*- 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; +} + +// 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; +} + +if (params.interactiveDebugger) { + 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; +} + +// 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 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) { + RunSet.runall(); + } +} + +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.ini b/testing/mochitest/tests/browser/browser.ini new file mode 100644 index 0000000000..82af0d1c46 --- /dev/null +++ b/testing/mochitest/tests/browser/browser.ini @@ -0,0 +1,56 @@ +[DEFAULT] +support-files = + head.js + dummy.html + +[browser_add_task.js] +[browser_async.js] +[browser_browserLoaded_content_loaded.js] +https_first_disabled = true +[browser_BrowserTestUtils.js] +skip-if = verify +[browser_document_builder_sjs.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[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_pass.js] +[browser_parameters.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..8be0b5f348 --- /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..c3b56b6d85 --- /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..21a7ad4ac3 --- /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..23524b9f23 --- /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..b7330a0fc0 --- /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..dea405d97d --- /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..cf867e656c --- /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(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..cd9565f743 --- /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..3441eeb3b1 --- /dev/null +++ b/testing/mochitest/tests/moz.build @@ -0,0 +1,14 @@ +# -*- 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"] + +TESTING_JS_MODULES += [ + "Harness_sanity/ImportTesting.jsm", +] + +MOCHITEST_MANIFESTS += ["Harness_sanity/mochitest.ini"] +BROWSER_CHROME_MANIFESTS += ["browser/browser.ini"] diff --git a/testing/mochitest/tests/python/conftest.py b/testing/mochitest/tests/python/conftest.py new file mode 100644 index 0000000000..e418dfb816 --- /dev/null +++ b/testing/mochitest/tests/python/conftest.py @@ -0,0 +1,157 @@ +# 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") + + 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, + "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): + return { + "name": test, + "relpath": test, + "path": os.path.join(test_root, test), + # add a dummy manifest file because mochitest expects it + "manifest": os.path.join(test_root, manifest_name), + "manifest_relpath": manifest_name, + "skip-if": runFailures, + } + + 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))) + 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.ini b/testing/mochitest/tests/python/files/browser-args.ini new file mode 100644 index 0000000000..1425512c1c --- /dev/null +++ b/testing/mochitest/tests/python/files/browser-args.ini @@ -0,0 +1,7 @@ +[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_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_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.ini b/testing/mochitest/tests/python/python.ini new file mode 100644 index 0000000000..ecdf4f39b6 --- /dev/null +++ b/testing/mochitest/tests/python/python.ini @@ -0,0 +1,9 @@ +[DEFAULT] +subsuite = mochitest + +[test_mochitest_integration.py] +sequential = true +[test_build_profile.py] +[test_get_active_tests.py] +[test_message_logger.py] +[test_create_directories.py] 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..438efb4a04 --- /dev/null +++ b/testing/mochitest/tests/python/test_build_profile.py @@ -0,0 +1,82 @@ +# 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): + # We set e10s=False here because MochitestDesktop.buildProfile overwrites + # the value defined in the base profile. + # TODO stop setting browser.tabs.remote.autostart in the base profile + md, result = build_profile(e10s=False) + + 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..e2ae89bdaf --- /dev/null +++ b/testing/mochitest/tests/python/test_mochitest_integration.py @@ -0,0 +1,314 @@ +# 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.ini"), + ], +) +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.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": 245, + "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"] + + test_end = filter_action("test_end", lines) + assert len(test_end) == results["lines"] + # TODO: this should be ASSERT, but moving the assertion check before + # the test_end action caused a bunch of failures. + 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() |