zotero/test/tests/attachmentReadObserverTest.js
Abe Jellinek 34039991f8
Track attachment last-read time, add Recently Read virtual collection (#2854)
Track when attachments are last opened or read, storing a `lastRead` Unix timestamp on the attachment. For user library items, `lastRead` syncs as an attachment property in item JSON. For group library items, it syncs via a per-user synced setting (like `lastPageIndex`).

- Add `lastRead` column to `itemAttachments`
- Add `AttachmentReadObserver` to update `lastRead` on file open and page change (throttled to 5 min for page changes)
- Add "Recently Read" virtual collection (items read in last 14 days, sorted by `lastRead` descending)
- Add `lastRead` search condition with date operators
- Add `lastRead` item tree column with new `dependsOnChildren` property for parent item aggregation
- Add `getItemLastRead()` to return max `lastRead` across child attachments

Also:

- Generalize collection tree SCSS to support universal (context-fill) icons alongside themed icons

---------

Co-authored-by: Dan Stillman <dstillman@zotero.org>
2026-03-17 13:52:00 -04:00

73 lines
3 KiB
JavaScript

"use strict";
describe("Zotero.AttachmentReadObserver", function () {
describe("file events", function () {
beforeEach(function () {
Zotero.Libraries.userLibrary.lastReadItemInSession = null;
});
it("should update an attachment's attachmentLastRead every time it is opened", async function () {
let attachment = await importPDFAttachment(null);
// We open the attachment at midnight on January 1
let stub = sinon.stub(Zotero.AttachmentReadObserver, '_getCurrentDate')
.callsFake(() => new Date(2023, 1, 1, 0, 0, 0));
await Zotero.Notifier.trigger('open', 'file', [attachment.id]);
let initialLastRead = attachment.attachmentLastRead;
assert.isNumber(initialLastRead);
// We open it again, only five seconds later
stub.callsFake(() => new Date(2023, 1, 1, 0, 0, 5));
await Zotero.Notifier.trigger('open', 'file', [attachment.id]);
assert.isAbove(attachment.attachmentLastRead, initialLastRead);
stub.restore();
});
it("should update an attachment's attachmentLastRead every five minutes when the page changes", async function () {
let attachment = await importPDFAttachment(null);
// We open the attachment at midnight on January 1
let stub = sinon.stub(Zotero.AttachmentReadObserver, '_getCurrentDate')
.callsFake(() => new Date(2023, 1, 1, 0, 0, 0));
await Zotero.Notifier.trigger('open', 'file', [attachment.id]);
let initialLastRead = attachment.attachmentLastRead;
assert.isNumber(initialLastRead);
// We change pages, only five seconds later
stub.callsFake(() => new Date(2023, 1, 1, 0, 0, 5));
await Zotero.Notifier.trigger('pageChange', 'file', [attachment.id]);
assert.equal(attachment.attachmentLastRead, initialLastRead);
// We change pages again, five minutes later
stub.callsFake(() => new Date(2023, 1, 1, 0, 5, 5));
await Zotero.Notifier.trigger('pageChange', 'file', [attachment.id]);
let updatedLastRead = attachment.attachmentLastRead;
assert.isAbove(updatedLastRead, initialLastRead);
// We change pages again, a minute after that
stub.callsFake(() => new Date(2023, 1, 1, 0, 6, 5));
await Zotero.Notifier.trigger('pageChange', 'file', [attachment.id]);
assert.equal(attachment.attachmentLastRead, updatedLastRead);
stub.restore();
});
});
describe("setting events", function () {
it("should update a group attachment's attachmentLastRead when the associated synced setting changes", async function () {
let group = await createGroup();
let item = await createDataObject('item', { libraryID: group.libraryID });
let attachment = await importPDFAttachment(item);
let key = attachment._getLastReadSettingKey();
let firstValue = 1674668000;
await Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, key, firstValue);
assert.equal(attachment.attachmentLastRead, firstValue);
let secondValue = 1674668123;
await Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, key, secondValue);
assert.equal(attachment.attachmentLastRead, secondValue);
});
});
});