dataObjectTest.js (17141B)
1 "use strict"; 2 3 describe("Zotero.DataObject", function() { 4 var types = ['collection', 'item', 'search']; 5 6 describe("#library", function () { 7 it("should return a Zotero.Library", function* () { 8 var item = yield createDataObject('item'); 9 assert.equal(item.library, Zotero.Libraries.userLibrary); 10 }); 11 }); 12 13 describe("#libraryID", function () { 14 it("should return a libraryID", function* () { 15 var item = yield createDataObject('item'); 16 assert.isNumber(item.libraryID); 17 assert.equal(item.libraryID, Zotero.Libraries.userLibraryID); 18 }); 19 }); 20 21 describe("#key", function () { 22 it("shouldn't update .loaded on get if unset", function* () { 23 for (let type of types) { 24 let param; 25 if (type == 'item') { 26 param = 'book'; 27 } 28 let obj = new Zotero[Zotero.Utilities.capitalize(type)](param); 29 obj.libraryID = Zotero.Libraries.userLibraryID; 30 assert.isNull(obj.key, 'key is null for ' + type); 31 assert.isFalse(obj._loaded.primaryData, 'primary data not loaded for ' + type); 32 obj.key = Zotero.DataObjectUtilities.generateKey(); 33 } 34 }) 35 }) 36 37 describe("#version", function () { 38 it("should be set to 0 after creating object", function* () { 39 for (let type of types) { 40 let obj = yield createDataObject(type); 41 assert.equal(obj.version, 0); 42 yield obj.eraseTx(); 43 } 44 }) 45 46 it("should be set after creating object", function* () { 47 for (let type of types) { 48 let obj = yield createDataObject(type, { version: 1234 }); 49 assert.equal(obj.version, 1234, type + " version mismatch"); 50 yield obj.eraseTx(); 51 } 52 }) 53 }) 54 55 describe("#synced", function () { 56 it("should be set to false after creating object", function* () { 57 for (let type of types) { 58 var obj = createUnsavedDataObject(type); 59 var id = yield obj.saveTx(); 60 assert.isFalse(obj.synced); 61 yield obj.eraseTx(); 62 } 63 }); 64 65 it("should be set to false after modifying object", function* () { 66 for (let type of types) { 67 var obj = createUnsavedDataObject(type); 68 var id = yield obj.saveTx(); 69 70 obj.synced = true; 71 yield obj.saveTx(); 72 73 if (type == 'item') { 74 obj.setField('title', Zotero.Utilities.randomString()); 75 } 76 else { 77 obj.name = Zotero.Utilities.randomString(); 78 } 79 yield obj.saveTx(); 80 assert.isFalse(obj.synced); 81 82 yield obj.eraseTx(); 83 } 84 }); 85 86 it("should be changed to true explicitly with no other changes", function* () { 87 for (let type of types) { 88 var obj = createUnsavedDataObject(type); 89 var id = yield obj.saveTx(); 90 91 obj.synced = true; 92 yield obj.saveTx(); 93 assert.isTrue(obj.synced); 94 95 yield obj.eraseTx(); 96 } 97 }); 98 99 it("should be changed to true explicitly with other field changes", function* () { 100 for (let type of types) { 101 var obj = createUnsavedDataObject(type); 102 var id = yield obj.saveTx(); 103 104 if (type == 'item') { 105 obj.setField('title', Zotero.Utilities.randomString()); 106 } 107 else { 108 obj.name = Zotero.Utilities.randomString(); 109 } 110 obj.synced = true; 111 yield obj.saveTx(); 112 assert.isTrue(obj.synced); 113 114 yield obj.eraseTx(); 115 } 116 }); 117 118 it("should remain at true if set explicitly", function* () { 119 for (let type of types) { 120 var obj = createUnsavedDataObject(type); 121 obj.synced = true; 122 var id = yield obj.saveTx(); 123 assert.isTrue(obj.synced); 124 125 if (type == 'item') { 126 obj.setField('title', 'test'); 127 } 128 else { 129 obj.name = Zotero.Utilities.randomString(); 130 } 131 obj.synced = true; 132 yield obj.saveTx(); 133 assert.isTrue(obj.synced); 134 135 yield obj.eraseTx(); 136 } 137 }); 138 139 it("should be unchanged if skipSyncedUpdate passed", function* () { 140 for (let type of types) { 141 var obj = createUnsavedDataObject(type); 142 var id = yield obj.saveTx(); 143 144 obj.synced = 1; 145 yield obj.saveTx(); 146 147 if (type == 'item') { 148 obj.setField('title', Zotero.Utilities.randomString()); 149 } 150 else { 151 obj.name = Zotero.Utilities.randomString(); 152 } 153 yield obj.saveTx({ 154 skipSyncedUpdate: true 155 }); 156 assert.ok(obj.synced); 157 158 yield obj.eraseTx(); 159 } 160 }); 161 }) 162 163 describe("#loadPrimaryData()", function () { 164 it("should load unloaded primary data if partially set", function* () { 165 var objs = {}; 166 for (let type of types) { 167 let obj = createUnsavedDataObject(type); 168 yield obj.save({ 169 skipCache: true 170 }); 171 objs[type] = { 172 key: obj.key, 173 version: obj.version 174 }; 175 } 176 177 for (let type of types) { 178 let obj = new Zotero[Zotero.Utilities.capitalize(type)]; 179 obj.libraryID = Zotero.Libraries.userLibraryID; 180 obj.key = objs[type].key; 181 yield obj.loadPrimaryData(); 182 assert.equal(obj.version, objs[type].version); 183 } 184 }) 185 }) 186 187 describe("#loadAllData()", function () { 188 it("should load data on a regular item", function* () { 189 var item = new Zotero.Item('book'); 190 var id = yield item.saveTx(); 191 yield item.loadAllData(); 192 assert.throws(item.getNote.bind(item), 'getNote() can only be called on notes and attachments'); 193 }) 194 195 it("should load data on an attachment item", function* () { 196 var item = new Zotero.Item('attachment'); 197 var id = yield item.saveTx(); 198 yield item.loadAllData(); 199 assert.equal(item.getNote(), ''); 200 }) 201 202 it("should load data on a note item", function* () { 203 var item = new Zotero.Item('note'); 204 var id = yield item.saveTx(); 205 yield item.loadAllData(); 206 assert.equal(item.getNote(), ''); 207 }) 208 }) 209 210 211 describe("#hasChanged()", function () { 212 it("should return false if 'synced' was set but unchanged and nothing else changed", function* () { 213 for (let type of types) { 214 // True 215 var obj = createUnsavedDataObject(type); 216 obj.synced = true; 217 var id = yield obj.saveTx(); 218 assert.isTrue(obj.synced); 219 220 obj.synced = true; 221 assert.isFalse(obj.hasChanged(), type + " shouldn't be changed"); 222 223 // False 224 var obj = createUnsavedDataObject(type); 225 obj.synced = false; 226 var id = yield obj.saveTx(); 227 assert.isFalse(obj.synced); 228 obj.synced = false; 229 assert.isFalse(obj.hasChanged(), type + " shouldn't be changed"); 230 } 231 }) 232 233 it("should return true if 'synced' was set but unchanged and another primary field changed", function* () { 234 for (let type of types) { 235 let obj = createUnsavedDataObject(type); 236 obj.synced = true; 237 yield obj.saveTx(); 238 239 obj.synced = true; 240 obj.version = 1234; 241 assert.isTrue(obj.hasChanged()); 242 } 243 }) 244 }); 245 246 247 describe("#save()", function () { 248 it("should add new identifiers to cache", function* () { 249 for (let type of types) { 250 let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); 251 let obj = createUnsavedDataObject(type); 252 let id = yield obj.saveTx(); 253 let { libraryID, key } = objectsClass.getLibraryAndKeyFromID(id); 254 assert.typeOf(key, 'string'); 255 assert.equal(objectsClass.getIDFromLibraryAndKey(libraryID, key), id); 256 } 257 }) 258 259 it("should reset changed state on objects", function* () { 260 for (let type of types) { 261 let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); 262 let obj = createUnsavedDataObject(type); 263 yield obj.saveTx(); 264 assert.isFalse(obj.hasChanged()); 265 } 266 }) 267 268 it("should handle additional tag change in the middle of a save", function* () { 269 var item = yield createDataObject('item'); 270 item.setTags(['a']); 271 272 var deferred = new Zotero.Promise.defer(); 273 var origFunc = Zotero.Notifier.queue.bind(Zotero.Notifier); 274 sinon.stub(Zotero.Notifier, "queue").callsFake(function (event, type, ids, extraData) { 275 // Add a new tag after the first one has been added to the DB and before the save is 276 // finished. The changed state should've cleared before saving to the DB the first 277 // time, so the second setTags() should mark the item as changed and allow the new tag 278 // to be saved in the second saveTx(). 279 if (event == 'add' && type == 'item-tag') { 280 item.setTags(['a', 'b']); 281 Zotero.Notifier.queue.restore(); 282 deferred.resolve(item.saveTx()); 283 } 284 origFunc(...arguments); 285 }); 286 287 yield Zotero.Promise.all([item.saveTx(), deferred.promise]); 288 assert.sameMembers(item.getTags().map(o => o.tag), ['a', 'b']); 289 var tags = yield Zotero.DB.columnQueryAsync( 290 "SELECT name FROM tags JOIN itemTags USING (tagID) WHERE itemID=?", item.id 291 ); 292 assert.sameMembers(tags, ['a', 'b']); 293 }); 294 295 describe("Edit Check", function () { 296 var group; 297 298 before(function* () { 299 group = yield createGroup({ 300 editable: false 301 }); 302 }); 303 304 it("should disallow saving to read-only libraries", function* () { 305 let item = createUnsavedDataObject('item', { libraryID: group.libraryID }); 306 var e = yield getPromiseError(item.saveTx()); 307 assert.ok(e); 308 assert.include(e.message, "read-only"); 309 }); 310 311 it("should allow saving if skipEditCheck is passed", function* () { 312 let item = createUnsavedDataObject('item', { libraryID: group.libraryID }); 313 var e = yield getPromiseError(item.saveTx({ 314 skipEditCheck: true 315 })); 316 assert.isFalse(e); 317 }); 318 319 it("should allow saving if skipAll is passed", function* () { 320 let item = createUnsavedDataObject('item', { libraryID: group.libraryID }); 321 var e = yield getPromiseError(item.saveTx({ 322 skipAll: true 323 })); 324 assert.isFalse(e); 325 }); 326 }); 327 328 describe("Options", function () { 329 describe("#skipAll", function () { 330 it("should include edit check", function* () { 331 332 }); 333 }); 334 }); 335 }) 336 337 describe("#erase()", function () { 338 it("shouldn't trigger notifier if skipNotifier is passed", function* () { 339 let observerIDs = []; 340 let promises = []; 341 for (let type of types) { 342 let obj = yield createDataObject(type); 343 // For items, test automatic child item deletion 344 if (type == 'item') { 345 yield createDataObject(type, { itemType: 'note', parentID: obj.id }); 346 } 347 348 let deferred = Zotero.Promise.defer(); 349 promises.push(deferred.promise); 350 observerIDs.push(Zotero.Notifier.registerObserver( 351 { 352 notify: function (event) { 353 if (event == 'delete') { 354 deferred.reject("Notifier called for erase on " + type); 355 } 356 } 357 }, 358 type, 359 'test' 360 )); 361 yield obj.eraseTx({ 362 skipNotifier: true 363 }); 364 } 365 yield Zotero.Promise.all(promises) 366 // Give notifier time to trigger 367 .timeout(100).catch(Zotero.Promise.TimeoutError, (e) => {}) 368 369 for (let id of observerIDs) { 370 Zotero.Notifier.unregisterObserver(id); 371 } 372 }) 373 374 it("should delete object versions from sync cache", function* () { 375 for (let type of types) { 376 let obj = yield createDataObject(type); 377 let libraryID = obj.libraryID; 378 let key = obj.key; 379 let json = obj.toJSON(); 380 yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]); 381 yield obj.eraseTx(); 382 let versions = yield Zotero.Sync.Data.Local.getCacheObjectVersions( 383 type, libraryID, key 384 ); 385 assert.lengthOf(versions, 0); 386 } 387 }) 388 }) 389 390 describe("#updateVersion()", function() { 391 it("should update the object version", function* () { 392 for (let type of types) { 393 let obj = yield createDataObject(type); 394 assert.equal(obj.version, 0); 395 396 yield obj.updateVersion(1234); 397 assert.equal(obj.version, 1234); 398 assert.isFalse(obj.hasChanged()); 399 400 obj.synced = true; 401 assert.ok(obj.hasChanged()); 402 yield obj.updateVersion(1235); 403 assert.equal(obj.version, 1235); 404 assert.ok(obj.hasChanged()); 405 406 yield obj.eraseTx(); 407 } 408 }) 409 }) 410 411 describe("#updateSynced()", function() { 412 it("should update the object sync status", function* () { 413 for (let type of types) { 414 let obj = yield createDataObject(type); 415 assert.isFalse(obj.synced); 416 417 yield obj.updateSynced(false); 418 assert.isFalse(obj.synced); 419 assert.isFalse(obj.hasChanged()); 420 421 yield obj.updateSynced(true); 422 assert.ok(obj.synced); 423 assert.isFalse(obj.hasChanged()); 424 425 obj.version = 1234; 426 assert.ok(obj.hasChanged()); 427 yield obj.updateSynced(false); 428 assert.isFalse(obj.synced); 429 assert.ok(obj.hasChanged()); 430 431 yield obj.eraseTx(); 432 } 433 }) 434 435 it("should clear changed status", function* () { 436 var item = createUnsavedDataObject('item'); 437 item.synced = true; 438 yield item.saveTx(); 439 440 // Only synced changed 441 item.synced = false; 442 assert.isTrue(item.hasChanged()); 443 assert.isTrue(item._changed.primaryData.synced); 444 yield item.updateSynced(true); 445 assert.isFalse(item.hasChanged()); 446 // Should clear primary data change object 447 assert.isUndefined(item._changed.primaryData); 448 449 // Another primary field also changed 450 item.setField('dateModified', '2017-02-27 12:34:56'); 451 item.synced = false; 452 assert.isTrue(item.hasChanged()); 453 assert.isTrue(item._changed.primaryData.synced); 454 yield item.updateSynced(true); 455 assert.isTrue(item.hasChanged()); 456 // Should clear only 'synced' change status 457 assert.isUndefined(item._changed.primaryData.synced); 458 }); 459 }) 460 461 describe("Relations", function () { 462 var types = ['collection', 'item']; 463 464 function makeObjectURI(objectType) { 465 var objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType); 466 return 'http://zotero.org/groups/1/' + objectTypePlural + '/' 467 + Zotero.Utilities.generateObjectKey(); 468 } 469 470 describe("#addRelation()", function () { 471 it("should add a relation to an object", function* () { 472 for (let type of types) { 473 let predicate = 'owl:sameAs'; 474 let object = makeObjectURI(type); 475 let obj = createUnsavedDataObject(type); 476 obj.addRelation(predicate, object); 477 yield obj.saveTx(); 478 var relations = obj.getRelations(); 479 assert.property(relations, predicate); 480 assert.include(relations[predicate], object); 481 } 482 }) 483 }) 484 485 describe("#removeRelation()", function () { 486 it("should remove a relation from an object", function* () { 487 for (let type of types) { 488 let predicate = 'owl:sameAs'; 489 let object = makeObjectURI(type); 490 let obj = createUnsavedDataObject(type); 491 obj.addRelation(predicate, object); 492 yield obj.saveTx(); 493 494 obj.removeRelation(predicate, object); 495 yield obj.saveTx(); 496 497 assert.lengthOf(Object.keys(obj.getRelations()), 0); 498 } 499 }) 500 }) 501 502 describe("#hasRelation()", function () { 503 it("should return true if an object has a given relation", function* () { 504 for (let type of types) { 505 let predicate = 'owl:sameAs'; 506 let object = makeObjectURI(type); 507 let obj = createUnsavedDataObject(type); 508 obj.addRelation(predicate, object); 509 yield obj.saveTx(); 510 assert.ok(obj.hasRelation(predicate, object)); 511 } 512 }) 513 }) 514 515 describe("#setRelations()", function () { 516 it("shouldn't allow invalid 'relations' predicates", function* () { 517 var item = new Zotero.Item("book"); 518 assert.throws(() => { 519 item.setRelations({ 520 "0": ["http://example.com/foo"] 521 }); 522 }); 523 }); 524 }); 525 526 describe("#_getLinkedObject()", function () { 527 it("should return a linked object in another library", function* () { 528 var group = yield getGroup(); 529 var item1 = yield createDataObject('item'); 530 var item2 = yield createDataObject('item', { libraryID: group.libraryID }); 531 var item2URI = Zotero.URI.getItemURI(item2); 532 533 yield item2.addLinkedItem(item1); 534 var linkedItem = yield item1.getLinkedItem(item2.libraryID); 535 assert.equal(linkedItem.id, item2.id); 536 }) 537 538 it("shouldn't return reverse linked objects by default", function* () { 539 var group = yield getGroup(); 540 var item1 = yield createDataObject('item'); 541 var item1URI = Zotero.URI.getItemURI(item1); 542 var item2 = yield createDataObject('item', { libraryID: group.libraryID }); 543 544 yield item2.addLinkedItem(item1); 545 var linkedItem = yield item2.getLinkedItem(item1.libraryID); 546 assert.isFalse(linkedItem); 547 }) 548 549 it("should return reverse linked objects with bidirectional flag", function* () { 550 var group = yield getGroup(); 551 var item1 = yield createDataObject('item'); 552 var item1URI = Zotero.URI.getItemURI(item1); 553 var item2 = yield createDataObject('item', { libraryID: group.libraryID }); 554 555 yield item2.addLinkedItem(item1); 556 var linkedItem = yield item2.getLinkedItem(item1.libraryID, true); 557 assert.equal(linkedItem.id, item1.id); 558 }) 559 }) 560 561 describe("#_addLinkedObject()", function () { 562 it("should add an owl:sameAs relation", function* () { 563 var group = yield getGroup(); 564 var item1 = yield createDataObject('item'); 565 var dateModified = item1.getField('dateModified'); 566 var item2 = yield createDataObject('item', { libraryID: group.libraryID }); 567 var item2URI = Zotero.URI.getItemURI(item2); 568 569 yield item2.addLinkedItem(item1); 570 var preds = item1.getRelationsByPredicate(Zotero.Relations.linkedObjectPredicate); 571 assert.include(preds, item2URI); 572 573 // Make sure Date Modified hasn't changed 574 assert.equal(item1.getField('dateModified'), dateModified); 575 }) 576 }) 577 }) 578 })