@vitronai/themis 0.1.13 → 0.1.14

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/CHANGELOG.md CHANGED
@@ -4,12 +4,16 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.1.14 - 2026-03-27
8
+
7
9
  - Added first-party `npx themis test --fix` support so generated-test repair loops can apply fix-handoff autofixes, tighten hints when needed, and rerun the suite directly from the CLI.
8
10
  - Moved the generated contract runtime into the npm package (`@vitronai/themis/contract-runtime`), stopped `init` from creating `tests/example.test.js`, taught generated tests to emit `.generated.test.ts` for TS/TSX sources, and made `init` / `migrate` add `.themis/` to downstream `.gitignore`.
9
11
  - Reorganized framework-managed artifacts under `.themis/` into subdirectories like `.themis/runs/`, `.themis/diffs/`, `.themis/generate/`, `.themis/reports/`, `.themis/migration/`, and `.themis/benchmarks/` so volatile output stays bundled but easier to navigate.
10
12
  - Added native React showcase fixtures for Themis, Jest, and Vitest plus a dedicated first-party Themis CI showcase job.
11
13
  - Added a same-host React showcase benchmark job and uploaded performance artifact so CI now records one direct Themis vs Jest vs Vitest timing comparison for the exact same showcase specs.
12
14
  - Added ESLint with a dedicated CI lint job and folded lint into local validation and prepublish checks.
15
+ - Generated `.generated.test.ts` output now typechecks cleanly under strict TypeScript without requiring downstream `types` overrides, and the packaged `@vitronai/themis/contract-runtime` surface now ships first-party type declarations.
16
+ - Refined the in-repo VS Code extension scaffold with judge-only branding assets, semantic Themis review colors, and a themed HTML report webview shell so artifact review inside VS Code reads like the rest of the product.
13
17
 
14
18
  ## 0.1.4 - 2026-03-26
15
19
 
package/README.md CHANGED
@@ -93,7 +93,7 @@ Themis is built for modern Node.js and TypeScript projects:
93
93
 
94
94
  ## Adopt In Another Repo
95
95
 
96
- Use the AI Quickstart above as the canonical install/generate/test flow. Generated files land under `__themis__/tests` by default.
96
+ Use the AI Quickstart above as the canonical install/generate/test flow. Generated files land under `__themis__/tests` by default. TypeScript-generated tests are emitted as strict-typecheckable artifacts and self-reference Themis globals so downstream TS projects do not need a special `types` override just to compile generated output.
97
97
  TypeScript-generated suites use `import` syntax so downstream ESLint and ESM-style rules do not flag Themis output as legacy `require(...)` code.
98
98
 
99
99
  If another repo wants its agents to reliably choose Themis, put the framework choice directly in that repo's agent instructions instead of assuming agents will infer it from package metadata alone.
@@ -296,7 +296,7 @@ Short version:
296
296
  - Migration proof job runs `npm run proof:migration` against checked-in Jest/Vitest fixtures for basic suites, table tests, RTL/jsdom flows, timers, module mocking, and a context/provider-heavy RTL example, then uploads the resulting migration reports plus Themis run artifacts as evidence.
297
297
  - Themis React Showcase job verifies a straight-up native Themis React fixture as a first-party example.
298
298
  - React showcase perf job runs `npm run benchmark:showcase` on the exact same React scenarios for Themis, Jest, and Vitest on one CI host, then uploads `.themis/benchmarks/showcase-comparison/perf-summary.{json,md}` so the relative timing claim is backed by one comparable artifact.
299
- - Release `0.1.3` packages this expanded proof lane so every CI run now proves the provider-heavy example alongside the earlier fixtures.
299
+ - Release `0.1.14` packages this expanded proof lane so every CI run now proves the provider-heavy example alongside the earlier fixtures.
300
300
 
301
301
  ## Agent Guide
302
302
 
@@ -0,0 +1 @@
1
+ export * from './src/contract-runtime';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitronai/themis",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Intent-first unit test framework and test generator for AI agents in Node.js and TypeScript",
5
5
  "license": "MIT",
6
6
  "author": "Vitron AI",
@@ -55,6 +55,7 @@
55
55
  "default": "./globals.js"
56
56
  },
57
57
  "./contract-runtime": {
58
+ "types": "./contract-runtime.d.ts",
58
59
  "require": "./src/contract-runtime.js",
59
60
  "default": "./src/contract-runtime.js"
60
61
  },
