@elench/testkit 0.1.146 → 0.1.148

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
@@ -203,6 +203,10 @@ export default defineConfig({
203
203
  });
204
204
  ```
205
205
 
206
+ Managed UI suites should import Playwright APIs from `@elench/testkit/ui`.
207
+ Testkit runs those suites with its own Playwright runtime so product-local
208
+ Playwright installs cannot split the test registry.
209
+
206
210
  For scripts and app-runtime code, `@elench/testkit/env` provides shared helpers
207
211
  for managed runtime detection, dotenv loading, and local-database safety:
208
212
 
@@ -563,12 +567,17 @@ export default defineConfig({
563
567
 
564
568
  const suite = defineHttpSuite({ profile: "defaultAuth" }, ({ actor, actors, req }) => {
565
569
  req.get("/api/auth/session");
570
+ req("GET", "/api/auth/session");
566
571
  actor?.req.get("/api/auth/session");
567
572
  req.as("outsider").get("/api/auth/session");
568
573
  actors.get("reviewer").rawReq.get("/api/auth/session");
569
574
  });
570
575
  ```
571
576
 
577
+ Generated `auth.fixture(...)` identities are isolated per managed Testkit file run,
578
+ so parallel suites do not reuse the same login credentials unless an actor explicitly
579
+ sets `email`.
580
+
572
581
  DAL suites:
573
582
 
574
583
  ```ts
@@ -596,6 +605,9 @@ const suite = defineDalSuite({ fixtures }, ({ db, fixtureScope, fixtures }) => {
596
605
  export default suite;
597
606
  ```
598
607
 
608
+ DAL files for the same service database are serialized by default to avoid
609
+ exhausting Postgres connection limits during highly parallel runs.
610
+
599
611
  `defineDalFixtures(...)` is the package-owned DAL seeding model. It gives every
600
612
  suite a deterministic `fixtureScope` with:
601
613
 
