feat: add tests (#1220)
This commit is contained in:
parent
c4a88df09f
commit
65c220f292
|
|
@ -1,9 +1,12 @@
|
||||||
name: Release
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- v**
|
- v**
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
@ -11,8 +14,30 @@ permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install deps
|
||||||
|
run: npm install -f
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
# If it's triggered by a tag and the test job is successful, release the package
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: test
|
||||||
|
if: github.event_name == 'push' && needs.test.result == 'success'
|
||||||
env:
|
env:
|
||||||
# Allow triggering other workflows
|
# Allow triggering other workflows
|
||||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ async function startup({ id, version, resourceURI, rootURI }, reason) {
|
||||||
`${rootURI}/chrome/content/scripts/__addonRef__.js`,
|
`${rootURI}/chrome/content/scripts/__addonRef__.js`,
|
||||||
ctx,
|
ctx,
|
||||||
);
|
);
|
||||||
Zotero.__addonInstance__.hooks.onStartup();
|
await Zotero.__addonInstance__.hooks.onStartup();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMainWindowLoad({ window: win }) {
|
function onMainWindowLoad({ window: win }) {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -17,7 +17,7 @@
|
||||||
"build-dev": "tsc --noEmit && zotero-plugin build --dev && cd build/addon && zip -r ../zotero-better-notes-dev.xpi .",
|
"build-dev": "tsc --noEmit && zotero-plugin build --dev && cd build/addon && zip -r ../zotero-better-notes-dev.xpi .",
|
||||||
"release": "zotero-plugin release",
|
"release": "zotero-plugin release",
|
||||||
"lint": "prettier --write . && eslint . --ext .ts --fix",
|
"lint": "prettier --write . && eslint . --ext .ts --fix",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "zotero-plugin test --abort-on-fail --exit-on-finish",
|
||||||
"update-deps": "npm update --save"
|
"update-deps": "npm update --save"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
@ -57,15 +57,18 @@
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"unist-util-visit-parents": "^6.0.1",
|
"unist-util-visit-parents": "^6.0.1",
|
||||||
"yamljs": "^0.3.0",
|
"yamljs": "^0.3.0",
|
||||||
"zotero-plugin-toolkit": "^4.0.9"
|
"zotero-plugin-scaffold": "^0.1.8-beta.3",
|
||||||
|
"zotero-plugin-toolkit": "^4.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||||
"@prettier/plugin-xml": "^3.2.2",
|
"@prettier/plugin-xml": "^3.2.2",
|
||||||
"@types/browser-or-node": "^1.3.2",
|
"@types/browser-or-node": "^1.3.2",
|
||||||
|
"@types/chai": "^5.0.1",
|
||||||
"@types/diff": "^5.0.9",
|
"@types/diff": "^5.0.9",
|
||||||
"@types/html-docx-js": "^0.3.4",
|
"@types/html-docx-js": "^0.3.4",
|
||||||
"@types/katex": "^0.16.7",
|
"@types/katex": "^0.16.7",
|
||||||
|
"@types/mocha": "^10.0.10",
|
||||||
"@types/node": "^20.10.4",
|
"@types/node": "^20.10.4",
|
||||||
"@types/path-browserify": "^1.0.2",
|
"@types/path-browserify": "^1.0.2",
|
||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^3.0.8",
|
||||||
|
|
@ -85,7 +88,6 @@
|
||||||
"replace-in-file": "^7.2.0",
|
"replace-in-file": "^7.2.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"xslt3": "^2.6.0",
|
"xslt3": "^2.6.0",
|
||||||
"zotero-plugin-scaffold": "^0.1.6",
|
|
||||||
"zotero-types": "^3.0.2"
|
"zotero-types": "^3.0.2"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ class Addon {
|
||||||
public data: {
|
public data: {
|
||||||
uid: string;
|
uid: string;
|
||||||
alive: boolean;
|
alive: boolean;
|
||||||
// Env type, see build.js
|
env: "development" | "production" | "test";
|
||||||
env: "development" | "production";
|
initialized?: boolean;
|
||||||
ztoolkit: ZToolkit;
|
ztoolkit: ZToolkit;
|
||||||
// ztoolkit: ZoteroToolkit;
|
// ztoolkit: ZoteroToolkit;
|
||||||
locale?: {
|
locale?: {
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,9 @@ async function onStartup() {
|
||||||
setSyncing();
|
setSyncing();
|
||||||
|
|
||||||
await onMainWindowLoad(Zotero.getMainWindow());
|
await onMainWindowLoad(Zotero.getMainWindow());
|
||||||
|
|
||||||
|
// For testing
|
||||||
|
addon.data.initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onMainWindowLoad(win: _ZoteroTypes.MainWindow): Promise<void> {
|
async function onMainWindowLoad(win: _ZoteroTypes.MainWindow): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ function closeRelationServer() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRelationServer() {
|
async function getRelationServer(): Promise<MessageHelper<typeof handlers>> {
|
||||||
if (!addon.data.relation.server) {
|
if (!addon.data.relation.server) {
|
||||||
const worker = new Worker(
|
const worker = new Worker(
|
||||||
`chrome://${config.addonRef}/content/scripts/relationWorker.js`,
|
`chrome://${config.addonRef}/content/scripts/relationWorker.js`,
|
||||||
|
|
@ -95,7 +95,9 @@ async function updateNoteLinkRelation(noteID: number) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNoteLinkOutboundRelation(noteID: number) {
|
async function getNoteLinkOutboundRelation(
|
||||||
|
noteID: number,
|
||||||
|
): Promise<LinkModel[]> {
|
||||||
const note = Zotero.Items.get(noteID);
|
const note = Zotero.Items.get(noteID);
|
||||||
const fromLibID = note.libraryID;
|
const fromLibID = note.libraryID;
|
||||||
const fromKey = note.key;
|
const fromKey = note.key;
|
||||||
|
|
@ -104,7 +106,9 @@ async function getNoteLinkOutboundRelation(noteID: number) {
|
||||||
).proxy.getOutboundLinks(fromLibID, fromKey);
|
).proxy.getOutboundLinks(fromLibID, fromKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNoteLinkInboundRelation(noteID: number) {
|
async function getNoteLinkInboundRelation(
|
||||||
|
noteID: number,
|
||||||
|
): Promise<LinkModel[]> {
|
||||||
const note = Zotero.Items.get(noteID);
|
const note = Zotero.Items.get(noteID);
|
||||||
const toLibID = note.libraryID;
|
const toLibID = note.libraryID;
|
||||||
const toKey = note.key;
|
const toKey = note.key;
|
||||||
|
|
@ -135,13 +139,19 @@ async function linkAnnotationToTarget(model: AnnotationModel) {
|
||||||
return await (await getRelationServer()).proxy.linkAnnotationToTarget(model);
|
return await (await getRelationServer()).proxy.linkAnnotationToTarget(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLinkTargetByAnnotation(fromLibID: number, fromKey: string) {
|
async function getLinkTargetByAnnotation(
|
||||||
|
fromLibID: number,
|
||||||
|
fromKey: string,
|
||||||
|
): Promise<AnnotationModel | undefined> {
|
||||||
return await (
|
return await (
|
||||||
await getRelationServer()
|
await getRelationServer()
|
||||||
).proxy.getLinkTargetByAnnotation(fromLibID, fromKey);
|
).proxy.getLinkTargetByAnnotation(fromLibID, fromKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAnnotationByLinkTarget(toLibID: number, toKey: string) {
|
async function getAnnotationByLinkTarget(
|
||||||
|
toLibID: number,
|
||||||
|
toKey: string,
|
||||||
|
): Promise<AnnotationModel | undefined> {
|
||||||
return await (
|
return await (
|
||||||
await getRelationServer()
|
await getRelationServer()
|
||||||
).proxy.getAnnotationByLinkTarget(toLibID, toKey);
|
).proxy.getAnnotationByLinkTarget(toLibID, toKey);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { config } from "../../package.json";
|
||||||
|
|
||||||
|
describe("Startup", function () {
|
||||||
|
it("should have plugin instance defined", function () {
|
||||||
|
assert.isNotEmpty(Zotero[config.addonRef]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"include": ["./**/*.ts", "typings", "../node_modules/zotero-types"],
|
||||||
|
"exclude": []
|
||||||
|
}
|
||||||
|
|
@ -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<void>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type * as chai from "chai";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
const expect: typeof chai.expect;
|
||||||
|
const assert: typeof chai.assert;
|
||||||
|
const debug: (...data: any[]) => void;
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
export async function waitNoMoreThan<T>(
|
||||||
|
promise: Promise<T>,
|
||||||
|
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<Window>((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");
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"alwaysStrict": false,
|
"alwaysStrict": false,
|
||||||
"strict": true
|
"strict": true,
|
||||||
},
|
},
|
||||||
"include": ["src", "typings", "node_modules/zotero-types"],
|
"include": ["src", "typings", "node_modules/zotero-types"],
|
||||||
"exclude": ["build", "addon"]
|
"exclude": ["build", "addon", "test"],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ import pkg from "./package.json";
|
||||||
import { defineConfig } from "zotero-plugin-scaffold";
|
import { defineConfig } from "zotero-plugin-scaffold";
|
||||||
import { replaceInFile } from "replace-in-file";
|
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({
|
export default defineConfig({
|
||||||
source: ["src", "addon"],
|
source: ["src", "addon"],
|
||||||
dist: "build",
|
dist: "build",
|
||||||
|
|
@ -64,6 +68,14 @@ export default defineConfig({
|
||||||
all: true,
|
all: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
test: {
|
||||||
|
entries: ["test/"],
|
||||||
|
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:
|
// If you need to see a more detailed build log, uncomment the following line:
|
||||||
// logLevel: "trace",
|
// logLevel: "trace",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue