zotero/test/tests/pluginAPITest.js
windingwind 7296583f2a
Fix item pane custom info row render after item change (#4875)
Add test for info row render after item change
fix: #4874
2024-11-29 16:18:31 -05:00

572 lines
16 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);
});
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="${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="${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.${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);
});
});
});