@@ -63,6 +64,7 @@
63
64
  "files": [
64
65
  "bin",
65
66
  "src/*.js",
67
+ "src/*.d.ts",
66
68
  "src/assets/*",
67
69
  "docs",
68
70
  "templates",
@@ -70,6 +72,7 @@
70
72
  "index.d.ts",
71
73
  "globals.js",
72
74
  "globals.d.ts",
75
+ "contract-runtime.d.ts",
73
76
  "themis.ai.json",
74
77
  "README.md",
75
78
  "CHANGELOG.md",
@@ -0,0 +1,62 @@
1
+ export interface ModuleContractEntry {
2
+ kind: string;
3
+ value?: unknown;
4
+ arity?: number;
5
+ ownKeys?: string[];
6
+ prototypeKeys?: string[];
7
+ length?: number;
8
+ itemTypes?: string[];
9
+ source?: string;
10
+ flags?: string;
11
+ size?: number;
12
+ keys?: string[];
13
+ constructor?: string;
14
+ name?: string;
15
+ }
16
+
17
+ export type ModuleContract = Record<string, ModuleContractEntry>;
18
+
19
+ export interface RequestSpec {
20
+ method?: string;
21
+ url: string;
22
+ headers?: Record<string, string>;
23
+ body?: unknown;
24
+ json?: unknown;
25
+ }
26
+
27
+ export function listExportNames(moduleExports: unknown): string[];
28
+ export function buildModuleContract(moduleExports: unknown): ModuleContract;
29
+ export function readExportValue<TValue = unknown>(moduleExports: unknown, name: string): TValue;
30
+ export function normalizeBehaviorValue(value: unknown): unknown;
31
+ export function normalizeRouteResult(value: unknown): Promise<unknown>;
32
+ export function createRequestFromSpec(spec: RequestSpec): unknown;
33
+ export function assertSourceFreshness(
34
+ sourceFile: string,
35
+ expectedHash: string,
36
+ sourceLabel: string,
37
+ regenerateCommand: string
38
+ ): void;
39
+ export function runComponentInteractionContract(
40
+ sourceFile: string,
41
+ exportName: string,
42
+ props: Record<string, unknown>,
43
+ interactionPlan?: unknown[],
44
+ options?: {
45
+ wrapRender?: (element: unknown) => unknown;
46
+ }
47
+ ): Promise<unknown>;
48
+ export function runComponentBehaviorFlowContract(
49
+ sourceFile: string,
50
+ exportName: string,
51
+ props: Record<string, unknown>,
52
+ flowPlan?: unknown[],
53
+ options?: {
54
+ wrapRender?: (element: unknown) => unknown;
55
+ }
56
+ ): Promise<unknown>;
57
+ export function runHookInteractionContract(
58
+ sourceFile: string,
59
+ exportName: string,
60
+ args: unknown[],
61
+ interactionPlan?: unknown[]
62
+ ): unknown;
package/src/generate.js CHANGED
@@ -3233,6 +3233,8 @@ function renderGeneratedTest({ projectRoot, helperFile, outputFile, analysis })
3233
3233
  const sourceImport = normalizeRelativeModule(path.relative(path.dirname(outputFile), analysis.file));
3234
3234
  const sourceAbsolutePath = normalizePath(path.relative(path.dirname(outputFile), analysis.file));
3235
3235
  const useImportSyntax = path.extname(outputFile).toLowerCase() === '.ts';
3236
+ const typeReferences = useImportSyntax ? '/// <reference types="@vitronai/themis/globals" />\n' : '';
3237
+ const typePrelude = useImportSyntax ? `${renderGeneratedTypePrelude()}\n\n` : '';
3236
3238
  const expectedExportContracts = buildExpectedExportContracts(analysis);
3237
3239
  const providerImport = analysis.projectProviderFile
3238
3240
  ? normalizeRelativeModule(path.relative(path.dirname(outputFile), analysis.projectProviderFile))
@@ -3246,48 +3248,182 @@ function renderGeneratedTest({ projectRoot, helperFile, outputFile, analysis })
3246
3248
  : renderGeneratedRequirePrelude({ helperImport });
3247
3249
  const providerLoadExpression = useImportSyntax ? 'PROJECT_PROVIDER_MODULE' : 'require(PROJECT_PROVIDER_IMPORT)';
3248
3250
  const loadModuleBody = useImportSyntax
3249
- ? ' assertSourceFreshness(SOURCE_FILE, SOURCE_HASH, SOURCE_PATH, REGENERATE_COMMAND);\n return import(SOURCE_IMPORT);'
3251
+ ? ' assertSourceFreshness(SOURCE_FILE, SOURCE_HASH, SOURCE_PATH, REGENERATE_COMMAND);\n return import(SOURCE_IMPORT) as Promise<RuntimeModuleExports>;'
3250
3252
  : ' assertSourceFreshness(SOURCE_FILE, SOURCE_HASH, SOURCE_PATH, REGENERATE_COMMAND);\n const resolved = require.resolve(SOURCE_IMPORT);\n delete require.cache[resolved];\n return require(SOURCE_IMPORT);';
3253
+ const sourceFileDeclaration = useImportSyntax
3254
+ ? 'const SOURCE_FILE = require.resolve(SOURCE_IMPORT);'
3255
+ : `const SOURCE_FILE = path.resolve(__dirname, ${JSON.stringify(sourceAbsolutePath)});`;
3256
+ const scannedExportsDeclaration = useImportSyntax
3257
+ ? `const SCANNED_EXPORTS: readonly string[] | null = ${analysis.exactExports ? JSON.stringify(analysis.exportNames, null, 2) : 'null'};`
3258
+ : `const SCANNED_EXPORTS = ${analysis.exactExports ? JSON.stringify(analysis.exportNames, null, 2) : 'null'};`;
3259
+ const expectedExportDeclaration = useImportSyntax
3260
+ ? `const EXPECTED_EXPORT_CONTRACTS: RuntimeModuleContractExpectations = ${JSON.stringify(expectedExportContracts, null, 2)};`
3261
+ : `const EXPECTED_EXPORT_CONTRACTS = ${JSON.stringify(expectedExportContracts, null, 2)};`;
3262
+ const providerImportDeclaration = useImportSyntax
3263
+ ? `const PROJECT_PROVIDER_IMPORT: string | null = ${providerImport ? JSON.stringify(providerImport) : 'null'};`
3264
+ : `const PROJECT_PROVIDER_IMPORT = ${providerImport ? JSON.stringify(providerImport) : 'null'};`;
3265
+ const providerIndexesDeclaration = useImportSyntax
3266
+ ? `const PROJECT_PROVIDER_INDEXES: readonly number[] = ${JSON.stringify(analysis.providerRuntimeIndexes || [], null, 2)};`
3267
+ : `const PROJECT_PROVIDER_INDEXES = ${JSON.stringify(analysis.providerRuntimeIndexes || [], null, 2)};`;
3268
+ const providerPresetsDeclaration = useImportSyntax
3269
+ ? `const PROJECT_PROVIDER_PRESETS: readonly ProviderPreset[] = ${JSON.stringify(analysis.providerRuntimePresets || [], null, 2)};`
3270
+ : `const PROJECT_PROVIDER_PRESETS = ${JSON.stringify(analysis.providerRuntimePresets || [], null, 2)};`;
3271
+ const nextAppCasesDeclaration = useImportSyntax
3272
+ ? `const NEXT_APP_CASES: readonly ComponentCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-app-component'), null, 2)};`
3273
+ : `const NEXT_APP_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-app-component'), null, 2)};`;
3274
+ const nextRouteCasesDeclaration = useImportSyntax
3275
+ ? `const NEXT_ROUTE_CASES: readonly RouteCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-route-handler'), null, 2)};`
3276
+ : `const NEXT_ROUTE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-route-handler'), null, 2)};`;
3277
+ const componentCasesDeclaration = useImportSyntax
3278
+ ? `const COMPONENT_CASES: readonly ComponentCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-component'), null, 2)};`
3279
+ : `const COMPONENT_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-component'), null, 2)};`;
3280
+ const hookCasesDeclaration = useImportSyntax
3281
+ ? `const HOOK_CASES: readonly HookCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-hook'), null, 2)};`
3282
+ : `const HOOK_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-hook'), null, 2)};`;
3283
+ const routeCasesDeclaration = useImportSyntax
3284
+ ? `const ROUTE_CASES: readonly RouteCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'route-handler'), null, 2)};`
3285
+ : `const ROUTE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'route-handler'), null, 2)};`;
3286
+ const serviceCasesDeclaration = useImportSyntax
3287
+ ? `const SERVICE_CASES: readonly ServiceCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'node-service'), null, 2)};`
3288
+ : `const SERVICE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'node-service'), null, 2)};`;
3289
+ const loadModuleSignature = useImportSyntax
3290
+ ? 'function loadModuleExports(): Promise<RuntimeModuleExports> {'
3291
+ : 'function loadModuleExports() {';
3292
+ const resolveProvidersSignature = useImportSyntax
3293
+ ? 'function resolveProjectProviders(loaded: unknown): ProjectProviderRuntime[] {'
3294
+ : 'function resolveProjectProviders(loaded) {';
3295
+ const applyMocksSignature = useImportSyntax
3296
+ ? 'function applyProjectProviderMocks(exportName: string, scenarioName: ScenarioName): void {'
3297
+ : 'function applyProjectProviderMocks(exportName, scenarioName) {';
3298
+ const applyRenderSignature = useImportSyntax
3299
+ ? 'function applyProjectProviderRender(element: unknown, exportName: string, scenarioName: ScenarioName): unknown {'
3300
+ : 'function applyProjectProviderRender(element, exportName, scenarioName) {';
3301
+ const createRenderOptionsSignature = useImportSyntax
3302
+ ? 'function createProjectProviderRenderOptions(exportName: string, scenarioName: ScenarioName): { wrapRender(element: unknown): unknown } {'
3303
+ : 'function createProjectProviderRenderOptions(exportName, scenarioName) {';
3304
+ const applyPresetsSignature = useImportSyntax
3305
+ ? 'function applyProjectProviderPresets(element: unknown): unknown {'
3306
+ : 'function applyProjectProviderPresets(element) {';
3307
+ const withProviderShellSignature = useImportSyntax
3308
+ ? 'function withProviderShell(type: string, element: unknown, attributes: ProviderConfig = {}): ReactTestElement {'
3309
+ : 'function withProviderShell(type, element, attributes = {}) {';
3310
+ const providerHelperSignature = (name) => useImportSyntax
3311
+ ? `function ${name}(element: unknown, config: ProviderConfig = {}): ReactTestElement {`
3312
+ : `function ${name}(element, config = {}) {`;
3313
+ const serializeProviderDataSignature = useImportSyntax
3314
+ ? 'function serializeProviderData(value: unknown): string {'
3315
+ : 'function serializeProviderData(value) {';
3316
+ const assertExpectedRuntimeContractSignature = useImportSyntax
3317
+ ? 'function assertExpectedRuntimeContract(runtime: RuntimeModuleExports): void {'
3318
+ : 'function assertExpectedRuntimeContract(runtime) {';
3319
+ const assertNormalizedRenderContractSignature = useImportSyntax
3320
+ ? 'function assertNormalizedRenderContract(rendered: unknown): void {'
3321
+ : 'function assertNormalizedRenderContract(rendered) {';
3322
+ const assertDomContractShapeSignature = useImportSyntax
3323
+ ? 'function assertDomContractShape(contract: DomContract): void {'
3324
+ : 'function assertDomContractShape(contract) {';
3325
+ const countPlannedStepsSignature = useImportSyntax
3326
+ ? 'function countPlannedSteps(plan: readonly RepeatedPlanStep[] | null | undefined): number {'
3327
+ : 'function countPlannedSteps(plan) {';
3328
+ const assertComponentInteractionContractShapeSignature = useImportSyntax
3329
+ ? 'function assertComponentInteractionContractShape(contract: ComponentInteractionContract, plan: readonly InteractionPlanStep[] | null | undefined): void {'
3330
+ : 'function assertComponentInteractionContractShape(contract, plan) {';
3331
+ const assertFlowContractShapeSignature = useImportSyntax
3332
+ ? 'function assertFlowContractShape(flow: ComponentBehaviorFlowContract, plan: readonly FlowPlanStep[] | null | undefined): void {'
3333
+ : 'function assertFlowContractShape(flow, plan) {';
3334
+ const assertHookResultContractSignature = useImportSyntax
3335
+ ? 'function assertHookResultContract(result: unknown): void {'
3336
+ : 'function assertHookResultContract(result) {';
3337
+ const assertHookInteractionContractShapeSignature = useImportSyntax
3338
+ ? 'function assertHookInteractionContractShape(contract: HookInteractionContract, plan: readonly HookInteractionPlanStep[] | null | undefined): void {'
3339
+ : 'function assertHookInteractionContractShape(contract, plan) {';
3340
+ const assertRouteResultContractShapeSignature = useImportSyntax
3341
+ ? 'function assertRouteResultContractShape(response: unknown): void {'
3342
+ : 'function assertRouteResultContractShape(response) {';
3343
+ const assertServiceResultContractShapeSignature = useImportSyntax
3344
+ ? 'function assertServiceResultContractShape(result: unknown): void {'
3345
+ : 'function assertServiceResultContractShape(result) {';
3346
+ const componentReadExpression = useImportSyntax
3347
+ ? 'readExportValue<(props: Record<string, unknown>) => unknown>(moduleExports, testCase.exportName)'
3348
+ : 'readExportValue(moduleExports, testCase.exportName)';
3349
+ const hookReadExpression = useImportSyntax
3350
+ ? 'readExportValue<(...args: unknown[]) => unknown>(moduleExports, testCase.exportName)'
3351
+ : 'readExportValue(moduleExports, testCase.exportName)';
3352
+ const handlerReadExpression = useImportSyntax
3353
+ ? 'readExportValue<(request: unknown, context?: unknown) => unknown>(moduleExports, testCase.exportName)'
3354
+ : 'readExportValue(moduleExports, testCase.exportName)';
3355
+ const serviceReadExpression = useImportSyntax
3356
+ ? 'readExportValue<(...args: unknown[]) => unknown>(moduleExports, testCase.exportName)'
3357
+ : 'readExportValue(moduleExports, testCase.exportName)';
3358
+ const interactionContractExpression = useImportSyntax
3359
+ ? 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\')) as ComponentInteractionContract'
3360
+ : 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\'))';
3361
+ const nextDomContractExpression = useImportSyntax
3362
+ ? 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\')) as ComponentInteractionContract'
3363
+ : 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\'))';
3364
+ const nextFlowExpression = useImportSyntax
3365
+ ? 'await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\')) as ComponentBehaviorFlowContract'
3366
+ : 'await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\'))';
3367
+ const componentInteractionExpression = useImportSyntax
3368
+ ? 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'react-component\')) as ComponentInteractionContract'
3369
+ : 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'react-component\'))';
3370
+ const componentDomExpression = useImportSyntax
3371
+ ? 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'react-component\')) as ComponentInteractionContract'
3372
+ : 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'react-component\'))';
3373
+ const componentFlowExpression = useImportSyntax
3374
+ ? 'await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, createProjectProviderRenderOptions(testCase.exportName, \'react-component\')) as ComponentBehaviorFlowContract'
3375
+ : 'await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, createProjectProviderRenderOptions(testCase.exportName, \'react-component\'))';
3376
+ const hookInteractionExpression = useImportSyntax
3377
+ ? 'runHookInteractionContract(SOURCE_FILE, testCase.exportName, testCase.args, testCase.interactions) as HookInteractionContract'
3378
+ : 'runHookInteractionContract(SOURCE_FILE, testCase.exportName, testCase.args, testCase.interactions)';
3251
3379
 
3252
3380
  return `${GENERATED_MARKER}
3253
- // Source: ${relativeSourcePath}
3381
+ ${typeReferences}// Source: ${relativeSourcePath}
3254
3382
 
3255
3383
  ${runtimePrelude}
3256
-
3257
- const SOURCE_PATH = ${JSON.stringify(relativeSourcePath)};
3384
+ ${typePrelude}const SOURCE_PATH = ${JSON.stringify(relativeSourcePath)};
3258
3385
  const SOURCE_IMPORT = ${JSON.stringify(sourceImport)};
3259
- const SOURCE_FILE = path.resolve(__dirname, ${JSON.stringify(sourceAbsolutePath)});
3386
+ ${sourceFileDeclaration}
3260
3387
  const SOURCE_HASH = ${JSON.stringify(analysis.sourceHash)};
3261
3388
  const REGENERATE_COMMAND = ${JSON.stringify(`npx themis generate ${relativeSourcePath}`)};
3262
- const SCANNED_EXPORTS = ${analysis.exactExports ? JSON.stringify(analysis.exportNames, null, 2) : 'null'};
3263
- const EXPECTED_EXPORT_CONTRACTS = ${JSON.stringify(expectedExportContracts, null, 2)};
3264
- const PROJECT_PROVIDER_IMPORT = ${providerImport ? JSON.stringify(providerImport) : 'null'};
3265
- const PROJECT_PROVIDER_INDEXES = ${JSON.stringify(analysis.providerRuntimeIndexes || [], null, 2)};
3266
- const PROJECT_PROVIDER_PRESETS = ${JSON.stringify(analysis.providerRuntimePresets || [], null, 2)};
3267
- const NEXT_APP_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-app-component'), null, 2)};
3268
- const NEXT_ROUTE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-route-handler'), null, 2)};
3269
- const COMPONENT_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-component'), null, 2)};
3270
- const HOOK_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-hook'), null, 2)};
3271
- const ROUTE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'route-handler'), null, 2)};
3272
- const SERVICE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'node-service'), null, 2)};
3273
-
3274
- function loadModuleExports() {
3389
+ ${scannedExportsDeclaration}
3390
+ ${expectedExportDeclaration}
3391
+ ${providerImportDeclaration}
3392
+ ${providerIndexesDeclaration}
3393
+ ${providerPresetsDeclaration}
3394
+ ${nextAppCasesDeclaration}
3395
+ ${nextRouteCasesDeclaration}
3396
+ ${componentCasesDeclaration}
3397
+ ${hookCasesDeclaration}
3398
+ ${routeCasesDeclaration}
3399
+ ${serviceCasesDeclaration}
3400
+
3401
+ ${loadModuleSignature}
3275
3402
  ${loadModuleBody}
3276
3403
  }
3277
3404
 
3278
- function applyProjectProviderMocks(exportName, scenarioName) {
3405
+ ${resolveProvidersSignature}
3406
+ if (Array.isArray(loaded)) {
3407
+ return loaded;
3408
+ }
3409
+
3410
+ if (loaded && typeof loaded === 'object') {
3411
+ const moduleValue = ${useImportSyntax ? 'loaded as { providers?: ProjectProviderRuntime[] } & ProjectProviderRuntime' : 'loaded'};
3412
+ if (Array.isArray(moduleValue.providers)) {
3413
+ return moduleValue.providers;
3414
+ }
3415
+ return [moduleValue];
3416
+ }
3417
+
3418
+ return [];
3419
+ }
3420
+
3421
+ ${applyMocksSignature}
3279
3422
  if (!PROJECT_PROVIDER_IMPORT || PROJECT_PROVIDER_INDEXES.length === 0) {
3280
3423
  return;
3281
3424
  }
3282
3425
 
3283
- const loaded = ${providerLoadExpression};
3284
- const providers = Array.isArray(loaded)
3285
- ? loaded
3286
- : Array.isArray(loaded && loaded.providers)
3287
- ? loaded.providers
3288
- : loaded
3289
- ? [loaded]
3290
- : [];
3426
+ const providers = resolveProjectProviders(${providerLoadExpression});
3291
3427
 
3292
3428
  for (const providerIndex of PROJECT_PROVIDER_INDEXES) {
3293
3429
  const provider = providers[providerIndex];
@@ -3314,21 +3450,14 @@ function applyProjectProviderMocks(exportName, scenarioName) {
3314
3450
  }
3315
3451
  }
3316
3452
 
3317
- function applyProjectProviderRender(element, exportName, scenarioName) {
3453
+ ${applyRenderSignature}
3318
3454
  let current = applyProjectProviderPresets(element);
3319
3455
 
3320
3456
  if (!PROJECT_PROVIDER_IMPORT) {
3321
3457
  return current;
3322
3458
  }
3323
3459
 
3324
- const loaded = ${providerLoadExpression};
3325
- const providers = Array.isArray(loaded)
3326
- ? loaded
3327
- : Array.isArray(loaded && loaded.providers)
3328
- ? loaded.providers
3329
- : loaded
3330
- ? [loaded]
3331
- : [];
3460
+ const providers = resolveProjectProviders(${providerLoadExpression});
3332
3461
 
3333
3462
  for (const providerIndex of PROJECT_PROVIDER_INDEXES) {
3334
3463
  const provider = providers[providerIndex];
@@ -3343,24 +3472,12 @@ function applyProjectProviderRender(element, exportName, scenarioName) {
3343
3472
  scenario: scenarioName,
3344
3473
  element: current,
3345
3474
  withProviderShell,
3346
- withReactRouter(elementValue, config) {
3347
- return withReactRouter(elementValue, config);
3348
- },
3349
- withNextNavigation(elementValue, config) {
3350
- return withNextNavigation(elementValue, config);
3351
- },
3352
- withAuthSession(elementValue, config) {
3353
- return withAuthSession(elementValue, config);
3354
- },
3355
- withReactQuery(elementValue, config) {
3356
- return withReactQuery(elementValue, config);
3357
- },
3358
- withZustandStore(elementValue, config) {
3359
- return withZustandStore(elementValue, config);
3360
- },
3361
- withReduxStore(elementValue, config) {
3362
- return withReduxStore(elementValue, config);
3363
- }
3475
+ withReactRouter,
3476
+ withNextNavigation,
3477
+ withAuthSession,
3478
+ withReactQuery,
3479
+ withZustandStore,
3480
+ withReduxStore
3364
3481
  });
3365
3482
 
3366
3483
  if (nextValue !== undefined) {
@@ -3371,7 +3488,13 @@ function applyProjectProviderRender(element, exportName, scenarioName) {
3371
3488
  return current;
3372
3489
  }
3373
3490
 
3374
- function applyProjectProviderPresets(element) {
3491
+ ${createRenderOptionsSignature}
3492
+ return {
3493
+ wrapRender: (element${useImportSyntax ? ': unknown' : ''}) => applyProjectProviderRender(element, exportName, scenarioName)
3494
+ };
3495
+ }
3496
+
3497
+ ${applyPresetsSignature}
3375
3498
  let current = element;
3376
3499
 
3377
3500
  for (const preset of PROJECT_PROVIDER_PRESETS) {
@@ -3398,7 +3521,7 @@ function applyProjectProviderPresets(element) {
3398
3521
  return current;
3399
3522
  }
3400
3523
 
3401
- function withProviderShell(type, element, attributes = {}) {
3524
+ ${withProviderShellSignature}
3402
3525
  return {
3403
3526
  $$typeof: 'react.test.element',
3404
3527
  type,
@@ -3411,7 +3534,7 @@ function withProviderShell(type, element, attributes = {}) {
3411
3534
  };
3412
3535
  }
3413
3536
 
3414
- function withReactRouter(element, config = {}) {
3537
+ ${providerHelperSignature('withReactRouter')}
3415
3538
  return withProviderShell('themis-router-provider', element, {
3416
3539
  role: 'navigation',
3417
3540
  'data-themis-provider': 'router',
@@ -3424,7 +3547,7 @@ function withReactRouter(element, config = {}) {
3424
3547
  });
3425
3548
  }
3426
3549
 
3427
- function withReactQuery(element, config = {}) {
3550
+ ${providerHelperSignature('withReactQuery')}
3428
3551
  return withProviderShell('themis-react-query-provider', element, {
3429
3552
  'data-themis-provider': 'react-query',
3430
3553
  'data-query-client': typeof config.clientName === 'string' ? config.clientName : 'themis-query-client',
@@ -3436,7 +3559,7 @@ function withReactQuery(element, config = {}) {
3436
3559
  });
3437
3560
  }
3438
3561
 
3439
- function withNextNavigation(element, config = {}) {
3562
+ ${providerHelperSignature('withNextNavigation')}
3440
3563
  return withProviderShell('themis-next-navigation-provider', element, {
3441
3564
  'data-themis-provider': 'next-navigation',
3442
3565
  'data-next-pathname': typeof config.pathname === 'string' ? config.pathname : '/',
@@ -3447,7 +3570,7 @@ function withNextNavigation(element, config = {}) {
3447
3570
  });
3448
3571
  }
3449
3572
 
3450
- function withAuthSession(element, config = {}) {
3573
+ ${providerHelperSignature('withAuthSession')}
3451
3574
  return withProviderShell('themis-auth-provider', element, {
3452
3575
  'data-themis-provider': 'auth',
3453
3576
  'data-auth-user': typeof config.user === 'string' ? config.user : 'anonymous',
@@ -3458,7 +3581,7 @@ function withAuthSession(element, config = {}) {
3458
3581
  });
3459
3582
  }
3460
3583
 
3461
- function withZustandStore(element, config = {}) {
3584
+ ${providerHelperSignature('withZustandStore')}
3462
3585
  return withProviderShell('themis-zustand-provider', element, {
3463
3586
  'data-themis-provider': 'zustand',
3464
3587
  'data-store-name': typeof config.name === 'string' ? config.name : 'zustand-store',
@@ -3468,7 +3591,7 @@ function withZustandStore(element, config = {}) {
3468
3591
  });
3469
3592
  }
3470
3593
 
3471
- function withReduxStore(element, config = {}) {
3594
+ ${providerHelperSignature('withReduxStore')}
3472
3595
  return withProviderShell('themis-redux-provider', element, {
3473
3596
  'data-themis-provider': 'redux',
3474
3597
  'data-redux-slice': typeof config.slice === 'string' ? config.slice : 'root',
@@ -3478,14 +3601,14 @@ function withReduxStore(element, config = {}) {
3478
3601
  });
3479
3602
  }
3480
3603
 
3481
- function serializeProviderData(value) {
3604
+ ${serializeProviderDataSignature}
3482
3605
  if (value === undefined) {
3483
3606
  return '';
3484
3607
  }
3485
3608
  return JSON.stringify(normalizeBehaviorValue(value));
3486
3609
  }
3487
3610
 
3488
- function assertExpectedRuntimeContract(runtime) {
3611
+ ${assertExpectedRuntimeContractSignature}
3489
3612
  expect(typeof runtime).toBe('object');
3490
3613
  expect(runtime === null).toBe(false);
3491
3614
 
@@ -3495,7 +3618,7 @@ function assertExpectedRuntimeContract(runtime) {
3495
3618
  }
3496
3619
 
3497
3620
  for (const [exportName, expected] of Object.entries(EXPECTED_EXPORT_CONTRACTS)) {
3498
- const actual = runtime[exportName];
3621
+ const actual = ${useImportSyntax ? 'runtime[exportName] as Record<string, unknown>' : 'runtime[exportName]'};
3499
3622
  expect(Boolean(actual)).toBe(true);
3500
3623
 
3501
3624
  if (expected.kind && expected.kind !== 'unknown') {
@@ -3512,7 +3635,7 @@ function assertExpectedRuntimeContract(runtime) {
3512
3635
  }
3513
3636
  }
3514
3637
 
3515
- function assertNormalizedRenderContract(rendered) {
3638
+ ${assertNormalizedRenderContractSignature}
3516
3639
  const normalized = normalizeBehaviorValue(rendered);
3517
3640
  expect(normalized !== undefined && normalized !== null).toBe(true);
3518
3641
 
@@ -3521,21 +3644,22 @@ function assertNormalizedRenderContract(rendered) {
3521
3644
  return;
3522
3645
  }
3523
3646
 
3524
- if (typeof normalized === 'object') {
3525
- if (normalized.kind === 'element') {
3526
- expect(Boolean(normalized.type)).toBe(true);
3527
- expect(typeof normalized.props).toBe('object');
3647
+ if (normalized && typeof normalized === 'object') {
3648
+ const normalizedRecord = ${useImportSyntax ? 'normalized as Record<string, unknown>' : 'normalized'};
3649
+ if (normalizedRecord.kind === 'element') {
3650
+ expect(Boolean(normalizedRecord.type)).toBe(true);
3651
+ expect(typeof normalizedRecord.props).toBe('object');
3528
3652
  return;
3529
3653
  }
3530
3654
 
3531
- expect(Object.keys(normalized).length >= 0).toBe(true);
3655
+ expect(Object.keys(normalizedRecord).length >= 0).toBe(true);
3532
3656
  return;
3533
3657
  }
3534
3658
 
3535
3659
  expect(['string', 'number', 'boolean'].includes(typeof normalized)).toBe(true);
3536
3660
  }
3537
3661
 
3538
- function assertDomContractShape(contract) {
3662
+ ${assertDomContractShapeSignature}
3539
3663
  expect(typeof contract).toBe('object');
3540
3664
  expect(contract === null).toBe(false);
3541
3665
  expect(Array.isArray(contract.nodes)).toBe(true);
@@ -3543,7 +3667,7 @@ function assertDomContractShape(contract) {
3543
3667
  expect(typeof contract.textContent).toBe('string');
3544
3668
  }
3545
3669
 
3546
- function countPlannedSteps(plan) {
3670
+ ${countPlannedStepsSignature}
3547
3671
  if (!Array.isArray(plan)) {
3548
3672
  return 0;
3549
3673
  }
@@ -3554,7 +3678,7 @@ function countPlannedSteps(plan) {
3554
3678
  }, 0);
3555
3679
  }
3556
3680
 
3557
- function assertComponentInteractionContractShape(contract, plan) {
3681
+ ${assertComponentInteractionContractShapeSignature}
3558
3682
  assertNormalizedRenderContract(contract.rendered);
3559
3683
  assertDomContractShape(contract.dom);
3560
3684
  expect(Array.isArray(contract.interactions)).toBe(true);
@@ -3571,7 +3695,7 @@ function assertComponentInteractionContractShape(contract, plan) {
3571
3695
  }
3572
3696
  }
3573
3697
 
3574
- function assertFlowContractShape(flow, plan) {
3698
+ ${assertFlowContractShapeSignature}
3575
3699
  assertDomContractShape(flow.dom);
3576
3700
  expect(Array.isArray(flow.steps)).toBe(true);
3577
3701
  expect(flow.steps.length).toBe(Array.isArray(plan) ? plan.length : 0);
@@ -3624,7 +3748,7 @@ function assertFlowContractShape(flow, plan) {
3624
3748
  }
3625
3749
 
3626
3750
  if (expected.expected && expected.expected.rolesInclude !== undefined) {
3627
- const expectedRoles = Array.isArray(expected.expected.rolesInclude)
3751
+ const expectedRoles${useImportSyntax ? ': string[]' : ''} = Array.isArray(expected.expected.rolesInclude)
3628
3752
  ? expected.expected.rolesInclude
3629
3753
  : [expected.expected.rolesInclude];
3630
3754
  const matchesRoles = [step.immediateDom, step.settledDom].some((dom) => {
@@ -3636,12 +3760,12 @@ function assertFlowContractShape(flow, plan) {
3636
3760
  }
3637
3761
  }
3638
3762
 
3639
- function assertHookResultContract(result) {
3763
+ ${assertHookResultContractSignature}
3640
3764
  const normalized = normalizeBehaviorValue(result);
3641
3765
  expect(normalized !== undefined).toBe(true);
3642
3766
  }
3643
3767
 
3644
- function assertHookInteractionContractShape(contract, plan) {
3768
+ ${assertHookInteractionContractShapeSignature}
3645
3769
  expect(Array.isArray(contract.interactions)).toBe(true);
3646
3770
 
3647
3771
  if (countPlannedSteps(plan) > 0) {
@@ -3654,18 +3778,19 @@ function assertHookInteractionContractShape(contract, plan) {
3654
3778
  }
3655
3779
  }
3656
3780
 
3657
- function assertRouteResultContractShape(response) {
3781
+ ${assertRouteResultContractShapeSignature}
3658
3782
  expect(response !== undefined && response !== null).toBe(true);
3659
3783
 
3660
- if (response && typeof response === 'object' && response.kind === 'response') {
3661
- expect(typeof response.status).toBe('number');
3662
- expect(response.status >= 100).toBe(true);
3663
- expect(response.status < 600).toBe(true);
3664
- expect(typeof response.headers).toBe('object');
3784
+ if (response && typeof response === 'object' && ${useImportSyntax ? '(response as Record<string, unknown>).kind' : 'response.kind'} === 'response') {
3785
+ const routeResponse = ${useImportSyntax ? 'response as { status: number; headers: unknown }' : 'response'};
3786
+ expect(typeof routeResponse.status).toBe('number');
3787
+ expect(routeResponse.status >= 100).toBe(true);
3788
+ expect(routeResponse.status < 600).toBe(true);
3789
+ expect(typeof routeResponse.headers).toBe('object');
3665
3790
  }
3666
3791
  }
3667
3792
 
3668
- function assertServiceResultContractShape(result) {
3793
+ ${assertServiceResultContractShapeSignature}
3669
3794
  expect(result !== undefined).toBe(true);
3670
3795
  }
3671
3796
 
@@ -3684,28 +3809,20 @@ describe(${JSON.stringify(suiteName)}, () => {
3684
3809
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3685
3810
  applyProjectProviderMocks(testCase.exportName, 'next-app-component');
3686
3811
  const moduleExports = await loadModuleExports();
3687
- const component = readExportValue(moduleExports, testCase.exportName);
3812
+ const component = ${componentReadExpression};
3688
3813
  const rendered = applyProjectProviderRender(component(testCase.props), testCase.exportName, 'next-app-component');
3689
3814
  assertNormalizedRenderContract(rendered);
3690
3815
  });
3691
3816
 
3692
3817
  test(testCase.exportName + ' next interaction contract', async () => {
3693
3818
  applyProjectProviderMocks(testCase.exportName, 'next-app-component');
3694
- const interaction = await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, {
3695
- wrapRender(element) {
3696
- return applyProjectProviderRender(element, testCase.exportName, 'next-app-component');
3697
- }
3698
- });
3819
+ const interaction = ${interactionContractExpression};
3699
3820
  assertComponentInteractionContractShape(interaction, testCase.interactions);
3700
3821
  });
3701
3822
 
3702
3823
  test(testCase.exportName + ' next dom state contract', async () => {
3703
3824
  applyProjectProviderMocks(testCase.exportName, 'next-app-component');
3704
- const contract = await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, {
3705
- wrapRender(element) {
3706
- return applyProjectProviderRender(element, testCase.exportName, 'next-app-component');
3707
- }
3708
- });
3825
+ const contract = ${nextDomContractExpression};
3709
3826
  assertDomContractShape(contract.dom);
3710
3827
  if (Array.isArray(testCase.interactions) && testCase.interactions.length > 0) {
3711
3828
  expect(contract.interactions.length > 0).toBe(true);
@@ -3715,11 +3832,7 @@ describe(${JSON.stringify(suiteName)}, () => {
3715
3832
  if (Array.isArray(testCase.flows) && testCase.flows.length > 0) {
3716
3833
  test(testCase.exportName + ' next behavioral flow contract', async () => {
3717
3834
  applyProjectProviderMocks(testCase.exportName, 'next-app-component');
3718
- const flow = await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, {
3719
- wrapRender(element) {
3720
- return applyProjectProviderRender(element, testCase.exportName, 'next-app-component');
3721
- }
3722
- });
3835
+ const flow = ${nextFlowExpression};
3723
3836
  assertFlowContractShape(flow, testCase.flows);
3724
3837
  expect(flow.steps.some((step) => !step.skipped)).toBe(true);
3725
3838
  });
@@ -3734,28 +3847,20 @@ describe(${JSON.stringify(suiteName)}, () => {
3734
3847
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3735
3848
  applyProjectProviderMocks(testCase.exportName, 'react-component');
3736
3849
  const moduleExports = await loadModuleExports();
3737
- const component = readExportValue(moduleExports, testCase.exportName);
3850
+ const component = ${componentReadExpression};
3738
3851
  const rendered = applyProjectProviderRender(component(testCase.props), testCase.exportName, 'react-component');
3739
3852
  assertNormalizedRenderContract(rendered);
3740
3853
  });
3741
3854
 
3742
3855
  test(testCase.exportName + ' interaction contract', async () => {
3743
3856
  applyProjectProviderMocks(testCase.exportName, 'react-component');
3744
- const interaction = await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, {
3745
- wrapRender(element) {
3746
- return applyProjectProviderRender(element, testCase.exportName, 'react-component');
3747
- }
3748
- });
3857
+ const interaction = ${componentInteractionExpression};
3749
3858
  assertComponentInteractionContractShape(interaction, testCase.interactions);
3750
3859
  });
3751
3860
 
3752
3861
  test(testCase.exportName + ' dom state contract', async () => {
3753
3862
  applyProjectProviderMocks(testCase.exportName, 'react-component');
3754
- const contract = await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, {
3755
- wrapRender(element) {
3756
- return applyProjectProviderRender(element, testCase.exportName, 'react-component');
3757
- }
3758
- });
3863
+ const contract = ${componentDomExpression};
3759
3864
  assertDomContractShape(contract.dom);
3760
3865
  if (Array.isArray(testCase.interactions) && testCase.interactions.length > 0) {
3761
3866
  expect(contract.interactions.length > 0).toBe(true);
@@ -3765,11 +3870,7 @@ describe(${JSON.stringify(suiteName)}, () => {
3765
3870
  if (Array.isArray(testCase.flows) && testCase.flows.length > 0) {
3766
3871
  test(testCase.exportName + ' behavioral flow contract', async () => {
3767
3872
  applyProjectProviderMocks(testCase.exportName, 'react-component');
3768
- const flow = await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, {
3769
- wrapRender(element) {
3770
- return applyProjectProviderRender(element, testCase.exportName, 'react-component');
3771
- }
3772
- });
3873
+ const flow = ${componentFlowExpression};
3773
3874
  assertFlowContractShape(flow, testCase.flows);
3774
3875
  expect(flow.steps.some((step) => !step.skipped)).toBe(true);
3775
3876
  });
@@ -3784,14 +3885,14 @@ describe(${JSON.stringify(suiteName)}, () => {
3784
3885
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3785
3886
  applyProjectProviderMocks(testCase.exportName, 'react-hook');
3786
3887
  const moduleExports = await loadModuleExports();
3787
- const hook = readExportValue(moduleExports, testCase.exportName);
3888
+ const hook = ${hookReadExpression};
3788
3889
  const result = hook(...testCase.args);
3789
3890
  assertHookResultContract(result);
3790
3891
  });
3791
3892
 
3792
3893
  test(testCase.exportName + ' interaction contract', () => {
3793
3894
  applyProjectProviderMocks(testCase.exportName, 'react-hook');
3794
- const interaction = runHookInteractionContract(SOURCE_FILE, testCase.exportName, testCase.args, testCase.interactions);
3895
+ const interaction = ${hookInteractionExpression};
3795
3896
  assertHookInteractionContractShape(interaction, testCase.interactions);
3796
3897
  });
3797
3898
  }
@@ -3804,7 +3905,7 @@ describe(${JSON.stringify(suiteName)}, () => {
3804
3905
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3805
3906
  applyProjectProviderMocks(testCase.exportName, 'next-route-handler');
3806
3907
  const moduleExports = await loadModuleExports();
3807
- const handler = readExportValue(moduleExports, testCase.exportName);
3908
+ const handler = ${handlerReadExpression};
3808
3909
  const request = createRequestFromSpec(testCase.request);
3809
3910
  const response = await Promise.resolve(handler(request, testCase.context));
3810
3911
  const normalizedResponse = await normalizeRouteResult(response);
@@ -3820,7 +3921,7 @@ describe(${JSON.stringify(suiteName)}, () => {
3820
3921
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3821
3922
  applyProjectProviderMocks(testCase.exportName, 'route-handler');
3822
3923
  const moduleExports = await loadModuleExports();
3823
- const handler = readExportValue(moduleExports, testCase.exportName);
3924
+ const handler = ${handlerReadExpression};
3824
3925
  const request = createRequestFromSpec(testCase.request);
3825
3926
  const response = await Promise.resolve(handler(request, testCase.context));
3826
3927
  const normalizedResponse = await normalizeRouteResult(response);
@@ -3836,7 +3937,7 @@ describe(${JSON.stringify(suiteName)}, () => {
3836
3937
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3837
3938
  applyProjectProviderMocks(testCase.exportName, 'node-service');
3838
3939
  const moduleExports = await loadModuleExports();
3839
- const service = readExportValue(moduleExports, testCase.exportName);
3940
+ const service = ${serviceReadExpression};
3840
3941
  const result = await Promise.resolve(service(...testCase.args));
3841
3942
  assertServiceResultContractShape(normalizeBehaviorValue(result));
3842
3943
  });
@@ -3866,10 +3967,9 @@ const {
3866
3967
  function renderGeneratedImportPrelude({ helperImport, providerImport }) {
3867
3968
  const providerLine = providerImport
3868
3969
  ? `import * as PROJECT_PROVIDER_MODULE from ${JSON.stringify(providerImport)};`
3869
- : 'const PROJECT_PROVIDER_MODULE = null;';
3970
+ : 'const PROJECT_PROVIDER_MODULE: unknown = null;';
3870
3971
 
3871
- return `import path from 'path';
3872
- import {
3972
+ return `import {
3873
3973
  listExportNames,
3874
3974
  buildModuleContract,
3875
3975
  readExportValue,
@@ -3881,9 +3981,228 @@ import {
3881
3981
  runComponentBehaviorFlowContract,
3882
3982
  runHookInteractionContract
3883
3983
  } from ${JSON.stringify(helperImport)};
3984
+ import type {
3985
+ AdvanceTimersByTime,
3986
+ FlushMicrotasks,
3987
+ Fn,
3988
+ MockFetch,
3989
+ MockModule,
3990
+ ResetFetchMocks,
3991
+ RestoreFetch,
3992
+ RunAllTimers,
3993
+ UseFakeTimers,
3994
+ UseRealTimers
3995
+ } from "@vitronai/themis";
3884
3996
  ${providerLine}`;
3885
3997
  }
3886
3998
 
3999
+ function renderGeneratedTypePrelude() {
4000
+ return `declare const require: {
4001
+ resolve(id: string): string;
4002
+ };
4003
+
4004
+ type ScenarioName =
4005
+ | 'next-app-component'
4006
+ | 'next-route-handler'
4007
+ | 'react-component'
4008
+ | 'react-hook'
4009
+ | 'route-handler'
4010
+ | 'node-service';
4011
+ type RuntimeModuleExports = Record<string, unknown>;
4012
+ type RuntimeModuleContractExpectations = Record<string, {
4013
+ kind: string;
4014
+ arity: number | null;
4015
+ prototypeKeys: string[] | null;
4016
+ }>;
4017
+ type ProviderConfig = Record<string, unknown>;
4018
+
4019
+ interface RepeatedPlanStep {
4020
+ repeat?: number | null;
4021
+ }
4022
+
4023
+ interface InteractionPlanStep extends RepeatedPlanStep {
4024
+ event?: string | null;
4025
+ labelIncludes?: string | null;
4026
+ elementType?: string | null;
4027
+ }
4028
+
4029
+ interface FlowExpectation {
4030
+ immediateTextIncludes?: string;
4031
+ beforeTextIncludes?: string;
4032
+ settledTextIncludes?: string;
4033
+ textExcludes?: string;
4034
+ attributes?: Record<string, unknown>;
4035
+ rolesInclude?: string | string[];
4036
+ }
4037
+
4038
+ interface FlowPlanStep extends RepeatedPlanStep {
4039
+ label?: string | null;
4040
+ event: string;
4041
+ labelIncludes?: string | null;
4042
+ elementType?: string | null;
4043
+ target?: Record<string, unknown>;
4044
+ awaitResult?: boolean;
4045
+ flushMicrotasks?: number;
4046
+ advanceTimersByTime?: number;
4047
+ runAllTimers?: boolean;
4048
+ expected?: FlowExpectation;
4049
+ }
4050
+
4051
+ interface HookInteractionPlanStep extends RepeatedPlanStep {
4052
+ method?: string | null;
4053
+ }
4054
+
4055
+ interface ComponentCase {
4056
+ exportName: string;
4057
+ displayName: string;
4058
+ caseName: string;
4059
+ props: Record<string, unknown>;
4060
+ interactions: InteractionPlanStep[];
4061
+ flows: FlowPlanStep[];
4062
+ confidence: string;
4063
+ }
4064
+
4065
+ interface HookCase {
4066
+ exportName: string;
4067
+ displayName: string;
4068
+ caseName: string;
4069
+ args: unknown[];
4070
+ interactions: HookInteractionPlanStep[];
4071
+ confidence: string;
4072
+ }
4073
+
4074
+ interface RouteCase {
4075
+ exportName: string;
4076
+ displayName: string;
4077
+ caseName: string;
4078
+ request: {
4079
+ method?: string;
4080
+ url: string;
4081
+ headers?: Record<string, string>;
4082
+ body?: unknown;
4083
+ json?: unknown;
4084
+ };
4085
+ context: Record<string, unknown>;
4086
+ confidence: string;
4087
+ }
4088
+
4089
+ interface ServiceCase {
4090
+ exportName: string;
4091
+ displayName: string;
4092
+ caseName: string;
4093
+ args: unknown[];
4094
+ confidence: string;
4095
+ }
4096
+
4097
+ interface ProviderPreset {
4098
+ router?: ProviderConfig;
4099
+ nextNavigation?: ProviderConfig;
4100
+ auth?: ProviderConfig;
4101
+ reactQuery?: ProviderConfig;
4102
+ zustand?: ProviderConfig;
4103
+ redux?: ProviderConfig;
4104
+ }
4105
+
4106
+ type ProviderTransform = (element: unknown, config?: ProviderConfig) => unknown;
4107
+
4108
+ interface ProjectProviderMockContext {
4109
+ sourceFile: string;
4110
+ sourcePath: string;
4111
+ exportName: string;
4112
+ scenario: ScenarioName;
4113
+ mock: MockModule | null;
4114
+ fn: Fn | null;
4115
+ mockFetch: MockFetch | null;
4116
+ resetFetchMocks: ResetFetchMocks | null;
4117
+ restoreFetch: RestoreFetch | null;
4118
+ useFakeTimers: UseFakeTimers | null;
4119
+ useRealTimers: UseRealTimers | null;
4120
+ advanceTimersByTime: AdvanceTimersByTime | null;
4121
+ runAllTimers: RunAllTimers | null;
4122
+ flushMicrotasks: FlushMicrotasks | null;
4123
+ }
4124
+
4125
+ interface ProjectProviderRenderContext {
4126
+ sourceFile: string;
4127
+ sourcePath: string;
4128
+ exportName: string;
4129
+ scenario: ScenarioName;
4130
+ element: unknown;
4131
+ withProviderShell(type: string, element: unknown, attributes?: ProviderConfig): unknown;
4132
+ withReactRouter: ProviderTransform;
4133
+ withNextNavigation: ProviderTransform;
4134
+ withAuthSession: ProviderTransform;
4135
+ withReactQuery: ProviderTransform;
4136
+ withZustandStore: ProviderTransform;
4137
+ withReduxStore: ProviderTransform;
4138
+ }
4139
+
4140
+ interface ProjectProviderRuntime {
4141
+ providers?: ProjectProviderRuntime[];
4142
+ applyMocks?(context: ProjectProviderMockContext): void;
4143
+ wrapRender?(context: ProjectProviderRenderContext): unknown;
4144
+ }
4145
+
4146
+ interface DomRoleContract {
4147
+ role: string;
4148
+ name: string;
4149
+ path: string;
4150
+ type: string;
4151
+ attributes: Record<string, unknown>;
4152
+ }
4153
+
4154
+ type DomNodeContract =
4155
+ | { kind: 'text'; value: string; path: string }
4156
+ | { kind: 'value'; value: unknown; path: string }
4157
+ | { kind: 'element'; type: string; path: string; textContent: string; attributes: Record<string, unknown> };
4158
+
4159
+ interface DomContract {
4160
+ textContent: string;
4161
+ roles: DomRoleContract[];
4162
+ nodes: DomNodeContract[];
4163
+ }
4164
+
4165
+ interface ComponentInteractionEntry {
4166
+ label: string;
4167
+ beforeDom: DomContract;
4168
+ afterDom: DomContract;
4169
+ }
4170
+
4171
+ interface ComponentInteractionContract {
4172
+ rendered: unknown;
4173
+ dom: DomContract;
4174
+ interactions: ComponentInteractionEntry[];
4175
+ }
4176
+
4177
+ interface FlowStepContract {
4178
+ label: string;
4179
+ skipped?: boolean;
4180
+ beforeDom: DomContract;
4181
+ immediateDom: DomContract;
4182
+ settledDom: DomContract;
4183
+ }
4184
+
4185
+ interface ComponentBehaviorFlowContract {
4186
+ dom: DomContract;
4187
+ steps: FlowStepContract[];
4188
+ }
4189
+
4190
+ interface HookInteractionEntry {
4191
+ label: string;
4192
+ }
4193
+
4194
+ interface HookInteractionContract {
4195
+ interactions: HookInteractionEntry[];
4196
+ }
4197
+
4198
+ interface ReactTestElement {
4199
+ $$typeof: 'react.test.element';
4200
+ type: string;
4201
+ key: null;
4202
+ props: Record<string, unknown>;
4203
+ }`;
4204
+ }
4205
+
3887
4206
  function flattenScenarioCases(scenarios, kind) {
3888
4207
  const scenario = scenarios.find((candidate) => candidate.kind === kind);
3889
4208
  return scenario && Array.isArray(scenario.cases) ? scenario.cases : [];