@@ -243,8 +243,9 @@ function buildSessionBundle({ actorNames, contract, env, primaryActor, topology
243
243
  }
244
244
 
245
245
  function resolveActorSession({ actorDefinition, actorIndex, contract, env }) {
246
+ const resolvedActorDefinition = materializeActorDefinition(actorDefinition, env);
246
247
  const context = {
247
- actor: actorDefinition.actorName,
248
+ actor: resolvedActorDefinition.actorName,
248
249
  actorIndex,
249
250
  env,
250
251
  };
@@ -253,13 +254,13 @@ function resolveActorSession({ actorDefinition, actorIndex, contract, env }) {
253
254
  try {
254
255
  runProfileRequest({
255
256
  requestConfig: {
256
- body: () => buildSignupBody(actorDefinition),
257
+ body: () => buildSignupBody(resolvedActorDefinition),
257
258
  expect: contract.signup.expect,
258
259
  method: "POST",
259
260
  path: contract.signup.path,
260
261
  },
261
262
  context: { ...context, phase: "signup" },
262
- label: `auth.fixture signup for actor "${actorDefinition.actorName}"`,
263
+ label: `auth.fixture signup for actor "${resolvedActorDefinition.actorName}"`,
263
264
  });
264
265
  } catch {
265
266
  // Provisioning is best-effort. Some apps report duplicate-account races as 500s
@@ -269,28 +270,57 @@ function resolveActorSession({ actorDefinition, actorIndex, contract, env }) {
269
270
 
270
271
  const response = runProfileRequest({
271
272
  requestConfig: {
272
- body: () => buildLoginBody(actorDefinition),
273
+ body: () => buildLoginBody(resolvedActorDefinition),
273
274
  expect: contract.login.expect,
274
275
  method: "POST",
275
276
  path: contract.login.path,
276
277
  },
277
278
  context: { ...context, phase: "login" },
278
- label: `auth.fixture login for actor "${actorDefinition.actorName}"`,
279
+ label: `auth.fixture login for actor "${resolvedActorDefinition.actorName}"`,
279
280
  });
280
281
  const session = extractSessionData(response, contract.session);
281
282
 
282
283
  return {
283
284
  actorIndex,
284
- actorName: actorDefinition.actorName,
285
- email: actorDefinition.email,
286
- name: actorDefinition.name,
287
- organizationKey: actorDefinition.organizationKey,
288
- organizationName: actorDefinition.organizationName,
285
+ actorName: resolvedActorDefinition.actorName,
286
+ email: resolvedActorDefinition.email,
287
+ name: resolvedActorDefinition.name,
288
+ organizationKey: resolvedActorDefinition.organizationKey,
289
+ organizationName: resolvedActorDefinition.organizationName,
289
290
  session,
290
291
  ...session,
291
292
  };
292
293
  }
293
294
 
295
+ function materializeActorDefinition(actorDefinition, env = {}) {
296
+ if (!actorDefinition.emailGenerated) return actorDefinition;
297
+ const suffix = buildRuntimeEmailSuffix(env.rawEnv || {});
298
+ if (!suffix) return actorDefinition;
299
+
300
+ return {
301
+ ...actorDefinition,
302
+ email: appendEmailSuffix(actorDefinition.email, suffix),
303
+ };
304
+ }
305
+
306
+ function buildRuntimeEmailSuffix(rawEnv = {}) {
307
+ if (rawEnv.TESTKIT_ACTIVE !== "1") return "";
308
+ return [
309
+ rawEnv.TESTKIT_LEASE_ID,
310
+ rawEnv.TESTKIT_TEST_FILE,
311
+ rawEnv.TESTKIT_SCENARIO_SEED,
312
+ ]
313
+ .map(slugifyToken)
314
+ .filter(Boolean)
315
+ .join(".");
316
+ }
317
+
318
+ function appendEmailSuffix(email, suffix) {
319
+ const [local, domain] = String(email).split("@");
320
+ if (!local || !domain || !suffix) return email;
321
+ return `${local}.${suffix}@${domain}`;
322
+ }
323
+
294
324
  function buildSignupBody(actorDefinition) {
295
325
  return {
296
326
  email: actorDefinition.email,
@@ -421,6 +451,7 @@ function finalizeActorEntries({ namespace, actorEntries, orgs, defaultOrgKey, de
421
451
  email:
422
452
  normalizeOptionalString(config.email) ||
423
453
  buildGeneratedEmail(namespace, actorName),
454
+ emailGenerated: !normalizeOptionalString(config.email),
424
455
  loginBody: normalizePlainObject(config.loginBody, `${label} actor "${actorName}" loginBody`),
425
456
  name:
426
457
  normalizeOptionalString(config.name) ||
@@ -97,6 +97,8 @@ export async function runDefaultRuntimeTask(
97
97
  lease,
98
98
  {
99
99
  ...buildFileTimeoutEnv(fileTimeoutSeconds, startedAt),
100
+ TESTKIT_TEST_FILE: task.file,
101
+ TESTKIT_TEST_ID: String(task.id),
100
102
  ...(task.scenarioSeed ? { TESTKIT_SCENARIO_SEED: task.scenarioSeed } : {}),
101
103
  },
102
104
  process.env
@@ -1,5 +1,6 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
+ import { createRequire } from "module";
3
4
  import { parsePlaywrightJsonResults } from "../reporters/playwright.mjs";
4
5
  import { resolveServiceCwd } from "../config/paths.mjs";
5
6
  import { buildFileTimeoutEnv, formatFileTimeoutBudgetError } from "../shared/file-timeout.mjs";
@@ -10,6 +11,8 @@ import { normalizePathSeparators } from "./state.mjs";
10
11
  import { buildPlaywrightEnv } from "./template.mjs";
11
12
  import { settleManagedSubprocess, startManagedSubprocess } from "./subprocess.mjs";
12
13
 
14
+ const require = createRequire(import.meta.url);
15
+
13
16
  export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, reporter = null) {
14
17
  const local = targetConfig.testkit.local;
15
18
  if (!local?.baseUrl) {
@@ -33,9 +36,10 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, re
33
36
  { globalTimeoutMs: fileTimeoutSeconds * 1000 }
34
37
  );
35
38
  const jsonReportPath = buildPlaywrightJsonReportPath(lease, task);
39
+ const playwrightCliPath = resolvePlaywrightCliPath();
36
40
  const subprocess = startManagedSubprocess(
37
- "npx",
38
- ["playwright", "test", "--config", playwrightConfigPath, "--reporter=json"],
41
+ process.execPath,
42
+ [playwrightCliPath, "test", "--config", playwrightConfigPath, "--reporter=json"],
39
43
  {
40
44
  cwd,
41
45
  env: {
@@ -113,6 +117,10 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, re
113
117
  };
114
118
  }
115
119
 
120
+ export function resolvePlaywrightCliPath() {
121
+ return require.resolve("@playwright/test/cli");
122
+ }
123
+
116
124
  export function buildPlaywrightJsonReportPath(lease, task) {
117
125
  if (!lease?.leaseDir) {
118
126
  throw new Error(`Playwright task ${task?.file || ""} requires a lease-scoped directory`);
@@ -105,6 +105,9 @@ export function compareScheduledTasks(a, b) {
105
105
 
106
106
  function resolveTaskLocks(config, suite, file) {
107
107
  const locks = new Set();
108
+ if (suite.type === "dal") {
109
+ locks.add(`database:${config.name}`);
110
+ }
108
111
  const matchedSuiteRules = config.testkit.requirements?.suites || [];
109
112
  for (const rule of matchedSuiteRules) {
110
113
  if (matchesSuiteSelectors(suite.displayType, suite.name, [rule.selector])) {
@@ -61,6 +61,7 @@ export interface WaitForOptions {
61
61
  export interface RuntimeEnv {
62
62
  BASE: string;
63
63
  MACHINE_ID?: string;
64
+ rawEnv?: Record<string, string | undefined>;
64
65
  routeParams: RuntimeHeaders;
65
66
  }
66
67
 
@@ -290,6 +291,13 @@ export interface ActorRequestClient {
290
291
  }
291
292
 
292
293
  export interface HttpClient<TSetup = unknown> {
294
+ (
295
+ method: RuntimeMethod,
296
+ path: string,
297
+ setupData?: TSetup | null,
298
+ body?: unknown,
299
+ extraHeaders?: RuntimeHeaders
300
+ ): RuntimeResponse;
293
301
  as(actorName: string): ActorRequestClient;
294
302
  headers(extraHeaders?: RuntimeHeaders): RuntimeHeaders;
295
303
  multipart: MultipartRequestClient;
@@ -233,7 +233,12 @@ export function createHttpClient(config) {
233
233
  const defaultClient = createActorClient(defaultActor);
234
234
  const rawClient = createRawInvoker(null);
235
235
 
236
- return {
236
+ function client(method, path, setupDataOrBody = null, body, extraHeaders = {}) {
237
+ const requestBody = arguments.length >= 4 ? body : setupDataOrBody;
238
+ return defaultClient.request(method, path, requestBody, extraHeaders);
239
+ }
240
+
241
+ Object.assign(client, {
237
242
  rawHttp: http,
238
243
  headers(extraHeaders = {}) {
239
244
  return resolvedHeadersFor(defaultActor, extraHeaders);
@@ -299,7 +304,9 @@ export function createHttpClient(config) {
299
304
  return rawClient.multipart.patch(path, payload, extraHeaders);
300
305
  },
301
306
  },
302
- };
307
+ });
308
+
309
+ return client;
303
310
  }
304
311
 
305
312
  export function makeReq(baseUrl, sessionBundle = null, routeHeaders = {}, getHeaders = null, defaultActor = null) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/next-analysis",
3
- "version": "0.1.146",
3
+ "version": "0.1.148",
4
4
  "description": "SWC-backed Next.js source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-bridge",
3
- "version": "0.1.146",
3
+ "version": "0.1.148",
4
4
  "description": "Browser bridge helpers for testkit",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-protocol",
3
- "version": "0.1.146",
3
+ "version": "0.1.148",
4
4
  "description": "Shared browser protocol for testkit bridge and extension consumers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/ts-analysis",
3
- "version": "0.1.146",
3
+ "version": "0.1.148",
4
4
  "description": "TypeScript compiler-backed source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {