@elench/testkit 0.1.118 → 0.1.120

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.
Files changed (34) hide show
  1. package/lib/app/doctor.mjs +11 -113
  2. package/lib/cli/assistant/command-observer.mjs +1 -1
  3. package/lib/cli/assistant/state.mjs +2 -0
  4. package/lib/cli/commands/lint.mjs +41 -0
  5. package/lib/cli/entrypoint.mjs +1 -0
  6. package/lib/cli/operations/lint/operation.mjs +20 -0
  7. package/lib/cli/renderers/doctor/text.mjs +5 -0
  8. package/lib/cli/renderers/lint/text.mjs +20 -0
  9. package/lib/config/index.mjs +1 -0
  10. package/lib/config-api/database-steps.mjs +340 -0
  11. package/lib/config-api/index.d.ts +126 -3
  12. package/lib/config-api/index.mjs +176 -12
  13. package/lib/lint/index.mjs +689 -0
  14. package/lib/runner/template-steps.mjs +8 -0
  15. package/lib/runner/template.mjs +13 -0
  16. package/lib/runtime/index.d.ts +43 -0
  17. package/lib/runtime/index.mjs +24 -0
  18. package/lib/runtime-src/k6/http-assertions.js +82 -0
  19. package/lib/shared/configured-steps.mjs +16 -0
  20. package/lib/ui/index.d.ts +118 -0
  21. package/lib/ui/index.mjs +21 -0
  22. package/lib/ui/provisioning.mjs +283 -0
  23. package/lib/ui/sandbox.mjs +250 -0
  24. package/node_modules/@elench/next-analysis/package.json +1 -1
  25. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  26. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  27. package/node_modules/@elench/ts-analysis/package.json +1 -1
  28. package/node_modules/es-toolkit/CHANGELOG.md +801 -0
  29. package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +1 -0
  30. package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +3 -0
  31. package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +4 -0
  32. package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +4 -0
  33. package/node_modules/esprima/ChangeLog +235 -0
  34. package/package.json +5 -5
