diff options
Diffstat (limited to 'testing/mochitest')
249 files changed, 66997 insertions, 0 deletions
diff --git a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm new file mode 100644 index 000000000..eebcbb6bb --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm @@ -0,0 +1,1329 @@ +/* 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. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "BrowserTestUtils", +]; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Timer.jsm"); +Cu.import("resource://testing-common/TestUtils.jsm"); +Cu.import("resource://testing-common/ContentTask.jsm"); + +Cc["@mozilla.org/globalmessagemanager;1"] + .getService(Ci.nsIMessageListenerManager) + .loadFrameScript( + "chrome://mochikit/content/tests/BrowserTestUtils/content-utils.js", true); + +XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils", + "resource:///modules/E10SUtils.jsm"); + +// For now, we'll allow tests to use CPOWs in this module for +// some cases. +Cu.permitCPOWsInScope(this); + +var gSendCharCount = 0; +var gSynthesizeKeyCount = 0; +var gSynthesizeCompositionCount = 0; +var gSynthesizeCompositionChangeCount = 0; + +const kAboutPageRegistrationContentScript = + "chrome://mochikit/content/tests/BrowserTestUtils/content-about-page-utils.js"; + +this.BrowserTestUtils = { + /** + * Loads a page in a new tab, executes a Task and closes the tab. + * + * @param options + * An object or string. + * If this is a string it is the url to open and will be opened in the + * currently active browser window. + * If an object it should have the following properties: + * { + * gBrowser: + * Reference to the "tabbrowser" element where the new tab should + * be opened. + * url: + * String with the URL of the page to load. + * } + * @param taskFn + * Generator function representing a Task 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 {} Returns the value that is returned from taskFn. + * @resolves When the tab has been closed. + * @rejects Any exception from taskFn is propagated. + */ + withNewTab: Task.async(function* (options, taskFn) { + if (typeof(options) == "string") { + options = { + gBrowser: Services.wm.getMostRecentWindow("navigator:browser").gBrowser, + url: options + } + } + let tab = yield BrowserTestUtils.openNewForegroundTab(options.gBrowser, options.url); + let originalWindow = tab.ownerDocument.defaultView; + let result = yield taskFn(tab.linkedBrowser); + let finalWindow = tab.ownerDocument.defaultView; + if (originalWindow == finalWindow && !tab.closing && tab.linkedBrowser) { + yield 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. + * + * @param {tabbrowser} tabbrowser + * The tabbrowser to open the tab new in. + * @param {string} opening + * 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. + * + * @return {Promise} + * Resolves when the tab is ready and loaded as necessary. + * @resolves The new tab. + */ + openNewForegroundTab(tabbrowser, opening = "about:blank", aWaitForLoad = true, aWaitForStateStop = false) { + let tab; + let promises = [ + BrowserTestUtils.switchTab(tabbrowser, function () { + if (typeof opening == "function") { + opening(); + tab = tabbrowser.selectedTab; + } + else { + tabbrowser.selectedTab = tab = tabbrowser.addTab(opening); + } + }) + ]; + + if (aWaitForLoad) { + promises.push(BrowserTestUtils.browserLoaded(tab.linkedBrowser)); + } + if (aWaitForStateStop) { + promises.push(BrowserTestUtils.browserStopped(tab.linkedBrowser)); + } + + return Promise.all(promises).then(() => tab); + }, + + /** + * 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 promise = new Promise(resolve => { + tabbrowser.addEventListener("TabSwitchDone", function onSwitch() { + tabbrowser.removeEventListener("TabSwitchDone", onSwitch); + TestUtils.executeSoon(() => resolve(tabbrowser.selectedTab)); + }); + }); + + 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. This is guaranteed to work because the + * way we're listening for the load is in the content-utils.js frame script, + * and then sending an async message up, so we can't miss the message. + * + * @param {xul:browser} browser + * A xul:browser. + * @param {Boolean} includeSubFrames + * A boolean indicating if loads from subframes should be included. + * @param {optional string or function} wantLoad + * 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. + * + * @return {Promise} + * @resolves When a load event is triggered for the browser. + */ + browserLoaded(browser, includeSubFrames=false, wantLoad=null) { + function isWanted(url) { + if (!wantLoad) { + return true; + } else if (typeof(wantLoad) == "function") { + return wantLoad(url); + } else { + // It's a string. + return wantLoad == url; + } + } + + return new Promise(resolve => { + let mm = browser.ownerDocument.defaultView.messageManager; + mm.addMessageListener("browser-test-utils:loadEvent", function onLoad(msg) { + if (msg.target == browser && (!msg.data.subframe || includeSubFrames) && + isWanted(msg.data.url)) { + mm.removeMessageListener("browser-test-utils:loadEvent", onLoad); + resolve(msg.data.url); + } + }); + }); + }, + + /** + * 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 {win} + * A newly opened window for which we're waiting for the + * first browser load. + * + * @return {Promise} + * @resolves Once the selected browser fires its load event. + */ + firstBrowserLoaded(win, aboutBlank = true) { + let mm = win.messageManager; + return this.waitForMessage(mm, "browser-test-utils:loadEvent", (msg) => { + let selectedBrowser = win.gBrowser.selectedBrowser; + return msg.target == selectedBrowser && + (aboutBlank || selectedBrowser.currentURI.spec != "about:blank") + }); + }, + + /** + * 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. + * + * @return {Promise} + * @resolves When STATE_STOP reaches the tab's progress listener + */ + browserStopped(browser) { + return new Promise(resolve => { + let wpl = { + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aWebProgress.isTopLevel) { + browser.webProgress.removeProgressListener(filter); + filter.removeProgressListener(wpl); + resolve(); + }; + }, + onSecurityChange() {}, + onStatusChange() {}, + onLocationChange() {}, + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIWebProgressListener, + Ci.nsIWebProgressListener2, + ]), + }; + const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(Ci.nsIWebProgress); + filter.addProgressListener(wpl, Ci.nsIWebProgress.NOTIFY_ALL); + browser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL); + }); + }, + + /** + * Waits for the next tab to open and load a given URL. + * + * 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} url + * A string URL to look for in the new tab. If null, allows any non-blank URL. + * + * @return {Promise} + * @resolves With the {xul:tab} when a tab is opened and its location changes to the given URL. + * + * 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, url) { + return new Promise((resolve, reject) => { + tabbrowser.tabContainer.addEventListener("TabOpen", function onTabOpen(openEvent) { + tabbrowser.tabContainer.removeEventListener("TabOpen", onTabOpen); + + let progressListener = { + onLocationChange(aBrowser) { + if (aBrowser != openEvent.target.linkedBrowser || + (url && aBrowser.currentURI.spec != url) || + (!url && aBrowser.currentURI.spec == "about:blank")) { + return; + } + + tabbrowser.removeTabsProgressListener(progressListener); + resolve(openEvent.target); + }, + }; + 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) { + if ((url && aBrowser.currentURI.spec != url) || + (!url && aBrowser.currentURI.spec == "about:blank")) { + return; + } + + tabbrowser.removeTabsProgressListener(progressListener); + resolve(); + }, + }; + tabbrowser.addTabsProgressListener(progressListener); + }); + }, + + /** + * Waits for the next browser window to open and be fully loaded. + * + * @param {bool} delayedStartup (optional) + * Whether or not to wait for the browser-delayed-startup-finished + * observer notification before resolving. Defaults to true. + * @param {string} initialBrowserLoaded (optional) + * 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. + * @return {Promise} + * A Promise which resolves the next time that a DOM window + * opens and the delayed startup observer notification fires. + */ + waitForNewWindow: Task.async(function* (delayedStartup=true, + initialBrowserLoaded=null) { + let win = yield this.domWindowOpened(); + + let promises = [ + TestUtils.topicObserved("browser-delayed-startup-finished", + subject => subject == win), + ]; + + if (initialBrowserLoaded) { + yield this.waitForEvent(win, "DOMContentLoaded"); + + let browser = win.gBrowser.selectedBrowser; + + // Retrieve the given browser's current process type. + let process = + browser.isRemoteBrowser ? Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT + : Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + if (win.gMultiProcessBrowser && + !E10SUtils.canLoadURIInProcess(initialBrowserLoaded, process)) { + yield this.waitForEvent(browser, "XULFrameLoaderCreated"); + } + + let loadPromise = this.browserLoaded(browser, false, initialBrowserLoaded); + promises.push(loadPromise); + } + + yield Promise.all(promises); + + return win; + }), + + /** + * Loads a new URI in the given browser and waits until we really started + * loading. In e10s browser.loadURI() can be an asynchronous operation due + * to having to switch the browser's remoteness and keep its shistory data. + * + * @param {xul:browser} browser + * A xul:browser. + * @param {string} uri + * The URI to load. + * + * @return {Promise} + * @resolves When we started loading the given URI. + */ + loadURI: Task.async(function* (browser, uri) { + // Load the new URI. + browser.loadURI(uri); + + // Nothing to do in non-e10s mode. + if (!browser.ownerDocument.defaultView.gMultiProcessBrowser) { + return; + } + + // Retrieve the given browser's current process type. + let process = browser.isRemoteBrowser ? Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT + : Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + + // If the new URI can't load in the browser's current process then we + // should wait for the new frameLoader to be created. This will happen + // asynchronously when the browser's remoteness changes. + if (!E10SUtils.canLoadURIInProcess(uri, process)) { + yield this.waitForEvent(browser, "XULFrameLoaderCreated"); + } + }), + + /** + * @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 => { + function observer(subject, topic, data) { + if (topic == "domwindowopened" && (!win || subject === win)) { + let observedWindow = subject.QueryInterface(Ci.nsIDOMWindow); + if (checkFn && !checkFn(observedWindow)) { + return; + } + Services.ww.unregisterNotification(observer); + resolve(observedWindow); + } + } + Services.ww.registerNotification(observer); + }); + }, + + /** + * @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.QueryInterface(Ci.nsIDOMWindow)); + } + } + Services.ww.registerNotification(observer); + }); + }, + + /** + * @param {Object} options + * { + * private: A boolean indicating if the window should be + * private + * remote: A boolean indicating if the window should run + * remote browser tabs or not. If omitted, the window + * will choose the profile default state. + * width: Desired width of window + * height: Desired height of window + * } + * @return {Promise} + * Resolves with the new window once it is loaded. + */ + openNewBrowserWindow: Task.async(function*(options={}) { + let argString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + argString.data = ""; + let features = "chrome,dialog=no,all"; + + if (options.private) { + features += ",private"; + } + + if (options.width) { + features += ",width=" + options.width; + } + if (options.height) { + features += ",height=" + options.height; + } + + if (options.hasOwnProperty("remote")) { + let remoteState = options.remote ? "remote" : "non-remote"; + features += `,${remoteState}`; + } + + let win = Services.ww.openWindow( + null, Services.prefs.getCharPref("browser.chromeURL"), "_blank", + features, argString); + + // Wait for browser-delayed-startup-finished notification, it indicates + // that the window has loaded completely and is ready to be used for + // testing. + let startupPromise = + TestUtils.topicObserved("browser-delayed-startup-finished", + subject => subject == win).then(() => win); + + let loadPromise = this.firstBrowserLoaded(win); + + yield startupPromise; + yield loadPromise; + + return win; + }), + + /** + * Closes a window. + * + * @param {Window} + * 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} + * 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"); + + if (winType == "navigator:browser") { + let finalMsgsPromise = new Promise((resolve) => { + let browserSet = new Set(win.gBrowser.browsers); + let mm = win.getGroupMessageManager("browsers"); + + mm.addMessageListener("SessionStore:update", function onMessage(msg) { + if (browserSet.has(msg.target) && msg.data.isFinal) { + browserSet.delete(msg.target); + if (!browserSet.size) { + mm.removeMessageListener("SessionStore:update", onMessage); + // 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); + } + } + }, true); + }); + + promises.push(finalMsgsPromise); + } + + return Promise.all(promises); + }, + + /** + * Waits for an event to be fired on a specified element. + * + * Usage: + * let promiseEvent = BrowserTestUtils.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. + * @param {bool} wantsUntrusted [optional] + * 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) { + return new Promise((resolve, reject) => { + subject.addEventListener(eventName, function listener(event) { + try { + if (checkFn && !checkFn(event)) { + return; + } + subject.removeEventListener(eventName, listener, capture); + resolve(event); + } catch (ex) { + try { + subject.removeEventListener(eventName, listener, capture); + } catch (ex2) { + // Maybe the provided object does not support removeEventListener. + } + reject(ex); + } + }, capture, wantsUntrusted); + }); + }, + + /** + * 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} wantsUntrusted [optional] + * Whether to accept untrusted events + * + * @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} + */ + waitForContentEvent(browser, eventName, capture = false, checkFn, wantsUntrusted = false) { + let parameters = { + eventName, + capture, + checkFnSource: checkFn ? checkFn.toSource() : null, + wantsUntrusted, + }; + return ContentTask.spawn(browser, parameters, + function({ eventName, capture, checkFnSource, wantsUntrusted }) { + let checkFn; + if (checkFnSource) { + checkFn = eval(`(() => (${checkFnSource}))()`); + } + return new Promise((resolve, reject) => { + addEventListener(eventName, function listener(event) { + let completion = resolve; + try { + if (checkFn && !checkFn(event)) { + return; + } + } catch (e) { + completion = () => reject(e); + } + removeEventListener(eventName, listener, capture); + completion(); + }, capture, wantsUntrusted); + }); + }); + }, + + /** + * Like browserLoaded, but waits for an error page to appear. + * This explicitly deals with cases where the browser is not currently remote and a + * remoteness switch will occur before the error page is loaded, which is tricky + * because error pages don't fire 'regular' load events that we can rely on. + * + * @param {xul:browser} browser + * A xul:browser. + * + * @return {Promise} + * @resolves When an error page has been loaded in the browser. + */ + waitForErrorPage(browser) { + let waitForLoad = () => + this.waitForContentEvent(browser, "AboutNetErrorLoad", false, null, true); + + let win = browser.ownerDocument.defaultView; + let tab = win.gBrowser.getTabForBrowser(browser); + if (!tab || browser.isRemoteBrowser || !win.gMultiProcessBrowser) { + return waitForLoad(); + } + + // We're going to switch remoteness when loading an error page. We need to be + // quite careful in order to make sure we're adding the listener in time to + // get this event: + return new Promise((resolve, reject) => { + tab.addEventListener("TabRemotenessChange", function onTRC() { + tab.removeEventListener("TabRemotenessChange", onTRC); + waitForLoad().then(resolve, reject); + }); + }); + }, + + /** + * 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 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 CPOW element (for easier test-conversion). + * - 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 {Browser} browser + * Browser element, must not be null + * + * @returns {Promise} + * @resolves True if the mouse event was cancelled. + */ + synthesizeMouse(target, offsetX, offsetY, event, browser) + { + return new Promise((resolve, reject) => { + let mm = browser.messageManager; + mm.addMessageListener("Test:SynthesizeMouseDone", function mouseMsg(message) { + mm.removeMessageListener("Test:SynthesizeMouseDone", mouseMsg); + if (message.data.hasOwnProperty("defaultPrevented")) { + resolve(message.data.defaultPrevented); + } else { + reject(new Error(message.data.error)); + } + }); + + let cpowObject = null; + let targetFn = null; + if (typeof target == "function") { + targetFn = target.toString(); + target = null; + } else if (typeof target != "string") { + cpowObject = target; + target = null; + } + + mm.sendAsyncMessage("Test:SynthesizeMouse", + {target, targetFn, x: offsetX, y: offsetY, event: event}, + {object: cpowObject}); + }); + }, + + /** + * 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(); + } + }); + }); + }, + + /** + * 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, browser) + { + // Use a flag to indicate to center rather than having a separate message. + event.centered = true; + return BrowserTestUtils.synthesizeMouse(target, 0, 0, event, browser); + }, + + /** + * 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, browser) + { + return BrowserTestUtils.synthesizeMouse(null, offsetX, offsetY, event, browser); + }, + + /** + * Removes the given tab from its parent tabbrowser and + * waits until its final message has reached the parent. + */ + removeTab(tab, options = {}) { + let dontRemove = options && options.dontRemove; + + return new Promise(resolve => { + let {messageManager: mm, frameLoader} = tab.linkedBrowser; + mm.addMessageListener("SessionStore:update", function onMessage(msg) { + if (msg.targetFrameLoader == frameLoader && msg.data.isFinal) { + mm.removeMessageListener("SessionStore:update", onMessage); + resolve(); + } + }, true); + + if (!dontRemove && !tab.closing) { + tab.ownerDocument.defaultView.gBrowser.removeTab(tab); + } + }); + }, + + /** + * Crashes a remote browser 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. + * + * @returns (Promise) + * @resolves An Object with key-value pairs representing the data from the + * crash report's extra file (if applicable). + */ + crashBrowser: Task.async(function*(browser, shouldShowTabCrashPage=true) { + let extra = {}; + let KeyValueParser = {}; + if (AppConstants.MOZ_CRASHREPORTER) { + Cu.import("resource://gre/modules/KeyValueParser.jsm", KeyValueParser); + } + + 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); + } + } + + // This frame script is injected into the remote browser, and used to + // intentionally crash the tab. We crash by using js-ctypes and dereferencing + // a bad pointer. The crash should happen immediately upon loading this + // frame script. + let frame_script = () => { + const Cu = Components.utils; + Cu.import("resource://gre/modules/ctypes.jsm"); + + let dies = function() { + privateNoteIntentionalCrash(); + let zero = new ctypes.intptr_t(8); + let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t)); + badptr.contents + }; + + dump("\nEt tu, Brute?\n"); + dies(); + } + + let expectedPromises = []; + + let crashCleanupPromise = new Promise((resolve, reject) => { + let observer = (subject, topic, data) => { + if (topic != "ipc:content-shutdown") { + return reject("Received incorrect observer topic: " + topic); + } + if (!(subject instanceof Ci.nsIPropertyBag2)) { + return reject("Subject did not implement nsIPropertyBag2"); + } + // 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; + } + + let dumpID; + if ('nsICrashReporter' in Ci) { + dumpID = subject.getPropertyAsAString('dumpID'); + if (!dumpID) { + return reject("dumpID was not present despite crash reporting " + + "being enabled"); + } + } + + if (dumpID) { + let minidumpDirectory = getMinidumpDirectory(); + let extrafile = minidumpDirectory.clone(); + extrafile.append(dumpID + '.extra'); + if (extrafile.exists()) { + dump(`\nNo .extra file for dumpID: ${dumpID}\n`); + if (AppConstants.MOZ_CRASHREPORTER) { + extra = KeyValueParser.parseKeyValuePairsFromFile(extrafile); + } else { + dump('\nCrashReporter not enabled - will not return any extra data\n'); + } + } + + removeFile(minidumpDirectory, dumpID + '.dmp'); + removeFile(minidumpDirectory, dumpID + '.extra'); + } + + Services.obs.removeObserver(observer, 'ipc:content-shutdown'); + 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', false); + }); + + expectedPromises.push(crashCleanupPromise); + + if (shouldShowTabCrashPage) { + expectedPromises.push(new Promise((resolve, reject) => { + browser.addEventListener("AboutTabCrashedReady", function onCrash() { + browser.removeEventListener("AboutTabCrashedReady", onCrash, false); + dump("\nabout:tabcrashed loaded and ready\n"); + resolve(); + }, false, true); + })); + } + + // This frame script will crash the remote browser as soon as it is + // evaluated. + let mm = browser.messageManager; + mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", false); + + yield Promise.all(expectedPromises); + + if (shouldShowTabCrashPage) { + let gBrowser = browser.ownerDocument.defaultView.gBrowser; + let tab = gBrowser.getTabForBrowser(browser); + if (tab.getAttribute("crashed") != "true") { + throw new Error("Tab should be marked as crashed"); + } + } + + return extra; + }), + + /** + * 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.ownerDocument.defaultView.MutationObserver; + return new Promise(resolve => { + let mut = new MutationObserver(mutations => { + if ((!value && element.getAttribute(attr)) || + (value && element.getAttribute(attr) === value)) { + resolve(); + mut.disconnect(); + return; + } + }); + + 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 object is required to be + * passed to this function. + * + * @param {String} char + * A character for the keypress event that is sent to the browser. + * @param {Browser} browser + * Browser element, must not be null. + * + * @returns {Promise} + * @resolves True if the keypress event was synthesized. + */ + sendChar(char, browser) { + return new Promise(resolve => { + let seq = ++gSendCharCount; + let mm = browser.messageManager; + + mm.addMessageListener("Test:SendCharDone", function charMsg(message) { + if (message.data.seq != seq) + return; + + mm.removeMessageListener("Test:SendCharDone", charMsg); + resolve(message.data.result); + }); + + mm.sendAsyncMessage("Test:SendChar", { + char: char, + seq: seq + }); + }); + }, + + /** + * 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 object 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 {Browser} browser + * Browser element, must not be null. + * + * @returns {Promise} + */ + synthesizeKey(key, event, browser) { + return new Promise(resolve => { + let seq = ++gSynthesizeKeyCount; + let mm = browser.messageManager; + + mm.addMessageListener("Test:SynthesizeKeyDone", function keyMsg(message) { + if (message.data.seq != seq) + return; + + mm.removeMessageListener("Test:SynthesizeKeyDone", keyMsg); + resolve(); + }); + + mm.sendAsyncMessage("Test:SynthesizeKey", { key, event, seq }); + }); + }, + + /** + * 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 object is + * required to be passed to this function. + * + * @param {Object} event + * See the documentation available for EventUtils#synthesizeComposition. + * @param {Browser} browser + * Browser element, must not be null. + * + * @returns {Promise} + * @resolves False if the composition event could not be synthesized. + */ + synthesizeComposition(event, browser) { + return new Promise(resolve => { + let seq = ++gSynthesizeCompositionCount; + let mm = browser.messageManager; + + mm.addMessageListener("Test:SynthesizeCompositionDone", function compMsg(message) { + if (message.data.seq != seq) + return; + + mm.removeMessageListener("Test:SynthesizeCompositionDone", compMsg); + resolve(message.data.result); + }); + + mm.sendAsyncMessage("Test:SynthesizeComposition", { event, seq }); + }); + }, + + /** + * 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 object is required to be passed to this function. + * + * @param {Object} event + * See the documentation available for EventUtils#synthesizeCompositionChange. + * @param {Browser} browser + * Browser element, must not be null. + * + * @returns {Promise} + */ + synthesizeCompositionChange(event, browser) { + return new Promise(resolve => { + let seq = ++gSynthesizeCompositionChangeCount; + let mm = browser.messageManager; + + mm.addMessageListener("Test:SynthesizeCompositionChangeDone", function compMsg(message) { + if (message.data.seq != seq) + return; + + mm.removeMessageListener("Test:SynthesizeCompositionChangeDone", compMsg); + resolve(); + }); + + mm.sendAsyncMessage("Test:SynthesizeCompositionChange", { event, seq }); + }); + }, + + /** + * 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. The + * function can be a generator. + * @param interval + * The time interval to poll the condition function. Defaults + * to 100ms. + * @param attempts + * 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. + */ + waitForCondition(condition, msg, interval=100, maxTries=50) { + return new Promise((resolve, reject) => { + let tries = 0; + let intervalID = setInterval(Task.async(function* () { + if (tries >= maxTries) { + clearInterval(intervalID); + msg += ` - timed out after ${maxTries} tries.`; + reject(msg); + return; + } + + let conditionPassed = false; + try { + conditionPassed = yield condition(); + } catch(e) { + msg += ` - threw exception: ${e}`; + clearInterval(intervalID); + reject(msg); + return; + } + + if (conditionPassed) { + clearInterval(intervalID); + resolve(); + } + tries++; + }), interval); + }); + }, + + /** + * Waits for a <xul:notification> with a particular value to appear + * for the <xul:notificationbox> of the passed in browser. + * + * @param tabbrowser (<xul:tabbrowser>) + * The gBrowser that hosts the browser that should show + * the notification. For most tests, this will probably be + * gBrowser. + * @param browser (<xul:browser>) + * The browser that should be showing the notification. + * @param notificationValue (string) + * 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 win (<xul:window>) + * The browser window in whose global notificationbox the + * notification is expected to appear. + * @param notificationValue (string) + * 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) { + let notificationBox = + win.document.getElementById("high-priority-global-notificationbox"); + return this.waitForNotificationInNotificationBox(notificationBox, + notificationValue); + }, + + waitForNotificationInNotificationBox(notificationBox, notificationValue) { + return new Promise((resolve) => { + let check = (event) => { + return event.target.value == notificationValue; + }; + + BrowserTestUtils.waitForEvent(notificationBox, "AlertActive", + false, check).then((event) => { + // The originalTarget of the AlertActive on a notificationbox + // will be the notification itself. + resolve(event.originalTarget); + }); + }); + }, + + /** + * Returns a Promise that will resolve once MozAfterPaint + * has been fired in the content of a browser. + * + * @param browser (<xul:browser>) + * The browser for which we're waiting for the MozAfterPaint + * event to occur in. + * @returns Promise + */ + contentPainted(browser) { + return ContentTask.spawn(browser, null, function*() { + return new Promise((resolve) => { + addEventListener("MozAfterPaint", function onPaint() { + removeEventListener("MozAfterPaint", onPaint); + resolve(); + }) + }); + }); + }, + + _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 registerCleanupFunction (Function) + * 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 aboutModule (String) + * The name of the about page. + * @param pageURI (String) + * The URI the about: page should point to. + * @param flags (Number) + * The nsIAboutModule flags to use for registration. + * @returns 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)); + }, + + *_removeAboutPageRegistrations() { + for (let aboutModule of this._knownAboutPages) { + yield this.unregisterAboutPage(aboutModule); + } + Services.ppmm.removeDelayedProcessScript(kAboutPageRegistrationContentScript); + }, +}; diff --git a/testing/mochitest/BrowserTestUtils/ContentTask.jsm b/testing/mochitest/BrowserTestUtils/ContentTask.jsm new file mode 100644 index 000000000..a52dd90d3 --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/ContentTask.jsm @@ -0,0 +1,128 @@ +/* -*- 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"; + +this.EXPORTED_SYMBOLS = [ + "ContentTask" +]; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const FRAME_SCRIPT = "chrome://mochikit/content/tests/BrowserTestUtils/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. + */ +this.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: id, + runnable: task.toString(), + arg: 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.ok(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); + } + }, +}; + +Services.mm.addMessageListener("content-task:complete", ContentMessageListener); +Services.mm.addMessageListener("content-task:test-result", ContentMessageListener); +Services.mm.addMessageListener("content-task:test-info", ContentMessageListener); diff --git a/testing/mochitest/BrowserTestUtils/ContentTaskUtils.jsm b/testing/mochitest/BrowserTestUtils/ContentTaskUtils.jsm new file mode 100644 index 000000000..27da0de22 --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/ContentTaskUtils.jsm @@ -0,0 +1,120 @@ +/* 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. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "ContentTaskUtils", +]; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/Timer.jsm"); + +this.ContentTaskUtils = { + /** + * 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 interval + * The time interval to poll the condition function. Defaults + * to 100ms. + * @param attempts + * 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. + */ + waitForCondition(condition, msg, interval=100, maxTries=50) { + return new Promise((resolve, reject) => { + let tries = 0; + let intervalID = setInterval(() => { + if (tries >= maxTries) { + clearInterval(intervalID); + msg += ` - timed out after ${maxTries} tries.`; + reject(msg); + return; + } + + let conditionPassed = false; + try { + conditionPassed = condition(); + } catch(e) { + msg += ` - threw exception: ${e}`; + clearInterval(intervalID); + reject(msg); + return; + } + + if (conditionPassed) { + clearInterval(intervalID); + resolve(); + } + tries++; + }, interval); + }); + }, + + /** + * 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) => { + subject.addEventListener(eventName, function listener(event) { + try { + if (checkFn && !checkFn(event)) { + return; + } + subject.removeEventListener(eventName, listener, capture); + resolve(event); + } catch (ex) { + try { + subject.removeEventListener(eventName, listener, capture); + } catch (ex2) { + // Maybe the provided object does not support removeEventListener. + } + reject(ex); + } + }, capture, wantsUntrusted); + }); + }, +}; 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 000000000..974f084ea --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/content/content-about-page-utils.js @@ -0,0 +1,76 @@ +"use strict"; + +var {classes: Cc, interfaces: Ci, utils: Cu, manager: Cm} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); + +function AboutPage(aboutHost, chromeURL, uriFlags) { + this.chromeURL = chromeURL; + this.aboutHost = aboutHost; + this.classID = Components.ID(generateUUID().number); + this.description = "BrowserTestUtils: " + aboutHost; + this.uriFlags = uriFlags; +} + +AboutPage.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]), + getURIFlags(aURI) { // eslint-disable-line no-unused-vars + return this.uriFlags; + }, + + newChannel(aURI, aLoadInfo) { + let newURI = Services.io.newURI(this.chromeURL, null, null); + 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(outer, iid) { + if (outer !== null) { + throw Cr.NS_ERROR_NO_AGGREGATION; + } + 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 000000000..3eb5d3d01 --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/content/content-task.js @@ -0,0 +1,71 @@ +/* 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"; + +var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://testing-common/ContentTaskUtils.jsm", this); +const AssertCls = Cu.import("resource://testing-common/Assert.jsm", null).Assert; + +addMessageListener("content-task:spawn", 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: 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("content-task:test-todo", {id, expr, name}); + } + + function info(name) { + sendAsyncMessage("content-task:test-info", {id, name}); + } + + try { + let runnablestr = ` + (() => { + return (${source}); + })();` + + let runnable = eval(runnablestr); + let iterator = runnable.call(this, msg.data.arg); + Task.spawn(iterator).then((val) => { + sendAsyncMessage("content-task:complete", { + id: id, + result: val, + }); + }, (e) => { + sendAsyncMessage("content-task:complete", { + id: id, + error: e.toString(), + }); + }); + } catch (e) { + sendAsyncMessage("content-task:complete", { + id: id, + error: e.toString(), + }); + } +}); diff --git a/testing/mochitest/BrowserTestUtils/content/content-utils.js b/testing/mochitest/BrowserTestUtils/content/content-utils.js new file mode 100644 index 000000000..c1e56364c --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/content/content-utils.js @@ -0,0 +1,16 @@ +/* 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"; + +var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +addEventListener("load", function(event) { + let subframe = event.target != content.document; + sendAsyncMessage("browser-test-utils:loadEvent", + {subframe: subframe, url: event.target.documentURI}); +}, true); + diff --git a/testing/mochitest/BrowserTestUtils/moz.build b/testing/mochitest/BrowserTestUtils/moz.build new file mode 100644 index 000000000..25ef72d55 --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +TESTING_JS_MODULES += [ + 'BrowserTestUtils.jsm', + 'ContentTask.jsm', + 'ContentTaskUtils.jsm', +] diff --git a/testing/mochitest/Makefile.in b/testing/mochitest/Makefile.in new file mode 100644 index 000000000..519d9cabe --- /dev/null +++ b/testing/mochitest/Makefile.in @@ -0,0 +1,36 @@ +# +# 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/. + + +_DEST_DIR = $(DEPTH)/_tests/$(relativesrcdir) + +include $(topsrcdir)/config/rules.mk +# We're installing to _tests/testing/mochitest, so this is the depth +# necessary for relative objdir paths. +TARGET_DEPTH = ../../.. +include $(topsrcdir)/build/automation-build.mk + +libs:: + (cd $(DIST)/xpi-stage && tar $(TAR_CREATE_FLAGS) - mochijar) | (cd $(_DEST_DIR) && tar -xf -) + +$(_DEST_DIR): + $(NSINSTALL) -D $@ + +# On Android only, include a release signed Robocop APK in the test package. +ifeq ($(MOZ_BUILD_APP),mobile/android) +include $(topsrcdir)/config/android-common.mk + +ifndef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE +robocop_apk := $(topobjdir)/mobile/android/tests/browser/robocop/robocop-debug-unsigned-unaligned.apk +else +robocop_apk := $(topobjdir)/gradle/build/mobile/android/app/outputs/apk/app-automation-debug-androidTest-unaligned.apk +endif + +stage-package-android: + $(NSINSTALL) -D $(_DEST_DIR) + $(call RELEASE_SIGN_ANDROID_APK,$(robocop_apk),$(_DEST_DIR)/robocop.apk) + +stage-package: stage-package-android +endif diff --git a/testing/mochitest/MochiKit/Async.js b/testing/mochitest/MochiKit/Async.js new file mode 100644 index 000000000..8ffaad259 --- /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 000000000..3a91e31e3 --- /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 000000000..ac9e4151a --- /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 000000000..539b09d37 --- /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 000000000..caff07ec2 --- /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 000000000..1b517b3a5 --- /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 000000000..b23a5ecd3 --- /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 000000000..8890bd573 --- /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 000000000..bb3767de9 --- /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 000000000..4d0065bef --- /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 000000000..b3aed964c --- /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 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/MochiKit/LoggingPane.js b/testing/mochitest/MochiKit/LoggingPane.js new file mode 100644 index 000000000..8ecf410a7 --- /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 000000000..d0935e365 --- /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 000000000..3f8654a98 --- /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 000000000..529f1e722 --- /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 000000000..74199c170 --- /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 000000000..2bee90b5d --- /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 000000000..6abf6d717 --- /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 000000000..632356a43 --- /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 000000000..bf8b3dfc3 --- /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 000000000..4c58c3947 --- /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/MochiKit/packed.js b/testing/mochitest/MochiKit/packed.js new file mode 100644 index 000000000..7dd8d1af7 --- /dev/null +++ b/testing/mochitest/MochiKit/packed.js @@ -0,0 +1,6112 @@ +/*** + + MochiKit.MochiKit 1.4 : PACKED VERSION + + THIS FILE IS AUTOMATICALLY GENERATED. If creating patches, please + diff against the source tree, not this file. + + 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"; +MochiKit.Base.update=function(_1,_2){ +if(_1===null){ +_1={}; +} +for(var i=1;i<arguments.length;i++){ +var o=arguments[i]; +if(typeof (o)!="undefined"&&o!==null){ +for(var k in o){ +_1[k]=o[k]; +} +} +} +return _1; +}; +MochiKit.Base.update(MochiKit.Base,{__repr__:function(){ +return "["+this.NAME+" "+this.VERSION+"]"; +},toString:function(){ +return this.__repr__(); +},camelize:function(_6){ +var _7=_6.split("-"); +var cc=_7[0]; +for(var i=1;i<_7.length;i++){ +cc+=_7[i].charAt(0).toUpperCase()+_7[i].substring(1); +} +return cc; +},counter:function(n){ +if(arguments.length===0){ +n=1; +} +return function(){ +return n++; +}; +},clone:function(_b){ +var me=arguments.callee; +if(arguments.length==1){ +me.prototype=_b; +return new me(); +} +},_flattenArray:function(_d,_e){ +for(var i=0;i<_e.length;i++){ +var o=_e[i]; +if(o instanceof Array){ +arguments.callee(_d,o); +}else{ +_d.push(o); +} +} +return _d; +},flattenArray:function(lst){ +return MochiKit.Base._flattenArray([],lst); +},flattenArguments:function(lst){ +var res=[]; +var m=MochiKit.Base; +var _15=m.extend(null,arguments); +while(_15.length){ +var o=_15.shift(); +if(o&&typeof (o)=="object"&&typeof (o.length)=="number"){ +for(var i=o.length-1;i>=0;i--){ +_15.unshift(o[i]); +} +}else{ +res.push(o); +} +} +return res; +},extend:function(_18,obj,_1a){ +if(!_1a){ +_1a=0; +} +if(obj){ +var l=obj.length; +if(typeof (l)!="number"){ +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(!_18){ +_18=[]; +} +for(var i=_1a;i<l;i++){ +_18.push(obj[i]); +} +} +return _18; +},updatetree:function(_1d,obj){ +if(_1d===null){ +_1d={}; +} +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 (_1d[k])=="object"&&typeof (v)=="object"){ +arguments.callee(_1d[k],v); +}else{ +_1d[k]=v; +} +} +} +} +return _1d; +},setdefault:function(_23,obj){ +if(_23===null){ +_23={}; +} +for(var i=1;i<arguments.length;i++){ +var o=arguments[i]; +for(var k in o){ +if(!(k in _23)){ +_23[k]=o[k]; +} +} +} +return _23; +},keys:function(obj){ +var _29=[]; +for(var _2a in obj){ +_29.push(_2a); +} +return _29; +},values:function(obj){ +var _2c=[]; +for(var _2d in obj){ +_2c.push(obj[_2d]); +} +return _2c; +},items:function(obj){ +var _2f=[]; +var e; +for(var _31 in obj){ +var v; +try{ +v=obj[_31]; +} +catch(e){ +continue; +} +_2f.push([_31,v]); +} +return _2f; +},_newNamedError:function(_33,_34,_35){ +_35.prototype=new MochiKit.Base.NamedError(_33.NAME+"."+_34); +_33[_34]=_35; +},operator:{truth:function(a){ +return !!a; +},lognot:function(a){ +return !a; +},identity:function(a){ +return a; +},not:function(a){ +return ~a; +},neg:function(a){ +return -a; +},add:function(a,b){ +return a+b; +},sub:function(a,b){ +return a-b; +},div:function(a,b){ +return a/b; +},mod:function(a,b){ +return a%b; +},mul:function(a,b){ +return a*b; +},and:function(a,b){ +return a&b; +},or:function(a,b){ +return a|b; +},xor:function(a,b){ +return a^b; +},lshift:function(a,b){ +return a<<b; +},rshift:function(a,b){ +return a>>b; +},zrshift:function(a,b){ +return a>>>b; +},eq:function(a,b){ +return a==b; +},ne:function(a,b){ +return a!=b; +},gt:function(a,b){ +return a>b; +},ge:function(a,b){ +return a>=b; +},lt:function(a,b){ +return a<b; +},le:function(a,b){ +return a<=b; +},seq:function(a,b){ +return a===b; +},sne:function(a,b){ +return a!==b; +},ceq:function(a,b){ +return MochiKit.Base.compare(a,b)===0; +},cne:function(a,b){ +return MochiKit.Base.compare(a,b)!==0; +},cgt:function(a,b){ +return MochiKit.Base.compare(a,b)==1; +},cge:function(a,b){ +return MochiKit.Base.compare(a,b)!=-1; +},clt:function(a,b){ +return MochiKit.Base.compare(a,b)==-1; +},cle:function(a,b){ +return MochiKit.Base.compare(a,b)!=1; +},logand:function(a,b){ +return a&&b; +},logor:function(a,b){ +return a||b; +},contains:function(a,b){ +return b in a; +}},forwardCall:function(_73){ +return function(){ +return this[_73].apply(this,arguments); +}; +},itemgetter:function(_74){ +return function(arg){ +return arg[_74]; +}; +},typeMatcher:function(){ +var _76={}; +for(var i=0;i<arguments.length;i++){ +var typ=arguments[i]; +_76[typ]=typ; +} +return function(){ +for(var i=0;i<arguments.length;i++){ +if(!(typeof (arguments[i]) in _76)){ +return false; +} +} +return true; +}; +},isNull:function(){ +for(var i=0;i<arguments.length;i++){ +if(arguments[i]!==null){ +return false; +} +} +return true; +},isUndefinedOrNull:function(){ +for(var i=0;i<arguments.length;i++){ +var o=arguments[i]; +if(!(typeof (o)=="undefined"||o===null)){ +return false; +} +} +return true; +},isEmpty:function(obj){ +return !MochiKit.Base.isNotEmpty.apply(this,arguments); +},isNotEmpty:function(obj){ +for(var i=0;i<arguments.length;i++){ +var o=arguments[i]; +if(!(o&&o.length)){ +return false; +} +} +return true; +},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; +},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; +},xmap:function(fn){ +if(fn===null){ +return MochiKit.Base.extend(null,arguments,1); +} +var _87=[]; +for(var i=1;i<arguments.length;i++){ +_87.push(fn(arguments[i])); +} +return _87; +},map:function(fn,lst){ +var m=MochiKit.Base; +var itr=MochiKit.Iter; +var _8d=m.isArrayLike; +if(arguments.length<=2){ +if(!_8d(lst)){ +if(itr){ +lst=itr.list(lst); +if(fn===null){ +return lst; +} +}else{ +throw new TypeError("Argument not an array-like and MochiKit.Iter not present"); +} +} +if(fn===null){ +return m.extend(null,lst); +} +var _8e=[]; +for(var i=0;i<lst.length;i++){ +_8e.push(fn(lst[i])); +} +return _8e; +}else{ +if(fn===null){ +fn=Array; +} +var _90=null; +for(i=1;i<arguments.length;i++){ +if(!_8d(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"); +} +} +var l=arguments[i].length; +if(_90===null||_90>l){ +_90=l; +} +} +_8e=[]; +for(i=0;i<_90;i++){ +var _92=[]; +for(var j=1;j<arguments.length;j++){ +_92.push(arguments[j][i]); +} +_8e.push(fn.apply(this,_92)); +} +return _8e; +} +},xfilter:function(fn){ +var _95=[]; +if(fn===null){ +fn=MochiKit.Base.operator.truth; +} +for(var i=1;i<arguments.length;i++){ +var o=arguments[i]; +if(fn(o)){ +_95.push(o); +} +} +return _95; +},filter:function(fn,lst,_9a){ +var _9b=[]; +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"){ +return Array.prototype.filter.call(lst,fn,_9a); +}else{ +if(typeof (_9a)=="undefined"||_9a===null){ +for(var i=0;i<lst.length;i++){ +var o=lst[i]; +if(fn(o)){ +_9b.push(o); +} +} +}else{ +for(i=0;i<lst.length;i++){ +o=lst[i]; +if(fn.call(_9a,o)){ +_9b.push(o); +} +} +} +} +return _9b; +},_wrapDumbFunction:function(_9f){ +return function(){ +switch(arguments.length){ +case 0: +return _9f(); +case 1: +return _9f(arguments[0]); +case 2: +return _9f(arguments[0],arguments[1]); +case 3: +return _9f(arguments[0],arguments[1],arguments[2]); +} +var _a0=[]; +for(var i=0;i<arguments.length;i++){ +_a0.push("arguments["+i+"]"); +} +return eval("(func("+_a0.join(",")+"))"); +}; +},methodcaller:function(_a2){ +var _a3=MochiKit.Base.extend(null,arguments,1); +if(typeof (_a2)=="function"){ +return function(obj){ +return _a2.apply(obj,_a3); +}; +}else{ +return function(obj){ +return obj[_a2].apply(obj,_a3); +}; +} +},method:function(_a6,_a7){ +var m=MochiKit.Base; +return m.bind.apply(this,m.extend([_a7,_a6],arguments,2)); +},compose:function(f1,f2){ +var _ab=[]; +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"); +} +_ab.push(fn); +} +return function(){ +var _af=arguments; +for(var i=_ab.length-1;i>=0;i--){ +_af=[_ab[i].apply(this,_af)]; +} +return _af[0]; +}; +},bind:function(_b1,_b2){ +if(typeof (_b1)=="string"){ +_b1=_b2[_b1]; +} +var _b3=_b1.im_func; +var _b4=_b1.im_preargs; +var _b5=_b1.im_self; +var m=MochiKit.Base; +if(typeof (_b1)=="function"&&typeof (_b1.apply)=="undefined"){ +_b1=m._wrapDumbFunction(_b1); +} +if(typeof (_b3)!="function"){ +_b3=_b1; +} +if(typeof (_b2)!="undefined"){ +_b5=_b2; +} +if(typeof (_b4)=="undefined"){ +_b4=[]; +}else{ +_b4=_b4.slice(); +} +m.extend(_b4,arguments,2); +var _b7=function(){ +var _b8=arguments; +var me=arguments.callee; +if(me.im_preargs.length>0){ +_b8=m.concat(me.im_preargs,_b8); +} +var _ba=me.im_self; +if(!_ba){ +_ba=this; +} +return me.im_func.apply(_ba,_b8); +}; +_b7.im_self=_b5; +_b7.im_func=_b3; +_b7.im_preargs=_b4; +return _b7; +},bindMethods:function(_bb){ +var _bc=MochiKit.Base.bind; +for(var k in _bb){ +var _be=_bb[k]; +if(typeof (_be)=="function"){ +_bb[k]=_bc(_be,_bb); +} +} +},registerComparator:function(_bf,_c0,_c1,_c2){ +MochiKit.Base.comparatorRegistry.register(_bf,_c0,_c1,_c2); +},_primitives:{"boolean":true,"string":true,"number":true},compare:function(a,b){ +if(a==b){ +return 0; +} +var _c5=(typeof (a)=="undefined"||a===null); +var _c6=(typeof (b)=="undefined"||b===null); +if(_c5&&_c6){ +return 0; +}else{ +if(_c5){ +return -1; +}else{ +if(_c6){ +return 1; +} +} +} +var m=MochiKit.Base; +var _c8=m._primitives; +if(!(typeof (a) in _c8&&typeof (b) in _c8)){ +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; +} +} +var _c9=m.repr; +throw new TypeError(_c9(a)+" and "+_c9(b)+" can not be compared"); +},compareDateLike:function(a,b){ +return MochiKit.Base.compare(a.getTime(),b.getTime()); +},compareArrayLike:function(a,b){ +var _ce=MochiKit.Base.compare; +var _cf=a.length; +var _d0=0; +if(_cf>b.length){ +_d0=1; +_cf=b.length; +}else{ +if(_cf<b.length){ +_d0=-1; +} +} +for(var i=0;i<_cf;i++){ +var cmp=_ce(a[i],b[i]); +if(cmp){ +return cmp; +} +} +return _d0; +},registerRepr:function(_d3,_d4,_d5,_d6){ +MochiKit.Base.reprRegistry.register(_d3,_d4,_d5,_d6); +},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 _d8=(o+""); +} +catch(e){ +return "["+typeof (o)+"]"; +} +if(typeof (o)=="function"){ +o=_d8.replace(/^\s+/,""); +var idx=o.indexOf("{"); +if(idx!=-1){ +o=o.substr(0,idx)+"{...}"; +} +} +return _d8; +},reprArrayLike:function(o){ +var m=MochiKit.Base; +return "["+m.map(m.repr,o).join(", ")+"]"; +},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"); +},reprNumber:function(o){ +return o+""; +},registerJSON:function(_de,_df,_e0,_e1){ +MochiKit.Base.jsonRegistry.register(_de,_df,_e0,_e1); +},evalJSON:function(){ +return eval("("+arguments[0]+")"); +},serializeJSON:function(o){ +var _e3=typeof (o); +if(_e3=="number"||_e3=="boolean"){ +return o+""; +}else{ +if(o===null){ +return "null"; +} +} +var m=MochiKit.Base; +var _e5=m.reprString; +if(_e3=="string"){ +return _e5(o); +} +var me=arguments.callee; +var _e7; +if(typeof (o.__json__)=="function"){ +_e7=o.__json__(); +if(o!==_e7){ +return me(_e7); +} +} +if(typeof (o.json)=="function"){ +_e7=o.json(); +if(o!==_e7){ +return me(_e7); +} +} +if(_e3!="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(", ")+"]"; +} +try{ +_e7=m.jsonRegistry.match(o); +if(o!==_e7){ +return me(_e7); +} +} +catch(e){ +if(e!=m.NotFound){ +throw e; +} +} +if(_e3=="undefined"){ +throw new TypeError("undefined can not be serialized as JSON"); +} +if(_e3=="function"){ +return null; +} +res=[]; +for(var k in o){ +var _ec; +if(typeof (k)=="number"){ +_ec="\""+k+"\""; +}else{ +if(typeof (k)=="string"){ +_ec=_e5(k); +}else{ +continue; +} +} +val=me(o[k]); +if(typeof (val)!="string"){ +continue; +} +res.push(_ec+":"+val); +} +return "{"+res.join(", ")+"}"; +},objEqual:function(a,b){ +return (MochiKit.Base.compare(a,b)===0); +},arrayEqual:function(_ef,arr){ +if(_ef.length!=arr.length){ +return false; +} +return (MochiKit.Base.compare(_ef,arr)===0); +},concat:function(){ +var _f1=[]; +var _f2=MochiKit.Base.extend; +for(var i=0;i<arguments.length;i++){ +_f2(_f1,arguments[i]); +} +return _f1; +},keyComparator:function(key){ +var m=MochiKit.Base; +var _f6=m.compare; +if(arguments.length==1){ +return function(a,b){ +return _f6(a[key],b[key]); +}; +} +var _f9=m.extend(null,arguments); +return function(a,b){ +var _fc=0; +for(var i=0;(_fc===0)&&(i<_f9.length);i++){ +var key=_f9[i]; +_fc=_f6(a[key],b[key]); +} +return _fc; +}; +},reverseKeyComparator:function(key){ +var _100=MochiKit.Base.keyComparator.apply(this,arguments); +return function(a,b){ +return _100(b,a); +}; +},partial:function(func){ +var m=MochiKit.Base; +return m.bind.apply(this,m.extend([func,undefined],arguments,1)); +},listMinMax:function(_105,lst){ +if(lst.length===0){ +return null; +} +var cur=lst[0]; +var _108=MochiKit.Base.compare; +for(var i=1;i<lst.length;i++){ +var o=lst[i]; +if(_108(o,cur)==_105){ +cur=o; +} +} +return cur; +},objMax:function(){ +return MochiKit.Base.listMinMax(1,arguments); +},objMin:function(){ +return MochiKit.Base.listMinMax(-1,arguments); +},findIdentical:function(lst,_10c,_10d,end){ +if(typeof (end)=="undefined"||end===null){ +end=lst.length; +} +if(typeof (_10d)=="undefined"||_10d===null){ +_10d=0; +} +for(var i=_10d;i<end;i++){ +if(lst[i]===_10c){ +return i; +} +} +return -1; +},mean:function(){ +var sum=0; +var m=MochiKit.Base; +var args=m.extend(null,arguments); +var _113=args.length; +while(args.length){ +var o=args.shift(); +if(o&&typeof (o)=="object"&&typeof (o.length)=="number"){ +_113+=o.length-1; +for(var i=o.length-1;i>=0;i--){ +sum+=o[i]; +} +}else{ +sum+=o; +} +} +if(_113<=0){ +throw new TypeError("mean() requires at least one argument"); +} +return sum/_113; +},median:function(){ +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 _117=data.length/2; +return (data[_117]+data[_117-1])/2; +}else{ +return data[(data.length-1)/2]; +} +},findValue:function(lst,_119,_11a,end){ +if(typeof (end)=="undefined"||end===null){ +end=lst.length; +} +if(typeof (_11a)=="undefined"||_11a===null){ +_11a=0; +} +var cmp=MochiKit.Base.compare; +for(var i=_11a;i<end;i++){ +if(cmp(lst[i],_119)===0){ +return i; +} +} +return -1; +},nodeWalk:function(node,_11f){ +var _120=[node]; +var _121=MochiKit.Base.extend; +while(_120.length){ +var res=_11f(_120.shift()); +if(res){ +_121(_120,res); +} +} +},nameFunctions:function(_123){ +var base=_123.NAME; +if(typeof (base)=="undefined"){ +base=""; +}else{ +base=base+"."; +} +for(var name in _123){ +var o=_123[name]; +if(typeof (o)=="function"&&typeof (o.NAME)=="undefined"){ +try{ +o.NAME=base+name; +} +catch(e){ +} +} +} +},queryString:function(_127,_128){ +if(typeof (MochiKit.DOM)!="undefined"&&arguments.length==1&&(typeof (_127)=="string"||(typeof (_127.nodeType)!="undefined"&&_127.nodeType>0))){ +var kv=MochiKit.DOM.formContents(_127); +_127=kv[0]; +_128=kv[1]; +}else{ +if(arguments.length==1){ +var o=_127; +_127=[]; +_128=[]; +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++){ +_127.push(k); +_128.push(v[i]); +} +}else{ +_127.push(k); +_128.push(v); +} +} +} +} +} +var rval=[]; +var len=Math.min(_127.length,_128.length); +var _130=MochiKit.Base.urlEncode; +for(var i=0;i<len;i++){ +v=_128[i]; +if(typeof (v)!="undefined"&&v!==null){ +rval.push(_130(_127[i])+"="+_130(v)); +} +} +return rval.join("&"); +},parseQueryString:function(_131,_132){ +var qstr=(_131[0]=="?")?_131.substring(1):_131; +var _134=qstr.replace(/\+/g,"%20").split(/(\&\;|\&\#38\;|\&|\&)/); +var o={}; +var _136; +if(typeof (decodeURIComponent)!="undefined"){ +_136=decodeURIComponent; +}else{ +_136=unescape; +} +if(_132){ +for(var i=0;i<_134.length;i++){ +var pair=_134[i].split("="); +if(pair.length!==2){ +continue; +} +var name=_136(pair[0]); +var arr=o[name]; +if(!(arr instanceof Array)){ +arr=[]; +o[name]=arr; +} +arr.push(_136(pair[1])); +} +}else{ +for(i=0;i<_134.length;i++){ +pair=_134[i].split("="); +if(pair.length!==2){ +continue; +} +o[_136(pair[0])]=_136(pair[1]); +} +} +return o; +}}); +MochiKit.Base.AdapterRegistry=function(){ +this.pairs=[]; +}; +MochiKit.Base.AdapterRegistry.prototype={register:function(name,_13c,wrap,_13e){ +if(_13e){ +this.pairs.unshift([name,_13c,wrap]); +}else{ +this.pairs.push([name,_13c,wrap]); +} +},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; +},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(_144,_145){ +if(!MochiKit.__export__){ +return; +} +var all=_145.EXPORT_TAGS[":all"]; +for(var i=0;i<all.length;i++){ +_144[all[i]]=_145[all[i]]; +} +}; +MochiKit.Base.__new__=function(){ +var m=this; +m.noop=m.operator.identity; +m.forward=m.forwardCall; +m.find=m.findValue; +if(typeof (encodeURIComponent)!="undefined"){ +m.urlEncode=function(_149){ +return encodeURIComponent(_149).replace(/\'/g,"%27"); +}; +}else{ +m.urlEncode=function(_14a){ +return escape(_14a).replace(/\+/g,"%2B").replace(/\"/g,"%22").rval.replace(/\'/g,"%27"); +}; +} +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")}); +m.NotFound=new m.NamedError("MochiKit.Base.NotFound"); +m.listMax=m.partial(m.listMinMax,1); +m.listMin=m.partial(m.listMinMax,-1); +m.isCallable=m.typeMatcher("function"); +m.isUndefined=m.typeMatcher("undefined"); +m.merge=m.partial(m.update,null); +m.zip=m.partial(m.map,null); +m.average=m.mean; +m.comparatorRegistry=new m.AdapterRegistry(); +m.registerComparator("dateLike",m.isDateLike,m.compareDateLike); +m.registerComparator("arrayLike",m.isArrayLike,m.compareArrayLike); +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); +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__(); +if(MochiKit.__export__){ +compare=MochiKit.Base.compare; +compose=MochiKit.Base.compose; +serializeJSON=MochiKit.Base.serializeJSON; +} +MochiKit.Base._exportSymbols(this,MochiKit.Base); +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__(); +},registerIteratorFactory:function(name,_14e,_14f,_150){ +MochiKit.Iter.iteratorRegistry.register(name,_14e,_14f,_150); +},iter:function(_151,_152){ +var self=MochiKit.Iter; +if(arguments.length==2){ +return self.takewhile(function(a){ +return a!=_152; +},_151); +} +if(typeof (_151.next)=="function"){ +return _151; +}else{ +if(typeof (_151.iter)=="function"){ +return _151.iter(); +} +} +try{ +return self.iteratorRegistry.match(_151); +} +catch(e){ +var m=MochiKit.Base; +if(e==m.NotFound){ +e=new TypeError(typeof (_151)+": "+m.repr(_151)+" is not iterable"); +} +throw e; +} +},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)}; +},cycle:function(p){ +var self=MochiKit.Iter; +var m=MochiKit.Base; +var lst=[]; +var _15c=self.iter(p); +return {repr:function(){ +return "cycle(...)"; +},toString:m.forwardCall("repr"),next:function(){ +try{ +var rval=_15c.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(); +} +}}; +},repeat:function(elem,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; +}}; +},next:function(_162){ +return _162.next(); +},izip:function(p,q){ +var m=MochiKit.Base; +var self=MochiKit.Iter; +var next=self.next; +var _168=m.map(self.iter,arguments); +return {repr:function(){ +return "izip(...)"; +},toString:m.forwardCall("repr"),next:function(){ +return m.map(next,_168); +}}; +},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; +} +} +return undefined; +}}; +},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; +} +} +return undefined; +}}; +},islice:function(seq){ +var self=MochiKit.Iter; +var m=MochiKit.Base; +seq=self.iter(seq); +var _174=0; +var stop=0; +var step=1; +var i=-1; +if(arguments.length==2){ +stop=arguments[1]; +}else{ +if(arguments.length==3){ +_174=arguments[1]; +stop=arguments[2]; +}else{ +_174=arguments[1]; +stop=arguments[2]; +step=arguments[3]; +} +} +return {repr:function(){ +return "islice("+["...",_174,stop,step].join(", ")+")"; +},toString:m.forwardCall("repr"),next:function(){ +var rval; +while(i<_174){ +rval=seq.next(); +i++; +} +if(_174>=stop){ +throw self.StopIteration; +} +_174+=step; +return rval; +}}; +},imap:function(fun,p,q){ +var m=MochiKit.Base; +var self=MochiKit.Iter; +var _17e=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,_17e)); +}}; +},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()); +}}; +},chain:function(p,q){ +var self=MochiKit.Iter; +var m=MochiKit.Base; +if(arguments.length==1){ +return self.iter(arguments[0]); +} +var _189=m.map(self.iter,arguments); +return {repr:function(){ +return "chain(...)"; +},toString:m.forwardCall("repr"),next:function(){ +while(_189.length>1){ +try{ +return _189[0].next(); +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +_189.shift(); +} +} +if(_189.length==1){ +var arg=_189.shift(); +this.next=m.bind("next",arg); +return this.next(); +} +throw self.StopIteration; +}}; +},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; +}}; +},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(_194,sync,_196){ +sync.pos[_194]=-1; +var m=MochiKit.Base; +var _198=m.listMin; +return {repr:function(){ +return "tee("+_194+", ...)"; +},toString:m.forwardCall("repr"),next:function(){ +var rval; +var i=sync.pos[_194]; +if(i==sync.max){ +rval=_196.next(); +sync.deque.push(rval); +sync.max+=1; +sync.pos[_194]+=1; +}else{ +rval=sync.deque[i-sync.min]; +sync.pos[_194]+=1; +if(i==sync.min&&_198(sync.pos)!=sync.min){ +sync.min+=1; +sync.deque.shift(); +} +} +return rval; +}}; +},tee:function(_19b,n){ +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; +_19b=self.iter(_19b); +var _tee=self._tee; +for(var i=0;i<n;i++){ +rval.push(_tee(i,sync,_19b)); +} +return rval; +},list:function(_1a2){ +var m=MochiKit.Base; +if(typeof (_1a2.slice)=="function"){ +return _1a2.slice(); +}else{ +if(m.isArrayLike(_1a2)){ +return m.concat(_1a2); +} +} +var self=MochiKit.Iter; +_1a2=self.iter(_1a2); +var rval=[]; +try{ +while(true){ +rval.push(_1a2.next()); +} +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +return rval; +} +return undefined; +},reduce:function(fn,_1a7,_1a8){ +var i=0; +var x=_1a8; +var self=MochiKit.Iter; +_1a7=self.iter(_1a7); +if(arguments.length<3){ +try{ +x=_1a7.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,_1a7.next()); +} +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +} +return x; +},range:function(){ +var _1ac=0; +var stop=0; +var step=1; +if(arguments.length==1){ +stop=arguments[0]; +}else{ +if(arguments.length==2){ +_1ac=arguments[0]; +stop=arguments[1]; +}else{ +if(arguments.length==3){ +_1ac=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&&_1ac>=stop)||(step<0&&_1ac<=stop)){ +throw MochiKit.Iter.StopIteration; +} +var rval=_1ac; +_1ac+=step; +return rval; +},repr:function(){ +return "range("+[_1ac,stop,step].join(", ")+")"; +},toString:MochiKit.Base.forwardCall("repr")}; +},sum:function(_1b0,_1b1){ +if(typeof (_1b1)=="undefined"||_1b1===null){ +_1b1=0; +} +var x=_1b1; +var self=MochiKit.Iter; +_1b0=self.iter(_1b0); +try{ +while(true){ +x+=_1b0.next(); +} +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +} +return x; +},exhaust:function(_1b4){ +var self=MochiKit.Iter; +_1b4=self.iter(_1b4); +try{ +while(true){ +_1b4.next(); +} +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +} +},forEach:function(_1b6,func,self){ +var m=MochiKit.Base; +if(arguments.length>2){ +func=m.bind(func,self); +} +if(m.isArrayLike(_1b6)){ +try{ +for(var i=0;i<_1b6.length;i++){ +func(_1b6[i]); +} +} +catch(e){ +if(e!=MochiKit.Iter.StopIteration){ +throw e; +} +} +}else{ +self=MochiKit.Iter; +self.exhaust(self.imap(func,_1b6)); +} +},every:function(_1bb,func){ +var self=MochiKit.Iter; +try{ +self.ifilterfalse(func,_1bb).next(); +return false; +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +return true; +} +},sorted:function(_1be,cmp){ +var rval=MochiKit.Iter.list(_1be); +if(arguments.length==1){ +cmp=MochiKit.Base.compare; +} +rval.sort(cmp); +return rval; +},reversed:function(_1c1){ +var rval=MochiKit.Iter.list(_1c1); +rval.reverse(); +return rval; +},some:function(_1c3,func){ +var self=MochiKit.Iter; +try{ +self.ifilter(func,_1c3).next(); +return true; +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +return false; +} +},iextend:function(lst,_1c7){ +if(MochiKit.Base.isArrayLike(_1c7)){ +for(var i=0;i<_1c7.length;i++){ +lst.push(_1c7[i]); +} +}else{ +var self=MochiKit.Iter; +_1c7=self.iter(_1c7); +try{ +while(true){ +lst.push(_1c7.next()); +} +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +} +} +return lst; +},groupby:function(_1ca,_1cb){ +var m=MochiKit.Base; +var self=MochiKit.Iter; +if(arguments.length<2){ +_1cb=m.operator.identity; +} +_1ca=self.iter(_1ca); +var pk=undefined; +var k=undefined; +var v; +function fetch(){ +v=_1ca.next(); +k=_1cb(v); +} +function eat(){ +var ret=v; +v=undefined; +return ret; +} +var _1d2=true; +var _1d3=m.compare; +return {repr:function(){ +return "groupby(...)"; +},next:function(){ +while(_1d3(k,pk)===0){ +fetch(); +if(_1d2){ +_1d2=false; +break; +} +} +pk=k; +return [k,{next:function(){ +if(v==undefined){ +fetch(); +} +if(_1d3(k,pk)!==0){ +throw self.StopIteration; +} +return eat(); +}}]; +}}; +},groupby_as_array:function(_1d4,_1d5){ +var m=MochiKit.Base; +var self=MochiKit.Iter; +if(arguments.length<2){ +_1d5=m.operator.identity; +} +_1d4=self.iter(_1d4); +var _1d8=[]; +var _1d9=true; +var _1da; +var _1db=m.compare; +while(true){ +try{ +var _1dc=_1d4.next(); +var key=_1d5(_1dc); +} +catch(e){ +if(e==self.StopIteration){ +break; +} +throw e; +} +if(_1d9||_1db(key,_1da)!==0){ +var _1de=[]; +_1d8.push([key,_1de]); +} +_1de.push(_1dc); +_1d9=false; +_1da=key; +} +return _1d8; +},arrayLikeIter:function(_1df){ +var i=0; +return {repr:function(){ +return "arrayLikeIter(...)"; +},toString:MochiKit.Base.forwardCall("repr"),next:function(){ +if(i>=_1df.length){ +throw MochiKit.Iter.StopIteration; +} +return _1df[i++]; +}}; +},hasIterateNext:function(_1e1){ +return (_1e1&&typeof (_1e1.iterateNext)=="function"); +},iterateNextIter:function(_1e2){ +return {repr:function(){ +return "iterateNextIter(...)"; +},toString:MochiKit.Base.forwardCall("repr"),next:function(){ +var rval=_1e2.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; +if(typeof (StopIteration)!="undefined"){ +this.StopIteration=StopIteration; +}else{ +this.StopIteration=new m.NamedError("StopIteration"); +} +this.iteratorRegistry=new m.AdapterRegistry(); +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__(); +if(MochiKit.__export__){ +reduce=MochiKit.Iter.reduce; +} +MochiKit.Base._exportSymbols(this,MochiKit.Iter); +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"]; +MochiKit.Logging.LogMessage=function(num,_1e6,info){ +this.num=num; +this.level=_1e6; +this.info=info; +this.timestamp=new Date(); +}; +MochiKit.Logging.LogMessage.prototype={repr:function(){ +var m=MochiKit.Base; +return "LogMessage("+m.map(m.repr,[this.num,this.level,this.info]).join(", ")+")"; +},toString:MochiKit.Base.forwardCall("repr")}; +MochiKit.Base.update(MochiKit.Logging,{logLevelAtLeast:function(_1e9){ +var self=MochiKit.Logging; +if(typeof (_1e9)=="string"){ +_1e9=self.LogLevel[_1e9]; +} +return function(msg){ +var _1ec=msg.level; +if(typeof (_1ec)=="string"){ +_1ec=self.LogLevel[_1ec]; +} +return _1ec>=_1e9; +}; +},isLogMessage:function(){ +var _1ed=MochiKit.Logging.LogMessage; +for(var i=0;i<arguments.length;i++){ +if(!(arguments[i] instanceof _1ed)){ +return false; +} +} +return true; +},compareLogMessage:function(a,b){ +return MochiKit.Base.compare([a.level,a.info],[b.level,b.info]); +},alertListener:function(msg){ +alert("num: "+msg.num+"\nlevel: "+msg.level+"\ninfo: "+msg.info.join(" ")); +}}); +MochiKit.Logging.Logger=function(_1f2){ +this.counter=0; +if(typeof (_1f2)=="undefined"||_1f2===null){ +_1f2=-1; +} +this.maxSize=_1f2; +this._messages=[]; +this.listeners={}; +this.useNativeConsole=false; +}; +MochiKit.Logging.Logger.prototype={clear:function(){ +this._messages.splice(0,this._messages.length); +},logToConsole:function(msg){ +if(typeof (window)!="undefined"&&window.console&&window.console.log){ +window.console.log(msg.replace(/%/g,"\uff05")); +}else{ +if(typeof (opera)!="undefined"&&opera.postError){ +opera.postError(msg); +}else{ +if(typeof (printfire)=="function"){ +printfire(msg); +}else{ +if(typeof (Debug)!="undefined"&&Debug.writeln){ +Debug.writeln(msg); +}else{ +if(typeof (debug)!="undefined"&&debug.trace){ +debug.trace(msg); +} +} +} +} +} +},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); +} +},addListener:function(_1f7,_1f8,_1f9){ +if(typeof (_1f8)=="string"){ +_1f8=MochiKit.Logging.logLevelAtLeast(_1f8); +} +var _1fa=[_1f8,_1f9]; +_1fa.ident=_1f7; +this.listeners[_1f7]=_1fa; +},removeListener:function(_1fb){ +delete this.listeners[_1fb]; +},baseLog:function(_1fc,_1fd){ +var msg=new MochiKit.Logging.LogMessage(this.counter,_1fc,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(); +} +},getMessages:function(_1ff){ +var _200=0; +if(!(typeof (_1ff)=="undefined"||_1ff===null)){ +_200=Math.max(0,this._messages.length-_1ff); +} +return this._messages.slice(_200); +},getMessageText:function(_201){ +if(typeof (_201)=="undefined"||_201===null){ +_201=30; +} +var _202=this.getMessages(_201); +if(_202.length){ +var lst=map(function(m){ +return "\n ["+m.num+"] "+m.level+": "+m.info.join(" "); +},_202); +lst.unshift("LAST "+_202.length+" MESSAGES:"); +return lst.join(""); +} +return ""; +},debuggingBookmarklet:function(_205){ +if(typeof (MochiKit.LoggingPane)=="undefined"){ +alert(this.getMessageText()); +}else{ +MochiKit.LoggingPane.createLoggingPane(_205||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 _207=m.partial; +var _208=this.Logger; +var _209=_208.prototype.baseLog; +m.update(this.Logger.prototype,{debug:_207(_209,"DEBUG"),log:_207(_209,"INFO"),error:_207(_209,"ERROR"),fatal:_207(_209,"FATAL"),warning:_207(_209,"WARNING")}); +var self=this; +var _20b=function(name){ +return function(){ +self.logger[name].apply(self.logger,arguments); +}; +}; +this.log=_20b("log"); +this.logError=_20b("error"); +this.logDebug=_20b("debug"); +this.logFatal=_20b("fatal"); +this.logWarning=_20b("warning"); +this.logger=new _208(); +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"){ +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); +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__(); +}; +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}))?)?)?)?)?/; +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,_213,day,hour,min,sec,msec; +year=parseInt(res[1],10); +if(typeof (res[2])=="undefined"||res[2]===""){ +return new Date(year); +} +_213=parseInt(res[2],10)-1; +day=parseInt(res[3],10); +if(typeof (res[4])=="undefined"||res[4]===""){ +return new Date(year,_213,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*parseFloat("0."+res[7])); +}else{ +msec=0; +} +if((typeof (res[8])=="undefined"||res[8]==="")&&(typeof (res[9])=="undefined"||res[9]==="")){ +return new Date(year,_213,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,_213,day,hour,min,sec,msec)-ofs); +}; +MochiKit.DateTime.toISOTime=function(date,_21b){ +if(typeof (date)=="undefined"||date===null){ +return null; +} +var hh=date.getHours(); +var mm=date.getMinutes(); +var ss=date.getSeconds(); +var lst=[((_21b&&(hh<10))?"0"+hh:hh),((mm<10)?"0"+mm:mm),((ss<10)?"0"+ss:ss)]; +return lst.join(":"); +}; +MochiKit.DateTime.toISOTimestamp=function(date,_221){ +if(typeof (date)=="undefined"||date===null){ +return null; +} +var sep=_221?"T":" "; +var foot=_221?"Z":""; +if(_221){ +date=new Date(date.getTime()+(date.getTimezoneOffset()*60000)); +} +return MochiKit.DateTime.toISODate(date)+sep+MochiKit.DateTime.toISOTime(date,_221)+foot; +}; +MochiKit.DateTime.toISODate=function(date){ +if(typeof (date)=="undefined"||date===null){ +return null; +} +var _225=MochiKit.DateTime._padTwo; +return [date.getFullYear(),_225(date.getMonth()+1),_225(date.getDate())].join("-"); +}; +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.toPaddedAmericanDate=function(d){ +if(typeof (d)=="undefined"||d===null){ +return null; +} +var _22a=MochiKit.DateTime._padTwo; +return [_22a(d.getMonth()+1),_22a(d.getDate()),d.getFullYear()].join("/"); +}; +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(){ +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){ +} +} +} +}; +MochiKit.DateTime.__new__(); +if(typeof (MochiKit.Base)!="undefined"){ +MochiKit.Base._exportSymbols(this,MochiKit.DateTime); +}else{ +(function(_22f,_230){ +if((typeof (JSAN)=="undefined"&&typeof (dojo)=="undefined")||(MochiKit.__export__===false)){ +var all=_230.EXPORT_TAGS[":all"]; +for(var i=0;i<all.length;i++){ +_22f[all[i]]=_230[all[i]]; +} +} +})(this,MochiKit.DateTime); +} +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(_233,_234,_235,_236,_237,_238,_239,_23a,_23b){ +return function(num){ +num=parseFloat(num); +if(typeof (num)=="undefined"||num===null||isNaN(num)){ +return _233; +} +var _23d=_234; +var _23e=_235; +if(num<0){ +num=-num; +}else{ +_23d=_23d.replace(/-/,""); +} +var me=arguments.callee; +var fmt=MochiKit.Format.formatLocale(_236); +if(_237){ +num=num*100; +_23e=fmt.percent+_23e; +} +num=MochiKit.Format.roundToFixed(num,_238); +var _241=num.split(/\./); +var _242=_241[0]; +var frac=(_241.length==1)?"":_241[1]; +var res=""; +while(_242.length<_239){ +_242="0"+_242; +} +if(_23a){ +while(_242.length>_23a){ +var i=_242.length-_23a; +res=fmt.separator+_242.substring(i,_242.length)+res; +_242=_242.substring(0,i); +} +} +res=_242+res; +if(_238>0){ +while(frac.length<_23b){ +frac=frac+"0"; +} +res=res+fmt.decimal+frac; +} +return _23d+res+_23e; +}; +}; +MochiKit.Format.numberFormatter=function(_246,_247,_248){ +if(typeof (_247)=="undefined"){ +_247=""; +} +var _249=_246.match(/((?:[0#]+,)?[0#]+)(?:\.([0#]+))?(%)?/); +if(!_249){ +throw TypeError("Invalid pattern"); +} +var _24a=_246.substr(0,_249.index); +var _24b=_246.substr(_249.index+_249[0].length); +if(_24a.search(/-/)==-1){ +_24a=_24a+"-"; +} +var _24c=_249[1]; +var frac=(typeof (_249[2])=="string"&&_249[2]!="")?_249[2]:""; +var _24e=(typeof (_249[3])=="string"&&_249[3]!=""); +var tmp=_24c.split(/,/); +var _250; +if(typeof (_248)=="undefined"){ +_248="default"; +} +if(tmp.length==1){ +_250=null; +}else{ +_250=tmp[1].length; +} +var _251=_24c.length-_24c.replace(/0/g,"").length; +var _252=frac.length-frac.replace(/0/g,"").length; +var _253=frac.length; +var rval=MochiKit.Format._numberFormatter(_247,_24a,_24b,_248,_24e,_253,_251,_250,_252); +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; +}; +MochiKit.Format.formatLocale=function(_258){ +if(typeof (_258)=="undefined"||_258===null){ +_258="default"; +} +if(typeof (_258)=="string"){ +var rval=MochiKit.Format.LOCALE[_258]; +if(typeof (rval)=="string"){ +rval=arguments.callee(rval); +MochiKit.Format.LOCALE[_258]=rval; +} +return rval; +}else{ +return _258; +} +}; +MochiKit.Format.twoDigitAverage=function(_25a,_25b){ +if(_25b){ +var res=_25a/_25b; +if(!isNaN(res)){ +return MochiKit.Format.twoDigitFloat(_25a/_25b); +} +} +return "0"; +}; +MochiKit.Format.twoDigitFloat=function(_25d){ +var sign=(_25d<0?"-":""); +var s=Math.floor(Math.abs(_25d)*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; +} +} +}; +MochiKit.Format.lstrip=function(str,_263){ +str=str+""; +if(typeof (str)!="string"){ +return null; +} +if(!_263){ +return str.replace(/^\s+/,""); +}else{ +return str.replace(new RegExp("^["+_263+"]+"),""); +} +}; +MochiKit.Format.rstrip=function(str,_265){ +str=str+""; +if(typeof (str)!="string"){ +return null; +} +if(!_265){ +return str.replace(/\s+$/,""); +}else{ +return str.replace(new RegExp("["+_265+"]+$"),""); +} +}; +MochiKit.Format.strip=function(str,_267){ +var self=MochiKit.Format; +return self.rstrip(self.lstrip(str,_267),_267); +}; +MochiKit.Format.truncToFixed=function(_269,_26a){ +_269=Math.floor(_269*Math.pow(10,_26a)); +var res=(_269*Math.pow(10,-_26a)).toFixed(_26a); +if(res.charAt(0)=="."){ +res="0"+res; +} +return res; +}; +MochiKit.Format.roundToFixed=function(_26c,_26d){ +return MochiKit.Format.truncToFixed(_26c+0.5*Math.pow(10,-_26d),_26d); +}; +MochiKit.Format.percentFormat=function(_26e){ +return MochiKit.Format.twoDigitFloat(100*_26e)+"%"; +}; +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(){ +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){ +} +} +} +}; +MochiKit.Format.__new__(); +if(typeof (MochiKit.Base)!="undefined"){ +MochiKit.Base._exportSymbols(this,MochiKit.Format); +}else{ +(function(_273,_274){ +if((typeof (JSAN)=="undefined"&&typeof (dojo)=="undefined")||(MochiKit.__export__===false)){ +var all=_274.EXPORT_TAGS[":all"]; +for(var i=0;i<all.length;i++){ +_273[all[i]]=_274[all[i]]; +} +} +})(this,MochiKit.Format); +} +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__(); +}; +MochiKit.Async.Deferred=function(_277){ +this.chain=[]; +this.id=this._nextId(); +this.fired=-1; +this.paused=0; +this.results=[null,null]; +this.canceller=_277; +this.silentlyCancelled=false; +this.chained=false; +}; +MochiKit.Async.Deferred.prototype={repr:function(){ +var _278; +if(this.fired==-1){ +_278="unfired"; +}else{ +if(this.fired===0){ +_278="success"; +}else{ +_278="error"; +} +} +return "Deferred("+this.id+", "+_278+")"; +},toString:MochiKit.Base.forwardCall("repr"),_nextId:MochiKit.Base.counter(),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){ +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; +} +},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); +},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); +},addBoth:function(fn){ +if(arguments.length>1){ +fn=MochiKit.Base.partial.apply(null,arguments); +} +return this.addCallbacks(fn,fn); +},addCallback:function(fn){ +if(arguments.length>1){ +fn=MochiKit.Base.partial.apply(null,arguments); +} +return this.addCallbacks(fn,null); +},addErrback:function(fn){ +if(arguments.length>1){ +fn=MochiKit.Base.partial.apply(null,arguments); +} +return this.addCallbacks(null,fn); +},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(){ +var _283=this.chain; +var _284=this.fired; +var res=this.results[_284]; +var self=this; +var cb=null; +while(_283.length>0&&this.paused===0){ +var pair=_283.shift(); +var f=pair[_284]; +if(f===null){ +continue; +} +try{ +res=f(res); +_284=((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){ +_284=1; +if(!(err instanceof Error)){ +err=new MochiKit.Async.GenericError(err); +} +res=err; +} +} +this.fired=_284; +this.results[_284]=res; +if(cb&&this.paused){ +res.addBoth(cb); +res.chained=true; +} +}}; +MochiKit.Base.update(MochiKit.Async,{evalJSONRequest:function(){ +return eval("("+arguments[0].responseText+")"); +},succeed:function(_28b){ +var d=new MochiKit.Async.Deferred(); +d.callback.apply(d,arguments); +return d; +},fail:function(_28d){ +var d=new MochiKit.Async.Deferred(); +d.errback.apply(d,arguments); +return d; +},getXMLHttpRequest:function(){ +var self=arguments.callee; +if(!self.XMLHttpRequest){ +var _290=[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<_290.length;i++){ +var func=_290[i]; +try{ +self.XMLHttpRequest=func; +return func(); +} +catch(e){ +} +} +} +return self.XMLHttpRequest(); +},_xhr_onreadystatechange:function(d){ +var m=MochiKit.Base; +if(this.readyState==4){ +try{ +this.onreadystatechange=null; +} +catch(e){ +try{ +this.onreadystatechange=m.noop; +} +catch(e){ +} +} +var _295=null; +try{ +_295=this.status; +if(!_295&&m.isNotEmpty(this.responseText)){ +_295=304; +} +} +catch(e){ +} +if(_295==200||_295==304){ +d.callback(this); +}else{ +var err=new MochiKit.Async.XMLHttpRequestError(this,"Request failed"); +if(err.number){ +d.errback(err); +}else{ +d.errback(err); +} +} +} +},_xhr_canceller:function(req){ +try{ +req.onreadystatechange=null; +} +catch(e){ +try{ +req.onreadystatechange=MochiKit.Base.noop; +} +catch(e){ +} +} +req.abort(); +},sendXMLHttpRequest:function(req,_299){ +if(typeof (_299)=="undefined"||_299===null){ +_299=""; +} +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(_299); +} +catch(e){ +try{ +req.onreadystatechange=null; +} +catch(ignore){ +} +d.errback(e); +} +return d; +},doXHR:function(url,opts){ +var m=MochiKit.Base; +opts=m.update({method:"GET",sendContent:""},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 _2a3=opts.headers; +if(!m.isArrayLike(_2a3)){ +_2a3=m.items(_2a3); +} +for(var i=0;i<_2a3.length;i++){ +var _2a5=_2a3[i]; +var name=_2a5[0]; +var _2a7=_2a5[1]; +req.setRequestHeader(name,_2a7); +} +} +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; +},doSimpleXMLHttpRequest:function(url){ +var self=MochiKit.Async; +url=self._buildURL.apply(self,arguments); +return self.doXHR(url); +},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; +},wait:function(_2b0,_2b1){ +var d=new MochiKit.Async.Deferred(); +var m=MochiKit.Base; +if(typeof (_2b1)!="undefined"){ +d.addCallback(function(){ +return _2b1; +}); +} +var _2b4=setTimeout(m.bind("callback",d),Math.floor(_2b0*1000)); +d.canceller=function(){ +try{ +clearTimeout(_2b4); +} +catch(e){ +} +}; +return d; +},callLater:function(_2b5,func){ +var m=MochiKit.Base; +var _2b8=m.partial.apply(m,m.extend(null,arguments,1)); +return MochiKit.Async.wait(_2b5).addCallback(function(res){ +return _2b8(); +}); +}}); +MochiKit.Async.DeferredLock=function(){ +this.waiting=[]; +this.locked=false; +this.id=this._nextId(); +}; +MochiKit.Async.DeferredLock.prototype={__class__:MochiKit.Async.DeferredLock,acquire:function(){ +var d=new MochiKit.Async.Deferred(); +if(this.locked){ +this.waiting.push(d); +}else{ +this.locked=true; +d.callback(this); +} +return d; +},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 _2bb; +if(this.locked){ +_2bb="locked, "+this.waiting.length+" waiting"; +}else{ +_2bb="unlocked"; +} +return "DeferredLock("+this.id+", "+_2bb+")"; +},toString:MochiKit.Base.forwardCall("repr")}; +MochiKit.Async.DeferredList=function(list,_2bd,_2be,_2bf,_2c0){ +MochiKit.Async.Deferred.apply(this,[_2c0]); +this.list=list; +var _2c1=[]; +this.resultList=_2c1; +this.finishedCount=0; +this.fireOnOneCallback=_2bd; +this.fireOnOneErrback=_2be; +this.consumeErrors=_2bf; +var cb=MochiKit.Base.bind(this._cbDeferred,this); +for(var i=0;i<list.length;i++){ +var d=list[i]; +_2c1.push(undefined); +d.addCallback(cb,i,true); +d.addErrback(cb,i,false); +} +if(list.length===0&&!_2bd){ +this.callback(this.resultList); +} +}; +MochiKit.Async.DeferredList.prototype=new MochiKit.Async.Deferred(); +MochiKit.Async.DeferredList.prototype._cbDeferred=function(_2c5,_2c6,_2c7){ +this.resultList[_2c5]=[_2c6,_2c7]; +this.finishedCount+=1; +if(this.fired==-1){ +if(_2c6&&this.fireOnOneCallback){ +this.callback([_2c5,_2c7]); +}else{ +if(!_2c6&&this.fireOnOneErrback){ +this.errback(_2c7); +}else{ +if(this.finishedCount==this.list.length){ +this.callback(this.resultList); +} +} +} +} +if(!_2c6&&this.consumeErrors){ +_2c7=null; +} +return _2c7; +}; +MochiKit.Async.gatherResults=function(_2c8){ +var d=new MochiKit.Async.DeferredList(_2c8,false,true,false); +d.addCallback(function(_2ca){ +var ret=[]; +for(var i=0;i<_2ca.length;i++){ +ret.push(_2ca[i][1]); +} +return ret; +}); +return d; +}; +MochiKit.Async.maybeDeferred=function(func){ +var self=MochiKit.Async; +var _2cf; +try{ +var r=func.apply(null,MochiKit.Base.extend([],arguments,1)); +if(r instanceof self.Deferred){ +_2cf=r; +}else{ +if(r instanceof Error){ +_2cf=self.fail(r); +}else{ +_2cf=self.succeed(r); +} +} +} +catch(e){ +_2cf=self.fail(e); +} +return _2cf; +}; +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",function(_2d3){ +this.deferred=_2d3; +}); +ne("CancelledError",function(_2d4){ +this.deferred=_2d4; +}); +ne("BrowserComplianceError",function(msg){ +this.message=msg; +}); +ne("GenericError",function(msg){ +this.message=msg; +}); +ne("XMLHttpRequestError",function(req,msg){ +this.req=req; +this.message=msg; +try{ +this.number=req.status; +} +catch(e){ +} +}); +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); +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"],["elementDimensions","MochiKit.Style.getElementDimensions","1.4"],["elementPosition","MochiKit.Style.getElementPosition","1.4"],["hideElement","MochiKit.Style.hideElement","1.4"],["setElementDimensions","MochiKit.Style.setElementDimensions","1.4"],["setElementPosition","MochiKit.Style.setElementPosition","1.4"],["setDisplayForElement","MochiKit.Style.setDisplayForElement","1.4"],["setOpacity","MochiKit.Style.setOpacity","1.4"],["showElement","MochiKit.Style.showElement","1.4"],["Coordinates","MochiKit.Style.Coordinates","1.4"],["Dimensions","MochiKit.Style.Dimensions","1.4"]]; +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,{currentWindow:function(){ +return MochiKit.DOM._window; +},currentDocument:function(){ +return MochiKit.DOM._document; +},withWindow:function(win,func){ +var self=MochiKit.DOM; +var _2dc=self._document; +var _2dd=self._win; +var rval; +try{ +self._window=win; +self._document=win.document; +rval=func(); +} +catch(e){ +self._window=_2dd; +self._document=_2dc; +throw e; +} +self._window=_2dd; +self._document=_2dc; +return rval; +},formContents:function(elem){ +var _2e0=[]; +var _2e1=[]; +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 _2e6=elem.tagName.toUpperCase(); +if(_2e6==="INPUT"&&(elem.type=="radio"||elem.type=="checkbox")&&!elem.checked){ +return null; +} +if(_2e6==="SELECT"){ +if(elem.type=="select-one"){ +if(elem.selectedIndex>=0){ +var opt=elem.options[elem.selectedIndex]; +_2e0.push(name); +_2e1.push(opt.value); +return null; +} +_2e0.push(name); +_2e1.push(""); +return null; +}else{ +var opts=elem.options; +if(!opts.length){ +_2e0.push(name); +_2e1.push(""); +return null; +} +for(var i=0;i<opts.length;i++){ +var opt=opts[i]; +if(!opt.selected){ +continue; +} +_2e0.push(name); +_2e1.push(opt.value); +} +return null; +} +} +if(_2e6==="FORM"||_2e6==="P"||_2e6==="SPAN"||_2e6==="DIV"){ +return elem.childNodes; +} +_2e0.push(name); +_2e1.push(elem.value||""); +return null; +} +return elem.childNodes; +}); +return [_2e0,_2e1]; +},withDocument:function(doc,func){ +var self=MochiKit.DOM; +var _2ed=self._document; +var rval; +try{ +self._document=doc; +rval=func(); +} +catch(e){ +self._document=_2ed; +throw e; +} +self._document=_2ed; +return rval; +},registerDOMConverter:function(name,_2f0,wrap,_2f2){ +MochiKit.DOM.domConverters.register(name,_2f0,wrap,_2f2); +},coerceToDOM:function(node,ctx){ +var m=MochiKit.Base; +var im=MochiKit.Iter; +var self=MochiKit.DOM; +if(im){ +var iter=im.iter; +var _2f9=im.repeat; +var map=m.map; +} +var _2fb=self.domConverters; +var _2fc=arguments.callee; +var _2fd=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(); +} +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){ +var _2fe=null; +try{ +_2fe=iter(node); +} +catch(e){ +} +if(_2fe){ +return map(_2fc,_2fe,_2f9(ctx)); +} +} +try{ +node=_2fb.match(node,ctx); +continue; +} +catch(e){ +if(e!=_2fd){ +throw e; +} +} +return self._document.createTextNode(node.toString()); +} +return undefined; +},isChildNode:function(node,_300){ +var self=MochiKit.DOM; +if(typeof (node)=="string"){ +node=self.getElement(node); +} +if(typeof (_300)=="string"){ +_300=self.getElement(_300); +} +if(node===_300){ +return true; +} +while(node&&node.tagName.toUpperCase()!="BODY"){ +node=node.parentNode; +if(node===_300){ +return true; +} +} +return false; +},setNodeAttribute:function(node,attr,_304){ +var o={}; +o[attr]=_304; +try{ +return MochiKit.DOM.updateNodeAttributes(node,o); +} +catch(e){ +} +return null; +},getNodeAttribute:function(node,attr){ +var self=MochiKit.DOM; +var _309=self.attributeArray.renames[attr]; +node=self.getElement(node); +try{ +if(_309){ +return node[_309]; +} +return node.getAttribute(attr); +} +catch(e){ +} +return null; +},updateNodeAttributes:function(node,_30b){ +var elem=node; +var self=MochiKit.DOM; +if(typeof (node)=="string"){ +elem=self.getElement(node); +} +if(_30b){ +var _30e=MochiKit.Base.updatetree; +if(self.attributeArray.compliant){ +for(var k in _30b){ +var v=_30b[k]; +if(typeof (v)=="object"&&typeof (elem[k])=="object"){ +_30e(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{ +var _311=self.attributeArray.renames; +for(k in _30b){ +v=_30b[k]; +var _312=_311[k]; +if(k=="style"&&typeof (v)=="string"){ +elem.style.cssText=v; +}else{ +if(typeof (_312)=="string"){ +elem[_312]=v; +}else{ +if(typeof (elem[k])=="object"&&typeof (v)=="object"){ +_30e(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; +},appendChildNodes:function(node){ +var elem=node; +var self=MochiKit.DOM; +if(typeof (node)=="string"){ +elem=self.getElement(node); +} +var _316=[self.coerceToDOM(MochiKit.Base.extend(null,arguments,1),elem)]; +var _317=MochiKit.Base.concat; +while(_316.length){ +var n=_316.shift(); +if(typeof (n)=="undefined"||n===null){ +}else{ +if(typeof (n.nodeType)=="number"){ +elem.appendChild(n); +}else{ +_316=_317(n,_316); +} +} +} +return elem; +},replaceChildNodes:function(node){ +var elem=node; +var self=MochiKit.DOM; +if(typeof (node)=="string"){ +elem=self.getElement(node); +arguments[0]=elem; +} +var _31c; +while((_31c=elem.firstChild)){ +elem.removeChild(_31c); +} +if(arguments.length<2){ +return elem; +}else{ +return self.appendChildNodes.apply(this,arguments); +} +},createDOM:function(name,_31e){ +var elem; +var self=MochiKit.DOM; +var m=MochiKit.Base; +if(typeof (_31e)=="string"||typeof (_31e)=="number"){ +var args=m.extend([name,null],arguments,1); +return arguments.callee.apply(this,args); +} +if(typeof (name)=="string"){ +var _323=self._xhtml; +if(_31e&&!self.attributeArray.compliant){ +var _324=""; +if("name" in _31e){ +_324+=" name=\""+self.escapeHTML(_31e.name)+"\""; +} +if(name=="input"&&"type" in _31e){ +_324+=" type=\""+self.escapeHTML(_31e.type)+"\""; +} +if(_324){ +name="<"+name+_324+">"; +_323=false; +} +} +var d=self._document; +if(_323&&d===document){ +elem=d.createElementNS("http://www.w3.org/1999/xhtml",name); +}else{ +elem=d.createElement(name); +} +}else{ +elem=name; +} +if(_31e){ +self.updateNodeAttributes(elem,_31e); +} +if(arguments.length<=2){ +return elem; +}else{ +var args=m.extend([elem],arguments,2); +return self.appendChildNodes.apply(this,args); +} +},createDOMFunc:function(){ +var m=MochiKit.Base; +return m.partial.apply(this,m.extend([MochiKit.DOM.createDOM],arguments)); +},removeElement:function(elem){ +var e=MochiKit.DOM.getElement(elem); +e.parentNode.removeChild(e); +return e; +},swapDOM:function(dest,src){ +var self=MochiKit.DOM; +dest=self.getElement(dest); +var _32c=dest.parentNode; +if(src){ +src=self.getElement(src); +_32c.replaceChild(src,dest); +}else{ +_32c.removeChild(dest); +} +return src; +},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); +} +},getElementsByTagAndClassName:function(_32f,_330,_331){ +var self=MochiKit.DOM; +if(typeof (_32f)=="undefined"||_32f===null){ +_32f="*"; +} +if(typeof (_331)=="undefined"||_331===null){ +_331=self._document; +} +_331=self.getElement(_331); +var _333=(_331.getElementsByTagName(_32f)||self._document.all); +if(typeof (_330)=="undefined"||_330===null){ +return MochiKit.Base.extend(null,_333); +} +var _334=[]; +for(var i=0;i<_333.length;i++){ +var _336=_333[i]; +var cls=_336.className; +if(!cls){ +continue; +} +var _338=cls.split(" "); +for(var j=0;j<_338.length;j++){ +if(_338[j]==_330){ +_334.push(_336); +break; +} +} +} +return _334; +},_newCallStack:function(path,once){ +var rval=function(){ +var _33d=arguments.callee.callStack; +for(var i=0;i<_33d.length;i++){ +if(_33d[i].apply(this,arguments)===false){ +break; +} +} +if(once){ +try{ +this[path]=null; +} +catch(e){ +} +} +}; +rval.callStack=[]; +return rval; +},addToCallStack:function(_33f,path,func,once){ +var self=MochiKit.DOM; +var _344=_33f[path]; +var _345=_344; +if(!(typeof (_344)=="function"&&typeof (_344.callStack)=="object"&&_344.callStack!==null)){ +_345=self._newCallStack(path,once); +if(typeof (_344)=="function"){ +_345.callStack.push(_344); +} +_33f[path]=_345; +} +_345.callStack.push(func); +},addLoadEvent:function(func){ +var self=MochiKit.DOM; +self.addToCallStack(self._window,"onload",func,true); +},focusOnLoad:function(_348){ +var self=MochiKit.DOM; +self.addLoadEvent(function(){ +_348=self.getElement(_348); +if(_348){ +_348.focus(); +} +}); +},setElementClass:function(_34a,_34b){ +var self=MochiKit.DOM; +var obj=self.getElement(_34a); +if(self.attributeArray.compliant){ +obj.setAttribute("class",_34b); +}else{ +obj.setAttribute("className",_34b); +} +},toggleElementClass:function(_34e){ +var self=MochiKit.DOM; +for(var i=1;i<arguments.length;i++){ +var obj=self.getElement(arguments[i]); +if(!self.addElementClass(obj,_34e)){ +self.removeElementClass(obj,_34e); +} +} +},addElementClass:function(_352,_353){ +var self=MochiKit.DOM; +var obj=self.getElement(_352); +var cls=obj.className; +if(cls==undefined||cls.length===0){ +self.setElementClass(obj,_353); +return true; +} +if(cls==_353){ +return false; +} +var _357=cls.split(" "); +for(var i=0;i<_357.length;i++){ +if(_357[i]==_353){ +return false; +} +} +self.setElementClass(obj,cls+" "+_353); +return true; +},removeElementClass:function(_359,_35a){ +var self=MochiKit.DOM; +var obj=self.getElement(_359); +var cls=obj.className; +if(cls==undefined||cls.length===0){ +return false; +} +if(cls==_35a){ +self.setElementClass(obj,""); +return true; +} +var _35e=cls.split(" "); +for(var i=0;i<_35e.length;i++){ +if(_35e[i]==_35a){ +_35e.splice(i,1); +self.setElementClass(obj,_35e.join(" ")); +return true; +} +} +return false; +},swapElementClass:function(_360,_361,_362){ +var obj=MochiKit.DOM.getElement(_360); +var res=MochiKit.DOM.removeElementClass(obj,_361); +if(res){ +MochiKit.DOM.addElementClass(obj,_362); +} +return res; +},hasElementClass:function(_365,_366){ +var obj=MochiKit.DOM.getElement(_365); +var cls=obj.className; +if(!cls){ +return false; +} +var _369=cls.split(" "); +for(var i=1;i<arguments.length;i++){ +var good=false; +for(var j=0;j<_369.length;j++){ +if(_369[j]==arguments[i]){ +good=true; +break; +} +} +if(!good){ +return false; +} +} +return true; +},escapeHTML:function(s){ +return s.replace(/&/g,"&").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">"); +},toHTML:function(dom){ +return MochiKit.DOM.emitHTML(dom).join(""); +},emitHTML:function(dom,lst){ +if(typeof (lst)=="undefined"||lst===null){ +lst=[]; +} +var _371=[dom]; +var self=MochiKit.DOM; +var _373=self.escapeHTML; +var _374=self.attributeArray; +while(_371.length){ +dom=_371.pop(); +if(typeof (dom)=="string"){ +lst.push(dom); +}else{ +if(dom.nodeType==1){ +lst.push("<"+dom.tagName.toLowerCase()); +var _375=[]; +var _376=_374(dom); +for(var i=0;i<_376.length;i++){ +var a=_376[i]; +_375.push([" ",a.name,"=\"",_373(a.value),"\""]); +} +_375.sort(); +for(i=0;i<_375.length;i++){ +var _379=_375[i]; +for(var j=0;j<_379.length;j++){ +lst.push(_379[j]); +} +} +if(dom.hasChildNodes()){ +lst.push(">"); +_371.push("</"+dom.tagName.toLowerCase()+">"); +var _37b=dom.childNodes; +for(i=_37b.length-1;i>=0;i--){ +_371.push(_37b[i]); +} +}else{ +lst.push("/>"); +} +}else{ +if(dom.nodeType==3){ +lst.push(_373(dom.nodeValue)); +} +} +} +} +return lst; +},scrapeText:function(node,_37d){ +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 _382=node.nodeValue; +if(typeof (_382)=="string"){ +rval.push(_382); +} +})(MochiKit.DOM.getElement(node)); +if(_37d){ +return rval; +}else{ +return rval.join(""); +} +},removeEmptyTextNodes:function(_383){ +_383=MochiKit.DOM.getElement(_383); +for(var i=0;i<_383.childNodes.length;i++){ +var node=_383.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 _388=this._document.createElement("span"); +var _389; +if(_388&&_388.attributes&&_388.attributes.length>0){ +var _38a=m.filter; +_389=function(node){ +return _38a(_389.ignoreAttrFilter,node.attributes); +}; +_389.ignoreAttr={}; +var _38c=_388.attributes; +var _38d=_389.ignoreAttr; +for(var i=0;i<_38c.length;i++){ +var a=_38c[i]; +_38d[a.name]=a.value; +} +_389.ignoreAttrFilter=function(a){ +return (_389.ignoreAttr[a.name]!=a.value); +}; +_389.compliant=false; +_389.renames={"class":"className","checked":"defaultChecked","usemap":"useMap","for":"htmlFor","readonly":"readOnly","colspan":"colSpan","bgcolor":"bgColor"}; +}else{ +_389=function(node){ +return node.attributes; +}; +_389.compliant=true; +_389.renames={}; +} +this.attributeArray=_389; +var _392=function(_393,arr){ +var _395=arr[1].split("."); +var str=""; +var obj={}; +str+="if (!MochiKit."+_395[1]+") { throw new Error(\""; +str+="This function has been deprecated and depends on MochiKit."; +str+=_395[1]+".\");}"; +str+="return MochiKit."+_395[1]+"."+arr[0]; +str+=".apply(this, arguments);"; +obj[_395[2]]=new Function(str); +MochiKit.Base.update(MochiKit[_393],obj); +}; +for(var i;i<MochiKit.DOM.DEPRECATED.length;i++){ +_392("DOM",MochiKit.DOM.DEPRECATED[i]); +} +var _398=this.createDOMFunc; +this.UL=_398("ul"); +this.OL=_398("ol"); +this.LI=_398("li"); +this.TD=_398("td"); +this.TR=_398("tr"); +this.TBODY=_398("tbody"); +this.THEAD=_398("thead"); +this.TFOOT=_398("tfoot"); +this.TABLE=_398("table"); +this.TH=_398("th"); +this.INPUT=_398("input"); +this.SPAN=_398("span"); +this.A=_398("a"); +this.DIV=_398("div"); +this.IMG=_398("img"); +this.BUTTON=_398("button"); +this.TT=_398("tt"); +this.PRE=_398("pre"); +this.H1=_398("h1"); +this.H2=_398("h2"); +this.H3=_398("h3"); +this.BR=_398("br"); +this.HR=_398("hr"); +this.LABEL=_398("label"); +this.TEXTAREA=_398("textarea"); +this.FORM=_398("form"); +this.P=_398("p"); +this.SELECT=_398("select"); +this.OPTION=_398("option"); +this.OPTGROUP=_398("optgroup"); +this.LEGEND=_398("legend"); +this.FIELDSET=_398("fieldset"); +this.STRONG=_398("strong"); +this.CANVAS=_398("canvas"); +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)); +if(MochiKit.__export__){ +withWindow=MochiKit.DOM.withWindow; +withDocument=MochiKit.DOM.withDocument; +} +MochiKit.Base._exportSymbols(this,MochiKit.DOM); +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","computedStyle","getElementDimensions","elementDimensions","setElementDimensions","getElementPosition","elementPosition","setElementPosition","setDisplayForElement","hideElement","showElement","getViewportDimensions","getViewportPosition","Dimensions","Coordinates"]; +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__(); +}; +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,{computedStyle:function(elem,_3a0){ +var dom=MochiKit.DOM; +var d=dom._document; +elem=dom.getElement(elem); +_3a0=MochiKit.Base.camelize(_3a0); +if(!elem||elem==d){ +return undefined; +} +if(_3a0=="opacity"&&elem.filters){ +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[_3a0]; +} +if(typeof (d.defaultView)=="undefined"){ +return undefined; +} +if(d.defaultView===null){ +return undefined; +} +var _3a3=d.defaultView.getComputedStyle(elem,null); +if(typeof (_3a3)=="undefined"||_3a3===null){ +return undefined; +} +var _3a4=_3a0.replace(/([A-Z])/g,"-$1").toLowerCase(); +return _3a3.getPropertyValue(_3a4); +},getStyle:function(elem,_3a6){ +elem=MochiKit.DOM.getElement(elem); +var _3a7=elem.style[MochiKit.Base.camelize(_3a6)]; +if(!_3a7){ +if(document.defaultView&&document.defaultView.getComputedStyle){ +var css=document.defaultView.getComputedStyle(elem,null); +_3a7=css?css.getPropertyValue(_3a6):null; +}else{ +if(elem.currentStyle){ +_3a7=elem.currentStyle[MochiKit.Base.camelize(_3a6)]; +} +} +} +if(/Opera/.test(navigator.userAgent)&&(MochiKit.Base.find(["left","top","right","bottom"],_3a6)!=-1)){ +if(MochiKit.Style.getStyle(elem,"position")=="static"){ +_3a7="auto"; +} +} +return _3a7=="auto"?null:_3a7; +},setStyle:function(elem,_3aa){ +elem=MochiKit.DOM.getElement(elem); +for(name in _3aa){ +elem.style[MochiKit.Base.camelize(name)]=_3aa[name]; +} +},getOpacity:function(elem){ +var _3ac; +if(_3ac=MochiKit.Style.getStyle(elem,"opacity")){ +return parseFloat(_3ac); +} +if(_3ac=(MochiKit.Style.getStyle(elem,"filter")||"").match(/alpha\(opacity=(.*)\)/)){ +if(_3ac[1]){ +return parseFloat(_3ac[1])/100; +} +} +return 1; +},setOpacity:function(elem,o){ +elem=MochiKit.DOM.getElement(elem); +var self=MochiKit.Style; +if(o==1){ +var _3b0=/Gecko/.test(navigator.userAgent)&&!(/Konqueror|Safari|KHTML/.test(navigator.userAgent)); +self.setStyle(elem,{opacity:_3b0?0.999999:1}); +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:function(elem,_3b2){ +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 _3b7=null; +var d=MochiKit.DOM._document; +var de=d.documentElement; +var b=d.body; +if(!elem.parentNode&&elem.x&&elem.y){ +c.x+=elem.x||0; +c.y+=elem.y||0; +}else{ +if(elem.getBoundingClientRect){ +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; +_3b7=elem.offsetParent; +if(_3b7!=elem){ +while(_3b7){ +c.x+=_3b7.offsetLeft; +c.y+=_3b7.offsetTop; +_3b7=_3b7.offsetParent; +} +} +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 (_3b2)!="undefined"){ +_3b2=arguments.callee(_3b2); +if(_3b2){ +c.x-=(_3b2.x||0); +c.y-=(_3b2.y||0); +} +} +if(elem.parentNode){ +_3b7=elem.parentNode; +}else{ +_3b7=null; +} +while(_3b7){ +var _3bc=_3b7.tagName.toUpperCase(); +if(_3bc==="BODY"||_3bc==="HTML"){ +break; +} +c.x-=_3b7.scrollLeft; +c.y-=_3b7.scrollTop; +if(_3b7.parentNode){ +_3b7=_3b7.parentNode; +}else{ +_3b7=null; +} +} +return c; +},setElementPosition:function(elem,_3be,_3bf){ +elem=MochiKit.DOM.getElement(elem); +if(typeof (_3bf)=="undefined"){ +_3bf="px"; +} +var _3c0={}; +var _3c1=MochiKit.Base.isUndefinedOrNull; +if(!_3c1(_3be.x)){ +_3c0["left"]=_3be.x+_3bf; +} +if(!_3c1(_3be.y)){ +_3c0["top"]=_3be.y+_3bf; +} +MochiKit.DOM.updateNodeAttributes(elem,{"style":_3c0}); +},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"); +if(disp!="none"&&disp!=""&&typeof (disp)!="undefined"){ +return new self.Dimensions(elem.offsetWidth||0,elem.offsetHeight||0); +} +var s=elem.style; +var _3c7=s.visibility; +var _3c8=s.position; +s.visibility="hidden"; +s.position="absolute"; +s.display=""; +var _3c9=elem.offsetWidth; +var _3ca=elem.offsetHeight; +s.display="none"; +s.position=_3c8; +s.visibility=_3c7; +return new self.Dimensions(_3c9,_3ca); +},setElementDimensions:function(elem,_3cc,_3cd){ +elem=MochiKit.DOM.getElement(elem); +if(typeof (_3cd)=="undefined"){ +_3cd="px"; +} +var _3ce={}; +var _3cf=MochiKit.Base.isUndefinedOrNull; +if(!_3cf(_3cc.w)){ +_3ce["width"]=_3cc.w+_3cd; +} +if(!_3cf(_3cc.h)){ +_3ce["height"]=_3cc.h+_3cd; +} +MochiKit.DOM.updateNodeAttributes(elem,{"style":_3ce}); +},setDisplayForElement:function(_3d0,_3d1){ +var _3d2=MochiKit.Base.extend(null,arguments,1); +var _3d3=MochiKit.DOM.getElement; +for(var i=0;i<_3d2.length;i++){ +var _3d1=_3d3(_3d2[i]); +if(_3d1){ +_3d1.style.display=_3d0; +} +} +},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; +},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); +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__(); +}; +MochiKit.LoggingPane.createLoggingPane=function(_3dd){ +var m=MochiKit.LoggingPane; +_3dd=!(!_3dd); +if(m._loggingPane&&m._loggingPane.inline!=_3dd){ +m._loggingPane.closePane(); +m._loggingPane=null; +} +if(!m._loggingPane||m._loggingPane.closed){ +m._loggingPane=new m.LoggingPane(_3dd,MochiKit.Logging.logger); +} +return m._loggingPane; +}; +MochiKit.LoggingPane.LoggingPane=function(_3df,_3e0){ +if(typeof (_3e0)=="undefined"||_3e0===null){ +_3e0=MochiKit.Logging.logger; +} +this.logger=_3e0; +var _3e1=MochiKit.Base.update; +var _3e2=MochiKit.Base.updatetree; +var bind=MochiKit.Base.bind; +var _3e4=MochiKit.Base.clone; +var win=window; +var uid="_MochiKit_LoggingPane"; +if(typeof (MochiKit.DOM)!="undefined"){ +win=MochiKit.DOM.currentWindow(); +} +if(!_3df){ +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; +var _3eb=doc.getElementById(uid); +var _3ec=!!_3eb; +if(_3eb&&typeof (_3eb.loggingPane)!="undefined"){ +_3eb.loggingPane.logger=this.logger; +_3eb.loggingPane.buildAndApplyFilter(); +return _3eb.loggingPane; +} +if(_3ec){ +var _3ed; +while((_3ed=_3eb.firstChild)){ +_3eb.removeChild(_3ed); +} +}else{ +_3eb=doc.createElement("div"); +_3eb.id=uid; +} +_3eb.loggingPane=this; +var _3ee=doc.createElement("input"); +var _3ef=doc.createElement("input"); +var _3f0=doc.createElement("button"); +var _3f1=doc.createElement("button"); +var _3f2=doc.createElement("button"); +var _3f3=doc.createElement("button"); +var _3f4=doc.createElement("div"); +var _3f5=doc.createElement("div"); +var _3f6=uid+"_Listener"; +this.colorTable=_3e4(this.colorTable); +var _3f7=[]; +var _3f8=null; +var _3f9=function(msg){ +var _3fb=msg.level; +if(typeof (_3fb)=="number"){ +_3fb=MochiKit.Logging.LogLevel[_3fb]; +} +return _3fb; +}; +var _3fc=function(msg){ +return msg.info.join(" "); +}; +var _3fe=bind(function(msg){ +var _400=_3f9(msg); +var text=_3fc(msg); +var c=this.colorTable[_400]; +var p=doc.createElement("span"); +p.className="MochiKit-LogMessage MochiKit-LogLevel-"+_400; +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(_400+": "+text)); +_3f5.appendChild(p); +_3f5.appendChild(doc.createElement("br")); +if(_3f4.offsetHeight>_3f4.scrollHeight){ +_3f4.scrollTop=0; +}else{ +_3f4.scrollTop=_3f4.scrollHeight; +} +},this); +var _404=function(msg){ +_3f7[_3f7.length]=msg; +_3fe(msg); +}; +var _406=function(){ +var _407,_408; +try{ +_407=new RegExp(_3ee.value); +_408=new RegExp(_3ef.value); +} +catch(e){ +logDebug("Error in filter regex: "+e.message); +return null; +} +return function(msg){ +return (_407.test(_3f9(msg))&&_408.test(_3fc(msg))); +}; +}; +var _40a=function(){ +while(_3f5.firstChild){ +_3f5.removeChild(_3f5.firstChild); +} +}; +var _40b=function(){ +_3f7=[]; +_40a(); +}; +var _40c=bind(function(){ +if(this.closed){ +return; +} +this.closed=true; +if(MochiKit.LoggingPane._loggingPane==this){ +MochiKit.LoggingPane._loggingPane=null; +} +this.logger.removeListener(_3f6); +_3eb.loggingPane=null; +if(_3df){ +_3eb.parentNode.removeChild(_3eb); +}else{ +this.win.close(); +} +},this); +var _40d=function(){ +_40a(); +for(var i=0;i<_3f7.length;i++){ +var msg=_3f7[i]; +if(_3f8===null||_3f8(msg)){ +_3fe(msg); +} +} +}; +this.buildAndApplyFilter=function(){ +_3f8=_406(); +_40d(); +this.logger.removeListener(_3f6); +this.logger.addListener(_3f6,_3f8,_404); +}; +var _410=bind(function(){ +_3f7=this.logger.getMessages(); +_40d(); +},this); +var _411=bind(function(_412){ +_412=_412||window.event; +key=_412.which||_412.keyCode; +if(key==13){ +this.buildAndApplyFilter(); +} +},this); +var _413="display: block; z-index: 1000; left: 0px; bottom: 0px; position: fixed; width: 100%; background-color: white; font: "+this.logFont; +if(_3df){ +_413+="; height: 10em; border-top: 2px solid black"; +}else{ +_413+="; height: 100%;"; +} +_3eb.style.cssText=_413; +if(!_3ec){ +doc.body.appendChild(_3eb); +} +_413={"cssText":"width: 33%; display: inline; font: "+this.logFont}; +_3e2(_3ee,{"value":"FATAL|ERROR|WARNING|INFO|DEBUG","onkeypress":_411,"style":_413}); +_3eb.appendChild(_3ee); +_3e2(_3ef,{"value":".*","onkeypress":_411,"style":_413}); +_3eb.appendChild(_3ef); +_413="width: 8%; display:inline; font: "+this.logFont; +_3f0.appendChild(doc.createTextNode("Filter")); +_3f0.onclick=bind("buildAndApplyFilter",this); +_3f0.style.cssText=_413; +_3eb.appendChild(_3f0); +_3f1.appendChild(doc.createTextNode("Load")); +_3f1.onclick=_410; +_3f1.style.cssText=_413; +_3eb.appendChild(_3f1); +_3f2.appendChild(doc.createTextNode("Clear")); +_3f2.onclick=_40b; +_3f2.style.cssText=_413; +_3eb.appendChild(_3f2); +_3f3.appendChild(doc.createTextNode("Close")); +_3f3.onclick=_40c; +_3f3.style.cssText=_413; +_3eb.appendChild(_3f3); +_3f4.style.cssText="overflow: auto; width: 100%"; +_3f5.style.cssText="width: 100%; height: "+(_3df?"8em":"100%"); +_3f4.appendChild(_3f5); +_3eb.appendChild(_3f4); +this.buildAndApplyFilter(); +_410(); +if(_3df){ +this.win=undefined; +}else{ +this.win=win; +} +this.inline=_3df; +this.closePane=_40c; +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); +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__(); +}; +MochiKit.Color.Color=function(red,_415,blue,_417){ +if(typeof (_417)=="undefined"||_417===null){ +_417=1; +} +this.rgb={r:red,g:_415,b:blue,a:_417}; +}; +MochiKit.Color.Color.prototype={__class__:MochiKit.Color.Color,colorWithAlpha:function(_418){ +var rgb=this.rgb; +var m=MochiKit.Color; +return m.Color.fromRGB(rgb.r,rgb.g,rgb.b,_418); +},colorWithHue:function(hue){ +var hsl=this.asHSL(); +hsl.h=hue; +var m=MochiKit.Color; +return m.Color.fromHSL(hsl); +},colorWithSaturation:function(_41e){ +var hsl=this.asHSL(); +hsl.s=_41e; +var m=MochiKit.Color; +return m.Color.fromHSL(hsl); +},colorWithLightness:function(_421){ +var hsl=this.asHSL(); +hsl.l=_421; +var m=MochiKit.Color; +return m.Color.fromHSL(hsl); +},darkerColorWithLevel:function(_424){ +var hsl=this.asHSL(); +hsl.l=Math.max(hsl.l-_424,0); +var m=MochiKit.Color; +return m.Color.fromHSL(hsl); +},lighterColorWithLevel:function(_427){ +var hsl=this.asHSL(); +hsl.l=Math.min(hsl.l+_427,1); +var m=MochiKit.Color; +return m.Color.fromHSL(hsl); +},blendedColor:function(_42a,_42b){ +if(typeof (_42b)=="undefined"||_42b===null){ +_42b=0.5; +} +var sf=1-_42b; +var s=this.rgb; +var d=_42a.rgb; +var df=_42b; +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)); +},compareRGB:function(_430){ +var a=this.asRGB(); +var b=_430.asRGB(); +return MochiKit.Base.compare([a.r,a.g,a.b,a.a],[b.r,b.g,b.b,b.a]); +},isLight:function(){ +return this.asHSL().b>0.5; +},isDark:function(){ +return (!this.isLight()); +},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; +},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; +},asRGB:function(){ +return MochiKit.Base.clone(this.rgb); +},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; +},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); +},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); +},toString:function(){ +return this.toRGBString(); +},repr:function(){ +var c=this.rgb; +var col=[c.r,c.g,c.b,c.a]; +return this.__class__.NAME+"("+col.join(", ")+")"; +}}; +MochiKit.Base.update(MochiKit.Color.Color,{fromRGB:function(red,_447,blue,_449){ +var _44a=MochiKit.Color.Color; +if(arguments.length==1){ +var rgb=red; +red=rgb.r; +_447=rgb.g; +blue=rgb.b; +if(typeof (rgb.a)=="undefined"){ +_449=undefined; +}else{ +_449=rgb.a; +} +} +return new _44a(red,_447,blue,_449); +},fromHSL:function(hue,_44d,_44e,_44f){ +var m=MochiKit.Color; +return m.Color.fromRGB(m.hslToRGB.apply(m,arguments)); +},fromHSV:function(hue,_452,_453,_454){ +var m=MochiKit.Color; +return m.Color.fromRGB(m.hsvToRGB.apply(m,arguments)); +},fromName:function(name){ +var _457=MochiKit.Color.Color; +if(name.charAt(0)=="\""){ +name=name.substr(1,name.length-2); +} +var _458=_457._namedColors[name.toLowerCase()]; +if(typeof (_458)=="string"){ +return _457.fromHexString(_458); +}else{ +if(name=="transparent"){ +return _457.transparentColor(); +} +} +return null; +},fromString:function(_459){ +var self=MochiKit.Color.Color; +var _45b=_459.substr(0,3); +if(_45b=="rgb"){ +return self.fromRGBString(_459); +}else{ +if(_45b=="hsl"){ +return self.fromHSLString(_459); +}else{ +if(_459.charAt(0)=="#"){ +return self.fromHexString(_459); +} +} +} +return self.fromName(_459); +},fromHexString:function(_45c){ +if(_45c.charAt(0)=="#"){ +_45c=_45c.substring(1); +} +var _45d=[]; +var i,hex; +if(_45c.length==3){ +for(i=0;i<3;i++){ +hex=_45c.substr(i,1); +_45d.push(parseInt(hex+hex,16)/255); +} +}else{ +for(i=0;i<6;i+=2){ +hex=_45c.substr(i,2); +_45d.push(parseInt(hex,16)/255); +} +} +var _460=MochiKit.Color.Color; +return _460.fromRGB.apply(_460,_45d); +},_fromColorString:function(pre,_462,_463,_464){ +if(_464.indexOf(pre)===0){ +_464=_464.substring(_464.indexOf("(",3)+1,_464.length-1); +} +var _465=_464.split(/\s*,\s*/); +var _466=[]; +for(var i=0;i<_465.length;i++){ +var c=_465[i]; +var val; +var _46a=c.substring(c.length-3); +if(c.charAt(c.length-1)=="%"){ +val=0.01*parseFloat(c.substring(0,c.length-1)); +}else{ +if(_46a=="deg"){ +val=parseFloat(c)/360; +}else{ +if(_46a=="rad"){ +val=parseFloat(c)/(Math.PI*2); +}else{ +val=_463[i]*parseFloat(c); +} +} +} +_466.push(val); +} +return this[_462].apply(this,_466); +},fromComputedStyle:function(elem,_46c){ +var d=MochiKit.DOM; +var cls=MochiKit.Color.Color; +for(elem=d.getElement(elem);elem;elem=elem.parentNode){ +var _46f=MochiKit.Style.computedStyle.apply(d,arguments); +if(!_46f){ +continue; +} +var _470=cls.fromString(_46f); +if(!_470){ +break; +} +if(_470.asRGB().a>0){ +return _470; +} +} +return null; +},fromBackground:function(elem){ +var cls=MochiKit.Color.Color; +return cls.fromComputedStyle(elem,"backgroundColor","background-color")||cls.whiteColor(); +},fromText:function(elem){ +var cls=MochiKit.Color.Color; +return cls.fromComputedStyle(elem,"color","color")||cls.blackColor(); +},namedColors:function(){ +return MochiKit.Base.clone(MochiKit.Color.Color._namedColors); +}}); +MochiKit.Base.update(MochiKit.Color,{clampColorComponent:function(v,_476){ +v*=_476; +if(v<0){ +return 0; +}else{ +if(v>_476){ +return _476; +}else{ +return v; +} +} +},_hslValue:function(n1,n2,hue){ +if(hue>6){ +hue-=6; +}else{ +if(hue<0){ +hue+=6; +} +} +var val; +if(hue<1){ +val=n1+(n2-n1)*hue; +}else{ +if(hue<3){ +val=n2; +}else{ +if(hue<4){ +val=n1+(n2-n1)*(4-hue); +}else{ +val=n1; +} +} +} +return val; +},hsvToRGB:function(hue,_47c,_47d,_47e){ +if(arguments.length==1){ +var hsv=hue; +hue=hsv.h; +_47c=hsv.s; +_47d=hsv.v; +_47e=hsv.a; +} +var red; +var _481; +var blue; +if(_47c===0){ +red=0; +_481=0; +blue=0; +}else{ +var i=Math.floor(hue*6); +var f=(hue*6)-i; +var p=_47d*(1-_47c); +var q=_47d*(1-(_47c*f)); +var t=_47d*(1-(_47c*(1-f))); +switch(i){ +case 1: +red=q; +_481=_47d; +blue=p; +break; +case 2: +red=p; +_481=_47d; +blue=t; +break; +case 3: +red=p; +_481=q; +blue=_47d; +break; +case 4: +red=t; +_481=p; +blue=_47d; +break; +case 5: +red=_47d; +_481=p; +blue=q; +break; +case 6: +case 0: +red=_47d; +_481=t; +blue=p; +break; +} +} +return {r:red,g:_481,b:blue,a:_47e}; +},hslToRGB:function(hue,_489,_48a,_48b){ +if(arguments.length==1){ +var hsl=hue; +hue=hsl.h; +_489=hsl.s; +_48a=hsl.l; +_48b=hsl.a; +} +var red; +var _48e; +var blue; +if(_489===0){ +red=_48a; +_48e=_48a; +blue=_48a; +}else{ +var m2; +if(_48a<=0.5){ +m2=_48a*(1+_489); +}else{ +m2=_48a+_489-(_48a*_489); +} +var m1=(2*_48a)-m2; +var f=MochiKit.Color._hslValue; +var h6=hue*6; +red=f(m1,m2,h6+2); +_48e=f(m1,m2,h6); +blue=f(m1,m2,h6-2); +} +return {r:red,g:_48e,b:blue,a:_48b}; +},rgbToHSV:function(red,_495,blue,_497){ +if(arguments.length==1){ +var rgb=red; +red=rgb.r; +_495=rgb.g; +blue=rgb.b; +_497=rgb.a; +} +var max=Math.max(Math.max(red,_495),blue); +var min=Math.min(Math.min(red,_495),blue); +var hue; +var _49c; +var _49d=max; +if(min==max){ +hue=0; +_49c=0; +}else{ +var _49e=(max-min); +_49c=_49e/max; +if(red==max){ +hue=(_495-blue)/_49e; +}else{ +if(_495==max){ +hue=2+((blue-red)/_49e); +}else{ +hue=4+((red-_495)/_49e); +} +} +hue/=6; +if(hue<0){ +hue+=1; +} +if(hue>1){ +hue-=1; +} +} +return {h:hue,s:_49c,v:_49d,a:_497}; +},rgbToHSL:function(red,_4a0,blue,_4a2){ +if(arguments.length==1){ +var rgb=red; +red=rgb.r; +_4a0=rgb.g; +blue=rgb.b; +_4a2=rgb.a; +} +var max=Math.max(red,Math.max(_4a0,blue)); +var min=Math.min(red,Math.min(_4a0,blue)); +var hue; +var _4a7; +var _4a8=(max+min)/2; +var _4a9=max-min; +if(_4a9===0){ +hue=0; +_4a7=0; +}else{ +if(_4a8<=0.5){ +_4a7=_4a9/(max+min); +}else{ +_4a7=_4a9/(2-max-min); +} +if(red==max){ +hue=(_4a0-blue)/_4a9; +}else{ +if(_4a0==max){ +hue=2+((blue-red)/_4a9); +}else{ +hue=4+((red-_4a0)/_4a9); +} +} +hue/=6; +if(hue<0){ +hue+=1; +} +if(hue>1){ +hue-=1; +} +} +return {h:hue,s:_4a7,l:_4a8,a:_4a2}; +},toColorPart:function(num){ +num=Math.round(num); +var _4ab=num.toString(16); +if(num<16){ +return "0"+_4ab; +} +return _4ab; +},__new__:function(){ +var m=MochiKit.Base; +this.Color.fromRGBString=m.bind(this.Color._fromColorString,this.Color,"rgb","fromRGB",[1/255,1/255,1/255,1]); +this.Color.fromHSLString=m.bind(this.Color._fromColorString,this.Color,"hsl","fromHSL",[1/360,0.01,0.01,1]); +var _4ad=1/3; +var _4ae={black:[0,0,0],blue:[0,0,1],brown:[0.6,0.4,0.2],cyan:[0,1,1],darkGray:[_4ad,_4ad,_4ad],gray:[0.5,0.5,0.5],green:[0,1,0],lightGray:[2*_4ad,2*_4ad,2*_4ad],magenta:[1,0,1],orange:[1,0.5,0],purple:[0.5,0,0.5],red:[1,0,0],transparent:[0,0,0,0],white:[1,1,1],yellow:[1,1,0]}; +var _4af=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 _4ae){ +var name=k+"Color"; +var _4b8=m.concat([_4af,this.Color,name],_4ae[k]); +this.Color[name]=m.bind.apply(null,_4b8); +} +var _4b9=function(){ +for(var i=0;i<arguments.length;i++){ +if(!(arguments[i] instanceof Color)){ +return false; +} +} +return true; +}; +var _4bb=function(a,b){ +return a.compareRGB(b); +}; +m.nameFunctions(this); +m.registerComparator(this.Color.NAME,_4b9,_4bb); +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); +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"}; +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=[]; +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; +},toString:function(){ +return this.__repr__(); +},src:function(){ +return this._src; +},event:function(){ +return this._event; +},type:function(){ +return this._event.type||undefined; +},target:function(){ +return this._event.target||this._event.srcElement; +},_relatedTarget:null,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,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; +m.shift=this._event.shiftKey; +m.any=m.alt||m.ctrl||m.shift||m.meta; +this._modifier=m; +return m; +},_key:null,key:function(){ +if(this._key!==null){ +return this._key; +} +var k={}; +if(this.type()&&this.type().indexOf("key")===0){ +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; +}else{ +if(this.type()=="keypress"){ +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"){ +k.code=this._event.keyCode; +k.string=String.fromCharCode(k.code); +} +} +this._key=k; +return k; +} +} +} +return undefined; +},_mouse:null,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{ +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; +if(e.which){ +m.button.left=(e.which==1); +m.button.middle=(e.which==2); +m.button.right=(e.which==3); +}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; +},stop:function(){ +this.stopPropagation(); +this.preventDefault(); +},stopPropagation:function(){ +if(this._event.stopPropagation){ +this._event.stopPropagation(); +}else{ +this._event.cancelBubble=true; +} +},preventDefault:function(){ +if(this._event.preventDefault){ +this._event.preventDefault(); +}else{ +if(this._confirmUnload===null){ +this._event.returnValue=false; +} +} +},_confirmUnload:null,confirmUnload:function(msg){ +if(this.type()=="beforeunload"){ +this._confirmUnload=msg; +this._event.returnValue=msg; +} +}}); +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"}; +(function(){ +var _4ca=MochiKit.Signal._specialMacKeys; +for(i=63236;i<=63242;i++){ +_4ca[i]="KEY_F"+(i-63236+1); +} +})(); +MochiKit.Signal._specialKeys={8:"KEY_BACKSPACE",9:"KEY_TAB",12:"KEY_NUM_PAD_CLEAR",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",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"}; +(function(){ +var _4cb=MochiKit.Signal._specialKeys; +for(var i=48;i<=57;i++){ +_4cb[i]="KEY_"+(i-48); +} +for(i=65;i<=90;i++){ +_4cb[i]="KEY_"+String.fromCharCode(i); +} +for(i=96;i<=105;i++){ +_4cb[i]="KEY_NUM_PAD_"+(i-96); +} +for(i=112;i<=123;i++){ +_4cb[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 _4ce=self._observers; +for(var i=0;i<_4ce.length;i++){ +self._disconnect(_4ce[i]); +} +delete self._observers; +try{ +window.onload=undefined; +} +catch(e){ +} +try{ +window.onunload=undefined; +} +catch(e){ +} +},_listener:function(src,func,obj,_4d3){ +var self=MochiKit.Signal; +var E=self.Event; +if(!_4d3){ +return MochiKit.Base.bind(func,obj); +} +obj=obj||src; +if(typeof (func)=="string"){ +return function(_4d6){ +obj[func].apply(obj,[new E(src,_4d6)]); +}; +}else{ +return function(_4d7){ +func.apply(obj,[new E(src,_4d7)]); +}; +} +},_browserAlreadyHasMouseEnterAndLeave:function(){ +return /MSIE/.test(navigator.userAgent); +},_mouseEnterListener:function(src,sig,func,obj){ +var E=MochiKit.Signal.Event; +return function(_4dd){ +var e=new E(src,_4dd); +try{ +e.relatedTarget().nodeName; +} +catch(err){ +return; +} +e.stop(); +if(MochiKit.DOM.isChildNode(e.relatedTarget(),src)){ +return; +} +e.type=function(){ +return sig; +}; +if(typeof (func)=="string"){ +return obj[func].apply(obj,[e]); +}else{ +return func.apply(obj,[e]); +} +}; +},_getDestPair:function(_4df,_4e0){ +var obj=null; +var func=null; +if(typeof (_4e0)!="undefined"){ +obj=_4df; +func=_4e0; +if(typeof (_4e0)=="string"){ +if(typeof (_4df[_4e0])!="function"){ +throw new Error("'funcOrStr' must be a function on 'objOrFunc'"); +} +}else{ +if(typeof (_4e0)!="function"){ +throw new Error("'funcOrStr' must be a function or string"); +} +} +}else{ +if(typeof (_4df)!="function"){ +throw new Error("'objOrFunc' must be a function if 'funcOrStr' is not given"); +}else{ +func=_4df; +} +} +return [obj,func]; +},connect:function(src,sig,_4e5,_4e6){ +src=MochiKit.DOM.getElement(src); +var self=MochiKit.Signal; +if(typeof (sig)!="string"){ +throw new Error("'sig' must be a string"); +} +var _4e8=self._getDestPair(_4e5,_4e6); +var obj=_4e8[0]; +var func=_4e8[1]; +if(typeof (obj)=="undefined"||obj===null){ +obj=src; +} +var _4eb=!!(src.addEventListener||src.attachEvent); +if(_4eb&&(sig==="onmouseenter"||sig==="onmouseleave")&&!self._browserAlreadyHasMouseEnterAndLeave()){ +var _4ec=self._mouseEnterListener(src,sig.substr(2),func,obj); +if(sig==="onmouseenter"){ +sig="onmouseover"; +}else{ +sig="onmouseout"; +} +}else{ +var _4ec=self._listener(src,func,obj,_4eb); +} +if(src.addEventListener){ +src.addEventListener(sig.substr(2),_4ec,false); +}else{ +if(src.attachEvent){ +src.attachEvent(sig,_4ec); +} +} +var _4ed=[src,sig,_4ec,_4eb,_4e5,_4e6,true]; +self._observers.push(_4ed); +if(!_4eb&&typeof (src.__connect__)=="function"){ +var args=MochiKit.Base.extend([_4ed],arguments,1); +src.__connect__.apply(src,args); +} +return _4ed; +},_disconnect:function(_4ef){ +if(!_4ef[6]){ +return; +} +_4ef[6]=false; +if(!_4ef[3]){ +return; +} +var src=_4ef[0]; +var sig=_4ef[1]; +var _4f2=_4ef[2]; +if(src.removeEventListener){ +src.removeEventListener(sig.substr(2),_4f2,false); +}else{ +if(src.detachEvent){ +src.detachEvent(sig,_4f2); +}else{ +throw new Error("'src' must be a DOM element"); +} +} +},disconnect:function(_4f3){ +var self=MochiKit.Signal; +var _4f5=self._observers; +var m=MochiKit.Base; +if(arguments.length>1){ +var src=MochiKit.DOM.getElement(arguments[0]); +var sig=arguments[1]; +var obj=arguments[2]; +var func=arguments[3]; +for(var i=_4f5.length-1;i>=0;i--){ +var o=_4f5[i]; +if(o[0]===src&&o[1]===sig&&o[4]===obj&&o[5]===func){ +self._disconnect(o); +if(!self._lock){ +_4f5.splice(i,1); +}else{ +self._dirty=true; +} +return true; +} +} +}else{ +var idx=m.findIdentical(_4f5,_4f3); +if(idx>=0){ +self._disconnect(_4f3); +if(!self._lock){ +_4f5.splice(idx,1); +}else{ +self._dirty=true; +} +return true; +} +} +return false; +},disconnectAllTo:function(_4fe,_4ff){ +var self=MochiKit.Signal; +var _501=self._observers; +var _502=self._disconnect; +var _503=self._lock; +var _504=self._dirty; +if(typeof (_4ff)==="undefined"){ +_4ff=null; +} +for(var i=_501.length-1;i>=0;i--){ +var _506=_501[i]; +if(_506[4]===_4fe&&(_4ff===null||_506[5]===_4ff)){ +_502(_506); +if(_503){ +_504=true; +}else{ +_501.splice(i,1); +} +} +} +self._dirty=_504; +},disconnectAll:function(src,sig){ +src=MochiKit.DOM.getElement(src); +var m=MochiKit.Base; +var _50a=m.flattenArguments(m.extend(null,arguments,1)); +var self=MochiKit.Signal; +var _50c=self._disconnect; +var _50d=self._observers; +var i,_50f; +var _510=self._lock; +var _511=self._dirty; +if(_50a.length===0){ +for(i=_50d.length-1;i>=0;i--){ +_50f=_50d[i]; +if(_50f[0]===src){ +_50c(_50f); +if(!_510){ +_50d.splice(i,1); +}else{ +_511=true; +} +} +} +}else{ +var sigs={}; +for(i=0;i<_50a.length;i++){ +sigs[_50a[i]]=true; +} +for(i=_50d.length-1;i>=0;i--){ +_50f=_50d[i]; +if(_50f[0]===src&&_50f[1] in sigs){ +_50c(_50f); +if(!_510){ +_50d.splice(i,1); +}else{ +_511=true; +} +} +} +} +self._dirty=_511; +},signal:function(src,sig){ +var self=MochiKit.Signal; +var _516=self._observers; +src=MochiKit.DOM.getElement(src); +var args=MochiKit.Base.extend(null,arguments,2); +var _518=[]; +self._lock=true; +for(var i=0;i<_516.length;i++){ +var _51a=_516[i]; +if(_51a[0]===src&&_51a[1]===sig){ +try{ +_51a[2].apply(src,args); +} +catch(e){ +_518.push(e); +} +} +} +self._lock=false; +if(self._dirty){ +self._dirty=false; +for(var i=_516.length-1;i>=0;i--){ +if(!_516[i][6]){ +_516.splice(i,1); +} +} +} +if(_518.length==1){ +throw _518[0]; +}else{ +if(_518.length>1){ +var e=new Error("Multiple errors thrown in handling 'sig', see errors property"); +e.errors=_518; +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){ +} +this.EXPORT_TAGS={":common":this.EXPORT,":all":m.concat(this.EXPORT,this.EXPORT_OK)}; +m.nameFunctions(this); +}; +MochiKit.Signal.__new__(this); +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); +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,_51f){ +e=MochiKit.DOM.getElement(e); +this._setOptions(_51f); +if(this.options.__unstable__wrapElement){ +e=this._doWrap(e); +} +var _520=this.options.color; +var C=MochiKit.Color.Color; +if(this.options.color==="fromElement"){ +_520=C.fromBackground(e); +}else{ +if(!(_520 instanceof C)){ +_520=C.fromString(_520); +} +} +this.isTransparent=(_520.asRGB().a<=0); +var _522=this.options.bgColor; +if(this.options.bgColor==="fromParent"){ +_522=C.fromBackground(e.offsetParent); +}else{ +if(!(_522 instanceof C)){ +_522=C.fromString(_522); +} +} +this._roundCornersImpl(e,_520,_522); +}; +MochiKit.Visual._RoundCorners.prototype={_doWrap:function(e){ +var _524=e.parentNode; +var doc=MochiKit.DOM.currentDocument(); +if(typeof (doc.defaultView)==="undefined"||doc.defaultView===null){ +return e; +} +var _526=doc.defaultView.getComputedStyle(e,null); +if(typeof (_526)==="undefined"||_526===null){ +return e; +} +var _527=MochiKit.DOM.DIV({"style":{display:"block",marginTop:_526.getPropertyValue("padding-top"),marginRight:_526.getPropertyValue("padding-right"),marginBottom:_526.getPropertyValue("padding-bottom"),marginLeft:_526.getPropertyValue("padding-left"),padding:"0px"}}); +_527.innerHTML=e.innerHTML; +e.innerHTML=""; +e.appendChild(_527); +return e; +},_roundCornersImpl:function(e,_529,_52a){ +if(this.options.border){ +this._renderBorder(e,_52a); +} +if(this._isTopRounded()){ +this._roundTopCorners(e,_529,_52a); +} +if(this._isBottomRounded()){ +this._roundBottomCorners(e,_529,_52a); +} +},_renderBorder:function(el,_52c){ +var _52d="1px solid "+this._borderColor(_52c); +var _52e="border-left: "+_52d; +var _52f="border-right: "+_52d; +var _530="style='"+_52e+";"+_52f+"'"; +el.innerHTML="<div "+_530+">"+el.innerHTML+"</div>"; +},_roundTopCorners:function(el,_532,_533){ +var _534=this._createCorner(_533); +for(var i=0;i<this.options.numSlices;i++){ +_534.appendChild(this._createCornerSlice(_532,_533,i,"top")); +} +el.style.paddingTop=0; +el.insertBefore(_534,el.firstChild); +},_roundBottomCorners:function(el,_537,_538){ +var _539=this._createCorner(_538); +for(var i=(this.options.numSlices-1);i>=0;i--){ +_539.appendChild(this._createCornerSlice(_537,_538,i,"bottom")); +} +el.style.paddingBottom=0; +el.appendChild(_539); +},_createCorner:function(_53b){ +var dom=MochiKit.DOM; +return dom.DIV({style:{backgroundColor:_53b.toString()}}); +},_createCornerSlice:function(_53d,_53e,n,_540){ +var _541=MochiKit.DOM.SPAN(); +var _542=_541.style; +_542.backgroundColor=_53d.toString(); +_542.display="block"; +_542.height="1px"; +_542.overflow="hidden"; +_542.fontSize="1px"; +var _543=this._borderColor(_53d,_53e); +if(this.options.border&&n===0){ +_542.borderTopStyle="solid"; +_542.borderTopWidth="1px"; +_542.borderLeftWidth="0px"; +_542.borderRightWidth="0px"; +_542.borderBottomWidth="0px"; +_542.height="0px"; +_542.borderColor=_543.toString(); +}else{ +if(_543){ +_542.borderColor=_543.toString(); +_542.borderStyle="solid"; +_542.borderWidth="0px 1px"; +} +} +if(!this.options.compact&&(n==(this.options.numSlices-1))){ +_542.height="2px"; +} +this._setMargin(_541,n,_540); +this._setBorder(_541,n,_540); +return _541; +},_setOptions:function(_544){ +this.options={corners:"all",color:"fromElement",bgColor:"fromParent",blend:true,border:false,compact:false,__unstable__wrapElement:false}; +MochiKit.Base.update(this.options,_544); +this.options.numSlices=(this.options.compact?2:4); +},_whichSideTop:function(){ +var _545=this.options.corners; +if(this._hasString(_545,"all","top")){ +return ""; +} +var _546=(_545.indexOf("tl")!=-1); +var _547=(_545.indexOf("tr")!=-1); +if(_546&&_547){ +return ""; +} +if(_546){ +return "left"; +} +if(_547){ +return "right"; +} +return ""; +},_whichSideBottom:function(){ +var _548=this.options.corners; +if(this._hasString(_548,"all","bottom")){ +return ""; +} +var _549=(_548.indexOf("bl")!=-1); +var _54a=(_548.indexOf("br")!=-1); +if(_549&&_54a){ +return ""; +} +if(_549){ +return "left"; +} +if(_54a){ +return "right"; +} +return ""; +},_borderColor:function(_54b,_54c){ +if(_54b=="transparent"){ +return _54c; +}else{ +if(this.options.border){ +return this.options.border; +}else{ +if(this.options.blend){ +return _54c.blendedColor(_54b); +} +} +} +return ""; +},_setMargin:function(el,n,_54f){ +var _550=this._marginSize(n)+"px"; +var _551=(_54f=="top"?this._whichSideTop():this._whichSideBottom()); +var _552=el.style; +if(_551=="left"){ +_552.marginLeft=_550; +_552.marginRight="0px"; +}else{ +if(_551=="right"){ +_552.marginRight=_550; +_552.marginLeft="0px"; +}else{ +_552.marginLeft=_550; +_552.marginRight=_550; +} +} +},_setBorder:function(el,n,_555){ +var _556=this._borderSize(n)+"px"; +var _557=(_555=="top"?this._whichSideTop():this._whichSideBottom()); +var _558=el.style; +if(_557=="left"){ +_558.borderLeftWidth=_556; +_558.borderRightWidth="0px"; +}else{ +if(_557=="right"){ +_558.borderRightWidth=_556; +_558.borderLeftWidth="0px"; +}else{ +_558.borderLeftWidth=_556; +_558.borderRightWidth=_556; +} +} +},_marginSize:function(n){ +if(this.isTransparent){ +return 0; +} +var o=this.options; +if(o.compact&&o.blend){ +var _55b=[1,0]; +return _55b[n]; +}else{ +if(o.compact){ +var _55c=[2,1]; +return _55c[n]; +}else{ +if(o.blend){ +var _55d=[3,2,1,0]; +return _55d[n]; +}else{ +var _55e=[5,3,2,1]; +return _55e[n]; +} +} +} +},_borderSize:function(n){ +var o=this.options; +var _561; +if(o.compact&&(o.blend||this.isTransparent)){ +return 1; +}else{ +if(o.compact){ +_561=[1,0]; +}else{ +if(o.blend){ +_561=[2,1,1,1]; +}else{ +if(o.border){ +_561=[0,2,0,0]; +}else{ +if(this.isTransparent){ +_561=[5,3,2,1]; +}else{ +return 0; +} +} +} +} +} +return _561[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); +}}; +MochiKit.Visual.roundElement=function(e,_566){ +new MochiKit.Visual._RoundCorners(e,_566); +}; +MochiKit.Visual.roundClass=function(_567,_568,_569){ +var _56a=MochiKit.DOM.getElementsByTagAndClassName(_567,_568); +for(var i=0;i<_56a.length;i++){ +MochiKit.Visual.roundElement(_56a[i],_569); +} +}; +MochiKit.Visual.tagifyText=function(_56c,_56d){ +var _56d=_56d||"position:relative"; +if(/MSIE/.test(navigator.userAgent)){ +_56d+=";zoom:1"; +} +_56c=MochiKit.DOM.getElement(_56c); +var ma=MochiKit.Base.map; +ma(function(_56f){ +if(_56f.nodeType==3){ +ma(function(_570){ +_56c.insertBefore(MochiKit.DOM.SPAN({style:_56d},_570==" "?String.fromCharCode(160):_570),_56f); +},_56f.nodeValue.split("")); +MochiKit.DOM.removeElement(_56f); +} +},_56c.childNodes); +}; +MochiKit.Visual.forceRerendering=function(_571){ +try{ +_571=MochiKit.DOM.getElement(_571); +var n=document.createTextNode(" "); +_571.appendChild(n); +_571.removeChild(n); +} +catch(e){ +} +}; +MochiKit.Visual.multiple=function(_573,_574,_575){ +_575=MochiKit.Base.update({speed:0.1,delay:0},_575||{}); +var _576=_575.delay; +var _577=0; +MochiKit.Base.map(function(_578){ +_575.delay=_577*_575.speed+_576; +new _574(_578,_575); +_577+=1; +},_573); +}; +MochiKit.Visual.PAIRS={"slide":["slideDown","slideUp"],"blind":["blindDown","blindUp"],"appear":["appear","fade"],"size":["grow","shrink"]}; +MochiKit.Visual.toggle=function(_579,_57a,_57b){ +_579=MochiKit.DOM.getElement(_579); +_57a=(_57a||"appear").toLowerCase(); +_57b=MochiKit.Base.update({queue:{position:"end",scope:(_579.id||"global"),limit:1}},_57b||{}); +var v=MochiKit.Visual; +v[_579.style.display!="none"?v.PAIRS[_57a][1]:v.PAIRS[_57a][0]](_579,_57b); +}; +MochiKit.Visual.Transitions={}; +MochiKit.Visual.Transitions.linear=function(pos){ +return pos; +}; +MochiKit.Visual.Transitions.sinoidal=function(pos){ +return (-Math.cos(pos*Math.PI)/2)+0.5; +}; +MochiKit.Visual.Transitions.reverse=function(pos){ +return 1-pos; +}; +MochiKit.Visual.Transitions.flicker=function(pos){ +return ((-Math.cos(pos*Math.PI)/4)+0.75)+Math.random()/4; +}; +MochiKit.Visual.Transitions.wobble=function(pos){ +return (-Math.cos(pos*Math.PI*(9*pos))/2)+0.5; +}; +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))); +}; +MochiKit.Visual.Transitions.none=function(pos){ +return 0; +}; +MochiKit.Visual.Transitions.full=function(pos){ +return 1; +}; +MochiKit.Visual.ScopedQueue=function(){ +this.__init__(); +}; +MochiKit.Base.update(MochiKit.Visual.ScopedQueue.prototype,{__init__:function(){ +this.effects=[]; +this.interval=null; +},add:function(_585){ +var _586=new Date().getTime(); +var _587=(typeof (_585.options.queue)=="string")?_585.options.queue:_585.options.queue.position; +var ma=MochiKit.Base.map; +switch(_587){ +case "front": +ma(function(e){ +if(e.state=="idle"){ +e.startOn+=_585.finishOn; +e.finishOn+=_585.finishOn; +} +},this.effects); +break; +case "end": +var _58a; +ma(function(e){ +var i=e.finishOn; +if(i>=(_58a||i)){ +_58a=i; +} +},this.effects); +_586=_58a||_586; +break; +case "break": +ma(function(e){ +e.finalize(); +},this.effects); +break; +} +_585.startOn+=_586; +_585.finishOn+=_586; +if(!_585.options.queue.limit||this.effects.length<_585.options.queue.limit){ +this.effects.push(_585); +} +if(!this.interval){ +this.interval=this.startLoop(MochiKit.Base.bind(this.loop,this),40); +} +},startLoop:function(func,_58f){ +return setInterval(func,_58f); +},remove:function(_590){ +this.effects=MochiKit.Base.filter(function(e){ +return e!=_590; +},this.effects); +if(this.effects.length==0){ +this.stopLoop(this.interval); +this.interval=null; +} +},stopLoop:function(_592){ +clearInterval(_592); +},loop:function(){ +var _593=new Date().getTime(); +MochiKit.Base.map(function(_594){ +_594.loop(_593); +},this.effects); +}}); +MochiKit.Visual.Queues={instances:{},get:function(_595){ +if(typeof (_595)!="string"){ +return _595; +} +if(!this.instances[_595]){ +this.instances[_595]=new MochiKit.Visual.ScopedQueue(); +} +return this.instances[_595]; +}}; +MochiKit.Visual.Queue=MochiKit.Visual.Queues.get("global"); +MochiKit.Visual.DefaultOptions={transition:MochiKit.Visual.Transitions.sinoidal,duration:1,fps:25,sync:false,from:0,to:1,delay:0,queue:"parallel"}; +MochiKit.Visual.Base=function(){ +}; +MochiKit.Visual.Base.prototype={__class__:MochiKit.Visual.Base,start:function(_596){ +var v=MochiKit.Visual; +this.options=MochiKit.Base.setdefault(_596||{},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); +} +},loop:function(_598){ +if(_598>=this.startOn){ +if(_598>=this.finishOn){ +return this.finalize(); +} +var pos=(_598-this.startOn)/(this.finishOn-this.startOn); +var _59a=Math.round(pos*this.options.fps*this.options.duration); +if(_59a>this.currentFrame){ +this.render(pos); +this.currentFrame=_59a; +} +} +},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"); +} +},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"; +},finalize:function(){ +this.render(1); +this.cancel(); +this.event("beforeFinish"); +this.finish(); +this.event("afterFinish"); +},setup:function(){ +},finish:function(){ +},update:function(_59c){ +},event:function(_59d){ +if(this.options[_59d+"Internal"]){ +this.options[_59d+"Internal"](this); +} +if(this.options[_59d]){ +this.options[_59d](this); +} +},repr:function(){ +return "["+this.__class__.NAME+", options:"+MochiKit.Base.repr(this.options)+"]"; +}}; +MochiKit.Visual.Parallel=function(_59e,_59f){ +this.__init__(_59e,_59f); +}; +MochiKit.Visual.Parallel.prototype=new MochiKit.Visual.Base(); +MochiKit.Base.update(MochiKit.Visual.Parallel.prototype,{__init__:function(_5a0,_5a1){ +this.effects=_5a0||[]; +this.start(_5a1); +},update:function(_5a2){ +MochiKit.Base.map(function(_5a3){ +_5a3.render(_5a2); +},this.effects); +},finish:function(){ +MochiKit.Base.map(function(_5a4){ +_5a4.finalize(); +},this.effects); +}}); +MochiKit.Visual.Opacity=function(_5a5,_5a6){ +this.__init__(_5a5,_5a6); +}; +MochiKit.Visual.Opacity.prototype=new MochiKit.Visual.Base(); +MochiKit.Base.update(MochiKit.Visual.Opacity.prototype,{__init__:function(_5a7,_5a8){ +var b=MochiKit.Base; +var s=MochiKit.Style; +this.element=MochiKit.DOM.getElement(_5a7); +if(this.element.currentStyle&&(!this.element.currentStyle.hasLayout)){ +s.setStyle(this.element,{zoom:1}); +} +_5a8=b.update({from:s.getOpacity(this.element)||0,to:1},_5a8||{}); +this.start(_5a8); +},update:function(_5ab){ +MochiKit.Style.setOpacity(this.element,_5ab); +}}); +MochiKit.Visual.Move=function(_5ac,_5ad){ +this.__init__(_5ac,_5ad); +}; +MochiKit.Visual.Move.prototype=new MochiKit.Visual.Base(); +MochiKit.Base.update(MochiKit.Visual.Move.prototype,{__init__:function(_5ae,_5af){ +this.element=MochiKit.DOM.getElement(_5ae); +_5af=MochiKit.Base.update({x:0,y:0,mode:"relative"},_5af||{}); +this.start(_5af); +},setup:function(){ +MochiKit.DOM.makePositioned(this.element); +var s=this.element.style; +var _5b1=s.visibility; +var _5b2=s.display; +if(_5b2=="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"){ +this.options.x-=this.originalLeft; +this.options.y-=this.originalTop; +} +if(_5b2=="none"){ +s.visibility=_5b1; +s.display=_5b2; +} +},update:function(_5b3){ +MochiKit.Style.setStyle(this.element,{left:Math.round(this.options.x*_5b3+this.originalLeft)+"px",top:Math.round(this.options.y*_5b3+this.originalTop)+"px"}); +}}); +MochiKit.Visual.Scale=function(_5b4,_5b5,_5b6){ +this.__init__(_5b4,_5b5,_5b6); +}; +MochiKit.Visual.Scale.prototype=new MochiKit.Visual.Base(); +MochiKit.Base.update(MochiKit.Visual.Scale.prototype,{__init__:function(_5b7,_5b8,_5b9){ +this.element=MochiKit.DOM.getElement(_5b7); +_5b9=MochiKit.Base.update({scaleX:true,scaleY:true,scaleContent:true,scaleFromCenter:false,scaleMode:"box",scaleFrom:100,scaleTo:_5b8},_5b9||{}); +this.start(_5b9); +},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 _5bd=MochiKit.Style.getStyle(this.element,"font-size")||"100%"; +ma(b(function(_5be){ +if(_5bd.indexOf(_5be)>0){ +this.fontSize=parseFloat(_5bd); +this.fontSizeType=_5be; +} +},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]; +} +} +},update:function(_5bf){ +var _5c0=(this.options.scaleFrom/100)+(this.factor*_5bf); +if(this.options.scaleContent&&this.fontSize){ +MochiKit.Style.setStyle(this.element,{fontSize:this.fontSize*_5c0+this.fontSizeType}); +} +this.setDimensions(this.dims[0]*_5c0,this.dims[1]*_5c0); +},finish:function(){ +if(this.restoreAfterFinish){ +MochiKit.Style.setStyle(this.element,this.originalStyle); +} +},setDimensions:function(_5c1,_5c2){ +var d={}; +var r=Math.round; +if(/MSIE/.test(navigator.userAgent)){ +r=Math.ceil; +} +if(this.options.scaleX){ +d.width=r(_5c2)+"px"; +} +if(this.options.scaleY){ +d.height=r(_5c1)+"px"; +} +if(this.options.scaleFromCenter){ +var topd=(_5c1-this.dims[0])/2; +var _5c6=(_5c2-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-_5c6+"px"; +} +}else{ +if(this.options.scaleY){ +d.top=-topd+"px"; +} +if(this.options.scaleX){ +d.left=-_5c6+"px"; +} +} +} +MochiKit.Style.setStyle(this.element,d); +}}); +MochiKit.Visual.Highlight=function(_5c7,_5c8){ +this.__init__(_5c7,_5c8); +}; +MochiKit.Visual.Highlight.prototype=new MochiKit.Visual.Base(); +MochiKit.Base.update(MochiKit.Visual.Highlight.prototype,{__init__:function(_5c9,_5ca){ +this.element=MochiKit.DOM.getElement(_5c9); +_5ca=MochiKit.Base.update({startcolor:"#ffff99"},_5ca||{}); +this.start(_5ca); +},setup:function(){ +var b=MochiKit.Base; +var s=MochiKit.Style; +if(s.getStyle(this.element,"display")=="none"){ +this.cancel(); +return; +} +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"); +} +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]); +},update:function(_5cf){ +var m="#"; +MochiKit.Base.map(MochiKit.Base.bind(function(i){ +m+=MochiKit.Color.toColorPart(Math.round(this._base[i]+this._delta[i]*_5cf)); +},this),[0,1,2]); +MochiKit.Style.setStyle(this.element,{backgroundColor:m}); +},finish:function(){ +MochiKit.Style.setStyle(this.element,MochiKit.Base.update(this.oldStyle,{backgroundColor:this.options.restorecolor})); +}}); +MochiKit.Visual.ScrollTo=function(_5d2,_5d3){ +this.__init__(_5d2,_5d3); +}; +MochiKit.Visual.ScrollTo.prototype=new MochiKit.Visual.Base(); +MochiKit.Base.update(MochiKit.Visual.ScrollTo.prototype,{__init__:function(_5d4,_5d5){ +this.element=MochiKit.DOM.getElement(_5d4); +this.start(_5d5||{}); +},setup:function(){ +var p=MochiKit.Position; +p.prepare(); +var _5d7=p.cumulativeOffset(this.element); +if(this.options.offset){ +_5d7.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=(_5d7.y>max?max:_5d7.y)-this.scrollStart; +},update:function(_5d9){ +var p=MochiKit.Position; +p.prepare(); +window.scrollTo(p.windowOffset.x,this.scrollStart+(_5d9*this.delta)); +}}); +MochiKit.Visual.fade=function(_5db,_5dc){ +var s=MochiKit.Style; +var _5de=MochiKit.DOM.getElement(_5db).style.opacity||""; +_5dc=MochiKit.Base.update({from:s.getOpacity(_5db)||1,to:0,afterFinishInternal:function(_5df){ +if(_5df.options.to!==0){ +return; +} +s.hideElement(_5df.element); +s.setStyle(_5df.element,{opacity:_5de}); +}},_5dc||{}); +return new MochiKit.Visual.Opacity(_5db,_5dc); +}; +MochiKit.Visual.appear=function(_5e0,_5e1){ +var s=MochiKit.Style; +var v=MochiKit.Visual; +_5e1=MochiKit.Base.update({from:(s.getStyle(_5e0,"display")=="none"?0:s.getOpacity(_5e0)||0),to:1,afterFinishInternal:function(_5e4){ +v.forceRerendering(_5e4.element); +},beforeSetupInternal:function(_5e5){ +s.setOpacity(_5e5.element,_5e5.options.from); +s.showElement(_5e5.element); +}},_5e1||{}); +return new v.Opacity(_5e0,_5e1); +}; +MochiKit.Visual.puff=function(_5e6,_5e7){ +var s=MochiKit.Style; +var v=MochiKit.Visual; +_5e6=MochiKit.DOM.getElement(_5e6); +var _5ea={opacity:_5e6.style.opacity||"",position:s.getStyle(_5e6,"position"),top:_5e6.style.top,left:_5e6.style.left,width:_5e6.style.width,height:_5e6.style.height}; +_5e7=MochiKit.Base.update({beforeSetupInternal:function(_5eb){ +MochiKit.Position.absolutize(_5eb.effects[0].element); +},afterFinishInternal:function(_5ec){ +s.hideElement(_5ec.effects[0].element); +s.setStyle(_5ec.effects[0].element,_5ea); +}},_5e7||{}); +return new v.Parallel([new v.Scale(_5e6,200,{sync:true,scaleFromCenter:true,scaleContent:true,restoreAfterFinish:true}),new v.Opacity(_5e6,{sync:true,to:0})],_5e7); +}; +MochiKit.Visual.blindUp=function(_5ed,_5ee){ +var d=MochiKit.DOM; +_5ed=d.getElement(_5ed); +var _5f0=d.makeClipping(_5ed); +_5ee=MochiKit.Base.update({scaleContent:false,scaleX:false,restoreAfterFinish:true,afterFinishInternal:function(_5f1){ +MochiKit.Style.hideElement(_5f1.element); +d.undoClipping(_5f1.element,_5f0); +}},_5ee||{}); +return new MochiKit.Visual.Scale(_5ed,0,_5ee); +}; +MochiKit.Visual.blindDown=function(_5f2,_5f3){ +var d=MochiKit.DOM; +var s=MochiKit.Style; +_5f2=d.getElement(_5f2); +var _5f6=s.getElementDimensions(_5f2); +var _5f7; +_5f3=MochiKit.Base.update({scaleContent:false,scaleX:false,scaleFrom:0,scaleMode:{originalHeight:_5f6.h,originalWidth:_5f6.w},restoreAfterFinish:true,afterSetupInternal:function(_5f8){ +_5f7=d.makeClipping(_5f8.element); +s.setStyle(_5f8.element,{height:"0px"}); +s.showElement(_5f8.element); +},afterFinishInternal:function(_5f9){ +d.undoClipping(_5f9.element,_5f7); +}},_5f3||{}); +return new MochiKit.Visual.Scale(_5f2,100,_5f3); +}; +MochiKit.Visual.switchOff=function(_5fa,_5fb){ +var d=MochiKit.DOM; +_5fa=d.getElement(_5fa); +var _5fd=_5fa.style.opacity||""; +var _5fe; +var _5fb=MochiKit.Base.update({duration:0.3,scaleFromCenter:true,scaleX:false,scaleContent:false,restoreAfterFinish:true,beforeSetupInternal:function(_5ff){ +d.makePositioned(_5ff.element); +_5fe=d.makeClipping(_5ff.element); +},afterFinishInternal:function(_600){ +MochiKit.Style.hideElement(_600.element); +d.undoClipping(_600.element,_5fe); +d.undoPositioned(_600.element); +MochiKit.Style.setStyle(_600.element,{opacity:_5fd}); +}},_5fb||{}); +var v=MochiKit.Visual; +return new v.appear(_5fa,{duration:0.4,from:0,transition:v.Transitions.flicker,afterFinishInternal:function(_602){ +new v.Scale(_602.element,1,_5fb); +}}); +}; +MochiKit.Visual.dropOut=function(_603,_604){ +var d=MochiKit.DOM; +var s=MochiKit.Style; +_603=d.getElement(_603); +var _607={top:s.getStyle(_603,"top"),left:s.getStyle(_603,"left"),opacity:_603.style.opacity||""}; +_604=MochiKit.Base.update({duration:0.5,beforeSetupInternal:function(_608){ +d.makePositioned(_608.effects[0].element); +},afterFinishInternal:function(_609){ +s.hideElement(_609.effects[0].element); +d.undoPositioned(_609.effects[0].element); +s.setStyle(_609.effects[0].element,_607); +}},_604||{}); +var v=MochiKit.Visual; +return new v.Parallel([new v.Move(_603,{x:0,y:100,sync:true}),new v.Opacity(_603,{sync:true,to:0})],_604); +}; +MochiKit.Visual.shake=function(_60b,_60c){ +var d=MochiKit.DOM; +var v=MochiKit.Visual; +var s=MochiKit.Style; +_60b=d.getElement(_60b); +_60c=MochiKit.Base.update({x:-20,y:0,duration:0.05,afterFinishInternal:function(_610){ +d.undoPositioned(_610.element); +s.setStyle(_610.element,_611); +}},_60c||{}); +var _611={top:s.getStyle(_60b,"top"),left:s.getStyle(_60b,"left")}; +return new v.Move(_60b,{x:20,y:0,duration:0.05,afterFinishInternal:function(_612){ +new v.Move(_612.element,{x:-40,y:0,duration:0.1,afterFinishInternal:function(_613){ +new v.Move(_613.element,{x:40,y:0,duration:0.1,afterFinishInternal:function(_614){ +new v.Move(_614.element,{x:-40,y:0,duration:0.1,afterFinishInternal:function(_615){ +new v.Move(_615.element,{x:40,y:0,duration:0.1,afterFinishInternal:function(_616){ +new v.Move(_616.element,_60c); +}}); +}}); +}}); +}}); +}}); +}; +MochiKit.Visual.slideDown=function(_617,_618){ +var d=MochiKit.DOM; +var b=MochiKit.Base; +var s=MochiKit.Style; +_617=d.getElement(_617); +if(!_617.firstChild){ +throw "MochiKit.Visual.slideDown must be used on a element with a child"; +} +d.removeEmptyTextNodes(_617); +var _61c=s.getStyle(_617.firstChild,"bottom")||0; +var _61d=s.getElementDimensions(_617); +var _61e; +_618=b.update({scaleContent:false,scaleX:false,scaleFrom:0,scaleMode:{originalHeight:_61d.h,originalWidth:_61d.w},restoreAfterFinish:true,afterSetupInternal:function(_61f){ +d.makePositioned(_61f.element); +d.makePositioned(_61f.element.firstChild); +if(/Opera/.test(navigator.userAgent)){ +s.setStyle(_61f.element,{top:""}); +} +_61e=d.makeClipping(_61f.element); +s.setStyle(_61f.element,{height:"0px"}); +s.showElement(_61f.element); +},afterUpdateInternal:function(_620){ +s.setStyle(_620.element.firstChild,{bottom:(_620.dims[0]-_620.element.clientHeight)+"px"}); +},afterFinishInternal:function(_621){ +d.undoClipping(_621.element,_61e); +if(/MSIE/.test(navigator.userAgent)){ +d.undoPositioned(_621.element); +d.undoPositioned(_621.element.firstChild); +}else{ +d.undoPositioned(_621.element.firstChild); +d.undoPositioned(_621.element); +} +s.setStyle(_621.element.firstChild,{bottom:_61c}); +}},_618||{}); +return new MochiKit.Visual.Scale(_617,100,_618); +}; +MochiKit.Visual.slideUp=function(_622,_623){ +var d=MochiKit.DOM; +var b=MochiKit.Base; +var s=MochiKit.Style; +_622=d.getElement(_622); +if(!_622.firstChild){ +throw "MochiKit.Visual.slideUp must be used on a element with a child"; +} +d.removeEmptyTextNodes(_622); +var _627=s.getStyle(_622.firstChild,"bottom"); +var _628; +_623=b.update({scaleContent:false,scaleX:false,scaleMode:"box",scaleFrom:100,restoreAfterFinish:true,beforeStartInternal:function(_629){ +d.makePositioned(_629.element); +d.makePositioned(_629.element.firstChild); +if(/Opera/.test(navigator.userAgent)){ +s.setStyle(_629.element,{top:""}); +} +_628=d.makeClipping(_629.element); +s.showElement(_629.element); +},afterUpdateInternal:function(_62a){ +s.setStyle(_62a.element.firstChild,{bottom:(_62a.dims[0]-_62a.element.clientHeight)+"px"}); +},afterFinishInternal:function(_62b){ +s.hideElement(_62b.element); +d.undoClipping(_62b.element,_628); +d.undoPositioned(_62b.element.firstChild); +d.undoPositioned(_62b.element); +s.setStyle(_62b.element.firstChild,{bottom:_627}); +}},_623||{}); +return new MochiKit.Visual.Scale(_622,0,_623); +}; +MochiKit.Visual.squish=function(_62c,_62d){ +var d=MochiKit.DOM; +var b=MochiKit.Base; +var _630; +_62d=b.update({restoreAfterFinish:true,beforeSetupInternal:function(_631){ +_630=d.makeClipping(_631.element); +},afterFinishInternal:function(_632){ +MochiKit.Style.hideElement(_632.element); +d.undoClipping(_632.element,_630); +}},_62d||{}); +return new MochiKit.Visual.Scale(_62c,/Opera/.test(navigator.userAgent)?1:0,_62d); +}; +MochiKit.Visual.grow=function(_633,_634){ +var d=MochiKit.DOM; +var v=MochiKit.Visual; +var s=MochiKit.Style; +_633=d.getElement(_633); +_634=MochiKit.Base.update({direction:"center",moveTransition:v.Transitions.sinoidal,scaleTransition:v.Transitions.sinoidal,opacityTransition:v.Transitions.full},_634||{}); +var _638={top:_633.style.top,left:_633.style.left,height:_633.style.height,width:_633.style.width,opacity:_633.style.opacity||""}; +var dims=s.getElementDimensions(_633); +var _63a,_63b; +var _63c,_63d; +switch(_634.direction){ +case "top-left": +_63a=_63b=_63c=_63d=0; +break; +case "top-right": +_63a=dims.w; +_63b=_63d=0; +_63c=-dims.w; +break; +case "bottom-left": +_63a=_63c=0; +_63b=dims.h; +_63d=-dims.h; +break; +case "bottom-right": +_63a=dims.w; +_63b=dims.h; +_63c=-dims.w; +_63d=-dims.h; +break; +case "center": +_63a=dims.w/2; +_63b=dims.h/2; +_63c=-dims.w/2; +_63d=-dims.h/2; +break; +} +var _63e=MochiKit.Base.update({beforeSetupInternal:function(_63f){ +s.setStyle(_63f.effects[0].element,{height:"0px"}); +s.showElement(_63f.effects[0].element); +},afterFinishInternal:function(_640){ +d.undoClipping(_640.effects[0].element); +d.undoPositioned(_640.effects[0].element); +s.setStyle(_640.effects[0].element,_638); +}},_634||{}); +return new v.Move(_633,{x:_63a,y:_63b,duration:0.01,beforeSetupInternal:function(_641){ +s.hideElement(_641.element); +d.makeClipping(_641.element); +d.makePositioned(_641.element); +},afterFinishInternal:function(_642){ +new v.Parallel([new v.Opacity(_642.element,{sync:true,to:1,from:0,transition:_634.opacityTransition}),new v.Move(_642.element,{x:_63c,y:_63d,sync:true,transition:_634.moveTransition}),new v.Scale(_642.element,100,{scaleMode:{originalHeight:dims.h,originalWidth:dims.w},sync:true,scaleFrom:/Opera/.test(navigator.userAgent)?1:0,transition:_634.scaleTransition,restoreAfterFinish:true})],_63e); +}}); +}; +MochiKit.Visual.shrink=function(_643,_644){ +var d=MochiKit.DOM; +var v=MochiKit.Visual; +var s=MochiKit.Style; +_643=d.getElement(_643); +_644=MochiKit.Base.update({direction:"center",moveTransition:v.Transitions.sinoidal,scaleTransition:v.Transitions.sinoidal,opacityTransition:v.Transitions.none},_644||{}); +var _648={top:_643.style.top,left:_643.style.left,height:_643.style.height,width:_643.style.width,opacity:_643.style.opacity||""}; +var dims=s.getElementDimensions(_643); +var _64a,_64b; +switch(_644.direction){ +case "top-left": +_64a=_64b=0; +break; +case "top-right": +_64a=dims.w; +_64b=0; +break; +case "bottom-left": +_64a=0; +_64b=dims.h; +break; +case "bottom-right": +_64a=dims.w; +_64b=dims.h; +break; +case "center": +_64a=dims.w/2; +_64b=dims.h/2; +break; +} +var _64c; +var _64d=MochiKit.Base.update({beforeStartInternal:function(_64e){ +_64c=d.makePositioned(_64e.effects[0].element); +d.makeClipping(_64e.effects[0].element); +},afterFinishInternal:function(_64f){ +s.hideElement(_64f.effects[0].element); +d.undoClipping(_64f.effects[0].element,_64c); +d.undoPositioned(_64f.effects[0].element); +s.setStyle(_64f.effects[0].element,_648); +}},_644||{}); +return new v.Parallel([new v.Opacity(_643,{sync:true,to:0,from:1,transition:_644.opacityTransition}),new v.Scale(_643,/Opera/.test(navigator.userAgent)?1:0,{sync:true,transition:_644.scaleTransition,restoreAfterFinish:true}),new v.Move(_643,{x:_64a,y:_64b,sync:true,transition:_644.moveTransition})],_64d); +}; +MochiKit.Visual.pulsate=function(_650,_651){ +var d=MochiKit.DOM; +var v=MochiKit.Visual; +var b=MochiKit.Base; +var _655=d.getElement(_650).style.opacity||""; +_651=b.update({duration:3,from:0,afterFinishInternal:function(_656){ +MochiKit.Style.setStyle(_656.element,{opacity:_655}); +}},_651||{}); +var _657=_651.transition||v.Transitions.sinoidal; +var _658=b.bind(function(pos){ +return _657(1-v.Transitions.pulse(pos)); +},_657); +b.bind(_658,_657); +return new v.Opacity(_650,b.update({transition:_658},_651)); +}; +MochiKit.Visual.fold=function(_65a,_65b){ +var d=MochiKit.DOM; +var v=MochiKit.Visual; +var s=MochiKit.Style; +_65a=d.getElement(_65a); +var _65f={top:_65a.style.top,left:_65a.style.left,width:_65a.style.width,height:_65a.style.height}; +var _660=d.makeClipping(_65a); +_65b=MochiKit.Base.update({scaleContent:false,scaleX:false,afterFinishInternal:function(_661){ +new v.Scale(_65a,1,{scaleContent:false,scaleY:false,afterFinishInternal:function(_662){ +s.hideElement(_662.element); +d.undoClipping(_662.element,_660); +s.setStyle(_662.element,_65f); +}}); +}},_65b||{}); +return new v.Scale(_65a,5,_65b); +}; +MochiKit.Visual.Color=MochiKit.Color.Color; +MochiKit.Visual.getElementsComputedStyle=MochiKit.DOM.computedStyle; +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); +if(typeof (MochiKit)=="undefined"){ +MochiKit={}; +} +if(typeof (MochiKit.MochiKit)=="undefined"){ +MochiKit.MochiKit={}; +} +MochiKit.MochiKit.NAME="MochiKit.MochiKit"; +MochiKit.MochiKit.VERSION="1.4"; +MochiKit.MochiKit.__repr__=function(){ +return "["+this.NAME+" "+this.VERSION+"]"; +}; +MochiKit.MochiKit.toString=function(){ +return this.__repr__(); +}; +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 _666=MochiKit.Base.extend; +var self=MochiKit.MochiKit; +var _668=self.SUBMODULES; +var _669=[]; +var _66a=[]; +var _66b={}; +var i,k,m,all; +for(i=0;i<_668.length;i++){ +m=MochiKit[_668[i]]; +_666(_669,m.EXPORT); +_666(_66a,m.EXPORT_OK); +for(k in m.EXPORT_TAGS){ +_66b[k]=_666(_66b[k],m.EXPORT_TAGS[k]); +} +all=m.EXPORT_TAGS[":all"]; +if(!all){ +all=_666(null,m.EXPORT,m.EXPORT_OK); +} +var j; +for(j=0;j<all.length;j++){ +k=all[j]; +self[k]=m[k]; +} +} +self.EXPORT=_669; +self.EXPORT_OK=_66a; +self.EXPORT_TAGS=_66b; +}()); +}else{ +if(typeof (MochiKit.__compat__)=="undefined"){ +MochiKit.__compat__=true; +} +(function(){ +if(typeof (document)=="undefined"){ +return; +} +var _671=document.getElementsByTagName("script"); +var _672="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +var base=null; +var _674=null; +var _675={}; +var i; +for(i=0;i<_671.length;i++){ +var src=_671[i].getAttribute("src"); +if(!src){ +continue; +} +_675[src]=true; +if(src.match(/MochiKit.js$/)){ +base=src.substring(0,src.lastIndexOf("MochiKit.js")); +_674=_671[i]; +} +} +if(base===null){ +return; +} +var _678=MochiKit.MochiKit.SUBMODULES; +for(var i=0;i<_678.length;i++){ +if(MochiKit[_678[i]]){ +continue; +} +var uri=base+_678[i]+".js"; +if(uri in _675){ +continue; +} +if(document.documentElement&&document.documentElement.namespaceURI==_672){ +var s=document.createElementNS(_672,"script"); +s.setAttribute("id","MochiKit_"+base+_678[i]); +s.setAttribute("src",uri); +s.setAttribute("type","application/x-javascript"); +_674.parentNode.appendChild(s); +}else{ +document.write("<script src=\""+uri+"\" type=\"text/javascript\"></script>"); +} +} +})(); +} + + diff --git a/testing/mochitest/README.txt b/testing/mochitest/README.txt new file mode 100644 index 000000000..45caa674d --- /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.jsm b/testing/mochitest/ShutdownLeaksCollector.jsm new file mode 100644 index 000000000..4786b65dc --- /dev/null +++ b/testing/mochitest/ShutdownLeaksCollector.jsm @@ -0,0 +1,54 @@ +/* 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 Ci = Components.interfaces; +var Cc = Components.classes; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +this.EXPORTED_SYMBOLS = ["ContentCollector"]; + +// 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. + +var ContentCollector = { + init: function() { + let processType = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType; + if (processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { + // In the main process, we handle triggering collections in browser-test.js + return; + } + + let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"] + .getService(Ci.nsISyncMessageSender); + cpmm.addMessageListener("browser-test:collect-request", this); + }, + + receiveMessage: function(aMessage) { + switch (aMessage.name) { + case "browser-test:collect-request": + Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize"); + + Cu.forceGC(); + Cu.forceCC(); + + Cu.schedulePreciseShrinkingGC(() => { + Cu.forceCC(); + + let pid = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processID; + dump("Completed ShutdownLeaks collections in process " + pid + "\n")}); + + let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"] + .getService(Ci.nsISyncMessageSender); + cpmm.removeMessageListener("browser-test:collect-request", this); + + break; + } + } + +}; +ContentCollector.init(); diff --git a/testing/mochitest/bisection.py b/testing/mochitest/bisection.py new file mode 100644 index 000000000..f3847bd51 --- /dev/null +++ b/testing/mochitest/bisection.py @@ -0,0 +1,280 @@ +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
+
+ 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/bootstrap.js b/testing/mochitest/bootstrap.js new file mode 100644 index 000000000..b6111232c --- /dev/null +++ b/testing/mochitest/bootstrap.js @@ -0,0 +1,84 @@ +/* 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 { utils: Cu, interfaces: Ci, classes: Cc } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +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: function(win) { + win.nativeConsole = win.console; + XPCOMUtils.defineLazyModuleGetter(win, "console", + "resource://gre/modules/Console.jsm"); + }, + + tearDownWindow: function(win) { + if (win.nativeConsole) { + win.console = win.nativeConsole; + win.nativeConsole = undefined; + } + }, + + onOpenWindow: function (win) { + win = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); + + win.addEventListener("load", function listener() { + win.removeEventListener("load", listener, false); + if (win.document.documentElement.getAttribute("windowtype") == "navigator:browser") { + WindowListener.setupWindow(win); + } + }, false); + } +} + +function loadMochitest(e) { + let flavor = e.detail[0]; + let url = e.detail[1]; + + let win = Services.wm.getMostRecentWindow("navigator:browser"); + win.removeEventListener('mochitest-load', loadMochitest); + + // for mochitest-plain, navigating to the url is all we need + win.loadURI(url); + if (flavor == "mochitest") { + return; + } + + WindowListener.setupWindow(win); + Services.wm.addListener(WindowListener); + + let overlay; + if (flavor == "jetpack-addon") { + overlay = "chrome://mochikit/content/jetpack-addon-overlay.xul"; + } else if (flavor == "jetpack-package") { + overlay = "chrome://mochikit/content/jetpack-package-overlay.xul"; + } else { + overlay = "chrome://mochikit/content/browser-test-overlay.xul"; + } + + win.document.loadOverlay(overlay, null); +} + +function startup(data, reason) { + let win = Services.wm.getMostRecentWindow("navigator:browser"); + // wait for event fired from start_desktop.js containing the + // suite and url to load + win.addEventListener('mochitest-load', loadMochitest); +} + +function shutdown(data, reason) { + let windows = Services.wm.getEnumerator("navigator:browser"); + while (windows.hasMoreElements()) { + let win = windows.getNext().QueryInterface(Ci.nsIDOMWindow); + WindowListener.tearDownWindow(win); + } + + Services.wm.removeListener(WindowListener); +} + +function install(data, reason) {} +function uninstall(data, reason) {} diff --git a/testing/mochitest/browser-harness.xul b/testing/mochitest/browser-harness.xul new file mode 100644 index 000000000..eef627732 --- /dev/null +++ b/testing/mochitest/browser-harness.xul @@ -0,0 +1,337 @@ +<?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 type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/MozillaLogger.js"/> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/LogController.js"/> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/StructuredLog.jsm"/> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/TestRunner.js"/> + <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"/> + <script type="application/javascript" src="chrome://mochikit/content/manifestLibrary.js" /> + <script type="application/javascript" src="chrome://mochikit/content/chunkifyTests.js"/> + <style xmlns="http://www.w3.org/1999/xhtml"><![CDATA[ + #results { + margin: 5px; + background-color: window; + -moz-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;version=1.7"><![CDATA[ + if (Cc === undefined) { + var Cc = Components.classes; + var Ci = Components.interfaces; + } + + 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.xul) | " + + "Error trying to log to " + gConfig.logFile + ": " + ex + "\n"); + } + } + delete this.fileLogger; + return this.fileLogger = logger; + }, + structuredLogger: TestRunner.structuredLogger, + dump: function (str) { + this.structuredLogger.info(str); + + if (this.fileLogger) + this.fileLogger.log(str); + }, + + done: function () { + 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.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; + }, + + addResult: function addResult(result) { + this.lastOutputTime = Date.now(); + this.results.push(result); + + if (result.info) { + if (result.msg) { + this.dumper.structuredLogger.info(result.msg); + } + return; + } + + this.dumper.structuredLogger.testStatus(this.path, + result.name, + result.status, + result.expected, + result.msg); + }, + + 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 scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + var srvScope = {}; + 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 windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']. + getService(Ci.nsIWindowMediator); + var winType = gConfig.testRoot == "browser" ? "navigator:browser" : null; + if (!winType) { + throw new Error("Unrecognized gConfig.testRoot: " + gConfig.testRoot); + } + var testWin = windowMediator.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) { + let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager); + tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); + } + + function waitForFocus(callback, win) { + // If "win" is already focused, just call the callback. + let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + if (fm.focusedWindow == win) { + executeSoon(callback); + return; + } + + // Otherwise focus it, and wait for the focus event. + win.addEventListener("focus", function listener() { + win.removeEventListener("focus", listener, true); + executeSoon(callback); + }, 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=\""; + log += failCount != 0 ? "failure" : + 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) { + // Focus our window, to display the results + window.focus(); + + if (gConfig.closeWhenDone) { + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup); + appStartup.quit(Ci.nsIAppStartup.eForceQuit); + return; + } + + // UI + document.getElementById("results").innerHTML = getHTMLLogFromTests(aTests); + setStatus("Done."); + } + + function scrollTo(id) { + var line = document.getElementById(id); + if (!line) + return; + + var boxObject = document.getElementById("results").parentNode.boxObject; + boxObject.scrollToElement(line); + } + ]]></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-overlay.xul b/testing/mochitest/browser-test-overlay.xul new file mode 100644 index 000000000..f383f153d --- /dev/null +++ b/testing/mochitest/browser-test-overlay.xul @@ -0,0 +1,12 @@ +<?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/. --> + +<overlay id="browserTestOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"/> + <script type="application/javascript" src="chrome://mochikit/content/mochitest-e10s-utils.js"/> + <script type="application/javascript" src="chrome://mochikit/content/browser-test.js"/> +</overlay> diff --git a/testing/mochitest/browser-test.js b/testing/mochitest/browser-test.js new file mode 100644 index 000000000..dbaaf29a8 --- /dev/null +++ b/testing/mochitest/browser-test.js @@ -0,0 +1,1087 @@ +/* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */ +// Test timeout (seconds) +var gTimeoutSeconds = 45; +var gConfig; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch", + "resource:///modules/ContentSearch.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "SelfSupportBackend", + "resource:///modules/SelfSupportBackend.jsm"); + +const SIMPLETEST_OVERRIDES = + ["ok", "is", "isnot", "todo", "todo_is", "todo_isnot", "info", "expectAssertions", "requestCompleteLog"]; + +// non-android is bootstrapped by marionette +if (Services.appinfo.OS == 'Android') { + window.addEventListener("load", function testOnLoad() { + window.removeEventListener("load", testOnLoad); + window.addEventListener("MozAfterPaint", function testOnMozAfterPaint() { + window.removeEventListener("MozAfterPaint", testOnMozAfterPaint); + setTimeout(testInit, 0); + }); + }); +} else { + setTimeout(testInit, 0); +} + +var TabDestroyObserver = { + outstanding: new Set(), + promiseResolver: null, + + init: function() { + Services.obs.addObserver(this, "message-manager-close", false); + Services.obs.addObserver(this, "message-manager-disconnect", false); + }, + + destroy: function() { + Services.obs.removeObserver(this, "message-manager-close"); + Services.obs.removeObserver(this, "message-manager-disconnect"); + }, + + observe: function(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: function() { + 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.xul", "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) { + 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 + var webNav = content.window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation); + webNav.loadURI(url, null, null, null, null); + }; + + 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);'; + messageManager.addMessageListener("chromeEvent", messageHandler); + 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", processCount); + } + + let globalMM = Cc["@mozilla.org/globalmessagemanager;1"] + .getService(Ci.nsIMessageListenerManager); + globalMM.loadFrameScript("chrome://mochikit/content/shutdown-leaks-collector.js", true); + } else { + // In non-e10s, only run the ShutdownLeaksCollector in the parent process. + Components.utils.import("chrome://mochikit/content/ShutdownLeaksCollector.jsm"); + } + + let gmm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager); + gmm.loadFrameScript("chrome://mochikit/content/tests/SimpleTest/AsyncUtilsContent.js", true); +} + +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); + var simpleTestScope = {}; + this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/specialpowersAPI.js", simpleTestScope); + this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/SpecialPowersObserverAPI.js", simpleTestScope); + this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromePowers.js", 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; + + 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.Task = Task; + this.ContentTask = Components.utils.import("resource://testing-common/ContentTask.jsm", null).ContentTask; + this.BrowserTestUtils = Components.utils.import("resource://testing-common/BrowserTestUtils.jsm", null).BrowserTestUtils; + this.TestUtils = Components.utils.import("resource://testing-common/TestUtils.jsm", null).TestUtils; + this.Task.Debugging.maintainStack = true; + this.Promise = Components.utils.import("resource://gre/modules/Promise.jsm", null).Promise; + this.Assert = Components.utils.import("resource://testing-common/Assert.jsm", null).Assert; + + this.SimpleTestOriginal = {}; + SIMPLETEST_OVERRIDES.forEach(m => { + this.SimpleTestOriginal[m] = this.SimpleTest[m]; + }); + + this._coverageCollector = null; + + this._toleratedUncaughtRejections = null; + this._uncaughtErrorObserver = function({message, date, fileName, stack, lineNumber}) { + let error = message; + if (fileName || lineNumber) { + error = { + fileName: fileName, + lineNumber: lineNumber, + message: message, + toString: function() { + return message; + } + }; + } + + // We may have a whitelist of rejections we wish to tolerate. + let tolerate = this._toleratedUncaughtRejections && + this._toleratedUncaughtRejections.indexOf(message) != -1; + let name = "A promise chain failed to handle a rejection: "; + if (tolerate) { + name = "WARNING: (PLEASE FIX THIS AS PART OF BUG 1077403) " + name; + } + + this.currentTest.addResult( + new testResult( + /*success*/tolerate, + /*name*/name, + /*error*/error, + /*known*/tolerate, + /*stack*/stack)); + }.bind(this); +} +Tester.prototype = { + EventUtils: {}, + SimpleTest: {}, + Task: null, + ContentTask: null, + ExtensionTestUtils: null, + Assert: null, + + repeat: 0, + runUntilFailure: false, + checker: null, + currentTestIndex: -1, + lastStartTime: null, + lastAssertionCount: 0, + failuresFromInitialWindowState: 0, + + get currentTest() { + return this.tests[this.currentTestIndex]; + }, + get done() { + return this.currentTestIndex == this.tests.length - 1; + }, + + 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.repeat) + this.repeat = gConfig.repeat; + + if (gConfig.jscovDirPrefix) { + let coveragePath = gConfig.jscovDirPrefix; + let {CoverageCollector} = Cu.import("resource://testing-common/CoverageUtils.jsm", + {}); + 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", + ]; + + this.Promise.Debugging.clearUncaughtErrorObservers(); + this.Promise.Debugging.addUncaughtErrorObserver(this._uncaughtErrorObserver); + + if (this.tests.length) + this.waitForGraphicsTestWindowToBeGone(this.nextTest.bind(this)); + else + this.finish(); + }, + + waitForGraphicsTestWindowToBeGone(aCallback) { + let windowsEnum = Services.wm.getEnumerator(null); + while (windowsEnum.hasMoreElements()) { + let win = windowsEnum.getNext(); + 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; + let startTime = Date.now(); + 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 && gBrowser.tabs.length > 1) { + while (gBrowser.tabs.length > 1) { + let lastTab = gBrowser.tabContainer.lastChild; + let msg = baseMsg.replace("{elt}", "tab") + + ": " + lastTab.linkedBrowser.currentURI.spec; + this.currentTest.addResult(new testResult(false, msg, "", false)); + gBrowser.removeTab(lastTab); + } + } + + // Replace the last tab with a fresh one + if (window.gBrowser) { + let newTab = gBrowser.addTab("about:blank", { skipAnimation: true }); + gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true }); + gBrowser.stop(); + } + + // Remove stale windows + this.structuredLogger.info("checking window state"); + let windowsEnum = Services.wm.getEnumerator(null); + let createdFakeTestForLogging = false; + while (windowsEnum.hasMoreElements()) { + let win = windowsEnum.getNext(); + if (win != window && !win.closed && + win.document.documentElement.getAttribute("id") != "browserTestHarness") { + let type = win.document.documentElement.getAttribute("windowtype"); + switch (type) { + case "navigator:browser": + type = "browser 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(false, msg, "", false)); + } else { + if (!createdFakeTestForLogging) { + createdFakeTestForLogging = true; + this.structuredLogger.testStart("browser-test.js"); + } + this.failuresFromInitialWindowState++; + this.structuredLogger.testStatus("browser-test.js", + msg, "FAIL", false, ""); + } + + win.close(); + } + } + if (createdFakeTestForLogging) { + let time = Date.now() - startTime; + this.structuredLogger.testEnd("browser-test.js", + "OK", + undefined, + "finished window state check in " + time + "ms"); + } + + // Make sure the window is raised before each test. + this.SimpleTest.waitForFocus(aCallback); + }, + + finish: function Tester_finish(aSkipSummary) { + this.Promise.Debugging.flushUncaughtErrors(); + + 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; + + if (this.repeat > 0) { + --this.repeat; + this.currentTestIndex = -1; + this.nextTest(); + } else { + TabDestroyObserver.destroy(); + Services.console.unregisterListener(this); + this.Promise.Debugging.clearUncaughtErrorObservers(); + this._treatUncaughtRejectionsAsFailures = false; + + // In the main process, we print the ShutdownLeaksCollector message here. + let pid = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processID; + dump("Completed ShutdownLeaks collections in process " + pid + "\n"); + + this.structuredLogger.info("TEST-START | Shutdown"); + + if (this.tests.length) { + let e10sMode = 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.testEnd("browser-test.js", + "FAIL", + "PASS", + "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.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 + } + }, + + nextTest: Task.async(function*() { + if (this.currentTest) { + this.Promise.Debugging.flushUncaughtErrors(); + if (this._coverageCollector) { + this._coverageCollector.recordTestCoverage(this.currentTest.path); + } + + // Run cleanup functions for the current test before moving on to the + // next one. + let testScope = this.currentTest.scope; + while (testScope.__cleanupFunctions.length > 0) { + let func = testScope.__cleanupFunctions.shift(); + try { + yield func.apply(testScope); + } + catch (ex) { + this.currentTest.addResult(new testResult(false, "Cleanup function threw an exception", ex, false)); + } + } + + if (this.currentTest.passCount === 0 && + this.currentTest.failCount === 0 && + this.currentTest.todoCount === 0) { + this.currentTest.addResult(new testResult(false, "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.", "", false)); + } + + if (testScope.__expected == 'fail' && testScope.__num_failed <= 0) { + this.currentTest.addResult(new testResult(false, "We expected at least one assertion to fail because this test file was marked as fail-if in the manifest!", "", true)); + } + + this.Promise.Debugging.flushUncaughtErrors(); + + let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + if (winUtils.isTestControllingRefreshes) { + this.currentTest.addResult(new testResult(false, "test left refresh driver under test control", "", false)); + winUtils.restoreNormalRefresh(); + } + + if (this.SimpleTest.isExpectingUncaughtException()) { + this.currentTest.addResult(new testResult(false, "expectUncaughtException was called but no uncaught exception was detected!", "", false)); + } + + 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.indexOf(prop) == -1) { + this._globalProperties.push(prop); + if (this._globalPropertyWhitelist.indexOf(prop) == -1) + this.currentTest.addResult(new testResult(false, "test left unexpected property on window: " + prop, "", false)); + } + }, this); + + // Clear document.popupNode. The test could have set it to a custom value + // for its own purposes, nulling it out it will go back to the default + // behavior of returning the last opened popup. + document.popupNode = null; + + yield new Promise(resolve => SpecialPowers.flushPrefEnv(resolve)); + + // Notify a long running test problem if it didn't end up in a timeout. + if (this.currentTest.unexpectedTimeouts && !this.currentTest.timedOut) { + let msg = "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."; + this.currentTest.addResult(new testResult(false, msg, "", false)); + } + + // 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) { + let msg = "Assertion count " + numAsserts + + " is greater than expected range " + + min + "-" + max + " assertions."; + // TEST-UNEXPECTED-FAIL (TEMPORARILY TEST-KNOWN-FAIL) + //this.currentTest.addResult(new testResult(false, msg, "", false)); + this.currentTest.addResult(new testResult(true, msg, "", true)); + } else if (numAsserts < min) { + let msg = "Assertion count " + numAsserts + + " is less than expected range " + + min + "-" + max + " assertions."; + // TEST-UNEXPECTED-PASS + this.currentTest.addResult(new testResult(false, msg, "", true)); + } else if (numAsserts > 0) { + let msg = "Assertion count " + numAsserts + + " is within expected range " + + min + "-" + max + " assertions."; + // TEST-KNOWN-FAIL + this.currentTest.addResult(new testResult(true, msg, "", true)); + } + } + + // Dump memory stats for main thread. + if (Cc["@mozilla.org/xre/runtime;1"] + .getService(Ci.nsIXULRuntime) + .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 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((function () { + if (this.done) { + if (this._coverageCollector) { + this._coverageCollector.finalize(); + } + + // 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"); + sidebar.setAttribute("src", "data:text/html;charset=utf-8,"); + sidebar.docShell.createAboutBlankContentViewer(null); + sidebar.setAttribute("src", "about:blank"); + + SelfSupportBackend.uninit(); + SocialShare.uninit(); + } + + // Destroy BackgroundPageThumbs resources. + let {BackgroundPageThumbs} = + Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", {}); + BackgroundPageThumbs._destroy(); + + // Destroy preloaded browsers. + if (gBrowser._preloadedBrowser) { + let browser = gBrowser._preloadedBrowser; + gBrowser._preloadedBrowser = null; + gBrowser.getNotificationBox(browser).remove(); + } + } + + // 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} = + Cu.import("resource://gre/modules/AsyncShutdown.jsm", {}); + + 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", null); + + 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; + } + + this.currentTestIndex++; + this.execTest(); + }).bind(this)); + }), + + execTest: function Tester_execTest() { + this.structuredLogger.testStart(this.currentTest.path); + + this.SimpleTest.reset(); + + // Load the tests into a testscope + let currentScope = this.currentTest.scope = new testScope(this, this.currentTest, this.currentTest.expected); + let currentTest = this.currentTest; + + // Import utils in the test scope. + this.currentTest.scope.EventUtils = this.EventUtils; + this.currentTest.scope.SimpleTest = this.SimpleTest; + this.currentTest.scope.gTestPath = this.currentTest.path; + this.currentTest.scope.Task = this.Task; + this.currentTest.scope.ContentTask = this.ContentTask; + this.currentTest.scope.BrowserTestUtils = this.BrowserTestUtils; + this.currentTest.scope.TestUtils = this.TestUtils; + this.currentTest.scope.ExtensionTestUtils = this.ExtensionTestUtils; + // Pass a custom report function for mochitest style reporting. + this.currentTest.scope.Assert = new this.Assert(function(err, message, stack) { + let res; + if (err) { + res = new testResult(false, err.message, err.stack, false, err.stack); + } else { + res = new testResult(true, message, "", false, stack); + } + currentTest.addResult(res); + }); + + this.ContentTask.setTestScope(currentScope); + + // Allow Assert.jsm methods to be tacked to the current scope. + this.currentTest.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]; + }, this.currentTest.scope); + + //load the tools to work with chrome .jar and remote + try { + this._scriptLoader.loadSubScript("chrome://mochikit/content/chrome-harness.js", this.currentTest.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, this.currentTest.scope); + } catch (ex) { + // Ignore if no head.js exists, but report all other errors. Note this + // will also ignore an existing head.js attempting to import a missing + // module - see bug 755558 for why this strategy is preferred anyway. + if (!/^Error opening input stream/.test(ex.toString())) { + this.currentTest.addResult(new testResult(false, "head.js import threw an exception", ex, false)); + } + } + + // Import the test script. + try { + this._scriptLoader.loadSubScript(this.currentTest.path, + this.currentTest.scope); + this.Promise.Debugging.flushUncaughtErrors(); + // 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 "Cannot run both a add_task test and a normal test at the same time."; + } + let Promise = this.Promise; + this.Task.spawn(function*() { + let task; + while ((task = this.__tasks.shift())) { + this.SimpleTest.info("Entering test " + task.name); + try { + yield task(); + } catch (ex) { + let isExpected = !!this.SimpleTest.isExpectingUncaughtException(); + let stack = (typeof ex == "object" && "stack" in ex)?ex.stack:null; + let name = "Uncaught exception"; + let result = new testResult(isExpected, name, ex, false, stack); + currentTest.addResult(result); + } + Promise.Debugging.flushUncaughtErrors(); + this.SimpleTest.info("Leaving test " + task.name); + } + this.finish(); + }.bind(currentScope)); + } else if (typeof this.currentTest.scope.test == "function") { + this.currentTest.scope.test(); + } else { + throw "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) { + let isExpected = !!this.SimpleTest.isExpectingUncaughtException(); + if (!this.SimpleTest.isIgnoringAllUncaughtExceptions()) { + this.currentTest.addResult(new testResult(isExpected, "Exception thrown", ex, false)); + 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; + } + + self.currentTest.addResult(new testResult(false, "Test timed out", null, false)); + self.currentTest.timedOut = true; + self.currentTest.scope.__waitTimer = null; + self.nextTest(); + }, gTimeoutSeconds * 1000]); + } + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIConsoleListener) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } +}; + +function testResult(aCondition, aName, aDiag, aIsTodo, aStack) { + this.name = aName; + this.msg = ""; + + this.info = false; + this.pass = !!aCondition; + this.todo = aIsTodo; + + if (this.pass) { + if (aIsTodo) { + this.status = "FAIL"; + this.expected = "FAIL"; + } else { + this.status = "PASS"; + this.expected = "PASS"; + } + + } else { + if (aDiag) { + if (typeof aDiag == "object" && "fileName" in aDiag) { + // we have an exception - print filename and linenumber information + this.msg += "at " + aDiag.fileName + ":" + aDiag.lineNumber + " - "; + } + this.msg += String(aDiag); + } + if (aStack) { + this.msg += "\nStack trace:\n"; + let normalized; + if (aStack instanceof Components.interfaces.nsIStackFrame) { + let frames = []; + for (let frame = aStack; frame; frame = frame.caller) { + frames.push(frame.filename + ":" + frame.name + ":" + frame.lineNumber); + } + normalized = frames.join("\n"); + } else { + normalized = "" + aStack; + } + this.msg += Task.Debugging.generateReadableStack(normalized, " "); + } + if (aIsTodo) { + this.status = "PASS"; + this.expected = "FAIL"; + } else { + this.status = "FAIL"; + this.expected = "PASS"; + } + + if (gConfig.debugOnFailure) { + // You've hit this line because you requested to break into the + // debugger upon a testcase failure on your test run. + debugger; + } + } +} + +function testMessage(aName) { + this.msg = aName || ""; + 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; + this.__expected = expected; + this.__num_failed = 0; + + var self = this; + this.ok = function test_ok(condition, name, diag, stack) { + if (self.__expected == 'fail') { + if (!condition) { + self.__num_failed++; + condition = true; + } + } + + aTest.addResult(new testResult(condition, name, diag, false, + stack ? stack : Components.stack.caller)); + }; + this.is = function test_is(a, b, name) { + self.ok(a == b, name, "Got " + a + ", expected " + b, false, + Components.stack.caller); + }; + this.isnot = function test_isnot(a, b, name) { + self.ok(a != b, name, "Didn't expect " + a + ", but got it", false, + Components.stack.caller); + }; + this.todo = function test_todo(condition, name, diag, stack) { + aTest.addResult(new testResult(!condition, name, diag, true, + stack ? stack : Components.stack.caller)); + }; + this.todo_is = function test_todo_is(a, b, name) { + self.todo(a == b, name, "Got " + a + ", expected " + b, + Components.stack.caller); + }; + this.todo_isnot = function test_todo_isnot(a, b, name) { + self.todo(a != b, name, "Didn't expect " + a + ", but got it", + Components.stack.caller); + }; + this.info = function test_info(name) { + aTest.addResult(new testMessage(name)); + }; + + this.executeSoon = function test_executeSoon(func) { + Services.tm.mainThread.dispatch({ + run: function() { + func(); + } + }, Ci.nsIThread.DISPATCH_NORMAL); + }; + + 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.thisTestLeaksUncaughtRejectionsAndShouldBeFixed = function(...rejections) { + if (!aTester._toleratedUncaughtRejections) { + aTester._toleratedUncaughtRejections = []; + } + aTester._toleratedUncaughtRejections.push(...rejections); + }; + + 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 "bad parameter to expectAssertions"; + } + self.__expectedMinAsserts = min; + self.__expectedMaxAsserts = max; + }; + + this.setExpected = function test_setExpected() { + self.__expected = 'fail'; + }; + + 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(); + }) + }; +} +testScope.prototype = { + __done: true, + __tasks: null, + __waitTimer: null, + __cleanupFunctions: [], + __timeoutFactor: 1, + __expectedMinAsserts: 0, + __expectedMaxAsserts: 0, + __expected: 'pass', + + EventUtils: {}, + SimpleTest: {}, + Task: null, + ContentTask: null, + BrowserTestUtils: null, + TestUtils: null, + ExtensionTestUtils: null, + Assert: null, + + /** + * Add a test function which is a Task function. + * + * Task functions are functions fed into Task.jsm's Task.spawn(). They are + * generators that emit promises. + * + * If an exception is thrown, an assertion fails, or if a rejected + * promise is yielded, the test function aborts immediately and the test is + * reported as a failure. Execution continues with the next test function. + * + * To trigger premature (but successful) termination of the function, simply + * return or throw a Task.Result instance. + * + * Example usage: + * + * add_task(function test() { + * let result = yield Promise.resolve(true); + * + * ok(result); + * + * let secondary = yield someFunctionThatReturnsAPromise(result); + * is(secondary, "expected value"); + * }); + * + * add_task(function test_early_return() { + * let result = yield somethingThatReturnsAPromise(); + * + * if (!result) { + * // Test is ended immediately, with success. + * return; + * } + * + * is(result, "foo"); + * }); + */ + add_task: function(aFunction) { + if (!this.__tasks) { + this.waitForExplicitFinish(); + this.__tasks = []; + } + this.__tasks.push(aFunction.bind(this)); + }, + + destroy: function test_destroy() { + for (let prop in this) + delete this[prop]; + } +}; diff --git a/testing/mochitest/browser.eslintrc.js b/testing/mochitest/browser.eslintrc.js new file mode 100644 index 000000000..c4e3349cd --- /dev/null +++ b/testing/mochitest/browser.eslintrc.js @@ -0,0 +1,50 @@ +// Parent config file for all browser-chrome files. +module.exports = { + "rules": { + "mozilla/import-headjs-globals": "warn", + "mozilla/import-browserjs-globals": "warn", + "mozilla/mark-test-function-used": "warn", + }, + + "env": { + "browser": true, + //"node": true + }, + + // All globals made available in the test environment. + "globals": { + "add_task": false, + "Assert": false, + "BrowserTestUtils": false, + "content": false, + "ContentTask": false, + "ContentTaskUtils": false, + "EventUtils": false, + "executeSoon": false, + "expectUncaughtException": false, + "export_assertions": false, + "extractJarToTmp": false, + "finish": false, + "getJar": false, + "getRootDirectory": false, + "getTestFilePath": false, + "gTestPath": false, + "info": false, + "ignoreAllUncaughtExceptions": false, + "is": false, + "isnot": false, + "ok": false, + "registerCleanupFunction": false, + "requestLongerTimeout": false, + "SimpleTest": false, + "SpecialPowers": false, + "TestUtils": false, + "thisTestLeaksUncaughtRejectionsAndShouldBeFixed": false, + "todo": false, + "todo_is": false, + "todo_isnot": false, + "waitForClipboard": false, + "waitForExplicitFinish": false, + "waitForFocus": false, + } +}; diff --git a/testing/mochitest/chrome-harness.js b/testing/mochitest/chrome-harness.js new file mode 100644 index 000000000..569fa2ce6 --- /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/. */ + + +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +/* + * 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) { + var ios = Components.classes["@mozilla.org/network/io-service;1"]. + getService(Components.interfaces.nsIIOService); + return ios.newURI(url, null, null); +} + +/* + * 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 = Components.classes["@mozilla.org/chrome/chrome-registry;1"]. + getService(Components.interfaces.nsIChromeRegistry). + convertChromeURL(chromeURI); + + try { + resolvedURI = resolvedURI.QueryInterface(Components.interfaces.nsIJARURI); + } catch (ex) {} //not a jar file + + return resolvedURI; +} + +/** + * getChromeDir is intended to be called after getResolvedURI and convert + * the input URI into a nsILocalFile (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 = Components.classes["@mozilla.org/network/protocol;1?name=file"]. + getService(Components.interfaces.nsIFileProtocolHandler); + var chromeDir = fileHandler.getFileFromURLSpec(resolvedURI.spec); + return chromeDir.parent.QueryInterface(Components.interfaces.nsILocalFile); +} + +//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(Components.interfaces.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 = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties) + .get("ProfD", Components.interfaces.nsILocalFile); + 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(Components.interfaces.nsIFile.DIRECTORY_TYPE, parseInt("0777", 8)); + + var zReader = Components.classes["@mozilla.org/libjar/zip-reader;1"]. + createInstance(Components.interfaces.nsIZipReader); + + var fileHandler = Components.classes["@mozilla.org/network/protocol;1?name=file"]. + getService(Components.interfaces.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. + */ + var dirs = zReader.findEntries(filepath + '*/'); + while (dirs.hasMore()) { + var targetDir = buildRelativePath(dirs.getNext(), 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(Components.interfaces.nsIFile.DIRECTORY_TYPE, parseInt("0777", 8)); + } + } + + //now do the files + var files = zReader.findEntries(filepath + "*"); + while (files.hasMore()) { + var fname = files.getNext(); + 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 = Components.classes["@mozilla.org/network/protocol;1?name=file"]. + getService(Components.interfaces.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 nsILocalFile. + */ +function buildRelativePath(jarentryname, destdir, basepath) +{ + var baseParts = basepath.split('/'); + if (baseParts[baseParts.length-1] == '') { + baseParts.pop(); + } + + var parts = jarentryname.split('/'); + + var targetFile = Components.classes["@mozilla.org/file/local;1"] + .createInstance(Components.interfaces.nsILocalFile); + 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 fileLocator = Components.classes["@mozilla.org/file/directory_service;1"]. + getService(Components.interfaces.nsIProperties); + var configFile = fileLocator.get("ProfD", Components.interfaces.nsIFile); + configFile.append(filename); + + if (!configFile.exists()) + return {}; + + var fileInStream = Components.classes["@mozilla.org/network/file-input-stream;1"]. + createInstance(Components.interfaces.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 = 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); + return; +} diff --git a/testing/mochitest/chrome.eslintrc.js b/testing/mochitest/chrome.eslintrc.js new file mode 100644 index 000000000..1084846ea --- /dev/null +++ b/testing/mochitest/chrome.eslintrc.js @@ -0,0 +1,39 @@ +// Parent config file for all mochitest files. +module.exports = { + rules: { + "mozilla/import-headjs-globals": "warn", + "mozilla/mark-test-function-used": "warn", + }, + + "env": { + "browser": true, + }, + + // All globals made available in the test environment. + "globals": { + "add_task": false, + "Assert": false, + "EventUtils": false, + "executeSoon": false, + "export_assertions": false, + "finish": false, + "getRootDirectory": false, + "getTestFilePath": false, + "gTestPath": false, + "info": false, + "is": false, + "isnot": false, + "ok": false, + "promise": false, + "registerCleanupFunction": false, + "requestLongerTimeout": false, + "SimpleTest": false, + "SpecialPowers": false, + "todo": false, + "todo_is": false, + "todo_isnot": false, + "waitForClipboard": false, + "waitForExplicitFinish": false, + "waitForFocus": false, + } +}; diff --git a/testing/mochitest/chrome/chrome.ini b/testing/mochitest/chrome/chrome.ini new file mode 100644 index 000000000..b29e97257 --- /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.xul] +[test_sanityAddTask.xul] +[test_sanityEventUtils.xul] +[test_sanityPluginUtils.html] +[test_sanityException.xul] +[test_sanityException2.xul] +[test_sanityManifest.xul] +fail-if = true +[test_sanityManifest_pf.xul] +fail-if = true +[test_sanitySpawnTask.xul] +[test_chromeGetTestFile.xul] diff --git a/testing/mochitest/chrome/test-dir/test-file b/testing/mochitest/chrome/test-dir/test-file new file mode 100644 index 000000000..257cc5642 --- /dev/null +++ b/testing/mochitest/chrome/test-dir/test-file @@ -0,0 +1 @@ +foo diff --git a/testing/mochitest/chrome/test_chromeGetTestFile.xul b/testing/mochitest/chrome/test_chromeGetTestFile.xul new file mode 100644 index 000000000..e1372722b --- /dev/null +++ b/testing/mochitest/chrome/test_chromeGetTestFile.xul @@ -0,0 +1,55 @@ +<?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 type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script> + <script type="application/javascript"> + <![CDATA[ + let {Promise} = Components.utils.import("resource://gre/modules/Promise.jsm"); + Components.utils.import("resource://gre/modules/osfile.jsm"); + let decoder = new TextDecoder(); + + SimpleTest.waitForExplicitFinish(); + + SimpleTest.doesThrow(function () { + getTestFilePath("/test_chromeGetTestFile.xul") + }, "getTestFilePath rejects absolute paths"); + + Promise.all([ + OS.File.exists(getTestFilePath("test_chromeGetTestFile.xul")) + .then(function (exists) { + ok(exists, "getTestFilePath consider the path as being relative"); + }), + + OS.File.exists(getTestFilePath("./test_chromeGetTestFile.xul")) + .then(function (exists) { + ok(exists, "getTestFilePath also accepts explicit relative path"); + }), + + OS.File.exists(getTestFilePath("./test_chromeGetTestFileTypo.xul")) + .then(function (exists) { + ok(!exists, "getTestFilePath do not throw if the file doesn't exists"); + }), + + OS.File.read(getTestFilePath("test-dir/test-file")) + .then(function (array) { + is(decoder.decode(array), "foo\n", "getTestFilePath can reach sub-folder files 1/2"); + }), + + OS.File.read(getTestFilePath("./test-dir/test-file")) + .then(function (array) { + is(decoder.decode(array), "foo\n", "getTestFilePath can reach sub-folder files 2/2"); + }) + + ]).then(function () { + SimpleTest.finish(); + }, console.error); + ]]> + </script> +</window> diff --git a/testing/mochitest/chrome/test_sample.xul b/testing/mochitest/chrome/test_sample.xul new file mode 100644 index 000000000..957bcd29c --- /dev/null +++ b/testing/mochitest/chrome/test_sample.xul @@ -0,0 +1,36 @@ +<?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 type="application/javascript" + 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_sanityAddTask.xul b/testing/mochitest/chrome/test_sanityAddTask.xul new file mode 100644 index 000000000..006acbaa7 --- /dev/null +++ b/testing/mochitest/chrome/test_sanityAddTask.xul @@ -0,0 +1,43 @@ +<?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 spawnTawk function" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"/> + <script type="application/javascript"> + <![CDATA[ + + // Check that we can 'add_task' a few times and all tasks run asynchronously before test finishes. + + add_task(function* () { + var x = yield Promise.resolve(1); + is(x, 1, "task yields Promise value as expected"); + }); + + add_task(function* () { + var x = yield [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]; + is(x.join(""), "123", "task yields Promise value as expected"); + }); + + add_task(function* () { + var x = yield (function* () { + return 3; + }()); + is(x, 3, "task yields generator function return value as expected"); + }); + + ]]> + </script> + <body xmlns="http://www.w3.org/1999/xhtml" > + </body> +</window> + + + + diff --git a/testing/mochitest/chrome/test_sanityEventUtils.xul b/testing/mochitest/chrome/test_sanityEventUtils.xul new file mode 100644 index 000000000..6ac098d2e --- /dev/null +++ b/testing/mochitest/chrome/test_sanityEventUtils.xul @@ -0,0 +1,192 @@ +<?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 type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="text/javascript"> + var start = new Date(); + </script> + <script type="application/javascript" + 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 = Components.classes["@mozilla.org/file/directory_service;1"]. + getService(Components.interfaces.nsIProperties). + get("CurWorkD", Components.interfaces.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 : function(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"); + 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 synthesizeDragStart */ + result = synthesizeDragStart($("drag1"), drag1, window); + is(result, null, "drag1 is text/uri-list"); + result = synthesizeDragStart($("drag1"), drag1WrongFlavor, window); + isnot(result, null, "drag1 is not text/plain"); + result = synthesizeDragStart($("drag1"), drag2items, window); + isnot(result, null, "drag1 is not 2 items"); + result = synthesizeDragStart($("drag2"), drag2, window); + is(result, null, "drag2 is ordered text/plain then text/uri-list"); + result = synthesizeDragStart($("drag2"), drag1, window); + isnot(result, null, "drag2 is not one flavor"); + result = synthesizeDragStart($("drag2"), drag2WrongOrder, window); + isnot(result, null, "drag2 is not ordered text/uri-list then text/plain"); + result = synthesizeDragStart($("dragfile"), dragfile, window); + is(result, null, "dragfile is nsIFile"); + result = synthesizeDragStart($("drag1"), null, window); + is(result, regularDtForDrag1, "synthesizeDragStart accepts null expectedDragData"); + + /* 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.xul b/testing/mochitest/chrome/test_sanityException.xul new file mode 100644 index 000000000..420269e4d --- /dev/null +++ b/testing/mochitest/chrome/test_sanityException.xul @@ -0,0 +1,23 @@ +<?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 type="application/javascript" 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"); +throw "this is a deliberately thrown exception"; + +]]></script> +</body> + +</window> diff --git a/testing/mochitest/chrome/test_sanityException2.xul b/testing/mochitest/chrome/test_sanityException2.xul new file mode 100644 index 000000000..450fd838c --- /dev/null +++ b/testing/mochitest/chrome/test_sanityException2.xul @@ -0,0 +1,29 @@ +<?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 type="application/javascript" 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(); + throw "this is a deliberately thrown exception"; +}); +SimpleTest.executeSoon(function() { + SimpleTest.finish(); +}); + +]]></script> +</body> + +</window> diff --git a/testing/mochitest/chrome/test_sanityManifest.xul b/testing/mochitest/chrome/test_sanityManifest.xul new file mode 100644 index 000000000..ff92c3732 --- /dev/null +++ b/testing/mochitest/chrome/test_sanityManifest.xul @@ -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 type="application/javascript" 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.xul b/testing/mochitest/chrome/test_sanityManifest_pf.xul new file mode 100644 index 000000000..7500d155e --- /dev/null +++ b/testing/mochitest/chrome/test_sanityManifest_pf.xul @@ -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 type="application/javascript" 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_sanityPluginUtils.html b/testing/mochitest/chrome/test_sanityPluginUtils.html new file mode 100644 index 000000000..f0814259e --- /dev/null +++ b/testing/mochitest/chrome/test_sanityPluginUtils.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="text/javascript"> + var start = new Date(); + </script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript"> + var loadTime = new Date(); + </script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body onload="starttest()"> +<!-- load the test plugin defined at $(DIST)/bin/plugins/Test.plugin/ --> +<embed id="plugin1" type="application/x-test" width="200" height="200"></embed> +<script class="testbody" type="text/javascript"> +info("\nProfile::PluginUtilsLoadTime: " + (loadTime - start) + "\n"); +function starttest() { + SimpleTest.waitForExplicitFinish(); + var startTime = new Date(); + //increase the runtime of the test so it is detectible, otherwise we get 0-1ms + runtimes = 100; + function runTest(plugin) { + is(plugin.version, "1.0.0.0", "Make sure version is correct"); + is(plugin.name, "Test Plug-in"); + }; + while (runtimes > 0) { + ok(PluginUtils.withTestPlugin(runTest), "Test plugin should be found"); + --runtimes; + } + var endTime = new Date(); + info("\nProfile::PluginUtilsRunTime: " + (endTime-startTime) + "\n"); + SimpleTest.finish(); +}; +</script> +</body> +</html> diff --git a/testing/mochitest/chrome/test_sanitySpawnTask.xul b/testing/mochitest/chrome/test_sanitySpawnTask.xul new file mode 100644 index 000000000..d3f0ccc1f --- /dev/null +++ b/testing/mochitest/chrome/test_sanitySpawnTask.xul @@ -0,0 +1,70 @@ +<?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 spawnTawk function" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"/> + <script type="application/javascript"> + <![CDATA[ + SimpleTest.waitForExplicitFinish(); + + var externalGeneratorFunction = function* () { + return 8; + }; + + var nestedFunction = function* () { + return yield function* () { + return yield function* () { + return yield function* () { + return yield Promise.resolve(9); + }(); + }(); + }(); + } + + var variousTests = function* () { + var val1 = yield [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]; + is(val1.join(""), "123", "Array of promises -> Promise.all"); + var val2 = yield Promise.resolve(2); + is(val2, 2, "Resolved promise yields value."); + var val3 = yield function* () { return 3; }; + is(val3, 3, "Generator functions are spawned."); + //var val4 = yield function () { return 4; }; + //is(val4, 4, "Plain functions run and return."); + var val5 = yield (function* () { return 5; }()); + is(val5, 5, "Generators are spawned."); + try { + var val6 = yield Promise.reject(Error("error6")); + ok(false, "Shouldn't reach this line."); + } catch (error) { + is(error.message, "error6", "Rejected promise throws error."); + } + try { + var val7 = yield function* () { throw Error("error7"); }; + ok(false, "Shouldn't reach this line."); + } catch (error) { + is(error.message, "error7", "Thrown error propagates."); + } + var val8 = yield externalGeneratorFunction(); + is(val8, 8, "External generator also spawned."); + var val9 = yield nestedFunction(); + is(val9, 9, "Nested generator functions work."); + return 10; + }; + + spawn_task(variousTests).then(function(result) { + is(result, 10, "spawn_task(...) returns promise"); + SimpleTest.finish(); + }); + ]]> + </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 000000000..02f972e8d --- /dev/null +++ b/testing/mochitest/chunkifyTests.js @@ -0,0 +1,26 @@ +/* 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/dynamic/getMyDirectory.sjs b/testing/mochitest/dynamic/getMyDirectory.sjs new file mode 100644 index 000000000..1bf6d660b --- /dev/null +++ b/testing/mochitest/dynamic/getMyDirectory.sjs @@ -0,0 +1,15 @@ +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 000000000..e69de29bb --- /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 000000000..04fbaa08f --- /dev/null +++ b/testing/mochitest/embed/Xm5i5kbIXzc^headers^ @@ -0,0 +1,2 @@ +HTTP 200 OK +Content-Type: text/html diff --git a/testing/mochitest/gen_template.pl b/testing/mochitest/gen_template.pl new file mode 100644 index 000000000..5212d5e82 --- /dev/null +++ b/testing/mochitest/gen_template.pl @@ -0,0 +1,42 @@ +#!/usr/bin/perl + +# This script makes mochitest test case templates. See +# https://developer.mozilla.org/en-US/docs/Mochitest#Test_templates +# +# It takes two arguments: +# +# -b: a bugnumber +# -type: template type. One of {plain|xhtml|xul|th|chrome|chromexul}. +# Defaults to th (testharness.js). +# +# For example, this command: +# +# perl gen_template.pl -b 345876 -type xul +# +# writes a XUL test case template for bug 345876 to stdout. + +use FindBin; +use Getopt::Long; +GetOptions("b=i"=> \$bug_number, + "type:s"=> \$template_type); + +if ($template_type eq "xul") { + $template_type = "$FindBin::RealBin/static/xul.template.txt"; +} elsif ($template_type eq "xhtml") { + $template_type = "$FindBin::RealBin/static/xhtml.template.txt"; +} elsif ($template_type eq "chrome") { + $template_type = "$FindBin::RealBin/static/chrome.template.txt"; +} elsif ($template_type eq "chromexul") { + $template_type = "$FindBin::RealBin/static/chromexul.template.txt"; +} elsif ($template_type eq "plain") { + $template_type = "$FindBin::RealBin/static/test.template.txt"; +} else { + $template_type = "$FindBin::RealBin/static/th.template.txt"; +} + +open(IN,$template_type) or die("Failed to open myfile for reading."); +while((defined(IN)) && ($line = <IN>)) { + $line =~ s/{BUGNUMBER}/$bug_number/g; + print STDOUT $line; +} +close(IN); diff --git a/testing/mochitest/harness.xul b/testing/mochitest/harness.xul new file mode 100644 index 000000000..3ad3d692f --- /dev/null +++ b/testing/mochitest/harness.xul @@ -0,0 +1,116 @@ +<?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 type="text/javascript" + src="chrome://mochikit/content/tests/SimpleTest/LogController.js"/> + <script type="text/javascript" + src="chrome://mochikit/content/tests/SimpleTest/MemoryStats.js"/> + <script type="text/javascript" + src="chrome://mochikit/content/tests/SimpleTest/StructuredLog.jsm"/> + <script type="text/javascript" + src="chrome://mochikit/content/tests/SimpleTest/TestRunner.js"/> + <script type="text/javascript" + src="chrome://mochikit/content/tests/SimpleTest/MozillaLogger.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" + src="chrome://mochikit/content/chunkifyTests.js" /> + <script type="application/javascript" + src="chrome://mochikit/content/manifestLibrary.js" /> + <script type="text/javascript" + src="chrome://mochikit/content/tests/SimpleTest/setup.js" /> + <script type="application/javascript;version=1.7"><![CDATA[ + +if (Cc === undefined) { + var Cc = Components.classes; + var Ci = Components.interfaces; +} + +function loadTests() +{ + window.removeEventListener("load", loadTests, false); + getTestList({}, linkAndHookup); +} + +function linkAndHookup(links) { + // load server.js in so we can share template functions + var scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + var srvScope = {}; + scriptLoader.loadSubScript('chrome://mochikit/content/server.js', + srvScope); + + // generate our test list + srvScope.makeTags(); + var tableContent = srvScope.linksToTableRows(links, 0); + + function populate() { + document.getElementById("test-table").innerHTML += tableContent; + } + gTestList = eval(srvScope.jsonArrayOfTestFiles(links)); + populate(); + + hookup(); +} + + window.addEventListener("load", loadTests, false); + ]]> + </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/install.rdf b/testing/mochitest/install.rdf new file mode 100644 index 000000000..6d758b3bc --- /dev/null +++ b/testing/mochitest/install.rdf @@ -0,0 +1,27 @@ +<?xml version="1.0"?> + +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>mochikit@mozilla.org</em:id> + <em:version>1.0</em:version> +#ifdef MOCHITEST_BOOTSTRAP + <em:bootstrap>true</em:bootstrap> +#endif + <em:targetApplication> + <Description> + <em:id>toolkit@mozilla.org</em:id> +#expand <em:minVersion>__MOZILLA_VERSION_U__</em:minVersion> + <!-- Set to * so toolkit/mozapps/update/chrome tests pass. --> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + <!-- Front End MetaData --> + <em:name>Mochitest</em:name> + <em:description>Mochikit test harness</em:description> + <em:creator>Joel Maher</em:creator> + </Description> +</RDF> + + + diff --git a/testing/mochitest/jar.mn b/testing/mochitest/jar.mn new file mode 100644 index 000000000..5753068ee --- /dev/null +++ b/testing/mochitest/jar.mn @@ -0,0 +1,47 @@ +mochikit.jar: +% content mochikit %content/ + content/browser-harness.xul (browser-harness.xul) + content/browser-test.js (browser-test.js) + content/browser-test-overlay.xul (browser-test-overlay.xul) + content/jetpack-package-harness.js (jetpack-package-harness.js) + content/jetpack-package-overlay.xul (jetpack-package-overlay.xul) + content/jetpack-addon-harness.js (jetpack-addon-harness.js) + content/jetpack-addon-overlay.xul (jetpack-addon-overlay.xul) + content/chrome-harness.js (chrome-harness.js) + content/mochitest-e10s-utils.js (mochitest-e10s-utils.js) + content/shutdown-leaks-collector.js (shutdown-leaks-collector.js) + content/ShutdownLeaksCollector.jsm (ShutdownLeaksCollector.jsm) + content/harness.xul (harness.xul) + content/redirect.html (redirect.html) + content/server.js (server.js) + content/chunkifyTests.js (chunkifyTests.js) + content/manifestLibrary.js (manifestLibrary.js) + content/nested_setup.js (nested_setup.js) + content/dynamic/getMyDirectory.sjs (dynamic/getMyDirectory.sjs) + content/static/harness.css (static/harness.css) + content/tests/SimpleTest/ChromePowers.js (tests/SimpleTest/ChromePowers.js) + content/tests/SimpleTest/EventUtils.js (tests/SimpleTest/EventUtils.js) + content/tests/SimpleTest/ExtensionTestUtils.js (tests/SimpleTest/ExtensionTestUtils.js) + content/tests/SimpleTest/SpawnTask.js (tests/SimpleTest/SpawnTask.js) + content/tests/SimpleTest/AsyncUtilsContent.js (tests/SimpleTest/AsyncUtilsContent.js) + content/tests/SimpleTest/LogController.js (tests/SimpleTest/LogController.js) + content/tests/SimpleTest/MemoryStats.js (tests/SimpleTest/MemoryStats.js) + content/tests/SimpleTest/MozillaLogger.js (../specialpowers/content/MozillaLogger.js) + content/tests/SimpleTest/specialpowers.js (../specialpowers/content/specialpowers.js) + content/tests/SimpleTest/SpecialPowersObserverAPI.js (../specialpowers/content/SpecialPowersObserverAPI.js) +* content/tests/SimpleTest/specialpowersAPI.js (../specialpowers/content/specialpowersAPI.js) + content/tests/SimpleTest/setup.js (tests/SimpleTest/setup.js) + content/tests/SimpleTest/SimpleTest.js (tests/SimpleTest/SimpleTest.js) + content/tests/SimpleTest/StructuredLog.jsm (../modules/StructuredLog.jsm) + content/tests/SimpleTest/test.css (tests/SimpleTest/test.css) + content/tests/SimpleTest/TestRunner.js (tests/SimpleTest/TestRunner.js) + content/tests/SimpleTest/iframe-between-tests.html (tests/SimpleTest/iframe-between-tests.html) + content/tests/SimpleTest/WindowSnapshot.js (tests/SimpleTest/WindowSnapshot.js) + content/tests/SimpleTest/MockObjects.js (tests/SimpleTest/MockObjects.js) + content/tests/SimpleTest/NativeKeyCodes.js (tests/SimpleTest/NativeKeyCodes.js) + content/tests/SimpleTest/paint_listener.js (tests/SimpleTest/paint_listener.js) + content/tests/SimpleTest/docshell_helpers.js (../../docshell/test/chrome/docshell_helpers.js) + content/tests/BrowserTestUtils/content-task.js (BrowserTestUtils/content/content-task.js) + content/tests/BrowserTestUtils/content-about-page-utils.js (BrowserTestUtils/content/content-about-page-utils.js) + content/tests/BrowserTestUtils/content-utils.js (BrowserTestUtils/content/content-utils.js) + diff --git a/testing/mochitest/jetpack-addon-harness.js b/testing/mochitest/jetpack-addon-harness.js new file mode 100644 index 000000000..3b938db9d --- /dev/null +++ b/testing/mochitest/jetpack-addon-harness.js @@ -0,0 +1,235 @@ +/* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */ +var gConfig; + +if (Cc === undefined) { + var Cc = Components.classes; + var Ci = Components.interfaces; + var Cu = Components.utils; +} + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", + "resource://gre/modules/AddonManager.jsm"); + +// How long to wait for an add-on to uninstall before aborting +const MAX_UNINSTALL_TIME = 10000; +setTimeout(testInit, 0); + +var sdkpath = null; + +// Strip off the chrome prefix to get the actual path of the test directory +function realPath(chrome) { + return chrome.substring("chrome://mochitests/content/jetpack-addon/".length) + .replace(".xpi", ""); +} + +const chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"] + .getService(Ci.nsIChromeRegistry); + +// Installs a single add-on returning a promise for when install is completed +function installAddon(url) { + let chromeURL = Services.io.newURI(url, null, null); + let file = chromeRegistry.convertChromeURL(chromeURL) + .QueryInterface(Ci.nsIFileURL).file; + + let addon; + const listener = { + onInstalling(_addon) { + addon = _addon; + // Set add-on's test options + const options = { + test: { + iterations: 1, + stop: false, + keepOpen: true, + }, + profile: { + memory: false, + leaks: false, + }, + output: { + logLevel: "verbose", + format: "tbpl", + }, + console: { + logLevel: "info", + }, + } + setPrefs("extensions." + addon.id + ".sdk", options); + + // If necessary override the add-ons module paths to point somewhere + // else + if (sdkpath) { + let paths = {} + for (let path of ["dev", "diffpatcher", "framescript", "method", "node", "sdk", "toolkit"]) { + paths[path] = sdkpath + path; + } + setPrefs("extensions.modules." + addon.id + ".path", paths); + } + }, + }; + AddonManager.addAddonListener(listener); + + return AddonManager.installTemporaryAddon(file) + .then(() => { + AddonManager.removeAddonListener(listener); + return addon; + }); +} + +// Uninstalls an add-on returning a promise for when it is gone +function uninstallAddon(oldAddon) { + return new Promise(function(resolve, reject) { + AddonManager.addAddonListener({ + onUninstalled: function(addon) { + if (addon.id != oldAddon.id) + return; + + AddonManager.removeAddonListener(this); + + dump("TEST-INFO | jetpack-addon-harness.js | Uninstalled test add-on " + addon.id + "\n"); + + // Some add-ons do async work on uninstall, we must wait for that to + // complete + setTimeout(resolve, 500); + } + }); + + oldAddon.uninstall(); + + // The uninstall should happen quickly, if not throw an exception + setTimeout(() => { + reject(new Error(`Addon ${oldAddon.id} failed to uninstall in a timely fashion.`)); + }, MAX_UNINSTALL_TIME); + }); +} + +// Waits for a test add-on to signal it has completed its tests +function waitForResults() { + return new Promise(function(resolve, reject) { + Services.obs.addObserver(function(subject, topic, data) { + Services.obs.removeObserver(arguments.callee, "sdk:test:results"); + + resolve(JSON.parse(data)); + }, "sdk:test:results", false); + }); +} + +// Runs tests for the add-on available at URL. +var testAddon = Task.async(function*({ url }) { + dump("TEST-INFO | jetpack-addon-harness.js | Installing test add-on " + realPath(url) + "\n"); + let addon = yield installAddon(url); + + let results = yield waitForResults(); + + dump("TEST-INFO | jetpack-addon-harness.js | Uninstalling test add-on " + addon.id + "\n"); + yield uninstallAddon(addon); + + dump("TEST-INFO | jetpack-addon-harness.js | Testing add-on " + realPath(url) + " is complete\n"); + return results; +}); + +// Sets a set of prefs for test add-ons +function setPrefs(root, options) { + Object.keys(options).forEach(id => { + const key = root + "." + id; + const value = options[id] + const type = typeof(value); + + value === null ? void(0) : + value === undefined ? void(0) : + type === "boolean" ? Services.prefs.setBoolPref(key, value) : + type === "string" ? Services.prefs.setCharPref(key, value) : + type === "number" ? Services.prefs.setIntPref(key, parseInt(value)) : + type === "object" ? setPrefs(key, value) : + void(0); + }); +} + +function testInit() { + // Make sure to run the test harness for the first opened window only + if (Services.prefs.prefHasUserValue("testing.jetpackTestHarness.running")) + return; + + Services.prefs.setBoolPref("testing.jetpackTestHarness.running", true); + + // Get the list of tests to run + let config = readConfig(); + getTestList(config, function(links) { + try { + let fileNames = []; + let fileNameRegexp = /.+\.xpi$/; + arrayOfTestFiles(links, fileNames, fileNameRegexp); + + if (config.startAt || config.endAt) { + fileNames = skipTests(fileNames, config.startAt, config.endAt); + } + + // Override the SDK modules if necessary + try { + let sdklibs = Services.prefs.getCharPref("extensions.sdk.path"); + // sdkpath is a file path, make it a URI + let sdkfile = Cc["@mozilla.org/file/local;1"]. + createInstance(Ci.nsIFile); + sdkfile.initWithPath(sdklibs); + sdkpath = Services.io.newFileURI(sdkfile).spec; + } + catch (e) { + // Stick with the built-in modules + } + + let passed = 0; + let failed = 0; + + function finish() { + if (passed + failed == 0) { + dump("TEST-UNEXPECTED-FAIL | jetpack-addon-harness.js | " + + "No tests to run. Did you pass invalid test_paths?\n"); + } + else { + dump("Jetpack Addon Test Summary\n"); + dump("\tPassed: " + passed + "\n" + + "\tFailed: " + failed + "\n" + + "\tTodo: 0\n"); + } + + if (config.closeWhenDone) { + dump("TEST-INFO | jetpack-addon-harness.js | Shutting down.\n"); + + const appStartup = Cc['@mozilla.org/toolkit/app-startup;1']. + getService(Ci.nsIAppStartup); + appStartup.quit(appStartup.eAttemptQuit); + } + } + + function testNextAddon() { + if (fileNames.length == 0) + return finish(); + + let filename = fileNames.shift(); + dump("TEST-INFO | jetpack-addon-harness.js | Starting test add-on " + realPath(filename.url) + "\n"); + testAddon(filename).then(results => { + passed += results.passed; + failed += results.failed; + }).then(testNextAddon, error => { + // If something went wrong during the test then a previous test add-on + // may still be installed, this leaves us in an unexpected state so + // probably best to just abandon testing at this point + failed++; + dump("TEST-UNEXPECTED-FAIL | jetpack-addon-harness.js | Error testing " + realPath(filename.url) + ": " + error + "\n"); + finish(); + }); + } + + testNextAddon(); + } + catch (e) { + dump("TEST-UNEXPECTED-FAIL | jetpack-addon-harness.js | error starting test harness (" + e + ")\n"); + dump(e.stack); + } + }); +} diff --git a/testing/mochitest/jetpack-addon-overlay.xul b/testing/mochitest/jetpack-addon-overlay.xul new file mode 100644 index 000000000..bb3514acd --- /dev/null +++ b/testing/mochitest/jetpack-addon-overlay.xul @@ -0,0 +1,14 @@ +<?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/. --> + +<overlay id="jetpackTestOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"/> + <script type="application/javascript" src="chrome://mochikit/content/manifestLibrary.js"/> + <script type="application/javascript" src="chrome://mochikit/content/chunkifyTests.js"/> + <script type="application/javascript" src="chrome://mochikit/content/server.js"/> + <script type="application/javascript" src="chrome://mochikit/content/jetpack-addon-harness.js"/> +</overlay> diff --git a/testing/mochitest/jetpack-package-harness.js b/testing/mochitest/jetpack-package-harness.js new file mode 100644 index 000000000..a25706f49 --- /dev/null +++ b/testing/mochitest/jetpack-package-harness.js @@ -0,0 +1,250 @@ +/* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */ +const TEST_PACKAGE = "chrome://mochitests/content/"; + +// Make sure to use the real add-on ID to get the e10s shims activated +const TEST_ID = "mochikit@mozilla.org"; + +var gConfig; + +if (Cc === undefined) { + var Cc = Components.classes; + var Ci = Components.interfaces; + var Cu = Components.utils; +} + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +setTimeout(testInit, 0); + +// Tests a single module +function testModule(require, { url, expected }) { + return new Promise(resolve => { + let path = url.substring(TEST_PACKAGE.length); + + const { stdout } = require("sdk/system"); + + const { runTests } = require("sdk/test/harness"); + const loaderModule = require("toolkit/loader"); + const options = require("sdk/test/options"); + + function findAndRunTests(loader, nextIteration) { + const { TestRunner } = loaderModule.main(loader, "sdk/deprecated/unit-test"); + + const NOT_TESTS = ['setup', 'teardown']; + var runner = new TestRunner(); + + let tests = []; + + let suiteModule; + try { + dump("TEST-INFO: " + path + " | Loading test module\n"); + suiteModule = loaderModule.main(loader, "tests/" + path.substring(0, path.length - 3)); + } + catch (e) { + // If `Unsupported Application` error thrown during test, + // skip the test suite + suiteModule = { + 'test suite skipped': assert => assert.pass(e.message) + }; + } + + for (let name of Object.keys(suiteModule).sort()) { + if (NOT_TESTS.indexOf(name) != -1) + continue; + + tests.push({ + setup: suiteModule.setup, + teardown: suiteModule.teardown, + testFunction: suiteModule[name], + name: path + "." + name + }); + } + + runner.startMany({ + tests: { + getNext: () => Promise.resolve(tests.shift()) + }, + stopOnError: options.stopOnError, + onDone: nextIteration + }); + } + + runTests({ + findAndRunTests: findAndRunTests, + iterations: options.iterations, + filter: options.filter, + profileMemory: options.profileMemory, + stopOnError: options.stopOnError, + verbose: options.verbose, + parseable: options.parseable, + print: stdout.write, + onDone: resolve + }); + }); +} + +// Sets the test prefs +function setPrefs(root, options) { + Object.keys(options).forEach(id => { + const key = root + "." + id; + const value = options[id] + const type = typeof(value); + + value === null ? void(0) : + value === undefined ? void(0) : + type === "boolean" ? Services.prefs.setBoolPref(key, value) : + type === "string" ? Services.prefs.setCharPref(key, value) : + type === "number" ? Services.prefs.setIntPref(key, parseInt(value)) : + type === "object" ? setPrefs(key, value) : + void(0); + }); +} + +function testInit() { + // Make sure to run the test harness for the first opened window only + if (Services.prefs.prefHasUserValue("testing.jetpackTestHarness.running")) + return; + + Services.prefs.setBoolPref("testing.jetpackTestHarness.running", true); + + // Need to set this very early, otherwise the false value gets cached in + // DOM bindings code. + Services.prefs.setBoolPref("dom.indexedDB.experimental", true); + + // Get the list of tests to run + let config = readConfig(); + getTestList(config, function(links) { + try { + let fileNames = []; + let fileNameRegexp = /test-.+\.js$/; + arrayOfTestFiles(links, fileNames, fileNameRegexp); + + if (config.startAt || config.endAt) { + fileNames = skipTests(fileNames, config.startAt, config.endAt); + } + + // The SDK assumes it is being run from resource URIs + let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIChromeRegistry); + let realPath = chromeReg.convertChromeURL(Services.io.newURI(TEST_PACKAGE, null, null)); + let resProtocol = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsIResProtocolHandler); + resProtocol.setSubstitution("jetpack-package-tests", realPath); + + // Set the test options + const options = { + test: { + iterations: config.runUntilFailure ? config.repeat : 1, + stop: false, + keepOpen: true, + }, + profile: { + memory: false, + leaks: false, + }, + output: { + logLevel: "verbose", + format: "tbpl", + }, + console: { + logLevel: "info", + }, + } + setPrefs("extensions." + TEST_ID + ".sdk", options); + + // Override the SDK modules if necessary + let sdkpath = "resource://gre/modules/commonjs/"; + try { + let sdklibs = Services.prefs.getCharPref("extensions.sdk.path"); + // sdkpath is a file path, make it a URI and map a resource URI to it + let sdkfile = Cc["@mozilla.org/file/local;1"]. + createInstance(Ci.nsIFile); + sdkfile.initWithPath(sdklibs); + let sdkuri = Services.io.newFileURI(sdkfile); + resProtocol.setSubstitution("jetpack-modules", sdkuri); + sdkpath = "resource://jetpack-modules/"; + } + catch (e) { + // Stick with the built-in modules + } + + const paths = { + "": sdkpath, + "tests/": "resource://jetpack-package-tests/", + }; + + // Create the base module loader to load the test harness + const loaderID = "toolkit/loader"; + const loaderURI = paths[""] + loaderID + ".js"; + const loaderModule = Cu.import(loaderURI, {}).Loader; + + const modules = {}; + + // Manually set the loader's module cache to include itself; + // which otherwise fails due to lack of `Components`. + modules[loaderID] = loaderModule; + modules["@test/options"] = {}; + + let loader = loaderModule.Loader({ + id: TEST_ID, + name: "addon-sdk", + version: "1.0", + loadReason: "install", + paths: paths, + modules: modules, + isNative: true, + rootURI: paths["tests/"], + prefixURI: paths["tests/"], + metadata: {}, + }); + + const module = loaderModule.Module(loaderID, loaderURI); + const require = loaderModule.Require(loader, module); + + // Wait until the add-on window is ready + require("sdk/addon/window").ready.then(() => { + let passed = 0; + let failed = 0; + + function finish() { + if (passed + failed == 0) { + dump("TEST-UNEXPECTED-FAIL | jetpack-package-harness.js | " + + "No tests to run. Did you pass invalid test_paths?\n"); + } + else { + dump("Jetpack Package Test Summary\n"); + dump("\tPassed: " + passed + "\n" + + "\tFailed: " + failed + "\n" + + "\tTodo: 0\n"); + } + + if (config.closeWhenDone) { + require("sdk/system").exit(failed == 0 ? 0 : 1); + } + else { + loaderModule.unload(loader, "shutdown"); + } + } + + function testNextModule() { + if (fileNames.length == 0) + return finish(); + + let filename = fileNames.shift(); + testModule(require, filename).then(tests => { + passed += tests.passed; + failed += tests.failed; + }).then(testNextModule); + } + + testNextModule(); + }); + } + catch (e) { + dump("TEST-UNEXPECTED-FAIL: jetpack-package-harness.js | error starting test harness (" + e + ")\n"); + dump(e.stack); + } + }); +} diff --git a/testing/mochitest/jetpack-package-overlay.xul b/testing/mochitest/jetpack-package-overlay.xul new file mode 100644 index 000000000..70db8aa22 --- /dev/null +++ b/testing/mochitest/jetpack-package-overlay.xul @@ -0,0 +1,14 @@ +<?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/. --> + +<overlay id="jetpackTestOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"/> + <script type="application/javascript" src="chrome://mochikit/content/manifestLibrary.js"/> + <script type="application/javascript" src="chrome://mochikit/content/chunkifyTests.js"/> + <script type="application/javascript" src="chrome://mochikit/content/server.js"/> + <script type="application/javascript" src="chrome://mochikit/content/jetpack-package-harness.js"/> +</overlay> diff --git a/testing/mochitest/leaks.py b/testing/mochitest/leaks.py new file mode 100644 index 000000000..d090c902f --- /dev/null +++ b/testing/mochitest/leaks.py @@ -0,0 +1,262 @@ +# 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 + + +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.leakedDocShells = set() + self.currentTest = None + self.seenShutdown = set() + + def log(self, message): + if message['action'] == 'log': + line = message['message'] + if line[2:11] == "DOMWINDOW": + self._logWindow(line) + elif line[2:10] == "DOCSHELL": + self._logDocShell(line) + elif line.startswith("Completed ShutdownLeaks collections in process"): + pid = int(line.split()[-1]) + self.seenShutdown.add(pid) + elif message['action'] == 'test_start': + fileName = message['test'].replace( + "chrome://mochitests/content/browser/", "") + self.currentTest = { + "fileName": fileName, "windows": set(), "docShells": set()} + elif message['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): + if not self.seenShutdown: + self.logger.error( + "TEST-UNEXPECTED-FAIL | ShutdownLeaks | process() called before end of test suite") + + 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)) + + 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"]))) + self.logger.info("TEST-INFO | %s | docShell(s) leaked: %s" % + (test["fileName"], ', '.join(["[pid = %s] [id = %s]" % + x for x in test["leakedDocShells"]] + ))) + + def _logWindow(self, line): + created = line[:2] == "++" + pid = self._parseValue(line, "pid") + serial = self._parseValue(line, "serial") + + # log line has invalid format + if not pid or not serial: + self.logger.error( + "TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse 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: + self.leakedWindows[key] = self._parseValue(line, "url") + + def _logDocShell(self, line): + created = line[:2] == "++" + pid = self._parseValue(line, "pid") + id = self._parseValue(line, "id") + + # log line has invalid format + if not pid or not id: + self.logger.error( + "TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse 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: + self.leakedDocShells.add(key) + + 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["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["leakCount"] = len( + test["leakedWindows"]) + len(test["leakedDocShells"]) + + if test["leakCount"]: + 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) + + +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.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", "malloc_", "__interceptor_malloc", "moz_xmalloc", + "calloc", "js_calloc", "calloc_", "__interceptor_calloc", "moz_xcalloc", + "realloc", "js_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.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 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): + if self.fatalError: + self.logger.error("TEST-UNEXPECTED-FAIL | LeakSanitizer | LeakSanitizer " + "has encountered a fatal 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) + + 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 000000000..fb261ec82 --- /dev/null +++ b/testing/mochitest/mach_commands.py @@ -0,0 +1,567 @@ +# 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/. + +from __future__ import absolute_import, unicode_literals + +from argparse import Namespace +from collections import defaultdict +from itertools import chain +import logging +import os +import sys +import warnings + +from mozbuild.base import ( + MachCommandBase, + MachCommandConditions as conditions, + MozbuildObject, +) + +from mach.decorators import ( + CommandArgument, + CommandProvider, + Command, +) + +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() + +ROBOCOP_TESTS_NOT_FOUND = ''' +The robocop command could not find any tests under the following +test path(s): + +{} + +Please check spelling and make sure the named tests exist. +'''.lstrip() + +NOW_RUNNING = ''' +###### +### Now running mochitest-{}. +###### +''' + + +# 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', + } + }, + 'chrome': { + 'suite': 'chrome', + 'aliases': ('chrome', 'mochitest-chrome'), + 'enabled_apps': ('firefox', 'android'), + 'extra_args': { + 'flavor': 'chrome', + } + }, + 'browser-chrome': { + 'suite': 'browser', + 'aliases': ('browser', 'browser-chrome', 'mochitest-browser-chrome', 'bc'), + 'enabled_apps': ('firefox',), + 'extra_args': { + 'flavor': 'browser', + } + }, + 'jetpack-package': { + 'suite': 'jetpack-package', + 'aliases': ('jetpack-package', 'mochitest-jetpack-package', 'jpp'), + 'enabled_apps': ('firefox',), + 'extra_args': { + 'flavor': 'jetpack-package', + } + }, + 'jetpack-addon': { + 'suite': 'jetpack-addon', + 'aliases': ('jetpack-addon', 'mochitest-jetpack-addon', 'jpa'), + 'enabled_apps': ('firefox',), + 'extra_args': { + 'flavor': 'jetpack-addon', + } + }, + 'a11y': { + 'suite': 'a11y', + 'aliases': ('a11y', 'mochitest-a11y', 'accessibility'), + 'enabled_apps': ('firefox',), + 'extra_args': { + 'flavor': 'a11y', + } + }, +} + +SUPPORTED_APPS = ['firefox', 'android'] +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()]) + +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 mozbuild.testing import TestResolver + resolver = self._spawn(TestResolver) + tests = list(resolver.resolve_tests(paths=test_paths, cwd=cwd)) + return tests + + def run_desktop_test(self, context, tests=None, suite=None, **kwargs): + """Runs a mochitest. + + suite is the type of mochitest to run. It can be one of ('plain', + 'chrome', 'browser', 'a11y', 'jetpack-package', 'jetpack-addon'). + """ + # 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) + + 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 suite == '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, context, tests, suite=None, **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 + + return runtestsremote.run_test_harness(parser, options) + + def run_robocop_test(self, context, tests, suite=None, **kwargs): + host_ret = verify_host_bin() + if host_ret != 0: + return host_ret + + import imp + path = os.path.join(self.mochitest_dir, 'runrobocop.py') + with open(path, 'r') as fh: + imp.load_module('runrobocop', fh, path, + ('.py', 'r', imp.PY_SOURCE)) + import runrobocop + + options = Namespace(**kwargs) + + from manifestparser import TestManifest + if tests and not options.manifestFile: + manifest = TestManifest() + manifest.tests.extend(tests) + options.manifestFile = manifest + + return runrobocop.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') + 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 verify_android_device + verify_android_device(build_obj, install=True, xre=True) + + global parser + parser = MochitestArgumentParser() + return parser + + +# condition filters + +def is_buildapp_in(*apps): + def is_buildapp_supported(cls): + for a in apps: + c = getattr(conditions, 'is_{}'.format(a), None) + if c and c(cls): + return True + return False + + is_buildapp_supported.__doc__ = 'Must have a {} build.'.format( + ' or '.join(apps)) + return is_buildapp_supported + + +def verify_host_bin(): + # validate MOZ_HOST_BIN environment variables for Android tests + 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 ' + 'xpcshell') + 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')): + print('$MOZ_HOST_BIN/xpcshell does not exist') + return 1 + return 0 + + +@CommandProvider +class MachCommands(MachCommandBase): + @Command('mochitest', category='testing', + conditions=[is_buildapp_in(*SUPPORTED_APPS)], + description='Run any flavor of mochitest (integration test).', + parser=setup_argument_parser) + @CommandArgument('-f', '--flavor', + metavar='{{{}}}'.format(', '.join(CANONICAL_FLAVORS)), + choices=SUPPORTED_FLAVORS, + help='Only run tests of this flavor.') + def run_mochitest_general(self, flavor=None, test_objects=None, resolve_tests=True, **kwargs): + buildapp = None + for app in SUPPORTED_APPS: + if is_buildapp_in(app)(self): + buildapp = app + break + + flavors = None + if flavor: + for fname, fobj in ALL_FLAVORS.iteritems(): + if flavor in fobj['aliases']: + if buildapp not in fobj['enabled_apps']: + continue + flavors = [fname] + break + else: + flavors = [f for f, v in ALL_FLAVORS.iteritems() if buildapp in v['enabled_apps']] + + from mozbuild.controller.building import BuildDriver + self._ensure_state_subdir_exists('.') + + test_paths = kwargs['test_paths'] + kwargs['test_paths'] = [] + + mochitest = self._spawn(MochitestRunner) + tests = [] + if resolve_tests: + tests = mochitest.resolve_tests(test_paths, test_objects, cwd=self._mach_context.cwd) + + driver = self._spawn(BuildDriver) + driver.install_tests(tests) + + subsuite = kwargs.get('subsuite') + if subsuite == 'default': + kwargs['subsuite'] = None + + suites = defaultdict(list) + unsupported = set() + for test in tests: + # 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) + + if ('mochitest', 'media') in suites: + req = os.path.join('testing', 'tools', 'websocketprocessbridge', + 'websocketprocessbridge_requirements.txt') + self.virtualenv_manager.activate() + self.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 = self.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 grant_runtime_permissions + grant_runtime_permissions(self) + run_mochitest = mochitest.run_android_test + else: + run_mochitest = mochitest.run_desktop_test + + overall = None + for (flavor, subsuite), tests in sorted(suites.items()): + fobj = ALL_FLAVORS[flavor] + msg = fobj['aliases'][0] + if subsuite: + msg = '{} with subsuite {}'.format(msg, subsuite) + print(NOW_RUNNING.format(msg)) + + harness_args = kwargs.copy() + harness_args['subsuite'] = subsuite + harness_args.update(fobj.get('extra_args', {})) + + result = run_mochitest( + self._mach_context, + tests=tests, + suite=fobj['suite'], + **harness_args) + + if result: + overall = result + + # TODO consolidate summaries from all suites + return overall + + +@CommandProvider +class RobocopCommands(MachCommandBase): + + @Command('robocop', category='testing', + conditions=[conditions.is_android], + description='Run a Robocop test.', + parser=setup_argument_parser) + @CommandArgument('--serve', default=False, action='store_true', + help='Run no tests but start the mochi.test web server ' + 'and launch Fennec with a test profile.') + def run_robocop(self, serve=False, **kwargs): + if serve: + kwargs['autorun'] = False + + if not kwargs.get('robocopIni'): + kwargs['robocopIni'] = os.path.join(self.topobjdir, '_tests', 'testing', + 'mochitest', 'robocop.ini') + + if not kwargs.get('robocopApk'): + kwargs['robocopApk'] = os.path.join(self.topobjdir, 'mobile', 'android', + 'tests', 'browser', 'robocop', + 'robocop-debug.apk') + + from mozbuild.controller.building import BuildDriver + self._ensure_state_subdir_exists('.') + + test_paths = kwargs['test_paths'] + kwargs['test_paths'] = [] + + from mozbuild.testing import TestResolver + resolver = self._spawn(TestResolver) + tests = list(resolver.resolve_tests(paths=test_paths, cwd=self._mach_context.cwd, + flavor='instrumentation', subsuite='robocop')) + driver = self._spawn(BuildDriver) + driver.install_tests(tests) + + if len(tests) < 1: + print(ROBOCOP_TESTS_NOT_FOUND.format('\n'.join( + sorted(list(test_paths))))) + return 1 + + from mozrunner.devices.android_device import grant_runtime_permissions + grant_runtime_permissions(self) + + mochitest = self._spawn(MochitestRunner) + return mochitest.run_robocop_test(self._mach_context, tests, 'robocop', **kwargs) + + +# NOTE python/mach/mach/commands/commandinfo.py references this function +# by name. If this function is renamed or removed, that file should +# be updated accordingly as well. +def REMOVED(cls): + """Command no longer exists! Use |mach mochitest| instead. + + The |mach mochitest| command will automatically detect which flavors and + subsuites exist in a given directory. If desired, flavors and subsuites + can be restricted using `--flavor` and `--subsuite` respectively. E.g: + + $ ./mach mochitest dom/indexedDB + + will run all of the plain, chrome and browser-chrome mochitests in that + directory. To only run the plain mochitests: + + $ ./mach mochitest -f plain dom/indexedDB + """ + return False + + +@CommandProvider +class DeprecatedCommands(MachCommandBase): + @Command('mochitest-plain', category='testing', conditions=[REMOVED]) + def mochitest_plain(self): + pass + + @Command('mochitest-chrome', category='testing', conditions=[REMOVED]) + def mochitest_chrome(self): + pass + + @Command('mochitest-browser', category='testing', conditions=[REMOVED]) + def mochitest_browser(self): + pass + + @Command('mochitest-devtools', category='testing', conditions=[REMOVED]) + def mochitest_devtools(self): + pass + + @Command('mochitest-a11y', category='testing', conditions=[REMOVED]) + def mochitest_a11y(self): + pass + + @Command('jetpack-addon', category='testing', conditions=[REMOVED]) + def jetpack_addon(self): + pass + + @Command('jetpack-package', category='testing', conditions=[REMOVED]) + def jetpack_package(self): + pass diff --git a/testing/mochitest/mach_test_package_commands.py b/testing/mochitest/mach_test_package_commands.py new file mode 100644 index 000000000..71fe62428 --- /dev/null +++ b/testing/mochitest/mach_test_package_commands.py @@ -0,0 +1,85 @@ +# 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/. + +from __future__ import unicode_literals + +import os +from argparse import Namespace +from functools import partial + +from mach.decorators import ( + CommandProvider, + Command, +) + +here = os.path.abspath(os.path.dirname(__file__)) +parser = None + + +def run_mochitest(context, **kwargs): + args = Namespace(**kwargs) + args.e10s = context.mozharness_config.get('e10s', args.e10s) + args.certPath = context.certs_dir + + if args.test_paths: + test_root = os.path.join(context.package_root, 'mochitest', 'tests') + normalize = partial(context.normalize_test_path, test_root) + args.test_paths = 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')) + + from runtests import run_test_harness + return run_test_harness(parser, args) + + +def run_mochitest_android(context, args): + args.app = args.app or 'org.mozilla.fennec' + args.extraProfileFiles.append(os.path.join(context.package_root, 'mochitest', 'fonts')) + args.utilityPath = context.hostutils + args.xrePath = context.hostutils + + config = context.mozharness_config + if config: + args.remoteWebServer = config['remote_webserver'] + args.httpPort = config['emulator']['http_port'] + args.sslPort = config['emulator']['ssl_port'] + args.adbPath = config['exes']['adb'] % {'abs_work_dir': context.mozharness_workdir} + + from runtestsremote import run_test_harness + return run_test_harness(parser, args) + + +def setup_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) + return parser + + +@CommandProvider +class MochitestCommands(object): + + def __init__(self, context): + self.context = context + + @Command('mochitest', category='testing', + description='Run the mochitest harness.', + parser=setup_argument_parser) + def mochitest(self, **kwargs): + return run_mochitest(self.context, **kwargs) diff --git a/testing/mochitest/manifest.webapp b/testing/mochitest/manifest.webapp new file mode 100644 index 000000000..a3c2e2592 --- /dev/null +++ b/testing/mochitest/manifest.webapp @@ -0,0 +1,39 @@ +{ + "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":{}, + "embed-apps":{}, + "audio-channel-content":{}, + "audio-channel-alarm":{}, + "before-after-keyboard-event":{} + }, + "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 000000000..51b35a4d2 --- /dev/null +++ b/testing/mochitest/manifestLibrary.js @@ -0,0 +1,159 @@ +/* -*- 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) { + var links = {}; + var 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 (var obj of testManifest['tests']) { + var 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) { + name = params.baseurl + '/' + params.testRoot + '/' + path; + links[name] = {'test': {'url': name, 'expected': obj['expected']}}; + } else { + name = params.testPrefix + path; + paths.push({'test': {'url': name, 'expected': obj['expected']}}); + } + } + if (paths.length > 0) { + callback(paths); + } else { + callback(links); + } +} + +function getTestManifest(url, params, callback) { + var 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: setup.js | error parsing " + url + " (" + e + ")\n"); + throw e; + } + } else { + dump("TEST-UNEXPECTED-FAIL: setup.js | error loading " + url + "\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) { + + var filteredTests = []; + var removedTests = []; + var runtests = {}; + var 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; + } + } + + var testRoot = config.testRoot || "tests"; + // Start with testList, and put everything that's in 'runtests' in + // filteredTests. + if (Object.keys(runtests).length) { + for (var i = 0; i < testList.length; i++) { + if ((testList[i] instanceof Object) && ('test' in testList[i])) { + var testpath = testList[i]['test']['url']; + } else { + var testpath = testList[i]; + } + var tmppath = testpath.replace(/^\//, ''); + for (var f in runtests) { + // Remove leading /tests/ if exists + 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; + } + + var refilteredTests = []; + for (var i = 0; i < filteredTests.length; i++) { + var found = false; + if ((filteredTests[i] instanceof Object) && ('test' in filteredTests[i])) { + var testpath = filteredTests[i]['test']['url']; + } else { + var testpath = filteredTests[i]; + } + var tmppath = testpath.replace(/^\//, ''); + for (var f in excludetests) { + // Remove leading /tests/ if exists + 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/autophone-media.ini b/testing/mochitest/manifests/autophone-media.ini new file mode 100644 index 000000000..2a5670ee1 --- /dev/null +++ b/testing/mochitest/manifests/autophone-media.ini @@ -0,0 +1,10 @@ +[DEFAULT] +subsuite = media + +[../tests/dom/media/test/test_can_play_type.html] +[../tests/dom/media/test/test_can_play_type_mpeg.html] +[../tests/dom/media/test/test_audio1.html] +[../tests/dom/media/test/test_decode_error.html] +[../tests/dom/media/test/test_imagecapture.html] +[../tests/dom/media/test/test_played.html] +[../tests/dom/media/test/test_playback.html] diff --git a/testing/mochitest/manifests/autophone-webrtc.ini b/testing/mochitest/manifests/autophone-webrtc.ini new file mode 100644 index 000000000..0e7b13602 --- /dev/null +++ b/testing/mochitest/manifests/autophone-webrtc.ini @@ -0,0 +1,156 @@ +[DEFAULT] +subsuite = media + +[../tests/dom/media/tests/mochitest/test_dataChannel_basicAudio.html] +[../tests/dom/media/tests/mochitest/test_dataChannel_basicAudioVideoCombined.html] +skip-if = true # Bug 1189784 +[../tests/dom/media/tests/mochitest/test_dataChannel_basicAudioVideo.html] +[../tests/dom/media/tests/mochitest/test_dataChannel_basicAudioVideoNoBundle.html] +[../tests/dom/media/tests/mochitest/test_dataChannel_basicDataOnly.html] +[../tests/dom/media/tests/mochitest/test_dataChannel_basicVideo.html] +[../tests/dom/media/tests/mochitest/test_dataChannel_bug1013809.html] +[../tests/dom/media/tests/mochitest/test_dataChannel_noOffer.html] +[../tests/dom/media/tests/mochitest/test_enumerateDevices.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_addTrackRemoveTrack.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_audioCapture.html] +skip-if = true # timeouts, see Bug 1264333 +[../tests/dom/media/tests/mochitest/test_getUserMedia_basicAudio.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_basicScreenshare.html] +skip-if = true # OverConstrained error, no screenshare on Android +[../tests/dom/media/tests/mochitest/test_getUserMedia_basicTabshare.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_basicVideoAudio.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_basicVideo.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_basicWindowshare.html] +skip-if = true # OverConstrained error, no windowshare on Android +[../tests/dom/media/tests/mochitest/test_getUserMedia_bug1223696.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_callbacks.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_constraints.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_gumWithinGum.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_loadedmetadata.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_mediaStreamConstructors.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_peerIdentity.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_playAudioTwice.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_playVideoAudioTwice.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_playVideoTwice.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_spinEventLoop.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_stopAudioStream.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_stopAudioStreamWithFollowupAudio.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStream.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStreamWithFollowupVideoAudio.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_stopVideoStream.html] +[../tests/dom/media/tests/mochitest/test_getUserMedia_stopVideoStreamWithFollowupVideo.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_addDataChannel.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_addDataChannelNoBundle.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_addIceCandidate.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStreamNoBundle.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStream.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStreamNoBundle.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_answererAddSecondAudioStream.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_basicAudio.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_basicAudioPcmaPcmuOnly.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_basicAudioRequireEOC.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoCombined.html] +skip-if = true # Bug 1189784 +[../tests/dom/media/tests/mochitest/test_peerConnection_basicAudioVideo.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoNoBundle.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoNoRtcpMux.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_basicH264Video.html] +skip-if = true # Bug 1149374 +[../tests/dom/media/tests/mochitest/test_peerConnection_basicScreenshare.html] +skip-if = true # No screenshare on Android +[../tests/dom/media/tests/mochitest/test_peerConnection_basicVideo.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_basicWindowshare.html] +skip-if = true # No windowshare on Android +[../tests/dom/media/tests/mochitest/test_peerConnection_bug1013809.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_bug1042791.html] +skip-if = true # Bug 1149374 +[../tests/dom/media/tests/mochitest/test_peerConnection_bug1064223.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_bug1227781.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_bug822674.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_bug825703.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_bug827843.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_bug834153.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_callbacks.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_capturedVideo.html] +skip-if = true # Bug 1264340 +[../tests/dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_2d.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_webgl.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_certificates.html] +skip-if = true # Bug 1180968 +[../tests/dom/media/tests/mochitest/test_peerConnection_closeDuringIce.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_close.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_errorCallbacks.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_forwarding_basicAudioVideoCombined.html] +skip-if = true # Bug 1189784 +[../tests/dom/media/tests/mochitest/test_peerConnection_iceFailure.html] +skip-if = true # Bug 1180388 +[../tests/dom/media/tests/mochitest/test_peerConnection_localReofferRollback.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_localRollback.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_multiple_captureStream_canvas_2d.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_noTrickleAnswer.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_noTrickleOfferAnswer.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_noTrickleOffer.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveAudio.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideoAudio.html] +skip-if = true # Bug 1189784 +[../tests/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideo.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_relayOnly.html] +skip-if = true # Bug 1222983 +[../tests/dom/media/tests/mochitest/test_peerConnection_remoteReofferRollback.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_remoteRollback.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_removeAudioTrack.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrack.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrackNoBundle.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrack.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrackNoBundle.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_removeVideoTrack.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_replaceTrack.html] +skip-if = true # Bug 1189784 +[../tests/dom/media/tests/mochitest/test_peerConnection_replaceVideoThenRenegotiate.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_scaleResolution.html] +skip-if = true # Bug 1264343 +[../tests/dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInHaveLocalOffer.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInStable.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_setLocalOfferInHaveRemoteOffer.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_setParameters.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_setRemoteAnswerInStable.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_setRemoteOfferInHaveLocalOffer.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_simulcastOffer.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_syncSetDescription.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_throwInCallbacks.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_toJSON.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_trackDisabling.html] +skip-if = true # Bug 1265878 +[../tests/dom/media/tests/mochitest/test_peerConnection_twoAudioStreams.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_twoAudioTracksInOneStream.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_twoAudioVideoStreamsCombined.html] +skip-if = true # Bug 1189784 +[../tests/dom/media/tests/mochitest/test_peerConnection_twoAudioVideoStreams.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_twoVideoStreams.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_twoVideoTracksInOneStream.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_verifyAudioAfterRenegotiation.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_verifyVideoAfterRenegotiation.html] +[../tests/dom/media/tests/mochitest/test_peerConnection_webAudio.html] +[../tests/dom/media/tests/mochitest/test_selftest.html] +[../tests/dom/media/tests/mochitest/identity/test_fingerprints.html] +skip-if = true # Bug 1200411 +[../tests/dom/media/tests/mochitest/identity/test_getIdentityAssertion.html] +skip-if = true # Bug 1200411 +[../tests/dom/media/tests/mochitest/identity/test_idpproxy.html] +skip-if = true # Bug 1200411 +[../tests/dom/media/tests/mochitest/identity/test_loginNeeded.html] +skip-if = true # Bug 1200411 +[../tests/dom/media/tests/mochitest/identity/test_peerConnection_asymmetricIsolation.html] +skip-if = true # Bug 1200411 +[../tests/dom/media/tests/mochitest/identity/test_peerConnection_peerIdentity.html] +skip-if = true # Bug 1200411 +[../tests/dom/media/tests/mochitest/identity/test_setIdentityProvider.html] +skip-if = true # Bug 1200411 +[../tests/dom/media/tests/mochitest/identity/test_setIdentityProviderWithErrors.html] +skip-if = true # Bug 1200411 diff --git a/testing/mochitest/manifests/emulator-jb.ini b/testing/mochitest/manifests/emulator-jb.ini new file mode 100644 index 000000000..236af51ae --- /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 000000000..c2a8e7bd8 --- /dev/null +++ b/testing/mochitest/manifests/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +TEST_HARNESS_FILES.testing.mochitest.manifests += [ + 'autophone-media.ini', + 'autophone-webrtc.ini', + 'emulator-jb.ini', +] diff --git a/testing/mochitest/mochitest-e10s-utils.js b/testing/mochitest/mochitest-e10s-utils.js new file mode 100644 index 000000000..5897c6542 --- /dev/null +++ b/testing/mochitest/mochitest-e10s-utils.js @@ -0,0 +1,11 @@ +// 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; + Cu.reportError("remote browser crashed while on " + + (uri ? uri.spec : "<unknown>") + "\n"); + }, true); +} diff --git a/testing/mochitest/mochitest.eslintrc.js b/testing/mochitest/mochitest.eslintrc.js new file mode 100644 index 000000000..3532ecf74 --- /dev/null +++ b/testing/mochitest/mochitest.eslintrc.js @@ -0,0 +1,41 @@ +// Parent config file for all mochitest files. +module.exports = { + rules: { + "mozilla/import-headjs-globals": "warn", + "mozilla/mark-test-function-used": "warn", + "no-shadow": "error", + }, + + "env": { + "browser": true, + }, + + // All globals made available in the test environment. + "globals": { + "add_task": false, + "Assert": false, + "EventUtils": false, + "executeSoon": false, + "export_assertions": false, + "finish": false, + "getRootDirectory": false, + "getTestFilePath": false, + "gTestPath": false, + "info": false, + "is": false, + "isDeeply": false, + "isnot": false, + "ok": false, + "promise": false, + "registerCleanupFunction": false, + "requestLongerTimeout": false, + "SimpleTest": false, + "SpecialPowers": false, + "todo": false, + "todo_is": false, + "todo_isnot": false, + "waitForClipboard": false, + "waitForExplicitFinish": false, + "waitForFocus": false, + } +}; diff --git a/testing/mochitest/mochitest_options.py b/testing/mochitest/mochitest_options.py new file mode 100644 index 000000000..9e61670a5 --- /dev/null +++ b/testing/mochitest/mochitest_options.py @@ -0,0 +1,1060 @@ +# 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/. + +from abc import ABCMeta, abstractmethod, abstractproperty +from argparse import ArgumentParser, SUPPRESS +from distutils.util import strtobool +from urlparse import urlparse +import json +import os +import tempfile + +from mozdevice import DroidADB, DroidSUT +from mozprofile import DEFAULT_PORTS +import mozinfo +import mozlog +import moznetwork + + +here = os.path.abspath(os.path.dirname(__file__)) + +try: + from mozbuild.base import ( + MozbuildObject, + MachCommandConditions as conditions, + ) + build_obj = MozbuildObject.from_environment(cwd=here) +except ImportError: + build_obj = None + conditions = None + + +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-redhat-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-redhat-linux-gnu.sup")) + rv.append(os.path.join(supps_path, "cross-architecture.sup")) + + return rv + + +class ArgumentContainer(): + __metaclass__ = ABCMeta + + @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.""" + + FLAVORS = ('a11y', 'browser', 'chrome', 'jetpack-addon', 'jetpack-package', 'plain') + 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"], + {"default": "plain", + "choices": FLAVORS, + "help": "Mochitest flavor to run, one of {}. Defaults to 'plain'.".format(FLAVORS), + "suppress": build_obj is not None, + }], + [["--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", + "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-dir"], + {"action": "store_true", + "dest": "runByDir", + "help": "Run each directory in a single browser instance with a fresh profile.", + "default": False, + }], + [["--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, + }], + [["--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.", + }], + [["--metro-immersive"], + {"action": "store_true", + "dest": "immersiveMode", + "default": False, + "help": "Launches tests in an immersive browser.", + "suppress": True, + }], + [["--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.", + }], + [["--jsdebugger"], + {"action": "store_true", + "default": False, + "help": "Start the browser JS debugger before running the test. Implies --no-autorun.", + }], + [["--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.", + }], + [["--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, + }], + [["--strict-content-sandbox"], + {"action": "store_true", + "default": False, + "dest": "strictContentSandbox", + "help": "Run tests with a more strict content sandbox (Windows only).", + "suppress": not mozinfo.isWin, + }], + [["--nested_oop"], + {"action": "store_true", + "default": False, + "help": "Run tests with nested_oop preferences and test filtering enabled.", + }], + [["--dmd"], + {"action": "store_true", + "default": False, + "help": "Run tests with DMD active.", + }], + [["--dmd-path"], + {"default": None, + "dest": "dmdPath", + "help": "Specifies the path to the directory containing the shared library for DMD.", + "suppress": True, + }], + [["--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 after each test in the directory specified " + "by --dump-output-directory.", + }], + [["--slowscript"], + {"action": "store_true", + "default": False, + "help": "Do not set the JS_DISABLE_SLOW_SCRIPT_SIGNALS env variable; " + "when not set, recoverable but misleading SIGSEGV instances " + "may occur in Ion/Odin JIT code.", + }], + [["--screenshot-on-fail"], + {"action": "store_true", + "default": False, + "dest": "screenshotOnFail", + "help": "Take screenshots on all test failures. Set $MOZ_UPLOAD_DIR to a directory " + "for storing the screenshots." + }], + [["--quiet"], + {"action": "store_true", + "dest": "quiet", + "default": False, + "help": "Do not print test log lines unless a failure occurs.", + }], + [["--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.", + }], + [["--enable-cpow-warnings"], + {"action": "store_true", + "dest": "enableCPOWWarnings", + "help": "Enable logging of unsafe CPOW usage, which is disabled by default for tests", + "suppress": True, + }], + [["--marionette"], + {"default": None, + "help": "host:port to use when connecting to Marionette", + }], + [["--marionette-port-timeout"], + {"default": None, + "help": "Timeout while waiting for the marionette port to open.", + "suppress": True, + }], + [["--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.", + }], + ] + + defaults = { + # Bug 1065098 - The geckomediaplugin process fails to produce a leak + # log for some reason. + 'ignoreMissingLeaks': ["geckomediaplugin"], + '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.""" + + # for test manifest parsing. + mozinfo.update({"strictContentSandbox": options.strictContentSandbox}) + # for test manifest parsing. + mozinfo.update({"nested_oop": options.nested_oop}) + + # and android doesn't use 'app' the same way, so skip validation + if parser.app != 'android': + if options.app is None: + if build_obj: + options.app = build_obj.get_binary_path() + 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.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.dmdPath: + options.dmdPath = self.get_full_path(options.dmdPath, parser.oldcwd) + + if options.dmd and not options.dmdPath: + if build_obj: + options.dmdPath = build_obj.bindir + else: + parser.error( + "could not find dmd libraries, specify them with --dmd-path") + + 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.jsdebugger: + options.extraPrefs += [ + "devtools.debugger.remote-enabled=true", + "devtools.chrome.enabled=true", + "devtools.debugger.prompt-connection=false" + ] + options.autorun = False + + if options.debugOnFailure and not options.jsdebugger: + parser.error( + "--debug-on-failure 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_port_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: + if build_obj: + options.testingModulesDir = os.path.join( + build_obj.topobjdir, '_tests', 'modules') + else: + # Try to guess the testing modules directory. + # This somewhat grotesque hack allows the buildbot machines to find the + # modules directory without having to configure the buildbot hosts. This + # code should never be executed in local runs because the build system + # should always set the flag that populates this variable. If buildbot ever + # passes this argument, this code can be deleted. + possible = os.path.join(here, os.path.pardir, 'modules') + + if os.path.isdir(possible): + options.testingModulesDir = possible + + if build_obj: + plugins_dir = os.path.join(build_obj.distdir, 'plugins') + if 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.immersiveMode: + if not mozinfo.isWin: + parser.error("immersive is only supported on Windows 8 and up.") + options.immersiveHelperPath = os.path.join( + options.utilityPath, "metrotestharness.exe") + if not os.path.exists(options.immersiveHelperPath): + parser.error("%s not found, cannot launch immersive tests." % + options.immersiveHelperPath) + + 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') + for f in ['/usr/bin/gst-launch-0.10', '/usr/bin/pactl']: + if not os.path.isfile(f): + parser.error( + 'Missing binary %s required for ' + '--use-test-media-devices' % f) + + if options.nested_oop: + options.e10s = True + + options.leakThresholds = { + "default": options.defaultLeakThreshold, + "tab": 10000, # See dependencies of bug 1051230. + # GMP rarely gets a log, but when it does, it leaks a little. + "geckomediaplugin": 20000, + } + + # 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 = [ + [["--remote-app-path"], + {"dest": "remoteAppPath", + "help": "Path to remote executable relative to device root using \ + only forward slashes. Either this or app must be specified \ + but not both.", + "default": None, + }], + [["--deviceIP"], + {"dest": "deviceIP", + "help": "ip address of remote device to test", + "default": None, + }], + [["--deviceSerial"], + {"dest": "deviceSerial", + "help": "ip address of remote device to test", + "default": None, + }], + [["--dm_trans"], + {"choices": ["adb", "sut"], + "default": "adb", + "help": "The transport to use for communication with the device [default: adb].", + "suppress": True, + }], + [["--adbpath"], + {"dest": "adbPath", + "default": None, + "help": "Path to adb binary.", + "suppress": True, + }], + [["--devicePort"], + {"dest": "devicePort", + "type": int, + "default": 20701, + "help": "port of remote device to test", + }], + [["--remote-product-name"], + {"dest": "remoteProductName", + "default": "fennec", + "help": "The executable's name of remote product to test - either \ + fennec or firefox, defaults to fennec", + "suppress": True, + }], + [["--remote-logfile"], + {"dest": "remoteLogFile", + "default": None, + "help": "Name of log file on the device relative to the device \ + root. PLEASE ONLY USE A FILENAME.", + }], + [["--remote-webserver"], + {"dest": "remoteWebServer", + "default": None, + "help": "ip address where the remote web server is hosted at", + }], + [["--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, + }], + [["--robocop-ini"], + {"dest": "robocopIni", + "default": "", + "help": "name of the .ini file containing the list of tests to run", + }], + [["--robocop-apk"], + {"dest": "robocopApk", + "default": "", + "help": "name of the Robocop APK to use for ADB test running", + }], + [["--remoteTestRoot"], + {"dest": "remoteTestRoot", + "default": None, + "help": "remote directory to use as test root \ + (eg. /mnt/sdcard/tests or /data/local/tests)", + "suppress": True, + }], + ] + + defaults = { + 'dm': None, + # 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 = '-' + + device_args = {'deviceRoot': options.remoteTestRoot} + if options.dm_trans == "adb": + device_args['adbPath'] = options.adbPath + if options.deviceIP: + device_args['host'] = options.deviceIP + device_args['port'] = options.devicePort + elif options.deviceSerial: + device_args['deviceSerial'] = options.deviceSerial + options.dm = DroidADB(**device_args) + elif options.dm_trans == 'sut': + if options.deviceIP is None: + parser.error( + "If --dm_trans = sut, you must provide a device IP") + device_args['host'] = options.deviceIP + device_args['port'] = options.devicePort + options.dm = DroidSUT(**device_args) + + if not options.remoteTestRoot: + options.remoteTestRoot = options.dm.deviceRoot + + if options.remoteWebServer is None: + if os.name != "nt": + options.remoteWebServer = moznetwork.get_ip() + else: + parser.error( + "you must specify a --remote-webserver=<ip address>") + + options.webServer = options.remoteWebServer + + if options.remoteLogFile is None: + options.remoteLogFile = options.remoteTestRoot + \ + '/logs/mochitest.log' + + if options.remoteLogFile.count('/') < 1: + options.remoteLogFile = options.remoteTestRoot + \ + '/' + options.remoteLogFile + + if options.remoteAppPath and options.app: + parser.error( + "You cannot specify both the remoteAppPath and the app setting") + elif options.remoteAppPath: + options.app = options.remoteTestRoot + "/" + options.remoteAppPath + elif options.app is None: + if build_obj: + options.app = build_obj.substs['ANDROID_PACKAGE_NAME'] + else: + # Neither remoteAppPath nor app are set -- error + parser.error("You must specify either appPath or app") + + 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 options.pidFile != "": + f = open(options.pidFile, 'w') + f.write("%s" % os.getpid()) + f.close() + + # Robocop specific options + if options.robocopIni != "": + if not os.path.exists(options.robocopIni): + parser.error( + "Unable to find specified robocop .ini manifest '%s'" % + options.robocopIni) + options.robocopIni = os.path.abspath(options.robocopIni) + + if not options.robocopApk and build_obj: + options.robocopApk = os.path.join(build_obj.topobjdir, 'mobile', 'android', + 'tests', 'browser', + 'robocop', 'robocop-debug.apk') + + if options.robocopApk != "": + if not os.path.exists(options.robocopApk): + parser.error( + "Unable to find robocop APK '%s'" % + options.robocopApk) + options.robocopApk = os.path.abspath(options.robocopApk) + + # Disable e10s by default on Android because we don't run Android + # e10s jobs anywhere yet. + options.e10s = False + mozinfo.update({'e10s': options.e10s}) + + # allow us to keep original application around for cleanup while + # running robocop via 'am' + 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 000000000..dd4cff324 --- /dev/null +++ b/testing/mochitest/moz.build @@ -0,0 +1,167 @@ +# -*- 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' + +JAR_MANIFESTS += ['jar.mn'] + +USE_EXTENSION_MANIFEST = True + +FINAL_TARGET_PP_FILES += ['install.rdf'] + +if CONFIG['OS_TARGET'] != 'Android': + DEFINES['MOCHITEST_BOOTSTRAP'] = True + FINAL_TARGET_FILES += ['bootstrap.js'] + +MOCHITEST_MANIFESTS += [ + 'tests/MochiKit-1.4.2/tests/mochitest.ini', +] +MOCHITEST_CHROME_MANIFESTS += ['chrome/chrome.ini'] + +GENERATED_FILES += [ + 'automation.py', +] + +TEST_HARNESS_FILES.testing.mochitest += [ + '!automation.py', + '/build/mobile/remoteautomation.py', + '/build/pgo/server-locations.txt', + '/build/sanitizers/lsan_suppressions.txt', + '/build/valgrind/cross-architecture.sup', + '/build/valgrind/i386-redhat-linux-gnu.sup', + '/build/valgrind/x86_64-redhat-linux-gnu.sup', + '/netwerk/test/httpserver/httpd.js', + 'bisection.py', + 'browser-harness.xul', + 'browser-test-overlay.xul', + 'browser-test.js', + 'chrome-harness.js', + 'chunkifyTests.js', + 'gen_template.pl', + 'harness.xul', + 'jetpack-addon-harness.js', + 'jetpack-addon-overlay.xul', + 'jetpack-package-harness.js', + 'jetpack-package-overlay.xul', + 'leaks.py', + 'mach_test_package_commands.py', + 'manifest.webapp', + 'manifestLibrary.js', + 'mochitest_options.py', + 'nested_setup.js', + 'pywebsocket_wrapper.py', + 'redirect.html', + 'runrobocop.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.pywebsocket += [ + 'pywebsocket/standalone.py', +] + +TEST_HARNESS_FILES.testing.mochitest.pywebsocket.mod_pywebsocket += [ + 'pywebsocket/mod_pywebsocket/__init__.py', + 'pywebsocket/mod_pywebsocket/_stream_base.py', + 'pywebsocket/mod_pywebsocket/_stream_hixie75.py', + 'pywebsocket/mod_pywebsocket/_stream_hybi.py', + 'pywebsocket/mod_pywebsocket/common.py', + 'pywebsocket/mod_pywebsocket/dispatch.py', + 'pywebsocket/mod_pywebsocket/extensions.py', + 'pywebsocket/mod_pywebsocket/fast_masking.i', + 'pywebsocket/mod_pywebsocket/headerparserhandler.py', + 'pywebsocket/mod_pywebsocket/http_header_util.py', + 'pywebsocket/mod_pywebsocket/memorizingfile.py', + 'pywebsocket/mod_pywebsocket/msgutil.py', + 'pywebsocket/mod_pywebsocket/mux.py', + 'pywebsocket/mod_pywebsocket/stream.py', + 'pywebsocket/mod_pywebsocket/util.py', + 'pywebsocket/mod_pywebsocket/xhr_benchmark_handler.py', +] + +TEST_HARNESS_FILES.testing.mochitest.pywebsocket.mod_pywebsocket.handshake += [ + 'pywebsocket/mod_pywebsocket/handshake/__init__.py', + 'pywebsocket/mod_pywebsocket/handshake/_base.py', + 'pywebsocket/mod_pywebsocket/handshake/hybi.py', + 'pywebsocket/mod_pywebsocket/handshake/hybi00.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.txt', +] + diff --git a/testing/mochitest/nested_setup.js b/testing/mochitest/nested_setup.js new file mode 100644 index 000000000..d42eaf944 --- /dev/null +++ b/testing/mochitest/nested_setup.js @@ -0,0 +1,31 @@ + +var gTestURL = ''; + +function addPermissions() +{ + SpecialPowers.pushPermissions( + [{ type: "browser", allow: true, context: document }], + addPreferences); +} + +function addPreferences() +{ + SpecialPowers.pushPrefEnv( + {"set": [["dom.mozBrowserFramesEnabled", true]]}, + insertFrame); +} + +function insertFrame() +{ + SpecialPowers.nestedFrameSetup(); + + var iframe = document.createElement('iframe'); + iframe.id = 'nested-parent-frame'; + iframe.width = "100%"; + iframe.height = "100%"; + iframe.scoring = "no"; + iframe.setAttribute("remote", "true"); + iframe.setAttribute("mozbrowser", "true"); + iframe.src = gTestURL; + document.getElementById("holder-div").appendChild(iframe); +}
\ No newline at end of file diff --git a/testing/mochitest/pywebsocket/COPYING b/testing/mochitest/pywebsocket/COPYING new file mode 100644 index 000000000..989d02e4c --- /dev/null +++ b/testing/mochitest/pywebsocket/COPYING @@ -0,0 +1,28 @@ +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. diff --git a/testing/mochitest/pywebsocket/README b/testing/mochitest/pywebsocket/README new file mode 100644 index 000000000..c8c758f5e --- /dev/null +++ b/testing/mochitest/pywebsocket/README @@ -0,0 +1,17 @@ +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 Apache module, run this to read the document: +$ pydoc mod_pywebsocket + +To use pywebsocket as standalone server, run this to read the document: +$ pydoc mod_pywebsocket.standalone diff --git a/testing/mochitest/pywebsocket/README-MOZILLA b/testing/mochitest/pywebsocket/README-MOZILLA new file mode 100644 index 000000000..468391519 --- /dev/null +++ b/testing/mochitest/pywebsocket/README-MOZILLA @@ -0,0 +1,96 @@ +This pywebsocket code is mostly unchanged from the source at + + svn checkout http://pywebsocket.googlecode.com/svn/trunk/ pywebsocket-read-only + +The current Mozilla code is based on + + svnversion: 860 (supports RFC 6455, permessage compression extension) + +-------------------------------------------------------------------------------- +STEPS TO UPDATE MOZILLA TO NEWER PYWEBSOCKET VERSION +-------------------------------------------------------------------------------- +- Get new pywebsocket checkout from googlecode (into, for instance, 'src') + + svn checkout http://pywebsocket.googlecode.com/svn/trunk/ pywebsocket-read-only + +- Export a version w/o SVN files: + + svn export src dist + +- rsync new version into our tree, deleting files that aren't needed any more + (NOTE: this will blow away this file! hg revert it or keep a copy.) + + rsync -rv --delete dist/ $MOZ_SRC/testing/mochitest/pywebsocket + +- Get rid of examples/test directory and some cruft: + + rm -rf example test setup.py MANIFEST.in + +- Manually move the 'standalone.py' file from the mmod_pywebsocket/ directory to + the parent directory (not sure why we moved it: probably no reason) + +- 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: + + make mochitest-plain TEST_PATH=dom/base/test/test_websocket.html + +- If this doesn't take a look at the pywebsocket server log, + $OBJDIR/_tests/testing/mochitest/websock.log + +- Upgrade the svnversion number at top of this file to whatever version we're + now based off of. + +-------------------------------------------------------------------------------- +PATCH TO hybi.py for HSTS support: + + +diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py +--- a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py ++++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py +@@ -299,16 +299,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 + +@@ -396,16 +399,21 @@ class Handshaker(object): + 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: Add HSTS header if requested to ++ 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('\r\n') + + return ''.join(response) diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/__init__.py b/testing/mochitest/pywebsocket/mod_pywebsocket/__init__.py new file mode 100644 index 000000000..70933a220 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/__init__.py @@ -0,0 +1,224 @@ +# 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 extension for Apache HTTP Server. + +mod_pywebsocket is a WebSocket extension for Apache HTTP Server +intended for testing or experimental purposes. mod_python is required. + + +Installation +============ + +0. Prepare an Apache HTTP Server for which mod_python is enabled. + +1. Specify the following Apache HTTP Server directives to suit your + configuration. + + If mod_pywebsocket is not in the Python path, specify the following. + <websock_lib> is the directory where mod_pywebsocket is installed. + + PythonPath "sys.path+['<websock_lib>']" + + Always specify the following. <websock_handlers> is the directory where + user-written WebSocket handlers are placed. + + PythonOption mod_pywebsocket.handler_root <websock_handlers> + PythonHeaderParserHandler mod_pywebsocket.headerparserhandler + + To limit the search for WebSocket handlers to a directory <scan_dir> + under <websock_handlers>, configure as follows: + + PythonOption mod_pywebsocket.handler_scan <scan_dir> + + <scan_dir> is useful in saving scan time when <websock_handlers> + contains many non-WebSocket handler files. + + If you want to allow handlers whose canonical path is not under the root + directory (i.e. symbolic link is in root directory but its target is not), + configure as follows: + + PythonOption mod_pywebsocket.allow_handlers_outside_root_dir On + + Example snippet of httpd.conf: + (mod_pywebsocket is in /websock_lib, WebSocket handlers are in + /websock_handlers, port is 80 for ws, 443 for wss.) + + <IfModule python_module> + PythonPath "sys.path+['/websock_lib']" + PythonOption mod_pywebsocket.handler_root /websock_handlers + PythonHeaderParserHandler mod_pywebsocket.headerparserhandler + </IfModule> + +2. Tune Apache parameters for serving WebSocket. We'd like to note that at + least TimeOut directive from core features and RequestReadTimeout + directive from mod_reqtimeout should be modified not to kill connections + in only a few seconds of idle time. + +3. Verify installation. You can use example/console.html to poke the server. + + +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_location, +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_location (HyBi 00 only) +- ws_extensions (HyBi 06 and later) +- ws_deflate (HyBi 06 and later) +- ws_protocol +- ws_requested_protocols (HyBi 06 and later) + +The last two are a bit tricky. See the next subsection. + + +Subprotocol Negotiation +----------------------- + +For HyBi 06 and later, 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. + +For HyBi 00, when web_socket_do_extra_handshake is called, +ws_protocol is set to the value given by the client in +Sec-WebSocket-Protocol header or None if +such header was not found in the opening handshake request. Finish extra +handshake with ws_protocol untouched to accept the request subprotocol. +Then, Sec-WebSocket-Protocol header will be sent to +the client in response with the same value as requested. Raise an exception +in web_socket_do_extra_handshake to reject the requested subprotocol. + + +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 if the server (Apache or +standalone.py) is configured to use threads. + + +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/pywebsocket/mod_pywebsocket/_stream_base.py b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_base.py new file mode 100644 index 000000000..8235666bb --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_base.py @@ -0,0 +1,181 @@ +# 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. + + +"""Base stream class. +""" + + +# 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. + + +import socket + +from mod_pywebsocket import util + + +# 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 + + +class StreamBase(object): + """Base stream class.""" + + def __init__(self, request): + """Construct an instance. + + Args: + request: mod_python request. + """ + + self._logger = util.get_class_logger(self) + + self._request = request + + 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 socket.error, e: + # Catch a socket.error. Because it's not a child class of the + # IOError prior to Python 2.6, we cannot omit this except clause. + # Use %s rather than %r for the exception to use human friendly + # format. + raise ConnectionTerminatedException( + 'Receiving %d byte failed. socket.error (%s) occurred' % + (length, e)) + except IOError, 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, 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. + + 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 ''.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 ''.join(read_bytes) + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hixie75.py b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hixie75.py new file mode 100644 index 000000000..94cf5b31b --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hixie75.py @@ -0,0 +1,229 @@ +# 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 a class for parsing/building frames of the WebSocket +protocol version HyBi 00 and Hixie 75. + +Specification: +- HyBi 00 http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00 +- Hixie 75 http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75 +""" + + +from mod_pywebsocket import common +from mod_pywebsocket._stream_base import BadOperationException +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import InvalidFrameException +from mod_pywebsocket._stream_base import StreamBase +from mod_pywebsocket._stream_base import UnsupportedFrameException +from mod_pywebsocket import util + + +class StreamHixie75(StreamBase): + """A class for parsing/building frames of the WebSocket protocol version + HyBi 00 and Hixie 75. + """ + + def __init__(self, request, enable_closing_handshake=False): + """Construct an instance. + + Args: + request: mod_python request. + enable_closing_handshake: to let StreamHixie75 perform closing + handshake as specified in HyBi 00, set + this option to True. + """ + + StreamBase.__init__(self, request) + + self._logger = util.get_class_logger(self) + + self._enable_closing_handshake = enable_closing_handshake + + self._request.client_terminated = False + self._request.server_terminated = False + + def send_message(self, message, end=True, binary=False): + """Send message. + + Args: + message: unicode string to send. + binary: not used in hixie75. + + Raises: + BadOperationException: when called on a server-terminated + connection. + """ + + if not end: + raise BadOperationException( + 'StreamHixie75 doesn\'t support send_message with end=False') + + if binary: + raise BadOperationException( + 'StreamHixie75 doesn\'t support send_message with binary=True') + + if self._request.server_terminated: + raise BadOperationException( + 'Requested send_message after sending out a closing handshake') + + self._write(''.join(['\x00', message.encode('utf-8'), '\xff'])) + + def _read_payload_length_hixie75(self): + """Reads a length header in a Hixie75 version frame with length. + + Raises: + ConnectionTerminatedException: when read returns empty string. + """ + + length = 0 + while True: + b_str = self._read(1) + b = ord(b_str) + length = length * 128 + (b & 0x7f) + if (b & 0x80) == 0: + break + return length + + def receive_message(self): + """Receive a WebSocket frame and return its payload an unicode string. + + Returns: + payload unicode string in a WebSocket frame. + + Raises: + ConnectionTerminatedException: when read returns empty + string. + BadOperationException: when called on a client-terminated + connection. + """ + + if self._request.client_terminated: + raise BadOperationException( + 'Requested receive_message after receiving a closing ' + 'handshake') + + while True: + # Read 1 byte. + # mp_conn.read will block if no bytes are available. + # Timeout is controlled by TimeOut directive of Apache. + frame_type_str = self.receive_bytes(1) + frame_type = ord(frame_type_str) + if (frame_type & 0x80) == 0x80: + # The payload length is specified in the frame. + # Read and discard. + length = self._read_payload_length_hixie75() + if length > 0: + _ = self.receive_bytes(length) + # 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the + # /client terminated/ flag and abort these steps. + if not self._enable_closing_handshake: + continue + + if frame_type == 0xFF and length == 0: + self._request.client_terminated = True + + if self._request.server_terminated: + self._logger.debug( + 'Received ack for server-initiated closing ' + 'handshake') + return None + + self._logger.debug( + 'Received client-initiated closing handshake') + + self._send_closing_handshake() + self._logger.debug( + 'Sent ack for client-initiated closing handshake') + return None + else: + # The payload is delimited with \xff. + bytes = self._read_until('\xff') + # The WebSocket protocol section 4.4 specifies that invalid + # characters must be replaced with U+fffd REPLACEMENT + # CHARACTER. + message = bytes.decode('utf-8', 'replace') + if frame_type == 0x00: + return message + # Discard data of other types. + + def _send_closing_handshake(self): + if not self._enable_closing_handshake: + raise BadOperationException( + 'Closing handshake is not supported in Hixie 75 protocol') + + self._request.server_terminated = True + + # 5.3 the server may decide to terminate the WebSocket connection by + # running through the following steps: + # 1. send a 0xFF byte and a 0x00 byte to the client to indicate the + # start of the closing handshake. + self._write('\xff\x00') + + def close_connection(self, unused_code='', unused_reason=''): + """Closes a WebSocket connection. + + Raises: + ConnectionTerminatedException: when closing handshake was + not successfull. + """ + + if self._request.server_terminated: + self._logger.debug( + 'Requested close_connection but server is already terminated') + return + + if not self._enable_closing_handshake: + self._request.server_terminated = True + self._logger.debug('Connection closed') + return + + self._send_closing_handshake() + self._logger.debug('Sent server-initiated closing handshake') + + # 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, and if we couldn't receive non-handshake + # frame, we take it as ConnectionTerminatedException. + 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): + raise BadOperationException( + 'StreamHixie75 doesn\'t support send_ping') + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hybi.py b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hybi.py new file mode 100644 index 000000000..104221b4c --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hybi.py @@ -0,0 +1,894 @@ +# 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 classes and helper functions for parsing/building frames +of the WebSocket protocol (RFC 6455). + +Specification: +http://tools.ietf.org/html/rfc6455 +""" + + +from collections import deque +import logging +import os +import struct +import time + +from mod_pywebsocket import common +from mod_pywebsocket import util +from mod_pywebsocket._stream_base import BadOperationException +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import InvalidFrameException +from mod_pywebsocket._stream_base import InvalidUTF8Exception +from mod_pywebsocket._stream_base import StreamBase +from mod_pywebsocket._stream_base import UnsupportedFrameException + + +_NOOP_MASKER = util.NoopMasker() + + +class Frame(object): + + def __init__(self, fin=1, rsv1=0, rsv2=0, rsv3=0, + opcode=None, payload=''): + 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 chr(mask_bit | length) + elif length < (1 << 16): + return chr(mask_bit | 126) + struct.pack('!H', length) + elif length < (1 << 63): + return chr(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 = '' + + first_byte = ((fin << 7) + | (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4) + | opcode) + header += chr(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') + + received = receive_bytes(2) + + first_byte = ord(received[0]) + 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(received[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): + 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): + 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 = '' + 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(StreamBase): + """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. + """ + + StreamBase.__init__(self, request) + + self._logger = util.get_class_logger(self) + + self._options = options + + 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 _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, unicode): + raise BadOperationException( + 'Message for binary frame must be instance of str') + + 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, 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 = ''.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, e: + 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, e: + # 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, e: + 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. + # Timeout is controlled by TimeOut directive of Apache. + + 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, 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, str) and not isinstance(reason, unicode): + raise BadOperationException( + 'close reason must be an instance of str 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=''): + 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/pywebsocket/mod_pywebsocket/common.py b/testing/mochitest/pywebsocket/mod_pywebsocket/common.py new file mode 100644 index 000000000..8c1524284 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/common.py @@ -0,0 +1,301 @@ +# 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 mod_pywebsocket import http_header_util + + +# Additional log level definitions. +LOGLEVEL_FINE = 9 + +# Constants indicating WebSocket protocol version. +VERSION_HIXIE75 = -1 +VERSION_HYBI00 = 0 +VERSION_HYBI01 = 1 +VERSION_HYBI02 = 2 +VERSION_HYBI03 = 2 +VERSION_HYBI04 = 4 +VERSION_HYBI05 = 5 +VERSION_HYBI06 = 6 +VERSION_HYBI07 = 7 +VERSION_HYBI08 = 8 +VERSION_HYBI09 = 8 +VERSION_HYBI10 = 8 +VERSION_HYBI11 = 8 +VERSION_HYBI12 = 8 +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 + +# UUIDs used by HyBi 04 and later opening handshake and frame masking. +WEBSOCKET_ACCEPT_UUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + +# Opening handshake header names and expected values. +UPGRADE_HEADER = 'Upgrade' +WEBSOCKET_UPGRADE_TYPE = 'websocket' +WEBSOCKET_UPGRADE_TYPE_HIXIE75 = 'WebSocket' +CONNECTION_HEADER = 'Connection' +UPGRADE_CONNECTION_TYPE = 'Upgrade' +HOST_HEADER = 'Host' +ORIGIN_HEADER = 'Origin' +SEC_WEBSOCKET_ORIGIN_HEADER = 'Sec-WebSocket-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' +SEC_WEBSOCKET_DRAFT_HEADER = 'Sec-WebSocket-Draft' +SEC_WEBSOCKET_KEY1_HEADER = 'Sec-WebSocket-Key1' +SEC_WEBSOCKET_KEY2_HEADER = 'Sec-WebSocket-Key2' +SEC_WEBSOCKET_LOCATION_HEADER = 'Sec-WebSocket-Location' + +# Extensions +DEFLATE_FRAME_EXTENSION = 'deflate-frame' +PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate' +X_WEBKIT_DEFLATE_FRAME_EXTENSION = 'x-webkit-deflate-frame' +MUX_EXTENSION = 'mux_DO_NOT_USE' + +# 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): + """Holds information about an extension which 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 self._name + + def add_parameter(self, name, value): + self._parameters.append((name, value)) + + def get_parameters(self): + return self._parameters + + def get_parameter_names(self): + return [name for name, unused_value in self._parameters] + + def has_parameter(self, name): + for param_name, param_value in self._parameters: + if param_name == name: + return True + return False + + def get_parameter_value(self, name): + for param_name, param_value in self._parameters: + if param_name == name: + return param_value + + +class ExtensionParsingException(Exception): + 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, e: + raise ExtensionParsingException( + 'Failed to parse parameter for %r (%r)' % + (extension_token, e)) + + return extension + + +def parse_extensions(data): + """Parses 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): + """Formats 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): + """Formats 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/pywebsocket/mod_pywebsocket/dispatch.py b/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py new file mode 100644 index 000000000..96c91e0c9 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py @@ -0,0 +1,393 @@ +# 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. +""" + + +import logging +import os +import re + +from mod_pywebsocket import common +from mod_pywebsocket import handshake +from mod_pywebsocket import msgutil +from mod_pywebsocket import mux +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: + exec handler_definition in global_dic + except Exception: + raise DispatchException('Error in sourcing handler:' + + util.get_stack_trace()) + 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, e: + # Re-raise to tell the caller of this function to finish this + # connection without sending any error. + self._logger.debug('%s', util.get_stack_trace()) + raise + except Exception, 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: + if mux.use_mux(request): + mux.start(request, self) + else: + 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, e: + self._logger.debug('%s', util.get_stack_trace()) + raise + except msgutil.BadOperationException, e: + self._logger.debug('%s', e) + request.ws_stream.close_connection( + common.STATUS_INTERNAL_ENDPOINT_ERROR) + except msgutil.InvalidFrameException, 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, e: + self._logger.debug('%s', e) + request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA) + except stream.InvalidUTF8Exception, e: + self._logger.debug('%s', e) + request.ws_stream.close_connection( + common.STATUS_INVALID_FRAME_PAYLOAD_DATA) + except msgutil.ConnectionTerminatedException, e: + self._logger.debug('%s', e) + except Exception, 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: + handler_suite = _source_handler_file(open(path).read()) + except DispatchException, 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/pywebsocket/mod_pywebsocket/extensions.py b/testing/mochitest/pywebsocket/mod_pywebsocket/extensions.py new file mode 100644 index 000000000..1edd988f6 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/extensions.py @@ -0,0 +1,764 @@ +# 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 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 = {} +_compression_extension_names = [] + + +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 DeflateFrameExtensionProcessor(ExtensionProcessorInterface): + """deflate-frame extension processor. + + Specification: + http://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate + """ + + _WINDOW_BITS_PARAM = 'max_window_bits' + _NO_CONTEXT_TAKEOVER_PARAM = 'no_context_takeover' + + def __init__(self, request): + ExtensionProcessorInterface.__init__(self, request) + self._logger = util.get_class_logger(self) + + self._response_window_bits = None + self._response_no_context_takeover = False + self._bfinal = 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 name(self): + return common.DEFLATE_FRAME_EXTENSION + + def _get_extension_response_internal(self): + # Any unknown parameter will be just ignored. + + window_bits = None + if self._request.has_parameter(self._WINDOW_BITS_PARAM): + window_bits = self._request.get_parameter_value( + self._WINDOW_BITS_PARAM) + try: + window_bits = _parse_window_bits(window_bits) + except ValueError, e: + return None + + no_context_takeover = self._request.has_parameter( + self._NO_CONTEXT_TAKEOVER_PARAM) + if (no_context_takeover and + self._request.get_parameter_value( + self._NO_CONTEXT_TAKEOVER_PARAM) is not None): + return None + + self._rfc1979_deflater = util._RFC1979Deflater( + window_bits, no_context_takeover) + + self._rfc1979_inflater = util._RFC1979Inflater() + + self._compress_outgoing = True + + response = common.ExtensionParameter(self._request.name()) + + if self._response_window_bits is not None: + response.add_parameter( + self._WINDOW_BITS_PARAM, str(self._response_window_bits)) + if self._response_no_context_takeover: + response.add_parameter( + self._NO_CONTEXT_TAKEOVER_PARAM, None) + + self._logger.debug( + 'Enable %s extension (' + 'request: window_bits=%s; no_context_takeover=%r, ' + 'response: window_wbits=%s; no_context_takeover=%r)' % + (self._request.name(), + window_bits, + no_context_takeover, + self._response_window_bits, + self._response_no_context_takeover)) + + return response + + def _setup_stream_options_internal(self, stream_options): + + class _OutgoingFilter(object): + + def __init__(self, parent): + self._parent = parent + + def filter(self, frame): + self._parent._outgoing_filter(frame) + + class _IncomingFilter(object): + + def __init__(self, parent): + self._parent = parent + + def filter(self, frame): + self._parent._incoming_filter(frame) + + stream_options.outgoing_frame_filters.append( + _OutgoingFilter(self)) + stream_options.incoming_frame_filters.insert( + 0, _IncomingFilter(self)) + + def set_response_window_bits(self, value): + self._response_window_bits = value + + def set_response_no_context_takeover(self, value): + self._response_no_context_takeover = value + + def set_bfinal(self, value): + self._bfinal = value + + def enable_outgoing_compression(self): + self._compress_outgoing = True + + def disable_outgoing_compression(self): + self._compress_outgoing = False + + def _outgoing_filter(self, frame): + """Transform outgoing frames. This method is called only by + an _OutgoingFilter instance. + """ + + original_payload_size = len(frame.payload) + self._outgoing_average_ratio_calculator.add_original_bytes( + original_payload_size) + + if (not self._compress_outgoing or + common.is_control_opcode(frame.opcode)): + self._outgoing_average_ratio_calculator.add_result_bytes( + original_payload_size) + return + + frame.payload = self._rfc1979_deflater.filter( + frame.payload, bfinal=self._bfinal) + frame.rsv1 = 1 + + filtered_payload_size = len(frame.payload) + 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()) + + def _incoming_filter(self, frame): + """Transform incoming frames. This method is called only by + an _IncomingFilter instance. + """ + + received_payload_size = len(frame.payload) + self._incoming_average_ratio_calculator.add_result_bytes( + received_payload_size) + + if frame.rsv1 != 1 or common.is_control_opcode(frame.opcode): + self._incoming_average_ratio_calculator.add_original_bytes( + received_payload_size) + return + + frame.payload = self._rfc1979_inflater.filter(frame.payload) + frame.rsv1 = 0 + + filtered_payload_size = len(frame.payload) + 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()) + + +_available_processors[common.DEFLATE_FRAME_EXTENSION] = ( + DeflateFrameExtensionProcessor) +_compression_extension_names.append(common.DEFLATE_FRAME_EXTENSION) + +_available_processors[common.X_WEBKIT_DEFLATE_FRAME_EXTENSION] = ( + DeflateFrameExtensionProcessor) +_compression_extension_names.append(common.X_WEBKIT_DEFLATE_FRAME_EXTENSION) + + +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, draft08=True): + """Construct PerMessageDeflateExtensionProcessor + + Args: + draft08: Follow the constraints on the parameters that were not + specified for permessage-compress but are specified for + permessage-deflate as on + draft-ietf-hybi-permessage-compression-08. + """ + + ExtensionProcessorInterface.__init__(self, request) + self._logger = util.get_class_logger(self) + + self._preferred_client_max_window_bits = None + self._client_no_context_takeover = False + + self._draft08 = draft08 + + def name(self): + # This method returns "deflate" (not "permessage-deflate") for + # compatibility. + return 'deflate' + + def _get_extension_response_internal(self): + if self._draft08: + 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 + else: + # Any unknown parameter will be just ignored. + pass + + 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, 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 (self._draft08 and + 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 self._draft08 and 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) +# TODO(tyoshino): Reorganize class names. +_compression_extension_names.append('deflate') + + +class MuxExtensionProcessor(ExtensionProcessorInterface): + """WebSocket multiplexing extension processor.""" + + _QUOTA_PARAM = 'quota' + + def __init__(self, request): + ExtensionProcessorInterface.__init__(self, request) + self._quota = 0 + self._extensions = [] + + def name(self): + return common.MUX_EXTENSION + + def check_consistency_with_other_processors(self, processors): + before_mux = True + for processor in processors: + name = processor.name() + if name == self.name(): + before_mux = False + continue + if not processor.is_active(): + continue + if before_mux: + # Mux extension cannot be used after extensions + # that depend on frame boundary, extension data field, or any + # reserved bits which are attributed to each frame. + if (name == common.DEFLATE_FRAME_EXTENSION or + name == common.X_WEBKIT_DEFLATE_FRAME_EXTENSION): + self.set_active(False) + return + else: + # Mux extension should not be applied before any history-based + # compression extension. + if (name == 'deflate' or + name == common.DEFLATE_FRAME_EXTENSION or + name == common.X_WEBKIT_DEFLATE_FRAME_EXTENSION): + self.set_active(False) + return + + def _get_extension_response_internal(self): + self._active = False + quota = self._request.get_parameter_value(self._QUOTA_PARAM) + if quota is not None: + try: + quota = int(quota) + except ValueError, e: + return None + if quota < 0 or quota >= 2 ** 32: + return None + self._quota = quota + + self._active = True + return common.ExtensionParameter(common.MUX_EXTENSION) + + def _setup_stream_options_internal(self, stream_options): + pass + + def set_quota(self, quota): + self._quota = quota + + def quota(self): + return self._quota + + def set_extensions(self, extensions): + self._extensions = extensions + + def extensions(self): + return self._extensions + + +_available_processors[common.MUX_EXTENSION] = MuxExtensionProcessor + + +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) + + +def is_compression_extension(extension_name): + return extension_name in _compression_extension_names + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/fast_masking.i b/testing/mochitest/pywebsocket/mod_pywebsocket/fast_masking.i new file mode 100644 index 000000000..ddaad27f5 --- /dev/null +++ b/testing/mochitest/pywebsocket/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/pywebsocket/mod_pywebsocket/handshake/__init__.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/__init__.py new file mode 100644 index 000000000..194f6b395 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/__init__.py @@ -0,0 +1,110 @@ +# 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. +""" + + +import logging + +from mod_pywebsocket import common +from mod_pywebsocket.handshake import hybi00 +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, allowDraft75=False, strict=False): + """Performs WebSocket handshake. + + Args: + request: mod_python request. + dispatcher: Dispatcher (dispatch.Dispatcher). + allowDraft75: obsolete argument. ignored. + strict: obsolete argument. ignored. + + 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))) + handshakers.append( + ('HyBi 00', hybi00.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, e: + _LOGGER.debug( + 'Failed to complete opening handshake as %s protocol: %r', + name, e) + if e.status: + raise e + except AbortedByUserException, e: + raise + except VersionException, 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/pywebsocket/mod_pywebsocket/handshake/_base.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/_base.py new file mode 100644 index 000000000..c993a584b --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/_base.py @@ -0,0 +1,182 @@ +# 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 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. '8, 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, e: + raise HandshakeException('Invalid port number format: %r' % e) + + +def format_header(name, value): + return '%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 != 'GET': + raise HandshakeException('Method is not GET: %r' % request.method) + + if request.protocol != '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/pywebsocket/mod_pywebsocket/handshake/hybi.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py new file mode 100644 index 000000000..44e71f179 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py @@ -0,0 +1,428 @@ +# 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 +""" + + +# Note: request.connection.write is used in this module, even though mod_python +# document says that it should be used only in connection handlers. +# Unfortunately, we have no other options. For example, request.write is not +# suitable because it doesn't allow direct raw bytes writing. + + +import base64 +import logging +import os +import re + +from mod_pywebsocket import common +from mod_pywebsocket.extensions import get_extension_processor +from mod_pywebsocket.extensions import is_compression_extension +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 + + +# 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 = util.sha1_hash( + key + common.WEBSOCKET_ACCEPT_UUID).digest() + accept = base64.b64encode(accept_binary) + + return (accept, accept_binary) + + +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, 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, accept_binary) = compute_accept(key) + self._logger.debug( + '%s: %r (%s)', + common.SEC_WEBSOCKET_ACCEPT_HEADER, + accept, + util.hexify(accept_binary)) + + 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 = filter(lambda processor: processor is not None, + self._request.ws_extension_processors) + + # 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 = filter(lambda processor: processor.is_active(), + processors) + + accepted_extensions = [] + + # We need to take into account of mux extension here. + # If mux extension exists: + # - Remove processors of extensions for logical channel, + # which are processors located before the mux processor + # - Pass extension requests for logical channel to mux processor + # - Attach the mux processor to the request. It will be referred + # by dispatcher to see whether the dispatcher should use mux + # handler or not. + mux_index = -1 + for i, processor in enumerate(processors): + if processor.name() == common.MUX_EXTENSION: + mux_index = i + break + if mux_index >= 0: + logical_channel_extensions = [] + for processor in processors[:mux_index]: + logical_channel_extensions.append(processor.request()) + processor.set_active(False) + self._request.mux_processor = processors[mux_index] + self._request.mux_processor.set_extensions( + logical_channel_extensions) + processors = filter(lambda processor: processor.is_active(), + processors) + + 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) + + if not is_compression_extension(processor.name()): + continue + + # Inactivate all of the following compression extensions. + for j in xrange(index + 1, len(processors)): + if is_compression_extension(processors[j].name()): + processors[j].set_active(False) + + if len(accepted_extensions) > 0: + self._request.ws_extensions = accepted_extensions + self._logger.debug( + 'Extensions accepted: %r', + 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, 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, e: + raise HandshakeException( + 'Failed to parse Sec-WebSocket-Extensions header: %r' % e) + + self._logger.debug( + 'Extensions requested: %r', + 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, 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 + + def _create_stream(self, stream_options): + return Stream(self._request, stream_options) + + def _create_handshake_response(self, accept): + response = [] + + response.append('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)) + 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: Add HSTS header if requested to + 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('\r\n') + + return ''.join(response) + + def _send_handshake(self, accept): + raw_response = self._create_handshake_response(accept) + self._request.connection.write(raw_response) + self._logger.debug('Sent server\'s opening handshake: %r', + raw_response) + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi00.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi00.py new file mode 100644 index 000000000..8757717a6 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi00.py @@ -0,0 +1,293 @@ +# 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 the opening handshake processor for the WebSocket +protocol version HyBi 00. + +Specification: +http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00 +""" + + +# 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. + + +import logging +import re +import struct + +from mod_pywebsocket import common +from mod_pywebsocket.stream import StreamHixie75 +from mod_pywebsocket import util +from mod_pywebsocket.handshake._base import HandshakeException +from mod_pywebsocket.handshake._base import check_request_line +from mod_pywebsocket.handshake._base import format_header +from mod_pywebsocket.handshake._base import get_default_port +from mod_pywebsocket.handshake._base import get_mandatory_header +from mod_pywebsocket.handshake._base import parse_host_header +from mod_pywebsocket.handshake._base import validate_mandatory_header + + +_MANDATORY_HEADERS = [ + # key, expected value or None + [common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75], + [common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE], +] + + +def _validate_subprotocol(subprotocol): + """Checks if characters in subprotocol are in range between U+0020 and + U+007E. A value in the Sec-WebSocket-Protocol field need to satisfy this + requirement. + + See the Section 4.1. Opening handshake of the spec. + """ + + if not subprotocol: + raise HandshakeException('Invalid subprotocol name: empty') + + # Parameter should be in the range U+0020 to U+007E. + for c in subprotocol: + if not 0x20 <= ord(c) <= 0x7e: + raise HandshakeException( + 'Illegal character in subprotocol name: %r' % c) + + +def _check_header_lines(request, mandatory_headers): + check_request_line(request) + + # The expected field names, and the meaning of their corresponding + # values, are as follows. + # |Upgrade| and |Connection| + for key, expected_value in mandatory_headers: + validate_mandatory_header(request, key, expected_value) + + +def _build_location(request): + """Build WebSocket location for request.""" + + location_parts = [] + if request.is_https(): + location_parts.append(common.WEB_SOCKET_SECURE_SCHEME) + else: + location_parts.append(common.WEB_SOCKET_SCHEME) + location_parts.append('://') + host, port = parse_host_header(request) + connection_port = request.connection.local_addr[1] + if port != connection_port: + raise HandshakeException('Header/connection port mismatch: %d/%d' % + (port, connection_port)) + location_parts.append(host) + if (port != get_default_port(request.is_https())): + location_parts.append(':') + location_parts.append(str(port)) + location_parts.append(request.unparsed_uri) + return ''.join(location_parts) + + +class Handshaker(object): + """Opening handshake processor for the WebSocket protocol version HyBi 00. + """ + + 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 in performing + handshake. + """ + + self._logger = util.get_class_logger(self) + + self._request = request + self._dispatcher = dispatcher + + def do_handshake(self): + """Perform WebSocket Handshake. + + On _request, we set + ws_resource, ws_protocol, ws_location, ws_origin, ws_challenge, + ws_challenge_md5: WebSocket handshake information. + ws_stream: Frame generation/parsing class. + ws_version: Protocol version. + + Raises: + HandshakeException: when any error happened in parsing the opening + handshake request. + """ + + # 5.1 Reading the client's opening handshake. + # dispatcher sets it in self._request. + _check_header_lines(self._request, _MANDATORY_HEADERS) + self._set_resource() + self._set_subprotocol() + self._set_location() + self._set_origin() + self._set_challenge_response() + self._set_protocol_version() + + self._dispatcher.do_extra_handshake(self._request) + + self._send_handshake() + + def _set_resource(self): + self._request.ws_resource = self._request.uri + + def _set_subprotocol(self): + # |Sec-WebSocket-Protocol| + subprotocol = self._request.headers_in.get( + common.SEC_WEBSOCKET_PROTOCOL_HEADER) + if subprotocol is not None: + _validate_subprotocol(subprotocol) + self._request.ws_protocol = subprotocol + + def _set_location(self): + # |Host| + host = self._request.headers_in.get(common.HOST_HEADER) + if host is not None: + self._request.ws_location = _build_location(self._request) + # TODO(ukai): check host is this host. + + def _set_origin(self): + # |Origin| + origin = self._request.headers_in.get(common.ORIGIN_HEADER) + if origin is not None: + self._request.ws_origin = origin + + def _set_protocol_version(self): + # |Sec-WebSocket-Draft| + draft = self._request.headers_in.get(common.SEC_WEBSOCKET_DRAFT_HEADER) + if draft is not None and draft != '0': + raise HandshakeException('Illegal value for %s: %s' % + (common.SEC_WEBSOCKET_DRAFT_HEADER, + draft)) + + self._logger.debug('Protocol version is HyBi 00') + self._request.ws_version = common.VERSION_HYBI00 + self._request.ws_stream = StreamHixie75(self._request, True) + + def _set_challenge_response(self): + # 5.2 4-8. + self._request.ws_challenge = self._get_challenge() + # 5.2 9. let /response/ be the MD5 finterprint of /challenge/ + self._request.ws_challenge_md5 = util.md5_hash( + self._request.ws_challenge).digest() + self._logger.debug( + 'Challenge: %r (%s)', + self._request.ws_challenge, + util.hexify(self._request.ws_challenge)) + self._logger.debug( + 'Challenge response: %r (%s)', + self._request.ws_challenge_md5, + util.hexify(self._request.ws_challenge_md5)) + + def _get_key_value(self, key_field): + key_value = get_mandatory_header(self._request, key_field) + + self._logger.debug('%s: %r', key_field, key_value) + + # 5.2 4. let /key-number_n/ be the digits (characters in the range + # U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_n/, + # interpreted as a base ten integer, ignoring all other characters + # in /key_n/. + try: + key_number = int(re.sub("\\D", "", key_value)) + except: + raise HandshakeException('%s field contains no digit' % key_field) + # 5.2 5. let /spaces_n/ be the number of U+0020 SPACE characters + # in /key_n/. + spaces = re.subn(" ", "", key_value)[1] + if spaces == 0: + raise HandshakeException('%s field contains no space' % key_field) + + self._logger.debug( + '%s: Key-number is %d and number of spaces is %d', + key_field, key_number, spaces) + + # 5.2 6. if /key-number_n/ is not an integral multiple of /spaces_n/ + # then abort the WebSocket connection. + if key_number % spaces != 0: + raise HandshakeException( + '%s: Key-number (%d) is not an integral multiple of spaces ' + '(%d)' % (key_field, key_number, spaces)) + # 5.2 7. let /part_n/ be /key-number_n/ divided by /spaces_n/. + part = key_number / spaces + self._logger.debug('%s: Part is %d', key_field, part) + return part + + def _get_challenge(self): + # 5.2 4-7. + key1 = self._get_key_value(common.SEC_WEBSOCKET_KEY1_HEADER) + key2 = self._get_key_value(common.SEC_WEBSOCKET_KEY2_HEADER) + # 5.2 8. let /challenge/ be the concatenation of /part_1/, + challenge = '' + challenge += struct.pack('!I', key1) # network byteorder int + challenge += struct.pack('!I', key2) # network byteorder int + challenge += self._request.connection.read(8) + return challenge + + def _send_handshake(self): + response = [] + + # 5.2 10. send the following line. + response.append('HTTP/1.1 101 WebSocket Protocol Handshake\r\n') + + # 5.2 11. send the following fields to the client. + response.append(format_header( + common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75)) + response.append(format_header( + common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)) + response.append(format_header( + common.SEC_WEBSOCKET_LOCATION_HEADER, self._request.ws_location)) + response.append(format_header( + common.SEC_WEBSOCKET_ORIGIN_HEADER, self._request.ws_origin)) + if self._request.ws_protocol: + response.append(format_header( + common.SEC_WEBSOCKET_PROTOCOL_HEADER, + self._request.ws_protocol)) + # 5.2 12. send two bytes 0x0D 0x0A. + response.append('\r\n') + # 5.2 13. send /response/ + response.append(self._request.ws_challenge_md5) + + raw_response = ''.join(response) + self._request.connection.write(raw_response) + self._logger.debug('Sent server\'s opening handshake: %r', + raw_response) + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/headerparserhandler.py b/testing/mochitest/pywebsocket/mod_pywebsocket/headerparserhandler.py new file mode 100644 index 000000000..c244421cf --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/headerparserhandler.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. + + +"""PythonHeaderParserHandler for mod_pywebsocket. + +Apache HTTP Server and mod_python must be configured such that this +function is called to handle WebSocket request. +""" + + +import logging + +from mod_python import apache + +from mod_pywebsocket import common +from mod_pywebsocket import dispatch +from mod_pywebsocket import handshake +from mod_pywebsocket import util + + +# PythonOption to specify the handler root directory. +_PYOPT_HANDLER_ROOT = 'mod_pywebsocket.handler_root' + +# PythonOption to specify the handler scan directory. +# This must be a directory under the root directory. +# The default is the root directory. +_PYOPT_HANDLER_SCAN = 'mod_pywebsocket.handler_scan' + +# PythonOption to allow handlers whose canonical path is +# not under the root directory. It's disallowed by default. +# Set this option with value of 'yes' to allow. +_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT = ( + 'mod_pywebsocket.allow_handlers_outside_root_dir') +# Map from values to their meanings. 'Yes' and 'No' are allowed just for +# compatibility. +_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT_DEFINITION = { + 'off': False, 'no': False, 'on': True, 'yes': True} + +# (Obsolete option. Ignored.) +# PythonOption to specify to allow handshake defined in Hixie 75 version +# protocol. The default is None (Off) +_PYOPT_ALLOW_DRAFT75 = 'mod_pywebsocket.allow_draft75' +# Map from values to their meanings. +_PYOPT_ALLOW_DRAFT75_DEFINITION = {'off': False, 'on': True} + + +class ApacheLogHandler(logging.Handler): + """Wrapper logging.Handler to emit log message to apache's error.log.""" + + _LEVELS = { + logging.DEBUG: apache.APLOG_DEBUG, + logging.INFO: apache.APLOG_INFO, + logging.WARNING: apache.APLOG_WARNING, + logging.ERROR: apache.APLOG_ERR, + logging.CRITICAL: apache.APLOG_CRIT, + } + + def __init__(self, request=None): + logging.Handler.__init__(self) + self._log_error = apache.log_error + if request is not None: + self._log_error = request.log_error + + # Time and level will be printed by Apache. + self._formatter = logging.Formatter('%(name)s: %(message)s') + + def emit(self, record): + apache_level = apache.APLOG_DEBUG + if record.levelno in ApacheLogHandler._LEVELS: + apache_level = ApacheLogHandler._LEVELS[record.levelno] + + msg = self._formatter.format(record) + + # "server" parameter must be passed to have "level" parameter work. + # If only "level" parameter is passed, nothing shows up on Apache's + # log. However, at this point, we cannot get the server object of the + # virtual host which will process WebSocket requests. The only server + # object we can get here is apache.main_server. But Wherever (server + # configuration context or virtual host context) we put + # PythonHeaderParserHandler directive, apache.main_server just points + # the main server instance (not any of virtual server instance). Then, + # Apache follows LogLevel directive in the server configuration context + # to filter logs. So, we need to specify LogLevel in the server + # configuration context. Even if we specify "LogLevel debug" in the + # virtual host context which actually handles WebSocket connections, + # DEBUG level logs never show up unless "LogLevel debug" is specified + # in the server configuration context. + # + # TODO(tyoshino): Provide logging methods on request object. When + # request is mp_request object (when used together with Apache), the + # methods call request.log_error indirectly. When request is + # _StandaloneRequest, the methods call Python's logging facility which + # we create in standalone.py. + self._log_error(msg, apache_level, apache.main_server) + + +def _configure_logging(): + logger = logging.getLogger() + # Logs are filtered by Apache based on LogLevel directive in Apache + # configuration file. We must just pass logs for all levels to + # ApacheLogHandler. + logger.setLevel(logging.DEBUG) + logger.addHandler(ApacheLogHandler()) + + +_configure_logging() + +_LOGGER = logging.getLogger(__name__) + + +def _parse_option(name, value, definition): + if value is None: + return False + + meaning = definition.get(value.lower()) + if meaning is None: + raise Exception('Invalid value for PythonOption %s: %r' % + (name, value)) + return meaning + + +def _create_dispatcher(): + _LOGGER.info('Initializing Dispatcher') + + options = apache.main_server.get_options() + + handler_root = options.get(_PYOPT_HANDLER_ROOT, None) + if not handler_root: + raise Exception('PythonOption %s is not defined' % _PYOPT_HANDLER_ROOT, + apache.APLOG_ERR) + + handler_scan = options.get(_PYOPT_HANDLER_SCAN, handler_root) + + allow_handlers_outside_root = _parse_option( + _PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT, + options.get(_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT), + _PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT_DEFINITION) + + dispatcher = dispatch.Dispatcher( + handler_root, handler_scan, allow_handlers_outside_root) + + for warning in dispatcher.source_warnings(): + apache.log_error( + 'mod_pywebsocket: Warning in source loading: %s' % warning, + apache.APLOG_WARNING) + + return dispatcher + + +# Initialize +_dispatcher = _create_dispatcher() + + +def headerparserhandler(request): + """Handle request. + + Args: + request: mod_python request. + + This function is named headerparserhandler because it is the default + name for a PythonHeaderParserHandler. + """ + + handshake_is_done = False + try: + # Fallback to default http handler for request paths for which + # we don't have request handlers. + if not _dispatcher.get_handler_suite(request.uri): + request.log_error( + 'mod_pywebsocket: No handler for resource: %r' % request.uri, + apache.APLOG_INFO) + request.log_error( + 'mod_pywebsocket: Fallback to Apache', apache.APLOG_INFO) + return apache.DECLINED + except dispatch.DispatchException, e: + request.log_error( + 'mod_pywebsocket: Dispatch failed for error: %s' % e, + apache.APLOG_INFO) + if not handshake_is_done: + return e.status + + try: + allow_draft75 = _parse_option( + _PYOPT_ALLOW_DRAFT75, + apache.main_server.get_options().get(_PYOPT_ALLOW_DRAFT75), + _PYOPT_ALLOW_DRAFT75_DEFINITION) + + try: + handshake.do_handshake( + request, _dispatcher, allowDraft75=allow_draft75) + except handshake.VersionException, e: + request.log_error( + 'mod_pywebsocket: Handshake failed for version error: %s' % e, + apache.APLOG_INFO) + request.err_headers_out.add(common.SEC_WEBSOCKET_VERSION_HEADER, + e.supported_versions) + return apache.HTTP_BAD_REQUEST + except handshake.HandshakeException, e: + # Handshake for ws/wss failed. + # Send http response with error status. + request.log_error( + 'mod_pywebsocket: Handshake failed for error: %s' % e, + apache.APLOG_INFO) + return e.status + + handshake_is_done = True + request._dispatcher = _dispatcher + _dispatcher.transfer_data(request) + except handshake.AbortedByUserException, e: + request.log_error('mod_pywebsocket: Aborted: %s' % e, apache.APLOG_INFO) + except Exception, e: + # DispatchException can also be thrown if something is wrong in + # pywebsocket code. It's caught here, then. + + request.log_error('mod_pywebsocket: Exception occurred: %s\n%s' % + (e, util.get_stack_trace()), + apache.APLOG_ERR) + # Unknown exceptions before handshake mean Apache must handle its + # request with another handler. + if not handshake_is_done: + return apache.DECLINED + # Set assbackwards to suppress response header generation by Apache. + request.assbackwards = 1 + return apache.DONE # Return DONE such that no other handlers are invoked. + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/http_header_util.py b/testing/mochitest/pywebsocket/mod_pywebsocket/http_header_util.py new file mode 100644 index 000000000..b77465393 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/http_header_util.py @@ -0,0 +1,263 @@ +# 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. +""" + + +import urlparse + + +_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 = urlparse.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, e: + # port property cause ValueError on invalid null port description like + # 'ws://host:/path'. + 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 + + +try: + urlparse.uses_netloc.index('ws') +except ValueError, e: + # urlparse in Python2.5.1 doesn't have 'ws' and 'wss' entries. + urlparse.uses_netloc.append('ws') + urlparse.uses_netloc.append('wss') + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/memorizingfile.py b/testing/mochitest/pywebsocket/mod_pywebsocket/memorizingfile.py new file mode 100644 index 000000000..4d4cd9585 --- /dev/null +++ b/testing/mochitest/pywebsocket/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. +""" + + +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.maxint): + """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): + 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/pywebsocket/mod_pywebsocket/msgutil.py b/testing/mochitest/pywebsocket/mod_pywebsocket/msgutil.py new file mode 100644 index 000000000..4c1a0114b --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/msgutil.py @@ -0,0 +1,219 @@ +# 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. +""" + + +import Queue +import threading + + +# Export Exception symbols from msgutil for backward compatibility +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import InvalidFrameException +from mod_pywebsocket._stream_base import BadOperationException +from mod_pywebsocket._stream_base 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 = 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 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 = 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/pywebsocket/mod_pywebsocket/mux.py b/testing/mochitest/pywebsocket/mod_pywebsocket/mux.py new file mode 100644 index 000000000..76334685b --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/mux.py @@ -0,0 +1,1889 @@ +# 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 classes and helper functions for multiplexing extension. + +Specification: +http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-06 +""" + + +import collections +import copy +import email +import email.parser +import logging +import math +import struct +import threading +import traceback + +from mod_pywebsocket import common +from mod_pywebsocket import handshake +from mod_pywebsocket import util +from mod_pywebsocket._stream_base import BadOperationException +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import InvalidFrameException +from mod_pywebsocket._stream_hybi import Frame +from mod_pywebsocket._stream_hybi import Stream +from mod_pywebsocket._stream_hybi import StreamOptions +from mod_pywebsocket._stream_hybi import create_binary_frame +from mod_pywebsocket._stream_hybi import create_closing_handshake_body +from mod_pywebsocket._stream_hybi import create_header +from mod_pywebsocket._stream_hybi import create_length_header +from mod_pywebsocket._stream_hybi import parse_frame +from mod_pywebsocket.handshake import hybi + + +_CONTROL_CHANNEL_ID = 0 +_DEFAULT_CHANNEL_ID = 1 + +_MUX_OPCODE_ADD_CHANNEL_REQUEST = 0 +_MUX_OPCODE_ADD_CHANNEL_RESPONSE = 1 +_MUX_OPCODE_FLOW_CONTROL = 2 +_MUX_OPCODE_DROP_CHANNEL = 3 +_MUX_OPCODE_NEW_CHANNEL_SLOT = 4 + +_MAX_CHANNEL_ID = 2 ** 29 - 1 + +_INITIAL_NUMBER_OF_CHANNEL_SLOTS = 64 +_INITIAL_QUOTA_FOR_CLIENT = 8 * 1024 + +_HANDSHAKE_ENCODING_IDENTITY = 0 +_HANDSHAKE_ENCODING_DELTA = 1 + +# We need only these status code for now. +_HTTP_BAD_RESPONSE_MESSAGES = { + common.HTTP_STATUS_BAD_REQUEST: 'Bad Request', +} + +# DropChannel reason code +# TODO(bashi): Define all reason code defined in -05 draft. +_DROP_CODE_NORMAL_CLOSURE = 1000 + +_DROP_CODE_INVALID_ENCAPSULATING_MESSAGE = 2001 +_DROP_CODE_CHANNEL_ID_TRUNCATED = 2002 +_DROP_CODE_ENCAPSULATED_FRAME_IS_TRUNCATED = 2003 +_DROP_CODE_UNKNOWN_MUX_OPCODE = 2004 +_DROP_CODE_INVALID_MUX_CONTROL_BLOCK = 2005 +_DROP_CODE_CHANNEL_ALREADY_EXISTS = 2006 +_DROP_CODE_NEW_CHANNEL_SLOT_VIOLATION = 2007 +_DROP_CODE_UNKNOWN_REQUEST_ENCODING = 2010 + +_DROP_CODE_SEND_QUOTA_VIOLATION = 3005 +_DROP_CODE_SEND_QUOTA_OVERFLOW = 3006 +_DROP_CODE_ACKNOWLEDGED = 3008 +_DROP_CODE_BAD_FRAGMENTATION = 3009 + + +class MuxUnexpectedException(Exception): + """Exception in handling multiplexing extension.""" + pass + + +# Temporary +class MuxNotImplementedException(Exception): + """Raised when a flow enters unimplemented code path.""" + pass + + +class LogicalConnectionClosedException(Exception): + """Raised when logical connection is gracefully closed.""" + pass + + +class PhysicalConnectionError(Exception): + """Raised when there is a physical connection error.""" + def __init__(self, drop_code, message=''): + super(PhysicalConnectionError, self).__init__( + 'code=%d, message=%r' % (drop_code, message)) + self.drop_code = drop_code + self.message = message + + +class LogicalChannelError(Exception): + """Raised when there is a logical channel error.""" + def __init__(self, channel_id, drop_code, message=''): + super(LogicalChannelError, self).__init__( + 'channel_id=%d, code=%d, message=%r' % ( + channel_id, drop_code, message)) + self.channel_id = channel_id + self.drop_code = drop_code + self.message = message + + +def _encode_channel_id(channel_id): + if channel_id < 0: + raise ValueError('Channel id %d must not be negative' % channel_id) + + if channel_id < 2 ** 7: + return chr(channel_id) + if channel_id < 2 ** 14: + return struct.pack('!H', 0x8000 + channel_id) + if channel_id < 2 ** 21: + first = chr(0xc0 + (channel_id >> 16)) + return first + struct.pack('!H', channel_id & 0xffff) + if channel_id < 2 ** 29: + return struct.pack('!L', 0xe0000000 + channel_id) + + raise ValueError('Channel id %d is too large' % channel_id) + + +def _encode_number(number): + return create_length_header(number, False) + + +def _create_add_channel_response(channel_id, encoded_handshake, + encoding=0, rejected=False): + if encoding != 0 and encoding != 1: + raise ValueError('Invalid encoding %d' % encoding) + + first_byte = ((_MUX_OPCODE_ADD_CHANNEL_RESPONSE << 5) | + (rejected << 4) | encoding) + block = (chr(first_byte) + + _encode_channel_id(channel_id) + + _encode_number(len(encoded_handshake)) + + encoded_handshake) + return block + + +def _create_drop_channel(channel_id, code=None, message=''): + if len(message) > 0 and code is None: + raise ValueError('Code must be specified if message is specified') + + first_byte = _MUX_OPCODE_DROP_CHANNEL << 5 + block = chr(first_byte) + _encode_channel_id(channel_id) + if code is None: + block += _encode_number(0) # Reason size + else: + reason = struct.pack('!H', code) + message + reason_size = _encode_number(len(reason)) + block += reason_size + reason + + return block + + +def _create_flow_control(channel_id, replenished_quota): + first_byte = _MUX_OPCODE_FLOW_CONTROL << 5 + block = (chr(first_byte) + + _encode_channel_id(channel_id) + + _encode_number(replenished_quota)) + return block + + +def _create_new_channel_slot(slots, send_quota): + if slots < 0 or send_quota < 0: + raise ValueError('slots and send_quota must be non-negative.') + first_byte = _MUX_OPCODE_NEW_CHANNEL_SLOT << 5 + block = (chr(first_byte) + + _encode_number(slots) + + _encode_number(send_quota)) + return block + + +def _create_fallback_new_channel_slot(): + first_byte = (_MUX_OPCODE_NEW_CHANNEL_SLOT << 5) | 1 # Set the F flag + block = (chr(first_byte) + _encode_number(0) + _encode_number(0)) + return block + + +def _parse_request_text(request_text): + request_line, header_lines = request_text.split('\r\n', 1) + + words = request_line.split(' ') + if len(words) != 3: + raise ValueError('Bad Request-Line syntax %r' % request_line) + [command, path, version] = words + if version != 'HTTP/1.1': + raise ValueError('Bad request version %r' % version) + + # email.parser.Parser() parses RFC 2822 (RFC 822) style headers. + # RFC 6455 refers RFC 2616 for handshake parsing, and RFC 2616 refers + # RFC 822. + headers = email.parser.Parser().parsestr(header_lines) + return command, path, version, headers + + +class _ControlBlock(object): + """A structure that holds parsing result of multiplexing control block. + Control block specific attributes will be added by _MuxFramePayloadParser. + (e.g. encoded_handshake will be added for AddChannelRequest and + AddChannelResponse) + """ + + def __init__(self, opcode): + self.opcode = opcode + + +class _MuxFramePayloadParser(object): + """A class that parses multiplexed frame payload.""" + + def __init__(self, payload): + self._data = payload + self._read_position = 0 + self._logger = util.get_class_logger(self) + + def read_channel_id(self): + """Reads channel id. + + Raises: + ValueError: when the payload doesn't contain + valid channel id. + """ + + remaining_length = len(self._data) - self._read_position + pos = self._read_position + if remaining_length == 0: + raise ValueError('Invalid channel id format') + + channel_id = ord(self._data[pos]) + channel_id_length = 1 + if channel_id & 0xe0 == 0xe0: + if remaining_length < 4: + raise ValueError('Invalid channel id format') + channel_id = struct.unpack('!L', + self._data[pos:pos+4])[0] & 0x1fffffff + channel_id_length = 4 + elif channel_id & 0xc0 == 0xc0: + if remaining_length < 3: + raise ValueError('Invalid channel id format') + channel_id = (((channel_id & 0x1f) << 16) + + struct.unpack('!H', self._data[pos+1:pos+3])[0]) + channel_id_length = 3 + elif channel_id & 0x80 == 0x80: + if remaining_length < 2: + raise ValueError('Invalid channel id format') + channel_id = struct.unpack('!H', + self._data[pos:pos+2])[0] & 0x3fff + channel_id_length = 2 + self._read_position += channel_id_length + + return channel_id + + def read_inner_frame(self): + """Reads an inner frame. + + Raises: + PhysicalConnectionError: when the inner frame is invalid. + """ + + if len(self._data) == self._read_position: + raise PhysicalConnectionError( + _DROP_CODE_ENCAPSULATED_FRAME_IS_TRUNCATED) + + bits = ord(self._data[self._read_position]) + self._read_position += 1 + fin = (bits & 0x80) == 0x80 + rsv1 = (bits & 0x40) == 0x40 + rsv2 = (bits & 0x20) == 0x20 + rsv3 = (bits & 0x10) == 0x10 + opcode = bits & 0xf + payload = self.remaining_data() + # Consume rest of the message which is payload data of the original + # frame. + self._read_position = len(self._data) + return fin, rsv1, rsv2, rsv3, opcode, payload + + def _read_number(self): + if self._read_position + 1 > len(self._data): + raise ValueError( + 'Cannot read the first byte of number field') + + number = ord(self._data[self._read_position]) + if number & 0x80 == 0x80: + raise ValueError( + 'The most significant bit of the first byte of number should ' + 'be unset') + self._read_position += 1 + pos = self._read_position + if number == 127: + if pos + 8 > len(self._data): + raise ValueError('Invalid number field') + self._read_position += 8 + number = struct.unpack('!Q', self._data[pos:pos+8])[0] + if number > 0x7FFFFFFFFFFFFFFF: + raise ValueError('Encoded number(%d) >= 2^63' % number) + if number <= 0xFFFF: + raise ValueError( + '%d should not be encoded by 9 bytes encoding' % number) + return number + if number == 126: + if pos + 2 > len(self._data): + raise ValueError('Invalid number field') + self._read_position += 2 + number = struct.unpack('!H', self._data[pos:pos+2])[0] + if number <= 125: + raise ValueError( + '%d should not be encoded by 3 bytes encoding' % number) + return number + + def _read_size_and_contents(self): + """Reads data that consists of followings: + - the size of the contents encoded the same way as payload length + of the WebSocket Protocol with 1 bit padding at the head. + - the contents. + """ + + try: + size = self._read_number() + except ValueError, e: + raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + str(e)) + pos = self._read_position + if pos + size > len(self._data): + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Cannot read %d bytes data' % size) + + self._read_position += size + return self._data[pos:pos+size] + + def _read_add_channel_request(self, first_byte, control_block): + reserved = (first_byte >> 2) & 0x7 + if reserved != 0: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Reserved bits must be unset') + + # Invalid encoding will be handled by MuxHandler. + encoding = first_byte & 0x3 + try: + control_block.channel_id = self.read_channel_id() + except ValueError, e: + raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK) + control_block.encoding = encoding + encoded_handshake = self._read_size_and_contents() + control_block.encoded_handshake = encoded_handshake + return control_block + + def _read_add_channel_response(self, first_byte, control_block): + reserved = (first_byte >> 2) & 0x3 + if reserved != 0: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Reserved bits must be unset') + + control_block.accepted = (first_byte >> 4) & 1 + control_block.encoding = first_byte & 0x3 + try: + control_block.channel_id = self.read_channel_id() + except ValueError, e: + raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK) + control_block.encoded_handshake = self._read_size_and_contents() + return control_block + + def _read_flow_control(self, first_byte, control_block): + reserved = first_byte & 0x1f + if reserved != 0: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Reserved bits must be unset') + + try: + control_block.channel_id = self.read_channel_id() + control_block.send_quota = self._read_number() + except ValueError, e: + raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + str(e)) + + return control_block + + def _read_drop_channel(self, first_byte, control_block): + reserved = first_byte & 0x1f + if reserved != 0: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Reserved bits must be unset') + + try: + control_block.channel_id = self.read_channel_id() + except ValueError, e: + raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK) + reason = self._read_size_and_contents() + if len(reason) == 0: + control_block.drop_code = None + control_block.drop_message = '' + elif len(reason) >= 2: + control_block.drop_code = struct.unpack('!H', reason[:2])[0] + control_block.drop_message = reason[2:] + else: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Received DropChannel that conains only 1-byte reason') + return control_block + + def _read_new_channel_slot(self, first_byte, control_block): + reserved = first_byte & 0x1e + if reserved != 0: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Reserved bits must be unset') + control_block.fallback = first_byte & 1 + try: + control_block.slots = self._read_number() + control_block.send_quota = self._read_number() + except ValueError, e: + raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + str(e)) + return control_block + + def read_control_blocks(self): + """Reads control block(s). + + Raises: + PhysicalConnectionError: when the payload contains invalid control + block(s). + StopIteration: when no control blocks left. + """ + + while self._read_position < len(self._data): + first_byte = ord(self._data[self._read_position]) + self._read_position += 1 + opcode = (first_byte >> 5) & 0x7 + control_block = _ControlBlock(opcode=opcode) + if opcode == _MUX_OPCODE_ADD_CHANNEL_REQUEST: + yield self._read_add_channel_request(first_byte, control_block) + elif opcode == _MUX_OPCODE_ADD_CHANNEL_RESPONSE: + yield self._read_add_channel_response( + first_byte, control_block) + elif opcode == _MUX_OPCODE_FLOW_CONTROL: + yield self._read_flow_control(first_byte, control_block) + elif opcode == _MUX_OPCODE_DROP_CHANNEL: + yield self._read_drop_channel(first_byte, control_block) + elif opcode == _MUX_OPCODE_NEW_CHANNEL_SLOT: + yield self._read_new_channel_slot(first_byte, control_block) + else: + raise PhysicalConnectionError( + _DROP_CODE_UNKNOWN_MUX_OPCODE, + 'Invalid opcode %d' % opcode) + + assert self._read_position == len(self._data) + raise StopIteration + + def remaining_data(self): + """Returns remaining data.""" + + return self._data[self._read_position:] + + +class _LogicalRequest(object): + """Mimics mod_python request.""" + + def __init__(self, channel_id, command, path, protocol, headers, + connection): + """Constructs an instance. + + Args: + channel_id: the channel id of the logical channel. + command: HTTP request command. + path: HTTP request path. + headers: HTTP headers. + connection: _LogicalConnection instance. + """ + + self.channel_id = channel_id + self.method = command + self.uri = path + self.protocol = protocol + self.headers_in = headers + self.connection = connection + self.server_terminated = False + self.client_terminated = False + + def is_https(self): + """Mimics request.is_https(). Returns False because this method is + used only by old protocols (hixie and hybi00). + """ + + return False + + +class _LogicalConnection(object): + """Mimics mod_python mp_conn.""" + + # For details, see the comment of set_read_state(). + STATE_ACTIVE = 1 + STATE_GRACEFULLY_CLOSED = 2 + STATE_TERMINATED = 3 + + def __init__(self, mux_handler, channel_id): + """Constructs an instance. + + Args: + mux_handler: _MuxHandler instance. + channel_id: channel id of this connection. + """ + + self._mux_handler = mux_handler + self._channel_id = channel_id + self._incoming_data = '' + + # - Protects _waiting_write_completion + # - Signals the thread waiting for completion of write by mux handler + self._write_condition = threading.Condition() + self._waiting_write_completion = False + + self._read_condition = threading.Condition() + self._read_state = self.STATE_ACTIVE + + def get_local_addr(self): + """Getter to mimic mp_conn.local_addr.""" + + return self._mux_handler.physical_connection.get_local_addr() + local_addr = property(get_local_addr) + + def get_remote_addr(self): + """Getter to mimic mp_conn.remote_addr.""" + + return self._mux_handler.physical_connection.get_remote_addr() + remote_addr = property(get_remote_addr) + + def get_memorized_lines(self): + """Gets memorized lines. Not supported.""" + + raise MuxUnexpectedException('_LogicalConnection does not support ' + 'get_memorized_lines') + + def write(self, data): + """Writes data. mux_handler sends data asynchronously. The caller will + be suspended until write done. + + Args: + data: data to be written. + + Raises: + MuxUnexpectedException: when called before finishing the previous + write. + """ + + try: + self._write_condition.acquire() + if self._waiting_write_completion: + raise MuxUnexpectedException( + 'Logical connection %d is already waiting the completion ' + 'of write' % self._channel_id) + + self._waiting_write_completion = True + self._mux_handler.send_data(self._channel_id, data) + self._write_condition.wait() + # TODO(tyoshino): Raise an exception if woke up by on_writer_done. + finally: + self._write_condition.release() + + def write_control_data(self, data): + """Writes data via the control channel. Don't wait finishing write + because this method can be called by mux dispatcher. + + Args: + data: data to be written. + """ + + self._mux_handler.send_control_data(data) + + def on_write_data_done(self): + """Called when sending data is completed.""" + + try: + self._write_condition.acquire() + if not self._waiting_write_completion: + raise MuxUnexpectedException( + 'Invalid call of on_write_data_done for logical ' + 'connection %d' % self._channel_id) + self._waiting_write_completion = False + self._write_condition.notify() + finally: + self._write_condition.release() + + def on_writer_done(self): + """Called by the mux handler when the writer thread has finished.""" + + try: + self._write_condition.acquire() + self._waiting_write_completion = False + self._write_condition.notify() + finally: + self._write_condition.release() + + + def append_frame_data(self, frame_data): + """Appends incoming frame data. Called when mux_handler dispatches + frame data to the corresponding application. + + Args: + frame_data: incoming frame data. + """ + + self._read_condition.acquire() + self._incoming_data += frame_data + self._read_condition.notify() + self._read_condition.release() + + def read(self, length): + """Reads data. Blocks until enough data has arrived via physical + connection. + + Args: + length: length of data to be read. + Raises: + LogicalConnectionClosedException: when closing handshake for this + logical channel has been received. + ConnectionTerminatedException: when the physical connection has + closed, or an error is caused on the reader thread. + """ + + self._read_condition.acquire() + while (self._read_state == self.STATE_ACTIVE and + len(self._incoming_data) < length): + self._read_condition.wait() + + try: + if self._read_state == self.STATE_GRACEFULLY_CLOSED: + raise LogicalConnectionClosedException( + 'Logical channel %d has closed.' % self._channel_id) + elif self._read_state == self.STATE_TERMINATED: + raise ConnectionTerminatedException( + 'Receiving %d byte failed. Logical channel (%d) closed' % + (length, self._channel_id)) + + value = self._incoming_data[:length] + self._incoming_data = self._incoming_data[length:] + finally: + self._read_condition.release() + + return value + + def set_read_state(self, new_state): + """Sets the state of this connection. Called when an event for this + connection has occurred. + + Args: + new_state: state to be set. new_state must be one of followings: + - STATE_GRACEFULLY_CLOSED: when closing handshake for this + connection has been received. + - STATE_TERMINATED: when the physical connection has closed or + DropChannel of this connection has received. + """ + + self._read_condition.acquire() + self._read_state = new_state + self._read_condition.notify() + self._read_condition.release() + + +class _InnerMessage(object): + """Holds the result of _InnerMessageBuilder.build(). + """ + + def __init__(self, opcode, payload): + self.opcode = opcode + self.payload = payload + + +class _InnerMessageBuilder(object): + """A class that holds the context of inner message fragmentation and + builds a message from fragmented inner frame(s). + """ + + def __init__(self): + self._control_opcode = None + self._pending_control_fragments = [] + self._message_opcode = None + self._pending_message_fragments = [] + self._frame_handler = self._handle_first + + def _handle_first(self, frame): + if frame.opcode == common.OPCODE_CONTINUATION: + raise InvalidFrameException('Sending invalid continuation opcode') + + if common.is_control_opcode(frame.opcode): + return self._process_first_fragmented_control(frame) + else: + return self._process_first_fragmented_message(frame) + + def _process_first_fragmented_control(self, frame): + self._control_opcode = frame.opcode + self._pending_control_fragments.append(frame.payload) + if not frame.fin: + self._frame_handler = self._handle_fragmented_control + return None + return self._reassemble_fragmented_control() + + def _process_first_fragmented_message(self, frame): + self._message_opcode = frame.opcode + self._pending_message_fragments.append(frame.payload) + if not frame.fin: + self._frame_handler = self._handle_fragmented_message + return None + return self._reassemble_fragmented_message() + + def _handle_fragmented_control(self, frame): + if frame.opcode != common.OPCODE_CONTINUATION: + raise InvalidFrameException( + 'Sending invalid opcode %d while sending fragmented control ' + 'message' % frame.opcode) + self._pending_control_fragments.append(frame.payload) + if not frame.fin: + return None + return self._reassemble_fragmented_control() + + def _reassemble_fragmented_control(self): + opcode = self._control_opcode + payload = ''.join(self._pending_control_fragments) + self._control_opcode = None + self._pending_control_fragments = [] + if self._message_opcode is not None: + self._frame_handler = self._handle_fragmented_message + else: + self._frame_handler = self._handle_first + return _InnerMessage(opcode, payload) + + def _handle_fragmented_message(self, frame): + # Sender can interleave a control message while sending fragmented + # messages. + if common.is_control_opcode(frame.opcode): + if self._control_opcode is not None: + raise MuxUnexpectedException( + 'Should not reach here(Bug in builder)') + return self._process_first_fragmented_control(frame) + + if frame.opcode != common.OPCODE_CONTINUATION: + raise InvalidFrameException( + 'Sending invalid opcode %d while sending fragmented message' % + frame.opcode) + self._pending_message_fragments.append(frame.payload) + if not frame.fin: + return None + return self._reassemble_fragmented_message() + + def _reassemble_fragmented_message(self): + opcode = self._message_opcode + payload = ''.join(self._pending_message_fragments) + self._message_opcode = None + self._pending_message_fragments = [] + self._frame_handler = self._handle_first + return _InnerMessage(opcode, payload) + + def build(self, frame): + """Build an inner message. Returns an _InnerMessage instance when + the given frame is the last fragmented frame. Returns None otherwise. + + Args: + frame: an inner frame. + Raises: + InvalidFrameException: when received invalid opcode. (e.g. + receiving non continuation data opcode but the fin flag of + the previous inner frame was not set.) + """ + + return self._frame_handler(frame) + + +class _LogicalStream(Stream): + """Mimics the Stream class. This class interprets multiplexed WebSocket + frames. + """ + + def __init__(self, request, stream_options, send_quota, receive_quota): + """Constructs an instance. + + Args: + request: _LogicalRequest instance. + stream_options: StreamOptions instance. + send_quota: Initial send quota. + receive_quota: Initial receive quota. + """ + + # Physical stream is responsible for masking. + stream_options.unmask_receive = False + Stream.__init__(self, request, stream_options) + + self._send_closed = False + self._send_quota = send_quota + # - Protects _send_closed and _send_quota + # - Signals the thread waiting for send quota replenished + self._send_condition = threading.Condition() + + # The opcode of the first frame in messages. + self._message_opcode = common.OPCODE_TEXT + # True when the last message was fragmented. + self._last_message_was_fragmented = False + + self._receive_quota = receive_quota + self._write_inner_frame_semaphore = threading.Semaphore() + + self._inner_message_builder = _InnerMessageBuilder() + + def _create_inner_frame(self, opcode, payload, end=True): + frame = Frame(fin=end, opcode=opcode, payload=payload) + for frame_filter in self._options.outgoing_frame_filters: + frame_filter.filter(frame) + + if len(payload) != len(frame.payload): + raise MuxUnexpectedException( + 'Mux extension must not be used after extensions which change ' + ' frame boundary') + + first_byte = ((frame.fin << 7) | (frame.rsv1 << 6) | + (frame.rsv2 << 5) | (frame.rsv3 << 4) | frame.opcode) + return chr(first_byte) + frame.payload + + def _write_inner_frame(self, opcode, payload, end=True): + payload_length = len(payload) + write_position = 0 + + try: + # An inner frame will be fragmented if there is no enough send + # quota. This semaphore ensures that fragmented inner frames are + # sent in order on the logical channel. + # Note that frames that come from other logical channels or + # multiplexing control blocks can be inserted between fragmented + # inner frames on the physical channel. + self._write_inner_frame_semaphore.acquire() + + # Consume an octet quota when this is the first fragmented frame. + if opcode != common.OPCODE_CONTINUATION: + try: + self._send_condition.acquire() + while (not self._send_closed) and self._send_quota == 0: + self._send_condition.wait() + + if self._send_closed: + raise BadOperationException( + 'Logical connection %d is closed' % + self._request.channel_id) + + self._send_quota -= 1 + finally: + self._send_condition.release() + + while write_position < payload_length: + try: + self._send_condition.acquire() + while (not self._send_closed) and self._send_quota == 0: + self._logger.debug( + 'No quota. Waiting FlowControl message for %d.' % + self._request.channel_id) + self._send_condition.wait() + + if self._send_closed: + raise BadOperationException( + 'Logical connection %d is closed' % + self.request._channel_id) + + remaining = payload_length - write_position + write_length = min(self._send_quota, remaining) + inner_frame_end = ( + end and + (write_position + write_length == payload_length)) + + inner_frame = self._create_inner_frame( + opcode, + payload[write_position:write_position+write_length], + inner_frame_end) + self._send_quota -= write_length + self._logger.debug('Consumed quota=%d, remaining=%d' % + (write_length, self._send_quota)) + finally: + self._send_condition.release() + + # Writing data will block the worker so we need to release + # _send_condition before writing. + self._logger.debug('Sending inner frame: %r' % inner_frame) + self._request.connection.write(inner_frame) + write_position += write_length + + opcode = common.OPCODE_CONTINUATION + + except ValueError, e: + raise BadOperationException(e) + finally: + self._write_inner_frame_semaphore.release() + + def replenish_send_quota(self, send_quota): + """Replenish send quota.""" + + try: + self._send_condition.acquire() + if self._send_quota + send_quota > 0x7FFFFFFFFFFFFFFF: + self._send_quota = 0 + raise LogicalChannelError( + self._request.channel_id, _DROP_CODE_SEND_QUOTA_OVERFLOW) + self._send_quota += send_quota + self._logger.debug('Replenished send quota for channel id %d: %d' % + (self._request.channel_id, self._send_quota)) + finally: + self._send_condition.notify() + self._send_condition.release() + + def consume_receive_quota(self, amount): + """Consumes receive quota. Returns False on failure.""" + + if self._receive_quota < amount: + self._logger.debug('Violate quota on channel id %d: %d < %d' % + (self._request.channel_id, + self._receive_quota, amount)) + return False + self._receive_quota -= amount + return True + + def send_message(self, message, end=True, binary=False): + """Override Stream.send_message.""" + + if self._request.server_terminated: + raise BadOperationException( + 'Requested send_message after sending out a closing handshake') + + if binary and isinstance(message, unicode): + raise BadOperationException( + 'Message for binary frame must be instance of str') + + if binary: + opcode = common.OPCODE_BINARY + else: + opcode = common.OPCODE_TEXT + message = message.encode('utf-8') + + for message_filter in self._options.outgoing_message_filters: + message = message_filter.filter(message, end, binary) + + if self._last_message_was_fragmented: + if opcode != self._message_opcode: + raise BadOperationException('Message types are different in ' + 'frames for the same message') + opcode = common.OPCODE_CONTINUATION + else: + self._message_opcode = opcode + + self._write_inner_frame(opcode, message, end) + self._last_message_was_fragmented = not end + + def _receive_frame(self): + """Overrides Stream._receive_frame. + + In addition to call Stream._receive_frame, this method adds the amount + of payload to receiving quota and sends FlowControl to the client. + We need to do it here because Stream.receive_message() handles + control frames internally. + """ + + opcode, payload, fin, rsv1, rsv2, rsv3 = Stream._receive_frame(self) + amount = len(payload) + # Replenish extra one octet when receiving the first fragmented frame. + if opcode != common.OPCODE_CONTINUATION: + amount += 1 + self._receive_quota += amount + frame_data = _create_flow_control(self._request.channel_id, + amount) + self._logger.debug('Sending flow control for %d, replenished=%d' % + (self._request.channel_id, amount)) + self._request.connection.write_control_data(frame_data) + return opcode, payload, fin, rsv1, rsv2, rsv3 + + def _get_message_from_frame(self, frame): + """Overrides Stream._get_message_from_frame. + """ + + try: + inner_message = self._inner_message_builder.build(frame) + except InvalidFrameException: + raise LogicalChannelError( + self._request.channel_id, _DROP_CODE_BAD_FRAGMENTATION) + + if inner_message is None: + return None + self._original_opcode = inner_message.opcode + return inner_message.payload + + def receive_message(self): + """Overrides Stream.receive_message.""" + + # Just call Stream.receive_message(), but catch + # LogicalConnectionClosedException, which is raised when the logical + # connection has closed gracefully. + try: + return Stream.receive_message(self) + except LogicalConnectionClosedException, e: + self._logger.debug('%s', e) + return None + + def _send_closing_handshake(self, code, reason): + """Overrides Stream._send_closing_handshake.""" + + body = create_closing_handshake_body(code, reason) + self._logger.debug('Sending closing handshake for %d: (%r, %r)' % + (self._request.channel_id, code, reason)) + self._write_inner_frame(common.OPCODE_CLOSE, body, end=True) + + self._request.server_terminated = True + + def send_ping(self, body=''): + """Overrides Stream.send_ping""" + + self._logger.debug('Sending ping on logical channel %d: %r' % + (self._request.channel_id, body)) + self._write_inner_frame(common.OPCODE_PING, body, end=True) + + self._ping_queue.append(body) + + def _send_pong(self, body): + """Overrides Stream._send_pong""" + + self._logger.debug('Sending pong on logical channel %d: %r' % + (self._request.channel_id, body)) + self._write_inner_frame(common.OPCODE_PONG, body, end=True) + + def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason=''): + """Overrides Stream.close_connection.""" + + # TODO(bashi): Implement + self._logger.debug('Closing logical connection %d' % + self._request.channel_id) + self._request.server_terminated = True + + def stop_sending(self): + """Stops accepting new send operation (_write_inner_frame).""" + + self._send_condition.acquire() + self._send_closed = True + self._send_condition.notify() + self._send_condition.release() + + +class _OutgoingData(object): + """A structure that holds data to be sent via physical connection and + origin of the data. + """ + + def __init__(self, channel_id, data): + self.channel_id = channel_id + self.data = data + + +class _PhysicalConnectionWriter(threading.Thread): + """A thread that is responsible for writing data to physical connection. + + TODO(bashi): Make sure there is no thread-safety problem when the reader + thread reads data from the same socket at a time. + """ + + def __init__(self, mux_handler): + """Constructs an instance. + + Args: + mux_handler: _MuxHandler instance. + """ + + threading.Thread.__init__(self) + self._logger = util.get_class_logger(self) + self._mux_handler = mux_handler + self.setDaemon(True) + + # When set, make this thread stop accepting new data, flush pending + # data and exit. + self._stop_requested = False + # The close code of the physical connection. + self._close_code = common.STATUS_NORMAL_CLOSURE + # Deque for passing write data. It's protected by _deque_condition + # until _stop_requested is set. + self._deque = collections.deque() + # - Protects _deque, _stop_requested and _close_code + # - Signals threads waiting for them to be available + self._deque_condition = threading.Condition() + + def put_outgoing_data(self, data): + """Puts outgoing data. + + Args: + data: _OutgoingData instance. + + Raises: + BadOperationException: when the thread has been requested to + terminate. + """ + + try: + self._deque_condition.acquire() + if self._stop_requested: + raise BadOperationException('Cannot write data anymore') + + self._deque.append(data) + self._deque_condition.notify() + finally: + self._deque_condition.release() + + def _write_data(self, outgoing_data): + message = (_encode_channel_id(outgoing_data.channel_id) + + outgoing_data.data) + try: + self._mux_handler.physical_stream.send_message( + message=message, end=True, binary=True) + except Exception, e: + util.prepend_message_to_exception( + 'Failed to send message to %r: ' % + (self._mux_handler.physical_connection.remote_addr,), e) + raise + + # TODO(bashi): It would be better to block the thread that sends + # control data as well. + if outgoing_data.channel_id != _CONTROL_CHANNEL_ID: + self._mux_handler.notify_write_data_done(outgoing_data.channel_id) + + def run(self): + try: + self._deque_condition.acquire() + while not self._stop_requested: + if len(self._deque) == 0: + self._deque_condition.wait() + continue + + outgoing_data = self._deque.popleft() + + self._deque_condition.release() + self._write_data(outgoing_data) + self._deque_condition.acquire() + + # Flush deque. + # + # At this point, self._deque_condition is always acquired. + try: + while len(self._deque) > 0: + outgoing_data = self._deque.popleft() + self._write_data(outgoing_data) + finally: + self._deque_condition.release() + + # Close physical connection. + try: + # Don't wait the response here. The response will be read + # by the reader thread. + self._mux_handler.physical_stream.close_connection( + self._close_code, wait_response=False) + except Exception, e: + util.prepend_message_to_exception( + 'Failed to close the physical connection: %r' % e) + raise + finally: + self._mux_handler.notify_writer_done() + + def stop(self, close_code=common.STATUS_NORMAL_CLOSURE): + """Stops the writer thread.""" + + self._deque_condition.acquire() + self._stop_requested = True + self._close_code = close_code + self._deque_condition.notify() + self._deque_condition.release() + + +class _PhysicalConnectionReader(threading.Thread): + """A thread that is responsible for reading data from physical connection. + """ + + def __init__(self, mux_handler): + """Constructs an instance. + + Args: + mux_handler: _MuxHandler instance. + """ + + threading.Thread.__init__(self) + self._logger = util.get_class_logger(self) + self._mux_handler = mux_handler + self.setDaemon(True) + + def run(self): + while True: + try: + physical_stream = self._mux_handler.physical_stream + message = physical_stream.receive_message() + if message is None: + break + # Below happens only when a data message is received. + opcode = physical_stream.get_last_received_opcode() + if opcode != common.OPCODE_BINARY: + self._mux_handler.fail_physical_connection( + _DROP_CODE_INVALID_ENCAPSULATING_MESSAGE, + 'Received a text message on physical connection') + break + + except ConnectionTerminatedException, e: + self._logger.debug('%s', e) + break + + try: + self._mux_handler.dispatch_message(message) + except PhysicalConnectionError, e: + self._mux_handler.fail_physical_connection( + e.drop_code, e.message) + break + except LogicalChannelError, e: + self._mux_handler.fail_logical_channel( + e.channel_id, e.drop_code, e.message) + except Exception, e: + self._logger.debug(traceback.format_exc()) + break + + self._mux_handler.notify_reader_done() + + +class _Worker(threading.Thread): + """A thread that is responsible for running the corresponding application + handler. + """ + + def __init__(self, mux_handler, request): + """Constructs an instance. + + Args: + mux_handler: _MuxHandler instance. + request: _LogicalRequest instance. + """ + + threading.Thread.__init__(self) + self._logger = util.get_class_logger(self) + self._mux_handler = mux_handler + self._request = request + self.setDaemon(True) + + def run(self): + self._logger.debug('Logical channel worker started. (id=%d)' % + self._request.channel_id) + try: + # Non-critical exceptions will be handled by dispatcher. + self._mux_handler.dispatcher.transfer_data(self._request) + except LogicalChannelError, e: + self._mux_handler.fail_logical_channel( + e.channel_id, e.drop_code, e.message) + finally: + self._mux_handler.notify_worker_done(self._request.channel_id) + + +class _MuxHandshaker(hybi.Handshaker): + """Opening handshake processor for multiplexing.""" + + _DUMMY_WEBSOCKET_KEY = 'dGhlIHNhbXBsZSBub25jZQ==' + + def __init__(self, request, dispatcher, send_quota, receive_quota): + """Constructs an instance. + Args: + request: _LogicalRequest instance. + dispatcher: Dispatcher instance (dispatch.Dispatcher). + send_quota: Initial send quota. + receive_quota: Initial receive quota. + """ + + hybi.Handshaker.__init__(self, request, dispatcher) + self._send_quota = send_quota + self._receive_quota = receive_quota + + # Append headers which should not be included in handshake field of + # AddChannelRequest. + # TODO(bashi): Make sure whether we should raise exception when + # these headers are included already. + request.headers_in[common.UPGRADE_HEADER] = ( + common.WEBSOCKET_UPGRADE_TYPE) + request.headers_in[common.SEC_WEBSOCKET_VERSION_HEADER] = ( + str(common.VERSION_HYBI_LATEST)) + request.headers_in[common.SEC_WEBSOCKET_KEY_HEADER] = ( + self._DUMMY_WEBSOCKET_KEY) + + def _create_stream(self, stream_options): + """Override hybi.Handshaker._create_stream.""" + + self._logger.debug('Creating logical stream for %d' % + self._request.channel_id) + return _LogicalStream( + self._request, stream_options, self._send_quota, + self._receive_quota) + + def _create_handshake_response(self, accept): + """Override hybi._create_handshake_response.""" + + response = [] + + response.append('HTTP/1.1 101 Switching Protocols\r\n') + + # Upgrade and Sec-WebSocket-Accept should be excluded. + response.append('%s: %s\r\n' % ( + common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)) + if self._request.ws_protocol is not None: + response.append('%s: %s\r\n' % ( + 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('%s: %s\r\n' % ( + common.SEC_WEBSOCKET_EXTENSIONS_HEADER, + common.format_extensions(self._request.ws_extensions))) + response.append('\r\n') + + return ''.join(response) + + def _send_handshake(self, accept): + """Override hybi.Handshaker._send_handshake.""" + + # Don't send handshake response for the default channel + if self._request.channel_id == _DEFAULT_CHANNEL_ID: + return + + handshake_response = self._create_handshake_response(accept) + frame_data = _create_add_channel_response( + self._request.channel_id, + handshake_response) + self._logger.debug('Sending handshake response for %d: %r' % + (self._request.channel_id, frame_data)) + self._request.connection.write_control_data(frame_data) + + +class _LogicalChannelData(object): + """A structure that holds information about logical channel. + """ + + def __init__(self, request, worker): + self.request = request + self.worker = worker + self.drop_code = _DROP_CODE_NORMAL_CLOSURE + self.drop_message = '' + + +class _HandshakeDeltaBase(object): + """A class that holds information for delta-encoded handshake.""" + + def __init__(self, headers): + self._headers = headers + + def create_headers(self, delta=None): + """Creates request headers for an AddChannelRequest that has + delta-encoded handshake. + + Args: + delta: headers should be overridden. + """ + + headers = copy.copy(self._headers) + if delta: + for key, value in delta.items(): + # The spec requires that a header with an empty value is + # removed from the delta base. + if len(value) == 0 and headers.has_key(key): + del headers[key] + else: + headers[key] = value + return headers + + +class _MuxHandler(object): + """Multiplexing handler. When a handler starts, it launches three + threads; the reader thread, the writer thread, and a worker thread. + + The reader thread reads data from the physical stream, i.e., the + ws_stream object of the underlying websocket connection. The reader + thread interprets multiplexed frames and dispatches them to logical + channels. Methods of this class are mostly called by the reader thread. + + The writer thread sends multiplexed frames which are created by + logical channels via the physical connection. + + The worker thread launched at the starting point handles the + "Implicitly Opened Connection". If multiplexing handler receives + an AddChannelRequest and accepts it, the handler will launch a new worker + thread and dispatch the request to it. + """ + + def __init__(self, request, dispatcher): + """Constructs an instance. + + Args: + request: mod_python request of the physical connection. + dispatcher: Dispatcher instance (dispatch.Dispatcher). + """ + + self.original_request = request + self.dispatcher = dispatcher + self.physical_connection = request.connection + self.physical_stream = request.ws_stream + self._logger = util.get_class_logger(self) + self._logical_channels = {} + self._logical_channels_condition = threading.Condition() + # Holds client's initial quota + self._channel_slots = collections.deque() + self._handshake_base = None + self._worker_done_notify_received = False + self._reader = None + self._writer = None + + def start(self): + """Starts the handler. + + Raises: + MuxUnexpectedException: when the handler already started, or when + opening handshake of the default channel fails. + """ + + if self._reader or self._writer: + raise MuxUnexpectedException('MuxHandler already started') + + self._reader = _PhysicalConnectionReader(self) + self._writer = _PhysicalConnectionWriter(self) + self._reader.start() + self._writer.start() + + # Create "Implicitly Opened Connection". + logical_connection = _LogicalConnection(self, _DEFAULT_CHANNEL_ID) + headers = copy.copy(self.original_request.headers_in) + # Add extensions for logical channel. + headers[common.SEC_WEBSOCKET_EXTENSIONS_HEADER] = ( + common.format_extensions( + self.original_request.mux_processor.extensions())) + self._handshake_base = _HandshakeDeltaBase(headers) + logical_request = _LogicalRequest( + _DEFAULT_CHANNEL_ID, + self.original_request.method, + self.original_request.uri, + self.original_request.protocol, + self._handshake_base.create_headers(), + logical_connection) + # Client's send quota for the implicitly opened connection is zero, + # but we will send FlowControl later so set the initial quota to + # _INITIAL_QUOTA_FOR_CLIENT. + self._channel_slots.append(_INITIAL_QUOTA_FOR_CLIENT) + send_quota = self.original_request.mux_processor.quota() + if not self._do_handshake_for_logical_request( + logical_request, send_quota=send_quota): + raise MuxUnexpectedException( + 'Failed handshake on the default channel id') + self._add_logical_channel(logical_request) + + # Send FlowControl for the implicitly opened connection. + frame_data = _create_flow_control(_DEFAULT_CHANNEL_ID, + _INITIAL_QUOTA_FOR_CLIENT) + logical_request.connection.write_control_data(frame_data) + + def add_channel_slots(self, slots, send_quota): + """Adds channel slots. + + Args: + slots: number of slots to be added. + send_quota: initial send quota for slots. + """ + + self._channel_slots.extend([send_quota] * slots) + # Send NewChannelSlot to client. + frame_data = _create_new_channel_slot(slots, send_quota) + self.send_control_data(frame_data) + + def wait_until_done(self, timeout=None): + """Waits until all workers are done. Returns False when timeout has + occurred. Returns True on success. + + Args: + timeout: timeout in sec. + """ + + self._logical_channels_condition.acquire() + try: + while len(self._logical_channels) > 0: + self._logger.debug('Waiting workers(%d)...' % + len(self._logical_channels)) + self._worker_done_notify_received = False + self._logical_channels_condition.wait(timeout) + if not self._worker_done_notify_received: + self._logger.debug('Waiting worker(s) timed out') + return False + finally: + self._logical_channels_condition.release() + + # Flush pending outgoing data + self._writer.stop() + self._writer.join() + + return True + + def notify_write_data_done(self, channel_id): + """Called by the writer thread when a write operation has done. + + Args: + channel_id: objective channel id. + """ + + try: + self._logical_channels_condition.acquire() + if channel_id in self._logical_channels: + channel_data = self._logical_channels[channel_id] + channel_data.request.connection.on_write_data_done() + else: + self._logger.debug('Seems that logical channel for %d has gone' + % channel_id) + finally: + self._logical_channels_condition.release() + + def send_control_data(self, data): + """Sends data via the control channel. + + Args: + data: data to be sent. + """ + + self._writer.put_outgoing_data(_OutgoingData( + channel_id=_CONTROL_CHANNEL_ID, data=data)) + + def send_data(self, channel_id, data): + """Sends data via given logical channel. This method is called by + worker threads. + + Args: + channel_id: objective channel id. + data: data to be sent. + """ + + self._writer.put_outgoing_data(_OutgoingData( + channel_id=channel_id, data=data)) + + def _send_drop_channel(self, channel_id, code=None, message=''): + frame_data = _create_drop_channel(channel_id, code, message) + self._logger.debug( + 'Sending drop channel for channel id %d' % channel_id) + self.send_control_data(frame_data) + + def _send_error_add_channel_response(self, channel_id, status=None): + if status is None: + status = common.HTTP_STATUS_BAD_REQUEST + + if status in _HTTP_BAD_RESPONSE_MESSAGES: + message = _HTTP_BAD_RESPONSE_MESSAGES[status] + else: + self._logger.debug('Response message for %d is not found' % status) + message = '???' + + response = 'HTTP/1.1 %d %s\r\n\r\n' % (status, message) + frame_data = _create_add_channel_response(channel_id, + encoded_handshake=response, + encoding=0, rejected=True) + self.send_control_data(frame_data) + + def _create_logical_request(self, block): + if block.channel_id == _CONTROL_CHANNEL_ID: + # TODO(bashi): Raise PhysicalConnectionError with code 2006 + # instead of MuxUnexpectedException. + raise MuxUnexpectedException( + 'Received the control channel id (0) as objective channel ' + 'id for AddChannel') + + if block.encoding > _HANDSHAKE_ENCODING_DELTA: + raise PhysicalConnectionError( + _DROP_CODE_UNKNOWN_REQUEST_ENCODING) + + method, path, version, headers = _parse_request_text( + block.encoded_handshake) + if block.encoding == _HANDSHAKE_ENCODING_DELTA: + headers = self._handshake_base.create_headers(headers) + + connection = _LogicalConnection(self, block.channel_id) + request = _LogicalRequest(block.channel_id, method, path, version, + headers, connection) + return request + + def _do_handshake_for_logical_request(self, request, send_quota=0): + try: + receive_quota = self._channel_slots.popleft() + except IndexError: + raise LogicalChannelError( + request.channel_id, _DROP_CODE_NEW_CHANNEL_SLOT_VIOLATION) + + handshaker = _MuxHandshaker(request, self.dispatcher, + send_quota, receive_quota) + try: + handshaker.do_handshake() + except handshake.VersionException, e: + self._logger.info('%s', e) + self._send_error_add_channel_response( + request.channel_id, status=common.HTTP_STATUS_BAD_REQUEST) + return False + except handshake.HandshakeException, e: + # TODO(bashi): Should we _Fail the Logical Channel_ with 3001 + # instead? + self._logger.info('%s', e) + self._send_error_add_channel_response(request.channel_id, + status=e.status) + return False + except handshake.AbortedByUserException, e: + self._logger.info('%s', e) + self._send_error_add_channel_response(request.channel_id) + return False + + return True + + def _add_logical_channel(self, logical_request): + try: + self._logical_channels_condition.acquire() + if logical_request.channel_id in self._logical_channels: + self._logger.debug('Channel id %d already exists' % + logical_request.channel_id) + raise PhysicalConnectionError( + _DROP_CODE_CHANNEL_ALREADY_EXISTS, + 'Channel id %d already exists' % + logical_request.channel_id) + worker = _Worker(self, logical_request) + channel_data = _LogicalChannelData(logical_request, worker) + self._logical_channels[logical_request.channel_id] = channel_data + worker.start() + finally: + self._logical_channels_condition.release() + + def _process_add_channel_request(self, block): + try: + logical_request = self._create_logical_request(block) + except ValueError, e: + self._logger.debug('Failed to create logical request: %r' % e) + self._send_error_add_channel_response( + block.channel_id, status=common.HTTP_STATUS_BAD_REQUEST) + return + if self._do_handshake_for_logical_request(logical_request): + if block.encoding == _HANDSHAKE_ENCODING_IDENTITY: + # Update handshake base. + # TODO(bashi): Make sure this is the right place to update + # handshake base. + self._handshake_base = _HandshakeDeltaBase( + logical_request.headers_in) + self._add_logical_channel(logical_request) + else: + self._send_error_add_channel_response( + block.channel_id, status=common.HTTP_STATUS_BAD_REQUEST) + + def _process_flow_control(self, block): + try: + self._logical_channels_condition.acquire() + if not block.channel_id in self._logical_channels: + return + channel_data = self._logical_channels[block.channel_id] + channel_data.request.ws_stream.replenish_send_quota( + block.send_quota) + finally: + self._logical_channels_condition.release() + + def _process_drop_channel(self, block): + self._logger.debug( + 'DropChannel received for %d: code=%r, reason=%r' % + (block.channel_id, block.drop_code, block.drop_message)) + try: + self._logical_channels_condition.acquire() + if not block.channel_id in self._logical_channels: + return + channel_data = self._logical_channels[block.channel_id] + channel_data.drop_code = _DROP_CODE_ACKNOWLEDGED + + # Close the logical channel + channel_data.request.connection.set_read_state( + _LogicalConnection.STATE_TERMINATED) + channel_data.request.ws_stream.stop_sending() + finally: + self._logical_channels_condition.release() + + def _process_control_blocks(self, parser): + for control_block in parser.read_control_blocks(): + opcode = control_block.opcode + self._logger.debug('control block received, opcode: %d' % opcode) + if opcode == _MUX_OPCODE_ADD_CHANNEL_REQUEST: + self._process_add_channel_request(control_block) + elif opcode == _MUX_OPCODE_ADD_CHANNEL_RESPONSE: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Received AddChannelResponse') + elif opcode == _MUX_OPCODE_FLOW_CONTROL: + self._process_flow_control(control_block) + elif opcode == _MUX_OPCODE_DROP_CHANNEL: + self._process_drop_channel(control_block) + elif opcode == _MUX_OPCODE_NEW_CHANNEL_SLOT: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Received NewChannelSlot') + else: + raise MuxUnexpectedException( + 'Unexpected opcode %r' % opcode) + + def _process_logical_frame(self, channel_id, parser): + self._logger.debug('Received a frame. channel id=%d' % channel_id) + try: + self._logical_channels_condition.acquire() + if not channel_id in self._logical_channels: + # We must ignore the message for an inactive channel. + return + channel_data = self._logical_channels[channel_id] + fin, rsv1, rsv2, rsv3, opcode, payload = parser.read_inner_frame() + consuming_byte = len(payload) + if opcode != common.OPCODE_CONTINUATION: + consuming_byte += 1 + if not channel_data.request.ws_stream.consume_receive_quota( + consuming_byte): + # The client violates quota. Close logical channel. + raise LogicalChannelError( + channel_id, _DROP_CODE_SEND_QUOTA_VIOLATION) + header = create_header(opcode, len(payload), fin, rsv1, rsv2, rsv3, + mask=False) + frame_data = header + payload + channel_data.request.connection.append_frame_data(frame_data) + finally: + self._logical_channels_condition.release() + + def dispatch_message(self, message): + """Dispatches message. The reader thread calls this method. + + Args: + message: a message that contains encapsulated frame. + Raises: + PhysicalConnectionError: if the message contains physical + connection level errors. + LogicalChannelError: if the message contains logical channel + level errors. + """ + + parser = _MuxFramePayloadParser(message) + try: + channel_id = parser.read_channel_id() + except ValueError, e: + raise PhysicalConnectionError(_DROP_CODE_CHANNEL_ID_TRUNCATED) + if channel_id == _CONTROL_CHANNEL_ID: + self._process_control_blocks(parser) + else: + self._process_logical_frame(channel_id, parser) + + def notify_worker_done(self, channel_id): + """Called when a worker has finished. + + Args: + channel_id: channel id corresponded with the worker. + """ + + self._logger.debug('Worker for channel id %d terminated' % channel_id) + try: + self._logical_channels_condition.acquire() + if not channel_id in self._logical_channels: + raise MuxUnexpectedException( + 'Channel id %d not found' % channel_id) + channel_data = self._logical_channels.pop(channel_id) + finally: + self._worker_done_notify_received = True + self._logical_channels_condition.notify() + self._logical_channels_condition.release() + + if not channel_data.request.server_terminated: + self._send_drop_channel( + channel_id, code=channel_data.drop_code, + message=channel_data.drop_message) + + def notify_reader_done(self): + """This method is called by the reader thread when the reader has + finished. + """ + + self._logger.debug( + 'Termiating all logical connections waiting for incoming data ' + '...') + self._logical_channels_condition.acquire() + for channel_data in self._logical_channels.values(): + try: + channel_data.request.connection.set_read_state( + _LogicalConnection.STATE_TERMINATED) + except Exception: + self._logger.debug(traceback.format_exc()) + self._logical_channels_condition.release() + + def notify_writer_done(self): + """This method is called by the writer thread when the writer has + finished. + """ + + self._logger.debug( + 'Termiating all logical connections waiting for write ' + 'completion ...') + self._logical_channels_condition.acquire() + for channel_data in self._logical_channels.values(): + try: + channel_data.request.connection.on_writer_done() + except Exception: + self._logger.debug(traceback.format_exc()) + self._logical_channels_condition.release() + + def fail_physical_connection(self, code, message): + """Fail the physical connection. + + Args: + code: drop reason code. + message: drop message. + """ + + self._logger.debug('Failing the physical connection...') + self._send_drop_channel(_CONTROL_CHANNEL_ID, code, message) + self._writer.stop(common.STATUS_INTERNAL_ENDPOINT_ERROR) + + def fail_logical_channel(self, channel_id, code, message): + """Fail a logical channel. + + Args: + channel_id: channel id. + code: drop reason code. + message: drop message. + """ + + self._logger.debug('Failing logical channel %d...' % channel_id) + try: + self._logical_channels_condition.acquire() + if channel_id in self._logical_channels: + channel_data = self._logical_channels[channel_id] + # Close the logical channel. notify_worker_done() will be + # called later and it will send DropChannel. + channel_data.drop_code = code + channel_data.drop_message = message + + channel_data.request.connection.set_read_state( + _LogicalConnection.STATE_TERMINATED) + channel_data.request.ws_stream.stop_sending() + else: + self._send_drop_channel(channel_id, code, message) + finally: + self._logical_channels_condition.release() + + +def use_mux(request): + return hasattr(request, 'mux_processor') and ( + request.mux_processor.is_active()) + + +def start(request, dispatcher): + mux_handler = _MuxHandler(request, dispatcher) + mux_handler.start() + + mux_handler.add_channel_slots(_INITIAL_NUMBER_OF_CHANNEL_SLOTS, + _INITIAL_QUOTA_FOR_CLIENT) + + mux_handler.wait_until_done() + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/stream.py b/testing/mochitest/pywebsocket/mod_pywebsocket/stream.py new file mode 100644 index 000000000..edc533279 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/stream.py @@ -0,0 +1,57 @@ +# 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 exports public symbols. +""" + + +from mod_pywebsocket._stream_base import BadOperationException +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import InvalidFrameException +from mod_pywebsocket._stream_base import InvalidUTF8Exception +from mod_pywebsocket._stream_base import UnsupportedFrameException +from mod_pywebsocket._stream_hixie75 import StreamHixie75 +from mod_pywebsocket._stream_hybi import Frame +from mod_pywebsocket._stream_hybi import Stream +from mod_pywebsocket._stream_hybi import StreamOptions + +# These methods are intended to be used by WebSocket client developers to have +# their implementations receive broken data in tests. +from mod_pywebsocket._stream_hybi import create_close_frame +from mod_pywebsocket._stream_hybi import create_header +from mod_pywebsocket._stream_hybi import create_length_header +from mod_pywebsocket._stream_hybi import create_ping_frame +from mod_pywebsocket._stream_hybi import create_pong_frame +from mod_pywebsocket._stream_hybi import create_binary_frame +from mod_pywebsocket._stream_hybi import create_text_frame +from mod_pywebsocket._stream_hybi import create_closing_handshake_body + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/util.py b/testing/mochitest/pywebsocket/mod_pywebsocket/util.py new file mode 100644 index 000000000..d224ae394 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/util.py @@ -0,0 +1,416 @@ +# 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. +""" + + +import array +import errno + +# Import hash classes from a module available and recommended for each Python +# version and re-export those symbol. Use sha and md5 module in Python 2.4, and +# hashlib module in Python 2.6. +try: + import hashlib + md5_hash = hashlib.md5 + sha1_hash = hashlib.sha1 +except ImportError: + import md5 + import sha + md5_hash = md5.md5 + sha1_hash = sha.sha + +import StringIO +import logging +import os +import re +import socket +import traceback +import zlib + +try: + from mod_pywebsocket import fast_masking +except ImportError: + pass + + +def get_stack_trace(): + """Get the current stack trace as string. + + This is needed to support Python 2.3. + TODO: Remove this when we only support Python 2.4 and above. + Use traceback.format_exc instead. + """ + + out = StringIO.StringIO() + traceback.print_exc(file=out) + return out.getvalue() + + +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): + """Gets #!-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(map(lambda x: '%02x' % ord(x), s)) + + +def get_class_logger(o): + return logging.getLogger( + '%s.%s' % (o.__class__.__module__, o.__class__.__name__)) + + +class NoopMasker(object): + """A masking object that has the same interface as RepeatedXorMasker but + just returns the string passed in without making any change. + """ + + def __init__(self): + pass + + def mask(self, s): + return s + + +class RepeatedXorMasker(object): + """A masking object that applies XOR on the 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): + 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): + result = array.array('B') + result.fromstring(s) + + # Use temporary local variables to eliminate the cost to access + # attributes + masking_key = map(ord, self._masking_key) + masking_key_size = len(masking_key) + masking_key_index = self._masking_key_index + + for i in xrange(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 result.tostring() + + 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) + + 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 = '' + + self.reset() + + def decompress(self, size): + if not (size == -1 or size > 0): + raise Exception('size must be -1 or positive') + + data = '' + + while True: + if size == -1: + data += self._decompress.decompress(self._unconsumed) + # See Python bug http://bugs.python.org/issue12050 to + # understand why the same code cannot be used for updating + # self._unconsumed for here and else block. + self._unconsumed = '' + else: + data += self._decompress.decompress( + self._unconsumed, 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 + chr(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 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 + '\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 '' + 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/pywebsocket/mod_pywebsocket/xhr_benchmark_handler.py b/testing/mochitest/pywebsocket/mod_pywebsocket/xhr_benchmark_handler.py new file mode 100644 index 000000000..6735c7e2a --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/xhr_benchmark_handler.py @@ -0,0 +1,109 @@ +# Copyright 2014 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the COPYING file or at +# https://developers.google.com/open-source/licenses/bsd + + +from mod_pywebsocket import util + + +class XHRBenchmarkHandler(object): + def __init__(self, headers, rfile, wfile): + self._logger = util.get_class_logger(self) + + self.headers = headers + self.rfile = rfile + self.wfile = wfile + + def do_send(self): + content_length = int(self.headers.getheader('Content-Length')) + + self._logger.debug('Requested to receive %s bytes', content_length) + + RECEIVE_BLOCK_SIZE = 1024 * 1024 + + bytes_to_receive = content_length + while bytes_to_receive > 0: + bytes_to_receive_in_this_loop = bytes_to_receive + if bytes_to_receive_in_this_loop > RECEIVE_BLOCK_SIZE: + bytes_to_receive_in_this_loop = RECEIVE_BLOCK_SIZE + received_data = self.rfile.read(bytes_to_receive_in_this_loop) + if received_data != ('a' * bytes_to_receive_in_this_loop): + self._logger.debug('Request body verification failed') + return + bytes_to_receive -= len(received_data) + if bytes_to_receive < 0: + self._logger.debug('Received %d more bytes than expected' % + (-bytes_to_receive)) + return + + # Return the number of received bytes back to the client. + response_body = '%d' % content_length + self.wfile.write( + 'HTTP/1.1 200 OK\r\n' + 'Content-Type: text/html\r\n' + 'Content-Length: %d\r\n' + '\r\n%s' % (len(response_body), response_body)) + self.wfile.flush() + + def do_receive(self): + content_length = int(self.headers.getheader('Content-Length')) + request_body = self.rfile.read(content_length) + + request_array = request_body.split(' ') + if len(request_array) < 2: + self._logger.debug('Malformed request body: %r', request_body) + return + + # Parse the size parameter. + bytes_to_send = request_array[0] + try: + bytes_to_send = int(bytes_to_send) + except ValueError, e: + self._logger.debug('Malformed size parameter: %r', bytes_to_send) + return + self._logger.debug('Requested to send %s bytes', bytes_to_send) + + # Parse the transfer encoding parameter. + chunked_mode = False + mode_parameter = request_array[1] + if mode_parameter == 'chunked': + self._logger.debug('Requested chunked transfer encoding') + chunked_mode = True + elif mode_parameter != 'none': + self._logger.debug('Invalid mode parameter: %r', mode_parameter) + return + + # Write a header + response_header = ( + 'HTTP/1.1 200 OK\r\n' + 'Content-Type: application/octet-stream\r\n') + if chunked_mode: + response_header += 'Transfer-Encoding: chunked\r\n\r\n' + else: + response_header += ( + 'Content-Length: %d\r\n\r\n' % bytes_to_send) + self.wfile.write(response_header) + self.wfile.flush() + + # Write a body + SEND_BLOCK_SIZE = 1024 * 1024 + + while bytes_to_send > 0: + bytes_to_send_in_this_loop = bytes_to_send + if bytes_to_send_in_this_loop > SEND_BLOCK_SIZE: + bytes_to_send_in_this_loop = SEND_BLOCK_SIZE + + if chunked_mode: + self.wfile.write('%x\r\n' % bytes_to_send_in_this_loop) + self.wfile.write('a' * bytes_to_send_in_this_loop) + if chunked_mode: + self.wfile.write('\r\n') + self.wfile.flush() + + bytes_to_send -= bytes_to_send_in_this_loop + + if chunked_mode: + self.wfile.write('0\r\n\r\n') + self.wfile.flush() diff --git a/testing/mochitest/pywebsocket/standalone.py b/testing/mochitest/pywebsocket/standalone.py new file mode 100755 index 000000000..e17632743 --- /dev/null +++ b/testing/mochitest/pywebsocket/standalone.py @@ -0,0 +1,1185 @@ +#!/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 without Apache HTTP 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. +""" + +import BaseHTTPServer +import CGIHTTPServer +import SimpleHTTPServer +import SocketServer +import ConfigParser +import base64 +import httplib +import logging +import logging.handlers +import optparse +import os +import re +import select +import socket +import sys +import threading +import time + +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 +from mod_pywebsocket.xhr_benchmark_handler import XHRBenchmarkHandler + + +_DEFAULT_LOG_MAX_BYTES = 1024 * 256 +_DEFAULT_LOG_BACKUP_COUNT = 5 + +_DEFAULT_REQUEST_QUEUE_SIZE = 128 + +# 1024 is practically large enough to contain WebSocket handshake lines. +_MAX_MEMORIZED_LINES = 1024 + +# Constants for the --tls_module flag. +_TLS_BY_STANDARD_MODULE = 'ssl' +_TLS_BY_PYOPENSSL = 'pyopenssl' + + +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 + + +def _import_ssl(): + global ssl + try: + import ssl + return True + except ImportError: + return False + + +def _import_pyopenssl(): + global OpenSSL + try: + import OpenSSL.SSL + return True + except ImportError: + return False + + +class _StandaloneSSLConnection(object): + """A wrapper class for OpenSSL.SSL.Connection to + - provide makefile method which is not supported by the class + - tweak shutdown method since OpenSSL.SSL.Connection.shutdown doesn't + accept the "how" argument. + - convert SysCallError exceptions that its recv method may raise into a + return value of '', meaning EOF. We cannot overwrite the recv method on + self._connection since it's immutable. + """ + + _OVERRIDDEN_ATTRIBUTES = ['_connection', 'makefile', 'shutdown', 'recv'] + + def __init__(self, connection): + self._connection = connection + + def __getattribute__(self, name): + if name in _StandaloneSSLConnection._OVERRIDDEN_ATTRIBUTES: + return object.__getattribute__(self, name) + return self._connection.__getattribute__(name) + + def __setattr__(self, name, value): + if name in _StandaloneSSLConnection._OVERRIDDEN_ATTRIBUTES: + return object.__setattr__(self, name, value) + return self._connection.__setattr__(name, value) + + def makefile(self, mode='r', bufsize=-1): + return socket._fileobject(self, mode, bufsize) + + def shutdown(self, unused_how): + self._connection.shutdown() + + def recv(self, bufsize, flags=0): + if flags != 0: + raise ValueError('Non-zero flags not allowed') + + try: + return self._connection.recv(bufsize) + except OpenSSL.SSL.SysCallError, (err, message): + if err == -1: + # Suppress "unexpected EOF" exception. See the OpenSSL document + # for SSL_get_error. + return '' + raise + + +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 + """ + + fp = open(websock_handlers_map_file) + try: + for line in fp: + 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, e: + logging.error(str(e)) + finally: + fp.close() + + +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, e: + self._logger.info('Skip by failure: %r', e) + continue + server_options = self.websocket_server_options + if server_options.use_tls: + # For the case of _HAS_OPEN_SSL, we do wrapper setup after + # accept. + if server_options.tls_module == _TLS_BY_STANDARD_MODULE: + 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, + ssl_version=ssl.PROTOCOL_SSLv23, + ca_certs=server_options.tls_client_ca, + cert_reqs=client_cert_, + do_handshake_on_connect=False) + 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, 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, 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, + util.get_stack_trace()) + # Note: client_address is a tuple. + + def get_request(self): + """Override TCPServer.get_request to wrap OpenSSL.SSL.Connection + object with _StandaloneSSLConnection to provide makefile method. We + cannot substitute OpenSSL.SSL.Connection.makefile since it's readonly + attribute. + """ + + accepted_socket, client_address = self.socket.accept() + + server_options = self.websocket_server_options + if server_options.use_tls: + if server_options.tls_module == _TLS_BY_STANDARD_MODULE: + try: + accepted_socket.do_handshake() + except ssl.SSLError, e: + self._logger.debug('%r', e) + raise + + # 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()) + elif server_options.tls_module == _TLS_BY_PYOPENSSL: + # We cannot print the cipher in use. pyOpenSSL doesn't provide + # any method to fetch that. + + ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) + ctx.use_privatekey_file(server_options.private_key) + ctx.use_certificate_file(server_options.certificate) + + def default_callback(conn, cert, errnum, errdepth, ok): + return ok == 1 + + # See the OpenSSL document for SSL_CTX_set_verify. + if server_options.tls_client_auth: + verify_mode = OpenSSL.SSL.VERIFY_PEER + if not server_options.tls_client_cert_optional: + verify_mode |= OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT + ctx.set_verify(verify_mode, default_callback) + ctx.load_verify_locations(server_options.tls_client_ca, + None) + else: + ctx.set_verify(OpenSSL.SSL.VERIFY_NONE, default_callback) + + accepted_socket = OpenSSL.SSL.Connection(ctx, accepted_socket) + accepted_socket.set_accept_state() + + # Convert SSL related error into socket.error so that + # SocketServer ignores them and keeps running. + # + # TODO(tyoshino): Convert all kinds of errors. + try: + accepted_socket.do_handshake() + except OpenSSL.SSL.Error, e: + # Set errno part to 1 (SSL_ERROR_SSL) like the ssl module + # does. + self._logger.debug('%r', e) + raise socket.error(1, '%r' % e) + cert = accepted_socket.get_peer_certificate() + if cert is not None: + self._logger.debug('Client cert subject: %r', + cert.get_subject().get_components()) + accepted_socket = _StandaloneSSLConnection(accepted_socket) + else: + raise ValueError('No TLS support module is available') + + 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() + + +class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): + """CGIHTTPRequestHandler specialized for WebSocket.""" + + # Use httplib.HTTPMessage instead of mimetools.Message. + MessageClass = httplib.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.getheader('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) + + # Special paths for XMLHttpRequest benchmark + xhr_benchmark_helper_prefix = '/073be001e10950692ccbf3a2ad21c245' + if resource == (xhr_benchmark_helper_prefix + '_send'): + xhr_benchmark_handler = XHRBenchmarkHandler( + self.headers, self.rfile, self.wfile) + xhr_benchmark_handler.do_send() + return False + if resource == (xhr_benchmark_helper_prefix + '_receive'): + xhr_benchmark_handler = XHRBenchmarkHandler( + self.headers, self.rfile, self.wfile) + xhr_benchmark_handler.do_receive() + return False + + 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, 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, + allowDraft75=self._options.allow_draft75, + strict=self._options.strict) + except handshake.VersionException, 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, 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, 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 + + +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) + + +def _build_option_parser(): + parser = optparse.OptionParser() + + parser.add_option('--config', dest='config_file', type='string', + default=None, + help=('Path to configuration file. See the file comment ' + 'at the top of this file for the configuration ' + 'file format')) + parser.add_option('-H', '--server-host', '--server_host', + dest='server_host', + default='', + help='server hostname to listen to') + parser.add_option('-V', '--validation-host', '--validation_host', + dest='validation_host', + default=None, + help='server hostname to validate in absolute path.') + parser.add_option('-p', '--port', dest='port', type='int', + default=common.DEFAULT_WEB_SOCKET_PORT, + help='port to listen to') + parser.add_option('-P', '--validation-port', '--validation_port', + dest='validation_port', type='int', + default=None, + help='server port to validate in absolute path.') + parser.add_option('-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_option('-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_option('-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_option('--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_option('-d', '--document-root', '--document_root', + dest='document_root', default='.', + help='Document root directory.') + parser.add_option('-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_option('-t', '--tls', dest='use_tls', action='store_true', + default=False, help='use TLS (wss://)') + parser.add_option('--tls-module', '--tls_module', dest='tls_module', + type='choice', + choices = [_TLS_BY_STANDARD_MODULE, _TLS_BY_PYOPENSSL], + help='Use ssl module if "%s" is specified. ' + 'Use pyOpenSSL module if "%s" is specified' % + (_TLS_BY_STANDARD_MODULE, _TLS_BY_PYOPENSSL)) + parser.add_option('-k', '--private-key', '--private_key', + dest='private_key', + default='', help='TLS private key file.') + parser.add_option('-c', '--certificate', dest='certificate', + default='', help='TLS certificate file.') + parser.add_option('--tls-client-auth', dest='tls_client_auth', + action='store_true', default=False, + help='Requests TLS client auth on every connection.') + parser.add_option('--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_option('--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_option('--basic-auth', dest='use_basic_auth', + action='store_true', default=False, + help='Requires Basic authentication.') + parser.add_option('--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_option('-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_option('--log-level', '--log_level', type='choice', + dest='log_level', default='warn', + choices=['fine', + 'debug', 'info', 'warning', 'warn', 'error', + 'critical'], + help='Log level.') + parser.add_option('--deflate-log-level', '--deflate_log_level', + type='choice', + dest='deflate_log_level', default='warn', + choices=['debug', 'info', 'warning', 'warn', 'error', + 'critical'], + help='Log level for _Deflater and _Inflater.') + parser.add_option('--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_option('--log-max', '--log_max', dest='log_max', type='int', + default=_DEFAULT_LOG_MAX_BYTES, + help='Log maximum bytes') + parser.add_option('--log-count', '--log_count', dest='log_count', + type='int', default=_DEFAULT_LOG_BACKUP_COUNT, + help='Log backup count') + parser.add_option('--allow-draft75', dest='allow_draft75', + action='store_true', default=False, + help='Obsolete option. Ignored.') + parser.add_option('--strict', dest='strict', action='store_true', + default=False, help='Obsolete option. Ignored.') + parser.add_option('-q', '--queue', dest='request_queue_size', type='int', + default=_DEFAULT_REQUEST_QUEUE_SIZE, + help='request queue size') + + return parser + + +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) + + +def _parse_args_and_config(args): + parser = _build_option_parser() + + # First, parse options without configuration file. + temporary_options, temporary_args = parser.parse_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, 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_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) + + _configure_logging(options) + + if options.allow_draft75: + logging.warning('--allow_draft75 option is obsolete.') + + if options.strict: + logging.warning('--strict option is obsolete.') + + # 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: + if options.tls_module is None: + if _import_ssl(): + options.tls_module = _TLS_BY_STANDARD_MODULE + logging.debug('Using ssl module') + elif _import_pyopenssl(): + options.tls_module = _TLS_BY_PYOPENSSL + logging.debug('Using pyOpenSSL module') + else: + logging.critical( + 'TLS support requires ssl or pyOpenSSL module.') + sys.exit(1) + elif options.tls_module == _TLS_BY_STANDARD_MODULE: + if not _import_ssl(): + logging.critical('ssl module is not available') + sys.exit(1) + elif options.tls_module == _TLS_BY_PYOPENSSL: + if not _import_pyopenssl(): + logging.critical('pyOpenSSL module is not available') + sys.exit(1) + else: + logging.critical('Invalid --tls-module option: %r', + options.tls_module) + sys.exit(1) + + 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_module is not None: + logging.critical('Use --tls-module option only together with ' + '--use-tls option.') + sys.exit(1) + + 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) + + try: + if options.thread_monitor_interval_in_sec > 0: + # Run a thread monitor to show the status of server threads for + # debugging. + ThreadMonitor(options.thread_monitor_interval_in_sec).start() + + server = WebSocketServer(options) + server.serve_forever() + except Exception, e: + logging.critical('mod_pywebsocket: %s' % e) + logging.critical('mod_pywebsocket: %s' % util.get_stack_trace()) + sys.exit(1) + + +if __name__ == '__main__': + _main(sys.argv[1:]) + + +# 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 000000000..59c4a97b2 --- /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 = ['pywebsocket'] + sys.path + 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 000000000..d2b009534 --- /dev/null +++ b/testing/mochitest/redirect.html @@ -0,0 +1,42 @@ +<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.xul"); + } + + function onLoad() { + // Wait for MozAfterPaint, since the listener in browser-test.js is not + // added until then. + window.addEventListener("MozAfterPaint", function testOnMozAfterPaint() { + window.removeEventListener("MozAfterPaint", testOnMozAfterPaint); + setTimeout(redirectToHarness, 0); + // In case the listener was not ready, try again after a few seconds. + setTimeout(redirectToHarness, 5000); + }); + + } + </script> +</head> + +<body onload="onLoad();"> +redirecting... +</body> +</html> diff --git a/testing/mochitest/runrobocop.py b/testing/mochitest/runrobocop.py new file mode 100644 index 000000000..c95964e8d --- /dev/null +++ b/testing/mochitest/runrobocop.py @@ -0,0 +1,587 @@ +# 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 shutil +import sys +import tempfile +import traceback + +sys.path.insert( + 0, os.path.abspath( + os.path.realpath( + os.path.dirname(__file__)))) + +from automation import Automation +from remoteautomation import RemoteAutomation, fennecLogcatFilters +from runtests import KeyValueParseError, MochitestDesktop, MessageLogger, parseKeyValue +from mochitest_options import MochitestArgumentParser + +from manifestparser import TestManifest +from manifestparser.filters import chunk_by_slice +import mozdevice +import mozinfo + +SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) + + +class RobocopTestRunner(MochitestDesktop): + """ + A test harness for Robocop. Robocop tests are UI tests for Firefox for Android, + based on the Robotium test framework. This harness leverages some functionality + from mochitest, for convenience. + """ + auto = None + dm = None + # Some robocop tests run for >60 seconds without generating any output. + NO_OUTPUT_TIMEOUT = 180 + + def __init__(self, automation, devmgr, options): + """ + Simple one-time initialization. + """ + MochitestDesktop.__init__(self, options) + + self.auto = automation + self.dm = devmgr + self.dm.default_timeout = 320 + self.options = options + self.options.logFile = "robocop.log" + self.environment = self.auto.environment + self.deviceRoot = self.dm.getDeviceRoot() + self.remoteProfile = options.remoteTestRoot + "/profile" + self.remoteProfileCopy = options.remoteTestRoot + "/profile-copy" + self.auto.setRemoteProfile(self.remoteProfile) + self.remoteConfigFile = os.path.join( + self.deviceRoot, "robotium.config") + self.remoteLog = options.remoteLogFile + self.auto.setRemoteLog(self.remoteLog) + self.remoteScreenshots = "/mnt/sdcard/Robotium-Screenshots" + self.remoteMozLog = os.path.join(options.remoteTestRoot, "mozlog") + self.auto.setServerInfo( + self.options.webServer, self.options.httpPort, self.options.sslPort) + self.localLog = options.logFile + self.localProfile = None + productPieces = self.options.remoteProductName.split('.') + if (productPieces is not None): + self.auto.setProduct(productPieces[0]) + else: + self.auto.setProduct(self.options.remoteProductName) + self.auto.setAppName(self.options.remoteappname) + self.certdbNew = True + self.remoteCopyAvailable = True + self.passed = 0 + self.failed = 0 + self.todo = 0 + + def startup(self): + """ + Second-stage initialization: One-time initialization which may require cleanup. + """ + # 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 orphan servers before + # trying to start new ones. + self.killNamedOrphans('ssltunnel') + self.killNamedOrphans('xpcshell') + self.auto.deleteANRs() + self.auto.deleteTombstones() + self.dm.killProcess(self.options.app.split('/')[-1]) + self.dm.removeDir(self.remoteScreenshots) + self.dm.removeDir(self.remoteMozLog) + self.dm.mkDir(self.remoteMozLog) + self.dm.mkDir(os.path.dirname(self.options.remoteLogFile)) + # Add Android version (SDK level) to mozinfo so that manifest entries + # can be conditional on android_version. + androidVersion = self.dm.shellCheckOutput( + ['getprop', 'ro.build.version.sdk']) + self.log.info( + "Android sdk version '%s'; will use this to filter manifests" % + str(androidVersion)) + mozinfo.info['android_version'] = androidVersion + if (self.options.dm_trans == 'adb' and self.options.robocopApk): + self.dm._checkCmd(["install", "-r", self.options.robocopApk]) + self.log.debug("Robocop APK %s installed" % + self.options.robocopApk) + # Display remote diagnostics; if running in mach, keep output terse. + if self.options.log_mach is None: + self.printDeviceInfo() + self.setupLocalPaths() + self.buildProfile() + # ignoreSSLTunnelExts is a workaround for bug 1109310 + self.startServers( + self.options, + debuggerInfo=None, + ignoreSSLTunnelExts=True) + self.log.debug("Servers started") + + def cleanup(self): + """ + Cleanup at end of job run. + """ + self.log.debug("Cleaning up...") + self.stopServers() + self.dm.killProcess(self.options.app.split('/')[-1]) + blobberUploadDir = os.environ.get('MOZ_UPLOAD_DIR', None) + if blobberUploadDir: + self.log.debug("Pulling any remote moz logs and screenshots to %s." % + blobberUploadDir) + self.dm.getDirectory(self.remoteMozLog, blobberUploadDir) + self.dm.getDirectory(self.remoteScreenshots, blobberUploadDir) + MochitestDesktop.cleanup(self, self.options) + if self.localProfile: + os.system("rm -Rf %s" % self.localProfile) + self.dm.removeDir(self.remoteProfile) + self.dm.removeDir(self.remoteProfileCopy) + self.dm.removeDir(self.remoteScreenshots) + self.dm.removeDir(self.remoteMozLog) + self.dm.removeFile(self.remoteConfigFile) + if self.dm.fileExists(self.remoteLog): + self.dm.removeFile(self.remoteLog) + self.log.debug("Cleanup complete.") + + 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 + + def makeLocalAutomation(self): + localAutomation = Automation() + localAutomation.IS_WIN32 = False + localAutomation.IS_LINUX = False + localAutomation.IS_MAC = False + localAutomation.UNIXISH = False + hostos = sys.platform + if (hostos == 'mac' or hostos == 'darwin'): + localAutomation.IS_MAC = True + elif (hostos == 'linux' or hostos == 'linux2'): + localAutomation.IS_LINUX = True + localAutomation.UNIXISH = True + elif (hostos == 'win32' or hostos == 'win64'): + localAutomation.BIN_SUFFIX = ".exe" + localAutomation.IS_WIN32 = True + return localAutomation + + def setupLocalPaths(self): + """ + Setup xrePath and utilityPath and verify xpcshell. + + This is similar to switchToLocalPaths in runtestsremote.py. + """ + localAutomation = self.makeLocalAutomation() + paths = [ + self.options.xrePath, + localAutomation.DIST_BIN, + self.auto._product, + os.path.join('..', self.auto._product) + ] + self.options.xrePath = self.findPath(paths) + if self.options.xrePath is None: + self.log.error( + "unable to find xulrunner path for %s, please specify with --xre-path" % + os.name) + sys.exit(1) + self.log.debug("using xre path %s" % self.options.xrePath) + xpcshell = "xpcshell" + if (os.name == "nt"): + xpcshell += ".exe" + if self.options.utilityPath: + paths = [self.options.utilityPath, self.options.xrePath] + else: + paths = [self.options.xrePath] + self.options.utilityPath = self.findPath(paths, xpcshell) + if self.options.utilityPath is None: + self.log.error( + "unable to find utility path for %s, please specify with --utility-path" % + os.name) + sys.exit(1) + self.log.debug("using utility path %s" % self.options.utilityPath) + xpcshell_path = os.path.join(self.options.utilityPath, xpcshell) + if localAutomation.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) + self.log.debug("xpcshell found at %s" % xpcshell_path) + + def buildProfile(self): + """ + Build a profile locally, keep it locally for use by servers and + push a copy to the remote profile-copy directory. + + This is similar to buildProfile in runtestsremote.py. + """ + self.options.extraPrefs.append('browser.search.suggest.enabled=true') + self.options.extraPrefs.append('browser.search.suggest.prompted=true') + self.options.extraPrefs.append('layout.css.devPixelsPerPx=1.0') + self.options.extraPrefs.append('browser.chrome.dynamictoolbar=false') + self.options.extraPrefs.append('browser.snippets.enabled=false') + self.options.extraPrefs.append('browser.casting.enabled=true') + self.options.extraPrefs.append('extensions.autoupdate.enabled=false') + + # Override the telemetry init delay for integration testing. + self.options.extraPrefs.append('toolkit.telemetry.initDelay=1') + + self.options.extensionsToExclude.extend([ + 'mochikit@mozilla.org', + 'worker-test@mozilla.org.xpi', + 'workerbootstrap-test@mozilla.org.xpi', + 'indexedDB-test@mozilla.org.xpi', + ]) + + manifest = MochitestDesktop.buildProfile(self, self.options) + self.localProfile = self.options.profilePath + self.log.debug("Profile created at %s" % self.localProfile) + # some files are not needed for robocop; save time by not pushing + shutil.rmtree(os.path.join(self.localProfile, 'webapps')) + os.remove(os.path.join(self.localProfile, 'userChrome.css')) + try: + self.dm.pushDir(self.localProfile, self.remoteProfileCopy) + except mozdevice.DMError: + self.log.error( + "Automation Error: Unable to copy profile to device.") + raise + + return manifest + + def setupRemoteProfile(self): + """ + Remove any remote profile and re-create it. + """ + self.log.debug("Updating remote profile at %s" % self.remoteProfile) + self.dm.removeDir(self.remoteProfile) + if self.remoteCopyAvailable: + try: + self.dm.shellCheckOutput( + ['cp', '-r', self.remoteProfileCopy, self.remoteProfile], + root=True, timeout=60) + except mozdevice.DMError: + # For instance, cp is not available on some older versions of + # Android. + self.log.info( + "Unable to copy remote profile; falling back to push.") + self.remoteCopyAvailable = False + if not self.remoteCopyAvailable: + self.dm.pushDir(self.localProfile, self.remoteProfile) + + def parseLocalLog(self): + """ + Read and parse the local log file, noting any failures. + """ + with open(self.localLog) as currentLog: + data = currentLog.readlines() + os.unlink(self.localLog) + start_found = False + end_found = False + fail_found = False + for line in data: + try: + message = json.loads(line) + if not isinstance(message, dict) or 'action' not in message: + continue + except ValueError: + continue + if message['action'] == 'test_end': + end_found = True + start_found = False + break + if start_found and not end_found: + if 'status' in message: + if 'expected' in message: + self.failed += 1 + elif message['status'] == 'PASS': + self.passed += 1 + elif message['status'] == 'FAIL': + self.todo += 1 + if message['action'] == 'test_start': + start_found = True + if 'expected' in message: + fail_found = True + result = 0 + if fail_found: + result = 1 + if not end_found: + self.log.info( + "PROCESS-CRASH | Automation Error: Missing end of test marker (process crashed?)") + result = 1 + return result + + def logTestSummary(self): + """ + Print a summary of all tests run to stdout, for treeherder parsing + (logging via self.log does not work here). + """ + print("0 INFO TEST-START | Shutdown") + print("1 INFO Passed: %s" % (self.passed)) + print("2 INFO Failed: %s" % (self.failed)) + print("3 INFO Todo: %s" % (self.todo)) + print("4 INFO SimpleTest FINISHED") + if self.failed > 0: + return 1 + return 0 + + def printDeviceInfo(self, printLogcat=False): + """ + Log remote device information and logcat (if requested). + + This is similar to printDeviceInfo in runtestsremote.py + """ + try: + if printLogcat: + logcat = self.dm.getLogcat( + filterOutRegexps=fennecLogcatFilters) + self.log.info( + '\n' + + ''.join(logcat).decode( + 'utf-8', + 'replace')) + self.log.info("Device info:") + devinfo = self.dm.getInfo() + for category in devinfo: + if type(devinfo[category]) is list: + self.log.info(" %s:" % category) + for item in devinfo[category]: + self.log.info(" %s" % item) + else: + self.log.info(" %s: %s" % (category, devinfo[category])) + self.log.info("Test root: %s" % self.dm.deviceRoot) + except mozdevice.DMError: + self.log.warning("Error getting device information") + + def setupRobotiumConfig(self, browserEnv): + """ + Create robotium.config and push it to the device. + """ + fHandle = tempfile.NamedTemporaryFile(suffix='.config', + prefix='robotium-', + dir=os.getcwd(), + delete=False) + fHandle.write("profile=%s\n" % (self.remoteProfile)) + fHandle.write("logfile=%s\n" % (self.options.remoteLogFile)) + fHandle.write("host=http://mochi.test:8888/tests\n") + fHandle.write( + "rawhost=http://%s:%s/tests\n" % + (self.options.remoteWebServer, self.options.httpPort)) + if browserEnv: + envstr = "" + delim = "" + for key, value in browserEnv.items(): + try: + value.index(',') + self.log.error("setupRobotiumConfig: browserEnv - Found a ',' " + "in our value, unable to process value. key=%s,value=%s" % + (key, value)) + self.log.error("browserEnv=%s" % browserEnv) + except ValueError: + envstr += "%s%s=%s" % (delim, key, value) + delim = "," + fHandle.write("envvars=%s\n" % envstr) + fHandle.close() + self.dm.removeFile(self.remoteConfigFile) + self.dm.pushFile(fHandle.name, self.remoteConfigFile) + os.unlink(fHandle.name) + + def buildBrowserEnv(self): + """ + Return an environment dictionary suitable for remote use. + + This is similar to buildBrowserEnv in runtestsremote.py. + """ + browserEnv = self.environment( + xrePath=None, + debugger=None) + # remove desktop environment not used on device + if "MOZ_WIN_INHERIT_STD_HANDLES_PRE_VISTA" in browserEnv: + del browserEnv["MOZ_WIN_INHERIT_STD_HANDLES_PRE_VISTA"] + if "XPCOM_MEM_BLOAT_LOG" in browserEnv: + del browserEnv["XPCOM_MEM_BLOAT_LOG"] + browserEnv["MOZ_LOG_FILE"] = os.path.join( + self.remoteMozLog, + self.mozLogName) + + try: + browserEnv.update( + dict( + parseKeyValue( + self.options.environment, + context='--setenv'))) + except KeyValueParseError as e: + self.log.error(str(e)) + return None + + return browserEnv + + def runSingleTest(self, test): + """ + Run the specified test. + """ + self.log.debug("Running test %s" % test['name']) + self.mozLogName = "moz-%s.log" % test['name'] + browserEnv = self.buildBrowserEnv() + self.setupRobotiumConfig(browserEnv) + self.setupRemoteProfile() + self.options.app = "am" + if self.options.autorun: + # This launches a test (using "am instrument") and instructs + # Fennec to /quit/ the browser (using Robocop:Quit) and to + # /finish/ all opened activities. + browserArgs = [ + "instrument", + "-w", + "-e", "quit_and_finish", "1", + "-e", "deviceroot", self.deviceRoot, + "-e", "class", + "org.mozilla.gecko.tests.%s" % test['name'].split('/')[-1].split('.java')[0], + "org.mozilla.roboexample.test/org.mozilla.gecko.FennecInstrumentationTestRunner"] + else: + # This does not launch a test at all. It launches an activity + # that starts Fennec and then waits indefinitely, since cat + # never returns. + browserArgs = ["start", "-n", + "org.mozilla.roboexample.test/org.mozilla." + "gecko.LaunchFennecWithConfigurationActivity", "&&", "cat"] + self.dm.default_timeout = sys.maxint # Forever. + self.log.info("") + self.log.info("Serving mochi.test Robocop root at http://%s:%s/tests/robocop/" % + (self.options.remoteWebServer, self.options.httpPort)) + self.log.info("") + result = -1 + log_result = -1 + try: + self.dm.recordLogcat() + timeout = self.options.timeout + if not timeout: + timeout = self.NO_OUTPUT_TIMEOUT + result = self.auto.runApp( + None, browserEnv, "am", self.localProfile, browserArgs, + timeout=timeout, symbolsPath=self.options.symbolsPath) + self.log.debug("runApp completes with status %d" % result) + if result != 0: + self.log.error("runApp() exited with code %s" % result) + if self.dm.fileExists(self.remoteLog): + self.dm.getFile(self.remoteLog, self.localLog) + self.dm.removeFile(self.remoteLog) + self.log.debug("Remote log %s retrieved to %s" % + (self.remoteLog, self.localLog)) + else: + self.log.warning( + "Unable to retrieve log file (%s) from remote device" % + self.remoteLog) + log_result = self.parseLocalLog() + if result != 0 or log_result != 0: + # Display remote diagnostics; if running in mach, keep output + # terse. + if self.options.log_mach is None: + self.printDeviceInfo(printLogcat=True) + except: + self.log.error( + "Automation Error: Exception caught while running tests") + traceback.print_exc() + result = 1 + self.log.debug("Test %s completes with status %d (log status %d)" % + (test['name'], int(result), int(log_result))) + return result + + def runTests(self): + self.startup() + if isinstance(self.options.manifestFile, TestManifest): + mp = self.options.manifestFile + else: + mp = TestManifest(strict=False) + mp.read(self.options.robocopIni) + filters = [] + if self.options.totalChunks: + filters.append( + chunk_by_slice(self.options.thisChunk, self.options.totalChunks)) + robocop_tests = mp.active_tests( + exists=False, filters=filters, **mozinfo.info) + if not self.options.autorun: + # Force a single loop iteration. The iteration will start Fennec and + # the httpd server, but not actually run a test. + self.options.test_paths = [robocop_tests[0]['name']] + active_tests = [] + for test in robocop_tests: + if self.options.test_paths and test['name'] not in self.options.test_paths: + continue + if 'disabled' in test: + self.log.info('TEST-INFO | skipping %s | %s' % + (test['name'], test['disabled'])) + continue + active_tests.append(test) + self.log.suite_start([t['name'] for t in active_tests]) + worstTestResult = None + for test in active_tests: + result = self.runSingleTest(test) + if worstTestResult is None or worstTestResult == 0: + worstTestResult = result + if worstTestResult is None: + self.log.warning( + "No tests run. Did you pass an invalid TEST_PATH?") + worstTestResult = 1 + else: + print "INFO | runtests.py | Test summary: start." + logResult = self.logTestSummary() + print "INFO | runtests.py | Test summary: end." + if worstTestResult == 0: + worstTestResult = logResult + return worstTestResult + + +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") + message_logger = MessageLogger(logger=None) + process_args = {'messageLogger': message_logger} + auto = RemoteAutomation(None, "fennec", processArgs=process_args) + auto.setDeviceManager(options.dm) + runResult = -1 + robocop = RobocopTestRunner(auto, options.dm, options) + + # Check that Firefox is installed + expected = options.app.split('/')[-1] + installed = options.dm.shellCheckOutput(['pm', 'list', 'packages', expected]) + if expected not in installed: + robocop.log.error("%s is not installed on this device" % expected) + return 1 + + try: + message_logger.logger = robocop.log + message_logger.buffering = False + robocop.message_logger = message_logger + robocop.log.debug("options=%s" % vars(options)) + runResult = robocop.runTests() + except KeyboardInterrupt: + robocop.log.info("runrobocop.py | Received keyboard interrupt") + runResult = -1 + except: + traceback.print_exc() + robocop.log.error( + "runrobocop.py | Received unexpected exception while running tests") + runResult = 1 + finally: + try: + robocop.cleanup() + except mozdevice.DMError: + # ignore device error while cleaning up + pass + message_logger.finish() + return runResult + + +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/runtests.py b/testing/mochitest/runtests.py new file mode 100644 index 000000000..7d0c89a99 --- /dev/null +++ b/testing/mochitest/runtests.py @@ -0,0 +1,2738 @@ +# 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. +""" + +from __future__ import with_statement +import os +import sys +SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) +sys.path.insert(0, SCRIPT_DIR) + +from argparse import Namespace +import ctypes +import glob +import json +import mozcrash +import mozdebug +import mozinfo +import mozprocess +import mozrunner +import numbers +import platform +import re +import shutil +import signal +import socket +import subprocess +import sys +import tempfile +import time +import traceback +import urllib2 +import uuid +import zipfile +import bisection + +from datetime import datetime +from manifestparser import TestManifest +from manifestparser.filters import ( + chunk_by_dir, + chunk_by_runtime, + chunk_by_slice, + pathprefix, + subsuite, + tags, +) + +try: + from marionette_driver.addons import Addons + from marionette_harness import Marionette +except ImportError, e: + # Defer ImportError until attempt to use Marionette + def reraise(*args, **kwargs): + raise(e) + Marionette = reraise + +from leaks import ShutdownLeaks, LSANLeaks +from mochitest_options import ( + MochitestArgumentParser, build_obj, get_default_valgrind_suppression_files +) +from mozprofile import Profile, Preferences +from mozprofile.permissions import ServerLocations +from urllib import quote_plus as encodeURIComponent +from mozlog.formatters import TbplFormatter +from mozlog import commandline +from mozrunner.utils import get_stack_fixer_function, test_environment +from mozscreenshot import dump_screen +import mozleak + +HAVE_PSUTIL = False +try: + import psutil + HAVE_PSUTIL = True +except ImportError: + pass + +here = os.path.abspath(os.path.dirname(__file__)) + + +######################################## +# 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 all log files +# on tbpl.mozilla.org. + +MOZ_LOG = "" + +##################### +# Test log handling # +##################### + +# output processing + + +class MochitestFormatter(TbplFormatter): + + """ + The purpose of this class is to maintain compatibility with legacy users. + Mozharness' summary parser expects the count prefix, and others expect python + logging to contain a line prefix picked up by TBPL (bug 1043420). + Those directly logging "TEST-UNEXPECTED" require no prefix to log output + in order to turn a build orange (bug 1044206). + + Once updates are propagated to Mozharness, this class may be removed. + """ + log_num = 0 + + def __init__(self): + super(MochitestFormatter, self).__init__() + + def __call__(self, data): + output = super(MochitestFormatter, self).__call__(data) + log_level = data.get('level', 'info').upper() + + if 'js_source' in data or log_level == 'ERROR': + data.pop('js_source', None) + output = '%d %s %s' % ( + MochitestFormatter.log_num, log_level, output) + MochitestFormatter.log_num += 1 + + return output + +# 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 = u'\ue175\uee31\u2c32\uacbf' + BUFFERED_ACTIONS = set(['test_status', 'log']) + VALID_ACTIONS = set(['suite_start', 'suite_end', 'test_start', 'test_end', + 'test_status', 'log', + 'buffering_on', 'buffering_off']) + TEST_PATH_PREFIXES = ['/tests/', + 'chrome://mochitests/content/browser/', + 'chrome://mochitests/content/chrome/'] + + def __init__(self, logger, buffering=True): + self.logger = logger + + # 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 + + # Message buffering + self.buffered_messages = [] + + # Failures reporting, after the end of the tests execution + self.errors = [] + + def valid_message(self, obj): + """True if the given object is a valid structured message + (only does a superficial validation)""" + return isinstance(obj, dict) and 'action' in obj and obj[ + 'action'] in MessageLogger.VALID_ACTIONS + + def _fix_test_name(self, message): + """Normalize a logged test path to match the relative path from the sourcedir. + """ + if 'test' in message: + test = message['test'] + for prefix in MessageLogger.TEST_PATH_PREFIXES: + if test.startswith(prefix): + message['test'] = test[len(prefix):] + 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'], unicode): + message['message'] = unicode(message['message']) + + def parse_line(self, line): + """Takes a given line of input (structured or not) and + returns a list of structured messages""" + line = line.rstrip().decode("UTF-8", "replace") + + messages = [] + for fragment in line.split(MessageLogger.DELIMITER): + if not fragment: + continue + try: + message = json.loads(fragment) + if not self.valid_message(message): + message = dict( + action='log', + level='info', + message=fragment, + unstructured=True) + except ValueError: + message = dict( + action='log', + level='info', + message=fragment, + unstructured=True) + self._fix_test_name(message) + self._fix_message_format(message) + messages.append(message) + + return messages + + 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': + self.buffering = True + return + if message['action'] == 'buffering_off': + self.buffering = False + return + + unstructured = False + if 'unstructured' in message: + unstructured = True + message.pop('unstructured') + + # 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[ + 'message'].startswith('TEST-UNEXPECTED'))): + # Saving errors/failures to be shown at the end of the test run + self.errors.append(message) + 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) + # If we don't do any buffering, or the tests haven't started, or the message was + # unstructured, it is directly logged. + elif any([not self.buffering, + unstructured, + message['action'] not in self.BUFFERED_ACTIONS]): + self.logger.log_raw(message) + else: + # Buffering the message + self.buffered_messages.append(message) + + # If a test ended, we clean the buffer + if message['action'] == 'test_end': + self.buffered_messages = [] + self.restore_buffering = self.restore_buffering or self.buffering + self.buffering = False + + if message['action'] == 'test_start': + 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: + 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 + process = mozprocess.ProcessHandler(*args, **kwargs) + process.run() + return process.wait() + + +def killPid(pid, log): + # see also https://bugzilla.mozilla.org/show_bug.cgi?id=911249#c58 + 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 + pExitCode = ctypes.wintypes.DWORD() + ctypes.windll.kernel32.GetExitCodeProcess( + pHandle, + ctypes.byref(pExitCode)) + ctypes.windll.kernel32.CloseHandle(pHandle) + return pExitCode.value == STILL_ACTIVE + +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 == errno.ESRCH or err.errno == errno.ECHILD: + 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'] + self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % { + "server": self.webServer, + "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" + env["LD_LIBRARY_PATH"] = self._xrePath + + # 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 slaves. 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" + + if mozinfo.isWin: + env["PATH"] = env["PATH"] + ";" + str(self._xrePath) + + args = [ + "-g", + self._xrePath, + "-v", + "170", + "-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(.05) + i += .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 urllib2.urlopen(self.shutdownURL) as c: + c.read() + + # TODO: need ProcessHandler.poll() + # https://bugzilla.mozilla.org/show_bug.cgi?id=912285 + # rtncode = self._process.poll() + rtncode = self._process.proc.poll() + if rtncode is None: + # TODO: need ProcessHandler.terminate() and/or .send_signal() + # https://bugzilla.mozilla.org/show_bug.cgi?id=912285 + # self._process.terminate() + self._process.proc.terminate() + except: + self._process.kill() + + +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 += ['-p', str(self.port), '-w', self._scriptdir, '-l', + os.path.join(self._scriptdir, "websock.log"), + '--log-level=debug', '--allow-handlers-outside-root-dir'] + # start the process + self._process = mozprocess.ProcessHandler(cmd, cwd=SCRIPT_DIR) + self._process.run() + pid = self._process.pid + self._log.info("runtests.py | Websocket server pid: %d" % pid) + + def stop(self): + self._process.kill() + + +class SSLTunnel: + + def __init__(self, options, logger, ignoreSSLTunnelExts=False): + 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.useSSLTunnelExts = not ignoreSSLTunnelExts + + 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 self.useSSLTunnelExts and option in ( + 'tls1', + 'ssl3', + 'rc4', + 'failHandshake'): + config.write( + "%s:%s:%s:%s\n" % + (option, loc.host, loc.port, self.sslPort)) + + def buildConfig(self, locations): + """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)) + config.write("listen:*:%s:pgo server certificate\n" % 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 + bin_suffix = mozinfo.info.get('bin_suffix', '') + ssltunnel = os.path.join(self.utilityPath, "ssltunnel" + bin_suffix) + 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('libc.so.6') + 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(device, O_RDWR) + if fd < 0: + return False, '' + + vcap = v4l2_capability() + if libc.ioctl(fd, VIDIOC_QUERYCAP, ctypes.byref(vcap)) != 0: + return False, '' + + if 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, 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 + subprocess.check_call(['/usr/bin/gst-launch-0.10', 'videotestsrc', + 'pattern=green', 'num-buffers=1', '!', + 'v4l2sink', 'device=%s' % device]) + info['video'] = name + + # Use pactl to see if the PulseAudio module-sine-source module is loaded. + def sine_source_loaded(): + o = subprocess.check_output( + ['/usr/bin/pactl', 'list', 'short', 'modules']) + return filter(lambda x: 'module-sine-source' in x, o.splitlines()) + + if not sine_source_loaded(): + # Load module-sine-source + subprocess.check_call(['/usr/bin/pactl', 'load-module', + 'module-sine-source']) + if not sine_source_loaded(): + log.error('Couldn\'t load module-sine-source') + return None + + # Hardcode the name since it's always the same. + info['audio'] = 'Sine source at 440 Hz' + return info + + +class KeyValueParseError(Exception): + + """error when parsing strings of serialized key-values""" + + def __init__(self, msg, errors=()): + self.errors = errors + Exception.__init__(self, msg) + + +def parseKeyValue(strings, separator='=', context='key, value: '): + """ + parse string-serialized key-value pairs in the form of + `key = value`. Returns a list of 2-tuples. + Note that whitespace is not stripped. + """ + + # syntax check + missing = [string for string in strings if separator not in string] + if missing: + raise KeyValueParseError( + "Error: syntax error in %s" % + (context, ','.join(missing)), errors=missing) + return [string.split(separator, 1) for string in strings] + + +class MochitestDesktop(object): + """ + Mochitest class for desktop firefox. + """ + oldcwd = os.getcwd() + mochijar = os.path.join(SCRIPT_DIR, 'mochijar') + + # Path to the test script on the server + TEST_PATH = "tests" + NESTED_OOP_TEST_PATH = "nested_oop" + CHROME_PATH = "redirect.html" + log = None + + certdbNew = False + sslTunnel = None + DEFAULT_TIMEOUT = 60.0 + mediaDevices = None + + # 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, logger_options, quiet=False): + self.update_mozinfo() + self.server = None + self.wsserver = None + self.websocketProcessBridge = None + self.sslTunnel = None + self._active_tests = None + self._locations = None + + self.marionette = None + self.start_script = None + self.mozLogs = None + self.start_script_args = [] + self.urlOpts = [] + + if self.log is None: + commandline.log_formatters["tbpl"] = ( + MochitestFormatter, + "Mochitest specific tbpl formatter") + self.log = commandline.setup_logging("mochitest", + logger_options, + { + "tbpl": sys.stdout + }) + MochitestDesktop.log = self.log + + self.message_logger = MessageLogger(logger=self.log, buffering=quiet) + # 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') + self.disable_leak_checking = False + + def update_mozinfo(self): + """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) + + def environment(self, **kwargs): + kwargs['log'] = self.log + return test_environment(**kwargs) + + def extraPrefs(self, extraPrefs): + """interpolate extra preferences from option strings""" + + try: + return dict(parseKeyValue(extraPrefs, context='--setpref=')) + except KeyValueParseError as e: + print str(e) + sys.exit(1) + + 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. + """ + + 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', 'jetpack-addon', 'jetpack-package'): + 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 options.repeat > 0 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") + + 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 == 'jetpack-package': + allow_js_css = True + testPattern = re.compile(r"test-.+\.js") + elif options.flavor == 'jetpack-addon': + testPattern = re.compile(r".+\.xpi") + 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 + + if options.flavor == 'browser' and options.immersiveMode: + self.testRoot = 'metro' + else: + self.testRoot = self.TEST_PATH + self.testRootAbs = os.path.join(SCRIPT_DIR, self.testRoot) + + def buildTestURL(self, options): + testHost = "http://mochi.test:8888" + testURL = "/".join([testHost, self.TEST_PATH]) + + if len(options.test_paths) == 1: + if options.repeat > 0 and 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 in ('browser', 'jetpack-addon', 'jetpack-package'): + testURL = "about:blank" + if options.nested_oop: + testURL = "/".join([testHost, self.NESTED_OOP_TEST_PATH]) + return testURL + + def buildTestPath(self, options, testsToFilter=None, disabled=True): + """ 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 + paths.append(test) + + # Bug 883865 - add this functionality into manifestparser + with open(os.path.join(SCRIPT_DIR, options.testRunManifestFile), 'w') as manifestFile: + manifestFile.write(json.dumps({'tests': paths})) + options.manifestFile = options.testRunManifestFile + + return self.buildTestURL(options) + + 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: + time.sleep(0.1) + else: + self.log.error("runtests.py | Timed out while waiting for " + "websocket/process bridge startup.") + + def startServers(self, options, debuggerInfo, ignoreSSLTunnelExts=False): + # 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) + + if options.subsuite in ["media"]: + self.startWebsocketProcessBridge(options) + + # start SSL pipe + self.sslTunnel = SSLTunnel( + options, + logger=self.log, + ignoreSSLTunnelExts=ignoreSSLTunnelExts) + self.sslTunnel.buildConfig(self.locations) + 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.log.info('Stopping websocket/process bridge') + except Exception: + self.log.critical('Exception stopping websocket/process bridge') + + 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) + + if not os.path.isdir(self.mochijar): + self.log.error( + "TEST-UNEXPECTED-FAIL | invalid setup: missing mochikit extension") + return None + + 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([t['path'] for t in tests]) + 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 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 + + manifest = self.getTestManifest(options) + if manifest: + if options.extra_mozinfo_json: + mozinfo.update(options.extra_mozinfo_json) + info = mozinfo.info + + # Bug 1089034 - imptest failure expectations are encoded as + # test manifests, even though they aren't tests. This gross + # hack causes several problems in automation including + # throwing off the chunking numbers. Remove them manually + # until bug 1089034 is fixed. + def remove_imptest_failure_expectations(tests, values): + return (t for t in tests + if 'imptests/failures' not in t['path']) + + filters = [ + remove_imptest_failure_expectations, + subsuite(options.subsuite), + ] + + 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.chunkByRuntime: + runtime_file = self.resolve_runtime_file(options) + if not os.path.exists(runtime_file): + self.log.warning("runtime file %s not found; defaulting to chunk-by-dir" % + runtime_file) + options.chunkByRuntime = None + if options.flavor == 'browser': + # these values match current mozharness configs + options.chunkbyDir = 5 + else: + options.chunkByDir = 4 + + if options.chunkByDir: + filters.append(chunk_by_dir(options.thisChunk, + options.totalChunks, + options.chunkByDir)) + elif options.chunkByRuntime: + with open(runtime_file, 'r') as f: + runtime_data = json.loads(f.read()) + runtimes = runtime_data['runtimes'] + default = runtime_data['excluded_test_average'] + filters.append( + chunk_by_runtime(options.thisChunk, + options.totalChunks, + runtimes, + default_runtime=default)) + else: + filters.append(chunk_by_slice(options.thisChunk, + options.totalChunks)) + + tests = manifest.active_tests( + exists=False, disabled=disabled, filters=filters, **info) + + if len(tests) == 0: + self.log.error("no tests to run using specified " + "combination of filters: {}".format( + 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 pathAbs.startswith(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 + + testob = {'path': tp} + if 'disabled' in test: + testob['disabled'] = test['disabled'] + if 'expected' in test: + testob['expected'] = test['expected'] + paths.append(testob) + + def path_sort(ob1, ob2): + path1 = ob1['path'].split('/') + path2 = ob2['path'].split('/') + return cmp(path1, path2) + + paths.sort(path_sort) + self._active_tests = paths + 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': self._active_tests})) + + self.log.info("Dumping active_tests to %s file." % options.dump_tests) + sys.exit() + + 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: + 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, (basestring, 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' + 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"]: + lsanPath = SCRIPT_DIR + else: + lsanPath = None + + browserEnv = self.environment( + xrePath=options.xrePath, + env=env, + debugger=debugger, + dmdPath=options.dmdPath, + lsanPath=lsanPath) + + # These variables are necessary for correct application startup; change + # via the commandline at your own risk. + browserEnv["XPCOM_DEBUG_BREAK"] = "stack" + + # When creating child processes on Windows pre-Vista (e.g. Windows XP) we + # don't normally inherit stdout/err handles, because you can only do it by + # inheriting all other inheritable handles as well. + # We need to inherit them for plain mochitests for test logging purposes, so + # we do so on the basis of a specific environment variable. + if options.flavor == 'plain': + browserEnv["MOZ_WIN_INHERIT_STD_HANDLES_PRE_VISTA"] = "1" + + # interpolate environment passed with options + try: + browserEnv.update( + dict( + parseKeyValue( + options.environment, + context='--setenv'))) + except KeyValueParseError as e: + self.log.error(str(e)) + return None + + if not self.disable_leak_checking: + browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file + + 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 + + if debugger and not options.slowscript: + browserEnv["JS_DISABLE_SLOW_SCRIPT_SIGNALS"] = "1" + + # For e10s, our tests default to suppressing the "unsafe CPOW usage" + # warnings that can plague test logs. + if not options.enableCPOWWarnings: + browserEnv["DISABLE_UNSAFE_CPOW_WARNINGS"] = "1" + + return browserEnv + + def killNamedOrphans(self, pname): + """ Kill orphan processes matching the given command name """ + self.log.info("Checking for orphan %s processes..." % pname) + + def _psInfo(line): + if pname in line: + self.log.info(line) + + process = mozprocess.ProcessHandler(['ps', '-f'], + processOutputLine=_psInfo) + process.run() + process.wait() + + def _psKill(line): + parts = line.split() + if len(parts) == 3 and parts[0].isdigit(): + pid = int(parts[0]) + if parts[2] == pname and parts[1] == '1': + self.log.info("killing %s orphan with pid %d" % (pname, pid)) + killPid(pid, self.log) + process = mozprocess.ProcessHandler(['ps', '-o', 'pid,ppid,comm'], + processOutputLine=_psKill) + 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_args) + + 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 + + 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 buildProfile(self, options): + """ create the profile and add optional chrome bits and files if requested """ + if options.flavor == 'browser' and options.timeout: + options.extraPrefs.append( + "testing.browserTestHarness.timeout=%d" % + 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") + options.extraPrefs.append("testing.browserTestHarness.timeout=90") + + options.extraPrefs.append( + "browser.tabs.remote.autostart=%s" % + ('true' if options.e10s else 'false')) + if options.strictContentSandbox: + options.extraPrefs.append("security.sandbox.content.level=1") + options.extraPrefs.append( + "dom.ipc.tabs.nested.enabled=%s" % + ('true' if options.nested_oop else 'false')) + + # get extensions to install + extensions = self.getExtensionsToInstall(options) + + # web apps + appsPath = os.path.join( + SCRIPT_DIR, + 'profile_data', + 'webapps_mochitest.json') + if os.path.exists(appsPath): + with open(appsPath) as apps_file: + apps = json.load(apps_file) + else: + apps = None + + # preferences + preferences = [ + os.path.join( + SCRIPT_DIR, + 'profile_data', + 'prefs_general.js')] + + prefs = {} + for path in preferences: + prefs.update(Preferences.read_prefs(path)) + + prefs.update(self.extraPrefs(options.extraPrefs)) + + # Bug 1262954: For windows XP + e10s disable acceleration + if platform.system() in ("Windows", "Microsoft") and \ + '5.1' in platform.version() and options.e10s: + prefs['layers.acceleration.disabled'] = True + + # interpolate preferences + interpolation = { + "server": "%s:%s" % + (options.webServer, options.httpPort)} + + prefs = json.loads(json.dumps(prefs) % interpolation) + for pref in prefs: + prefs[pref] = Preferences.cast(prefs[pref]) + # TODO: make this less hacky + # https://bugzilla.mozilla.org/show_bug.cgi?id=913152 + + # 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) + proxy = {'remote': options.webServer, + 'http': options.httpPort, + 'https': options.sslPort, + 'ws': options.sslPort + } + + # See if we should use fake media devices. + if options.useTestMediaDevices: + prefs['media.audio_loopback_dev'] = self.mediaDevices['audio'] + prefs['media.video_loopback_dev'] = self.mediaDevices['video'] + + # create a profile + self.profile = Profile(profile=options.profilePath, + addons=extensions, + locations=self.locations, + preferences=prefs, + apps=apps, + proxy=proxy + ) + + # 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 + + 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): + """ remove temporary files and profile """ + if hasattr(self, 'manifest') and self.manifest is not None: + os.remove(self.manifest) + if hasattr(self, 'profile'): + del self.profile + if options.pidFile != "": + try: + os.remove(options.pidFile) + if os.path.exists(options.pidFile + ".xpcshell.pid"): + os.remove(options.pidFile + ".xpcshell.pid") + except: + self.log.warning( + "cleaning up pidfile '%s' was unsuccessful from the test harness" % + options.pidFile) + options.manifestFile = 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 + 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. + """ + if parent_pid and HAVE_PSUTIL: + self.log.info("Determining child pids from psutil") + return [p.pid for p in psutil.Process(parent_pid).children()] + + 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.append(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 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): + """ + 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 valgrindArgs.split(",")) + + 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() + + # 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_args.append(testUrl or 'about:blank') + + if detectShutdownLeaks and not self.disable_leak_checking: + shutdownLeaks = ShutdownLeaks(self.log) + else: + shutdownLeaks = None + + if mozinfo.info["asan"] and (mozinfo.isLinux or mozinfo.isMac) \ + and not self.disable_leak_checking: + 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] + + # 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) + self.log.process_start("Main app process") + + # start marionette and kick off the tests + marionette_args = marionette_args or {} + port_timeout = marionette_args.pop('port_timeout') + self.marionette = Marionette(**marionette_args) + self.marionette.start_session(timeout=port_timeout) + + # install specialpowers and mochikit as temporary addons + addons = Addons(self.marionette) + + if mozinfo.info.get('toolkit') != 'gonk': + addons.install(os.path.join(here, 'extensions', 'specialpowers'), temp=True) + addons.install(self.mochijar, temp=True) + + 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 + + # 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 + status = proc.wait() + 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() + self.log.error( + "TEST-UNEXPECTED-FAIL | %s | application terminated with exit code %s" % + (self.lastTestSeen, status)) + 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 + minidump_path = os.path.join(self.profile.profile, "minidumps") + crash_count = mozcrash.log_crashes( + self.log, + minidump_path, + symbolsPath, + test=self.lastTestSeen) + + if crash_count or zombieProcesses: + status = 1 + + finally: + # cleanup + if os.path.exists(processLog): + os.remove(processLog) + + return status + + 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. + """ + self.expectedError.clear() + self.result.clear() + options.manifestFile = None + options.profilePath = None + self.urlOpts = [] + + def resolve_runtime_file(self, options): + """ + Return a path to the runtimes file for a given flavor and + subsuite. + """ + template = "mochitest-{suite_slug}{e10s}.runtimes.json" + data_dir = os.path.join(SCRIPT_DIR, 'runtimes') + + # Determine the suite slug in the runtimes file name + slug = self.normflavor(options.flavor) + if slug == 'browser-chrome' and options.subsuite == 'devtools': + slug = 'devtools-chrome' + elif slug == 'mochitest': + slug = 'plain' + if options.subsuite: + slug = options.subsuite + + e10s = '' + if options.e10s: + e10s = '-e10s' + + return os.path.join(data_dir, template.format( + e10s=e10s, suite_slug=slug)) + + 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 getTestsToRun(self, options): + """ + This method makes a list of tests that are to be run. Required mainly for --bisect-chunk. + """ + tests = self.getActiveTests(options) + self.logPreamble(tests) + + testsToRun = [] + for test in tests: + if 'disabled' in test: + continue + testsToRun.append(test['path']) + + return testsToRun + + def runMochitests(self, options, testsToRun): + "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) + 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 runTests(self, options): + """ Prepare, configure, run tests and cleanup """ + + # a11y and chrome tests don't run with e10s enabled in CI. Need to set + # this here since |mach mochitest| sets the flavor after argument parsing. + if options.flavor in ('a11y', 'chrome'): + options.e10s = False + mozinfo.update({"e10s": options.e10s}) # for test manifest parsing. + + 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 orphan servers before + # trying to start new ones. + self.killNamedOrphans('ssltunnel') + self.killNamedOrphans('xpcshell') + + if options.cleanupCrashes: + mozcrash.cleanup_pending_crash_reports() + + # Until we have all green, this only runs on bc*/dt*/mochitest-chrome + # jobs, not jetpack*, a11yr (for perf reasons), or plain + + testsToRun = self.getTestsToRun(options) + if not options.runByDir: + return self.runMochitests(options, testsToRun) + + # code for --run-by-dir + dirs = self.getDirectories(options) + + result = 1 # default value, if no tests are run. + for d in dirs: + print "dir: %s" % d + + # BEGIN LEAKCHECK HACK + # Leak checking was broken in mochitest unnoticed for a length of time. During + # this time, several leaks slipped through. The leak checking was fixed by bug + # 1325148, but it couldn't land until all the regressions were also fixed or + # backed out. Rather than waiting and risking new regressions, in the meantime + # this code will selectively disable leak checking on flavors/directories where + # known regressions exist. At least this way we can prevent further damage while + # they get fixed. + + skip_leak_conditions = [ + (options.flavor in ('browser', 'chrome', 'plain') and d.startswith('toolkit/components/extensions/test/mochitest'), 'bug 1325158'), # noqa + ] + + for condition, reason in skip_leak_conditions: + if condition: + self.log.warning('WARNING | disabling leakcheck due to {}'.format(reason)) + self.disable_leak_checking = True + break + else: + self.disable_leak_checking = False + + # END LEAKCHECK HACK + + tests_in_dir = [t for t in testsToRun if os.path.dirname(t) == d] + + # If we are using --run-by-dir, 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. + result = self.runMochitests(options, tests_in_dir) + + # Dump the logging buffer + self.message_logger.dump_buffered() + + if result == -1: + break + + e10s_mode = "e10s" if options.e10s else "non-e10s" + + # 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" + + return result + + def doTests(self, options, testsToFilter=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.runByDir: + 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 + + # 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())) + + try: + self.startServers(options, debuggerInfo) + + # testsToFilter parameter is used to filter out the test list that + # is sent to buildTestPath + testURL = self.buildTestPath(options, testsToFilter) + + # read the number of tests here, if we are not going to run any, + # terminate early + if os.path.exists( + os.path.join( + SCRIPT_DIR, + options.testRunManifestFile)): + with open(os.path.join(SCRIPT_DIR, options.testRunManifestFile)) as fHandle: + tests = json.load(fHandle) + count = 0 + for test in tests['tests']: + count += 1 + if count == 0: + return 1 + + self.buildURLOptions(options, self.browserEnv) + if self.urlOpts: + testURL += "?" + "&".join(self.urlOpts) + + if options.immersiveMode: + options.browserArgs.extend(('-firefoxpath', options.app)) + options.app = self.immersiveHelperPath + + if options.jsdebugger: + options.browserArgs.extend(['-jsdebugger']) + + # 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: + timeout = 330.0 # default JS harness timeout is 300 seconds + + # 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' + + self.start_script_args.append(self.normflavor(options.flavor)) + marionette_args = { + 'symbols_path': options.symbolsPath, + 'socket_timeout': options.marionette_socket_timeout, + 'port_timeout': options.marionette_port_timeout, + 'startup_timeout': options.marionette_startup_timeout, + } + + if options.marionette: + host, port = options.marionette.split(':') + marionette_args['host'] = host + marionette_args['port'] = int(port) + + self.log.info("runtests.py | Running with e10s: {}".format(options.e10s)) + self.log.info("runtests.py | Running tests: start.\n") + status = self.runApp(testURL, + self.browserEnv, + options.app, + profile=self.profile, + extraArgs=options.browserArgs, + 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, + ) + except KeyboardInterrupt: + self.log.info("runtests.py | Received keyboard interrupt.\n") + status = -1 + except: + traceback.print_exc() + self.log.error( + "Automation Error: Received unexpected exception while running application\n") + status = 1 + finally: + self.stopServers() + + ignoreMissingLeaks = options.ignoreMissingLeaks + leakThresholds = options.leakThresholds + + # 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 + + mozleak.process_leak_log( + self.leak_report_file, + leak_thresholds=leakThresholds, + ignore_missing_leaks=ignoreMissingLeaks, + log=self.log, + stack_fixer=get_stack_fixer_function(options.utilityPath, + options.symbolsPath), + ) + + self.log.info("runtests.py | Running tests: end.") + + if self.manifest is not None: + self.cleanup(options) + + 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: + child_procs = [psutil.Process(pid) for pid in child_pids] + 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) + 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) + + 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 + + # With metro browser runs this script launches the metro test harness which launches + # the browser. The metro test harness hands back the real browser process id via log + # output which we need to pick up on and parse out. This variable tracks the real + # browser process id if we find it. + 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.shutdownLeaks.process() + + if self.lsanLeaks: + 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': + return message + + line = message['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 message['action'] == 'log' and self.stackFixerFunction: + message['message'] = self.stackFixerFunction(message['message']) + return message + + def record_last_test(self, message): + """record last test on harness""" + if message['action'] == 'test_start': + self.harness.lastTestSeen = 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'] == 'log': + self.lsanLeaks.log(message['message']) + return message + + def trackShutdownLeaks(self, message): + if self.shutdownLeaks: + self.shutdownLeaks.log(message) + return message + + def getDirectories(self, options): + """ + Make the list of directories by parsing manifests + """ + tests = self.getActiveTests(options) + dirlist = [] + for test in tests: + if 'disabled' in test: + continue + + rootdir = '/'.join(test['path'].split('/')[:-1]) + if rootdir not in dirlist: + dirlist.append(rootdir) + + return dirlist + + +def run_test_harness(parser, options): + parser.validate(options) + + logger_options = { + key: value for key, value in vars(options).iteritems() + if key.startswith('log') or key == 'valgrind'} + + runner = MochitestDesktop(logger_options, quiet=options.quiet) + + options.runByDir = False + + if options.flavor in ('plain', 'browser', 'chrome'): + options.runByDir = True + + result = runner.runTests(options) + + if runner.mozLogs: + with zipfile.ZipFile("{}/mozLogs.zip".format(runner.browserEnv["MOZ_UPLOAD_DIR"]), + "w", zipfile.ZIP_DEFLATED) as logzip: + for logfile in glob.glob("{}/moz*.log*".format(runner.browserEnv["MOZ_UPLOAD_DIR"])): + logzip.write(logfile) + os.remove(logfile) + logzip.close() + + # don't dump failures if running from automation as treeherder already displays them + if build_obj: + if runner.message_logger.errors: + result = 1 + runner.message_logger.logger.warning("The following tests failed:") + for error in runner.message_logger.errors: + runner.message_logger.logger.log_raw(error) + + 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 000000000..8010703e9 --- /dev/null +++ b/testing/mochitest/runtestsremote.py @@ -0,0 +1,392 @@ +# 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 sys +import traceback + +sys.path.insert( + 0, os.path.abspath( + os.path.realpath( + os.path.dirname(__file__)))) + +from automation import Automation +from remoteautomation import RemoteAutomation, fennecLogcatFilters +from runtests import MochitestDesktop, MessageLogger +from mochitest_options import MochitestArgumentParser + +import mozdevice +import mozinfo + +SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) + + +class MochiRemote(MochitestDesktop): + _automation = None + _dm = None + localProfile = None + logMessages = [] + + def __init__(self, automation, devmgr, options): + MochitestDesktop.__init__(self, options) + + self._automation = automation + self._dm = devmgr + self.environment = self._automation.environment + self.remoteProfile = os.path.join(options.remoteTestRoot, "profile/") + self.remoteModulesDir = os.path.join(options.remoteTestRoot, "modules/") + self._automation.setRemoteProfile(self.remoteProfile) + self.remoteLog = options.remoteLogFile + self.localLog = options.logFile + self._automation.deleteANRs() + self._automation.deleteTombstones() + self.certdbNew = True + self.remoteMozLog = os.path.join(options.remoteTestRoot, "mozlog") + self._dm.removeDir(self.remoteMozLog) + self._dm.mkDir(self.remoteMozLog) + self.remoteChromeTestDir = os.path.join( + options.remoteTestRoot, + "chrome") + self._dm.removeDir(self.remoteChromeTestDir) + self._dm.mkDir(self.remoteChromeTestDir) + + def cleanup(self, options): + if self._dm.fileExists(self.remoteLog): + self._dm.getFile(self.remoteLog, self.localLog) + self._dm.removeFile(self.remoteLog) + else: + self.log.warning( + "Unable to retrieve log file (%s) from remote device" % + self.remoteLog) + self._dm.removeDir(self.remoteProfile) + self._dm.removeDir(self.remoteChromeTestDir) + blobberUploadDir = os.environ.get('MOZ_UPLOAD_DIR', None) + if blobberUploadDir: + self._dm.getDirectory(self.remoteMozLog, blobberUploadDir) + MochitestDesktop.cleanup(self, options) + + 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 + + def makeLocalAutomation(self): + localAutomation = Automation() + localAutomation.IS_WIN32 = False + localAutomation.IS_LINUX = False + localAutomation.IS_MAC = False + localAutomation.UNIXISH = False + hostos = sys.platform + if (hostos == 'mac' or hostos == 'darwin'): + localAutomation.IS_MAC = True + elif (hostos == 'linux' or hostos == 'linux2'): + localAutomation.IS_LINUX = True + localAutomation.UNIXISH = True + elif (hostos == 'win32' or hostos == 'win64'): + localAutomation.BIN_SUFFIX = ".exe" + localAutomation.IS_WIN32 = True + return localAutomation + + # 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 + + localAutomation = self.makeLocalAutomation() + paths = [ + options.xrePath, + localAutomation.DIST_BIN, + self._automation._product, + os.path.join('..', self._automation._product) + ] + 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 localAutomation.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): + """ Create the servers on the host and start them up """ + restoreRemotePaths = self.switchToLocalPaths(options) + # ignoreSSLTunnelExts is a workaround for bug 1109310 + MochitestDesktop.startServers( + self, + options, + debuggerInfo, + ignoreSSLTunnelExts=True) + restoreRemotePaths() + + def buildProfile(self, options): + restoreRemotePaths = self.switchToLocalPaths(options) + if options.testingModulesDir: + try: + self._dm.pushDir(options.testingModulesDir, self.remoteModulesDir) + self._dm.chmodDir(self.remoteModulesDir) + except mozdevice.DMError: + 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 addChromeToProfile(self, options): + manifest = MochitestDesktop.addChromeToProfile(self, options) + + # Support Firefox (browser), SeaMonkey (navigator), and Webapp Runtime (webapp). + if options.flavor == 'chrome': + # append overlay to chrome.manifest + chrome = ("overlay chrome://browser/content/browser.xul " + "chrome://mochikit/content/browser-test-overlay.xul") + path = os.path.join(options.profilePath, 'extensions', 'staged', + 'mochikit@mozilla.org', 'chrome.manifest') + with open(path, "a") as f: + f.write(chrome) + return manifest + + def buildURLOptions(self, options, env): + self.localLog = options.logFile + options.logFile = self.remoteLog + options.fileLevel = 'INFO' + 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._dm.pushDir(options.profilePath, self.remoteProfile) + self._dm.chmodDir(self.remoteProfile) + except mozdevice.DMError: + self.log.error( + "Automation Error: Unable to copy profile to device.") + raise + + options.profilePath = self.remoteProfile + options.logFile = self.localLog + return retVal + + def getChromeTestDir(self, options): + local = super(MochiRemote, self).getChromeTestDir(options) + local = os.path.join(local, "chrome") + remote = self.remoteChromeTestDir + if options.flavor == 'chrome': + self.log.info("pushing %s to %s on device..." % (local, remote)) + self._dm.pushDir(local, remote) + return remote + + def getLogFilePath(self, logFile): + return logFile + + def printDeviceInfo(self, printLogcat=False): + try: + if printLogcat: + logcat = self._dm.getLogcat( + filterOutRegexps=fennecLogcatFilters) + self.log.info( + '\n' + + ''.join(logcat).decode( + 'utf-8', + 'replace')) + self.log.info("Device info:") + devinfo = self._dm.getInfo() + for category in devinfo: + if type(devinfo[category]) is list: + self.log.info(" %s:" % category) + for item in devinfo[category]: + self.log.info(" %s" % item) + else: + self.log.info(" %s: %s" % (category, devinfo[category])) + self.log.info("Test root: %s" % self._dm.deviceRoot) + except mozdevice.DMError: + self.log.warning("Error getting device information") + + def getGMPPluginPath(self, options): + # TODO: bug 1149374 + return None + + def buildBrowserEnv(self, options, debugger=False): + browserEnv = MochitestDesktop.buildBrowserEnv( + self, + options, + debugger=debugger) + # remove desktop environment not used on device + if "MOZ_WIN_INHERIT_STD_HANDLES_PRE_VISTA" in browserEnv: + del browserEnv["MOZ_WIN_INHERIT_STD_HANDLES_PRE_VISTA"] + if "XPCOM_MEM_BLOAT_LOG" in browserEnv: + del browserEnv["XPCOM_MEM_BLOAT_LOG"] + # override mozLogs to avoid processing in MochitestDesktop base class + self.mozLogs = None + browserEnv["MOZ_LOG_FILE"] = os.path.join( + self.remoteMozLog, + self.mozLogName) + return browserEnv + + def runApp(self, *args, **kwargs): + """front-end automation.py's `runApp` functionality until FennecRunner is written""" + + # automation.py/remoteautomation `runApp` takes the profile path, + # whereas runtest.py's `runApp` takes a mozprofile object. + if 'profileDir' not in kwargs and 'profile' in kwargs: + kwargs['profileDir'] = kwargs.pop('profile').profile + + # remove args not supported by automation.py + kwargs.pop('marionette_args', None) + + return self._automation.runApp(*args, **kwargs) + + +def run_test_harness(parser, options): + parser.validate(options) + + message_logger = MessageLogger(logger=None) + process_args = {'messageLogger': message_logger} + auto = RemoteAutomation(None, "fennec", processArgs=process_args) + + if options is None: + raise ValueError("Invalid options specified, use --help for a list of valid options") + + options.runByDir = False + # roboextender is used by mochitest-chrome tests like test_java_addons.html, + # but not by any plain mochitests + if options.flavor != 'chrome': + options.extensionsToExclude.append('roboextender@mozilla.org') + + dm = options.dm + auto.setDeviceManager(dm) + mochitest = MochiRemote(auto, dm, options) + + log = mochitest.log + message_logger.logger = log + mochitest.message_logger = message_logger + + # Check that Firefox is installed + expected = options.app.split('/')[-1] + installed = dm.shellCheckOutput(['pm', 'list', 'packages', expected]) + if expected not in installed: + log.error("%s is not installed on this device" % expected) + return 1 + + productPieces = options.remoteProductName.split('.') + if (productPieces is not None): + auto.setProduct(productPieces[0]) + else: + auto.setProduct(options.remoteProductName) + auto.setAppName(options.remoteappname) + + logParent = os.path.dirname(options.remoteLogFile) + dm.mkDir(logParent) + auto.setRemoteLog(options.remoteLogFile) + auto.setServerInfo(options.webServer, options.httpPort, options.sslPort) + + if options.log_mach is None: + mochitest.printDeviceInfo() + + # Add Android version (SDK level) to mozinfo so that manifest entries + # can be conditional on android_version. + androidVersion = dm.shellCheckOutput(['getprop', 'ro.build.version.sdk']) + log.info( + "Android sdk version '%s'; will use this to filter manifests" % + str(androidVersion)) + mozinfo.info['android_version'] = androidVersion + + deviceRoot = dm.deviceRoot + if options.dmdPath: + dmdLibrary = "libdmd.so" + dmdPathOnDevice = os.path.join(deviceRoot, dmdLibrary) + dm.removeFile(dmdPathOnDevice) + dm.pushFile(os.path.join(options.dmdPath, dmdLibrary), dmdPathOnDevice) + options.dmdPath = deviceRoot + + options.dumpOutputDirectory = deviceRoot + + procName = options.app.split('/')[-1] + dm.killProcess(procName) + + mochitest.mozLogName = "moz.log" + try: + dm.recordLogcat() + retVal = mochitest.runTests(options) + except: + log.error("Automation Error: Exception caught while running tests") + traceback.print_exc() + mochitest.stopServers() + try: + mochitest.cleanup(options) + except mozdevice.DMError: + # device error cleaning up... oh well! + pass + retVal = 1 + + if options.log_mach is None: + mochitest.printDeviceInfo(printLogcat=True) + + 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/server.js b/testing/mochitest/server.js new file mode 100644 index 000000000..112cb2200 --- /dev/null +++ b/testing/mochitest/server.js @@ -0,0 +1,759 @@ +/* -*- 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/. */ + +// Note that the server script itself already defines Cc, Ci, and Cr for us, +// and because they're constants it's not safe to redefine them. Scope leakage +// sucks. + +// Disable automatic network detection, so tests work correctly when +// not connected to a network. +var ios = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService2); +ios.manageOfflineStatus = false; +ios.offline = false; + +var server; // for use in the shutdown handler, if necessary + +// +// HTML GENERATION +// +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 "invalid _SERVER_ADDR, please specify a valid IP Address"; + } + } + } else { + throw "please defined _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 "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.nsILocalFile); + + if (typeof(_PROFILE_PATH) == "undefined") { + serverAlive.initWithFile(serverBasePath); + serverAlive.append("mochitesttestingprofile"); + } else { + serverAlive.initWithPath(_PROFILE_PATH); + } + + // If we're running outside of the test harness, there might + // not be a test profile directory present + 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(); + } + + 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.registerPathHandler("/nested_oop", nestedTest); + 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.setIndexHandler(defaultDirHandler); + + var serverRoot = + { + getFile: function getFile(path) + { + var file = serverBasePath.clone().QueryInterface(Ci.nsILocalFile); + path.split("/").forEach(function(p) { + file.appendRelativePath(p); + }); + return file; + }, + QueryInterface: function(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 "Syntax error in server-locations.txt, line " + lineno; + + var [, scheme, host, port, options] = match; + if (options) + { + if (options.split(",").indexOf("primary") >= 0) + { + if (seenPrimary) + { + throw "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()) { + var file = en.getNext(); + yield file.QueryInterface(Ci.nsILocalFile); + } +} + +/** + * 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.indexOf("SimpleTest") == -1) { + 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.indexOf(".js") == -1 && + filename.indexOf(".css") == -1 && + !/\^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, count] = 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) +{ + Cu.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 that has one remote iframe + */ +function nestedTest(metadata, response) +{ + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-type", "text/html;charset=utf-8", false); + response.write( + HTML( + HEAD( + TITLE("Mochitest | ", metadata.path), + LINK({rel: "stylesheet", + type: "text/css", href: "/static/harness.css"}), + SCRIPT({type: "text/javascript", + src: "/nested_setup.js"}), + SCRIPT({type: "text/javascript"}, + "window.onload = addPermissions; gTestURL = '/tests?" + metadata.queryString + "';") + ), + BODY( + DIV({class: "container"}, + DIV({class: "frameholder", id: "holder-div"}) + ) + ))); +} + +/** + * 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.indexOf('manifestFile') == -1) { + [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/StructuredLog.jsm"}), + 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 000000000..f754eaafe --- /dev/null +++ b/testing/mochitest/shutdown-leaks-collector.js @@ -0,0 +1,7 @@ +/* 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 run this code in a .jsm rather than here to avoid keeping the current +// compartment alive. +Components.utils.import("chrome://mochikit/content/ShutdownLeaksCollector.jsm"); diff --git a/testing/mochitest/ssltunnel/moz.build b/testing/mochitest/ssltunnel/moz.build new file mode 100644 index 000000000..b72dbc16e --- /dev/null +++ b/testing/mochitest/ssltunnel/moz.build @@ -0,0 +1,23 @@ +# -*- 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 +DISABLE_STL_WRAPPING = True + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-shadow'] diff --git a/testing/mochitest/ssltunnel/ssltunnel.cpp b/testing/mochitest/ssltunnel/ssltunnel.cpp new file mode 100644 index 000000000..a80fe624d --- /dev/null +++ b/testing/mochitest/ssltunnel/ssltunnel.cpp @@ -0,0 +1,1635 @@ +/* -*- 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 "key.h" +#include "ssl.h" +#include "sslproto.h" +#include "plhash.h" +#include "mozilla/Sprintf.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 +typedef struct { + 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_rc4_table; + PLHashTable* host_failhandshake_table; +} server_info_t; + +typedef struct { + 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; +} connection_info_t; + +typedef struct { + string fullHost; + bool matched; +} server_match_t; + +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; + +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; + else // (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_RC4 = 1 << 1, + FAIL_HANDSHAKE = 1 << 2, + USE_TLS1 = 1 << 4 +}; + +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_rc4_table, token)) { + *flags |= USE_RC4; + } + + if (PL_HashTableLookup(server_info->host_tls1_table, token)) { + *flags |= USE_TLS1; + } + + 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); + + if (clientAuth != caNone) + { + SSL_OptionSet(ssl_socket, SSL_REQUEST_CERTIFICATE, true); + SSL_OptionSet(ssl_socket, SSL_REQUIRE_CERTIFICATE, clientAuth == caRequire); + } + + if (flags & USE_SSL3) { + SSLVersionRange range = { SSL_LIBRARY_VERSION_3_0, + SSL_LIBRARY_VERSION_3_0 }; + SSL_VersionRangeSet(ssl_socket, &range); + } + + if (flags & USE_TLS1) { + SSLVersionRange range = { SSL_LIBRARY_VERSION_TLS_1_0, + SSL_LIBRARY_VERSION_TLS_1_0 }; + SSL_VersionRangeSet(ssl_socket, &range); + } + + if (flags & USE_RC4) { + for (uint16_t i = 0; i < SSL_NumImplementedCiphers; ++i) { + uint16_t cipher_id = SSL_ImplementedCiphers[i]; + switch (cipher_id) { + case TLS_ECDHE_ECDSA_WITH_RC4_128_SHA: + case TLS_ECDHE_RSA_WITH_RC4_128_SHA: + case TLS_RSA_WITH_RC4_128_SHA: + case TLS_RSA_WITH_RC4_128_MD5: + SSL_CipherPrefSet(ssl_socket, cipher_id, true); + break; + + default: + SSL_CipherPrefSet(ssl_socket, cipher_id, false); + break; + } + } + } + + 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_rc4_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; + } + else + { + 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; + PR_InitializeNetAddr(PR_IpAddrAny, 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 (vector<server_info_t>::iterator it = servers.begin(); + it != servers.end(); it++) + { + if (it->listen_port == portnumber) + return &(*it); + } + + 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_rc4_table(server_info_t* server) +{ + return server->host_rc4_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 + if (!strcmp(keyword, "listen")) + { + char* hostname = strtok2(_caret, ":", &_caret); + char* hostportstring = nullptr; + if (strcmp(hostname, "*")) + { + 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)) + { + 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_rc4_table = PL_NewHashTable(0, PL_HashString, PL_CompareStrings, + PL_CompareStrings, nullptr, nullptr);; + if (!server.host_rc4_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, "rc4")) { + return parseWeakCryptoConfig(keyword, _caret, get_rc4_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 freeTLS1HashItems(PLHashEntry *he, int i, void *arg) +{ + delete [] (char*)he->key; + return HT_ENUMERATE_REMOVE; +} + +int freeRC4HashItems(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 (vector<server_info_t>::iterator it = servers.begin(); + it != servers.end(); it++) { + // Not actually using this PRJob*... + // PRJob* server_job = + PR_QueueJob(threads, StartServer, &(*it), 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 (vector<server_info_t>::iterator it = servers.begin(); + it != servers.end(); it++) + { + PL_HashTableEnumerateEntries(it->host_cert_table, freeHostCertHashItems, nullptr); + PL_HashTableEnumerateEntries(it->host_clientauth_table, freeClientAuthHashItems, nullptr); + PL_HashTableEnumerateEntries(it->host_redir_table, freeHostRedirHashItems, nullptr); + PL_HashTableEnumerateEntries(it->host_ssl3_table, freeSSL3HashItems, nullptr); + PL_HashTableEnumerateEntries(it->host_tls1_table, freeTLS1HashItems, nullptr); + PL_HashTableEnumerateEntries(it->host_rc4_table, freeRC4HashItems, nullptr); + PL_HashTableEnumerateEntries(it->host_failhandshake_table, freeRC4HashItems, nullptr); + PL_HashTableDestroy(it->host_cert_table); + PL_HashTableDestroy(it->host_clientauth_table); + PL_HashTableDestroy(it->host_redir_table); + PL_HashTableDestroy(it->host_ssl3_table); + PL_HashTableDestroy(it->host_tls1_table); + PL_HashTableDestroy(it->host_rc4_table); + PL_HashTableDestroy(it->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 000000000..c45b4d8d3 --- /dev/null +++ b/testing/mochitest/start_desktop.js @@ -0,0 +1,15 @@ +/* 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 flavor = __webDriverArguments[0] +const url = __webDriverArguments[1] + +let wm = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator); +let win = wm.getMostRecentWindow("navigator:browser"); + +// 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/chrome.template.txt b/testing/mochitest/static/chrome.template.txt new file mode 100644 index 000000000..02afb2ccf --- /dev/null +++ b/testing/mochitest/static/chrome.template.txt @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER} +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug {BUGNUMBER}</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://global/skin"/> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug {BUGNUMBER} **/ + + + + + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER}">Mozilla Bug {BUGNUMBER}</a> +<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 000000000..8c90f9bcf --- /dev/null +++ b/testing/mochitest/static/chromexul.template.txt @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER} +--> +<window title="Mozilla Bug {BUGNUMBER}" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER}" + target="_blank">Mozilla Bug {BUGNUMBER}</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + /** Test for Bug {BUGNUMBER} **/ + + + + ]]> + </script> +</window> diff --git a/testing/mochitest/static/harness.css b/testing/mochitest/static/harness.css new file mode 100644 index 000000000..d6652c071 --- /dev/null +++ b/testing/mochitest/static/harness.css @@ -0,0 +1,127 @@ +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/test.template.txt b/testing/mochitest/static/test.template.txt new file mode 100644 index 000000000..1870cb4ef --- /dev/null +++ b/testing/mochitest/static/test.template.txt @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER} +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug {BUGNUMBER}</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug {BUGNUMBER} **/ + + + + + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER}">Mozilla Bug {BUGNUMBER}</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/testing/mochitest/static/th.template.txt b/testing/mochitest/static/th.template.txt new file mode 100644 index 000000000..cce145f49 --- /dev/null +++ b/testing/mochitest/static/th.template.txt @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test for ...</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +test(function() { + +}, "Description"); +</script> diff --git a/testing/mochitest/static/xhtml.template.txt b/testing/mochitest/static/xhtml.template.txt new file mode 100644 index 000000000..e8f3ec71f --- /dev/null +++ b/testing/mochitest/static/xhtml.template.txt @@ -0,0 +1,29 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER} +--> +<head> + <title>Test for Bug {BUGNUMBER}</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug {BUGNUMBER} **/ + + + + + ]]> +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER}">Mozilla Bug {BUGNUMBER}</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/testing/mochitest/static/xul.template.txt b/testing/mochitest/static/xul.template.txt new file mode 100644 index 000000000..5d42dd555 --- /dev/null +++ b/testing/mochitest/static/xul.template.txt @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER} +--> +<window title="Mozilla Bug {BUGNUMBER}" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug {BUGNUMBER} **/ + + + + ]]> + </script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER}" + target="_blank">Mozilla Bug {BUGNUMBER}</a> + </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 000000000..b4d5089ff --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/ImportTesting.jsm @@ -0,0 +1,5 @@ +this.EXPORTED_SYMBOLS = ["ImportTesting"]; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +// 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 000000000..731081832 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/SpecialPowersLoadChromeScript.js @@ -0,0 +1,15 @@ +// 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 000000000..e69de29bb --- /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 000000000..623460c52 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/file_SpecialPowersFrame1.html @@ -0,0 +1,14 @@ +<html> + <head> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" 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/importtesting_chromescript.js b/testing/mochitest/tests/Harness_sanity/importtesting_chromescript.js new file mode 100644 index 000000000..2c2f9bd55 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/importtesting_chromescript.js @@ -0,0 +1,3 @@ +addMessageListener("ImportTesting:IsModuleLoaded", function (msg) { + sendAsyncMessage("ImportTesting:IsModuleLoadedReply", Components.utils.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 000000000..e9f744102 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/mochitest.ini @@ -0,0 +1,44 @@ +[DEFAULT] +[test_TestsRunningAfterSimpleTestFinish.html] +skip-if = true #depends on fix for bug 1048446 +[test_add_task.html] +[test_createFiles.html] +[test_importInMainProcess.html] +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] +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_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 = toolkit == 'android' # bug 688052 +[test_sanitySimpletest.html] +subsuite = clipboard +skip-if = toolkit == 'android' # bug 688052 +[test_sanity_manifest.html] +skip-if = toolkit == 'android' # we use the old manifest style on android +fail-if = true +[test_sanity_manifest_pf.html] +skip-if = toolkit == 'android' # we use the old manifest style on android +fail-if = true +[test_spawn_task.html] +[test_sanity_waitForCondition.html] diff --git a/testing/mochitest/tests/Harness_sanity/specialPowers_framescript.js b/testing/mochitest/tests/Harness_sanity/specialPowers_framescript.js new file mode 100644 index 000000000..da1b87782 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/specialPowers_framescript.js @@ -0,0 +1,13 @@ +Components.utils.import("resource://gre/modules/Services.jsm"); + +var permChangedObs = { + observe: function(subject, topic, data) { + if (topic == 'perm-changed') { + var permission = subject.QueryInterface(Components.interfaces.nsIPermission); + var msg = { op: data, type: permission.type }; + sendAsyncMessage('perm-changed', msg); + } + } +}; + +Services.obs.addObserver(permChangedObs, 'perm-changed', false); 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 000000000..c07d3f7e2 --- /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 type="text/javascript" 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 000000000..299497e15 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersExtension.html @@ -0,0 +1,192 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers extension</title> + <script type="text/javascript" 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(); +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 + SpecialPowers.setBoolPref('extensions.checkCompatibility', true); + is(SpecialPowers.getBoolPref('extensions.checkCompatibility'), true, "Check to see if we can set a preference properly"); + SpecialPowers.clearUserPref('extensions.checkCompatibility'); + + // Test a int pref + SpecialPowers.setIntPref('extensions.foobar', 42); + is(SpecialPowers.getIntPref('extensions.foobar'), 42, "Check int pref"); + SpecialPowers.clearUserPref('extensions.foobar'); + + // Test a string pref + SpecialPowers.setCharPref("extensions.foobaz", "hi there"); + is(SpecialPowers.getCharPref("extensions.foobaz"), "hi there", "Check string pref"); + SpecialPowers.clearUserPref("extensions.foobaz"); + + // Test an invalid pref + var retVal = null; + try { + retVal = SpecialPowers.getBoolPref('extensions.checkCompat0123456789'); + } catch (ex) { + retVal = ex; + } + is(retVal, "Error getting pref 'extensions.checkCompat0123456789'", "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); + ok(SpecialPowers.can_QI(window)); + ok(SpecialPowers.do_QueryInterface(window, "nsIDOMWindow")); + is(SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(window, "nsIDOMWindow"), "document.nodeName"), "#document"); + + //try to run garbage collection + SpecialPowers.gc(); + + // + // Test the SpecialPowers wrapper. + // + + // Try some basic stuff with XHR. + var xhr2 = SpecialPowers.Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(SpecialPowers.Ci.nsIXMLHttpRequest); + is(xhr2.readyState, XMLHttpRequest.UNSENT, "Should be able to get props off privileged objects"); + var testURI = SpecialPowers.Cc['@mozilla.org/network/standard-url;1'] + .createInstance(SpecialPowers.Ci.nsIURI); + testURI.spec = "http://www.foobar.org/"; + 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 webnav = SpecialPowers.wrap(window).QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) + .getInterface(SpecialPowers.Ci.nsIWebNavigation); + webnav.QueryInterface(SpecialPowers.Ci.nsIDocShell); + ok(webnav.allowJavascript, "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 000000000..fc77aa82a --- /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 type="text/javascript" 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 000000000..4b78928bf --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers.loadChromeScript</title> + <script type="text/javascript" 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", endOfTest); +} + +function endOfTest() { + script.destroy(); + SimpleTest.finish(); +} + +script.sendAsyncMessage("foo", MESSAGE); + +/* + * [0][0] is because we're using one real message listener in SpecialPowersObserverAPI.js + * and dispatching that to multiple _chromeScriptListeners. The outer array comes + * from the message manager since there can be multiple real listeners. The inner + * array is for the return values of _chromeScriptListeners. + */ +is(script.sendSyncMessage("sync-message")[0][0], "Received a synchronous message.", + "Check sync return value"); + +</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 000000000..da29eadb8 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript_function.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers.loadChromeScript</title> + <script type="text/javascript" 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() { + // 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); +} + +function endOfTest() { + script.destroy(); + SimpleTest.finish(); +} + +script.sendAsyncMessage("foo", MESSAGE); + +/* + * [0][0] is because we're using one real message listener in SpecialPowersObserverAPI.js + * and dispatching that to multiple _chromeScriptListeners. The outer array comes + * from the message manager since there can be multiple real listeners. The inner + * array is for the return values of _chromeScriptListeners. + */ +is(script.sendSyncMessage("sync-message")[0][0], "Received a synchronous message.", + "Check sync return value"); + +</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 000000000..e9b545376 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadPrivilegedScript.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers.loadChromeScript</title> + <script type="text/javascript" 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(); + +function loadPrivilegedScriptTest() { + var Cc = Components.classes; + var Ci = Components.interfaces; + 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.toSource()); +} 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 000000000..709d2cc7d --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersPushPermissions.html @@ -0,0 +1,237 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers extension</title> + <script type="text/javascript" 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 ACCESS_ALLOW_FIRST_PARTY_ONLY = SpecialPowers.Ci.nsICookiePermission.ACCESS_ALLOW_FIRST_PARTY_ONLY; +const ACCESS_LIMIT_THIRD_PARTY = SpecialPowers.Ci.nsICookiePermission.ACCESS_LIMIT_THIRD_PARTY; + +const EXPIRE_TIME = SpecialPowers.Ci.nsIPermissionManager.EXPIRE_TIME; +// expire Setting: +// start expire time point +// ----|------------------------|----------- +// <------------------------> +// PERIOD +var start; +// PR_Now() that called in nsPermissionManager 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); + SpecialPowers.addPermission("pFIRSTPARTY", ACCESS_ALLOW_FIRST_PARTY_ONLY, document); + SpecialPowers.addPermission("pTHIRDPARTY", ACCESS_LIMIT_THIRD_PARTY, document); + + setTimeout(test1, 0); +} + +SimpleTest.waitForExplicitFinish(); + +function test1() { + if (!SpecialPowers.testPermission('pALLOW', ALLOW_ACTION, document)) { + dump('/**** allow not set ****/\n'); + setTimeout(test1, 0); + } else if (!SpecialPowers.testPermission('pDENY', DENY_ACTION, document)) { + dump('/**** deny not set ****/\n'); + setTimeout(test1, 0); + } else if (!SpecialPowers.testPermission('pPROMPT', PROMPT_ACTION, document)) { + dump('/**** prompt not set ****/\n'); + setTimeout(test1, 0); + } else if (!SpecialPowers.testPermission('pREMOVE', ALLOW_ACTION, document)) { + dump('/**** remove not set ****/\n'); + setTimeout(test1, 0); + } else if (!SpecialPowers.testPermission('pSESSION', ACCESS_SESSION, document)) { + dump('/**** ACCESS_SESSION not set ****/\n'); + setTimeout(test1, 0); + } else if (!SpecialPowers.testPermission('pFIRSTPARTY', ACCESS_ALLOW_FIRST_PARTY_ONLY, document)) { + dump('/**** ACCESS_ALLOW_FIRST_PARTY_ONLY not set ****/\n'); + setTimeout(test1, 0); + } else if (!SpecialPowers.testPermission('pTHIRDPARTY', ACCESS_LIMIT_THIRD_PARTY, document)) { + dump('/**** ACCESS_LIMIT_THIRD_PARTY not set ****/\n'); + setTimeout(test1, 0); + } else { + test2(); + } +} + +function test2() { + ok(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': 'pFIRSTPARTY', 'allow': true, 'context': document}, {'type': 'pTHIRDPARTY', 'allow': true, 'context': document}, {'type': 'pREMOVE', 'remove': true, 'context': document}], test3); +} + +function test3() { + ok(SpecialPowers.testPermission('pUNKNOWN', ALLOW_ACTION, document), 'pUNKNOWN value should have ALLOW permission'); + ok(SpecialPowers.testPermission('pPROMPT', ALLOW_ACTION, document), 'pPROMPT value should have ALLOW permission'); + ok(SpecialPowers.testPermission('pALLOW', DENY_ACTION, document), 'pALLOW should have DENY permission'); + ok(SpecialPowers.testPermission('pDENY', ALLOW_ACTION, document), 'pDENY should have ALLOW permission'); + ok(SpecialPowers.testPermission('pREMOVE', UNKNOWN_ACTION, document), 'pREMOVE should have REMOVE permission'); + ok(SpecialPowers.testPermission('pSESSION', ALLOW_ACTION, document), 'pSESSION should have ALLOW permission'); + ok(SpecialPowers.testPermission('pFIRSTPARTY', ALLOW_ACTION, document), 'pFIRSTPARTY should have ALLOW permission'); + ok(SpecialPowers.testPermission('pTHIRDPARTY', ALLOW_ACTION, document), 'pTHIRDPARTY 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); +} + +function test3b() { + ok(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); +} + +function test4() { + ok(SpecialPowers.testPermission('pUNKNOWN', DENY_ACTION, document), 'pUNKNOWN value should have DENY permission'); + ok(SpecialPowers.testPermission('pPROMPT', ALLOW_ACTION, document), 'pPROMPT value should have ALLOW permission'); + ok(SpecialPowers.testPermission('pALLOW', PROMPT_ACTION, document), 'pALLOW should have PROMPT permission'); + ok(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); +} + + +function test5() { + ok(SpecialPowers.testPermission('pUNKNOWN', UNKNOWN_ACTION, document), 'pUNKNOWN should have UNKNOWN permission'); + ok(SpecialPowers.testPermission('pALLOW', ALLOW_ACTION, document), 'pALLOW should have ALLOW permission'); + ok(SpecialPowers.testPermission('pDENY', DENY_ACTION, document), 'pDENY should have DENY permission'); + ok(SpecialPowers.testPermission('pPROMPT', PROMPT_ACTION, document), 'pPROMPT should have PROMPT permission'); + ok(SpecialPowers.testPermission('pREMOVE', ALLOW_ACTION, document), 'pREMOVE should have ALLOW permission'); + ok(SpecialPowers.testPermission('pSESSION', ACCESS_SESSION, document), 'pSESSION should have ACCESS_SESSION permission'); + ok(SpecialPowers.testPermission('pFIRSTPARTY', ACCESS_ALLOW_FIRST_PARTY_ONLY, document), 'pFIRSTPARTY should have ACCESS_ALLOW_FIRST_PARTY_ONLY permission'); + ok(SpecialPowers.testPermission('pTHIRDPARTY', ACCESS_LIMIT_THIRD_PARTY, document), 'pTHIRDPARTY should have ACCESS_LIMIT_THIRD_PARTY permission'); + + SpecialPowers.removePermission("pPROMPT", document); + SpecialPowers.removePermission("pALLOW", document); + SpecialPowers.removePermission("pDENY", document); + SpecialPowers.removePermission("pREMOVE", document); + SpecialPowers.removePermission("pSESSION", document); + SpecialPowers.removePermission("pFIRSTPARTY", document); + SpecialPowers.removePermission("pTHIRDPARTY", document); + + setTimeout(test6, 0); +} + +function test6() { + if (!SpecialPowers.testPermission('pALLOW', UNKNOWN_ACTION, document)) { + dump('/**** allow still set ****/\n'); + setTimeout(test6, 0); + } else if (!SpecialPowers.testPermission('pDENY', UNKNOWN_ACTION, document)) { + dump('/**** deny still set ****/\n'); + setTimeout(test6, 0); + } else if (!SpecialPowers.testPermission('pPROMPT', UNKNOWN_ACTION, document)) { + dump('/**** prompt still set ****/\n'); + setTimeout(test6, 0); + } else if (!SpecialPowers.testPermission('pREMOVE', UNKNOWN_ACTION, document)) { + dump('/**** remove still set ****/\n'); + setTimeout(test6, 0); + } else if (!SpecialPowers.testPermission('pSESSION', UNKNOWN_ACTION, document)) { + dump('/**** pSESSION still set ****/\n'); + setTimeout(test6, 0); + } else if (!SpecialPowers.testPermission('pFIRSTPARTY', UNKNOWN_ACTION, document)) { + dump('/**** pFIRSTPARTY still set ****/\n'); + setTimeout(test6, 0); + } else if (!SpecialPowers.testPermission('pTHIRDPARTY', UNKNOWN_ACTION, document)) { + dump('/**** pTHIRDPARTY 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(); + } + }); +} + +function permissionPollingCheck() { + var now = Number(Date.now()); + if (now < (start + PERIOD)) { + if (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 (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 nsPermissionManager 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 000000000..cc352aeaa --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersPushPrefEnv.html @@ -0,0 +1,211 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers extension</title> + <script type="text/javascript" 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"> +function starttest() { + try { + SpecialPowers.setBoolPref("test.bool", 1); + } catch(e) { + SpecialPowers.setBoolPref("test.bool", true); + } + try { + SpecialPowers.setIntPref("test.int", true); + } catch(e) { + SpecialPowers.setIntPref("test.int", 1); + } + SpecialPowers.setCharPref("test.char", 'test'); + + 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) { + 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_TestsRunningAfterSimpleTestFinish.html b/testing/mochitest/tests/Harness_sanity/test_TestsRunningAfterSimpleTestFinish.html new file mode 100644 index 000000000..7d2a97eb5 --- /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 type="text/javascript" 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_add_task.html b/testing/mochitest/tests/Harness_sanity/test_add_task.html new file mode 100644 index 000000000..0b551e453 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_add_task.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for mochitest add_task, found in SpawnTask.js</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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 1187701</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +// Check that we can 'add_task' a few times and all tasks run asynchronously before test finishes. + +add_task(function* () { + var x = yield Promise.resolve(1); + is(x, 1, "task yields Promise value as expected"); +}); + +add_task(function* () { + var x = yield [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]; + is(x.join(""), "123", "task yields Promise value as expected"); +}); + +add_task(function* () { + var x = yield (function* () { + return 3; + }()); + is(x, 3, "task yields generator function return value as expected"); +}); +</script> +</pre> +</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 000000000..d6205da34 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_bug649012.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=649012 +--> +<head> + <title>Test for Bug 649012</title> + <script type="application/javascript" src="/MochiKit/packed.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" 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 000000000..502592acc --- /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 type="text/javascript" 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_importInMainProcess.html b/testing/mochitest/tests/Harness_sanity/test_importInMainProcess.html new file mode 100644 index 000000000..569966074 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_importInMainProcess.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SpecialPowers.importInMainProcess</title> + <script type="text/javascript" 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(); + + var failed = false; + try { + 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); + + function handleFirstReply(aMsg) { + ok(!aMsg, "ImportTesting.jsm shouldn't be loaded before we import it"); + + try { + 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 000000000..3c9266685 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanity.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for mochitest harness sanity</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" 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(); +synthesizeKey("x", {}); +is($("testKeyEvent1").value, "x", "synthesizeKey should work"); +is(press1, true, "synthesizeKey should dispatch keyPress"); + +var press2 = false; +$("testKeyEvent2").focus(); +synthesizeKey("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 000000000..07efaaf4d --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanityEventUtils.html @@ -0,0 +1,188 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Profiling test suite for EventUtils</title> + <script type="text/javascript"> + var start = new Date(); + </script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript"> + var loadTime = new Date(); + </script> + <script type="text/javascript" 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( + function () { + SimpleTest.waitForExplicitFinish(); + var startTime = new Date(); + var check = false; + + /* test send* functions */ + $("testMouseEvent").addEventListener("click", function() { check=true; }, false); + sendMouseEvent({type:'click'}, "testMouseEvent"); + is(check, true, 'sendMouseEvent should dispatch click event'); + + check = false; + $("testKeyEvent").addEventListener("keypress", function() { check = true; }, false); + $("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 = ""; + + check = false; + $("testKeyEvent").focus(); + sendKey("DOWN"); + is(check, true, "sendKey should dispatch keyPress"); + + /* test synthesizeMouse* */ + //focus trick enables us to run this in iframes + $("radioTarget1").addEventListener('focus', function (aEvent) { + $("radioTarget1").removeEventListener('focus', arguments.callee, false); + 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); + }); + $("radioTarget1").focus(); + + //focus trick enables us to run this in iframes + $("textBoxA").addEventListener("focus", function (aEvent) { + $("textBoxA").removeEventListener("focus", arguments.callee, false); + check = false; + $("textBoxA").addEventListener("click", function() { check = true; }, false); + synthesizeMouseAtCenter($("textBoxA"), {}); + is(check, true, 'synthesizeMouse should dispatch mouse event'); + + check = false; + synthesizeMouseExpectEvent($("textBoxA"), 1, 1, {}, $("textBoxA"), "click", "synthesizeMouseExpectEvent should fire click event"); + is(check, true, 'synthesizeMouse should dispatch mouse event'); + }); + $("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", function() { check = true; }, false); + $("testKeyEvent").focus(); + synthesizeKey("a", {}); + is($("testKeyEvent").value, "a", "synthesizeKey should work"); + is(check, true, "synthesizeKey should dispatch keyPress"); + $("testKeyEvent").value = ""; + + check = false; + synthesizeKeyExpectEvent("a", {}, $("testKeyEvent"), "keypress"); + is($("testKeyEvent").value, "a", "synthesizeKey should work"); + is(check, true, "synthesizeKey should dispatch keyPress"); + $("testKeyEvent").value = ""; + + /* test synthesizeComposition */ + $("textBoxB").focus(); + check = false; + window.addEventListener("compositionstart", function() { check = true; }, false); + synthesizeComposition({ type: "compositionstart" }); + is(check, true, 'synthesizeComposition() should dispatch compositionstart'); + + check = false; + window.addEventListener("compositionupdate", function() { check = true; }, false); + synthesizeComposition({ type: "compositionupdate", data: "a" }); + is(check, false, 'synthesizeComposition() should not dispatch compositionupdate without error'); + + check = false; + window.addEventListener("text", function() { check = true; }, false); + synthesizeCompositionChange( + { "composition": + { "string": "a", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + } + ); + is(check, true, "synthesizeCompositionChange should cause dispatching a DOM text event"); + + synthesizeCompositionChange( + { "composition": + { "string": "a", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 1, "length": 0 } + } + ); + + check = false; + window.addEventListener("compositionend", function() { check = true; }, false); + synthesizeComposition({ type: "compositionend", data: "a" }); + is(check, false, 'synthesizeComposition() should not dispatch compositionend'); + + synthesizeComposition({ type: "compositioncommit", data: "a" }); + is(check, true, 'synthesizeComposition() should dispatch compositionend'); + + 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"); + 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 000000000..e1ed14836 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanityException.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test that uncaught exceptions in mochitests cause failures</title> + <script type="text/javascript" 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(); +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 000000000..57d04764f --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanityException2.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test that uncaught exceptions in mochitests cause failures</title> + <script type="text/javascript" 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(); + 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 000000000..0d870f77f --- /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 type="text/javascript" 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 000000000..6c8fc3524 --- /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 type="text/javascript" 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 000000000..105fa6614 --- /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 type="text/javascript" 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 000000000..1dc49a5d7 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanitySimpletest.html @@ -0,0 +1,95 @@ +<!--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 type="text/javascript" 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", "This should be true"); + 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; + }, + function () { + check = false; + } + ); + is(check, true, "waitForClipboard should work"); + + //use helper functions + 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 + 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 000000000..4b8e9f3a4 --- /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 type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script> + <script type="text/javascript"> + var loadTime = new Date(); + </script> + <script type="text/javascript" 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 000000000..af8d4fd31 --- /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 type="text/javascript" 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 000000000..169d43a61 --- /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 type="text/javascript" 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 (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 000000000..d8c791b4e --- /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 type="text/javascript" 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 000000000..1f71fdd5a --- /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 type="text/javascript" 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 000000000..5ac1daf2e --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_sanity_waitForCondition.html @@ -0,0 +1,53 @@ +<!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, diag) { + if (!captureFailure) { + SimpleTest.ok(cond, name, diag); + } else { + if (cond) { + SimpleTest.ok(false, `Expect a failure with "${name}"`); + } else { + capturedFailures.push(name); + } + } +}; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("test behavior SimpleTest.waitForCondition"); + +addLoadEvent(testNormal); + +function testNormal() { + var condition = false; + SimpleTest.waitForCondition(() => condition, () => { + ok(condition, "Should only be called when condition is true"); + SimpleTest.executeSoon(testTimeout); + }, "Shouldn't timeout"); + setTimeout(() => { condition = true; }, 1000); +} + +function testTimeout() { + captureFailure = true; + SimpleTest.waitForCondition(() => false, () => { + captureFailure = false; + is(capturedFailures.length, 1, "Should captured one failure"); + is(capturedFailures[0], "Should timeout", + "Should capture the failure passed in"); + SimpleTest.executeSoon(() => SimpleTest.finish()); + }, "Should timeout"); +} + +</script> +</body> +</html> diff --git a/testing/mochitest/tests/Harness_sanity/test_spawn_task.html b/testing/mochitest/tests/Harness_sanity/test_spawn_task.html new file mode 100644 index 000000000..326318746 --- /dev/null +++ b/testing/mochitest/tests/Harness_sanity/test_spawn_task.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for mochitest SpawnTask.js sanity</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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 1078657</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for sanity **/ +SimpleTest.waitForExplicitFinish(); + +var externalGeneratorFunction = function* () { + return 8; +}; + +var nestedFunction = function* () { + return yield function* () { + return yield function* () { + return yield function* () { + return yield Promise.resolve(9); + }(); + }(); + }(); +} + +var variousTests = function* () { + var val1 = yield [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]; + is(val1.join(""), "123", "Array of promises -> Promise.all"); + var val2 = yield Promise.resolve(2); + is(val2, 2, "Resolved promise yields value."); + var val3 = yield function* () { return 3; }; + is(val3, 3, "Generator functions are spawned."); + //var val4 = yield function () { return 4; }; + //is(val4, 4, "Plain functions run and return."); + var val5 = yield (function* () { return 5; }()); + is(val5, 5, "Generators are spawned."); + try { + var val6 = yield Promise.reject(Error("error6")); + ok(false, "Shouldn't reach this line."); + } catch (error) { + is(error.message, "error6", "Rejected promise throws error."); + } + try { + var val7 = yield function* () { throw Error("error7"); }; + ok(false, "Shouldn't reach this line."); + } catch (error) { + is(error.message, "error7", "Thrown error propagates."); + } + var val8 = yield externalGeneratorFunction(); + is(val8, 8, "External generator also spawned."); + var val9 = yield nestedFunction(); + is(val9, 9, "Nested generator functions work."); + return 10; +}; + +spawn_task(variousTests).then(function(result) { + is(result, 10, "spawn_task(...) returns promise"); + SimpleTest.finish(); +}); + + +</script> +</pre> +</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 000000000..4d0065bef --- /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 000000000..6c5da1592 --- /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 000000000..39b151c58 --- /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 000000000..a0d5bd8c3 --- /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 000000000..b43c7baa4 --- /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 000000000..cbb3c91b6 --- /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 000000000..b23b10208 --- /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 000000000..36b71537c --- /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 000000000..99f3155b8 --- /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 000000000..463ccd0b7 --- /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 000000000..95c4fe590 --- /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 000000000..1ec157ed2 --- /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 000000000..250d12eed --- /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 000000000..70288c7c1 --- /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 000000000..e428cad16 --- /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 000000000..d49513c45 --- /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 000000000..463cce2fd --- /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 000000000..1488110c4 --- /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 000000000..30bfb4c88 --- /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 000000000..df975544d --- /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 000000000..9fc0a3f8d --- /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 000000000..ed8916049 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/mochitest.ini @@ -0,0 +1,32 @@ +[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] +skip-if = (android_version == '18' && debug) # intermittent time-out +[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] +# This test is broken: "Error: JSAN is not defined ... Line: 10". +# (And is removed in future MochiKit v1.5) +#[test_MochiKit-JSAN.html] diff --git a/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Base.js b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_Base.js new file mode 100644 index 000000000..eb0c5f577 --- /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 000000000..17d418d8c --- /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.ok( Color.whiteColor().toRGBString(), "rgb(255,255,255)", "toRGBString white" ); + t.ok( 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 000000000..a7178086d --- /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 000000000..d3a3c5837 --- /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 000000000..dd18a8ff7 --- /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 000000000..15b7b8932 --- /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 000000000..b368e58b4 --- /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 000000000..7a321b955 --- /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 type="text/javascript" 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 000000000..037e18c81 --- /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 000000000..92e6b3de0 --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Base.html @@ -0,0 +1,34 @@ +<html> +<head> + <script type="text/javascript" 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 000000000..9b45618c6 --- /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 type="text/javascript" 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 000000000..6b274835e --- /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 type="text/javascript" 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 000000000..5fb07cdbf --- /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 type="text/javascript" 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 000000000..69d656ea1 --- /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 type="text/javascript" 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 000000000..5e9f9e6a1 --- /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 type="text/javascript" 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 000000000..198239b9d --- /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 type="text/javascript" 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 000000000..04854e88b --- /dev/null +++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/test_MochiKit-Iter.html @@ -0,0 +1,38 @@ +<html> +<head> + <script type="text/javascript" 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 000000000..53a0e0ed0 --- /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 000000000..d7eb9482b --- /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 type="text/javascript" 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 000000000..f75e7428b --- /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 type="text/javascript" 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 000000000..4e82b6907 --- /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 type="text/javascript" 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 000000000..ba5d00267 --- /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 type="text/javascript" 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 000000000..701db956f --- /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 type="text/javascript" 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 000000000..4fdab615c --- /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 type="text/javascript" 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 000000000..cbee78575 --- /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/AsyncUtilsContent.js b/testing/mochitest/tests/SimpleTest/AsyncUtilsContent.js new file mode 100644 index 000000000..0f1cc0608 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/AsyncUtilsContent.js @@ -0,0 +1,98 @@ +/* + * This code is used for handling synthesizeMouse in a content process. + * Generally it just delegates to EventUtils.js. + */ + +// 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. +var EventUtils = {}; +EventUtils.window = {}; +EventUtils.parent = EventUtils.window; +EventUtils._EU_Ci = Components.interfaces; +EventUtils._EU_Cc = Components.classes; +// EventUtils' `sendChar` function relies on the navigator to synthetize events. +EventUtils.navigator = content.document.defaultView.navigator; +EventUtils.KeyboardEvent = content.document.defaultView.KeyboardEvent; + +Services.scriptloader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils); + +addMessageListener("Test:SynthesizeMouse", (message) => { + let data = message.data; + let target = data.target; + if (typeof target == "string") { + target = content.document.querySelector(target); + } + else if (typeof data.targetFn == "string") { + let runnablestr = ` + (() => { + return (${data.targetFn}); + })();` + target = eval(runnablestr)(); + } + else { + target = message.objects.object; + } + + let left = data.x; + let top = data.y; + if (target) { + if (target.ownerDocument !== content.document) { + // Account for nodes found in iframes. + let cur = target; + do { + let frame = cur.ownerDocument.defaultView.frameElement; + let rect = frame.getBoundingClientRect(); + + left += rect.left; + top += rect.top; + + cur = frame; + } while (cur && cur.ownerDocument !== content.document); + + // node must be in this document tree. + if (!cur) { + sendAsyncMessage("Test:SynthesizeMouseDone", + { error: "target must be in the main document tree" }); + return; + } + } + + let rect = target.getBoundingClientRect(); + left += rect.left; + top += rect.top; + + if (data.event.centered) { + left += rect.width / 2; + top += rect.height / 2; + } + } + + let result; + if (data.event && data.event.wheel) { + EventUtils.synthesizeWheelAtPoint(left, top, data.event, content); + } else { + result = EventUtils.synthesizeMouseAtPoint(left, top, data.event, content); + } + sendAsyncMessage("Test:SynthesizeMouseDone", { defaultPrevented: result }); +}); + +addMessageListener("Test:SendChar", message => { + let result = EventUtils.sendChar(message.data.char, content); + sendAsyncMessage("Test:SendCharDone", { result, seq: message.data.seq }); +}); + +addMessageListener("Test:SynthesizeKey", message => { + EventUtils.synthesizeKey(message.data.key, message.data.event || {}, content); + sendAsyncMessage("Test:SynthesizeKeyDone", { seq: message.data.seq }); +}); + +addMessageListener("Test:SynthesizeComposition", message => { + let result = EventUtils.synthesizeComposition(message.data.event, content); + sendAsyncMessage("Test:SynthesizeCompositionDone", { result, seq: message.data.seq }); +}); + +addMessageListener("Test:SynthesizeCompositionChange", message => { + EventUtils.synthesizeCompositionChange(message.data.event, content); + sendAsyncMessage("Test:SynthesizeCompositionChangeDone", { seq: message.data.seq }); +}); diff --git a/testing/mochitest/tests/SimpleTest/ChromePowers.js b/testing/mochitest/tests/SimpleTest/ChromePowers.js new file mode 100644 index 000000000..97de57815 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/ChromePowers.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/. */ + +function ChromePowers(window) { + this.window = Components.utils.getWeakReference(window); + + // In the case of browser-chrome tests, we are running as a [ChromeWindow] + // and we have no window.QueryInterface available, content.window is what we need + if (typeof(window) == "ChromeWindow" && typeof(content.window) == "Window") { + this.DOMWindowUtils = bindDOMWindowUtils(content.window); + this.window = Components.utils.getWeakReference(content.window); + } else { + this.DOMWindowUtils = bindDOMWindowUtils(window); + } + + this.spObserver = new SpecialPowersObserverAPI(); + this.spObserver._sendReply = this._sendReply.bind(this); + this.listeners = new Map(); +} + +ChromePowers.prototype = new SpecialPowersAPI(); + +ChromePowers.prototype.toString = function() { return "[ChromePowers]"; }; +ChromePowers.prototype.sanityCheck = function() { return "foo"; }; + +// This gets filled in in the constructor. +ChromePowers.prototype.DOMWindowUtils = undefined; + +ChromePowers.prototype._sendReply = function(aOrigMsg, aType, aMsg) { + var msg = {'name':aType, 'json': aMsg, 'data': aMsg}; + if (!this.listeners.has(aType)) { + throw new Error(`No listener for ${aType}`); + } + this.listeners.get(aType)(msg); +}; + +ChromePowers.prototype._sendSyncMessage = function(aType, aMsg) { + var msg = {'name':aType, 'json': aMsg, 'data': aMsg}; + return [this._receiveMessage(msg)]; +}; + +ChromePowers.prototype._sendAsyncMessage = function(aType, aMsg) { + var msg = {'name':aType, 'json': aMsg, 'data': aMsg}; + this._receiveMessage(msg); +}; + +ChromePowers.prototype._addMessageListener = function(aType, aCallback) { + if (this.listeners.has(aType)) { + throw new Error(`unable to handle multiple listeners for ${aType}`); + } + this.listeners.set(aType, aCallback); +}; +ChromePowers.prototype._removeMessageListener = function(aType, aCallback) { + this.listeners.delete(aType); +}; + +ChromePowers.prototype.registerProcessCrashObservers = function() { + this._sendSyncMessage("SPProcessCrashService", { op: "register-observer" }); +}; + +ChromePowers.prototype.unregisterProcessCrashObservers = function() { + this._sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" }); +}; + +ChromePowers.prototype._receiveMessage = function(aMessage) { + switch (aMessage.name) { + case "SpecialPowers.Quit": + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup); + appStartup.quit(Ci.nsIAppStartup.eForceQuit); + break; + case "SPProcessCrashService": + if (aMessage.json.op == "register-observer" || aMessage.json.op == "unregister-observer") { + // Hack out register/unregister specifically for browser-chrome leaks + break; + } else if (aMessage.type == "crash-observed") { + for (let e of msg.dumpIDs) { + this._encounteredCrashDumpFiles.push(e.id + "." + e.extension); + } + } + default: + // All calls go here, because we need to handle SPProcessCrashService calls as well + return this.spObserver._receiveMessageAPI(aMessage); + } + return undefined; // Avoid warning. +}; + +ChromePowers.prototype.quit = function() { + // We come in here as SpecialPowers.quit, but SpecialPowers is really ChromePowers. + // For some reason this.<func> resolves to TestRunner, so using SpecialPowers + // allows us to use the ChromePowers object which we defined below. + SpecialPowers._sendSyncMessage("SpecialPowers.Quit", {}); +}; + +ChromePowers.prototype.focus = function(aWindow) { + // We come in here as SpecialPowers.focus, but SpecialPowers is really ChromePowers. + // For some reason this.<func> resolves to TestRunner, so using SpecialPowers + // allows us to use the ChromePowers object which we defined below. + if (aWindow) + aWindow.focus(); +}; + +ChromePowers.prototype.executeAfterFlushingMessageQueue = function(aCallback) { + aCallback(); +}; + +// Expose everything but internal APIs (starting with underscores) to +// web content. We cannot use Object.keys to view SpecialPowers.prototype since +// we are using the functions from SpecialPowersAPI.prototype +ChromePowers.prototype.__exposedProps__ = {}; +for (var i in ChromePowers.prototype) { + if (i.charAt(0) != "_") + ChromePowers.prototype.__exposedProps__[i] = "r"; +} + +if ((window.parent !== null) && + (window.parent !== undefined) && + (window.parent.wrappedJSObject.SpecialPowers) && + !(window.wrappedJSObject.SpecialPowers)) { + window.wrappedJSObject.SpecialPowers = window.parent.SpecialPowers; +} else { + window.wrappedJSObject.SpecialPowers = new ChromePowers(window); +} + diff --git a/testing/mochitest/tests/SimpleTest/EventUtils.js b/testing/mochitest/tests/SimpleTest/EventUtils.js new file mode 100644 index 000000000..17243625d --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/EventUtils.js @@ -0,0 +1,2143 @@ +/** + * EventUtils provides some utility methods for creating and sending DOM events. + * Current methods: + * sendMouseEvent + * sendDragEvent + * sendChar + * sendString + * sendKey + * sendWheelAndPaint + * synthesizeMouse + * synthesizeMouseAtCenter + * synthesizePointer + * synthesizeWheel + * synthesizeWheelAtPoint + * synthesizeKey + * synthesizeNativeKey + * synthesizeMouseExpectEvent + * synthesizeKeyExpectEvent + * synthesizeNativeClick + * + * When adding methods to this file, please add a performance test for it. + */ + +// 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. +window.__defineGetter__('_EU_Ci', function() { + // 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. + var c = Object.getOwnPropertyDescriptor(window, 'Components'); + return c.value && !c.writable ? Components.interfaces : SpecialPowers.Ci; +}); + +window.__defineGetter__('_EU_Cc', function() { + var c = Object.getOwnPropertyDescriptor(window, 'Components'); + return c.value && !c.writable ? Components.classes : SpecialPowers.Cc; +}); + +window.__defineGetter__('_EU_Cu', function() { + var c = Object.getOwnPropertyDescriptor(window, 'Components'); + return c.value && !c.writable ? Components.utils : SpecialPowers.Cu; +}); + +window.__defineGetter__("_EU_OS", function() { + delete this._EU_OS; + try { + this._EU_OS = this._EU_Cu.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; +} + +/** + * 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. + * E.g. to send an click event to the node with id 'node' you might do this: + * + * sendMouseEvent({type:'click'}, 'node'); + */ +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 sendMouseEvent(aEvent, aTarget, aWindow) { + if (['click', 'contextmenu', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout'].indexOf(aEvent.type) == -1) { + throw new Error("sendMouseEvent doesn't know about event type '" + aEvent.type + "'"); + } + + if (!aWindow) { + aWindow = window; + } + + if (typeof aTarget == "string") { + aTarget = aWindow.document.getElementById(aTarget); + } + + var event = aWindow.document.createEvent('MouseEvent'); + + var typeArg = aEvent.type; + var canBubbleArg = true; + var cancelableArg = true; + var viewArg = aWindow; + var detailArg = aEvent.detail || (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); + + return SpecialPowers.dispatchEvent(aWindow, aTarget, event); +} + +/** + * 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'].indexOf(aEvent.type) == -1) { + throw new Error("sendDragEvent doesn't know about event type '" + aEvent.type + "'"); + } + + if (typeof aTarget == "string") { + aTarget = aWindow.document.getElementById(aTarget); + } + + 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); + + var utils = _getDOMWindowUtils(aWindow); + return utils.dispatchDOMEventViaPresShell(aTarget, event, true); +} + +/** + * 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 == 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 navigator = _getNavigator(aWindow); + 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 + * + * If the type is specified, an mouse event of that type is fired. Otherwise, + * a mousedown followed by a mouse up 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(); + synthesizeTouchAtPoint(rect.left + aOffsetX, rect.top + aOffsetY, + aEvent, aWindow); +} +function synthesizePointer(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) +{ + var rect = aTarget.getBoundingClientRect(); + return synthesizePointerAtPoint(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 + * + * If the type is specified, an mouse event of that type is fired. Otherwise, + * a mousedown followed by a mouse up 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; + var inputSource = ("inputSource" in aEvent) ? aEvent.inputSource : 0; + var isDOMEventSynthesized = + ("isSynthesized" in aEvent) ? aEvent.isSynthesized : true; + var isWidgetEventSynthesized = + ("isWidgetEventSynthesized" in aEvent) ? aEvent.isWidgetEventSynthesized : false; + var buttons = ("buttons" in aEvent) ? aEvent.buttons : + utils.MOUSE_BUTTONS_NOT_SPECIFIED; + if (("type" in aEvent) && aEvent.type) { + defaultPrevented = utils.sendMouseEvent(aEvent.type, left, top, button, + clickCount, modifiers, false, + pressure, inputSource, + isDOMEventSynthesized, + isWidgetEventSynthesized, + buttons); + } + else { + utils.sendMouseEvent("mousedown", left, top, button, clickCount, modifiers, + false, pressure, inputSource, isDOMEventSynthesized, + isWidgetEventSynthesized, buttons); + utils.sendMouseEvent("mouseup", left, top, button, clickCount, modifiers, + false, pressure, inputSource, isDOMEventSynthesized, + isWidgetEventSynthesized, buttons); + } + } + + return defaultPrevented; +} + +function synthesizeTouchAtPoint(left, top, aEvent, aWindow = window) +{ + var utils = _getDOMWindowUtils(aWindow); + + if (utils) { + var id = aEvent.id || 0; + var rx = aEvent.rx || 1; + var ry = aEvent.rx || 1; + var angle = aEvent.angle || 0; + var force = aEvent.force || 1; + var modifiers = _parseModifiers(aEvent, aWindow); + + if (("type" in aEvent) && aEvent.type) { + utils.sendTouchEvent(aEvent.type, [id], [left], [top], [rx], [ry], [angle], [force], 1, modifiers); + } + else { + utils.sendTouchEvent("touchstart", [id], [left], [top], [rx], [ry], [angle], [force], 1, modifiers); + utils.sendTouchEvent("touchend", [id], [left], [top], [rx], [ry], [angle], [force], 1, modifiers); + } + } +} + +function synthesizePointerAtPoint(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; + var inputSource = ("inputSource" in aEvent) ? aEvent.inputSource : 0; + var synthesized = ("isSynthesized" in aEvent) ? aEvent.isSynthesized : true; + var isPrimary = ("isPrimary" in aEvent) ? aEvent.isPrimary : false; + + if (("type" in aEvent) && aEvent.type) { + defaultPrevented = utils.sendPointerEventToWindow(aEvent.type, left, top, button, + clickCount, modifiers, false, + pressure, inputSource, + synthesized, 0, 0, 0, 0, isPrimary); + } + else { + utils.sendPointerEventToWindow("pointerdown", left, top, button, clickCount, modifiers, false, pressure, inputSource); + utils.sendPointerEventToWindow("pointerup", left, top, button, clickCount, modifiers, false, pressure, inputSource); + } + } + + 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(); + synthesizeTouch(aTarget, rect.width / 2, 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; + } + } + var isNoLineOrPageDelta = aEvent.isNoLineOrPageDelta; + + // 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 = + aEvent.lineOrPageDeltaX != null ? aEvent.lineOrPageDeltaX : + aEvent.deltaX > 0 ? Math.floor(aEvent.deltaX) : + Math.ceil(aEvent.deltaX); + var lineOrPageDeltaY = + 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); +} + +/** + * 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) { + 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, 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", false); + aWindow.waitForAllPaintsFlushed(function() { + utils.restoreNormalRefresh(); + aCallback(); + }); + } + + SpecialPowers.Services.obs.addObserver(waitForPaints, "apz-repaints-flushed", false); + 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); + synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow); +} + +function synthesizeNativeMouseMove(aTarget, aOffsetX, aOffsetY, aCallback, aWindow = window) { + var utils = _getDOMWindowUtils(aWindow); + if (!utils) + return; + + var rect = aTarget.getBoundingClientRect(); + var x = aOffsetX + window.mozInnerScreenX + rect.left; + var y = aOffsetY + window.mozInnerScreenY + rect.top; + var scale = utils.screenPixelsPerCSSPixel; + + var observer = { + observe: (subject, topic, data) => { + if (aCallback && topic == "mouseevent") { + aCallback(data); + } + } + }; + utils.sendNativeMouseMove(x * scale, y * scale, null, observer); +} + +function _computeKeyCodeFromChar(aChar) +{ + if (aChar.length != 1) { + return 0; + } + var KeyEvent = _EU_Ci.nsIDOMKeyEvent; + if (aChar >= 'a' && aChar <= 'z') { + return KeyEvent.DOM_VK_A + aChar.charCodeAt(0) - 'a'.charCodeAt(0); + } + if (aChar >= 'A' && aChar <= 'Z') { + return KeyEvent.DOM_VK_A + aChar.charCodeAt(0) - 'A'.charCodeAt(0); + } + if (aChar >= '0' && aChar <= '9') { + return KeyEvent.DOM_VK_0 + aChar.charCodeAt(0) - '0'.charCodeAt(0); + } + // returns US keyboard layout's keycode + switch (aChar) { + case '~': + case '`': + return KeyEvent.DOM_VK_BACK_QUOTE; + case '!': + return KeyEvent.DOM_VK_1; + case '@': + return KeyEvent.DOM_VK_2; + case '#': + return KeyEvent.DOM_VK_3; + case '$': + return KeyEvent.DOM_VK_4; + case '%': + return KeyEvent.DOM_VK_5; + case '^': + return KeyEvent.DOM_VK_6; + case '&': + return KeyEvent.DOM_VK_7; + case '*': + return KeyEvent.DOM_VK_8; + case '(': + return KeyEvent.DOM_VK_9; + case ')': + return KeyEvent.DOM_VK_0; + case '-': + case '_': + return KeyEvent.DOM_VK_SUBTRACT; + case '+': + case '=': + return KeyEvent.DOM_VK_EQUALS; + case '{': + case '[': + return KeyEvent.DOM_VK_OPEN_BRACKET; + case '}': + case ']': + return KeyEvent.DOM_VK_CLOSE_BRACKET; + case '|': + case '\\': + return KeyEvent.DOM_VK_BACK_SLASH; + case ':': + case ';': + return KeyEvent.DOM_VK_SEMICOLON; + case '\'': + case '"': + return KeyEvent.DOM_VK_QUOTE; + case '<': + case ',': + return KeyEvent.DOM_VK_COMMA; + case '>': + case '.': + return KeyEvent.DOM_VK_PERIOD; + case '?': + case '/': + return KeyEvent.DOM_VK_SLASH; + case '\n': + return KeyEvent.DOM_VK_RETURN; + case ' ': + return KeyEvent.DOM_VK_SPACE; + default: + return 0; + } +} + +/** + * Synthesize a key event. It is targeted at whatever would be targeted by an + * actual keypress by the user, typically the focused element. + * + * aKey should be: + * - key value (recommended). If you specify a non-printable key name, + * append "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. + * + * aEvent is an object which may contain the properties: + * - code: If you emulates a physical keyboard's key event, this should be + * specified. + * - repeat: If you emulates auto-repeat, you should set the count of repeat. + * This method will automatically synthesize keydown (and keypress). + * - 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. + * - 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. + * - 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). + * - keyCode: Must be 0 - 255 (0xFF). If this is specified explicitly, + * .keyCode value is initialized with this value. + * + * aWindow is optional, and defaults to the current window object. + */ +function synthesizeKey(aKey, aEvent, aWindow = window) +{ + var TIP = _getTIP(aWindow); + if (!TIP) { + return; + } + var KeyboardEvent = _getKeyboardEvent(aWindow); + var modifiers = _emulateToActivateModifiers(TIP, aEvent, aWindow); + var keyEventDict = _createKeyboardEventDictionary(aKey, aEvent, aWindow); + var keyEvent = new KeyboardEvent("", keyEventDict.dictionary); + var dispatchKeydown = + !("type" in aEvent) || aEvent.type === "keydown" || !aEvent.type; + var dispatchKeyup = + !("type" in aEvent) || aEvent.type === "keyup" || !aEvent.type; + + try { + if (dispatchKeydown) { + TIP.keydown(keyEvent, keyEventDict.flags); + if ("repeat" in aEvent && aEvent.repeat > 1) { + keyEventDict.dictionary.repeat = true; + var repeatedKeyEvent = new KeyboardEvent("", keyEventDict.dictionary); + for (var i = 1; i < aEvent.repeat; i++) { + TIP.keydown(repeatedKeyEvent, keyEventDict.flags); + } + } + } + if (dispatchKeyup) { + TIP.keyup(keyEvent, keyEventDict.flags); + } + } finally { + _emulateToInactivateModifiers(TIP, modifiers, aWindow); + } +} + +function _parseNativeModifiers(aModifiers, aWindow = window) +{ + var navigator = _getNavigator(aWindow); + var modifiers; + if (aModifiers.capsLockKey) { + modifiers |= 0x00000001; + } + if (aModifiers.numLockKey) { + modifiers |= 0x00000002; + } + if (aModifiers.shiftKey) { + modifiers |= 0x00000100; + } + if (aModifiers.shiftRightKey) { + modifiers |= 0x00000200; + } + if (aModifiers.ctrlKey) { + modifiers |= 0x00000400; + } + if (aModifiers.ctrlRightKey) { + modifiers |= 0x00000800; + } + if (aModifiers.altKey) { + modifiers |= 0x00001000; + } + if (aModifiers.altRightKey) { + modifiers |= 0x00002000; + } + if (aModifiers.metaKey) { + modifiers |= 0x00004000; + } + if (aModifiers.metaRightKey) { + modifiers |= 0x00008000; + } + if (aModifiers.helpKey) { + modifiers |= 0x00010000; + } + if (aModifiers.fnKey) { + modifiers |= 0x00100000; + } + if (aModifiers.numericKeyPadKey) { + modifiers |= 0x01000000; + } + + if (aModifiers.accelKey) { + modifiers |= _EU_isMac(aWindow) ? 0x00004000 : 0x00000400; + } + if (aModifiers.accelRightKey) { + modifiers |= _EU_isMac(aWindow) ? 0x00008000 : 0x00000800; + } + if (aModifiers.altGrKey) { + modifiers |= _EU_isWin(aWindow) ? 0x00002800 : 0x00001000; + } + 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 }; +const KEYBOARD_LAYOUT_ARABIC_PC = + { name: "Arabic - PC", Mac: 7, Win: null }; +const KEYBOARD_LAYOUT_BRAZILIAN_ABNT = + { name: "Brazilian ABNT", Mac: null, Win: 0x00000416 }; +const KEYBOARD_LAYOUT_DVORAK_QWERTY = + { name: "Dvorak-QWERTY", Mac: 4, Win: null }; +const KEYBOARD_LAYOUT_EN_US = + { name: "US", Mac: 0, Win: 0x00000409 }; +const KEYBOARD_LAYOUT_FRENCH = + { name: "French", Mac: 8, Win: 0x0000040C }; +const KEYBOARD_LAYOUT_GREEK = + { name: "Greek", Mac: 1, Win: 0x00000408 }; +const KEYBOARD_LAYOUT_GERMAN = + { name: "German", Mac: 2, Win: 0x00000407 }; +const KEYBOARD_LAYOUT_HEBREW = + { name: "Hebrew", Mac: 9, Win: 0x0000040D }; +const KEYBOARD_LAYOUT_JAPANESE = + { name: "Japanese", Mac: null, Win: 0x00000411 }; +const KEYBOARD_LAYOUT_KHMER = + { name: "Khmer", Mac: null, Win: 0x00000453 }; // available on Win7 or later. +const KEYBOARD_LAYOUT_LITHUANIAN = + { name: "Lithuanian", Mac: 10, Win: 0x00010427 }; +const KEYBOARD_LAYOUT_NORWEGIAN = + { name: "Norwegian", Mac: 11, Win: 0x00000414 }; +const KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC = + { name: "Russian - Mnemonic", Mac: null, Win: 0x00020419 }; // available on Win8 or later. +const KEYBOARD_LAYOUT_SPANISH = + { name: "Spanish", Mac: 12, Win: 0x0000040A }; +const KEYBOARD_LAYOUT_SWEDISH = + { name: "Swedish", Mac: 3, Win: 0x0000041D }; +const KEYBOARD_LAYOUT_THAI = + { name: "Thai", Mac: 5, Win: 0x0002041E }; + +/** + * 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 navigator = _getNavigator(aWindow); + 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: function(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, false); + 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, false); + 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; + } + + // 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 + .QueryInterface(_EU_Ci.nsIInterfaceRequestor) + .getInterface(_EU_Ci.nsIDOMWindowUtils); +} + +function _defineConstant(name, value) { + Object.defineProperty(this, name, { + value: 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; +} + +function _getNavigator(aWindow = window) +{ + if (typeof navigator != "undefined") { + return navigator; + } + return aWindow.navigator; +} + +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_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, 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"; + if (aKey.indexOf("KEY_") == 0) { + keyName = aKey.substr("KEY_".length); + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY; + } else if (aKey.indexOf("VK_") == 0) { + keyCode = _EU_Ci.nsIDOMKeyEvent["DOM_" + aKey]; + if (!keyCode) { + throw "Unknown key: " + aKey; + } + keyName = _guessKeyNameFromKeyCode(keyCode, aWindow); + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY; + } else if (aKey != "") { + keyName = aKey; + if (!keyCodeIsDefined) { + keyCode = _computeKeyCodeFromChar(aKey.charAt(0)); + } + if (!keyCode) { + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO; + } + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY; + } + var locationIsDefined = "location" in aKeyEvent; + if (locationIsDefined && aKeyEvent.location === 0) { + result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEY_LOCATION_STANDARD; + } + result.dictionary = { + key: keyName, + code: "code" in aKeyEvent ? aKeyEvent.code : "", + location: locationIsDefined ? aKeyEvent.location : 0, + repeat: "repeat" in aKeyEvent ? aKeyEvent.repeat === true : false, + keyCode: keyCode, + }; + return result; +} + +function _emulateToActivateModifiers(aTIP, aKeyEvent, aWindow = window) +{ + if (!aKeyEvent) { + return null; + } + var KeyboardEvent = _getKeyboardEvent(aWindow); + var navigator = _getNavigator(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 (var i = 0; i < modifiers.normal.length; i++) { + if (!aKeyEvent[modifiers.normal[i].attr]) { + continue; + } + if (aTIP.getModifierState(modifiers.normal[i].key)) { + continue; // already activated. + } + var 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 (var i = 0; i < modifiers.lockable.length; i++) { + if (!aKeyEvent[modifiers.lockable[i].attr]) { + continue; + } + if (aTIP.getModifierState(modifiers.lockable[i].key)) { + continue; // already activated. + } + var 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 (var i = 0; i < aModifiers.normal.length; i++) { + if (!aModifiers.normal[i].activated) { + continue; + } + var event = new KeyboardEvent("", { key: aModifiers.normal[i].key }); + aTIP.keyup(event, + aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + } + for (var 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? + } + var 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. + * + * @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 specified, the key event may be + * dispatched. This can emulates changing + * composition state caused by key operation. + * Its key value should start with "KEY_" if the + * value is non-printable key name defined in D3E. + * @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 false; + } + var KeyboardEvent = _getKeyboardEvent(aWindow); + var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow); + var ret = false; + var keyEventDict = + "key" in aEvent ? + _createKeyboardEventDictionary(aEvent.key.key, aEvent.key, aWindow) : + { dictionary: null, flags: 0 }; + var keyEvent = + "key" in aEvent ? + new KeyboardEvent(aEvent.type === "keydown" ? "keydown" : "", + keyEventDict.dictionary) : + null; + try { + switch (aEvent.type) { + case "compositionstart": + ret = TIP.startComposition(keyEvent, keyEventDict.flags); + break; + case "compositioncommitasis": + ret = TIP.commitComposition(keyEvent, keyEventDict.flags); + break; + case "compositioncommit": + ret = TIP.commitCompositionWith(aEvent.data, keyEvent, + keyEventDict.flags); + break; + } + } finally { + _emulateToInactivateModifiers(TIP, modifiers, aWindow); + } +} +/** + * Synthesize a compositionchange event which causes a DOM text event and + * compositionupdate event if it's necessary. + * + * @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 specified, the key event may be dispatched. + * This can emulates changing composition state caused by key + * operation. Its key value should start with "KEY_" if the + * value is non-printable key name defined in D3E. + * + * @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"); + break; + } + } + } + + if (aEvent.caret) { + TIP.setCaretInPendingComposition(aEvent.caret.start); + } + + var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow); + try { + var keyEventDict = + "key" in aEvent ? + _createKeyboardEventDictionary(aEvent.key.key, aEvent.key, aWindow) : + { dictionary: null, flags: 0 }; + var keyEvent = + "key" in aEvent ? + new KeyboardEvent(aEvent.type === "keydown" ? "keydown" : "", + keyEventDict.dictionary) : + null; + 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 nullptr; + } + 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); + if (!utils) { + return null; + } + + 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 native mouse click event at a particular point in screen. + * This function should be used only for testing native event loop. + * Use synthesizeMouse instead for most case. + * + * This works only on OS X. Throws an error on other OS. Also throws an error + * when the library or any of function are not found, or something goes wrong + * in native functions. + */ +function synthesizeNativeOSXClick(x, y) +{ + var { ctypes } = _EU_Cu.import("resource://gre/modules/ctypes.jsm", {}); + + // Library + var CoreFoundation = ctypes.open("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"); + var CoreGraphics = ctypes.open("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics"); + + // Contants + var kCGEventLeftMouseDown = 1; + var kCGEventLeftMouseUp = 2; + var kCGEventSourceStateHIDSystemState = 1; + var kCGHIDEventTap = 0; + var kCGMouseButtonLeft = 0; + var kCGMouseEventClickState = 1; + + // Types + var CGEventField = ctypes.uint32_t; + var CGEventRef = ctypes.voidptr_t; + var CGEventSourceRef = ctypes.voidptr_t; + var CGEventSourceStateID = ctypes.uint32_t; + var CGEventTapLocation = ctypes.uint32_t; + var CGEventType = ctypes.uint32_t; + var CGFloat = ctypes.voidptr_t.size == 4 ? ctypes.float : ctypes.double; + var CGMouseButton = ctypes.uint32_t; + + var CGPoint = new ctypes.StructType( + "CGPoint", + [ { "x" : CGFloat }, + { "y" : CGFloat } ]); + + // Functions + var CGEventSourceCreate = CoreGraphics.declare( + "CGEventSourceCreate", + ctypes.default_abi, + CGEventSourceRef, CGEventSourceStateID); + var CGEventCreateMouseEvent = CoreGraphics.declare( + "CGEventCreateMouseEvent", + ctypes.default_abi, + CGEventRef, + CGEventSourceRef, CGEventType, CGPoint, CGMouseButton); + var CGEventSetIntegerValueField = CoreGraphics.declare( + "CGEventSetIntegerValueField", + ctypes.default_abi, + ctypes.void_t, + CGEventRef, CGEventField, ctypes.int64_t); + var CGEventPost = CoreGraphics.declare( + "CGEventPost", + ctypes.default_abi, + ctypes.void_t, + CGEventTapLocation, CGEventRef); + var CFRelease = CoreFoundation.declare( + "CFRelease", + ctypes.default_abi, + ctypes.void_t, + CGEventRef); + + var source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); + if (!source) { + throw new Error("CGEventSourceCreate returns null"); + } + + var loc = new CGPoint({ x: x, y: y }); + var event = CGEventCreateMouseEvent(source, kCGEventLeftMouseDown, loc, + kCGMouseButtonLeft); + if (!event) { + throw new Error("CGEventCreateMouseEvent returns null"); + } + CGEventSetIntegerValueField(event, kCGMouseEventClickState, + new ctypes.Int64(1)); + CGEventPost(kCGHIDEventTap, event); + CFRelease(event); + + event = CGEventCreateMouseEvent(source, kCGEventLeftMouseUp, loc, + kCGMouseButtonLeft); + if (!event) { + throw new Error("CGEventCreateMouseEvent returns null"); + } + CGEventSetIntegerValueField(event, kCGMouseEventClickState, + new ctypes.Int64(1)); + CGEventPost(kCGHIDEventTap, event); + CFRelease(event); + + CFRelease(source); + + CoreFoundation.close(); + CoreGraphics.close(); +} + +/** + * Emulate a dragstart event. + * element - element to fire the dragstart event on + * expectedDragData - the data you expect the data transfer to contain afterwards + * This data is in the format: + * [ [ {type: value, data: value, test: function}, ... ], ... ] + * can be null + * aWindow - optional; defaults to the current window object. + * x - optional; initial x coordinate + * y - optional; initial y coordinate + * Returns null if data matches. + * Returns the event.dataTransfer if data does not match + * + * eqTest is an optional function if comparison can't be done with x == y; + * function (actualData, expectedData) {return boolean} + * @param actualData from dataTransfer + * @param expectedData from expectedDragData + * see bug 462172 for example of use + * + */ +function synthesizeDragStart(element, expectedDragData, aWindow, x, y) +{ + if (!aWindow) + aWindow = window; + x = x || 2; + y = y || 2; + const step = 9; + + var result = "trapDrag was not called"; + var trapDrag = function(event) { + try { + // We must wrap only in plain mochitests, not chrome + var c = Object.getOwnPropertyDescriptor(window, 'Components'); + var dataTransfer = c.value && !c.writable + ? event.dataTransfer : SpecialPowers.wrap(event.dataTransfer); + result = null; + if (!dataTransfer) + throw "no dataTransfer"; + if (expectedDragData == null || + dataTransfer.mozItemCount != expectedDragData.length) + throw dataTransfer; + for (var i = 0; i < dataTransfer.mozItemCount; i++) { + var dtTypes = dataTransfer.mozTypesAt(i); + if (dtTypes.length != expectedDragData[i].length) + throw dataTransfer; + for (var j = 0; j < dtTypes.length; j++) { + if (dtTypes[j] != expectedDragData[i][j].type) + throw dataTransfer; + var dtData = dataTransfer.mozGetDataAt(dtTypes[j],i); + if (expectedDragData[i][j].eqTest) { + if (!expectedDragData[i][j].eqTest(dtData, expectedDragData[i][j].data)) + throw dataTransfer; + } + else if (expectedDragData[i][j].data != dtData) + throw dataTransfer; + } + } + } catch(ex) { + result = ex; + } + event.preventDefault(); + event.stopPropagation(); + } + aWindow.addEventListener("dragstart", trapDrag, false); + synthesizeMouse(element, x, y, { type: "mousedown" }, aWindow); + x += step; y += step; + synthesizeMouse(element, x, y, { type: "mousemove" }, aWindow); + x += step; y += step; + synthesizeMouse(element, x, y, { type: "mousemove" }, aWindow); + aWindow.removeEventListener("dragstart", trapDrag, false); + synthesizeMouse(element, x, y, { type: "mouseup" }, aWindow); + return result; +} + +/** + * 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 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, aWindow) +{ + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return nullptr; + } + return utils.sendQueryContentEvent(utils.QUERY_TEXT_RECT, + aOffset, aLength, 0, 0, + QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK); +} + +/** + * 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); + if (!utils) { + return nullptr; + } + 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); + if (!utils) { + return nullptr; + } + 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); + if (!utils) { + return nullptr; + } + 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; + } + return Object.assign({ type: aType, + screenX: destScreenX, screenY: destScreenY, + clientX: destClientX, clientY: destClientY, + dataTransfer: aDataTransfer }, aDragEvent); +} + +/** + * Emulate a event sequence of dragstart, dragenter, and dragover. + * + * @param aSrcElement The element to use to start the drag. + * @param aDestElement The element to fire the dragover, dragenter events + * @param 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 aDropEffect The drop effect to set during the dragstart event, or + * 'move' if null. + * @param aWindow Optional; Defaults to the current window object. + * @param aDestWindow Optional; Defaults to aWindow. + * Used when aDestElement is in a different window than + * aSrcElement. + * @param aDragEvent Optional; Defaults to empty object. Overwrites an object + * passed to sendDragEvent. + * @return 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; + } + + var dataTransfer; + var trapDrag = function(event) { + dataTransfer = event.dataTransfer; + if (aDragData) { + for (var i = 0; i < aDragData.length; i++) { + var item = aDragData[i]; + for (var j = 0; j < item.length; j++) { + dataTransfer.mozSetDataAt(item[j].type, item[j].data, i); + } + } + } + dataTransfer.dropEffect = aDropEffect || "move"; + event.preventDefault(); + }; + + // need to use real mouse action + aWindow.addEventListener("dragstart", trapDrag, true); + 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", trapDrag, true); + + var event = createDragEventObject("dragenter", aDestElement, aDestWindow, + dataTransfer, aDragEvent); + sendDragEvent(event, aDestElement, aDestWindow); + + 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 aDataTransfer The second element of the array returned from + * synthesizeDragOver. + * @param aDestElement The element to fire the drop event. + * @param aDestWindow Optional; Defaults to the current window object. + * @param aDragEvent Optional; Defaults to empty object. Overwrites an + * object passed to sendDragEvent. + * @return "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); + } + + synthesizeMouseAtCenter(aDestElement, { type: "mouseup" }, aDestWindow); + + return effect; +} + +/** + * Emulate a drag and drop by emulating a dragstart and firing events dragenter, + * dragover, and drop. + * + * @param aSrcElement The element to use to start the drag. + * @param aDestElement The element to fire the dragover, dragenter events + * @param 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 aDropEffect The drop effect to set during the dragstart event, or + * 'move' if null. + * @param aWindow Optional; Defaults to the current window object. + * @param aDestWindow Optional; Defaults to aWindow. + * Used when aDestElement is in a different window than + * aSrcElement. + * @param aDragEvent Optional; Defaults to empty object. Overwrites an object + * passed to sendDragEvent. + * @return 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); + + ds.startDragSession(); + + try { + var [result, dataTransfer] = synthesizeDragOver(aSrcElement, aDestElement, + aDragData, aDropEffect, + aWindow, aDestWindow, + aDragEvent); + return synthesizeDropAfterDragOver(result, dataTransfer, aDestElement, + aDestWindow, aDragEvent); + } finally { + ds.endDragSession(true); + } +} + +var PluginUtils = +{ + withTestPlugin : function(callback) + { + var ph = _EU_Cc["@mozilla.org/plugin/host;1"] + .getService(_EU_Ci.nsIPluginHost); + var tags = ph.getPluginTags(); + + // Find the test plugin + for (var i = 0; i < tags.length; i++) { + if (tags[i].name == "Test Plug-in") { + callback(tags[i]); + return true; + } + } + todo(false, "Need a test plugin on this platform"); + return false; + } +}; diff --git a/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js new file mode 100644 index 000000000..921d1a83f --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js @@ -0,0 +1,139 @@ +var ExtensionTestUtils = {}; + +ExtensionTestUtils.loadExtension = function(ext) +{ + // Cleanup functions need to be registered differently depending on + // whether we're in browser chrome or plain mochitests. + var registerCleanup; + 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, stack] = args; + SimpleTest.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`, undefined, stack); + } else if (kind == "test-log") { + SimpleTest.info(msg); + } else if (kind == "test-result") { + SimpleTest.ok(pass, msg, undefined, args[0]); + } + } + + var handler = { + testResult(kind, pass, msg, ...args) { + if (kind == "test-done") { + SimpleTest.ok(pass, msg, undefined, args[0]); + return testResolve(msg); + } + testHandler(kind, pass, msg, ...args); + }, + + testMessage(msg, ...args) { + var handler = messageHandler.get(msg); + if (handler) { + handler(...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") { + ext.files[filename] = `(${file})();` + } + } + } + if (typeof ext.background == "function") { + ext.background = `(${ext.background})();` + } + + var extension = SpecialPowers.loadExtension(ext, handler); + + registerCleanup(() => { + if (extension.state == "pending" || extension.state == "running") { + SimpleTest.ok(false, "Extension left running at test shutdown") + return 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; +} diff --git a/testing/mochitest/tests/SimpleTest/LICENSE_SpawnTask b/testing/mochitest/tests/SimpleTest/LICENSE_SpawnTask new file mode 100644 index 000000000..088c54c9d --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/LICENSE_SpawnTask @@ -0,0 +1,24 @@ +LICENSE for SpawnTask.js (the co library): + +(The MIT License) + +Copyright (c) 2014 TJ Holowaychuk <tj@vision-media.ca> + +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. diff --git a/testing/mochitest/tests/SimpleTest/LogController.js b/testing/mochitest/tests/SimpleTest/LogController.js new file mode 100644 index 000000000..52fe9eea8 --- /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 000000000..2af971184 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/MemoryStats.js @@ -0,0 +1,122 @@ +/* -*- 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 { + var value = 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 > 0) { + info("MEMORY STAT" + statMessage); + } + + if (dumpAboutMemory) { + var basename = "about-memory-" + testNumber + ".json.gz"; + var dumpfile = MemoryStats.constructPathname(dumpOutputDirectory, + basename); + info(testURL + " | MEMDUMP-START " + dumpfile); + var md = MemoryStats._getService("@mozilla.org/memory-info-dumper;1", + "nsIMemoryInfoDumper"); + md.dumpMemoryReportsToNamedFile(dumpfile, function () { + info("TEST-INFO | " + testURL + " | MEMDUMP-END"); + }, null, /* anonymize = */ false); + } + + // This is the old, deprecated function. + if (dumpDMD && typeof(DMDReportAndDump) != undefined) { + var basename = "dmd-" + testNumber + "-deprecated.txt"; + var dumpfile = MemoryStats.constructPathname(dumpOutputDirectory, + basename); + info(testURL + " | DMD-DUMP-deprecated " + dumpfile); + DMDReportAndDump(dumpfile); + } + + if (dumpDMD && typeof(DMDAnalyzeReports) != undefined) { + var basename = "dmd-" + testNumber + ".txt"; + var dumpfile = MemoryStats.constructPathname(dumpOutputDirectory, + basename); + info(testURL + " | DMD-DUMP " + dumpfile); + DMDAnalyzeReports(dumpfile); + } +}; diff --git a/testing/mochitest/tests/SimpleTest/MockObjects.js b/testing/mochitest/tests/SimpleTest/MockObjects.js new file mode 100644 index 000000000..d00f5127b --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/MockObjects.js @@ -0,0 +1,90 @@ +/* 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._originalFactory) + throw new Exception("Invalid object state when calling register()"); + + // Define a factory that creates a new object using the given constructor. + var providedConstructor = this._replacementCtor; + this._mockFactory = { + createInstance: function MF_createInstance(aOuter, aIid) { + if (aOuter != null) + throw SpecialPowers.Cr.NS_ERROR_NO_AGGREGATION; + return new providedConstructor().QueryInterface(aIid); + } + }; + + var retVal = SpecialPowers.swapFactoryRegistration(this._cid, this._contractID, this._mockFactory, this._originalFactory); + if ('error' in retVal) { + throw new Exception("ERROR: " + retVal.error); + } else { + this._cid = retVal.cid; + this._originalFactory = retVal.originalFactory; + } + }, + + /** + * Restores the original factory. + */ + unregister: function MOR_unregister() { + if (!this._originalFactory) + throw new Exception("Invalid object state when calling unregister()"); + + // Free references to the mock factory. + SpecialPowers.swapFactoryRegistration(this._cid, this._contractID, this._mockFactory, this._originalFactory); + + // Allow registering a mock factory again later. + this._cid = null; + this._originalFactory = null; + this._mockFactory = null; + }, + + // --- Private methods and properties --- + + /** + * The factory of the component being replaced. + */ + _originalFactory: null, + + /** + * The CID under which the mock contractID was registered. + */ + _cid: null, + + /** + * The nsIFactory that was automatically generated by this object. + */ + _mockFactory: null +} diff --git a/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js b/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js new file mode 100644 index 000000000..8130f3e18 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js @@ -0,0 +1,370 @@ +/** + * 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 000000000..37713737c --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js @@ -0,0 +1,1639 @@ +/* -*- js-indent-level: 4; tab-width: 4; indent-tabs-mode: nil -*- */ +/* vim:set ts=4 sw=4 sts=4 et: */ +/** + * SimpleTest, a partial Test.Simple/Test.More API compatible test library. + * + * Why? + * + * Test.Simple doesn't work on IE < 6. + * TODO: + * * Support the Test.Simple API used by MochiKit, to be able to test MochiKit + * itself against IE 5.5 + * + * NOTE: Pay attention to cross-browser compatibility in this file. For + * instance, do not use const or JS > 1.5 features which are not yet + * implemented everywhere. + * +**/ + +var SimpleTest = { }; +var parentRunner = null; + +// 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) +try { + var isPrimaryTestWindow = !!parent.TestRunner || isSingleTestRun; +} catch(e) { + dump("TEST-UNEXPECTED-FAIL, Exception caught: " + e.message + + ", at: " + e.fileName + " (" + e.lineNumber + + "), location: " + window.location.href + "\n"); +} +// 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.xul and we need SpecialPowers in the iframe, and also for tests +// like test_focus.xul 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; + } + + var w = ancestor(window); + while (w && (!parentRunner || !window.SpecialPowers)) { + if (!parentRunner) { + parentRunner = w.TestRunner; + if (!parentRunner && w.wrappedJSObject) { + parentRunner = w.wrappedJSObject.TestRunner; + } + } + if (!window.SpecialPowers) { + window.SpecialPowers = w.SpecialPowers; + } + w = ancestor(w); + } + + if (parentRunner) { + SimpleTest.harnessParameters = parentRunner.getParameterInfo(); + } +})(); + +/* Helper functions pulled out of various MochiKit modules */ +if (typeof(repr) == 'undefined') { + this.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(); + } + } 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 (var i = 1; i < arguments.length; i++) { + args.push(arguments[i]); + } + return function() { + if (arguments.length > 0) { + for (var 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 () { + var callStack = arguments.callee.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, null); + 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.expected = 'pass'; +SimpleTest.num_failed = 0; +SimpleTest._inChaosMode = false; + +SimpleTest.setExpected = function () { + if (parent.TestRunner) { + SimpleTest.expected = parent.TestRunner.expected; + } +} +SimpleTest.setExpected(); + +/** + * Something like assert. +**/ +SimpleTest.ok = function (condition, name, diag, stack = null) { + + var test = {'result': !!condition, 'name': name, 'diag': diag}; + if (SimpleTest.expected == 'fail') { + if (!test.result) { + SimpleTest.num_failed++; + test.result = !test.result; + } + var successInfo = {status:"FAIL", expected:"FAIL", message:"TEST-KNOWN-FAIL"}; + var failureInfo = {status:"PASS", expected:"FAIL", message:"TEST-UNEXPECTED-PASS"}; + } else { + var successInfo = {status:"PASS", expected:"PASS", message:"TEST-PASS"}; + var 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.ok(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.ok(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.ok(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': name, 'diag': diag, todo: true}; + 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 lastSlashIdx = path.lastIndexOf("/") + 1; + var filename = path.substr(lastSlashIdx); + 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 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 = 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. + */ +SimpleTest.requestLongerTimeout = function (factor) { + if (parentRunner) { + parentRunner.requestLongerTimeout(factor); + } +} + +/** + * 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: + * + * // 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. + * + * The reason parameter should be a string representation of the + * reason why using such flaky timeouts. + * + * 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! + */ +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; +} + +SimpleTest._pendingWaitForFocusCount = 0; + +/** + * Version of waitForFocus that returns a promise. The Promise will + * not resolve to the focused window, as it might be a CPOW (and Promises + * cannot be resolved with CPOWs). If you require the focused window, + * you should use waitForFocus instead. + */ +SimpleTest.promiseFocus = function *(targetWindow, expectBlankPage) +{ + return new Promise(function (resolve, reject) { + SimpleTest.waitForFocus(win => { + // Just resolve, without passing the window (see bug 1233497) + resolve(); + }, targetWindow, expectBlankPage); + }); +} + +/** + * If the page is not yet loaded, waits for the load event. In addition, if + * the page is not yet focused, focuses and waits for the window to be + * focused. Calls the callback when completed. 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. + * + * targetWindow should be specified if it is different than 'window'. The actual + * focused window may be a descendant of targetWindow. + * + * @param callback + * function called when load and focus are complete + * @param targetWindow + * optional window to be loaded and focused, defaults to 'window'. + * This may also be a <browser> element, in which case the window within + * that browser will be focused. + * @param expectBlankPage + * true if targetWindow.location is 'about:blank'. Defaults to false + */ +SimpleTest.waitForFocus = function (callback, targetWindow, expectBlankPage) { + // A separate method is used that is serialized and passed to the child + // process via loadFrameScript. Once the child window is focused, the + // child will send the WaitForFocus:ChildFocused notification to the parent. + // If a child frame in a child process must be focused, a + // WaitForFocus:FocusChild message is then sent to the child to focus that + // child. This message is used so that the child frame can be passed to it. + function waitForFocusInner(targetWindow, isChildProcess, expectBlankPage) + { + /* Indicates whether the desired targetWindow has loaded or focused. The + finished flag is set when the callback has been called and is used to + reject extraneous events from invoking the callback again. */ + var loaded = false, focused = false, finished = false; + + function info(msg) { + if (!isChildProcess) { + SimpleTest.info(msg); + } + } + + function focusedWindow() { + if (isChildProcess) { + return Components.classes["@mozilla.org/focus-manager;1"]. + getService(Components.interfaces.nsIFocusManager).focusedWindow; + } + return SpecialPowers.focusedWindow(); + } + + function getHref(aWindow) { + return isChildProcess ? aWindow.location.href : + SpecialPowers.getPrivilegedProps(aWindow, 'location.href'); + } + + /* Event listener for the load or focus events. It will also be called with + event equal to null to check if the page is already focused and loaded. */ + function focusedOrLoaded(event) { + try { + if (event) { + if (event.type == "load") { + if (expectBlankPage != (event.target.location == "about:blank")) { + return; + } + + loaded = true; + } else if (event.type == "focus") { + focused = true; + } + + event.currentTarget.removeEventListener(event.type, focusedOrLoaded, true); + } + + if (loaded && focused && !finished) { + finished = true; + if (isChildProcess) { + sendAsyncMessage("WaitForFocus:ChildFocused", {}, null); + } else { + SimpleTest._pendingWaitForFocusCount--; + SimpleTest.executeSoon(function() { callback(targetWindow) }); + } + } + } catch (e) { + if (!isChildProcess) { + SimpleTest.ok(false, "Exception caught in focusedOrLoaded: " + e.message + + ", at: " + e.fileName + " (" + e.lineNumber + ")"); + } + } + } + + function waitForLoadAndFocusOnWindow(desiredWindow) { + /* If the current document is about:blank and we are not expecting a blank + page (or vice versa), and the document has not yet loaded, wait for the + page to load. A common situation is to wait for a newly opened window + to load its content, and we want to skip over any intermediate blank + pages that load. This issue is described in bug 554873. */ + loaded = expectBlankPage ? + getHref(desiredWindow) == "about:blank" : + getHref(desiredWindow) != "about:blank" && + desiredWindow.document.readyState == "complete"; + if (!loaded) { + info("must wait for load"); + desiredWindow.addEventListener("load", focusedOrLoaded, true); + } + + var childDesiredWindow = { }; + if (isChildProcess) { + var fm = Components.classes["@mozilla.org/focus-manager;1"]. + getService(Components.interfaces.nsIFocusManager); + fm.getFocusedElementForWindow(desiredWindow, true, childDesiredWindow); + childDesiredWindow = childDesiredWindow.value; + } else { + childDesiredWindow = SpecialPowers.getFocusedElementForWindow(desiredWindow, true); + } + + /* If this is a child frame, ensure that the frame is focused. */ + focused = (focusedWindow() == childDesiredWindow); + if (!focused) { + info("must wait for focus"); + childDesiredWindow.addEventListener("focus", focusedOrLoaded, true); + if (isChildProcess) { + childDesiredWindow.focus(); + } + else { + SpecialPowers.focus(childDesiredWindow); + } + } + + focusedOrLoaded(null); + } + + if (isChildProcess) { + /* This message is used when an inner child frame must be focused. */ + addMessageListener("WaitForFocus:FocusChild", function focusChild(msg) { + removeMessageListener("WaitForFocus:FocusChild", focusChild); + finished = false; + waitForLoadAndFocusOnWindow(msg.objects.child); + }); + } + + waitForLoadAndFocusOnWindow(targetWindow); + } + + SimpleTest._pendingWaitForFocusCount++; + if (!targetWindow) { + targetWindow = window; + } + + expectBlankPage = !!expectBlankPage; + + // If this is a request to focus a remote child window, the request must + // be forwarded to the child process. + // XXXndeakin now sure what this issue with Components.utils is about, but + // browser tests require the former and plain tests require the latter. + var Cu = Components.utils || SpecialPowers.Cu; + var Ci = Components.interfaces || SpecialPowers.Ci; + + var browser = null; + if (typeof(XULElement) != "undefined" && + targetWindow instanceof XULElement && + targetWindow.localName == "browser") { + browser = targetWindow; + } + + var isWrapper = Cu.isCrossProcessWrapper(targetWindow); + if (isWrapper || (browser && browser.isRemoteBrowser)) { + var mustFocusSubframe = false; + if (isWrapper) { + // Look for a tabbrowser and see if targetWindow corresponds to one + // within that tabbrowser. If not, just return. + var tabBrowser = document.getElementsByTagName("tabbrowser")[0] || null; + browser = tabBrowser ? tabBrowser.getBrowserForContentWindow(targetWindow.top) : null; + if (!browser) { + SimpleTest.info("child process window cannot be focused"); + return; + } + + mustFocusSubframe = (targetWindow != targetWindow.top); + } + + // If a subframe in a child process needs to be focused, first focus the + // parent frame, then send a WaitForFocus:FocusChild message to the child + // containing the subframe to focus. + browser.messageManager.addMessageListener("WaitForFocus:ChildFocused", function waitTest(msg) { + if (mustFocusSubframe) { + mustFocusSubframe = false; + var mm = gBrowser.selectedBrowser.messageManager; + mm.sendAsyncMessage("WaitForFocus:FocusChild", {}, { child: targetWindow } ); + } + else { + browser.messageManager.removeMessageListener("WaitForFocus:ChildFocused", waitTest); + SimpleTest._pendingWaitForFocusCount--; + setTimeout(callback, 0, browser ? browser.contentWindowAsCPOW : targetWindow); + } + }); + + // Serialize the waitForFocusInner function and run it in the child process. + var frameScript = "data:,(" + waitForFocusInner.toString() + + ")(content, true, " + expectBlankPage + ");"; + browser.messageManager.loadFrameScript(frameScript, true); + browser.focus(); + } + else { + // Otherwise, this is an attempt to focus a single process or parent window, + // so pass false for isChildProcess. + if (browser) { + targetWindow = browser.contentWindow; + } + + waitForFocusInner(targetWindow, false, expectBlankPage); + } +}; + +SimpleTest.waitForClipboard_polls = 0; + +/* + * 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 aExpectedStringOrValidatorFn + * The string value that is expected to be on the clipboard or a + * validator function getting cripboard data and returning a bool. + * @param aSetupFn + * A function responsible for setting the clipboard to the expected value, + * called after the known value setting succeeds. + * @param aSuccessFn + * A function called when the expected value is found on the clipboard. + * @param 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 aFlavor [optional] The flavor to look for. Defaults to "text/unicode". + * @param aTimeout [optional] + * The timeout (in milliseconds) to wait for a clipboard change. + * Defaults to 5000. + * @param aExpectFailure [optional] + * 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. + * Defaults to false. + */ +SimpleTest.__waitForClipboardMonotonicCounter = 0; +SimpleTest.__defineGetter__("_waitForClipboardMonotonicCounter", function () { + return SimpleTest.__waitForClipboardMonotonicCounter++; +}); +SimpleTest.waitForClipboard = function(aExpectedStringOrValidatorFn, aSetupFn, + aSuccessFn, aFailureFn, aFlavor, aTimeout, aExpectFailure) { + var requestedFlavor = aFlavor || "text/unicode"; + + // The known value we put on the clipboard before running aSetupFn + var initialVal = SimpleTest._waitForClipboardMonotonicCounter + + "-waitForClipboard-known-value"; + + var 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; + }; + } else { + // Build a default validator function for common string input. + inputValidatorFn = typeof(aExpectedStringOrValidatorFn) == "string" + ? function(aData) { return aData == aExpectedStringOrValidatorFn; } + : aExpectedStringOrValidatorFn; + } + + var maxPolls = aTimeout ? aTimeout / 100 : 50; + + // reset for the next use + function reset() { + SimpleTest.waitForClipboard_polls = 0; + } + + var lastValue; + function wait(validatorFn, successFn, failureFn, flavor) { + if (SimpleTest.waitForClipboard_polls == 0) { + lastValue = undefined; + } + + if (++SimpleTest.waitForClipboard_polls > maxPolls) { + // Log the failure. + SimpleTest.ok(aExpectFailure, "Timed out while polling clipboard for pasted data"); + dump("Got this value: " + lastValue); + reset(); + failureFn(); + return; + } + + var data = SpecialPowers.getClipboardData(flavor); + + if (validatorFn(data)) { + // Don't show the success message when waiting for preExpectedVal + if (preExpectedVal) + preExpectedVal = null; + else + SimpleTest.ok(!aExpectFailure, "Clipboard has the given value"); + reset(); + successFn(); + } else { + lastValue = data; + SimpleTest._originalSetTimeout.apply(window, [function() { return wait(validatorFn, successFn, failureFn, flavor); }, 100]); + } + } + + // First we wait for a known value different from the expected one. + var preExpectedVal = initialVal; + SpecialPowers.clipboardCopyString(preExpectedVal); + wait(function(aData) { return aData == preExpectedVal; }, + function() { + // Call the original setup fn + aSetupFn(); + wait(inputValidatorFn, aSuccessFn, aFailureFn, requestedFlavor); + }, aFailureFn, "text/unicode"); +} + +/** + * Wait for a condition for a while (actually up to 3s here). + * + * @param aCond + * A function returns the result of the condition + * @param aCallback + * A function called after the condition is passed or timeout. + * @param aErrorMsg + * The message displayed when the condition failed to pass + * before timeout. + */ +SimpleTest.waitForCondition = function (aCond, aCallback, aErrorMsg) { + var tries = 0; + var interval = setInterval(() => { + if (tries >= 30) { + ok(false, aErrorMsg); + moveOn(); + return; + } + var conditionPassed; + try { + conditionPassed = aCond(); + } catch (e) { + ok(false, `${e}\n${e.stack}`); + conditionPassed = false; + } + if (conditionPassed) { + moveOn(); + } + tries++; + }, 100); + var moveOn = () => { clearInterval(interval); aCallback(); }; +}; +SimpleTest.promiseWaitForCondition = function (aCond, aErrorMsg) { + return new Promise(resolve => { + this.waitForCondition(aCond, resolve, aErrorMsg); + }); +}; + +/** + * Executes a function shortly after the call, but lets the caller continue + * working (or finish). + */ +SimpleTest.executeSoon = function(aFunc) { + if ("SpecialPowers" in window) { + return SpecialPowers.executeSoon(aFunc, window); + } + setTimeout(aFunc, 0); + return null; // Avoid warning. +}; + +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; +}; + +SimpleTest.timeout = function() { + for (let func of SimpleTest._timeoutFunctions) { + func(); + } + SimpleTest._timeoutFunctions = []; +} + +/** + * Finishes the tests. This is automatically called, except when + * SimpleTest.waitForExplicitFinish() has been invoked. +**/ +SimpleTest.finish = function() { + if (SimpleTest._alreadyFinished) { + var err = "[SimpleTest.finish()] this test already called finish!"; + if (parentRunner) { + parentRunner.structuredLogger.error(err); + } else { + dump(err + '\n'); + } + } + + if (SimpleTest.expected == 'fail' && SimpleTest.num_failed <= 0) { + msg = 'We expected at least one failure'; + var test = {'result': false, 'name': 'fail-if condition in manifest', 'diag': msg}; + var successInfo = {status:"FAIL", expected:"FAIL", message:"TEST-KNOWN-FAIL"}; + var failureInfo = {status:"PASS", expected:"FAIL", message:"TEST-UNEXPECTED-PASS"}; + + 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 = 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._pendingWaitForFocusCount != 0) { + SimpleTest.is(SimpleTest._pendingWaitForFocusCount, 0, + "[SimpleTest.finish()] waitForFocus() was called a " + + "different number of times from the number of " + + "callbacks run. Maybe the test terminated " + + "prematurely -- be sure to use " + + "SimpleTest.waitForExplicitFinish()."); + } + if (SimpleTest._tests.length == 0) { + 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.)"); + } + if (SimpleTest._expectingRegisteredServiceWorker) { + if (!SpecialPowers.isServiceWorkerRegistered()) { + SimpleTest.ok(false, "This test is expected to leave a service worker registered"); + } + } else { + if (SpecialPowers.isServiceWorkerRegistered()) { + SimpleTest.ok(false, "This test left a service worker registered without cleaning it up"); + } + } + + 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(); + }); + }); + } + } + + 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(); +}; + +/** + * 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, isException, isStrict: 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) { + var 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 = msgs.toSource(); + function listener(msg) { + if (msg.message === "SENTINEL" && !msg.isScriptError) { + is(counter, msgs.length, + "monitorConsole | number of messages " + assertionLabel); + SimpleTest.executeSoon(continuation); + return; + } + for (var 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 (var 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; for (var i in o1) o1Size++; + var o2Size = 0; for (var i in o2) o2Size++; + var bigger = o1Size > o2Size ? o1 : o2; + for (var 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 (var 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 (var 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.ok(true, name); + } else { + SimpleTest.ok(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 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.ok(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.indexOf("Mobile") == -1 && + nav.userAgent.indexOf("Tablet") == -1) { + 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.indexOf("Android") != -1 ? + 'version' : 'sdk_version'; + gAndroidSdk = SpecialPowers.Cc['@mozilla.org/system-info;1'] + .getService(SpecialPowers.Ci.nsIPropertyBag2) + .getProperty(versionString); + } + document.documentElement.removeChild(iframe); + } + return gAndroidSdk; +} diff --git a/testing/mochitest/tests/SimpleTest/SpawnTask.js b/testing/mochitest/tests/SimpleTest/SpawnTask.js new file mode 100644 index 000000000..7ac598f88 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/SpawnTask.js @@ -0,0 +1,296 @@ +// # SpawnTask.js +// Directly copied from the "co" library by TJ Holowaychuk. +// See https://github.com/tj/co/tree/4.6.0 +// For use with mochitest-plain and mochitest-chrome. + +// spawn_task(generatorFunction): +// Expose only the `co` function, which is very similar to Task.spawn in Task.jsm. +// We call this function spawn_task to make its purpose more plain, and to +// reduce the chance of name collisions. +var spawn_task = (function () { + +/** + * slice() reference. + */ + +var slice = Array.prototype.slice; + +/** + * Wrap the given generator `fn` into a + * function that returns a promise. + * This is a separate function so that + * every `co()` call doesn't create a new, + * unnecessary closure. + * + * @param {GeneratorFunction} fn + * @return {Function} + * @api public + */ + +co.wrap = function (fn) { + createPromise.__generatorFunction__ = fn; + return createPromise; + function createPromise() { + return co.call(this, fn.apply(this, arguments)); + } +}; + +/** + * Execute the generator function or a generator + * and return a promise. + * + * @param {Function} fn + * @return {Promise} + * @api public + */ + +function co(gen) { + var ctx = this; + var args = slice.call(arguments, 1) + + // we wrap everything in a promise to avoid promise chaining, + // which leads to memory leak errors. + // see https://github.com/tj/co/issues/180 + return new Promise(function(resolve, reject) { + if (typeof gen === 'function') gen = gen.apply(ctx, args); + if (!gen || typeof gen.next !== 'function') return resolve(gen); + + onFulfilled(); + + /** + * @param {Mixed} res + * @return {Promise} + * @api private + */ + + function onFulfilled(res) { + var ret; + try { + ret = gen.next(res); + } catch (e) { + return reject(e); + } + next(ret); + } + + /** + * @param {Error} err + * @return {Promise} + * @api private + */ + + function onRejected(err) { + var ret; + try { + ret = gen.throw(err); + } catch (e) { + return reject(e); + } + next(ret); + } + + /** + * Get the next value in the generator, + * return a promise. + * + * @param {Object} ret + * @return {Promise} + * @api private + */ + + function next(ret) { + if (ret.done) return resolve(ret.value); + var value = toPromise.call(ctx, ret.value); + if (value && isPromise(value)) return value.then(onFulfilled, onRejected); + return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + + 'but the following object was passed: "' + String(ret.value) + '"')); + } + }); +} + +/** + * Convert a `yield`ed value into a promise. + * + * @param {Mixed} obj + * @return {Promise} + * @api private + */ + +function toPromise(obj) { + if (!obj) return obj; + if (isPromise(obj)) return obj; + if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); + if ('function' == typeof obj) return thunkToPromise.call(this, obj); + if (Array.isArray(obj)) return arrayToPromise.call(this, obj); + if (isObject(obj)) return objectToPromise.call(this, obj); + return obj; +} + +/** + * Convert a thunk to a promise. + * + * @param {Function} + * @return {Promise} + * @api private + */ + +function thunkToPromise(fn) { + var ctx = this; + return new Promise(function (resolve, reject) { + fn.call(ctx, function (err, res) { + if (err) return reject(err); + if (arguments.length > 2) res = slice.call(arguments, 1); + resolve(res); + }); + }); +} + +/** + * Convert an array of "yieldables" to a promise. + * Uses `Promise.all()` internally. + * + * @param {Array} obj + * @return {Promise} + * @api private + */ + +function arrayToPromise(obj) { + return Promise.all(obj.map(toPromise, this)); +} + +/** + * Convert an object of "yieldables" to a promise. + * Uses `Promise.all()` internally. + * + * @param {Object} obj + * @return {Promise} + * @api private + */ + +function objectToPromise(obj){ + var results = new obj.constructor(); + var keys = Object.keys(obj); + var promises = []; + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var promise = toPromise.call(this, obj[key]); + if (promise && isPromise(promise)) defer(promise, key); + else results[key] = obj[key]; + } + return Promise.all(promises).then(function () { + return results; + }); + + function defer(promise, key) { + // predefine the key in the result + results[key] = undefined; + promises.push(promise.then(function (res) { + results[key] = res; + })); + } +} + +/** + * Check if `obj` is a promise. + * + * @param {Object} obj + * @return {Boolean} + * @api private + */ + +function isPromise(obj) { + return 'function' == typeof obj.then; +} + +/** + * Check if `obj` is a generator. + * + * @param {Mixed} obj + * @return {Boolean} + * @api private + */ + +function isGenerator(obj) { + return 'function' == typeof obj.next && 'function' == typeof obj.throw; +} + +/** + * Check if `obj` is a generator function. + * + * @param {Mixed} obj + * @return {Boolean} + * @api private + */ +function isGeneratorFunction(obj) { + var constructor = obj.constructor; + if (!constructor) return false; + if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true; + return isGenerator(constructor.prototype); +} + +/** + * Check for plain object. + * + * @param {Mixed} val + * @return {Boolean} + * @api private + */ + +function isObject(val) { + return Object == val.constructor; +} + +return co; +})(); + +// 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 = []; + // The "add_task" function + return function (generatorFunction) { + 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 () { + spawn_task(function* () { + // 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 || ""; + info("SpawnTask.js | Entering test " + name); + yield task(); + info("SpawnTask.js | Leaving test " + name); + } + } catch (ex) { + try { + ok(false, "" + ex); + } catch (ex2) { + ok(false, "(The exception cannot be converted to string.)"); + } + } + // All tasks are finished. + SimpleTest.finish(); + }); + }); + } + // Add the task to the list of tasks to run after + // the main thread is finished. + task_list.push(generatorFunction); + }; +})(); diff --git a/testing/mochitest/tests/SimpleTest/TestRunner.js b/testing/mochitest/tests/SimpleTest/TestRunner.js new file mode 100644 index 000000000..aa0af2f20 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/TestRunner.js @@ -0,0 +1,754 @@ +/* -*- js-indent-level: 4; indent-tabs-mode: nil -*- */ +/* + * e10s event dispatcher from content->chrome + * + * type = eventName (QuitApplication) + * data = json object {"filename":filename} <- for LoggerInit + */ + +"use strict"; + +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": sync, + "type": 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; +} + +/** + * 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 = 5 * 60 * 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._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; + +TestRunner._checkForHangs = function() { + function reportError(win, msg) { + if ("SimpleTest" in win) { + win.SimpleTest.ok(false, msg); + } else if ("W3CTest" in win) { + win.W3CTest.logFailure(msg); + } + } + + function killTest(win) { + if ("SimpleTest" in win) { + win.SimpleTest.timeout(); + win.SimpleTest.finish(); + } else if ("W3CTest" in win) { + win.W3CTest.timeout(); + } + } + + if (TestRunner._currentTest < TestRunner._urls.length) { + var runtime = new Date().valueOf() - TestRunner._currentTestStartTime; + if (runtime >= TestRunner.timeout * TestRunner._timeoutFactor) { + var frameWindow = $('testframe').contentWindow.wrappedJSObject || + $('testframe').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(function delayedKillTest() { killTest(frameWindow); }, 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 "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 SpecialPowersLogger(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 = String.fromCharCode(0xe175) + String.fromCharCode(0xee31) + String.fromCharCode(0x2c32) + String.fromCharCode(0xacbf); + +// A log callback for StructuredLog.jsm +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://dxr.mozilla.org/mozilla-central/source/testing/modules/StructuredLog.jsm +TestRunner.structuredLogger = new StructuredLogger('mochitest', TestRunner._dumpMessage); +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. + 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('TestRunner._makeIframe("'+url+'", '+(retry+1)+')', 1000); + return; + } + + TestRunner.structuredLogger.info("Error: Unable to restore focus, expect failures and timeouts."); + } + window.scrollTo(0, $('indicator').offsetTop); + iframe.src = url; + iframe.name = url; + iframe.width = "500"; + return iframe; +}; + +/** + * 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 () { + var prefix = ""; + // handle mochitest-chrome URIs + if ($('testframe').contentWindow.location.protocol == "chrome:") { + prefix = "chrome://mochitests"; + } + return prefix + $('testframe').contentWindow.location.pathname; +}; + +TestRunner.setParameterInfo = function (params) { + this._params = params; +}; + +TestRunner.getParameterInfo = function() { + return this._params; +}; + +/** + * TestRunner entry point. + * + * The arguments are the URLs of the test to be ran. + * +**/ +TestRunner.runTests = function (/*url...*/) { + TestRunner.structuredLogger.info("SimpleTest START"); + TestRunner.originalTestURL = $("current-test").innerHTML; + + SpecialPowers.registerProcessCrashObservers(); + + 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; +TestRunner.runNextTest = function() { + if (TestRunner._currentTest < TestRunner._urls.length && + !TestRunner._haltTests) + { + var url = TestRunner.getNextUrl(); + TestRunner.currentTestURL = url; + + $("current-test-path").innerHTML = url; + + TestRunner._currentTestStartTime = new Date().valueOf(); + TestRunner._timeoutFactor = 1; + TestRunner._expectedMinAsserts = 0; + TestRunner._expectedMaxAsserts = 0; + + TestRunner.structuredLogger.testStart(url); + + 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.testEnd('SimpleTest/TestRunner.js', + "ERROR", + "OK", + "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"; + } + + SpecialPowers.unregisterProcessCrashObservers(); + + 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 + if (TestRunner.repeat === 0) { + 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(); + } +}; + +TestRunner.expectChildProcessCrash = function() { + TestRunner._expectingProcessCrash = true; +}; + +/** + * This stub is called by SimpleTest when a test is finished. +**/ +TestRunner.testFinished = function(tests) { + // 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; + } + 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); + + function cleanUpCrashDumpFiles() { + if (!SpecialPowers.removeExpectedCrashDumpFiles(TestRunner._expectingProcessCrash)) { + TestRunner.structuredLogger.testEnd(TestRunner.currentTestURL, + "ERROR", + "OK", + "This test did not leave any crash dumps behind, but we were expecting some!"); + tests.push({ result: false }); + } + var unexpectedCrashDumpFiles = + SpecialPowers.findUnexpectedCrashDumpFiles(); + TestRunner._expectingProcessCrash = false; + if (unexpectedCrashDumpFiles.length) { + TestRunner.structuredLogger.testEnd(TestRunner.currentTestURL, + "ERROR", + "OK", + "This test left crash dumps behind, but we " + + "weren't expecting it to!", + {unexpected_crashdump_files: unexpectedCrashDumpFiles}); + tests.push({ result: false }); + unexpectedCrashDumpFiles.sort().forEach(function(aFilename) { + TestRunner.structuredLogger.info("Found unexpected crash dump file " + + aFilename + "."); + }); + } + } + + 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()}); + tests.push({ result: false }); + } + + var runtime = new Date().valueOf() - TestRunner._currentTestStartTime; + + TestRunner.structuredLogger.testEnd(TestRunner.currentTestURL, + "OK", + undefined, + "Finished in " + runtime + "ms", + {runtime: runtime} + ); + + if (TestRunner.slowestTestTime < runtime && TestRunner._timeoutFactor >= 1) { + TestRunner.slowestTestTime = runtime; + TestRunner.slowestTestURL = TestRunner.currentTestURL; + } + + TestRunner.updateUI(tests); + + // 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 ($('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 + $('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.testStatus(TestRunner.currentTestURL, wrongtestname, 'FAIL', 'PASS', "Result logged after SimpleTest.finish()"); + } + TestRunner.updateUI([{ result: false }]); + } + } , false); + TestRunner._makeIframe(interstitialURL, 0); + } + + SpecialPowers.executeAfterFlushingMessageQueue(function() { + cleanUpCrashDumpFiles(); + SpecialPowers.flushPermissions(function () { SpecialPowers.flushPrefEnv(runNextTest); }); + }); +}; + +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(); + var numAsserts = newAssertionCount - TestRunner._lastAssertionCount; + TestRunner._lastAssertionCount = newAssertionCount; + + var url = TestRunner.getNextUrl(); + var max = TestRunner._expectedMaxAsserts; + var min = TestRunner._expectedMinAsserts; + if (numAsserts > max) { + TestRunner.structuredLogger.testEnd(url, + "ERROR", + "OK", + "Assertion count " + numAsserts + " is greater than expected range " + + min + "-" + max + " assertions.", + {assertions: numAsserts, min_asserts: min, max_asserts: max}); + TestRunner.updateUI([{ result: false }]); + } else if (numAsserts < min) { + TestRunner.structuredLogger.testEnd(url, + "OK", + "ERROR", + "Assertion count " + numAsserts + " is less than expected range " + + min + "-" + max + " assertions.", + {assertions: numAsserts, min_asserts: min, max_asserts: max}); + TestRunner.updateUI([{ result: false }]); + } else if (numAsserts > 0) { + TestRunner.structuredLogger.testEnd(url, + "ERROR", + "ERROR", + "Assertion count " + numAsserts + " within expected range " + + min + "-" + max + " assertions.", + {assertions: numAsserts, min_asserts: min, max_asserts: 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 == 0) { + //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); + } +} diff --git a/testing/mochitest/tests/SimpleTest/WindowSnapshot.js b/testing/mochitest/tests/SimpleTest/WindowSnapshot.js new file mode 100644 index 000000000..c4ced41dd --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/WindowSnapshot.js @@ -0,0 +1,92 @@ +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 - comparing them makes no sense"); + 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) { + // 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 TEST-UNEXPECTED-FAIL | " + 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"; + } + 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/iframe-between-tests.html b/testing/mochitest/tests/SimpleTest/iframe-between-tests.html new file mode 100644 index 000000000..8de879f20 --- /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 000000000..461a6f49b --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/moz.build @@ -0,0 +1,24 @@ +# -*- 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', + '/testing/specialpowers/content/MozillaLogger.js', + 'EventUtils.js', + 'ExtensionTestUtils.js', + 'iframe-between-tests.html', + 'LogController.js', + 'MemoryStats.js', + 'MockObjects.js', + 'NativeKeyCodes.js', + 'paint_listener.js', + 'setup.js', + 'SimpleTest.js', + 'SpawnTask.js', + 'test.css', + 'TestRunner.js', + 'WindowSnapshot.js', +] diff --git a/testing/mochitest/tests/SimpleTest/paint_listener.js b/testing/mochitest/tests/SimpleTest/paint_listener.js new file mode 100644 index 000000000..304a0fd62 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/paint_listener.js @@ -0,0 +1,83 @@ +(function() { + var accumulatedRect = null; + var onpaint = new Array(); + var debug = false; + const FlushModes = { + FLUSH: 0, + NOFLUSH: 1 + }; + + function paintListener(event) { + if (event.target != window) + return; + var eventRect = + [ event.boundingClientRect.left, + event.boundingClientRect.top, + event.boundingClientRect.right, + event.boundingClientRect.bottom ]; + 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; + while (onpaint.length > 0) { + window.setTimeout(onpaint.pop(), 0); + } + } + window.addEventListener("MozAfterPaint", paintListener, false); + + 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); + }; +})(); diff --git a/testing/mochitest/tests/SimpleTest/setup.js b/testing/mochitest/tests/SimpleTest/setup.js new file mode 100644 index 000000000..e6689022b --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/setup.js @@ -0,0 +1,260 @@ +/* -*- 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"; + +TestRunner.logEnabled = true; +TestRunner.logger = LogController; + +/* Helper function */ +function parseQueryString(encodedString, useArrays) { + // strip a leading '?' from the encoded string + var qstr = (encodedString.length > 0 && 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 { + 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 { + 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; +} + +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 spl = new SpecialPowersLogger(params.logFile); + TestRunner.logger.addListener("mozLogger", fileLevel + "", spl.getLogCallback()); +} + +// 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.maxTimeouts) { + TestRunner.maxTimeouts = params.maxTimeouts; +} + +// 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("http://mochi.test:8888/" + 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 = ""; + var addParam = ""; + if (params.autorun) { + window.location.search += ""; + 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("http://mochi.test:8888/" + params.manifestFile, params, hookupTests); + } else { + hookupTests(gTestList); + } +} + +function hookupTests(testList) { + if (testList.length > 0) { + 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(); + } +} diff --git a/testing/mochitest/tests/SimpleTest/test.css b/testing/mochitest/tests/SimpleTest/test.css new file mode 100644 index 000000000..e6fe345b9 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/test.css @@ -0,0 +1,43 @@ +.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; +} + +browser[remote="true"] { + -moz-binding: url("chrome://global/content/bindings/remote-browser.xml#remote-browser"); +} diff --git a/testing/mochitest/tests/browser/browser.ini b/testing/mochitest/tests/browser/browser.ini new file mode 100644 index 000000000..091ccb68e --- /dev/null +++ b/testing/mochitest/tests/browser/browser.ini @@ -0,0 +1,49 @@ +[DEFAULT] +support-files = + head.js + +[browser_browserLoaded_content_loaded.js] +[browser_add_task.js] +[browser_async.js] +[browser_BrowserTestUtils.js] +support-files = + dummy.html +[browser_head.js] +[browser_pass.js] +[browser_parameters.js] +[browser_popupNode.js] +[browser_popupNode_check.js] +[browser_privileges.js] +[browser_sanityException.js] +[browser_sanityException2.js] +[browser_waitForFocus.js] +skip-if = (os == "win" && e10s && debug) +[browser_getTestFile.js] +support-files = + test-dir/* + waitForFocusPage.html + +# Disabled because it would take too long, useful to check functionality though. +# browser_requestLongerTimeout.js +[browser_zz_fail_openwindow.js] +skip-if = true # this catches outside of the main loop to find an extra window +[browser_fail.js] +skip-if = true +[browser_fail_add_task.js] +skip-if = true # fail-if doesnt catch an exception outside the test +[browser_fail_async_throw.js] +skip-if = true # fail-if doesnt catch an exception outside the test +[browser_fail_fp.js] +fail-if = true +[browser_fail_pf.js] +fail-if = true +[browser_fail_throw.js] +skip-if = true # fail-if doesnt catch an exception outside the test + +# Disabled beacuse it takes too long (bug 1178959) +[browser_fail_timeout.js] +skip-if = true +# Disabled beacuse it takes too long (bug 1178959) +[browser_fail_unexpectedTimeout.js] +skip-if = true + diff --git a/testing/mochitest/tests/browser/browser_BrowserTestUtils.js b/testing/mochitest/tests/browser/browser_BrowserTestUtils.js new file mode 100644 index 000000000..0b3700263 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_BrowserTestUtils.js @@ -0,0 +1,70 @@ +function getLastEventDetails(browser) +{ + return ContentTask.spawn(browser, {}, function* () { + return content.document.getElementById('out').textContent; + }); +} + +add_task(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: 30px; height: 40px;'>Test</button>" + + "<div onmousedown='event.preventDefault()' style='margin: 0; width: 80px; height: 60px;'>Other</div>" + + "<span id='out'></span></body>"; + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "data:text/html," + url); + + let browser = tab.linkedBrowser; + yield BrowserTestUtils.synthesizeMouseAtCenter("#one", {}, browser); + let details = yield getLastEventDetails(browser); + + is(details, "button,31,34", "synthesizeMouseAtCenter"); + + yield BrowserTestUtils.synthesizeMouse("#one", 4, 9, {}, browser); + details = yield getLastEventDetails(browser); + is(details, "button,20,23", "synthesizeMouse"); + + yield BrowserTestUtils.synthesizeMouseAtPoint(15, 6, {}, browser); + details = yield getLastEventDetails(browser); + is(details, "body,15,6", "synthesizeMouseAtPoint on body"); + + yield BrowserTestUtils.synthesizeMouseAtPoint(20, 22, {}, browser); + details = yield getLastEventDetails(browser); + is(details, "button,20,22", "synthesizeMouseAtPoint on button"); + + yield BrowserTestUtils.synthesizeMouseAtCenter("body > div", {}, browser); + details = yield getLastEventDetails(browser); + is(details, "div,40,84", "synthesizeMouseAtCenter with complex selector"); + + let cancelled = yield BrowserTestUtils.synthesizeMouseAtCenter("body > div", { type: "mousedown" }, browser); + details = yield getLastEventDetails(browser); + is(details, "div,40,84", "synthesizeMouseAtCenter mousedown with complex selector"); + ok(cancelled, "synthesizeMouseAtCenter mousedown with complex selector not cancelled"); + + cancelled = yield BrowserTestUtils.synthesizeMouseAtCenter("body > div", { type: "mouseup" }, browser); + details = yield 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(function* () { + yield BrowserTestUtils.registerAboutPage( + registerCleanupFunction, "about-pages-are-cool", + getRootDirectory(gTestPath) + "dummy.html", 0); + let tab = yield BrowserTestUtils.openNewForegroundTab( + gBrowser, "about:about-pages-are-cool", true); + ok(tab, "Successfully created an about: page and loaded it."); + yield BrowserTestUtils.removeTab(tab); + try { + yield 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"); + } + yield 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"); + }); +}); 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 000000000..5318b09d4 --- /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(function* asyncTest_no1() { + yield executeWithTimeout(); + test1Complete = true; +}); + +add_task(function* asyncTest_no2() { + yield 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 000000000..51ba0700e --- /dev/null +++ b/testing/mochitest/tests/browser/browser_async.js @@ -0,0 +1,8 @@ +function test() { + waitForExplicitFinish(); + function done() { + ok(true, "timeout ran"); + finish(); + } + setTimeout(done, 10000); +} 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 000000000..5de7794f5 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_browserLoaded_content_loaded.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +'use strict'; + +function isDOMLoaded(browser) { + return ContentTask.spawn(browser, null, function*() { + Assert.equal(content.document.readyState, "complete", + "Browser should be loaded."); + }); +} + +// It checks if calling BrowserTestUtils.browserLoaded() yields +// browser object. +add_task(function*() { + let tab = gBrowser.addTab('http://example.com'); + let browser = tab.linkedBrowser; + yield BrowserTestUtils.browserLoaded(browser); + yield isDOMLoaded(browser); + gBrowser.removeTab(tab); +}); + +// It checks that BrowserTestUtils.browserLoaded() works well with +// promise.all(). +add_task(function*() { + let tabURLs = [ + `http://example.org`, + `http://mochi.test:8888`, + `http://test:80`, + ]; + //Add tabs, get the respective browsers + let browsers = [ + for (u of tabURLs) gBrowser.addTab(u).linkedBrowser + ]; + //wait for promises to settle + yield Promise.all(( + for (b of browsers) BrowserTestUtils.browserLoaded(b) + )); + let expected = 'Expected all promised browsers to have loaded.'; + for (const browser of browsers) { + yield isDOMLoaded(browser); + } + //cleanup + browsers + .map(browser => gBrowser.getTabForBrowser(browser)) + .forEach(tab => gBrowser.removeTab(tab)); +}); diff --git a/testing/mochitest/tests/browser/browser_fail.js b/testing/mochitest/tests/browser/browser_fail.js new file mode 100644 index 000000000..3d91439ea --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail.js @@ -0,0 +1,8 @@ +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 000000000..9ef20f7c7 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_add_task.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test is designed to fail. +// It ensures that throwing an asynchronous error from add_task will +// fail the test. + +var passedTests = 0; + +function rejectWithTimeout(error = undefined) { + let deferred = Promise.defer(); + executeSoon(function() { + ok(true, "we get here after a timeout"); + deferred.reject(error); + }); + return deferred.promise; +} + +add_task(function failWithoutError() { + try { + yield rejectWithTimeout(); + } finally { + ++passedTests; + } +}); + +add_task(function failWithString() { + try { + yield rejectWithTimeout("Meaningless error"); + } finally { + ++passedTests; + } +}); + +add_task(function failWithoutInt() { + try { + yield rejectWithTimeout(42); + } finally { + ++passedTests; + } +}); + + +// This one should display a stack trace +add_task(function failWithError() { + try { + yield rejectWithTimeout(new Error("This is an error")); + } finally { + ++passedTests; + } +}); + +add_task(function done() { + is(passedTests, 4, "Passed all tests"); +}); diff --git a/testing/mochitest/tests/browser/browser_fail_async_throw.js b/testing/mochitest/tests/browser/browser_fail_async_throw.js new file mode 100644 index 000000000..201cb241e --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_async_throw.js @@ -0,0 +1,7 @@ +function test() { + function end() { + throw "thrown exception"; + } + waitForExplicitFinish(); + setTimeout(end, 1000); +} diff --git a/testing/mochitest/tests/browser/browser_fail_fp.js b/testing/mochitest/tests/browser/browser_fail_fp.js new file mode 100644 index 000000000..04cae37cd --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_fp.js @@ -0,0 +1,4 @@ +function test() { + ok(false, "first fail ok"); + ok(true, "then pass ok"); +} diff --git a/testing/mochitest/tests/browser/browser_fail_pf.js b/testing/mochitest/tests/browser/browser_fail_pf.js new file mode 100644 index 000000000..88ed1d949 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_pf.js @@ -0,0 +1,4 @@ +function test() { + ok(true, "first pass ok"); + ok(false, "then 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 000000000..aee0238df --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_throw.js @@ -0,0 +1,3 @@ +function test() { + throw "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 000000000..6b99693d9 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_timeout.js @@ -0,0 +1,8 @@ +function test() { + function end() { + ok(false, "should have timed out"); + finish(); + } + waitForExplicitFinish(); + setTimeout(end, 40000); +} 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 000000000..2175eea27 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_unexpectedTimeout.js @@ -0,0 +1,12 @@ +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(); + setTimeout(message, 20000); + 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 000000000..99456b633 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_getTestFile.js @@ -0,0 +1,44 @@ +function test() { + let {Promise} = Components.utils.import("resource://gre/modules/Promise.jsm"); + Components.utils.import("resource://gre/modules/osfile.jsm"); + let decoder = new TextDecoder(); + + waitForExplicitFinish(); + + SimpleTest.doesThrow(function () { + getTestFilePath("/browser_getTestFile.js") + }, "getTestFilePath rejects absolute paths"); + + Promise.all([ + OS.File.exists(getTestFilePath("browser_getTestFile.js")) + .then(function (exists) { + ok(exists, "getTestFilePath consider the path as being relative"); + }), + + OS.File.exists(getTestFilePath("./browser_getTestFile.js")) + .then(function (exists) { + ok(exists, "getTestFilePath also accepts explicit relative path"); + }), + + OS.File.exists(getTestFilePath("./browser_getTestFileTypo.xul")) + .then(function (exists) { + ok(!exists, "getTestFilePath do not throw if the file doesn't exists"); + }), + + OS.File.read(getTestFilePath("test-dir/test-file")) + .then(function (array) { + is(decoder.decode(array), "foo\n", "getTestFilePath can reach sub-folder files 1/2"); + }), + + OS.File.read(getTestFilePath("./test-dir/test-file")) + .then(function (array) { + is(decoder.decode(array), "foo\n", "getTestFilePath can reach sub-folder files 2/2"); + }) + + ]).then(function () { + finish(); + }, function (error) { + ok(false, error); + finish(); + }); +} diff --git a/testing/mochitest/tests/browser/browser_head.js b/testing/mochitest/tests/browser/browser_head.js new file mode 100644 index 000000000..0e1f7dd25 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_head.js @@ -0,0 +1,12 @@ +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 000000000..32ba82d92 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_parameters.js @@ -0,0 +1,4 @@ +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 000000000..dbdfa1f17 --- /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, 1, "pass indirect is"); +} diff --git a/testing/mochitest/tests/browser/browser_popupNode.js b/testing/mochitest/tests/browser/browser_popupNode.js new file mode 100644 index 000000000..c6042011c --- /dev/null +++ b/testing/mochitest/tests/browser/browser_popupNode.js @@ -0,0 +1,4 @@ +function test() { + document.popupNode = document; + isnot(document.popupNode, null, "document.popupNode has been correctly set"); +} diff --git a/testing/mochitest/tests/browser/browser_popupNode_check.js b/testing/mochitest/tests/browser/browser_popupNode_check.js new file mode 100644 index 000000000..fb85378d9 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_popupNode_check.js @@ -0,0 +1,3 @@ +function test() { + is(document.popupNode, null, "document.popupNode has been correctly cleared"); +} diff --git a/testing/mochitest/tests/browser/browser_privileges.js b/testing/mochitest/tests/browser/browser_privileges.js new file mode 100644 index 000000000..7b6b1978c --- /dev/null +++ b/testing/mochitest/tests/browser/browser_privileges.js @@ -0,0 +1,16 @@ +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 { + var prefs = Components.classes["@mozilla.org/preferences-service;1"]. + getService(Components.interfaces.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 000000000..cb53e13a2 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_requestLongerTimeout.js @@ -0,0 +1,9 @@ +function test() { + requestLongerTimeout(2); + function end() { + ok(true, "should not time out"); + finish(); + } + waitForExplicitFinish(); + 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 000000000..2039946f9 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_sanityException.js @@ -0,0 +1,5 @@ +function test() { + ok(true, "ok called"); + expectUncaughtException(); + throw "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 000000000..0b9296041 --- /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 "this is a deliberately thrown exception"; + }); + executeSoon(function() { + finish(); + }); +} diff --git a/testing/mochitest/tests/browser/browser_waitForFocus.js b/testing/mochitest/tests/browser/browser_waitForFocus.js new file mode 100644 index 000000000..8f9e27586 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_waitForFocus.js @@ -0,0 +1,69 @@ + +const gBaseURL = "https://example.com/browser/testing/mochitest/tests/browser/"; + +function *promiseTabLoadEvent(tab, url) +{ + return new Promise(function (resolve, reject) { + function handleLoadEvent(event) { + if (event.originalTarget != tab.linkedBrowser.contentDocument || + event.target.location.href == "about:blank" || + (url && event.target.location.href != url)) { + return; + } + + tab.linkedBrowser.removeEventListener("load", handleLoadEvent, true); + resolve(event); + } + + tab.linkedBrowser.addEventListener("load", handleLoadEvent, true, true); + if (url) + tab.linkedBrowser.loadURI(url); + }); +} + +// Load a new blank tab +add_task(function *() { + yield BrowserTestUtils.openNewForegroundTab(gBrowser); + + gURLBar.focus(); + + let browser = gBrowser.selectedBrowser; + yield SimpleTest.promiseFocus(browser.contentWindowAsCPOW, 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(function *() { + let tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + yield promiseTabLoadEvent(tab, gBaseURL + "waitForFocusPage.html"); + + yield SimpleTest.promiseFocus(browser.contentWindowAsCPOW); + + is(document.activeElement, browser, "Browser is focused when page is loaded"); + + yield SimpleTest.promiseFocus(browser.contentWindowAsCPOW.frames[0]); + + is(browser.contentWindowAsCPOW.document.activeElement.localName, "iframe", "Child iframe is focused"); + + gBrowser.removeCurrentTab(); +}); + +// Pass a browser to promiseFocus +add_task(function *() { + yield BrowserTestUtils.openNewForegroundTab(gBrowser, gBaseURL + "waitForFocusPage.html"); + + gURLBar.focus(); + + yield SimpleTest.promiseFocus(gBrowser.selectedBrowser); + + is(document.activeElement, gBrowser.selectedBrowser, "Browser is focused when promiseFocus is passed a browser"); + + gBrowser.removeCurrentTab(); +}); 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 000000000..e9fe71d14 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_zz_fail_openwindow.js @@ -0,0 +1,12 @@ +function test() { + waitForExplicitFinish(); + function done() { + ok(true, "timeout ran"); + finish(); + } + + ok(OpenBrowserWindow(), "opened browser window"); + // and didn't close it! + + setTimeout(done, 10000); +} diff --git a/testing/mochitest/tests/browser/dummy.html b/testing/mochitest/tests/browser/dummy.html new file mode 100644 index 000000000..c49925c19 --- /dev/null +++ b/testing/mochitest/tests/browser/dummy.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> + <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 000000000..279333791 --- /dev/null +++ b/testing/mochitest/tests/browser/head.js @@ -0,0 +1,12 @@ +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 000000000..257cc5642 --- /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 000000000..286ad7849 --- /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 000000000..db43722db --- /dev/null +++ b/testing/mochitest/tests/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/. + +DIRS += [ + 'SimpleTest' +] + +TESTING_JS_MODULES += [ + 'Harness_sanity/ImportTesting.jsm', +] + +MOCHITEST_MANIFESTS += ['Harness_sanity/mochitest.ini'] +BROWSER_CHROME_MANIFESTS += ['browser/browser.ini'] |