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.
package/dist/shared/{zotero-plugin-scaffold.DfqO5HjX.mjs → zotero-plugin-scaffold.CLDX-OU6.mjs}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadConfig as loadConfig$1 } from 'c12';
|
|
2
|
-
import { isPlainObject, kebabCase, mapValues, toMerged, escapeRegExp,
|
|
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
|
-
"
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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(/^## .*/)
|
|
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
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
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
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
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
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
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
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
if (
|
|
2198
|
-
|
|
2199
|
-
|
|
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
|
-
|
|
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
|
|
2206
|
-
const
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
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
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
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
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
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
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
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
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
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
|
-
|
|
2319
|
-
|
|
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 = `${
|
|
2650
|
+
const targetPath = `${TESTER_PLUGIN_DIR}/content/${pkg.name}`;
|
|
2338
2651
|
if (pkg.local && await pathExists(pkg.local)) {
|
|
2339
|
-
|
|
2652
|
+
logger.debug(`Local ${pkg.name} package found`);
|
|
2340
2653
|
await copy(pkg.local, targetPath);
|
|
2341
2654
|
return;
|
|
2342
2655
|
}
|
|
2343
|
-
const cachePath = `${
|
|
2656
|
+
const cachePath = `${CACHE_DIR}/${pkg.name}`;
|
|
2344
2657
|
if (await pathExists(`${cachePath}`)) {
|
|
2345
|
-
|
|
2658
|
+
logger.debug(`Cache ${pkg.name} package found`);
|
|
2346
2659
|
await copy(cachePath, targetPath);
|
|
2347
2660
|
return;
|
|
2348
2661
|
}
|
|
2349
|
-
|
|
2350
|
-
await saveResource(pkg.remote, `${
|
|
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
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
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
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
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
|
-
|
|
2592
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2627
|
-
|
|
2628
|
-
await
|
|
2629
|
-
await
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
await
|
|
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.
|
|
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:
|
|
2645
|
-
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:
|
|
2654
|
-
sourceDir:
|
|
2788
|
+
id: TESTER_PLUGIN_ID,
|
|
2789
|
+
sourceDir: TESTER_PLUGIN_DIR
|
|
2655
2790
|
}]
|
|
2656
2791
|
}
|
|
2657
2792
|
});
|
|
2658
|
-
await this.
|
|
2793
|
+
await this.zotero.run();
|
|
2794
|
+
this.zotero.zotero?.on("close", () => this.onZoteroExit());
|
|
2659
2795
|
}
|
|
2660
|
-
|
|
2661
|
-
this.
|
|
2662
|
-
this.runner?.exit();
|
|
2796
|
+
onZoteroExit = () => {
|
|
2797
|
+
this.reporter.stop();
|
|
2663
2798
|
this.ctx.hooks.callHook("test:exit", this.ctx);
|
|
2664
|
-
if (
|
|
2665
|
-
this.logger.error("Test run failed");
|
|
2799
|
+
if (this.reporter.failed)
|
|
2666
2800
|
process.exit(1);
|
|
2667
|
-
|
|
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,
|