diff --git a/chrome/content/zotero/tools/testTranslators/testTranslators.css b/chrome/content/zotero/tools/testTranslators/testTranslators.css
new file mode 100644
index 0000000000..0ebb201b21
--- /dev/null
+++ b/chrome/content/zotero/tools/testTranslators/testTranslators.css
@@ -0,0 +1,51 @@
+body {
+ font-family: Helvetica, sans;
+ font-size: 12px;
+}
+
+table {
+ border-color: black;
+ border-width: 0 0 1px 1px;
+ border-style: solid;
+ border-collapse: collapse;
+}
+
+td, th {
+ border-color: black;
+ border-width: 1px 1px 0 0;
+ border-style: solid;
+ padding: 2px;
+}
+
+.th-translator {
+ width: 500px;
+ max-width: 500px;
+}
+
+.th-status {
+ width: 100px;
+}
+
+.th-pending, .th-supported, .th-succeeded, .th-failed, .th-unknown {
+ width: 75px;
+}
+
+.status-succeeded, .supported-yes {
+ background-color: #90ff90;
+}
+
+.status-failed, .supported-no {
+ background-color: #ff9090;
+}
+
+.status-unknown {
+ background-color: #FFB;
+}
+
+.status-untested {
+ background-color: #ececec;
+}
+
+.status-pending {
+ background-color: #9FF;
+}
\ No newline at end of file
diff --git a/chrome/content/zotero/tools/testTranslators/testTranslators.html b/chrome/content/zotero/tools/testTranslators/testTranslators.html
new file mode 100644
index 0000000000..14fab8e091
--- /dev/null
+++ b/chrome/content/zotero/tools/testTranslators/testTranslators.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+ Zotero Translator Tester
+
+
+
+
\ No newline at end of file
diff --git a/chrome/content/zotero/tools/testTranslators/testTranslators.js b/chrome/content/zotero/tools/testTranslators/testTranslators.js
new file mode 100644
index 0000000000..eaaa7dba85
--- /dev/null
+++ b/chrome/content/zotero/tools/testTranslators/testTranslators.js
@@ -0,0 +1,228 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2011 Center for History and New Media
+ George Mason University, Fairfax, Virginia, USA
+ http://zotero.org
+
+ This file is part of Zotero.
+
+ Zotero is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Zotero is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Zotero. If not, see .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+const CHROME_SAFARI_SCRIPTS = [
+ "zotero.js",
+ "zotero/date.js",
+ "zotero/debug.js",
+ "zotero/inject/translator.js",
+ "zotero/openurl.js",
+ "zotero/translate.js",
+ "zotero/utilities.js",
+ "zotero/messages.js",
+ "messaging_inject.js",
+ "translatorTests.js"
+];
+
+const TRANSLATOR_TYPES = ["Web", "Import", "Export", "Search"];
+const TABLE_COLUMNS = ["Translator", "Supported", "Status", "Pending", "Succeeded", "Failed", "Unknown"];
+var translatorTables = {};
+var translatorTestViewsToRun = {};
+var Zotero;
+
+/**
+ * Encapsulates a set of tests for a specific translator and type
+ * @constructor
+ */
+var TranslatorTestView = function(translator, type) {
+ this._translator = translator;
+
+ var row = document.createElement("tr");
+
+ // Translator
+ this._label = document.createElement("td");
+ this._label.appendChild(document.createTextNode(translator.label));
+ row.appendChild(this._label);
+
+ // Supported
+ this._supported = document.createElement("td");
+ var isSupported = translator.runMode === Zotero.Translator.RUN_MODE_IN_BROWSER;
+ this._supported.appendChild(document.createTextNode(isSupported ? "Yes" : "No"));
+ this._supported.className = isSupported ? "supported-yes" : "supported-no";
+ row.appendChild(this._supported);
+
+ // Status
+ this._status = document.createElement("td");
+ row.appendChild(this._status);
+
+ // Unknown
+ this._pending = document.createElement("td");
+ row.appendChild(this._pending);
+
+ // Succeeded
+ this._succeeded = document.createElement("td");
+ row.appendChild(this._succeeded);
+
+ // Failed
+ this._failed = document.createElement("td");
+ row.appendChild(this._failed);
+
+ // Unknown
+ this._unknown = document.createElement("td");
+ row.appendChild(this._unknown);
+
+ // append to table
+ translatorTables[type].appendChild(row);
+
+ // create translator tester and update status based on what it knows
+ this._translatorTester = new Zotero_TranslatorTester(translator, type);
+ this.updateStatus();
+ this.hasTests = !!this._translatorTester.tests.length;
+}
+
+/**
+ * Changes the displayed status of a translator
+ */
+TranslatorTestView.prototype.updateStatus = function() {
+ if(this._translatorTester.tests.length) {
+ if(this._translatorTester.pending.length) {
+ this._status.className = "status-pending";
+ this._status.textContent = "Pending";
+ } else if(this._translatorTester.failed.length) {
+ this._status.className = "status-failed";
+ this._status.textContent = "Failed";
+ } else if(this._translatorTester.unknown.length) {
+ this._status.className = "status-unknown";
+ this._status.textContent = "Unknown";
+ } else {
+ this._status.className = "status-succeeded";
+ this._status.textContent = "Succeeded";
+ }
+ } else {
+ this._status.className = "status-untested";
+ this._status.textContent = "Untested";
+ }
+
+ this._pending.textContent = this._translatorTester.pending.length;
+ this._succeeded.textContent = this._translatorTester.succeeded.length;
+ this._failed.textContent = this._translatorTester.failed.length;
+ this._unknown.textContent = this._translatorTester.unknown.length;
+}
+
+/**
+ * Runs test for this translator
+ */
+TranslatorTestView.prototype.runTests = function(doneCallback) {
+ var me = this;
+ if(Zotero.isFx) {
+ // yay, no message passing
+ var i = 1;
+ this._translatorTester.runTests(function(status, message) {
+ me.updateStatus();
+ if(me._translatorTester.pending.length === 0) {
+ doneCallback();
+ }
+ });
+ }
+}
+
+/**
+ * Called when loaded
+ */
+function load(event) {
+ if(window.chrome || window.safari) {
+ // load scripts
+ for(var i in CHROME_SAFARI_SCRIPTS) {
+ var script = document.createElement("script");
+ script.setAttribute("type", "text/javascript");
+ script.setAttribute("src", CHROME_SAFARI_SCRIPTS[i]);
+ document.head.appendChild(script);
+ }
+
+ // initialize
+ Zotero.initInject();
+ } else {
+ // load scripts
+ Zotero = Components.classes["@zotero.org/Zotero;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+ Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Components.interfaces.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://zotero/content/tools/testTranslators/translatorTester.js");
+ }
+
+ for(var i in TRANSLATOR_TYPES) {
+ var displayType = TRANSLATOR_TYPES[i];
+ var translatorType = displayType.toLowerCase();
+
+ // create header
+ var h1 = document.createElement("h1");
+ h1.appendChild(document.createTextNode(displayType+" Translators"));
+ document.body.appendChild(h1);
+
+ // create table
+ var translatorTable = document.createElement("table");
+ translatorTables[translatorType] = translatorTable;
+
+ // add headings to table
+ var headings = document.createElement("tr");
+ for(var j in TABLE_COLUMNS) {
+ var th = document.createElement("th");
+ th.className = "th-"+TABLE_COLUMNS[j].toLowerCase();
+ th.appendChild(document.createTextNode(TABLE_COLUMNS[j]));
+ headings.appendChild(th);
+ }
+
+ // append to document
+ translatorTable.appendChild(headings);
+ document.body.appendChild(translatorTable);
+
+ // get translators, with code for unsupported translators
+ Zotero.Translators.getAllForType(translatorType, new function() {
+ var type = translatorType;
+ return function(translators) {
+ haveTranslators(translators, type);
+ }
+ }, true);
+ }
+}
+
+/**
+ * Called after translators are returned from main script
+ */
+function haveTranslators(translators, type) {
+ translatorTestViewsToRun[type] = [];
+
+ for(var i in translators) {
+ var translatorTestView = new TranslatorTestView(translators[i], type);
+ if(translatorTestView.hasTests) {
+ translatorTestViewsToRun[type].push(translatorTestView);
+ }
+ }
+
+ runTranslatorTests(type);
+}
+
+/**
+ * Runs translator tests recursively, after translatorTestViews has been populated
+ */
+function runTranslatorTests(type) {
+ if(translatorTestViewsToRun[type].length) {
+ var translatorTestView = translatorTestViewsToRun[type].shift();
+ translatorTestView.runTests(function() { runTranslatorTests(type) });
+ }
+}
+
+window.addEventListener("load", load, false);
\ No newline at end of file
diff --git a/chrome/content/zotero/tools/testTranslators/translatorTester.js b/chrome/content/zotero/tools/testTranslators/translatorTester.js
new file mode 100644
index 0000000000..35bc7b623f
--- /dev/null
+++ b/chrome/content/zotero/tools/testTranslators/translatorTester.js
@@ -0,0 +1,169 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2009 Center for History and New Media
+ George Mason University, Fairfax, Virginia, USA
+ http://zotero.org
+
+ This file is part of Zotero.
+
+ Zotero is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Zotero is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Zotero. If not, see .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+const Zotero_TranslatorTester_IGNORE_FIELDS = ["complete", "accessDate", "checkFields"];
+
+/**
+ * A tool to run unit tests for a given translator
+ *
+ * @property {Array} tests All tests for this translator
+ * @property {Array} pending All tests for this translator
+ * @property {Array} succeeded All tests for this translator
+ * @property {Array} failed All tests for this translator
+ * @property {Array} unknown All tests for this translator
+ * @constructor
+ * @param {Zotero.Translator[]} translator The translator for which to run tests
+ * @param {String} type The type of tests to run (web, import, export, or search)
+ */
+var Zotero_TranslatorTester = function(translator, type, debug) {
+ this._type = type;
+ this._translator = translator;
+ this._debug = (debug ? debug : function(a, b) { Zotero.debug(a, b) });
+
+ this.tests = [];
+ this.pending = [];
+ this.succeeded = [];
+ this.failed = [];
+ this.unknown = [];
+
+ var code = translator.code;
+ var testStart = code.indexOf("/** BEGIN TEST CASES **/");
+ var testEnd = code.indexOf("/** END TEST CASES **/");
+ if (testStart !== -1 && testEnd !== -1) {
+ var test = code.substring(testStart + 24, testEnd);
+ test = test.replace(/var testCases = /,'');
+ // The JSON parser doesn't like final semicolons
+ if (test.lastIndexOf(';') == (test.length-1)) {
+ test = test.slice(0,-1);
+ }
+ try {
+ var testObject = JSON.parse(test);
+ } catch (e) {
+ Zotero.logError(e);
+ }
+
+ for(var i in testObject) {
+ if(testObject[i].type === type) {
+ this.tests.push(testObject[i]);
+ this.pending.push(testObject[i]);
+ }
+ }
+ }
+}
+
+/**
+ * Executes tests for this translator
+ * @param {Function} testDoneCallback A callback to be executed each time a test is complete
+ */
+Zotero_TranslatorTester.prototype.runTests = function(testDoneCallback, recursiveRun) {
+ if(!recursiveRun) {
+ this._debug("TranslatorTester: Running "+this.pending.length+" tests for "+this._translator.label);
+ }
+ if(!this.pending.length) {
+ // always call testDoneCallback once if there are no tests
+ if(!recursiveRun) testDoneCallback("unknown", "No tests present");
+ return;
+ }
+
+ var test = this.pending.shift();
+ var testNumber = this.tests.length-this.pending.length;
+ var me = this;
+
+ var callback = function(status, message) {
+ me._debug("TranslatorTester: "+me._translator.label+" Test "+testNumber+": "+status+" ("+message+")");
+ me[status].push(test);
+ if(testDoneCallback) testDoneCallback(status, message);
+ me.runTests(testDoneCallback, true);
+ };
+
+ Zotero.HTTP.processDocuments(test.url,
+ function(doc) {
+ me.runTest(test, doc, callback);
+ },
+ null,
+ function(e) {
+ callback("failed", "Translation failed to initialize: "+e);
+ });
+}
+
+/**
+ * Executes a test for a translator, given the document to test upon
+ * @param {Object} test Test to execute
+ * @param {Document} doc DOM document to test against
+ * @param {Function} testDoneCallback A callback to be executed when test is complete
+ */
+Zotero_TranslatorTester.prototype.runTest = function(test, doc, testDoneCallback) {
+ this._debug(test);
+ var me = this;
+ var translate = Zotero.Translate.newInstance(this._type);
+ translate.setDocument(doc);
+ translate.setTranslator(this._translator);
+ translate.setHandler("done", function(obj, returnValue) { me._checkResult(test, obj, returnValue, testDoneCallback) });
+ translate.translate(false);
+}
+
+/**
+ * Checks whether the results of translation match what is expected by the test
+ * @param {Object} test Test that was executed
+ * @param {Zotero.Translate} translate The Zotero.Translate instance
+ * @param {Boolean} returnValue Whether translation completed successfully
+ * @param {Function} testDoneCallback A callback to be executed when test is complete
+ */
+Zotero_TranslatorTester.prototype._checkResult = function(test, translate, returnValue, testDoneCallback) {
+ if(!returnValue) {
+ testDoneCallback("failed", "Translation failed; examine debug output for errors");
+ return;
+ }
+
+ if(!translate.newItems.length) {
+ testDoneCallback("failed", "Translation failed; no items returned");
+ return;
+ }
+
+ if(translate.newItems.length !== test.items.length) {
+ testDoneCallback("unknown", "Expected "+test.items.length+" items; got "+translate.newItems.length);
+ return;
+ }
+
+ for(var i in test.items) {
+ var testItem = test.items[i];
+ var translatedItem = translate.newItems[i];
+
+ for(var j in Zotero_TranslatorTester_IGNORE_FIELDS) {
+ delete testItem[Zotero_TranslatorTester_IGNORE_FIELDS[j]];
+ delete translatedItem[Zotero_TranslatorTester_IGNORE_FIELDS[j]];
+ }
+
+ var testItemJSON = JSON.stringify(testItem);
+ var translatedItemJSON = JSON.stringify(translatedItem);
+ if(testItemJSON != translatedItemJSON) {
+ testDoneCallback("unknown", "Item "+i+" does not match");
+ this._debug("TranslatorTester: Mismatch between "+testItemJSON+" and "+translatedItemJSON);
+ return;
+ }
+ }
+
+ testDoneCallback("succeeded", "Test succeeded");
+}
\ No newline at end of file