move copy tests from file interface to quick copy

Tests in fileInterfaceTests were essentially just
testing quickCopy behavior, so move those tests to
quickCopyTest.js
This commit is contained in:
Bogdan Abaev 2026-05-26 11:24:18 -07:00
parent e53e412504
commit 96623354fe
2 changed files with 272 additions and 320 deletions

View file

@ -234,6 +234,15 @@ describe("Zotero_File_Interface", function () {
assert.include(str, '<i>A</i>');
assert.include(str, '<i>B</i>');
});
it("should write text and html flavors directly to the clipboard", function () {
win.Zotero_File_Interface.writeToClipboard({
text: 'plain text',
html: '<p>html text</p>'
});
assert.equal(getDataForFlavor('text/plain'), 'plain text');
assert.equal(getDataForFlavor('text/html'), '<p>html text</p>');
});
});
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 = '<div data-schema-version="9">'
+ '<p>Some text</p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationOne))
+ '">(<span class="citation-item">lastOne and lastTwo</span>)</span></p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationTwo))
+ '">(<span class="citation-item">lastTwo and lastThree</span>)</span></p>'
+ '<p>Done</p>'
+ '</div>';
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 = '<div data-citation-items="'
+ encodeURIComponent(JSON.stringify(storedCitationItems))
+ '" data-schema-version="9">'
+ '<p>Before</p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationOne))
+ '">(<span class="citation-item">lastOne and lastTwo</span>)</span></p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationDeleted))
+ '">(<span class="citation-item">lastDeleted</span>)</span></p>'
+ '<p>After</p>'
+ '</div>';
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();
});
});
});

View file

@ -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 = '<div data-schema-version="9">'
+ '<p>Some text</p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationOne))
+ '">(<span class="citation-item">lastOne and lastTwo</span>)</span></p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationTwo))
+ '">(<span class="citation-item">lastTwo and lastThree</span>)</span></p>'
+ '<p>Done</p>'
+ '</div>';
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 = '<div data-citation-items="'
+ encodeURIComponent(JSON.stringify(storedCitationItems))
+ '" data-schema-version="9">'
+ '<p>Before</p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationOne))
+ '">(<span class="citation-item">lastOne and lastTwo</span>)</span></p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationDeleted))
+ '">(<span class="citation-item">lastDeleted</span>)</span></p>'
+ '<p>After</p>'
+ '</div>';
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('<p>Test note content</p>');
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('<p>Test note content</p>');
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);
});
});
});
});