@elench/testkit 0.1.12 → 0.1.13

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
@@ -48,6 +48,7 @@ npx @elench/testkit destroy
48
48
 
49
49
  1. **Discovery** — reads `runner.manifest.json` for services, suites, files, and frameworks
50
50
  2. **Config** — reads `testkit.config.json` for local runtime, migration, dependency, and database settings
51
+ Per-service `.env` files declared in config are loaded when present.
51
52
  3. **Database** — provisions a Neon branch when a service declares one
52
53
  4. **Seed** — runs optional product seed commands against the provisioned database
53
54
  5. **Runtime** — starts required local services, waits for readiness, and injects test env
@@ -59,6 +60,11 @@ npx @elench/testkit destroy
59
60
  - `runner.manifest.json`: canonical test inventory
60
61
  - `testkit.config.json`: local execution and provisioning config
61
62
 
63
+ `testkit.config.json` can also declare:
64
+
65
+ - `envFile` / `envFiles` for service-specific environment loading
66
+ - `databaseFrom` for dependency services that must share another service's `DATABASE_URL`
67
+
62
68
  ## Parallel execution
63
69
 
64
70
  `@elench/testkit` can run suites in parallel with `--jobs <n>`.
package/lib/config.mjs CHANGED
@@ -68,13 +68,17 @@ export function loadConfigs(opts = {}) {
68
68
  }
69
69
 
70
70
  validateMergedService(name, runnerService, serviceConfig, productDir);
71
+ const serviceEnv = loadServiceEnv(productDir, serviceConfig);
71
72
 
72
73
  return {
73
74
  name,
74
75
  productDir,
75
76
  stateDir: path.join(productDir, ".testkit", name),
76
77
  suites: runnerService.suites,
77
- testkit: serviceConfig,
78
+ testkit: {
79
+ ...serviceConfig,
80
+ serviceEnv,
81
+ },
78
82
  };
79
83
  });
80
84
  }
@@ -228,6 +232,20 @@ function validateConfigCoverage(runner, config) {
228
232
  );
229
233
  }
230
234
  }
235
+
236
+ const databaseFrom = config.services[serviceName].databaseFrom;
237
+ if (databaseFrom) {
238
+ if (!config.services[databaseFrom]) {
239
+ throw new Error(
240
+ `Service "${serviceName}" databaseFrom "${databaseFrom}" is missing from ${TESTKIT_CONFIG}`
241
+ );
242
+ }
243
+ if (!runner.services[databaseFrom]) {
244
+ throw new Error(
245
+ `Service "${serviceName}" databaseFrom "${databaseFrom}" is missing from ${RUNNER_MANIFEST}`
246
+ );
247
+ }
248
+ }
231
249
  }
232
250
  }
233
251
 
@@ -263,7 +281,11 @@ export function isSiblingProduct(name) {
263
281
 
264
282
  function validateMergedService(name, runnerService, serviceConfig, productDir) {
265
283
  const usesLocalExecution = Object.values(runnerService.suites).some((suites) =>
266
- suites.some((suite) => suite.framework !== "k6" || !isDalSuiteType(suite, runnerService, suites))
284
+ suites.some(
285
+ (suite) =>
286
+ (suite.framework && suite.framework !== "k6") ||
287
+ !isDalSuiteType(suite, runnerService, suites)
288
+ )
267
289
  );
268
290
 
269
291
  if (usesLocalExecution && !isObject(serviceConfig.local)) {
@@ -306,6 +328,7 @@ function validateMergedService(name, runnerService, serviceConfig, productDir) {
306
328
  );
307
329
  }
308
330
  }
331
+
309
332
  }
310
333
 
311
334
  function validateServiceConfig(name, service, configPath) {
@@ -319,6 +342,27 @@ function validateServiceConfig(name, service, configPath) {
319
342
  }
320
343
  }
321
344
 
