Use Translator Tester code from shared repo for Scaffold

This commit is contained in:
Adomas Venčkauskas 2021-07-28 15:32:13 +03:00 committed by Adomas Ven
parent 8f90cfbcfd
commit 4d497afea0
8 changed files with 14 additions and 1612 deletions

View file

@ -269,7 +269,7 @@ var Scaffold = new function() {
}
//Strip JSON metadata
var code = yield translator.getCode();
var code = yield _translatorProvider.getCodeForTranslator(translator);
var lastUpdatedIndex = code.indexOf('"lastUpdated"');
var header = code.substr(0, lastUpdatedIndex + 50);
var m = /^\s*{[\S\s]*?}\s*?[\r\n]+/.exec(header);
@ -747,10 +747,6 @@ var Scaffold = new function() {
translator[props[i]] = metadata[props[i]];
}
translator.getCode = function () {
return Zotero.Promise.resolve(this.code);
};
if(!translator.configOptions) translator.configOptions = {};
if(!translator.displayOptions) translator.displayOptions = {};
if(!translator.browserSupport) translator.browserSupport = "g";

View file

@ -34,7 +34,7 @@
title="Scaffold"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="chrome://zotero/content/include.js"/>
<script src="chrome://zotero/content/tools/testTranslators/translatorTester.js"/>
<script src="chrome://zotero/content/xpcom/translate/testTranslators/translatorTester.js"/>
<script src="translators.js"/>
<script src="scaffold.js"/>

View file

@ -107,6 +107,17 @@ var Scaffold_Translators = {
return translator ? translator.translator : false;
}.bind(this),
getCodeForTranslator: async function (translator) {
if (translator.code) return translator.code;
return Zotero.File.getContentsAsync(translator.path).then(function(code) {
if (translator.cacheCode) {
// See Translator.init() for cache rules
translator.code = code;
}
return code;
});
}.bind(this),
getAllForType: async function (type) {
if (!this._translators.size) {
await this.load();

View file

@ -1,86 +0,0 @@
body {
font-family: Helvetica, sans;
font-size: 12px;
}
table {
border-color: black;
border-width: 0 0 1px 1px;
border-style: solid;
border-collapse: collapse;
width: 100%;
}
td, th {
border-color: black;
border-width: 1px 1px 0 0;
border-style: solid;
padding: 2px;
}
.th-translator {
}
.th-status {
width: 100px;
max-width: 100px;
}
.th-pending, .th-supported, .th-succeeded, .th-failed, .th-mismatch {
width: 75px;
max-width: 75px;
}
.th-issues {
}
.status-succeeded, .supported-yes {
background-color: #90ff90;
}
.status-failed, .supported-no {
background-color: #ff9090;
}
.status-mismatch {
background-color: #FFB;
}
.status-untested {
background-color: #ececec;
}
.status-pending, .status-running {
background-color: #9FF;
}
.status-partial-failure {
background-color: rgb(249, 180, 98);
}
tr.output-displayed > td {
background-color: #b4d5ff !important;
}
#translator-box {
position: absolute;
top: 0;
bottom: 25%;
left: 0;
right: 0;
padding: 5px;
overflow: scroll;
}
#output-box {
position: absolute;
top: 75%;
bottom: 0;
left: 0;
right: 0;
padding: 5px;
white-space: pre;
overflow: scroll;
font-family: Monaco, Courier, monospace;
font-size: 10px;
}

View file

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
***** 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 <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script type="text/javascript" src="testTranslators.js"></script>
<script type="text/javascript" src="chrome://zotero/content/include.js"></script>
<script type="text/javascript" src="translatorTester.js"></script>
<link rel="stylesheet" type="text/css" media="screen" href="testTranslators.css" />
<title>Zotero Translator Tester</title>
</head>
<body>
</body>
</html>

View file

