zotero-plugin-scaffold 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { loadConfig as loadConfig$1 } from 'c12';
2
- import { isPlainObject, kebabCase, mapValues, toMerged, escapeRegExp, delay, debounce } from 'es-toolkit';
2
+ import { isPlainObject, kebabCase, mapValues, toMerged, escapeRegExp, debounce, delay } from 'es-toolkit';
3
3
  import { readJsonSync, outputFile, emptyDir, copy, readJSON, outputJSON, move, writeJson, pathExists, ensureDir, remove } from 'fs-extra/esm';
4
4
  import { createHooks } from 'hookable';
5
5
  import process from 'node:process';
@@ -11,7 +11,7 @@ import { createReadStream, existsSync } from 'node:fs';
11
11
  import { readFile, writeFile, stat } from 'node:fs/promises';
12
12
  import { basename, dirname, join, resolve, extname } from 'node:path';
13
13
  import AdmZip from 'adm-zip';
14
- import { build } from 'esbuild';
14
+ import { build, context } from 'esbuild';
15
15
  import { glob, globSync } from 'tinyglobby';
16
16
  import { createHash } from 'node:crypto';
17
17
  import { parseSync, printSync } from '@swc/core';
@@ -23,8 +23,8 @@ import net from 'node:net';
23
23
  import { Buffer } from 'node:buffer';
24
24
  import domain from 'node:domain';
25
25
  import EventEmitter from 'node:events';
26
- import http from 'node:http';
27
26
  import { Xvfb } from 'xvfb-ts';
27
+ import http from 'node:http';
28
28
 
29
29
  const factory = (...formats) =>
