@elench/testkit 0.1.132 → 0.1.134
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/lib/cli/commands/guard/unsafe-entrypoint.mjs +27 -0
- package/lib/cli/entrypoint.mjs +3 -0
- package/lib/env/index.d.ts +27 -0
- package/lib/env/index.mjs +54 -0
- package/lib/runtime/index.d.ts +16 -4
- package/lib/runtime-src/k6/http-assertions.js +73 -14
- package/lib/runtime-src/shared/assertion-details.mjs +98 -0
- package/lib/ui/index.d.ts +13 -0
- package/lib/ui/index.mjs +3 -0
- package/lib/ui/provisioning.mjs +19 -11
- package/lib/ui/sandbox.mjs +47 -1
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +5 -5
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +188 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +1 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +293 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +1 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +25 -0
- package/node_modules/es-toolkit/CHANGELOG.md +0 -801
- package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +0 -1
- package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +0 -3
- package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +0 -4
- package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +0 -4
- package/node_modules/esprima/ChangeLog +0 -235
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Command, Flags } from "@oclif/core";
|
|
2
|
+
|
|
3
|
+
export default class GuardUnsafeEntrypointCommand extends Command {
|
|
4
|
+
static summary = "Fail a local script that must be run through Testkit";
|
|
5
|
+
|
|
6
|
+
static flags = {
|
|
7
|
+
command: Flags.string({
|
|
8
|
+
char: "c",
|
|
9
|
+
description: "Disabled command name shown in the error message",
|
|
10
|
+
required: true,
|
|
11
|
+
}),
|
|
12
|
+
use: Flags.string({
|
|
13
|
+
char: "u",
|
|
14
|
+
description: "Replacement command shown in the error message",
|
|
15
|
+
required: true,
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
async run() {
|
|
20
|
+
const { flags } = await this.parse(GuardUnsafeEntrypointCommand);
|
|
21
|
+
this.error(
|
|
22
|
+
`${flags.command} is disabled because DB-touching tests must run through the isolated testkit harness.\n` +
|
|
23
|
+
`Use ${flags.use} instead.`,
|
|
24
|
+
{ exit: 1 }
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/lib/cli/entrypoint.mjs
CHANGED
|
@@ -16,6 +16,7 @@ export function normalizeCliArgs(argv) {
|
|
|
16
16
|
"lint",
|
|
17
17
|
"browser",
|
|
18
18
|
"db",
|
|
19
|
+
"guard",
|
|
19
20
|
]);
|
|
20
21
|
const runTypeShortcuts = new Set(publicTestTypeList({ includeAll: true }));
|
|
21
22
|
const valueFlags = new Set([
|
|
@@ -40,6 +41,8 @@ export function normalizeCliArgs(argv) {
|
|
|
40
41
|
"--provider-arg",
|
|
41
42
|
"--message",
|
|
42
43
|
"--prompt",
|
|
44
|
+
"--command",
|
|
45
|
+
"--use",
|
|
43
46
|
]);
|
|
44
47
|
const positionals = findPositionals(argv, valueFlags);
|
|
45
48
|
const firstPositional = positionals[0] || null;
|
package/lib/env/index.d.ts
CHANGED
|
@@ -6,6 +6,29 @@ export interface LoadDotenvFilesOptions {
|
|
|
6
6
|
override?: boolean;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export interface PostgresConnectionEnvFallback {
|
|
10
|
+
database?: string;
|
|
11
|
+
host?: string;
|
|
12
|
+
password?: string;
|
|
13
|
+
port?: number | string;
|
|
14
|
+
ssl?: boolean;
|
|
15
|
+
username?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PostgresConnectionEnvOptions {
|
|
19
|
+
env?: NodeJS.ProcessEnv;
|
|
20
|
+
fallback?: PostgresConnectionEnvFallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PostgresConnectionEnvValue {
|
|
24
|
+
database: string;
|
|
25
|
+
host: string;
|
|
26
|
+
password: string;
|
|
27
|
+
port: number;
|
|
28
|
+
ssl: boolean;
|
|
29
|
+
username: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
9
32
|
export declare function isManagedRuntime(env?: NodeJS.ProcessEnv): boolean;
|
|
10
33
|
export declare function shouldLoadDotenv(env?: NodeJS.ProcessEnv): boolean;
|
|
11
34
|
export declare function loadDotenvFiles(options?: LoadDotenvFilesOptions): {
|
|
@@ -15,3 +38,7 @@ export declare function assertLocalDatabaseUrl(
|
|
|
15
38
|
env?: NodeJS.ProcessEnv,
|
|
16
39
|
consumer?: string
|
|
17
40
|
): void;
|
|
41
|
+
export declare function readPostgresConnectionFromEnv(
|
|
42
|
+
prefix: string,
|
|
43
|
+
options?: PostgresConnectionEnvOptions
|
|
44
|
+
): PostgresConnectionEnvValue;
|
package/lib/env/index.mjs
CHANGED
|
@@ -63,3 +63,57 @@ export function assertLocalDatabaseUrl(env = process.env, consumer = "This comma
|
|
|
63
63
|
);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
export function readPostgresConnectionFromEnv(prefix, options = {}) {
|
|
68
|
+
const env = options.env || process.env;
|
|
69
|
+
const normalizedPrefix = String(prefix || "").trim();
|
|
70
|
+
if (!normalizedPrefix) {
|
|
71
|
+
throw new Error("readPostgresConnectionFromEnv requires an environment variable prefix");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const fallback = options.fallback || {};
|
|
75
|
+
return {
|
|
76
|
+
host: readEnvValue(env, `${normalizedPrefix}_HOST`, fallback.host),
|
|
77
|
+
port: readEnvPort(env, `${normalizedPrefix}_PORT`, fallback.port),
|
|
78
|
+
database: readEnvValue(env, `${normalizedPrefix}_NAME`, fallback.database),
|
|
79
|
+
username: readEnvValue(env, `${normalizedPrefix}_USER`, fallback.username),
|
|
80
|
+
password: readEnvValue(env, `${normalizedPrefix}_PASSWORD`, fallback.password),
|
|
81
|
+
ssl: readEnvBoolean(env, `${normalizedPrefix}_SSL`, fallback.ssl ?? false),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readEnvValue(env, name, fallback) {
|
|
86
|
+
const value = env?.[name]?.trim();
|
|
87
|
+
if (value) return value;
|
|
88
|
+
if (fallback != null && String(fallback).trim()) return String(fallback).trim();
|
|
89
|
+
throw new Error(`${name} environment variable is required`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readEnvPort(env, name, fallback) {
|
|
93
|
+
const raw = env?.[name]?.trim();
|
|
94
|
+
const value = raw || (fallback == null ? "" : String(fallback));
|
|
95
|
+
const parsed = Number.parseInt(value, 10);
|
|
96
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
97
|
+
throw new Error(`${name} must be a positive integer`);
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readEnvBoolean(env, name, fallback) {
|
|
103
|
+
const value = env?.[name];
|
|
104
|
+
if (!value) return Boolean(fallback);
|
|
105
|
+
switch (value.trim().toLowerCase()) {
|
|
106
|
+
case "1":
|
|
107
|
+
case "true":
|
|
108
|
+
case "yes":
|
|
109
|
+
case "on":
|
|
110
|
+
return true;
|
|
111
|
+
case "0":
|
|
112
|
+
case "false":
|
|
113
|
+
case "no":
|
|
114
|
+
case "off":
|
|
115
|
+
return false;
|
|
116
|
+
default:
|
|
117
|
+
return Boolean(fallback);
|
|
118
|
+
}
|
|
119
|
+
}
|
package/lib/runtime/index.d.ts
CHANGED
|
@@ -432,11 +432,23 @@ export declare function expectResponse(
|
|
|
432
432
|
export declare function expectValue<T>(
|
|
433
433
|
value: T,
|
|
434
434
|
predicate: (value: T) => boolean,
|
|
435
|
-
label: string
|
|
435
|
+
label: string,
|
|
436
|
+
options?: {
|
|
437
|
+
actual?: ((value: T) => unknown) | unknown;
|
|
438
|
+
describeFailure?: ((value: T) => string) | string;
|
|
439
|
+
expected?: unknown;
|
|
440
|
+
kind?: string;
|
|
441
|
+
}
|
|
436
442
|
): boolean;
|
|
437
443
|
export declare function expectCondition(
|
|
438
444
|
predicate: () => boolean,
|
|
439
|
-
label: string
|
|
445
|
+
label: string,
|
|
446
|
+
options?: {
|
|
447
|
+
actual?: (() => unknown) | unknown;
|
|
448
|
+
describeFailure?: (() => string) | string;
|
|
449
|
+
expected?: unknown;
|
|
450
|
+
kind?: string;
|
|
451
|
+
}
|
|
440
452
|
): boolean;
|
|
441
453
|
export declare function expectRowCount<T>(
|
|
442
454
|
rows: T[],
|
|
@@ -452,7 +464,7 @@ export declare function expectNoRows<T>(
|
|
|
452
464
|
label?: string | null
|
|
453
465
|
): boolean;
|
|
454
466
|
export declare function expectField<T extends Record<string, unknown>>(
|
|
455
|
-
row: T,
|
|
467
|
+
row: T | T[],
|
|
456
468
|
field: keyof T | string,
|
|
457
469
|
predicateOrExpected: unknown | ((value: unknown) => boolean),
|
|
458
470
|
label?: string | null
|
|
@@ -470,7 +482,7 @@ export declare function expectType(
|
|
|
470
482
|
export declare function captureError(fn: () => unknown): unknown | null;
|
|
471
483
|
export declare function expectError(
|
|
472
484
|
errorOrFn: unknown | (() => unknown) | null,
|
|
473
|
-
predicate?: ((error: unknown) => boolean) | null,
|
|
485
|
+
predicate?: ((error: unknown) => boolean) | string | null,
|
|
474
486
|
label?: string | null
|
|
475
487
|
): boolean;
|
|
476
488
|
|
|
@@ -3,6 +3,15 @@ import {
|
|
|
3
3
|
extractErrorMessageFromBody,
|
|
4
4
|
hasStandardErrorShape,
|
|
5
5
|
} from "../shared/error-body.mjs";
|
|
6
|
+
import {
|
|
7
|
+
buildValueAssertionDetail,
|
|
8
|
+
formatAssertionValue,
|
|
9
|
+
normalizeErrorValue,
|
|
10
|
+
resolveActual,
|
|
11
|
+
resolveConditionActual,
|
|
12
|
+
resolveConditionFailureMessage,
|
|
13
|
+
resolveFailureMessage,
|
|
14
|
+
} from "../shared/assertion-details.mjs";
|
|
6
15
|
import { safeJson, summarizeHttpTrace, toBodyPreview } from "./http.js";
|
|
7
16
|
|
|
8
17
|
export function expectStatus(response, expected, label = null) {
|
|
@@ -140,16 +149,34 @@ export function expectResponse(response, predicate, label) {
|
|
|
140
149
|
});
|
|
141
150
|
}
|
|
142
151
|
|
|
143
|
-
export function expectValue(value, predicate, label) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
export function expectValue(value, predicate, label, options = {}) {
|
|
153
|
+
const title = normalizeLabel(label, "value matches expectation");
|
|
154
|
+
return evaluateCheck(value, title, predicate, () =>
|
|
155
|
+
buildValueAssertionDetail({
|
|
156
|
+
kind: options.kind || "value-assertion",
|
|
157
|
+
title,
|
|
158
|
+
actual: resolveActual(value, options.actual),
|
|
159
|
+
expected: options.expected,
|
|
160
|
+
message: resolveFailureMessage(value, title, options.describeFailure),
|
|
161
|
+
})
|
|
162
|
+
);
|
|
147
163
|
}
|
|
148
164
|
|
|
149
|
-
export function expectCondition(predicate, label) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
165
|
+
export function expectCondition(predicate, label, options = {}) {
|
|
166
|
+
const title = normalizeLabel(label, "condition holds");
|
|
167
|
+
return evaluateCheck(
|
|
168
|
+
null,
|
|
169
|
+
title,
|
|
170
|
+
() => Boolean(predicate()),
|
|
171
|
+
() =>
|
|
172
|
+
buildValueAssertionDetail({
|
|
173
|
+
kind: options.kind || "condition-assertion",
|
|
174
|
+
title,
|
|
175
|
+
actual: resolveConditionActual(options.actual),
|
|
176
|
+
expected: options.expected,
|
|
177
|
+
message: resolveConditionFailureMessage(title, options.describeFailure),
|
|
178
|
+
})
|
|
179
|
+
);
|
|
153
180
|
}
|
|
154
181
|
|
|
155
182
|
export function expectRowCount(rows, expectedCount, label = null) {
|
|
@@ -160,7 +187,14 @@ export function expectRowCount(rows, expectedCount, label = null) {
|
|
|
160
187
|
return expectValue(
|
|
161
188
|
rows,
|
|
162
189
|
(value) => Array.isArray(value) && value.length === expected,
|
|
163
|
-
normalizeLabel(label, `row count is ${expected}`)
|
|
190
|
+
normalizeLabel(label, `row count is ${expected}`),
|
|
191
|
+
{
|
|
192
|
+
actual: (value) => (Array.isArray(value) ? value.length : undefined),
|
|
193
|
+
describeFailure: (value) =>
|
|
194
|
+
`expected ${expected} row(s), got ${Array.isArray(value) ? value.length : "non-array value"}`,
|
|
195
|
+
expected,
|
|
196
|
+
kind: "row-count-assertion",
|
|
197
|
+
}
|
|
164
198
|
);
|
|
165
199
|
}
|
|
166
200
|
|
|
@@ -173,12 +207,23 @@ export function expectNoRows(rows, label = "query returns no rows") {
|
|
|
173
207
|
return expectRowCount(rows, 0, label);
|
|
174
208
|
}
|
|
175
209
|
|
|
176
|
-
export function expectField(
|
|
210
|
+
export function expectField(rowOrRows, field, predicateOrExpected, label = null) {
|
|
177
211
|
const fieldName = String(field || "");
|
|
212
|
+
const row = Array.isArray(rowOrRows) ? rowOrRows[0] : rowOrRows;
|
|
213
|
+
const expected = typeof predicateOrExpected === "function" ? undefined : predicateOrExpected;
|
|
178
214
|
return expectValue(
|
|
179
215
|
row?.[fieldName],
|
|
180
216
|
normalizeFieldPredicate(predicateOrExpected),
|
|
181
|
-
normalizeLabel(label, `${fieldName || "field"} matches expectation`)
|
|
217
|
+
normalizeLabel(label, `${fieldName || "field"} matches expectation`),
|
|
218
|
+
{
|
|
219
|
+
actual: row?.[fieldName],
|
|
220
|
+
describeFailure: () =>
|
|
221
|
+
Array.isArray(rowOrRows) && rowOrRows.length === 0
|
|
222
|
+
? `expected ${fieldName || "field"}=${formatAssertionValue(expected)}, but query returned 0 rows`
|
|
223
|
+
: `${fieldName || "field"} expected ${formatAssertionValue(expected)}, got ${formatAssertionValue(row?.[fieldName])}`,
|
|
224
|
+
expected,
|
|
225
|
+
kind: "field-assertion",
|
|
226
|
+
}
|
|
182
227
|
);
|
|
183
228
|
}
|
|
184
229
|
|
|
@@ -198,7 +243,13 @@ export function expectType(value, typeName, label = null) {
|
|
|
198
243
|
return expectValue(
|
|
199
244
|
value,
|
|
200
245
|
(actual) => actualType(actual) === expected,
|
|
201
|
-
normalizeLabel(label, `type is ${expected}`)
|
|
246
|
+
normalizeLabel(label, `type is ${expected}`),
|
|
247
|
+
{
|
|
248
|
+
actual: (actual) => actualType(actual),
|
|
249
|
+
describeFailure: (actual) => `expected type ${expected}, got ${actualType(actual)}`,
|
|
250
|
+
expected,
|
|
251
|
+
kind: "type-assertion",
|
|
252
|
+
}
|
|
202
253
|
);
|
|
203
254
|
}
|
|
204
255
|
|
|
@@ -213,13 +264,21 @@ export function captureError(fn) {
|
|
|
213
264
|
|
|
214
265
|
export function expectError(errorOrFn, predicate = null, label = null) {
|
|
215
266
|
const error = typeof errorOrFn === "function" ? captureError(errorOrFn) : errorOrFn;
|
|
267
|
+
const predicateFn = typeof predicate === "function" ? predicate : null;
|
|
268
|
+
const title = typeof predicate === "string" && label == null ? predicate : label;
|
|
216
269
|
return expectValue(
|
|
217
270
|
error,
|
|
218
271
|
(value) => {
|
|
219
272
|
if (!value) return false;
|
|
220
|
-
return
|
|
273
|
+
return predicateFn ? Boolean(predicateFn(value)) : true;
|
|
221
274
|
},
|
|
222
|
-
normalizeLabel(
|
|
275
|
+
normalizeLabel(title, "error is captured"),
|
|
276
|
+
{
|
|
277
|
+
actual: normalizeErrorValue(error),
|
|
278
|
+
describeFailure: () => "expected operation to throw",
|
|
279
|
+
expected: "error",
|
|
280
|
+
kind: "error-assertion",
|
|
281
|
+
}
|
|
223
282
|
);
|
|
224
283
|
}
|
|
225
284
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export function buildValueAssertionDetail({ actual, expected, kind, message, title }) {
|
|
2
|
+
const location = captureCallsite();
|
|
3
|
+
return {
|
|
4
|
+
kind,
|
|
5
|
+
key: location ? `${location.path}:${location.line}:${location.column}` : title,
|
|
6
|
+
title,
|
|
7
|
+
message,
|
|
8
|
+
expected: normalizeAssertionValue(expected),
|
|
9
|
+
actual: normalizeAssertionValue(actual),
|
|
10
|
+
location,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveActual(value, actual) {
|
|
15
|
+
if (typeof actual === "function") return actual(value);
|
|
16
|
+
if (actual !== undefined) return actual;
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolveConditionActual(actual) {
|
|
21
|
+
if (typeof actual === "function") return actual();
|
|
22
|
+
return actual;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveFailureMessage(value, label, describeFailure) {
|
|
26
|
+
if (typeof describeFailure === "function") return describeFailure(value);
|
|
27
|
+
if (typeof describeFailure === "string" && describeFailure.trim().length > 0) {
|
|
28
|
+
return describeFailure;
|
|
29
|
+
}
|
|
30
|
+
return label;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveConditionFailureMessage(label, describeFailure) {
|
|
34
|
+
if (typeof describeFailure === "function") return describeFailure();
|
|
35
|
+
if (typeof describeFailure === "string" && describeFailure.trim().length > 0) {
|
|
36
|
+
return describeFailure;
|
|
37
|
+
}
|
|
38
|
+
return label;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function normalizeAssertionValue(value) {
|
|
42
|
+
if (
|
|
43
|
+
value === null ||
|
|
44
|
+
value === undefined ||
|
|
45
|
+
typeof value === "boolean" ||
|
|
46
|
+
typeof value === "number" ||
|
|
47
|
+
typeof value === "string"
|
|
48
|
+
) {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === "bigint") return value.toString();
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(JSON.stringify(value));
|
|
54
|
+
} catch {
|
|
55
|
+
return String(value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatAssertionValue(value) {
|
|
60
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
61
|
+
if (value === undefined) return "undefined";
|
|
62
|
+
if (typeof value === "bigint") return value.toString();
|
|
63
|
+
try {
|
|
64
|
+
return JSON.stringify(value);
|
|
65
|
+
} catch {
|
|
66
|
+
return String(value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function normalizeErrorValue(error) {
|
|
71
|
+
if (!error) return null;
|
|
72
|
+
if (error instanceof Error) return error.message;
|
|
73
|
+
return String(error);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function captureCallsite() {
|
|
77
|
+
const stack = new Error().stack;
|
|
78
|
+
if (!stack) return null;
|
|
79
|
+
|
|
80
|
+
for (const line of stack.split("\n").slice(1)) {
|
|
81
|
+
const match = line.match(/(file:\/\/[^\s)]+|\/[^\s):]+):(\d+):(\d+)/);
|
|
82
|
+
if (!match) continue;
|
|
83
|
+
const [rawPath, rawLine, rawColumn] = match.slice(1);
|
|
84
|
+
if (!rawPath || !rawLine || !rawColumn) continue;
|
|
85
|
+
const stackPath = rawPath.startsWith("file://") ? rawPath.slice("file://".length) : rawPath;
|
|
86
|
+
if (stackPath.endsWith("/runtime-src/shared/assertion-details.mjs")) continue;
|
|
87
|
+
if (stackPath.endsWith("/runtime-src/k6/http-assertions.js")) continue;
|
|
88
|
+
if (stackPath.endsWith("/runtime-src/k6/checks.js")) continue;
|
|
89
|
+
if (stackPath.endsWith("/runtime/index.mjs")) continue;
|
|
90
|
+
return {
|
|
91
|
+
path: stackPath,
|
|
92
|
+
line: Number(rawLine),
|
|
93
|
+
column: Number(rawColumn),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
package/lib/ui/index.d.ts
CHANGED
|
@@ -105,6 +105,19 @@ export declare function waitForAnimationFrames(
|
|
|
105
105
|
page: import("@playwright/test").Page,
|
|
106
106
|
frames?: number
|
|
107
107
|
): Promise<void>;
|
|
108
|
+
export declare function waitForNetworkIdle(
|
|
109
|
+
page: import("@playwright/test").Page,
|
|
110
|
+
options?: { frames?: number; timeout?: number }
|
|
111
|
+
): Promise<void>;
|
|
112
|
+
export declare function waitForElementStable(
|
|
113
|
+
page: import("@playwright/test").Page,
|
|
114
|
+
selector: string,
|
|
115
|
+
options?: { frames?: number; timeout?: number }
|
|
116
|
+
): Promise<void>;
|
|
117
|
+
export declare function waitForImagesLoaded(
|
|
118
|
+
page: import("@playwright/test").Page,
|
|
119
|
+
containerSelector?: string | null
|
|
120
|
+
): Promise<void>;
|
|
108
121
|
export declare function readUiConfig(env?: NodeJS.ProcessEnv): unknown;
|
|
109
122
|
export declare function provisionUiIdentity(
|
|
110
123
|
page: import("@playwright/test").Page,
|
package/lib/ui/index.mjs
CHANGED
package/lib/ui/provisioning.mjs
CHANGED
|
@@ -196,20 +196,28 @@ async function runProvisionSql({ config, identity, options, profile, session, sq
|
|
|
196
196
|
if (!databaseUrl) {
|
|
197
197
|
throw new Error("UI provisioning SQL requires auth.databaseUrl, TESTKIT_UI_DATABASE_URL, or DATABASE_URL");
|
|
198
198
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
199
|
+
const renderedSql = sql
|
|
200
|
+
.map((statement) => renderSql(statement, { identity, options, profile, session }).trim())
|
|
201
|
+
.filter(Boolean)
|
|
202
|
+
.map(terminateSqlStatement)
|
|
203
|
+
.join("\n");
|
|
204
|
+
if (!renderedSql) return;
|
|
205
|
+
|
|
206
|
+
const result = await execa("psql", [databaseUrl, "-v", "ON_ERROR_STOP=1", "-X", "-q", "-c", renderedSql], {
|
|
207
|
+
env: process.env,
|
|
208
|
+
reject: false,
|
|
209
|
+
stdout: "pipe",
|
|
210
|
+
stderr: "pipe",
|
|
211
|
+
});
|
|
212
|
+
if (result.exitCode !== 0) {
|
|
213
|
+
throw new Error(result.stderr || result.shortMessage || `UI provisioning SQL failed with exit code ${result.exitCode}`);
|
|
210
214
|
}
|
|
211
215
|
}
|
|
212
216
|
|
|
217
|
+
function terminateSqlStatement(statement) {
|
|
218
|
+
return /;\s*$/.test(statement) ? statement : `${statement};`;
|
|
219
|
+
}
|
|
220
|
+
|
|
213
221
|
function defaultSignupBody() {
|
|
214
222
|
return {
|
|
215
223
|
email: "{identity.email}",
|
package/lib/ui/sandbox.mjs
CHANGED
|
@@ -220,7 +220,7 @@ export async function waitForUiSettled(page, options = {}) {
|
|
|
220
220
|
const timeout = options.timeout ?? 5_000;
|
|
221
221
|
await page.waitForLoadState("domcontentloaded", { timeout });
|
|
222
222
|
await waitForAnimationFrames(page, options.frames ?? 2);
|
|
223
|
-
await page
|
|
223
|
+
await waitForNetworkIdle(page, { frames: options.frames ?? 2, timeout });
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
export async function waitForAnimationFrames(page, frames = 2) {
|
|
@@ -243,6 +243,52 @@ export async function waitForAnimationFrames(page, frames = 2) {
|
|
|
243
243
|
);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
export async function waitForNetworkIdle(page, options = {}) {
|
|
247
|
+
const timeout = options.timeout ?? 2_000;
|
|
248
|
+
try {
|
|
249
|
+
await page.waitForLoadState("networkidle", { timeout });
|
|
250
|
+
} catch {
|
|
251
|
+
await page.waitForLoadState("load", { timeout }).catch(() => {});
|
|
252
|
+
await waitForAnimationFrames(page, options.frames ?? 2);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function waitForElementStable(page, selector, options = {}) {
|
|
257
|
+
const stableFrames = Math.max(1, Number(options.frames) || 2);
|
|
258
|
+
const element = page.locator(selector);
|
|
259
|
+
await element.waitFor({ state: "visible", timeout: options.timeout });
|
|
260
|
+
await element.evaluate(async (node, requiredStableFrames) => {
|
|
261
|
+
let stable = 0;
|
|
262
|
+
let previous = JSON.stringify(node.getBoundingClientRect().toJSON());
|
|
263
|
+
while (stable < requiredStableFrames) {
|
|
264
|
+
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
265
|
+
const current = JSON.stringify(node.getBoundingClientRect().toJSON());
|
|
266
|
+
if (current === previous) {
|
|
267
|
+
stable += 1;
|
|
268
|
+
} else {
|
|
269
|
+
stable = 0;
|
|
270
|
+
}
|
|
271
|
+
previous = current;
|
|
272
|
+
}
|
|
273
|
+
}, stableFrames);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function waitForImagesLoaded(page, containerSelector = null) {
|
|
277
|
+
const selector = containerSelector ? `${containerSelector} img` : "img";
|
|
278
|
+
const images = page.locator(selector);
|
|
279
|
+
const count = await images.count();
|
|
280
|
+
|
|
281
|
+
for (let index = 0; index < count; index += 1) {
|
|
282
|
+
await images.nth(index).evaluate((element) => {
|
|
283
|
+
if (element.complete) return undefined;
|
|
284
|
+
return new Promise((resolve) => {
|
|
285
|
+
element.addEventListener("load", resolve, { once: true });
|
|
286
|
+
element.addEventListener("error", resolve, { once: true });
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
246
292
|
function resolveBaseUrl(value) {
|
|
247
293
|
const normalized = String(value || "").trim();
|
|
248
294
|
if (!normalized) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.134",
|
|
4
4
|
"description": "Browser bridge helpers for testkit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@elench/testkit-protocol": "0.1.
|
|
25
|
+
"@elench/testkit-protocol": "0.1.134"
|
|
26
26
|
},
|
|
27
27
|
"private": false
|
|
28
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.134",
|
|
4
4
|
"description": "Assistant-first CLI for running, inspecting, and debugging local testkit suites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -95,10 +95,10 @@
|
|
|
95
95
|
},
|
|
96
96
|
"dependencies": {
|
|
97
97
|
"@babel/code-frame": "^7.29.0",
|
|
98
|
-
"@elench/next-analysis": "0.1.
|
|
99
|
-
"@elench/testkit-bridge": "0.1.
|
|
100
|
-
"@elench/testkit-protocol": "0.1.
|
|
101
|
-
"@elench/ts-analysis": "0.1.
|
|
98
|
+
"@elench/next-analysis": "0.1.134",
|
|
99
|
+
"@elench/testkit-bridge": "0.1.134",
|
|
100
|
+
"@elench/testkit-protocol": "0.1.134",
|
|
101
|
+
"@elench/ts-analysis": "0.1.134",
|
|
102
102
|
"@oclif/core": "^4.10.6",
|
|
103
103
|
"@playwright/test": "^1.52.0",
|
|
104
104
|
"esbuild": "^0.25.11",
|