572 lines
16 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|