@ -1,653 +0,0 @@
/*
***** 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 <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
const NUM_CONCURRENT_TESTS = 6;
const TABLE_COLUMNS = ["Translator", "Supported", "Status", "Pending", "Succeeded", "Failed", "Mismatch", "Issues"];
// Not using const to prevent const collisions in connectors
var TRANSLATOR_TYPES = ["Web", "Import", "Export", "Search"];
var translatorTables = {},
translatorTestViews = {},
translatorTestViewsToRun = {},
translatorTestStats = {},
translatorBox,
outputBox,
allOutputView,
currentOutputView,
seleniumOutput = {},
viewerMode = true;
/**
* Fetches issue information from GitHub
*/
var Issues = new function() {
var _executeWhenRetrieved = [];
var githubInfo;
/**
* Gets issues for a specific translator
* @param {String} translatorLabel Gets issues starting with translatorLabel
* @param {Function} callback Function to call when issue information is available
*/
this.getFor = function(translatorLabel, callback) {
translatorLabel = translatorLabel.toLowerCase();
var whenRetrieved = function() {
var issues = [];
for(var i=0; i<githubInfo.length; i++) {
var issue = githubInfo[i];
if(issue.title.substr(0, translatorLabel.length).toLowerCase() === translatorLabel) {
issues.push(issue);
}
}
callback(issues);
};
if(githubInfo) {
whenRetrieved();
} else {
_executeWhenRetrieved.push(whenRetrieved);
}
};
var req = new XMLHttpRequest();
req.open("GET", "https://api.github.com/repos/zotero/translators/issues?per_page=100", true);
req.onreadystatechange = function(e) {
if(req.readyState != 4) return;
githubInfo = JSON.parse(req.responseText);
for(var i=0; i<_executeWhenRetrieved.length; i++) {
_executeWhenRetrieved[i]();
}
_executeWhenRetrieved = [];
};
req.send();
}
/**
* Handles adding debug output to the output box
* @param {HTMLElement} el An element to add class="selected" to when this outputView is displayed
*/
var OutputView = function(el) {
this._output = [];
this._el = el;
}
/**
* Sets whether this output is currently displayed in the output box
* @param {Boolean} isDisplayed
*/
OutputView.prototype.setDisplayed = function(isDisplayed) {
this.isDisplayed = isDisplayed;
if(this.isDisplayed) outputBox.textContent = this._output.join("\n");
if(this._el) this._el.className = (isDisplayed ? "output-displayed" : "output-hidden");
currentOutputView = this;
}
/**
* Adds output to the output view
*/
OutputView.prototype.addOutput = function(msg, level) {
this._output.push(msg);
if(this.isDisplayed) outputBox.textContent = this._output.join("\n");
}
/**
* Gets output to the output view
*/
OutputView.prototype.getOutput = function() {
return this._output.join("\n");
}
/**
* Encapsulates a set of tests for a specific translator and type
* @constructor
*/
var TranslatorTestView = function() {
var row = this._row = document.createElement("tr");
// Translator
this._label = document.createElement("td");
row.appendChild(this._label);
// Supported
this._supported = document.createElement("td");
row.appendChild(this._supported);
// Status
this._status = document.createElement("td");
row.appendChild(this._status);
// Pending
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);
// Mismatch
this._unknown = document.createElement("td");
row.appendChild(this._unknown);
// Issues
this._issues = document.createElement("td");
row.appendChild(this._issues);
// create output view and debug function
var outputView = this._outputView = new OutputView(row);
this._debug = function(obj, msg, level) {
outputView.addOutput(msg, level);
allOutputView.addOutput(msg, level);
const translatorID = obj.translator.translatorID;
if (!seleniumOutput[translatorID]) {
seleniumOutput[translatorID] = { label: obj.translator.label, message: "" };
}
seleniumOutput[translatorID].message += msg + "\n";
}
// put click handler on row to allow display of debug output
row.addEventListener("click", function(e) {
// don't run deselect click event handler
e.stopPropagation();
currentOutputView.setDisplayed(false);
outputView.setDisplayed(true);
}, false);
// create translator tester and update status based on what it knows
this.isRunning = false;
}
/**
* Sets the label and retrieves corresponding GitHub issues
*/
TranslatorTestView.prototype.setLabel = function(label) {
this._label.appendChild(document.createTextNode(label));
var issuesNode = this._issues;
Issues.getFor(label, function(issues) {
for(var i=0; i<issues.length; i++) {
var issue = issues[i];
var div = document.createElement("div"),
a = document.createElement("a");
var date = issue.updated_at;
date = new Date(Date.UTC(date.substr(0, 4), date.substr(5, 2)-1, date.substr(8, 2),
date.substr(11, 2), date.substr(14, 2), date.substr(17, 2)));
if("toLocaleFormat" in date) {
date = date.toLocaleFormat("%x");
} else {
date = date.getFullYear()+"-"+date.getMonth()+"-"+date.getDate();
}
a.textContent = issue.title+" (#"+issue.number+"; "+date+")";
a.setAttribute("href", issue.html_url);
a.setAttribute("target", "_blank");
div.appendChild(a);
issuesNode.appendChild(div);
}
});
}
/**
* Initializes TranslatorTestView given a translator and its type
*/
TranslatorTestView.prototype.initWithTranslatorAndType = function(translator, type) {
this.setLabel(translator.label);
this._translatorTester = new Zotero_TranslatorTester(translator, type, this._debug);
this.canRun = !!this._translatorTester.tests.length;
this.updateStatus(this._translatorTester);
this._type = type;
translatorTestViews[type].push(this);
translatorTables[this._type].appendChild(this._row);
}
/**
* Initializes TranslatorTestView given a JSON-ified translatorTester
*/
TranslatorTestView.prototype.unserialize = function(serializedData) {
this._outputView.addOutput(serializedData.output);
this.setLabel(serializedData.label);
this._type = serializedData.type;
translatorTestViews[serializedData.type].push(this);
this.canRun = false;
this.updateStatus(serializedData);
translatorTables[this._type].appendChild(this._row);
}
/**
* Initializes TranslatorTestView given a JSON-ified translatorTester
*/
TranslatorTestView.prototype.serialize = function(serializedData) {
return this._translatorTester.serialize();
}
/**
* Changes the displayed status of a translator
*/
TranslatorTestView.prototype.updateStatus = function(obj, status) {
while(this._status.hasChildNodes()) {
this._status.removeChild(this._status.firstChild);
}
this._supported.textContent = obj.isSupported ? "Yes" : "No";
this._supported.className = obj.isSupported ? "supported-yes" : "supported-no";
var pending = typeof obj.pending === "object" ? obj.pending.length : obj.pending;
var succeeded = typeof obj.succeeded === "object" ? obj.succeeded.length : obj.succeeded;
var failed = typeof obj.failed === "object" ? obj.failed.length : obj.failed;
var unknown = typeof obj.unknown === "object" ? obj.unknown.length : obj.unknown;
if(pending || succeeded || failed || unknown) {
if(pending) {
if(this.isRunning) {
this._status.className = "status-running";
this._status.textContent = "Running";
} else if(status && status === "pending") {
this._status.className = "status-pending";
this._status.textContent = "Pending";
} else if(this.canRun) {
// show link to start
var me = this;
var a = document.createElement("a");
a.href = "#";
a.addEventListener("click", function(e) {
e.preventDefault();
me.runTests();
}, false);
a.textContent = "Run";
this._status.appendChild(a);
} else {
this._status.textContent = "Not Run";
}
} else if((succeeded || unknown) && failed) {
this._status.className = "status-partial-failure";
this._status.textContent = "Partial Failure";
} else if(failed) {
this._status.className = "status-failed";
this._status.textContent = "Failure";
} else if(unknown) {
this._status.className = "status-mismatch";
this._status.textContent = "Data Mismatch";
} else {
this._status.className = "status-succeeded";
this._status.textContent = "Success";
}
} else {
this._status.className = "status-untested";
this._status.textContent = "Untested";
}
this._pending.textContent = pending;
this._succeeded.textContent = succeeded;
this._failed.textContent = failed;
this._unknown.textContent = unknown;
if(this._type) translatorTestStats[this._type].update();
}
/**
* Runs test for this translator
*/
TranslatorTestView.prototype.runTests = function(doneCallback) {
if(this.isRunning) return;
this.isRunning = true;
// show as running
this.updateStatus(this._translatorTester);
// set up callback
var me = this;
var newCallback = function(obj, test, status, message) {
me.updateStatus(obj);
if(obj.pending.length === 0 && doneCallback) {
doneCallback();
}
};
this._translatorTester.runTests(newCallback);
}
/**
* Gets overall stats for translators
*/
var TranslatorTestStats = function(translatorType) {
this.translatorType = translatorType
this.node = document.createElement("p");
};
TranslatorTestStats.prototype.update = function() {
var types = {
"Success":0,
"Data Mismatch":0,
"Partial Failure":0,
"Failure":0,
"Untested":0,
"Running":0,
"Pending":0,
"Not Run":0
};
var testViews = translatorTestViews[this.translatorType];
for(var i in testViews) {
var status = testViews[i]._status ? testViews[i]._status.textContent : "Not Run";
if(status in types) {
types[status] += 1;
}
}
var typeInfo = [];
for(var i in types) {
if(types[i]) {
typeInfo.push(i+": "+types[i]);
}
}
this.node.textContent = typeInfo.join(" | ");
};
/**
* Called when loaded
*/
function load(event) {
try {
viewerMode = !Zotero;
} catch(e) {};
if(!viewerMode && (window.chrome || window.safari)) {
// initialize injection
Zotero.initInject();
// make sure that connector is online
Zotero.Connector.checkIsOnline(function (status) {
if (status || Zotero.allowRepoTranslatorTester) {
init();
} else {
document.body.textContent = "To avoid excessive repo requests, the translator tester may only be used when Zotero Standalone is running.";
}
});
} else {
init();
}
}
/**
* Builds translator display and retrieves translators
*/
async function init() {
// create translator box
translatorBox = document.createElement("div");
translatorBox.id = "translator-box";
document.body.appendChild(translatorBox);
// create output box
outputBox = document.createElement("div");
outputBox.id = "output-box";
document.body.appendChild(outputBox);
// set click handler for translator box to display all output, so that when the user clicks
// outside of a translator, it will revert to this state
translatorBox.addEventListener("click", function(e) {
currentOutputView.setDisplayed(false);
allOutputView.setDisplayed(true);
}, false);
// create output view for all output and display
allOutputView = new OutputView();
allOutputView.setDisplayed(true);
await Promise.all(TRANSLATOR_TYPES.map(async displayType => {
let translatorType = displayType.toLowerCase();
translatorTestViews[translatorType] = [];
// create header
var h1 = document.createElement("h1");
h1.appendChild(document.createTextNode(displayType+" Translators "));
if(!viewerMode) {
// create "run all"
var runAll = document.createElement("a");
runAll.href = "#";
runAll.appendChild(document.createTextNode("(Run)"));
runAll.addEventListener("click", new function() {
var type = translatorType;
return function(e) {
e.preventDefault();
runTranslatorTests(type);
}
}, false);
h1.appendChild(runAll);
}
translatorBox.appendChild(h1);
// create table
var translatorTable = document.createElement("table");
translatorTables[translatorType] = translatorTable;
translatorTestStats[translatorType] = new TranslatorTestStats(translatorType);
translatorBox.appendChild(translatorTestStats[translatorType].node);
// 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);
translatorBox.appendChild(translatorTable);
// get translators, with code for unsupported translators
if(!viewerMode) {
let translators = await Zotero.Translators.getAllForType(translatorType, true);
haveTranslators(translators, translatorType);
}
}));
if(viewerMode) {
// if no Zotero object, try to unserialize data
var req = new XMLHttpRequest();
var loc = "testResults.json";
if(window.location.hash) {
var hashVars = {};
var hashVarsSplit = window.location.hash.substr(1).split("&");
for(var i=0; i<hashVarsSplit.length; i++) {
var myVar = hashVarsSplit[i];
var index = myVar.indexOf("=");
hashVars[myVar.substr(0, index)] = myVar.substr(index+1);
}
if(hashVars["browser"] && /^[a-z]+$/.test(hashVars["browser"])
&& hashVars["version"] && /^[0-9a-zA-Z\-._]/.test(hashVars["version"])) {
loc = "testResults-"+hashVars["browser"]+"-"+hashVars["version"]+".json";
}
if(hashVars["date"] && /^[0-9\-]+$/.test(hashVars["date"])) {
loc = hashVars["date"]+"/"+loc;
}
}
req.open("GET", loc, true);
req.overrideMimeType("text/plain");
req.onreadystatechange = function(e) {
if(req.readyState != 4) return;
if(req.status === 200 && req.responseText) { // success; unserialize
var data = JSON.parse(req.responseText);
for(var i=0, n=data.results.length; i<n; i++) {
var translatorTestView = new TranslatorTestView();
translatorTestView.unserialize(data.results[i]);
}
} else {
jsonNotFound("XMLHttpRequest returned "+req.status);
}
};
try {
req.send();
} catch(e) {
jsonNotFound(e.toString());
}
} else {
// create "serialize" link at bottom
var lastP = document.createElement("p");
var serialize = document.createElement("a");
serialize.href = "#";
serialize.appendChild(document.createTextNode("Serialize Results"));
serialize.addEventListener("click", serializeToDownload, false);
lastP.appendChild(serialize);
translatorBox.appendChild(lastP);
// Run translators specified in the hash params if any
runURLSpecifiedTranslators();
}
}
/**
* Indicates no JSON file could be found.
*/
function jsonNotFound(str) {
var body = document.body;
while(body.hasChildNodes()) body.removeChild(body.firstChild);
body.textContent = "testResults.json could not be loaded ("+str+").";
}
/**
* Called after translators are returned from main script
*/
function haveTranslators(translators, type) {
translatorTestViewsToRun[type] = [];
translators = translators.sort(function(a, b) {
return a.label.localeCompare(b.label);
});
var promises = [];
for(var i in translators) {
promises.push(Zotero.Translators.getCodeForTranslator(translators[i]));
}
return Promise.all(promises).then(function(codes) {
for(var i in translators) {
// Make sure translator code is cached on the object
translators[i].code = codes[i];
var translatorTestView = new TranslatorTestView();
translatorTestView.initWithTranslatorAndType(translators[i], type);
if(translatorTestView.canRun) {
translatorTestViewsToRun[type].push(translatorTestView);
}
}
translatorTestStats[type].update();
var ev = document.createEvent('HTMLEvents');
ev.initEvent('ZoteroHaveTranslators-'+type, true, true);
document.dispatchEvent(ev);
});
}
async function runURLSpecifiedTranslators() {
const href = document.location.href;
let hashParams = href.split('#')[1];
if (!hashParams) return;
let translatorIDs = new Set(hashParams.split('translators=')[1].split(',').map(decodeURI));
let translatorTestViews = [];
for (let type in translatorTestViewsToRun) {
for (const translatorTestView of translatorTestViewsToRun[type]) {
if (translatorIDs.has(translatorTestView._translatorTester.translator.translatorID)) {
translatorTestViews.push(translatorTestView);
}
}
}
for (const translatorTestView of translatorTestViews) {
await new Promise((resolve) => {
translatorTestView.runTests(resolve);
});
}
var elem = document.createElement('p');
elem.setAttribute('id', 'translator-tests-complete');
document.body.appendChild(elem);
}
/**
* Begin running all translator tests of a given type
*/
function runTranslatorTests(type, callback) {
for(var i in translatorTestViewsToRun[type]) {
var testView = translatorTestViewsToRun[type][i];
testView.updateStatus(testView._translatorTester, "pending");
}
for(var i=0; i<NUM_CONCURRENT_TESTS; i++) {
initTests(type, callback);
}
}
/**
* Run translator tests recursively, after translatorTestViews has been populated
*/
function initTests(type, callback, runCallbackIfComplete) {
if(translatorTestViewsToRun[type].length) {
if(translatorTestViewsToRun[type].length === 1) runCallbackIfComplete = true;
var translatorTestView = translatorTestViewsToRun[type].shift();
translatorTestView.runTests(function() { initTests(type, callback, runCallbackIfComplete) });
} else if(callback && runCallbackIfComplete) {
callback();
}
}
/**
* Serializes translator tests to JSON
*/
function serializeToJSON() {
var serializedData = {"browser":Zotero.browser, "version":Zotero.version, "results":[]};
for(var i in translatorTestViews) {
var n = translatorTestViews[i].length;
for(var j=0; j<n; j++) {
serializedData.results.push(translatorTestViews[i][j].serialize());
}
}
return serializedData;
}
/**
* Serializes all run translator tests
*/
function serializeToDownload(e) {
var serializedData = serializeToJSON();
document.location.href = "data:application/octet-stream,"+encodeURIComponent(JSON.stringify(serializedData, null, "\t"));
e.preventDefault();
}
window.addEventListener("load", load, false);

