From 02c43c3643c5059d2a42de8543bd3e488d2a1ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Thu, 6 Apr 2017 14:19:25 +0300 Subject: [PATCH] Add integrationTests.js Contains a dummy doc plugin, which is useful for: - Testing integration.js functionality - Serving as succint documentation for development of new integration plugins --- chrome/content/zotero/xpcom/integration.js | 81 +++---- test/tests/integrationTests.js | 241 +++++++++++++++++++++ test/tests/styleTest.js | 2 +- 3 files changed, 286 insertions(+), 38 deletions(-) create mode 100644 test/tests/integrationTests.js diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index bae225e10..f03266baa 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -40,6 +40,19 @@ const FORCE_CITATIONS_RESET_TEXT = 2; // this is used only for update checking const INTEGRATION_PLUGINS = ["zoteroMacWordIntegration@zotero.org", "zoteroOpenOfficeIntegration@zotero.org", "zoteroWinWordIntegration@zotero.org"]; + +// These must match the constants defined in zoteroIntegration.idl +const DIALOG_ICON_STOP = 0; +const DIALOG_ICON_WARNING = 1; +const DIALOG_ICON_CAUTION = 2; + +const DIALOG_BUTTONS_OK = 0; +const DIALOG_BUTTONS_OK_CANCEL = 1; +const DIALOG_BUTTONS_YES_NO = 2; +const DIALOG_BUTTONS_YES_NO_CANCEL = 3; + +const NOTE_FOOTNOTE = 1; +const NOTE_ENDNOTE = 2; Zotero.Integration = new function() { Components.utils.import("resource://gre/modules/Services.jsm"); @@ -210,6 +223,19 @@ Zotero.Integration = new function() { }; } + this.getApplication = function(agent, command, docId) { + // Try to load the appropriate Zotero component; otherwise display an error + try { + var componentClass = "@zotero.org/Zotero/integration/application?agent="+agent+";1"; + Zotero.debug("Integration: Instantiating "+componentClass+" for command "+command+(docId ? " with doc "+docId : "")); + return Components.classes[componentClass] + .getService(Components.interfaces.zoteroIntegrationApplication); + } catch(e) { + throw new Zotero.Exception.Alert("integration.error.notInstalled", + [], "integration.error.title"); + } + }, + /** * Executes an integration command, first checking to make sure that versions are compatible */ @@ -230,17 +256,8 @@ Zotero.Integration = new function() { inProgress = true; // Check integration component versions - _checkPluginVersions().then(function() { - // Try to load the appropriate Zotero component; otherwise display an error - try { - var componentClass = "@zotero.org/Zotero/integration/application?agent="+agent+";1"; - Zotero.debug("Integration: Instantiating "+componentClass+" for command "+command+(docId ? " with doc "+docId : "")); - var application = Components.classes[componentClass] - .getService(Components.interfaces.zoteroIntegrationApplication); - } catch(e) { - throw new Zotero.Exception.Alert("integration.error.notInstalled", - [], "integration.error.title"); - } + return _checkPluginVersions().then(function() { + var application = Zotero.Integration.getApplication(agent, command, docId); // Try to execute the command; otherwise display an error in alert service or word processor // (depending on what is possible) @@ -267,9 +284,7 @@ Zotero.Integration = new function() { if(document) { try { document.activate(); - document.displayAlert(displayError, - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK); + document.displayAlert(displayError, DIALOG_ICON_STOP, DIALOG_BUTTONS_OK); } catch(e) { showErrorInFirefox = true; } @@ -821,10 +836,7 @@ Zotero.Integration.CorruptFieldException.prototype = { field = this.fieldGetter._fields[this.fieldIndex]; field.select(); this.fieldGetter._doc.activate(); - var result = this.fieldGetter._doc.displayAlert(msg, - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_YES_NO_CANCEL); - + var result = this.fieldGetter._doc.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_YES_NO_CANCEL); if(result == 0) { return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("document update")); } else if(result == 1) { // No @@ -878,9 +890,7 @@ Zotero.Integration.CorruptBibliographyException.prototype = { var msg = Zotero.getString("integration.corruptBibliography")+'\n\n'+ Zotero.getString('integration.corruptBibliography.description'); - var result = this.fieldGetter._doc.displayAlert(msg, - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL); + var result = this.fieldGetter._doc.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_OK_CANCEL); if(result == 0) { return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("clearing corrupted bibliography")); } else { @@ -991,8 +1001,7 @@ Zotero.Integration.Document.prototype._getSession = Zotero.Promise.coroutine(fun } var warning = this._doc.displayAlert(Zotero.getString("integration.upgradeWarning"), - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_WARNING, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL); + DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL); if(!warning) { return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("document upgrade")); } @@ -1007,9 +1016,11 @@ Zotero.Integration.Document.prototype._getSession = Zotero.Promise.coroutine(fun [], "integration.error.title")); } - if(Zotero.Integration.sessions[data.sessionID]) { + if (Zotero.Integration.sessions[data.sessionID]) { + // If communication occured with this document since restart this._session = Zotero.Integration.sessions[data.sessionID]; } else { + // Document has zotero data, but has not communicated since Zotero restart this._session = this._createNewSession(data); try { yield this._session.setData(data); @@ -1027,7 +1038,6 @@ Zotero.Integration.Document.prototype._getSession = Zotero.Promise.coroutine(fun } } - this._doc.setDocumentData(this._session.data.serializeXML()); this._session.reload = true; } return Zotero.Promise.resolve(this._session); @@ -1156,8 +1166,7 @@ Zotero.Integration.Document.prototype.removeCodes = function() { return fieldGetter.get() }).then(function(fields) { var result = me._doc.displayAlert(Zotero.getString("integration.removeCodesWarning"), - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_WARNING, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL); + DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL); if(result) { for(var i=fields.length-1; i>=0; i--) { fields[i].removeCode(); @@ -1301,8 +1310,8 @@ Zotero.Integration.Fields.prototype.addField = function(note) { var field = this._doc.cursorInField(this._session.data.prefs['fieldType']); if(field) { if(!this._doc.displayAlert(Zotero.getString("integration.replace"), - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL)) { + DIALOG_ICON_STOP, + DIALOG_BUTTONS_OK_CANCEL)) { return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("inserting citation")); } } @@ -1607,8 +1616,7 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, field.select(); var result = this._doc.displayAlert( Zotero.getString("integration.citationChanged")+"\n\n"+Zotero.getString("integration.citationChanged.description"), - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_YES_NO); + DIALOG_ICON_CAUTION, DIALOG_BUTTONS_YES_NO); if(result) { citation.properties.dontUpdate = true; } @@ -1752,8 +1760,7 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f + "Current: " + field.getText() ); if(!this._doc.displayAlert(Zotero.getString("integration.citationChanged.edit"), - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_WARNING, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL)) { + DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL)) { throw new Zotero.Exception.UserCancelled("editing citation"); } } @@ -2351,9 +2358,9 @@ Zotero.Integration.Session.prototype.lookupItems = Zotero.Promise.coroutine(func this.updateIndices[index] = true; } } else { - if(citationItem.key) { + if(citationItem.key && citationItem.libraryID) { // DEBUG: why no library id? - zoteroItem = Zotero.Items.getByLibraryAndKey(0, citationItem.key); + zoteroItem = Zotero.Items.getByLibraryAndKey(citationItem.libraryID, citationItem.key); } else if(citationItem.itemID) { zoteroItem = Zotero.Items.get(citationItem.itemID); } else if(citationItem.id) { @@ -3085,9 +3092,9 @@ Zotero.Integration.DocumentData.prototype.unserialize = function(input) { "storeReferences":false}; if(prefParameters[2] == "note") { if(prefParameters[4] == "1" || prefParameters[4] == "True") { - this.prefs.noteType = Components.interfaces.zoteroIntegrationDocument.NOTE_ENDNOTE; + this.prefs.noteType = NOTE_ENDNOTE; } else { - this.prefs.noteType = Components.interfaces.zoteroIntegrationDocument.NOTE_FOOTNOTE; + this.prefs.noteType = NOTE_FOOTNOTE; } } else { this.prefs.noteType = 0; diff --git a/test/tests/integrationTests.js b/test/tests/integrationTests.js new file mode 100644 index 000000000..1c3c3ea43 --- /dev/null +++ b/test/tests/integrationTests.js @@ -0,0 +1,241 @@ +"use strict"; + +describe("Zotero.Integration", function () { + Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + // Fully functional document plugin dummy based on word-for-windows-integration + // Functions should be stubbed when testing as needed + var DocumentPluginDummy = {}; + + DocumentPluginDummy.Application = function() { + this.doc = new DocumentPluginDummy.Document(); + this.primaryFieldType = "Field"; + this.secondaryFieldType = "Bookmark"; + this.fields = []; + }; + DocumentPluginDummy.Application.prototype = { + getActiveDocument: function() {return this.doc}, + getDocument: function() {return this.doc}, + QueryInterface: function() {return this}, + }; + + DocumentPluginDummy.Document = function() {this.fields = []}; + DocumentPluginDummy.Document.prototype = { + // Needs to be stubbed for expected return values depending on prompt type + // - Yes: 2, No: 1, Cancel: 0 + // - Yes: 1, No: 0 + // - Ok: 1, Cancel: 0 + // - Ok: 0 + displayAlert: () => 0, + activate: () => 0, + canInsertField: () => true, + cursorInField: () => false, + getDocumentData: function() {return this.data}, + setDocumentData: function(data) {this.data = data}, + insertField: function() { var field = new DocumentPluginDummy.Field(this); this.fields.push(field); return field }, + getFields: function() {return new DocumentPluginDummy.FieldEnumerator(this)}, + getFieldsAsync: function(fieldType, observer) { + observer.observe(this.getFields(fieldType), 'fields-available', null) + }, + setBibliographyStyle: () => 0, + convert: () => 0, + cleanup: () => 0, + complete: () => 0, + QueryInterface: function() {return this}, + }; + + DocumentPluginDummy.FieldEnumerator = function(doc) {this.doc = doc; this.idx = 0}; + DocumentPluginDummy.FieldEnumerator.prototype = { + hasMoreElements: function() {return this.idx < this.doc.fields.length;}, + getNext: function() {return this.doc.fields[this.idx++]}, + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports, + Components.interfaces.nsISimpleEnumerator]) + }; + + DocumentPluginDummy.Field = function(doc) { + this.doc = doc; + this.code = this.text = ''; + this.noteIndex = DocumentPluginDummy.Field.noteIndex++; + this.wrappedJSObject = this; + }; + DocumentPluginDummy.Field.noteIndex = 0; + DocumentPluginDummy.Field.prototype = { + delete: function() {this.doc.fields.filter((field) => field != this)}, + removeCode: function() {this.code = ""}, + select: () => 0, + setText: function(text) {this.text = text}, + getText: function() {return this.text}, + setCode: function(code) {this.code = code}, + getCode: function() {return this.code}, + equals: function(field) {return this == field}, + getNoteIndex: function() {return this.noteIndex}, + QueryInterface: function() {return this}, + }; + + for (let cls of ['Application', 'Document', 'FieldEnumerator', 'Field']) { + for (let methodName in DocumentPluginDummy[cls].prototype) { + if (methodName !== 'QueryInterface') { + let method = DocumentPluginDummy[cls].prototype[methodName]; + DocumentPluginDummy[cls].prototype[methodName] = function() { + try { + Zotero.debug(`DocumentPluginDummy: ${cls}.${methodName} invoked with args ${JSON.stringify(arguments)}`, 2); + } catch (e) { + Zotero.debug(`DocumentPluginDummy: ${cls}.${methodName} invoked with args ${arguments}`, 2); + } + var result = method.apply(this, arguments); + try { + Zotero.debug(`Result: ${JSON.stringify(result)}`, 2); + } catch (e) { + Zotero.debug(`Result: ${result}`, 2); + } + return result; + } + } + } + } + + var testItems; + var applications = {}; + var addEditCitationSpy, displayDialogStub; + var styleID = "http://www.zotero.org/styles/cell"; + var stylePath = OS.Path.join(getTestDataDirectory().path, 'cell.csl'); + + var commandList = [ + 'addCitation', 'editCitation', 'addEditCitation', + 'addBibliography', 'editBibliography', 'addEditBibliography', + 'refresh', 'removeCodes', 'setDocPrefs' + ]; + + function execCommand(command, docID) { + if (! commandList.includes(command)) { + throw new Error(`${command} is not a valid document command`); + } + if (typeof docID === "undefined") { + throw new Error(`docID cannot be undefined`) + } + return Zotero.Integration.execCommand("dummy", command, docID); + } + + var dialogResults = { + addCitationDialog: {}, + quickFormat: {}, + integrationDocPrefs: {}, + selectItemsDialog: {}, + editBibliographyDialog: {} + }; + + function initDoc(docID, options={}) { + applications[docID] = new DocumentPluginDummy.Application(); + var data = new Zotero.Integration.DocumentData(); + data.prefs = { + noteType: 0, + fieldType: "Field", + storeReferences: true, + automaticJournalAbbreviations: true + }; + data.style = {styleID, locale: 'en-US', hasBibliography: true, bibliographyStyleHasBeenSet: true}; + data.sessionID = Zotero.Utilities.randomString(10); + Object.assign(data, options); + applications[docID].getActiveDocument().setDocumentData(data.serializeXML()); + } + + function setDefaultIntegrationDocPrefs() { + dialogResults.integrationDocPrefs = { + style: "http://www.zotero.org/styles/cell", + locale: 'en-US', + fieldType: 'Field', + storeReferences: true, + automaticJournalAbbreviations: false, + useEndnotes: 0 + }; + } + setDefaultIntegrationDocPrefs(); + + function setAddEditItems(items) { + if (items.length == undefined) items = [items]; + dialogResults.quickFormat = function(doc, dialogName) { + var citationItems = items.map((i) => {return {id: i.id} }); + var field = doc.insertField(); + field.setCode('TEMP'); + var integrationDoc = addEditCitationSpy.lastCall.thisValue; + var fieldGetter = new Zotero.Integration.Fields(integrationDoc._session, integrationDoc._doc, () => 0); + var io = new Zotero.Integration.CitationEditInterface( + { citationItems, properties: {} }, + field, + fieldGetter, + integrationDoc._session + ); + io._acceptDeferred.resolve(); + return io; + } + } + + before(function* () { + yield Zotero.Styles.init(); + yield Zotero.Styles.install({file: stylePath}, styleID, true); + + testItems = []; + for (let i = 0; i < 5; i++) { + testItems.push(yield createDataObject('item', {libraryID: Zotero.Libraries.userLibraryID})); + } + setAddEditItems(testItems[0]); + + sinon.stub(Zotero.Integration, 'getApplication', function(agent, command, docID) { + if (!applications[docID]) { + applications[docID] = new DocumentPluginDummy.Application(); + } + return applications[docID]; + }); + + displayDialogStub = sinon.stub(Zotero.Integration, 'displayDialog', function(doc, dialogName, prefs, io) { + var ioResult = dialogResults[dialogName.substring(dialogName.lastIndexOf('/')+1, dialogName.length-4)]; + if (typeof ioResult == 'function') { + ioResult = ioResult(doc, dialogName); + } + Object.assign(io, ioResult); + return Zotero.Promise.resolve(); + }); + + addEditCitationSpy = sinon.spy(Zotero.Integration.Document.prototype, 'addEditCitation'); + }); + + after(function() { + Zotero.Integration.getApplication.restore(); + displayDialogStub.restore(); + addEditCitationSpy.restore(); + }); + + describe('Document', function() { + describe('#addEditCitation', function() { + var setDocumentDataSpy; + var docID = this.fullTitle(); + + before(function() { + setDocumentDataSpy = sinon.spy(DocumentPluginDummy.Document.prototype, 'setDocumentData'); + }); + + afterEach(function() { + setDocumentDataSpy.reset(); + }); + + after(function() { + setDocumentDataSpy.restore(); + }); + + it('should call doc.setDocumentData on a fresh document', function* () { + yield execCommand('addEditCitation', docID); + assert.isTrue(setDocumentDataSpy.calledOnce); + }); + + it('should not call doc.setDocumentData on subsequent invocations', function* () { + yield execCommand('addEditCitation', docID); + assert.isFalse(setDocumentDataSpy.called); + }); + + it('should not call doc.setDocumentData when document communicates for first time since restart, but has data', function* () { + Zotero.Integration.sessions = {}; + yield execCommand('addEditCitation', docID); + assert.isFalse(setDocumentDataSpy.called); + }); + }) + }) +}); diff --git a/test/tests/styleTest.js b/test/tests/styleTest.js index 1e627a401..198750380 100644 --- a/test/tests/styleTest.js +++ b/test/tests/styleTest.js @@ -12,7 +12,7 @@ describe("Zotero.Styles", function() { }); describe("Zotero.Styles.install", function() { - afterEach(function* (){ + afterEach(`${styleID} style should be installed`, function* (){ assert.isOk(Zotero.Styles.get(styleID)); yield Zotero.Styles.get(styleID).remove(); });