mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
Some checks are pending
CI / Build, Upload, Test (push) Waiting to run
In other words, don't show the children that make the parent "recently read" as context rows. Children that don't match the Recently Read condition (such as notes, as well as other child attachments that weren't read since the cutoff) are still shown as context rows. And add tests for the new behavior, and clean up an unused local var.
802 lines
26 KiB
JavaScript
802 lines
26 KiB
JavaScript
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 <i>in italics</i>', 'A title in italics'],
|
||
['An unmatched </b> tag', 'An unmatched tag'],
|
||
['A <sup>title</sub> with mismatched tags', 'A title with mismatched tags'],
|
||
['A title with a valid <span style="font-variant:small-caps;">span</span>', 'A title with a valid span'],
|
||
['Another title with a valid <span class="nocase">span</span>', 'Another title with a valid span'],
|
||
['A random <span>span tag</span>', 'A random <span>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);
|
||
});
|
||
});
|
||
});
|