support.js (26716B)
1 chai.use(chaiAsPromised); 2 3 // Useful "constants" 4 var sqlDateTimeRe = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/; 5 var isoDateTimeRe = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/; 6 var zoteroObjectKeyRe = /^[23456789ABCDEFGHIJKLMNPQRSTUVWXYZ]{8}$/; // based on Zotero.Utilities::generateObjectKey() 7 8 /** 9 * Waits for a DOM event on the specified node. Returns a promise 10 * resolved with the event. 11 */ 12 function waitForDOMEvent(target, event, capture) { 13 var deferred = Zotero.Promise.defer(); 14 var func = function(ev) { 15 target.removeEventListener(event, func, capture); 16 deferred.resolve(ev); 17 } 18 target.addEventListener(event, func, capture); 19 return deferred.promise; 20 } 21 22 async function waitForRecognizer() { 23 var win = await waitForWindow('chrome://zotero/content/recognizePDFDialog.xul') 24 // Wait for status to show as complete 25 var completeStr = Zotero.getString("recognizePDF.complete.label"); 26 while (win.document.getElementById("label").value != completeStr) { 27 await Zotero.Promise.delay(20); 28 } 29 return win; 30 } 31 32 /** 33 * Open a chrome window and return a promise for the window 34 * 35 * @return {Promise<ChromeWindow>} 36 */ 37 function loadWindow(winurl, argument) { 38 var win = window.openDialog(winurl, "_blank", "chrome", argument); 39 return waitForDOMEvent(win, "load").then(function() { 40 return win; 41 }); 42 } 43 44 /** 45 * Open a browser window and return a promise for the window 46 * 47 * @return {Promise<ChromeWindow>} 48 */ 49 function loadBrowserWindow() { 50 var win = window.openDialog("chrome://browser/content/browser.xul", "", "all,height=700,width=1000"); 51 return waitForDOMEvent(win, "load").then(function() { 52 return win; 53 }); 54 } 55 56 /** 57 * Opens the Zotero pane and selects My Library. Returns the containing window. 58 * 59 * @param {Window} [win] - Existing window to use; if not specified, a new window is opened 60 */ 61 var loadZoteroPane = Zotero.Promise.coroutine(function* (win) { 62 if (!win) { 63 var win = yield loadBrowserWindow(); 64 } 65 Zotero.Prefs.clear('lastViewedFolder'); 66 win.ZoteroOverlay.toggleDisplay(true); 67 68 yield waitForItemsLoad(win, 0); 69 70 return win; 71 }); 72 73 var loadPrefPane = Zotero.Promise.coroutine(function* (paneName) { 74 var id = 'zotero-prefpane-' + paneName; 75 var win = yield loadWindow("chrome://zotero/content/preferences/preferences.xul", { 76 pane: id 77 }); 78 var doc = win.document; 79 var defer = Zotero.Promise.defer(); 80 var pane = doc.getElementById(id); 81 if (!pane.loaded) { 82 pane.addEventListener('paneload', () => defer.resolve()); 83 yield defer.promise; 84 } 85 return win; 86 }); 87 88 89 /** 90 * Waits for a window with a specific URL to open. Returns a promise for the window, and 91 * optionally passes the window to a callback immediately for use with modal dialogs, 92 * which prevent async code from continuing 93 */ 94 function waitForWindow(uri, callback) { 95 var deferred = Zotero.Promise.defer(); 96 var loadobserver = function(ev) { 97 ev.originalTarget.removeEventListener("load", loadobserver, false); 98 Zotero.debug("Window opened: " + ev.target.location.href); 99 100 if (ev.target.location.href != uri) { 101 Zotero.debug(`Ignoring window ${uri} in waitForWindow()`); 102 return; 103 } 104 105 Services.ww.unregisterNotification(winobserver); 106 var win = ev.target.docShell 107 .QueryInterface(Components.interfaces.nsIInterfaceRequestor) 108 .getInterface(Components.interfaces.nsIDOMWindow); 109 // Give window code time to run on load 110 win.setTimeout(function () { 111 if (callback) { 112 try { 113 // If callback returns a promise, wait for it 114 let maybePromise = callback(win); 115 if (maybePromise && maybePromise.then) { 116 maybePromise.then(() => deferred.resolve(win)).catch(e => deferred.reject(e)); 117 return; 118 } 119 } 120 catch (e) { 121 Zotero.logError(e); 122 win.close(); 123 deferred.reject(e); 124 return; 125 } 126 } 127 deferred.resolve(win); 128 }); 129 }; 130 var winobserver = {"observe":function(subject, topic, data) { 131 if(topic != "domwindowopened") return; 132 var win = subject.QueryInterface(Components.interfaces.nsIDOMWindow); 133 win.addEventListener("load", loadobserver, false); 134 }}; 135 Services.ww.registerNotification(winobserver); 136 return deferred.promise; 137 } 138 139 /** 140 * Wait for an alert or confirmation dialog to pop up and then close it 141 * 142 * @param {Function} [onOpen] - Function that is passed the dialog once it is opened. 143 * Can be used to make assertions on the dialog contents 144 * (e.g., with dialog.document.documentElement.textContent) 145 * @param {String} [button='accept'] - Button in dialog to press (e.g., 'cancel', 'extra1') 146 * @return {Promise} 147 */ 148 function waitForDialog(onOpen, button='accept', url) { 149 return waitForWindow(url || "chrome://global/content/commonDialog.xul", Zotero.Promise.method(function (dialog) { 150 var failure = false; 151 if (onOpen) { 152 try { 153 onOpen(dialog); 154 } 155 catch (e) { 156 failure = e; 157 } 158 } 159 if (button === false) { 160 if (failure) { 161 throw failure; 162 } 163 } 164 else if (button != 'cancel') { 165 let deferred = Zotero.Promise.defer(); 166 function acceptWhenEnabled() { 167 // Handle delayed buttons 168 if (dialog.document.documentElement.getButton(button).disabled) { 169 dialog.setTimeout(function () { 170 acceptWhenEnabled(); 171 }, 250); 172 } 173 else { 174 dialog.document.documentElement.getButton(button).click(); 175 if (failure) { 176 deferred.reject(failure); 177 } 178 else { 179 deferred.resolve(); 180 } 181 } 182 } 183 acceptWhenEnabled(); 184 return deferred.promise; 185 } 186 else { 187 dialog.document.documentElement.getButton(button).click(); 188 if (failure) { 189 throw failure; 190 } 191 } 192 })) 193 } 194 195 var selectLibrary = Zotero.Promise.coroutine(function* (win, libraryID) { 196 libraryID = libraryID || Zotero.Libraries.userLibraryID; 197 yield win.ZoteroPane.collectionsView.selectLibrary(libraryID); 198 yield waitForItemsLoad(win); 199 }); 200 201 var waitForItemsLoad = Zotero.Promise.coroutine(function* (win, collectionRowToSelect) { 202 var zp = win.ZoteroPane; 203 var cv = zp.collectionsView; 204 205 yield cv.waitForLoad(); 206 if (collectionRowToSelect !== undefined) { 207 yield cv.selectWait(collectionRowToSelect); 208 } 209 yield zp.itemsView.waitForLoad(); 210 }); 211 212 var waitForTagSelector = function (win) { 213 var zp = win.ZoteroPane; 214 var deferred = Zotero.Promise.defer(); 215 if (zp.tagSelectorShown()) { 216 var tagSelector = win.document.getElementById('zotero-tag-selector'); 217 var onRefresh = () => { 218 tagSelector.removeEventListener('refresh', onRefresh); 219 deferred.resolve(); 220 }; 221 tagSelector.addEventListener('refresh', onRefresh); 222 } 223 else { 224 deferred.resolve(); 225 } 226 return deferred.promise; 227 }; 228 229 /** 230 * Waits for a single item event. Returns a promise for the item ID(s). 231 */ 232 function waitForItemEvent(event) { 233 return waitForNotifierEvent(event, 'item').then(x => x.ids); 234 } 235 236 /** 237 * Wait for a single notifier event and return a promise for the data 238 */ 239 function waitForNotifierEvent(event, type) { 240 if (!event) throw new Error("event not provided"); 241 242 var deferred = Zotero.Promise.defer(); 243 var notifierID = Zotero.Notifier.registerObserver({notify:function(ev, type, ids, extraData) { 244 if(ev == event) { 245 Zotero.Notifier.unregisterObserver(notifierID); 246 deferred.resolve({ 247 ids: ids, 248 extraData: extraData 249 }); 250 } 251 }}, [type]); 252 return deferred.promise; 253 } 254 255 /** 256 * Looks for windows with a specific URL. 257 */ 258 function getWindows(uri) { 259 var enumerator = Services.wm.getEnumerator(null); 260 var wins = []; 261 while(enumerator.hasMoreElements()) { 262 var win = enumerator.getNext(); 263 if(win.location == uri) { 264 wins.push(win); 265 } 266 } 267 return wins; 268 } 269 270 /** 271 * Resolve a promise when a specified callback returns true. interval 272 * specifies the interval between checks. timeout specifies when we 273 * should assume failure. 274 */ 275 function waitForCallback(cb, interval, timeout) { 276 var deferred = Zotero.Promise.defer(); 277 if(interval === undefined) interval = 100; 278 if(timeout === undefined) timeout = 10000; 279 var start = Date.now(); 280 var id = setInterval(function() { 281 var success = cb(); 282 if(success) { 283 clearInterval(id); 284 deferred.resolve(success); 285 } else if(Date.now() - start > timeout*1000) { 286 clearInterval(id); 287 deferred.reject(new Error("Promise timed out")); 288 } 289 }, interval); 290 return deferred.promise; 291 } 292 293 294 function clickOnItemsRow(itemsView, row, button = 0) { 295 var x = {}; 296 var y = {}; 297 var width = {}; 298 var height = {}; 299 itemsView._treebox.getCoordsForCellItem( 300 row, 301 itemsView._treebox.columns.getNamedColumn('zotero-items-column-title'), 302 'text', 303 x, y, width, height 304 ); 305 306 // Select row to trigger multi-select 307 var tree = itemsView._treebox.treeBody; 308 var rect = tree.getBoundingClientRect(); 309 var x = rect.left + x.value; 310 var y = rect.top + y.value; 311 tree.dispatchEvent(new MouseEvent("mousedown", { 312 clientX: x, 313 clientY: y, 314 button, 315 detail: 1 316 })); 317 } 318 319 320 /** 321 * Synchronous inflate 322 */ 323 function gunzip(gzdata) { 324 return pako.inflate(gzdata, { to: 'string' }); 325 } 326 327 328 /** 329 * Get a default group used by all tests that want one, creating one if necessary 330 */ 331 var _defaultGroup; 332 var getGroup = Zotero.Promise.method(function () { 333 // Cleared in resetDB() 334 if (_defaultGroup) { 335 return _defaultGroup; 336 } 337 return _defaultGroup = createGroup({ 338 name: "My Group" 339 }); 340 }); 341 342 343 var createGroup = Zotero.Promise.coroutine(function* (props = {}) { 344 var group = new Zotero.Group; 345 group.id = props.id || Zotero.Utilities.rand(10000, 1000000); 346 group.name = props.name || "Test " + Zotero.Utilities.randomString(); 347 group.description = props.description || ""; 348 group.editable = props.editable === undefined ? true : props.editable; 349 group.filesEditable = props.filesEditable === undefined ? true : props.filesEditable; 350 group.version = props.version === undefined ? Zotero.Utilities.rand(1000, 10000) : props.version; 351 if (props.libraryVersion) { 352 group.libraryVersion = props.libraryVersion; 353 } 354 group.archived = props.archived === undefined ? false : props.archived; 355 yield group.saveTx(); 356 return group; 357 }); 358 359 var createFeed = Zotero.Promise.coroutine(function* (props = {}) { 360 var feed = new Zotero.Feed; 361 feed.name = props.name || "Test " + Zotero.Utilities.randomString(); 362 feed.description = props.description || ""; 363 feed.url = props.url || 'http://www.' + Zotero.Utilities.randomString() + '.com/feed.rss'; 364 feed.refreshInterval = props.refreshInterval || 12; 365 feed.cleanupReadAfter = props.cleanupReadAfter || 2; 366 feed.cleanupUnreadAfter = props.cleanupUnreadAfter || 30; 367 yield feed.saveTx(props.saveOptions); 368 return feed; 369 }); 370 371 var clearFeeds = Zotero.Promise.coroutine(function* () { 372 let feeds = Zotero.Feeds.getAll(); 373 for (let i=0; i<feeds.length; i++) { 374 yield feeds[i].eraseTx(); 375 } 376 }); 377 378 // 379 // Data objects 380 // 381 /** 382 * @param {String} objectType - 'collection', 'item', 'search' 383 * @param {Object} [params] 384 * @param {Integer} [params.libraryID] 385 * @param {String} [params.itemType] - Item type 386 * @param {String} [params.title] - Item title 387 * @param {Boolean} [params.setTitle] - Assign a random item title 388 * @param {String} [params.name] - Collection/search name 389 * @param {Integer} [params.parentID] 390 * @param {String} [params.parentKey] 391 * @param {Boolean} [params.synced] 392 * @param {Integer} [params.version] 393 * @param {Integer} [params.dateAdded] - Allowed for items 394 * @param {Integer} [params.dateModified] - Allowed for items 395 */ 396 function createUnsavedDataObject(objectType, params = {}) { 397 if (!objectType) { 398 throw new Error("Object type not provided"); 399 } 400 401 var allowedParams = ['libraryID', 'parentID', 'parentKey', 'synced', 'version']; 402 403 var itemType; 404 if (objectType == 'item' || objectType == 'feedItem') { 405 itemType = params.itemType || 'book'; 406 allowedParams.push('deleted', 'dateAdded', 'dateModified'); 407 } 408 if (objectType == 'item') { 409 allowedParams.push('inPublications'); 410 } 411 if (objectType == 'feedItem') { 412 params.guid = params.guid || Zotero.randomString(); 413 allowedParams.push('guid'); 414 } 415 416 var obj = new Zotero[Zotero.Utilities.capitalize(objectType)](itemType); 417 if (params.libraryID) { 418 obj.libraryID = params.libraryID; 419 } 420 421 switch (objectType) { 422 case 'item': 423 case 'feedItem': 424 if (params.parentItemID) { 425 params.parentID = params.parentItemID; 426 delete params.parentItemID; 427 } 428 if (params.title !== undefined || params.setTitle) { 429 obj.setField('title', params.title !== undefined ? params.title : Zotero.Utilities.randomString()); 430 } 431 if (params.collections !== undefined) { 432 obj.setCollections(params.collections); 433 } 434 if (params.tags !== undefined) { 435 obj.setTags(params.tags); 436 } 437 if (params.note !== undefined) { 438 obj.setNote(params.note); 439 } 440 break; 441 442 case 'collection': 443 case 'search': 444 obj.name = params.name !== undefined ? params.name : Zotero.Utilities.randomString(); 445 break; 446 } 447 448 if (objectType == 'search') { 449 obj.addCondition('title', 'contains', Zotero.Utilities.randomString()); 450 obj.addCondition('title', 'isNot', Zotero.Utilities.randomString()); 451 } 452 453 Zotero.Utilities.assignProps(obj, params, allowedParams); 454 455 return obj; 456 } 457 458 var createDataObject = Zotero.Promise.coroutine(function* (objectType, params = {}, saveOptions) { 459 var obj = createUnsavedDataObject(objectType, params); 460 yield obj.saveTx(saveOptions); 461 return obj; 462 }); 463 464 function getNameProperty(objectType) { 465 return objectType == 'item' ? 'title' : 'name'; 466 } 467 468 var modifyDataObject = function (obj, params = {}, saveOptions) { 469 switch (obj.objectType) { 470 case 'item': 471 obj.setField( 472 'title', 473 params.title !== undefined ? params.title : Zotero.Utilities.randomString() 474 ); 475 break; 476 477 default: 478 obj.name = params.name !== undefined ? params.name : Zotero.Utilities.randomString(); 479 } 480 return obj.saveTx(saveOptions); 481 }; 482 483 /** 484 * Return a promise for the error thrown by a promise, or false if none 485 */ 486 function getPromiseError(promise) { 487 return promise.thenReturn(false).catch(e => e); 488 } 489 490 /** 491 * Init paths for PDF tools and data 492 */ 493 function initPDFToolsPath() { 494 let pdfConvertedFileName = 'pdftotext'; 495 let pdfInfoFileName = 'pdfinfo'; 496 497 if (Zotero.isWin) { 498 pdfConvertedFileName += '-win.exe'; 499 pdfInfoFileName += '-win.exe'; 500 } 501 else if (Zotero.isMac) { 502 pdfConvertedFileName += '-mac'; 503 pdfInfoFileName += '-mac'; 504 } 505 else { 506 let cpu = Zotero.platform.split(' ')[1]; 507 pdfConvertedFileName += '-linux-' + cpu; 508 pdfInfoFileName += '-linux-' + cpu; 509 } 510 511 let pdfToolsPath = OS.Path.join(Zotero.Profile.dir, 'pdftools'); 512 let pdfConverterPath = OS.Path.join(pdfToolsPath, pdfConvertedFileName); 513 let pdfInfoPath = OS.Path.join(pdfToolsPath, pdfInfoFileName); 514 let pdfDataPath = OS.Path.join(pdfToolsPath, 'poppler-data'); 515 516 Zotero.FullText.setPDFConverterPath(pdfConverterPath); 517 Zotero.FullText.setPDFInfoPath(pdfInfoPath); 518 Zotero.FullText.setPDFDataPath(pdfDataPath); 519 } 520 521 /** 522 * Returns the nsIFile corresponding to the test data directory 523 * (i.e., test/tests/data) 524 */ 525 function getTestDataDirectory() { 526 var resource = Services.io.getProtocolHandler("resource"). 527 QueryInterface(Components.interfaces.nsIResProtocolHandler), 528 resURI = Services.io.newURI("resource://zotero-unit-tests/data", null, null); 529 return Services.io.newURI(resource.resolveURI(resURI), null, null). 530 QueryInterface(Components.interfaces.nsIFileURL).file; 531 } 532 533 function getTestDataUrl(path) { 534 path = path.split('/'); 535 if (path[0].length == 0) { 536 path.splice(0, 1); 537 } 538 return "resource://zotero-unit-tests/data/" + path.join('/'); 539 } 540 541 /** 542 * Returns an absolute path to an empty temporary directory 543 */ 544 var getTempDirectory = Zotero.Promise.coroutine(function* getTempDirectory() { 545 Components.utils.import("resource://gre/modules/osfile.jsm"); 546 let path, 547 attempts = 3, 548 zoteroTmpDirPath = Zotero.getTempDirectory().path; 549 while (attempts--) { 550 path = OS.Path.join(zoteroTmpDirPath, Zotero.Utilities.randomString()); 551 try { 552 yield OS.File.makeDir(path, { ignoreExisting: false }); 553 break; 554 } catch (e) { 555 if (!attempts) throw e; // Throw on last attempt 556 } 557 } 558 559 return path; 560 }); 561 562 var removeDir = Zotero.Promise.coroutine(function* (dir) { 563 // OS.File.DirectoryIterator, used by OS.File.removeDir(), isn't reliable on Travis, 564 // returning entry.isDir == false for subdirectories, so use nsIFile instead 565 //yield OS.File.removeDir(zipDir); 566 dir = Zotero.File.pathToFile(dir); 567 if (dir.exists()) { 568 dir.remove(true); 569 } 570 }); 571 572 /** 573 * Resets the Zotero DB and restarts Zotero. Returns a promise resolved 574 * when this finishes. 575 * 576 * @param {Object} [options] - Initialization options, as passed to Zotero.init(), overriding 577 * any that were set at startup 578 */ 579 async function resetDB(options = {}) { 580 // Hack to avoid CustomizableUI warnings in console from icon.js 581 var toolbarIconAdded = Zotero.toolbarIconAdded; 582 resetPrefs(); 583 584 if (options.thisArg) { 585 options.thisArg.timeout(60000); 586 } 587 var db = Zotero.DataDirectory.getDatabase(); 588 await Zotero.reinit( 589 Zotero.Promise.coroutine(function* () { 590 yield OS.File.remove(db); 591 _defaultGroup = null; 592 }), 593 false, 594 options 595 ); 596 Zotero.toolbarIconAdded = toolbarIconAdded; 597 await Zotero.Schema.schemaUpdatePromise; 598 initPDFToolsPath(); 599 } 600 601 /** 602 * Equivalent to JSON.stringify, except that object properties are stringified 603 * in a sorted order. 604 */ 605 function stableStringify(obj) { 606 return JSON.stringify(obj, function(k, v) { 607 if (v && typeof v == "object" && !Array.isArray(v)) { 608 let o = {}, 609 keys = Object.keys(v).sort(); 610 for (let i = 0; i < keys.length; i++) { 611 o[keys[i]] = v[keys[i]]; 612 } 613 return o; 614 } 615 return v; 616 }, "\t"); 617 } 618 619 /** 620 * Loads specified sample data from file 621 */ 622 function loadSampleData(dataName) { 623 let data = Zotero.File.getContentsFromURL('resource://zotero-unit-tests/data/' + dataName + '.js'); 624 return JSON.parse(data); 625 } 626 627 /** 628 * Generates sample item data that is stored in data/sampleItemData.js 629 */ 630 function generateAllTypesAndFieldsData() { 631 let data = {}; 632 let itemTypes = Zotero.ItemTypes.getTypes(); 633 // For most fields, use the field name as the value, but this doesn't 634 // work well for some fields that expect values in certain formats 635 let specialValues = { 636 date: '1999-12-31', 637 filingDate: '2000-01-02', 638 accessDate: '1997-06-13T23:59:58Z', 639 number: 3, 640 numPages: 4, 641 issue: 5, 642 volume: 6, 643 numberOfVolumes: 7, 644 edition: 8, 645 seriesNumber: 9, 646 ISBN: '978-1-234-56789-7', 647 ISSN: '1234-5679', 648 url: 'http://www.example.com', 649 pages: '1-10', 650 DOI: '10.1234/example.doi', 651 runningTime: '1:22:33', 652 language: 'en-US' 653 }; 654 655 // Item types that should not be included in sample data 656 let excludeItemTypes = ['note', 'attachment']; 657 658 for (let i = 0; i < itemTypes.length; i++) { 659 if (excludeItemTypes.indexOf(itemTypes[i].name) != -1) continue; 660 661 let itemFields = data[itemTypes[i].name] = { 662 itemType: itemTypes[i].name 663 }; 664 665 let fields = Zotero.ItemFields.getItemTypeFields(itemTypes[i].id); 666 for (let j = 0; j < fields.length; j++) { 667 let field = fields[j]; 668 field = Zotero.ItemFields.getBaseIDFromTypeAndField(itemTypes[i].id, field) || field; 669 670 let name = Zotero.ItemFields.getName(field), 671 value; 672 673 // Use field name as field value 674 if (specialValues[name]) { 675 value = specialValues[name]; 676 } else { 677 value = name.charAt(0).toUpperCase() + name.substr(1); 678 // Make it look nice (sentence case) 679 value = value.replace(/([a-z])([A-Z])/g, '$1 $2') 680 .replace(/ [A-Z](?![A-Z])/g, m => m.toLowerCase()); // not all-caps words 681 } 682 683 itemFields[name] = value; 684 } 685 686 let creatorTypes = Zotero.CreatorTypes.getTypesForItemType(itemTypes[i].id), 687 creators = itemFields.creators = []; 688 for (let j = 0; j < creatorTypes.length; j++) { 689 let typeName = creatorTypes[j].name; 690 creators.push({ 691 creatorType: typeName, 692 firstName: typeName + 'First', 693 lastName: typeName + 'Last' 694 }); 695 } 696 697 // Also add a single-field mode author, which is valid for all types 698 let primaryCreatorType = Zotero.CreatorTypes.getName( 699 Zotero.CreatorTypes.getPrimaryIDForType(itemTypes[i].id) 700 ); 701 creators.push({ 702 creatorType: primaryCreatorType, 703 lastName: 'Institutional Author', 704 fieldMode: 1 705 }); 706 } 707 708 return data; 709 } 710 711 /** 712 * Populates the database with sample items 713 * The field values should be in the form exactly as they would appear in Zotero 714 */ 715 function populateDBWithSampleData(data) { 716 return Zotero.DB.executeTransaction(function* () { 717 for (let itemName in data) { 718 let item = data[itemName]; 719 let zItem = new Zotero.Item; 720 zItem.fromJSON(item); 721 item.id = yield zItem.save(); 722 } 723 724 return data; 725 }); 726 } 727 728 var generateItemJSONData = Zotero.Promise.coroutine(function* generateItemJSONData(options, currentData) { 729 let items = yield populateDBWithSampleData(loadSampleData('allTypesAndFields')), 730 jsonData = {}; 731 732 for (let itemName in items) { 733 let zItem = yield Zotero.Items.getAsync(items[itemName].id); 734 jsonData[itemName] = zItem.toJSON(options); 735 736 // Don't replace some fields that _always_ change (e.g. item keys) 737 // as long as it follows expected format 738 // This makes it easier to generate more meaningful diffs 739 if (!currentData || !currentData[itemName]) continue; 740 741 for (let field in jsonData[itemName]) { 742 let oldVal = currentData[itemName][field]; 743 if (!oldVal) continue; 744 745 let val = jsonData[itemName][field]; 746 switch (field) { 747 case 'dateAdded': 748 case 'dateModified': 749 if (!isoDateTimeRe.test(oldVal) || !isoDateTimeRe.test(val)) continue; 750 break; 751 case 'key': 752 if (!zoteroObjectKeyRe.test(oldVal) || !zoteroObjectKeyRe.test(val)) continue; 753 break; 754 default: 755 continue; 756 } 757 758 jsonData[itemName][field] = oldVal; 759 } 760 } 761 762 return jsonData; 763 }); 764 765 var generateCiteProcJSExportData = Zotero.Promise.coroutine(function* generateCiteProcJSExportData(currentData) { 766 let items = yield populateDBWithSampleData(loadSampleData('allTypesAndFields')), 767 cslExportData = {}; 768 769 for (let itemName in items) { 770 let zItem = yield Zotero.Items.getAsync(items[itemName].id); 771 cslExportData[itemName] = Zotero.Cite.System.prototype.retrieveItem(zItem); 772 773 if (!currentData || !currentData[itemName]) continue; 774 775 // Don't replace id as long as it follows expected format 776 if (Number.isInteger(currentData[itemName].id) 777 && Number.isInteger(cslExportData[itemName].id) 778 ) { 779 cslExportData[itemName].id = currentData[itemName].id; 780 } 781 } 782 783 return cslExportData; 784 }); 785 786 var generateTranslatorExportData = Zotero.Promise.coroutine(function* generateTranslatorExportData(legacy, currentData) { 787 let items = yield populateDBWithSampleData(loadSampleData('allTypesAndFields')), 788 translatorExportData = {}; 789 790 let itemGetter = new Zotero.Translate.ItemGetter(); 791 itemGetter.legacy = !!legacy; 792 793 for (let itemName in items) { 794 let zItem = yield Zotero.Items.getAsync(items[itemName].id); 795 itemGetter._itemsLeft = [zItem]; 796 translatorExportData[itemName] = itemGetter.nextItem(); 797 798 // Don't replace some fields that _always_ change (e.g. item keys) 799 if (!currentData || !currentData[itemName]) continue; 800 801 // For simplicity, be more lenient than for item key 802 let uriRe = /^http:\/\/zotero\.org\/users\/local\/\w{8}\/items\/\w{8}$/; 803 let itemIDRe = /^\d+$/; 804 for (let field in translatorExportData[itemName]) { 805 let oldVal = currentData[itemName][field]; 806 if (!oldVal) continue; 807 808 let val = translatorExportData[itemName][field]; 809 switch (field) { 810 case 'uri': 811 if (!uriRe.test(oldVal) || !uriRe.test(val)) continue; 812 break; 813 case 'itemID': 814 if (!itemIDRe.test(oldVal) || !itemIDRe.test(val)) continue; 815 break; 816 case 'key': 817 if (!zoteroObjectKeyRe.test(oldVal) || !zoteroObjectKeyRe.test(val)) continue; 818 break; 819 case 'dateAdded': 820 case 'dateModified': 821 if (legacy) { 822 if (!sqlDateTimeRe.test(oldVal) || !sqlDateTimeRe.test(val)) continue; 823 } else { 824 if (!isoDateTimeRe.test(oldVal) || !isoDateTimeRe.test(val)) continue; 825 } 826 break; 827 default: 828 continue; 829 } 830 831 translatorExportData[itemName][field] = oldVal; 832 } 833 } 834 835 return translatorExportData; 836 }); 837 838 839 /** 840 * Build a dummy translator that can be passed to Zotero.Translate 841 */ 842 function buildDummyTranslator(translatorType, code, info={}) { 843 const TRANSLATOR_TYPES = {"import":1, "export":2, "web":4, "search":8}; 844 info = Object.assign({ 845 "translatorID":"dummy-translator", 846 "translatorType": Number.isInteger(translatorType) ? translatorType : TRANSLATOR_TYPES[translatorType], 847 "label":"Dummy Translator", 848 "creator":"Simon Kornblith", 849 "target":"", 850 "priority":100, 851 "browserSupport":"g", 852 "inRepository":false, 853 "lastUpdated":"0000-00-00 00:00:00", 854 }, info); 855 let translator = new Zotero.Translator(info); 856 translator.code = JSON.stringify(info) + "\n" + code; 857 return translator; 858 } 859 860 861 /** 862 * Imports an attachment from a test file. 863 * @param {string} filename - The filename to import (in data directory) 864 * @return {Promise<Zotero.Item>} 865 */ 866 function importFileAttachment(filename, options = {}) { 867 let file = getTestDataDirectory(); 868 filename.split('/').forEach((part) => file.append(part)); 869 let importOptions = { 870 file, 871 parentItemID: options.parentID 872 }; 873 Object.assign(importOptions, options); 874 return Zotero.Attachments.importFromFile(importOptions); 875 } 876 877 878 function importTextAttachment() { 879 return importFileAttachment('test.txt', { contentType: 'text/plain', charset: 'utf-8' }); 880 } 881 882 883 function importHTMLAttachment() { 884 return importFileAttachment('test.html', { contentType: 'text/html', charset: 'utf-8' }); 885 } 886 887 888 /** 889 * Sets the fake XHR server to response to a given response 890 * 891 * @param {Object} server - Sinon FakeXMLHttpRequest server 892 * @param {Object|String} response - Dot-separated path to predefined response in responses 893 * object (e.g., keyInfo.fullAccess) or a JSON object 894 * that defines the response 895 * @param {Object} responses - Predefined responses 896 */ 897 function setHTTPResponse(server, baseURL, response, responses) { 898 if (typeof response == 'string') { 899 let [topic, key] = response.split('.'); 900 if (!responses[topic]) { 901 throw new Error("Invalid topic"); 902 } 903 if (!responses[topic][key]) { 904 throw new Error("Invalid response key"); 905 } 906 response = responses[topic][key]; 907 } 908 909 var responseArray = [response.status !== undefined ? response.status : 200, {}, ""]; 910 if (response.json) { 911 responseArray[1]["Content-Type"] = "application/json"; 912 responseArray[2] = JSON.stringify(response.json); 913 } 914 else { 915 responseArray[1]["Content-Type"] = "text/plain"; 916 responseArray[2] = response.text || ""; 917 } 918 919 if (!response.headers) { 920 response.headers = {}; 921 } 922 response.headers["Fake-Server-Match"] = 1; 923 for (let i in response.headers) { 924 responseArray[1][i] = response.headers[i]; 925 } 926 927 server.respondWith(response.method, baseURL + response.url, responseArray); 928 }