@elench/testkit 0.1.85 → 0.1.86
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 +37 -3
- package/lib/index.d.ts +30 -8
- package/lib/index.mjs +3 -0
- package/lib/runtime/index.d.ts +77 -0
- package/lib/runtime-src/k6/dal-fixtures.js +66 -0
- package/lib/runtime-src/k6/dal-suite.js +21 -1
- package/lib/runtime-src/shared/fixture-engine.mjs +320 -0
- 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/README.md
CHANGED
|
@@ -423,15 +423,49 @@ const suite = defineHttpSuite({ profile: "defaultAuth" }, ({ actor, actors, req
|
|
|
423
423
|
DAL suites:
|
|
424
424
|
|
|
425
425
|
```ts
|
|
426
|
-
import { defineDalSuite } from "@elench/testkit";
|
|
426
|
+
import { defineDalFixtures, defineDalSuite } from "@elench/testkit";
|
|
427
|
+
|
|
428
|
+
const fixtures = defineDalFixtures(({ db, fixtureScope }) => ({
|
|
429
|
+
widget() {
|
|
430
|
+
return fixtureScope.seed("widget", "primary", { name: "Primary Widget" }, () => {
|
|
431
|
+
const widgetId = fixtureScope.id("widget");
|
|
432
|
+
db.exec(`
|
|
433
|
+
INSERT INTO widgets (id, name)
|
|
434
|
+
VALUES ('${widgetId}', 'Primary Widget')
|
|
435
|
+
`);
|
|
436
|
+
return { widgetId };
|
|
437
|
+
});
|
|
438
|
+
},
|
|
439
|
+
}));
|
|
427
440
|
|
|
428
|
-
const suite = defineDalSuite(({ db }) => {
|
|
429
|
-
|
|
441
|
+
const suite = defineDalSuite({ fixtures }, ({ db, fixtureScope, fixtures }) => {
|
|
442
|
+
const widget = fixtures.widget();
|
|
443
|
+
db.query(`select id from widgets where id = '${widget.widgetId}'`);
|
|
444
|
+
fixtureScope.records();
|
|
430
445
|
});
|
|
431
446
|
|
|
432
447
|
export default suite;
|
|
433
448
|
```
|
|
434
449
|
|
|
450
|
+
`defineDalFixtures(...)` is the package-owned DAL seeding model. It gives every
|
|
451
|
+
suite a deterministic `fixtureScope` with:
|
|
452
|
+
|
|
453
|
+
- `fixtureScope.id(label)` / `uuid(label)`
|
|
454
|
+
- `fixtureScope.slug(label)`
|
|
455
|
+
- `fixtureScope.email(label)`
|
|
456
|
+
- `fixtureScope.string(label, options)`
|
|
457
|
+
- `fixtureScope.token(label)`
|
|
458
|
+
- `fixtureScope.seed(kind, key, signature, create)`
|
|
459
|
+
- `fixtureScope.records()`
|
|
460
|
+
|
|
461
|
+
`testkit` enforces strict fixture behavior:
|
|
462
|
+
|
|
463
|
+
- one logical fixture per `kind + key`
|
|
464
|
+
- identical reseeds reuse the same seeded value
|
|
465
|
+
- conflicting reseeds fail immediately
|
|
466
|
+
- dependency cycles fail immediately
|
|
467
|
+
- seeded fixture records are persisted as a `testkit.dal-fixtures` artifact
|
|
468
|
+
|
|
435
469
|
Scenario suites:
|
|
436
470
|
|
|
437
471
|
```ts
|
package/lib/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ActorRequestClient,
|
|
3
|
+
DalFixtureDefinition,
|
|
3
4
|
HttpClient,
|
|
4
5
|
HttpClientConfig,
|
|
5
6
|
RawRequestClient,
|
|
6
7
|
RuntimeDb,
|
|
7
8
|
RuntimeDalContext,
|
|
8
9
|
RuntimeEnv,
|
|
10
|
+
RuntimeFixtureScope,
|
|
9
11
|
RuntimeHeaders,
|
|
10
12
|
RuntimeOptions,
|
|
11
13
|
RuntimeResponse,
|
|
@@ -120,16 +122,24 @@ export interface HttpSuiteConfig<TSession = unknown> {
|
|
|
120
122
|
options?: RuntimeOptions;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
export interface DalSuiteContext<TSetup = unknown
|
|
125
|
+
export interface DalSuiteContext<TSetup = unknown, TFixtures = Record<string, unknown>> {
|
|
124
126
|
db: RuntimeDb;
|
|
125
127
|
dal: RuntimeDalContext;
|
|
128
|
+
fixtureScope: RuntimeFixtureScope;
|
|
129
|
+
fixtures: TFixtures;
|
|
126
130
|
setupData: TSetup | null;
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
export interface DalSuiteConfig<TSetup = unknown
|
|
133
|
+
export interface DalSuiteConfig<TSetup = unknown, TFixtures = Record<string, unknown>> {
|
|
130
134
|
db?: RuntimeDb;
|
|
135
|
+
fixtures?: DalFixtureDefinition<TFixtures> | null;
|
|
131
136
|
options?: RuntimeOptions;
|
|
132
|
-
setup?: (context: {
|
|
137
|
+
setup?: (context: {
|
|
138
|
+
db: RuntimeDb;
|
|
139
|
+
dal: RuntimeDalContext;
|
|
140
|
+
fixtureScope: RuntimeFixtureScope;
|
|
141
|
+
fixtures: TFixtures;
|
|
142
|
+
}) => TSetup;
|
|
133
143
|
}
|
|
134
144
|
|
|
135
145
|
export declare function defineHttpSuite<TSetup = unknown>(
|
|
@@ -150,13 +160,23 @@ export declare function defineScenarioSuite<TSetup = unknown>(
|
|
|
150
160
|
run: (context: ScenarioSuiteContext<TSetup>) => unknown
|
|
151
161
|
): TestkitSuite<TSetup>;
|
|
152
162
|
|
|
153
|
-
export declare function
|
|
154
|
-
|
|
163
|
+
export declare function defineDalFixtures<TFixtures = Record<string, unknown>>(
|
|
164
|
+
factory: (context: {
|
|
165
|
+
db: RuntimeDb;
|
|
166
|
+
dal: RuntimeDalContext;
|
|
167
|
+
fixtureScope: RuntimeFixtureScope;
|
|
168
|
+
seed: RuntimeFixtureScope["seed"];
|
|
169
|
+
values: RuntimeFixtureScope["values"];
|
|
170
|
+
}) => TFixtures
|
|
171
|
+
): DalFixtureDefinition<TFixtures>;
|
|
172
|
+
|
|
173
|
+
export declare function defineDalSuite<TSetup = unknown, TFixtures = Record<string, unknown>>(
|
|
174
|
+
run: (context: DalSuiteContext<TSetup, TFixtures>) => unknown
|
|
155
175
|
): TestkitSuite<TSetup>;
|
|
156
176
|
|
|
157
|
-
export declare function defineDalSuite<TSetup = unknown
|
|
158
|
-
config: DalSuiteConfig<TSetup>,
|
|
159
|
-
run: (context: DalSuiteContext<TSetup>) => unknown
|
|
177
|
+
export declare function defineDalSuite<TSetup = unknown, TFixtures = Record<string, unknown>>(
|
|
178
|
+
config: DalSuiteConfig<TSetup, TFixtures>,
|
|
179
|
+
run: (context: DalSuiteContext<TSetup, TFixtures>) => unknown
|
|
160
180
|
): TestkitSuite<TSetup>;
|
|
161
181
|
|
|
162
182
|
export declare function createAuthAdapter<TSetup = unknown>(
|
|
@@ -164,11 +184,13 @@ export declare function createAuthAdapter<TSetup = unknown>(
|
|
|
164
184
|
): AuthAdapter<TSetup>;
|
|
165
185
|
|
|
166
186
|
export type {
|
|
187
|
+
DalFixtureDefinition,
|
|
167
188
|
HttpClient,
|
|
168
189
|
HttpClientConfig,
|
|
169
190
|
RuntimeDb,
|
|
170
191
|
RuntimeDalContext,
|
|
171
192
|
RuntimeEnv,
|
|
193
|
+
RuntimeFixtureScope,
|
|
172
194
|
RuntimeHeaders,
|
|
173
195
|
RuntimeOptions,
|
|
174
196
|
RuntimeResponse,
|
package/lib/index.mjs
CHANGED
package/lib/runtime/index.d.ts
CHANGED
|
@@ -82,6 +82,83 @@ export interface RuntimeDalContext {
|
|
|
82
82
|
truncate(...tables: string[]): void;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
export interface RuntimeFixtureLocation {
|
|
86
|
+
column: number;
|
|
87
|
+
line: number;
|
|
88
|
+
path: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface RuntimeFixtureReference {
|
|
92
|
+
key: string;
|
|
93
|
+
kind: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface RuntimeFixtureRecord {
|
|
97
|
+
dependencies: RuntimeFixtureReference[];
|
|
98
|
+
key: string;
|
|
99
|
+
kind: string;
|
|
100
|
+
location?: RuntimeFixtureLocation;
|
|
101
|
+
order: number;
|
|
102
|
+
reuseCount: number;
|
|
103
|
+
signature: unknown;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface RuntimeFixtureStringOptions {
|
|
107
|
+
fallback?: string;
|
|
108
|
+
maxLength?: number;
|
|
109
|
+
prefix?: string;
|
|
110
|
+
suffixLength?: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface RuntimeFixtureEmailOptions {
|
|
114
|
+
domain?: string;
|
|
115
|
+
fallback?: string;
|
|
116
|
+
suffixLength?: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface RuntimeFixtureSlugOptions {
|
|
120
|
+
fallback?: string;
|
|
121
|
+
maxLength?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface RuntimeFixtureValueHelpers {
|
|
125
|
+
email(label: string, options?: RuntimeFixtureEmailOptions): string;
|
|
126
|
+
slug(label: string, options?: RuntimeFixtureSlugOptions): string;
|
|
127
|
+
string(label: string, options?: RuntimeFixtureStringOptions): string;
|
|
128
|
+
token(label: string, options?: RuntimeFixtureStringOptions): string;
|
|
129
|
+
uuid(label: string): string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface RuntimeFixtureScope {
|
|
133
|
+
email(label: string, options?: RuntimeFixtureEmailOptions): string;
|
|
134
|
+
id(label: string): string;
|
|
135
|
+
namespace: string;
|
|
136
|
+
records(): RuntimeFixtureRecord[];
|
|
137
|
+
seed<TResult>(
|
|
138
|
+
kind: string,
|
|
139
|
+
key: string,
|
|
140
|
+
signature: unknown,
|
|
141
|
+
create: () => TResult
|
|
142
|
+
): TResult;
|
|
143
|
+
slug(label: string, options?: RuntimeFixtureSlugOptions): string;
|
|
144
|
+
string(label: string, options?: RuntimeFixtureStringOptions): string;
|
|
145
|
+
token(label: string, options?: RuntimeFixtureStringOptions): string;
|
|
146
|
+
uuid(label: string): string;
|
|
147
|
+
values: RuntimeFixtureValueHelpers;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface DalFixtureDefinitionContext {
|
|
151
|
+
dal: RuntimeDalContext;
|
|
152
|
+
db: RuntimeDb;
|
|
153
|
+
fixtureScope: RuntimeFixtureScope;
|
|
154
|
+
seed: RuntimeFixtureScope["seed"];
|
|
155
|
+
values: RuntimeFixtureValueHelpers;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface DalFixtureDefinition<TFixtures = Record<string, unknown>> {
|
|
159
|
+
create(context: DalFixtureDefinitionContext): TFixtures;
|
|
160
|
+
}
|
|
161
|
+
|
|
85
162
|
export interface HttpRequestParams {
|
|
86
163
|
headers?: RuntimeHeaders;
|
|
87
164
|
redirects?: number;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { emitArtifact } from "./artifacts.js";
|
|
2
|
+
import { createFixtureScope } from "../shared/fixture-engine.mjs";
|
|
3
|
+
import { readTestkitContext } from "../../shared/test-context.mjs";
|
|
4
|
+
|
|
5
|
+
export function defineDalFixtures(factory) {
|
|
6
|
+
if (typeof factory !== "function") {
|
|
7
|
+
throw new Error("defineDalFixtures(factory) requires a fixture factory function.");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
create: factory,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createDalFixtureRuntime({ db, dal, definition = null, env = __ENV } = {}) {
|
|
16
|
+
const runtimeContext = readTestkitContext(env);
|
|
17
|
+
const fixtureScope = createFixtureScope({
|
|
18
|
+
namespace: `dal-${runtimeContext.namespace}`,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const fixtureDefinition = normalizeFixtureDefinition(definition);
|
|
22
|
+
const fixtures = fixtureDefinition
|
|
23
|
+
? fixtureDefinition.create({
|
|
24
|
+
db,
|
|
25
|
+
dal,
|
|
26
|
+
fixtureScope,
|
|
27
|
+
seed: fixtureScope.seed,
|
|
28
|
+
values: fixtureScope.values,
|
|
29
|
+
}) || {}
|
|
30
|
+
: {};
|
|
31
|
+
|
|
32
|
+
let artifactEmitted = false;
|
|
33
|
+
|
|
34
|
+
function emitArtifactOnce() {
|
|
35
|
+
if (artifactEmitted) return;
|
|
36
|
+
const records = fixtureScope.records();
|
|
37
|
+
if (records.length === 0) return;
|
|
38
|
+
|
|
39
|
+
emitArtifact(
|
|
40
|
+
"dal-fixtures",
|
|
41
|
+
{
|
|
42
|
+
namespace: fixtureScope.namespace,
|
|
43
|
+
fixtures: records,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
kind: "testkit.dal-fixtures",
|
|
47
|
+
summary: `${records.length} DAL fixture(s)`,
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
artifactEmitted = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
fixtures,
|
|
55
|
+
fixtureScope,
|
|
56
|
+
emitArtifactOnce,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeFixtureDefinition(definition) {
|
|
61
|
+
if (!definition) return null;
|
|
62
|
+
if (typeof definition.create !== "function") {
|
|
63
|
+
throw new Error("DAL fixture definitions must be created with defineDalFixtures(...).");
|
|
64
|
+
}
|
|
65
|
+
return definition;
|
|
66
|
+
}
|
|
@@ -6,23 +6,40 @@ import {
|
|
|
6
6
|
startFailureCollection,
|
|
7
7
|
} from "./checks.js";
|
|
8
8
|
import { createDalContext, openDb } from "./dal.js";
|
|
9
|
+
import { createDalFixtureRuntime } from "./dal-fixtures.js";
|
|
9
10
|
|
|
10
11
|
export function defineDalSuite(configOrRun, maybeRun) {
|
|
11
12
|
const { config, run } = normalizeSuiteArgs(configOrRun, maybeRun);
|
|
12
13
|
const db = config.db || openDb();
|
|
13
14
|
const dal = createDalContext(db);
|
|
15
|
+
const fixtureRuntime = createDalFixtureRuntime({
|
|
16
|
+
db,
|
|
17
|
+
dal,
|
|
18
|
+
definition: config.fixtures || null,
|
|
19
|
+
});
|
|
14
20
|
|
|
15
21
|
return {
|
|
16
22
|
options: config.options || defaultOptions,
|
|
17
23
|
setup() {
|
|
18
24
|
if (typeof config.setup !== "function") return null;
|
|
19
25
|
startFailureCollection("setup");
|
|
26
|
+
let setupCompleted = false;
|
|
20
27
|
try {
|
|
21
|
-
|
|
28
|
+
const result = config.setup({
|
|
29
|
+
db,
|
|
30
|
+
dal,
|
|
31
|
+
fixtures: fixtureRuntime.fixtures,
|
|
32
|
+
fixtureScope: fixtureRuntime.fixtureScope,
|
|
33
|
+
});
|
|
34
|
+
setupCompleted = true;
|
|
35
|
+
return result;
|
|
22
36
|
} catch (error) {
|
|
23
37
|
recordRuntimeFailure();
|
|
24
38
|
fail(formatFatalSuiteError("setup", error));
|
|
25
39
|
} finally {
|
|
40
|
+
if (!setupCompleted) {
|
|
41
|
+
fixtureRuntime.emitArtifactOnce();
|
|
42
|
+
}
|
|
26
43
|
emitFailureCollectionArtifact();
|
|
27
44
|
}
|
|
28
45
|
},
|
|
@@ -32,12 +49,15 @@ export function defineDalSuite(configOrRun, maybeRun) {
|
|
|
32
49
|
return run({
|
|
33
50
|
db,
|
|
34
51
|
dal,
|
|
52
|
+
fixtures: fixtureRuntime.fixtures,
|
|
53
|
+
fixtureScope: fixtureRuntime.fixtureScope,
|
|
35
54
|
setupData,
|
|
36
55
|
});
|
|
37
56
|
} catch (error) {
|
|
38
57
|
recordRuntimeFailure();
|
|
39
58
|
fail(formatFatalSuiteError("exec", error));
|
|
40
59
|
} finally {
|
|
60
|
+
fixtureRuntime.emitArtifactOnce();
|
|
41
61
|
emitFailureCollectionArtifact();
|
|
42
62
|
}
|
|
43
63
|
},
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
const DEFAULT_SLUG_MAX_LENGTH = 63;
|
|
2
|
+
const DEFAULT_STRING_SUFFIX_LENGTH = 12;
|
|
3
|
+
const DEFAULT_TOKEN_SUFFIX_LENGTH = 16;
|
|
4
|
+
const DEFAULT_EMAIL_DOMAIN = "example.test";
|
|
5
|
+
const STACK_LOCATION_PATTERN = /(file:\/\/[^\s)]+|\/[^\s):]+):(\d+):(\d+)/;
|
|
6
|
+
|
|
7
|
+
export function createFixtureScope({ namespace, locationSkips = [] } = {}) {
|
|
8
|
+
const normalizedNamespace = normalizeNamespace(namespace);
|
|
9
|
+
const entries = new Map();
|
|
10
|
+
const creationOrder = [];
|
|
11
|
+
const activeEntries = [];
|
|
12
|
+
const values = createFixtureValues(normalizedNamespace);
|
|
13
|
+
const skipMatchers = [
|
|
14
|
+
"/runtime-src/shared/fixture-engine.mjs",
|
|
15
|
+
"/runtime-src/k6/dal-fixtures.js",
|
|
16
|
+
...locationSkips,
|
|
17
|
+
].filter(Boolean);
|
|
18
|
+
|
|
19
|
+
function seed(kind, key, signature, create) {
|
|
20
|
+
const normalizedKind = normalizeRequiredString(kind, "fixture kind");
|
|
21
|
+
const normalizedKey = normalizeRequiredString(key, "fixture key");
|
|
22
|
+
if (typeof create !== "function") {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Fixture ${formatFixtureRef(normalizedKind, normalizedKey)} requires a synchronous create callback.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const entryId = buildEntryId(normalizedKind, normalizedKey);
|
|
29
|
+
const parentEntry = activeEntries[activeEntries.length - 1] || null;
|
|
30
|
+
if (parentEntry) {
|
|
31
|
+
parentEntry.dependencies.add(entryId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const normalizedSignature = normalizeJsonValue(signature);
|
|
35
|
+
const signatureJson = stableJsonStringify(normalizedSignature);
|
|
36
|
+
const existing = entries.get(entryId);
|
|
37
|
+
if (existing) {
|
|
38
|
+
if (existing.state === "creating") {
|
|
39
|
+
throw new Error(formatFixtureCycleError(existing, activeEntries, normalizedKind, normalizedKey));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (existing.signatureJson !== signatureJson) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
formatFixtureConflictError(existing, normalizedSignature, signatureJson)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
existing.reuseCount += 1;
|
|
49
|
+
return existing.result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const entry = {
|
|
53
|
+
entryId,
|
|
54
|
+
kind: normalizedKind,
|
|
55
|
+
key: normalizedKey,
|
|
56
|
+
state: "creating",
|
|
57
|
+
signature: normalizedSignature,
|
|
58
|
+
signatureJson,
|
|
59
|
+
location: captureCallsite(skipMatchers),
|
|
60
|
+
dependencies: new Set(),
|
|
61
|
+
reuseCount: 0,
|
|
62
|
+
order: creationOrder.length + 1,
|
|
63
|
+
result: undefined,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
entries.set(entryId, entry);
|
|
67
|
+
creationOrder.push(entryId);
|
|
68
|
+
activeEntries.push(entry);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const result = create();
|
|
72
|
+
if (result && typeof result.then === "function") {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Fixture ${formatFixtureRef(normalizedKind, normalizedKey)} returned a Promise. DAL fixtures must be synchronous.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
entry.state = "ready";
|
|
78
|
+
entry.result = result;
|
|
79
|
+
return result;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
entries.delete(entryId);
|
|
82
|
+
creationOrder.pop();
|
|
83
|
+
throw error;
|
|
84
|
+
} finally {
|
|
85
|
+
activeEntries.pop();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function records() {
|
|
90
|
+
return creationOrder
|
|
91
|
+
.map((entryId) => entries.get(entryId))
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.map((entry) => ({
|
|
94
|
+
kind: entry.kind,
|
|
95
|
+
key: entry.key,
|
|
96
|
+
signature: cloneJsonValue(entry.signature),
|
|
97
|
+
reuseCount: entry.reuseCount,
|
|
98
|
+
order: entry.order,
|
|
99
|
+
dependencies: [...entry.dependencies]
|
|
100
|
+
.map((dependencyId) => entries.get(dependencyId))
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.map((dependency) => ({
|
|
103
|
+
kind: dependency.kind,
|
|
104
|
+
key: dependency.key,
|
|
105
|
+
})),
|
|
106
|
+
...(entry.location ? { location: { ...entry.location } } : {}),
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
namespace: normalizedNamespace,
|
|
112
|
+
values,
|
|
113
|
+
id: values.uuid,
|
|
114
|
+
uuid: values.uuid,
|
|
115
|
+
slug: values.slug,
|
|
116
|
+
email: values.email,
|
|
117
|
+
string: values.string,
|
|
118
|
+
token: values.token,
|
|
119
|
+
seed,
|
|
120
|
+
records,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function createFixtureValues(namespace) {
|
|
125
|
+
const normalizedNamespace = normalizeNamespace(namespace);
|
|
126
|
+
|
|
127
|
+
function uuid(label) {
|
|
128
|
+
const hex = namespacedHex(normalizedNamespace, label).slice(0, 32).split("");
|
|
129
|
+
hex[12] = "4";
|
|
130
|
+
hex[16] = ((Number.parseInt(hex[16] || "0", 16) & 0x3) | 0x8).toString(16);
|
|
131
|
+
return `${hex.slice(0, 8).join("")}-${hex.slice(8, 12).join("")}-${hex.slice(12, 16).join("")}-${hex.slice(16, 20).join("")}-${hex.slice(20, 32).join("")}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function slug(label, options = {}) {
|
|
135
|
+
const fallback = normalizeToken(options.fallback) || "resource";
|
|
136
|
+
const base = normalizeToken(label) || fallback;
|
|
137
|
+
const maxLength = normalizePositiveInteger(options.maxLength) || DEFAULT_SLUG_MAX_LENGTH;
|
|
138
|
+
return `${base}-${namespacedSuffix(normalizedNamespace, `slug:${label}`, DEFAULT_STRING_SUFFIX_LENGTH)}`.slice(
|
|
139
|
+
0,
|
|
140
|
+
maxLength
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function email(label, options = {}) {
|
|
145
|
+
const fallback = normalizeToken(options.fallback) || "user";
|
|
146
|
+
const base = normalizeToken(label) || fallback;
|
|
147
|
+
const domain = normalizeEmailDomain(options.domain) || DEFAULT_EMAIL_DOMAIN;
|
|
148
|
+
const suffixLength =
|
|
149
|
+
normalizePositiveInteger(options.suffixLength) || DEFAULT_STRING_SUFFIX_LENGTH;
|
|
150
|
+
return `${base}-${namespacedSuffix(normalizedNamespace, `email:${label}`, suffixLength)}@${domain}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function string(label, options = {}) {
|
|
154
|
+
const fallback = normalizeToken(options.fallback) || "value";
|
|
155
|
+
const prefix = normalizeToken(options.prefix) || normalizeToken(label) || fallback;
|
|
156
|
+
const suffixLength =
|
|
157
|
+
normalizePositiveInteger(options.suffixLength) || DEFAULT_STRING_SUFFIX_LENGTH;
|
|
158
|
+
const maxLength = normalizePositiveInteger(options.maxLength) || null;
|
|
159
|
+
const value = `${prefix}-${namespacedSuffix(normalizedNamespace, `string:${label}:${prefix}`, suffixLength)}`;
|
|
160
|
+
return maxLength ? value.slice(0, maxLength) : value;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function token(label, options = {}) {
|
|
164
|
+
return string(label, {
|
|
165
|
+
...options,
|
|
166
|
+
fallback: options.fallback || "token",
|
|
167
|
+
suffixLength: normalizePositiveInteger(options.suffixLength) || DEFAULT_TOKEN_SUFFIX_LENGTH,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
uuid,
|
|
173
|
+
slug,
|
|
174
|
+
email,
|
|
175
|
+
string,
|
|
176
|
+
token,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function formatFixtureConflictError(existing, nextSignature, nextSignatureJson) {
|
|
181
|
+
const location = existing.location ? ` First declared at ${formatLocation(existing.location)}.` : "";
|
|
182
|
+
return [
|
|
183
|
+
`Fixture ${formatFixtureRef(existing.kind, existing.key)} was seeded twice with conflicting signatures.`,
|
|
184
|
+
location,
|
|
185
|
+
` Existing: ${existing.signatureJson}`,
|
|
186
|
+
` New: ${nextSignatureJson || stableJsonStringify(normalizeJsonValue(nextSignature))}`,
|
|
187
|
+
].join("");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function formatFixtureCycleError(existing, activeEntries, kind, key) {
|
|
191
|
+
const cycleStartIndex = activeEntries.findIndex((entry) => entry.entryId === existing.entryId);
|
|
192
|
+
const cycleEntries = cycleStartIndex >= 0 ? activeEntries.slice(cycleStartIndex) : activeEntries;
|
|
193
|
+
const chain = [...cycleEntries.map((entry) => formatFixtureRef(entry.kind, entry.key)), formatFixtureRef(kind, key)];
|
|
194
|
+
return `Fixture dependency cycle detected: ${chain.join(" -> ")}.`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formatFixtureRef(kind, key) {
|
|
198
|
+
return `${kind}:${key}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function formatLocation(location) {
|
|
202
|
+
return `${location.path}:${location.line}:${location.column}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function buildEntryId(kind, key) {
|
|
206
|
+
return `${kind}::${key}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizeNamespace(namespace) {
|
|
210
|
+
return normalizeToken(namespace) || "standalone";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function normalizeRequiredString(value, label) {
|
|
214
|
+
const normalized = String(value || "").trim();
|
|
215
|
+
if (normalized.length === 0) {
|
|
216
|
+
throw new Error(`Fixture ${label} must be a non-empty string.`);
|
|
217
|
+
}
|
|
218
|
+
return normalized;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function normalizePositiveInteger(value) {
|
|
222
|
+
if (!Number.isInteger(value) || value <= 0) return null;
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function normalizeEmailDomain(value) {
|
|
227
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
228
|
+
return normalized.length > 0 ? normalized : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function normalizeToken(value) {
|
|
232
|
+
return String(value || "")
|
|
233
|
+
.trim()
|
|
234
|
+
.toLowerCase()
|
|
235
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
236
|
+
.replace(/^-+|-+$/g, "");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function hash32(input) {
|
|
240
|
+
let hash = 0x811c9dc5;
|
|
241
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
242
|
+
hash ^= input.charCodeAt(index);
|
|
243
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
244
|
+
}
|
|
245
|
+
return hash.toString(16).padStart(8, "0");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function namespacedHex(namespace, label) {
|
|
249
|
+
return [0, 1, 2, 3].map((part) => hash32(`${namespace}:${label}:${part}`)).join("");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function namespacedSuffix(namespace, label, length) {
|
|
253
|
+
return namespacedHex(namespace, label).slice(0, length);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function captureCallsite(skipMatchers) {
|
|
257
|
+
const stack = new Error().stack;
|
|
258
|
+
if (!stack) return null;
|
|
259
|
+
|
|
260
|
+
for (const line of stack.split("\n").slice(1)) {
|
|
261
|
+
const match = line.match(STACK_LOCATION_PATTERN);
|
|
262
|
+
if (!match) continue;
|
|
263
|
+
const [rawPath, rawLine, rawColumn] = match.slice(1);
|
|
264
|
+
if (!rawPath || !rawLine || !rawColumn) continue;
|
|
265
|
+
const path = normalizeStackPath(rawPath);
|
|
266
|
+
if (skipMatchers.some((matcher) => path.endsWith(matcher) || path.includes(matcher))) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
path,
|
|
271
|
+
line: Number(rawLine),
|
|
272
|
+
column: Number(rawColumn),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function normalizeStackPath(rawPath) {
|
|
280
|
+
return String(rawPath || "").replace(/^file:\/\//, "");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function stableJsonStringify(value) {
|
|
284
|
+
return JSON.stringify(normalizeJsonValue(value));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function cloneJsonValue(value) {
|
|
288
|
+
return normalizeJsonValue(value);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function normalizeJsonValue(value) {
|
|
292
|
+
if (
|
|
293
|
+
value === null ||
|
|
294
|
+
value === undefined ||
|
|
295
|
+
typeof value === "boolean" ||
|
|
296
|
+
typeof value === "number" ||
|
|
297
|
+
typeof value === "string"
|
|
298
|
+
) {
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (typeof value === "bigint") {
|
|
303
|
+
return value.toString();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (Array.isArray(value)) {
|
|
307
|
+
return value.map((entry) => normalizeJsonValue(entry));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (typeof value === "object") {
|
|
311
|
+
return Object.keys(value)
|
|
312
|
+
.sort()
|
|
313
|
+
.reduce((result, key) => {
|
|
314
|
+
result[key] = normalizeJsonValue(value[key]);
|
|
315
|
+
return result;
|
|
316
|
+
}, {});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return String(value);
|
|
320
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.86",
|
|
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.86"
|
|
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.86",
|
|
4
4
|
"description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -82,10 +82,10 @@
|
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
84
|
"@babel/code-frame": "^7.29.0",
|
|
85
|
-
"@elench/next-analysis": "0.1.
|
|
86
|
-
"@elench/testkit-bridge": "0.1.
|
|
87
|
-
"@elench/testkit-protocol": "0.1.
|
|
88
|
-
"@elench/ts-analysis": "0.1.
|
|
85
|
+
"@elench/next-analysis": "0.1.86",
|
|
86
|
+
"@elench/testkit-bridge": "0.1.86",
|
|
87
|
+
"@elench/testkit-protocol": "0.1.86",
|
|
88
|
+
"@elench/ts-analysis": "0.1.86",
|
|
89
89
|
"@oclif/core": "^4.10.6",
|
|
90
90
|
"esbuild": "^0.25.11",
|
|
91
91
|
"execa": "^9.5.0",
|