Merge branch '4.0' into sjk/659

This commit is contained in:
Simon Kornblith 2015-05-31 23:59:15 -04:00
commit 9bb01d737c
21 changed files with 10319 additions and 158 deletions

View file

@ -723,6 +723,7 @@ var Zotero_Browser = new function() {
Zotero_Browser.progress.show();
Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.lookup.performing"));
tab.page.translate.translate(false);
e.stopPropagation();
}
}

View file

@ -510,12 +510,6 @@ Zotero.Cite.System.prototype = {
return embeddedCitation;
}
}
} else {
// is an item ID
//if(this._cache[item]) return this._cache[item];
try {
zoteroItem = Zotero.Items.get(item);
} catch(e) {}
}
if(!zoteroItem) {
@ -524,6 +518,9 @@ Zotero.Cite.System.prototype = {
var cslItem = Zotero.Utilities.itemToCSLJSON(zoteroItem);
// TEMP: citeproc-js currently expects the id property to be the item DB id
cslItem.id = zoteroItem.id;
if (!Zotero.Prefs.get("export.citePaperJournalArticleURL")) {
var itemType = Zotero.ItemTypes.getName(zoteroItem.itemTypeID);
// don't return URL or accessed information for journal articles if a

View file

@ -4181,6 +4181,7 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options) {
obj.dateAdded = Zotero.Date.sqlToISO8601(this.dateAdded);
obj.dateModified = Zotero.Date.sqlToISO8601(this.dateModified);
if (obj.accessDate) obj.accessDate = Zotero.Date.sqlToISO8601(obj.accessDate);
if (mode == 'patch') {
for (let i in options.patchBase) {

View file

@ -2161,6 +2161,10 @@ Zotero.Translate.Export.prototype._prepareTranslation = function() {
// initialize ItemGetter
this._itemGetter = new Zotero.Translate.ItemGetter();
// Toggle legacy mode for translators pre-4.0.27
this._itemGetter.legacy = Services.vc.compare('4.0.27', this._translatorInfo.minVersion) > 0;
var configOptions = this._translatorInfo.configOptions || {},
getCollections = configOptions.getCollections || false;
switch (this._export.type) {

View file

@ -83,7 +83,8 @@ Zotero.Translate.ItemSaver.prototype = {
* @param items Items in Zotero.Item.toArray() format
* @param {Function} callback A callback to be executed when saving is complete. If saving
* succeeded, this callback will be passed true as the first argument and a list of items
* saved as the second. If saving failed, the callback will be passed false as the first
* saved as the second. If
saving failed, the callback will be passed false as the first
* argument and an error object as the second
* @param {Function} [attachmentCallback] A callback that receives information about attachment
* save progress. The callback will be called as attachmentCallback(attachment, false, error)
@ -700,9 +701,10 @@ Zotero.Translate.ItemSaver.prototype = {
}
Zotero.Translate.ItemGetter = function() {
this._itemsLeft = null;
this._itemsLeft = [];
this._collectionsLeft = null;
this._exportFileDirectory = null;
this.legacy = false;
};
Zotero.Translate.ItemGetter.prototype = {
@ -782,14 +784,9 @@ Zotero.Translate.ItemGetter.prototype = {
/**
* Converts an attachment to array format and copies it to the export folder if desired
*/
"_attachmentToArray":function(attachment) {
var attachmentArray = this._itemToArray(attachment);
"_attachmentToArray":Zotero.Promise.coroutine(function* (attachment) {
var attachmentArray = yield Zotero.Utilities.Internal.itemToExportFormat(attachment, this.legacy);
var linkMode = attachment.attachmentLinkMode;
// Get mime type
attachmentArray.mimeType = attachmentArray.uniqueFields.mimeType = attachment.attachmentMIMEType;
// Get charset
attachmentArray.charset = attachmentArray.uniqueFields.charset = attachment.attachmentCharset;
if(linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
var attachFile = attachment.getFile();
attachmentArray.localPath = attachFile.path;
@ -800,7 +797,7 @@ Zotero.Translate.ItemGetter.prototype = {
// Add path and filename if not an internet link
var attachFile = attachment.getFile();
if(attachFile) {
attachmentArray.defaultPath = "files/" + attachmentArray.itemID + "/" + attachFile.leafName;
attachmentArray.defaultPath = "files/" + attachment.id + "/" + attachFile.leafName;
attachmentArray.filename = attachFile.leafName;
/**
@ -914,59 +911,28 @@ Zotero.Translate.ItemGetter.prototype = {
}
}
attachmentArray.itemType = "attachment";
return attachmentArray;
},
/**
* Converts an item to array format
*/
"_itemToArray":function(returnItem) {
// TODO use Zotero.Item#serialize()
var returnItemArray = returnItem.toArray();
// Remove SQL date from multipart dates
if (returnItemArray.date) {
returnItemArray.date = Zotero.Date.multipartToStr(returnItemArray.date);
}
var returnItemArray = Zotero.Utilities.itemToExportFormat(returnItemArray);
// TODO: Change tag.tag references in translators to tag.name
// once translators are 1.5-only
// TODO: Preserve tag type?
if (returnItemArray.tags) {
for (var i in returnItemArray.tags) {
returnItemArray.tags[i].tag = returnItemArray.tags[i].fields.name;
}
}
// add URI
returnItemArray.uri = Zotero.URI.getItemURI(returnItem);
return returnItemArray;
},
}),
/**
* Retrieves the next available item
*/
"nextItem":function() {
"nextItem":Zotero.Promise.coroutine(function* () {
while(this._itemsLeft.length != 0) {
var returnItem = this._itemsLeft.shift();
// export file data for single files
if(returnItem.isAttachment()) { // an independent attachment
var returnItemArray = this._attachmentToArray(returnItem);
var returnItemArray = yield this._attachmentToArray(returnItem);
if(returnItemArray) return returnItemArray;
} else {
var returnItemArray = this._itemToArray(returnItem);
var returnItemArray = yield Zotero.Utilities.Internal.itemToExportFormat(returnItem, this.legacy);
// get attachments, although only urls will be passed if exportFileData is off
returnItemArray.attachments = new Array();
returnItemArray.attachments = [];
var attachments = returnItem.getAttachments();
for each(var attachmentID in attachments) {
var attachment = Zotero.Items.get(attachmentID);
var attachmentInfo = this._attachmentToArray(attachment);
var attachment = yield Zotero.Items.getAsync(attachmentID);
var attachmentInfo = yield this._attachmentToArray(attachment);
if(attachmentInfo) {
returnItemArray.attachments.push(attachmentInfo);
@ -977,7 +943,7 @@ Zotero.Translate.ItemGetter.prototype = {
}
}
return false;
},
}),
"nextCollection":function() {
if(!this._collectionsLeft || this._collectionsLeft.length == 0) return false;

View file

@ -61,7 +61,7 @@ const CSL_TEXT_MAPPINGS = {
"number-of-volumes":["numberOfVolumes"],
"number-of-pages":["numPages"],
"edition":["edition"],
"version":["version"],
"version":["versionNumber"],
"section":["section", "committee"],
"genre":["type", "programmingLanguage"],
"source":["libraryCatalog"],
@ -133,7 +133,10 @@ const CSL_TYPE_MAPPINGS = {
'tvBroadcast':"broadcast",
'radioBroadcast':"broadcast",
'podcast':"song", // ??
'computerProgram':"book" // ??
'computerProgram':"book", // ??
'document':"article",
'note':"article",
'attachment':"article"
};
/**
@ -1425,49 +1428,6 @@ Zotero.Utilities = {
return dumpedText;
},
/**
* Adds all fields to an item in toArray() format and adds a unique (base) fields to
* uniqueFields array
*/
"itemToExportFormat":function(item) {
const CREATE_ARRAYS = ['creators', 'notes', 'tags', 'seeAlso', 'attachments'];
for(var i=0; i<CREATE_ARRAYS.length; i++) {
var createArray = CREATE_ARRAYS[i];
if(!item[createArray]) item[createArray] = [];
}
item.uniqueFields = {};
// get base fields, not just the type-specific ones
var itemTypeID = (item.itemTypeID ? item.itemTypeID : Zotero.ItemTypes.getID(item.itemType));
var allFields = Zotero.ItemFields.getItemTypeFields(itemTypeID);
for(var i in allFields) {
var field = allFields[i];
var fieldName = Zotero.ItemFields.getName(field);
if(item[fieldName] !== undefined) {
var baseField = Zotero.ItemFields.getBaseIDFromTypeAndField(itemTypeID, field);
var baseName = null;
if(baseField && baseField != field) {
baseName = Zotero.ItemFields.getName(baseField);
}
if(baseName) {
item[baseName] = item[fieldName];
item.uniqueFields[baseName] = item[fieldName];
} else {
item.uniqueFields[fieldName] = item[fieldName];
}
}
}
// preserve notes
if(item.note) item.uniqueFields.note = item.note;
return item;
},
/**
* Converts an item from toArray() format to an array of items in
* the content=json format used by the server
@ -1607,14 +1567,19 @@ Zotero.Utilities = {
*/
"itemToCSLJSON":function(zoteroItem) {
if (zoteroItem instanceof Zotero.Item) {
zoteroItem = zoteroItem.toArray();
return Zotero.Utilities.Internal.itemToExportFormat(zoteroItem).
then(Zotero.Utilities.itemToCSLJSON);
}
var cslType = CSL_TYPE_MAPPINGS[zoteroItem.itemType];
if (!cslType) {
throw new Error('Unexpected Zotero Item type "' + zoteroItem.itemType + '"');
}
var cslType = CSL_TYPE_MAPPINGS[zoteroItem.itemType] || "article";
var itemTypeID = Zotero.ItemTypes.getID(zoteroItem.itemType);
var cslItem = {
'id':zoteroItem.itemID,
'id':zoteroItem.uri,
'type':cslType
};
@ -1628,11 +1593,13 @@ Zotero.Utilities = {
if(field in zoteroItem) {
value = zoteroItem[field];
} else {
if (field == 'versionNumber') field = 'version'; // Until https://github.com/zotero/zotero/issues/670
var fieldID = Zotero.ItemFields.getID(field),
baseMapping;
if(Zotero.ItemFields.isValidForType(fieldID, itemTypeID)
&& (baseMapping = Zotero.ItemFields.getBaseIDFromTypeAndField(itemTypeID, fieldID))) {
value = zoteroItem[Zotero.ItemTypes.getName(baseMapping)];
typeFieldID;
if(fieldID
&& (typeFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, fieldID))
) {
value = zoteroItem[Zotero.ItemFields.getName(typeFieldID)];
}
}
@ -1656,30 +1623,39 @@ Zotero.Utilities = {
}
// separate name variables
var author = Zotero.CreatorTypes.getName(Zotero.CreatorTypes.getPrimaryIDForType(itemTypeID));
var creators = zoteroItem.creators;
for(var i=0; i<creators.length; i++) {
var creator = creators[i];
var creatorType = creator.creatorType;
if(creatorType == author) {
creatorType = "author";
}
creatorType = CSL_NAMES_MAPPINGS[creatorType];
if(!creatorType) continue;
var nameObj = {'family':creator.lastName, 'given':creator.firstName};
if(cslItem[creatorType]) {
cslItem[creatorType].push(nameObj);
} else {
cslItem[creatorType] = [nameObj];
if (zoteroItem.type != "attachment" && zoteroItem.type != "note") {
var author = Zotero.CreatorTypes.getName(Zotero.CreatorTypes.getPrimaryIDForType(itemTypeID));
var creators = zoteroItem.creators;
for(var i=0; creators && i<creators.length; i++) {
var creator = creators[i];
var creatorType = creator.creatorType;
if(creatorType == author) {
creatorType = "author";
}
creatorType = CSL_NAMES_MAPPINGS[creatorType];
if(!creatorType) continue;
var nameObj = {'family':creator.lastName, 'given':creator.firstName};
if(cslItem[creatorType]) {
cslItem[creatorType].push(nameObj);
} else {
cslItem[creatorType] = [nameObj];
}
}
}
// get date variables
for(var variable in CSL_DATE_MAPPINGS) {
var date = zoteroItem[CSL_DATE_MAPPINGS[variable]];
if (!date) {
var typeSpecificFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, CSL_DATE_MAPPINGS[variable]);
if (typeSpecificFieldID) {
date = zoteroItem[Zotero.ItemFields.getName(typeSpecificFieldID)];
}
}
if(date) {
var dateObj = Zotero.Date.strToDate(date);
// otherwise, use date-parts
@ -1705,7 +1681,12 @@ Zotero.Utilities = {
}
}
}
// Special mapping for note title
if (zoteroItem.itemType == 'note' && zoteroItem.note) {
cslItem.title = Zotero.Notes.noteToTitle(zoteroItem.note);
}
// extract PMID
var extra = zoteroItem.extra;
if(typeof extra === "string") {

View file

@ -251,7 +251,6 @@ Zotero.Utilities.Internal = {
return s;
},
/**
* Display a prompt from an error with custom buttons and a callback
*/
@ -594,6 +593,139 @@ Zotero.Utilities.Internal = {
},
/**
* Converts Zotero.Item to a format expected by translators
* This is mostly the Zotero web API item JSON format, but with an attachments
* and notes arrays and optional compatibility mappings for older translators.
*
* @param {Zotero.Item} zoteroItem
* @param {Boolean} legacy Add mappings for legacy (pre-4.0.27) translators
* @return {Object}
*/
"itemToExportFormat": new function() {
return Zotero.Promise.coroutine(function* (zoteroItem, legacy) {
var item = yield zoteroItem.toJSON();
item.uri = Zotero.URI.getItemURI(zoteroItem);
delete item.key;
if (!zoteroItem.isAttachment() && !zoteroItem.isNote()) {
// Include attachments
item.attachments = [];
let attachments = zoteroItem.getAttachments();
for (let i=0; i<attachments.length; i++) {
let zoteroAttachment = yield Zotero.Items.getAsync(attachments[i]),
attachment = yield zoteroAttachment.toJSON();
if (legacy) addCompatibilityMappings(attachment, zoteroAttachment);
item.attachments.push(attachment);
}
// Include notes
item.notes = [];
let notes = zoteroItem.getNotes();
for (let i=0; i<notes.length; i++) {
let zoteroNote = yield Zotero.Items.getAsync(notes[i]),
note = yield zoteroNote.toJSON();
if (legacy) addCompatibilityMappings(note, zoteroNote);
item.notes.push(note);
}
}
if (legacy) addCompatibilityMappings(item, zoteroItem);
return item;
});
function addCompatibilityMappings(item, zoteroItem) {
item.uniqueFields = {};
// Meaningless local item ID, but some older export translators depend on it
item.itemID = zoteroItem.id;
item.key = zoteroItem.key; // CSV translator exports this
// "version" is expected to be a field for "computerProgram", which is now
// called "versionNumber"
delete item.version;
if (item.versionNumber) {
item.version = item.uniqueFields.version = item.versionNumber;
delete item.versionNumber;
}
// SQL instead of ISO-8601
item.dateAdded = zoteroItem.dateAdded;
item.dateModified = zoteroItem.dateModified;
if (item.accessDate) {
item.accessDate = zoteroItem.getField('accessDate');
}
// Map base fields
for (let field in item) {
let id = Zotero.ItemFields.getID(field);
if (!id || !Zotero.ItemFields.isValidForType(id, zoteroItem.itemTypeID)) {
continue;
}
let baseField = Zotero.ItemFields.getName(
Zotero.ItemFields.getBaseIDFromTypeAndField(item.itemType, field)
);
if (!baseField || baseField == field) {
item.uniqueFields[field] = item[field];
} else {
item[baseField] = item[field];
item.uniqueFields[baseField] = item[field];
}
}
// Add various fields for compatibility with translators pre-4.0.27
item.itemID = zoteroItem.id;
item.libraryID = zoteroItem.libraryID == 1 ? null : zoteroItem.libraryID;
// Creators
if (item.creators) {
for (let i=0; i<item.creators.length; i++) {
let creator = item.creators[i];
if (creator.name) {
creator.fieldMode = 1;
creator.lastName = creator.name;
delete creator.name;
}
// Old format used to supply creatorID (the database ID), but no
// translator ever used it
}
}
if (!zoteroItem.isRegularItem()) {
item.sourceItemKey = item.parentItem;
}
// Tags
for (let i=0; i<item.tags.length; i++) {
if (!item.tags[i].type) {
item.tags[i].type = 0;
}
// No translator ever used "primary", "fields", or "linkedItems" objects
}
// "related" was never used (array of itemIDs)
// seeAlso was always present, but it was always an empty array.
// Zotero RDF translator pretended to use it
item.seeAlso = [];
// Fix linkMode
if (zoteroItem.isAttachment()) {
item.linkMode = zoteroItem.attachmentLinkMode;
item.mimeType = item.contentType;
}
return item;
}
},
/**
* Hyphenate an ISBN based on the registrant table available from
* https://www.isbn-international.org/range_file_generation

View file

@ -31,8 +31,11 @@ function ZoteroUnit() {
ZoteroUnit.prototype = {
/* nsICommandLineHandler */
handle:function(cmdLine) {
this.tests = cmdLine.handleFlagWithParam("test", false);
this.noquit = cmdLine.handleFlag("noquit", false);
this.tests = cmdLine.handleFlagWithParam("test", false);
this.noquit = cmdLine.handleFlag("noquit", false);
this.makeTestData = cmdLine.handleFlag("makeTestData", false);
this.noquit = !this.makeTestData && this.noquit;
this.runTests = !this.makeTestData;
this.bail = cmdLine.handleFlag("bail", false);
this.grep = cmdLine.handleFlagWithParam("grep", false);
},

View file

@ -1,16 +1,16 @@
Components.utils.import("resource://gre/modules/FileUtils.jsm");
Components.utils.import("resource://gre/modules/osfile.jsm");
var EventUtils = Components.utils.import("resource://zotero-unit/EventUtils.jsm");
var ZoteroUnit = Components.classes["@mozilla.org/commandlinehandler/general-startup;1?type=zotero-unit"].
getService(Components.interfaces.nsISupports).
wrappedJSObject;
getService(Components.interfaces.nsISupports).
wrappedJSObject;
var dump = ZoteroUnit.dump;
function quit(failed) {
// Quit with exit status
if(!failed) {
OS.File.writeAtomic(FileUtils.getFile("ProfD", ["success"]).path, new Uint8Array(0));
OS.File.writeAtomic(OS.Path.join(OS.Constants.Path.profileDir, "success"), new Uint8Array(0));
}
if(!ZoteroUnit.noquit) {
setTimeout(function () {
@ -21,6 +21,69 @@ function quit(failed) {
}
}
if (ZoteroUnit.makeTestData) {
let dataPath = getTestDataDirectory().path;
Zotero.Prefs.set("export.citePaperJournalArticleURL", true);
let dataFiles = [
{
name: 'allTypesAndFields',
func: generateAllTypesAndFieldsData
},
{
name: 'itemJSON',
func: generateItemJSONData,
args: [null]
},
// {
// name: 'citeProcJSExport',
// func: generateCiteProcJSExportData
// },
{
name: 'translatorExportLegacy',
func: generateTranslatorExportData,
args: [true]
},
{
name: 'translatorExport',
func: generateTranslatorExportData,
args: [false]
}
];
Zotero.Promise.coroutine(function* () {
yield Zotero.initializationPromise;
for (let i=0; i<dataFiles.length; i++) {
let first = !i;
let params = dataFiles[i];
// Make sure to not run next loop if previous fails
if (!first) dump('\n');
dump('Generating data for ' + params.name + '...');
let filePath = OS.Path.join(dataPath, params.name + '.js');
let exists = yield OS.File.exists(filePath);
let currentData;
if (exists) {
currentData = loadSampleData(params.name);
}
let args = params.args || [];
args.push(currentData);
let newData = params.func.apply(null, args);
if (newData instanceof Zotero.Promise) {
newData = yield newData;
}
let str = stableStringify(newData);
yield OS.File.writeAtomic(OS.Path.join(dataPath, params.name + '.js'), str);
dump("done.");
}
})()
.catch(function(e) { dump('\n'); dump(Zotero.Utilities.varDump(e)) })
.finally(function() { quit(false) });
}
function Reporter(runner) {
var indents = 0, passed = 0, failed = 0, aborted = false;
@ -41,7 +104,7 @@ function Reporter(runner) {
});
runner.on('pending', function(test){
dump(indent()+"pending -"+test.title);
dump("\r"+indent()+"pending -"+test.title+"\n");
});
runner.on('pass', function(test){
@ -75,6 +138,21 @@ function Reporter(runner) {
});
}
// Monkey-patch Mocha to check instanceof Error using compartent-local
// Error object
Mocha.Runner.prototype.fail = function(test, err){
++this.failures;
test.state = 'failed';
if ('string' == typeof err) {
err = new Error('the string "' + err + '" was thrown, throw an Error :)');
} else if (!(err instanceof Components.utils.getGlobalForObject(err).Error)) {
err = new Error('the ' + Mocha.utils.type(err) + ' ' + Mocha.utils.stringify(err) + ' was thrown, throw an Error :)');
}
this.emit('fail', test, err);
};
// Setup Mocha
mocha.setup({
ui: "bdd",
@ -99,8 +177,8 @@ var assert = chai.assert,
expect = chai.expect;
// Set up tests to run
var run = true;
if(ZoteroUnit.tests) {
var run = ZoteroUnit.runTests;
if(run && ZoteroUnit.tests) {
var testDirectory = getTestDataDirectory().parent,
testFiles = [];
if(ZoteroUnit.tests == "all") {

View file

@ -1,3 +1,8 @@
// Useful "constants"
var sqlDateTimeRe = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
var isoDateTimeRe = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
var zoteroObjectKeyRe = /^[23456789ABCDEFGHIJKLMNPQRSTUVWXYZ]{8}$/; // based on Zotero.Utilities::generateObjectKey()
/**
* Waits for a DOM event on the specified node. Returns a promise
* resolved with the event.
@ -217,8 +222,8 @@ function uninstallPDFTools() {
}
/**
* Returns a promise for the nsIFile corresponding to the test data
* directory (i.e., test/tests/data)
* Returns the nsIFile corresponding to the test data directory
* (i.e., test/tests/data)
*/
function getTestDataDirectory() {
Components.utils.import("resource://gre/modules/Services.jsm");
@ -229,6 +234,28 @@ function getTestDataDirectory() {
QueryInterface(Components.interfaces.nsIFileURL).file;
}
/**
* Returns an absolute path to an empty temporary directory
* (i.e., test/tests/data)
*/
var getTempDirectory = Zotero.Promise.coroutine(function* getTempDirectory() {
Components.utils.import("resource://gre/modules/osfile.jsm");
let path,
attempts = 3,
zoteroTmpDirPath = Zotero.getTempDirectory().path;
while (attempts--) {
path = OS.Path.join(zoteroTmpDirPath, Zotero.Utilities.randomString());
try {
yield OS.File.makeDir(path, { ignoreExisting: false });
break;
} catch (e) {
if (!attempts) throw e; // Throw on last attempt
}
}
return path;
});
/**
* Resets the Zotero DB and restarts Zotero. Returns a promise resolved
* when this finishes.
@ -242,6 +269,253 @@ function resetDB() {
});
}
/**
* Equivalent to JSON.stringify, except that object properties are stringified
* in a sorted order.
*/
function stableStringify(obj) {
return JSON.stringify(obj, function(k, v) {
if (v && typeof v == "object" && !Array.isArray(v)) {
let o = {},
keys = Object.keys(v).sort();
for (let i = 0; i < keys.length; i++) {
o[keys[i]] = v[keys[i]];
}
return o;
}
return v;
}, "\t");
}
/**
* Loads specified sample data from file
*/
function loadSampleData(dataName) {
let data = Zotero.File.getContentsFromURL('resource://zotero-unit-tests/data/' + dataName + '.js');
return JSON.parse(data);
}
/**
* Generates sample item data that is stored in data/sampleItemData.js
*/
function generateAllTypesAndFieldsData() {
let data = {};
let itemTypes = Zotero.ItemTypes.getTypes();
// For most fields, use the field name as the value, but this doesn't
// work well for some fields that expect values in certain formats
let specialValues = {
date: '1999-12-31',
filingDate: '2000-01-02',
accessDate: '1997-06-13 23:59:58',
number: 3,
numPages: 4,
issue: 5,
volume: 6,
numberOfVolumes: 7,
edition: 8,
seriesNumber: 9,
ISBN: '978-1-234-56789-7',
ISSN: '1234-5679',
url: 'http://www.example.com',
pages: '1-10',
DOI: '10.1234/example.doi',
runningTime: '1:22:33',
language: 'en-US'
};
// Item types that should not be included in sample data
let excludeItemTypes = ['note', 'attachment'];
for (let i = 0; i < itemTypes.length; i++) {
if (excludeItemTypes.indexOf(itemTypes[i].name) != -1) continue;
let itemFields = data[itemTypes[i].name] = {
itemType: itemTypes[i].name
};
let fields = Zotero.ItemFields.getItemTypeFields(itemTypes[i].id);
for (let j = 0; j < fields.length; j++) {
let field = fields[j];
field = Zotero.ItemFields.getBaseIDFromTypeAndField(itemTypes[i].id, field) || field;
let name = Zotero.ItemFields.getName(field),
value;
// Use field name as field value
if (specialValues[name]) {
value = specialValues[name];
} else {
value = name.charAt(0).toUpperCase() + name.substr(1);
// Make it look nice (sentence case)
value = value.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/ [A-Z](?![A-Z])/g, m => m.toLowerCase()); // not all-caps words
}
itemFields[name] = value;
}
let creatorTypes = Zotero.CreatorTypes.getTypesForItemType(itemTypes[i].id),
creators = itemFields.creators = [];
for (let j = 0; j < creatorTypes.length; j++) {
let typeName = creatorTypes[j].name;
creators.push({
creatorType: typeName,
firstName: typeName + 'First',
lastName: typeName + 'Last'
});
}
}
return data;
}
/**
* Populates the database with sample items
* The field values should be in the form exactly as they would appear in Zotero
*/
function populateDBWithSampleData(data) {
return Zotero.DB.executeTransaction(function* () {
for (let itemName in data) {
let item = data[itemName];
let zItem = new Zotero.Item(item.itemType);
for (let itemField in item) {
if (itemField == 'itemType') continue;
if (itemField == 'creators') {
zItem.setCreators(item[itemField]);
continue;
}
if (itemField == 'tags') {
// Must save item first
continue;
}
zItem.setField(itemField, item[itemField]);
}
if (item.tags && item.tags.length) {
for (let i=0; i<item.tags.length; i++) {
zItem.addTag(item.tags[i].tag, item.tags[i].type);
}
}
item.id = yield zItem.save();
}
return data;
});
}
var generateItemJSONData = Zotero.Promise.coroutine(function* generateItemJSONData(options, currentData) {
let items = yield populateDBWithSampleData(loadSampleData('allTypesAndFields')),
jsonData = {};
for (let itemName in items) {
let zItem = yield Zotero.Items.getAsync(items[itemName].id);
jsonData[itemName] = yield zItem.toJSON(options);
// Don't replace some fields that _always_ change (e.g. item keys)
// as long as it follows expected format
// This makes it easier to generate more meaningful diffs
if (!currentData || !currentData[itemName]) continue;
for (let field in jsonData[itemName]) {
let oldVal = currentData[itemName][field];
if (!oldVal) continue;
let val = jsonData[itemName][field];
switch (field) {
case 'dateAdded':
case 'dateModified':
if (!isoDateTimeRe.test(oldVal) || !isoDateTimeRe.test(val)) continue;
break;
case 'key':
if (!zoteroObjectKeyRe.test(oldVal) || !zoteroObjectKeyRe.test(val)) continue;
break;
default:
continue;
}
jsonData[itemName][field] = oldVal;
}
}
return jsonData;
});
var generateCiteProcJSExportData = Zotero.Promise.coroutine(function* generateCiteProcJSExportData(currentData) {
let items = yield populateDBWithSampleData(loadSampleData('allTypesAndFields')),
cslExportData = {};
for (let itemName in items) {
let zItem = yield Zotero.Items.getAsync(items[itemName].id);
cslExportData[itemName] = Zotero.Cite.System.prototype.retrieveItem(zItem);
if (!currentData || !currentData[itemName]) continue;
// Don't replace id as long as it follows expected format
if (Number.isInteger(currentData[itemName].id)
&& Number.isInteger(cslExportData[itemName].id)
) {
cslExportData[itemName].id = currentData[itemName].id;
}
}
return cslExportData;
});
var generateTranslatorExportData = Zotero.Promise.coroutine(function* generateTranslatorExportData(legacy, currentData) {
let items = yield populateDBWithSampleData(loadSampleData('allTypesAndFields')),
translatorExportData = {};
let itemGetter = new Zotero.Translate.ItemGetter();
itemGetter.legacy = !!legacy;
for (let itemName in items) {
let zItem = yield Zotero.Items.getAsync(items[itemName].id);
itemGetter._itemsLeft = [zItem];
translatorExportData[itemName] = yield itemGetter.nextItem();
// Don't replace some fields that _always_ change (e.g. item keys)
if (!currentData || !currentData[itemName]) continue;
// For simplicity, be more lenient than for item key
let uriRe = /^http:\/\/zotero\.org\/users\/local\/\w{8}\/items\/\w{8}$/;
let itemIDRe = /^\d+$/;
for (let field in translatorExportData[itemName]) {
let oldVal = currentData[itemName][field];
if (!oldVal) continue;
let val = translatorExportData[itemName][field];
switch (field) {
case 'uri':
if (!uriRe.test(oldVal) || !uriRe.test(val)) continue;
break;
case 'itemID':
if (!itemIDRe.test(oldVal) || !itemIDRe.test(val)) continue;
break;
case 'key':
if (!zoteroObjectKeyRe.test(oldVal) || !zoteroObjectKeyRe.test(val)) continue;
break;
case 'dateAdded':
case 'dateModified':
if (legacy) {
if (!sqlDateTimeRe.test(oldVal) || !sqlDateTimeRe.test(val)) continue;
} else {
if (!isoDateTimeRe.test(oldVal) || !isoDateTimeRe.test(val)) continue;
}
break;
default:
continue;
}
translatorExportData[itemName][field] = oldVal;
}
}
return translatorExportData;
});
/**
* Imports an attachment from a test file.
@ -253,4 +527,3 @@ function importFileAttachment(filename) {
filename.split('/').forEach((part) => testfile.append(part));
return Zotero.Attachments.importFromFile({file: testfile});
}

View file

@ -15,48 +15,60 @@ function makePath {
}
DEBUG=false
if [ "`uname`" == "Darwin" ]; then
FX_EXECUTABLE="/Applications/Firefox.app/Contents/MacOS/firefox"
else
FX_EXECUTABLE="firefox"
if [ -z "$FX_EXECUTABLE" ]; then
if [ "`uname`" == "Darwin" ]; then
FX_EXECUTABLE="/Applications/Firefox.app/Contents/MacOS/firefox"
else
FX_EXECUTABLE="firefox"
fi
fi
FX_ARGS=""
function usage {
cat >&2 <<DONE
Usage: $0 [-x FX_EXECUTABLE] [TESTS...]
Usage: $0 [option] [TESTS...]
Options
-x FX_EXECUTABLE path to Firefox executable (default: $FX_EXECUTABLE)
-d enable debug logging
-b skip bundled translator/style installation
-c open JavaScript console and don't quit on completion
-d LEVEL enable debug logging
-f stop after first test failure
-g only run tests matching the given pattern (grep)
-t generate test data and quit
-x FX_EXECUTABLE path to Firefox executable (default: $FX_EXECUTABLE)
-b skip bundled translator/style installation
TESTS set of tests to run (default: all)
DONE
exit 1
}
while getopts "x:dcfg:b" opt; do
while getopts "bcd:fg:tx:" opt; do
case $opt in
x)
FX_EXECUTABLE="$OPTARG"
b)
FX_ARGS="$FX_ARGS -ZoteroSkipBundledFiles"
;;
c)
FX_ARGS="$FX_ARGS -jsconsole -noquit"
;;
d)
DEBUG=true
;;
c)
FX_ARGS="$FX_ARGS -jsconsole -noquit"
;;
DEBUG=true
DEBUG_LEVEL="$OPTARG"
;;
f)
FX_ARGS="$FX_ARGS -bail"
;;
g)
FX_ARGS="$FX_ARGS -makeTestData"
;;
g)
GREP="$OPTARG"
;;
b)
FX_ARGS="$FX_ARGS -ZoteroSkipBundledFiles"
;;
t)
FX_ARGS="$FX_ARGS -makeTestData"
;;
x)
FX_EXECUTABLE="$OPTARG"
;;
*)
usage
;;
@ -90,6 +102,7 @@ user_pref("extensions.autoDisableScopes", 0);
user_pref("browser.uitour.enabled", false);
user_pref("browser.shell.checkDefaultBrowser", false);
user_pref("extensions.zotero.debug.log", $DEBUG);
user_pref("extensions.zotero.debug.level", $DEBUG_LEVEL);
user_pref("extensions.zotero.debug.time", $DEBUG);
user_pref("extensions.zotero.firstRunGuidance", false);
user_pref("extensions.zotero.firstRun2", false);
@ -101,8 +114,9 @@ if [ -z $IS_CYGWIN ]; then
echo "`MOZ_NO_REMOTE=1 NO_EM_RESTART=1 \"$FX_EXECUTABLE\" -v`"
fi
if [ "$TRAVIS" = true ]; then
FX_ARGS="$FX_ARGS --ZoteroNoUserInput"
FX_ARGS="$FX_ARGS -ZoteroNoUserInput"
fi
# Clean up on exit

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

BIN
test/tests/data/empty.pdf Normal file

Binary file not shown.

1604
test/tests/data/itemJSON.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,55 @@
{
"journalArticle": {
"DOI": "10.1234/example.doi",
"ISSN": "1234-5679",
"abstractNote": "Abstract note",
"accessDate": "1997-06-13 23:59:58",
"archive": "Archive",
"archiveLocation": "Archive location",
"callNumber": "Call number",
"creators": [
{
"creatorType": "author",
"firstName": "authorFirst",
"lastName": "authorLast"
},
{
"creatorType": "contributor",
"firstName": "contributorFirst",
"lastName": "contributorLast"
},
{
"creatorType": "editor",
"firstName": "editorFirst",
"lastName": "editorLast"
},
{
"creatorType": "reviewedAuthor",
"firstName": "reviewedAuthorFirst",
"lastName": "reviewedAuthorLast"
},
{
"creatorType": "translator",
"firstName": "translatorFirst",
"lastName": "translatorLast"
}
],
"date": "1999-12-31",
"extra": "Extra",
"issue": 5,
"itemType": "journalArticle",
"journalAbbreviation": "Journal abbreviation",
"language": "en-US",
"libraryCatalog": "Library catalog",
"pages": "1-10",
"publicationTitle": "Publication title",
"rights": "Rights",
"series": "Series",
"seriesText": "Series text",
"seriesTitle": "Series title",
"shortTitle": "Short title",
"title": "Title",
"url": "http://www.example.com",
"volume": 6
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -11,4 +11,185 @@ describe("Support Functions for Unit Testing", function() {
assert.equal(item.getField("url"), "https://www.zotero.org/support/quick_start_guide");
});
});
describe("loadSampleData", function() {
it("should load data from file", function() {
let data = loadSampleData('journalArticle');
assert.isObject(data, 'loaded data object');
assert.isNotNull(data);
assert.isAbove(Object.keys(data).length, 0, 'data object is not empty');
});
});
describe("populateDBWithSampleData", function() {
it("should populate database with data", Zotero.Promise.coroutine(function* () {
let data = loadSampleData('journalArticle');
yield populateDBWithSampleData(data);
let skipFields = ['id', 'itemType', 'creators']; // Special comparisons
for (let itemName in data) {
let item = data[itemName];
assert.isAbove(item.id, 0, 'assigned new item ID');
let zItem = yield Zotero.Items.getAsync(item.id);
assert.ok(zItem, 'inserted item into database');
// Compare item type
assert.equal(item.itemType, Zotero.ItemTypes.getName(zItem.itemTypeID), 'inserted item has the same item type');
// Compare simple properties
for (let prop in item) {
if (skipFields.indexOf(prop) != -1) continue;
// Using base-mapped fields
assert.equal(item[prop], zItem.getField(prop, false, true), 'inserted item property has the same value as sample data');
}
if (item.creators) {
// Compare creators
for (let i=0; i<item.creators.length; i++) {
let creator = item.creators[i];
let zCreator = zItem.getCreator(i);
assert.ok(zCreator, 'creator was added to item');
assert.equal(creator.firstName, zCreator.firstName, 'first names match');
assert.equal(creator.lastName, zCreator.lastName, 'last names match');
assert.equal(creator.creatorType, Zotero.CreatorTypes.getName(zCreator.creatorTypeID), 'creator types match');
}
}
}
}));
it("should populate items with tags", Zotero.Promise.coroutine(function* () {
let data = yield populateDBWithSampleData({
itemWithTags: {
itemType: "journalArticle",
tags: [
{ tag: "automatic tag", type: 0 },
{ tag: "manual tag", type: 1}
]
}
});
let zItem = yield Zotero.Items.getAsync(data.itemWithTags.id);
assert.ok(zItem, 'inserted item with tags into database');
let tags = data.itemWithTags.tags;
for (let i=0; i<tags.length; i++) {
let tagID = Zotero.Tags.getID(zItem.libraryID, tags[i].tag);
assert.ok(tagID, '"' + tags[i].tag + '" tag was inserted into the database');
assert.ok(zItem.hasTag(tags[i].tag), '"' + tags[i].tag + '" tag was assigned to item');
}
}));
});
describe("generateAllTypesAndFieldsData", function() {
it("should generate all types and fields data", function() {
let data = generateAllTypesAndFieldsData();
assert.isObject(data, 'created data object');
assert.isNotNull(data);
assert.isAbove(Object.keys(data).length, 0, 'data object is not empty');
});
it("all types and fields sample data should be up to date", function() {
assert.deepEqual(loadSampleData('allTypesAndFields'), generateAllTypesAndFieldsData());
});
});
describe("generateItemJSONData", function() {
it("item JSON data should be up to date", Zotero.Promise.coroutine(function* () {
let oldData = loadSampleData('itemJSON'),
newData = yield generateItemJSONData();
assert.isObject(newData, 'created data object');
assert.isNotNull(newData);
assert.isAbove(Object.keys(newData).length, 0, 'data object is not empty');
// Ignore data that is not stable, but make sure it is set
let ignoreFields = ['dateAdded', 'dateModified', 'key'];
for (let itemName in oldData) {
for (let i=0; i<ignoreFields.length; i++) {
let field = ignoreFields[i]
if (oldData[itemName][field] !== undefined) {
assert.isDefined(newData[itemName][field], field + ' is set');
delete oldData[itemName][field];
delete newData[itemName][field];
}
}
}
assert.deepEqual(oldData, newData);
}));
});
// describe("generateCiteProcJSExportData", function() {
// let citeURL = Zotero.Prefs.get("export.citePaperJournalArticleURL");
// before(function () {
// Zotero.Prefs.set("export.citePaperJournalArticleURL", true);
// });
// after(function() {
// Zotero.Prefs.set("export.citePaperJournalArticleURL", citeURL);
// });
// it("all citeproc-js export data should be up to date", Zotero.Promise.coroutine(function* () {
// let oldData = loadSampleData('citeProcJSExport'),
// newData = yield generateCiteProcJSExportData();
// assert.isObject(newData, 'created data object');
// assert.isNotNull(newData);
// assert.isAbove(Object.keys(newData).length, 0, 'citeproc-js export object is not empty');
// // Ignore item ID
// for (let itemName in oldData) {
// delete oldData[itemName].id;
// }
// for (let itemName in newData) {
// delete newData[itemName].id;
// }
// assert.deepEqual(oldData, newData, 'citeproc-js export data has not changed');
// }));
// });
describe("generateTranslatorExportData", function() {
it("legacy mode data should be up to date", Zotero.Promise.coroutine(function* () {
let oldData = loadSampleData('translatorExportLegacy'),
newData = yield generateTranslatorExportData(true);
assert.isObject(newData, 'created data object');
assert.isNotNull(newData);
assert.isAbove(Object.keys(newData).length, 0, 'translator export object is not empty');
// Ignore data that is not stable, but make sure it is set
let ignoreFields = ['itemID', 'dateAdded', 'dateModified', 'uri', 'key'];
for (let itemName in oldData) {
for (let i=0; i<ignoreFields.length; i++) {
let field = ignoreFields[i]
if (oldData[itemName][field] !== undefined) {
assert.isDefined(newData[itemName][field], field + ' is set');
delete oldData[itemName][field];
delete newData[itemName][field];
}
}
}
assert.deepEqual(oldData, newData, 'translator export data has not changed');
}));
it("data should be up to date", Zotero.Promise.coroutine(function* () {
let oldData = loadSampleData('translatorExport'),
newData = yield generateTranslatorExportData();
assert.isObject(newData, 'created data object');
assert.isNotNull(newData);
assert.isAbove(Object.keys(newData).length, 0, 'translator export object is not empty');
// Ignore data that is not stable, but make sure it is set
let ignoreFields = ['dateAdded', 'dateModified', 'uri'];
for (let itemName in oldData) {
for (let i=0; i<ignoreFields.length; i++) {
let field = ignoreFields[i]
if (oldData[itemName][field] !== undefined) {
assert.isDefined(newData[itemName][field], field + ' is set');
delete oldData[itemName][field];
delete newData[itemName][field];
}
}
}
assert.deepEqual(oldData, newData, 'translator export data has not changed');
}));
});
});

589
test/tests/translateTest.js Normal file
View file

@ -0,0 +1,589 @@
Components.utils.import("resource://gre/modules/osfile.jsm");
describe("Zotero.Translate.ItemGetter", function() {
describe("nextItem", function() {
it('should return false for an empty database', Zotero.Promise.coroutine(function* () {
let getter = new Zotero.Translate.ItemGetter();
assert.isFalse(yield getter.nextItem());
}));
it('should return items in order they are supplied', Zotero.Promise.coroutine(function* () {
let getter = new Zotero.Translate.ItemGetter();
let items, itemIDs, itemURIs;
yield Zotero.DB.executeTransaction(function* () {
items = [
yield new Zotero.Item('journalArticle'),
yield new Zotero.Item('book')
];
itemIDs = [ yield items[0].save(), yield items[1].save() ];
itemURIs = items.map(i => Zotero.URI.getItemURI(i));
});
getter._itemsLeft = items;
assert.equal((yield getter.nextItem()).uri, itemURIs[0], 'first item comes out first');
assert.equal((yield getter.nextItem()).uri, itemURIs[1], 'second item comes out second');
assert.isFalse((yield getter.nextItem()), 'end of item queue');
}));
it('should return items with tags in expected format', Zotero.Promise.coroutine(function* () {
let getter = new Zotero.Translate.ItemGetter();
let itemWithAutomaticTag, itemWithManualTag, itemWithMultipleTags
yield Zotero.DB.executeTransaction(function* () {
itemWithAutomaticTag = new Zotero.Item('journalArticle');
itemWithAutomaticTag.addTag('automatic tag', 0);
yield itemWithAutomaticTag.save();
itemWithManualTag = new Zotero.Item('journalArticle');
itemWithManualTag.addTag('manual tag', 1);
yield itemWithManualTag.save();
itemWithMultipleTags = new Zotero.Item('journalArticle');
itemWithMultipleTags.addTag('tag1', 0);
itemWithMultipleTags.addTag('tag2', 1);
yield itemWithMultipleTags.save();
});
let legacyMode = [false, true];
for (let i=0; i<legacyMode.length; i++) {
getter._itemsLeft = [itemWithAutomaticTag, itemWithManualTag, itemWithMultipleTags];
getter.legacy = legacyMode[i];
let suffix = legacyMode[i] ? ' in legacy mode' : '';
// itemWithAutomaticTag
let translatorItem = yield getter.nextItem();
assert.isArray(translatorItem.tags, 'item contains automatic tags in an array' + suffix);
assert.isObject(translatorItem.tags[0], 'automatic tag is an object' + suffix);
assert.equal(translatorItem.tags[0].tag, 'automatic tag', 'automatic tag name provided as "tag" property' + suffix);
if (legacyMode[i]) {
assert.equal(translatorItem.tags[0].type, 0, 'automatic tag "type" is 0' + suffix);
} else {
assert.isUndefined(translatorItem.tags[0].type, '"type" is undefined for automatic tag' + suffix);
}
// itemWithManualTag
translatorItem = yield getter.nextItem();
assert.isArray(translatorItem.tags, 'item contains manual tags in an array' + suffix);
assert.isObject(translatorItem.tags[0], 'manual tag is an object' + suffix);
assert.equal(translatorItem.tags[0].tag, 'manual tag', 'manual tag name provided as "tag" property' + suffix);
assert.equal(translatorItem.tags[0].type, 1, 'manual tag "type" is 1' + suffix);
// itemWithMultipleTags
translatorItem = yield getter.nextItem();
assert.isArray(translatorItem.tags, 'item contains multiple tags in an array' + suffix);
assert.lengthOf(translatorItem.tags, 2, 'expected number of tags returned' + suffix);
}
}));
it('should return item collections in expected format', Zotero.Promise.coroutine(function* () {
let getter = new Zotero.Translate.ItemGetter();
let items, collections;
yield Zotero.DB.executeTransaction(function* () {
items = getter._itemsLeft = [
new Zotero.Item('journalArticle'), // Not in collection
new Zotero.Item('journalArticle'), // In a single collection
new Zotero.Item('journalArticle'), //In two collections
new Zotero.Item('journalArticle') // In a nested collection
];
yield Zotero.Promise.all(items.map(item => item.save()));
collections = [
new Zotero.Collection,
new Zotero.Collection,
new Zotero.Collection,
new Zotero.Collection
];
collections[0].name = "test1";
collections[1].name = "test2";
collections[2].name = "subTest1";
collections[3].name = "subTest2";
yield collections[0].save();
yield collections[1].save();
collections[2].parentID = collections[0].id;
collections[3].parentID = collections[1].id;
yield collections[2].save();
yield collections[3].save();
yield collections[0].addItems([items[1].id, items[2].id]);
yield collections[1].addItem(items[2].id);
yield collections[2].addItem(items[3].id);
});
let translatorItem = yield getter.nextItem();
assert.isArray(translatorItem.collections, 'item in library root has a collections array');
assert.equal(translatorItem.collections.length, 0, 'item in library root does not list any collections');
translatorItem = yield getter.nextItem();
assert.isArray(translatorItem.collections, 'item in a single collection has a collections array');
assert.equal(translatorItem.collections.length, 1, 'item in a single collection lists one collection');
assert.equal(translatorItem.collections[0], collections[0].key, 'item in a single collection identifies correct collection');
translatorItem = yield getter.nextItem();
assert.isArray(translatorItem.collections, 'item in two collections has a collections array');
assert.equal(translatorItem.collections.length, 2, 'item in two collections lists two collections');
assert.deepEqual(
translatorItem.collections.sort(),
[collections[0].key, collections[1].key].sort(),
'item in two collections identifies correct collections'
);
translatorItem = yield getter.nextItem();
assert.isArray(translatorItem.collections, 'item in a nested collection has a collections array');
assert.equal(translatorItem.collections.length, 1, 'item in a single nested collection lists one collection');
assert.equal(translatorItem.collections[0], collections[2].key, 'item in a single collection identifies correct collection');
}));
// it('should return item relations in expected format', Zotero.Promise.coroutine(function* () {
// let getter = new Zotero.Translate.ItemGetter();
// let items;
// yield Zotero.DB.executeTransaction(function* () {
// items = [
// new Zotero.Item('journalArticle'), // Item with no relations
// new Zotero.Item('journalArticle'), // Relation set on this item
// new Zotero.Item('journalArticle'), // To this item
// new Zotero.Item('journalArticle'), // This item is related to two items below
// new Zotero.Item('journalArticle'), // But this item is not related to the item below
// new Zotero.Item('journalArticle')
// ];
// yield Zotero.Promise.all(items.map(item => item.save()));
// yield items[1].addRelatedItem(items[2].id);
// yield items[3].addRelatedItem(items[4].id);
// yield items[3].addRelatedItem(items[5].id);
// });
// getter._itemsLeft = items.slice();
// let translatorItem = yield getter.nextItem();
// assert.isObject(translatorItem.relations, 'item with no relations has a relations object');
// assert.equal(Object.keys(translatorItem.relations).length, 0, 'item with no relations does not list any relations');
// translatorItem = yield getter.nextItem();
// assert.isObject(translatorItem.relations, 'item that is the subject of a single relation has a relations object');
// assert.equal(Object.keys(translatorItem.relations).length, 1, 'item that is the subject of a single relation list one relations predicate');
// assert.isDefined(translatorItem.relations['dc:relation'], 'item that is the subject of a single relation uses "dc:relation" as the predicate');
// assert.isString(translatorItem.relations['dc:relation'], 'item that is the subject of a single relation lists "dc:relation" object as a string');
// assert.equal(translatorItem.relations['dc:relation'], Zotero.URI.getItemURI(items[2]), 'item that is the subject of a single relation identifies correct object URI');
// translatorItem = yield getter.nextItem();
// assert.isObject(translatorItem.relations, 'item that is the object of a single relation has a relations object');
// assert.equal(Object.keys(translatorItem.relations).length, 1, 'item that is the object of a single relation list one relations predicate');
// assert.isDefined(translatorItem.relations['dc:relation'], 'item that is the object of a single relation uses "dc:relation" as the predicate');
// assert.isString(translatorItem.relations['dc:relation'], 'item that is the object of a single relation lists "dc:relation" object as a string');
// assert.equal(translatorItem.relations['dc:relation'], Zotero.URI.getItemURI(items[1]), 'item that is the object of a single relation identifies correct subject URI');
// translatorItem = yield getter.nextItem();
// assert.isObject(translatorItem.relations, 'item that is the subject of two relations has a relations object');
// assert.equal(Object.keys(translatorItem.relations).length, 1, 'item that is the subject of two relations list one relations predicate');
// assert.isDefined(translatorItem.relations['dc:relation'], 'item that is the subject of two relations uses "dc:relation" as the predicate');
// assert.isArray(translatorItem.relations['dc:relation'], 'item that is the subject of two relations lists "dc:relation" object as an array');
// assert.equal(translatorItem.relations['dc:relation'].length, 2, 'item that is the subject of two relations lists two relations in the "dc:relation" array');
// assert.deepEqual(translatorItem.relations['dc:relation'].sort(),
// [Zotero.URI.getItemURI(items[4]), Zotero.URI.getItemURI(items[5])].sort(),
// 'item that is the subject of two relations identifies correct object URIs'
// );
// translatorItem = yield getter.nextItem();
// assert.isObject(translatorItem.relations, 'item that is the object of one relation from item with two relations has a relations object');
// assert.equal(Object.keys(translatorItem.relations).length, 1, 'item that is the object of one relation from item with two relations list one relations predicate');
// assert.isDefined(translatorItem.relations['dc:relation'], 'item that is the object of one relation from item with two relations uses "dc:relation" as the predicate');
// assert.isString(translatorItem.relations['dc:relation'], 'item that is the object of one relation from item with two relations lists "dc:relation" object as a string');
// assert.equal(translatorItem.relations['dc:relation'], Zotero.URI.getItemURI(items[3]), 'item that is the object of one relation from item with two relations identifies correct subject URI');
// }));
it('should return standalone note in expected format', Zotero.Promise.coroutine(function* () {
let relatedItem, note, collection;
yield Zotero.DB.executeTransaction(function* () {
relatedItem = new Zotero.Item('journalArticle');
yield relatedItem.save();
note = new Zotero.Item('note');
note.setNote('Note');
note.addTag('automaticTag', 0);
note.addTag('manualTag', 1);
// note.addRelatedItem(relatedItem.id);
yield note.save();
collection = new Zotero.Collection;
collection.name = 'test';
yield collection.save();
yield collection.addItem(note.id);
});
let legacyMode = [false, true];
for (let i=0; i<legacyMode.length; i++) {
let getter = new Zotero.Translate.ItemGetter();
getter._itemsLeft = [note];
let legacy = getter.legacy = legacyMode[i];
let suffix = legacy ? ' in legacy mode' : '';
let translatorNote = yield getter.nextItem();
assert.isDefined(translatorNote, 'returns standalone note' + suffix);
assert.equal(translatorNote.itemType, 'note', 'itemType is correct' + suffix);
assert.equal(translatorNote.note, 'Note', 'note is correct' + suffix);
assert.isString(translatorNote.dateAdded, 'dateAdded is string' + suffix);
assert.isString(translatorNote.dateModified, 'dateModified is string' + suffix);
if (legacy) {
assert.isTrue(sqlDateTimeRe.test(translatorNote.dateAdded), 'dateAdded is in correct format' + suffix);
assert.isTrue(sqlDateTimeRe.test(translatorNote.dateModified), 'dateModified is in correct format' + suffix);
assert.isNumber(translatorNote.itemID, 'itemID is set' + suffix);
assert.isString(translatorNote.key, 'key is set' + suffix);
} else {
assert.isTrue(isoDateTimeRe.test(translatorNote.dateAdded), 'dateAdded is in correct format' + suffix);
assert.isTrue(isoDateTimeRe.test(translatorNote.dateModified), 'dateModified is in correct format' + suffix);
}
// Tags
assert.isArray(translatorNote.tags, 'contains tags as array' + suffix);
assert.equal(translatorNote.tags.length, 2, 'contains correct number of tags' + suffix);
let possibleTags = [
{ tag: 'automaticTag', type: 0 },
{ tag: 'manualTag', type: 1 }
];
for (let i=0; i<possibleTags.length; i++) {
let match = false;
for (let j=0; j<translatorNote.tags.length; j++) {
if (possibleTags[i].tag == translatorNote.tags[j].tag) {
let type = possibleTags[i].type;
if (!legacy && type == 0) type = undefined;
assert.equal(translatorNote.tags[j].type, type, possibleTags[i].tag + ' tag is correct' + suffix);
match = true;
break;
}
}
assert.isTrue(match, 'has ' + possibleTags[i].tag + ' tag ' + suffix);
}
// Relations
// assert.isObject(translatorNote.relations, 'has relations as object' + suffix);
// assert.equal(translatorNote.relations['dc:relation'], Zotero.URI.getItemURI(relatedItem), 'relation is correct' + suffix);
/** TODO: test other relations and multiple relations per predicate (should be an array) **/
if (!legacy) {
// Collections
assert.isArray(translatorNote.collections, 'has a collections array' + suffix);
assert.equal(translatorNote.collections.length, 1, 'lists one collection' + suffix);
assert.equal(translatorNote.collections[0], collection.key, 'identifies correct collection' + suffix);
}
}
}));
it('should return attached note in expected format', Zotero.Promise.coroutine(function* () {
let relatedItem, items, collection, note;
yield Zotero.DB.executeTransaction(function* () {
relatedItem = new Zotero.Item('journalArticle');
yield relatedItem.save();
items = [
new Zotero.Item('journalArticle'),
new Zotero.Item('journalArticle')
];
yield Zotero.Promise.all(items.map(item => item.save()));
collection = new Zotero.Collection;
collection.name = 'test';
yield collection.save();
yield collection.addItem(items[0].id);
yield collection.addItem(items[1].id);
note = new Zotero.Item('note');
note.setNote('Note');
note.addTag('automaticTag', 0);
note.addTag('manualTag', 1);
yield note.save();
// note.addRelatedItem(relatedItem.id);
});
let legacyMode = [false, true];
for (let i=0; i<legacyMode.length; i++) {
let item = items[i];
let getter = new Zotero.Translate.ItemGetter();
getter._itemsLeft = [item];
let legacy = getter.legacy = legacyMode[i];
let suffix = legacy ? ' in legacy mode' : '';
let translatorItem = yield getter.nextItem();
assert.isArray(translatorItem.notes, 'item with no notes contains notes array' + suffix);
assert.equal(translatorItem.notes.length, 0, 'item with no notes contains empty notes array' + suffix);
note.parentID = item.id;
yield note.saveTx();
getter = new Zotero.Translate.ItemGetter();
getter._itemsLeft = [item];
getter.legacy = legacy;
translatorItem = yield getter.nextItem();
assert.isArray(translatorItem.notes, 'item with no notes contains notes array' + suffix);
assert.equal(translatorItem.notes.length, 1, 'item with one note contains array with one note' + suffix);
let translatorNote = translatorItem.notes[0];
assert.equal(translatorNote.itemType, 'note', 'itemType is correct' + suffix);
assert.equal(translatorNote.note, 'Note', 'note is correct' + suffix);
assert.isString(translatorNote.dateAdded, 'dateAdded is string' + suffix);
assert.isString(translatorNote.dateModified, 'dateModified is string' + suffix);
if (legacy) {
assert.isTrue(sqlDateTimeRe.test(translatorNote.dateAdded), 'dateAdded is in correct format' + suffix);
assert.isTrue(sqlDateTimeRe.test(translatorNote.dateModified), 'dateModified is in correct format' + suffix);
assert.isNumber(translatorNote.itemID, 'itemID is set' + suffix);
assert.isString(translatorNote.key, 'key is set' + suffix);
} else {
assert.isTrue(isoDateTimeRe.test(translatorNote.dateAdded), 'dateAdded is in correct format' + suffix);
assert.isTrue(isoDateTimeRe.test(translatorNote.dateModified), 'dateModified is in correct format' + suffix);
}
// Tags
assert.isArray(translatorNote.tags, 'contains tags as array' + suffix);
assert.equal(translatorNote.tags.length, 2, 'contains correct number of tags' + suffix);
let possibleTags = [
{ tag: 'automaticTag', type: 0 },
{ tag: 'manualTag', type: 1 }
];
for (let i=0; i<possibleTags.length; i++) {
let match = false;
for (let j=0; j<translatorNote.tags.length; j++) {
if (possibleTags[i].tag == translatorNote.tags[j].tag) {
let type = possibleTags[i].type;
if (!legacy && type == 0) type = undefined;
assert.equal(translatorNote.tags[j].type, type, possibleTags[i].tag + ' tag is correct' + suffix);
match = true;
break;
}
}
assert.isTrue(match, 'has ' + possibleTags[i].tag + ' tag ' + suffix);
}
// Relations
// assert.isObject(translatorNote.relations, 'has relations as object' + suffix);
// assert.equal(translatorNote.relations['dc:relation'], Zotero.URI.getItemURI(relatedItem), 'relation is correct' + suffix);
/** TODO: test other relations and multiple relations per predicate (should be an array) **/
if (!legacy) {
// Collections
assert.isUndefined(translatorNote.collections, 'has no collections array' + suffix);
}
}
}));
it('should return stored/linked file and URI attachments in expected format', Zotero.Promise.coroutine(function* () {
this.timeout(60000);
let file = getTestDataDirectory();
let item, relatedItem;
file.append("empty.pdf");
yield Zotero.DB.executeTransaction(function* () {
item = new Zotero.Item('journalArticle');
yield item.save();
relatedItem = new Zotero.Item('journalArticle');
yield relatedItem.save();
});
// Attachment items
let attachments = [
yield Zotero.Attachments.importFromFile({"file":file}), // Standalone stored file
yield Zotero.Attachments.linkFromFile({"file":file}), // Standalone link to file
yield Zotero.Attachments.importFromFile({"file":file, "parentItemID":item.id}), // Attached stored file
yield Zotero.Attachments.linkFromFile({"file":file, "parentItemID":item.id}), // Attached link to file
yield Zotero.Attachments.linkFromURL({"url":'http://example.com', "parentItemID":item.id, "contentType":'application/pdf', "title":'empty.pdf'}) // Attached link to URL
];
yield Zotero.DB.executeTransaction(function* () {
// Make sure all fields are populated
for (let i=0; i<attachments.length; i++) {
let attachment = attachments[i];
attachment.setField('accessDate', '2001-02-03 12:13:14');
attachment.attachmentCharset = Zotero.CharacterSets.getID('utf-8');
attachment.setField('url', 'http://example.com');
attachment.setNote('note');
attachment.addTag('automaticTag', 0);
attachment.addTag('manualTag', 1);
// attachment.addRelatedItem(relatedItem.id);
yield attachment.save();
}
});
let items = [ attachments[0], attachments[1], item ]; // Standalone attachments and item with child attachments
// Run tests
let legacyMode = [false, true];
for (let i=0; i<legacyMode.length; i++) {
let getter = new Zotero.Translate.ItemGetter();
getter._itemsLeft = items.slice();
let exportDir = yield getTempDirectory();
getter._exportFileDirectory = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
getter._exportFileDirectory.initWithPath(exportDir);
let legacy = getter.legacy = legacyMode[i];
let suffix = legacy ? ' in legacy mode' : '';
// Gather all standalone and child attachments into a single array,
// since tests are mostly the same
let translatorAttachments = [], translatorItem;
let itemsLeft = items.length, attachmentsLeft = attachments.length;
while (translatorItem = yield getter.nextItem()) {
assert.isString(translatorItem.itemType, 'itemType is set' + suffix);
// Standalone attachments
if (translatorItem.itemType == 'attachment') {
translatorAttachments.push({
child: false,
attachment: translatorItem
});
attachmentsLeft--;
// Child attachments
} else if (translatorItem.itemType == 'journalArticle') {
assert.isArray(translatorItem.attachments, 'item contains attachment array' + suffix);
assert.equal(translatorItem.attachments.length, 3, 'attachment array contains all items' + suffix);
for (let i=0; i<translatorItem.attachments.length; i++) {
let attachment = translatorItem.attachments[i];
assert.equal(attachment.itemType, 'attachment', 'item attachment is of itemType "attachment"' + suffix);
translatorAttachments.push({
child: true,
attachment: attachment
});
attachmentsLeft--;
}
// Unexpected
} else {
assert.fail(translatorItem.itemType, 'attachment or journalArticle', 'expected itemType returned');
}
itemsLeft--;
}
assert.equal(itemsLeft, 0, 'all items returned by getter');
assert.equal(attachmentsLeft, 0, 'all attachments returned by getter');
// Since we make no guarantees on the order of child attachments,
// we have to rely on URI as the identifier
let uriMap = {};
for (let i=0; i<attachments.length; i++) {
uriMap[Zotero.URI.getItemURI(attachments[i])] = attachments[i];
}
for (let j=0; j<translatorAttachments.length; j++) {
let childAttachment = translatorAttachments[j].child;
let attachment = translatorAttachments[j].attachment;
assert.isString(attachment.uri, 'uri is set' + suffix);
let zoteroItem = uriMap[attachment.uri];
assert.isDefined(zoteroItem, 'uri is correct' + suffix);
delete uriMap[attachment.uri];
let storedFile = zoteroItem.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE
|| zoteroItem.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL;
let linkToURL = zoteroItem.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL;
let prefix = (childAttachment ? 'attached ' : '')
+ (storedFile ? 'stored ' : 'link to ')
+ (linkToURL ? 'URL ' : 'file ');
// Set fields
assert.equal(attachment.itemType, 'attachment', prefix + 'itemType is correct' + suffix);
assert.equal(attachment.title, 'empty.pdf', prefix + 'title is correct' + suffix);
assert.equal(attachment.url, 'http://example.com', prefix + 'url is correct' + suffix);
assert.equal(attachment.note, 'note', prefix + 'note is correct' + suffix);
// Automatically set fields
assert.isString(attachment.dateAdded, prefix + 'dateAdded is set' + suffix);
assert.isString(attachment.dateModified, prefix + 'dateModified is set' + suffix);
// Legacy mode fields
if (legacy) {
assert.isNumber(attachment.itemID, prefix + 'itemID is set' + suffix);
assert.isString(attachment.key, prefix + 'key is set' + suffix);
assert.equal(attachment.mimeType, 'application/pdf', prefix + 'mimeType is correct' + suffix);
assert.equal(attachment.accessDate, '2001-02-03 12:13:14', prefix + 'accessDate is correct' + suffix);
assert.isTrue(sqlDateTimeRe.test(attachment.dateAdded), prefix + 'dateAdded matches SQL format' + suffix);
assert.isTrue(sqlDateTimeRe.test(attachment.dateModified), prefix + 'dateModified matches SQL format' + suffix);
} else {
assert.equal(attachment.contentType, 'application/pdf', prefix + 'contentType is correct' + suffix);
assert.equal(attachment.accessDate, '2001-02-03T12:13:14Z', prefix + 'accessDate is correct' + suffix);
assert.isTrue(isoDateTimeRe.test(attachment.dateAdded), prefix + 'dateAdded matches ISO-8601 format' + suffix);
assert.isTrue(isoDateTimeRe.test(attachment.dateModified), prefix + 'dateModified matches ISO-8601 format' + suffix);
}
if (!linkToURL) {
// localPath
assert.isString(attachment.localPath, prefix + 'localPath is set' + suffix);
let attachmentFile = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
attachmentFile.initWithPath(attachment.localPath);
assert.isTrue(attachmentFile.exists(), prefix + 'localPath points to a file' + suffix);
assert.isTrue(attachmentFile.equals(attachments[j].getFile(null, true)), prefix + 'localPath points to the correct file' + suffix);
assert.equal(attachment.filename, 'empty.pdf', prefix + 'filename is correct' + suffix);
assert.equal(attachment.defaultPath, 'files/' + attachments[j].id + '/' + attachment.filename, prefix + 'defaultPath is correct' + suffix);
// saveFile function
assert.isFunction(attachment.saveFile, prefix + 'has saveFile function' + suffix);
attachment.saveFile(attachment.defaultPath);
assert.equal(attachment.path, OS.Path.join(exportDir, OS.Path.normalize(attachment.defaultPath)), prefix + 'path is set correctly after saveFile call' + suffix);
let fileExists = yield OS.File.exists(attachment.path);
assert.isTrue(fileExists, prefix + 'file was copied to the correct path by saveFile function' + suffix);
fileExists = yield OS.File.exists(attachment.localPath);
assert.isTrue(fileExists, prefix + 'file was not removed from original location' + suffix);
assert.throws(attachment.saveFile.bind(attachment, attachment.defaultPath), /^ERROR_FILE_EXISTS /, prefix + 'saveFile does not overwrite existing file by default' + suffix);
assert.throws(attachment.saveFile.bind(attachment, 'file/../../'), /./, prefix + 'saveFile does not allow exporting outside export directory' + suffix);
/** TODO: check if overwriting existing file works **/
}
// Tags
assert.isArray(attachment.tags, prefix + 'contains tags as array' + suffix);
assert.equal(attachment.tags.length, 2, prefix + 'contains correct number of tags' + suffix);
let possibleTags = [
{ tag: 'automaticTag', type: 0 },
{ tag: 'manualTag', type: 1 }
];
for (let i=0; i<possibleTags.length; i++) {
let match = false;
for (let j=0; j<attachment.tags.length; j++) {
if (possibleTags[i].tag == attachment.tags[j].tag) {
let type = possibleTags[i].type;
if (!legacy && type == 0) type = undefined;
assert.equal(attachment.tags[j].type, type, prefix + possibleTags[i].tag + ' tag is correct' + suffix);
match = true;
break;
}
}
assert.isTrue(match, prefix + ' has ' + possibleTags[i].tag + ' tag ' + suffix);
}
// Relations
// assert.isObject(attachment.relations, prefix + 'has relations as object' + suffix);
// assert.equal(attachment.relations['dc:relation'], Zotero.URI.getItemURI(relatedItem), prefix + 'relation is correct' + suffix);
/** TODO: test other relations and multiple relations per predicate (should be an array) **/
}
}
}));
});
});

View file

@ -172,4 +172,105 @@ describe("Zotero.Utilities", function() {
assert.equal(cleanISSN('<b>ISSN</b>:1234\xA0-\t5679(print)\n<b>eISSN (electronic)</b>:0028-0836'), '1234-5679');
});
});
describe("itemToCSLJSON", function() {
it("should accept Zotero.Item and Zotero export item format", Zotero.Promise.coroutine(function* () {
let data = yield populateDBWithSampleData(loadSampleData('journalArticle'));
let item = yield Zotero.Items.getAsync(data.journalArticle.id);
let fromZoteroItem;
try {
fromZoteroItem = yield Zotero.Utilities.itemToCSLJSON(item);
} catch(e) {
assert.fail(e, null, 'accepts Zotero Item');
}
assert.isObject(fromZoteroItem, 'converts Zotero Item to object');
assert.isNotNull(fromZoteroItem, 'converts Zotero Item to non-null object');
let fromExportItem;
try {
fromExportItem = Zotero.Utilities.itemToCSLJSON(
yield Zotero.Utilities.Internal.itemToExportFormat(item)
);
} catch(e) {
assert.fail(e, null, 'accepts Zotero export item');
}
assert.isObject(fromExportItem, 'converts Zotero export item to object');
assert.isNotNull(fromExportItem, 'converts Zotero export item to non-null object');
assert.deepEqual(fromZoteroItem, fromExportItem, 'conversion from Zotero Item and from export item are the same');
}));
it("should convert standalone notes to expected format", Zotero.Promise.coroutine(function* () {
let note = new Zotero.Item('note');
note.setNote('Some note longer than 50 characters, which will become the title.');
yield note.saveTx();
let cslJSONNote = yield Zotero.Utilities.itemToCSLJSON(note);
assert.equal(cslJSONNote.type, 'article', 'note is exported as "article"');
assert.equal(cslJSONNote.title, note.getNoteTitle(), 'note title is set to Zotero pseudo-title');
}));
it("should convert standalone attachments to expected format", Zotero.Promise.coroutine(function* () {
let file = getTestDataDirectory();
file.append("empty.pdf");
let attachment = yield Zotero.Attachments.importFromFile({"file":file});
attachment.setField('title', 'Empty');
attachment.setField('accessDate', '2001-02-03 12:13:14');
attachment.setField('url', 'http://example.com');
attachment.setNote('Note');
yield attachment.saveTx();
cslJSONAttachment = yield Zotero.Utilities.itemToCSLJSON(attachment);
assert.equal(cslJSONAttachment.type, 'article', 'attachment is exported as "article"');
assert.equal(cslJSONAttachment.title, 'Empty', 'attachment title is correct');
assert.deepEqual(cslJSONAttachment.accessed, {"date-parts":[["2001",2,3]]}, 'attachment access date is mapped correctly');
}));
it("should refuse to convert unexpected item types", Zotero.Promise.coroutine(function* () {
let data = yield populateDBWithSampleData(loadSampleData('journalArticle'));
let item = yield Zotero.Items.getAsync(data.journalArticle.id);
let exportFormat = Zotero.Utilities.Internal.itemToExportFormat(item);
exportFormat.itemType = 'foo';
assert.throws(Zotero.Utilities.itemToCSLJSON.bind(Zotero.Utilities, exportFormat), /^Unexpected Zotero Item type ".*"$/, 'throws an error when trying to map invalid item types');
}));
it("should map additional fields from Extra field", Zotero.Promise.coroutine(function* () {
let item = new Zotero.Item('journalArticle');
item.setField('extra', 'PMID: 12345\nPMCID:123456');
yield item.saveTx();
let cslJSON = yield Zotero.Utilities.itemToCSLJSON(item);
assert.equal(cslJSON.PMID, '12345', 'PMID from Extra is mapped to PMID');
assert.equal(cslJSON.PMCID, '123456', 'PMCID from Extra is mapped to PMCID');
item.setField('extra', 'PMID: 12345');
yield item.saveTx();
cslJSON = yield Zotero.Utilities.itemToCSLJSON(item);
assert.equal(cslJSON.PMID, '12345', 'single-line entry is extracted correctly');
item.setField('extra', 'some junk: note\nPMID: 12345\nstuff in-between\nPMCID: 123456\nlast bit of junk!');
yield item.saveTx();
cslJSON = yield Zotero.Utilities.itemToCSLJSON(item);
assert.equal(cslJSON.PMID, '12345', 'PMID from mixed Extra field is mapped to PMID');
assert.equal(cslJSON.PMCID, '123456', 'PMCID from mixed Extra field is mapped to PMCID');
item.setField('extra', 'a\n PMID: 12345\nfoo PMCID: 123456');
yield item.saveTx();
cslJSON = yield Zotero.Utilities.itemToCSLJSON(item);
assert.isUndefined(cslJSON.PMCID, 'field label must not be preceded by other text');
assert.isUndefined(cslJSON.PMID, 'field label must not be preceded by a space');
assert.equal(cslJSON.note, 'a\n PMID: 12345\nfoo PMCID: 123456', 'note is left untouched if nothing is extracted');
item.setField('extra', 'something\npmid: 12345\n');
yield item.saveTx();
cslJSON = yield Zotero.Utilities.itemToCSLJSON(item);
assert.isUndefined(cslJSON.PMID, 'field labels are case-sensitive');
}));
});
});