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