diff --git a/test/tests/fileInterfaceTest.js b/test/tests/fileInterfaceTest.js index a571930791..e18c2a716e 100644 --- a/test/tests/fileInterfaceTest.js +++ b/test/tests/fileInterfaceTest.js @@ -234,6 +234,15 @@ describe("Zotero_File_Interface", function () { assert.include(str, 'A'); assert.include(str, 'B'); }); + + it("should write text and html flavors directly to the clipboard", function () { + win.Zotero_File_Interface.writeToClipboard({ + text: 'plain text', + html: '

html text

' + }); + assert.equal(getDataForFlavor('text/plain'), 'plain text'); + assert.equal(getDataForFlavor('text/html'), '

html text

'); + }); }); describe('Citavi annotations', () => { @@ -306,184 +315,4 @@ describe("Zotero_File_Interface", function () { }); }); - describe("Copy citations to clipboard", function () { - var noteItem, itemOne, itemTwo; - var styleID = 'http://www.zotero.org/styles/chicago-notes-bibliography'; - - before(async function () { - await Zotero.Styles.init(); - await Zotero.Translators.init(); - - itemOne = createUnsavedDataObject('item', { - itemType: 'book', - title: 'Item One', - creators: [ - { firstName: 'firstOne', lastName: 'lastOne', creatorType: 'author' }, - { firstName: 'firstTwo', lastName: 'lastTwo', creatorType: 'author' } - ] - }); - await itemOne.saveTx(); - - itemTwo = createUnsavedDataObject('item', { - itemType: 'book', - title: 'Item Two', - creators: [ - { firstName: 'firstTwo', lastName: 'lastTwo', creatorType: 'author' }, - { firstName: 'firstThree', lastName: 'lastThree', creatorType: 'author' } - ] - }); - await itemTwo.saveTx(); - - var uriOne = Zotero.URI.getItemURI(itemOne); - var uriTwo = Zotero.URI.getItemURI(itemTwo); - var citationOne = { - citationItems: [{ uris: [uriOne] }], - properties: {} - }; - var citationTwo = { - citationItems: [{ uris: [uriTwo] }], - properties: {} - }; - var noteHTML = '
' - + '

Some text

' - + '

(lastOne and lastTwo)

' - + '

(lastTwo and lastThree)

' - + '

Done

' - + '
'; - - noteItem = createUnsavedDataObject('item', { - itemType: 'note', - note: noteHTML - }); - await noteItem.saveTx(); - - // Preload note translators - let markdownTranslator = Zotero.Translators.get(Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN); - markdownTranslator.cacheCode = true; - await Zotero.Translators.getCodeForTranslator(markdownTranslator); - let htmlTranslator = Zotero.Translators.get(Zotero.Translators.TRANSLATOR_ID_NOTE_HTML); - htmlTranslator.cacheCode = true; - await Zotero.Translators.getCodeForTranslator(htmlTranslator); - }); - - afterEach(function () { - Zotero.Prefs.clear('export.quickCopy.bibliographySetting'); - Zotero.Prefs.clear('export.noteQuickCopy.setting'); - }); - - after(async function () { - await Zotero.Items.erase([noteItem.id, itemOne.id, itemTwo.id]); - }); - - function getClipboardText() { - let transferable = Components.classes['@mozilla.org/widget/transferable;1'] - .createInstance(Components.interfaces.nsITransferable); - let clipboardService = Components.classes['@mozilla.org/widget/clipboard;1'] - .getService(Components.interfaces.nsIClipboard); - transferable.addDataFlavor('text/plain'); - clipboardService.getData(transferable, Components.interfaces.nsIClipboard.kGlobalClipboard); - let str = {}; - transferable.getTransferData('text/plain', str); - return str.value.QueryInterface(Components.interfaces.nsISupportsString).data; - } - - it(`should copy CSL-formatted citations to clipboard`, async function () { - Zotero.Prefs.set('export.quickCopy.bibliographySetting', JSON.stringify({ - mode: 'bibliography', id: styleID, contentType: '', locale: '' - })); - Zotero.Prefs.set('export.noteQuickCopy.setting', JSON.stringify({ - mode: 'export', - id: Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT, - markdownOptions: { includeAppLinks: false }, - htmlOptions: { includeAppLinks: false } - })); - - await win.ZoteroPane.selectItem(noteItem.id); - win.ZoteroPane.copySelectedItemsToClipboard(false, 'bibliography'); - await Zotero.Promise.delay(); - - var text = getClipboardText(); - var expected = 'Some text\n\n' - + '(firstOne lastOne and firstTwo lastTwo, Item One (n.d.).)\n\n' - + '(firstTwo lastTwo and firstThree lastThree, Item Two (n.d.).)\n\n' - + 'Done'; - assert.equal(text, expected); - }); - - it("should use embedded item data for deleted items when copying to clipboard", async function () { - var deletedItem = createUnsavedDataObject('item', { - itemType: 'book', - title: 'Deleted Item', - creators: [ - { firstName: 'firstDeleted', lastName: 'lastDeleted', creatorType: 'author' } - ] - }); - await deletedItem.saveTx(); - - var uriOne = Zotero.URI.getItemURI(itemOne); - var uriDeleted = Zotero.URI.getItemURI(deletedItem); - var itemDataOne = Zotero.Utilities.Item.itemToCSLJSON(itemOne); - var itemDataDeleted = Zotero.Utilities.Item.itemToCSLJSON(deletedItem); - - var citationOne = { - citationItems: [{ uris: [uriOne] }], - properties: {} - }; - var citationDeleted = { - citationItems: [{ uris: [uriDeleted] }], - properties: {} - }; - - var storedCitationItems = [ - { uris: [uriOne], itemData: itemDataOne }, - { uris: [uriDeleted], itemData: itemDataDeleted } - ]; - var deletedNoteHTML = '
' - + '

Before

' - + '

(lastOne and lastTwo)

' - + '

(lastDeleted)

' - + '

After

' - + '
'; - - var deletedNoteItem = createUnsavedDataObject('item', { - itemType: 'note', - note: deletedNoteHTML - }); - await deletedNoteItem.saveTx(); - - await deletedItem.eraseTx(); - - Zotero.Prefs.set('export.quickCopy.bibliographySetting', JSON.stringify({ mode: 'bibliography', id: styleID, contentType: '', locale: '' })); - - let items = Zotero.QuickCopy.reformatNoteCitations([deletedNoteItem]); - let format = { - mode: 'export', - id: Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT, - markdownOptions: { includeAppLinks: false }, - htmlOptions: { includeAppLinks: false } - }; - let content = Zotero.QuickCopy.getContentFromItems(items, format); - win.Zotero_File_Interface.writeToClipboard(content); - await Zotero.Promise.delay(); - - var text = getClipboardText(); - var expected = 'Before\n\n' - + '(firstOne lastOne and firstTwo lastTwo, Item One (n.d.).)\n\n' - + '(firstDeleted lastDeleted, Deleted Item (n.d.).)\n\n' - + 'After'; - assert.equal(text, expected); - - await deletedNoteItem.eraseTx(); - }); - }); }); diff --git a/test/tests/quickCopyTest.js b/test/tests/quickCopyTest.js index 1435b66da5..b411f80e69 100644 --- a/test/tests/quickCopyTest.js +++ b/test/tests/quickCopyTest.js @@ -1,5 +1,6 @@ describe("Zotero.QuickCopy", function () { let domain = "test.org"; + var win, zp, doc; const DEFAULT_BIB = { mode: 'bibliography', @@ -16,7 +17,25 @@ describe("Zotero.QuickCopy", function () { const BIBTEX_TRANSLATOR_ID = '9cb70025-a888-4a29-a210-93ec52da40d4'; // BibTeX before(async function () { + win = await loadZoteroPane(); + zp = win.ZoteroPane; + doc = win.document; + await Zotero.Styles.init(); + await Zotero.Translators.init(); await Zotero.QuickCopy.loadSiteSettings(); + + let translatorMarkdown = Zotero.Translators.get(Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN); + translatorMarkdown.cacheCode = true; + await Zotero.Translators.getCodeForTranslator(translatorMarkdown); + + let translatorHTML = Zotero.Translators.get(Zotero.Translators.TRANSLATOR_ID_NOTE_HTML); + translatorHTML.cacheCode = true; + await Zotero.Translators.getCodeForTranslator(translatorHTML); + }); + + after(function () { + win.Zotero_Tabs.closeAll(); + win.close(); }); beforeEach(function () { @@ -288,8 +307,6 @@ describe("Zotero.QuickCopy", function () { describe("#getContentFromItems()", function () { it("should generate BibTeX", async function () { var item = await createDataObject('item'); - - await Zotero.Translators.init(); var translatorID = '9cb70025-a888-4a29-a210-93ec52da40d4'; // BibTeX var format = 'export=' + translatorID; @@ -305,7 +322,138 @@ describe("Zotero.QuickCopy", function () { assert.isTrue(content.text.trim().startsWith('@')); }); }); - + + describe("#reformatNoteCitations()", function () { + var noteItem, itemOne, itemTwo; + var styleID = 'http://www.zotero.org/styles/chicago-notes-bibliography'; + + before(async function () { + itemOne = createUnsavedDataObject('item', { + itemType: 'book', + title: 'Item One', + creators: [ + { firstName: 'firstOne', lastName: 'lastOne', creatorType: 'author' }, + { firstName: 'firstTwo', lastName: 'lastTwo', creatorType: 'author' } + ] + }); + await itemOne.saveTx(); + + itemTwo = createUnsavedDataObject('item', { + itemType: 'book', + title: 'Item Two', + creators: [ + { firstName: 'firstTwo', lastName: 'lastTwo', creatorType: 'author' }, + { firstName: 'firstThree', lastName: 'lastThree', creatorType: 'author' } + ] + }); + await itemTwo.saveTx(); + + var uriOne = Zotero.URI.getItemURI(itemOne); + var uriTwo = Zotero.URI.getItemURI(itemTwo); + var citationOne = { citationItems: [{ uris: [uriOne] }], properties: {} }; + var citationTwo = { citationItems: [{ uris: [uriTwo] }], properties: {} }; + var noteHTML = '
' + + '

Some text

' + + '

(lastOne and lastTwo)

' + + '

(lastTwo and lastThree)

' + + '

Done

' + + '
'; + + noteItem = createUnsavedDataObject('item', { itemType: 'note', note: noteHTML }); + await noteItem.saveTx(); + }); + + after(async function () { + await Zotero.Items.erase([noteItem.id, itemOne.id, itemTwo.id]); + }); + + // getContentFromItems() calls reformatNoteCitations() internally when + // items are notes, so we can read the reformatted output off content.text. + function getNoteFormat() { + return { + mode: 'export', + id: Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT, + markdownOptions: { includeAppLinks: false }, + htmlOptions: { includeAppLinks: false } + }; + } + + it("should reformat note citations in the current CSL style", function () { + Zotero.Prefs.set('export.quickCopy.bibliographySetting', JSON.stringify({ + mode: 'bibliography', id: styleID, contentType: '', locale: '' + })); + + let content = Zotero.QuickCopy.getContentFromItems([noteItem], getNoteFormat()); + var expected = 'Some text\n\n' + + '(firstOne lastOne and firstTwo lastTwo, Item One (n.d.).)\n\n' + + '(firstTwo lastTwo and firstThree lastThree, Item Two (n.d.).)\n\n' + + 'Done'; + assert.equal(content.text, expected); + }); + + it("should use embedded item data for deleted items", async function () { + var deletedItem = createUnsavedDataObject('item', { + itemType: 'book', + title: 'Deleted Item', + creators: [ + { firstName: 'firstDeleted', lastName: 'lastDeleted', creatorType: 'author' } + ] + }); + await deletedItem.saveTx(); + + var uriOne = Zotero.URI.getItemURI(itemOne); + var uriDeleted = Zotero.URI.getItemURI(deletedItem); + var itemDataOne = Zotero.Utilities.Item.itemToCSLJSON(itemOne); + var itemDataDeleted = Zotero.Utilities.Item.itemToCSLJSON(deletedItem); + + var citationOne = { citationItems: [{ uris: [uriOne] }], properties: {} }; + var citationDeleted = { citationItems: [{ uris: [uriDeleted] }], properties: {} }; + + var storedCitationItems = [ + { uris: [uriOne], itemData: itemDataOne }, + { uris: [uriDeleted], itemData: itemDataDeleted } + ]; + var deletedNoteHTML = '
' + + '

Before

' + + '

(lastOne and lastTwo)

' + + '

(lastDeleted)

' + + '

After

' + + '
'; + + var deletedNoteItem = createUnsavedDataObject('item', { + itemType: 'note', + note: deletedNoteHTML + }); + await deletedNoteItem.saveTx(); + // Delete the item so its URI no longer resolves -- forces the + // fallback path through embedded data-citation-items JSON. + await deletedItem.eraseTx(); + + Zotero.Prefs.set('export.quickCopy.bibliographySetting', JSON.stringify({ + mode: 'bibliography', id: styleID, contentType: '', locale: '' + })); + + let content = Zotero.QuickCopy.getContentFromItems([deletedNoteItem], getNoteFormat()); + var expected = 'Before\n\n' + + '(firstOne lastOne and firstTwo lastTwo, Item One (n.d.).)\n\n' + + '(firstDeleted lastDeleted, Deleted Item (n.d.).)\n\n' + + 'After'; + assert.equal(content.text, expected); + + await deletedNoteItem.eraseTx(); + }); + }); + it("should generate bibliography in default locale if Quick Copy locale not set", async function () { var item = createUnsavedDataObject('item', { itemType: 'webpage', title: 'Foo' }); item.setField('date', '2020-03-11'); @@ -313,7 +461,6 @@ describe("Zotero.QuickCopy", function () { // This shouldn't be used Zotero.Prefs.set('export.lastLocale', 'fr-FR'); - await Zotero.Styles.init(); var format = 'bibliography=http://www.zotero.org/styles/apa'; Zotero.Prefs.set("export.quickCopy.bibliographySetting", format); @@ -349,152 +496,128 @@ describe("Zotero.QuickCopy", function () { let content = Zotero.QuickCopy.getContentFromItems([item], format, { asCitations: true }); assert.equal(content.text, '《新型数据财产的行为主义保护:基于财产权理论的分析》。'); }); -}); - -describe("Smart copy", function () { - var win, zp, doc; - var regularItem, attachment, annotation, note; - var clipboardService; - const SMART_COPY_BIB = { - mode: 'bibliography', - id: 'http://www.zotero.org/styles/apa', - contentType: '', - locale: '' - }; - - before(async function () { - win = await loadZoteroPane(); - zp = win.ZoteroPane; - doc = win.document; - clipboardService = Components.classes["@mozilla.org/widget/clipboard;1"] - .getService(Components.interfaces.nsIClipboard); - - await Zotero.Styles.init(); - await Zotero.Translators.init(); - - // Preload Note Markdown / Note HTML translators - for (let id of [ - Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN, - Zotero.Translators.TRANSLATOR_ID_NOTE_HTML, - ]) { - let translator = Zotero.Translators.get(id); - translator.cacheCode = true; - await Zotero.Translators.getCodeForTranslator(translator); - } - - regularItem = createUnsavedDataObject('item', { - itemType: 'journalArticle', - title: 'My Smart Copy Paper' - }); - regularItem.setField('date', '2020'); - regularItem.setCreators([ - { firstName: 'Jane', lastName: 'Smartcopy', creatorType: 'author' } - ]); - await regularItem.saveTx(); - - attachment = await importFileAttachment('test.pdf', { parentItemID: regularItem.id }); - annotation = await createAnnotation('highlight', attachment); - - note = createUnsavedDataObject('item', { itemType: 'note' }); - note.setNote('

Test note content

'); - await note.saveTx(); - }); - - // The test runner has a root-level afterEach (test/content/runtests.js) - // that clears any user-set pref after every test. Re-pin the bibliography - // style here before each test. - beforeEach(function () { - Zotero.Prefs.set('export.quickCopy.bibliographySetting', JSON.stringify(SMART_COPY_BIB)); - }); - - after(async function () { - win.Zotero_Tabs.closeAll(); - win.close(); - }); - - function getClipboardText() { - let transferable = Components.classes["@mozilla.org/widget/transferable;1"] - .createInstance(Components.interfaces.nsITransferable); - transferable.init(null); - transferable.addDataFlavor('text/plain'); - - clipboardService.getData(transferable, Components.interfaces.nsIClipboard.kGlobalClipboard); - let str = {}; - transferable.getTransferData('text/plain', str, {}); - return str.value.QueryInterface(Components.interfaces.nsISupportsString).data; - } - - function clearClipboard() { - clipboardService.emptyClipboard(Components.interfaces.nsIClipboard.kGlobalClipboard); - } - - describe("Library tab", function () { - beforeEach(async function () { - await selectLibrary(win); - doc.getElementById('item-tree-main').focus(); - }); - - it("should copy citation for a regular item", async function () { - await zp.itemsView.selectItem(regularItem.id); - clearClipboard(); - doc.getElementById('key_smartCopy').doCommand(); - assert.equal(getClipboardText(), '(Smartcopy, 2020)'); - }); - - it("should copy annotation content when an annotation is selected", async function () { - await zp.itemsView.selectItem(annotation.id); - clearClipboard(); - doc.getElementById('key_smartCopy').doCommand(); - assert.include(getClipboardText(), annotation.annotationText); - }); - - it("should copy note content when a note is selected", async function () { - await zp.itemsView.selectItem(note.id); - clearClipboard(); - doc.getElementById('key_smartCopy').doCommand(); - assert.include(getClipboardText(), 'Test note content'); - }); - }); - - describe("Reader tab", function () { - var reader; - // Use a local sandbox: sinon's default sandbox tracks every stub - // forever, so a later `sinon.restore()` elsewhere would - // re-invoke our stub's restore against the already dead reader. - var sandbox; + describe("Smart copy", function () { + var regularItem, attachment, annotation, note; + var clipboardService; + const SMART_COPY_BIB = { + mode: 'bibliography', + id: 'http://www.zotero.org/styles/apa', + contentType: '', + locale: '' + }; before(async function () { - sandbox = sinon.createSandbox(); - reader = await Zotero.Reader.open(attachment.id); - await reader._initPromise; - await reader._internalReader._primaryView.initializedPromise; - while (!reader._iframeWindow) { - await Zotero.Promise.delay(50); - } - // Pretend that the reader is focused - sandbox.stub(reader._iframeWindow.document, 'hasFocus').returns(true); + clipboardService = Components.classes["@mozilla.org/widget/clipboard;1"] + .getService(Components.interfaces.nsIClipboard); + + regularItem = createUnsavedDataObject('item', { + itemType: 'journalArticle', + title: 'My Smart Copy Paper' + }); + regularItem.setField('date', '2020'); + regularItem.setCreators([ + { firstName: 'Jane', lastName: 'Smartcopy', creatorType: 'author' } + ]); + await regularItem.saveTx(); + + attachment = await importFileAttachment('test.pdf', { parentItemID: regularItem.id }); + annotation = await createAnnotation('highlight', attachment); + + note = createUnsavedDataObject('item', { itemType: 'note' }); + note.setNote('

Test note content

'); + await note.saveTx(); }); - beforeEach(async function () { - await win.Zotero_Tabs.select(reader.tabID); + // The parent `beforeEach` re-pins prefs to the file's defaults, so + // override the bibliography setting here for Smart copy assertions. + beforeEach(function () { + Zotero.Prefs.set('export.quickCopy.bibliographySetting', JSON.stringify(SMART_COPY_BIB)); }); - after(function () { - sandbox.restore(); + function getClipboardText() { + let transferable = Components.classes["@mozilla.org/widget/transferable;1"] + .createInstance(Components.interfaces.nsITransferable); + transferable.init(null); + transferable.addDataFlavor('text/plain'); + + clipboardService.getData(transferable, Components.interfaces.nsIClipboard.kGlobalClipboard); + let str = {}; + transferable.getTransferData('text/plain', str, {}); + return str.value.QueryInterface(Components.interfaces.nsISupportsString).data; + } + + function clearClipboard() { + clipboardService.emptyClipboard(Components.interfaces.nsIClipboard.kGlobalClipboard); + } + + describe("Library tab", function () { + beforeEach(async function () { + await selectLibrary(win); + doc.getElementById('item-tree-main').focus(); + }); + + it("should copy citation for a regular item", async function () { + await zp.itemsView.selectItem(regularItem.id); + clearClipboard(); + doc.getElementById('key_smartCopy').doCommand(); + assert.equal(getClipboardText(), '(Smartcopy, 2020)'); + }); + + it("should copy annotation content when an annotation is selected", async function () { + await zp.itemsView.selectItem(annotation.id); + clearClipboard(); + doc.getElementById('key_smartCopy').doCommand(); + assert.include(getClipboardText(), annotation.annotationText); + }); + + it("should copy note content when a note is selected", async function () { + await zp.itemsView.selectItem(note.id); + clearClipboard(); + doc.getElementById('key_smartCopy').doCommand(); + assert.include(getClipboardText(), 'Test note content'); + }); }); - it("should copy parent citation when nothing is selected in the reader", function () { - clearClipboard(); - doc.getElementById('key_smartCopy').doCommand(); - assert.equal(getClipboardText(), '(Smartcopy, 2020)'); - }); + describe("Reader tab", function () { + var reader; + // Use a local sandbox: sinon's default sandbox tracks every stub + // forever, so a later `sinon.restore()` elsewhere would + // re-invoke our stub's restore against the already dead reader. + var sandbox; - it("should copy selected annotation", function () { - sandbox.stub(reader, 'getSelectedAnnotationIDs').returns([annotation.key]); - clearClipboard(); - doc.getElementById('key_smartCopy').doCommand(); - assert.include(getClipboardText(), annotation.annotationText); + before(async function () { + sandbox = sinon.createSandbox(); + reader = await Zotero.Reader.open(attachment.id); + await reader._initPromise; + await reader._internalReader._primaryView.initializedPromise; + while (!reader._iframeWindow) { + await Zotero.Promise.delay(50); + } + // Pretend that the reader is focused + sandbox.stub(reader._iframeWindow.document, 'hasFocus').returns(true); + }); + + beforeEach(async function () { + await win.Zotero_Tabs.select(reader.tabID); + }); + + after(function () { + sandbox.restore(); + }); + + it("should copy parent citation when nothing is selected in the reader", function () { + clearClipboard(); + doc.getElementById('key_smartCopy').doCommand(); + assert.equal(getClipboardText(), '(Smartcopy, 2020)'); + }); + + it("should copy selected annotation", function () { + sandbox.stub(reader, 'getSelectedAnnotationIDs').returns([annotation.key]); + clearClipboard(); + doc.getElementById('key_smartCopy').doCommand(); + assert.include(getClipboardText(), annotation.annotationText); + }); }); }); });