www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

testTranslators.js (18172B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2011 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     ***** END LICENSE BLOCK *****
     24 */
     25 
     26 const NUM_CONCURRENT_TESTS = 6;
     27 const TABLE_COLUMNS = ["Translator", "Supported", "Status", "Pending", "Succeeded", "Failed", "Mismatch", "Issues"];
     28 // Not using const to prevent const collisions in connectors
     29 var TRANSLATOR_TYPES = ["Web", "Import", "Export", "Search"];
     30 var translatorTables = {},
     31 	translatorTestViews = {},
     32 	translatorTestViewsToRun = {},
     33 	translatorTestStats = {},
     34 	translatorBox,
     35 	outputBox,
     36 	allOutputView,
     37 	currentOutputView,
     38 	viewerMode = true;
     39 
     40 /**
     41  * Fetches issue information from GitHub
     42  */
     43 var Issues = new function() {
     44 	var _executeWhenRetrieved = [];
     45 	var githubInfo;
     46 	
     47 	/**
     48 	 * Gets issues for a specific translator
     49 	 * @param {String} translatorLabel Gets issues starting with translatorLabel
     50 	 * @param {Function} callback Function to call when issue information is available
     51 	 */
     52 	this.getFor = function(translatorLabel, callback) {
     53 		translatorLabel = translatorLabel.toLowerCase();
     54 		
     55 		var whenRetrieved = function() {
     56 			var issues = [];
     57 			for(var i=0; i<githubInfo.length; i++) {
     58 				var issue = githubInfo[i];
     59 				if(issue.title.substr(0, translatorLabel.length).toLowerCase() === translatorLabel) {
     60 					issues.push(issue);
     61 				}
     62 			}
     63 			callback(issues);
     64 		};
     65 		
     66 		if(githubInfo) {
     67 			whenRetrieved();
     68 		} else {
     69 			_executeWhenRetrieved.push(whenRetrieved);
     70 		}
     71 	};
     72 	
     73 	var req = new XMLHttpRequest();
     74 	req.open("GET", "https://api.github.com/repos/zotero/translators/issues?per_page=100", true);
     75 	req.onreadystatechange = function(e) {
     76 		if(req.readyState != 4) return;
     77 		
     78 		githubInfo = JSON.parse(req.responseText);
     79 		for(var i=0; i<_executeWhenRetrieved.length; i++) {
     80 			_executeWhenRetrieved[i]();
     81 		}
     82 		_executeWhenRetrieved = [];
     83 	};
     84 	req.send();
     85 }
     86 
     87 /**
     88  * Handles adding debug output to the output box
     89  * @param {HTMLElement} el An element to add class="selected" to when this outputView is displayed
     90  */
     91 var OutputView = function(el) {
     92 	this._output = [];
     93 	this._el = el;
     94 }
     95 
     96 /**
     97  * Sets whether this output is currently displayed in the output box
     98  * @param {Boolean} isDisplayed
     99  */
    100 OutputView.prototype.setDisplayed = function(isDisplayed) {
    101 	this.isDisplayed = isDisplayed;
    102 	if(this.isDisplayed) outputBox.textContent = this._output.join("\n");
    103 	if(this._el) this._el.className = (isDisplayed ? "output-displayed" : "output-hidden");
    104 	currentOutputView = this;
    105 }
    106 
    107 /**
    108  * Adds output to the output view
    109  */
    110 OutputView.prototype.addOutput = function(msg, level) {
    111 	this._output.push(msg);
    112 	if(this.isDisplayed) outputBox.textContent = this._output.join("\n");
    113 }
    114 
    115 /**
    116  * Gets output to the output view
    117  */
    118 OutputView.prototype.getOutput = function() {
    119 	return this._output.join("\n");
    120 }
    121 
    122 /**
    123  * Encapsulates a set of tests for a specific translator and type
    124  * @constructor
    125  */
    126 var TranslatorTestView = function(translator, type) {
    127 	var row = this._row = document.createElement("tr");
    128 	
    129 	// Translator
    130 	this._label = document.createElement("td");
    131 	row.appendChild(this._label);
    132 	
    133 	// Supported
    134 	this._supported = document.createElement("td");
    135 	row.appendChild(this._supported);
    136 	
    137 	// Status
    138 	this._status = document.createElement("td");
    139 	row.appendChild(this._status);
    140 	
    141 	// Pending
    142 	this._pending = document.createElement("td");
    143 	row.appendChild(this._pending);
    144 	
    145 	// Succeeded
    146 	this._succeeded = document.createElement("td");
    147 	row.appendChild(this._succeeded);
    148 	
    149 	// Failed
    150 	this._failed = document.createElement("td");
    151 	row.appendChild(this._failed);
    152 	
    153 	// Mismatch
    154 	this._unknown = document.createElement("td");
    155 	row.appendChild(this._unknown);
    156 	
    157 	// Issues
    158 	this._issues = document.createElement("td");
    159 	row.appendChild(this._issues);
    160 	
    161 	// create output view and debug function
    162 	var outputView = this._outputView = new OutputView(row);
    163 	this._debug = function(obj, msg, level) {
    164 		outputView.addOutput(msg, level);
    165 		allOutputView.addOutput(msg, level);
    166 	}
    167 	
    168 	// put click handler on row to allow display of debug output
    169 	row.addEventListener("click", function(e) {
    170 		// don't run deselect click event handler
    171 		e.stopPropagation();
    172 		
    173 		currentOutputView.setDisplayed(false);
    174 		outputView.setDisplayed(true);
    175 	}, false);
    176 	
    177 	// create translator tester and update status based on what it knows
    178 	this.isRunning = false;
    179 }
    180 
    181 /**
    182  * Sets the label and retrieves corresponding GitHub issues
    183  */
    184 TranslatorTestView.prototype.setLabel = function(label) {
    185 	this._label.appendChild(document.createTextNode(label));
    186 	var issuesNode = this._issues;
    187 	Issues.getFor(label, function(issues) {
    188 		for(var i=0; i<issues.length; i++) {
    189 			var issue = issues[i];
    190 			var div = document.createElement("div"),
    191 				a = document.createElement("a");
    192 			
    193 			var date = issue.updated_at;
    194 			date = new Date(Date.UTC(date.substr(0, 4), date.substr(5, 2)-1, date.substr(8, 2),
    195 				date.substr(11, 2), date.substr(14, 2), date.substr(17, 2)));
    196 			if("toLocaleFormat" in date) {
    197 				date = date.toLocaleFormat("%x");
    198 			} else {
    199 				date = date.getFullYear()+"-"+date.getMonth()+"-"+date.getDate();
    200 			}
    201 			
    202 			a.textContent = issue.title+" (#"+issue.number+"; "+date+")";
    203 			a.setAttribute("href", issue.html_url);
    204 			a.setAttribute("target", "_blank");
    205 			div.appendChild(a);
    206 			issuesNode.appendChild(div);
    207 		}
    208 	});
    209 }
    210 
    211 /**
    212  * Initializes TranslatorTestView given a translator and its type
    213  */
    214 TranslatorTestView.prototype.initWithTranslatorAndType = function(translator, type) {
    215 	this.setLabel(translator.label);
    216 	
    217 	this._translatorTester = new Zotero_TranslatorTester(translator, type, this._debug);
    218 	this.canRun = !!this._translatorTester.tests.length;
    219 	this.updateStatus(this._translatorTester);
    220 	
    221 	this._type = type;
    222 	translatorTestViews[type].push(this);
    223 	translatorTables[this._type].appendChild(this._row);
    224 }
    225 
    226 /**
    227  * Initializes TranslatorTestView given a JSON-ified translatorTester
    228  */
    229 TranslatorTestView.prototype.unserialize = function(serializedData) {
    230 	this._outputView.addOutput(serializedData.output);
    231 	this.setLabel(serializedData.label);
    232 	
    233 	this._type = serializedData.type;
    234 	translatorTestViews[serializedData.type].push(this);
    235 	
    236 	this.canRun = false;
    237 	this.updateStatus(serializedData);
    238 	translatorTables[this._type].appendChild(this._row);
    239 }
    240 
    241 /**
    242  * Initializes TranslatorTestView given a JSON-ified translatorTester
    243  */
    244 TranslatorTestView.prototype.serialize = function(serializedData) {
    245 	return this._translatorTester.serialize();
    246 }
    247 
    248 /**
    249  * Changes the displayed status of a translator
    250  */
    251 TranslatorTestView.prototype.updateStatus = function(obj, status) {
    252 	while(this._status.hasChildNodes()) {
    253 		this._status.removeChild(this._status.firstChild);
    254 	}
    255 	
    256 	this._supported.textContent = obj.isSupported ? "Yes" : "No";
    257 	this._supported.className = obj.isSupported ? "supported-yes" : "supported-no";
    258 	
    259 	var pending = typeof obj.pending === "object" ? obj.pending.length : obj.pending;
    260 	var succeeded = typeof obj.succeeded === "object" ? obj.succeeded.length : obj.succeeded;
    261 	var failed = typeof obj.failed === "object" ? obj.failed.length : obj.failed;
    262 	var unknown = typeof obj.unknown === "object" ? obj.unknown.length : obj.unknown;
    263 	
    264 	if(pending || succeeded || failed || unknown) {
    265 		if(pending) {
    266 			if(this.isRunning) {
    267 				this._status.className = "status-running";
    268 				this._status.textContent = "Running";
    269 			} else if(status && status === "pending") {
    270 				this._status.className = "status-pending";
    271 				this._status.textContent = "Pending";
    272 			} else if(this.canRun) {
    273 				// show link to start
    274 				var me = this;
    275 				var a = document.createElement("a");
    276 				a.href = "#";
    277 				a.addEventListener("click", function(e) {
    278 					e.preventDefault();
    279 					me.runTests();
    280 				}, false);
    281 				a.textContent = "Run";
    282 				this._status.appendChild(a);
    283 			} else {
    284 				this._status.textContent = "Not Run";
    285 			}
    286 		} else if((succeeded || unknown) && failed) {
    287 			this._status.className = "status-partial-failure";
    288 			this._status.textContent = "Partial Failure";
    289 		} else if(failed) {
    290 			this._status.className = "status-failed";
    291 			this._status.textContent = "Failure";
    292 		} else if(unknown) {
    293 			this._status.className = "status-mismatch";
    294 			this._status.textContent = "Data Mismatch";
    295 		} else {
    296 			this._status.className = "status-succeeded";
    297 			this._status.textContent = "Success";
    298 		}
    299 	} else {
    300 		this._status.className = "status-untested";
    301 		this._status.textContent = "Untested";
    302 	}
    303 	
    304 	this._pending.textContent = pending;
    305 	this._succeeded.textContent = succeeded;
    306 	this._failed.textContent = failed;
    307 	this._unknown.textContent = unknown;
    308 	
    309 	if(this._type) translatorTestStats[this._type].update();
    310 }
    311 
    312 /**
    313  * Runs test for this translator
    314  */
    315 TranslatorTestView.prototype.runTests = function(doneCallback) {
    316 	if(this.isRunning) return;
    317 	this.isRunning = true;
    318 	
    319 	// show as running
    320 	this.updateStatus(this._translatorTester);
    321 	
    322 	// set up callback
    323 	var me = this;
    324 	var newCallback = function(obj, test, status, message) {
    325 		me.updateStatus(obj);
    326 		if(obj.pending.length === 0 && doneCallback) {
    327 			doneCallback();
    328 		}
    329 	};
    330 	
    331 	this._translatorTester.runTests(newCallback);
    332 }
    333 
    334 /**
    335  * Gets overall stats for translators
    336  */
    337 var TranslatorTestStats = function(translatorType) {
    338 	this.translatorType = translatorType
    339 	this.node = document.createElement("p");
    340 };
    341 
    342 TranslatorTestStats.prototype.update = function() {
    343 	var types = {
    344 		"Success":0,
    345 		"Data Mismatch":0,
    346 		"Partial Failure":0,
    347 		"Failure":0,
    348 		"Untested":0,
    349 		"Running":0,
    350 		"Pending":0,
    351 		"Not Run":0
    352 	};
    353 	
    354 	var testViews = translatorTestViews[this.translatorType];
    355 	for(var i in testViews) {
    356 		var status = testViews[i]._status ? testViews[i]._status.textContent : "Not Run";
    357 		if(status in types) {
    358 			types[status] += 1;
    359 		}
    360 	}
    361 	
    362 	var typeInfo = [];
    363 	for(var i in types) {
    364 		if(types[i]) {
    365 			typeInfo.push(i+": "+types[i]);
    366 		}
    367 	}
    368 	
    369 	this.node.textContent = typeInfo.join(" | ");
    370 };
    371 
    372 /**
    373  * Called when loaded
    374  */
    375 function load(event) {
    376 	try {
    377 		viewerMode = !Zotero;
    378 	} catch(e) {};
    379 	
    380 	if(!viewerMode && (window.chrome || window.safari)) {
    381 		// initialize injection
    382 		Zotero.initInject();
    383 		// make sure that connector is online
    384 		Zotero.Connector.checkIsOnline(function(status) {
    385 			if(status) {
    386 				init();
    387 			} else {
    388 				document.body.textContent = "To avoid excessive repo requests, the translator tester may only be used when Zotero Standalone is running.";
    389 			}
    390 		});
    391 	} else {
    392 		init();
    393 	}
    394 }
    395 
    396 /**
    397  * Builds translator display and retrieves translators
    398  */
    399 function init() {
    400 	// create translator box
    401 	translatorBox = document.createElement("div");
    402 	translatorBox.id = "translator-box";
    403 	document.body.appendChild(translatorBox);
    404 	
    405 	// create output box
    406 	outputBox = document.createElement("div");
    407 	outputBox.id = "output-box";
    408 	document.body.appendChild(outputBox);
    409 	
    410 	// set click handler for translator box to display all output, so that when the user clicks
    411 	// outside of a translator, it will revert to this state
    412 	translatorBox.addEventListener("click", function(e) {
    413 		currentOutputView.setDisplayed(false);
    414 		allOutputView.setDisplayed(true);
    415 	}, false);
    416 	
    417 	// create output view for all output and display
    418 	allOutputView = new OutputView();
    419 	allOutputView.setDisplayed(true);
    420 
    421 	for(var i in TRANSLATOR_TYPES) {
    422 		var displayType = TRANSLATOR_TYPES[i];
    423 		var translatorType = displayType.toLowerCase();
    424 		
    425 		translatorTestViews[translatorType] = [];
    426 		
    427 		// create header
    428 		var h1 = document.createElement("h1");
    429 		h1.appendChild(document.createTextNode(displayType+" Translators "));
    430 		
    431 		if(!viewerMode) {
    432 			// create "run all"
    433 			var runAll = document.createElement("a");
    434 			runAll.href = "#";
    435 			runAll.appendChild(document.createTextNode("(Run)"));
    436 			runAll.addEventListener("click", new function() {
    437 				var type = translatorType;
    438 				return function(e) {
    439 					e.preventDefault();
    440 					runTranslatorTests(type);
    441 				}
    442 			}, false);
    443 			h1.appendChild(runAll);
    444 		}
    445 		
    446 		translatorBox.appendChild(h1);
    447 		
    448 		// create table
    449 		var translatorTable = document.createElement("table");
    450 		translatorTables[translatorType] = translatorTable;
    451 		
    452 		translatorTestStats[translatorType] = new TranslatorTestStats(translatorType);
    453 		translatorBox.appendChild(translatorTestStats[translatorType].node);
    454 		
    455 		// add headings to table
    456 		var headings = document.createElement("tr");
    457 		for(var j in TABLE_COLUMNS) {
    458 			var th = document.createElement("th");
    459 			th.className = "th-"+TABLE_COLUMNS[j].toLowerCase();
    460 			th.appendChild(document.createTextNode(TABLE_COLUMNS[j]));
    461 			headings.appendChild(th);
    462 		}
    463 		
    464 		// append to document
    465 		translatorTable.appendChild(headings);
    466 		translatorBox.appendChild(translatorTable);
    467 		
    468 		// get translators, with code for unsupported translators
    469 		if(!viewerMode) {
    470 			Zotero.Translators.getAllForType(translatorType, true).
    471 			then(new function() {
    472 				var type = translatorType;
    473 				return function(translators) {
    474 					haveTranslators(translators, type);
    475 				}
    476 			});
    477 		}
    478 	}
    479 	
    480 	if(viewerMode) {
    481 		// if no Zotero object, try to unserialize data
    482 		var req = new XMLHttpRequest();
    483 		var loc = "testResults.json";
    484 		if(window.location.hash) {
    485 			var hashVars = {};
    486 			var hashVarsSplit = window.location.hash.substr(1).split("&");
    487 			for(var i=0; i<hashVarsSplit.length; i++) {
    488 				var myVar = hashVarsSplit[i];
    489 				var index = myVar.indexOf("=");
    490 				hashVars[myVar.substr(0, index)] = myVar.substr(index+1);
    491 			}
    492 			
    493 			if(hashVars["browser"] && /^[a-z]+$/.test(hashVars["browser"])
    494 					&& hashVars["version"] && /^[0-9a-zA-Z\-._]/.test(hashVars["version"])) {
    495 				loc = "testResults-"+hashVars["browser"]+"-"+hashVars["version"]+".json";
    496 			}
    497 			if(hashVars["date"] && /^[0-9\-]+$/.test(hashVars["date"])) {
    498 				loc = hashVars["date"]+"/"+loc;
    499 			}
    500 		}
    501 		req.open("GET", loc, true);
    502 		req.overrideMimeType("text/plain");
    503 		req.onreadystatechange = function(e) {
    504 			if(req.readyState != 4) return;
    505 
    506 			if(req.status === 200 && req.responseText) {	// success; unserialize
    507 				var data = JSON.parse(req.responseText);
    508 				for(var i=0, n=data.results.length; i<n; i++) {
    509 					var translatorTestView = new TranslatorTestView();
    510 					translatorTestView.unserialize(data.results[i]);
    511 				}
    512 			} else {
    513 				jsonNotFound("XMLHttpRequest returned "+req.status);
    514 			}
    515 		};
    516 		
    517 		try {
    518 			req.send();
    519 		} catch(e) {
    520 			jsonNotFound(e.toString());
    521 		}
    522 	} else {
    523 		// create "serialize" link at bottom
    524 		var lastP = document.createElement("p");
    525 		var serialize = document.createElement("a");
    526 		serialize.href = "#";
    527 		serialize.appendChild(document.createTextNode("Serialize Results"));
    528 		serialize.addEventListener("click", serializeToDownload, false);
    529 		lastP.appendChild(serialize);
    530 		translatorBox.appendChild(lastP);
    531 	}
    532 }
    533 
    534 /**
    535  * Indicates no JSON file could be found.
    536  */
    537 function jsonNotFound(str) {
    538 	var body = document.body;
    539 	while(body.hasChildNodes()) body.removeChild(body.firstChild);
    540 	body.textContent = "testResults.json could not be loaded ("+str+").";
    541 }
    542 
    543 /**
    544  * Called after translators are returned from main script
    545  */
    546 function haveTranslators(translators, type) {
    547 	translatorTestViewsToRun[type] = [];
    548 	
    549 	translators = translators.sort(function(a, b) {
    550 		return a.label.localeCompare(b.label);
    551 	});
    552 	
    553 	var promises = [];
    554 	for(var i in translators) {
    555 		promises.push(translators[i].getCode());
    556 	}
    557 	
    558 	return Promise.all(promises).then(function(codes) {
    559 		for(var i in translators) {
    560 			// Make sure translator code is cached on the object
    561 			translators[i].code = codes[i];
    562 			var translatorTestView = new TranslatorTestView();
    563 			translatorTestView.initWithTranslatorAndType(translators[i], type);
    564 			if(translatorTestView.canRun) {
    565 				translatorTestViewsToRun[type].push(translatorTestView);
    566 			}
    567 		}
    568 		
    569 		translatorTestStats[type].update();
    570 		var ev = document.createEvent('HTMLEvents');
    571 		ev.initEvent('ZoteroHaveTranslators-'+type, true, true);
    572 		document.dispatchEvent(ev);	
    573 	});
    574 }
    575 
    576 /**
    577  * Begin running all translator tests of a given type
    578  */
    579 function runTranslatorTests(type, callback) {
    580 	for(var i in translatorTestViewsToRun[type]) {
    581 		var testView = translatorTestViewsToRun[type][i];
    582 		testView.updateStatus(testView._translatorTester, "pending");
    583 	}
    584 	for(var i=0; i<NUM_CONCURRENT_TESTS; i++) {
    585 		initTests(type, callback);
    586 	}
    587 }
    588 
    589 /**
    590  * Run translator tests recursively, after translatorTestViews has been populated
    591  */
    592 function initTests(type, callback, runCallbackIfComplete) {
    593 	if(translatorTestViewsToRun[type].length) {
    594 		if(translatorTestViewsToRun[type].length === 1) runCallbackIfComplete = true;
    595 		var translatorTestView = translatorTestViewsToRun[type].shift();
    596 		translatorTestView.runTests(function() { initTests(type, callback, runCallbackIfComplete) });
    597 	} else if(callback && runCallbackIfComplete) {
    598 		callback();
    599 	}
    600 }
    601 
    602 /**
    603  * Serializes translator tests to JSON
    604  */
    605 function serializeToJSON() {
    606 	var serializedData = {"browser":Zotero.browser, "version":Zotero.version, "results":[]};
    607 	for(var i in translatorTestViews) {
    608 		var n = translatorTestViews[i].length;
    609 		for(var j=0; j<n; j++) {
    610 			serializedData.results.push(translatorTestViews[i][j].serialize());
    611 		}
    612 	}
    613 	return serializedData;
    614 }
    615 
    616 /**
    617  * Serializes all run translator tests
    618  */
    619 function serializeToDownload(e) {
    620 	var serializedData = serializeToJSON();
    621 	document.location.href = "data:application/octet-stream,"+encodeURIComponent(JSON.stringify(serializedData, null, "\t"));
    622 	e.preventDefault();
    623 }
    624 
    625 window.addEventListener("load", load, false);