@bsb/tests 0.0.1 → 9.5.5

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
- # BSB Node.js Tests
2
-
3
- Shared test suites for BSB core and Node.js plugins.
4
-
5
- ## Docs
6
-
7
- See `tests/nodejs/docs/index.md` for configuration, custom tests, and runner behavior.
1
+ # BSB Node.js Tests
2
+
3
+ Shared test suites for BSB core and Node.js plugins.
4
+
5
+ ## Docs
6
+
7
+ See `tests/nodejs/docs/index.md` for configuration, custom tests, and runner behavior.
package/bin/bsb-tests.cjs CHANGED
@@ -1,376 +1,376 @@
1
- #!/usr/bin/env node
2
- /* eslint-disable no-console */
3
- const fs = require("fs");
4
- const path = require("path");
5
- const { spawnSync } = require("child_process");
6
-
7
- const argv = process.argv.slice(2);
8
-
9
- const getArg = (name) => {
10
- const idx = argv.indexOf(name);
11
- if (idx === -1) return null;
12
- return argv[idx + 1] || null;
13
- };
14
-
15
- const hasFlag = (name) => argv.includes(name);
16
-
17
- const cwd = path.resolve(getArg("--cwd") || process.cwd());
18
- const pluginFilter = getArg("--plugin");
19
- const forceTs = hasFlag("--ts");
20
- const forceJs = hasFlag("--js");
21
- const enableCoverage = !hasFlag("--no-coverage");
22
-
23
- const ignoreDirs = new Set([
24
- "node_modules",
25
- ".git",
26
- "lib",
27
- "dist",
28
- ".bsb",
29
- ".npm-cache",
30
- ".claude",
31
- ]);
32
-
33
- const findFiles = (dir, filename, results = []) => {
34
- const entries = fs.readdirSync(dir, { withFileTypes: true });
35
- for (const entry of entries) {
36
- if (entry.isDirectory()) {
37
- if (ignoreDirs.has(entry.name)) continue;
38
- findFiles(path.join(dir, entry.name), filename, results);
39
- continue;
40
- }
41
- if (entry.isFile() && entry.name === filename) {
42
- results.push(path.join(dir, entry.name));
43
- }
44
- }
45
- return results;
46
- };
47
-
48
- const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, "utf8"));
49
-
50
- const resolvePluginModule = (pluginRoot, pluginPath, useTs) => {
51
- const base = path.resolve(pluginRoot, pluginPath || "");
52
- if (useTs) {
53
- return path.join(base, "index.ts");
54
- }
55
- const directJs = path.join(base, "index.js");
56
- if (fs.existsSync(directJs)) return directJs;
57
- const swapped = base.includes(`${path.sep}src${path.sep}`)
58
- ? base.replace(`${path.sep}src${path.sep}`, `${path.sep}lib${path.sep}`)
59
- : base.replace(`${path.sep}src`, `${path.sep}lib`);
60
- return path.join(swapped, "index.js");
61
- };
62
-
63
- const resolveLocalBaseEntry = (repoRoot, useTs) => {
64
- const baseRoot = path.join(repoRoot, "nodejs");
65
- const entryTs = path.join(baseRoot, "src", "index.ts");
66
- const entryJs = path.join(baseRoot, "lib", "index.js");
67
- if (useTs && fs.existsSync(entryTs)) return entryTs;
68
- if (!useTs && fs.existsSync(entryJs)) return entryJs;
69
- return fs.existsSync(entryTs) ? entryTs : entryJs;
70
- };
71
-
72
- const loadTestsManifest = (manifestDir) => {
73
- const configPath = path.join(manifestDir, "bsb-tests.json");
74
- if (!fs.existsSync(configPath)) return null;
75
- return readJson(configPath);
76
- };
77
-
78
- const mergeConfig = (baseConfig, overrideConfig) => {
79
- return {
80
- ...(baseConfig || {}),
81
- ...(overrideConfig || {}),
82
- };
83
- };
84
-
85
- const normalizeSetup = (setupValue) => {
86
- if (!setupValue) return null;
87
- if (typeof setupValue === "string") {
88
- return { beforeAll: setupValue, afterAll: null };
89
- }
90
- if (typeof setupValue === "object") {
91
- return {
92
- beforeAll: setupValue.beforeAll || null,
93
- afterAll: setupValue.afterAll || null,
94
- };
95
- }
96
- return null;
97
- };
98
-
99
- const normalizeDispose = (disposeValue) => {
100
- if (!disposeValue) return null;
101
- if (typeof disposeValue === "string") {
102
- return { afterAll: disposeValue };
103
- }
104
- if (typeof disposeValue === "object") {
105
- return {
106
- afterAll: disposeValue.afterAll || null,
107
- };
108
- }
109
- return null;
110
- };
111
-
112
- const manifests = findFiles(cwd, "bsb-plugin.json");
113
-
114
- if (manifests.length === 0) {
115
- console.error("No bsb-plugin.json files found in", cwd);
116
- process.exit(2);
117
- }
118
-
119
- const plugins = [];
120
- for (const manifestPath of manifests) {
121
- const manifestDir = path.dirname(manifestPath);
122
- const manifest = readJson(manifestPath);
123
- const testsManifest = loadTestsManifest(manifestDir);
124
- const testsById = new Map();
125
- if (testsManifest && Array.isArray(testsManifest.nodejs)) {
126
- for (const entry of testsManifest.nodejs) {
127
- if (entry && entry.id) {
128
- testsById.set(entry.id, entry);
129
- }
130
- }
131
- }
132
- for (const platform of Object.keys(manifest)) {
133
- const items = Array.isArray(manifest[platform]) ? manifest[platform] : [];
134
- for (const plugin of items) {
135
- if (!plugin || !plugin.id) continue;
136
- const pluginRoot = path.resolve(manifestDir, plugin.basePath || ".");
137
- const testsEntry = testsById.get(plugin.id) || null;
138
- plugins.push({
139
- platform,
140
- id: plugin.id,
141
- name: plugin.name || plugin.id,
142
- pluginPath: plugin.pluginPath,
143
- pluginRoot,
144
- testsEntry,
145
- });
146
- }
147
- }
148
- }
149
-
150
- const filtered = pluginFilter
151
- ? plugins.filter((p) => p.id === pluginFilter || p.name === pluginFilter || p.pluginRoot.endsWith(pluginFilter))
152
- : plugins;
153
-
154
- if (filtered.length === 0) {
155
- console.error("No plugins matched filter:", pluginFilter);
156
- process.exit(2);
157
- }
158
-
159
- const repoRoot = cwd;
160
- const mochaBin = require.resolve("mocha/bin/mocha.mjs");
161
- const nycBin = require.resolve("nyc/bin/nyc.js");
162
- const tsNodeRegister = require.resolve("ts-node/register");
163
- const setupHook = path.join(__dirname, "..", "src", "runner", "setup.ts");
164
- const pluginEventsRunner = path.join(__dirname, "..", "src", "runner", "plugin-events.ts");
165
- const pluginObservableRunner = path.join(__dirname, "..", "src", "runner", "plugin-observable.ts");
166
- const pluginCustomRunner = path.join(__dirname, "..", "src", "runner", "plugin-custom.ts");
167
- const eventsDefaultSpec = path.join(__dirname, "..", "src", "plugins", "events-default", "index.ts");
168
- const loggingDefaultSpec = path.join(__dirname, "..", "src", "plugins", "logging-default", "index.ts");
169
- const configDefaultSpec = path.join(__dirname, "..", "src", "plugins", "config-default", "index.ts");
170
- const observableDefaultSpec = path.join(__dirname, "..", "src", "plugins", "observable-default", "index.ts");
171
-
172
- const runMocha = (env, spec, useTs, coverage, coverageInclude) => {
173
- const mochaArgs = [];
174
- if (useTs) {
175
- mochaArgs.push("--require", tsNodeRegister);
176
- }
177
- mochaArgs.push("--require", setupHook, spec);
178
-
179
- if (!coverage) {
180
- const result = spawnSync(process.execPath, [mochaBin, ...mochaArgs], {
181
- stdio: "inherit",
182
- env,
183
- });
184
- return result.status || 0;
185
- }
186
-
187
- const nycArgs = [
188
- "--all",
189
- "--check-coverage",
190
- "--lines",
191
- "100",
192
- "--functions",
193
- "100",
194
- "--branches",
195
- "100",
196
- "--statements",
197
- "100",
198
- "--extension",
199
- useTs ? ".ts" : ".js",
200
- "--reporter",
201
- "text",
202
- "--reporter",
203
- "lcov",
204
- ];
205
-
206
- if (coverageInclude && coverageInclude.length) {
207
- for (const inc of coverageInclude) {
208
- nycArgs.push("--include", inc);
209
- }
210
- }
211
-
212
- nycArgs.push(mochaBin, ...mochaArgs);
213
-
214
- const result = spawnSync(process.execPath, [nycBin, ...nycArgs], {
215
- stdio: "inherit",
216
- env,
217
- });
218
- return result.status || 0;
219
- };
220
-
221
- let exitCode = 0;
222
-
223
- for (const plugin of filtered) {
224
- if (plugin.testsEntry && plugin.testsEntry.skip === true) {
225
- console.warn("Skipping tests for plugin (skipped):", plugin.name);
226
- continue;
227
- }
228
-
229
- const useTs = forceTs || (!forceJs && fs.existsSync(path.join(plugin.pluginRoot, "src")));
230
- const pluginModule = resolvePluginModule(plugin.pluginRoot, plugin.pluginPath, useTs);
231
- if (!fs.existsSync(pluginModule)) {
232
- console.error("Plugin module not found:", pluginModule);
233
- exitCode = 2;
234
- continue;
235
- }
236
-
237
- const localBaseEntry = resolveLocalBaseEntry(repoRoot, useTs);
238
- const testsEntry = plugin.testsEntry || {};
239
- const defaultConfig = testsEntry.default?.config || null;
240
- const defaultSetup = normalizeSetup(testsEntry.default?.setup || null);
241
- const defaultDispose = normalizeDispose(testsEntry.default?.dispose || null);
242
- const testCases = Array.isArray(testsEntry.tests) && testsEntry.tests.length > 0
243
- ? testsEntry.tests
244
- : [ { name: "default", config: null, setup: null, dispose: null } ];
245
-
246
- const coverageInclude = useTs
247
- ? [path.join(plugin.pluginRoot, "src", "**", "*.ts")]
248
- : [path.join(plugin.pluginRoot, "lib", "**", "*.js")];
249
-
250
- const runSetupScript = (scriptPath, phase) => {
251
- if (!scriptPath) return 0;
252
- const resolved = path.resolve(plugin.pluginRoot, scriptPath);
253
- if (!fs.existsSync(resolved)) {
254
- console.error(`Setup script not found (${phase}):`, resolved);
255
- return 2;
256
- }
257
- const ext = path.extname(resolved).toLowerCase();
258
- let result;
259
- if (ext === ".ts") {
260
- result = spawnSync(process.execPath, ["-r", tsNodeRegister, resolved], {
261
- stdio: "inherit",
262
- env: {
263
- ...process.env,
264
- BSB_TEST_PLUGIN_NAME: plugin.name,
265
- BSB_TEST_PLUGIN_ID: plugin.id,
266
- },
267
- });
268
- } else if (ext === ".js" || ext === ".cjs" || ext === ".mjs") {
269
- result = spawnSync(process.execPath, [resolved], {
270
- stdio: "inherit",
271
- env: {
272
- ...process.env,
273
- BSB_TEST_PLUGIN_NAME: plugin.name,
274
- BSB_TEST_PLUGIN_ID: plugin.id,
275
- },
276
- });
277
- } else {
278
- result = spawnSync(resolved, {
279
- stdio: "inherit",
280
- shell: true,
281
- env: {
282
- ...process.env,
283
- BSB_TEST_PLUGIN_NAME: plugin.name,
284
- BSB_TEST_PLUGIN_ID: plugin.id,
285
- },
286
- });
287
- }
288
- return result.status || 0;
289
- };
290
-
291
- for (const testCase of testCases) {
292
- if (testCase && testCase.skip === true) {
293
- console.warn("Skipping test case (skipped):", testCase.name || "unnamed");
294
- continue;
295
- }
296
-
297
- const mergedConfig = mergeConfig(defaultConfig, testCase?.config);
298
- const setup = normalizeSetup(testCase?.setup) || defaultSetup;
299
- const dispose = normalizeDispose(testCase?.dispose) || defaultDispose;
300
- const beforeAll = setup?.beforeAll || null;
301
- const afterAll = setup?.afterAll || null;
302
-
303
- const env = {
304
- ...process.env,
305
- BSB_TEST_PLUGIN_MODULE: pluginModule,
306
- BSB_TEST_PLUGIN_NAME: plugin.name,
307
- BSB_TEST_PLUGIN_CONFIG: JSON.stringify(mergedConfig || null),
308
- BSB_TEST_LOCAL_BASE_ENTRY: localBaseEntry || "",
309
- TS_NODE_PROJECT: path.join(__dirname, "..", "tsconfig.json"),
310
- };
311
-
312
- if (beforeAll) {
313
- const code = runSetupScript(beforeAll, "beforeAll");
314
- if (code !== 0) {
315
- exitCode = code;
316
- continue;
317
- }
318
- }
319
-
320
- const isEvents = plugin.id.startsWith("events-") || plugin.name.startsWith("events-");
321
- if (isEvents) {
322
- const code = runMocha(env, pluginEventsRunner, useTs, enableCoverage, coverageInclude);
323
- if (code !== 0) exitCode = code;
324
- }
325
-
326
- const isObservable = plugin.id.startsWith("observable-") || plugin.name.startsWith("observable-");
327
- if (isObservable) {
328
- const code = runMocha(env, pluginObservableRunner, useTs, enableCoverage, coverageInclude);
329
- if (code !== 0) exitCode = code;
330
- }
331
-
332
- if (plugin.id === "events-default") {
333
- const code = runMocha(env, eventsDefaultSpec, useTs, enableCoverage, coverageInclude);
334
- if (code !== 0) exitCode = code;
335
- }
336
-
337
- if (plugin.id === "logging-default") {
338
- const code = runMocha(env, loggingDefaultSpec, useTs, enableCoverage, coverageInclude);
339
- if (code !== 0) exitCode = code;
340
- }
341
-
342
- if (plugin.id === "config-default") {
343
- const code = runMocha(env, configDefaultSpec, useTs, enableCoverage, coverageInclude);
344
- if (code !== 0) exitCode = code;
345
- }
346
-
347
- if (plugin.id === "observable-default") {
348
- const code = runMocha(env, observableDefaultSpec, useTs, enableCoverage, coverageInclude);
349
- if (code !== 0) exitCode = code;
350
- }
351
-
352
- const customEnv = {
353
- ...env,
354
- BSB_TEST_PLUGIN_ID: plugin.id,
355
- BSB_TEST_PLUGIN_ROOT: plugin.pluginRoot,
356
- };
357
- const customCode = runMocha(customEnv, pluginCustomRunner, useTs, enableCoverage, coverageInclude);
358
- if (customCode !== 0) exitCode = customCode;
359
-
360
- if (afterAll) {
361
- const code = runSetupScript(afterAll, "afterAll");
362
- if (code !== 0) {
363
- exitCode = code;
364
- }
365
- }
366
-
367
- if (dispose?.afterAll) {
368
- const code = runSetupScript(dispose.afterAll, "dispose");
369
- if (code !== 0) {
370
- exitCode = code;
371
- }
372
- }
373
- }
374
- }
375
-
376
- process.exit(exitCode);
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { spawnSync } = require("child_process");
6
+
7
+ const argv = process.argv.slice(2);
8
+
9
+ const getArg = (name) => {
10
+ const idx = argv.indexOf(name);
11
+ if (idx === -1) return null;
12
+ return argv[idx + 1] || null;
13
+ };
14
+
15
+ const hasFlag = (name) => argv.includes(name);
16
+
17
+ const cwd = path.resolve(getArg("--cwd") || process.cwd());
18
+ const pluginFilter = getArg("--plugin");
19
+ const forceTs = hasFlag("--ts");
20
+ const forceJs = hasFlag("--js");
21
+ const enableCoverage = !hasFlag("--no-coverage");
22
+
23
+ const ignoreDirs = new Set([
24
+ "node_modules",
25
+ ".git",
26
+ "lib",
27
+ "dist",
28
+ ".bsb",
29
+ ".npm-cache",
30
+ ".claude",
31
+ ]);
32
+
33
+ const findFiles = (dir, filename, results = []) => {
34
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
35
+ for (const entry of entries) {
36
+ if (entry.isDirectory()) {
37
+ if (ignoreDirs.has(entry.name)) continue;
38
+ findFiles(path.join(dir, entry.name), filename, results);
39
+ continue;
40
+ }
41
+ if (entry.isFile() && entry.name === filename) {
42
+ results.push(path.join(dir, entry.name));
43
+ }
44
+ }
45
+ return results;
46
+ };
47
+
48
+ const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, "utf8"));
49
+
50
+ const resolvePluginModule = (pluginRoot, pluginPath, useTs) => {
51
+ const base = path.resolve(pluginRoot, pluginPath || "");
52
+ if (useTs) {
53
+ return path.join(base, "index.ts");
54
+ }
55
+ const directJs = path.join(base, "index.js");
56
+ if (fs.existsSync(directJs)) return directJs;
57
+ const swapped = base.includes(`${path.sep}src${path.sep}`)
58
+ ? base.replace(`${path.sep}src${path.sep}`, `${path.sep}lib${path.sep}`)
59
+ : base.replace(`${path.sep}src`, `${path.sep}lib`);
60
+ return path.join(swapped, "index.js");
61
+ };
62
+
63
+ const resolveLocalBaseEntry = (repoRoot, useTs) => {
64
+ const baseRoot = path.join(repoRoot, "nodejs");
65
+ const entryTs = path.join(baseRoot, "src", "index.ts");
66
+ const entryJs = path.join(baseRoot, "lib", "index.js");
67
+ if (useTs && fs.existsSync(entryTs)) return entryTs;
68
+ if (!useTs && fs.existsSync(entryJs)) return entryJs;
69
+ return fs.existsSync(entryTs) ? entryTs : entryJs;
70
+ };
71
+
72
+ const loadTestsManifest = (manifestDir) => {
73
+ const configPath = path.join(manifestDir, "bsb-tests.json");
74
+ if (!fs.existsSync(configPath)) return null;
75
+ return readJson(configPath);
76
+ };
77
+
78
+ const mergeConfig = (baseConfig, overrideConfig) => {
79
+ return {
80
+ ...(baseConfig || {}),
81
+ ...(overrideConfig || {}),
82
+ };
83
+ };
84
+
85
+ const normalizeSetup = (setupValue) => {
86
+ if (!setupValue) return null;
87
+ if (typeof setupValue === "string") {
88
+ return { beforeAll: setupValue, afterAll: null };
89
+ }
90
+ if (typeof setupValue === "object") {
91
+ return {
92
+ beforeAll: setupValue.beforeAll || null,
93
+ afterAll: setupValue.afterAll || null,
94
+ };
95
+ }
96
+ return null;
97
+ };
98
+
99
+ const normalizeDispose = (disposeValue) => {
100
+ if (!disposeValue) return null;
101
+ if (typeof disposeValue === "string") {
102
+ return { afterAll: disposeValue };
103
+ }
104
+ if (typeof disposeValue === "object") {
105
+ return {
106
+ afterAll: disposeValue.afterAll || null,
107
+ };
108
+ }
109
+ return null;
110
+ };
111
+
112
+ const manifests = findFiles(cwd, "bsb-plugin.json");
113
+
114
+ if (manifests.length === 0) {
115
+ console.error("No bsb-plugin.json files found in", cwd);
116
+ process.exit(2);
117
+ }
118
+
119
+ const plugins = [];
120
+ for (const manifestPath of manifests) {
121
+ const manifestDir = path.dirname(manifestPath);
122
+ const manifest = readJson(manifestPath);
123
+ const testsManifest = loadTestsManifest(manifestDir);
124
+ const testsById = new Map();
125
+ if (testsManifest && Array.isArray(testsManifest.nodejs)) {
126
+ for (const entry of testsManifest.nodejs) {
127
+ if (entry && entry.id) {
128
+ testsById.set(entry.id, entry);
129
+ }
130
+ }
131
+ }
132
+ for (const platform of Object.keys(manifest)) {
133
+ const items = Array.isArray(manifest[platform]) ? manifest[platform] : [];
134
+ for (const plugin of items) {
135
+ if (!plugin || !plugin.id) continue;
136
+ const pluginRoot = path.resolve(manifestDir, plugin.basePath || ".");
137
+ const testsEntry = testsById.get(plugin.id) || null;
138
+ plugins.push({
139
+ platform,
140
+ id: plugin.id,
141
+ name: plugin.name || plugin.id,
142
+ pluginPath: plugin.pluginPath,
143
+ pluginRoot,
144
+ testsEntry,
145
+ });
146
+ }
147
+ }
148
+ }
149
+
150
+ const filtered = pluginFilter
151
+ ? plugins.filter((p) => p.id === pluginFilter || p.name === pluginFilter || p.pluginRoot.endsWith(pluginFilter))
152
+ : plugins;
153
+
154
+ if (filtered.length === 0) {
155
+ console.error("No plugins matched filter:", pluginFilter);
156
+ process.exit(2);
157
+ }
158
+
159
+ const repoRoot = cwd;
160
+ const mochaBin = require.resolve("mocha/bin/mocha.mjs");
161
+ const nycBin = require.resolve("nyc/bin/nyc.js");
162
+ const tsNodeRegister = require.resolve("ts-node/register");
163
+ const setupHook = path.join(__dirname, "..", "src", "runner", "setup.ts");
164
+ const pluginEventsRunner = path.join(__dirname, "..", "src", "runner", "plugin-events.ts");
165
+ const pluginObservableRunner = path.join(__dirname, "..", "src", "runner", "plugin-observable.ts");
166
+ const pluginCustomRunner = path.join(__dirname, "..", "src", "runner", "plugin-custom.ts");
167
+ const eventsDefaultSpec = path.join(__dirname, "..", "src", "plugins", "events-default", "index.ts");
168
+ const loggingDefaultSpec = path.join(__dirname, "..", "src", "plugins", "logging-default", "index.ts");
169
+ const configDefaultSpec = path.join(__dirname, "..", "src", "plugins", "config-default", "index.ts");
170
+ const observableDefaultSpec = path.join(__dirname, "..", "src", "plugins", "observable-default", "index.ts");
171
+
172
+ const runMocha = (env, spec, useTs, coverage, coverageInclude) => {
173
+ const mochaArgs = [];
174
+ if (useTs) {
175
+ mochaArgs.push("--require", tsNodeRegister);
176
+ }
177
+ mochaArgs.push("--require", setupHook, spec);
178
+
179
+ if (!coverage) {
180
+ const result = spawnSync(process.execPath, [mochaBin, ...mochaArgs], {
181
+ stdio: "inherit",
182
+ env,
183
+ });
184
+ return result.status || 0;
185
+ }
186
+
187
+ const nycArgs = [
188
+ "--all",
189
+ "--check-coverage",
190
+ "--lines",
191
+ "100",
192
+ "--functions",
193
+ "100",
194
+ "--branches",
195
+ "100",
196
+ "--statements",
197
+ "100",
198
+ "--extension",
199
+ useTs ? ".ts" : ".js",
200
+ "--reporter",
201
+ "text",
202
+ "--reporter",
203
+ "lcov",
204
+ ];
205
+
206
+ if (coverageInclude && coverageInclude.length) {
207
+ for (const inc of coverageInclude) {
208
+ nycArgs.push("--include", inc);
209
+ }
210
+ }
211
+
212
+ nycArgs.push(mochaBin, ...mochaArgs);
213
+
214
+ const result = spawnSync(process.execPath, [nycBin, ...nycArgs], {
215
+ stdio: "inherit",
216
+ env,
217
+ });
218
+ return result.status || 0;
219
+ };
220
+
221
+ let exitCode = 0;
222
+
223
+ for (const plugin of filtered) {
224
+ if (plugin.testsEntry && plugin.testsEntry.skip === true) {
225
+ console.warn("Skipping tests for plugin (skipped):", plugin.name);
226
+ continue;
227
+ }
228
+
229
+ const useTs = forceTs || (!forceJs && fs.existsSync(path.join(plugin.pluginRoot, "src")));
230
+ const pluginModule = resolvePluginModule(plugin.pluginRoot, plugin.pluginPath, useTs);
231
+ if (!fs.existsSync(pluginModule)) {
232
+ console.error("Plugin module not found:", pluginModule);
233
+ exitCode = 2;
234
+ continue;
235
+ }
236
+
237
+ const localBaseEntry = resolveLocalBaseEntry(repoRoot, useTs);
238
+ const testsEntry = plugin.testsEntry || {};
239
+ const defaultConfig = testsEntry.default?.config || null;
240
+ const defaultSetup = normalizeSetup(testsEntry.default?.setup || null);
241
+ const defaultDispose = normalizeDispose(testsEntry.default?.dispose || null);
242
+ const testCases = Array.isArray(testsEntry.tests) && testsEntry.tests.length > 0
243
+ ? testsEntry.tests
244
+ : [ { name: "default", config: null, setup: null, dispose: null } ];
245
+
246
+ const coverageInclude = useTs
247
+ ? [path.join(plugin.pluginRoot, "src", "**", "*.ts")]
248
+ : [path.join(plugin.pluginRoot, "lib", "**", "*.js")];
249
+
250
+ const runSetupScript = (scriptPath, phase) => {
251
+ if (!scriptPath) return 0;
252
+ const resolved = path.resolve(plugin.pluginRoot, scriptPath);
253
+ if (!fs.existsSync(resolved)) {
254
+ console.error(`Setup script not found (${phase}):`, resolved);
255
+ return 2;
256
+ }
257
+ const ext = path.extname(resolved).toLowerCase();
258
+ let result;
259
+ if (ext === ".ts") {
260
+ result = spawnSync(process.execPath, ["-r", tsNodeRegister, resolved], {
261
+ stdio: "inherit",
262
+ env: {
263
+ ...process.env,
264
+ BSB_TEST_PLUGIN_NAME: plugin.name,
265
+ BSB_TEST_PLUGIN_ID: plugin.id,
266
+ },
267
+ });
268
+ } else if (ext === ".js" || ext === ".cjs" || ext === ".mjs") {
269
+ result = spawnSync(process.execPath, [resolved], {
270
+ stdio: "inherit",
271
+ env: {
272
+ ...process.env,
273
+ BSB_TEST_PLUGIN_NAME: plugin.name,
274
+ BSB_TEST_PLUGIN_ID: plugin.id,
275
+ },
276
+ });
277
+ } else {
278
+ result = spawnSync(resolved, {
279
+ stdio: "inherit",
280
+ shell: true,
281
+ env: {
282
+ ...process.env,
283
+ BSB_TEST_PLUGIN_NAME: plugin.name,
284
+ BSB_TEST_PLUGIN_ID: plugin.id,
285
+ },
286
+ });
287
+ }
288
+ return result.status || 0;
289
+ };
290
+
291
+ for (const testCase of testCases) {
292
+ if (testCase && testCase.skip === true) {
293
+ console.warn("Skipping test case (skipped):", testCase.name || "unnamed");
294
+ continue;
295
+ }
296
+
297
+ const mergedConfig = mergeConfig(defaultConfig, testCase?.config);
298
+ const setup = normalizeSetup(testCase?.setup) || defaultSetup;
299
+ const dispose = normalizeDispose(testCase?.dispose) || defaultDispose;
300
+ const beforeAll = setup?.beforeAll || null;
301
+ const afterAll = setup?.afterAll || null;
302
+
303
+ const env = {
304
+ ...process.env,
305
+ BSB_TEST_PLUGIN_MODULE: pluginModule,
306
+ BSB_TEST_PLUGIN_NAME: plugin.name,
307
+ BSB_TEST_PLUGIN_CONFIG: JSON.stringify(mergedConfig || null),
308
+ BSB_TEST_LOCAL_BASE_ENTRY: localBaseEntry || "",
309
+ TS_NODE_PROJECT: path.join(__dirname, "..", "tsconfig.json"),
310
+ };
311
+
312
+ if (beforeAll) {
313
+ const code = runSetupScript(beforeAll, "beforeAll");
314
+ if (code !== 0) {
315
+ exitCode = code;
316
+ continue;
317
+ }
318
+ }
319
+
320
+ const isEvents = plugin.id.startsWith("events-") || plugin.name.startsWith("events-");
321
+ if (isEvents) {
322
+ const code = runMocha(env, pluginEventsRunner, useTs, enableCoverage, coverageInclude);
323
+ if (code !== 0) exitCode = code;
324
+ }
325
+
326
+ const isObservable = plugin.id.startsWith("observable-") || plugin.name.startsWith("observable-");
327
+ if (isObservable) {
328
+ const code = runMocha(env, pluginObservableRunner, useTs, enableCoverage, coverageInclude);
329
+ if (code !== 0) exitCode = code;
330
+ }
331
+
332
+ if (plugin.id === "events-default") {
333
+ const code = runMocha(env, eventsDefaultSpec, useTs, enableCoverage, coverageInclude);
334
+ if (code !== 0) exitCode = code;
335
+ }
336
+
337
+ if (plugin.id === "logging-default") {
338
+ const code = runMocha(env, loggingDefaultSpec, useTs, enableCoverage, coverageInclude);
339
+ if (code !== 0) exitCode = code;
340
+ }
341
+
342
+ if (plugin.id === "config-default") {
343
+ const code = runMocha(env, configDefaultSpec, useTs, enableCoverage, coverageInclude);
344
+ if (code !== 0) exitCode = code;
345
+ }
346
+
347
+ if (plugin.id === "observable-default") {
348
+ const code = runMocha(env, observableDefaultSpec, useTs, enableCoverage, coverageInclude);
349
+ if (code !== 0) exitCode = code;
350
+ }
351
+
352
+ const customEnv = {
353
+ ...env,
354
+ BSB_TEST_PLUGIN_ID: plugin.id,
355
+ BSB_TEST_PLUGIN_ROOT: plugin.pluginRoot,
356
+ };
357
+ const customCode = runMocha(customEnv, pluginCustomRunner, useTs, enableCoverage, coverageInclude);
358
+ if (customCode !== 0) exitCode = customCode;
359
+
360
+ if (afterAll) {
361
+ const code = runSetupScript(afterAll, "afterAll");
362
+ if (code !== 0) {
363
+ exitCode = code;
364
+ }
365
+ }
366
+
367
+ if (dispose?.afterAll) {
368
+ const code = runSetupScript(dispose.afterAll, "dispose");
369
+ if (code !== 0) {
370
+ exitCode = code;
371
+ }
372
+ }
373
+ }
374
+ }
375
+
376
+ process.exit(exitCode);
package/docs/index.md CHANGED
@@ -1,101 +1,101 @@
1
- # @bsb/tests
2
-
3
- Shared test runner for BSB plugins.
4
-
5
- ## Usage
6
-
7
- ```bash
8
- npx @bsb/tests
9
- npx @bsb/tests --plugin events-rabbitmq
10
- npx @bsb/tests --no-coverage
11
- ```
12
-
13
- ## bsb-tests.json
14
-
15
- Place `bsb-tests.json` next to `bsb-plugin.json`.
16
-
17
- Format:
18
-
19
- ```json
20
- {
21
- "nodejs": [
22
- {
23
- "id": "events-rabbitmq",
24
- "skip": false,
25
- "default": {
26
- "config": {
27
- "endpoints": ["amqp://127.0.0.1:5670"]
28
- },
29
- "setup": "scripts/test-setup.sh",
30
- "dispose": "scripts/test-dispose.sh"
31
- },
32
- "tests": [
33
- {
34
- "name": "primary",
35
- "skip": false,
36
- "config": {
37
- "prefetch": 10
38
- },
39
- "setup": {
40
- "beforeAll": "scripts/test-setup.js",
41
- "afterAll": "scripts/test-teardown.js"
42
- },
43
- "dispose": {
44
- "afterAll": "scripts/test-dispose.js"
45
- }
46
- }
47
- ]
48
- }
49
- ]
50
- }
51
- ```
52
-
53
- Notes:
54
-
55
- - `skip: true` skips plugin tests or specific test cases.
56
- - `setup` can be a single script path or `{ "beforeAll": "...", "afterAll": "..." }`.
57
- - `dispose` is an optional cleanup hook (run after tests).
58
- - If `tests` is empty, the `default` config is used once.
59
- - Script paths are relative to the plugin root.
60
-
61
- ## Custom Tests
62
-
63
- Add custom tests under `tests/{plugin-id}` in the repo root.
64
-
65
- Layout:
66
-
67
- ```
68
- tests/
69
- events-rabbitmq/
70
- _before.ts
71
- _after.ts
72
- connection.ts
73
- streaming/
74
- _before.ts
75
- _after.ts
76
- roundtrip.ts
77
- ```
78
-
79
- Rules:
80
-
81
- - Each `*.ts/js` file (not starting with `_`) exports a single test function.
82
- - Files starting with `_` are ignored as tests and can be used for helpers.
83
- - `_before` and `_after` run before/after all tests in the same folder.
84
- - If `_before` returns a value, it is passed to each test in that folder.
85
-
86
- Test function signature:
87
-
88
- ```ts
89
- export default async function test(ctx: any, data?: any) {
90
- // ctx: { pluginId, pluginName, pluginRoot, config, group }
91
- // data: value returned by _before (optional)
92
- }
93
- ```
94
-
95
- ## Built-in Suites
96
-
97
- - `events-*` plugins
98
- - `observable-*` plugins
99
- - `events-default` additional suite
100
- - `observable-default` additional suite
101
- - `config-default`
1
+ # @bsb/tests
2
+
3
+ Shared test runner for BSB plugins.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx @bsb/tests
9
+ npx @bsb/tests --plugin events-rabbitmq
10
+ npx @bsb/tests --no-coverage
11
+ ```
12
+
13
+ ## bsb-tests.json
14
+
15
+ Place `bsb-tests.json` next to `bsb-plugin.json`.
16
+
17
+ Format:
18
+
19
+ ```json
20
+ {
21
+ "nodejs": [
22
+ {
23
+ "id": "events-rabbitmq",
24
+ "skip": false,
25
+ "default": {
26
+ "config": {
27
+ "endpoints": ["amqp://127.0.0.1:5670"]
28
+ },
29
+ "setup": "scripts/test-setup.sh",
30
+ "dispose": "scripts/test-dispose.sh"
31
+ },
32
+ "tests": [
33
+ {
34
+ "name": "primary",
35
+ "skip": false,
36
+ "config": {
37
+ "prefetch": 10
38
+ },
39
+ "setup": {
40
+ "beforeAll": "scripts/test-setup.js",
41
+ "afterAll": "scripts/test-teardown.js"
42
+ },
43
+ "dispose": {
44
+ "afterAll": "scripts/test-dispose.js"
45
+ }
46
+ }
47
+ ]
48
+ }
49
+ ]
50
+ }
51
+ ```
52
+
53
+ Notes:
54
+
55
+ - `skip: true` skips plugin tests or specific test cases.
56
+ - `setup` can be a single script path or `{ "beforeAll": "...", "afterAll": "..." }`.
57
+ - `dispose` is an optional cleanup hook (run after tests).
58
+ - If `tests` is empty, the `default` config is used once.
59
+ - Script paths are relative to the plugin root.
60
+
61
+ ## Custom Tests
62
+
63
+ Add custom tests under `tests/{plugin-id}` in the repo root.
64
+
65
+ Layout:
66
+
67
+ ```
68
+ tests/
69
+ events-rabbitmq/
70
+ _before.ts
71
+ _after.ts
72
+ connection.ts
73
+ streaming/
74
+ _before.ts
75
+ _after.ts
76
+ roundtrip.ts
77
+ ```
78
+
79
+ Rules:
80
+
81
+ - Each `*.ts/js` file (not starting with `_`) exports a single test function.
82
+ - Files starting with `_` are ignored as tests and can be used for helpers.
83
+ - `_before` and `_after` run before/after all tests in the same folder.
84
+ - If `_before` returns a value, it is passed to each test in that folder.
85
+
86
+ Test function signature:
87
+
88
+ ```ts
89
+ export default async function test(ctx: any, data?: any) {
90
+ // ctx: { pluginId, pluginName, pluginRoot, config, group }
91
+ // data: value returned by _before (optional)
92
+ }
93
+ ```
94
+
95
+ ## Built-in Suites
96
+
97
+ - `events-*` plugins
98
+ - `observable-*` plugins
99
+ - `events-default` additional suite
100
+ - `observable-default` additional suite
101
+ - `config-default`
package/package.json CHANGED
@@ -1,47 +1,47 @@
1
- {
1
+ {
2
2
  "name": "@bsb/tests",
3
- "version": "0.0.1",
3
+ "version": "9.5.5",
4
4
  "description": "Shared test suites for BSB Node.js core and plugins",
5
- "license": "(AGPL-3.0-only OR Commercial)",
6
- "author": {
7
- "name": "BetterCorp (PTY) Ltd",
8
- "email": "ninja@bettercorp.dev",
9
- "url": "https://bettercorp.dev/"
10
- },
5
+ "license": "(AGPL-3.0-only OR Commercial)",
6
+ "author": {
7
+ "name": "BetterCorp (PTY) Ltd",
8
+ "email": "ninja@bettercorp.dev",
9
+ "url": "https://bettercorp.dev/"
10
+ },
11
11
  "repository": {
12
12
  "type": "git",
13
13
  "url": "git+https://github.com/BetterCorp/better-service-base.git",
14
14
  "directory": "tests/nodejs"
15
15
  },
16
- "main": "lib/index.js",
17
- "types": "lib/index.d.ts",
16
+ "main": "lib/index.js",
17
+ "types": "lib/index.d.ts",
18
18
  "files": [
19
19
  "bin/**/*",
20
20
  "lib/**/*",
21
21
  "README.md",
22
22
  "docs/**/*"
23
23
  ],
24
- "scripts": {
25
- "clean": "rimraf lib",
26
- "build": "tsc -p tsconfig.json",
27
- "test": "mocha --config .mocharc.cjs",
28
- "cli": "node ./bin/bsb-tests.cjs"
29
- },
30
- "dependencies": {
31
- "@bsb/base": "^9.0.0"
32
- },
33
- "devDependencies": {
34
- "@types/mocha": "^10.0.6",
35
- "@types/node": "^25.0.0",
36
- "mocha": "^12.0.0-beta-9.6",
37
- "rimraf": "^6.1.2",
38
- "ts-node": "^10.9.2",
39
- "typescript": "^5.9.3"
40
- },
41
- "engines": {
42
- "node": ">=23.0.0",
43
- "npm": ">=11.0.0"
44
- },
24
+ "scripts": {
25
+ "clean": "rimraf lib",
26
+ "build": "tsc -p tsconfig.json",
27
+ "test": "mocha --config .mocharc.cjs",
28
+ "cli": "node ./bin/bsb-tests.cjs"
29
+ },
30
+ "dependencies": {
31
+ "@bsb/base": "^9.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/mocha": "^10.0.6",
35
+ "@types/node": "^25.0.0",
36
+ "mocha": "^12.0.0-beta-9.6",
37
+ "rimraf": "^6.1.2",
38
+ "ts-node": "^10.9.2",
39
+ "typescript": "^5.9.3"
40
+ },
41
+ "engines": {
42
+ "node": ">=23.0.0",
43
+ "npm": ">=11.0.0"
44
+ },
45
45
  "bin": {
46
46
  "bsb-tests": "bin/bsb-tests.cjs"
47
47
  },