zotero/test/tests/pluginAPITest.js
windingwind aec6e61cb3 Add ItemPaneManager.registerInfoRow API
Unify plugin API classes

Add info box custom row API tests

Refactor itemBox.js create element

Wrap hooks in API for safe call

Add test for item tree api and hook error handling

Remove try/catch from #4816

Move plugin API definitions to xpcom/pluginAPI
2024-11-11 11:27:47 -05:00

515 lines
14 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="${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="${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="${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;
await Zotero.Reader.getByTabID(tabID)._waitForReader();
// Ensure context pane is open
ZoteroContextPane.splitter.setAttribute("state", "open");
result = await itemChangePromise;
let itemDetails = ZoteroContextPane.context._getItemContext(tabID);
rowElem = itemDetails.getPane("info").querySelector(`[data-custom-row-id="${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="${rowID}"]`);
assert.notExists(rowElem.previousElementSibling);
await waitForUnregister(rowID);
// Row after creator rows
rowID = await waitForRegister(afterCreatorsOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${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="${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="${rowID}"]`);
let valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.readOnly);
await waitForUnregister(rowID);
rowID = await waitForRegister(editableOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.readOnly);
await waitForUnregister(rowID);
rowID = await waitForRegister(notEditableOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${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="${rowID}"]`);
let valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.multiline);
await waitForUnregister(rowID);
rowID = await waitForRegister(multilineOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
valueElem = rowElem.querySelector(".value");
assert.isTrue(valueElem.multiline);
await waitForUnregister(rowID);
rowID = await waitForRegister(notMultilineOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${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="${rowID}"]`);
let valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.noWrap);
await waitForUnregister(rowID);
rowID = await waitForRegister(noWrapOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
valueElem = rowElem.querySelector(".value");
assert.isTrue(valueElem.noWrap);
await waitForUnregister(rowID);
rowID = await waitForRegister(wrapOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.noWrap);
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.${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 .${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);
});
});
});