From 2b928b8a01452a331d8700ca6f804d26e6b84ace Mon Sep 17 00:00:00 2001 From: TheNEwmanator15 <3686761+Thenewmanator15@users.noreply.github.com> Date: Sat, 26 Sep 2026 01:19:45 +0100 Subject: [PATCH] Test item data loading and the field lookup cache The _loadItemData() tests pass on the code before this change too: they pin the normalization it must keep (newlines, ISBN hyphenation, base-mapped fields, absent fields) and that setField() is still called for every value. Co-Authored-By: Claude Opus 5.5 --- test/tests/itemFieldsTest.js | 38 +++++++++++++++++++ test/tests/itemsTest.js | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/test/tests/itemFieldsTest.js b/test/tests/itemFieldsTest.js index 5ea3e09f09..c2c48ba0c1 100644 --- a/test/tests/itemFieldsTest.js +++ b/test/tests/itemFieldsTest.js @@ -64,4 +64,42 @@ describe("Zotero.ItemFields", function () { assert.equal(Zotero.ItemFields.getDirection('book', 'creator-0-lastName', 'ar'), 'rtl'); }); }); + + describe("#_getLoadInfo()", function () { + it("should resolve a base field to the item type's field", function () { + var info = Zotero.ItemFields._getLoadInfo( + Zotero.ItemTypes.getID('thesis'), Zotero.ItemFields.getID('publisher') + ); + assert.equal(info.fieldID, Zotero.ItemFields.getID('university')); + assert.isTrue(info.valid); + }); + + it("should mark a field that isn't valid for the item type", function () { + var info = Zotero.ItemFields._getLoadInfo( + Zotero.ItemTypes.getID('book'), Zotero.ItemFields.getID('websiteTitle') + ); + assert.isFalse(info.valid); + }); + + it("should return false for an unknown field", function () { + assert.isFalse(Zotero.ItemFields._getLoadInfo(Zotero.ItemTypes.getID('book'), 999999)); + }); + + // A plugin that replaces a lookup method must still be consulted + it("should use a replaced lookup method", function () { + var bookID = Zotero.ItemTypes.getID('book'); + var titleID = Zotero.ItemFields.getID('title'); + assert.isTrue(Zotero.ItemFields._getLoadInfo(bookID, titleID).valid); + + var original = Zotero.ItemFields.isValidForType; + Zotero.ItemFields.isValidForType = () => false; + try { + assert.isFalse(Zotero.ItemFields._getLoadInfo(bookID, titleID).valid); + } + finally { + Zotero.ItemFields.isValidForType = original; + } + assert.isTrue(Zotero.ItemFields._getLoadInfo(bookID, titleID).valid); + }); + }); }) diff --git a/test/tests/itemsTest.js b/test/tests/itemsTest.js index 9842f2bd60..fb442e012c 100644 --- a/test/tests/itemsTest.js +++ b/test/tests/itemsTest.js @@ -799,4 +799,75 @@ describe("Zotero.Items", function () { assert.include(ids, att.id); }); }); + + describe("#_loadItemData()", function () { + // Point a stored field at a raw value, bypassing setField(), then reload from the DB + async function loadRaw(item, field, raw) { + var fieldID = Zotero.ItemFields.getID(field); + var valueID = await Zotero.DB.valueQueryAsync("SELECT valueID FROM itemDataValues WHERE value=?", raw); + if (!valueID) { + valueID = Zotero.ID.get('itemDataValues'); + await Zotero.DB.queryAsync("INSERT INTO itemDataValues (valueID, value) VALUES (?, ?)", [valueID, raw]); + } + await Zotero.DB.queryAsync( + "REPLACE INTO itemData (itemID, fieldID, valueID) VALUES (?, ?, ?)", [item.id, fieldID, valueID] + ); + await Zotero.Items._loadDataTypeInLibrary('itemData', item.libraryID, [item.id]); + } + + it("should strip newlines from single-line fields when loading", async function () { + var item = await createDataObject('item', { title: "Title" }); + await loadRaw(item, 'title', "Line one\nLine two"); + assert.equal(item.getField('title'), "Line one Line two"); + }); + + it("should keep newlines in multiline fields when loading", async function () { + var item = await createDataObject('item'); + await loadRaw(item, 'abstractNote', "Line one\nLine two"); + assert.equal(item.getField('abstractNote'), "Line one\nLine two"); + }); + + it("should hyphenate ISBNs when loading", async function () { + var item = await createDataObject('item', { itemType: 'book' }); + await loadRaw(item, 'ISBN', "9780306406157"); + assert.equal(item.getField('ISBN'), "978-0-306-40615-7"); + }); + + it("should load a base-mapped field into the item type's field", async function () { + var item = await createDataObject('item', { itemType: 'thesis' }); + await loadRaw(item, 'university', "University of Somewhere"); + assert.equal(item.getField('university'), "University of Somewhere"); + assert.equal(item.getField('publisher', false, true), "University of Somewhere"); + }); + + it("should set absent fields to empty", async function () { + var item = await createDataObject('item', { itemType: 'book' }); + await Zotero.Items._loadDataTypeInLibrary('itemData', item.libraryID, [item.id]); + assert.strictEqual(item.getField('volume'), ""); + }); + + // Plugins wrap Zotero.Item.prototype.setField (e.g., zotero-plugin-toolkit's + // FieldHook), so loading must keep calling it for every value + it("should call setField for every loaded value", async function () { + var item = await createDataObject('item', { itemType: 'book', title: "Called" }); + var original = Zotero.Item.prototype.setField; + var calls = []; + Zotero.Item.prototype.setField = function (field, value, loadIn) { + if (this.id == item.id) { + calls.push([field, value, loadIn]); + } + return original.apply(this, arguments); + }; + try { + await Zotero.Items._loadDataTypeInLibrary('itemData', item.libraryID, [item.id]); + } + finally { + Zotero.Item.prototype.setField = original; + } + var titleID = Zotero.ItemFields.getID('title'); + assert.deepInclude(calls, [titleID, "Called", true]); + // One call per field of the item type: stored values plus absent ones set empty + assert.lengthOf(calls, Zotero.ItemFields.getItemTypeFields(item.itemTypeID).length); + }); + }); });