30
30
  new Proxy(util.styleText.bind(util, formats), {
@@ -138,7 +138,7 @@ ${error.stack}`;
138
138
  const { space = DEFAULT_OPTIONS.SPACE, newLine = DEFAULT_OPTIONS.NEW_LINE } = options;
139
139
  const formattedContent = this.formatContent(content);
140
140
  const output = [
141
- " ".repeat(space),
141
+ " ".repeat(space),
142
142
  config.symbol,
143
143
  formattedContent
144
144
  ].join(" ");
@@ -329,11 +329,10 @@ const defaultConfig = {
329
329
  timeout: 1e4
330
330
  },
331
331
  abortOnFail: false,
332
- exitOnFinish: false,
333
332
  headless: false,
334
333
  startupDelay: 1e3,
335
334
  waitForPlugin: "() => true",
336
- watch: false,
335
+ watch: true,
337
336
  hooks: {}
338
337
  },
339
338
  logLevel: "INFO"
@@ -447,7 +446,7 @@ class PrefsManager {
447
446
  return value;
448
447
  }
449
448
  /**
450
- * Parse Method 2 - Using swc
449
+ * Render Method 2 - Using swc
451
450
  */
452
451
  render() {
453
452
  const span = { start: 0, end: 0, ctxt: 0 };
@@ -516,7 +515,7 @@ class PrefsManager {
516
515
  return code;
517
516
  }
518
517
  /**
519
- * Parse Method 1 - Using string
518
+ * Render Method 1 - Using string
520
519
  * @deprecated
521
520
  */
522
521
  renderByString() {
@@ -626,21 +625,21 @@ class Build extends Base {
626
625
  await this.ctx.hooks.callHook("build:init", this.ctx);
627
626
  await emptyDir(dist);
628
627
  await this.ctx.hooks.callHook("build:mkdir", this.ctx);
629
- this.logger.tip("Preparing static assets");
628
+ this.logger.tip("Preparing static assets", { space: 1 });
630
629
  await this.makeAssets();
631
630
  await this.ctx.hooks.callHook("build:copyAssets", this.ctx);
632
- this.logger.debug("Preparing manifest");
631
+ this.logger.debug("Preparing manifest", { space: 2 });
633
632
  await this.makeManifest();
634
633
  await this.ctx.hooks.callHook("build:makeManifest", this.ctx);
635
- this.logger.debug("Preparing locale files");
634
+ this.logger.debug("Preparing locale files", { space: 2 });
636
635
  await this.prepareLocaleFiles();
637
636
  await this.ctx.hooks.callHook("build:fluent", this.ctx);
638
637
  await this.preparePrefs();
639
- this.logger.tip("Bundling scripts");
638
+ this.logger.tip("Bundling scripts", { space: 1 });
640
639
  await this.esbuild();
641
640
  await this.ctx.hooks.callHook("build:bundle", this.ctx);
642
641
  if (process.env.NODE_ENV === "production") {
643
- this.logger.tip("Packing plugin");
642
+ this.logger.tip("Packing plugin", { space: 1 });
644
643
  await this.pack();
645
644
  await this.ctx.hooks.callHook("build:pack", this.ctx);
646
645
  await this.makeUpdateJson();
@@ -1169,7 +1168,7 @@ class Release extends Base {
1169
1168
  conventionalChangelog({ releaseCount: 2, preset: "angular" }, { version }).on("data", (chunk) => {
1170
1169
  changelog += chunk.toString();
1171
1170
  }).on("end", () => {
1172
- changelog = changelog.split("\n").filter((line) => !line.match(/^## .*/) || !line.match(/^# .*/)).join("\n").trim();
1171
+ changelog = changelog.split("\n").filter((line) => !line.match(/^## .*/) && !line.match(/^# .*/)).join("\n").trim();
1173
1172
  resolve(changelog);
1174
1173
  }).on("error", (err) => {
1175
1174
  reject(err);
@@ -1243,6 +1242,42 @@ ${changelog}
1243
1242
  }
1244
1243
  }
1245
1244
 
1245
+ function watch(source, event) {
1246
+ const watcher = chokidar.watch(source, {
1247
+ ignored: /(^|[/\\])\../,
1248
+ // ignore dotfiles
1249
+ persistent: true
1250
+ });
1251
+ const onChangeDebounced = safeDebounce(event.onChange);
1252
+ watcher.on("ready", async () => {
1253
+ if (event.onReady)
1254
+ await event.onReady();
1255
+ logger.clear();
1256
+ logger.ready("Server Ready!");
1257
+ }).on("change", async (path) => {
1258
+ logger.clear();
1259
+ logger.info(`${path} changed`);
1260
+ await onChangeDebounced(path);
1261
+ }).on("error", async (err) => {
1262
+ if (event.onError)
1263
+ await event.onError(err);
1264
+ logger.fail("Server start failed!");
1265
+ logger.error(err);
1266
+ });
1267
+ }
1268
+ function safeDebounce(fn) {
1269
+ if (!fn)
1270
+ return () => {
1271
+ };
1272
+ return debounce(async (...args) => {
1273
+ try {
1274
+ await fn(...args);
1275
+ } catch (error) {
1276
+ logger.error(error);
1277
+ }
1278
+ }, 500);
1279
+ }
1280
+
1246
1281
  function isRunning(query) {
1247
1282
  let cmd = "";
1248
1283
  switch (process.platform) {
@@ -1719,9 +1754,7 @@ class RemoteFirefox {
1719
1754
  const addon = await this.getInstalledAddon(addonId);
1720
1755
  await this.checkForAddonReloading(addon);
1721
1756
  await this.addonRequest(addon, "reload");
1722
- logger.success(
1723
- `\rLast extension reload: ${(/* @__PURE__ */ new Date()).toTimeString()}`
1724
- );
1757
+ logger.success(`Last extension reload: ${(/* @__PURE__ */ new Date()).toTimeString()}`);
1725
1758
  }
1726
1759
  }
1727
1760
  function findFreeTcpPort() {
@@ -2037,37 +2070,23 @@ class Serve extends Base {
2037
2070
  */
2038
2071
  async watch() {
2039
2072
  const { source } = this.ctx;
2040
- const watcher = chokidar.watch(source, {
2041
- ignored: /(^|[/\\])\../,
2042
- // ignore dotfiles
2043
- persistent: true
2044
- });
2045
- const onChangeDebounced = debounce(async (path) => {
2046
- await this.onChange(path).catch((err) => {
2047
- this.logger.error(err);
2048
- });
2049
- }, 500);
2050
- watcher.on("ready", async () => {
2051
- await this.ctx.hooks.callHook("serve:ready", this.ctx);
2052
- this.logger.clear();
2053
- this.logger.ready("Server Ready!");
2054
- }).on("change", async (path) => {
2055
- this.logger.clear();
2056
- this.logger.info(`${path} changed`);
2057
- await onChangeDebounced(path);
2058
- }).on("error", (err) => {
2059
- this.logger.fail("Server start failed!");
2060
- this.logger.error(err);
2061
- });
2062
- }
2063
- async onChange(path) {
2064
- await this.ctx.hooks.callHook("serve:onChanged", this.ctx, path);
2065
- if (path.endsWith(".ts") || path.endsWith(".tsx")) {
2066
- await this.builder.esbuild();
2067
- } else {
2068
- await this.builder.run();
2069
- }
2070
- await this.reload();
2073
+ watch(
2074
+ source,
2075
+ {
2076
+ onReady: async () => {
2077
+ await this.ctx.hooks.callHook("serve:ready", this.ctx);
2078
+ },
2079
+ onChange: async (path) => {
2080
+ await this.ctx.hooks.callHook("serve:onChanged", this.ctx, path);
2081
+ if (path.endsWith(".ts") || path.endsWith(".tsx")) {
2082
+ await this.builder.esbuild();
2083
+ } else {
2084
+ await this.builder.run();
2085
+ }
2086
+ await this.reload();
2087
+ }
2088
+ }
2089
+ );
2071
2090
  }
2072
2091
  async reload() {
2073
2092
  this.logger.tip("Reloading...");
@@ -2101,11 +2120,24 @@ class Serve extends Base {
2101
2120
  }
2102
2121
  }
2103
2122
 
2104
- async function saveResource(url, path) {
2105
- const res = await fetch(url);
2106
- await outputFile(path, await res.text());
2107
- }
2123
+ const BASE_DIR = ".scaffold";
2124
+ const CACHE_DIR = `${BASE_DIR}/cache`;
2125
+ const TESTER_BASE_PATH = `${BASE_DIR}/test`;
2126
+ const TESTER_PROFILE_DIR = `${TESTER_BASE_PATH}/profile`;
2127
+ const TESTER_DATA_DIR = `${TESTER_BASE_PATH}/data`;
2128
+ const TESTER_PLUGIN_DIR = `${TESTER_BASE_PATH}/resource`;
2129
+ const TESTER_PLUGIN_REF = "zotero-plugin-scaffold-test-runner";
2130
+ const TESTER_PLUGIN_ID = "scaffold-test@northword.cn";
2108
2131
 
2132
+ async function prepareHeadless() {
2133
+ await installXvfb();
2134
+ await installDepsForUbuntu24();
2135
+ await installZoteroLinux();
2136
+ const xvfb = new Xvfb({
2137
+ timeout: 2e3
2138
+ });
2139
+ await xvfb.start();
2140
+ }
2109
2141
  function isPackageInstalled(packageName) {
2110
2142
  try {
2111
2143
  execSync(`dpkg-query -W ${packageName}`, { stdio: "ignore" });
@@ -2160,163 +2192,444 @@ async function installZoteroLinux() {
2160
2192
  return;
2161
2193
  }
2162
2194
  logger.debug("Installing Zotero...");
2195
+ execSync(`cd ${CACHE_DIR}`);
2163
2196
  execSync("wget -O zotero.tar.bz2 'https://www.zotero.org/download/client/dl?platform=linux-x86_64&channel=beta'", { stdio: "pipe" });
2164
2197
  execSync("tar -xvf zotero.tar.bz2", { stdio: "pipe" });
2165
2198
  process.env.ZOTERO_PLUGIN_ZOTERO_BIN_PATH = `${process.cwd()}/Zotero_linux-x86_64/zotero`;
2199
+ execSync("cd -");
2166
2200
  }
2167
2201
 
2168
- class Test extends Base {
2169
- builder;
2170
- runner;
2171
- communicator = {};
2172
- constructor(ctx) {
2173
- super(ctx);
2174
- process.env.NODE_ENV ??= "test";
2175
- this.builder = new Build(ctx);
2176
- if (isCI) {
2177
- this.ctx.test.exitOnFinish = true;
2178
- this.ctx.test.headless = true;
2179
- }
2202
+ class TestHttpReporter {
2203
+ _server;
2204
+ _port;
2205
+ passed = 0;
2206
+ failed = 0;
2207
+ constructor() {
2180
2208
  }
2181
- async run() {
2182
- process.on("SIGINT", this.exit);
2183
- await emptyDir(this.profilePath);
2184
- await emptyDir(this.dataDir);
2185
- await emptyDir(this.testPluginDir);
2186
- await this.ctx.hooks.callHook("test:init", this.ctx);
2187
- await this.builder.run();
2188
- await this.ctx.hooks.callHook("test:prebuild", this.ctx);
2189
- this.logger.clear();
2190
- await this.startHttpServer();
2191
- await this.ctx.hooks.callHook("test:listen", this.ctx);
2192
- await this.createTestPlugin();
2193
- await this.copyTestLibraries();
2194
- await this.ctx.hooks.callHook("test:copyAssets", this.ctx);
2195
- await this.bundleTests();
2196
- await this.ctx.hooks.callHook("test:bundleTests", this.ctx);
2197
- if (this.ctx.test.watch) ;
2198
- if (isCI || this.ctx.test.headless) {
2199
- await this.startZoteroHeadless();
2209
+ async getPort() {
2210
+ this._port = await findFreeTcpPort();
2211
+ }
2212
+ get port() {
2213
+ return this._port;
2214
+ }
2215
+ async start() {
2216
+ if (!this._port)
2217
+ await this.getPort();
2218
+ this._server = http.createServer(this.handleRequest.bind(this));
2219
+ this._server.listen(this._port, () => {
2220
+ logger.debug(`Server is listening on http://localhost:${this.port}`);
2221
+ });
2222
+ return this;
2223
+ }
2224
+ handleRequest(req, res) {
2225
+ if (req.method === "GET" && req.url === "/") {
2226
+ res.writeHead(200, { "Content-Type": "text/plain" });
2227
+ res.end("Zotero Plugin Test Server is running");
2228
+ } else if (req.method === "POST" && req.url === "/update") {
2229
+ let body = "";
2230
+ req.on("data", (chunk) => {
2231
+ body += chunk;
2232
+ });
2233
+ req.on("end", async () => {
2234
+ try {
2235
+ const jsonData = JSON.parse(body);
2236
+ await this.onData(jsonData);
2237
+ res.writeHead(200, { "Content-Type": "application/json" });
2238
+ res.end(JSON.stringify({ message: "Results received successfully" }));
2239
+ } catch (error) {
2240
+ logger.error(`Error parsing JSON:, ${error}`);
2241
+ res.writeHead(400, { "Content-Type": "application/json" });
2242
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
2243
+ }
2244
+ });
2200
2245
  } else {
2201
- await this.startZotero();
2246
+ res.writeHead(404, { "Content-Type": "application/json" });
2247
+ res.end(JSON.stringify({ error: "Not Found" }));
2202
2248
  }
2203
- await this.ctx.hooks.callHook("test:run", this.ctx);
2204
2249
  }
