@testsmith/api-spector 0.2.5 → 0.2.7

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/LICENSE CHANGED
@@ -1,26 +1,21 @@
1
- Copyright (c) 2024-2026 Testsmith.io
2
-
3
- All rights reserved.
4
-
5
- Permission is granted to view, copy, and run this software for private,
6
- internal, and non-commercial purposes only.
1
+ MIT License
7
2
 
8
- You may not, without prior written permission from the copyright holder:
3
+ Copyright (c) 2024-2026 Testsmith.io
9
4
 
10
- * use this software or derivative works for any commercial purpose;
11
- * sell, sublicense, rent, lease, or monetize this software;
12
- * host, publish, distribute, or make available any public copy of this
13
- software or derivative works;
14
- * use this software to provide services to third parties;
15
- * remove or alter this copyright notice or license text.
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
16
11
 
17
- Except as expressly permitted above, no rights are granted.
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
18
14
 
19
15
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
16
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
17
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
- COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
23
- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
24
- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
-
26
- For commercial licensing or other permissions, contact: info@testsmith.io
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/bin/cli.js CHANGED
@@ -63,7 +63,48 @@ if (!command) {
63
63
 
64
64
  // ui: spawn electron with the app dir
65
65
  if (command.runner === 'electron') {
66
- const electron = require('electron')
66
+ // `require('electron')` throws if electron's postinstall didn't download
67
+ // the platform binary (common behind corporate proxies on Windows: the
68
+ // npm install completes but the GitHub Releases download is blocked).
69
+ // The raw stack trace is intimidating; turn it into actionable steps.
70
+ let electron
71
+ try {
72
+ electron = require('electron')
73
+ } catch (err) {
74
+ const msg = err && err.message ? err.message : String(err)
75
+ const looksLikeBinaryMissing = /Electron failed to install correctly|Cannot find module 'electron'/i.test(msg)
76
+ console.error('')
77
+ console.error(' API Spector — failed to launch the UI.')
78
+ console.error('')
79
+ if (looksLikeBinaryMissing) {
80
+ const installDir = path.dirname(__dirname)
81
+ console.error(' Electron is installed, but its platform binary is missing — the')
82
+ console.error(' download during `npm install` did not complete (often a proxy or')
83
+ console.error(' firewall blocking github.com / electronjs.org).')
84
+ console.error('')
85
+ console.error(' Fix options (try in order):')
86
+ console.error('')
87
+ console.error(' 1. Reinstall and force the postinstall script to run:')
88
+ console.error(' npm install -g @testsmith/api-spector --force')
89
+ console.error('')
90
+ console.error(' 2. Behind a proxy? Set npm + electron mirrors and reinstall:')
91
+ console.error(' npm config set proxy http://your-proxy:port')
92
+ console.error(' npm config set https-proxy http://your-proxy:port')
93
+ console.error(' set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/')
94
+ console.error(' npm install -g @testsmith/api-spector --force')
95
+ console.error('')
96
+ console.error(' 3. Re-run electron\'s postinstall manually:')
97
+ console.error(` cd "${path.join(installDir, 'node_modules', 'electron')}"`)
98
+ console.error(' node install.js')
99
+ console.error('')
100
+ console.error(' CLI subcommands (run / mock / record / contract / wsdl) do not')
101
+ console.error(' need the UI binary and should work even while this is broken.')
102
+ } else {
103
+ console.error(` ${msg}`)
104
+ }
105
+ console.error('')
106
+ process.exit(1)
107
+ }
67
108
  const appDir = path.join(__dirname, '..')
68
109
  // Forward the user's cwd so the main process can decide whether to open a
69
110
  // workspace in this folder, or fall through to the welcome screen. Without
package/out/main/index.js CHANGED
@@ -1494,7 +1494,7 @@ function buildTestSuite(collection, environment, nameMap) {
1494
1494
  processFolder(collection.rootFolder);
1495
1495
  return lines.join("\n");
1496
1496
  }
1497
- function renderTree$5(paths) {
1497
+ function renderTree$6(paths) {
1498
1498
  const root = {};
1499
1499
  for (const p of [...paths].sort()) {
1500
1500
  let cur = root;
@@ -1513,8 +1513,8 @@ function renderTree$5(paths) {
1513
1513
  }
1514
1514
  return [".", ...render(root)].join("\n");
1515
1515
  }
1516
- function buildReadme$5(collectionName, filePaths) {
1517
- const tree = renderTree$5(filePaths);
1516
+ function buildReadme$6(collectionName, filePaths) {
1517
+ const tree = renderTree$6(filePaths);
1518
1518
  return `# ${collectionName} — API Tests (Robot Framework)
1519
1519
 
1520
1520
  ## Project structure
@@ -1553,7 +1553,7 @@ function generateRobotFramework(collection, environment) {
1553
1553
  ];
1554
1554
  const allPaths = ["requirements.txt", ...contentFiles.map((f) => f.path)];
1555
1555
  return [
1556
- { path: "README.md", content: buildReadme$5(collection.name, allPaths) },
1556
+ { path: "README.md", content: buildReadme$6(collection.name, allPaths) },
1557
1557
  { path: "requirements.txt", content: "robotframework\nrobotframework-requests\n" },
1558
1558
  ...contentFiles
1559
1559
  ];
@@ -1859,7 +1859,7 @@ function buildPackageJson$3(collectionName) {
1859
1859
  }
1860
1860
  }, null, 2) + "\n";
1861
1861
  }
1862
- function renderTree$4(paths) {
1862
+ function renderTree$5(paths) {
1863
1863
  const root = {};
1864
1864
  for (const p of [...paths].sort()) {
1865
1865
  let cur = root;
@@ -1878,8 +1878,8 @@ function renderTree$4(paths) {
1878
1878
  }
1879
1879
  return [".", ...render(root)].join("\n");
1880
1880
  }
1881
- function buildReadme$4(collectionName, filePaths) {
1882
- const tree = renderTree$4([...filePaths, ".env.local"]);
1881
+ function buildReadme$5(collectionName, filePaths) {
1882
+ const tree = renderTree$5([...filePaths, ".env.local"]);
1883
1883
  return `# ${collectionName} — API Tests (Playwright TypeScript)
1884
1884
 
1885
1885
  ## Project structure
@@ -1914,7 +1914,7 @@ function generatePlaywright(collection, environment) {
1914
1914
  files.unshift(
1915
1915
  { path: "package.json", content: buildPackageJson$3(collection.name) },
1916
1916
  { path: "playwright.config.ts", content: buildPlaywrightConfig$1(environment) },
1917
- { path: "README.md", content: buildReadme$4(collection.name, scaffoldPaths) }
1917
+ { path: "README.md", content: buildReadme$5(collection.name, scaffoldPaths) }
1918
1918
  );
1919
1919
  return files;
1920
1920
  }
@@ -2203,7 +2203,7 @@ function buildPackageJson$2(collectionName) {
2203
2203
  }
2204
2204
  }, null, 2) + "\n";
2205
2205
  }
2206
- function renderTree$3(paths) {
2206
+ function renderTree$4(paths) {
2207
2207
  const root = {};
2208
2208
  for (const p of [...paths].sort()) {
2209
2209
  let cur = root;
@@ -2222,8 +2222,8 @@ function renderTree$3(paths) {
2222
2222
  }
2223
2223
  return [".", ...render(root)].join("\n");
2224
2224
  }
2225
- function buildReadme$3(collectionName, filePaths) {
2226
- const tree = renderTree$3([...filePaths, ".env.local"]);
2225
+ function buildReadme$4(collectionName, filePaths) {
2226
+ const tree = renderTree$4([...filePaths, ".env.local"]);
2227
2227
  return `# ${collectionName} — API Tests (Playwright JavaScript)
2228
2228
 
2229
2229
  ## Project structure
@@ -2258,7 +2258,7 @@ function generatePlaywrightJs(collection, environment) {
2258
2258
  files.unshift(
2259
2259
  { path: "package.json", content: buildPackageJson$2(collection.name) },
2260
2260
  { path: "playwright.config.js", content: buildPlaywrightConfig(environment) },
2261
- { path: "README.md", content: buildReadme$3(collection.name, scaffoldPaths) }
2261
+ { path: "README.md", content: buildReadme$4(collection.name, scaffoldPaths) }
2262
2262
  );
2263
2263
  return files;
2264
2264
  }
@@ -2486,7 +2486,7 @@ function buildTsConfig() {
2486
2486
  exclude: ["node_modules", "dist"]
2487
2487
  }, null, 2) + "\n";
2488
2488
  }
2489
- function renderTree$2(paths) {
2489
+ function renderTree$3(paths) {
2490
2490
  const root = {};
2491
2491
  for (const p of [...paths].sort()) {
2492
2492
  let cur = root;
@@ -2505,8 +2505,8 @@ function renderTree$2(paths) {
2505
2505
  }
2506
2506
  return [".", ...render(root)].join("\n");
2507
2507
  }
2508
- function buildReadme$2(collectionName, filePaths) {
2509
- const tree = renderTree$2([...filePaths, ".env.local"]);
2508
+ function buildReadme$3(collectionName, filePaths) {
2509
+ const tree = renderTree$3([...filePaths, ".env.local"]);
2510
2510
  return `# ${collectionName} — API Tests (Supertest + Jest TypeScript)
2511
2511
 
2512
2512
  ## Project structure
@@ -2551,7 +2551,7 @@ function generateSupertestTs(collection, environment) {
2551
2551
  files.unshift(
2552
2552
  { path: "package.json", content: buildPackageJson$1(collection.name) },
2553
2553
  { path: "tsconfig.json", content: buildTsConfig() },
2554
- { path: "README.md", content: buildReadme$2(collection.name, scaffoldPaths) }
2554
+ { path: "README.md", content: buildReadme$3(collection.name, scaffoldPaths) }
2555
2555
  );
2556
2556
  return files;
2557
2557
  }
@@ -2755,7 +2755,7 @@ function buildPackageJson(collectionName) {
2755
2755
  }
2756
2756
  }, null, 2) + "\n";
2757
2757
  }
2758
- function renderTree$1(paths) {
2758
+ function renderTree$2(paths) {
2759
2759
  const root = {};
2760
2760
  for (const p of [...paths].sort()) {
2761
2761
  let cur = root;
@@ -2774,8 +2774,8 @@ function renderTree$1(paths) {
2774
2774
  }
2775
2775
  return [".", ...render(root)].join("\n");
2776
2776
  }
2777
- function buildReadme$1(collectionName, filePaths) {
2778
- const tree = renderTree$1([...filePaths, ".env.local"]);
2777
+ function buildReadme$2(collectionName, filePaths) {
2778
+ const tree = renderTree$2([...filePaths, ".env.local"]);
2779
2779
  return `# ${collectionName} — API Tests (Supertest + Jest JavaScript)
2780
2780
 
2781
2781
  ## Project structure
@@ -2819,11 +2819,11 @@ function generateSupertestJs(collection, environment) {
2819
2819
  const scaffoldPaths = ["package.json", ...files.map((f) => f.path)];
2820
2820
  files.unshift(
2821
2821
  { path: "package.json", content: buildPackageJson(collection.name) },
2822
- { path: "README.md", content: buildReadme$1(collection.name, scaffoldPaths) }
2822
+ { path: "README.md", content: buildReadme$2(collection.name, scaffoldPaths) }
2823
2823
  );
2824
2824
  return files;
2825
2825
  }
2826
- function javaClass(name) {
2826
+ function javaClass$1(name) {
2827
2827
  return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
2828
2828
  }
2829
2829
  function javaMethod(name) {
@@ -2854,7 +2854,7 @@ function interpolateJava(value, sharedVars = /* @__PURE__ */ new Set()) {
2854
2854
  return `" + System.getenv("${envKey}") + "`;
2855
2855
  }) + '"';
2856
2856
  }
2857
- function buildPom(collectionName) {
2857
+ function buildPom$1(collectionName) {
2858
2858
  const artifact = collectionName.replace(/\W+/g, "-").toLowerCase();
2859
2859
  return `<?xml version="1.0" encoding="UTF-8"?>
2860
2860
  <project xmlns="http://maven.apache.org/POM/4.0.0"
@@ -2966,7 +2966,7 @@ public class BaseTest {
2966
2966
  }
2967
2967
  function buildTestClass(folderName, folder, collection) {
2968
2968
  const requests = collection.requests;
2969
- const className = javaClass(folderName) + "Test";
2969
+ const className = javaClass$1(folderName) + "Test";
2970
2970
  const methods = [];
2971
2971
  const hooks = requestCollection.getAllApplicableHooks(folder.id, collection);
2972
2972
  const beforeAllH = hooks.beforeAll;
@@ -3122,7 +3122,7 @@ ${methods.join("\n\n")}
3122
3122
  }
3123
3123
  `;
3124
3124
  }
3125
- function renderTree(paths) {
3125
+ function renderTree$1(paths) {
3126
3126
  const root = {};
3127
3127
  for (const p of [...paths].sort()) {
3128
3128
  let cur = root;
@@ -3141,8 +3141,8 @@ function renderTree(paths) {
3141
3141
  }
3142
3142
  return [".", ...render(root)].join("\n");
3143
3143
  }
3144
- function buildReadme(collectionName, filePaths) {
3145
- const tree = renderTree(filePaths);
3144
+ function buildReadme$1(collectionName, filePaths) {
3145
+ const tree = renderTree$1(filePaths);
3146
3146
  return `# ${collectionName} — API Tests (REST Assured + JUnit 5)
3147
3147
 
3148
3148
  ## Project structure
@@ -3166,12 +3166,12 @@ BASE_URL=https://api.example.com mvn test
3166
3166
  }
3167
3167
  function generateRestAssured(collection, environment) {
3168
3168
  const files = [
3169
- { path: "pom.xml", content: buildPom(collection.name) },
3169
+ { path: "pom.xml", content: buildPom$1(collection.name) },
3170
3170
  { path: "src/test/java/com/example/api/BaseTest.java", content: buildBaseTest(environment) }
3171
3171
  ];
3172
3172
  function processFolder(folder, name) {
3173
3173
  if (folder.requestIds.length > 0) {
3174
- const className = javaClass(name) + "Test";
3174
+ const className = javaClass$1(name) + "Test";
3175
3175
  files.push({
3176
3176
  path: `src/test/java/com/example/api/${className}.java`,
3177
3177
  content: buildTestClass(name, folder, collection)
@@ -3187,6 +3187,442 @@ function generateRestAssured(collection, environment) {
3187
3187
  for (const sub of collection.rootFolder.folders) {
3188
3188
  processFolder(sub, sub.name);
3189
3189
  }
3190
+ files.unshift({ path: "README.md", content: buildReadme$1(collection.name, files.map((f) => f.path)) });
3191
+ return files;
3192
+ }
3193
+ function javaClass(name) {
3194
+ return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
3195
+ }
3196
+ function jsVar(name) {
3197
+ const parts = name.replace(/[^a-zA-Z0-9]+/g, " ").split(/\s+/).filter(Boolean).map((p) => p.toLowerCase());
3198
+ if (parts.length === 0) return "_";
3199
+ return parts[0] + parts.slice(1).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
3200
+ }
3201
+ function featureFileName(name) {
3202
+ const slug2 = name.replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").toLowerCase();
3203
+ return slug2 || "tests";
3204
+ }
3205
+ function configKey(envKey) {
3206
+ return jsVar(envKey);
3207
+ }
3208
+ function escSingle(s) {
3209
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
3210
+ }
3211
+ function interpolateKarate(value) {
3212
+ if (!value.includes("{{")) return `'${escSingle(value)}'`;
3213
+ const parts = [];
3214
+ let last = 0;
3215
+ const re = /\{\{([^}]+)\}\}/g;
3216
+ let m;
3217
+ while (m = re.exec(value)) {
3218
+ if (m.index > last) parts.push(`'${escSingle(value.slice(last, m.index))}'`);
3219
+ parts.push(configKey(m[1].trim()));
3220
+ last = m.index + m[0].length;
3221
+ }
3222
+ if (last < value.length) parts.push(`'${escSingle(value.slice(last))}'`);
3223
+ return parts.length === 1 ? parts[0] : parts.join(" + ");
3224
+ }
3225
+ function urlSteps(url) {
3226
+ const leadingVar = url.match(/^\{\{([^}]+)\}\}(.*)$/);
3227
+ if (leadingVar) {
3228
+ const baseVar = configKey(leadingVar[1].trim());
3229
+ const rest2 = leadingVar[2].replace(/^\//, "");
3230
+ const steps2 = [[`url ${baseVar}`]];
3231
+ if (rest2) steps2.push([`path ${interpolateKarate(rest2)}`]);
3232
+ return steps2;
3233
+ }
3234
+ if (/^https?:\/\//i.test(url)) {
3235
+ return [[`url ${interpolateKarate(url)}`]];
3236
+ }
3237
+ const rest = url.replace(/^\//, "");
3238
+ const steps = [[`url baseUrl`]];
3239
+ if (rest) steps.push([`path ${interpolateKarate(rest)}`]);
3240
+ return steps;
3241
+ }
3242
+ function buildPom(collectionName) {
3243
+ const artifact = collectionName.replace(/\W+/g, "-").toLowerCase();
3244
+ return `<?xml version="1.0" encoding="UTF-8"?>
3245
+ <project xmlns="http://maven.apache.org/POM/4.0.0"
3246
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3247
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
3248
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
3249
+ <modelVersion>4.0.0</modelVersion>
3250
+
3251
+ <groupId>com.example.api</groupId>
3252
+ <artifactId>${artifact}-karate</artifactId>
3253
+ <version>1.0.0-SNAPSHOT</version>
3254
+ <packaging>jar</packaging>
3255
+
3256
+ <properties>
3257
+ <java.version>17</java.version>
3258
+ <maven.compiler.source>\${java.version}</maven.compiler.source>
3259
+ <maven.compiler.target>\${java.version}</maven.compiler.target>
3260
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
3261
+ <karate.version>1.5.0</karate.version>
3262
+ <junit.version>5.10.2</junit.version>
3263
+ </properties>
3264
+
3265
+ <dependencies>
3266
+ <dependency>
3267
+ <groupId>io.karatelabs</groupId>
3268
+ <artifactId>karate-junit5</artifactId>
3269
+ <version>\${karate.version}</version>
3270
+ <scope>test</scope>
3271
+ </dependency>
3272
+ <dependency>
3273
+ <groupId>org.junit.jupiter</groupId>
3274
+ <artifactId>junit-jupiter</artifactId>
3275
+ <version>\${junit.version}</version>
3276
+ <scope>test</scope>
3277
+ </dependency>
3278
+ </dependencies>
3279
+
3280
+ <build>
3281
+ <plugins>
3282
+ <plugin>
3283
+ <groupId>org.apache.maven.plugins</groupId>
3284
+ <artifactId>maven-surefire-plugin</artifactId>
3285
+ <version>3.2.5</version>
3286
+ </plugin>
3287
+ </plugins>
3288
+ </build>
3289
+ </project>
3290
+ `;
3291
+ }
3292
+ function buildKarateConfig(environment) {
3293
+ const baseUrl = environment?.variables.find(
3294
+ (v) => ["base_url", "baseurl", "base-url"].includes(v.key.toLowerCase()) && !v.secret
3295
+ )?.value ?? "http://localhost:8080";
3296
+ const lines = [];
3297
+ lines.push(`function fn() {`);
3298
+ lines.push(` var env = karate.env || 'dev';`);
3299
+ lines.push(` karate.log('karate env:', env);`);
3300
+ lines.push(``);
3301
+ lines.push(` var config = {`);
3302
+ lines.push(` baseUrl: '${escSingle(baseUrl)}'`);
3303
+ const otherVars = (environment?.variables ?? []).filter((v) => {
3304
+ const k = v.key.toLowerCase();
3305
+ return v.enabled && !["base_url", "baseurl", "base-url"].includes(k);
3306
+ });
3307
+ for (const v of otherVars) {
3308
+ const key = configKey(v.key);
3309
+ const def = v.secret ? "''" : `'${escSingle(v.value ?? "")}'`;
3310
+ lines.push(`,`);
3311
+ lines.push(` ${key}: ${def}`);
3312
+ }
3313
+ lines.push(` };`);
3314
+ lines.push(``);
3315
+ lines.push(` // Allow each variable to be overridden via a process env var of the same`);
3316
+ lines.push(` // SHOUTY_SNAKE_CASE name (e.g. AUTH_TOKEN populates config.authToken).`);
3317
+ lines.push(` function envOverride(name, key) {`);
3318
+ lines.push(` var v = java.lang.System.getenv(name);`);
3319
+ lines.push(` if (v) config[key] = v;`);
3320
+ lines.push(` }`);
3321
+ lines.push(` envOverride('BASE_URL', 'baseUrl');`);
3322
+ for (const v of otherVars) {
3323
+ lines.push(` envOverride('${v.key}', '${configKey(v.key)}');`);
3324
+ }
3325
+ lines.push(``);
3326
+ lines.push(` return config;`);
3327
+ lines.push(`}`);
3328
+ return lines.join("\n") + "\n";
3329
+ }
3330
+ function buildRunner(collectionName) {
3331
+ const className = javaClass(collectionName) + "Runner";
3332
+ return `package karate;
3333
+
3334
+ import com.intuit.karate.junit5.Karate;
3335
+
3336
+ /**
3337
+ * JUnit 5 entry point — runs every .feature in the \`karate\` package
3338
+ * (this folder). Override the active env at run time with:
3339
+ *
3340
+ * mvn test -Dkarate.env=staging
3341
+ */
3342
+ public class ${className} {
3343
+
3344
+ @Karate.Test
3345
+ Karate all() {
3346
+ return Karate.run().relativeTo(getClass());
3347
+ }
3348
+ }
3349
+ `;
3350
+ }
3351
+ function buildBackground(folderId, collection) {
3352
+ const hooks = requestCollection.getAllApplicableHooks(folderId, collection);
3353
+ const lines = [];
3354
+ const defined = /* @__PURE__ */ new Set();
3355
+ for (const h of [...hooks.beforeAll, ...hooks.before]) {
3356
+ const method = h.method.toLowerCase();
3357
+ const declared = new Set((h.headers ?? []).filter((x) => x.enabled && x.key).map((x) => x.key.toLowerCase()));
3358
+ const has = (n) => declared.has(n.toLowerCase());
3359
+ lines.push(` # ${h.name}`);
3360
+ for (const block of urlSteps(h.url)) {
3361
+ lines.push(` * ${block[0]}`);
3362
+ for (let i = 1; i < block.length; i++) lines.push(block[i]);
3363
+ }
3364
+ for (const x of (h.headers ?? []).filter((x2) => x2.enabled && x2.key)) {
3365
+ lines.push(` * header ${x.key} = ${interpolateKarate(x.value)}`);
3366
+ }
3367
+ if (h.body.mode !== "none" && !["get", "head"].includes(method)) {
3368
+ for (const block of bodySteps(h.body, has)) {
3369
+ lines.push(` * ${block[0]}`);
3370
+ for (let i = 1; i < block.length; i++) lines.push(block[i]);
3371
+ }
3372
+ }
3373
+ lines.push(` * method ${method}`);
3374
+ const parsed = parsePostScript(h.postRequestScript);
3375
+ for (const e of parsed.extractions) {
3376
+ const jp = accessorToJsonPath(e.accessor).replace(/^json\.?/, "");
3377
+ const expr = jp ? `response.${jp}` : "response";
3378
+ const name = configKey(e.varName);
3379
+ defined.add(name);
3380
+ lines.push(` * def ${name} = ${expr}`);
3381
+ }
3382
+ }
3383
+ return { lines, defined };
3384
+ }
3385
+ function bodyDocstring(json) {
3386
+ const expanded = json.replace(/\{\{([^}]+)\}\}/g, (_, k) => `#(${configKey(k.trim())})`);
3387
+ let pretty = expanded.trim();
3388
+ try {
3389
+ pretty = JSON.stringify(JSON.parse(expanded), null, 2);
3390
+ } catch {
3391
+ }
3392
+ const out = [' """'];
3393
+ for (const l of pretty.split("\n")) out.push(` ${l}`);
3394
+ out.push(' """');
3395
+ return out;
3396
+ }
3397
+ function rawDocstring(text) {
3398
+ const expanded = text.replace(/\{\{([^}]+)\}\}/g, (_, k) => `#(${configKey(k.trim())})`);
3399
+ const out = [' """'];
3400
+ for (const l of expanded.replace(/\r\n/g, "\n").split("\n")) out.push(` ${l}`);
3401
+ out.push(' """');
3402
+ return out;
3403
+ }
3404
+ function bodySteps(body, alreadyHasHeader) {
3405
+ if (body.mode === "json" && body.json) {
3406
+ return [[`request`, ...bodyDocstring(body.json)]];
3407
+ }
3408
+ if (body.mode === "soap" && body.soap?.envelope) {
3409
+ const out = [];
3410
+ if (body.soap.soapAction && !alreadyHasHeader("soapaction")) {
3411
+ out.push([`header SOAPAction = '"${escSingle(body.soap.soapAction)}"'`]);
3412
+ }
3413
+ if (!alreadyHasHeader("content-type")) {
3414
+ out.push([`header Content-Type = 'text/xml; charset=utf-8'`]);
3415
+ }
3416
+ out.push([`request`, ...rawDocstring(body.soap.envelope)]);
3417
+ return out;
3418
+ }
3419
+ if (body.mode === "raw" && body.raw) {
3420
+ const out = [];
3421
+ if (body.rawContentType && !alreadyHasHeader("content-type")) {
3422
+ out.push([`header Content-Type = '${escSingle(body.rawContentType)}'`]);
3423
+ }
3424
+ out.push([`request`, ...rawDocstring(body.raw)]);
3425
+ return out;
3426
+ }
3427
+ if (body.mode === "graphql" && body.graphql) {
3428
+ const env = { query: body.graphql.query };
3429
+ if (body.graphql.variables?.trim()) {
3430
+ try {
3431
+ env.variables = JSON.parse(body.graphql.variables);
3432
+ } catch {
3433
+ }
3434
+ }
3435
+ if (body.graphql.operationName?.trim()) env.operationName = body.graphql.operationName.trim();
3436
+ return [[`request`, ...bodyDocstring(JSON.stringify(env))]];
3437
+ }
3438
+ return [];
3439
+ }
3440
+ function buildFeature(folderName, folder, collection) {
3441
+ const requests = collection.requests;
3442
+ const bg = buildBackground(folder.id, collection);
3443
+ const scenarios = [];
3444
+ const usedTags = /* @__PURE__ */ new Set();
3445
+ for (const reqId of folder.requestIds) {
3446
+ const req = requests[reqId];
3447
+ if (!req || req.disabled || req.hookType) continue;
3448
+ let tag = featureFileName(req.name);
3449
+ if (usedTags.has(tag)) {
3450
+ let i = 2;
3451
+ while (usedTags.has(`${tag}-${i}`)) i++;
3452
+ tag = `${tag}-${i}`;
3453
+ }
3454
+ usedTags.add(tag);
3455
+ const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
3456
+ const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
3457
+ const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
3458
+ const enabledParams = req.params.filter((p) => p.enabled && p.key);
3459
+ const method = req.method.toLowerCase();
3460
+ const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
3461
+ const lines = [];
3462
+ lines.push(`@${tag}`);
3463
+ lines.push(`Scenario: ${req.name}`);
3464
+ const setup = [...urlSteps(req.url)];
3465
+ if (effectiveAuth.type === "bearer") {
3466
+ const token = effectiveAuth.token ?? "";
3467
+ if (token.includes("{{")) {
3468
+ const single = token.match(/^\{\{([^}]+)\}\}$/);
3469
+ if (single) {
3470
+ setup.push([`header Authorization = 'Bearer ' + ${configKey(single[1].trim())}`]);
3471
+ } else {
3472
+ setup.push([`header Authorization = 'Bearer ' + ${interpolateKarate(token)}`]);
3473
+ }
3474
+ } else if (effectiveAuth.tokenSecretRef) {
3475
+ setup.push([`header Authorization = 'Bearer ' + ${configKey(effectiveAuth.tokenSecretRef)}`]);
3476
+ } else if (token) {
3477
+ setup.push([`header Authorization = 'Bearer ${escSingle(token)}'`]);
3478
+ }
3479
+ } else if (effectiveAuth.type === "basic") {
3480
+ const user = effectiveAuth.username ?? "";
3481
+ const pass = effectiveAuth.password ?? "";
3482
+ setup.push([`configure headers = ({ Authorization: 'Basic ' + java.util.Base64.getEncoder().encodeToString((${interpolateKarate(user)} + ':' + ${interpolateKarate(pass)}).getBytes()) })`]);
3483
+ }
3484
+ for (const h of allHeaders) {
3485
+ setup.push([`header ${h.key} = ${interpolateKarate(h.value)}`]);
3486
+ }
3487
+ for (const p of enabledParams) {
3488
+ setup.push([`param ${p.key} = ${interpolateKarate(p.value)}`]);
3489
+ }
3490
+ if (hasBody) {
3491
+ const declared = new Set(allHeaders.map((h) => h.key.toLowerCase()));
3492
+ const has = (n) => declared.has(n.toLowerCase());
3493
+ for (const block of bodySteps(req.body, has)) setup.push(block);
3494
+ }
3495
+ setup.forEach((step, i) => {
3496
+ const kw = i === 0 ? "Given" : "And";
3497
+ lines.push(` ${kw} ${step[0]}`);
3498
+ for (let j = 1; j < step.length; j++) lines.push(step[j]);
3499
+ });
3500
+ lines.push(` When method ${method}`);
3501
+ const asserts = [];
3502
+ const parsed = parsePostScript(req.postRequestScript);
3503
+ let statusEmitted = false;
3504
+ if (parsed.assertions.length > 0) {
3505
+ for (const a of parsed.assertions) {
3506
+ const jp = accessorToJsonPath(a.accessor).replace(/^json\.?/, "");
3507
+ const target = jp ? `response.${jp}` : "response";
3508
+ switch (a.kind) {
3509
+ case "status":
3510
+ asserts.push(`status ${a.expected ?? 200}`);
3511
+ statusEmitted = true;
3512
+ break;
3513
+ case "equals":
3514
+ asserts.push(`match ${target} == ${a.expected}`);
3515
+ break;
3516
+ case "contains":
3517
+ asserts.push(`match ${target} contains ${a.expected}`);
3518
+ break;
3519
+ case "exists":
3520
+ asserts.push(`match ${target} != null`);
3521
+ break;
3522
+ case "type": {
3523
+ const t = (a.expected ?? "").replace(/"/g, "");
3524
+ const fuzzy = ["string", "number", "boolean", "array", "object"].includes(t) ? `'#${t}'` : `'#notnull'`;
3525
+ asserts.push(`match ${target} == ${fuzzy}`);
3526
+ break;
3527
+ }
3528
+ case "above":
3529
+ asserts.push(`match ${target} > ${a.expected ?? 0}`);
3530
+ break;
3531
+ }
3532
+ }
3533
+ }
3534
+ if (!statusEmitted) asserts.unshift(`status 200`);
3535
+ asserts.forEach((step, i) => {
3536
+ const kw = i === 0 ? "Then" : "And";
3537
+ lines.push(` ${kw} ${step}`);
3538
+ });
3539
+ scenarios.push(lines.join("\n"));
3540
+ }
3541
+ const bgBlock = bg.lines.length > 0 ? `
3542
+ Background:
3543
+ ${bg.lines.join("\n")}
3544
+ ` : "";
3545
+ return `Feature: ${folderName}
3546
+ ${bgBlock}
3547
+ ${scenarios.join("\n\n")}
3548
+ `;
3549
+ }
3550
+ function renderTree(paths) {
3551
+ const root = {};
3552
+ for (const p of [...paths].sort()) {
3553
+ let cur = root;
3554
+ for (const part of p.split("/")) {
3555
+ cur = cur[part] ??= {};
3556
+ }
3557
+ }
3558
+ function render(node, prefix = "") {
3559
+ const entries = Object.entries(node);
3560
+ return entries.flatMap(([name, children], i) => {
3561
+ const last = i === entries.length - 1;
3562
+ const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
3563
+ if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
3564
+ return lines;
3565
+ });
3566
+ }
3567
+ return [".", ...render(root)].join("\n");
3568
+ }
3569
+ function buildReadme(collectionName, filePaths) {
3570
+ const tree = renderTree(filePaths);
3571
+ return `# ${collectionName} — API Tests (Karate + JUnit 5)
3572
+
3573
+ Karate is a BDD-flavoured API test framework that uses Gherkin feature files
3574
+ (no glue code) — see https://docs.karatelabs.io for the full reference.
3575
+
3576
+ ## Project structure
3577
+
3578
+ \`\`\`
3579
+ ${tree}
3580
+ \`\`\`
3581
+
3582
+ ## Setup
3583
+
3584
+ Requires Java 17+ and Maven 3.8+.
3585
+
3586
+ \`\`\`sh
3587
+ # Run all features
3588
+ mvn test
3589
+
3590
+ # Switch environment (read by karate-config.js)
3591
+ mvn test -Dkarate.env=staging
3592
+
3593
+ # Override individual values
3594
+ BASE_URL=https://api.example.com AUTH_TOKEN=eyJ... mvn test
3595
+
3596
+ # Filter by tag
3597
+ mvn test "-Dkarate.options=--tags @get-users"
3598
+ \`\`\`
3599
+ `;
3600
+ }
3601
+ function generateKarate(collection, environment) {
3602
+ const files = [
3603
+ { path: "pom.xml", content: buildPom(collection.name) },
3604
+ { path: "src/test/resources/karate-config.js", content: buildKarateConfig(environment) },
3605
+ {
3606
+ path: `src/test/java/karate/${javaClass(collection.name)}Runner.java`,
3607
+ content: buildRunner(collection.name)
3608
+ }
3609
+ ];
3610
+ function processFolder(folder, name) {
3611
+ if (folder.requestIds.some((id) => {
3612
+ const r = collection.requests[id];
3613
+ return r && !r.disabled && !r.hookType;
3614
+ })) {
3615
+ files.push({
3616
+ path: `src/test/resources/karate/${featureFileName(name)}.feature`,
3617
+ content: buildFeature(name, folder, collection)
3618
+ });
3619
+ }
3620
+ for (const sub of folder.folders) processFolder(sub, sub.name);
3621
+ }
3622
+ if (collection.rootFolder.requestIds.length > 0) {
3623
+ processFolder(collection.rootFolder, collection.name);
3624
+ }
3625
+ for (const sub of collection.rootFolder.folders) processFolder(sub, sub.name);
3190
3626
  files.unshift({ path: "README.md", content: buildReadme(collection.name, files.map((f) => f.path)) });
3191
3627
  return files;
3192
3628
  }
@@ -3206,6 +3642,8 @@ function registerGenerateHandlers(ipc) {
3206
3642
  return generateSupertestJs(collection, environment);
3207
3643
  case "rest_assured":
3208
3644
  return generateRestAssured(collection, environment);
3645
+ case "karate":
3646
+ return generateKarate(collection, environment);
3209
3647
  default:
3210
3648
  throw new Error(`Unknown target: ${target}`);
3211
3649
  }
@@ -4158,10 +4596,20 @@ function registerContractHandlers(ipc) {
4158
4596
  await snapshots.deleteSnapshot(dir, relPath);
4159
4597
  });
4160
4598
  }
4599
+ const GIT_BLOCK_TIMEOUT_MS = 6e4;
4161
4600
  function git() {
4162
4601
  const dir = getWorkspaceDir();
4163
4602
  if (!dir) throw new Error("No workspace open");
4164
- return simpleGit.simpleGit(dir);
4603
+ return simpleGit.simpleGit(dir, {
4604
+ timeout: { block: GIT_BLOCK_TIMEOUT_MS },
4605
+ unsafe: { allowUnsafeAskPass: true }
4606
+ }).env({
4607
+ ...process.env,
4608
+ GIT_TERMINAL_PROMPT: "0",
4609
+ GIT_ASKPASS: "echo",
4610
+ SSH_ASKPASS: "echo",
4611
+ GCM_INTERACTIVE: "Never"
4612
+ });
4165
4613
  }
4166
4614
  function registerGitHandlers(ipc) {
4167
4615
  ipc.handle("git:isRepo", async () => {
@@ -632,7 +632,7 @@ async function main() {
632
632
  if (envName && !env) {
633
633
  console.warn(color(`Warning: environment "${envName}" not found. Running without environment.`, C.yellow));
634
634
  }
635
- const version = `v${"0.2.5"}`;
635
+ const version = `v${"0.2.7"}`;
636
636
  console.log("");
637
637
  console.log(color(" API Test Runner" + (version ? ` ${version}` : ""), C.bold, C.white));
638
638
  console.log(color(` Workspace: ${wsPath}`, C.gray));
@@ -64976,7 +64976,8 @@ const TARGETS = [
64976
64976
  { id: "playwright_js", label: "Playwright JS", description: "JavaScript page-object API classes + spec files" },
64977
64977
  { id: "supertest_ts", label: "Supertest TS", description: "Jest + Supertest TypeScript tests" },
64978
64978
  { id: "supertest_js", label: "Supertest JS", description: "Jest + Supertest JavaScript tests" },
64979
- { id: "rest_assured", label: "REST Assured", description: "Java + JUnit 5 + Maven pom.xml" }
64979
+ { id: "rest_assured", label: "REST Assured", description: "Java + JUnit 5 + Maven pom.xml" },
64980
+ { id: "karate", label: "Karate", description: "Karate feature files + JUnit 5 runner + Maven" }
64980
64981
  ];
64981
64982
  function GeneratorPanel() {
64982
64983
  const setShowGeneratorPanel = useStore((s) => s.setShowGeneratorPanel);
@@ -70981,7 +70982,7 @@ function App() {
70981
70982
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#6aa3c8" }, children: "Spector" }),
70982
70983
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 text-[10px] font-normal opacity-50", children: [
70983
70984
  "v",
70984
- "0.2.5"
70985
+ "0.2.7"
70985
70986
  ] }),
70986
70987
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mx-2 opacity-30", children: "·" }),
70987
70988
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -5,7 +5,7 @@
5
5
  <meta charset="UTF-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>API Spector</title>
8
- <script type="module" crossorigin src="./assets/index-Cp3zkfSB.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-WH-P4X-L.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="./assets/index-DfkLUeA1.css">
10
10
  </head>
11
11
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@testsmith/api-spector",
3
3
  "productName": "API Spector",
4
- "version": "0.2.5",
4
+ "version": "0.2.7",
5
5
  "description": "Local-first API testing tool to inspect, test and mock APIs",
6
6
  "repository": {
7
7
  "type": "git",
@@ -11,7 +11,7 @@
11
11
  "url": "https://github.com/testsmith-io/api-spector/issues"
12
12
  },
13
13
  "homepage": "https://github.com/testsmith-io/api-spector",
14
- "license": "GPL-3.0-only",
14
+ "license": "MIT",
15
15
  "main": "out/main/index.js",
16
16
  "bin": {
17
17
  "api-spector": "bin/cli.js"
package/readme.md CHANGED
@@ -118,8 +118,6 @@ npm run package
118
118
 
119
119
  ## License
120
120
 
121
- Copyright (c) 2024-2026 Testsmith.io. All rights reserved.
121
+ API Spector is released under the [MIT License](LICENSE) — free to use, modify, and distribute, including for commercial purposes.
122
122
 
123
- This repository is publicly viewable for reference purposes only.
124
-
125
- Commercial use, public hosting, redistribution, and use for third-party services are not permitted without prior written permission. See [LICENSE](LICENSE) for full terms.
123
+ Copyright (c) 2024-2026 Testsmith.io