cbbff600a6
- Just a single huge commit. This has been developed over too long a
time, required many tiny changes across too many files and has seen too
many iterations to be separated into separate commits.
The original branch with all the messy commits will be kept around for
posterity
bb220ad0f2
...adomasven:feature/react-item-tree
- Replaces XUL <tree> element across the whole zotero client codebase
with a custom supermegafast virtualized-table inspired by
react-virtualized yet mimicking old XUL treeview API. The
virtualized-table sits on top on a raw-to-the-metal,
interpreted-at-runtime JS based windowing solution inspired by
react-window. React-based solutions could not be used because they were
slow and Zotero UI needs to be responsive and be able to
display thousands of rows in a treeview without any slowdowns.
- Attempts were made at making this screen-reader friendly, but yet to
be tested with something like JAWS
- RTL-friendly
- Styling and behaviour across all platforms was copied as closely as
possible to the original XUL tree
- Instead of row-based scroll snapping this has smooth-scrolling. If
you're using arrow keys to browse through the tree then it effectively
snap-scrolls. Current CSS snap scroll attributes do not seem to work in
the way we would require even on up-to-date browsers, yet alone the ESR
version of FX that Zotero is on. JS solutions are either terrible for
performance or produce inexcusable jitter.
- When dragging-and-dropping items the initial drag freezes the UI for
a fairly jarring amount of time. Does not seem to be fixable due to
the synchronous code that needs to be run in the dragstart handler.
Used to be possible to run that code async with the XUL tree.
- Item tree column picker no longer has a dedicated button. Just
right-click the columns. The column preferences (width, order, etc) are
no longer handled by XUL, which required a custom serialization and
storage solution that throws warnings in the developer console due to
the amount of data being stored. Might cause temporary freezing on HDDs
upon column resize/reorder/visibility toggling.
- Context menu handling code basically unchanged, but any UI changes
that plugins may have wanted to do (including adding new columns) will
have to be redone by them. No serious thought has gone into how plugin
developers would achieve that yet.
- Opens up the possibility for awesome alternative ways to render the
tree items, including things like multiple-row view for the item tree,
which has been requested for a long while especially by users switching
from other referencing software
2652 lines
72 KiB
JavaScript
2652 lines
72 KiB
JavaScript
"use strict";
|
|
|
|
describe("Connector Server", function () {
|
|
Components.utils.import("resource://zotero-unit/httpd.js");
|
|
var win, connectorServerPath, testServerPath, httpd;
|
|
var testServerPort = 16213;
|
|
|
|
before(function* () {
|
|
this.timeout(20000);
|
|
Zotero.Prefs.set("httpServer.enabled", true);
|
|
yield resetDB({
|
|
thisArg: this,
|
|
skipBundledFiles: true
|
|
});
|
|
yield Zotero.Translators.init();
|
|
|
|
win = yield loadZoteroPane();
|
|
connectorServerPath = 'http://127.0.0.1:' + Zotero.Prefs.get('httpServer.port');
|
|
});
|
|
|
|
beforeEach(function () {
|
|
// Alternate ports to prevent exceptions not catchable in JS
|
|
testServerPort += (testServerPort & 1) ? 1 : -1;
|
|
testServerPath = 'http://127.0.0.1:' + testServerPort;
|
|
httpd = new HttpServer();
|
|
httpd.start(testServerPort);
|
|
});
|
|
|
|
afterEach(function* () {
|
|
var defer = new Zotero.Promise.defer();
|
|
httpd.stop(() => defer.resolve());
|
|
yield defer.promise;
|
|
});
|
|
|
|
after(function () {
|
|
win.close();
|
|
});
|
|
|
|
|
|
describe('/connector/getTranslatorCode', function() {
|
|
it('should respond with translator code', function* () {
|
|
var code = 'function detectWeb() {}\nfunction doImport() {}';
|
|
var translator = buildDummyTranslator(4, code);
|
|
sinon.stub(Zotero.Translators, 'get').returns(translator);
|
|
|
|
var response = yield Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/getTranslatorCode",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
translatorID: "dummy-translator",
|
|
})
|
|
}
|
|
);
|
|
|
|
assert.isTrue(Zotero.Translators.get.calledWith('dummy-translator'));
|
|
let translatorCode = yield Zotero.Translators.getCodeForTranslator(translator);
|
|
assert.equal(response.response, translatorCode);
|
|
|
|
Zotero.Translators.get.restore();
|
|
})
|
|
});
|
|
|
|
|
|
describe("/connector/detect", function() {
|
|
it("should return relevant translators with proxies", function* () {
|
|
var code = 'function detectWeb() {return "newspaperArticle";}\nfunction doWeb() {}';
|
|
var translator = buildDummyTranslator("web", code, {target: "https://www.example.com/.*"});
|
|
sinon.stub(Zotero.Translators, 'getAllForType').resolves([translator]);
|
|
|
|
var response = yield Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/detect",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
uri: "https://www-example-com.proxy.example.com/article",
|
|
html: "<head><title>Owl</title></head><body><p>🦉</p></body>"
|
|
})
|
|
}
|
|
);
|
|
|
|
assert.equal(JSON.parse(response.response)[0].proxy.scheme, 'https://%h.proxy.example.com/%p');
|
|
|
|
Zotero.Translators.getAllForType.restore();
|
|
});
|
|
});
|
|
|
|
|
|
describe("/connector/saveItems", function () {
|
|
// TODO: Test cookies
|
|
it("should save a translated item to the current selected collection", function* () {
|
|
var collection = yield createDataObject('collection');
|
|
yield waitForItemsLoad(win);
|
|
|
|
var body = {
|
|
items: [
|
|
{
|
|
itemType: "newspaperArticle",
|
|
title: "Title",
|
|
creators: [
|
|
{
|
|
firstName: "First",
|
|
lastName: "Last",
|
|
creatorType: "author"
|
|
}
|
|
],
|
|
attachments: [
|
|
{
|
|
title: "Attachment",
|
|
url: `${testServerPath}/attachment`,
|
|
mimeType: "text/html"
|
|
}
|
|
]
|
|
}
|
|
],
|
|
uri: "http://example.com"
|
|
};
|
|
|
|
httpd.registerPathHandler(
|
|
"/attachment",
|
|
{
|
|
handle: function (request, response) {
|
|
response.setStatusLine(null, 200, "OK");
|
|
response.write("<html><head><title>Title</title><body>Body</body></html>");
|
|
}
|
|
}
|
|
);
|
|
|
|
var promise = waitForItemEvent('add');
|
|
var reqPromise = Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveItems",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(body)
|
|
}
|
|
);
|
|
|
|
// Check parent item
|
|
var ids = yield promise;
|
|
assert.lengthOf(ids, 1);
|
|
var item = Zotero.Items.get(ids[0]);
|
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'newspaperArticle');
|
|
assert.isTrue(collection.hasItem(item.id));
|
|
|
|
// Check attachment
|
|
promise = waitForItemEvent('add');
|
|
ids = yield promise;
|
|
assert.lengthOf(ids, 1);
|
|
item = Zotero.Items.get(ids[0]);
|
|
assert.isTrue(item.isImportedAttachment());
|
|
|
|
var req = yield reqPromise;
|
|
assert.equal(req.status, 201);
|
|
});
|
|
|
|
|
|
it("should switch to My Library if read-only library is selected", function* () {
|
|
var group = yield createGroup({
|
|
editable: false
|
|
});
|
|
yield selectLibrary(win, group.libraryID);
|
|
yield waitForItemsLoad(win);
|
|
|
|
var body = {
|
|
items: [
|
|
{
|
|
itemType: "newspaperArticle",
|
|
title: "Title",
|
|
creators: [
|
|
{
|
|
firstName: "First",
|
|
lastName: "Last",
|
|
creatorType: "author"
|
|
}
|
|
],
|
|
attachments: []
|
|
}
|
|
],
|
|
uri: "http://example.com"
|
|
};
|
|
|
|
var promise = waitForItemEvent('add');
|
|
var reqPromise = Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveItems",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(body),
|
|
successCodes: false
|
|
}
|
|
);
|
|
|
|
// My Library be selected, and the item should be in it
|
|
var ids = yield promise;
|
|
assert.equal(
|
|
win.ZoteroPane.collectionsView.getSelectedLibraryID(),
|
|
Zotero.Libraries.userLibraryID
|
|
);
|
|
assert.lengthOf(ids, 1);
|
|
var item = Zotero.Items.get(ids[0]);
|
|
assert.equal(item.libraryID, Zotero.Libraries.userLibraryID);
|
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'newspaperArticle');
|
|
|
|
var req = yield reqPromise;
|
|
assert.equal(req.status, 201);
|
|
});
|
|
|
|
it("should use the provided proxy to deproxify item url", function* () {
|
|
yield selectLibrary(win, Zotero.Libraries.userLibraryID);
|
|
yield waitForItemsLoad(win);
|
|
|
|
var body = {
|
|
items: [
|
|
{
|
|
itemType: "newspaperArticle",
|
|
title: "Title",
|
|
creators: [
|
|
{
|
|
firstName: "First",
|
|
lastName: "Last",
|
|
creatorType: "author"
|
|
}
|
|
],
|
|
attachments: [],
|
|
url: "https://www-example-com.proxy.example.com/path"
|
|
}
|
|
],
|
|
uri: "https://www-example-com.proxy.example.com/path",
|
|
proxy: {scheme: 'https://%h.proxy.example.com/%p', dotsToHyphens: true}
|
|
};
|
|
|
|
var promise = waitForItemEvent('add');
|
|
var req = yield Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveItems",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(body)
|
|
}
|
|
);
|
|
|
|
// Check item
|
|
var ids = yield promise;
|
|
assert.lengthOf(ids, 1);
|
|
var item = Zotero.Items.get(ids[0]);
|
|
assert.equal(item.getField('url'), 'https://www.example.com/path');
|
|
});
|
|
|
|
it("shouldn't return an attachment that isn't being saved", async function () {
|
|
Zotero.Prefs.set('automaticSnapshots', false);
|
|
|
|
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
|
await waitForItemsLoad(win);
|
|
|
|
var body = {
|
|
items: [
|
|
{
|
|
itemType: "webpage",
|
|
title: "Title",
|
|
creators: [],
|
|
attachments: [
|
|
{
|
|
url: "http://example.com/",
|
|
mimeType: "text/html"
|
|
}
|
|
],
|
|
url: "http://example.com/"
|
|
}
|
|
],
|
|
uri: "http://example.com/"
|
|
};
|
|
|
|
var req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveItems",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(body),
|
|
responseType: 'json'
|
|
}
|
|
);
|
|
|
|
Zotero.Prefs.clear('automaticSnapshots');
|
|
|
|
assert.equal(req.status, 201);
|
|
assert.lengthOf(req.response.items, 1);
|
|
assert.lengthOf(req.response.items[0].attachments, 0);
|
|
});
|
|
|
|
describe("PDF retrieval", function () {
|
|
var oaDOI = '10.1111/abcd';
|
|
var nonOADOI = '10.2222/bcde';
|
|
var pdfURL;
|
|
var badPDFURL;
|
|
var stub;
|
|
|
|
before(function () {
|
|
var origFunc = Zotero.HTTP.request.bind(Zotero.HTTP);
|
|
stub = sinon.stub(Zotero.HTTP, 'request');
|
|
stub.callsFake(function (method, url, options) {
|
|
// OA PDF lookup
|
|
if (url.startsWith(ZOTERO_CONFIG.SERVICES_URL)) {
|
|
let json = JSON.parse(options.body);
|
|
let response = [];
|
|
if (json.doi == oaDOI) {
|
|
response.push({
|
|
url: pdfURL,
|
|
version: 'submittedVersion'
|
|
});
|
|
}
|
|
return {
|
|
status: 200,
|
|
response
|
|
};
|
|
}
|
|
|
|
return origFunc(...arguments);
|
|
});
|
|
});
|
|
|
|
beforeEach(() => {
|
|
pdfURL = testServerPath + '/pdf';
|
|
badPDFURL = testServerPath + '/badpdf';
|
|
|
|
httpd.registerFile(
|
|
pdfURL.substr(testServerPath.length),
|
|
Zotero.File.pathToFile(OS.Path.join(getTestDataDirectory().path, 'test.pdf'))
|
|
);
|
|
// PDF URL that's actually an HTML page
|
|
httpd.registerFile(
|
|
badPDFURL.substr(testServerPath.length),
|
|
Zotero.File.pathToFile(OS.Path.join(getTestDataDirectory().path, 'test.html'))
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
stub.resetHistory();
|
|
});
|
|
|
|
after(() => {
|
|
stub.restore();
|
|
});
|
|
|
|
|
|
it("should download a translated PDF", async function () {
|
|
var collection = await createDataObject('collection');
|
|
await waitForItemsLoad(win);
|
|
|
|
var sessionID = Zotero.Utilities.randomString();
|
|
|
|
// Save item
|
|
var itemAddPromise = waitForItemEvent('add');
|
|
var saveItemsReq = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveItems",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
items: [
|
|
{
|
|
itemType: 'journalArticle',
|
|
title: 'Title',
|
|
DOI: nonOADOI,
|
|
attachments: [
|
|
{
|
|
title: "PDF",
|
|
url: pdfURL,
|
|
mimeType: 'application/pdf'
|
|
}
|
|
]
|
|
}
|
|
],
|
|
uri: 'http://website/article'
|
|
}),
|
|
responseType: 'json'
|
|
}
|
|
);
|
|
assert.equal(saveItemsReq.status, 201);
|
|
assert.lengthOf(saveItemsReq.response.items, 1);
|
|
// Translated attachment should show up in the initial response
|
|
assert.lengthOf(saveItemsReq.response.items[0].attachments, 1);
|
|
assert.notProperty(saveItemsReq.response.items[0], 'DOI');
|
|
assert.notProperty(saveItemsReq.response.items[0].attachments[0], 'progress');
|
|
|
|
// Check parent item
|
|
var ids = await itemAddPromise;
|
|
assert.lengthOf(ids, 1);
|
|
var item = Zotero.Items.get(ids[0]);
|
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'journalArticle');
|
|
assert.isTrue(collection.hasItem(item.id));
|
|
|
|
// Legacy endpoint should show 0
|
|
let attachmentProgressReq = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/attachmentProgress",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify([saveItemsReq.response.items[0].attachments[0].id]),
|
|
responseType: 'json'
|
|
}
|
|
);
|
|
assert.equal(attachmentProgressReq.status, 200);
|
|
let progress = attachmentProgressReq.response;
|
|
assert.sameOrderedMembers(progress, [0]);
|
|
|
|
// Wait for the attachment to finish saving
|
|
itemAddPromise = waitForItemEvent('add');
|
|
var i = 0;
|
|
while (i < 3) {
|
|
let sessionProgressReq = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/sessionProgress",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({ sessionID }),
|
|
responseType: 'json'
|
|
}
|
|
);
|
|
assert.equal(sessionProgressReq.status, 200);
|
|
let response = sessionProgressReq.response;
|
|
assert.lengthOf(response.items, 1);
|
|
let item = response.items[0];
|
|
if (item.attachments.length) {
|
|
let attachments = item.attachments;
|
|
assert.lengthOf(attachments, 1);
|
|
let attachment = attachments[0];
|
|
switch (i) {
|
|
// Translated PDF in progress
|
|
case 0:
|
|
if (attachment.title == "PDF"
|
|
&& Number.isInteger(attachment.progress)
|
|
&& attachment.progress < 100) {
|
|
assert.isFalse(response.done);
|
|
i++;
|
|
}
|
|
continue;
|
|
|
|
// Translated PDF finished
|
|
case 1:
|
|
if (attachment.title == "PDF" && attachment.progress == 100) {
|
|
i++;
|
|
}
|
|
continue;
|
|
|
|
// done: true
|
|
case 2:
|
|
if (response.done) {
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
await Zotero.Promise.delay(10);
|
|
}
|
|
|
|
// Legacy endpoint should show 100
|
|
attachmentProgressReq = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/attachmentProgress",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify([saveItemsReq.response.items[0].attachments[0].id]),
|
|
responseType: 'json'
|
|
}
|
|
);
|
|
assert.equal(attachmentProgressReq.status, 200);
|
|
progress = attachmentProgressReq.response;
|
|
assert.sameOrderedMembers(progress, [100]);
|
|
|
|
// Check attachment
|
|
var ids = await itemAddPromise;
|
|
assert.lengthOf(ids, 1);
|
|
item = Zotero.Items.get(ids[0]);
|
|
assert.isTrue(item.isImportedAttachment());
|
|
assert.equal(item.getField('title'), 'PDF');
|
|
});
|
|
|
|
|
|
it("should download open-access PDF if no PDF provided", async function () {
|
|
var collection = await createDataObject('collection');
|
|
await waitForItemsLoad(win);
|
|
|
|
var sessionID = Zotero.Utilities.randomString();
|
|
|
|
// Save item
|
|
var itemAddPromise = waitForItemEvent('add');
|
|
var saveItemsReq = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveItems",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
items: [
|
|
{
|
|
itemType: 'journalArticle',
|
|
title: 'Title',
|
|
DOI: oaDOI,
|
|
attachments: []
|
|
}
|
|
],
|
|
uri: 'http://website/article'
|
|
}),
|
|
responseType: 'json'
|
|
}
|
|
);
|
|
assert.equal(saveItemsReq.status, 201);
|
|
assert.lengthOf(saveItemsReq.response.items, 1);
|
|
// Attachment shouldn't show up in the initial response
|
|
assert.lengthOf(saveItemsReq.response.items[0].attachments, 0);
|
|
|
|
// Check parent item
|
|
var ids = await itemAddPromise;
|
|
assert.lengthOf(ids, 1);
|
|
var item = Zotero.Items.get(ids[0]);
|
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'journalArticle');
|
|
assert.isTrue(collection.hasItem(item.id));
|
|
|
|
// Wait for the attachment to finish saving
|
|
itemAddPromise = waitForItemEvent('add');
|
|
var wasZero = false;
|
|
var was100 = false;
|
|
while (true) {
|
|
let sessionProgressReq = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/sessionProgress",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({ sessionID }),
|
|
responseType: 'json'
|
|
}
|
|
);
|
|
assert.equal(sessionProgressReq.status, 200);
|
|
let response = sessionProgressReq.response;
|
|
assert.typeOf(response.items, 'array');
|
|
assert.lengthOf(response.items, 1);
|
|
let item = response.items[0];
|
|
if (item.attachments.length) {
|
|
// 'progress' should have started at 0
|
|
if (item.attachments[0].progress === 0) {
|
|
wasZero = true;
|
|
}
|
|
else if (!was100 && item.attachments[0].progress == 100) {
|
|
if (response.done) {
|
|
break;
|
|
}
|
|
was100 = true;
|
|
}
|
|
else if (response.done) {
|
|
break;
|
|
}
|
|
}
|
|
assert.isFalse(response.done);
|
|
await Zotero.Promise.delay(10);
|
|
}
|
|
assert.isTrue(wasZero);
|
|
|
|
// Check attachment
|
|
var ids = await itemAddPromise;
|
|
assert.lengthOf(ids, 1);
|
|
item = Zotero.Items.get(ids[0]);
|
|
assert.isTrue(item.isImportedAttachment());
|
|
assert.equal(item.getField('title'), Zotero.getString('attachment.submittedVersion'));
|
|
});
|
|
|
|
|
|
it("should download open-access PDF if a translated PDF fails", async function () {
|
|
var collection = await createDataObject('collection');
|
|
await waitForItemsLoad(win);
|
|
|
|
var sessionID = Zotero.Utilities.randomString();
|
|
|
|
// Save item
|
|
var itemAddPromise = waitForItemEvent('add');
|
|
var saveItemsReq = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveItems",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
items: [
|
|
{
|
|
itemType: 'journalArticle',
|
|
title: 'Title',
|
|
DOI: oaDOI,
|
|
attachments: [
|
|
{
|
|
title: "PDF",
|
|
url: badPDFURL,
|
|
mimeType: 'application/pdf'
|
|
}
|
|
]
|
|
}
|
|
],
|
|
uri: 'http://website/article'
|
|
}),
|
|
responseType: 'json'
|
|
}
|
|
);
|
|
assert.equal(saveItemsReq.status, 201);
|
|
assert.lengthOf(saveItemsReq.response.items, 1);
|
|
// Translated attachment should show up in the initial response
|
|
assert.lengthOf(saveItemsReq.response.items[0].attachments, 1);
|
|
assert.notProperty(saveItemsReq.response.items[0], 'DOI');
|
|
assert.notProperty(saveItemsReq.response.items[0].attachments[0], 'progress');
|
|
|
|
// Check parent item
|
|
var ids = await itemAddPromise;
|
|
assert.lengthOf(ids, 1);
|
|
var item = Zotero.Items.get(ids[0]);
|
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'journalArticle');
|
|
assert.isTrue(collection.hasItem(item.id));
|
|
|
|
// Legacy endpoint should show 0
|
|
let attachmentProgressReq = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/attachmentProgress",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify([saveItemsReq.response.items[0].attachments[0].id]),
|
|
responseType: 'json'
|
|
}
|
|
);
|
|
assert.equal(attachmentProgressReq.status, 200);
|
|
let progress = attachmentProgressReq.response;
|
|
assert.sameOrderedMembers(progress, [0]);
|
|
|
|
// Wait for the attachment to finish saving
|
|
itemAddPromise = waitForItemEvent('add');
|
|
var i = 0;
|
|
while (i < 4) {
|
|
let sessionProgressReq = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/sessionProgress",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({ sessionID }),
|
|
responseType: 'json'
|
|
}
|
|
);
|
|
assert.equal(sessionProgressReq.status, 200);
|
|
let response = sessionProgressReq.response;
|
|
assert.lengthOf(response.items, 1);
|
|
let item = response.items[0];
|
|
if (item.attachments.length) {
|
|
let attachments = item.attachments;
|
|
assert.lengthOf(attachments, 1);
|
|
let attachment = attachments[0];
|
|
switch (i) {
|
|
// Translated PDF in progress
|
|
case 0:
|
|
if (attachment.title == "PDF"
|
|
&& Number.isInteger(attachment.progress)
|
|
&& attachment.progress < 100) {
|
|
assert.isFalse(response.done);
|
|
i++;
|
|
}
|
|
continue;
|
|
|
|
// OA PDF in progress
|
|
case 1:
|
|
if (attachment.title == Zotero.getString('findPDF.openAccessPDF')
|
|
&& Number.isInteger(attachment.progress)
|
|
&& attachment.progress < 100) {
|
|
assert.isFalse(response.done);
|
|
i++;
|
|
}
|
|
continue;
|
|
|
|
// OA PDF finished
|
|
case 2:
|
|
if (attachment.progress === 100) {
|
|
assert.equal(attachment.title, Zotero.getString('findPDF.openAccessPDF'));
|
|
i++;
|
|
}
|
|
continue;
|
|
|
|
// done: true
|
|
case 3:
|
|
if (response.done) {
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
await Zotero.Promise.delay(10);
|
|
}
|
|
|
|
// Legacy endpoint should show 100
|
|
attachmentProgressReq = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/attachmentProgress",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify([saveItemsReq.response.items[0].attachments[0].id]),
|
|
responseType: 'json'
|
|
}
|
|
);
|
|
assert.equal(attachmentProgressReq.status, 200);
|
|
progress = attachmentProgressReq.response;
|
|
assert.sameOrderedMembers(progress, [100]);
|
|
|
|
// Check attachment
|
|
var ids = await itemAddPromise;
|
|
assert.lengthOf(ids, 1);
|
|
item = Zotero.Items.get(ids[0]);
|
|
assert.isTrue(item.isImportedAttachment());
|
|
assert.equal(item.getField('title'), Zotero.getString('attachment.submittedVersion'));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("/connector/saveSingleFile", function () {
|
|
it("should save a webpage item with /saveSnapshot", async function () {
|
|
var collection = await createDataObject('collection');
|
|
await waitForItemsLoad(win);
|
|
|
|
// Promise for item save
|
|
let promise = waitForItemEvent('add');
|
|
|
|
let testDataDirectory = getTestDataDirectory().path;
|
|
let indexPath = OS.Path.join(testDataDirectory, 'snapshot', 'index.html');
|
|
|
|
let title = Zotero.Utilities.randomString();
|
|
let sessionID = Zotero.Utilities.randomString();
|
|
let payload = {
|
|
sessionID,
|
|
url: "http://example.com/test",
|
|
title,
|
|
singleFile: true
|
|
};
|
|
|
|
await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSnapshot",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(payload)
|
|
}
|
|
);
|
|
|
|
// Await item save
|
|
let parentIDs = await promise;
|
|
|
|
// Check parent item
|
|
assert.lengthOf(parentIDs, 1);
|
|
var item = Zotero.Items.get(parentIDs[0]);
|
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'webpage');
|
|
assert.isTrue(collection.hasItem(item.id));
|
|
assert.equal(item.getField('title'), title);
|
|
|
|
// Promise for attachment save
|
|
promise = waitForItemEvent('add');
|
|
|
|
let body = JSON.stringify(Object.assign(payload, {
|
|
snapshotContent: await Zotero.File.getContentsAsync(indexPath)
|
|
}));
|
|
|
|
await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSingleFile",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body
|
|
}
|
|
);
|
|
|
|
// Await attachment save
|
|
let attachmentIDs = await promise;
|
|
|
|
// Check attachment
|
|
assert.lengthOf(attachmentIDs, 1);
|
|
item = Zotero.Items.get(attachmentIDs[0]);
|
|
assert.isTrue(item.isImportedAttachment());
|
|
assert.equal(item.getField('title'), title);
|
|
|
|
// Check attachment html file
|
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item).path;
|
|
let path = OS.Path.join(attachmentDirectory, 'test.html');
|
|
assert.isTrue(await OS.File.exists(path));
|
|
let contents = await Zotero.File.getContentsAsync(path);
|
|
let expectedContents = await Zotero.File.getContentsAsync(indexPath);
|
|
assert.equal(contents, expectedContents);
|
|
});
|
|
|
|
it("should save a webpage item with /saveItems", async function () {
|
|
let collection = await createDataObject('collection');
|
|
await waitForItemsLoad(win);
|
|
|
|
let title = Zotero.Utilities.randomString();
|
|
let sessionID = Zotero.Utilities.randomString();
|
|
let payload = {
|
|
sessionID: sessionID,
|
|
items: [
|
|
{
|
|
itemType: "newspaperArticle",
|
|
title: title,
|
|
creators: [
|
|
{
|
|
firstName: "First",
|
|
lastName: "Last",
|
|
creatorType: "author"
|
|
}
|
|
],
|
|
attachments: [
|
|
{
|
|
title: "Snapshot",
|
|
url: `${testServerPath}/attachment`,
|
|
mimeType: "text/html",
|
|
singleFile: true
|
|
}
|
|
]
|
|
}
|
|
],
|
|
uri: "http://example.com"
|
|
};
|
|
|
|
let promise = waitForItemEvent('add');
|
|
let req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveItems",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(payload)
|
|
}
|
|
);
|
|
assert.equal(req.status, 201);
|
|
|
|
// Check parent item
|
|
let itemIDs = await promise;
|
|
assert.lengthOf(itemIDs, 1);
|
|
let item = Zotero.Items.get(itemIDs[0]);
|
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'newspaperArticle');
|
|
assert.isTrue(collection.hasItem(item.id));
|
|
|
|
// Promise for attachment save
|
|
promise = waitForItemEvent('add');
|
|
|
|
let testDataDirectory = getTestDataDirectory().path;
|
|
let indexPath = OS.Path.join(testDataDirectory, 'snapshot', 'index.html');
|
|
|
|
let body = JSON.stringify(Object.assign(payload, {
|
|
snapshotContent: await Zotero.File.getContentsAsync(indexPath)
|
|
}));
|
|
|
|
req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSingleFile",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body
|
|
}
|
|
);
|
|
assert.equal(req.status, 201);
|
|
|
|
// Await attachment save
|
|
let attachmentIDs = await promise;
|
|
|
|
// Check attachment
|
|
assert.lengthOf(attachmentIDs, 1);
|
|
item = Zotero.Items.get(attachmentIDs[0]);
|
|
assert.isTrue(item.isImportedAttachment());
|
|
assert.equal(item.getField('title'), 'Snapshot');
|
|
|
|
// Check attachment html file
|
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item).path;
|
|
let path = OS.Path.join(attachmentDirectory, 'attachment.html');
|
|
assert.isTrue(await OS.File.exists(path));
|
|
let contents = await Zotero.File.getContentsAsync(path);
|
|
let expectedContents = await Zotero.File.getContentsAsync(indexPath);
|
|
assert.equal(contents, expectedContents);
|
|
});
|
|
|
|
it("should override SingleFileZ from old connector in /saveSnapshot", async function () {
|
|
Components.utils.import("resource://gre/modules/FileUtils.jsm");
|
|
var collection = await createDataObject('collection');
|
|
await waitForItemsLoad(win);
|
|
|
|
// Promise for item save
|
|
let promise = waitForItemEvent('add');
|
|
|
|
let testDataDirectory = getTestDataDirectory().path;
|
|
let indexPath = OS.Path.join(testDataDirectory, 'snapshot', 'index.html');
|
|
|
|
let prefix = '/' + Zotero.Utilities.randomString() + '/';
|
|
let uri = OS.Path.join(getTestDataDirectory().path, 'snapshot');
|
|
httpd.registerDirectory(prefix, new FileUtils.File(uri));
|
|
|
|
let title = Zotero.Utilities.randomString();
|
|
let sessionID = Zotero.Utilities.randomString();
|
|
let payload = {
|
|
sessionID,
|
|
url: testServerPath + prefix + 'index.html',
|
|
title,
|
|
singleFile: true
|
|
};
|
|
|
|
await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSnapshot",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(payload)
|
|
}
|
|
);
|
|
|
|
// Await item save
|
|
let parentIDs = await promise;
|
|
|
|
// Check parent item
|
|
assert.lengthOf(parentIDs, 1);
|
|
var item = Zotero.Items.get(parentIDs[0]);
|
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'webpage');
|
|
assert.isTrue(collection.hasItem(item.id));
|
|
assert.equal(item.getField('title'), title);
|
|
|
|
// Promise for attachment save
|
|
promise = waitForItemEvent('add');
|
|
|
|
let body = new FormData();
|
|
let uuid = 'binary-' + Zotero.Utilities.randomString();
|
|
body.append("payload", JSON.stringify(Object.assign(payload, {
|
|
pageData: {
|
|
content: await Zotero.File.getContentsAsync(indexPath),
|
|
resources: {
|
|
images: [
|
|
{
|
|
name: "img.gif",
|
|
content: uuid,
|
|
binary: true
|
|
}
|
|
]
|
|
}
|
|
}
|
|
})));
|
|
|
|
await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSingleFile",
|
|
{
|
|
headers: {
|
|
"Content-Type": "multipart/form-data",
|
|
"zotero-allowed-request": "true"
|
|
},
|
|
body
|
|
}
|
|
);
|
|
|
|
// Await attachment save
|
|
let attachmentIDs = await promise;
|
|
|
|
// Check attachment
|
|
assert.lengthOf(attachmentIDs, 1);
|
|
item = Zotero.Items.get(attachmentIDs[0]);
|
|
assert.isTrue(item.isImportedAttachment());
|
|
assert.equal(item.getField('title'), title);
|
|
|
|
// Check attachment html file
|
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item).path;
|
|
let path = OS.Path.join(attachmentDirectory, item.attachmentFilename);
|
|
assert.isTrue(await OS.File.exists(path));
|
|
let contents = await Zotero.File.getContentsAsync(path);
|
|
assert.match(contents, /^<html style><!--\n Page saved with SingleFile \n url:/);
|
|
});
|
|
|
|
it("should override SingleFileZ from old connector in /saveItems", async function () {
|
|
let collection = await createDataObject('collection');
|
|
await waitForItemsLoad(win);
|
|
|
|
let prefix = '/' + Zotero.Utilities.randomString() + '/';
|
|
let uri = OS.Path.join(getTestDataDirectory().path, 'snapshot');
|
|
httpd.registerDirectory(prefix, new FileUtils.File(uri));
|
|
|
|
let title = Zotero.Utilities.randomString();
|
|
let sessionID = Zotero.Utilities.randomString();
|
|
let payload = {
|
|
sessionID: sessionID,
|
|
items: [
|
|
{
|
|
itemType: "newspaperArticle",
|
|
title: title,
|
|
creators: [
|
|
{
|
|
firstName: "First",
|
|
lastName: "Last",
|
|
creatorType: "author"
|
|
}
|
|
],
|
|
attachments: [
|
|
{
|
|
title: "Snapshot",
|
|
url: testServerPath + prefix + 'index.html',
|
|
mimeType: "text/html",
|
|
singleFile: true
|
|
}
|
|
]
|
|
}
|
|
],
|
|
uri: "http://example.com"
|
|
};
|
|
|
|
let promise = waitForItemEvent('add');
|
|
let req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveItems",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(payload)
|
|
}
|
|
);
|
|
assert.equal(req.status, 201);
|
|
|
|
// Check parent item
|
|
let itemIDs = await promise;
|
|
assert.lengthOf(itemIDs, 1);
|
|
let item = Zotero.Items.get(itemIDs[0]);
|
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'newspaperArticle');
|
|
assert.isTrue(collection.hasItem(item.id));
|
|
|
|
// Promise for attachment save
|
|
promise = waitForItemEvent('add');
|
|
|
|
let testDataDirectory = getTestDataDirectory().path;
|
|
let indexPath = OS.Path.join(testDataDirectory, 'snapshot', 'index.html');
|
|
|
|
let body = new FormData();
|
|
let uuid = 'binary-' + Zotero.Utilities.randomString();
|
|
body.append("payload", JSON.stringify(Object.assign(payload, {
|
|
pageData: {
|
|
content: 'Foobar content',
|
|
resources: {
|
|
images: [
|
|
{
|
|
name: "img.gif",
|
|
content: uuid,
|
|
binary: true
|
|
}
|
|
]
|
|
}
|
|
}
|
|
})));
|
|
|
|
req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSingleFile",
|
|
{
|
|
headers: {
|
|
"Content-Type": "multipart/form-data",
|
|
"zotero-allowed-request": "true"
|
|
},
|
|
body
|
|
}
|
|
);
|
|
assert.equal(req.status, 201);
|
|
|
|
// Await attachment save
|
|
let attachmentIDs = await promise;
|
|
|
|
// Check attachment
|
|
assert.lengthOf(attachmentIDs, 1);
|
|
item = Zotero.Items.get(attachmentIDs[0]);
|
|
assert.isTrue(item.isImportedAttachment());
|
|
assert.equal(item.getField('title'), 'Snapshot');
|
|
|
|
// Check attachment html file
|
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item).path;
|
|
let path = OS.Path.join(attachmentDirectory, item.attachmentFilename);
|
|
assert.isTrue(await OS.File.exists(path));
|
|
let contents = await Zotero.File.getContentsAsync(path);
|
|
assert.match(contents, /^<html style><!--\n Page saved with SingleFile \n url:/);
|
|
});
|
|
|
|
it("should handle race condition with /saveItems", async function () {
|
|
let collection = await createDataObject('collection');
|
|
await waitForItemsLoad(win);
|
|
|
|
let pdfURL = testServerPath + '/pdf';
|
|
let nonOADOI = '10.2222/bcde';
|
|
|
|
// Promise for item saving
|
|
let parentIDs, attachmentIDs1, attachmentIDs2;
|
|
let promise = waitForItemEvent('add').then(function (ids) {
|
|
parentIDs = ids;
|
|
return waitForItemEvent('add').then(function (ids) {
|
|
attachmentIDs1 = ids;
|
|
return waitForItemEvent('add').then(function (ids) {
|
|
attachmentIDs2 = ids;
|
|
});
|
|
});
|
|
});
|
|
|
|
// Promise for snapshot having been saved
|
|
let singleFileResolve;
|
|
let singleFileDone = new Zotero.Promise(function (resolve, reject) {
|
|
singleFileResolve = resolve;
|
|
});
|
|
|
|
// Special handler to delay writing of file response for 5 seconds to allow
|
|
// `saveSingleFile` request to finish first before getting PDF
|
|
httpd.registerPathHandler(
|
|
'/pdf',
|
|
{
|
|
handle: async function (request, response) {
|
|
response.setStatusLine(null, 200, "OK");
|
|
let file = Zotero.File.pathToFile(OS.Path.join(getTestDataDirectory().path, 'test.pdf'));
|
|
response.processAsync();
|
|
// Delay the PDF processing (simulates a long network request) so that
|
|
// the SingleFile request below completes first.
|
|
await singleFileDone;
|
|
httpd._handler._writeFileResponse(request, file, response, 0, file.fileSize);
|
|
}
|
|
}
|
|
);
|
|
|
|
// Setup our `saveItems` and payload and call connector server
|
|
let title = Zotero.Utilities.randomString();
|
|
let sessionID = Zotero.Utilities.randomString();
|
|
let payload = {
|
|
sessionID,
|
|
items: [
|
|
{
|
|
itemType: 'journalArticle',
|
|
title: title,
|
|
DOI: nonOADOI,
|
|
attachments: [
|
|
{
|
|
title: "PDF",
|
|
url: pdfURL,
|
|
mimeType: 'application/pdf'
|
|
},
|
|
{
|
|
title: "Snapshot",
|
|
url: `${testServerPath}/attachment`,
|
|
mimeType: "text/html",
|
|
singleFile: true
|
|
}
|
|
]
|
|
}
|
|
],
|
|
uri: "http://example.com"
|
|
};
|
|
|
|
let req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveItems",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(payload)
|
|
}
|
|
);
|
|
assert.equal(req.status, 201);
|
|
|
|
// Now setup and call our `saveSingleFile` to save snapshot attachment
|
|
let testDataDirectory = getTestDataDirectory().path;
|
|
let indexPath = OS.Path.join(testDataDirectory, 'snapshot', 'index.html');
|
|
|
|
let body = JSON.stringify(Object.assign(payload, {
|
|
snapshotContent: await Zotero.File.getContentsAsync(indexPath)
|
|
}));
|
|
|
|
req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSingleFile",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body
|
|
}
|
|
);
|
|
assert.equal(req.status, 201);
|
|
|
|
// Trigger PDF saving to complete now that SingleFile is done.
|
|
singleFileResolve();
|
|
|
|
// Await all item saves
|
|
await promise;
|
|
|
|
// Once the PDF is saved, if the bug exists, the snapshot will saved again.
|
|
// Once that is completed, then the session will be marked as done so we
|
|
// wait for that to occur here. Then we can proceed to ensure we have the
|
|
// proper number of items.
|
|
let savingDone = false;
|
|
while (!savingDone) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/sessionProgress",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({ sessionID })
|
|
}
|
|
);
|
|
savingDone = JSON.parse(req.response).done;
|
|
if (!savingDone) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await Zotero.Promise.delay(1000);
|
|
}
|
|
}
|
|
|
|
// Check parent item
|
|
assert.lengthOf(parentIDs, 1);
|
|
let item = Zotero.Items.get(parentIDs[0]);
|
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'journalArticle');
|
|
assert.isTrue(collection.hasItem(item.id));
|
|
|
|
// Ensure we only have one snapshot and one PDF - this is the critical test
|
|
assert.equal(item.numChildren(), 2);
|
|
|
|
// Snapshot is saved first
|
|
assert.lengthOf(attachmentIDs1, 1);
|
|
item = Zotero.Items.get(attachmentIDs1[0]);
|
|
assert.isTrue(item.isImportedAttachment());
|
|
assert.equal(item.getField('title'), 'Snapshot');
|
|
|
|
// Double check snapshot html file has content
|
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item).path;
|
|
let path = OS.Path.join(attachmentDirectory, 'attachment.html');
|
|
assert.isTrue(await OS.File.exists(path));
|
|
let contents = await Zotero.File.getContentsAsync(path);
|
|
let expectedContents = await Zotero.File.getContentsAsync(indexPath);
|
|
assert.equal(contents, expectedContents);
|
|
|
|
// Then PDF is saved second
|
|
assert.lengthOf(attachmentIDs2, 1);
|
|
item = Zotero.Items.get(attachmentIDs2[0]);
|
|
assert.isTrue(item.isImportedAttachment());
|
|
assert.equal(item.getField('title'), 'PDF');
|
|
});
|
|
});
|
|
|
|
describe("/connector/saveSnapshot", function () {
|
|
it("should save a webpage item and snapshot to the current selected collection", function* () {
|
|
var collection = yield createDataObject('collection');
|
|
yield waitForItemsLoad(win);
|
|
|
|
// saveSnapshot saves parent and child before returning
|
|
var ids1, ids2;
|
|
var promise = waitForItemEvent('add').then(function (ids) {
|
|
ids1 = ids;
|
|
return waitForItemEvent('add').then(function (ids) {
|
|
ids2 = ids;
|
|
});
|
|
});
|
|
|
|
var file = getTestDataDirectory();
|
|
file.append('snapshot');
|
|
file.append('index.html');
|
|
httpd.registerFile("/test", file);
|
|
|
|
yield Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSnapshot",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
url: `${testServerPath}/test`,
|
|
html: "<html><head><title>Title</title><body>Body</body></html>"
|
|
})
|
|
}
|
|
);
|
|
|
|
assert.isTrue(promise.isFulfilled());
|
|
|
|
// Check parent item
|
|
assert.lengthOf(ids1, 1);
|
|
var item = Zotero.Items.get(ids1[0]);
|
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'webpage');
|
|
assert.isTrue(collection.hasItem(item.id));
|
|
assert.equal(item.getField('title'), 'Title');
|
|
|
|
// Check attachment
|
|
assert.lengthOf(ids2, 1);
|
|
item = Zotero.Items.get(ids2[0]);
|
|
assert.isTrue(item.isImportedAttachment());
|
|
assert.equal(item.getField('title'), 'Title');
|
|
});
|
|
|
|
it("should save a PDF to the current selected collection and retrieve metadata", async function () {
|
|
var collection = await createDataObject('collection');
|
|
await waitForItemsLoad(win);
|
|
|
|
var file = getTestDataDirectory();
|
|
file.append('test.pdf');
|
|
httpd.registerFile("/test.pdf", file);
|
|
|
|
var promise = waitForItemEvent('add');
|
|
|
|
var origRequest = Zotero.HTTP.request.bind(Zotero.HTTP);
|
|
var called = 0;
|
|
var stub = sinon.stub(Zotero.HTTP, 'request').callsFake(function (method, url, options) {
|
|
// Forward saveSnapshot request
|
|
if (url.endsWith('saveSnapshot')) {
|
|
return origRequest(...arguments);
|
|
}
|
|
|
|
// Fake recognizer response
|
|
return Zotero.Promise.resolve({
|
|
getResponseHeader: () => {},
|
|
responseText: JSON.stringify({
|
|
title: 'Test',
|
|
authors: []
|
|
})
|
|
});
|
|
});
|
|
|
|
let response = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSnapshot",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
url: testServerPath + "/test.pdf",
|
|
pdf: true,
|
|
singleFile: true
|
|
})
|
|
}
|
|
);
|
|
let json = JSON.parse(response.responseText);
|
|
assert.propertyVal(json, 'saveSingleFile', false);
|
|
|
|
var ids = await promise;
|
|
|
|
assert.lengthOf(ids, 1);
|
|
var attachment = Zotero.Items.get(ids[0]);
|
|
assert.isTrue(attachment.isImportedAttachment());
|
|
assert.equal(attachment.attachmentContentType, 'application/pdf');
|
|
assert.isTrue(collection.hasItem(attachment.id));
|
|
|
|
await waitForItemEvent('add');
|
|
await waitForItemEvent('modify');
|
|
await waitForItemEvent('modify');
|
|
|
|
assert.isFalse(attachment.isTopLevelItem());
|
|
|
|
stub.restore();
|
|
});
|
|
|
|
it("should switch to My Library if a read-only library is selected", function* () {
|
|
var group = yield createGroup({
|
|
editable: false
|
|
});
|
|
yield selectLibrary(win, group.libraryID);
|
|
yield waitForItemsLoad(win);
|
|
|
|
var promise = waitForItemEvent('add');
|
|
var reqPromise = Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSnapshot",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
url: "http://example.com",
|
|
html: "<html><head><title>Title</title><body>Body</body></html>"
|
|
}),
|
|
successCodes: false
|
|
}
|
|
);
|
|
|
|
// My Library be selected, and the item should be in it
|
|
var ids = yield promise;
|
|
assert.equal(
|
|
win.ZoteroPane.collectionsView.getSelectedLibraryID(),
|
|
Zotero.Libraries.userLibraryID
|
|
);
|
|
assert.lengthOf(ids, 1);
|
|
var item = Zotero.Items.get(ids[0]);
|
|
assert.equal(item.libraryID, Zotero.Libraries.userLibraryID);
|
|
|
|
var req = yield reqPromise;
|
|
assert.equal(req.status, 201);
|
|
});
|
|
});
|
|
|
|
describe("/connector/savePage", function() {
|
|
before(async function () {
|
|
await selectLibrary(win);
|
|
await waitForItemsLoad(win);
|
|
});
|
|
|
|
it("should return 500 if no translator available for page", function* () {
|
|
var xmlhttp = yield Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/savePage",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
uri: "http://example.com",
|
|
html: "<html><head><title>Title</title><body>Body</body></html>"
|
|
}),
|
|
successCodes: false
|
|
}
|
|
);
|
|
assert.equal(xmlhttp.status, 500);
|
|
});
|
|
|
|
it("should translate a page if translators are available", function* () {
|
|
var html = Zotero.File.getContentsFromURL(getTestDataUrl('coins.html'));
|
|
var promise = waitForItemEvent('add');
|
|
var xmlhttp = yield Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/savePage",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
uri: "https://example.com/test",
|
|
html
|
|
}),
|
|
successCodes: false
|
|
}
|
|
);
|
|
|
|
let ids = yield promise;
|
|
var item = Zotero.Items.get(ids[0]);
|
|
var title = "Test Page";
|
|
assert.equal(JSON.parse(xmlhttp.responseText).items[0].title, title);
|
|
assert.equal(item.getField('title'), title);
|
|
assert.equal(xmlhttp.status, 201);
|
|
});
|
|
});
|
|
|
|
describe("/connector/updateSession", function () {
|
|
it("should update collections and tags of item saved via /saveItems", async function () {
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection');
|
|
await waitForItemsLoad(win);
|
|
|
|
var sessionID = Zotero.Utilities.randomString();
|
|
var body = {
|
|
sessionID,
|
|
items: [
|
|
{
|
|
itemType: "newspaperArticle",
|
|
title: "Title",
|
|
creators: [
|
|
{
|
|
firstName: "First",
|
|
lastName: "Last",
|
|
creatorType: "author"
|
|
}
|
|
],
|
|
attachments: [
|
|
{
|
|
title: "Attachment",
|
|
url: `${testServerPath}/attachment`,
|
|
mimeType: "text/html"
|
|
}
|
|
]
|
|
}
|
|
],
|
|
uri: "http://example.com"
|
|
};
|
|
|
|
httpd.registerPathHandler(
|
|
"/attachment",
|
|
{
|
|
handle: function (request, response) {
|
|
response.setStatusLine(null, 200, "OK");
|
|
response.write("<html><head><title>Title</title><body>Body</body></html>");
|
|
}
|
|
}
|
|
);
|
|
|
|
var reqPromise = Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveItems",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(body)
|
|
}
|
|
);
|
|
|
|
var ids = await waitForItemEvent('add');
|
|
var item = Zotero.Items.get(ids[0]);
|
|
assert.isTrue(collection2.hasItem(item.id));
|
|
await waitForItemEvent('add');
|
|
|
|
var req = await reqPromise;
|
|
assert.equal(req.status, 201);
|
|
|
|
// Update saved item
|
|
var req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: collection1.treeViewID,
|
|
tags: "A, B"
|
|
})
|
|
}
|
|
);
|
|
|
|
assert.equal(req.status, 200);
|
|
assert.isTrue(collection1.hasItem(item.id));
|
|
assert.isTrue(item.hasTag("A"));
|
|
assert.isTrue(item.hasTag("B"));
|
|
});
|
|
|
|
it("should update collections and tags of PDF saved via /saveSnapshot", async function () {
|
|
var sessionID = Zotero.Utilities.randomString();
|
|
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection');
|
|
await waitForItemsLoad(win);
|
|
|
|
var file = getTestDataDirectory();
|
|
file.append('test.pdf');
|
|
httpd.registerFile("/test.pdf", file);
|
|
|
|
var ids;
|
|
var promise = waitForItemEvent('add');
|
|
var reqPromise = Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSnapshot",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
url: testServerPath + "/test.pdf",
|
|
pdf: true
|
|
})
|
|
}
|
|
);
|
|
|
|
var ids = await promise;
|
|
var item = Zotero.Items.get(ids[0]);
|
|
assert.isTrue(collection2.hasItem(item.id));
|
|
var req = await reqPromise;
|
|
assert.equal(req.status, 201);
|
|
|
|
// Update saved item
|
|
var req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: collection1.treeViewID,
|
|
tags: "A, B"
|
|
})
|
|
}
|
|
);
|
|
|
|
assert.equal(req.status, 200);
|
|
assert.isTrue(collection1.hasItem(item.id));
|
|
assert.isTrue(item.hasTag("A"));
|
|
assert.isTrue(item.hasTag("B"));
|
|
});
|
|
|
|
it("should update collections and tags of webpage saved via /saveSnapshot", async function () {
|
|
var sessionID = Zotero.Utilities.randomString();
|
|
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection');
|
|
await waitForItemsLoad(win);
|
|
|
|
// saveSnapshot saves parent and child before returning
|
|
var ids1, ids2;
|
|
var promise = waitForItemEvent('add').then(function (ids) {
|
|
ids1 = ids;
|
|
return waitForItemEvent('add').then(function (ids) {
|
|
ids2 = ids;
|
|
});
|
|
});
|
|
await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSnapshot",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
url: "http://example.com",
|
|
html: "<html><head><title>Title</title><body>Body</body></html>"
|
|
})
|
|
}
|
|
);
|
|
|
|
assert.isTrue(promise.isFulfilled());
|
|
|
|
var item = Zotero.Items.get(ids1[0]);
|
|
|
|
// Update saved item
|
|
var req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: collection1.treeViewID,
|
|
tags: "A, B"
|
|
})
|
|
}
|
|
);
|
|
|
|
assert.equal(req.status, 200);
|
|
assert.isTrue(collection1.hasItem(item.id));
|
|
assert.isTrue(item.hasTag("A"));
|
|
assert.isTrue(item.hasTag("B"));
|
|
});
|
|
|
|
it("should move item saved via /saveItems to another library", async function () {
|
|
let addItemsSpy = sinon.spy(Zotero.Server.Connector.SaveSession.prototype, 'addItems');
|
|
var group = await createGroup({ editable: true, filesEditable: false });
|
|
await selectLibrary(win);
|
|
await waitForItemsLoad(win);
|
|
|
|
var sessionID = Zotero.Utilities.randomString();
|
|
var body = {
|
|
sessionID,
|
|
items: [
|
|
{
|
|
itemType: "newspaperArticle",
|
|
title: "Title",
|
|
attachments: [
|
|
{
|
|
title: "Attachment",
|
|
url: `${testServerPath}/attachment`,
|
|
mimeType: "text/html"
|
|
}
|
|
]
|
|
}
|
|
],
|
|
uri: "http://example.com"
|
|
};
|
|
|
|
httpd.registerPathHandler(
|
|
"/attachment",
|
|
{
|
|
handle: function (request, response) {
|
|
response.setStatusLine(null, 200, "OK");
|
|
response.write("<html><head><title>Title</title><body>Body</body></html>");
|
|
}
|
|
}
|
|
);
|
|
|
|
var reqPromise = Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveItems",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(body)
|
|
}
|
|
);
|
|
|
|
var ids1 = await waitForItemEvent('add');
|
|
var item1 = Zotero.Items.get(ids1[0]);
|
|
// Attachment
|
|
await waitForItemEvent('add');
|
|
|
|
// There's an additional addItems call in saveItems that is not async returned and runs
|
|
// after attachment notifier add event callbacks are run, so we have to do some
|
|
// hacky waiting here, otherwise we get some crazy race-conditions due to
|
|
// collection changing being debounced
|
|
let callCount = addItemsSpy.callCount;
|
|
while (addItemsSpy.callCount <= callCount) {
|
|
await Zotero.Promise.delay(50);
|
|
}
|
|
await addItemsSpy.lastCall.returnValue;
|
|
|
|
var req = await reqPromise;
|
|
assert.equal(req.status, 201);
|
|
|
|
// Move item to group without file attachment
|
|
reqPromise = Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: group.treeViewID
|
|
})
|
|
}
|
|
);
|
|
|
|
var ids2 = await waitForItemEvent('add');
|
|
var item2 = Zotero.Items.get(ids2[0]);
|
|
|
|
req = await reqPromise;
|
|
assert.equal(req.status, 200);
|
|
assert.isFalse(Zotero.Items.exists(item1.id));
|
|
assert.equal(item2.libraryID, group.libraryID);
|
|
assert.equal(item2.numAttachments(), 0);
|
|
|
|
// Move back to My Library and resave attachment
|
|
reqPromise = Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: Zotero.Libraries.userLibrary.treeViewID
|
|
})
|
|
}
|
|
);
|
|
|
|
var ids3 = await waitForItemEvent('add');
|
|
var item3 = Zotero.Items.get(ids3[0]);
|
|
// Attachment
|
|
await waitForItemEvent('add');
|
|
|
|
req = await reqPromise;
|
|
assert.equal(req.status, 200);
|
|
assert.isFalse(Zotero.Items.exists(item2.id));
|
|
assert.equal(item3.libraryID, Zotero.Libraries.userLibraryID);
|
|
assert.equal(item3.numAttachments(), 1);
|
|
|
|
addItemsSpy.restore();
|
|
});
|
|
|
|
it("should move item saved via /saveSnapshot to another library", async function () {
|
|
var group = await createGroup({ editable: true, filesEditable: false });
|
|
await selectLibrary(win);
|
|
await waitForItemsLoad(win);
|
|
var sessionID = Zotero.Utilities.randomString();
|
|
|
|
// saveSnapshot saves parent and child before returning
|
|
var ids1;
|
|
var promise = waitForItemEvent('add').then(function (ids) {
|
|
ids1 = ids;
|
|
return waitForItemEvent('add').then(function (ids) {
|
|
ids1 = ids1.concat(ids);
|
|
});
|
|
});
|
|
await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSnapshot",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
url: "http://example.com",
|
|
html: "<html><head><title>Title</title><body>Body</body></html>"
|
|
})
|
|
}
|
|
);
|
|
|
|
assert.isTrue(promise.isFulfilled());
|
|
|
|
var item1 = Zotero.Items.get(ids1[0]);
|
|
|
|
// Move item to group without file attachment
|
|
var reqPromise = Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: group.treeViewID
|
|
})
|
|
}
|
|
);
|
|
|
|
var ids2 = await waitForItemEvent('add');
|
|
var item2 = Zotero.Items.get(ids2[0]);
|
|
|
|
var req = await reqPromise;
|
|
assert.equal(req.status, 200);
|
|
assert.isFalse(Zotero.Items.exists(item1.id));
|
|
assert.equal(item2.libraryID, group.libraryID);
|
|
assert.equal(item2.numAttachments(), 0);
|
|
|
|
// Move back to My Library and resave attachment
|
|
reqPromise = Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: Zotero.Libraries.userLibrary.treeViewID
|
|
})
|
|
}
|
|
);
|
|
|
|
var ids3 = await waitForItemEvent('add');
|
|
var item3 = Zotero.Items.get(ids3[0]);
|
|
await waitForItemEvent('add');
|
|
|
|
req = await reqPromise;
|
|
assert.equal(req.status, 200);
|
|
assert.isFalse(Zotero.Items.exists(item2.id));
|
|
assert.equal(item3.libraryID, Zotero.Libraries.userLibraryID);
|
|
assert.equal(item3.numAttachments(), 1);
|
|
});
|
|
|
|
it("should save item saved via /saveSnapshot and /saveSingleFile to another library", async function () {
|
|
let group = await createGroup({ editable: true, filesEditable: false });
|
|
await selectLibrary(win);
|
|
await waitForItemsLoad(win);
|
|
let sessionID = Zotero.Utilities.randomString();
|
|
|
|
// Wait for /saveSnapshot and /saveSingleFile to items
|
|
let ids1, ids2;
|
|
let promise = waitForItemEvent('add').then(function (ids) {
|
|
ids1 = ids;
|
|
return waitForItemEvent('add').then(function (ids) {
|
|
ids2 = ids;
|
|
});
|
|
});
|
|
|
|
let title = Zotero.Utilities.randomString();
|
|
let payload = {
|
|
sessionID,
|
|
url: "http://example.com/test",
|
|
title,
|
|
singleFile: true
|
|
};
|
|
|
|
await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSnapshot",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(payload)
|
|
}
|
|
);
|
|
|
|
let body = JSON.stringify(Object.assign(payload, {
|
|
snapshotContent: '<html><head><title>Title</title><body>Body'
|
|
}));
|
|
|
|
let req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSingleFile",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body
|
|
}
|
|
);
|
|
|
|
// Check an item exists
|
|
await promise;
|
|
assert.equal(req.status, 201);
|
|
let item1 = Zotero.Items.get(ids1[0]);
|
|
assert.equal(item1.numAttachments(), 1);
|
|
|
|
// Check attachment item
|
|
let item2 = Zotero.Items.get(ids2[0]);
|
|
assert.equal(item2.libraryID, Zotero.Libraries.userLibraryID);
|
|
assert.equal(item2.parentItemID, item1.id);
|
|
|
|
// Move item to group without file attachment
|
|
promise = waitForItemEvent('add');
|
|
req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: group.treeViewID
|
|
})
|
|
}
|
|
);
|
|
|
|
// Old items are gone
|
|
let ids3 = await promise;
|
|
assert.equal(req.status, 200);
|
|
assert.isFalse(Zotero.Items.exists(item2.id));
|
|
assert.isFalse(Zotero.Items.exists(item1.id));
|
|
|
|
// New item exists
|
|
let item3 = Zotero.Items.get(ids3[0]);
|
|
assert.equal(item3.libraryID, group.libraryID);
|
|
assert.equal(item3.numAttachments(), 0);
|
|
|
|
// Move back to My Library and resave attachment
|
|
let ids4, ids5;
|
|
promise = waitForItemEvent('add').then(function (ids) {
|
|
ids4 = ids;
|
|
return waitForItemEvent('add').then(function (ids) {
|
|
ids5 = ids;
|
|
});
|
|
});
|
|
req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: Zotero.Libraries.userLibrary.treeViewID
|
|
})
|
|
}
|
|
);
|
|
|
|
await promise;
|
|
let item4 = Zotero.Items.get(ids4[0]);
|
|
let item5 = Zotero.Items.get(ids5[0]);
|
|
|
|
// Check item
|
|
assert.equal(req.status, 200);
|
|
assert.isFalse(Zotero.Items.exists(item3.id));
|
|
assert.equal(item4.libraryID, Zotero.Libraries.userLibraryID);
|
|
assert.equal(item5.libraryID, Zotero.Libraries.userLibraryID);
|
|
assert.equal(item4.numAttachments(), 1);
|
|
|
|
// Check attachment html file
|
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item5).path;
|
|
let path = OS.Path.join(attachmentDirectory, 'test.html');
|
|
assert.isTrue(await OS.File.exists(path));
|
|
let contents = await Zotero.File.getContentsAsync(path);
|
|
assert.equal(contents, '<html><head><title>Title</title><body>Body');
|
|
});
|
|
|
|
it("should resave item saved via /saveSnapshot and /saveSingleFile when moved to filesEditable library", async function () {
|
|
let group = await createGroup({ editable: true, filesEditable: false });
|
|
await selectLibrary(win);
|
|
await waitForItemsLoad(win);
|
|
let sessionID = Zotero.Utilities.randomString();
|
|
|
|
// Wait for /saveSnapshot to save parent item
|
|
let promise = waitForItemEvent('add');
|
|
|
|
let title = Zotero.Utilities.randomString();
|
|
let payload = {
|
|
sessionID,
|
|
url: "http://example.com/test",
|
|
title,
|
|
singleFile: true
|
|
};
|
|
|
|
await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSnapshot",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(payload)
|
|
}
|
|
);
|
|
|
|
// Check an item exists
|
|
let ids1 = await promise;
|
|
let item1 = Zotero.Items.get(ids1[0]);
|
|
|
|
// Move item to group without file attachment
|
|
promise = waitForItemEvent('add');
|
|
let reqPromise = Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: group.treeViewID
|
|
})
|
|
}
|
|
);
|
|
|
|
let req = await reqPromise;
|
|
assert.equal(req.status, 200);
|
|
// Assert original item no longer exists
|
|
assert.isFalse(Zotero.Items.exists(item1.id));
|
|
|
|
// Get new item
|
|
let ids2 = await promise;
|
|
let item2 = Zotero.Items.get(ids2[0]);
|
|
assert.equal(item2.libraryID, group.libraryID);
|
|
assert.equal(item2.numAttachments(), 0);
|
|
|
|
let body = JSON.stringify(Object.assign(payload, {
|
|
snapshotContent: '<html><head><title>Title</title><body>Body'
|
|
}));
|
|
|
|
req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSingleFile",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body
|
|
}
|
|
);
|
|
|
|
// Check the attachment was not saved
|
|
assert.equal(req.status, 200);
|
|
assert.equal(item2.numAttachments(), 0);
|
|
|
|
// Move back to My Library and resave attachment
|
|
let ids3, ids4;
|
|
promise = waitForItemEvent('add').then(function (ids) {
|
|
ids3 = ids;
|
|
return waitForItemEvent('add').then(function (ids) {
|
|
ids4 = ids;
|
|
});
|
|
});
|
|
req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: Zotero.Libraries.userLibrary.treeViewID
|
|
})
|
|
}
|
|
);
|
|
|
|
// Wait for item add and then attachment add
|
|
await promise;
|
|
let item3 = Zotero.Items.get(ids3[0]);
|
|
let item4 = Zotero.Items.get(ids4[0]);
|
|
|
|
// Check item
|
|
assert.equal(req.status, 200);
|
|
assert.isFalse(Zotero.Items.exists(item2.id));
|
|
assert.equal(item3.libraryID, Zotero.Libraries.userLibraryID);
|
|
assert.equal(item3.numAttachments(), 1);
|
|
|
|
// Check attachment
|
|
assert.equal(item4.libraryID, Zotero.Libraries.userLibraryID);
|
|
assert.equal(item4.parentItemID, item3.id);
|
|
// Check attachment html file
|
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item4).path;
|
|
let path = OS.Path.join(attachmentDirectory, 'test.html');
|
|
assert.isTrue(await OS.File.exists(path));
|
|
let contents = await Zotero.File.getContentsAsync(path);
|
|
assert.equal(contents, '<html><head><title>Title</title><body>Body');
|
|
});
|
|
|
|
it("should save item saved via /saveItems and /saveSingleFile to another library", async function () {
|
|
let group = await createGroup({ editable: true, filesEditable: false });
|
|
await selectLibrary(win);
|
|
await waitForItemsLoad(win);
|
|
let sessionID = Zotero.Utilities.randomString();
|
|
|
|
// Wait for /saveItems and /saveSingleFile to items
|
|
let ids1, ids2;
|
|
let promise = waitForItemEvent('add').then(function (ids) {
|
|
ids1 = ids;
|
|
return waitForItemEvent('add').then(function (ids) {
|
|
ids2 = ids;
|
|
});
|
|
});
|
|
|
|
let title = Zotero.Utilities.randomString();
|
|
let payload = {
|
|
sessionID: sessionID,
|
|
items: [
|
|
{
|
|
itemType: "newspaperArticle",
|
|
title: title,
|
|
creators: [
|
|
{
|
|
firstName: "First",
|
|
lastName: "Last",
|
|
creatorType: "author"
|
|
}
|
|
],
|
|
attachments: [
|
|
{
|
|
title: "Snapshot",
|
|
url: `https://example.com/attachment`,
|
|
mimeType: "text/html",
|
|
singleFile: true
|
|
}
|
|
]
|
|
}
|
|
],
|
|
uri: "http://example.com"
|
|
};
|
|
|
|
await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveItems",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(payload)
|
|
}
|
|
);
|
|
|
|
let body = JSON.stringify(Object.assign(payload, {
|
|
snapshotContent: '<html><head><title>Title</title><body>Body'
|
|
}));
|
|
|
|
let req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSingleFile",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body
|
|
}
|
|
);
|
|
|
|
// Check an item exists
|
|
await promise;
|
|
assert.equal(req.status, 201);
|
|
let item1 = Zotero.Items.get(ids1[0]);
|
|
assert.equal(item1.numAttachments(), 1);
|
|
|
|
// Check attachment item
|
|
let item2 = Zotero.Items.get(ids2[0]);
|
|
assert.equal(item2.libraryID, Zotero.Libraries.userLibraryID);
|
|
assert.equal(item2.parentItemID, item1.id);
|
|
|
|
// Move item to group without file attachment
|
|
promise = waitForItemEvent('add');
|
|
req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: group.treeViewID
|
|
})
|
|
}
|
|
);
|
|
|
|
// Old items are gone
|
|
let ids3 = await promise;
|
|
assert.equal(req.status, 200);
|
|
assert.isFalse(Zotero.Items.exists(item2.id));
|
|
assert.isFalse(Zotero.Items.exists(item1.id));
|
|
|
|
// New item exists
|
|
let item3 = Zotero.Items.get(ids3[0]);
|
|
assert.equal(item3.libraryID, group.libraryID);
|
|
assert.equal(item3.numAttachments(), 0);
|
|
|
|
// Move back to My Library and resave attachment
|
|
let ids4, ids5;
|
|
promise = waitForItemEvent('add').then(function (ids) {
|
|
ids4 = ids;
|
|
return waitForItemEvent('add').then(function (ids) {
|
|
ids5 = ids;
|
|
});
|
|
});
|
|
req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: Zotero.Libraries.userLibrary.treeViewID
|
|
})
|
|
}
|
|
);
|
|
|
|
await promise;
|
|
let item4 = Zotero.Items.get(ids4[0]);
|
|
let item5 = Zotero.Items.get(ids5[0]);
|
|
|
|
// Check item
|
|
assert.equal(req.status, 200);
|
|
assert.isFalse(Zotero.Items.exists(item3.id));
|
|
assert.equal(item4.libraryID, Zotero.Libraries.userLibraryID);
|
|
assert.equal(item5.libraryID, Zotero.Libraries.userLibraryID);
|
|
assert.equal(item4.numAttachments(), 1);
|
|
|
|
// Check attachment html file
|
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item5).path;
|
|
let path = OS.Path.join(attachmentDirectory, 'attachment.html');
|
|
assert.isTrue(await OS.File.exists(path));
|
|
let contents = await Zotero.File.getContentsAsync(path);
|
|
assert.equal(contents, '<html><head><title>Title</title><body>Body');
|
|
});
|
|
|
|
it("should save item saved via /saveItems and /saveSingleFile when moved to filesEditable library", async function () {
|
|
let group = await createGroup({ editable: true, filesEditable: false });
|
|
await selectLibrary(win);
|
|
await waitForItemsLoad(win);
|
|
let sessionID = Zotero.Utilities.randomString();
|
|
|
|
// Wait for /saveItems to save parent item
|
|
let promise = waitForItemEvent('add');
|
|
|
|
let title = Zotero.Utilities.randomString();
|
|
let payload = {
|
|
sessionID: sessionID,
|
|
items: [
|
|
{
|
|
itemType: "newspaperArticle",
|
|
title: title,
|
|
creators: [
|
|
{
|
|
firstName: "First",
|
|
lastName: "Last",
|
|
creatorType: "author"
|
|
}
|
|
],
|
|
attachments: [
|
|
{
|
|
title: "Snapshot",
|
|
url: `https://example.com/attachment`,
|
|
mimeType: "text/html",
|
|
singleFile: true
|
|
}
|
|
]
|
|
}
|
|
],
|
|
uri: "http://example.com"
|
|
};
|
|
|
|
await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveItems",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(payload)
|
|
}
|
|
);
|
|
|
|
// Check an item exists
|
|
let ids1 = await promise;
|
|
let item1 = Zotero.Items.get(ids1[0]);
|
|
|
|
// Move item to group without file attachment
|
|
promise = waitForItemEvent('add');
|
|
let reqPromise = Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: group.treeViewID
|
|
})
|
|
}
|
|
);
|
|
|
|
let req = await reqPromise;
|
|
assert.equal(req.status, 200);
|
|
// Assert original item no longer exists
|
|
assert.isFalse(Zotero.Items.exists(item1.id));
|
|
|
|
// Get new item
|
|
let ids2 = await promise;
|
|
let item2 = Zotero.Items.get(ids2[0]);
|
|
assert.equal(item2.libraryID, group.libraryID);
|
|
assert.equal(item2.numAttachments(), 0);
|
|
|
|
let body = JSON.stringify(Object.assign(payload, {
|
|
snapshotContent: '<html><head><title>Title</title><body>Body'
|
|
}));
|
|
|
|
req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/saveSingleFile",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body
|
|
}
|
|
);
|
|
|
|
// Check the attachment was not saved
|
|
assert.equal(req.status, 200);
|
|
assert.equal(item2.numAttachments(), 0);
|
|
|
|
// Move back to My Library and resave attachment
|
|
let ids3, ids4;
|
|
promise = waitForItemEvent('add').then(function (ids) {
|
|
ids3 = ids;
|
|
return waitForItemEvent('add').then(function (ids) {
|
|
ids4 = ids;
|
|
});
|
|
});
|
|
req = await Zotero.HTTP.request(
|
|
'POST',
|
|
connectorServerPath + "/connector/updateSession",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
sessionID,
|
|
target: Zotero.Libraries.userLibrary.treeViewID
|
|
})
|
|
}
|
|
);
|
|
|
|
// Wait for item add and then attachment add
|
|
await promise;
|
|
let item3 = Zotero.Items.get(ids3[0]);
|
|
let item4 = Zotero.Items.get(ids4[0]);
|
|
|
|
// Check item
|
|
assert.equal(req.status, 200);
|
|
assert.isFalse(Zotero.Items.exists(item2.id));
|
|
assert.equal(item3.libraryID, Zotero.Libraries.userLibraryID);
|
|
assert.equal(item3.numAttachments(), 1);
|
|
|
|
// Check attachment
|
|
assert.equal(item4.libraryID, Zotero.Libraries.userLibraryID);
|
|
assert.equal(item4.parentItemID, item3.id);
|
|
// Check attachment html file
|
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item4).path;
|
|
let path = OS.Path.join(attachmentDirectory, 'attachment.html');
|
|
assert.isTrue(await OS.File.exists(path));
|
|
let contents = await Zotero.File.getContentsAsync(path);
|
|
assert.equal(contents, '<html><head><title>Title</title><body>Body');
|
|
});
|
|
});
|
|
|
|
describe('/connector/installStyle', function() {
|
|
var endpoint;
|
|
var style;
|
|
|
|
before(function() {
|
|
endpoint = connectorServerPath + "/connector/installStyle";
|
|
style = `<?xml version="1.0" encoding="utf-8"?>
|
|
<style xmlns="http://purl.org/net/xbiblio/csl" version="1.0" default-locale="de-DE">
|
|
<info>
|
|
<title>Test1</title>
|
|
<id>http://www.example.com/test2</id>
|
|
<link href="http://www.zotero.org/styles/cell" rel="independent-parent"/>
|
|
</info>
|
|
</style>
|
|
`;
|
|
});
|
|
|
|
it('should reject styles with invalid text', function* () {
|
|
var error = yield getPromiseError(Zotero.HTTP.request(
|
|
'POST',
|
|
endpoint,
|
|
{
|
|
headers: { "Content-Type": "application/json" },
|
|
body: '{}'
|
|
}
|
|
));
|
|
assert.instanceOf(error, Zotero.HTTP.UnexpectedStatusException);
|
|
assert.equal(error.xmlhttp.status, 400);
|
|
assert.equal(error.xmlhttp.responseText, Zotero.getString("styles.installError", "(null)"));
|
|
});
|
|
|
|
it('should import a style with application/vnd.citationstyles.style+xml content-type', function* () {
|
|
sinon.stub(Zotero.Styles, 'install').callsFake(function(style) {
|
|
var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
|
|
.createInstance(Components.interfaces.nsIDOMParser),
|
|
doc = parser.parseFromString(style, "application/xml");
|
|
|
|
return Zotero.Promise.resolve({
|
|
styleTitle: Zotero.Utilities.xpathText(
|
|
doc, '/csl:style/csl:info[1]/csl:title[1]', Zotero.Styles.ns
|
|
),
|
|
styleID: Zotero.Utilities.xpathText(
|
|
doc, '/csl:style/csl:info[1]/csl:id[1]', Zotero.Styles.ns
|
|
)
|
|
});
|
|
});
|
|
|
|
var response = yield Zotero.HTTP.request(
|
|
'POST',
|
|
endpoint,
|
|
{
|
|
headers: { "Content-Type": "application/vnd.citationstyles.style+xml" },
|
|
body: style
|
|
}
|
|
);
|
|
assert.equal(response.status, 201);
|
|
assert.equal(response.response, JSON.stringify({name: 'Test1'}));
|
|
Zotero.Styles.install.restore();
|
|
});
|
|
|
|
it('should accept text/plain request with X-Zotero-Connector-API-Version or Zotero-Allowed-Request', async function () {
|
|
sinon.stub(Zotero.Styles, 'install').callsFake(function(style) {
|
|
var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
|
|
.createInstance(Components.interfaces.nsIDOMParser),
|
|
doc = parser.parseFromString(style, "application/xml");
|
|
|
|
return Zotero.Promise.resolve({
|
|
styleTitle: Zotero.Utilities.xpathText(
|
|
doc, '/csl:style/csl:info[1]/csl:title[1]', Zotero.Styles.ns
|
|
),
|
|
styleID: Zotero.Utilities.xpathText(
|
|
doc, '/csl:style/csl:info[1]/csl:id[1]', Zotero.Styles.ns
|
|
)
|
|
});
|
|
});
|
|
|
|
// X-Zotero-Connector-API-Version
|
|
var response = await Zotero.HTTP.request(
|
|
'POST',
|
|
endpoint,
|
|
{
|
|
headers: {
|
|
"Content-Type": "text/plain",
|
|
"X-Zotero-Connector-API-Version": "2"
|
|
},
|
|
body: style
|
|
}
|
|
);
|
|
assert.equal(response.status, 201);
|
|
|
|
// Zotero-Allowed-Request
|
|
response = await Zotero.HTTP.request(
|
|
'POST',
|
|
endpoint,
|
|
{
|
|
headers: {
|
|
"Content-Type": "text/plain",
|
|
"Zotero-Allowed-Request": "1"
|
|
},
|
|
body: style
|
|
}
|
|
);
|
|
assert.equal(response.status, 201);
|
|
|
|
Zotero.Styles.install.restore();
|
|
});
|
|
|
|
it('should reject text/plain request without X-Zotero-Connector-API-Version', async function () {
|
|
var req = await Zotero.HTTP.request(
|
|
'POST',
|
|
endpoint,
|
|
{
|
|
headers: {
|
|
"Content-Type": "text/plain"
|
|
},
|
|
body: style,
|
|
successCodes: [403]
|
|
}
|
|
);
|
|
assert.equal(req.status, 403);
|
|
});
|
|
});
|
|
|
|
describe('/connector/import', function() {
|
|
var endpoint;
|
|
|
|
before(function() {
|
|
endpoint = connectorServerPath + "/connector/import";
|
|
});
|
|
|
|
it('should reject resources that do not contain import data', function* () {
|
|
var error = yield getPromiseError(Zotero.HTTP.request(
|
|
'POST',
|
|
endpoint,
|
|
{
|
|
headers: {
|
|
"Content-Type": "text/plain",
|
|
"X-Zotero-Connector-API-Version": "2"
|
|
},
|
|
body: 'Owl'
|
|
}
|
|
));
|
|
assert.instanceOf(error, Zotero.HTTP.UnexpectedStatusException);
|
|
assert.equal(error.xmlhttp.status, 400);
|
|
});
|
|
|
|
it('should reject requests without X-Zotero-Connector-API-Version', async function () {
|
|
var req = await Zotero.HTTP.request(
|
|
'POST',
|
|
endpoint,
|
|
{
|
|
headers: {
|
|
"Content-Type": "text/plain"
|
|
},
|
|
successCodes: [403]
|
|
}
|
|
);
|
|
assert.equal(req.status, 403);
|
|
});
|
|
|
|
it('should import resources (BibTeX) into selected collection', function* () {
|
|
var collection = yield createDataObject('collection');
|
|
yield waitForItemsLoad(win);
|
|
|
|
var resource = `@book{test1,
|
|
title={Test1},
|
|
author={Owl},
|
|
year={1000},
|
|
publisher={Curly Braces Publishing},
|
|
keywords={A, B}
|
|
}`;
|
|
|
|
var addedItemIDsPromise = waitForItemEvent('add');
|
|
var req = yield Zotero.HTTP.request(
|
|
'POST',
|
|
endpoint,
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/x-bibtex",
|
|
"X-Zotero-Connector-API-Version": "2"
|
|
},
|
|
body: resource
|
|
}
|
|
);
|
|
assert.equal(req.status, 201);
|
|
assert.equal(JSON.parse(req.responseText)[0].title, 'Test1');
|
|
|
|
let itemIDs = yield addedItemIDsPromise;
|
|
assert.isTrue(collection.hasItem(itemIDs[0]));
|
|
var item = Zotero.Items.get(itemIDs[0]);
|
|
assert.sameDeepMembers(item.getTags(), [{ tag: 'A', type: 1 }, { tag: 'B', type: 1 }]);
|
|
});
|
|
|
|
|
|
it('should switch to My Library if read-only library is selected', function* () {
|
|
var group = yield createGroup({
|
|
editable: false
|
|
});
|
|
yield selectLibrary(win, group.libraryID);
|
|
yield waitForItemsLoad(win);
|
|
|
|
var resource = `@book{test1,
|
|
title={Test1},
|
|
author={Owl},
|
|
year={1000},
|
|
publisher={Curly Braces Publishing}
|
|
}`;
|
|
|
|
var addedItemIDsPromise = waitForItemEvent('add');
|
|
var req = yield Zotero.HTTP.request(
|
|
'POST',
|
|
endpoint,
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/x-bibtex",
|
|
"X-Zotero-Connector-API-Version": "2"
|
|
},
|
|
body: resource,
|
|
successCodes: false
|
|
}
|
|
);
|
|
|
|
assert.equal(req.status, 201);
|
|
assert.equal(
|
|
win.ZoteroPane.collectionsView.getSelectedLibraryID(),
|
|
Zotero.Libraries.userLibraryID
|
|
);
|
|
|
|
let itemIDs = yield addedItemIDsPromise;
|
|
var item = Zotero.Items.get(itemIDs[0]);
|
|
assert.equal(item.libraryID, Zotero.Libraries.userLibraryID);
|
|
});
|
|
});
|
|
});
|