2205
- async createTestPlugin() {
2206
- const manifest = {
2207
- manifest_version: 2,
2208
- name: this.testPluginRef,
2209
- version: "0.0.1",
2210
- description: "Test suite for the Zotero plugin. This is a runtime-generated plugin only for testing purposes.",
2211
- applications: {
2212
- zotero: {
2213
- id: this.testPluginID,
2214
- update_url: "https://invalid.com",
2215
- // strict_min_version: "*.*.*",
2216
- strict_max_version: "999.*.*"
2217
- }
2250
+ async onData(body) {
2251
+ const { type, data } = body;
2252
+ const logger_option = { space: data.indents - 1 };
2253
+ switch (type) {
2254
+ case "debug":
2255
+ logger.log(data);
2256
+ break;
2257
+ case "start":
2258
+ logger.newLine();
2259
+ break;
2260
+ case "suite":
2261
+ if (data.title)
2262
+ logger.tip(data.title, logger_option);
2263
+ break;
2264
+ case "pass":
2265
+ this.passed++;
2266
+ logger.success(`${data.title} ${styleText.gray(`${data.duration}ms`)}`, logger_option);
2267
+ break;
2268
+ case "fail":
2269
+ this.failed++;
2270
+ logger.fail(styleText.red(`${data.title}, ${body.data?.error?.message}`), logger_option);
2271
+ break;
2272
+ case "pending":
2273
+ logger.info(`${data.title} pending`, logger_option);
2274
+ break;
2275
+ case "suite end":
2276
+ break;
2277
+ case "end":
2278
+ logger.newLine();
2279
+ if (this.failed === 0)
2280
+ logger.success(`Test run completed - ${this.passed} passed`);
2281
+ else
2282
+ logger.fail(`Test run completed - ${this.passed} passed, ${this.failed} failed`);
2283
+ break;
2284
+ default:
2285
+ logger.log(data);
2286
+ break;
2287
+ }
2288
+ }
2289
+ stop() {
2290
+ this._server?.close();
2291
+ }
2292
+ }
2293
+
2294
+ async function saveResource(url, path) {
2295
+ const res = await fetch(url);
2296
+ await outputFile(path, await res.text());
2297
+ }
2298
+
2299
+ function generateBootstrap(options) {
2300
+ return `//Code generated by the zotero-plugin-scaffold tester
2301
+ var chromeHandle;
2302
+
2303
+ function install(data, reason) {}
2304
+
2305
+ async function startup({ id, version, resourceURI, rootURI }, reason) {
2306
+ await Zotero.initializationPromise;
2307
+ const aomStartup = Components.classes[
2308
+ "@mozilla.org/addons/addon-manager-startup;1"
2309
+ ].getService(Components.interfaces.amIAddonManagerStartup);
2310
+ const manifestURI = Services.io.newURI(rootURI + "manifest.json");
2311
+ chromeHandle = aomStartup.registerChrome(manifestURI, [
2312
+ ["content", "${TESTER_PLUGIN_REF}", rootURI + "content/"],
2313
+ ]);
2314
+
2315
+ launchTests().catch((error) => {
2316
+ Zotero.debug(error);
2317
+ Zotero.HTTP.request(
2318
+ "POST",
2319
+ "http://localhost:${options.port}/update",
2320
+ {
2321
+ body: JSON.stringify({
2322
+ type: "fail",
2323
+ data: {
2324
+ title: "Internal: Plugin awaiting timeout",
2325
+ stack: "",
2326
+ str: "Plugin awaiting timeout",
2327
+ },
2328
+ }),
2218
2329
  }
2219
- };
2220
- await outputJSON(`${this.testPluginDir}/manifest.json`, manifest, { spaces: 2 });
2221
- const bootstrap = `
2222
- /**
2223
- * Code generated by the zotero-plugin-scaffold tester
2224
- */
2225
-
2226
- var chromeHandle;
2227
-
2228
- function install(data, reason) {}
2229
-
2230
- async function startup({ id, version, resourceURI, rootURI }, reason) {
2231
- await Zotero.initializationPromise;
2232
- const aomStartup = Components.classes[
2233
- "@mozilla.org/addons/addon-manager-startup;1"
2234
- ].getService(Components.interfaces.amIAddonManagerStartup);
2235
- const manifestURI = Services.io.newURI(rootURI + "manifest.json");
2236
- chromeHandle = aomStartup.registerChrome(manifestURI, [
2237
- ["content", "${this.testPluginRef}", rootURI + "content/"],
2238
- ]);
2239
-
2240
- launchTests().catch((error) => {
2241
- Zotero.debug(error);
2242
- Zotero.HTTP.request(
2243
- "POST",
2244
- "http://localhost:${this.communicator.port}/update",
2245
- {
2246
- body: JSON.stringify({
2247
- type: "fail",
2248
- data: {
2249
- title: "Internal: Plugin awaiting timeout",
2250
- stack: "",
2251
- str: "Plugin awaiting timeout",
2252
- },
2253
- }),
2254
- }
2255
- );
2256
- });
2330
+ );
2331
+ });
2332
+ }
2333
+
2334
+ function onMainWindowLoad({ window: win }) {}
2335
+
2336
+ function onMainWindowUnload({ window: win }) {}
2337
+
2338
+ function shutdown({ id, version, resourceURI, rootURI }, reason) {
2339
+ if (reason === APP_SHUTDOWN) {
2340
+ return;
2341
+ }
2342
+
2343
+ if (chromeHandle) {
2344
+ chromeHandle.destruct();
2345
+ chromeHandle = null;
2346
+ }
2347
+ }
2348
+
2349
+ function uninstall(data, reason) {}
2350
+
2351
+ async function launchTests() {
2352
+ // Delay to allow plugin to fully load before opening the test page
2353
+ await Zotero.Promise.delay(${options.startupDelay || 1e3});
2354
+
2355
+ const waitForPlugin = "${options.waitForPlugin}";
2356
+
2357
+ if (waitForPlugin) {
2358
+ // Wait for a plugin to be installed
2359
+ await waitUtilAsync(() => {
2360
+ try {
2361
+ return !!eval(waitForPlugin)();
2362
+ } catch (error) {
2363
+ return false;
2257
2364
  }
2258
-
2259
- function onMainWindowLoad({ window: win }) {}
2260
-
2261
- function onMainWindowUnload({ window: win }) {}
2262
-
2263
- function shutdown({ id, version, resourceURI, rootURI }, reason) {
2264
- if (reason === APP_SHUTDOWN) {
2265
- return;
2266
- }
2267
-
2268
- if (chromeHandle) {
2269
- chromeHandle.destruct();
2270
- chromeHandle = null;
2271
- }
2365
+ }).catch(() => {
2366
+ throw new Error("Plugin awaiting timeout");
2367
+ });
2368
+ }
2369
+
2370
+ Services.ww.openWindow(
2371
+ null,
2372
+ "chrome://${TESTER_PLUGIN_REF}/content/index.xhtml",
2373
+ "Zotero Plugin Scaffold Test Runnner",
2374
+ "chrome,centerscreen,resizable=yes",
2375
+ {}
2376
+ );
2377
+ }
2378
+
2379
+ function waitUtilAsync(condition, interval = 100, timeout = 1e4) {
2380
+ return new Promise((resolve, reject) => {
2381
+ const start = Date.now();
2382
+ const intervalId = setInterval(() => {
2383
+ if (condition()) {
2384
+ clearInterval(intervalId);
2385
+ resolve();
2386
+ } else if (Date.now() - start > timeout) {
2387
+ clearInterval(intervalId);
2388
+ reject();
2272
2389
  }
2273
-
2274
- function uninstall(data, reason) {}
2275
-
2276
- async function launchTests() {
2277
- // Delay to allow plugin to fully load before opening the test page
2278
- await Zotero.Promise.delay(${this.ctx.test.startupDelay || 1e3});
2279
-
2280
- const waitForPlugin = "${this.ctx.test.waitForPlugin}";
2281
-
2282
- if (waitForPlugin) {
2283
- // Wait for a plugin to be installed
2284
- await waitUtilAsync(() => {
2285
- try {
2286
- return !!eval(waitForPlugin)();
2287
- } catch (error) {
2288
- return false;
2289
- }
2290
- }).catch(() => {
2291
- throw new Error("Plugin awaiting timeout");
2292
- });
2390
+ }, interval);
2391
+ });
2392
+ }
2393
+ `;
2394
+ }
2395
+
2396
+ function generateHtml(setupCode, testFiles) {
2397
+ const tests = testFiles.map((f) => `<script src="${f}"><\/script>`).join("\n ");
2398
+ return `<!-- Generated by zotero-plugin-scaffold -->
2399
+ <!DOCTYPE html>
2400
+ <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
2401
+ <head>
2402
+ <meta charset="UTF-8"></meta>
2403
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
2404
+ <title>Zotero Plugin Test</title>
2405
+ <style>
2406
+ html {
2407
+ min-width: 400px;
2408
+ min-height: 600px;
2293
2409
  }
2294
-
2295
- Services.ww.openWindow(
2296
- null,
2297
- "chrome://${this.testPluginRef}/content/index.xhtml",
2298
- "${this.ctx.namespace}-test",
2299
- "chrome,centerscreen,resizable=yes",
2300
- {}
2301
- );
2302
- }
2303
-
2304
- function waitUtilAsync(condition, interval = 100, timeout = 1e4) {
2305
- return new Promise((resolve, reject) => {
2306
- const start = Date.now();
2307
- const intervalId = setInterval(() => {
2308
- if (condition()) {
2309
- clearInterval(intervalId);
2310
- resolve();
2311
- } else if (Date.now() - start > timeout) {
2312
- clearInterval(intervalId);
2313
- reject();
2314
- }
2315
- }, interval);
2316
- });
2410
+ body {
2411
+ font-family: Arial, sans-serif;
2412
+ }
2413
+ </style>
2414
+ </head>
2415
+ <body>
2416
+ <div id="mocha"></div>
2417
+
2418
+ <!-- Include Zotero Vars -->
2419
+ <script src="chrome://zotero/content/include.js"><\/script>
2420
+
2421
+ <!-- Mocha and Chai Libraries -->
2422
+ <script src="mocha.js"><\/script>
2423
+ <script src="chai.js"><\/script>
2424
+
2425
+ <!-- Setup Mocha -->
2426
+ <script>
2427
+ ${setupCode}
2428
+ <\/script>
2429
+
2430
+ <!-- Unit tests -->
2431
+ ${tests}
2432
+
2433
+ <!-- Run Mocha -->
2434
+ <script class="mocha-exec">
2435
+ mocha.run();
2436
+ <\/script>
2437
+ </body>
2438
+ </html>
2439
+ `;
2440
+ }
2441
+
2442
+ function generateManifest() {
2443
+ return {
2444
+ manifest_version: 2,
2445
+ name: "Zotero Plugin Scaffold Test Runner",
2446
+ version: "0.0.1",
2447
+ description: "Test suite for the Zotero plugin. This is a runtime-generated plugin only for testing purposes.",
2448
+ applications: {
2449
+ zotero: {
2450
+ id: TESTER_PLUGIN_ID,
2451
+ update_url: "https://example.com",
2452
+ // strict_min_version: "*.*.*",
2453
+ strict_max_version: "999.*.*"
2317
2454
  }
2318
- `.replaceAll(/^ {6}/g, "");
2319
- await outputFile(`${this.testPluginDir}/bootstrap.js`, bootstrap);
2455
+ }
2456
+ };
2457
+ }
2458
+
2459
+ function generateMochaSetup(options) {
2460
+ return `// Generated by zotero-plugin-scaffold
2461
+ mocha.setup({
2462
+ ui: "bdd",
2463
+ reporter: Reporter,
2464
+ timeout: ${options.timeout} || 10000,
2465
+ bail: ${options.abortOnFail}
2466
+ });
2467
+
2468
+ window.expect = chai.expect;
2469
+ window.assert = chai.assert;
2470
+
2471
+ async function send(data) {
2472
+ const req = await Zotero.HTTP.request(
2473
+ "POST",
2474
+ "http://localhost:${options.port}/update",
2475
+ {
2476
+ body: JSON.stringify(data),
2477
+ }
2478
+ );
2479
+
2480
+ if (req.status !== 200) {
2481
+ dump("Error sending data to server" + req.responseText);
2482
+ return null;
2483
+ } else {
2484
+ const result = JSON.parse(req.responseText);
2485
+ return result;
2486
+ }
2487
+ }
2488
+
2489
+ window.debug = function (data) {
2490
+ send({ type: "debug", data });
2491
+ };
2492
+
2493
+ let indents = 0,
2494
+ passed = 0,
2495
+ failed = 0,
2496
+ aborted = false;
2497
+
2498
+ // Inherit the default test settings from Zotero
2499
+ function Reporter(runner) {
2500
+ function indent() {
2501
+ return " ".repeat(indents);
2502
+ }
2503
+
2504
+ function dump(str) {
2505
+ // console.log(str)
2506
+ // const p = document.createElement("p")
2507
+ // p.textContent = str
2508
+ // document.querySelector("#mocha").append(p)
2509
+ document.querySelector("#mocha").innerText += str;
2510
+ }
2511
+
2512
+ runner.on("start", async function () {
2513
+ console.log("start")
2514
+ await send({ type: "start", data: { indents } });
2515
+ });
2516
+
2517
+ runner.on("suite", async function (suite) {
2518
+ console.log("suite", suite)
2519
+ indents++;
2520
+ const str = indent() + suite.title + "\\n";
2521
+ dump(str);
2522
+ await send({ type: "suite", data: { title: suite.title, root: suite.root, indents } });
2523
+ });
2524
+
2525
+ runner.on("suite end", async function (suite) {
2526
+ console.log("suite end", suite)
2527
+ indents--;
2528
+ const str = indents === 1 ? "\\n" : "";
2529
+ dump(str);
2530
+ await send({ type: "suite end", data: { title: suite.title, root: suite.root, indents } });
2531
+ });
2532
+
2533
+ runner.on("pending", async function (test) {
2534
+ console.log("pending", test)
2535
+ const str = indent() + "pending -" + test.title + "\\n";
2536
+ dump(str);
2537
+ await send({ type: "pending", data: { title: test.title, fulltest: test.fullTitle(), duration: test.duration, indents: indents + 1 } });
2538
+ });
2539
+
2540
+ runner.on("pass", async function (test) {
2541
+ console.log("pass", test)
2542
+ passed++;
2543
+ let str = indent() + Mocha.reporters.Base.symbols.ok + " " + test.title;
2544
+ if ("fast" != test.speed) {
2545
+ str += " (" + Math.round(test.duration) + " ms)";
2546
+ }
2547
+ str += "\\n";
2548
+ dump(str);
2549
+ await send({ type: "pass", data: { title: test.title, fulltest: test.fullTitle(), duration: test.duration, indents: indents + 1 } });
2550
+
2551
+ });
2552
+
2553
+ runner.on("fail", async function (test, error) {
2554
+ console.log("fail", test, error)
2555
+
2556
+ // Make sure there's a blank line after all stack traces
2557
+ // err.stack = err.stack.replace(/\\s*$/, "\\n\\n");
2558
+
2559
+ failed++;
2560
+ let indentStr = indent();
2561
+ const str =
2562
+ indentStr +
2563
+ // Dark red X for errors
2564
+ Mocha.reporters.Base.symbols.err +
2565
+ // Trigger bell if interactive
2566
+ (Zotero.automatedTest ? "" : "\\x07") +
2567
+ " " +
2568
+ test.title +
2569
+ "\\n" +
2570
+ indentStr +
2571
+ " " +
2572
+ error.message +
2573
+ // " at\\n" +
2574
+ // err.stack.replace(/^/gm, indentStr + " ").trim() +
2575
+ "\\n\\n";
2576
+ dump(str);
2577
+
2578
+ await send({ type: "fail", data: { title: test.title, fulltest: test.fullTitle(), duration: test.duration, error, indents: indents + 1 } });
2579
+
2580
+ });
2581
+
2582
+ runner.on("end", async function () {
2583
+ console.log("end")
2584
+ const str =
2585
+ passed +
2586
+ "/" +
2587
+ (passed + failed) +
2588
+ " tests passed" +
2589
+ (aborted ? " -- aborting" : "") +
2590
+ "\\n";
2591
+ dump(str);
2592
+
2593
+ await send({
2594
+ type: "end",
2595
+ data: { passed: passed, failed: failed, aborted: aborted, str, indents },
2596
+ });
2597
+
2598
+ // Must exit on Zotero side, otherwise the exit code will not be 0 and CI will fail
2599
+ if (${options.exitOnFinish ? "true" : "false"}) {
2600
+ Zotero.Utilities.Internal.quit(0);
2601
+ }
2602
+ });
2603
+ }
2604
+ `;
2605
+ }
2606
+
2607
+ class TestBundler {
2608
+ constructor(ctx, port) {
2609
+ this.ctx = ctx;
2610
+ this.port = port;
2611
+ }
2612
+ esbuildContext;
2613
+ async generate() {
2614
+ await this.generateTestResources();
2615
+ await this.createTestHtml();
2616
+ }
2617
+ async regenerate(changedFile) {
2618
+ const esbuildResult = await this.esbuildContext?.rebuild();
2619
+ const tests = findImpactedTests(changedFile, esbuildResult?.metafile);
2620
+ await this.createTestHtml(tests);
2621
+ }
2622
+ async generateTestResources() {
2623
+ const manifest = generateManifest();
2624
+ await outputJSON(`${TESTER_PLUGIN_DIR}/manifest.json`, manifest, { spaces: 2 });
2625
+ const bootstrap = generateBootstrap({
2626
+ port: this.port,
2627
+ startupDelay: this.ctx.test.startupDelay,
2628
+ waitForPlugin: this.ctx.test.waitForPlugin
2629
+ });
2630
+ await outputFile(`${TESTER_PLUGIN_DIR}/bootstrap.js`, bootstrap);
2631
+ await this.copyTestLibraries();
2632
+ await this.bundleTests();
2320
2633
  }
2321
2634
  async copyTestLibraries() {
2322
2635
  const pkgs = [
@@ -2334,315 +2647,137 @@ class Test extends Base {
2334
2647
  }
2335
2648
  ];
2336
2649
  await Promise.all(pkgs.map(async (pkg) => {
2337
- const targetPath = `${this.testPluginDir}/content/${pkg.name}`;
2650
+ const targetPath = `${TESTER_PLUGIN_DIR}/content/${pkg.name}`;
2338
2651
  if (pkg.local && await pathExists(pkg.local)) {
2339
- this.logger.debug(`Local ${pkg.name} package found`);
2652
+ logger.debug(`Local ${pkg.name} package found`);
2340
2653
  await copy(pkg.local, targetPath);
2341
2654
  return;
2342
2655
  }
2343
- const cachePath = `${this.cacheDir}/${pkg.name}`;
2656
+ const cachePath = `${CACHE_DIR}/${pkg.name}`;
2344
2657
  if (await pathExists(`${cachePath}`)) {
2345
- this.logger.debug(`Cache ${pkg.name} package found`);
2658
+ logger.debug(`Cache ${pkg.name} package found`);
2346
2659
  await copy(cachePath, targetPath);
2347
2660
  return;
2348
2661
  }
2349
- this.logger.info(`No local ${pkg.name} found, we recommend you install ${pkg.name} package locally.`);
2350
- await saveResource(pkg.remote, `${this.cacheDir}/${pkg.name}`);
2662
+ logger.info(`No local ${pkg.name} found, we recommend you install ${pkg.name} package locally.`);
2663
+ await saveResource(pkg.remote, `${CACHE_DIR}/${pkg.name}`);
2351
2664
  await copy(cachePath, targetPath);
2352
2665
  }));
2353
2666
  }
2354
2667
  async bundleTests() {
2355
2668
  const testDirs = toArray(this.ctx.test.entries);
2356
- for (const dir of testDirs) {
2357
- let tsconfigPath = resolve(`${dir}/tsconfig.json`);
2358
- if (!await pathExists(tsconfigPath)) {
2359
- tsconfigPath = void 0;
2360
- }
2361
- await build({
2362
- entryPoints: await glob(`${dir}/**/*.spec.{js,ts}`),
2363
- outdir: `${this.testPluginDir}/content/units`,
2364
- bundle: true,
2365
- target: "firefox115",
2366
- tsconfig: tsconfigPath || void 0
2367
- });
2368
- }
2369
- const testFiles = (await glob(`**/*.spec.js`, { cwd: `${this.testPluginDir}/content` })).sort();
2370
- const setupCode = `
2371
- mocha.setup({ ui: "bdd", reporter: Reporter, timeout: ${this.ctx.test.mocha.timeout} || 10000, });
2372
-
2373
- window.expect = chai.expect;
2374
- window.assert = chai.assert;
2375
-
2376
- async function send(data) {
2377
- console.log("Sending data to server", data);
2378
- const req = await Zotero.HTTP.request(
2379
- "POST",
2380
- "http://localhost:${this.communicator.port}/update",
2381
- {
2382
- body: JSON.stringify(data),
2383
- }
2384
- );
2385
-
2386
- if (req.status !== 200) {
2387
- dump("Error sending data to server" + req.responseText);
2388
- return null;
2389
- } else {
2390
- const result = JSON.parse(req.responseText);
2391
- return result;
2392
- }
2393
- }
2394
-
2395
- window.debug = function (...data) {
2396
- const str = data.join("\\n");
2397
- Zotero.debug(str);
2398
- send({ type: "debug", data: { str } });
2399
- };
2400
-
2401
- // Inherit the default test settings from Zotero
2402
- function Reporter(runner) {
2403
- var indents = 0,
2404
- passed = 0,
2405
- failed = 0,
2406
- aborted = false;
2407
-
2408
- function indent() {
2409
- return Array(indents).join(" ");
2410
- }
2411
-
2412
- function dump(str) {
2413
- console.log(str);
2414
- document.querySelector("#mocha").innerText += str;
2415
- }
2416
-
2417
- runner.on("start", async function () {
2418
- await send({ type: "start" });
2419
- });
2420
-
2421
- runner.on("suite", async function (suite) {
2422
- ++indents;
2423
- const str = indent() + suite.title + "\\n";
2424
- dump(str);
2425
- await send({ type: "suite", data: { title: suite.title, str } });
2426
- });
2427
-
2428
- runner.on("suite end", async function (suite) {
2429
- --indents;
2430
- const str = indents === 1 ? "\\n" : "";
2431
- dump(str);
2432
- await send({ type: "suite end", data: { title: suite.title, str } });
2433
- });
2434
-
2435
- runner.on("pending", async function (test) {
2436
- const str = indent() + "pending -" + test.title + "\\n";
2437
- dump(str);
2438
- await send({ type: "pending", data: { title: test.title, str } });
2439
- });
2440
-
2441
- runner.on("pass", async function (test) {
2442
- passed++;
2443
- let str = indent() + Mocha.reporters.Base.symbols.ok + " " + test.title;
2444
- if ("fast" != test.speed) {
2445
- str += " (" + Math.round(test.duration) + " ms)";
2446
- }
2447
- str += "\\n";
2448
- dump(str);
2449
- await send({
2450
- type: "pass",
2451
- data: { title: test.title, duration: test.duration, str },
2452
- });
2453
- });
2454
-
2455
- runner.on("fail", async function (test, err) {
2456
- // Make sure there's a blank line after all stack traces
2457
- err.stack = err.stack.replace(/\\s*$/, "\\n\\n");
2458
-
2459
- failed++;
2460
- let indentStr = indent();
2461
- const str =
2462
- indentStr +
2463
- // Dark red X for errors
2464
- "\\x1B[31;40m" +
2465
- Mocha.reporters.Base.symbols.err +
2466
- " [FAIL]\\x1B[0m" +
2467
- // Trigger bell if interactive
2468
- (Zotero.automatedTest ? "" : "\\x07") +
2469
- " " +
2470
- test.title +
2471
- "\\n" +
2472
- indentStr +
2473
- " " +
2474
- err.message +
2475
- " at\\n" +
2476
- err.stack.replace(/^/gm, indentStr + " ").trim() +
2477
- "\\n\\n";
2478
- dump(str);
2479
-
2480
- if (${this.ctx.test.abortOnFail ? "true" : "false"}) {
2481
- aborted = true;
2482
- runner.abort();
2483
- }
2484
-
2485
- await send({
2486
- type: "fail",
2487
- data: { title: test.title, stack: err.stack, str },
2488
- });
2489
- });
2490
-
2491
- runner.on("end", async function () {
2492
- const str =
2493
- passed +
2494
- "/" +
2495
- (passed + failed) +
2496
- " tests passed" +
2497
- (aborted ? " -- aborting" : "") +
2498
- "\\n";
2499
- dump(str);
2500
-
2501
- await send({
2502
- type: "end",
2503
- data: { passed: passed, failed: failed, aborted: aborted, str },
2504
- });
2505
-
2506
- // Must exit on Zotero side, otherwise the exit code will not be 0 and CI will fail
2507
- if (${this.ctx.test.exitOnFinish ? "true" : "false"}) {
2508
- Zotero.Utilities.Internal.quit(0);
2509
- }
2510
- });
2511
- }
2512
- `.replaceAll(" ", " ");
2513
- const html = `
2514
- <!DOCTYPE html>
2515
- <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
2516
- <head>
2517
- <meta charset="UTF-8"></meta>
2518
- <meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
2519
- <title>Zotero Plugin Test</title>
2520
- <style>
2521
- html {
2522
- min-width: 400px;
2523
- min-height: 600px;
2524
- }
2525
- body {
2526
- font-family: Arial, sans-serif;
2527
- }
2528
- </style>
2529
- </head>
2530
- <body>
2531
- <div id="mocha"></div>
2532
-
2533
- <!-- Include Zotero Vars -->
2534
- <script src="chrome://zotero/content/include.js"><\/script>
2535
-
2536
- <!-- Mocha and Chai Libraries -->
2537
- <script src="mocha.js"><\/script>
2538
- <script src="chai.js"><\/script>
2539
-
2540
- <!-- Setup Mocha -->
2541
- <script>
2542
- ${setupCode}
2543
- <\/script>
2544
-
2545
- <!-- Unit tests -->
2546
- ${testFiles.map((f) => `<script src="${f}"><\/script>
2547
- `)}
2548
-
2549
- <!-- Run Mocha -->
2550
- <script class="mocha-exec">
2551
- mocha.run();
2552
- <\/script>
2553
- </body>
2554
- </html>
2555
- `.replaceAll(/^ {6}/gm, "");
2556
- await outputFile(`${this.testPluginDir}/content/index.xhtml`, html);
2557
- this.logger.tip(`Injected ${testFiles.length} test files`);
2558
- }
2559
- async startHttpServer() {
2560
- this.communicator.server = http.createServer((req, res) => {
2561
- if (req.method === "GET" && req.url === "/") {
2562
- res.writeHead(200, { "Content-Type": "text/plain" });
2563
- res.end("Zotero Plugin Test Server is running");
2564
- } else if (req.method === "POST" && req.url === "/update") {
2565
- let body = "";
2566
- req.on("data", (chunk) => {
2567
- body += chunk;
2568
- });
2569
- req.on("end", async () => {
2570
- try {
2571
- const jsonData = JSON.parse(body);
2572
- await this.onHttpDataUpdated(jsonData);
2573
- res.writeHead(200, { "Content-Type": "application/json" });
2574
- res.end(JSON.stringify({ message: "Results received successfully" }));
2575
- } catch (error) {
2576
- this.logger.error(`Error parsing JSON:, ${error}`);
2577
- res.writeHead(400, { "Content-Type": "application/json" });
2578
- res.end(JSON.stringify({ error: "Invalid JSON" }));
2579
- }
2580
- });
2581
- } else {
2582
- res.writeHead(404, { "Content-Type": "application/json" });
2583
- res.end(JSON.stringify({ error: "Not Found" }));
2584
- }
2669
+ const entryPoints = (await Promise.all(testDirs.map((dir) => glob(`${dir}/**/*.spec.{js,ts}`)))).flat();
2670
+ this.esbuildContext = await context({
2671
+ entryPoints,
2672
+ outdir: `${TESTER_PLUGIN_DIR}/content/units`,
2673
+ bundle: true,
2674
+ target: "firefox115",
2675
+ metafile: true
2585
2676
  });
2586
- const PORT = this.communicator.port = await findFreeTcpPort();
2587
- this.communicator.server.listen(PORT, () => {
2588
- this.logger.tip(`Server is listening on http://localhost:${PORT}`);
2677
+ await this.esbuildContext.rebuild();
2678
+ }
2679
+ async createTestHtml(tests = []) {
2680
+ const setupCode = generateMochaSetup({
2681
+ timeout: this.ctx.test.mocha.timeout,
2682
+ port: this.port,
2683
+ abortOnFail: this.ctx.test.abortOnFail,
2684
+ exitOnFinish: !this.ctx.test.watch
2589
2685
  });
2590
- }
2591
- async onHttpDataUpdated(body) {
2592
- if (body.type === "debug" && body.data?.str) {
2593
- for (const line of body.data?.str.split("\n")) {
2594
- this.logger.log(line);
2595
- this.logger.newLine();
2596
- }
2597
- }
2598
- const str = body.data?.str.replaceAll("\n", "");
2599
- if (body.type === "start") {
2600
- this.logger.newLine();
2601
- } else if (body.type === "suite" && !!str) {
2602
- this.logger.tip(str);
2603
- }
2604
- if (body.type === "pass" && !!str) {
2605
- this.logger.log(str);
2606
- } else if (body.type === "fail") {
2607
- this.logger.error(str);
2608
- if (this.ctx.test.abortOnFail) {
2609
- this.logger.error("Aborting test run due to failure");
2610
- if (this.ctx.test.exitOnFinish)
2611
- this.exit(1);
2612
- }
2613
- } else if (body.type === "suite end") {
2614
- this.logger.newLine();
2615
- } else if (body.type === "end") {
2616
- this.logger.success("Test run completed");
2617
- this.communicator.server?.close();
2618
- if (this.ctx.test.exitOnFinish)
2619
- this.exit();
2686
+ let testFiles = tests;
2687
+ if (testFiles.length === 0) {
2688
+ testFiles = (await glob(`**/*.spec.js`, { cwd: `${TESTER_PLUGIN_DIR}/content` })).sort();
2620
2689
  }
2690
+ const html = generateHtml(setupCode, testFiles);
2691
+ await outputFile(`${TESTER_PLUGIN_DIR}/content/index.xhtml`, html);
2621
2692
  }
2622
- async watch() {
2693
+ }
2694
+ function findImpactedTests(changedFilePath, buildMetadata) {
2695
+ if (!buildMetadata)
2696
+ return [];
2697
+ const resolvedChangedFile = resolve(changedFilePath);
2698
+ const impactedTestFiles = /* @__PURE__ */ new Set();
2699
+ for (const [outputFilePath, outputInfo] of Object.entries(buildMetadata.outputs)) {
2700
+ const testFilePath = outputFilePath.replace(`${TESTER_PLUGIN_DIR}/content/`, "");
2701
+ if (Object.keys(outputInfo.inputs).some((inputPath) => resolve(inputPath) === resolvedChangedFile)) {
2702
+ impactedTestFiles.add(testFilePath);
2703
+ }
2623
2704
  }
2624
- async onCodeChanged(_path) {
2705
+ return Array.from(impactedTestFiles);
2706
+ }
2707
+
2708
+ class Test extends Base {
2709
+ builder;
2710
+ zotero;
2711
+ reporter = new TestHttpReporter();
2712
+ testBundler;
2713
+ constructor(ctx) {
2714
+ super(ctx);
2715
+ process.env.NODE_ENV ??= "test";
2716
+ this.builder = new Build(ctx);
2717
+ if (isCI) {
2718
+ this.ctx.test.headless = true;
2719
+ this.ctx.test.watch = false;
2720
+ }
2625
2721
  }
2626
- async startZoteroHeadless() {
2627
- await installXvfb();
2628
- await installDepsForUbuntu24();
2629
- await installZoteroLinux();
2630
- const xvfb = new Xvfb({
2631
- timeout: 2e3
2632
- });
2633
- await xvfb.start();
2722
+ async run() {
2723
+ process.on("SIGINT", this.exit);
2724
+ await emptyDir(TESTER_PROFILE_DIR);
2725
+ await emptyDir(TESTER_DATA_DIR);
2726
+ await emptyDir(TESTER_PLUGIN_DIR);
2727
+ await this.ctx.hooks.callHook("test:init", this.ctx);
2728
+ await this.builder.run();
2729
+ await this.ctx.hooks.callHook("test:prebuild", this.ctx);
2730
+ await this.reporter.start();
2731
+ this.testBundler = new TestBundler(
2732
+ this.ctx,
2733
+ this.reporter.port
2734
+ );
2735
+ await this.testBundler.generate();
2736
+ await this.ctx.hooks.callHook("test:bundleTests", this.ctx);
2634
2737
  await this.startZotero();
2738
+ await this.ctx.hooks.callHook("test:run", this.ctx);
2739
+ if (this.ctx.test.watch) {
2740
+ this.watch();
2741
+ }
2742
+ }
2743
+ async watch() {
2744
+ const source = toArray(this.ctx.source).map((p) => resolve(p));
2745
+ const tests = toArray(this.ctx.test.entries).map((p) => resolve(p));
2746
+ function isSource(_path) {
2747
+ const path = resolve(_path);
2748
+ const isSource2 = source.find((s) => path.match(s)) || false;
2749
+ tests.find((t) => path.match(t)) || false;
2750
+ return isSource2;
2751
+ }
2752
+ watch(
2753
+ [this.ctx.source, this.ctx.test.entries].flat(),
2754
+ {
2755
+ onChange: async (path) => {
2756
+ if (isSource(path)) {
2757
+ await this.builder.run();
2758
+ await this.testBundler?.regenerate(path);
2759
+ await this.zotero?.reloadAllPlugins();
2760
+ } else {
2761
+ await this.testBundler?.regenerate(path);
2762
+ await this.zotero?.reloadTemporaryPluginBySourceDir(TESTER_PLUGIN_DIR);
2763
+ }
2764
+ }
2765
+ }
2766
+ );
2635
2767
  }
2636
2768
  async startZotero() {
2637
- this.runner = new ZoteroRunner({
2769
+ if (this.ctx.test.headless) {
2770
+ await prepareHeadless();
2771
+ }
2772
+ this.zotero = new ZoteroRunner({
2638
2773
  binary: {
2639
2774
  path: this.zoteroBinPath,
2640
2775
  devtools: this.ctx.server.devtools,
2641
2776
  args: this.ctx.server.startArgs
2642
2777
  },
2643
2778
  profile: {
2644
- path: this.profilePath,
2645
- dataDir: this.dataDir,
2779
+ path: TESTER_PROFILE_DIR,
2780
+ dataDir: TESTER_DATA_DIR,
2646
2781
  customPrefs: this.prefs
2647
2782
  },
2648
2783
  plugins: {
@@ -2650,51 +2785,36 @@ class Test extends Base {
2650
2785
  id: this.ctx.id,
2651
2786
  sourceDir: join(this.ctx.dist, "addon")
2652
2787
  }, {
2653
- id: this.testPluginID,
2654
- sourceDir: this.testPluginDir
2788
+ id: TESTER_PLUGIN_ID,
2789
+ sourceDir: TESTER_PLUGIN_DIR
2655
2790
  }]
2656
2791
  }
2657
2792
  });
2658
- await this.runner.run();
2793
+ await this.zotero.run();
2794
+ this.zotero.zotero?.on("close", () => this.onZoteroExit());
2659
2795
  }
2660
- exit = (code) => {
2661
- this.communicator.server?.close();
2662
- this.runner?.exit();
2796
+ onZoteroExit = () => {
2797
+ this.reporter.stop();
2663
2798
  this.ctx.hooks.callHook("test:exit", this.ctx);
2664
- if (code === 1) {
2665
- this.logger.error("Test run failed");
2799
+ if (this.reporter.failed)
2666
2800
  process.exit(1);
2667
- } else if (code === "SIGINT") {
2801
+ else
2802
+ process.exit(0);
2803
+ };
2804
+ exit = (code) => {
2805
+ if (code === "SIGINT") {
2668
2806
  this.logger.info("Tester shutdown by user request");
2669
- process.exit();
2670
- } else {
2671
- this.logger.success("Test run completed successfully");
2672
- process.exit();
2673
2807
  }
2808
+ this.reporter.stop();
2809
+ this.zotero?.exit();
2810
+ this.ctx.hooks.callHook("test:exit", this.ctx);
2811
+ process.exit();
2674
2812
  };
2675
2813
  get zoteroBinPath() {
2676
2814
  if (!process.env.ZOTERO_PLUGIN_ZOTERO_BIN_PATH)
2677
2815
  throw new Error("No Zotero Found.");
2678
2816
  return process.env.ZOTERO_PLUGIN_ZOTERO_BIN_PATH;
2679
2817
  }
2680
- get profilePath() {
2681
- return `.scaffold/test/profile`;
2682
- }
2683
- get dataDir() {
2684
- return `.scaffold/test/data`;
2685
- }
2686
- get testPluginDir() {
2687
- return `.scaffold/test/resource`;
2688
- }
2689
- get cacheDir() {
2690
- return `.scaffold/cache`;
2691
- }
2692
- get testPluginRef() {
2693
- return `${this.ctx.namespace}-test`;
2694
- }
2695
- get testPluginID() {
2696
- return `${this.testPluginRef}@only-for-testing.com`;
2697
- }
2698
2818
  get prefs() {
2699
2819
  const defaultPref = {
2700
2820
  "extensions.experiments.enabled": true,