zotero/test/tests/pluginAPITest.js

1141 lines
32 KiB
JavaScript

describe("Plugin API", function () {
var win, doc, ZoteroPane, Zotero_Tabs, ZoteroContextPane, _itemsView, infoSection, caches;
function resetCaches() {
caches = {};
}
function initCache(key) {
if (!caches[key]) {
caches[key] = {};
}
caches[key].deferred = Zotero.Promise.defer();
caches[key].result = "";
}
async function getCache(key) {
let cache = caches[key];
await cache.deferred.promise;
return cache.result;
}
function updateCache(key, value) {
let cache = caches[key];
if (!cache) return;
cache.result = value;
cache.deferred?.resolve();
}
before(async function () {
win = await loadZoteroPane();
doc = win.document;
ZoteroPane = win.ZoteroPane;
Zotero_Tabs = win.Zotero_Tabs;
ZoteroContextPane = win.ZoteroContextPane;
_itemsView = win.ZoteroPane.itemsView;
infoSection = win.ZoteroPane.itemPane._itemDetails.getPane('info');
});
after(function () {
win.close();
});
describe("Item pane info box custom section", function () {
let defaultOption = {
rowID: "default-test",
pluginID: "zotero@zotero.org",
label: {
l10nID: "general-print",
},
onGetData: ({ item }) => {
let data = `${item.id}`;
updateCache("onGetData", data);
return data;
},
};
let waitForRegister = async (option) => {
initCache("onGetData");
let getDataPromise = getCache("onGetData");
let rowID = Zotero.ItemPaneManager.registerInfoRow(option);
await getDataPromise;
return rowID;
};
let waitForUnregister = async (rowID) => {
let unregisterPromise = waitForNotifierEvent("refresh", "infobox");
let success = Zotero.ItemPaneManager.unregisterInfoRow(rowID);
await unregisterPromise;
return success;
};
beforeEach(async function () {
resetCaches();
});
afterEach(function () {
Zotero_Tabs.select("zotero-pane");
Zotero_Tabs.closeAll();
});
it("should render custom row and call onGetData hook", async function () {
initCache("onGetData");
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
let getDataPromise = getCache("onGetData");
let rowID = Zotero.ItemPaneManager.registerInfoRow(defaultOption);
let result = await getDataPromise;
// Should render custom row
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
assert.exists(rowElem);
// Should call onGetData and render
let valueElem = rowElem.querySelector(".value");
assert.equal(result, valueElem.value);
await waitForUnregister(rowID);
});
it("should call onSetData hook", async function () {
let option = Object.assign({}, defaultOption, {
onSetData: ({ value }) => {
let data = `${value}`;
updateCache("onSetData", data);
},
});
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
let rowID = await waitForRegister(option);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
let valueElem = rowElem.querySelector(".value");
// Should call onSetData on value change
initCache("onSetData");
let setDataPromise = getCache("onSetData");
let newValue = `TEST CUSTOM ROW`;
valueElem.focus();
valueElem.value = newValue;
let blurEvent = new Event("blur");
valueElem.dispatchEvent(blurEvent);
let result = await setDataPromise;
assert.equal(newValue, result);
await waitForUnregister(rowID);
});
it("should call onItemChange hook", async function () {
let option = Object.assign({}, defaultOption, {
onItemChange: ({ item, tabType, setEnabled, setEditable }) => {
let editable = item.itemType === "book";
let enabled = tabType === "library";
setEnabled(enabled);
setEditable(editable);
let data = { editable, enabled };
updateCache("onItemChange", data);
}
});
initCache("onItemChange");
let bookItem = new Zotero.Item('book');
await bookItem.saveTx();
await ZoteroPane.selectItem(bookItem.id);
let itemChangePromise = getCache("onItemChange");
let rowID = await waitForRegister(option);
let result = await itemChangePromise;
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
let valueElem = rowElem.querySelector(".value");
// Should be enabled and editable
assert.isTrue(result.editable);
assert.isFalse(valueElem.readOnly);
assert.isTrue(result.enabled);
assert.isFalse(rowElem.hidden);
initCache("onItemChange");
itemChangePromise = getCache("onItemChange");
let docItem = new Zotero.Item('document');
await docItem.saveTx();
await ZoteroPane.selectItem(docItem.id);
result = await itemChangePromise;
// Should be enabled and not editable
assert.isFalse(result.editable);
assert.isTrue(valueElem.readOnly);
assert.isTrue(result.enabled);
assert.isFalse(rowElem.hidden);
let file = getTestDataDirectory();
file.append('test.pdf');
let attachment = await Zotero.Attachments.importFromFile({
file,
parentItemID: docItem.id
});
initCache("onItemChange");
itemChangePromise = getCache("onItemChange");
await ZoteroPane.viewItems([attachment]);
let tabID = Zotero_Tabs.selectedID;
let reader = Zotero.Reader.getByTabID(tabID);
await reader._initPromise;
// Ensure context pane is open
ZoteroContextPane.collapsed = false;
result = await itemChangePromise;
let itemDetails = ZoteroContextPane.context._getItemContext(tabID);
rowElem = itemDetails.getPane("info").querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
valueElem = rowElem.querySelector(".value");
// Should not be enabled in non-library tab
assert.isFalse(result.enabled);
assert.isTrue(rowElem.hidden);
await waitForUnregister(rowID);
});
it("should render row at position", async function () {
let startOption = Object.assign({}, defaultOption, {
position: "start",
});
let afterCreatorsOption = Object.assign({}, defaultOption, {
position: "afterCreators",
});
let endOption = Object.assign({}, defaultOption, {
position: "end",
});
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
// Row at start
let rowID = await waitForRegister(startOption);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
assert.notExists(rowElem.previousElementSibling);
await waitForUnregister(rowID);
// Row after creator rows
rowID = await waitForRegister(afterCreatorsOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
assert.exists(rowElem.previousElementSibling.querySelector(".creator-type-value"));
assert.notExists(rowElem.nextElementSibling.querySelector(".creator-type-value"));
await waitForUnregister(rowID);
// Row at end
rowID = rowID = await waitForRegister(endOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
assert.exists(rowElem.nextElementSibling.querySelector("*[fieldname=dateAdded]"));
await waitForUnregister(rowID);
});
it("should set input editable", async function () {
let editableOption = Object.assign({}, defaultOption, {
editable: true,
});
let notEditableOption = Object.assign({}, defaultOption, {
editable: false,
});
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
let rowID = await waitForRegister(defaultOption);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
let valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.readOnly);
await waitForUnregister(rowID);
rowID = await waitForRegister(editableOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.readOnly);
await waitForUnregister(rowID);
rowID = await waitForRegister(notEditableOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
valueElem = rowElem.querySelector(".value");
assert.isTrue(valueElem.readOnly);
await waitForUnregister(rowID);
});
it("should set input multiline", async function () {
let multilineOption = Object.assign({}, defaultOption, {
multiline: true,
});
let notMultilineOption = Object.assign({}, defaultOption, {
multiline: false,
});
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
let rowID = await waitForRegister(defaultOption);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
let valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.multiline);
await waitForUnregister(rowID);
rowID = await waitForRegister(multilineOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
valueElem = rowElem.querySelector(".value");
assert.isTrue(valueElem.multiline);
await waitForUnregister(rowID);
rowID = await waitForRegister(notMultilineOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.multiline);
await waitForUnregister(rowID);
});
it("should set input nowrap", async function () {
let noWrapOption = Object.assign({}, defaultOption, {
nowrap: true,
});
let wrapOption = Object.assign({}, defaultOption, {
nowrap: false,
});
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
let rowID = await waitForRegister(defaultOption);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
let valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.noWrap);
await waitForUnregister(rowID);
rowID = await waitForRegister(noWrapOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
valueElem = rowElem.querySelector(".value");
assert.isTrue(valueElem.noWrap);
await waitForUnregister(rowID);
rowID = await waitForRegister(wrapOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.noWrap);
await waitForUnregister(rowID);
});
it("should refresh custom row value", async function () {
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
let rowID = await waitForRegister(defaultOption);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
let valueElem = rowElem.querySelector(".value");
let oldValue = valueElem.value;
// Since this row does not have `onSetData`, changing value does not do anything
// We just want to test if the value can be refreshed by calling `updateInfoRow`
let newValue = "TEST CUSTOM ROW EDITED";
valueElem.value = newValue;
let notifyPromise = waitForNotifierEvent("refresh", "infobox");
// Manually refresh the row
Zotero.ItemPaneManager.refreshInfoRow(rowID);
await notifyPromise;
assert.equal(oldValue, valueElem.value);
await waitForUnregister(rowID);
});
it("should render custom row value after item change", async function () {
// https://github.com/zotero/zotero/issues/4874
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
let rowID = await waitForRegister(defaultOption);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
let valueElem = rowElem.querySelector(".value");
let value = valueElem.value;
assert.equal(`${item.id}`, value);
initCache("onGetData");
let getDataPromise = getCache("onGetData");
let docItem = new Zotero.Item('document');
await docItem.saveTx();
await ZoteroPane.selectItem(docItem.id);
await getDataPromise;
value = valueElem.value;
assert.equal(`${docItem.id}`, value);
await waitForUnregister(rowID);
});
});
describe("Item tree custom column", function () {
// Only test hooks, as other column options are covered in item tree tests
let defaultOption = {
columnID: "default-test",
pluginID: "zotero@zotero.org",
dataKey: "api-test",
label: "APITest",
dataProvider: (item) => {
let data = `${item.id}`;
updateCache("dataProvider", data);
return data;
},
};
let waitForRegister = async (option) => {
initCache("dataProvider");
let getDataPromise = getCache("dataProvider");
let columnKey = Zotero.ItemTreeManager.registerColumn(option);
await getDataPromise;
return columnKey;
};
let waitForColumnEnable = async (dataKey) => {
_itemsView._columnPrefs[dataKey] = {
dataKey,
hidden: false,
};
let columns = _itemsView._getColumns();
let columnID = columns.findIndex(column => column.dataKey === dataKey);
if (columnID === -1) {
return;
}
let column = columns[columnID];
if (!column.hidden) {
return;
}
_itemsView.tree._columns.toggleHidden(columnID);
// Wait for column header to render
await waitForCallback(
() => !!doc.querySelector(`#zotero-items-tree .virtualized-table-header .cell.${CSS.escape(dataKey)}`),
100, 3);
};
let waitForUnregister = async (columnID) => {
let unregisterPromise = waitForNotifierEvent("refresh", "itemtree");
let success = Zotero.ItemTreeManager.unregisterColumn(columnID);
await unregisterPromise;
return success;
};
let getSelectedRowCell = (dataKey) => {
let cell = doc.querySelector(`#zotero-items-tree .row.selected .${CSS.escape(dataKey)}`);
return cell;
};
beforeEach(async function () {
resetCaches();
});
afterEach(function () {
Zotero_Tabs.select("zotero-pane");
Zotero_Tabs.closeAll();
});
it("should render custom column and call dataProvider hook", async function () {
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
let columnKey = await waitForRegister(defaultOption);
await waitForColumnEnable(columnKey);
// Should render custom column cell
let cellElem = getSelectedRowCell(columnKey);
assert.exists(cellElem);
// Should call dataProvider and render the value
assert.equal(`${item.id}`, cellElem.textContent);
await waitForUnregister(columnKey);
});
it("should use custom renderCell hook", async function () {
let customCellContent = "Custom renderCell";
let option = Object.assign({}, defaultOption, {
renderCell: (index, data, column, isFirstColumn, doc) => {
// index: the index of the row
// data: the data to display in the column, return of `dataProvider`
// column: the column options
// isFirstColumn: true if this is the first column
// doc: the document of the item tree
// return: the HTML to display in the cell
const cell = doc.createElement('span');
cell.className = `cell ${column.className}`;
cell.textContent = customCellContent;
cell.style.color = 'red';
updateCache("renderCell", cell.textContent);
return cell;
},
});
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
let columnKey = await waitForRegister(option);
await waitForColumnEnable(columnKey);
// Should render custom column cell
let cellElem = getSelectedRowCell(columnKey);
assert.exists(cellElem);
// Should call renderCell and render the value
assert.equal('rgb(255, 0, 0)', win.getComputedStyle(cellElem).color);
await waitForUnregister(columnKey);
});
it("should not break ui when hooks throw error", async function () {
let option = Object.assign({}, defaultOption, {
dataProvider: () => {
updateCache("dataProvider", "Test error");
throw new Error("Test error");
},
renderCell: () => {
updateCache("renderCell", "Test error");
throw new Error("Test error");
}
});
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
let columnKey = await waitForRegister(option);
await waitForColumnEnable(columnKey);
// Should not break ui
let columnElem = getSelectedRowCell(columnKey);
assert.exists(columnElem);
await waitForUnregister(columnKey);
});
});
describe("Custom menu", function () {
let defaultOption = {
menuID: "default-test",
pluginID: "zotero@zotero.org",
menus: [{
menuType: "menuitem",
l10nID: "menu-print",
onShowing: (event, context) => {
updateCache("onShowing", context);
},
onCommand: (event, context) => {
updateCache("onCommand", context);
}
}]
};
let defaultContextKeys = [
"menuElem",
"setEnabled",
"setIcon",
"setL10nArgs",
"setVisible",
"tabID",
"tabType",
"tabSubType",
];
// From https://searchfox.org/mozilla-esr128/source/browser/base/content/test/menubar/browser_file_close_tabs.js
async function simulateMenuOpen(menu) {
return new Promise((resolve) => {
menu.addEventListener("popupshown", resolve, { once: true });
menu.dispatchEvent(new MouseEvent("popupshowing"));
menu.dispatchEvent(new MouseEvent("popupshown"));
});
}
async function simulateMenuClosed(menu) {
return new Promise((resolve) => {
menu.addEventListener("popuphidden", resolve, { once: true });
menu.dispatchEvent(new MouseEvent("popuphiding"));
menu.dispatchEvent(new MouseEvent("popuphidden"));
});
}
async function simulateMenuItemClick(menuitem) {
return new Promise((resolve) => {
menuitem.addEventListener("command", resolve, { once: true });
menuitem.dispatchEvent(new MouseEvent("click"));
menuitem.dispatchEvent(new MouseEvent("command"));
});
}
async function checkMenuContext(option, popupSelector, contextKeys, targetDoc) {
if (!targetDoc) {
targetDoc = doc;
}
option = Object.assign({}, defaultOption, option);
let target = option.target;
initCache("onShowing");
let menuID = Zotero.MenuManager.registerMenu(option);
assert.isString(menuID);
let popup;
if (typeof popupSelector === "function") {
popup = await popupSelector();
}
else {
popup = targetDoc.querySelector(popupSelector);
await simulateMenuOpen(popup);
}
assert.exists(popup, `Popup for target ${target} should exist`);
let context = await getCache("onShowing");
if (contextKeys) {
for (let key of contextKeys) {
assert.property(context, key, `Context of ${target} should have key: ${key}`);
}
}
if (typeof popupSelector === "function") {
popup.hidePopup();
}
else {
await simulateMenuClosed(popup);
}
assert.isTrue(Zotero.MenuManager.unregisterMenu(menuID));
}
beforeEach(async function () {
resetCaches();
});
afterEach(function () {
Zotero_Tabs.select("zotero-pane");
Zotero_Tabs.closeAll();
});
it("should register custom menu and call hooks", async function () {
let option = Object.assign({}, defaultOption, {
target: "main/menubar/file",
menus: [{
menuType: "menuitem",
l10nID: "menu-print",
onShowing: (event, context) => {
updateCache("onShowing", context);
},
onShown: (event, context) => {
updateCache("onShown", context);
},
onHiding: (event, context) => {
updateCache("onHiding", context);
},
onHidden: (event, context) => {
updateCache("onHidden", context);
},
onCommand: (event, context) => {
updateCache("onCommand", context);
}
}]
});
initCache("onShowing");
initCache("onShown");
initCache("onHiding");
initCache("onHidden");
initCache("onCommand");
let menuID = Zotero.MenuManager.registerMenu(option);
assert.isString(menuID);
let popup = doc.querySelector("#menu_FilePopup");
await simulateMenuOpen(popup);
let context = await getCache("onShowing");
await getCache("onShown");
assert.exists(context.menuElem);
assert.equal(context.menuElem.parentNode, popup);
await simulateMenuItemClick(context.menuElem);
context = await getCache("onCommand");
assert.exists(context);
await simulateMenuClosed(popup);
await Promise.all([
getCache("onHiding"),
getCache("onHidden")
]);
// Unregister the menu
assert.isTrue(Zotero.MenuManager.unregisterMenu(menuID));
// After unregistering and re-opening the popup, the menu should not exist
popup.addEventListener("popupshown", () => {
assert.notExists(popup.querySelector('.zotero-custom-menu-item'));
}, { once: true });
// Since the event is manually dispatched, the above listener will be triggered before the promise resolves
await simulateMenuOpen(popup);
});
it("should hide custom menu when not enabled for tab type", async function () {
let option = Object.assign({}, defaultOption, {
target: "main/menubar/file",
menus: [
{
menuType: "menuitem",
l10nID: "menu-print",
onShowing: (event, context) => {
context.menuElem?.classList.add("test-menu-library");
},
enableForTabTypes: ["library"]
},
{
menuType: "menuitem",
l10nID: "menu-print",
onShowing: (event, context) => {
context.menuElem?.classList.add("test-menu-pdf");
},
enableForTabTypes: ["reader/pdf"]
},
{
menuType: "menuitem",
l10nID: "menu-print",
onShowing: (event, context) => {
context.menuElem?.classList.add("test-menu-epub");
},
enableForTabTypes: ["reader/epub"]
},
{
menuType: "menuitem",
l10nID: "menu-print",
onShowing: (event, context) => {
context.menuElem?.classList.add("test-menu-snapshot");
},
enableForTabTypes: ["reader/snapshot"]
},
{
menuType: "menuitem",
l10nID: "menu-print",
onShowing: (event, context) => {
context.menuElem?.classList.add("test-menu-anyReader");
},
enableForTabTypes: ["reader/*"]
}
]
});
// Create items for different tab types
let pdfItem = await importFileAttachment('test.pdf');
let epubItem = await importFileAttachment('stub.epub');
let snapshotItem = await importFileAttachment('test.html');
let menuID = Zotero.MenuManager.registerMenu(option);
assert.isString(menuID);
let popup = doc.querySelector("#menu_FilePopup");
function assertMenuVisibility(popup, visibilities) {
for (let [className, visible] of Object.entries(visibilities)) {
let menuItem = popup.querySelector(`.test-menu-${className}`);
if (visible) {
assert.isFalse(menuItem.hidden, `Menu item ${className} should be visible`);
}
else {
assert.isTrue(menuItem.hidden, `Menu item ${className} should be hidden`);
}
}
}
// Open the menu in library tab
await simulateMenuOpen(popup);
// Should show library menu item
assertMenuVisibility(popup, {
library: true,
pdf: false,
epub: false,
snapshot: false,
anyReader: false
});
await simulateMenuClosed(popup);
// Switch to PDF tab
await ZoteroPane.viewItems([pdfItem]);
let pdfTabID = Zotero_Tabs.selectedID;
await Zotero.Reader.getByTabID(pdfTabID)._initPromise;
await simulateMenuOpen(popup);
// Should show PDF and anyReader menu items
assertMenuVisibility(popup, {
library: false,
pdf: true,
epub: false,
snapshot: false,
anyReader: true
});
await simulateMenuClosed(popup);
// Switch to EPUB tab
await ZoteroPane.viewItems([epubItem]);
let epubTabID = Zotero_Tabs.selectedID;
await Zotero.Reader.getByTabID(epubTabID)._initPromise;
await simulateMenuOpen(popup);
// Should show EPUB and anyReader menu items
assertMenuVisibility(popup, {
library: false,
pdf: false,
epub: true,
snapshot: false,
anyReader: true
});
await simulateMenuClosed(popup);
// Switch to Snapshot tab
await ZoteroPane.viewItems([snapshotItem]);
let snapshotTabID = Zotero_Tabs.selectedID;
await Zotero.Reader.getByTabID(snapshotTabID)._initPromise;
await simulateMenuOpen(popup);
// Should show Snapshot and anyReader menu items
assertMenuVisibility(popup, {
library: false,
pdf: false,
epub: false,
snapshot: true,
anyReader: true
});
await simulateMenuClosed(popup);
// Unregister the menu
assert.isTrue(Zotero.MenuManager.unregisterMenu(menuID));
});
it("should register recursive submenu", async function () {
let testMenuClass = "test-recursive-submenu";
let option = Object.assign({}, defaultOption, {
target: "main/menubar/file",
menus: [{
menuType: "submenu",
l10nID: "menu-print",
onShowing: (event, context) => {
context.menuElem?.classList.add(testMenuClass);
},
menus: [{
menuType: "menuitem",
l10nID: "menu-print",
onShowing: (event, context) => {
updateCache("onShowing", context);
},
onCommand: (event, context) => {
updateCache("onCommand", context);
}
}]
}]
});
initCache("onCommand");
initCache("onShowing");
let menuID = Zotero.MenuManager.registerMenu(option);
assert.isString(menuID);
let popup = doc.querySelector("#menu_FilePopup");
await simulateMenuOpen(popup);
// The onShowing hook of child menu should not be called yet
assert.isEmpty(caches.onShowing.result, "Child menu onShowing should not be called yet");
let submenu = popup.querySelector(`.${testMenuClass}`);
assert.exists(submenu, "Submenu should be created");
let submenuPopup = submenu.querySelector("menupopup");
await simulateMenuOpen(submenuPopup);
let context = await getCache("onShowing");
assert.exists(context.menuElem, "Submenu onShowing context should have menuElem");
await simulateMenuItemClick(context.menuElem);
context = await getCache("onCommand");
assert.exists(context, "Submenu command context should exist");
assert.isTrue(Zotero.MenuManager.unregisterMenu(menuID));
});
it("should register main window menu", async function () {
let menuTargetMap = {
"main/menubar/file": "#menu_FilePopup",
"main/menubar/edit": "#menu_EditPopup",
"main/menubar/view": "#menu_viewPopup",
"main/menubar/go": "#menu_goPopup",
"main/menubar/tools": "#menu_ToolsPopup",
"main/menubar/help": "#menu_HelpPopup",
};
let contextKeys = [
...defaultContextKeys,
"items",
];
for (let [target, popupSelector] of Object.entries(menuTargetMap)) {
await checkMenuContext({ target }, popupSelector, contextKeys);
}
});
it("should register library menu", async function () {
let menuTargetMap = {
"main/library/item": {
popupSelector: () => {
ZoteroPane.onItemsContextMenuOpen(new MouseEvent("contextmenu"), 0, 0);
return doc.querySelector("#zotero-itemmenu");
},
contextKeys: [
...defaultContextKeys,
"items",
"collectionTreeRow",
]
},
"main/library/collection": {
popupSelector: () => {
ZoteroPane.onCollectionsContextMenuOpen(new MouseEvent("contextmenu"), 0, 0);
return doc.querySelector("#zotero-collectionmenu");
},
contextKeys: [
...defaultContextKeys,
"collectionTreeRow",
]
},
"main/library/addAttachment": {
popupSelector: "#menu_attachmentAdd > menupopup",
contextKeys: [
...defaultContextKeys,
"items",
]
},
"main/library/addNote": {
popupSelector: "#menu_noteAdd > menupopup",
contextKeys: [
...defaultContextKeys,
"items",
]
}
};
for (let [target, { popupSelector, contextKeys }] of Object.entries(menuTargetMap)) {
await checkMenuContext({ target }, popupSelector, contextKeys);
}
});
it("should register other main window menus", async function () {
let menuTargetMap = {
"main/tab": {
popupSelector: async () => {
let tabID = Zotero_Tabs.selectedID;
return Zotero_Tabs._openMenu(0, 0, tabID);
},
contextKeys: [
...defaultContextKeys,
"items"
]
},
"itemPane/info/row": {
popupSelector: async () => {
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
let itemDetails = ZoteroPane.itemPane._itemDetails;
let infoBox = itemDetails.getPane("info");
let row = infoBox.querySelector("#itembox-field-value-title");
let event = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
});
row.dispatchEvent(event);
let popup = infoBox.querySelector("#zotero-field-menu");
return popup;
},
contextKeys: [
...defaultContextKeys,
"items",
"editable",
"fieldName",
"targetElem"
]
},
"sidenav/locate": {
popupSelector: async () => {
let item = new Zotero.Item('book');
await item.saveTx();
await ZoteroPane.selectItem(item.id);
let sidenav = ZoteroPane.itemPane._sidenav;
let button = sidenav.querySelector("toolbarbutton[data-action='locate']");
let event = new MouseEvent("mousedown", {
bubbles: true,
cancelable: true,
});
button.dispatchEvent(event);
let popup = button.querySelector("menupopup");
return popup;
},
contextKeys: [
...defaultContextKeys,
"items",
]
}
};
for (let [target, { popupSelector, contextKeys }] of Object.entries(menuTargetMap)) {
await checkMenuContext({ target }, popupSelector, contextKeys);
}
});
it("should register note context menu", async function () {
let menuTargetMap = {
"notesPane/addItemNote": async () => {
let button = notesList.querySelector(".item-notes .section-custom-button.add");
let event = new MouseEvent("command", {
bubbles: true,
cancelable: true,
});
button.dispatchEvent(event);
let popup = notesContext.querySelector(".context-pane-add-child-note-button-popup");
return popup;
},
"notesPane/addStandaloneNote": async () => {
let button = notesList.querySelector(".all-notes .section-custom-button.add");
let event = new MouseEvent("command", {
bubbles: true,
cancelable: true,
});
button.dispatchEvent(event);
let popup = notesContext.querySelector(".context-pane-add-standalone-note-button-popup");
return popup;
}
};
let item = new Zotero.Item('book');
await item.saveTx();
let pdfItem = await importFileAttachment('test.pdf', {
parentItemID: item.id
});
await ZoteroPane.viewItems([pdfItem]);
await Zotero.Reader.getByTabID(Zotero_Tabs.selectedID)._initPromise;
ZoteroContextPane.collapsed = false;
ZoteroContextPane.sidenav._contextNotesPaneVisible = true;
let contextPane = ZoteroContextPane.context;
let notesContext = contextPane._getCurrentNotesContext();
let notesList = notesContext.notesList;
for (let [target, popupSelector] of Object.entries(menuTargetMap)) {
await checkMenuContext({ target }, popupSelector);
}
});
it("should register reader window menu", async function () {
let mainMenubarTargetMap = {
"reader/menubar/file": "#menu_FilePopup",
"reader/menubar/edit": "#menu_EditPopup",
"reader/menubar/view": "#menu_viewPopup",
"reader/menubar/go": "#menu_goPopup",
"reader/menubar/window": "#windowPopup",
};
let pdfAttachment = await importFileAttachment('test.pdf');
let reader = await Zotero.Reader.open(pdfAttachment.id, undefined, {
openInWindow: true,
});
await reader._initPromise;
let readerWin = reader._window;
let readerDoc = readerWin.document;
for (let [target, popupSelector] of Object.entries(mainMenubarTargetMap)) {
await checkMenuContext({ target }, popupSelector, undefined, readerDoc);
}
reader.close();
});
it("should group custom menus", async function () {
let option = Object.assign({}, defaultOption, {
target: "main/library/item",
menus: Array.from({length: 100}, () => ({
menuType: "menuitem",
l10nID: "menu-print",
})),
});
initCache("onShowing");
initCache("onCommand");
let menuID = Zotero.MenuManager.registerMenu(option);
assert.isString(menuID);
let popup = doc.querySelector("#zotero-itemmenu");
let promise = new Promise((resolve) => {
popup.addEventListener("popupshown", resolve, { once: true });
});
ZoteroPane.onItemsContextMenuOpen(new MouseEvent("contextmenu"), 0, 0);
await promise;
let groupedMenus = popup.querySelector(".zotero-custom-menu-group-submenu");
assert.exists(groupedMenus, "Grouped menus should be created");
assert.isTrue(Zotero.MenuManager.unregisterMenu(menuID));
});
});
});