@@ -23,6 +23,12 @@ import {
23
23
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
24
24
  const ROOT_ENTRY = path.join(PACKAGE_ROOT, "lib", "index.mjs");
25
25
  const CONFIG_ENTRY = path.join(PACKAGE_ROOT, "lib", "config-api", "index.mjs");
26
+ const CONFIG_DATABASE_STEPS_ENTRY = path.join(
27
+ PACKAGE_ROOT,
28
+ "lib",
29
+ "config-api",
30
+ "database-steps.mjs"
31
+ );
26
32
  const CONFIG_NEXT_TSCONFIG_ENTRY = path.join(
27
33
  PACKAGE_ROOT,
28
34
  "lib",
@@ -159,6 +165,7 @@ async function runConfiguredStep(config, step, env, resolvedToolchain, options =
159
165
  const bundledModule = await bundleConfiguredModule(config.productDir, step);
160
166
  const { exportName } = parseModuleSpecifier(step.specifier);
161
167
  const context = {
168
+ args: step.args ?? {},
162
169
  databaseUrl: runtimeEnv.DATABASE_URL || null,
163
170
  productDir: config.productDir,
164
171
  cwd,
@@ -267,6 +274,7 @@ function resolvePackageSubpath(specifier) {
267
274
  const subpath = specifier.slice("@elench/testkit".length);
268
275
  if (!subpath) return ROOT_ENTRY;
269
276
  if (subpath === "/config") return CONFIG_ENTRY;
277
+ if (subpath === "/config/database-steps") return CONFIG_DATABASE_STEPS_ENTRY;
270
278
  if (subpath === "/config/next-runtime-tsconfig") return CONFIG_NEXT_TSCONFIG_ENTRY;
271
279
  if (subpath === "/drizzle") return DRIZZLE_ENTRY;
272
280
  if (subpath === "/env") return ENV_ENTRY;
@@ -251,6 +251,7 @@ function buildExecutionEnvWithContext(config, lease, extraEnv, processEnv, optio
251
251
  }
252
252
 
253
253
  export function buildPlaywrightEnv(config, baseUrl, lease, processEnv = process.env) {
254
+ const templateContext = buildTemplateContext(config, lease);
254
255
  return buildTaskExecutionEnv(
255
256
  config,
256
257
  lease,
@@ -260,6 +261,9 @@ export function buildPlaywrightEnv(config, baseUrl, lease, processEnv = process.
260
261
  PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS:
261
262
  processEnv.PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS || "1",
262
263
  TESTKIT_MANAGED_SERVERS: "1",
264
+ ...(config.testkit?.ui
265
+ ? { TESTKIT_UI_CONFIG_JSON: JSON.stringify(finalizeSerializable(config.testkit.ui, templateContext)) }
266
+ : {}),
263
267
  },
264
268
  processEnv
265
269
  );
@@ -300,6 +304,15 @@ export function finalizeString(value, context) {
300
304
  return resolved;
301
305
  }
302
306
 
307
+ function finalizeSerializable(value, context) {
308
+ if (typeof value === "string") return finalizeString(value, context);
309
+ if (Array.isArray(value)) return value.map((entry) => finalizeSerializable(entry, context));
310
+ if (!value || typeof value !== "object") return value;
311
+ return Object.fromEntries(
312
+ Object.entries(value).map(([key, entry]) => [key, finalizeSerializable(entry, context)])
313
+ );
314
+ }
315
+
303
316
  export function resolveTemplateString(value, context) {
304
317
  if (typeof value !== "string") return value;
305
318
 
@@ -438,6 +438,41 @@ export declare function expectCondition(
438
438
  predicate: () => boolean,
439
439
  label: string
440
440
  ): boolean;
441
+ export declare function expectRowCount<T>(
442
+ rows: T[],
443
+ expectedCount: number,
444
+ label?: string | null
445
+ ): boolean;
446
+ export declare function expectSingleRow<T>(
447
+ rows: T[],
448
+ label?: string | null
449
+ ): T | null;
450
+ export declare function expectNoRows<T>(
451
+ rows: T[],
452
+ label?: string | null
453
+ ): boolean;
454
+ export declare function expectField<T extends Record<string, unknown>>(
455
+ row: T,
456
+ field: keyof T | string,
457
+ predicateOrExpected: unknown | ((value: unknown) => boolean),
458
+ label?: string | null
459
+ ): boolean;
460
+ export declare function expectTruthyField<T extends Record<string, unknown>>(
461
+ row: T,
462
+ field: keyof T | string,
463
+ label?: string | null
464
+ ): boolean;
465
+ export declare function expectType(
466
+ value: unknown,
467
+ typeName: "array" | "bigint" | "boolean" | "function" | "null" | "number" | "object" | "string" | "symbol" | "undefined",
468
+ label?: string | null
469
+ ): boolean;
470
+ export declare function captureError(fn: () => unknown): unknown | null;
471
+ export declare function expectError(
472
+ errorOrFn: unknown | (() => unknown) | null,
473
+ predicate?: ((error: unknown) => boolean) | null,
474
+ label?: string | null
475
+ ): boolean;
441
476
 
442
477
  export declare function runAuthGateChecks(
443
478
  rawReq: RawRequestClient,
@@ -458,17 +493,25 @@ export declare function runPaginationChecks(
458
493
  ): void;
459
494
 
460
495
  export interface RuntimeExpectNamespace {
496
+ captureError: typeof captureError;
461
497
  condition: typeof expectCondition;
462
498
  error: {
499
+ captured: typeof expectError;
463
500
  message: typeof expectErrorMessage;
464
501
  shape: typeof expectErrorShape;
465
502
  };
503
+ field: typeof expectField;
466
504
  json: typeof expectJson;
467
505
  jsonPath: typeof expectJsonPath;
506
+ noRows: typeof expectNoRows;
468
507
  notStatus: typeof expectNotStatus;
469
508
  response: typeof expectResponse;
509
+ rowCount: typeof expectRowCount;
510
+ singleRow: typeof expectSingleRow;
470
511
  status: typeof expectStatus;
471
512
  statusOneOf: typeof expectStatusOneOf;
513
+ truthyField: typeof expectTruthyField;
514
+ type: typeof expectType;
472
515
  value: typeof expectValue;
473
516
  }
474
517
 
@@ -39,15 +39,23 @@ export {
39
39
  singleIterationOptions,
40
40
  } from "../runtime-src/k6/checks.js";
41
41
  export {
42
+ captureError,
42
43
  expectCondition,
44
+ expectError,
43
45
  expectErrorMessage,
44
46
  expectErrorShape,
47
+ expectField,
48
+ expectNoRows,
45
49
  expectJson,
46
50
  expectJsonPath,
47
51
  expectNotStatus,
48
52
  expectResponse,
53
+ expectRowCount,
54
+ expectSingleRow,
49
55
  expectStatus,
50
56
  expectStatusOneOf,
57
+ expectTruthyField,
58
+ expectType,
51
59
  expectValue,
52
60
  } from "../runtime-src/k6/http-assertions.js";
53
61
  export {
@@ -72,15 +80,23 @@ export {
72
80
  } from "../runtime-src/k6/http.js";
73
81
 
74
82
  import {
83
+ captureError,
75
84
  expectCondition,
85
+ expectError,
76
86
  expectErrorMessage,
77
87
  expectErrorShape,
88
+ expectField,
89
+ expectNoRows,
78
90
  expectJson,
79
91
  expectJsonPath,
80
92
  expectNotStatus,
81
93
  expectResponse,
94
+ expectRowCount,
95
+ expectSingleRow,
82
96
  expectStatus,
83
97
  expectStatusOneOf,
98
+ expectTruthyField,
99
+ expectType,
84
100
  expectValue,
85
101
  } from "../runtime-src/k6/http-assertions.js";
86
102
  import {
@@ -90,17 +106,25 @@ import {
90
106
  import { safeJson } from "../runtime-src/k6/http.js";
91
107
 
92
108
  export const expect = {
109
+ captureError,
93
110
  condition: expectCondition,
94
111
  error: {
112
+ captured: expectError,
95
113
  message: expectErrorMessage,
96
114
  shape: expectErrorShape,
97
115
  },
116
+ field: expectField,
98
117
  json: expectJson,
99
118
  jsonPath: expectJsonPath,
119
+ noRows: expectNoRows,
100
120
  notStatus: expectNotStatus,
101
121
  response: expectResponse,
122
+ rowCount: expectRowCount,
123
+ singleRow: expectSingleRow,
102
124
  status: expectStatus,
103
125
  statusOneOf: expectStatusOneOf,
126
+ truthyField: expectTruthyField,
127
+ type: expectType,
104
128
  value: expectValue,
105
129
  };
106
130
 
@@ -152,6 +152,77 @@ export function expectCondition(predicate, label) {
152
152
  });
153
153
  }
154
154
 
155
+ export function expectRowCount(rows, expectedCount, label = null) {
156
+ const expected = Number(expectedCount);
157
+ if (!Number.isInteger(expected) || expected < 0) {
158
+ throw new Error("expectRowCount requires a non-negative integer count");
159
+ }
160
+ return expectValue(
161
+ rows,
162
+ (value) => Array.isArray(value) && value.length === expected,
163
+ normalizeLabel(label, `row count is ${expected}`)
164
+ );
165
+ }
166
+
167
+ export function expectSingleRow(rows, label = "query returns one row") {
168
+ if (!expectRowCount(rows, 1, label)) return null;
169
+ return rows[0];
170
+ }
171
+
172
+ export function expectNoRows(rows, label = "query returns no rows") {
173
+ return expectRowCount(rows, 0, label);
174
+ }
175
+
176
+ export function expectField(row, field, predicateOrExpected, label = null) {
177
+ const fieldName = String(field || "");
178
+ return expectValue(
179
+ row?.[fieldName],
180
+ normalizeFieldPredicate(predicateOrExpected),
181
+ normalizeLabel(label, `${fieldName || "field"} matches expectation`)
182
+ );
183
+ }
184
+
185
+ export function expectTruthyField(row, field, label = null) {
186
+ const fieldName = String(field || "");
187
+ return expectField(
188
+ row,
189
+ fieldName,
190
+ (value) => Boolean(value),
191
+ normalizeLabel(label, `${fieldName || "field"} is truthy`)
192
+ );
193
+ }
194
+
195
+ export function expectType(value, typeName, label = null) {
196
+ const expected = String(typeName || "").trim();
197
+ if (!expected) throw new Error("expectType requires a type name");
198
+ return expectValue(
199
+ value,
200
+ (actual) => actualType(actual) === expected,
201
+ normalizeLabel(label, `type is ${expected}`)
202
+ );
203
+ }
204
+
205
+ export function captureError(fn) {
206
+ try {
207
+ fn();
208
+ return null;
209
+ } catch (error) {
210
+ return error;
211
+ }
212
+ }
213
+
214
+ export function expectError(errorOrFn, predicate = null, label = null) {
215
+ const error = typeof errorOrFn === "function" ? captureError(errorOrFn) : errorOrFn;
216
+ return expectValue(
217
+ error,
218
+ (value) => {
219
+ if (!value) return false;
220
+ return typeof predicate === "function" ? Boolean(predicate(value)) : true;
221
+ },
222
+ normalizeLabel(label, "error is captured")
223
+ );
224
+ }
225
+
155
226
  function buildHttpAssertionDetail({ kind, title, trace, expected, actual, response, message }) {
156
227
  return {
157
228
  kind,
@@ -238,6 +309,17 @@ function toValuePreview(value) {
238
309
  }
239
310
  }
240
311
 
312
+ function normalizeFieldPredicate(predicateOrExpected) {
313
+ if (typeof predicateOrExpected === "function") return predicateOrExpected;
314
+ return (value) => Object.is(value, predicateOrExpected);
315
+ }
316
+
317
+ function actualType(value) {
318
+ if (value === null) return "null";
319
+ if (Array.isArray(value)) return "array";
320
+ return typeof value;
321
+ }
322
+
241
323
  function normalizeLabel(value, fallback) {
242
324
  const normalized = typeof value === "string" ? value.trim() : "";
243
325
  return normalized.length > 0 ? normalized : fallback;
@@ -55,31 +55,37 @@ export function normalizeConfiguredStep(step, label) {
55
55
  if (kind === "command") {
56
56
  const cmd = normalizeOptionalString(step.run);
57
57
  if (!cmd) throw new Error(`${label}.run must be a non-empty string`);
58
+ const args = normalizeStepArgs(step.args, label);
58
59
  return {
59
60
  kind,
60
61
  cmd,
61
62
  cwd: normalizeOptionalString(step.cwd),
62
63
  inputs: normalizeConfiguredStepInputs(step.inputs, label),
64
+ ...(args !== undefined ? { args } : {}),
63
65
  };
64
66
  }
65
67
  if (kind === "sql-file") {
66
68
  const filePath = normalizeOptionalString(step.path);
67
69
  if (!filePath) throw new Error(`${label}.path must be a non-empty string`);
70
+ const args = normalizeStepArgs(step.args, label);
68
71
  return {
69
72
  kind,
70
73
  path: filePath,
71
74
  cwd: normalizeOptionalString(step.cwd),
72
75
  inputs: normalizeConfiguredStepInputs(step.inputs, label),
76
+ ...(args !== undefined ? { args } : {}),
73
77
  };
74
78
  }
75
79
  if (kind === "module") {
76
80
  const specifier = normalizeOptionalString(step.target);
77
81
  if (!specifier) throw new Error(`${label}.target must be a non-empty string`);
82
+ const args = normalizeStepArgs(step.args, label);
78
83
  return {
79
84
  kind,
80
85
  specifier,
81
86
  cwd: normalizeOptionalString(step.cwd),
82
87
  inputs: normalizeConfiguredStepInputs(step.inputs, label),
88
+ ...(args !== undefined ? { args } : {}),
83
89
  };
84
90
  }
85
91
 
@@ -94,6 +100,15 @@ export function parseModuleSpecifier(specifier) {
94
100
  };
95
101
  }
96
102
 
103
+ export function normalizeStepArgs(value, label) {
104
+ if (value == null) return undefined;
105
+ try {
106
+ return JSON.parse(JSON.stringify(value));
107
+ } catch {
108
+ throw new Error(`${label}.args must be JSON-serializable`);
109
+ }
110
+ }
111
+
97
112
  export function isBareModuleSpecifier(modulePath) {
98
113
  if (typeof modulePath !== "string" || modulePath.length === 0) return false;
99
114
  if (modulePath.startsWith("./") || modulePath.startsWith("../")) return false;
@@ -155,6 +170,7 @@ export function finalizeConfiguredStep(step, transform) {
155
170
  ...(typeof step.path === "string" ? { path: transform(step.path) } : {}),
156
171
  ...(typeof step.specifier === "string" ? { specifier: transform(step.specifier) } : {}),
157
172
  inputs: finalizeConfiguredInputs(step.inputs || [], transform),
173
+ ...(step.args !== undefined ? { args: step.args } : {}),
158
174
  };
159
175
  }
160
176
 
package/lib/ui/index.d.ts CHANGED
@@ -1,3 +1,121 @@
1
1
  export * from "@playwright/test";
2
2
  export { defineConfig } from "../playwright/index";
3
3
  export type { PlaywrightConfigOptions } from "../playwright/index";
4
+
5
+ export interface UiSandboxOptions {
6
+ allowRemote?: boolean;
7
+ backendBaseUrl?: string;
8
+ baseUrl?: string;
9
+ frontendBaseUrl?: string;
10
+ headers?: Record<string, string>;
11
+ prefix?: string;
12
+ }
13
+
14
+ export interface UiSandboxFixture {
15
+ id: string;
16
+ backendBaseUrl: string;
17
+ frontendBaseUrl: string;
18
+ page: import("@playwright/test").Page;
19
+ }
20
+
21
+ export declare function createUiSandbox(options?: UiSandboxOptions): {
22
+ expect: typeof import("@playwright/test").expect;
23
+ test: import("@playwright/test").TestType<
24
+ import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
25
+ testkitSandbox: UiSandboxFixture;
26
+ },
27
+ import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions
28
+ >;
29
+ };
30
+ export interface ProvisionedUiSandbox {
31
+ id: string;
32
+ backendBaseUrl: string;
33
+ frontendBaseUrl: string;
34
+ identity: unknown;
35
+ owner: {
36
+ email: string;
37
+ id: string;
38
+ name: string;
39
+ organizationId: string;
40
+ organizationName: string;
41
+ password: string;
42
+ slug: string;
43
+ timeZone: string | null;
44
+ };
45
+ ensureAuthenticated(page: import("@playwright/test").Page): Promise<void>;
46
+ apiFetch<T = unknown>(
47
+ page: import("@playwright/test").Page,
48
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
49
+ path: string,
50
+ options?: Record<string, unknown>
51
+ ): Promise<T>;
52
+ }
53
+ export declare function createProvisionedUiSandbox(options?: UiSandboxOptions & Record<string, unknown>): {
54
+ expect: typeof import("@playwright/test").expect;
55
+ test: import("@playwright/test").TestType<
56
+ import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
57
+ guestPage: import("@playwright/test").Page;
58
+ sandbox: ProvisionedUiSandbox;
59
+ },
60
+ import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions
61
+ >;
62
+ };
63
+ export declare function createSandboxId(options?: Pick<UiSandboxOptions, "prefix">): string;
64
+ export declare function resolveFrontendBaseUrl(options?: UiSandboxOptions): string;
65
+ export declare function resolveBackendBaseUrl(options?: UiSandboxOptions): string;
66
+ export declare function assertSafeUiTarget(url: string, options?: Pick<UiSandboxOptions, "allowRemote">): string;
67
+ export declare function assertSafeUiTargets(
68
+ urls?: string[],
69
+ options?: Pick<UiSandboxOptions, "allowRemote">
70
+ ): string[];
71
+ export declare function createAuthedApiClient(
72
+ playwrightRequest?: typeof import("@playwright/test").request,
73
+ options?: UiSandboxOptions
74
+ ): Promise<import("@playwright/test").APIRequestContext>;
75
+ export declare function waitForUiSettled(
76
+ page: import("@playwright/test").Page,
77
+ options?: { frames?: number; timeout?: number }
78
+ ): Promise<void>;
79
+ export declare function waitForAnimationFrames(
80
+ page: import("@playwright/test").Page,
81
+ frames?: number
82
+ ): Promise<void>;
83
+ export declare function readUiConfig(env?: NodeJS.ProcessEnv): unknown;
84
+ export declare function provisionUiIdentity(
85
+ page: import("@playwright/test").Page,
86
+ profileName?: string,
87
+ options?: Record<string, unknown>
88
+ ): Promise<{
89
+ credentials: { email: string; password: string };
90
+ organizations: unknown[];
91
+ profile: string;
92
+ seeded: Record<string, unknown>;
93
+ session: Record<string, unknown>;
94
+ token?: string;
95
+ user?: Record<string, unknown>;
96
+ }>;
97
+ export declare function loginWithCredentials(
98
+ page: import("@playwright/test").Page,
99
+ email: string,
100
+ password: string,
101
+ options?: Record<string, unknown>
102
+ ): Promise<void>;
103
+ export declare function loginAsProvisioned(
104
+ page: import("@playwright/test").Page,
105
+ identity: { credentials: { email: string; password: string } },
106
+ options?: Record<string, unknown>
107
+ ): Promise<unknown>;
108
+ export declare function loginAsUiProfile(
109
+ page: import("@playwright/test").Page,
110
+ profileName?: string,
111
+ options?: Record<string, unknown>
112
+ ): Promise<unknown>;
113
+ export declare function persistUiSessionStorage(
114
+ page: import("@playwright/test").Page,
115
+ identity: unknown,
116
+ options?: Record<string, unknown>
117
+ ): Promise<void>;
118
+ export declare function expectAuthenticatedShell(
119
+ page: import("@playwright/test").Page,
120
+ options?: Record<string, unknown>
121
+ ): Promise<void>;
package/lib/ui/index.mjs CHANGED
@@ -1,2 +1,23 @@
1
1
  export * from "@playwright/test";
2
2
  export { defineConfig } from "../playwright/index.mjs";
3
+ export {
4
+ assertSafeUiTarget,
5
+ assertSafeUiTargets,
6
+ createAuthedApiClient,
7
+ createProvisionedUiSandbox,
8
+ createSandboxId,
9
+ createUiSandbox,
10
+ resolveBackendBaseUrl,
11
+ resolveFrontendBaseUrl,
12
+ waitForAnimationFrames,
13
+ waitForUiSettled,
14
+ } from "./sandbox.mjs";
15
+ export {
16
+ expectAuthenticatedShell,
17
+ loginAsProvisioned,
18
+ loginAsUiProfile,
19
+ loginWithCredentials,
20
+ persistUiSessionStorage,
21
+ provisionUiIdentity,
22
+ readUiConfig,
23
+ } from "./provisioning.mjs";