diff --git a/.gitmodules b/.gitmodules index 5000d9dc7..4eac6535f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,9 @@ [submodule "styles"] path = styles url = git://github.com/zotero/bundled-styles.git +[submodule "test/resource/chai"] + path = test/resource/chai + url = https://github.com/chaijs/chai.git +[submodule "test/resource/mocha"] + path = test/resource/mocha + url = https://github.com/mochajs/mocha.git diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..4e0365777 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +language: cpp +compiler: + - gcc +env: + matrix: + - FIREFOXVERSION="36.0.1" + - FIREFOXVERSION="31.5.0esr" +notifications: + email: false +before_install: + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + - wget http://ftp.mozilla.org/pub/firefox/releases/${FIREFOXVERSION}/linux-x86_64/en-US/firefox-${FIREFOXVERSION}.tar.bz2 + - tar -xjf firefox-${FIREFOXVERSION}.tar.bz2 +script: + - test/runtests.sh -x firefox/firefox diff --git a/chrome/content/zotero/bindings/guidancepanel.xml b/chrome/content/zotero/bindings/guidancepanel.xml index 41560f908..12f40106a 100644 --- a/chrome/content/zotero/bindings/guidancepanel.xml +++ b/chrome/content/zotero/bindings/guidancepanel.xml @@ -36,6 +36,7 @@ . + + ***** END LICENSE BLOCK ***** +*/ +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +function ZoteroUnit() { + this.wrappedJSObject = this; +} +ZoteroUnit.prototype = { + /* nsICommandLineHandler */ + handle:function(cmdLine) { + this.tests = cmdLine.handleFlagWithParam("test", false); + this.noquit = cmdLine.handleFlag("noquit", false); + }, + + dump:function(x) { + dump(x); + }, + + contractID: "@mozilla.org/commandlinehandler/general-startup;1?type=zotero-unit", + classDescription: "Zotero Unit Command Line Handler", + classID: Components.ID("{b8570031-be5e-46e8-9785-38cd50a5d911}"), + service: true, + _xpcom_categories: [{category:"command-line-handler", entry:"m-zotero-unit"}], + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsICommandLineHandler, + Components.interfaces.nsISupports]) +}; + + +var NSGetFactory = XPCOMUtils.generateNSGetFactory([ZoteroUnit]); diff --git a/test/content/runtests.html b/test/content/runtests.html new file mode 100644 index 000000000..85294a22d --- /dev/null +++ b/test/content/runtests.html @@ -0,0 +1,14 @@ + + + + Zotero Unit Tests + + + + + + + + + + \ No newline at end of file diff --git a/test/content/runtests.js b/test/content/runtests.js new file mode 100644 index 000000000..ae4adbd62 --- /dev/null +++ b/test/content/runtests.js @@ -0,0 +1,113 @@ +Components.utils.import("resource://gre/modules/FileUtils.jsm"); +Components.utils.import("resource://gre/modules/osfile.jsm"); +Components.utils.import("resource://zotero/q.js"); +var EventUtils = Components.utils.import("resource://zotero-unit/EventUtils.jsm"); + +var ZoteroUnit = Components.classes["@mozilla.org/commandlinehandler/general-startup;1?type=zotero-unit"]. + getService(Components.interfaces.nsISupports). + wrappedJSObject; +var dump = ZoteroUnit.dump; + +function quit(failed) { + // Quit with exit status + if(!failed) { + OS.File.writeAtomic(FileUtils.getFile("ProfD", ["success"]).path, Uint8Array(0)); + } + if(!ZoteroUnit.noquit) { + Components.classes['@mozilla.org/toolkit/app-startup;1']. + getService(Components.interfaces.nsIAppStartup). + quit(Components.interfaces.nsIAppStartup.eForceQuit); + } +} + +function Reporter(runner) { + var indents = 0, passed = 0, failed = 0; + + function indent() { + return Array(indents).join(' '); + } + + runner.on('start', function(){}); + + runner.on('suite', function(suite){ + ++indents; + dump(indent()+suite.title+"\n"); + }); + + runner.on('suite end', function(suite){ + --indents; + if (1 == indents) dump("\n"); + }); + + runner.on('pending', function(test){ + dump(indent()+"pending -"+test.title); + }); + + runner.on('pass', function(test){ + passed++; + var msg = "\r"+indent()+Mocha.reporters.Base.symbols.ok+" "+test.title; + if ('fast' != test.speed) { + msg += " ("+Math.round(test.duration)+" ms)"; + } + dump(msg+"\n"); + }); + + runner.on('fail', function(test, err){ + failed++; + dump("\r"+indent()+Mocha.reporters.Base.symbols.err+" "+test.title+"\n"+ + indent()+" "+err.toString()+" at\n"+ + indent()+" "+err.stack.replace("\n", "\n"+indent()+" ", "g")); + }); + + runner.on('end', function() { + dump(passed+"/"+(passed+failed)+" tests passed.\n"); + quit(failed != 0); + }); +} + +// Setup Mocha +mocha.setup({ui:"bdd", reporter:Reporter}); +var assert = chai.assert, + expect = chai.expect; + +// Set up tests to run +var run = true; +if(ZoteroUnit.tests) { + var testDirectory = getTestDataDirectory().parent, + testFiles = []; + if(ZoteroUnit.tests == "all") { + var enumerator = testDirectory.directoryEntries; + while(enumerator.hasMoreElements()) { + var file = enumerator.getNext().QueryInterface(Components.interfaces.nsIFile); + if(file.leafName.endsWith(".js")) { + testFiles.push(file.leafName); + } + } + } else { + var specifiedTests = ZoteroUnit.tests.split(","); + for(var test of specifiedTests) { + var fname = test+".js", + file = testDirectory.clone(); + file.append(fname); + if(!file.exists()) { + dump("Invalid test file "+test+"\n"); + run = false; + quit(true); + } + testFiles.push(fname); + } + } + + for(var fname of testFiles) { + var el = document.createElement("script"); + el.type = "application/javascript;version=1.8"; + el.src = "resource://zotero-unit-tests/"+fname; + document.body.appendChild(el); + } +} + +if(run) { + window.onload = function() { + mocha.run(); + }; +} \ No newline at end of file diff --git a/test/content/support.js b/test/content/support.js new file mode 100644 index 000000000..8ff21269c --- /dev/null +++ b/test/content/support.js @@ -0,0 +1,168 @@ +/** + * Waits for a DOM event on the specified node. Returns a promise + * resolved with the event. + */ +function waitForDOMEvent(target, event, capture) { + var deferred = Q.defer(); + var func = function(ev) { + target.removeEventListener("event", func, capture); + deferred.resolve(ev); + } + target.addEventListener(event, func, capture); + return deferred.promise; +} + +/** + * Open a window. Returns a promise for the window. + */ +function loadWindow(winurl, argument) { + var win = window.openDialog(winurl, "_blank", "chrome", argument); + return waitForDOMEvent(win, "load").then(function() { + return win; + }); +} + +/** + * Loads a Zotero pane in a new window. Returns the containing window. + */ +function loadZoteroPane() { + return loadWindow("chrome://browser/content/browser.xul").then(function(win) { + win.ZoteroOverlay.toggleDisplay(true); + + // Hack to wait for pane load to finish. This is the same hack + // we use in ZoteroPane.js, so either it's not good enough + // there or it should be good enough here. + return Q.delay(52).then(function() { + return win; + }); + }); +} + +/** + * Waits for a window with a specific URL to open. Returns a promise for the window. + */ +function waitForWindow(uri) { + var deferred = Q.defer(); + Components.utils.import("resource://gre/modules/Services.jsm"); + var loadobserver = function(ev) { + ev.originalTarget.removeEventListener("load", loadobserver, false); + if(ev.target.location == uri) { + Services.ww.unregisterNotification(winobserver); + deferred.resolve(ev.target.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor). + getInterface(Components.interfaces.nsIDOMWindow)); + } + }; + var winobserver = {"observe":function(subject, topic, data) { + if(topic != "domwindowopened") return; + var win = subject.QueryInterface(Components.interfaces.nsIDOMWindow); + win.addEventListener("load", loadobserver, false); + }}; + Services.ww.registerNotification(winobserver); + return deferred.promise; +} + +/** + * Waits for a single item event. Returns a promise for the item ID(s). + */ +function waitForItemEvent(event) { + var deferred = Q.defer(); + var notifierID = Zotero.Notifier.registerObserver({notify:function(ev, type, ids, extraData) { + if(ev == event) { + Zotero.Notifier.unregisterObserver(notifierID); + deferred.resolve(ids); + } + }}, ["item"]); + return deferred.promise; +} + +/** + * Looks for windows with a specific URL. + */ +function getWindows(uri) { + Components.utils.import("resource://gre/modules/Services.jsm"); + var enumerator = Services.wm.getEnumerator(null); + var wins = []; + while(enumerator.hasMoreElements()) { + var win = enumerator.getNext(); + if(win.location == uri) { + wins.push(win); + } + } + return wins; +} + +/** + * Resolve a promise when a specified callback returns true. interval + * specifies the interval between checks. timeout specifies when we + * should assume failure. + */ +function waitForCallback(cb, interval, timeout) { + var deferred = Q.defer(); + if(interval === undefined) interval = 100; + if(timeout === undefined) timeout = 10000; + var start = Date.now(); + var id = setInterval(function() { + var success = cb(); + if(success) { + clearInterval(id); + deferred.resolve(success); + } else if(Date.now() - start > timeout*1000) { + clearInterval(id); + deferred.reject(new Error("Promise timed out")); + } + }, interval); + return deferred.promise; +} + +/** + * Ensures that the PDF tools are installed, or installs them if not. + * Returns a promise. + */ +function installPDFTools() { + if(Zotero.Fulltext.pdfConverterIsRegistered() && Zotero.Fulltext.pdfInfoIsRegistered()) { + return Q(true); + } + + // Begin install procedure + return loadWindow("chrome://zotero/content/preferences/preferences.xul", { + pane: 'zotero-prefpane-search', + action: 'pdftools-install' + }).then(function(win) { + // Wait for confirmation dialog + return waitForWindow("chrome://global/content/commonDialog.xul").then(function(dlg) { + // Accept confirmation dialog + dlg.document.documentElement.acceptDialog(); + + // Wait for install to finish + return waitForCallback(function() { + return Zotero.Fulltext.pdfConverterIsRegistered() && Zotero.Fulltext.pdfInfoIsRegistered(); + }, 500, 30000).finally(function() { + win.close(); + }); + }); + }); +} + +/** + * Returns a promise for the nsIFile corresponding to the test data + * directory (i.e., test/tests/data) + */ +function getTestDataDirectory() { + Components.utils.import("resource://gre/modules/Services.jsm"); + var resource = Services.io.getProtocolHandler("resource"). + QueryInterface(Components.interfaces.nsIResProtocolHandler), + resURI = Services.io.newURI("resource://zotero-unit-tests/data", null, null); + return Services.io.newURI(resource.resolveURI(resURI), null, null). + QueryInterface(Components.interfaces.nsIFileURL).file; +} + +/** + * Resets the Zotero DB and restarts Zotero. Returns a promise resolved + * when this finishes. + */ +function resetDB() { + var db = Zotero.getZoteroDatabase(); + return Zotero.reinit(function() { + db.remove(false); + }); +} \ No newline at end of file diff --git a/test/install.rdf b/test/install.rdf new file mode 100644 index 000000000..86dd153a3 --- /dev/null +++ b/test/install.rdf @@ -0,0 +1,26 @@ + + + + + + zotero-unit@zotero.org + Zotero Unit Tests + 1.0 + Center for History and New Media + Simon Kornblith + http://www.zotero.org + chrome://zotero/skin/zotero-new-z-48px.png + 2 + + + + + {ec8030f7-c20a-464f-9b0e-13a3a9e97384} + 31.0 + 38.* + + + + + diff --git a/test/resource/EventUtils.jsm b/test/resource/EventUtils.jsm new file mode 100644 index 000000000..566c7dde7 --- /dev/null +++ b/test/resource/EventUtils.jsm @@ -0,0 +1,835 @@ +/* Taken from MozMill 6c0948d80eebcbb104ce7a776c65aeae634970dd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + + // Export all available functions for Mozmill +var EXPORTED_SYMBOLS = ["disableNonTestMouseEvents","sendMouseEvent", "sendChar", + "sendString", "sendKey", "synthesizeMouse", "synthesizeTouch", + "synthesizeMouseAtPoint", "synthesizeTouchAtPoint", + "synthesizeMouseAtCenter", "synthesizeTouchAtCenter", + "synthesizeWheel", "synthesizeKey", + "synthesizeMouseExpectEvent", "synthesizeKeyExpectEvent", + "synthesizeText", + "synthesizeComposition", "synthesizeQuerySelectedText"]; + +const Ci = Components.interfaces; +const Cc = Components.classes; + +var window = Cc["@mozilla.org/appshell/appShellService;1"] + .getService(Ci.nsIAppShellService).hiddenDOMWindow; + +var _EU_Ci = Ci; +var navigator = window.navigator; +var KeyEvent = window.KeyEvent; +var parent = window.parent; + +function is(aExpression1, aExpression2, aMessage) { + if (aExpression1 !== aExpression2) { + throw new Error(aMessage); + } +} + +/** + * EventUtils provides some utility methods for creating and sending DOM events. + * Current methods: + * sendMouseEvent + * sendChar + * sendString + * sendKey + * synthesizeMouse + * synthesizeMouseAtCenter + * synthesizeWheel + * synthesizeKey + * synthesizeMouseExpectEvent + * synthesizeKeyExpectEvent + * + * When adding methods to this file, please add a performance test for it. + */ + +/** + * 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 sendMouseEvent(aEvent, aTarget, aWindow) { + if (['click', '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 (!(aTarget instanceof aWindow.Element)) { + 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 = aEvent.button || 0; + var relatedTargetArg = aEvent.relatedTarget || null; + + event.initMouseEvent(typeArg, canBubbleArg, cancelableArg, viewArg, detailArg, + screenXArg, screenYArg, clientXArg, clientYArg, + ctrlKeyArg, altKeyArg, shiftKeyArg, metaKeyArg, + buttonArg, relatedTargetArg); + + aTarget.dispatchEvent(event); +} + +/** + * Send the char aChar to the focused element. This method handles casing of + * chars (sends the right charcode, and sends a shift key for uppercase chars). + * No other modifiers are handled at this point. + * + * For now this method only works for ASCII characters and emulates the shift + * key state on US keyboard layout. + */ +function sendChar(aChar, aWindow) { + var hasShift; + // Emulate US keyboard layout for the shiftKey state. + switch (aChar) { + case "!": + case "@": + case "#": + case "$": + case "%": + case "^": + case "&": + case "*": + case "(": + case ")": + case "_": + case "+": + case "{": + case "}": + case ":": + case "\"": + case "|": + case "<": + case ">": + case "?": + hasShift = true; + break; + default: + hasShift = (aChar == 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) +{ + const 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 |= (navigator.platform.indexOf("Mac") >= 0) ? + 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.numLockKey) { + mval |= nsIDOMWindowUtils.MODIFIER_NUMLOCK; + } + if (aEvent.scrollLockKey) { + mval |= nsIDOMWindowUtils.MODIFIER_SCROLLLOCK; + } + 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); +} + +/* + * 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) +{ + var utils = _getDOMWindowUtils(aWindow); + var defaultPrevented = false; + + if (utils) { + var button = aEvent.button || 0; + var clickCount = aEvent.clickCount || 1; + var modifiers = _parseModifiers(aEvent); + var pressure = ("pressure" in aEvent) ? aEvent.pressure : 0; + var inputSource = ("inputSource" in aEvent) ? aEvent.inputSource : 0; + + if (("type" in aEvent) && aEvent.type) { + defaultPrevented = utils.sendMouseEvent(aEvent.type, left, top, button, clickCount, modifiers, false, pressure, inputSource); + } + else { + utils.sendMouseEvent("mousedown", left, top, button, clickCount, modifiers, false, pressure, inputSource); + utils.sendMouseEvent("mouseup", left, top, button, clickCount, modifiers, false, pressure, inputSource); + } + } + + return defaultPrevented; +} +function synthesizeTouchAtPoint(left, top, aEvent, aWindow) +{ + 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); + + 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); + } + } +} +// Call synthesizeMouse with coordinates at the center of aTarget. +function synthesizeMouseAtCenter(aTarget, aEvent, aWindow) +{ + var rect = aTarget.getBoundingClientRect(); + 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 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, isPixelOnlyDevice, + * 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 utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return; + } + + var modifiers = _parseModifiers(aEvent); + var options = 0; + if (aEvent.isPixelOnlyDevice && + (aEvent.deltaMode == WheelEvent.DOM_DELTA_PIXEL)) { + options |= utils.WHEEL_EVENT_CAUSED_BY_PIXEL_ONLY_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 isPixelOnlyDevice = + aEvent.isPixelOnlyDevice && aEvent.deltaMode == WheelEvent.DOM_DELTA_PIXEL; + + // 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); + + var rect = aTarget.getBoundingClientRect(); + utils.sendWheelEvent(rect.left + aOffsetX, rect.top + aOffsetY, + aEvent.deltaX, aEvent.deltaY, aEvent.deltaZ, + aEvent.deltaMode, modifiers, + lineOrPageDeltaX, lineOrPageDeltaY, options); +} + +function _computeKeyCodeFromChar(aChar) +{ + if (aChar.length != 1) { + return 0; + } + const nsIDOMKeyEvent = _EU_Ci.nsIDOMKeyEvent; + if (aChar >= 'a' && aChar <= 'z') { + return nsIDOMKeyEvent.DOM_VK_A + aChar.charCodeAt(0) - 'a'.charCodeAt(0); + } + if (aChar >= 'A' && aChar <= 'Z') { + return nsIDOMKeyEvent.DOM_VK_A + aChar.charCodeAt(0) - 'A'.charCodeAt(0); + } + if (aChar >= '0' && aChar <= '9') { + return nsIDOMKeyEvent.DOM_VK_0 + aChar.charCodeAt(0) - '0'.charCodeAt(0); + } + // returns US keyboard layout's keycode + switch (aChar) { + case '~': + case '`': + return nsIDOMKeyEvent.DOM_VK_BACK_QUOTE; + case '!': + return nsIDOMKeyEvent.DOM_VK_1; + case '@': + return nsIDOMKeyEvent.DOM_VK_2; + case '#': + return nsIDOMKeyEvent.DOM_VK_3; + case '$': + return nsIDOMKeyEvent.DOM_VK_4; + case '%': + return nsIDOMKeyEvent.DOM_VK_5; + case '^': + return nsIDOMKeyEvent.DOM_VK_6; + case '&': + return nsIDOMKeyEvent.DOM_VK_7; + case '*': + return nsIDOMKeyEvent.DOM_VK_8; + case '(': + return nsIDOMKeyEvent.DOM_VK_9; + case ')': + return nsIDOMKeyEvent.DOM_VK_0; + case '-': + case '_': + return nsIDOMKeyEvent.DOM_VK_SUBTRACT; + case '+': + case '=': + return nsIDOMKeyEvent.DOM_VK_EQUALS; + case '{': + case '[': + return nsIDOMKeyEvent.DOM_VK_OPEN_BRACKET; + case '}': + case ']': + return nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET; + case '|': + case '\\': + return nsIDOMKeyEvent.DOM_VK_BACK_SLASH; + case ':': + case ';': + return nsIDOMKeyEvent.DOM_VK_SEMICOLON; + case '\'': + case '"': + return nsIDOMKeyEvent.DOM_VK_QUOTE; + case '<': + case ',': + return nsIDOMKeyEvent.DOM_VK_COMMA; + case '>': + case '.': + return nsIDOMKeyEvent.DOM_VK_PERIOD; + case '?': + case '/': + return nsIDOMKeyEvent.DOM_VK_SLASH; + default: + return 0; + } +} + +/** + * isKeypressFiredKey() returns TRUE if the given key should cause keypress + * event when widget handles the native key event. Otherwise, FALSE. + * + * aDOMKeyCode should be one of consts of nsIDOMKeyEvent::DOM_VK_*, or a key + * name begins with "VK_", or a character. + */ +function isKeypressFiredKey(aDOMKeyCode) +{ + if (typeof(aDOMKeyCode) == "string") { + if (aDOMKeyCode.indexOf("VK_") == 0) { + aDOMKeyCode = KeyEvent["DOM_" + aDOMKeyCode]; + if (!aDOMKeyCode) { + throw "Unknown key: " + aDOMKeyCode; + } + } else { + // If the key generates a character, it must cause a keypress event. + return true; + } + } + switch (aDOMKeyCode) { + case KeyEvent.DOM_VK_SHIFT: + case KeyEvent.DOM_VK_CONTROL: + case KeyEvent.DOM_VK_ALT: + case KeyEvent.DOM_VK_CAPS_LOCK: + case KeyEvent.DOM_VK_NUM_LOCK: + case KeyEvent.DOM_VK_SCROLL_LOCK: + case KeyEvent.DOM_VK_META: + return false; + default: + return true; + } +} + +/** + * 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 either a character or a keycode starting with VK_ such as + * VK_ENTER. + * + * aEvent is an object which may contain the properties: + * shiftKey, ctrlKey, altKey, metaKey, accessKey, type, location + * + * Sets one of KeyboardEvent.DOM_KEY_LOCATION_* to location. Otherwise, + * DOMWindowUtils will choose good location from the keycode. + * + * If the type is specified, a key event of that type is fired. Otherwise, + * a keydown, a keypress and then a keyup event are fired in sequence. + * + * aWindow is optional, and defaults to the current window object. + */ +function synthesizeKey(aKey, aEvent, aWindow) +{ + var utils = _getDOMWindowUtils(aWindow); + if (utils) { + var keyCode = 0, charCode = 0; + if (aKey.indexOf("VK_") == 0) { + keyCode = KeyEvent["DOM_" + aKey]; + if (!keyCode) { + throw "Unknown key: " + aKey; + } + } else { + charCode = aKey.charCodeAt(0); + keyCode = _computeKeyCodeFromChar(aKey.charAt(0)); + } + + var modifiers = _parseModifiers(aEvent); + var flags = 0; + if (aEvent.location != undefined) { + switch (aEvent.location) { + case KeyboardEvent.DOM_KEY_LOCATION_STANDARD: + flags |= utils.KEY_FLAG_LOCATION_STANDARD; + break; + case KeyboardEvent.DOM_KEY_LOCATION_LEFT: + flags |= utils.KEY_FLAG_LOCATION_LEFT; + break; + case KeyboardEvent.DOM_KEY_LOCATION_RIGHT: + flags |= utils.KEY_FLAG_LOCATION_RIGHT; + break; + case KeyboardEvent.DOM_KEY_LOCATION_NUMPAD: + flags |= utils.KEY_FLAG_LOCATION_NUMPAD; + break; + case KeyboardEvent.DOM_KEY_LOCATION_MOBILE: + flags |= utils.KEY_FLAG_LOCATION_MOBILE; + break; + case KeyboardEvent.DOM_KEY_LOCATION_JOYSTICK: + flags |= utils.KEY_FLAG_LOCATION_JOYSTICK; + break; + } + } + + if (!("type" in aEvent) || !aEvent.type) { + // Send keydown + (optional) keypress + keyup events. + var keyDownDefaultHappened = + utils.sendKeyEvent("keydown", keyCode, 0, modifiers, flags); + if (isKeypressFiredKey(keyCode)) { + if (!keyDownDefaultHappened) { + flags |= utils.KEY_FLAG_PREVENT_DEFAULT; + } + utils.sendKeyEvent("keypress", keyCode, charCode, modifiers, flags); + } + utils.sendKeyEvent("keyup", keyCode, 0, modifiers, flags); + } else if (aEvent.type == "keypress") { + // Send standalone keypress event. + utils.sendKeyEvent(aEvent.type, keyCode, charCode, modifiers, flags); + } else { + // Send other standalone event than keypress. + utils.sendKeyEvent(aEvent.type, keyCode, 0, modifiers, flags); + } + } +} + +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) +{ + 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); +} + +// Must be synchronized with nsIDOMWindowUtils. +const COMPOSITION_ATTR_RAWINPUT = 0x02; +const COMPOSITION_ATTR_SELECTEDRAWTEXT = 0x03; +const COMPOSITION_ATTR_CONVERTEDTEXT = 0x04; +const COMPOSITION_ATTR_SELECTEDCONVERTEDTEXT = 0x05; + +/** + * Synthesize a composition event. + * + * @param aEvent The composition event information. This must + * have |type| member. The value must be + * "compositionstart", "compositionend" or + * "compositionupdate". + * 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 would + * be ignored if the event type were + * "compositionstart". + * @param aWindow Optional (If null, current |window| will be used) + */ +function synthesizeComposition(aEvent, aWindow) +{ + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return; + } + + utils.sendCompositionEvent(aEvent.type, aEvent.data ? aEvent.data : "", + aEvent.locale ? aEvent.locale : ""); +} +/** + * Synthesize a text event. + * + * @param aEvent The text 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 + * + * 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 nsIDOMWindowUtils.COMPOSITION_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. + * + * @param aWindow Optional (If null, current |window| will be used) + */ +function synthesizeText(aEvent, aWindow) +{ + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return; + } + + if (!aEvent.composition || !aEvent.composition.clauses || + !aEvent.composition.clauses[0]) { + return; + } + + var firstClauseLength = aEvent.composition.clauses[0].length; + var firstClauseAttr = aEvent.composition.clauses[0].attr; + var secondClauseLength = 0; + var secondClauseAttr = 0; + var thirdClauseLength = 0; + var thirdClauseAttr = 0; + if (aEvent.composition.clauses[1]) { + secondClauseLength = aEvent.composition.clauses[1].length; + secondClauseAttr = aEvent.composition.clauses[1].attr; + if (aEvent.composition.clauses[2]) { + thirdClauseLength = aEvent.composition.clauses[2].length; + thirdClauseAttr = aEvent.composition.clauses[2].attr; + } + } + + var caretStart = -1; + var caretLength = 0; + if (aEvent.caret) { + caretStart = aEvent.caret.start; + caretLength = aEvent.caret.length; + } + + utils.sendTextEvent(aEvent.composition.string, + firstClauseLength, firstClauseAttr, + secondClauseLength, secondClauseAttr, + thirdClauseLength, thirdClauseAttr, + caretStart, caretLength); +} + +/** + * Synthesize a query selected text event. + * + * @param aWindow Optional (If null, current |window| will be used) + * @return An nsIQueryContentEventResult object. If this failed, + * the result might be null. + */ +function synthesizeQuerySelectedText(aWindow) +{ + var utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return null; + } + + return utils.sendQueryContentEvent(utils.QUERY_SELECTED_TEXT, 0, 0, 0, 0); +} diff --git a/test/resource/chai b/test/resource/chai new file mode 160000 index 000000000..d7cafca02 --- /dev/null +++ b/test/resource/chai @@ -0,0 +1 @@ +Subproject commit d7cafca0232756f767275bb00e66930a7823b027 diff --git a/test/resource/mocha b/test/resource/mocha new file mode 160000 index 000000000..65fc80ecd --- /dev/null +++ b/test/resource/mocha @@ -0,0 +1 @@ +Subproject commit 65fc80ecd96ca2159a792aff089bbc273d4bd86d diff --git a/test/runtests.sh b/test/runtests.sh new file mode 100755 index 000000000..7ef72c07c --- /dev/null +++ b/test/runtests.sh @@ -0,0 +1,71 @@ +#!/bin/bash +CWD="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +DEBUG=false +if [ "`uname`" == "Darwin" ]; then + FX_EXECUTABLE="/Applications/Firefox.app/Contents/MacOS/firefox" +else + FX_EXECUTABLE="firefox" +fi +FX_ARGS="" + +function usage { + cat >&2 < "$PROFILE/extensions/zotero-unit@zotero.org" +echo "`dirname "$CWD"`" > "$PROFILE/extensions/zotero@chnm.gmu.edu" +cat < "$PROFILE/prefs.js" +user_pref("extensions.autoDisableScopes", 0); +user_pref("extensions.zotero.debug.log", $DEBUG); +user_pref("extensions.zotero.firstRunGuidance", false); +user_pref("extensions.zotero.firstRun2", false); +EOF + +MOZ_NO_REMOTE=1 NO_EM_RESTART=1 "$FX_EXECUTABLE" -profile "$PROFILE" \ + -chrome chrome://zotero-unit/content/runtests.html -test "$TESTS" $FX_ARGS + +# Check for success +test -e "$PROFILE/success" +STATUS=$? + +# Clean up +rm -rf "$PROFILE" +exit $STATUS \ No newline at end of file diff --git a/test/tests/data/recognizePDF_test_DOI.pdf b/test/tests/data/recognizePDF_test_DOI.pdf new file mode 100644 index 000000000..324e83cee Binary files /dev/null and b/test/tests/data/recognizePDF_test_DOI.pdf differ diff --git a/test/tests/data/recognizePDF_test_GS.pdf b/test/tests/data/recognizePDF_test_GS.pdf new file mode 100644 index 000000000..28d78b5ca Binary files /dev/null and b/test/tests/data/recognizePDF_test_GS.pdf differ diff --git a/test/tests/lookup.js b/test/tests/lookup.js new file mode 100644 index 000000000..f49bef575 --- /dev/null +++ b/test/tests/lookup.js @@ -0,0 +1,51 @@ +function lookupIdentifier(win, identifier) { + var textbox = win.document.getElementById("zotero-lookup-textbox"); + textbox.value = identifier; + win.Zotero_Lookup.accept(textbox); + return waitForItemEvent("add"); +} + +describe("Add Item by Identifier", function() { + var win; + before(function() { + this.timeout(5000); + // Load a Zotero pane and update the translators (needed to + // make sure they're available before we run the tests) + return loadZoteroPane().then(function(w) { + win = w; + return Zotero.Schema.updateBundledFiles('translators', null, false); + }); + }); + after(function() { + win.close(); + }); + + it("should add an ISBN-10", function() { + this.timeout(10000); + return lookupIdentifier(win, "0838985890").then(function(ids) { + var item = Zotero.Items.get(ids[0]); + assert.equal(item.getField("title"), "Zotero: a guide for librarians, researchers, and educators"); + }); + }); + it("should add an ISBN-13", function() { + this.timeout(10000); + return lookupIdentifier(win, "978-0838985892").then(function(ids) { + var item = Zotero.Items.get(ids[0]); + assert.equal(item.getField("title"), "Zotero: a guide for librarians, researchers, and educators"); + }); + }); + it("should add a DOI", function() { + this.timeout(10000); + return lookupIdentifier(win, "10.4103/0976-500X.85940").then(function(ids) { + var item = Zotero.Items.get(ids[0]); + assert.equal(item.getField("title"), "Zotero: A bibliographic assistant to researcher"); + }); + }); + it("should add a PMID", function() { + this.timeout(10000); + return lookupIdentifier(win, "24297125").then(function(ids) { + var item = Zotero.Items.get(ids[0]); + assert.equal(item.getField("title"), "Taking control of your digital library: how modern citation managers do more than just referencing"); + }); + }); +}); \ No newline at end of file diff --git a/test/tests/recognizePDF.js b/test/tests/recognizePDF.js new file mode 100644 index 000000000..efba88d3a --- /dev/null +++ b/test/tests/recognizePDF.js @@ -0,0 +1,58 @@ +describe("PDF Recognition", function() { + Components.utils.import("resource://gre/modules/FileUtils.jsm"); + + var win; + before(function() { + this.timeout(60000); + // Load Zotero pane, install PDF tools, and load the + // translators + return Q.all([loadZoteroPane().then(function(w) { + win = w; + return Zotero.Schema.updateBundledFiles('translators', null, false); + }), installPDFTools()]); + }); + afterEach(function() { + for(let win of getWindows("chrome://zotero/content/pdfProgress.xul")) { + win.close(); + } + }); + after(function() { + win.close(); + }); + + it("should recognize a PDF with a DOI", function() { + this.timeout(30000); + // Import the PDF + var testdir = getTestDataDirectory(); + testdir.append("recognizePDF_test_DOI.pdf"); + var id = Zotero.Attachments.importFromFile(testdir); + + // Recognize the PDF + win.ZoteroPane.selectItem(id); + win.Zotero_RecognizePDF.recognizeSelected(); + + return waitForItemEvent("add").then(function(ids) { + var item = Zotero.Items.get(ids[0]); + assert.equal(item.getField("title"), "Shaping the Research Agenda"); + assert.equal(item.getField("libraryCatalog"), "CrossRef"); + }); + }); + + it("should recognize a PDF without a DOI", function() { + this.timeout(30000); + // Import the PDF + var testdir = getTestDataDirectory(); + testdir.append("recognizePDF_test_GS.pdf"); + var id = Zotero.Attachments.importFromFile(testdir); + + // Recognize the PDF + win.ZoteroPane.selectItem(id); + win.Zotero_RecognizePDF.recognizeSelected(); + + return waitForItemEvent("add").then(function(ids) { + var item = Zotero.Items.get(ids[0]); + assert.equal(item.getField("title"), "Scaling study of an improved fermion action on quenched lattices"); + assert.equal(item.getField("libraryCatalog"), "Google Scholar"); + }); + }); +}); \ No newline at end of file diff --git a/test/tests/support.js b/test/tests/support.js new file mode 100644 index 000000000..f8c91f6f3 --- /dev/null +++ b/test/tests/support.js @@ -0,0 +1,11 @@ +describe("Support Functions for Unit Testing", function() { + describe("resetDB", function() { + it("should restore the DB to factory settings", function() { + var quickstart = Zotero.Items.erase(1); + assert.equal(Zotero.Items.get(1), false); + return resetDB().then(function() { + assert.equal(Zotero.Items.get(1).getField("url"), "http://zotero.org/support/quick_start_guide"); + }); + }); + }); +}); diff --git a/test/tests/utilities.js b/test/tests/utilities.js new file mode 100644 index 000000000..b71dcb517 --- /dev/null +++ b/test/tests/utilities.js @@ -0,0 +1,20 @@ +describe("Zotero.Utilities", function() { + describe("cleanAuthor", function() { + it('should parse author names', function() { + for(let useComma of [false, true]) { + for(let first_expected of [["First", "First"], + ["First Middle", "First Middle"], + ["F. R. S.", "F. R. S."], + ["F.R.S.", "F. R. S."], + ["F R S", "F. R. S."], + ["FRS", "F. R. S."]]) { + let [first, expected] = first_expected; + let str = useComma ? "Last, "+first : first+" Last"; + let author = Zotero.Utilities.cleanAuthor(str, "author", useComma); + assert.equal(author.firstName, expected); + assert.equal(author.lastName, "Last"); + } + } + }); + }); +});