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);