integrationTest.js (29470B)
1 "use strict"; 2 3 describe("Zotero.Integration", function () { 4 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 5 const INTEGRATION_TYPE_ITEM = 1; 6 const INTEGRATION_TYPE_BIBLIOGRAPHY = 2; 7 const INTEGRATION_TYPE_TEMP = 3; 8 /** 9 * To be used as a reference for Zotero-Word Integration plugins 10 * 11 * NOTE: Functions must return promises instead of values! 12 * The functions defined for the dummy are promisified below 13 */ 14 var DocumentPluginDummy = {}; 15 16 /** 17 * The Application class corresponds to a word processing application. 18 */ 19 DocumentPluginDummy.Application = function() { 20 this.doc = new DocumentPluginDummy.Document(); 21 this.primaryFieldType = "Field"; 22 this.secondaryFieldType = "Bookmark"; 23 this.supportedNotes = ['footnotes', 'endnotes']; 24 this.fields = []; 25 }; 26 DocumentPluginDummy.Application.prototype = { 27 /** 28 * Gets the active document. 29 * @returns {DocumentPluginDummy.Document} 30 */ 31 getActiveDocument: function() {return this.doc}, 32 /** 33 * Gets the document by some app-specific identifier. 34 * @param {String|Number} docID 35 */ 36 getDocument: function(docID) {return this.doc}, 37 QueryInterface: function() {return this}, 38 }; 39 40 /** 41 * The Document class corresponds to a single word processing document. 42 */ 43 DocumentPluginDummy.Document = function() {this.fields = []}; 44 DocumentPluginDummy.Document.prototype = { 45 /** 46 * Displays a dialog in the word processing application 47 * @param {String} dialogText 48 * @param {Number} icon - one of the constants defined in integration.js for dialog icons 49 * @param {Number} buttons - one of the constants defined in integration.js for dialog buttons 50 * @returns {Number} 51 * - Yes: 2, No: 1, Cancel: 0 52 * - Yes: 1, No: 0 53 * - Ok: 1, Cancel: 0 54 * - Ok: 0 55 */ 56 displayAlert: (dialogText, icon, buttons) => 0, 57 /** 58 * Brings this document to the foreground (if necessary to return after displaying a dialog) 59 */ 60 activate: () => 0, 61 /** 62 * Determines whether a field can be inserted at the current position. 63 * @param {String} fieldType 64 * @returns {Boolean} 65 */ 66 canInsertField: (fieldType) => true, 67 /** 68 * Returns the field in which the cursor resides, or NULL if none. 69 * @param {String} fieldType 70 * @returns {Boolean} 71 */ 72 cursorInField: (fieldType) => false, 73 /** 74 * Get document data property from the current document 75 * @returns {String} 76 */ 77 getDocumentData: function() {return this.data}, 78 /** 79 * Set document data property 80 * @param {String} data 81 */ 82 setDocumentData: function(data) {this.data = data}, 83 /** 84 * Inserts a field at the given position and initializes the field object. 85 * @param {String} fieldType 86 * @param {Integer} noteType 87 * @returns {DocumentPluginDummy.Field} 88 */ 89 insertField: function(fieldType, noteType) { 90 if (typeof noteType != "number") { 91 throw new Error("noteType must be an integer"); 92 } 93 var field = new DocumentPluginDummy.Field(this); 94 this.fields.push(field); 95 return field; 96 }, 97 /** 98 * Gets all fields present in the document. 99 * @param {String} fieldType 100 * @returns {DocumentPluginDummy.Field[]} 101 */ 102 getFields: function(fieldType) {return Array.from(this.fields)}, 103 /** 104 * Sets the bibliography style, overwriting the current values for this document 105 */ 106 setBibliographyStyle: (firstLineIndent, bodyIndent, lineSpacing, entrySpacing, 107 tabStops, tabStopsCount) => 0, 108 /** 109 * Converts all fields in a document to a different fieldType or noteType 110 * @params {DocumentPluginDummy.Field[]} fields 111 */ 112 convert: (fields, toFieldType, toNoteType, count) => 0, 113 /** 114 * Cleans up the document state and resumes processor for editing 115 */ 116 cleanup: () => 0, 117 118 /** 119 * Informs the document processor that the operation is complete 120 */ 121 complete: () => 0, 122 }; 123 124 /** 125 * The Field class corresponds to a field containing an individual citation 126 * or bibliography 127 */ 128 DocumentPluginDummy.Field = function(doc) { 129 this.doc = doc; 130 this.code = ''; 131 // This is actually required and current integration code depends on text being non-empty upon insertion. 132 // insertBibliography will fail if there is no placeholder text. 133 this.text = '{Placeholder}'; 134 this.wrappedJSObject = this; 135 }; 136 DocumentPluginDummy.Field.noteIndex = 0; 137 DocumentPluginDummy.Field.prototype = { 138 /** 139 * Deletes this field and its contents. 140 */ 141 delete: function() {this.doc.fields.filter((field) => field !== this)}, 142 /** 143 * Removes this field, but maintains the field's contents. 144 */ 145 removeCode: function() {this.code = ""}, 146 /** 147 * Selects this field. 148 */ 149 select: () => 0, 150 /** 151 * Sets the text inside this field to a specified plain text string or pseudo-RTF formatted text 152 * string. 153 * @param {String} text 154 * @param {Boolean} isRich 155 */ 156 setText: function(text, isRich) {this.text = text}, 157 /** 158 * Gets the text inside this field, preferably with formatting, but potentially without 159 * @returns {String} 160 */ 161 getText: function() {return this.text}, 162 /** 163 * Sets field's code 164 * @param {String} code 165 */ 166 setCode: function(code) {this.code = code}, 167 /** 168 * Gets field's code. 169 * @returns {String} 170 */ 171 getCode: function() {return this.code}, 172 /** 173 * Returns true if this field and the passed field are actually references to the same field. 174 * @param {DocumentPluginDummy.Field} field 175 * @returns {Boolean} 176 */ 177 equals: function(field) {return this == field}, 178 /** 179 * This field's note index, if it is in a footnote or endnote; otherwise zero. 180 * @returns {Number} 181 */ 182 getNoteIndex: () => 0, 183 }; 184 185 // Processing functions for logging and promisification 186 for (let cls of ['Application', 'Document', 'Field']) { 187 for (let methodName in DocumentPluginDummy[cls].prototype) { 188 if (methodName !== 'QueryInterface') { 189 let method = DocumentPluginDummy[cls].prototype[methodName]; 190 DocumentPluginDummy[cls].prototype[methodName] = async function() { 191 try { 192 Zotero.debug(`DocumentPluginDummy: ${cls}.${methodName} invoked with args ${JSON.stringify(arguments)}`, 2); 193 } catch (e) { 194 Zotero.debug(`DocumentPluginDummy: ${cls}.${methodName} invoked with args ${arguments}`, 2); 195 } 196 var result = method.apply(this, arguments); 197 try { 198 Zotero.debug(`Result: ${JSON.stringify(result)}`, 2); 199 } catch (e) { 200 Zotero.debug(`Result: ${result}`, 2); 201 } 202 return result; 203 } 204 } 205 } 206 } 207 208 var testItems; 209 var applications = {}; 210 var addEditCitationSpy, displayDialogStub; 211 var styleID = "http://www.zotero.org/styles/cell"; 212 var stylePath = OS.Path.join(getTestDataDirectory().path, 'cell.csl'); 213 214 var commandList = [ 215 'addCitation', 'editCitation', 'addEditCitation', 216 'addBibliography', 'editBibliography', 'addEditBibliography', 217 'refresh', 'removeCodes', 'setDocPrefs' 218 ]; 219 220 function execCommand(command, docID) { 221 if (! commandList.includes(command)) { 222 throw new Error(`${command} is not a valid document command`); 223 } 224 if (typeof docID === "undefined") { 225 throw new Error(`docID cannot be undefined`) 226 } 227 Zotero.debug(`execCommand '${command}': ${docID}`, 2); 228 return Zotero.Integration.execCommand("dummy", command, docID); 229 } 230 231 var dialogResults = { 232 addCitationDialog: {}, 233 quickFormat: {}, 234 integrationDocPrefs: {}, 235 selectItemsDialog: {}, 236 editBibliographyDialog: {} 237 }; 238 239 async function initDoc(docID, options={}) { 240 applications[docID] = new DocumentPluginDummy.Application(); 241 var data = new Zotero.Integration.DocumentData(); 242 data.prefs = { 243 noteType: 0, 244 fieldType: "Field", 245 automaticJournalAbbreviations: true 246 }; 247 data.style = {styleID, locale: 'en-US', hasBibliography: true, bibliographyStyleHasBeenSet: true}; 248 data.sessionID = Zotero.Utilities.randomString(10); 249 Object.assign(data, options); 250 await (await applications[docID].getDocument(docID)).setDocumentData(data.serialize()); 251 } 252 253 function setDefaultIntegrationDocPrefs() { 254 dialogResults.integrationDocPrefs = { 255 style: "http://www.zotero.org/styles/cell", 256 locale: 'en-US', 257 fieldType: 'Field', 258 automaticJournalAbbreviations: false, 259 useEndnotes: 0 260 }; 261 } 262 setDefaultIntegrationDocPrefs(); 263 264 function setAddEditItems(items) { 265 if (items.length == undefined) items = [items]; 266 dialogResults.quickFormat = async function(dialogName, io) { 267 io.citation.citationItems = items.map(function(item) { 268 item = Zotero.Cite.getItem(item.id); 269 return {id: item.id, uris: item.cslURIs, itemData: item.cslItemData}; 270 }); 271 await io.previewFn(io.citation); 272 io._acceptDeferred.resolve(() => {}); 273 }; 274 } 275 276 before(function* () { 277 yield Zotero.Styles.init(); 278 yield Zotero.Styles.install({file: stylePath}, styleID, true); 279 280 testItems = []; 281 for (let i = 0; i < 5; i++) { 282 let testItem = yield createDataObject('item', {libraryID: Zotero.Libraries.userLibraryID}); 283 testItem.setField('title', `title${1}`); 284 testItem.setCreator(0, {creatorType: 'author', name: `Author No${i}`}); 285 testItems.push(testItem); 286 } 287 setAddEditItems(testItems[0]); 288 289 sinon.stub(Zotero.Integration, 'getApplication').callsFake(function(agent, command, docID) { 290 if (!applications[docID]) { 291 applications[docID] = new DocumentPluginDummy.Application(); 292 } 293 return applications[docID]; 294 }); 295 296 displayDialogStub = sinon.stub(Zotero.Integration, 'displayDialog'); 297 displayDialogStub.callsFake(async function(dialogName, prefs, io) { 298 Zotero.debug(`Display dialog: ${dialogName}`, 2); 299 var ioResult = dialogResults[dialogName.substring(dialogName.lastIndexOf('/')+1, dialogName.length-4)]; 300 if (typeof ioResult == 'function') { 301 await ioResult(dialogName, io); 302 } else { 303 Object.assign(io, ioResult); 304 } 305 }); 306 307 addEditCitationSpy = sinon.spy(Zotero.Integration.Interface.prototype, 'addEditCitation'); 308 309 sinon.stub(Zotero.Integration.Progress.prototype, 'show'); 310 }); 311 312 after(function() { 313 Zotero.Integration.Progress.prototype.show.restore(); 314 Zotero.Integration.getApplication.restore(); 315 displayDialogStub.restore(); 316 addEditCitationSpy.restore(); 317 }); 318 319 describe('Interface', function() { 320 describe('#execCommand', function() { 321 var setDocumentDataSpy; 322 var docID = this.fullTitle(); 323 324 before(function() { 325 setDocumentDataSpy = sinon.spy(DocumentPluginDummy.Document.prototype, 'setDocumentData'); 326 }); 327 328 afterEach(function() { 329 setDocumentDataSpy.reset(); 330 }); 331 332 after(function() { 333 setDocumentDataSpy.restore(); 334 }); 335 336 it('should call doc.setDocumentData once', function* () { 337 yield execCommand('addEditCitation', docID); 338 assert.isTrue(setDocumentDataSpy.calledOnce); 339 }); 340 341 describe('when style used in the document does not exist', function() { 342 var docID = this.fullTitle(); 343 var displayAlertStub; 344 var style; 345 before(function* () { 346 displayAlertStub = sinon.stub(DocumentPluginDummy.Document.prototype, 'displayAlert').resolves(0); 347 }); 348 349 beforeEach(async function () { 350 // 🦉birds? 351 style = {styleID: "http://www.example.com/csl/waterbirds", locale: 'en-US'}; 352 353 // Make sure style not in library 354 try { 355 Zotero.Styles.get(style.styleID).remove(); 356 } catch (e) {} 357 await initDoc(docID, {style}); 358 displayDialogStub.resetHistory(); 359 displayAlertStub.reset(); 360 }); 361 362 after(function* () { 363 displayAlertStub.restore(); 364 }); 365 366 describe('when the style is not from a trusted source', function() { 367 it('should download the style and if user clicks YES', function* () { 368 var styleInstallStub = sinon.stub(Zotero.Styles, "install").resolves(); 369 var style = Zotero.Styles.get(styleID); 370 var styleGetCalledOnce = false; 371 var styleGetStub = sinon.stub(Zotero.Styles, 'get').callsFake(function() { 372 if (!styleGetCalledOnce) { 373 styleGetCalledOnce = true; 374 return false; 375 } 376 return style; 377 }); 378 displayAlertStub.resolves(1); 379 yield execCommand('addEditCitation', docID); 380 assert.isTrue(displayAlertStub.calledOnce); 381 assert.isFalse(displayDialogStub.calledWith(applications[docID].doc, 'chrome://zotero/content/integration/integrationDocPrefs.xul')); 382 assert.isTrue(styleInstallStub.calledOnce); 383 assert.isOk(Zotero.Styles.get(style.styleID)); 384 styleInstallStub.restore(); 385 styleGetStub.restore(); 386 }); 387 388 it('should prompt with the document preferences dialog if user clicks NO', function* () { 389 displayAlertStub.resolves(0); 390 yield execCommand('addEditCitation', docID); 391 assert.isTrue(displayAlertStub.calledOnce); 392 // Prefs to select a new style and quickFormat 393 assert.isTrue(displayDialogStub.calledTwice); 394 assert.isNotOk(Zotero.Styles.get(style.styleID)); 395 }); 396 }); 397 398 it('should download the style without prompting if it is from zotero.org', function* (){ 399 yield initDoc(docID, {styleID: "http://www.zotero.org/styles/waterbirds", locale: 'en-US'}); 400 var styleInstallStub = sinon.stub(Zotero.Styles, "install").resolves(); 401 var style = Zotero.Styles.get(styleID); 402 var styleGetCalledOnce = false; 403 var styleGetStub = sinon.stub(Zotero.Styles, 'get').callsFake(function() { 404 if (!styleGetCalledOnce) { 405 styleGetCalledOnce = true; 406 return false; 407 } 408 return style; 409 }); 410 displayAlertStub.resolves(1); 411 yield execCommand('addEditCitation', docID); 412 assert.isFalse(displayAlertStub.called); 413 assert.isFalse(displayDialogStub.calledWith(applications[docID].doc, 'chrome://zotero/content/integration/integrationDocPrefs.xul')); 414 assert.isTrue(styleInstallStub.calledOnce); 415 assert.isOk(Zotero.Styles.get(style.styleID)); 416 styleInstallStub.restore(); 417 styleGetStub.restore(); 418 }); 419 }); 420 }); 421 422 describe('#addEditCitation', function() { 423 var insertMultipleCitations = Zotero.Promise.coroutine(function *() { 424 var docID = this.test.fullTitle(); 425 if (!(docID in applications)) yield initDoc(docID); 426 var doc = applications[docID].doc; 427 428 setAddEditItems(testItems[0]); 429 yield execCommand('addEditCitation', docID); 430 assert.equal(doc.fields.length, 1); 431 var citation = yield (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize(); 432 assert.equal(citation.citationItems.length, 1); 433 assert.equal(citation.citationItems[0].id, testItems[0].id); 434 435 setAddEditItems(testItems.slice(1, 3)); 436 yield execCommand('addEditCitation', docID); 437 assert.equal(doc.fields.length, 2); 438 citation = yield (new Zotero.Integration.CitationField(doc.fields[1], doc.fields[1].code)).unserialize(); 439 assert.equal(citation.citationItems.length, 2); 440 for (let i = 1; i < 3; i++) { 441 assert.equal(citation.citationItems[i-1].id, testItems[i].id); 442 } 443 }); 444 it('should insert citation if not in field', insertMultipleCitations); 445 446 it('should edit citation if in citation field', function* () { 447 yield insertMultipleCitations.call(this); 448 var docID = this.test.fullTitle(); 449 var doc = applications[docID].doc; 450 451 sinon.stub(doc, 'cursorInField').resolves(doc.fields[0]); 452 sinon.stub(doc, 'canInsertField').resolves(false); 453 454 setAddEditItems(testItems.slice(3, 5)); 455 yield execCommand('addEditCitation', docID); 456 assert.equal(doc.fields.length, 2); 457 var citation = yield (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize(); 458 assert.equal(citation.citationItems.length, 2); 459 assert.equal(citation.citationItems[0].id, testItems[3].id); 460 }); 461 462 it('should update bibliography if present', function* () { 463 yield insertMultipleCitations.call(this); 464 var docID = this.test.fullTitle(); 465 var doc = applications[docID].doc; 466 467 let getCiteprocBibliographySpy = 468 sinon.spy(Zotero.Integration.Bibliography.prototype, 'getCiteprocBibliography'); 469 470 yield execCommand('addEditBibliography', docID); 471 assert.isTrue(getCiteprocBibliographySpy.calledOnce); 472 473 assert.equal(getCiteprocBibliographySpy.lastCall.returnValue[0].entry_ids.length, 3); 474 getCiteprocBibliographySpy.reset(); 475 476 setAddEditItems(testItems[3]); 477 yield execCommand('addEditCitation', docID); 478 assert.equal(getCiteprocBibliographySpy.lastCall.returnValue[0].entry_ids.length, 4); 479 480 getCiteprocBibliographySpy.restore(); 481 }); 482 483 describe('when original citation text has been modified', function() { 484 var displayAlertStub; 485 before(function* () { 486 displayAlertStub = sinon.stub(DocumentPluginDummy.Document.prototype, 'displayAlert').resolves(0); 487 }); 488 beforeEach(function() { 489 displayAlertStub.reset(); 490 }); 491 after(function() { 492 displayAlertStub.restore(); 493 }); 494 it('should keep modification if "Cancel" selected in editCitation triggered alert', async function () { 495 await insertMultipleCitations.call(this); 496 var docID = this.test.fullTitle(); 497 var doc = applications[docID].doc; 498 499 doc.fields[0].text = "modified"; 500 sinon.stub(doc, 'cursorInField').resolves(doc.fields[0]); 501 sinon.stub(doc, 'canInsertField').resolves(false); 502 503 await execCommand('addEditCitation', docID); 504 assert.equal(doc.fields.length, 2); 505 assert.equal(doc.fields[0].text, "modified"); 506 }); 507 it('should display citation dialog if "OK" selected in editCitation triggered alert', async function () { 508 await insertMultipleCitations.call(this); 509 var docID = this.test.fullTitle(); 510 var doc = applications[docID].doc; 511 512 let origText = doc.fields[0].text; 513 doc.fields[0].text = "modified"; 514 // Return OK 515 displayAlertStub.resolves(1); 516 sinon.stub(doc, 'cursorInField').resolves(doc.fields[0]); 517 sinon.stub(doc, 'canInsertField').resolves(false); 518 setAddEditItems(testItems[0]); 519 520 await execCommand('addEditCitation', docID); 521 assert.isTrue(displayAlertStub.called); 522 assert.equal(doc.fields.length, 2); 523 assert.equal(doc.fields[0].text, origText); 524 }); 525 it('should set dontUpdate: true if "yes" selected in refresh prompt', async function() { 526 await insertMultipleCitations.call(this); 527 var docID = this.test.fullTitle(); 528 var doc = applications[docID].doc; 529 530 var citation = await (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize(); 531 assert.isNotOk(citation.properties.dontUpdate); 532 doc.fields[0].text = "modified"; 533 // Return Yes 534 displayAlertStub.resolves(1); 535 536 await execCommand('refresh', docID); 537 assert.isTrue(displayAlertStub.called); 538 assert.equal(doc.fields.length, 2); 539 assert.equal(doc.fields[0].text, "modified"); 540 var citation = await (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize(); 541 assert.isOk(citation.properties.dontUpdate); 542 }); 543 it('should reset citation text if "no" selected in refresh prompt', async function() { 544 await insertMultipleCitations.call(this); 545 var docID = this.test.fullTitle(); 546 var doc = applications[docID].doc; 547 548 var citation = await (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize(); 549 assert.isNotOk(citation.properties.dontUpdate); 550 let origText = doc.fields[0].text; 551 doc.fields[0].text = "modified"; 552 // Return No 553 displayAlertStub.resolves(0); 554 555 await execCommand('refresh', docID); 556 assert.isTrue(displayAlertStub.called); 557 assert.equal(doc.fields.length, 2); 558 assert.equal(doc.fields[0].text, origText); 559 var citation = await (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize(); 560 assert.isNotOk(citation.properties.dontUpdate); 561 }); 562 }); 563 564 describe('when there are copy-pasted citations', function() { 565 it('should resolve duplicate citationIDs and mark both as new citations', async function() { 566 var docID = this.test.fullTitle(); 567 if (!(docID in applications)) initDoc(docID); 568 var doc = applications[docID].doc; 569 570 setAddEditItems(testItems[0]); 571 await execCommand('addEditCitation', docID); 572 assert.equal(doc.fields.length, 1); 573 // Add a duplicate 574 doc.fields.push(new DocumentPluginDummy.Field(doc)); 575 doc.fields[1].code = doc.fields[0].code; 576 doc.fields[1].text = doc.fields[0].text; 577 578 var originalUpdateDocument = Zotero.Integration.Fields.prototype.updateDocument; 579 var stubUpdateDocument = sinon.stub(Zotero.Integration.Fields.prototype, 'updateDocument'); 580 try { 581 var indicesLength; 582 stubUpdateDocument.callsFake(function() { 583 indicesLength = Object.keys(Zotero.Integration.currentSession.newIndices).length; 584 return originalUpdateDocument.apply(this, arguments); 585 }); 586 587 setAddEditItems(testItems[1]); 588 await execCommand('addEditCitation', docID); 589 assert.equal(indicesLength, 3); 590 } finally { 591 stubUpdateDocument.restore(); 592 } 593 }); 594 595 it('should successfully process citations copied in from another doc', async function() { 596 var docID = this.test.fullTitle(); 597 if (!(docID in applications)) initDoc(docID); 598 var doc = applications[docID].doc; 599 600 setAddEditItems(testItems[0]); 601 await execCommand('addEditCitation', docID); 602 assert.equal(doc.fields.length, 1); 603 doc.fields.push(new DocumentPluginDummy.Field(doc)); 604 // Add a "citation copied from somewhere else" 605 // the content doesn't really matter, just make sure that the citationID is different 606 var newCitationID = Zotero.Utilities.randomString(); 607 doc.fields[1].code = doc.fields[0].code; 608 doc.fields[1].code = doc.fields[1].code.replace(/"citationID":"[A-Za-z0-9^"]*"/, 609 `"citationID":"${newCitationID}"`); 610 doc.fields[1].text = doc.fields[0].text; 611 612 var originalUpdateDocument = Zotero.Integration.Fields.prototype.updateDocument; 613 var stubUpdateDocument = sinon.stub(Zotero.Integration.Fields.prototype, 'updateDocument'); 614 try { 615 var indices; 616 stubUpdateDocument.callsFake(function() { 617 indices = Object.keys(Zotero.Integration.currentSession.newIndices); 618 return originalUpdateDocument.apply(this, arguments); 619 }); 620 621 setAddEditItems(testItems[1]); 622 await execCommand('addEditCitation', docID); 623 assert.equal(indices.length, 2); 624 assert.include(indices, '1'); 625 assert.include(indices, '2'); 626 } finally { 627 stubUpdateDocument.restore(); 628 } 629 }); 630 }); 631 632 describe('when delayCitationUpdates is set', function() { 633 it('should insert a citation with wave underlining', function* (){ 634 yield insertMultipleCitations.call(this); 635 var docID = this.test.fullTitle(); 636 var doc = applications[docID].doc; 637 var data = new Zotero.Integration.DocumentData(doc.data); 638 data.prefs.delayCitationUpdates = true; 639 doc.data = data.serialize(); 640 641 var setTextSpy = sinon.spy(DocumentPluginDummy.Field.prototype, 'setText'); 642 setAddEditItems(testItems[3]); 643 yield execCommand('addEditCitation', docID); 644 assert.isTrue(setTextSpy.lastCall.args[0].includes('\\uldash')); 645 646 setTextSpy.restore(); 647 }); 648 649 it('should not write to any other fields besides the one being updated', function* () { 650 yield insertMultipleCitations.call(this); 651 var docID = this.test.fullTitle(); 652 var doc = applications[docID].doc; 653 var data = new Zotero.Integration.DocumentData(doc.data); 654 data.prefs.delayCitationUpdates = true; 655 doc.data = data.serialize(); 656 657 var setTextSpy = sinon.spy(DocumentPluginDummy.Field.prototype, 'setText'); 658 var setCodeSpy = sinon.spy(DocumentPluginDummy.Field.prototype, 'setCode'); 659 660 setAddEditItems(testItems[3]); 661 yield execCommand('addEditCitation', docID); 662 var field = setTextSpy.firstCall.thisValue; 663 664 for (let i = 0; i < setTextSpy.callCount; i++) { 665 assert.isTrue(yield field.equals(setTextSpy.getCall(i).thisValue)); 666 } 667 668 for (let i = 0; i < setCodeSpy.callCount; i++) { 669 assert.isTrue(yield field.equals(setCodeSpy.getCall(i).thisValue)); 670 } 671 672 setTextSpy.restore(); 673 setCodeSpy.restore(); 674 }) 675 }); 676 }); 677 678 describe('#addEditBibliography', function() { 679 var docID = this.fullTitle(); 680 beforeEach(function* () { 681 yield initDoc(docID); 682 yield execCommand('addEditCitation', docID); 683 }); 684 685 it('should insert bibliography if no bibliography field present', function* () { 686 displayDialogStub.resetHistory(); 687 yield execCommand('addEditBibliography', docID); 688 assert.isFalse(displayDialogStub.called); 689 var biblPresent = false; 690 for (let i = applications[docID].doc.fields.length-1; i >= 0; i--) { 691 let field = yield Zotero.Integration.Field.loadExisting(applications[docID].doc.fields[i]); 692 if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { 693 biblPresent = true; 694 break; 695 } 696 } 697 assert.isTrue(biblPresent); 698 }); 699 700 it('should display the edit bibliography dialog if bibliography present', function* () { 701 yield execCommand('addEditBibliography', docID); 702 displayDialogStub.resetHistory(); 703 yield execCommand('addEditBibliography', docID); 704 assert.isTrue(displayDialogStub.calledOnce); 705 assert.isTrue(displayDialogStub.lastCall.args[0].includes('editBibliographyDialog')); 706 }); 707 }); 708 }); 709 710 describe("DocumentData", function() { 711 it('should properly unserialize old XML document data', function() { 712 var serializedXMLData = "<data data-version=\"3\" zotero-version=\"5.0.SOURCE\"><session id=\"F0NFmZ32\"/><style id=\"http://www.zotero.org/styles/cell\" hasBibliography=\"1\" bibliographyStyleHasBeenSet=\"1\"/><prefs><pref name=\"fieldType\" value=\"ReferenceMark\"/><pref name=\"automaticJournalAbbreviations\" value=\"true\"/><pref name=\"noteType\" value=\"0\"/></prefs></data>"; 713 var data = new Zotero.Integration.DocumentData(serializedXMLData); 714 var expectedData = { 715 style: { 716 styleID: 'http://www.zotero.org/styles/cell', 717 locale: null, 718 hasBibliography: true, 719 bibliographyStyleHasBeenSet: true 720 }, 721 prefs: { 722 fieldType: 'ReferenceMark', 723 automaticJournalAbbreviations: true, 724 noteType: 0 725 }, 726 sessionID: 'F0NFmZ32', 727 zoteroVersion: '5.0.SOURCE', 728 dataVersion: '3' 729 }; 730 // Convert to JSON to remove functions from DocumentData object 731 assert.equal(JSON.stringify(data), JSON.stringify(expectedData)); 732 }); 733 734 it('should properly unserialize JSON document data', function() { 735 var expectedData = JSON.stringify({ 736 style: { 737 styleID: 'http://www.zotero.org/styles/cell', 738 locale: 'en-US', 739 hasBibliography: true, 740 bibliographyStyleHasBeenSet: true 741 }, 742 prefs: { 743 fieldType: 'ReferenceMark', 744 automaticJournalAbbreviations: false, 745 noteType: 0 746 }, 747 sessionID: 'owl-sesh', 748 zoteroVersion: '5.0.SOURCE', 749 dataVersion: 4 750 }); 751 var data = new Zotero.Integration.DocumentData(expectedData); 752 // Convert to JSON to remove functions from DocumentData object 753 assert.equal(JSON.stringify(data), expectedData); 754 }); 755 756 it('should properly serialize document data to XML (data ver 3)', function() { 757 sinon.spy(Zotero, 'debug'); 758 var data = new Zotero.Integration.DocumentData(); 759 data.sessionID = "owl-sesh"; 760 data.zoteroVersion = Zotero.version; 761 data.dataVersion = 3; 762 data.style = { 763 styleID: 'http://www.zotero.org/styles/cell', 764 locale: 'en-US', 765 hasBibliography: false, 766 bibliographyStyleHasBeenSet: true 767 }; 768 data.prefs = { 769 noteType: 1, 770 fieldType: "Field", 771 automaticJournalAbbreviations: true 772 }; 773 774 var serializedData = data.serialize(); 775 // Make sure we serialized to XML here 776 assert.equal(serializedData[0], '<'); 777 // Serialize and unserialize (above test makes sure unserialize works properly). 778 var processedData = new Zotero.Integration.DocumentData(serializedData); 779 780 // This isn't ideal, but currently how it works. Better serialization which properly retains types 781 // coming with official 5.0 release. 782 data.dataVersion = "3"; 783 784 // Convert to JSON to remove functions from DocumentData objects 785 assert.equal(JSON.stringify(processedData), JSON.stringify(data)); 786 787 // Make sure we are not triggering debug traces in Utilities.htmlSpecialChars() 788 assert.isFalse(Zotero.debug.calledWith(sinon.match.string, 1)); 789 Zotero.debug.restore(); 790 }); 791 792 it('should properly serialize document data to JSON (data ver 4)', function() { 793 var data = new Zotero.Integration.DocumentData(); 794 // data version 4 triggers serialization to JSON 795 // (e.g. when we've retrieved data from the doc and it was ver 4 already) 796 data.dataVersion = 4; 797 data.sessionID = "owl-sesh"; 798 data.style = { 799 styleID: 'http://www.zotero.org/styles/cell', 800 locale: 'en-US', 801 hasBibliography: false, 802 bibliographyStyleHasBeenSet: true 803 }; 804 data.prefs = { 805 noteType: 1, 806 fieldType: "Field", 807 automaticJournalAbbreviations: true 808 }; 809 810 // Serialize and unserialize (above tests makes sure unserialize works properly). 811 var processedData = new Zotero.Integration.DocumentData(data.serialize()); 812 813 // Added in serialization routine 814 data.zoteroVersion = Zotero.version; 815 816 // Convert to JSON to remove functions from DocumentData objects 817 assert.deepEqual(JSON.parse(JSON.stringify(processedData)), JSON.parse(JSON.stringify(data))); 818 }); 819 }) 820 });