describe("Zotero.Items", function () { var win, collectionsView, zp; before(function* () { this.timeout(10000); win = yield loadZoteroPane(); collectionsView = win.ZoteroPane.collectionsView; zp = win.ZoteroPane; }) beforeEach(function () { return selectLibrary(win); }) after(function () { win.close(); }) describe("#addToPublications", function () { it("should add an item to My Publications", async function () { var item = await createDataObject('item'); await Zotero.Items.addToPublications([item]); assert.isTrue(item.inPublications); assert.equal( ((await Zotero.DB.valueQueryAsync( "SELECT COUNT(*) FROM publicationsItems WHERE itemID=?", item.id))), 1 ); }); describe("#license", function () { it("should set a license if specified", async function () { var item = createUnsavedDataObject('item'); item.setField('rights', 'Test'); await item.saveTx(); await Zotero.Items.addToPublications( [item], { license: 'reserved', licenseName: 'All Rights Reserved', keepRights: false } ); assert.equal(item.getField('rights'), 'All Rights Reserved'); }); it("should keep existing Rights field if .keepRights is true", async function () { var item1 = createUnsavedDataObject('item'); item1.setField('rights', 'Test'); await item1.saveTx(); var item2 = await createDataObject('item'); await Zotero.Items.addToPublications( [item1, item2], { license: 'reserved', licenseName: 'All Rights Reserved', keepRights: true } ); assert.equal(item1.getField('rights'), 'Test'); assert.equal(item2.getField('rights'), 'All Rights Reserved'); }); it("shouldn't set a license if not specified", async function () { var item = createUnsavedDataObject('item'); item.setField('rights', 'Test'); await item.saveTx(); await Zotero.Items.addToPublications([item]); assert.equal(item.getField('rights'), 'Test'); }); }); it("should add child notes if .childNotes is true", async function () { var item = await createDataObject('item'); var note = await createDataObject('item', { itemType: 'note', parentID: item.id }); var attachment = await Zotero.Attachments.linkFromURL({ url: "http://example.com", parentItemID: item.id, title: "Example" }); await Zotero.Items.addToPublications([item], { childNotes: true }); assert.isTrue(note.inPublications); assert.equal( ((await Zotero.DB.valueQueryAsync( "SELECT COUNT(*) FROM publicationsItems WHERE itemID=?", note.id))), 1 ); assert.isFalse(attachment.inPublications); }); it("should add child link attachments if .childLinks is true", async function () { var item = await createDataObject('item'); var attachment1 = await Zotero.Attachments.linkFromURL({ url: "http://example.com", parentItemID: item.id, title: "Example" }); var attachment2 = await importFileAttachment('test.png', { parentItemID: item.id }); var note = await createDataObject('item', { itemType: 'note', parentID: item.id }); await Zotero.Items.addToPublications([item], { childLinks: true }); assert.isTrue(attachment1.inPublications); assert.equal( ((await Zotero.DB.valueQueryAsync( "SELECT COUNT(*) FROM publicationsItems WHERE itemID=?", attachment1.id))), 1 ); assert.isFalse(attachment2.inPublications); assert.isFalse(note.inPublications); }); it("should add child file attachments if .childFileAttachments is true", async function () { var item = await createDataObject('item'); var attachment1 = await importFileAttachment('test.png', { parentItemID: item.id }); var attachment2 = await Zotero.Attachments.linkFromURL({ url: "http://example.com", parentItemID: item.id, title: "Example" }); var note = await createDataObject('item', { itemType: 'note', parentID: item.id }); await Zotero.Items.addToPublications([item], { childFileAttachments: true }); assert.isTrue(attachment1.inPublications); assert.equal( ((await Zotero.DB.valueQueryAsync( "SELECT COUNT(*) FROM publicationsItems WHERE itemID=?", attachment1.id))), 1 ); assert.isFalse(attachment2.inPublications); assert.isFalse(note.inPublications); }); }); describe("#removeFromPublications", function () { it("should remove an item from My Publications", async function () { var item = await createDataObject('item'); item.inPublications = true; await item.saveTx(); assert.equal( ((await Zotero.DB.valueQueryAsync( "SELECT COUNT(*) FROM publicationsItems WHERE itemID=?", item.id))), 1 ); await Zotero.Items.removeFromPublications([item]); assert.isFalse(item.inPublications); assert.equal( ((await Zotero.DB.valueQueryAsync( "SELECT COUNT(*) FROM publicationsItems WHERE itemID=?", item.id))), 0 ); }); }); describe("#copyChildItems()", function () { var group; before(async function () { group = await createGroup(); }); after(async function () { await group.eraseTx(); }); it("should copy annotations from a group library to a personal library", async function () { await Zotero.Users.setCurrentUserID(1); await Zotero.Users.setName(1, 'Name 1'); await Zotero.Users.setName(12345, 'Name 2'); var userLibraryID = Zotero.Libraries.userLibraryID; var item = await createDataObject('item', { libraryID: group.libraryID }); var file = getTestDataDirectory(); file.append('test.pdf'); var attachment = await Zotero.Attachments.importFromFile({ file, parentItemID: item.id }); // Annotation by this user var annotation1 = await createAnnotation('highlight', attachment); await annotation1.saveTx(); // Annotation by this user with createdByUserID set var annotation2 = await createAnnotation('highlight', attachment); annotation2.createdByUserID = 1; await annotation2.saveTx({ skipEditCheck: true }); // Annotation by another user var annotation3 = await createAnnotation('highlight', attachment); annotation3.createdByUserID = 12345; await annotation3.saveTx({ skipEditCheck: true }); var newItem = item.clone(userLibraryID); await newItem.saveTx(); var newAttachment = attachment.clone(userLibraryID); await newAttachment.saveTx(); await Zotero.DB.executeTransaction(async function () { await Zotero.Items.copyChildItems(attachment, newAttachment); }); // Check annotations var annotations = newAttachment.getAnnotations(); assert.lengthOf(annotations, 3); var newAnnotation1 = annotations.find(o => o.annotationText == annotation1.annotationText); var newAnnotation2 = annotations.find(o => o.annotationText == annotation2.annotationText); var newAnnotation3 = annotations.find(o => o.annotationText == annotation3.annotationText); // Current user's name shouldn't have been transferred assert.isNull(newAnnotation1.annotationAuthorName); assert.isNull(newAnnotation2.annotationAuthorName); // Other user's should've been transferred assert.equal(newAnnotation3.annotationAuthorName, 'Name 2'); }); }); describe("#trash()", function () { it("should send items to the trash", async function () { var items = []; items.push( ((await createDataObject('item', { synced: true }))), ((await createDataObject('item', { synced: true }))), ((await createDataObject('item', { synced: true }))) ); items.forEach(item => { // Sanity-checked as true in itemTest#deleted assert.isUndefined(item._changed.deleted); }); var ids = items.map(item => item.id); await Zotero.Items.trashTx(ids); items.forEach(item => { assert.isTrue(item.deleted); // Item should be saved (can't use hasChanged() because that includes .synced) assert.isUndefined(item._changed.deleted); assert.isFalse(item.synced); }); assert.equal(((await Zotero.DB.valueQueryAsync( `SELECT COUNT(*) FROM deletedItems WHERE itemID IN (${ids})` ))), 3); for (let item of items) { assert.equal(((await Zotero.DB.valueQueryAsync( `SELECT synced FROM items WHERE itemID=${item.id}` ))), 0); } }); it("should update parent item when trashing child item", async function () { var item = await createDataObject('item'); var note = await createDataObject('item', { itemType: 'note', parentID: item.id }); assert.lengthOf(item.getNotes(), 1); await Zotero.Items.trashTx([note.id]); assert.lengthOf(item.getNotes(), 0); }); }); describe("#emptyTrash()", function () { it("should delete items in the trash", async function () { var item1 = createUnsavedDataObject('item'); item1.setField('title', 'a'); item1.deleted = true; var id1 = await item1.saveTx(); var item2 = createUnsavedDataObject('item'); item2.setField('title', 'b'); item2.deleted = true; var id2 = await item2.saveTx(); var item3 = createUnsavedDataObject('item', { itemType: 'attachment', parentID: id2 }); item3.attachmentLinkMode = Zotero.Attachments.LINK_MODE_IMPORTED_URL; item3.deleted = true; var id3 = await item3.saveTx(); await collectionsView.selectTrash(Zotero.Libraries.userLibraryID); await Zotero.Items.emptyTrash(Zotero.Libraries.userLibraryID); assert.isFalse(await Zotero.Items.getAsync(id1)); assert.isFalse(await Zotero.Items.getAsync(id2)); assert.isFalse(await Zotero.Items.getAsync(id3)); // TEMP: This is failing on Travis due to a race condition //assert.equal(zp.itemsView.rowCount, 0) }) }) describe("#getFirstCreatorFromData()", function () { it("should handle single eligible creator", async function () { for (let creatorType of ['author', 'editor', 'contributor']) { assert.equal( Zotero.Items.getFirstCreatorFromData( Zotero.ItemTypes.getID('book'), [ { fieldMode: 0, firstName: 'A', lastName: 'B', creatorTypeID: Zotero.CreatorTypes.getID(creatorType) } ] ), 'B', creatorType ); } }); it("should ignore single ineligible creator", async function () { assert.strictEqual( Zotero.Items.getFirstCreatorFromData( Zotero.ItemTypes.getID('book'), [ { fieldMode: 0, firstName: 'A', lastName: 'B', creatorTypeID: Zotero.CreatorTypes.getID('translator') } ] ), '' ); }); it("should handle single eligible creator after ineligible creator", async function () { for (let creatorType of ['author', 'editor', 'contributor']) { assert.equal( Zotero.Items.getFirstCreatorFromData( Zotero.ItemTypes.getID('book'), [ { fieldMode: 0, firstName: 'A', lastName: 'B', creatorTypeID: Zotero.CreatorTypes.getID('translator') }, { fieldMode: 0, firstName: 'C', lastName: 'D', creatorTypeID: Zotero.CreatorTypes.getID(creatorType) } ] ), 'D', creatorType ); } }); it("should handle two eligible creators", async function () { for (let creatorType of ['author', 'editor', 'contributor']) { assert.equal( Zotero.Items.getFirstCreatorFromData( Zotero.ItemTypes.getID('book'), [ { fieldMode: 0, firstName: 'A', lastName: 'B', creatorTypeID: Zotero.CreatorTypes.getID(creatorType) }, { fieldMode: 0, firstName: 'C', lastName: 'D', creatorTypeID: Zotero.CreatorTypes.getID(creatorType) } ] ), Zotero.getString( 'general.andJoiner', ['\u2068' + 'B' + '\u2069', '\u2068' + 'D' + '\u2069'] ), creatorType ); } }); it("should handle three eligible creators", async function () { for (let creatorType of ['author', 'editor', 'contributor']) { assert.equal( Zotero.Items.getFirstCreatorFromData( Zotero.ItemTypes.getID('book'), [ { fieldMode: 0, firstName: 'A', lastName: 'B', creatorTypeID: Zotero.CreatorTypes.getID(creatorType) }, { fieldMode: 0, firstName: 'C', lastName: 'D', creatorTypeID: Zotero.CreatorTypes.getID(creatorType) }, { fieldMode: 0, firstName: 'E', lastName: 'F', creatorTypeID: Zotero.CreatorTypes.getID(creatorType) } ] ), 'B ' + Zotero.getString('general.etAl'), creatorType ); } }); it("should handle two eligible creators with intervening creators", async function () { for (let creatorType of ['author', 'editor', 'contributor']) { assert.equal( Zotero.Items.getFirstCreatorFromData( Zotero.ItemTypes.getID('book'), [ { fieldMode: 0, firstName: 'A', lastName: 'B', creatorTypeID: Zotero.CreatorTypes.getID('translator') }, { fieldMode: 0, firstName: 'C', lastName: 'D', creatorTypeID: Zotero.CreatorTypes.getID(creatorType) }, { fieldMode: 0, firstName: 'E', lastName: 'F', creatorTypeID: Zotero.CreatorTypes.getID('translator') }, { fieldMode: 0, firstName: 'G', lastName: 'H', creatorTypeID: Zotero.CreatorTypes.getID(creatorType) } ] ), Zotero.getString( 'general.andJoiner', ['\u2068' + 'D' + '\u2069', '\u2068' + 'H' + '\u2069'] ), creatorType ); } }); }); describe("#getAsync()", function () { it("should return Zotero.Item for item ID", async function () { let item = new Zotero.Item('journalArticle'); let id = await item.saveTx(); item = await Zotero.Items.getAsync(id); assert.notOk(item.isFeedItem); assert.instanceOf(item, Zotero.Item); assert.notInstanceOf(item, Zotero.FeedItem); }); it("should return Zotero.FeedItem for feed item ID", async function () { let feed = new Zotero.Feed({ name: 'foo', url: 'http://www.' + Zotero.randomString() + '.com' }); await feed.saveTx(); let feedItem = new Zotero.FeedItem('journalArticle', { guid: Zotero.randomString() }); feedItem.libraryID = feed.libraryID; let id = await feedItem.saveTx(); feedItem = await Zotero.Items.getAsync(id); assert.isTrue(feedItem.isFeedItem); assert.instanceOf(feedItem, Zotero.FeedItem); }); }); describe("#keepTopLevel()", function () { it("should remove child items of passed items", async function () { var item1 = await createDataObject('item'); var item2 = await createDataObject('item', { itemType: 'note', parentItemID: item1.id }); var item3 = await createDataObject('item', { itemType: 'note', parentItemID: item1.id }); var item4 = await createDataObject('item'); var item5 = await createDataObject('item', { itemType: 'note', parentItemID: item4.id }); var otherItem = await createDataObject('item'); var item6 = await createDataObject('item', { itemType: 'note', parentItemID: otherItem.id }); var items = Zotero.Items.keepTopLevel([item1, item2, item3, item4, item5, item6]); assert.sameMembers( // Convert to ids for clearer output items.map(item => item.id), [item1, item4, item6].map(item => item.id) ); }); it("shouldn't return parent item more than once when two child items are selected", async function () { var item1 = await createDataObject('item'); var item2 = await createDataObject('item', { itemType: 'note', parentItemID: item1.id }); var item3 = await createDataObject('item', { itemType: 'note', parentItemID: item1.id }); var items = Zotero.Items.keepTopLevel([item2, item3]); assert.sameMembers( items.map(item => item.id), [item2.id, item3.id] ) }); }); describe("#numDistinctFileAttachmentsForLabel()", function () { it("should return an approximate count of attachment files for the selected items", async function () { var item1 = await createDataObject('item'); var attachment1 = await importFileAttachment('test.png', { parentItemID: item1.id }); var attachment2 = await importFileAttachment('test.png', { parentItemID: item1.id }); function getNum() { return Zotero.Items.numDistinctFileAttachmentsForLabel(zp.getSelectedItems()); } zp.itemsView.selection.clearSelection(); assert.equal(getNum(), 0); // Uncached best-attachment state await zp.selectItems([item1.id]); assert.equal(getNum(), 1); await zp.selectItems([item1.id, attachment1.id]); // Make sure the best-attachment state is uncached item1._bestAttachmentState = null; // Should count parent item and best attachment as two item when uncached assert.equal(getNum(), 2); await zp.selectItems([item1.id, attachment1.id, attachment2.id]); // Max is 2 assert.equal(getNum(), 2); await item1.getBestAttachment(); // Cached best-attachment state await zp.selectItems([item1.id]); assert.equal(getNum(), 1); await zp.selectItems([item1.id, attachment1.id]); // Should count parent item and best attachment as one item when cached assert.equal(getNum(), 1); await zp.selectItems([item1.id, attachment1.id, attachment2.id]); assert.equal(getNum(), 2); }); it("should return 0 for a parent item with a non-PDF file attachment when passed `item.isPDFAttachment()` as a filter", async function () { var item = await createDataObject('item'); var attachment = await importFileAttachment('test.png', { parentItemID: item.id }); var numFiles = Zotero.Items.numDistinctFileAttachmentsForLabel( [item], item => item.isPDFAttachment() ); assert.equal(numFiles, 0); }); }); describe("#_loadChildItems()", function () { it("should mark child items as loaded for an attachment", async function () { var attachment = await importPDFAttachment(); var itemID = attachment.id; Zotero.Items.unload([itemID]); attachment = await Zotero.Items.getAsync(itemID); await attachment.loadDataType('childItems'); assert.isTrue(attachment._loaded.childItems); attachment.getAnnotations(); await attachment.eraseTx(); }); }); describe("#getSortTitle()", function () { it("should strip recognized markup tags", function () { let tests = [ ['A title in italics', 'A title in italics'], ['An unmatched tag', 'An unmatched tag'], ['A title with mismatched tags', 'A title with mismatched tags'], ['A title with a valid span', 'A title with a valid span'], ['Another title with a valid span', 'Another title with a valid span'], ['A random span tag', 'A random span tag'] ]; for (let [input, expected] of tests) { assert.equal(Zotero.Items.getSortTitle(input), expected); } }); it("should strip any punctuation at the beginning of the string besides @, #, and *", function () { let tests = [ ['_title', 'title'], ['-title', 'title'], ['-- longer title', 'longer title'], ['-_ longer title with different second character', '_ longer title with different second character'], ['"Quoted title', 'Quoted title'], ['@zotero on Twitter', '@zotero on Twitter'], ['#hashtag', '#hashtag'], ['*special', '*special'], ['**repeated', '**repeated'] ]; for (let [input, expected] of tests) { assert.equal(Zotero.Items.getSortTitle(input), expected); } }); it("should not strip dashes in the middle of the string", function () { let tests = [ ['123-456', '123-456'], ['Meyers-Briggs', 'Meyers-Briggs'], ['En–dash', 'En–dash'], ['Em—dash', 'Em—dash'] ]; for (let [input, expected] of tests) { assert.equal(Zotero.Items.getSortTitle(input), expected); } }); it("should strip opening punctuation after string-initial punctuation", function () { let tests = [ ['.[Test]', 'Test]'], ['"❮Word❯"', 'Word❯"'], ['"@": The Musical', '@": The Musical'], ]; for (let [input, expected] of tests) { assert.equal(Zotero.Items.getSortTitle(input), expected); } }); it("should sort titles with special characters correctly", function () { let tests = [ [ ['A*B*@!@C*D 1', 'ABCD 2', 'A*B*@!@C*D 3', 'ABCD 4'], ['A*B*@!@C*D 1', 'A*B*@!@C*D 3', 'ABCD 2', 'ABCD 4'] ], [ ['Why? Volume 1', 'Why! Volume 1', 'Why! Volume 2', 'Why? Volume 2'], ['Why! Volume 1', 'Why! Volume 2', 'Why? Volume 1', 'Why? Volume 2'] ], [ ['Sign and symbol', '"@" Sign. Its accidental history.', 'Sign language'], ['"@" Sign. Its accidental history.', 'Sign and symbol', 'Sign language'] ], ]; let st = Zotero.Items.getSortTitle; let collation = Zotero.getLocaleCollation(); for (let [input, expected] of tests) { input.sort((a, b) => collation.compareString(1, st(a), st(b))); assert.deepEqual(input, expected); } }); }); describe("#getLastRead()", function () { it("should return items read within two weeks of the most recently read", async function () { let group = await createGroup(); let libraryID = group.libraryID; let now = Math.round(Date.now() / 1000); let item1 = await createDataObject('item', { libraryID }); let att1 = await importPDFAttachment(item1); att1.attachmentLastRead = now; await att1.saveTx(); let item2 = await createDataObject('item', { libraryID }); let att2 = await importPDFAttachment(item2); att2.attachmentLastRead = now - (10 * 24 * 60 * 60); // 10 days before await att2.saveTx(); let item3 = await createDataObject('item', { libraryID }); let att3 = await importPDFAttachment(item3); att3.attachmentLastRead = now - (20 * 24 * 60 * 60); // 20 days before await att3.saveTx(); let ids = await Zotero.Items.getLastRead(libraryID); assert.include(ids, item1.id); assert.include(ids, item2.id); assert.notInclude(ids, item3.id); }); it("should return empty array when no items have been read", async function () { let group = await createGroup(); let ids = await Zotero.Items.getLastRead(group.libraryID); assert.lengthOf(ids, 0); }); it("should exclude deleted items", async function () { let group = await createGroup(); let libraryID = group.libraryID; let now = Math.round(Date.now() / 1000); let item = await createDataObject('item', { libraryID }); let att = await importPDFAttachment(item); att.attachmentLastRead = now; await att.saveTx(); item.deleted = true; await item.saveTx(); let ids = await Zotero.Items.getLastRead(libraryID); assert.notInclude(ids, item.id); }); it("should return items read months ago if within two weeks of each other", async function () { let group = await createGroup(); let libraryID = group.libraryID; let threeMonthsAgo = Math.round(Date.now() / 1000) - (90 * 24 * 60 * 60); let item1 = await createDataObject('item', { libraryID }); let att1 = await importPDFAttachment(item1); att1.attachmentLastRead = threeMonthsAgo; await att1.saveTx(); let item2 = await createDataObject('item', { libraryID }); let att2 = await importPDFAttachment(item2); att2.attachmentLastRead = threeMonthsAgo - (5 * 24 * 60 * 60); await att2.saveTx(); let ids = await Zotero.Items.getLastRead(libraryID); assert.include(ids, item1.id); assert.include(ids, item2.id); }); it("should return standalone attachments without parents", async function () { let group = await createGroup(); let libraryID = group.libraryID; let now = Math.round(Date.now() / 1000); let att = await importPDFAttachment(null, { libraryID }); att.attachmentLastRead = now; await att.saveTx(); let ids = await Zotero.Items.getLastRead(libraryID); assert.include(ids, att.id); }); it("should freeze cutoff so old items persist when a new item is read", async function () { let group = await createGroup(); let libraryID = group.libraryID; let threeMonthsAgo = Math.round(Date.now() / 1000) - (90 * 24 * 60 * 60); let now = Math.round(Date.now() / 1000); // Read an item 3 months ago let item1 = await createDataObject('item', { libraryID }); let att1 = await importPDFAttachment(item1); att1.attachmentLastRead = threeMonthsAgo; await att1.saveTx(); // First call freezes the cutoff based on this item let ids = await Zotero.Items.getLastRead(libraryID); assert.include(ids, item1.id); // Read something new today -- would shift the window if not frozen let item2 = await createDataObject('item', { libraryID }); let att2 = await importPDFAttachment(item2); att2.attachmentLastRead = now; await att2.saveTx(); // item1 should still appear because the cutoff is frozen ids = await Zotero.Items.getLastRead(libraryID); assert.include(ids, item1.id); assert.include(ids, item2.id); }); }); describe("#getLastReadAttachmentIDs()", function () { it("should return the read attachments themselves, not their parents", async function () { let group = await createGroup(); let libraryID = group.libraryID; let now = Math.round(Date.now() / 1000); let item = await createDataObject('item', { libraryID }); let readAttachment = await importPDFAttachment(item); readAttachment.attachmentLastRead = now; await readAttachment.saveTx(); let unreadAttachment = await importPDFAttachment(item); let ids = await Zotero.Items.getLastReadAttachmentIDs(libraryID); // The read attachment is included, but not its parent or its unread sibling assert.include(ids, readAttachment.id); assert.notInclude(ids, item.id); assert.notInclude(ids, unreadAttachment.id); }); it("should return standalone attachments", async function () { let group = await createGroup(); let libraryID = group.libraryID; let now = Math.round(Date.now() / 1000); let att = await importPDFAttachment(null, { libraryID }); att.attachmentLastRead = now; await att.saveTx(); let ids = await Zotero.Items.getLastReadAttachmentIDs(libraryID); assert.include(ids, att.id); }); }); });