View file

@ -1,829 +0,0 @@
/*
***** 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 <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
// Timeout for test to complete
var TEST_RUN_TIMEOUT = 15000;
var EXPORTED_SYMBOLS = ["Zotero_TranslatorTesters"];
// For debugging specific translators by label
var includeTranslators = [];
if (typeof window != "undefined") {
window.Zotero = window.Zotero;
} else if (typeof global != 'undefined') {
global.Zotero = global.Zotero;
} else if (typeof this != 'undefined') {
this.Zotero = this.Zotero;
}
var Zotero_TranslatorTesters = new function() {
const TEST_TYPES = ["web", "import", "export", "search"];
var collectedResults = {};
/**
* Runs all tests
*/
this.runAllTests = function (numConcurrentTests, skipTranslators, writeDataCallback) {
var id = Math.random() * (100000000 - 1) + 1;
if (!(typeof process === 'object' && process + '' === '[object process]')){
waitForDialog();
if(!Zotero) {
Zotero = Components.classes["@zotero.org/Zotero;1"]
.getService(Components.interfaces.nsISupports).wrappedJSObject;
}
}
var testers = [];
var waitingForTranslators = TEST_TYPES.length;
for(var i=0; i<TEST_TYPES.length; i++) {
Zotero.Translators.getAllForType(TEST_TYPES[i], true).
then(new function() {
var type = TEST_TYPES[i];
return function(translators) {
try {
for(var i=0; i<translators.length; i++) {
if (includeTranslators.length
&& !includeTranslators.some(x => translators[i].label.includes(x))) continue;
if (skipTranslators && skipTranslators[translators[i].translatorID]) continue;
testers.push(new Zotero_TranslatorTester(translators[i], type));
};
if(!(--waitingForTranslators)) {
runTesters(testers, numConcurrentTests, id, writeDataCallback);
}
} catch(e) {
Zotero.debug(e);
Zotero.logError(e);
}
};
});
};
};
/**
* Runs a specific set of tests
*/
function runTesters(testers, numConcurrentTests, id, writeDataCallback) {
var testersRunning = 0;
var results = []
var testerDoneCallback = function(tester) {
try {
if(tester.pending.length) return;
Zotero.debug("Done testing "+tester.translator.label);
// Done translating, so serialize test results
testersRunning--;
let results = tester.serialize();
let last = !testers.length && !testersRunning;
collectData(id, results, last, writeDataCallback);
if(testers.length) {
// Run next tester if one is available
runNextTester();
}
} catch(e) {
Zotero.debug(e);
Zotero.logError(e);
}
};
var runNextTester = function() {
if (!testers.length) {
return;
}
testersRunning++;
Zotero.debug("Testing "+testers[0].translator.label);
testers.shift().runTests(testerDoneCallback);
};
for(var i=0; i<numConcurrentTests; i++) {
runNextTester();
};
}
function waitForDialog() {
Components.utils.import("resource://gre/modules/Services.jsm");
var loadobserver = function (ev) {
ev.originalTarget.removeEventListener("load", loadobserver, false);
if (ev.target.location == "chrome://global/content/commonDialog.xul") {
let win = ev.target.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindow);
Zotero.debug("Closing rogue dialog box!\n\n" + win.document.documentElement.textContent, 2);
win.document.documentElement.getButton('accept').click();
}
};
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);
}
function collectData(id, results, last, writeDataCallback) {
if (!collectedResults[id]) {
collectedResults[id] = [];
}
collectedResults[id].push(results);
//
// TODO: Only do the below every x collections, or if last == true
//
// Sort results
if ("getLocaleCollation" in Zotero) {
let collation = Zotero.getLocaleCollation();
var strcmp = function (a, b) {
return collation.compareString(1, a, b);
};
}
else {
var strcmp = function (a, b) {
return a.toLowerCase().localeCompare(b.toLowerCase());
};
}
collectedResults[id].sort(function (a, b) {
if (a.type !== b.type) {
return TEST_TYPES.indexOf(a.type) - TEST_TYPES.indexOf(b.type);
}
return strcmp(a.label, b.label);
});
writeDataCallback(collectedResults[id], last);
}
}
/**
* 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)
* @param {Function} [debugCallback] A function to call to write debug output. If not present,
* Zotero.debug will be used.
* @param {Object} [translatorProvider] Used by Scaffold to override Zotero.Translators
*/
var Zotero_TranslatorTester = function(translator, type, debugCallback, translatorProvider) {
this.type = type;
this.translator = translator;
this.output = "";
this.isSupported = this.translator.runMode === Zotero.Translator.RUN_MODE_IN_BROWSER;
this.translator.runMode = Zotero.Translator.RUN_MODE_IN_BROWSER;
this.translatorProvider = translatorProvider;
this.tests = [];
this.pending = [];
this.succeeded = [];
this.failed = [];
this.unknown = [];
var me = this;
this._debug = function(obj, a, b) {
me.output += me.output ? "\n"+a : a;
if(debugCallback) {
debugCallback(me, a, b);
} else {
Zotero.debug(a, b);
}
};
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)
.replace(/^[\s\r\n]*var testCases = /, '')
.replace(/;[\s\r\n]*$/, '');
try {
var testObject = JSON.parse(test);
} catch (e) {
Zotero.logError(e+" parsing tests for "+translator.label);
return;
}
for(var i=0, n=testObject.length; i<n; i++) {
if(testObject[i].type === type) {
this.tests.push(testObject[i]);
this.pending.push(testObject[i]);
}
}
}
};
Zotero_TranslatorTester.DEFER_DELAY = 5000; // Delay for deferred tests
/**
* Removes document objects, which contain cyclic references, and other fields to be ignored from items
* @param {Object} Item, in the format returned by Zotero.Item.serialize()
*/
Zotero_TranslatorTester._sanitizeItem = function(item, testItem, keepValidFields) {
// remove cyclic references
if(item.attachments && item.attachments.length) {
// don't actually test URI equality
for (var i=0; i<item.attachments.length; i++) {
var attachment = item.attachments[i];
if(attachment.document) {
delete attachment.document;
// Mirror connector/server itemDone() behavior from translate.js
attachment.mimeType = 'text/html';
}
if(attachment.url) {
delete attachment.url;
}
if(attachment.complete) {
delete attachment.complete;
}
}
}
// try to convert to JSON and back to get rid of undesirable undeletable elements; this may fail
try {
item = JSON.parse(JSON.stringify(item));
} catch(e) {};
// remove fields that don't exist or aren't valid for this item type, and normalize base fields
// to fields specific to this item
var fieldID, itemFieldID,
typeID = Zotero.ItemTypes.getID(item.itemType);
const skipFields = ["note", "notes", "itemID", "attachments", "tags", "seeAlso",
"itemType", "complete", "creators"];
for(var field in item) {
if(skipFields.indexOf(field) !== -1) {
continue;
}
if((!item[field] && (!testItem || item[field] !== false))
|| !(fieldID = Zotero.ItemFields.getID(field))) {
delete item[field];
continue;
}
if(itemFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(typeID, fieldID)) {
var value = item[field];
delete item[field];
item[Zotero.ItemFields.getName(itemFieldID)] = value;
continue;
}
if(!Zotero.ItemFields.isValidForType(fieldID, typeID)) {
delete item[field];
}
}
// remove fields to be ignored
if(!keepValidFields && "accessDate" in item) delete item.accessDate;
// Sort tags
if (item.tags && Array.isArray(item.tags)) {
// Normalize tags -- necessary until tests are updated for 5.0
if (testItem) {
item.tags = Zotero.Translate.Base.prototype._cleanTags(item.tags);
}
item.tags.sort((a, b) => {
if (a.tag < b.tag) return -1;
if (b.tag < a.tag) return 1;
return 0;
});
}
return item;
};
/**
* Serializes translator tester results to JSON
*/
Zotero_TranslatorTester.prototype.serialize = function() {
return {
"translatorID":this.translator.translatorID,
"type":this.type,
"output":this.output,
"label":this.translator.label,
"isSupported":this.isSupported,
"pending":this.pending,
"failed":this.failed,
"succeeded":this.succeeded,
"unknown":this.unknown
};
};
/**
* Sets tests for this translatorTester
*/
Zotero_TranslatorTester.prototype.setTests = function(tests) {
this.tests = tests.slice(0);
this.pending = tests.slice(0);
this.succeeded = [];
this.failed = [];
this.unknown = [];
};
/**
* 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) {
var w = (this.pending.length === 1) ? "test" : "tests";
this._debug(this, "TranslatorTester: Running "+this.pending.length+" "+w+" for "+this.translator.label);
}
if(!this.pending.length) {
// always call testDoneCallback once if there are no tests
if(!recursiveRun && testDoneCallback) testDoneCallback(this, null, "unknown", "No tests present\n");
return;
}
this._runTestsRecursively(testDoneCallback);
};
/**
* Executes tests for this translator, without checks or a debug message
* @param {Function} testDoneCallback A callback to be executed each time a test is complete
*/
Zotero_TranslatorTester.prototype._runTestsRecursively = function(testDoneCallback) {
var test = this.pending.shift();
var testNumber = this.tests.length-this.pending.length;
var me = this;
this._debug(this, "TranslatorTester: Running "+this.translator.label+" Test "+testNumber);
var executedCallback = false;
var callback = function(obj, test, status, message) {
if(executedCallback) return;
executedCallback = true;
me._debug(this, "TranslatorTester: "+me.translator.label+" Test "+testNumber+": "+status+" ("+message+")");
me[status].push(test);
test.message = message;
if(testDoneCallback) testDoneCallback(me, test, status, message);
me.runTests(testDoneCallback, true);
};
if(this.type === "web") {
this.fetchPageAndRunTest(test, callback);
} else {
(Zotero.setTimeout ? Zotero : window).setTimeout(function() {
me.runTest(test, null, callback);
}, 0);
}
(Zotero.setTimeout ? Zotero : window).setTimeout(function() {
callback(me, test, "failed", "Test timed out after "+TEST_RUN_TIMEOUT/1000+" seconds");
}, TEST_RUN_TIMEOUT);
};
/**
* Fetches the page for a given test and runs it
*
* This function is only applicable in Firefox; it is overridden in translator_global.js in Chrome
* and Safari.
*
* @param {Object} test - Test to execute
* @param {Function} testDoneCallback - A callback to be executed when test is complete
*/
Zotero_TranslatorTester.prototype.fetchPageAndRunTest = function (test, testDoneCallback) {
// Scaffold
if (Zotero.isFx) {
let browser = Zotero.HTTP.loadDocuments(
test.url,
(doc) => {
if (test.defer) {
Zotero.debug("Waiting " + (Zotero_TranslatorTester.DEFER_DELAY / 1000)
+ " second(s) for page content to settle");
}
setTimeout(() => {
// Use cookies from document in translator HTTP requests
this._cookieSandbox = new Zotero.CookieSandbox(null, test.url, doc.cookie);
this.runTest(test, doc, function (obj, test, status, message) {
Zotero.Browser.deleteHiddenBrowser(browser);
testDoneCallback(obj, test, status, message);
});
}, test.defer ? Zotero_TranslatorTester.DEFER_DELAY : 0);
},
null,
(e) => {
Zotero.Browser.deleteHiddenBrowser(browser);
testDoneCallback(this, test, "failed", "Translation failed to initialize: " + e);
},
true
);
browser.docShell.allowMetaRedirects = true;
return
}
if (typeof process === 'object' && process + '' === '[object process]'){
this._cookieSandbox = require('request').jar();
}
Zotero.HTTP.processDocuments(
test.url,
(doc) => {
this.runTest(test, doc, function (obj, test, status, message) {
testDoneCallback(obj, test, status, message);
});
},
{
cookieSandbox: this._cookieSandbox
}
)
.catch(function (e) {
testDoneCallback(this, test, "failed", "Translation failed to initialize: " + e);
}.bind(this))
};
/**
* Executes a test for a translator, given the document to test upon
* @param {Object} test Test to execute
* @param {Document} data 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(this, "TranslatorTester: Translating"+(test.url ? " "+test.url : ""));
var me = this;
var translate = Zotero.Translate.newInstance(this.type);
if (this.translatorProvider) {
translate.setTranslatorProvider(this.translatorProvider);
}
if(this.type === "web") {
translate.setDocument(doc);
} else if(this.type === "import") {
translate.setString(test.input);
} else if(this.type === "search") {
translate.setSearch(test.input);
}
if (translate.setCookieSandbox && this._cookieSandbox) {
translate.setCookieSandbox(this._cookieSandbox);
}
translate.setHandler("translators", function(obj, translators) {
me._runTestTranslate(translate, translators, test, testDoneCallback);
});
translate.setHandler("debug", this._debug);
var errorReturned;
translate.setHandler("error", function(obj, err) {
errorReturned = err;
});
translate.setHandler("done", function(obj, returnValue) {
me._checkResult(test, obj, returnValue, errorReturned, testDoneCallback);
});
var selectCalled = false;
translate.setHandler("select", function(obj, items, callback) {
if(test.items !== "multiple" && test.items.length <= 1) {
testDoneCallback(me, test, "failed", "Zotero.selectItems() called, but only one item defined in test");
callback({});
return;
} else if(selectCalled) {
testDoneCallback(me, test, "failed", "Zotero.selectItems() called multiple times");
callback({});
return;
}
selectCalled = true;
var newItems = {};
var haveItems = false;
for(var i in items) {
if(items[i] && typeof(items[i]) == "object" && items[i].title !== undefined) {
newItems[i] = items[i].title;
} else {
newItems[i] = items[i];
}
haveItems = true;
// only save one item if "items":"multiple" (as opposed to an array of items)
if(test.items === "multiple") break;
}
if(!haveItems) {
testDoneCallback(me, test, "failed", "No items defined");
callback({});
}
callback(newItems);
});
translate.capitalizeTitles = false;
// internal hack to call detect on this translator
translate._potentialTranslators = [this.translator];
translate._foundTranslators = [];
translate._currentState = "detect";
translate._detect();
}
/**
* Runs translation for a translator, given a document to test against
*/
Zotero_TranslatorTester.prototype._runTestTranslate = function(translate, translators, test, testDoneCallback) {
if(!translators.length) {
testDoneCallback(this, test, "failed", "Detection failed");
return;
} else if(this.type === "web" && translators[0].itemType !== Zotero.Translator.RUN_MODE_ZOTERO_SERVER
&& (translators[0].itemType !== "multiple" && test.items.length > 1 ||
test.items.length === 1 && translators[0].itemType !== test.items[0].itemType)) {
// this handles "items":"multiple" too, since the string has length 8
testDoneCallback(this, test, "failed", "Detection returned wrong item type");
return;
}
translate.setTranslator(this.translator);
translate.translate({
libraryID: 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 {Error} error Error code, if one was specified
* @param {Function} testDoneCallback A callback to be executed when test is complete
*/
Zotero_TranslatorTester.prototype._checkResult = function(test, translate, returnValue, error, testDoneCallback) {
if(error) {
var errorString = "Translation failed: "+error.toString();
if(typeof error === "object") {
for(var i in error) {
if(typeof(error[i]) != "object") {
errorString += "\n"+i+' => '+error[i];
}
}
}
testDoneCallback(this, test, "failed", errorString);
return;
}
if(!returnValue) {
testDoneCallback(this, test, "failed", "Translation failed; examine debug output for errors");
return;
}
if(!translate.newItems.length) {
testDoneCallback(this, test, "failed", "Translation failed: no items returned");
return;
}
if(test.items !== "multiple") {
if(translate.newItems.length !== test.items.length) {
testDoneCallback(this, test, "unknown", "Expected "+test.items.length+" items; got "+translate.newItems.length);
return;
}
for(var i=0, n=test.items.length; i<n; i++) {
var testItem = Zotero_TranslatorTester._sanitizeItem(test.items[i], true);
var translatedItem = Zotero_TranslatorTester._sanitizeItem(translate.newItems[i]);
if(!Zotero_TranslatorTester._compare(testItem, translatedItem)) {
// Show diff
this._debug(this, "TranslatorTester: Data mismatch detected:");
this._debug(this, Zotero_TranslatorTester._generateDiff(testItem, translatedItem));
// Save items. This makes it easier to correct tests automatically.
var m = translate.newItems.length;
test.itemsReturned = new Array(m);
for(var j=0; j<m; j++) {
test.itemsReturned[j] = Zotero_TranslatorTester._sanitizeItem(translate.newItems[i]);
}
testDoneCallback(this, test, "unknown", "Item "+i+" does not match");
return;
}
}
}
testDoneCallback(this, test, "succeeded", "Test succeeded");
};
/**
* Creates a new test for a document
* @param {Document} doc DOM document to test against
* @param {Function} testReadyCallback A callback to be passed test (as object) when complete
*/
Zotero_TranslatorTester.prototype.newTest = function(doc, testReadyCallback) {
// keeps track of whether select was called
var multipleMode = false;
var me = this;
var translate = Zotero.Translate.newInstance(this.type);
if (this.translatorProvider) {
translate.setTranslatorProvider(this.translatorProvider);
}
translate.setDocument(doc);
// Use cookies from document
if (doc.cookie) {
translate.setCookieSandbox(new Zotero.CookieSandbox(
null,
doc.location.href,
doc.cookie
));
}
translate.setTranslator(this.translator);
translate.setHandler("debug", this._debug);
translate.setHandler("select", function(obj, items, callback) {
multipleMode = true;
var newItems = {};
for(var i in items) {
if(items[i] && typeof(items[i]) == "object" && items[i].title !== undefined) {
newItems[i] = items[i].title;
} else {
newItems[i] = items[i];
}
break;
}
callback(newItems);
});
translate.setHandler("done", function(obj, returnValue) { me._createTest(obj, multipleMode, returnValue, testReadyCallback) });
translate.capitalizeTitles = false;
translate.translate({
libraryID: false
});
};
/**
* Creates a new test for a document
* @param {Zotero.Translate} translate The Zotero.Translate instance
* @param {Function} testDoneCallback A callback to be passed test (as object) when complete
*/
Zotero_TranslatorTester.prototype._createTest = function(translate, multipleMode, returnValue, testReadyCallback) {
if(!returnValue) {
testReadyCallback(returnValue);
return;
}
if(!translate.newItems.length) {
testReadyCallback(false);
return;
}
if(multipleMode) {
var items = "multiple";
} else {
for(var i=0, n=translate.newItems.length; i<n; i++) {
Zotero_TranslatorTester._sanitizeItem(translate.newItems[i]);
}
var items = translate.newItems;
}
testReadyCallback(this, {"type":this.type, "url":translate.document.location.href,
"items":items});
};
/**
* Compare items or sets thereof
*/
Zotero_TranslatorTester._compare = function(a, b) {
// If a is false, comparisons always succeed. This allows us to explicitly set that
// certain properties are allowed.
if(a === false) return true;
if(((typeof a === "object" && a !== null) || typeof a === "function")
&& ((typeof a === "object" && b !== null) || typeof b === "function")) {
if((Object.prototype.toString.apply(a) === "[object Array]")
!== (Object.prototype.toString.apply(b) === "[object Array]")) {
return false;
}
for(var key in a) {
if(!a.hasOwnProperty(key)) continue;
if(a[key] !== false && !b.hasOwnProperty(key)) return false;
if(!Zotero_TranslatorTester._compare(a[key], b[key])) return false;
}
for(var key in b) {
if(!b.hasOwnProperty(key)) continue;
if(!a.hasOwnProperty(key)) return false;
}
return true;
} else if(typeof a === "string" && typeof b === "string") {
// Ignore whitespace mismatches on strings
return a === b || Zotero.Utilities.trimInternal(a) === Zotero.Utilities.trimInternal(b);
}
return a === b;
};
/**
* Generate a diff of items
*/
Zotero_TranslatorTester._generateDiff = new function() {
function show(a, action, prefix, indent) {
if((typeof a === "object" && a !== null) || typeof a === "function") {
var isArray = Object.prototype.toString.apply(a) === "[object Array]",
startBrace = (isArray ? "[" : "{"),
endBrace = (isArray ? "]" : "}"),
changes = "",
haveKeys = false;
for(var key in a) {
if(!a.hasOwnProperty(key)) continue;
haveKeys = true;
changes += show(a[key], action,
isArray ? "" : JSON.stringify(key)+": ", indent+" ");
}
if(haveKeys) {
return action+" "+indent+prefix+startBrace+"\n"+
changes+action+" "+indent+endBrace+"\n";
}
return action+" "+indent+prefix+startBrace+endBrace+"\n";
}
return action+" "+indent+prefix+JSON.stringify(a)+"\n";
}
function compare(a, b, prefix, indent) {
if(!prefix) prefix = "";
if(!indent) indent = "";
if(((typeof a === "object" && a !== null) || typeof a === "function")
&& ((typeof b === "object" && b !== null) || typeof b === "function")) {
var aIsArray = Object.prototype.toString.apply(a) === "[object Array]",
bIsArray = Object.prototype.toString.apply(b) === "[object Array]";
if(aIsArray === bIsArray) {
var startBrace = (aIsArray ? "[" : "{"),
endBrace = (aIsArray ? "]" : "}"),
changes = "",
haveKeys = false;
for(var key in a) {
if(!a.hasOwnProperty(key)) continue;
haveKeys = true;
var keyPrefix = aIsArray ? "" : JSON.stringify(key)+": ";
if(b.hasOwnProperty(key)) {
changes += compare(a[key], b[key], keyPrefix, indent+" ");
} else {
changes += show(a[key], "-", keyPrefix, indent+" ");
}
}
for(var key in b) {
if(!b.hasOwnProperty(key)) continue;
haveKeys = true;
if(!a.hasOwnProperty(key)) {
var keyPrefix = aIsArray ? "" : JSON.stringify(key)+": ";
changes += show(b[key], "+", keyPrefix, indent+" ");
}
}
if(haveKeys) {
return " "+indent+prefix+startBrace+"\n"+
changes+" "+indent+(aIsArray ? "]" : "}")+"\n";
}
return " "+indent+prefix+startBrace+endBrace+"\n";
}
}
if(a === b) {
return show(a, " ", prefix, indent);
}
return show(a, "-", prefix, indent)+show(b, "+", prefix, indent);
}
return function(a, b) {
// Remove last newline
var txt = compare(a, b);
return txt.substr(0, txt.length-1);
};
};
if (typeof process === 'object' && process + '' === '[object process]'){
module.exports = {
Tester: Zotero_TranslatorTesters,
TranslatorTester: Zotero_TranslatorTester
};
}

@ -1 +1 @@
Subproject commit 4eeca1b5a698bb4cf5f1ce9db554a05b82be6bcd
Subproject commit 0024058720198f5967a50a57d33eb714b1c321c2