diff --git a/package-lock.json b/package-lock.json index 2579d89..dab5b4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "yamljs": "^0.3.0", - "zotero-plugin-scaffold": "^0.1.8-beta.1", + "zotero-plugin-scaffold": "^0.1.8-beta.3", "zotero-plugin-toolkit": "^4.0.11" }, "devDependencies": { @@ -12259,9 +12259,9 @@ } }, "node_modules/zotero-plugin-scaffold": { - "version": "0.1.8-beta.1", - "resolved": "https://registry.npmjs.org/zotero-plugin-scaffold/-/zotero-plugin-scaffold-0.1.8-beta.1.tgz", - "integrity": "sha512-cZCtsR8S8GyV5IGmN+m3w4GOcA3okiV0cl55+FLvmUK4nNImLSU1p3FXKXXoMG9e5M4drBDRfiG17klfim+6NQ==", + "version": "0.1.8-beta.3", + "resolved": "https://registry.npmjs.org/zotero-plugin-scaffold/-/zotero-plugin-scaffold-0.1.8-beta.3.tgz", + "integrity": "sha512-i6Uu8EA+XXhwznhyB35+dfn9PyttKGNk5/AvGc18COSubW/wihOjJiWBLytrhl8JDsdch9pDh4qtUPfw/Il8iQ==", "dependencies": { "@commander-js/extra-typings": "^12.1.0", "@inquirer/prompts": "^7.0.0", diff --git a/package.json b/package.json index 140ebb2..bf42014 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "yamljs": "^0.3.0", - "zotero-plugin-scaffold": "^0.1.8-beta.1", + "zotero-plugin-scaffold": "^0.1.8-beta.3", "zotero-plugin-toolkit": "^4.0.11" }, "devDependencies": { diff --git a/src/addon.ts b/src/addon.ts index 3567db8..c0853de 100644 --- a/src/addon.ts +++ b/src/addon.ts @@ -14,6 +14,7 @@ class Addon { uid: string; alive: boolean; env: "development" | "production" | "test"; + initialized?: boolean; ztoolkit: ZToolkit; // ztoolkit: ZoteroToolkit; locale?: { diff --git a/src/hooks.ts b/src/hooks.ts index eb57d3e..48d9168 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -80,6 +80,9 @@ async function onStartup() { setSyncing(); await onMainWindowLoad(Zotero.getMainWindow()); + + // For testing + addon.data.initialized = true; } async function onMainWindowLoad(win: _ZoteroTypes.MainWindow): Promise { diff --git a/test/tests/array.spec.js b/test/tests/array.spec.js deleted file mode 100644 index 92e39af..0000000 --- a/test/tests/array.spec.js +++ /dev/null @@ -1,9 +0,0 @@ -describe("Array", function () { - it("should return -1 when the value is not present", function () { - expect([1, 2, 3].indexOf(4)).to.equal(-1); - }); - - it("should return the index when the value is present", function () { - expect([1, 2, 3].indexOf(2)).to.equal(1); - }); -}); diff --git a/test/tests/startup.spec.ts b/test/tests/startup.spec.ts new file mode 100644 index 0000000..2b579f9 --- /dev/null +++ b/test/tests/startup.spec.ts @@ -0,0 +1,7 @@ +import { config } from "../../package.json"; + +describe("Startup", function () { + it("should have plugin instance defined", function () { + assert.isNotEmpty(Zotero[config.addonRef]); + }); +}); diff --git a/test/tests/workspace.spec.ts b/test/tests/workspace.spec.ts new file mode 100644 index 0000000..89d371c --- /dev/null +++ b/test/tests/workspace.spec.ts @@ -0,0 +1,53 @@ +import { BasicTool } from "zotero-plugin-toolkit"; +import { waitForNoteWindow, waitForTabSelectEvent } from "../utils/wait"; +import { resetAll } from "../utils/status"; + +describe("Workspace", function () { + const tool = new BasicTool(); + + this.beforeAll(async function () { + await resetAll(); + }); + + this.afterEach(async function () { + await resetAll(); + }); + + it("should open note in tab", async function () { + const note = new Zotero.Item("note"); + await note.saveTx(); + + const promise = waitForTabSelectEvent(); + + // An example of how to debug the test + debug("Calling viewItems"); + + tool.getGlobal("ZoteroPane").viewItems([note]); + await promise; + + const selectedID = tool.getGlobal("Zotero_Tabs").selectedID; + const selectedTab = tool.getGlobal("Zotero_Tabs")._getTab(selectedID); + + expect(selectedTab.tab.data.itemID).to.be.equal(note.id); + }); + + it("should open note in window if shift key is pressed", async function () { + const note = new Zotero.Item("note"); + await note.saveTx(); + + const promise = waitForNoteWindow(); + + tool.getGlobal("ZoteroPane").viewItems([note], { shiftKey: true }); + const win = await promise; + + expect(win).to.be.not.null; + + const editor = win!.document.querySelector( + "#zotero-note-editor", + ) as EditorElement; + + expect(editor).to.be.not.null; + + expect(editor.item?.id).to.be.equal(note.id); + }); +}); diff --git a/test/tests/zotero.spec.ts b/test/tests/zotero.spec.ts deleted file mode 100644 index e83637e..0000000 --- a/test/tests/zotero.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe("Zotero", function () { - it("should have Zotero defined", function () { - expect(Zotero).to.not.be.undefined; - }); -}); diff --git a/test/tsconfig.json b/test/tsconfig.json index 172b640..ddfe1f0 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "../tsconfig.json", - "include": ["./**/*.spec.ts", "typings", "../node_modules/zotero-types"], + "include": ["./**/*.ts", "typings", "../node_modules/zotero-types"], "exclude": [] } diff --git a/test/typings/editor.d.ts b/test/typings/editor.d.ts new file mode 100644 index 0000000..76f91c9 --- /dev/null +++ b/test/typings/editor.d.ts @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/ban-types */ +declare interface EditorCore { + debouncedUpdate: Function; + disableDrag: boolean; + docChanged: boolean; + isAttachmentNote: false; + metadata: { + _citationItems: { itemData: { [k: string]: any } }[]; + uris: string[]; + }; + nodeViews: any[]; + onUpdateState: Function; + options: { + isAttachmentNote: false; + onImportImages: Function; + onInsertObject: Function; + onOpenAnnotation: Function; + onOpenCitationPage: Function; + onOpenCitationPopup: Function; + onOpenContextMenu: Function; + onOpenURL: Function; + onShowCitationItem: Function; + onSubscribe: Function; + onUnsubscribe: Function; + onUpdate: Function; + onUpdateCitationItemsList: Function; + placeholder: boolean; + readOnly: boolean; + reloaded: boolean; + smartQuotes: boolean; + unsaved: boolean; + value: string; + }; + pluginState: { [k: string]: any }; + provider: import("react").Provider; + readOnly: boolean; + reloaded: boolean; + view: import("prosemirror-view").EditorView & { + docView: NodeViewDesc; + }; +} + +declare type EditorAPI = + typeof import("../src/extras/editorScript").BetterNotesEditorAPI; + +declare interface EditorElement extends XULBoxElement { + _iframe: HTMLIFrameElement; + _editorInstance: Zotero.EditorInstance; + _initialized?: boolean; + mode?: "edit" | "view"; + viewMode?: string; + parent?: Zotero.Item; + item?: Zotero.Item; + getCurrentInstance(): Zotero.EditorInstance; + initEditor(): Promise; +} diff --git a/test/typings/global.d.ts b/test/typings/global.d.ts index a1fcf88..5ea8e81 100644 --- a/test/typings/global.d.ts +++ b/test/typings/global.d.ts @@ -3,4 +3,5 @@ import type * as chai from "chai"; declare global { const expect: typeof chai.expect; const assert: typeof chai.assert; + const debug: (...data: any[]) => void; } diff --git a/test/utils/status.ts b/test/utils/status.ts new file mode 100644 index 0000000..223c959 --- /dev/null +++ b/test/utils/status.ts @@ -0,0 +1,21 @@ +export async function resetData() { + // Delete collections, items, tags + const collections = await Zotero.Collections.getAllIDs( + Zotero.Libraries.userLibraryID, + ); + await Zotero.Collections.erase(collections); + + const items = await Zotero.Items.getAllIDs(Zotero.Libraries.userLibraryID); + await Zotero.Items.erase(items); +} + +export async function resetTabs() { + const win = Zotero.getMainWindow(); + const Zotero_Tabs = win.Zotero_Tabs; + Zotero_Tabs.closeAll(); +} + +export async function resetAll() { + await resetTabs(); + await resetData(); +} diff --git a/test/utils/wait.ts b/test/utils/wait.ts new file mode 100644 index 0000000..c674535 --- /dev/null +++ b/test/utils/wait.ts @@ -0,0 +1,96 @@ +export async function waitNoMoreThan( + promise: Promise, + timeout: number = 3000, + message: string = "Timeout", +) { + let resolved = false; + + return Promise.any([ + promise.then((result) => { + resolved = true; + return result; + }), + Zotero.Promise.delay(timeout).then(() => { + if (resolved) return; + throw new Error(message); + }), + ]); +} + +export async function waitForNotifierEvent( + event: _ZoteroTypes.Notifier.Event, + type: _ZoteroTypes.Notifier.Type, + timeout: number = 3000, +) { + if (!event) throw new Error("event not provided"); + let resolved = false; + + return waitNoMoreThan( + new Promise((resolve, reject) => { + const notifierID = Zotero.Notifier.registerObserver( + { + notify: function (ev, type, ids, extraData) { + if (ev == event) { + Zotero.Notifier.unregisterObserver(notifierID); + resolved = true; + + resolve({ + ids: ids, + extraData: extraData, + }); + } + }, + }, + [type], + "test", + 101, + ); + }), + timeout, + ); +} + +export function waitForTabSelectEvent(timeout: number = 3000) { + return waitForNotifierEvent("select", "tab", timeout); +} + +/** + * Waits for a window with a specific URL to open. Returns a promise for the window, and + * optionally passes the window to a callback immediately for use with modal dialogs, + * which prevent async code from continuing + */ +export async function waitForWindow(uri: string, timeout: number = 3000) { + return waitNoMoreThan( + new Promise((resolve, reject) => { + const loadObserver = function (ev: Event) { + ev.originalTarget?.removeEventListener("load", loadObserver, false); + const href = (ev.target as Window)?.location.href; + Zotero.debug("Window opened: " + href); + + if (href != uri) { + Zotero.debug(`Ignoring window ${href} in waitForWindow()`); + return; + } + + Services.ww.unregisterNotification(winObserver); + const win = ev.target?.ownerGlobal; + // Give window code time to run on load + win?.setTimeout(function () { + resolve(win); + }); + }; + const winObserver = { + observe: function (subject: Window, topic: string, data: any) { + if (topic != "domwindowopened") return; + subject.addEventListener("load", loadObserver, false); + }, + } as nsIObserver; + Services.ww.registerNotification(winObserver); + }), + timeout, + ); +} + +export async function waitForNoteWindow() { + return await waitForWindow("chrome://zotero/content/note.xhtml"); +} diff --git a/test/utils/plugin.ts b/test/utils/window.ts similarity index 100% rename from test/utils/plugin.ts rename to test/utils/window.ts diff --git a/zotero-plugin.config.ts b/zotero-plugin.config.ts index ddb37ac..36d3ea2 100644 --- a/zotero-plugin.config.ts +++ b/zotero-plugin.config.ts @@ -2,6 +2,10 @@ import pkg from "./package.json"; import { defineConfig } from "zotero-plugin-scaffold"; import { replaceInFile } from "replace-in-file"; +const TEST_PREFS = {}; +// Disable user guide, keep in sync with src/modules/userGuide.ts +TEST_PREFS[`${pkg.config.prefsPrefix}.latestTourVersion`] = 1; + export default defineConfig({ source: ["src", "addon"], dist: "build", @@ -66,10 +70,11 @@ export default defineConfig({ }, test: { entries: ["test/"], - prefs: {}, + prefs: TEST_PREFS, abortOnFail: true, exitOnFinish: false, hooks: {}, + waitForPlugin: `() => Zotero.${pkg.config.addonRef}.data.initialized`, }, // If you need to see a more detailed build log, uncomment the following line: