Fix attachments & annotation box refresh bugs and add tests (#4031)

Fixes #3993
Fixes #3995
Closes #4082
This commit is contained in:
windingwind 2024-04-22 09:15:22 +08:00 committed by Dan Stillman
parent 07e8f0a01d
commit 91c0c28b5d
10 changed files with 806 additions and 49 deletions

View file

@ -46,53 +46,95 @@
}
set item(item) {
super.item = item;
super.item = (item instanceof Zotero.Item && item.isFileAttachment()) ? item : null;
this._updateHidden();
}
init() {
this.initCollapsibleSection();
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'attachmentAnnotationsBox');
this._body = this.querySelector('.body');
this._annotationItems = [];
}
destroy() {}
destroy() {
Zotero.Notifier.unregisterObserver(this._notifierID);
}
notify(action, type, ids) {
if (action == 'modify' && this.item && ids.includes(this.item.id)) {
this._forceRenderAll();
notify(event, _type, ids, _extraData) {
if (!this.item) return;
this._annotationItems = this.item.getAnnotations();
let annotations = this._annotationItems.filter(
annotation => ids.includes(annotation.id));
if (["add", "modify"].includes(event)) {
for (let annotation of annotations) {
let row = this.querySelector(`annotation-row[annotation-id="${annotation.id}"]`);
row?.remove();
this.addRow(annotation);
}
}
else if (event == 'delete') {
for (let id of ids) {
let row = this.querySelector(`annotation-row[annotation-id="${id}"]`);
row?.remove();
}
}
this.updateCount();
}
render() {
this._annotationItems = this.item.getAnnotations();
this.updateCount();
}
async asyncRender() {
if (!this.initialized || !this.item?.isFileAttachment()) return;
if (this._isAlreadyRendered()) return;
let annotations = this.item.getAnnotations();
this._section.setCount(annotations.length);
await Zotero.PDFWorker.renderAttachmentAnnotations(this.item.id);
this._body.replaceChildren();
if (!this._section.open) {
return;
}
let count = annotations.length;
if (count === 0) {
this.hidden = true;
if (!this._section.open || this._annotationItems.length === 0) {
return;
}
this.hidden = false;
for (let annotation of annotations) {
let row = document.createXULElement('annotation-row');
row.annotation = annotation;
this._body.append(row);
for (let annotation of this._annotationItems) {
this.addRow(annotation);
}
}
addRow(annotation) {
let row = document.createXULElement('annotation-row');
row.annotation = annotation;
let index = this._annotationItems.findIndex(item => item.id == annotation.id);
if (index < 0 || index >= this._body.children.length) {
this._body.append(row);
}
else {
this._body.insertBefore(row, this._body.children[index]);
}
return row;
}
updateCount() {
let count = this._annotationItems.length;
this._section.setCount(count);
if (count === 0) {
this.hidden = true;
}
return count;
}
_updateHidden() {
this.hidden = !this.item?.isFileAttachment() || this.tabType == "reader";
this.hidden = !this.item || this.tabType == "reader";
}
}
customElements.define("attachment-annotations-box", AttachmentAnnotationsBox);

View file

@ -93,7 +93,7 @@
this._section = null;
this._preview = null;
this._isRendering = false;
this._asyncRendering = false;
this._isEditingFilename = false;
}
@ -304,12 +304,12 @@
async asyncRender() {
if (!this.item) return;
if (this._isRendering) return;
if (this._asyncRendering) return;
if (!this._section.open) return;
if (this._isAlreadyRendered("async")) return;
Zotero.debug('Refreshing attachment box');
this._isRendering = true;
this._asyncRendering = true;
// Cancel editing filename when refreshing
this._isEditingFilename = false;
@ -457,7 +457,7 @@
else {
selectButton.hidden = true;
}
this._isRendering = false;
this._asyncRendering = false;
}
onViewClick(event) {

View file

@ -165,6 +165,7 @@
this.addEventListener("focusin", this._handleFocusIn);
this.addEventListener("keypress", this._handleKeypress);
this.setAttribute("data-preview-type", "unknown");
this._notifierID = Zotero.Notifier.registerObserver(this, ["item"], "attachmentPreview");
}
destroy() {
@ -177,6 +178,38 @@
this.removeEventListener("click", this._handleFocusIn);
this.removeEventListener("focusin", this._handleFocusIn);
this.removeEventListener("keypress", this._handleKeypress);
Zotero.Notifier.unregisterObserver(this._notifierID);
}
notify(event, type, ids, extraData) {
if (!this.item) return;
if (this.isReaderType && this._reader) {
// Following chrome/content/zotero/xpcom/reader.js
if (event === "delete") {
let disappearedIDs = this._reader.annotationItemIDs.filter(x => ids.includes(x));
if (disappearedIDs.length) {
let keys = disappearedIDs.map(id => extraData[id].key);
this._reader.unsetAnnotations(keys);
}
}
else if (["add", "modify"].includes(event)) {
let annotationItems = this.item.getAnnotations();
this._reader.annotationItemIDs = annotationItems.map(x => x.id);
let affectedAnnotations = annotationItems.filter(({ id }) => (
ids.includes(id)
&& !(extraData && extraData[id] && extraData[id].instanceID === this._reader._instanceID)
));
if (affectedAnnotations.length) {
this._reader.setAnnotations(affectedAnnotations);
}
}
return;
}
if (this.isMediaType) {
if (["refresh", "modify"].includes(event) && ids.includes(this.item.id)) {
this.discard().then(() => this.render());
}
}
}
async render() {
@ -184,6 +217,14 @@
if (!this.initialized && itemID === this._renderingItemID) {
return;
}
// For tests
let resolve;
if (Zotero.test) {
this._renderPromise = new Promise(r => resolve = r);
// Expose `resolve` for `this.discard`
this._renderPromise.resolve = resolve;
}
this._renderingItemID = itemID;
let success = false;
if (this.isValidType && await this._item.fileExists()) {
@ -203,6 +244,9 @@
if (this._renderingItemID === itemID) {
this._renderingItemID = null;
}
if (Zotero.test) {
resolve();
}
}
async discard(force = false) {
@ -213,7 +257,7 @@
if (this._isDiscarding) {
return;
}
if (!force && this.isVisible) {
if (!force && (this.isVisible || !this._reader)) {
return;
}
this._isDiscarding = true;
@ -237,6 +281,7 @@
this._id("preview")?.after(this.nextPreview);
this.setPreviewStatus("loading");
this._isDiscarding = false;
this._renderPromise?.resolve();
}
async openAttachment(event) {

View file

@ -70,7 +70,9 @@
set usePreview(val) {
this.toggleAttribute('data-use-preview', val);
this.updatePreview();
if (this.item) {
this.updatePreview();
}
}
init() {
@ -83,11 +85,17 @@
this._addPopup.id = '';
this.querySelector('popupset').append(this._addPopup);
this.usePreview = Zotero.Prefs.get('showAttachmentPreview');
this._preview = this.querySelector('attachment-preview');
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'attachmentsBox');
this._section._contextMenu.addEventListener('popupshowing', this._handleContextMenu, { once: true });
// For tests
this._asyncRendering = false;
// Indicate if the preview should update, can be none | initial | final
this._renderStage = "none";
}
destroy() {
@ -96,16 +104,13 @@
}
notify(action, type, ids) {
if (ids.includes(this._item?.id)) {
this._resetRenderedFlags();
}
if (!this._item?.isRegularItem()) return;
this._updateAttachmentIDs().then(() => {
this.updatePreview();
let attachments = Zotero.Items.get((this._attachmentIDs).filter(id => ids.includes(id)));
if (attachments.length === 0) {
if (attachments.length === 0 && action !== "delete") {
return;
}
if (action == 'add') {
@ -113,20 +118,20 @@
this.addRow(attachment);
}
}
else if (action == 'modify') {
// When annotation added to attachment, action=modify
// When annotation deleted from attachment, action=refresh
else if (action == 'modify' || action == 'refresh') {
for (let attachment of attachments) {
let row = this.querySelector(`attachment-row[attachment-id="${attachment.id}"]`);
let open = false;
if (row) {
open = row.open;
row.remove();
}
this.addRow(attachment).open = open;
this.addRow(attachment);
}
}
else if (action == 'delete') {
for (let attachment of attachments) {
let row = this.querySelector(`attachment-row[attachment-id="${attachment.id}"]`);
for (let id of ids) {
let row = this.querySelector(`attachment-row[attachment-id="${id}"]`);
if (row) {
row.remove();
}
@ -137,11 +142,9 @@
});
}
addRow(attachment, open = false) {
addRow(attachment) {
let row = document.createXULElement('attachment-row');
this._updateRowAttributes(row, attachment);
// Set open state before adding to dom to prevent animation
row.toggleAttribute("open", open);
let index = this._attachmentIDs.indexOf(attachment.id);
if (index < 0 || index >= this._attachments.children.length) {
@ -156,12 +159,15 @@
render() {
if (!this._item) return;
if (this._isAlreadyRendered()) return;
this._renderStage = "initial";
this.updateCount();
}
async asyncRender() {
if (!this._item) return;
if (this._isAlreadyRendered("async")) return;
this._renderStage = "final";
this._asyncRendering = true;
await this._updateAttachmentIDs();
@ -171,15 +177,29 @@
for (let attachment of itemAttachments) {
this.addRow(attachment);
}
this.usePreview = Zotero.Prefs.get('showAttachmentPreview');
await this.updatePreview();
this._asyncRendering = false;
}
updateCount() {
if (!this._item?.isRegularItem()) {
return;
}
let count = this._item.numAttachments(this.inTrash);
this._section.setCount(count);
}
async updatePreview() {
// Skip if asyncRender is not finished/executed, which means the box is invisible
// The box will be rendered when it becomes visible
if (this._renderStage !== "final") {
return;
}
let attachment = await this._getPreviewAttachment();
this.toggleAttribute('data-use-preview', !!attachment && Zotero.Prefs.get('showAttachmentPreview'));
if (!attachment) {
return;
}
if (!this.usePreview
// Skip only when the section is manually collapsed (when there's attachment),
// This is necessary to ensure the rendering of the first added attachment
@ -187,11 +207,6 @@
|| (this._attachmentIDs.length && !this._section.open)) {
return;
}
let attachment = await this._getPreviewAttachment();
if (!attachment) {
this.toggleAttribute('data-use-preview', false);
return;
}
this._preview.item = attachment;
await this._preview.render();
}
@ -199,7 +214,7 @@
async _getPreviewAttachment() {
let attachment = await this._item.getBestAttachment();
if (this.tabType === "reader"
&& Zotero_Tabs._getTab(Zotero_Tabs.selectedID)?.tab?.data?.itemID == attachment.id) {
&& Zotero_Tabs._getTab(this.tabID)?.tab?.data?.itemID == attachment.id) {
// In the reader, only show the preview when viewing a secondary attachment
return null;
}

View file

@ -295,6 +295,7 @@
this._itemPaneDeck.appendChild(itemDetails);
itemDetails.editable = editable;
itemDetails.tabID = tabID;
itemDetails.tabType = "reader";
itemDetails.item = targetItem;
// Manually cache parentID

View file

@ -100,6 +100,14 @@
this.toggleAttribute('readonly', !editable);
}
get tabID() {
return this._tabID;
}
set tabID(tabID) {
this._tabID = tabID;
}
get tabType() {
return this._tabType;
}
@ -215,12 +223,18 @@
let item = this.item;
Zotero.debug('Viewing item');
this._isRendering = true;
// For tests
let resolve;
if (Zotero.test) {
this._renderPromise = new Promise(r => resolve = r);
}
this.renderCustomSections();
let panes = this.getPanes();
for (let box of [this._header, ...panes]) {
box.editable = this.editable;
box.tabID = this.tabID;
box.tabType = this.tabType;
box.item = item;
// Execute sync render immediately
@ -260,6 +274,9 @@
if (this.item.id == item.id) {
this._isRendering = false;
}
if (Zotero.test) {
resolve();
}
}
renderCustomSections() {

View file

@ -151,6 +151,7 @@
this.mode = "item";
this._itemDetails.editable = this.editable;
this._itemDetails.tabID = "zotero-pane";
this._itemDetails.tabType = "library";
this._itemDetails.item = item;

View file

@ -43,6 +43,14 @@ class ItemPaneSectionElementBase extends XULElementBase {
this.toggleAttribute('readonly', !editable);
}
get tabID() {
return this._tabID;
}
set tabID(tabID) {
this._tabID = tabID;
}
get tabType() {
return this._tabType;
}

View file

@ -384,6 +384,31 @@ async function delay(ms) {
return Zotero.Promise.delay(ms);
}
async function waitForFrame() {
return waitNoLongerThan(new Promise((resolve) => {
requestAnimationFrame(resolve);
}), 30);
}
async function waitForFrames(n) {
for (let i = 0; i < n; i++) {
await waitForFrame();
}
}
async function waitNoLongerThan(promise, ms = 1000) {
return Promise.race([
promise,
Zotero.Promise.delay(ms)
]);
}
async function waitForScrollToPane(itemDetails, paneID) {
await itemDetails._renderPromise;
itemDetails.scrollToPane(paneID, "instant");
// Wait for some frames or up to 150ms to ensure the pane is visible
await waitForFrames(5);
}
function clickOnItemsRow(win, itemsView, row) {
itemsView._treebox.scrollToRow(row);
@ -1115,3 +1140,4 @@ async function startHTTPServer(port = null) {
var baseURL = `http://localhost:${port}/`
return { httpd, port, baseURL };
}

View file

@ -1,9 +1,38 @@
describe("Item pane", function () {
var win, doc, itemsView;
var win, doc, ZoteroPane, Zotero_Tabs, ZoteroContextPane, itemsView;
async function waitForPreviewBoxRender(box) {
let success = await waitForCallback(
() => box._asyncRenderItemID && !box._asyncRendering);
if (!success) {
throw new Error("Wait for box render time out");
}
await box._preview._renderPromise;
return success;
}
async function waitForPreviewBoxReader(box, itemID) {
await waitForPreviewBoxRender(box);
let success = await waitForCallback(
() => box._preview._reader?.itemID == itemID, 100, 3000);
if (!success) {
throw new Error("Wait for box preview reader time out");
}
await box._preview._reader._initPromise;
return success;
}
function isPreviewDisplayed(box) {
return !!(box._preview.hasPreview
&& win.getComputedStyle(box._preview).display !== "none");
}
before(function* () {
win = yield loadZoteroPane();
doc = win.document;
ZoteroPane = win.ZoteroPane;
Zotero_Tabs = win.Zotero_Tabs;
ZoteroContextPane = win.ZoteroContextPane;
itemsView = win.ZoteroPane.itemsView;
});
after(function () {
@ -168,7 +197,7 @@ describe("Item pane", function () {
assert.equal(label.value, 'Test');
yield Zotero.Items.erase(id);
})
});
it("should swap creator names", async function () {
@ -344,7 +373,479 @@ describe("Item pane", function () {
'1'
);
});
})
});
describe("Attachments pane", function () {
let paneID = "attachments";
beforeEach(function () {
Zotero.Prefs.set("panes.attachments.open", true);
Zotero.Prefs.set("showAttachmentPreview", true);
Zotero_Tabs.select("zotero-pane");
});
afterEach(function () {
Zotero_Tabs.select("zotero-pane");
});
it("should show attachments pane in library for regular item", async function () {
// Regular item: show
let attachmentsBox = ZoteroPane.itemPane._itemDetails.getPane(paneID);
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
assert.isFalse(attachmentsBox.hidden);
// Child attachment: hide
let file = getTestDataDirectory();
file.append('test.pdf');
let attachment = await Zotero.Attachments.importFromFile({
file,
parentItemID: item.id
});
await ZoteroPane.selectItem(attachment.id);
assert.isTrue(attachmentsBox.hidden);
// Standalone attachment: hide
let attachment1 = await importFileAttachment('test.pdf');
await ZoteroPane.selectItem(attachment1.id);
assert.isTrue(attachmentsBox.hidden);
});
it("should not show attachments pane preview in reader best-matched attachment item", async function () {
let item = new Zotero.Item('book');
let file = getTestDataDirectory();
file.append('test.pdf');
await item.saveTx();
let attachment = await Zotero.Attachments.importFromFile({
file,
parentItemID: item.id
});
await ZoteroPane.viewItems([attachment]);
let tabID = Zotero_Tabs.selectedID;
let itemDetails = ZoteroContextPane.context._getItemContext(tabID);
let attachmentsBox = itemDetails.getPane(paneID);
assert.isFalse(attachmentsBox.hidden);
await waitForScrollToPane(itemDetails, paneID);
assert.isFalse(isPreviewDisplayed(attachmentsBox));
});
it("should not show attachments pane in reader standalone attachment item", async function () {
let attachment = await importFileAttachment('test.pdf');
await ZoteroPane.viewItems([attachment]);
let tabID = Zotero_Tabs.selectedID;
let itemDetails = ZoteroContextPane.context._getItemContext(tabID);
let attachmentsBox = itemDetails.getPane(paneID);
assert.isTrue(attachmentsBox.hidden);
});
it("should show attachments pane preview in reader non-best-matched attachment item", async function () {
let item = new Zotero.Item('book');
let file = getTestDataDirectory();
file.append('test.pdf');
await item.saveTx();
await Zotero.Attachments.importFromFile({
file,
parentItemID: item.id
});
await Zotero.Attachments.importFromFile({
file,
parentItemID: item.id
});
let bestAttachments = await item.getBestAttachments();
await ZoteroPane.viewItems([bestAttachments[1]]);
// Ensure context pane is open
ZoteroContextPane.splitter.setAttribute("state", "open");
await waitForFrame();
let tabID = Zotero_Tabs.selectedID;
let itemDetails = ZoteroContextPane.context._getItemContext(tabID);
let attachmentsBox = itemDetails.getPane(paneID);
assert.isFalse(attachmentsBox.hidden);
await waitForScrollToPane(itemDetails, paneID);
await waitForPreviewBoxRender(attachmentsBox);
assert.isTrue(isPreviewDisplayed(attachmentsBox));
});
it("should not render attachments pane preview when show preview is disabled", async function () {
Zotero.Prefs.set("showAttachmentPreview", false);
let itemDetails = ZoteroPane.itemPane._itemDetails;
let attachmentsBox = itemDetails.getPane(paneID);
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
assert.isFalse(attachmentsBox.hidden);
await waitForScrollToPane(itemDetails, paneID);
assert.isFalse(isPreviewDisplayed(attachmentsBox));
});
it("should only render after attachments pane becomes visible", async function () {
// Resize to very small height to ensure the attachment box is not in view
let height = doc.documentElement.clientHeight;
win.resizeTo(null, 100);
let itemDetails = ZoteroPane.itemPane._itemDetails;
let attachmentsBox = itemDetails.getPane(paneID);
let preview = attachmentsBox._preview;
// Force discard previous preview
await preview.discard(true);
let item = new Zotero.Item('book');
await item.saveTx();
let file = getTestDataDirectory();
file.append('test.pdf');
await Zotero.Attachments.importFromFile({
file,
parentItemID: item.id
});
await ZoteroPane.selectItem(item.id);
assert.isFalse(itemDetails.isPaneVisible(paneID));
// Do not use _isAlreadyRendered, since that changes the render flag state
assert.equal(attachmentsBox._syncRenderItemID, item.id);
assert.notEqual(attachmentsBox._asyncRenderItemID, item.id);
assert.isFalse(isPreviewDisplayed(attachmentsBox));
await waitForScrollToPane(itemDetails, paneID);
await waitForPreviewBoxRender(attachmentsBox);
assert.isTrue(itemDetails.isPaneVisible(paneID));
assert.equal(attachmentsBox._syncRenderItemID, item.id);
assert.equal(attachmentsBox._asyncRenderItemID, item.id);
assert.isTrue(isPreviewDisplayed(attachmentsBox));
assert.isTrue(preview.hasPreview);
win.resizeTo(null, height);
});
it("should update attachments pane when attachments changed", async function () {
// https://forums.zotero.org/discussion/113632/zotero-7-beta-pdf-attachment-preview-and-annotations-not-refreshed-after-adding-annotations
let itemDetails = ZoteroPane.itemPane._itemDetails;
let attachmentsBox = itemDetails.getPane(paneID);
let preview = attachmentsBox._preview;
// Force discard previous preview
await preview.discard(true);
// Pin the pane to ensure it's rendered
itemDetails.pinnedPane = paneID;
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
assert.isTrue(await waitForPreviewBoxRender(attachmentsBox));
// No preview
assert.isFalse(isPreviewDisplayed(attachmentsBox));
// No row
assert.equal(attachmentsBox.querySelectorAll("attachment-row").length, 0);
// Add an attachment
let file = getTestDataDirectory();
file.append('test.png');
let _attachment1 = await Zotero.Attachments.importFromFile({
file,
parentItemID: item.id
});
await ZoteroPane.selectItem(item.id);
await itemDetails._renderPromise;
await waitForPreviewBoxRender(attachmentsBox);
// Image preview for item with image attachment
assert.isTrue(isPreviewDisplayed(attachmentsBox));
assert.equal(preview.previewType, "image");
// 1 row
assert.equal(attachmentsBox.querySelectorAll("attachment-row").length, 1);
// Add an PDF attachment, which will be best match and update the preview
file = getTestDataDirectory();
file.append('test.pdf');
let attachment2 = await Zotero.Attachments.importFromFile({
file,
parentItemID: item.id
});
await waitForPreviewBoxReader(attachmentsBox, attachment2.id);
await Zotero.Promise.delay(100);
// PDF preview
assert.isTrue(isPreviewDisplayed(attachmentsBox));
assert.equal(preview.previewType, "pdf");
// 2 rows
assert.equal(attachmentsBox.querySelectorAll("attachment-row").length, 2);
// Created annotations should be update in preview and attachment row
let annotation = await createAnnotation('highlight', attachment2);
await Zotero.Promise.delay(100);
// Annotation updated in preview reader
let readerAnnotation
= preview._reader._internalReader._annotationManager._annotations.find(
a => a.libraryID === annotation.libraryID && a.id === annotation.key
);
assert.exists(readerAnnotation);
assert.equal(attachmentsBox.querySelectorAll("attachment-row").length, 2);
let attachmentRow = attachmentsBox.querySelector(`attachment-row[attachment-id="${attachment2.id}"]`);
assert.isFalse(attachmentRow._annotationButton.hidden);
// 1 annotation
assert.equal(attachmentRow._annotationButton.querySelector('.label').textContent, "1");
// Deleted annotations should be removed from preview and attachment row
await annotation.eraseTx();
await Zotero.Promise.delay(100);
// Annotation removed from preview reader
readerAnnotation
= preview._reader._internalReader._annotationManager._annotations.find(
a => a.libraryID === annotation.libraryID && a.id === annotation.key
);
assert.notExists(readerAnnotation);
// Row might be recreated
attachmentRow = attachmentsBox.querySelector(`attachment-row[attachment-id="${attachment2.id}"]`);
assert.isTrue(attachmentRow._annotationButton.hidden);
// 0 annotation
assert.equal(attachmentRow._annotationButton.querySelector('.label').textContent, "0");
// Delete attachment
await attachment2.eraseTx();
await Zotero.Promise.delay(100);
// Image preview for item with image attachment
assert.isTrue(isPreviewDisplayed(attachmentsBox));
assert.equal(preview.previewType, "image");
// 1 row
assert.equal(attachmentsBox.querySelectorAll("attachment-row").length, 1);
// The corresponding row should be removed
attachmentRow = attachmentsBox.querySelector(`attachment-row[attachment-id="${attachment2.id}"]`);
assert.notExists(attachmentRow);
// Unpin
itemDetails.pinnedPane = "";
});
it("should keep attachments pane preview status after switching tab", async function () {
// https://forums.zotero.org/discussion/113658/zotero-7-beta-preview-appearing-in-the-item-pane-of-the-pdf-tab
let item = new Zotero.Item('book');
let file = getTestDataDirectory();
file.append('test.pdf');
await item.saveTx();
let attachment = await Zotero.Attachments.importFromFile({
file,
parentItemID: item.id
});
// Open reader
await ZoteroPane.viewItems([attachment]);
let tabID = Zotero_Tabs.selectedID;
await Zotero.Reader.getByTabID(tabID)._waitForReader();
// Ensure context pane is open
ZoteroContextPane.splitter.setAttribute("state", "open");
await waitForFrame();
let itemDetails = ZoteroContextPane.context._getItemContext(tabID);
let attachmentsBox = itemDetails.getPane(paneID);
assert.isFalse(attachmentsBox.hidden);
await waitForScrollToPane(itemDetails, paneID);
assert.isFalse(isPreviewDisplayed(attachmentsBox));
// Select library tab
Zotero_Tabs.select("zotero-pane");
let libraryItemDetails = ZoteroPane.itemPane._itemDetails;
let libraryAttachmentsBox = libraryItemDetails.getPane(paneID);
await ZoteroPane.selectItem(item.id);
await waitForScrollToPane(libraryItemDetails, paneID);
// Collapse section
libraryAttachmentsBox.querySelector('collapsible-section > .head').click();
await Zotero.Promise.delay(50);
// Open section
libraryAttachmentsBox.querySelector('collapsible-section > .head').click();
await Zotero.Promise.delay(50);
// Select reader tab
Zotero_Tabs.select(tabID);
// Make sure the preview status is not changed in reader
assert.isFalse(isPreviewDisplayed(attachmentsBox));
});
/**
* This test is essential to ensure the proper functioning of the sync/async rendering,
* scrolling handler, and pinning mechanism of ItemDetails.
* AttachmentsBox serves as a good example since it involves both sync and async rendering.
* If this test fails, it is not recommended to add timeouts as a quick fix.
*/
it("should keep attachments pane status after changing selection", async function () {
let itemDetails = ZoteroPane.itemPane._itemDetails;
let attachmentsBox = itemDetails.getPane(paneID);
let preview = attachmentsBox._preview;
// Pin the pane to avoid always scrolling to the section
itemDetails.pinnedPane = paneID;
// item with attachment (1 annotation)
let item1 = new Zotero.Item('book');
await item1.saveTx();
let file = getTestDataDirectory();
file.append('test.pdf');
let attachment1 = await Zotero.Attachments.importFromFile({
file,
parentItemID: item1.id
});
let annotation = await createAnnotation('highlight', attachment1);
await itemDetails._renderPromise;
await waitForPreviewBoxReader(attachmentsBox, attachment1.id);
assert.isFalse(attachmentsBox.hidden);
let readerAnnotation
= preview._reader._internalReader._annotationManager._annotations.find(
a => a.libraryID === annotation.libraryID && a.id === annotation.key
);
assert.exists(readerAnnotation);
assert.equal(attachmentsBox.querySelectorAll("attachment-row").length, 1);
let attachmentRow = attachmentsBox.querySelector(`attachment-row[attachment-id="${attachment1.id}"]`);
assert.isFalse(attachmentRow._annotationButton.hidden);
// 1 annotation
assert.equal(attachmentRow._annotationButton.querySelector('.label').textContent, "1");
// item with attachment (no annotation)
let item2 = new Zotero.Item('book');
await item2.saveTx();
file = getTestDataDirectory();
file.append('wonderland_short.pdf');
let attachment2 = await Zotero.Attachments.importFromFile({
file,
parentItemID: item2.id
});
// Select item with attachment (no annotation)
await itemDetails._renderPromise;
await waitForPreviewBoxReader(attachmentsBox, attachment2.id);
assert.isFalse(attachmentsBox.hidden);
readerAnnotation
= preview._reader._internalReader._annotationManager._annotations.find(
a => a.libraryID === annotation.libraryID && a.id === annotation.key
);
assert.notExists(readerAnnotation);
assert.equal(attachmentsBox.querySelectorAll("attachment-row").length, 1);
attachmentRow = attachmentsBox.querySelector(`attachment-row[attachment-id="${attachment2.id}"]`);
assert.isTrue(attachmentRow._annotationButton.hidden);
// 0 annotation
assert.equal(attachmentRow._annotationButton.querySelector('.label').textContent, "0");
let item3 = new Zotero.Item('book');
await item3.saveTx();
// Select item without attachment
await itemDetails._renderPromise;
assert.isFalse(attachmentsBox.hidden);
assert.equal(attachmentsBox.querySelectorAll("attachment-row").length, 0);
// Again, select item with attachment (1 annotation)
await ZoteroPane.selectItem(item1.id);
await itemDetails._renderPromise;
await waitForPreviewBoxReader(attachmentsBox, attachment1.id);
assert.isFalse(attachmentsBox.hidden);
readerAnnotation
= preview._reader._internalReader._annotationManager._annotations.find(
a => a.libraryID === annotation.libraryID && a.id === annotation.key
);
assert.exists(readerAnnotation);
assert.equal(attachmentsBox.querySelectorAll("attachment-row").length, 1);
attachmentRow = attachmentsBox.querySelector(`attachment-row[attachment-id="${attachment1.id}"]`);
assert.isFalse(attachmentRow._annotationButton.hidden);
// 1 annotation
assert.equal(attachmentRow._annotationButton.querySelector('.label').textContent, "1");
// Unpin
itemDetails.pinnedPane = "";
});
it("should open attachment on clicking attachment row", async function () {
let itemDetails = ZoteroPane.itemPane._itemDetails;
let attachmentsBox = itemDetails.getPane(paneID);
let item = new Zotero.Item('book');
await item.saveTx();
let file = getTestDataDirectory();
file.append('test.pdf');
let attachment = await Zotero.Attachments.importFromFile({
file,
parentItemID: item.id
});
await ZoteroPane.selectItem(item.id);
await waitForScrollToPane(itemDetails, paneID);
await waitForPreviewBoxRender(attachmentsBox);
let attachmentRow = attachmentsBox.querySelector(`attachment-row[attachment-id="${attachment.id}"]`);
attachmentRow._attachmentButton.click();
await Zotero.Promise.delay(100);
let reader = await Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
// Should open attachment
assert.equal(reader.itemID, attachment.id);
});
it("should select attachment on clicking annotation button of attachment row", async function () {
let itemDetails = ZoteroPane.itemPane._itemDetails;
let attachmentsBox = itemDetails.getPane(paneID);
let item = new Zotero.Item('book');
await item.saveTx();
let file = getTestDataDirectory();
file.append('test.pdf');
let attachment = await Zotero.Attachments.importFromFile({
file,
parentItemID: item.id
});
let _annotation = await createAnnotation('highlight', attachment);
await ZoteroPane.selectItem(item.id);
await waitForScrollToPane(itemDetails, paneID);
await waitForPreviewBoxRender(attachmentsBox);
let attachmentRow = attachmentsBox.querySelector(`attachment-row[attachment-id="${attachment.id}"]`);
attachmentRow._annotationButton.click();
await Zotero.Promise.delay(100);
// Should select attachment
assert.equal(ZoteroPane.getSelectedItems(true)[0], attachment.id);
});
it("should open attachment on double-clicking attachments pane preview", async function () {
let itemDetails = ZoteroPane.itemPane._itemDetails;
let attachmentsBox = itemDetails.getPane(paneID);
let preview = attachmentsBox._preview;
let item = new Zotero.Item('book');
await item.saveTx();
let file = getTestDataDirectory();
file.append('test.pdf');
let attachment = await Zotero.Attachments.importFromFile({
file,
parentItemID: item.id
});
await ZoteroPane.selectItem(item.id);
await waitForScrollToPane(itemDetails, paneID);
await waitForPreviewBoxRender(attachmentsBox);
let event = new MouseEvent('dblclick', {
bubbles: true,
cancelable: true,
view: window
});
preview.dispatchEvent(event);
await Zotero.Promise.delay(100);
let reader = await Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
// Should open attachment
assert.equal(reader.itemID, attachment.id);
});
});
describe("Notes pane", function () {
@ -461,6 +962,18 @@ describe("Item pane", function () {
describe("Attachment pane", function () {
let paneID = "attachment-info";
beforeEach(function () {
Zotero.Prefs.set("panes.attachment-info.open", true);
Zotero.Prefs.set("showAttachmentPreview", true);
Zotero_Tabs.select("zotero-pane");
});
afterEach(function () {
Zotero_Tabs.select("zotero-pane");
});
it("should refresh on file rename", async function () {
let file = getTestDataDirectory();
file.append('test.png');
@ -499,6 +1012,95 @@ describe("Item pane", function () {
await promise;
assert.equal(label.value, newTitle);
});
it("should show attachment pane in library for attachment item", async function () {
// Regular item: hide
let itemDetails = ZoteroPane.itemPane._itemDetails;
let box = itemDetails.getPane(paneID);
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
await waitForScrollToPane(itemDetails, paneID);
assert.isTrue(box.hidden);
// Child attachment: show
let file = getTestDataDirectory();
file.append('test.pdf');
let attachment = await Zotero.Attachments.importFromFile({
file,
parentItemID: item.id
});
await ZoteroPane.selectItem(attachment.id);
await waitForScrollToPane(itemDetails, paneID);
await waitForPreviewBoxReader(box, attachment.id);
assert.isFalse(box.hidden);
await Zotero.Promise.delay(100);
assert.isTrue(isPreviewDisplayed(box));
// Standalone attachment: show
let attachment1 = await importFileAttachment('test.pdf');
await ZoteroPane.selectItem(attachment1.id);
await waitForScrollToPane(itemDetails, paneID);
await waitForPreviewBoxReader(box, attachment1.id);
assert.isFalse(box.hidden);
await Zotero.Promise.delay(100);
assert.isTrue(isPreviewDisplayed(box));
});
it("should show attachment pane without preview in reader for standalone attachment item", async function () {
// Attachment item with parent item: hide
let item = new Zotero.Item('book');
let file = getTestDataDirectory();
file.append('test.pdf');
await item.saveTx();
let attachment = await Zotero.Attachments.importFromFile({
file,
parentItemID: item.id
});
await ZoteroPane.viewItems([attachment]);
let tabID = Zotero_Tabs.selectedID;
let itemDetails = ZoteroContextPane.context._getItemContext(tabID);
let box = itemDetails.getPane(paneID);
assert.isTrue(box.hidden);
// Standalone attachment item: show
attachment = await importFileAttachment('test.pdf');
await ZoteroPane.viewItems([attachment]);
tabID = Zotero_Tabs.selectedID;
itemDetails = ZoteroContextPane.context._getItemContext(tabID);
box = itemDetails.getPane(paneID);
assert.isFalse(box.hidden);
await waitForScrollToPane(itemDetails, paneID);
// No preview
assert.isFalse(isPreviewDisplayed(box));
});
it("should only show attachment note container when exists", async function () {
let itemDetails = ZoteroPane.itemPane._itemDetails;
let box = itemDetails.getPane(paneID);
let noteContainer = box._id("note-container");
let noteEditor = box._id('attachment-note-editor');
// Hide note container by default
let attachment = await importFileAttachment('test.pdf');
await ZoteroPane.selectItem(attachment.id);
await itemDetails._renderPromise;
await waitForScrollToPane(itemDetails, paneID);
await waitForPreviewBoxRender(box);
assert.isTrue(noteContainer.hidden);
// Add attachment note
let itemModifyPromise = waitForItemEvent("modify");
attachment.setNote("<h1>TEST</h1>");
await attachment.saveTx();
await itemModifyPromise;
await waitForPreviewBoxRender(box);
// Should show note container
assert.isFalse(noteContainer.hidden);
// Should be readonly
assert.equal(noteEditor.mode, "view");
});
});
@ -602,4 +1204,4 @@ describe("Item pane", function () {
);
});
});
})
});