647e45c115
https://forums.zotero.org/discussion/comment/442114/#Comment_442114 is being fixed on the server, but test it here.
430 lines
12 KiB
JavaScript
430 lines
12 KiB
JavaScript
describe("Retractions", function () {
|
|
var userLibraryID;
|
|
var win;
|
|
var zp;
|
|
var checkQueueItemsStub;
|
|
var retractedDOI = '10.1056/NEJMoa1200303'; // mixed case
|
|
|
|
before(async function () {
|
|
userLibraryID = Zotero.Libraries.userLibraryID;
|
|
win = await loadZoteroPane();
|
|
zp = win.ZoteroPane;
|
|
|
|
await Zotero.Retractions.updateFromServer();
|
|
|
|
// Remove debouncing on checkQueuedItems()
|
|
checkQueueItemsStub = sinon.stub(Zotero.Retractions, 'checkQueuedItems').callsFake(() => {
|
|
return Zotero.Retractions._checkQueuedItemsInternal();
|
|
});
|
|
});
|
|
|
|
beforeEach(async function () {
|
|
var ids = await Zotero.DB.columnQueryAsync("SELECT itemID FROM retractedItems");
|
|
if (ids.length) {
|
|
await Zotero.Items.erase(ids);
|
|
}
|
|
});
|
|
|
|
afterEach(async function () {
|
|
win.document.getElementById('retracted-items-close').click();
|
|
checkQueueItemsStub.resetHistory();
|
|
});
|
|
|
|
after(async function () {
|
|
win.close();
|
|
checkQueueItemsStub.restore();
|
|
|
|
var ids = await Zotero.DB.columnQueryAsync("SELECT itemID FROM retractedItems");
|
|
if (ids.length) {
|
|
await Zotero.Items.erase(ids);
|
|
}
|
|
});
|
|
|
|
async function createRetractedItem(options = {}) {
|
|
var o = {
|
|
itemType: 'journalArticle'
|
|
};
|
|
Object.assign(o, options);
|
|
var item = createUnsavedDataObject('item', o);
|
|
item.setField('DOI', retractedDOI);
|
|
if (Zotero.DB.inTransaction()) {
|
|
await item.save();
|
|
}
|
|
else {
|
|
await item.saveTx();
|
|
}
|
|
|
|
while (!checkQueueItemsStub.called) {
|
|
await Zotero.Promise.delay(50);
|
|
}
|
|
await checkQueueItemsStub.returnValues[0];
|
|
checkQueueItemsStub.resetHistory();
|
|
|
|
return item;
|
|
}
|
|
|
|
async function createRetractedItemWithExtraDOI(options = {}) {
|
|
var o = {
|
|
itemType: 'journalArticle'
|
|
};
|
|
Object.assign(o, options);
|
|
var item = createUnsavedDataObject('item', o);
|
|
item.setField('extra', 'DOI: ' + retractedDOI);
|
|
if (Zotero.DB.inTransaction()) {
|
|
await item.save();
|
|
}
|
|
else {
|
|
await item.saveTx();
|
|
}
|
|
|
|
while (!checkQueueItemsStub.called) {
|
|
await Zotero.Promise.delay(50);
|
|
}
|
|
await checkQueueItemsStub.returnValues[0];
|
|
checkQueueItemsStub.resetHistory();
|
|
|
|
return item;
|
|
}
|
|
|
|
function bannerShown() {
|
|
var container = win.document.getElementById('retracted-items-container');
|
|
if (container.getAttribute('collapsed') == 'true') {
|
|
return false;
|
|
}
|
|
if (!container.hasAttribute('collapsed')) {
|
|
return true;
|
|
}
|
|
throw new Error("'collapsed' attribute not found");
|
|
}
|
|
|
|
|
|
describe("#updateFromServer()", function () {
|
|
var server;
|
|
var baseURL;
|
|
|
|
before(function () {
|
|
Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
|
|
baseURL = ZOTERO_CONFIG.API_URL + 'retractions/';
|
|
});
|
|
|
|
beforeEach(function () {
|
|
server = sinon.fakeServer.create();
|
|
server.autoRespond = true;
|
|
});
|
|
|
|
after(async function () {
|
|
Zotero.HTTP.mock = null;
|
|
// Restore the real list from the server. We could just mock it as part of the suite.
|
|
await Zotero.Retractions.updateFromServer();
|
|
});
|
|
|
|
/*it("shouldn't show banner or virtual collection for already flagged items on list update", async function () {
|
|
await Zotero.Retractions.updateFromServer();
|
|
});*/
|
|
|
|
it("should remove retraction flag from items that no longer match prefix list", async function () {
|
|
var doi = '10.1234/abcde';
|
|
var hash = Zotero.Utilities.Internal.sha1(doi);
|
|
var prefix = hash.substr(0, 5);
|
|
var lines = [
|
|
Zotero.Retractions.TYPE_DOI + prefix + ' 12345\n',
|
|
Zotero.Retractions.TYPE_DOI + 'aaaaa 23456\n'
|
|
];
|
|
|
|
var listCount = 0;
|
|
var searchCount = 0;
|
|
server.respond(function (req) {
|
|
if (req.method == 'GET' && req.url == baseURL + 'list') {
|
|
listCount++;
|
|
if (listCount == 1) {
|
|
req.respond(
|
|
200,
|
|
{
|
|
'Content-Type': 'text/plain',
|
|
'ETag': 'abcdefg'
|
|
},
|
|
lines.join('')
|
|
);
|
|
}
|
|
else if (listCount == 2) {
|
|
req.respond(
|
|
200,
|
|
{
|
|
'Content-Type': 'text/plain',
|
|
'ETag': 'bcdefgh'
|
|
},
|
|
lines[1]
|
|
);
|
|
}
|
|
}
|
|
else if (req.method == 'POST' && req.url == baseURL + 'search') {
|
|
searchCount++;
|
|
if (searchCount == 1) {
|
|
req.respond(
|
|
200,
|
|
{
|
|
'Content-Type': 'application/json'
|
|
},
|
|
JSON.stringify([
|
|
{
|
|
doi: hash,
|
|
retractionDOI: '10.1234/bcdef',
|
|
date: '2019-01-02',
|
|
reasons: [
|
|
"Error in Data"
|
|
],
|
|
urls: []
|
|
}
|
|
])
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
await Zotero.Retractions.updateFromServer();
|
|
|
|
// Create item with DOI from list
|
|
var promise = waitForItemEvent('refresh');
|
|
var item = createUnsavedDataObject('item', { itemType: 'journalArticle' });
|
|
item.setField('DOI', doi);
|
|
await item.saveTx();
|
|
await promise;
|
|
|
|
assert.isTrue(Zotero.Retractions.isRetracted(item));
|
|
|
|
// Make a second request, with the entry removed
|
|
promise = waitForItemEvent('refresh');
|
|
await Zotero.Retractions.updateFromServer();
|
|
await promise;
|
|
|
|
assert.isFalse(Zotero.Retractions.isRetracted(item));
|
|
});
|
|
});
|
|
|
|
|
|
describe("#shouldShowCitationWarning()", function () {
|
|
it("should return false if citation warning is hidden", async function () {
|
|
var item = await createRetractedItem();
|
|
assert.isTrue(Zotero.Retractions.shouldShowCitationWarning(item));
|
|
await Zotero.Retractions.disableCitationWarningsForItem(item);
|
|
assert.isFalse(Zotero.Retractions.shouldShowCitationWarning(item));
|
|
});
|
|
|
|
it("should return false if retraction is hidden", async function () {
|
|
var item = await createRetractedItem();
|
|
assert.isTrue(Zotero.Retractions.shouldShowCitationWarning(item));
|
|
await Zotero.Retractions.hideRetraction(item);
|
|
assert.isFalse(Zotero.Retractions.shouldShowCitationWarning(item));
|
|
});
|
|
});
|
|
|
|
|
|
describe("#getRetractionsFromJSON()", function () {
|
|
it("should identify object with retracted DOI", async function () {
|
|
var spy = sinon.spy(Zotero.HTTP, 'request');
|
|
var json = [
|
|
{
|
|
|
|
},
|
|
{
|
|
DOI: retractedDOI
|
|
},
|
|
{
|
|
DOI: '10.1234/abcd'
|
|
}
|
|
];
|
|
|
|
var indexes = await Zotero.Retractions.getRetractionsFromJSON(json);
|
|
assert.sameMembers(indexes, [1]);
|
|
assert.equal(spy.callCount, 1);
|
|
|
|
indexes = await Zotero.Retractions.getRetractionsFromJSON(json);
|
|
assert.sameMembers(indexes, [1]);
|
|
// Result should've been cached, so we should have it without another API request
|
|
assert.equal(spy.callCount, 1);
|
|
|
|
spy.restore();
|
|
});
|
|
|
|
|
|
it("should identify object with retracted DOI in Extra", async function () {
|
|
var json = [
|
|
{
|
|
extra: `DOI: ${retractedDOI}`
|
|
}
|
|
];
|
|
|
|
var indexes = await Zotero.Retractions.getRetractionsFromJSON(json);
|
|
assert.sameMembers(indexes, [0]);
|
|
});
|
|
|
|
it("should identify object with retracted DOI on subsequent line in Extra", async function () {
|
|
var json = [
|
|
{
|
|
extra: `Foo: Bar\nDOI: ${retractedDOI}`
|
|
}
|
|
];
|
|
|
|
var indexes = await Zotero.Retractions.getRetractionsFromJSON(json);
|
|
assert.sameMembers(indexes, [0]);
|
|
});
|
|
});
|
|
|
|
|
|
describe("Notification Banner", function () {
|
|
it("should show banner when retracted item is added", async function () {
|
|
assert.isFalse(bannerShown());
|
|
await createRetractedItem();
|
|
do {
|
|
await delay(10);
|
|
}
|
|
while (!bannerShown());
|
|
});
|
|
|
|
it("should show banner when retracted item with DOI in Extra is added", async function () {
|
|
assert.isFalse(bannerShown());
|
|
await createRetractedItemWithExtraDOI();
|
|
do {
|
|
await delay(10);
|
|
}
|
|
while (!bannerShown());
|
|
});
|
|
|
|
it("shouldn't show banner when item in trash is added", async function () {
|
|
await createRetractedItem({ deleted: true });
|
|
await delay(50);
|
|
assert.isFalse(bannerShown());
|
|
});
|
|
});
|
|
|
|
describe("virtual collection", function () {
|
|
it("should show/hide Retracted Items collection when a retracted item is found/erased", async function () {
|
|
// Create item
|
|
var item = await createRetractedItem();
|
|
assert.ok(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
|
|
|
|
// Erase item
|
|
var promise = waitForItemEvent('refresh');
|
|
await item.eraseTx();
|
|
await promise;
|
|
assert.isFalse(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
|
|
});
|
|
|
|
it("should unhide Retracted Items collection when retracted item is found", async function () {
|
|
await createRetractedItem();
|
|
|
|
// Hide collection
|
|
await zp.setVirtual(userLibraryID, 'retracted', false);
|
|
|
|
// Add another retracted item, which should unhide it
|
|
await createRetractedItem();
|
|
assert.ok(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
|
|
});
|
|
|
|
it("should hide Retracted Items collection when last retracted item is moved to trash", async function () {
|
|
var rowID = "R" + userLibraryID;
|
|
|
|
// Create item
|
|
var item = await createRetractedItem();
|
|
assert.ok(zp.collectionsView.getRowIndexByID(rowID));
|
|
|
|
// Select Retracted Items collection
|
|
await zp.collectionsView.selectByID(rowID);
|
|
await waitForItemsLoad(win);
|
|
|
|
// Erase item
|
|
item.deleted = true;
|
|
await item.saveTx();
|
|
await Zotero.Promise.delay(50);
|
|
// Retracted Items should be gone
|
|
assert.isFalse(zp.collectionsView.getRowIndexByID(rowID));
|
|
// And My Library should be selected
|
|
assert.equal(zp.collectionsView.selectedTreeRow.id, "L" + userLibraryID);
|
|
});
|
|
|
|
it("should hide Retracted Items collection when last retracted item is marked as hidden", async function () {
|
|
var rowID = "R" + userLibraryID;
|
|
|
|
// Create item
|
|
var item = await createRetractedItem();
|
|
assert.ok(zp.collectionsView.getRowIndexByID(rowID));
|
|
|
|
// Select Retracted Items collection
|
|
await zp.collectionsView.selectByID(rowID);
|
|
await waitForItemsLoad(win);
|
|
|
|
await Zotero.Retractions.hideRetraction(item);
|
|
|
|
await Zotero.Promise.delay(50);
|
|
// Retracted Items should be gone
|
|
assert.isFalse(zp.collectionsView.getRowIndexByID(rowID));
|
|
// And My Library should be selected
|
|
assert.equal(zp.collectionsView.selectedTreeRow.id, "L" + userLibraryID);
|
|
});
|
|
|
|
it("shouldn't hide Retracted Items collection when last retracted item is marked to not show a citation warning", async function () {
|
|
var rowID = "R" + userLibraryID;
|
|
|
|
// Create item
|
|
var item = await createRetractedItem();
|
|
assert.ok(zp.collectionsView.getRowIndexByID(rowID));
|
|
|
|
// Select Retracted Items collection
|
|
await zp.collectionsView.selectByID(rowID);
|
|
await waitForItemsLoad(win);
|
|
|
|
await Zotero.Retractions.disableCitationWarningsForItem(item);
|
|
|
|
await Zotero.Promise.delay(50);
|
|
// Should still be showing
|
|
assert.ok(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
|
|
});
|
|
|
|
it("should show Retracted Items collection when retracted item is restored from trash", async function () {
|
|
// Create trashed item
|
|
var item = await createRetractedItem({ deleted: true });
|
|
await Zotero.Promise.delay(50);
|
|
assert.isFalse(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
|
|
|
|
// Restore item
|
|
item.deleted = false;
|
|
await item.saveTx();
|
|
await Zotero.Promise.delay(50);
|
|
assert.ok(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
|
|
});
|
|
});
|
|
|
|
describe("retractions.enabled", function () {
|
|
beforeEach(function () {
|
|
Zotero.Prefs.clear('retractions.enabled');
|
|
});
|
|
|
|
it("should hide virtual collection and banner when false", async function () {
|
|
var item = await createRetractedItem();
|
|
await Zotero.Promise.delay(50);
|
|
var itemRetractionBox = win.document.getElementById('retraction-box');
|
|
assert.isFalse(itemRetractionBox.hidden);
|
|
|
|
var spies = [
|
|
sinon.spy(Zotero.Retractions, '_removeAllEntries'),
|
|
sinon.spy(Zotero.Retractions, 'isRetracted')
|
|
];
|
|
Zotero.Prefs.set('retractions.enabled', false);
|
|
|
|
while (!spies[0].called || !spies[1].called) {
|
|
await Zotero.Promise.delay(50);
|
|
}
|
|
await spies[0].returnValues[0];
|
|
await spies[1].returnValues[0];
|
|
spies.forEach(spy => spy.restore());
|
|
|
|
assert.isFalse(Zotero.Retractions.isRetracted(item));
|
|
assert.isFalse(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
|
|
assert.isFalse(bannerShown());
|
|
|
|
assert.isTrue(itemRetractionBox.hidden);
|
|
|
|
await item.eraseTx();
|
|
});
|
|
});
|
|
});
|