345
+ if (service.databaseFrom !== undefined && typeof service.databaseFrom !== "string") {
346
+ throw new Error(`Service "${name}" databaseFrom must be a string`);
347
+ }
348
+
349
+ if (service.database !== undefined && service.databaseFrom !== undefined) {
350
+ throw new Error(
351
+ `Service "${name}" cannot define both database and databaseFrom`
352
+ );
353
+ }
354
+
355
+ if (service.envFile !== undefined && typeof service.envFile !== "string") {
356
+ throw new Error(`Service "${name}" envFile must be a string`);
357
+ }
358
+
359
+ if (
360
+ service.envFiles !== undefined &&
361
+ (!Array.isArray(service.envFiles) || service.envFiles.some((v) => typeof v !== "string"))
362
+ ) {
363
+ throw new Error(`Service "${name}" envFiles must be an array of strings`);
364
+ }
365
+
322
366
  if (service.database !== undefined) {
323
367
  if (!isObject(service.database)) {
324
368
  throw new Error(`Service "${name}" database must be an object`);
@@ -385,6 +429,23 @@ function validateServiceConfig(name, service, configPath) {
385
429
  }
386
430
  }
387
431
 
432
+ function loadServiceEnv(productDir, serviceConfig) {
433
+ const env = {};
434
+ for (const envFile of getServiceEnvFiles(serviceConfig)) {
435
+ Object.assign(env, parseDotenv(resolveServiceCwd(productDir, envFile)));
436
+ }
437
+ return env;
438
+ }
439
+
440
+ function getServiceEnvFiles(serviceConfig) {
441
+ const files = [];
442
+ if (serviceConfig.envFile) files.push(serviceConfig.envFile);
443
+ if (Array.isArray(serviceConfig.envFiles)) {
444
+ files.push(...serviceConfig.envFiles);
445
+ }
446
+ return files;
447
+ }
448
+
388
449
  function requireString(obj, key, label) {
389
450
  if (typeof obj[key] !== "string" || obj[key].length === 0) {
390
451
  throw new Error(`${label} must be a non-empty string`);
package/lib/runner.mjs CHANGED
@@ -340,7 +340,7 @@ function resolveWorkerConfig(
340
340
  readyUrlByService,
341
341
  urlMappings
342
342
  ) {
343
- const stateDir = getWorkerServiceStateDir(workerStateDir, targetConfig.name, config.name);
343
+ const stateDir = resolveServiceStateDir(workerStateDir, targetConfig.name, config);
344
344
  const context = {
345
345
  workerId,
346
346
  serviceName: config.name,
@@ -476,7 +476,7 @@ async function runMigrations(runtimeConfigs) {
476
476
  const migrate = config.testkit.migrate;
477
477
  if (!migrate) continue;
478
478
 
479
- const env = { ...process.env };
479
+ const env = buildExecutionEnv(config);
480
480
  const dbUrl = readDatabaseUrl(config.stateDir);
481
481
  if (dbUrl) env.DATABASE_URL = dbUrl;
482
482
 
@@ -495,7 +495,7 @@ async function runSeeds(runtimeConfigs) {
495
495
  const seed = config.testkit.seed;
496
496
  if (!seed) continue;
497
497
 
498
- const env = { ...process.env };
498
+ const env = buildExecutionEnv(config);
499
499
  const dbUrl = readDatabaseUrl(config.stateDir);
500
500
  if (dbUrl) env.DATABASE_URL = dbUrl;
501
501
 
@@ -528,10 +528,7 @@ async function startLocalServices(runtimeConfigs) {
528
528
 
529
529
  async function startLocalService(config) {
530
530
  const cwd = resolveServiceCwd(config.productDir, config.testkit.local.cwd);
531
- const env = {
532
- ...process.env,
533
- ...config.testkit.local.env,
534
- };
531
+ const env = buildExecutionEnv(config, config.testkit.local.env);
535
532
  const port = config.testkit.local.port || numericPortFromUrl(config.testkit.local.baseUrl);
536
533
  if (port) {
537
534
  env.PORT = String(port);
@@ -603,7 +600,7 @@ async function runHttpK6Suite(targetConfig, suite) {
603
600
  try {
604
601
  await execa("k6", ["run", "-e", `BASE_URL=${baseUrl}`, absFile], {
605
602
  cwd: targetConfig.productDir,
606
- env: process.env,
603
+ env: buildExecutionEnv(targetConfig),
607
604
  stdio: "inherit",
608
605
  });
609
606
  } catch {
@@ -628,7 +625,7 @@ async function runDalSuite(targetConfig, suite) {
628
625
  try {
629
626
  await execa(k6Binary, ["run", "-e", `DATABASE_URL=${databaseUrl}`, absFile], {
630
627
  cwd: targetConfig.productDir,
631
- env: process.env,
628
+ env: buildExecutionEnv(targetConfig),
632
629
  stdio: "inherit",
633
630
  });
634
631
  } catch {
@@ -655,13 +652,7 @@ async function runPlaywrightSuite(targetConfig, suite) {
655
652
  try {
656
653
  await execa("npx", ["playwright", "test", ...files], {
657
654
  cwd,
658
- env: {
659
- ...process.env,
660
- BASE_URL: local.baseUrl,
661
- PLAYWRIGHT_HTML_OPEN: "never",
662
- TESTKIT_MANAGED_SERVERS: "1",
663
- TESTKIT_WORKER_ID: String(targetConfig.workerId),
664
- },
655
+ env: buildPlaywrightEnv(targetConfig, local.baseUrl),
665
656
  stdio: "inherit",
666
657
  });
667
658
  return { failed: false };
@@ -716,6 +707,11 @@ function needsLocalRuntime(suites) {
716
707
  return suites.some((suite) => suite.type !== "dal");
717
708
  }
718
709
 
710
+ function resolveServiceStateDir(workerStateDir, targetName, config) {
711
+ const dbSource = config.testkit.databaseFrom || config.name;
712
+ return getWorkerServiceStateDir(workerStateDir, targetName, dbSource);
713
+ }
714
+
719
715
  function getWorkerServiceStateDir(workerStateDir, targetName, serviceName) {
720
716
  if (targetName === serviceName) {
721
717
  return workerStateDir;
@@ -723,6 +719,44 @@ function getWorkerServiceStateDir(workerStateDir, targetName, serviceName) {
723
719
  return path.join(workerStateDir, "deps", serviceName);
724
720
  }
725
721
 
722
+ function buildExecutionEnv(config, extraEnv = {}) {
723
+ return {
724
+ ...process.env,
725
+ ...(config.testkit.serviceEnv || {}),
726
+ ...extraEnv,
727
+ };
728
+ }
729
+
730
+ function buildPlaywrightEnv(config, baseUrl) {
731
+ const env = buildExecutionEnv(config, {
732
+ BASE_URL: baseUrl,
733
+ PLAYWRIGHT_HTML_OPEN: "never",
734
+ PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS:
735
+ process.env.PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS || "1",
736
+ TESTKIT_MANAGED_SERVERS: "1",
737
+ TESTKIT_WORKER_ID: String(config.workerId),
738
+ });
739
+
740
+ const browsersPath = resolvePlaywrightBrowsersPath(env.PLAYWRIGHT_BROWSERS_PATH);
741
+ if (browsersPath) {
742
+ env.PLAYWRIGHT_BROWSERS_PATH = browsersPath;
743
+ }
744
+
745
+ return env;
746
+ }
747
+
748
+ function resolvePlaywrightBrowsersPath(configuredPath) {
749
+ const home = process.env.HOME;
750
+ if (home) {
751
+ const fallback = path.join(home, ".cache", "ms-playwright");
752
+ if (fs.existsSync(fallback)) {
753
+ return fallback;
754
+ }
755
+ }
756
+
757
+ return configuredPath;
758
+ }
759
+
726
760
  function readDatabaseUrl(stateDir) {
727
761
  return readStateValue(path.join(stateDir, "database_url"));
728
762
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "CLI for running manifest-defined local test suites across k6 and Playwright",
5
5
  "type": "module",
6
6
  "bin": {